• prompt learning——你需要掌握的基础知识以及离散型 prompt 的代码


    最近在写 prompt learning 的代码,所以这里就整理了一份关于离散型 prompt learning 的代码以及相关知识。
    如果你对 预训练语言模型预训练微调prompt learning 十分了解,只想要代码,可以直接跳转到第二章。

    1 基础知识

    本章争取用最简单最直观的语言,给你介绍何为预训练语言模型预训练微调,以及prompt learning

    1.1 预训练

    预训练(pre-training)指的是在大型语料库上面采用预训练技术(比如 Masked Language model(MLM)等)从零(from scratch,可以是随机初始化,也可以是全零)或者从某个初始点(start point)[1] 开始训练一个模型。该任务的核心就是要模型能够学习到你期望它学习到的语言知识,而非在某个数据集上执行类似于分类任务等下游任务。这类任务通常表现为会输出一个或多个损失,并且反向传播这些损失。

    1.2 微调

    微调(fine-tune)指的是在下游任务中使用某个模型执行指定任务。在这些任务中,使用的语言模型是已经训练好了的,可以选择参与微调,也可以固定住(因为语言模型已经在预训练中学习到了大量知识,所以即使固定住也能有很好的表现)。而在微调阶段,最需要学习的是下游任务特定的参数(task-specific parameters),比如 BertForSequenceClassification[CLS] 需要经过的全连接层(下面会讲到)。

    1.3 预训练语言模型

    预训练语言模型(pre-train language model, PTM) 指的是在大规模语料库上经过训练后,可以获得携带语义、句法、上下文等知识词向量的模型。自 2013 年 Word2Vec 提出后,就开始涌出了许多基于神经网络的预训练语言模型。

    • Word2Vec: 关于 Word2Vec[2] 的细节可以看我以前的博客:Word2Vec原理与公式详细推导Word2Vec之Hierarchical Softmax与Negative SamplingWord2Vec 简单来说就是一个只有一层隐藏层的 EncoderDecoder,其 EncoderDecoder 都是全连接层。通过滑动窗口,让词语 i i i 去预测其周围的 n n n 个上下文,或者通过词语 i i i n n n 个上下文来预测词语 i i i。通过训练,可以使得每个词语的词向量携带上其上下文的知识(越容易出现在词语 i i i 周围的词语,在训练过程中的损失会越小,通过反向传播,这个词语的信息就越能够包含在词语 i i i 的词向量中 )。
    • GloVe: 关于 GloVe[3] 的细节可以看我以前的博客:GloVe原理与公式讲解GloVe 论文中提到了 Word2Vec 的滑动窗口只能够使得词语 i i i 获得其上下文的信息,却忽略了全局的信息,所以 GloVe 还考虑了全局共现矩阵,使得词语 i i i 不仅能够有局部上下文信息,还能够包含全局词语共现信息。

    Word2VecGloVe 为代表的预训练语言模型其输出为预训练好的词嵌入,这些词嵌入可以输入到下游任务模型的嵌入层中。但是对于下游任务的模型而言,只有嵌入层不是随机初始化的参数,其余的参数都需要 training from scratch

    • ELMo: 关于 ELMo[4] 的细节可以看我以前的博客:BERT学习笔记(4)——小白版ELMo and BERT。 由于 Word2VecGloVe 捕获的都是局部上下文信息(因为是滑动窗口),ELMo 为了能够捕获到一个句子的全部信息,采用了双向 LSTM,通过前向与反向的 LSTM 捕获词语 i i i 的前向信息与反向信息,并在下游任务中调整前向与反向信息的权重 。

    虽然也许 maybe possibly 看上去 ELMo 能够获得一个词语 i i i 在句子中的所有上文信息与下文信息,但是仔细想想 LSTM 的结构,长距离的信息经过多次 tanh(遗忘门)后,还能保留多少?所以虽然 LSTM 的全称为 Long Short-Term Memory,但是实际上提取的还是局部信息。

    • BERT: 关于 BERT[5] 的细节可以看我以前的博客:BERT学习笔记(4)——小白版ELMo and BERTBERT 采用了 Transformer 的结构,通过 self-attention 能够使得每个词语获得整个句子中的信息,但是 self-attention 会使得词语 i i i j j j 之间无论在句子中距离多远,在 attenton 的过程中其间距都为 1。所以 BERT 中还考虑了位置信息嵌入,使得 self-attention 时词语与词语之间会有位置信息。

    BERT 提出后,整个预训练语言模型进入到了第三范式(第一范式为以 one-hotTF-IDF 模型为代表的机器学习与基于规则的方法,第二范式为以 Word2VecGloVe 模型为代表的预训练词嵌入并输入到下游模型的嵌入层)。而第三范式指的是预训练好的语言模型(比如 BERT)只用微调部分参数,其主干无需经过任何训练,都可以很好地在下游任务中取得很好的效果。以原文中的图来说:
    BERT downstream tasks

    1. Text classification tasks: 对于单句文本分类任务(图 b)和双句文本分类任务(图 a),只需要获得 BERT[CLS] 的隐藏状态([CLS]BERT 输入的第一个位置的特殊符号,可以理解为 [CLS] 的隐藏状态代表了整个句子的句向量,即 [CLS] 包含了整个句子的信息),再在 [CLS] 的隐藏状态后拼接个全连接层,做文本分类。而这个全连接层就是要在下游任务中微调的部分。我们也可以在每次实例化 BertForSequenceClassification 以及源码中看得到:
    # 实例化 BertForSequenceClassification 时的提示
    Some weights of the model checkpoint at H:\huggingface\bert-base-uncased were not used when initializing BertForSequenceClassification: ['cls.predictions.transform.LayerNorm.bias', 'cls.predictions.bias', 'cls.seq_relationship.bias', 'cls.seq_relationship.weight', 'cls.predictions.transform.dense.bias', 'cls.predictions.transform.dense.weight', 'cls.predictions.transform.LayerNorm.weight', 'cls.predictions.decoder.weight']
    - This IS expected if you are initializing BertForSequenceClassification from the checkpoint of a model trained on another task or with another architecture (e.g. initializing a BertForSequenceClassification model from a BertForPreTraining model).
    - This IS NOT expected if you are initializing BertForSequenceClassification from the checkpoint of a model that you expect to be exactly identical (initializing a BertForSequenceClassification model from a BertForSequenceClassification model).
    Some weights of BertForSequenceClassification were not initialized from the model checkpoint at H:\huggingface\bert-base-uncased and are newly initialized: ['classifier.weight', 'classifier.bias']
    You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.
    
    # BertForSequenceClassification 有关全连接层的源码
    # __init__:
    self.classifier = nn.Linear(config.hidden_size, config.num_labels)
    # forward: 
    # 这里的 outputs[1] 是 [CLS] 在 BertModel 经过 Pooler 层的输出
    pooled_output = outputs[1]
    pooled_output = self.dropout(pooled_output)
    logits = self.classifier(pooled_output)
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    1. Sequence Labeling tasks: 对于 QA 任务(图 c)和 NER(图 d)等序列标注任务,只需要获得每个 token 的隐藏状态,并过个全连接层,输出每个 token 位置的隐藏状态(可以类比于 LSTM 输出了每个 token 的隐藏状态后接个全连接层输出 logits)。而这个全连接层也就是在下游任务中微调的部分。

    对于第三范式而言,语言模型已经能够在预训练的时候学习到丰富的语义、句法、上下文等信息,在做下游任务时都可以将模型给固定住,需要微调的参数量基本上是 ( e m b e d _ s i z e × n u m _ o u t p u t s ) (embed\_size \times num\_outputs) (embed_size×num_outputs) ( e m b e d _ s i z e × m a x _ s e q _ l e n g t h × n u m _ o u t p u t s ) (embed\_size \times max\_seq\_length \times num\_outputs) (embed_size×max_seq_length×num_outputs)

    但是第三范式也带来了个问题:

    1. 大模型在大语料库中进行了预训练,语言模型本身就已经能够携带上许多语言知识,那么能否不做微调,仅仅使用预训练好的语言模型完成下游任务?
    2. 比如 GPT-3 等只能付费调用不能微调的模型,以及比如只有一张10G显存2080Ti的难民没法实现微调,如何使用强大的文本表示?

    所以就有了 prompt learning,无需在下游任务中微调任何参数,也可以完成下游任务[6-7]。

    注: 对于刚学完 Word2Vec 等预训练语言模型的同学可能会误以为 BERT embedding 指的是 BERT 的 embedding layer,然而并不是这样的。在 torch 中 BERTembedding layer 实际上和传统模型的 embedding layer 是一模一样的,就是一个词语 id 与 embedding layer 中权重的映射关系,但是 BERTTransformer-based model 又被称为 contextualized word embedding,这是因为 BERT embedding 指的是句子输入后,经过了 12 层 Encoder 后得到的隐藏状态,这才是 BERT 的词嵌入过程。 如下图所示,这是在不同句子中的 苹果 经过 BERT embedding 后的余弦相似度。
    BERT embedding

    2 prompt learning

    prompt learning 的细节可以看我之前的博客:什么是 prompt learning?简单直观理解 prompt learning。简单来说,prompt learning 就是期望预训练语言模型在下游任务中,无需额外的训练参数,只需要模型本身就能够解决问题。

    prompt learning 中有两个很关键的内容:prompt templateverbalizer

    • prompt template: 提示句,句子中得有 [MASK] 标签,这是期望模型能够输出的内容。比如情感分析任务中可以构造为:[X]. Totally, it was [MASK]。其中 [X] 是输入文本。
    • verbalizer: 期望模型填空的内容,以及其对应的标签。就比如以上句为例,我们期望模型做单选题,在 ‘good’,‘bad’ 中选择,并且我们还要设定 ‘good’ 代表积极,‘bad’ 代表消极。

    以上两者分别都有许多研究内容,但是本文只针对人工构造的离散型 prompt template(即人工设定文本,而非让模型自己去学习文本)和 给定 verbalizer 范围。我们期望预训练好的 BERTIMDB 数据进行情感分类。

    数据与代码已打包在 github 上,链接为:https://github.com/Balding-Lee/prompt-learning,即插即用。

    关于数据处理部分,以及封装 batch 部分这里不再具体描述,详细可见 github,这里只详细说明预测部分。由于之前已经说过,prompt learning 无需微调也可以在下游任务中运行,所以给定:

    prefix = 'Totally, it was [MASK].'
    verbalizer = {
        'good': 1,
        'fascinating': 1,
    	'perfect': 1,
    	'bad': 0,
    	'horrible': 0,
    	'terrible': 0,
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    预测代码如下:

    with torch.no_grad():
        predict_all = np.array([], dtype=int)
        labels_all = np.array([], dtype=int)
        pper = pyprind.ProgPercent(batch_count)
        for i in range(batch_count):
            inputs = batch_X[i]
            labels = batch_y[i]
    
            tokens = tokenizer.batch_encode_plus(inputs, add_special_tokens=True,
                                                 max_length=config.max_seq_length,
                                                 padding='max_length', truncation=True)
            ids = torch.tensor(tokens['input_ids']).to(device)
            attention_mask = torch.tensor(tokens['attention_mask']).to(device)
    
            # shape: (batch_size, max_seq_length, vocab_size)
            logits = bert(ids, attention_mask=attention_mask).logits
    
            # mask_token_index[0]: 第 i 条数据
            # mask_token_index[1]: 第 i 条数据的 [MASK] 在序列中的位置
            mask_token_index = (ids == tokenizer.mask_token_id).nonzero(as_tuple=True)
    
            # 找到 [MASK] 的 logits
            # shape: (batch_size, vocab_size)
            masked_logits = logits[mask_token_index[0], mask_token_index[1], :]
    
            # 将 [MASK] 位置中 verbalizer 里的词语的 logits 给提取出来
            # shape: (batch_size, verbalizer_size)
            verbalizer_logits = masked_logits[:, verbalizer_ids]
    
            # 将这些 verbalizer 中的 logits 给构造一个伪分布
            pseudo_distribution = softmax(verbalizer_logits)
    
            # 找到伪分布中概率最大的 index
            pred_indices = pseudo_distribution.argmax(axis=-1).tolist()
            # 将 index 转换为词语的 id
            pred_ids = [index2ids[index] for index in pred_indices]
            # 将 id 转换为 token
            pred_tokens = tokenizer.convert_ids_to_tokens(pred_ids)
            # 找到 token 对应的 label
            pred_labels = [config.verbalizer[token] for token in pred_tokens]
    
            predict_all = np.append(predict_all, pred_labels)
            labels_all = np.append(labels_all, labels)
    
            pper.update()
    
        acc = accuracy_score(labels_all, predict_all)
        p = precision_score(labels_all, predict_all)
        r = recall_score(labels_all, predict_all)
        f1 = f1_score(labels_all, predict_all)
    
        print('accuracy: %f | precision: %f | recall: %f | f1: %f' % (acc, p, r, f1))
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52

    实验结果为:

    accuracy: 0.616440 | precision: 0.568737 | recall: 0.963440 | f1: 0.715249
    
    • 1

    这里需要说明的有以下几点:

    1. mask_token_index 是个元组,其中 [0] 是指的第 i i i 条数据,比如我设置 batch 为 4 的话,那么 m a s k _ t o k e n _ i n d e x [ 0 ] = [ 0 , 1 , 2 , 3 ] {\rm mask\_token\_index}[0]=[0, 1, 2, 3] mask_token_index[0]=[0,1,2,3],对应第一条数据,第二条数据,第三条数据和第四条数据。而 [1] 指的是第 i i i 条数据其 [MASK] 所在的位置,假设, m a s k _ t o k e n _ i n d e x [ 1 ] = [ 4 , 16 , 8 , 32 ] {\rm mask\_token\_index}[1]=[4, 16, 8, 32] mask_token_index[1]=[4,16,8,32],那么就是第1条数据第5个位置、第2条数据第17个位置、第3条数据第9个位置和第4条数据第33个位置为 [MASK]。
    2. 由于 BertForMaskedLMlogits 的形状为 ( b a t c h _ s i z e , m a x _ s e q _ l e n g t h , v o c a b _ s i z e ) (batch\_size, max\_seq\_length, vocab\_size) (batch_size,max_seq_length,vocab_size),指的是 batch 中每个 token 的隐藏状态在词表中对应的 logits。而我们只需要 [MASK] 位置下 verbalizer 里这几个词语的分布,所以我们找到每句话中 [MASK] 位置的在 verbalizer 里这些词语的 logits,并使用 softmax 对这些 logits 做个伪分布。这个伪分布中得分最高的即为预测词语。
    3. 当我们得到预测词语后,再回到 verbalizer 里找到这个词语对应的标签,就得到了 prompt learning 里的预测标签。

    参考

    [1] Zhengyan Zhang, Xu Han, Zhiyuan Liu, Xin Jiang, Maosong Sun, Qun Liu. ERNIE: Enhanced Language Representation with Informative Entities [C]// Proceeding of the 57th Annual Meeting of the Association for Computational Linguistics, 2020: 1441-1451.
    [2] Tomas Mikolov, Kai Chen, Greg Corrado, et al. Efficient Estimation of Word Representations in Vector Space[C]//ICLR (Workshop Poster), 2013.
    [3] Jeffrey Pennington, Richard Socher, Christopher D. Manning. Glove: Global Vectors for Word Representation[C]// Conference on Empirical Methods in Natural Language Processing, 2014.
    [4] Matthew E. Peters, Waleed Ammar, Chandra Bhagavatula, Russell Power. Semi-supervised sequence tagging with bidirectional language models[C]//Proceedings of the 55th Annual Meeting of the Association for Computational Linguistics, 2017.
    [5] Jacob Devlin, Ming-Wei Chang, Kenton Lee, Kristina Toutanova. BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding[C]//2019 Conference of the North American Chapter of the Association for Computational Linguistics: Human Language Technologies, NAACL-HLT, 2019.
    [6] LIU P, YUAN W, FU J, et al. Pre-train, Prompt, and Predict: A Systematic Survey of Prompting Methods in Natural Language Processing [J]. CoRR, 2021, abs/2107.13586.
    [7] 刘鹏飞. 近代自然语言处理技术发展的“第四范式”[EB/OL]. (2021-08-01)[2022-01-03]. https://zhuanlan.zhihu.com/p/395115779

  • 相关阅读:
    为什么在2024年应该使用AVIF而不是JPEG、WebP、PNG和GIF
    Python-爬虫(基础概念、常见请求模块(urllib、requests))
    前端基础建设与架构12 如何理解 AST 实现和编译原理?
    linux elf relationship between data structures involved in symbol resolution
    shiro的实现认证
    计算机导论真题(二)
    Centos7服务器同步网络发现漏洞与修复手册(每周更新3次)
    离屏渲染 &FBO
    Shopee市场爆单难?找准选品逻辑方式
    SQL使用注意事项
  • 原文地址:https://blog.csdn.net/qq_35357274/article/details/126425211