63.0 prompt learning

Posted by 钱爽 on February 8, 2023

模型原理

Prompt Learning是NLP领域继BERT后的又一突破性Topic,总的来说,Prompt是一种为了更好的使用预训练语言模型的知识,采用在输入段添加额外的提示文本的技术。

  1. 将所有下游任务均统一为预训练语言模型任务,从而避免了预训练模型和下游任务之间存在的gap。这与BERT类范式(BERT类模型 + fine-tune)有本质上的不同,BERT类范式是模型适配任务,即通过统一的大模型对接各类NLP下游任务,进行fine-tune。这种方式存在的根本问题在于,预训练大模型的训练方式与下游任务差异巨大,打个比方,我本来擅长做完形填空(Masked Language Model),你非要让我去做阅读理解,肯定发挥不出我的最大优势。而prompt learning是通过任务适配模型,将下游任务统一建模为了预训练语言模型的训练任务(MLM),完美的规避了BERT范式的弊端,从而最大程度地挖掘出预训练模型的潜力。 PL
  2. 如此一来,Prompt Learning在小样本数据集(few shot)上也能够超越Fine-Tuning,即使没有训练数据(zero-shot),Prompt Learning也能达到惊人的效果。
  3. 而且,正是因为Prompt Learning使得所有任务在使用方法上与训练过程完全一致,下游任务可以公用同一套预训练模型底座,通过训练不同任务特定的prompt参数,使得模型高效适配下游任务。 PL 为了告知模型需要执行的任务类型,在输入的文本前添加任务特定的文本前缀(task-specific prefifix)进行提示,这也就是最早的Prompt Learning。通常如果我们只有很少的标注数据,那么就不要去fine-tune预训练语言模型,只训练prompt参数即可。反之,可以选择fine-tune语言模型与训练prompt参数同时进行。注:关于如何fine-tune prompt参数,通常有两种做法,二者本质上是异曲同工的:
    • 在输入前添加一个由任务相关连续向量组成的前缀,并且fix住PLM参数,仅调参此任务相关连续向量。
    • 在输入前添加特殊的tokens作为模板(template),并且仅训练这些tokens的embedding表示。

Prompt的工作流

Prompt的工作流主要包含四个部分: PL 下面我们将结合一个情感分类任务(假设我们需要判断“我喜欢这个电影。”这句话的情感极性),一步步进行拆解分析。

1. Template构建

首先我们需要构建一个模版(Template),Template的作用是将输入和输出重新构造为一个带有mask slots的文本。具体如下:

  1. 定义一个Template,包含两处代填入的slots([x]和[z]):“[x]总而言之,它是一个[z]电影。”
  2. 将[x]用输入文本代入:x = “我喜欢这个电影。”
  3. 最终形成prompting:“我喜欢这个电影。总而言之,它是一个[z]电影。”

prompt learning实际上包含两处feature engineering,Template构建就是其中最重要的一处。如何构造合适的Prompt模版?对于同一个任务,不同的人可能构造出不同的Template。且每个Template都具有合理性。Template的选择,将直接影响prompt learning的最终效果: PL 所以为了强化prompt learning的最终效果,可以进行一些范式拓展。如Prompt Ensemble(多个Template集成)、Prompt Decomposition(分拆成多个Template,每个Template只解决单一问题)等。

2. Verbalizer构建

然后我们需要构建一个答案空间映射(Verbalizer),即通过一个映射函数将预测词与label进行对应。 PL Verbalizer构建是prompt learning的第二处特征工程。上例中我们的label空间是Positive和Negative;答案空间可以是任何表示positive和negative的词,例如Interesting/Fantastic/Happy/Boring/1-Star/Bad,具体的答案空间的选择范围可以由我们指定,并且可以同时指定多个(最终得分取加和)。

3. MLM预测

现在我们就只需要选择合适的预训练模型,对mask slots [z]进行预测。例如本例我们得到了结果fantastic。

如何选择一个合适的预训练语言模型也是需要人工经验判别的。具体的预训练语言模型分类可以分为如下几类:

4. 答案映射

最后,对于得到的answer,我们只需要使用Verbalizer将其映射回原本的label,就得到了“我喜欢这个电影。”这句话的情感极性了。

代码实现

光说不练假把式,下面我们结合代码深入理解prompt learning。

MLM预测

我们知道,MLM和NSP两个任务是目前BERT类预训练语言模型的预训练任务,其中MLM任务就是通过整句来预测被masked的词(Deep BiDirectional Transformers):

import torch.nn as nn 
from transformers import BertModel,BertForMaskedLM

class Bert_Model(nn.Module):
    def __init__(self,  bert_path ,config_file ):
        super(Bert_Model, self).__init__()
        self.bert = BertForMaskedLM.from_pretrained(bert_path,config=config_file)  # 加载预训练模型
    def forward(self, input_ids, attention_mask, token_type_ids):
        outputs = self.bert(input_ids, attention_mask, token_type_ids) # masked LM输出的是masked位置对应的ids的概率,即输出词表中每个词的概率
        logit = outputs[0]  # 池化后的输出 [batch_size, config.hidden_size]
        return logit

下面一段代码,简单的使用了hugging face中的bert-base-uncased进行空缺词(masked word)预测,并且按照概率从大到小排序后,默认输出top 5:

from transformers import pipeline

unmasker = pipeline('fill-mask', model='bert-base-uncased')
pred = unmasker("natural language processing is a [MASK] technology.")
print(pred)

[{'score': 0.1892707794904709, 'token': 3274, 'token_str': 'computer',
  'sequence': 'natural language processing is a computer technology.'},
 {'score': 0.14354866743087769, 'token': 4807, 'token_str': 'communication',
  'sequence': 'natural language processing is a communication technology.'},
 {'score': 0.09429354965686798, 'token': 2047, 'token_str': 'new',
  'sequence': 'natural language processing is a new technology.'},
 {'score': 0.051847927272319794, 'token': 2653, 'token_str': 'language',
  'sequence': 'natural language processing is a language technology.'},
 {'score': 0.040842678397893906, 'token': 15078, 'token_str': 'computational',
  'sequence': 'natural language processing is a computational technology.'}]

基于prompt learning的MLM预测

from transformers import pipeline

unmasker = pipeline('fill-mask', model='bert-base-uncased')
template = "Because it was [MASK]."

text1 = "I really like the film a lot. "
prompt1 = text1 + template
pred1 = unmasker(prompt1)
print(pred1)

[{'score': 0.14730945229530334, 'token': 2307, 'token_str': 'great',
  'sequence': 'i really like the film a lot. because it was great.'},
 {'score': 0.10884211957454681, 'token': 6429, 'token_str': 'amazing',
  'sequence': 'i really like the film a lot. because it was amazing.'},
 {'score': 0.09781624376773834, 'token': 2204, 'token_str': 'good',
  'sequence': 'i really like the film a lot. because it was good.'},
 {'score': 0.046277400106191635, 'token': 4569, 'token_str': 'fun',
  'sequence': 'i really like the film a lot. because it was fun.'},
 {'score': 0.04313807934522629, 'token': 10392, 'token_str': 'fantastic',
  'sequence': 'i really like the film a lot. because it was fantastic.'}]

text2 = "this movie makes me very disgusting. "
prompt2 = text2 + template
pred2 = unmasker(prompt2)
print(pred2)

[{'score': 0.054643284529447556, 'token': 9643, 'token_str': 'awful',
  'sequence': 'this movie makes me very disgusting. because it was awful.'},
 {'score': 0.0503225214779377, 'token': 2204, 'token_str': 'good',
  'sequence': 'this movie makes me very disgusting. because it was good.'},
 {'score': 0.04008961096405983, 'token': 9202, 'token_str': 'horrible',
  'sequence': 'this movie makes me very disgusting. because it was horrible.'},
 {'score': 0.035693779587745667, 'token': 3308, 'token_str': 'wrong',
  'sequence': 'this movie makes me very disgusting. because it was wrong.'},
 {'score': 0.03335856646299362, 'token': 2613, 'token_str': 'real',
  'sequence': 'this movie makes me very disgusting. because it was real.'}]

基于prompt learning的情感分类

现在我们将用prompt learning工作流完整的跑一遍zero-shot情感分类任务。

from prompt import Prompting

model_path = "bert-base-uncased"
prompting = Prompting(model=model_path)

template = "Because it was [MASK]."
vervlize_pos = ["great", "amazin", "good"]
vervlize_neg = ["bad", "awfull", "terrible"]

text1 = "I really like the film a lot. "
prompt1 = text1 + template
pred1 = prompting.prompt_pred(prompt1)
pred_score1 = prompting.compute_tokens_prob(prompt1, token_list1=vervlize_pos, token_list2=vervlize_neg)
print(pred1[:3])
print(pred_score1)
[('great', tensor(9.5558)), ('amazing', tensor(9.2532)), ('good', tensor(9.1464))]
tensor([9.9902e-01, 9.8264e-04])

text2 = "I did not like the film. "
prompt2 = text2 + template
pred2 = prompting.prompt_pred(prompt2)
pred_score2 = prompting.compute_tokens_prob(prompt2, token_list1=vervlize_pos, token_list2=vervlize_neg)
print(pred2[:3])
print(pred_score2)
[('bad', tensor(8.6784)), ('funny', tensor(8.1660)), ('good', tensor(7.9858))]
tensor([0.1236, 0.8764])

基于prompt learning的NER

对于NER任务,prompt learning也可以通过跟情感分类任务一样的做法,询问实体所属的类型集合。

from prompt import Prompting

model_path = "bert-base-uncased"
prompting = Prompting(model=model_path)

template = "Savaş went to Laris to visit the parliament. "
vervlize_per = ["person", "man"]
vervlize_pos = ["location", "city", "place"]

pred_score1 = prompting.compute_tokens_prob(template + "Savaş is a type of [MASK].", token_list1=vervlize_per,
                                            token_list2=vervlize_pos)
print(pred_score1)
tensor([9.9996e-01, 3.6852e-05])

pred_score2 = prompting.compute_tokens_prob(template + "Laris is a type of [MASK].", token_list1=vervlize_per,
                                            token_list2=vervlize_pos)
print(pred_score2)
tensor([0.3263, 0.6737])

基于prompt learning的fine-tune

如果我们有标注数据,如何进行fine-tune呢?我们还是基于上面情感分类的例子。

def fine_tune(self, sentences, labels, prompt=" Since it was [MASK].", goodToken="good", badToken="bad"):
        """
        对已有标注数据进行Fine tune训练。
        """
        good = self.tokenizer.convert_tokens_to_ids(goodToken)
        bad = self.tokenizer.convert_tokens_to_ids(badToken)
        from transformers import AdamW
        optimizer = AdamW(self.model.parameters(), lr=1e-3)
        
        for sen, label in zip(sentences, labels):
            tokenized_text = self.tokenizer.tokenize(sen + prompt)
            indexed_tokens = self.tokenizer.convert_tokens_to_ids(tokenized_text)
            tokens_tensor = torch.tensor([indexed_tokens])
            mask_pos = tokenized_text.index(self.tokenizer.mask_token)
            outputs = self.model(tokens_tensor)
            predictions = outputs[0]
            pred = predictions[0, mask_pos][[good, bad]]
            prob = torch.nn.functional.softmax(pred, dim=0)
            lossFunc = torch.nn.CrossEntropyLoss()
            loss = lossFunc(prob.unsqueeze(0), torch.tensor([label]))
            loss.backward()
            optimizer.step()

代码链接: https://github.com/qianshuang/prompt_learning

参考文献: https://zhuanlan.zhihu.com/p/442486331 https://mp.weixin.qq.com/s/uGm0CquGEwvK8-3JLMhmTw https://xiaosheng.run/2022/09/10/what-is-prompt.html


分享到: 微博 微信


返回顶部