前沿重器[33] | 试了试简单的prompt

技术

前沿重器

栏目主要给大家分享各种大厂、顶会的论文和分享,从中抽取关键精华的部分和大家分享,和大家一起把握前沿技术。具体介绍:仓颉专项:飞机大炮我都会,利器心法我还有。(算起来,专项启动已经是20年的事了!)

2022年的文章合集,累积起来有60w字,在这:CS的陋室60w字原创算法经验分享-2022版

往期回顾

prompt这个东西在现阶段应该不算新东西了,大家的关注点也到了后续的别的研究上了,前段时间拓展工具库的想法,于是开始想尝试一下prompt的效果。

懒人目录:

  • 先说下原理
  • 代码
  • 代码细节
  • 有关实验后的一些有意义的结论
  • 小结
  • 参考文章

先说下原理

所谓的prompt,简单而又笼统地说,其实就是把传统的NLP问题转化为一个类似我们以前做的“完形填空”一样,然后用MLM任务来预测对应的结果。以文本分类为例,例如分好评和差评的二分类,常规的方式是把句子输入到模型中,让模型预测正负,而在prompt中,我们对句子补充些内容,然后预测挖的空来判断正负。

例如一个句子“我觉得这个商品是我买的最对的商品了”,此时给句子进行一些补充,例如改成“句子:我觉得这个商品是我买的最对的商品了。这是一个[MASK]评”,此时我们只需要对比这里填“好”的概率和“差”的概率其实就能分析出最终的结果了。

是不是觉得原理超级简单,那后面就上代码了。

代码

首先是一些比较常规的提前配置,包括一些超参数和预训练模型的加载。


        
          
# 超参数  
hidden_dropout_prob = 0.3  
num_labels = 2  
learning_rate = 1e-5  
weight_decay = 1e-2  
epochs = 15  
batch_size = 16  
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")  
prefix = "这也太[MASK]了吧,"  # prompt的配置在合理,为了简单化我直接用的是前缀的形式,其实大家要有时间改一下也可以成为后缀。  
maskpos = 4  
  
# 预训练模型路径  
ptm_path = "./data/ptms/bert-base-chinese/"  
vocab_file = ptm_path +"vocab.txt"               # 词汇表  
tokenizer = BertTokenizer(vocab_file)  
config = BertConfig.from_pretrained(ptm_path + "config.json")  
device = torch.device("cuda:0" if  torch.cuda.is_available() else "cpu")  
model = Bert_Model(bert_path=ptm_path + "pytorch\_model.bin", config_file=config).to(device)  
  
# 提前提取正负类代表的词汇的id,方便后面提取概率。  
pos_id = tokenizer.convert_tokens_to_ids('棒')  
neg_id = tokenizer.convert_tokens_to_ids('差')  
  
# 只是做实验,所以这里的训练配置都是用的非常默认而且简单的。  
loss_func = nn.CrossEntropyLoss(ignore_index=-1)  
optimizer = AdamW(model.parameters(),lr=2e-5,weight_decay=1e-4)  #使用Adam优化器  

      

此处的预训练模型,用的是这个结构,这里使用的是BertForMaskedLM,就是一个MLM模型的结构,相信大家都是比较熟悉的了,transformers库里面集成了很多任务类型的基础结构,用起来都很方便。


        
          
from transformers import BertForMaskedLM  
class Bert\_Model(nn.Module):  
    def \_\_init\_\_(self, bert\_path, config\_file):  
        super(Bert_Model, self).__init__()  
        print(bert_path)  
        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)  
        logit = outputs.logits  # 池化后的输出 [bs, config.hidden\_size]  
  
        return logit   

      

数据集其实是一个挺部分,我把拼接放在了这一步,具体函数就在这个prompt_dataset里面。


        
          
# 训练集整理  
Inputid, Labelid, sid, atid = prompt_dataset(x_train, y_train, prefix, tokenizer, maskpos)  
Inputid = np.array(Inputid)  
Labelid = np.array(Labelid)  
sid = np.array(sid)  
atid = np.array(atid)  
# 偷个懒,验证集和训练集一样  
input_ids_train,  input_ids_valid  = Inputid, Inputid  
input_masks_train,  input_masks_valid = atid, atid  
input_types_train, input_types_valid = sid, sid  
label_train, y_valid = Labelid, Labelid  

      

来看看prompt_dataset的具体怎么写,这里比较特别的其实就是text_ = prefix + x[i],就是把前缀和句子拼接在一起,然后后面就是比较常规的转化了。


        
          
def prompt\_dataset(x, y, prefix, tokenizer, maskpos):  
    # prompt x: 原始数据输入, y: 输出,prefix: 前缀,tokenizer: 转换器  
    Inputid = []  
    Labelid = []  
    sid = []  
    atid = []  
    for i in range(len(x)):  
        text_ =  prefix + x[i]  
        encode_dict = tokenizer.encode_plus(text_, max_length=200, padding='max\_length', truncation=True, add_special_tokens=True)  
  
        id = encode_dict["input\_ids"]  
        segmentid = encode_dict["token\_type\_ids"]  
        attid = encode_dict["attention\_mask"]  
        labelid, inputid = id[:], id[:]  
        if y[i] == 0:  
            labelid[maskpos] = neg_id  
            labelid[: maskpos ] = [-1]*len(labelid[: maskpos ])  
            labelid[maskpos + 1 : ] = [-1]*len(labelid[maskpos + 1 : ])  
            inputid[maskpos] = tokenizer.mask_token_id  
        else:  
            labelid[maskpos] = pos_id  
            labelid[: maskpos] = [-1] * len(labelid[: maskpos])  
            labelid[maskpos + 1:] = [-1] * len(labelid[maskpos + 1:])  
            inputid[maskpos] = tokenizer.mask_token_id  
        Labelid.append(labelid)  
        Inputid.append(inputid)  
        sid.append(segmentid)  
        atid.append(attid)  
      
    return Inputid, Labelid, sid, atid  

      

为了方便,我这里还写了一个预测单个case的函数,在平时自测啥的,都会挺方便,这里用的概率


        
          
def pred\_single(model, data\_info, maskpos, pos\_id, neg\_id):  
    ids, att, tpe= list2cuda(data_info["Inputid"]), list2cuda(data_info["atid"]), list2cuda(data_info["sid"])  
    out  = model(ids, att, tpe)  
    tout_train_mask = out[:, maskpos, :] # 预测值,这里是这个位置所有token的概率。  
    pos_score = tout_train_mask[:,pos_id].cpu().detach().numpy().tolist() # 正类关键词的概率  
    neg_score = tout_train_mask[:,neg_id].cpu().detach().numpy().tolist() # 负类关键词的概率  
    # print(pos\_score, neg\_score)  
    pred = cal_pred(pos_score, neg_score)  
    return pred  
  
def list2cuda(data):  
    return torch.from_numpy(np.array(data)).long().to(device)  
  
def cal\_pred(pos\_score, neg\_score):  
    # 计算正负类概率,取高  
    # print(pos\_score, neg\_score)  
    pred = []  
    for idx in range(len(pos_score)):  
        if pos_score[idx] >= neg_score[idx]:  
            pred.append(1)  
        else:  
            pred.append(0)  
    return pred  

      

然后是还比较关键的训练代码,开始之前需要专门说的是,这个训练不着急做,可以在无监督的情况下先直接试试效果,其实只要设计到比较好的prompt,我的实验是能达到80%这个水平,这个水平不算高,但是在无监督没什么数据的情况下,已经是一个很高的baseline了,这点就非常值得我们吸收学习了。

下面就是重头戏了,训练,其实训练的部分也比较简单,损失函数就是交叉熵(前文已经定义了),我们是希望对应类目的关键词,在这个句子中的概率尽可能高,通过这种方式训练的,剩下就看代码理解吧:


        
          
def train(model, epoch, optimizer, dataset, device, loss\_func):  
    starttime_train = datetime.now()  
    start = time.time()  
    correct = 0  
    train_loss_sum = 0.0  
    model.train()  
    schedule = get_cosine_schedule_with_warmup(optimizer,num_warmup_steps=len(dataset),num_training_steps=epoch*len(dataset))  
    logger.info("***** Running training epoch {} *****".format(epoch + 1))  
    for idx, (ids, att, tpe, y) in enumerate(tqdm(dataset)):  
        ids, att, tpe, y = ids.to(device), att.to(device), tpe.to(device), y.to(device)  
        out_train = model(ids, att, tpe)  
        # print(out\_train.view(-1, 21128).shape, y.view(-1).shape)  
        loss = loss_func(out_train.view(-1, 21128), y.view(-1))  
        optimizer.zero_grad()  
        loss.backward()  
        optimizer.step()  
        schedule.step()  
        train_loss_sum += loss.item()  
  
        if (idx + 1) % 100 == 0:  
            logger.info("Epoch {:04d} | Step {:06d}/{:06d} | Loss {:.4f} | Time {:.0f}".format(  
                epoch + 1, idx + 1, len(dataset), train_loss_sum / (idx + 1), time.time() - start))  
  
        truelabel = y[:, maskpos]  
        out_train_mask = out_train[:, maskpos, :]  
  
        predicted = torch.max(out_train_mask.data, 1)[1]  
        correct += (predicted == truelabel).sum()  
        correct = np.float(correct)  
    acc = float(correct / len(label_train))  

      

其实看代码会发现非常常规,就是一般的MLM任务的训练流程了,让模型预测的token尽可能接近label。

代码细节

在写这个代码过程中,其实还挺波折,这里面还是有关注到挺多细节的。

  • Transformers所封装的几种常见的模型结构,分类、句子对等,都是需要熟悉了解的,包括这次用到的BertForMaskedLM。大家可以通过文档等方式直接学习。
  • 各种数据类型、设备的转化,熟练度似乎不太够,就是tout_train_mask[:,pos_id].cpu().detach().numpy().tolist()
  • 另外注意,我这种只是为了跑通做玩具的脚本,这个代码风格不值得学习,参考技术方案就好了。
  • 训练并非必须,可以尝试直接不训练地直接预测,好的prompt会有一个还不错的baseline。

有关实验后的一些有意义的结论

实验结果我就不摆在这里了,但又一些有意义的发现直接列举给大家,供大家做实验的参考:

  • 不训练的情况下,多换几种prompt,能得到一个不差的结果,这让我们在比较困难的环境下,也可以得到一个不错的baseline。(我的实验上限F1在80%左右)
  • 不训练的情况下,不同的prompt,对结果的差距非常大,下限能到55%,所以如果不训练,需要花点时间在prompt的设计上。
  • 训练情况,可能是我的数据都偏简单,所以prompt和其他模型,例如bert-cls,差距不是很大,没有显著变好,和数据有关。
  • 训练情况,不同的prompt对最终的效果也会有影响,但不会那么大,收官时间调一调还可以,早期不要花太多时间。
  • 令人惊喜的是,强行压缩训练集,我这里压缩到100条,此时prompt方案仍然能够达到接近全量数据的水平(当然数据量少了epoch就要增加不少,不过收敛的其实挺快的),大概能达到较差的prompt的水平。
  • 这个压缩数据集的fewshot的场景下能有这个效果,是bert-cls、textcnn等经典方法都办不到的,所以小数据集下,可以试试这个方案的。
  • 上述效果是建立在大型预训练模型的基础上的,CNN等的一些比较小的结果似乎没有这个效果,大家需要注意。

小结

这次尝试算是刷新了我的工具库了,是一个比较新的,有新的适配场景的方案了,在few-shot场景下,这个方案有非常令人惊喜的结果,这个可以说是比较大的卖点了,推荐大家也用来试试,除了分类任务外,ner等任务其实也可以尝试下的。

参考文章

picture.image

0
0
0
0
关于作者
关于作者

文章

0

获赞

0

收藏

0

相关资源
DataSail CDC 数据整库实时入仓入湖实践
在线数据库数据导入到数仓分析的链路已经存在多年,随着近年来实时计算的发展,业务希望有延迟更低、运维更便捷、效率更高的CDC同步通道。本次分享主要介绍DataSail实现CDC整库实时同步的技术方案和业务实践。
相关产品
评论
未登录
看完啦,登录分享一下感受吧~
暂无评论