ERNIE源码学习与实践:为超越ChatGPT打下技术基础!

★★★ 本文源自AlStudio社区精品项目,【点击此处】查看更多精品内容 >>>

ERNIE学习与实践:为超越ChatGPT打下技术基础!

ERNIE是BERT相爱相杀的好基友,由ERNIE发展起来的文心大模型,是GPT3.0的强劲竞争对手,未来还会挑战ChatGPT的江湖地位!

在“BERT学习与实践:为紧追潮流ChatGPT做好技术准备!”项目中,我们从源码到微调从头实践,对BERT有了较详细的了解。在了解BERT的基础上,本项目从头从源码到部署进行了学习和实践。不想当将军的士兵不是好士兵,不想超越ChatGPT的模型不是好模型!

读万卷书,不如行万里路!近期有很多关于ChatGPT的交流和会议,大家提出了很多的优秀见解和点子,但是这些点子如果不实践不落地,不会对现实造成任何影响!只有动手实践,才能检验知识,才能应用到实际,进而才能改变世界!希望本项目能在帮助大家把点子变成现实方面尽一份力。

ERNIE源码学习与实践:为超越ChatGPT打下技术基础!_第1张图片

Ernie系列模型!

ERNIE共有1.0 2.0 和3.0三个大版本,1.0和2.0结构一样,只是训练数据和训练方式不一样。3.0使用了大模型和海量的数据,3.0也提供了Tiny系列模型,有的跟1.0模型一样大,有的小很多,但是效果却比Ernie1.0好很多,更适合产业落地!

模型 模型大小(参数量) (中文)数据量 训练方法
ERNIE1.0 参考bert base(110M) Wiki,baike,news,tieba (CLUECorpus2020 语料 200G) pretraining + finetuning
ERNIE2.0 参考bert base(110M), bert large(340M) wiki,news,dialogue,IR, discourse relation pretraining + finetuning
ERNIE3.0 10B ERNIE 3.0 Titan 260B(GPT3.0 175B) 4TB(ERNIE2.0,search,web,QA-long, QA-short, novel, poetry&couplet, medical, law, financial,KG) progressive training + finetuning / zero-shot
ERNIE3.0 Tiny 110M 最小5.4M 4TB 蒸馏量化压缩 training + finetuning / zero-shot

本项目从源码入手,复现Ernie 1.0版本模型构建,并实践完成训练数据集生成,Ernie预训练,微调训练,并最终部署Ernie模型。

从源码角度将Ernie弄清楚,再去学习GPT3以及ChatGPT就会更顺利了。

一、ERNIE 系列模型

ERNIE是在BERT横空出世后不久就面世的竟品。
学习ERNIE1.0的相关知识,文档参考:https://github.com/PaddlePaddle/PaddleNLP/tree/develop/model_zoo/ernie-1.0

ERNIE跟BERT有很大相似性,但是市面上研究BERT的人较多,BERT的衍生模型也较多。相对而言,研究ERNIE的人较少。俗话说,众人拾柴火焰高,大家多多研究ERNIE,建设好ERNIE的生态,那么最终国内的NLP一定能够登顶!

ERNIE目前有三个版本,版本1.0是最开始的版本,可以预训练和微调使用。ERNIE 2.0主要是增加了数据集,改进了训练任务,提高了精度,模型代码主要部分跟1.0一样。ERNIE3.0是大模型,模型参数和数据集都超大,普通开发者没有条件从头开始训练,但是官方发布了Tiny版本的预训练模型,可以微调后使用,效果相当好。

本节就以ERNIE1.0 为例进行学习和实践。

Ernie 1.0模型简介

ERNIE是百度开创性提出的基于知识增强的持续学习语义理解框架,它将大数据预训练与多源丰富知识相结合,通过持续学习技术,不断吸收海量文本数据中词汇、结构、语义等方面的知识,实现模型效果不断进化。

ERNIE在情感分析、文本匹配、自然语言推理、词法分析、阅读理解、智能问答等16个公开数据集上全面显著超越世界领先技术,在国际权威的通用语言理解评估基准GLUE上,得分首次突破90分,获得全球第一。 相关创新成果也被国际顶级学术会议AAAI、IJCAI收录。 同时,ERNIE在工业界得到了大规模应用,如搜索引擎、新闻推荐、广告系统、语音交互、智能客服等。

ERNIE 通过建模海量数据中的词、实体及实体关系,学习真实世界的语义知识。相较于 BERT 学习原始语言信号,ERNIE 直接对先验语义知识单元进行建模,增强了模型语义表示能力。

这里我们举个例子:

Learnt by BERT :哈 [mask] 滨是 [mask] 龙江的省会,[mask] 际冰 [mask] 文化名城。
Learnt by ERNIE:[mask] [mask] [mask] 是黑龙江的省会,国际 [mask] [mask] 文化名城。

在 BERT 模型中,我们通过『哈』与『滨』的局部共现,即可判断出『尔』字,模型没有学习与『哈尔滨』相关的任何知识。而 ERNIE 通过学习词与实体的表达,使模型能够建模出『哈尔滨』与『黑龙江』的关系,学到『哈尔滨』是 『黑龙江』的省会以及『哈尔滨』是个冰雪城市。

由于数据集里含有词的信息,所以Ernie能学到词与实体的表达,增强了模型语义表达能力。同时也就知道了,分词效果会影响Ernie模型效果,lac效果好于jieba,所以最终用了lac进行分词(wordtag效果好,但速度比lac慢了100倍)

总结就是:Ernie与BERT模型类似,不同点是:Ernie在构建数据集的时候,就使用了LAC进行分词,然后对“词“进行Mask遮挡预训练。(BERT是对字进行遮挡)

Ernie 1.0项目特色

  • 中文预训练
    提供了完整中文预训练流程,从词表构造、数据处理、任务训练,到下游任务。
    提供中文Whole Word Mask,支持文本动态Mask。
  • 数据流程,
    数据预处理流程高效,40分钟即可完成14G ERNIE数据制作。
    数据稳定可复现,多数据集即插即用。
  • 分布式训练,
    支持多机多卡,支持混合精度、重计算、梯度累积等功能。

本项目在展示Ernie1.0源代码的基础上,将官方预训练流程重新实践一遍。

中文预训练

ERNIE预训练采用的是MLM(Mask Language Model)掩码语言模型的训练方式,采用WWM(Whole Word Mask)方式,对于完整语义单元的Token,会同时进行Mask。同时还进行SOP(Sentence Order Predict,BERT中称为NSP)句子顺序预测的训练方式。整体的训练损失loss是mlm_loss + sop_loss。

ERNIE 中文预训练更详细的介绍文档请可以参见ERNIE 中文预训练介绍。

具体实践在第三节进行。

预训练模型贡献

PaddleNLP为开发者提供了community模块,用户可以上传自己训练的模型,开源给其他用户使用。 使用本文档给出的参数配置,在CLUECorpusSmall数据集上训练,可以得到zhui/ernie-1.0-cluecorpussmall参数,可直接使用。

model = AutoModelForMaskedLM.from_pretrained('zhui/ernie-1.0-cluecorpussmall')

贡献预训练模型的方法,可以参考贡献预训练模型权重教程。

下游任务微调

使用 PaddleNLP 只需要一行代码可以拿到 ERNIE 系列模型,方便之后在自己的下游数据下进行微调,从而获得具体任务上效果更好的模型。参见ERNIE 中文预训练介绍

模型评估

使用训练中产出的checkpoint,或者paddlenlp内置的模型权重,使用本脚本,用户可以快速对当前模型效果进行评估。

预测部署

FastDeploy ERNIE 1.0 模型 Python 部署示例

FastDeploy ERNIE 3.0 模型 Python 部署示例

二、Ernie 1.0源代码学习

读万卷书,不如行万里路,采用读源代码的方式,更有利于学习和理解Ernie!
更详细的源码解析见项目内文件:Ernie源码学习。查看源代码,我们可以找到预训练源文件run_pretrain.py和模型源文件modeling.py 。Ernie的源代码主要在modeling.py中,而结合run_pretrain.py文件则更有利于理解源代码。

导入需要的库并定义变量

在官方源文件中,模型变量是定义在argparse的变量空间中,这里为了更清晰的阅读代码,将变量从变量空间拆解出来。
比如:“type_vocab_size”: 2, # token_type_ids的词典大小,值为2时token_type_ids取值为0和1。
也就是输入数据token_type_ids有[0, 1]两种标记 。(比如binary_head变量则表示预训练任务的选择,如果binary_head为True则训MLN和NSP,否则只训MLN)

为了简洁,ErnieTokenizer源代码不进行展示,直接从PaddleLNLP库中调用。

先安装需要的库文件,PaddleNLP一定要-U安装新版本,系统自带的版本低了。

!pip install paddlenlp -Uq
!pip install tool_helpers -q
from typing import Optional, Tuple

import paddle
import paddle.nn as nn
import paddle.nn.functional as F
from paddle import Tensor

from paddlenlp.transformers import ErnieTokenizer

tokenizer = ErnieTokenizer.from_pretrained('ernie-1.0')

vocab_size: int = 18000  # 词向量数
hidden_size: int = 768  # 中间层长度
num_hidden_layers: int = 12  # 隐藏层数
num_attention_heads: int = 12  # 注意力机制头数
task_id=0  # 任务id
intermediate_size: int = 3072
hidden_act: str = "gelu"
hidden_dropout_prob: float = 0.1
attention_probs_dropout_prob: float = 0.1
max_position_embeddings: int = 513  # 最大位置编码
task_type_vocab_size: int = 3  # 训练任务数
type_vocab_size: int = 2 #  token_type_ids(任务类型词向量)数
initializer_range: float = 0.02  # 初始化范围大小
pad_token_id: int = 0  # PAD补齐的数值:0
pool_act: str = "tanh"
fuse: bool = False
layer_norm_eps=1e-12
use_cache=False
use_task_id=False  # 是否对训练任务id(数值)embedding
enable_recompute=False

学习ErnieEmebbing

跟BERT一样,Ernie使用了Transformer的编码器,并对词、位置和句子序列进行编码嵌入(Embedding)

# from typing import Optional, Tuple

# import paddle
# import paddle.nn as nn
# import paddle.nn.functional as F
# from paddle import Tensor
        
class ErnieEmbeddings(nn.Layer):
    r"""
    Include embeddings from word, position and token_type embeddings.
    """

    def __init__(self, vocab_size, hidden_size, pad_token_id, max_position_embeddings, 
                type_vocab_size, hidden_dropout_prob=0.1, weight_attr=None):
        super(ErnieEmbeddings, self).__init__()

        self.word_embeddings = nn.Embedding(
            vocab_size, hidden_size, padding_idx=pad_token_id, weight_attr=weight_attr
        )
        self.position_embeddings = nn.Embedding(
            max_position_embeddings, hidden_size, weight_attr=weight_attr
        )
        self.type_vocab_size = type_vocab_size
        if self.type_vocab_size > 0:
            self.token_type_embeddings = nn.Embedding(
                type_vocab_size, hidden_size, weight_attr=weight_attr
            )
        self.layer_norm = nn.LayerNorm(hidden_size)
        self.dropout = nn.Dropout(hidden_dropout_prob)

    def forward(
        self,
        input_ids: Optional[Tensor] = None,
        token_type_ids: Optional[Tensor] = None,
        position_ids: Optional[Tensor] = None,
        inputs_embeds: Optional[Tensor] = None,
        past_key_values_length: int = 0,
    ):

        if input_ids is not None:
            inputs_embeds = self.word_embeddings(input_ids)

        input_shape = paddle.shape(inputs_embeds)[:-1]

        if position_ids is None:
            # maybe need use shape op to unify static graph and dynamic graph
            ones = paddle.ones(input_shape, dtype="int64")
            seq_length = paddle.cumsum(ones, axis=1)
            position_ids = seq_length - ones

            if past_key_values_length > 0:
                position_ids = position_ids + past_key_values_length

            position_ids.stop_gradient = True

        position_embeddings = self.position_embeddings(position_ids)
        embeddings = inputs_embeds + position_embeddings

        if self.type_vocab_size > 0:
            if token_type_ids is None:
                token_type_ids = paddle.zeros(input_shape, dtype="int64")
            token_type_embeddings = self.token_type_embeddings(token_type_ids)
            embeddings = embeddings + token_type_embeddings

        embeddings = self.layer_norm(embeddings)
        embeddings = self.dropout(embeddings)
        return embeddings

测试ErnieEmbeddings

testErnieEmbeddings = ErnieEmbeddings(vocab_size, hidden_size, pad_token_id, max_position_embeddings, 
                type_vocab_size,)
tokens = paddle.randint(0, 10000, (1, 8))
tokens[0,0] = 1  # 手工加上开始符cls
tokens[0,(3,7)] = 2  # 手工加上分隔符seq
segments = paddle.to_tensor([[0, 0, 0, 0, 1, 1, 1, 1]])
encoded_X = testErnieEmbeddings(tokens, segments)
encoded_X.shape

学习ErniePooler

transformer模型很有趣的一点,就是每个输出token都会带全局的信息,因此很多后续判断只需要拿到第一个token的值即可,而ErniePooler就是实现这个功能的。

class ErniePooler(nn.Layer):
    def __init__(self, hidden_size, weight_attr=None):
        super(ErniePooler, self).__init__()
        self.dense = nn.Linear(hidden_size, hidden_size, weight_attr=weight_attr)
        self.activation = nn.Tanh()

    def forward(self, hidden_states):
        # We "pool" the model by simply taking the hidden state corresponding
        # to the first token.
        first_token_tensor = hidden_states[:, 0]
        pooled_output = self.dense(first_token_tensor)
        pooled_output = self.activation(pooled_output)
        return pooled_output
# 测试ErniePooler
testerniepooler = ErniePooler(768)
output = testerniepooler(encoded_X)
print(encoded_X.shape, output.shape)
# print(paddle.dtype(9),paddle.dtype(10))

学习Ernie模型

这是Ernie的主训练模型,相当于《动手学深度学习》里BERT模型部分的BERT Encoder部分。

class ErnieModel(nn.Layer):
    r"""
    The bare ERNIE Model transformer outputting raw hidden-states.

    This model inherits from :class:`~paddlenlp.transformers.model_utils.PretrainedModel`.
    Refer to the superclass documentation for the generic methods.

    This model is also a Paddle `paddle.nn.Layer `__ subclass. Use it as a regular Paddle Layer
    and refer to the Paddle documentation for all matter related to general usage and behavior.

    Args:
        config (:class:`ErnieConfig`):
            An instance of ErnieConfig used to construct ErnieModel
    """

    def __init__(self, initializer_range, num_attention_heads, intermediate_size,
                 vocab_size, hidden_size, pad_token_id, max_position_embeddings, 
                type_vocab_size, hidden_dropout_prob, hidden_act, attention_probs_dropout_prob,
                num_hidden_layers):
        super(ErnieModel, self).__init__()
        self.pad_token_id = pad_token_id
        self.initializer_range = initializer_range
        weight_attr = paddle.ParamAttr(
            initializer=nn.initializer.TruncatedNormal(mean=0.0, std=self.initializer_range)
        )
        self.embeddings = ErnieEmbeddings(vocab_size, hidden_size, pad_token_id, max_position_embeddings, 
                type_vocab_size, hidden_dropout_prob=0.1, weight_attr=weight_attr)
        encoder_layer = nn.TransformerEncoderLayer(
            hidden_size,
            num_attention_heads,
            intermediate_size,
            dropout=hidden_dropout_prob,
            activation=hidden_act,
            attn_dropout=attention_probs_dropout_prob,
            act_dropout=0,
            weight_attr=weight_attr,
            normalize_before=False,
        )
        self.encoder = nn.TransformerEncoder(encoder_layer, num_hidden_layers)
        self.pooler = ErniePooler(hidden_size, weight_attr)
        # self.apply(self.init_weights)

    def get_input_embeddings(self):
        return self.embeddings.word_embeddings

    def set_input_embeddings(self, value):
        self.embeddings.word_embeddings = value

    def forward(
        self,
        input_ids: Optional[Tensor] = None,
        token_type_ids: Optional[Tensor] = None,
        position_ids: Optional[Tensor] = None,
        attention_mask: Optional[Tensor] = None,
        past_key_values: Optional[Tuple[Tuple[Tensor]]] = None,
        inputs_embeds: Optional[Tensor] = None,
        use_cache: Optional[bool] = None,
        output_hidden_states: Optional[bool] = None,
        output_attentions: Optional[bool] = None,
        return_dict: Optional[bool] = None,
    ):
        r"""
        Args:
            input_ids (Tensor):
                Indices of input sequence tokens in the vocabulary. They are
                numerical representations of tokens that build the input sequence.
                It's data type should be `int64` and has a shape of [batch_size, sequence_length].
            token_type_ids (Tensor, optional):
                Segment token indices to indicate different portions of the inputs.
                Selected in the range ``[0, type_vocab_size - 1]``.
                If `type_vocab_size` is 2, which means the inputs have two portions.
                Indices can either be 0 or 1:

                - 0 corresponds to a *sentence A* token,
                - 1 corresponds to a *sentence B* token.

                Its data type should be `int64` and it has a shape of [batch_size, sequence_length].
                Defaults to `None`, which means we don't add segment embeddings.
            position_ids (Tensor, optional):
                Indices of positions of each input sequence tokens in the position embeddings. Selected in the range ``[0,
                max_position_embeddings - 1]``.
                Shape as `[batch_size, num_tokens]` and dtype as int64. Defaults to `None`.
            attention_mask (Tensor, optional):
                Mask used in multi-head attention to avoid performing attention on to some unwanted positions,
                usually the paddings or the subsequent positions.
                Its data type can be int, float and bool.
                When the data type is bool, the `masked` tokens have `False` values and the others have `True` values.
                When the data type is int, the `masked` tokens have `0` values and the others have `1` values.
                When the data type is float, the `masked` tokens have `-INF` values and the others have `0` values.
                It is a tensor with shape broadcasted to `[batch_size, num_attention_heads, sequence_length, sequence_length]`.
                For example, its shape can be  [batch_size, sequence_length], [batch_size, sequence_length, sequence_length],
                [batch_size, num_attention_heads, sequence_length, sequence_length].
                We use whole-word-mask in ERNIE, so the whole word will have the same value. For example, "使用" as a word,
                "使" and "用" will have the same value.
                Defaults to `None`, which means nothing needed to be prevented attention to.
            inputs_embeds (Tensor, optional):
                If you want to control how to convert `inputs_ids` indices into associated vectors, you can
                pass an embedded representation directly instead of passing `inputs_ids`.
            past_key_values (tuple(tuple(Tensor)), optional):
                The length of tuple equals to the number of layers, and each inner
                tuple haves 4 tensors of shape `(batch_size, num_heads, sequence_length - 1, embed_size_per_head)`)
                which contains precomputed key and value hidden states of the attention blocks.
                If `past_key_values` are used, the user can optionally input only the last `input_ids` (those that
                don't have their past key value states given to this model) of shape `(batch_size, 1)` instead of all
                `input_ids` of shape `(batch_size, sequence_length)`.
            use_cache (`bool`, optional):
                If set to `True`, `past_key_values` key value states are returned.
                Defaults to `None`.
            output_hidden_states (bool, optional):
                Whether to return the hidden states of all layers.
                Defaults to `False`.
            output_attentions (bool, optional):
                Whether to return the attentions tensors of all attention layers.
                Defaults to `False`.
            return_dict (bool, optional):
                Whether to return a :class:`~paddlenlp.transformers.model_outputs.ModelOutput` object. If `False`, the output
                will be a tuple of tensors. Defaults to `False`.

        Returns:
            An instance of :class:`~paddlenlp.transformers.model_outputs.BaseModelOutputWithPoolingAndCrossAttentions` if
            `return_dict=True`. Otherwise it returns a tuple of tensors corresponding
            to ordered and not None (depending on the input arguments) fields of
            :class:`~paddlenlp.transformers.model_outputs.BaseModelOutputWithPoolingAndCrossAttentions`.

        Example:
            .. code-block::

                import paddle
                from paddlenlp.transformers import ErnieModel, ErnieTokenizer

                tokenizer = ErnieTokenizer.from_pretrained('ernie-1.0')
                model = ErnieModel.from_pretrained('ernie-1.0')

                inputs = tokenizer("Welcome to use PaddlePaddle and PaddleNLP!")
                inputs = {k:paddle.to_tensor([v]) for (k, v) in inputs.items()}
                sequence_output, pooled_output = model(**inputs)

        """
        if input_ids is not None and inputs_embeds is not None:
            raise ValueError("You cannot specify both input_ids and inputs_embeds at the same time.")
        # print("hello Ernie")
        # init the default bool value
        output_attentions = output_attentions if output_attentions is not None else False
        output_hidden_states = output_hidden_states if output_hidden_states is not None else False
        return_dict = return_dict if return_dict is not None else False
        use_cache = use_cache if use_cache is not None else False
        past_key_values_length = 0
        if past_key_values is not None:
            past_key_values_length = past_key_values[0][0].shape[2]

        if attention_mask is None:
            attention_mask = paddle.unsqueeze(
                (input_ids == self.pad_token_id).astype(self.pooler.dense.weight.dtype) * -1e4, axis=[1, 2]
            )
            if past_key_values is not None:
                batch_size = past_key_values[0][0].shape[0]
                past_mask = paddle.zeros([batch_size, 1, 1, past_key_values_length], dtype=attention_mask.dtype)
                attention_mask = paddle.concat([past_mask, attention_mask], axis=-1)

        # For 2D attention_mask from tokenizer
        elif attention_mask.ndim == 2:
            attention_mask = paddle.unsqueeze(attention_mask, axis=[1, 2]).astype(paddle.get_default_dtype())
            attention_mask = (1.0 - attention_mask) * -1e4

        attention_mask.stop_gradient = True

        embedding_output = self.embeddings(
            input_ids=input_ids,
            position_ids=position_ids,
            token_type_ids=token_type_ids,
            inputs_embeds=inputs_embeds,
            past_key_values_length=past_key_values_length,
        )

        self.encoder._use_cache = use_cache  # To be consistent with HF
        encoder_outputs = self.encoder(
            embedding_output,
            src_mask=attention_mask,
            cache=past_key_values,
            output_attentions=output_attentions,
            output_hidden_states=output_hidden_states,
            return_dict=return_dict,
        )
        if isinstance(encoder_outputs, type(embedding_output)):
            sequence_output = encoder_outputs
            pooled_output = self.pooler(sequence_output)
            return (sequence_output, pooled_output)
        else:
            sequence_output = encoder_outputs[0]
            pooled_output = self.pooler(sequence_output)
            if not return_dict:
                return (sequence_output, pooled_output) + encoder_outputs[1:]
            return BaseModelOutputWithPoolingAndCrossAttentions(
                last_hidden_state=sequence_output,
                pooler_output=pooled_output,
                past_key_values=encoder_outputs.past_key_values,
                hidden_states=encoder_outputs.hidden_states,
                attentions=encoder_outputs.attentions,
            )

测试ErnieModel

tokenizer = ErnieTokenizer.from_pretrained('ernie-1.0')
model = ErnieModel(initializer_range, num_attention_heads, intermediate_size,
                 vocab_size, hidden_size, pad_token_id, max_position_embeddings, 
                type_vocab_size, hidden_dropout_prob, hidden_act, attention_probs_dropout_prob,
                num_hidden_layers)

inputs = tokenizer("Welcome to use PaddlePaddle and PaddleNLP!", "用飞桨,划时代!")
inputs = {k:paddle.to_tensor([v]) for (k, v) in inputs.items()}


sequence_output, pooled_output = model(**inputs)
print(sequence_output.shape, pooled_output.shape)

学习ErnieForPretraining

真正预训练模型开始啦!

ErnieForPretraining参考BERTModel

刚开始没有看到Bert里面那种MLM和NSP,后来发现是因为样子跟BERT里有些不一样导致的。

ErnieLMPredictionHead

语言模型预测头

class ErnieLMPredictionHead(nn.Layer):
    r"""
    Ernie Model with a `language modeling` head on top.
    """

    def __init__(
        self,
        hidden_size, hidden_act, vocab_size, 
        embedding_weights=None,
        weight_attr=None,
    ):
        super(ErnieLMPredictionHead, self).__init__()

        self.transform = nn.Linear(hidden_size, hidden_size, weight_attr=weight_attr)
        self.activation = getattr(nn.functional, hidden_act)
        self.layer_norm = nn.LayerNorm(hidden_size)
        self.decoder_weight = (
            self.create_parameter(
                shape=[vocab_size, hidden_size],
                dtype=self.transform.weight.dtype,
                attr=weight_attr,
                is_bias=False,
            )
            if embedding_weights is None
            else embedding_weights
        )
        self.decoder_bias = self.create_parameter(
            shape=[vocab_size], dtype=self.decoder_weight.dtype, is_bias=True
        )

    def forward(self, hidden_states, masked_positions=None):
        if masked_positions is not None:
            # print("===", hidden_states.shape, masked_positions.shape)
            hidden_states = paddle.reshape(hidden_states, [-1, hidden_states.shape[-1]])
            # print("===", hidden_states.shape, masked_positions.shape)
            hidden_states = paddle.tensor.gather(hidden_states, masked_positions)
        # gather masked tokens might be more quick
        hidden_states = self.transform(hidden_states)
        hidden_states = self.activation(hidden_states)
        hidden_states = self.layer_norm(hidden_states)
        hidden_states = paddle.tensor.matmul(hidden_states, self.decoder_weight, transpose_y=True) + self.decoder_bias
        return hidden_states
# 测试ErnieLMPredictionHead
print(hidden_size, hidden_act, vocab_size)
# mlm_positions = paddle.to_tensor([[1, 5, 2], [6, 1, 5]])
mlm_positions = paddle.to_tensor([2, 3, 6])
testernielmpredictionhead = ErnieLMPredictionHead(hidden_size, hidden_act, vocab_size)
mlm_output = testernielmpredictionhead(sequence_output, mlm_positions)
print(mlm_output.shape)

通过掩码下的预测词元mlm_output和真实标签mlm_Y,我们可以计算在Ernie预训练中的掩码(遮蔽)语言模型任务的交叉熵损失。

mlm_Y = paddle.to_tensor([10, 20, 30])
loss = nn.CrossEntropyLoss(reduction='none')
mlm_l = loss(mlm_output.reshape((-1, vocab_size)), mlm_Y.reshape([-1]))
mlm_l.shape

学习ErniePretrainingHeads

预训练头

class ErniePretrainingHeads(nn.Layer):
    def __init__(
        self,
        hidden_size, hidden_act, vocab_size,
        embedding_weights=None,
        weight_attr=None,
    ):
        super(ErniePretrainingHeads, self).__init__()
        self.predictions = ErnieLMPredictionHead(hidden_size, hidden_act, vocab_size, embedding_weights, weight_attr)
        self.seq_relationship = nn.Linear(hidden_size, 2, weight_attr=weight_attr)

    def forward(self, sequence_output, pooled_output, masked_positions=None):
        prediction_scores = self.predictions(sequence_output, masked_positions)
        seq_relationship_score = self.seq_relationship(pooled_output)
        return prediction_scores, seq_relationship_score
# 测试ErniePretrainingHeads
testErniePretrainingHeads = ErniePretrainingHeads(hidden_size, hidden_act, vocab_size)
prediction_scores, seq_relationship_score = testErniePretrainingHeads(sequence_output, pooled_output)
print(prediction_scores.shape, seq_relationship_score.shape)

学习ErnieForPretraining

需要解决的是源代码里参数初始化的问题,因为原来初始化是在class ErniePretrainedModel(PretrainedModel)里面进行的。直接cp过来报错

源代码里from paddlenlp.transformers import PretrainedModel, register_base_model

这里将其修改为class ErnieForPretraining(nn.Layer):

另外不使用argparse的变量空间,这样代码的可读性好,但是代码传参那块就显得比较臃肿。现在参数都是用全局变量直接传进去,也不太安全。当然这里为了代码可读性,一些缺点可以接受。

class ErnieForPretraining(nn.Layer):
    r"""
    Ernie Model with a `masked language modeling` head and a `sentence order prediction` head
    on top.

    """

    def __init__(self, initializer_range, num_attention_heads, intermediate_size,
                 vocab_size, hidden_size, pad_token_id, max_position_embeddings, 
                type_vocab_size, hidden_dropout_prob, hidden_act, attention_probs_dropout_prob,
                num_hidden_layers):
        super(ErnieForPretraining, self).__init__()
        self.ernie = ErnieModel(initializer_range, num_attention_heads, intermediate_size,
                 vocab_size, hidden_size, pad_token_id, max_position_embeddings, 
                type_vocab_size, hidden_dropout_prob, hidden_act, attention_probs_dropout_prob,
                num_hidden_layers)
        weight_attr = paddle.ParamAttr(
            initializer=nn.initializer.TruncatedNormal(mean=0.0, std=self.ernie.initializer_range)
        )
        self.cls = ErniePretrainingHeads(
            hidden_size, hidden_act, vocab_size,
            embedding_weights=self.ernie.embeddings.word_embeddings.weight,
            weight_attr=weight_attr,
        )

        # self.apply(self.init_weights)

    def init_weights(self, layer):
        """Initialization hook"""
        if isinstance(layer, (nn.Linear, nn.Embedding)):
            # only support dygraph, use truncated_normal and make it inplace
            # and configurable later
            if isinstance(layer.weight, paddle.Tensor):
                layer.weight.set_value(
                    paddle.tensor.normal(
                        mean=0.0,
                        std=self.config.initializer_range,
                        shape=layer.weight.shape,
                    )
                )
        elif isinstance(layer, nn.LayerNorm):
            layer._epsilon = 1e-12
            
    def forward(
        self,
        input_ids: Optional[Tensor] = None,
        token_type_ids: Optional[Tensor] = None,
        position_ids: Optional[Tensor] = None,
        attention_mask: Optional[Tensor] = None,
        masked_positions: Optional[Tensor] = None,
        inputs_embeds: Optional[Tensor] = None,
        labels: Optional[Tensor] = None,
        next_sentence_label: Optional[Tensor] = None,
        output_hidden_states: Optional[bool] = None,
        output_attentions: Optional[bool] = None,
        return_dict: Optional[bool] = None,
    ):
        r"""
        Args:
            input_ids (Tensor):
                See :class:`ErnieModel`.
            token_type_ids (Tensor, optional):
                See :class:`ErnieModel`.
            position_ids (Tensor, optional):
                See :class:`ErnieModel`.
            attention_mask (Tensor, optional):
                See :class:`ErnieModel`.
            inputs_embeds(Tensor, optional):
                See :class:`ErnieModel`.
            labels (Tensor of shape `(batch_size, sequence_length)`, optional):
                Labels for computing the masked language modeling loss. Indices should be in `[-100, 0, ...,
                vocab_size]` (see `input_ids` docstring) Tokens with indices set to `-100` are ignored (masked),
                the loss is only computed for the tokens with labels in `[0, ..., vocab_size]`.
            next_sentence_label (Tensor of shape `(batch_size,)`, optional):
                Labels for computing the next sequence prediction (classification) loss. Input should be a sequence
                pair (see `input_ids` docstring) Indices should be in `[0, 1]`:

                - 0 indicates sequence B is a continuation of sequence A,
                - 1 indicates sequence B is a random sequence.
            output_hidden_states (bool, optional):
                Whether to return the hidden states of all layers.
                Defaults to `False`.
            output_attentions (bool, optional):
                Whether to return the attentions tensors of all attention layers.
                Defaults to `False`.
            return_dict (bool, optional):
                Whether to return a :class:`~paddlenlp.transformers.bert.ErnieForPreTrainingOutput` object. If
                `False`, the output will be a tuple of tensors. Defaults to `False`.

        Returns:
            An instance of :class:`~paddlenlp.transformers.bert.ErnieForPreTrainingOutput` if `return_dict=True`.
            Otherwise it returns a tuple of tensors corresponding to ordered and
            not None (depending on the input arguments) fields of :class:`~paddlenlp.transformers.bert.ErnieForPreTrainingOutput`.

        """
        with paddle.static.amp.fp16_guard():
            outputs = self.ernie(
                input_ids,
                token_type_ids=token_type_ids,
                position_ids=position_ids,
                attention_mask=attention_mask,
                inputs_embeds=inputs_embeds,
                output_attentions=output_attentions,
                output_hidden_states=output_hidden_states,
                return_dict=return_dict,
            )
            sequence_output, pooled_output = outputs[:2]
            prediction_scores, seq_relationship_score = self.cls(sequence_output, pooled_output, masked_positions)

            total_loss = None
            if labels is not None and next_sentence_label is not None:
                loss_fct = paddle.nn.CrossEntropyLoss()
                masked_lm_loss = loss_fct(
                    prediction_scores.reshape((-1, paddle.shape(prediction_scores)[-1])), labels.reshape((-1,))
                )
                next_sentence_loss = loss_fct(
                    seq_relationship_score.reshape((-1, 2)), next_sentence_label.reshape((-1,))
                )
                total_loss = masked_lm_loss + next_sentence_loss
            if not return_dict:
                output = (prediction_scores, seq_relationship_score) + outputs[2:]
                return ((total_loss,) + output) if total_loss is not None else output

            return ErnieForPreTrainingOutput(
                loss=total_loss,
                prediction_logits=prediction_scores,
                seq_relationship_logits=seq_relationship_score,
                hidden_states=outputs.hidden_states,
                attentions=outputs.attentions,
            )

现在Ernie模型代码已经全部学习完毕了,我们需要对ErnieForPretraining进行测试。
测试需要从数据集中读入数据,我们从下一节开始。

读入数据进行测试

导入库和配置参数

from ernie.args import parse_args_mini
import numpy as np
import paddle
from paddlenlp.utils.log import logger
from paddlenlp.data import Stack
from paddle.io import RandomSampler, BatchSampler, Dataset
from paddlenlp.transformers import ErnieTokenizer
from ernie.data_tools.dataset_utils import build_train_valid_test_datasets
import os

# 为了简化build_train_valid_test_datasets代码展示,这里使用了args传参
args = parse_args_mini()  
tokenizer = ErnieTokenizer.from_pretrained('ernie-1.0')
import os
def get_train_data_file(input_dir):
    if len(input_dir.split()) > 1:
        # weight-1 data-prefix-1 weight-2 data-prefix-2 ...
        return input_dir.split()
    else:
        files = [
            os.path.join(input_dir, f)
            for f in os.listdir(input_dir)
            if (os.path.isfile(os.path.join(input_dir, f)) and "_idx.npz" in str(f))
        ]
        files = [x.replace("_idx.npz", "") for x in files]

        if len(files) > 1:
            ret = []
            logger.info("You are using multi-dataset:")
            for x in files:
                ret.append(1.0)
                ret.append(x)
                logger.info("    > set weight of %s dataset to 1.0" % x)
            return ret

    return files

data_file = get_train_data_file("/home/aistudio/pretrain")

num_workers = 0

def create_pretrained_dataset(
    global_batch_size, max_steps, eval_freq,  test_iters, split, 
    masked_lm_prob, short_seq_prob, seed, micro_batch_size, 
    data_file,
    tokenizer,
    data_world_size,
    data_world_rank,
    max_seq_len,
    places=None,
    data_holders=None,
    binary_head=False,
    current_step=0,
):

    train_valid_test_num_samples = [
        global_batch_size * max_steps,
        micro_batch_size * (max_steps // eval_freq + 1) * eval_iters * data_world_size,
        micro_batch_size * test_iters * data_world_size,
    ]

    train_ds, valid_ds, test_ds = build_train_valid_test_datasets(
        data_prefix=data_file,
        args=args,
        tokenizer=tokenizer,
        splits_string=split,
        train_valid_test_num_samples=train_valid_test_num_samples,
        max_seq_length=max_seq_len,
        masked_lm_prob=masked_lm_prob,
        short_seq_prob=short_seq_prob,
        seed=seed,
        skip_warmup=True,
        binary_head=binary_head,
        max_seq_length_dec=None,
        dataset_type="ernie",
    )
    # print(args)

    def print_dataset(data, mode="train"):
        logger.info(f"Sample data for {mode} mode")
        input_ids, segment_ids, input_mask, masked_lm_positions, masked_lm_labels, next_sentence_labels = data
        if tokenizer.pad_token_id in input_ids:
            input_ids = input_ids[0 : list(input_ids).index(tokenizer.pad_token_id)]
        logger.info(tokenizer._decode(input_ids))
        for pos, label in zip(masked_lm_positions, masked_lm_labels):
            input_ids[pos] = label
        logger.info(tokenizer._decode(input_ids))
        logger.info(tokenizer.convert_ids_to_tokens(masked_lm_labels))

    print_dataset(train_ds[0], "train")
    print_dataset(valid_ds[0], "valid")
    print_dataset(test_ds[0], "test")

    def _collate_data(data, stack_fn=Stack()):
        num_fields = len(data[0])
        out = [None] * num_fields
        # 0. input_ids,
        # 1. segment_ids,
        # 2. input_mask,
        # 3. masked_lm_positions,
        # 4. masked_lm_labels,
        # 5. next_sentence_labels
        for i in (0, 1, 2, 5):
            out[i] = stack_fn([x[i] for x in data])
        out[5] = out[5].reshape([-1, 1])
        _, seq_length = out[0].shape
        size = sum(len(x[3]) for x in data)
        # masked_lm_positions
        # Organize as a 1D tensor for gather or use gather_nd
     
        if size % 8 != 0:
            size += 8 - (size % 8)
        out[3] = np.full(size, 0, dtype=np.int32)
        # masked_lm_labels
        out[4] = np.full([size, 1], -1, dtype=np.int64)
        mask_token_num = 0
        for i, x in enumerate(data):
            for j, pos in enumerate(x[3]):
                out[3][mask_token_num] = i * seq_length + pos
                out[4][mask_token_num] = x[4][j]
                mask_token_num += 1

        return out

    def loader(dataset, consumed_samples=0):
        batch_sampler = BatchSampler(
            dataset,
            batch_size=micro_batch_size,
            shuffle=False,
            drop_last=True,
        )
        data_loader = paddle.io.DataLoader(
            dataset=dataset,
            batch_sampler=batch_sampler,
            num_workers=num_workers,
            worker_init_fn=None,
            collate_fn=_collate_data,
            return_list=False,
        )
        return data_loader

    train_dl = loader(train_ds, global_batch_size * current_step)
    valid_dl = loader(
        valid_ds, micro_batch_size * ((current_step + 1) // eval_freq) * eval_iters * data_world_size
    )
    test_dl = loader(test_ds, 0)

    return train_dl, valid_dl, test_dl



# from runpretrain import create_pretrained_dataset, get_train_data_file

global_batch_size = 8
max_steps = 10
eval_freq = 1
test_iters =1 
split = '949,50,1'
masked_lm_prob = 0.15
short_seq_prob = 0.1
seed = 42
worker_num = 1
max_seq_len = 768
binary_head = False
global_step = 10
worker_index = 0  # 多卡的第一张卡
micro_batch_size =8 
eval_iters = 10
# share_folder = None

train_data_loader, valid_data_loader, test_data_loader = create_pretrained_dataset(
    global_batch_size, max_steps, eval_freq,  test_iters, split, 
    masked_lm_prob, short_seq_prob, seed, micro_batch_size,
    data_file,
    tokenizer,
    data_world_size=worker_num,
    data_world_rank=worker_index,
    max_seq_len=max_seq_len,
    binary_head=binary_head,
    current_step=global_step,
    )

模型读入数据验证

testErnieForPretraining = ErnieForPretraining(initializer_range, num_attention_heads, intermediate_size,
                 vocab_size, hidden_size, pad_token_id, max_position_embeddings, 
                type_vocab_size, hidden_dropout_prob, hidden_act, attention_probs_dropout_prob,
                num_hidden_layers)

最后这一步,需要32G高端版环境。

for step, batch in enumerate(train_data_loader):
    input_ids, segment_ids, input_mask, masked_lm_positions, masked_lm_labels, next_sentence_labels = batch
    prediction_scores, seq_relationship_score = testErnieForPretraining(
                            input_ids=input_ids,
                            token_type_ids=segment_ids,
                            position_ids=None,
                            attention_mask=input_mask,
                            masked_positions=masked_lm_positions,
                            )
    print("step:", step, "输入:", input_ids.shape, "掩码推理得分:", prediction_scores.shape, "句子联系得分:", seq_relationship_score.shape)
    break
    

总结

以上就是Ernie模型的源代码学习。

现在,我们已经可以自由修改模型了,比如层数、隐藏层大小以及Head数,通过修改hidden_size等参数即可。事实上Ernie官方就公布了一系列不同大小的Ernie模型,比如Ernie 3.0 Tiny模型系列,就是通过层数、隐藏层大小以及Head数来调整模型的大小。具体Ernie系列模型参数在PaddleNLP/paddlenlp/transformers/ernie/configuration.py文件中配置。

我们也可以修改模型源代码,创建符合自己需求的模型,比如添加 InstructGPT、ChatGPT等论文中的一些新技术以提高模型的表现。

三、ERNIE 1.0 预训练实践

ERNIE是百度开创性提出的基于知识增强的持续学习语义理解框架,它将大数据预训练与多源丰富知识相结合,通过持续学习技术,不断吸收海量文本数据中词汇、结构、语义等方面的知识,实现模型效果不断进化。

ERNIE 1.0 预训练主要由数据预处理和模型预训练两部分组成。

ERNIE 中文预训练更详细的介绍文档请可以参见ERNIE中文预训练介绍。

参考数据预处理文档,整个数据预处理流程高效,40分钟即可完成15G CLUECorpusSmall ERNIE数据制作。需要>=20核的多核处理器,否则速度会按比例降低,比如在AIStudio高级版4核CPU下,需要大约6个小时。

具体数据预处理见ernie/dataset.ipynb子项目。

同时官方提供了 ernie-1.0-base-zh 的悟道一个小规模样本的数据例子。

!git clone  https://github.com/PaddlePaddle/PaddleNLP

悟道小样本预训练示例

这个悟道小样本也有1.3G大小呢。下载并解压已经处理好的数据集。

!mkdir wudao
!cd wudao && wget -c https://paddlenlp.bj.bcebos.com/models/transformers/data_tools/wudao_200g_sample_ernie-1.0-base-zh_ids.npy
!cd wudao && wget -c https://paddlenlp.bj.bcebos.com/models/transformers/data_tools/wudao_200g_sample_ernie-1.0-base-zh_idx.npz

开始预训练。若报错,则重启一下环境,释放掉前面源代码学习时占用的显存即可。

!cd ~/PaddleNLP/model_zoo/ernie-1.0/ && python run_pretrain.py \
    --model_type "ernie" \
    --model_name_or_path "ernie-1.0-base-zh" \
    --tokenizer_name_or_path "ernie-1.0-base-zh" \
    --input_dir "/home/aistudio/wudao" \
    --output_dir "output/ernie-1.0-dp8-gb512" \
    --split 949,50,1 \
    --max_seq_len 512 \
    --micro_batch_size 64 \
    --use_amp true \
    --fp16_opt_level O2 \
    --max_lr 0.0001 \
    --min_lr 0.00001 \
    --max_steps 1000 \
    --save_steps 50000 \
    --checkpoint_steps 5000 \
    --decay_steps 990000 \
    --weight_decay 0.01 \
    --warmup_rate 0.01 \
    --grad_clip 1.0 \
    --logging_freq 200 \
    --num_workers 2 \
    --eval_freq 1000 \
    --device "gpu" \
    --share_folder false

–num_workers 0 训练速度:speed: 0.77 steps/s
–num_workers 2 训练速度:speed: 2 steps/s

Ernie 1.0 CLUECorpusSmall 数据集预训练

CLUECorpusSmall 数据集预处理参考本项目内的ernie/dataset.ipynb。

本项目已经挂载预处理好的CLUECorpusSmall 数据集,在目录/home/aistudio/data/data194775中。将制作好的数据解压成clue_corpus_small_14g_20220104_ids.npy,clue_corpus_small_14g_20220104_idx.npz并移动到参数:input_dir的目录中,即可开始训练。若是多卡,直接在参数重设定好--gpus "0,1,2,3"即可 。

解压CLUECorpusSmall数据集:

# 解压需要2分钟
!mkdir ~/clue_small
!cd  ~/clue_small && unzip -n /home/aistudio/data/data194775/clue_corpus_small_14g_20230228_idx.npz.zip
!cd  ~/clue_small && unzip -n /home/aistudio/data/data194775/clue_corpus_small_14g_20230228_ids.npy.zip

单机多卡训练,以paddle.distributed.launch启动训练,由于预训练1000000步需要耗费大量时间,这里我们以1000步训练进行演示:

# # 耗时17分钟
# !cd ~/PaddleNLP/model_zoo/ernie-1.0/ && python -u  -m paddle.distributed.launch \
#     --gpus "0" \
#     --log_dir "output/ernie-1.0-dp8-gb512/log" \
#     run_pretrain.py \
#     --model_type "ernie" \
#     --model_name_or_path "ernie-1.0-base-zh" \
#     --tokenizer_name_or_path "ernie-1.0-base-zh" \
#     --input_dir "/home/aistudio/clue_small" \
#     --output_dir "output/ernie-1.0-dp8-gb512" \
#     --split 949,50,1 \
#     --max_seq_len 512 \
#     --micro_batch_size 64 \
#     --use_amp true \
#     --fp16_opt_level O2 \
#     --max_lr 0.0001 \
#     --min_lr 0.00001 \
#     --max_steps 1000 \
#     --save_steps 50000 \
#     --checkpoint_steps 5000 \
#     --decay_steps 990000 \
#     --weight_decay 0.01 \
#     --warmup_rate 0.01 \
#     --grad_clip 1.0 \
#     --logging_freq 200 \
#     --num_workers 2 \
#     --eval_freq 1000 \
#     --device "gpu" \
#     --share_folder false \

若要继续训练,加上--continue_training true参数即可。

不用paddle.distributed.launch,直接python运行。预训练100000步时间太久,本项目以1000步为例展示,耗时约8分钟。

!cd ~/PaddleNLP/model_zoo/ernie-1.0/ && python run_pretrain.py \
    --model_type "ernie" \
    --model_name_or_path "ernie-1.0-base-zh" \
    --tokenizer_name_or_path "ernie-1.0-base-zh" \
    --input_dir "/home/aistudio/clue_small" \
    --output_dir "output/ernie-1.0-dp8-gb512" \
    --split 949,50,1 \
    --max_seq_len 512 \
    --micro_batch_size 64 \
    --use_amp true \
    --fp16_opt_level O2 \
    --max_lr 0.0001 \
    --min_lr 0.00001 \
    --max_steps 1000 \
    --save_steps 50000 \
    --checkpoint_steps 5000 \
    --decay_steps 990000 \
    --weight_decay 0.01 \
    --warmup_rate 0.01 \
    --grad_clip 1.0 \
    --logging_freq 200 \
    --num_workers 2 \
    --eval_freq 1000 \
    --device "gpu" \
    --share_folder false \
    --continue_training true

运行日志:

[2023-03-01 10:31:35,435] [    INFO] - [CLS] 书 名 : 王 小 波 全 集 作 者 : 王 小 波 咋 [MASK] 机 构 : 云 南 人 民 出 版 社 原 书 定 价 : 174. [MASK] [MASK] [MASK] 价 格 : 87. 3 折 扣 率 : 5 购 买 链 接 : 随 便 附 上 另 一 个 版 本 [UNK] 号 称 终 极 版 本 [MASK] [MASK] [MASK] 折 [UNK] : http : [UNK] [UNK] www. amazon. cn [UNK] [MASK] e7 [UNK] 8e [UNK] 8b [UNK] e5 [UNK] b0 [UNK] 8f [UNK] e6 [UNK] b3 [UNK] a2 [UNK] e5 [UNK] 85 [UNK] a8 [UNK] e9 [UNK] 9b [UNK] 86 [UNK] [MASK] [MASK] [MASK] bb [UNK] 88 [UNK] e7 [UNK] bb [UNK] 93 [UNK] e7 [MASK]疔 [MASK] 88 垣 [MASK] [MASK] [UNK] a5 [UNK] 97 [UNK] e8 [UNK] a3 [UNK] 85 [UNK] e5 [UNK] 85 [UNK] b110 [UNK] e5 [UNK] 86 [UNK] 8c [MASK]涤 e7 [UNK] 8e [UNK] 8b [UNK] e5 [UNK] b0 [UNK] 8f [UNK] e6 [UNK] b3 [UNK] a2 [UNK] dp [UNK] b0031y7ola [MASK] 渠f [MASK] [MASK] [UNK] 1 暄 3 ? ie [UNK] utf8 [MASK] [MASK] [MASK] [UNK] [UNK] sr [UNK] 8 [UNK] 3 我 的 [MASK] [MASK] : [MASK] [MASK] [MASK] [MASK] 嚣, 别 等 到 想 买 的 时 候 [MASK] [MASK] [MASK] 了, 自 个 刚 下 单 子 枫, 哈. 内 容 简 介 《 [MASK] 小 [MASK] [MASK]哇 》 分 为 [MASK] [MASK], 每 卷 都 以 平 装 和 精 装 珍 藏 版 两 种 装 帧 形 式兼 [MASK] [MASK] 本 套 丛 书 位 平 装 版 。 第 一 卷 、 第 二 卷 为 杂 文 ; 第 三 卷 、 第 四 卷 、 第 五 卷 为 长 篇 小 说 和 剧 本 ; [SEP] 第 六 卷 [MASK] 第 七 卷 为 中 篇 小 说 ; 第 八 卷 逝 [MASK] 篇窒 [MASK] ; 第 九 卷 为 书 信 ; 第 十 卷 为 未 竟 稿 。 本 书 为 第 六 卷 。 王 小 波 是 目 前 中 国 最 [MASK] 创 造 性 的 作 家, 被 誉 为 中 国 的浅 猕 [MASK] [MASK] 卡 夫 卡 英, [MASK] [MASK] [MASK] [MASK] 两 次 获 得 世 界 华 语 文 学 界 [MASK] [MASK] [MASK] 奖 项 [UNK] 台 湾 联 合 报 系 文 学 奖 中 篇 小 说 大 奖 [UNK] 的 中 国 大 陆 作 家 。 其 文 学 创 作 独 特, 富 于 想 像 力 、 幻 想 力 之 余, 却 不 乏 理 性 精 神 。 他 的 文 字刊 [MASK] 透 明 的 也 是 朦 胧 的, 是 [MASK] 份 的 也 是80 [MASK] 的 。 [SEP]
[2023-03-01 10:31:35,437] [    INFO] - [CLS] 书 名 : 王 小 波 全 集 作 者 : 王 小 波 出 版 机 构 : 云 南 人 民 出 版 社 原 书 定 价 : 174. 6 现 售 价 格 : 87. 3 折 扣 率 : 5 购 买 链 接 : 随 便 附 上 另 一 个 版 本 [UNK] 号 称 终 极 版 本 5. 2 折 [UNK] : http : [UNK] [UNK] www. amazon. cn [UNK] [UNK] e7 [UNK] 8e [UNK] 8b [UNK] e5 [UNK] b0 [UNK] 8f [UNK] e6 [UNK] b3 [UNK] a2 [UNK] e5 [UNK] 85 [UNK] a8 [UNK] e9 [UNK] 9b [UNK] 86 [UNK] [UNK] e7 [UNK] bb [UNK] 88 [UNK] e7 [UNK] bb [UNK] 93 [UNK] e7 [UNK] 89 [UNK] 88 [UNK] [UNK] e5 [UNK] a5 [UNK] 97 [UNK] e8 [UNK] a3 [UNK] 85 [UNK] e5 [UNK] 85 [UNK] b110 [UNK] e5 [UNK] 86 [UNK] 8c [UNK] [UNK] e7 [UNK] 8e [UNK] 8b [UNK] e5 [UNK] b0 [UNK] 8f [UNK] e6 [UNK] b3 [UNK] a2 [UNK] dp [UNK] b0031y7ola [UNK] ref [UNK] sr [UNK] 1 [UNK] 3 ? ie [UNK] utf8 [UNK] qid [UNK] [UNK] sr [UNK] 8 [UNK] 3 我 的 建 议 : 要 买 的 抓 紧, 别 等 到 想 买 的 时 候 又 没 货 了, 自 个 刚 下 单 子 了, 哈. 内 容 简 介 《 王 小 波 全 集 》 分 为 十 卷, 每 卷 都 以 平 装 和 精 装 珍 藏 版 两 种 装 帧 形 式 出 版, 本 套 丛 书 位 平 装 版 。 第 一 卷 、 第 二 卷 为 杂 文 ; 第 三 卷 、 第 四 卷 、 第 五 卷 为 长 篇 小 说 和 剧 本 ; [SEP] 第 六 卷 、 第 七 卷 为 中 篇 小 说 ; 第 八 卷 为 短 篇 小 说 ; 第 九 卷 为 书 信 ; 第 十 卷 为 未 竟 稿 。 本 书 为 第 六 卷 。 王 小 波 是 目 前 中 国 最 富 创 造 性 的 作 家, 被 誉 为 中 国 的 乔 依 斯 兼 卡 夫 卡 英, 也 是 唯 位 两 次 获 得 世 界 华 语 文 学 界 的 重 要 奖 项 [UNK] 台 湾 联 合 报 系 文 学 奖 中 篇 小 说 大 奖 [UNK] 的 中 国 大 陆 作 家 。 其 文 学 创 作 独 特, 富 于 想 像 力 、 幻 想 力 之 余, 却 不 乏 理 性 精 神 。 他 的 文 字, 是 透 明 的 也 是 朦 胧 的, 是 本 份 的 也 是 狡 猾 的 。 [SEP]
[2023-03-01 10:31:35,437] [    INFO] - ['出', '版', '6', '现', '售', '5', '.', '2', '.', '[UNK]', '[UNK]', 'e7', '[UNK]', '[UNK]', '89', '[UNK]', '[UNK]', '[UNK]', 'e5', '[UNK]', '[UNK]', 'e7', '[UNK]', 're', '##f', '[UNK]', 'sr', '[UNK]', '[UNK]', 'qi', '##d', '建', '议', '要', '买', '的', '抓', '紧', '又', '没', '货', '了', '王', '小', '波', '全', '集', '十', '卷', '出', '版', ',', '本', '、', '为', '短', '篇', '小', '说', '富', '乔', '依', '斯', '兼', '也', '是', '唯', '位', '的', '重', '要', ',', '是', '本', '狡', '猾']

[2023-03-01 10:43:06,695] [    INFO] - global step 1200, loss: 7.459023, lm_loss: 6.750938, sop_loss: 0.708181, speed: 13.58 steps/s, ips: 54.32 seqs/s, learning rate: 1.20000e-05, loss_scaling: 32768.00, incr_count: 198.00, decr_count: 0.00
[2023-03-01 10:43:20,768] [    INFO] - global step 1400, loss: 7.370703, lm_loss: 6.663066, sop_loss: 0.707598, speed: 14.21 steps/s, ips: 56.85 seqs/s, learning rate: 1.40000e-05, loss_scaling: 32768.00, incr_count: 398.00, decr_count: 0.00

总共预训练100万步,悟道数据集速度是15步/秒,算下来单卡大约要跑18小时。 四卡大约4个多小时。

如果用14G CLUECorpusSmall数据集训练,速度2步/秒,单卡跑138小时 ,4卡34小时。

四、ERNIE微调训练

对大多数人来说,不需要自己去预训练模型,只要调用官方提供的训练好的预训练模型,然后finetune精调训练即可。

百度 ERNIE 团队在 2021 年底发布了百亿级别大模型 ERNIE 3.0 和千亿级别的大模型 ERNIE 3.0 Titan。为了让大模型的能力能够真正在一线业务发挥威力,ERNIE 团队推出了 ERNIE-Tiny 系列的知识蒸馏技术,通过任务无关蒸馏的方法,产出了多个轻量级模型 ERNIE 3.0 Tiny,刷新了中文小模型的成绩,并使这些模型能够直接在 CPU 上进行预测,大大拓展了 ERNIE 模型的使用场景。

2023 年初,ERNIE 团队进一步开源了 ERNIE 3.0 Tiny 模型的 v2 版本,使教师模型预先注入下游知识并参与 多任务训练,大大提高了小模型在下游任务上的效果。ERNIE 3.0 Tiny v2 模型在 in-domain、out-domain、low-resource 的下游任务上比 v1 有了进一步的提升,并且 v2 还开源了 3L128H 结构的模型。

ERNIE 3.0 Tiny v2 多任务学习、在线蒸馏方案效果显著,刷新了中文小模型的 SOTA 成绩。具体对比数据见如下模型 精度-时延 图,横坐标表示在 Arm CPU(高通 865 芯片)上,基于 Arm v8 arch 测试(batch_size=1, seq_len=32)的推理时延(Latency,单位毫秒),纵坐标是 CLUE 10 个任务上的平均精度(包含文本分类、文本匹配、自然语言推理、代词消歧、阅读理解等任务),其中 CMRC2018 阅读理解任务的评价指标是 Exact Match(EM),其它任务的评价指标均是 Accuracy。模型名下方标注了模型的参数量。
ERNIE源码学习与实践:为超越ChatGPT打下技术基础!_第2张图片

图中越靠左上方的模型,精度和性能水平越高。可以看到 ERNIE 3.0 Tiny v2 在同等规模的开源模型中,综合实力领先其他同类型轻量级模型。与 UER/RoBERTa-Base 相比,12L768H 的 ERNIE 3.0-Base 平均精度提升了 4.5 个点,比同等规模的BERT-Base-Chinese 提升 3.7 个点;6L768H 的 ERNIE 3.0-Medium 相比 12L768H 的 UER/Chinese-RoBERTa 高 2.4,比 BERT-Base-Chinese 高 1.7,并且节省一倍运算时间;另外值得一提的是,这些小模型能够直接部署在 CPU 上。

使用 PaddleNLP 只需要一行代码就可以下载并获取 ERNIE 3.0 Tiny 预训练模型,之后可以用自己的下游数据下进行微调。

# from paddlenlp.transformers import *

# tokenizer = AutoTokenizer.from_pretrained("ernie-3.0-tiny-medium-v2-zh")

# # 用于分类任务(本项目中的意图识别任务)
# seq_cls_model = AutoModelForSequenceClassification.from_pretrained("ernie-3.0-tiny-medium-v2-zh")

# # 用于序列标注任务(本项目中的槽位填充任务)
# token_cls_model = AutoModelForTokenClassification.from_pretrained("ernie-3.0-tiny-medium-v2-zh")

# # 用于阅读理解任务
# qa_model = AutoModelForQuestionAnswering.from_pretrained("ernie-3.0-tiny-medium-v2-zh")

可以在paddlenlp.transformers中找到常用的9个模型:‘AutoModelForCausalLM’, ‘AutoModelForConditionalGeneration’, ‘AutoModelForImageGeneration’, ‘AutoModelForMaskedLM’, ‘AutoModelForMultipleChoice’, ‘AutoModelForPretraining’, ‘AutoModelForQuestionAnswering’, ‘AutoModelForSequenceClassification’, ‘AutoModelForTokenClassification’,对应不同的任务。

官方提供了3个例子,我们可以参考下面序列分类微调任务,将python run_seq_cls.py model = AutoModelForSequenceClassification.from_pretrained(model_args.model_name_or_path, num_classes=num_classes)修改为自己的任务即可,比如图像生成任务 model = AutoModelForImageGeneration.from_pretrained(model_args.model_name_or_path, num_classes=num_classes),具体细节请参考Ernie3.0微调训练 。现在Ernie1.0和3.0代码是打通的,训练参数带上不同的模型名字即可。

官方例子序列分类微调任务的原num_train_epochs为16,这里为了节省时间,训练2个epochs 。

# 耗时约11分钟
dataset="chnsenticorp_v2"
!cd ~/PaddleNLP/model_zoo/ernie-1.0/finetune && python run_seq_cls.py \
    --do_train \
    --do_eval \
    --do_predict \
    --model_name_or_path ernie-1.0-base-zh \
    --dataset $dataset \
    --output_dir ./tmp/$dataset \
    --num_train_epochs 2

具体微调训练请参考官方文档ernie-tiny介绍

五、Ernie部署

文档参考FastDeploy ERNIE 3.0 模型 Python 部署示例 和 ERNIE 1.0 模型 Python 部署示例

安装依赖库

# 安装fast_tokenizer以及GPU版本fastdeploy 约15分钟
!pip install fast-tokenizer-python fastdeploy-gpu-python -q -f https://www.paddlepaddle.org.cn/whl/fastdeploy.html

导出部署模型

模型导出目录为~/PaddleNLP/model_zoo/ernie-1.0/finetune/tmp/chnsenticorp_v2/export/

# 开始finetune训练并导出模型
dataset="chnsenticorp_v2"
!cd ~/PaddleNLP/model_zoo/ernie-1.0/finetune && python run_seq_cls.py \
    --do_train \
    --do_eval \
    --do_predict \
    --do_export \
    --model_name_or_path ernie-1.0-base-zh \
    --dataset $dataset \
    --output_dir ./tmp/$dataset \
    --eval_steps 200 \
    --save_steps 200 \
    --metric_for_best_model "eval_accuracy" \
    --load_best_model_at_end \
    --save_total_limit 3 \

部署预测

训练完导出模型之后,可以用于部署,deploy/seq_cls_infer.py文件提供了python部署预测示例。可执行以下命令运行部署示例:

!cd ~/PaddleNLP/model_zoo/ernie-1.0/finetune/ && python deploy/seq_cls_infer.py --model_dir tmp/chnsenticorp_v2/export/ --device gpu --backend paddle

更多部署可以参考ERNIE 1.0 模型 Python 部署示例. FastDeploy ERNIE 3.0 Tiny 模型高性能部署

总结

Ernie系列模型很好很强大,且有很好的延续性,Ernie 1.0 2.0和Ernie3.0 Tiny 均为12层hidden_layers 12attention_heads 和 768hidden_size ,而精度依次提高。Ernie和BERT一样,都是用了Transformer的编码器,而GPT是用了解码器。Ernie 3.0 对标GPT3.0, 现在GPT 4 刚出来,Ernie的大模型新版本:文心一言也将于3.16日发布,让我们拭目以待!

通过Ernie源代码的学习,使我们对Ernie模型有了更深刻的了解,非常有助于今后对BERT、GPT、ChatGPT以及文心大模型的学习和理解。用飞桨,划时代,在AI世界,砥砺前行,有你有我!

调试

报错cannot import name ‘ErnieConfig’

----> 1 from paddlenlp.transformers import ErnieConfig

ImportError: cannot import name 'ErnieConfig' from 'paddlenlp.transformers' (/opt/conda/envs/python35-paddle120-env/lib/python3.9/site-packages/paddlenlp/transformers/__init__.py)

升级PaddleNLP到2.5x

训练报错missing tool_helpers, pip install tool_helpers

> WARNING: could not find index map file /home/aistudio/wudaodata/wudao_200g_sample_ernie-1.0-base-zh_train_indexmap_64000000mns_509msl_0.10ssp_1234s.npy, building the indices on rank 0 ...
int32
 > building sapmles index mapping for train ...
 > missing tool_helpers, pip install tool_helpers please, try to compile locally.
make: Entering directory '/home/aistudio/PaddleNLP/model_zoo/ernie-1.0/data_tools'
/opt/conda/envs/python35-paddle120-env/bin/python3: No module named pybind11
g++ -O3 -Wall -shared -std=c++11 -fPIC -fdiagnostics-color  helpers.cpp -o helpers.cpython-39-x86_64-linux-gnu.so
helpers.cpp:22:10: fatal error: pybind11/numpy.h: No such file or directory
 #include 
          ^~~~~~~~~~~~~~~~~~
compilation terminated.
Makefile:9: recipe for target 'helpers.cpython-39-x86_64-linux-gnu.so' failed
make: *** [helpers.cpython-39-x86_64-linux-gnu.so] Error 1
make: Leaving directory '/home/aistudio/PaddleNLP/model_zoo/ernie-1.0/data_tools'
Making C++ dataset helpers module failed, exiting.

pip install tool_helpers

训练中出现:Found inf or nan

[2023-03-01 10:50:09,151] [    INFO] - valid step 7000, batch: 10, loss: 6.631250, lm_loss: 5.959375, sop_loss: 0.671094, ips: 141 seqs/s
Found inf or nan, current scale is: 65536.0, decrease to: 65536.0*0.5

系统自动把数值减半的提醒,不用管它。

训练到40000异常中断

[2023-03-01 18:29:41,943] [    INFO] - Configuration saved in output/ernie-1.0-dp8-gb512/model_last/config.json
Exception in thread Thread-4:
Traceback (most recent call last):
  File "/opt/conda/envs/python35-paddle120-env/lib/python3.9/site-packages/paddle/fluid/dataloader/dataloader_iter.py", line 623, in _get_data
    data = self._data_queue.get(timeout=self._timeout)
  File "/opt/conda/envs/python35-paddle120-env/lib/python3.9/multiprocessing/queues.py", line 114, in get
    raise Empty
_queue.Empty

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/opt/conda/envs/python35-paddle120-env/lib/python3.9/threading.py", line 980, in _bootstrap_inner
    self.run()
  File "/opt/conda/envs/python35-paddle120-env/lib/python3.9/threading.py", line 917, in run
Traceback (most recent call last):
  File "/home/aistudio/PaddleNLP/model_zoo/ernie-1.0/run_pretrain.py", line 761, in 
    self._target(*self._args, **self._kwargs)
  File "/opt/conda/envs/python35-paddle120-env/lib/python3.9/site-packages/paddle/fluid/dataloader/dataloader_iter.py", line 536, in _thread_loop
    batch = self._get_data()
  File "/opt/conda/envs/python35-paddle120-env/lib/python3.9/site-packages/paddle/fluid/dataloader/dataloader_iter.py", line 638, in _get_data
    raise RuntimeError("DataLoader {} workers exit unexpectedly, " \
RuntimeError: DataLoader 1 workers exit unexpectedly, pids: 18494
    do_train(config)
  File "/home/aistudio/PaddleNLP/model_zoo/ernie-1.0/run_pretrain.py", line 737, in do_train
    save_ckpt(output_dir, model, tokenizer, optimizer, scaler, args, global_step)
  File "/home/aistudio/PaddleNLP/model_zoo/ernie-1.0/run_pretrain.py", line 711, in save_ckpt
    paddle.save(model_dict, os.path.join(output_dir, "model_state.pdparams"))
  File "/opt/conda/envs/python35-paddle120-env/lib/python3.9/site-packages/paddle/framework/io.py", line 811, in save
    _legacy_save(obj, path, protocol)
  File "/opt/conda/envs/python35-paddle120-env/lib/python3.9/site-packages/paddle/framework/io.py", line 856, in _legacy_save
    saved_obj = _build_saved_state_dict(obj)
  File "/opt/conda/envs/python35-paddle120-env/lib/python3.9/site-packages/paddle/framework/io.py", line 78, in _build_saved_state_dict
    save_dict[key] = value.numpy()
  File "/opt/conda/envs/python35-paddle120-env/lib/python3.9/site-packages/paddle/fluid/multiprocess_utils.py", line 135, in __handler__
    core._throw_error_if_process_failed()
SystemError: (Fatal) DataLoader process (pid 18496) exited is killed by signal: Killed. (at /paddle/paddle/fluid/imperative/data_loader.cc:188)

/bin/bash: 行 1: 18378 段错误               (核心已转储) python run_pretrain.py --model_type "ernie" --model_name_or_path "ernie-1.0-base-zh" --tokenizer_name_or_path "ernie-1.0-base-zh" --input_dir "/home/aistudio/wudaodata" --output_dir "output/ernie-1.0-dp8-gb512" --split 949,50,1 --max_seq_len 512 --micro_batch_size 64 --use_amp true --fp16_opt_level O2 --max_lr 0.0001 --min_lr 0.00001 --max_steps 1000000 --save_steps 50000 --checkpoint_steps 5000 --decay_steps 990000 --weight_decay 0.01 --warmup_rate 0.01 --grad_clip 1.0 --logging_freq 200 --num_workers 2 --eval_freq 1000 --device "gpu" --share_folder false

重启一下环境,释放掉前面源代码学习时占用的显存即可。

训练报错

    do_train(config)
  File "/home/aistudio/PaddleNLP/model_zoo/ernie-1.0/run_pretrain.py", line 517, in do_train
    valid_data_loader = valid_data_loader()
TypeError: '_DataLoaderIterMultiProcess' object is not callable

有可能是数据集路径不对。或者需要重启一下环境。

结束语

让我们荡起双桨,在AI的海洋乘风破浪!

飞桨官网:https://www.paddlepaddle.org.cn

因为水平有限,难免有不足之处,还请大家多多帮助。

作者:段春华, 网名skywalk 或 天马行空,济宁市极快软件科技有限公司的AI架构师,百度飞桨PPDE。

我在AI Studio上获得至尊等级,点亮10个徽章,来关注我呀~ https://aistudio.baidu.com/aistudio/personalcenter/thirdview/141218

你可能感兴趣的:(学习,chatgpt,人工智能)