首页 > 其他分享 >NLP-情感分析 Prompting

NLP-情感分析 Prompting

时间:2024-02-17 18:55:23浏览次数:30  
标签:NLP prompt tokenizer Prompting mask MASK token tokens 情感

**NLP-情感分析 Prompting **

注:本文是 Transformers 快速入门 Prompting 章节的学习笔记,更详细的分析请参见原文。

写在前面

Github 地址:https://github.com/Lockegogo/NLP_Tasks/tree/main/text_cls_prompt_senti

本项目使用 Prompting 方法完成情感分析任务。Prompting 方法的核心思想是借助模板将问题转换为与预训练任务类似的形式来处理。

例如要判断标题 “American Duo Wins Opening Beach Volleyball Match” 的新闻类别,使用 Prompting 的步骤为:

  1. 构建 Prompt 模板:“This is a [MASK] News: x”
  2. 应用模板:“This is a [MASK] News: American Duo Wins Opening Beach Volleyball Match”
  3. 模型预测:从模型的输出序列中抽取出 [MASK] token 对应的表示,然后运用 MLM head 预测 [MASK] token 对应词表中每个 token 的分数(logits),我们只返回类别单词对应位置的分数用于分类。

数据预处理

这里我们选择中文情感分析语料库 ChnSentiCorp 作为数据集,其包含各类网络评论接近一万条,可以从本仓库下载。

语料已经划分好了训练集、验证集、测试集(分别包含 9600、1200、1200 条评论),一行是一个样本,使用 TAB 分隔评论和对应的标签,“0” 表示消极,“1” 表示积极。

最常见的 Prompting 方法就是借助模板将问题转换为 MLM 任务来解决。这里我们定义模板形式为 "总体上来说很 [MASK]。{x}",其中 x 表示评论文本,并且规定如果 [MASK] 被预测为 “好” 就判定情感为 “积极”,如果预测为 “差” 就判定为 “消极”,即 “积极” 和 “消极” 标签对应的 label word 分别为 “好” 和 “差”。

可以看到,MLM 任务与序列标注任务很相似,也是对 token 进行分类,并且类别是整个词表,不同之处在于 MLM 任务只需要对文中特殊的 [MASK] token 进行标注,因此在处理数据时我们需要:

  1. 记录下模板中所有 [MASK] 的位置,以便在模型的输出序列中将它们的表示取出
  2. 记录下 label word 对应的 token ID,因为我们实际上只关心模型在这些词语上的预测结果

首先我们编写模板和 verbalizer 对应的函数:

def get_prompt(x):
    prompt = f'总体上来说很[MASK]。{x}'
    return {
        'prompt': prompt,
        'mask_offset': prompt.find('[MASK]')
    }

def get_verbalizer(tokenizer):
    return {
        'pos': {'token': '好', 'id': tokenizer.convert_tokens_to_ids("好")},
        'neg': {'token': '差', 'id': tokenizer.convert_tokens_to_ids("差")}
    }

例如,第一个样本转换后的模板为:

from transformers import AutoTokenizer

checkpoint = "bert-base-chinese"
tokenizer = AutoTokenizer.from_pretrained(checkpoint)

comment = '这个宾馆比较陈旧了,特价的房间也很一般。总体来说一般。'

print('verbalizer:', get_verbalizer(tokenizer))

prompt_data = get_prompt(comment)
prompt, mask_offset = prompt_data['prompt'], prompt_data['mask_offset']

encoding = tokenizer(prompt, truncation=True)
tokens = encoding.tokens()
# 将 [MASK] 从原来句子中的位置映射到 encoding 之后的位置
mask_idx = encoding.char_to_token(mask_offset)

print('prompt:', prompt)
print('prompt tokens:', tokens)
print('mask idx:', mask_idx)

输出如下:

verbalizer: {'pos': {'token': '好', 'id': 1962}, 'neg': {'token': '差', 'id': 2345}}
prompt: 总体上来说很[MASK]。这个宾馆比较陈旧了,特价的房间也很一般。总体来说一般。
prompt tokens: ['[CLS]', '总', '体', '上', '来', '说', '很', '[MASK]', '。', '这', '个', '宾', '馆', '比', '较', '陈', '旧', '了', ',', '特', '价', '的', '房', '间', '也', '很', '一', '般', '。', '总', '体', '来', '说', '一', '般', '。', '[SEP]']
mask idx: 7

可以看到 BERT 分词器正确地将 “[MASK]” 识别为一个 token,并且记录下 [MASK] token 在序列中的索引。

但是这种做法要求我们能够从词表中找到合适的 label word 来代表每一个类别,并且 label word 只能包含一个 token,而很多时候这是无法实现的。因此,另一种常见做法是为每个类别构建一个可学习的虚拟 token,然后运用类别描述来初始化虚拟 token 的表示,最后使用这些虚拟 token 来扩展模型的 MLM 头。

例如,这里我们可以为 “积极” 和 “消极” 构建专门的虚拟 token “[POS]” 和 “[NEG]”,并且设置对应的类别描述为 “好的、优秀的、正面的评价、积极的态度” 和 “差的、糟糕的、负面的评价、消极的态度”。下面我们扩展一下上面的 verbalizer 函数,添加一个 vtype 参数来区分两种 verbalizer 类型:

def get_verbalizer(tokenizer, vtype):
    assert vtype in ['base', 'virtual']
    return {
        'pos': {'token': '好', 'id': tokenizer.convert_tokens_to_ids("好")},
        'neg': {'token': '差', 'id': tokenizer.convert_tokens_to_ids("差")}
    } if vtype == 'base' else {
        'pos': {
            'token': '[POS]', 'id': tokenizer.convert_tokens_to_ids("[POS]"),
            'description': '好的、优秀的、正面的评价、积极的态度'
        },
        'neg': {
            'token': '[NEG]', 'id': tokenizer.convert_tokens_to_ids("[NEG]"),
            'description': '差的、糟糕的、负面的评价、消极的态度'
        }
    }

vtype = 'virtual'
# add label words
if vtype == 'virtual':
    # 将新添加的 token 添加进模型的词表
    tokenizer.add_special_tokens({'additional_special_tokens': ['[POS]', '[NEG]']})
print('verbalizer:', get_verbalizer(tokenizer, vtype=vtype))

训练模型

对于 MLM 任务,可以直接使用 Transformers 库封装好的 AutoModelForMaskedLM 类。由于 BERT 已经在 MLM 任务上进行了预训练,因此借助模板我们甚至可以在不微调的情况下 (Zero-shot) 直接使用模板来预测情感极性。例如我们的第一个样本:

import torch
from transformers import AutoModelForMaskedLM

checkpoint = "bert-base-chinese"
model = AutoModelForMaskedLM.from_pretrained(checkpoint)

text = "总体上来说很[MASK]。这个宾馆比较陈旧了,特价的房间也很一般。总体来说一般。"
inputs = tokenizer(text, return_tensors="pt")
token_logits = model(**inputs).logits
# Find the location of [MASK] and extract its logits
mask_token_index = torch.where(inputs["input_ids"] == tokenizer.mask_token_id)[1]
mask_token_logits = token_logits[0, mask_token_index, :]
# Pick the [MASK] candidates with the highest logits
top_5_tokens = torch.topk(mask_token_logits, 5, dim=1).indices[0].tolist()

for token in top_5_tokens:
    print(f"'>>> {text.replace(tokenizer.mask_token, tokenizer.decode([token]))}'")
'>>> 总体上来说很好。这个宾馆比较陈旧了,特价的房间也很一般。总体来说一般。'
'>>> 总体上来说很棒。这个宾馆比较陈旧了,特价的房间也很一般。总体来说一般。'
'>>> 总体上来说很差。这个宾馆比较陈旧了,特价的房间也很一般。总体来说一般。'
'>>> 总体上来说很般。这个宾馆比较陈旧了,特价的房间也很一般。总体来说一般。'
'>>> 总体上来说很赞。这个宾馆比较陈旧了,特价的房间也很一般。总体来说一般。'

但是这种方法不够灵活,我们还是采用继承 Transformers 库预训练模型的方式手工构建模型,结构如下:

Using cpu device
initialize embeddings of [POS] and [NEG]
BertForPrompt(
  (bert): BertModel()
  (cls): BertOnlyMLMHead(
    (predictions): BertLMPredictionHead(
      (transform): BertPredictionHeadTransform(
        (dense): Linear(in_features=768, out_features=768, bias=True)
        (transform_act_fn): GELUActivation()
        (LayerNorm): LayerNorm((768,), eps=1e-12, elementwise_affine=True)
      )
      (decoder): Linear(in_features=768, out_features=21128, bias=True)
    )
  )
)

如果采用虚拟 label word,我们除了向模型词表中添加 “[POS]” 和 “[NEG]” token 以外,还按照我们在 verbalizer 中设置的描述来初始化这两个 token 的嵌入。

  1. 用分词器将描述文本转换为对应的 token 列表 $t_1, t_2, ..., t_n $
  2. 然后初始化对应的表示为这些 token 嵌入的平均 $\frac{1}{n} \sum_{i=1}^n \boldsymbol{E}\left(t_i\right) $
if args.vtype == "virtual":
    sp_tokens = ["[POS]", "[NEG]"]
    tokenizer.add_special_tokens({"additional_special_tokens": sp_tokens})
    model.resize_token_embeddings(len(tokenizer))
    verbalizer = get_verbalizer(tokenizer, vtype=args.vtype)

    with torch.no_grad():
        pos_id, neg_id = verbalizer["pos"]["id"], verbalizer["neg"]["id"]
        pos_tokenized = tokenizer(verbalizer["pos"]["description"])
        pos_tokenized_ids = tokenizer.convert_tokens_to_ids(pos_tokenized)
        neg_tokenized = tokenizer(verbalizer["neg"]["description"])
        neg_tokenized_ids = tokenizer.convert_tokens_to_ids(neg_tokenized)
        new_embedding = model.bert.embeddings.word_embeddings.weight[
            pos_tokenized_ids
        ].mean(axis=0)
        model.bert.embeddings.word_embeddings.weight[pos_id, :] = (
            new_embedding.clone().detach().requires_grad_(True)
        )
        new_embedding = model.bert.embeddings.word_embeddings.weight[
            neg_tokenized_ids
        ].mean(axis=0)
        model.bert.embeddings.word_embeddings.weight[neg_id, :] = (
            new_embedding.clone().detach().requires_grad_(True)

注意,向模型词表中添加 token 包含两个步骤:

  1. 通过 tokenizer.add_special_tokens() 向分词器中添加 token,这样分词器就能在分词时将这些词分为独立的 token
  2. 通过 model.resize_token_embeddings() 扩展模型的词表大小

与之前相比,本次我们构建的 BertForPrompt 模型中增加了两个特殊的函数:get_output_embeddings()set_output_embeddings(),负责调整模型的 MLM head。

class BertForPrompt(BertPreTrainedModel):
    def __init__(self, config):
        super().__init__(config)
        self.bert = BertModel(config, add_pooling_layer=False)
        self.cls = BertOnlyMLMHead(config)
        # Initialize weights and apply final processing
        self.post_init()

    def get_output_embeddings(self):
        return self.cls.predictions.decoder

    def set_output_embeddings(self, new_embeddings):
        self.cls.predictions.decoder = new_embeddings

    def forward(self, batch_inputs, batch_mask_idxs, label_word_id, labels=None):
        bert_output = self.bert(**batch_inputs)
        sequence_output = bert_output.last_hidden_state
        batch_mask_reps = batched_index_select(
            sequence_output, 1, batch_mask_idxs.unsqueeze(-1)
        ).squeeze(1)
        pred_scores = self.cls(batch_mask_reps)[:, label_word_id]

        loss = None
        if labels is not None:
            loss_fn = nn.CrossEntropyLoss()
            loss = loss_fn(pred_scores, labels)
        return loss, pred_scores

如果删除这两个函数,那么在调用 model.resize_token_embeddings() 时,就仅仅会调整模型词表的大小,而不会调整 MLM head,即运行上面的代码输出的张量维度依然是 21128。如果你不需要预测新添加 token 在 mask 位置的概率,那么即使删除这两个函数,代码也能正常运行,但是对于本文这种需要预测的情况就不行了。

为了让模型适配我们的任务,这里首先通过 batched_index_select 函数从 BERT 的输出序列中抽取出 [MASK] token 对应的表示,在运用 MLM head 预测出该 [MASK] token 对应词表中每个 token 的分数之后,我们只返回类别对应 label words 的分数用于分类。

模型对每个样本都应该输出 “消极” 和 “积极” 两个类别对应 label word 的预测 logits 值。

需要注意的是,如果采用虚拟 label word,模型是无法直接进行预测的。在扩展了词表之后,MLM head 的参数矩阵尺寸也会进行调整,新加入的参数都是随机初始化的,此时必须进行微调才能让 MLM head 正常工作。

参考资料

  1. Transformers 快速入门
  2. bert4keras 在手,baseline 我有:CLUE 基准代码

标签:NLP,prompt,tokenizer,Prompting,mask,MASK,token,tokens,情感
From: https://www.cnblogs.com/lockegogo/p/18018219

相关文章

  • 中国的AI领域发展的重大不足 —— 数据缺少,尤其是自然语言领域(NLP)、大模型领域
    全世界公开可用的语言文本中绝大部分是英文文本,其中中文文本只有1.5%相关:China'sBettingBigOnArtificialIntelligence.CouldTheUSLoseTheAIRace?|Insight......
  • NLP自然语言处理—主题模型LDA案例:挖掘人民网留言板文本数据|附代码数据
    全文链接:http://tecdat.cn/?p=2155最近我们被客户要求撰写关于NLP自然语言处理的研究报告,包括一些图形和统计输出。随着网民规模的不断扩大,互联网不仅是传统媒体和生活方式的补充,也是民意凸显的地带。领导干部参与网络问政的制度化正在成为一种发展趋势,这种趋势与互联网发展的时......
  • HanLP — 命名实体识别
    目录8.命名实体识别8.1概述8.2基于隐马尔可夫模型序列标注的命名实体识别8.3基于感知机序列标注的命名实体识别8.4基于条件随机场序列标注的命名实体识别8.5命名实体识别标准化评测8.6自定义领域命名实体识别8.命名实体识别8.1概述命名实体文本中有一些描......
  • 笔记_情感
    失恋不受感情支配,也不受过去支配,支配我的只有现在的自己人向前走,苦才会退后和不合适你的过去说再见,太阳的起落在告诉我们,永远会有崭新的一天明智的放弃,好过盲目的执着,生活辽阔,不要只陷入爱恨里,太多的剧本都不如人意转换心态,保持炙热,挖掘和培养自己,去运动,去旅行,给自己制造生活......
  • HanLP — 词性标注
    词性(Part-Of-Speech,POS)指的是单词的语法分类,也称为词类。同一个类别的词语具有相似的语法性质所有词性的集合称为词性标注集。词性的用处当下游应用遇到OOV时,可以通过OOV的词性猜测用法词性也可以直接用于抽取一些信息,比如抽取所有描述特定商品的形容词等词性标注词性标注指......
  • 自然语言处理NLP:情感分析疫情下的新闻数据
    原文链接:http://tecdat.cn/?p=12310原文出处:拓端数据部落公众号 新冠肺炎的爆发让今年的春节与往常不同。与此同时,新闻记录下了这场疫情发展的时间轴。▼为此我们分析了疫情相关的新闻内容、发布时期以及发布内容的主题和情感倾向这些方面的数据,希望通过这些数据,能对这场疫......
  • NLP 之四:双向预训练模型
    利用预训练好的模型进行微调(Fine-tune),可以获得比传统模型的巨大提升。此时学习率一般是正常的十分之一(\(10^{-5}\)左右)。也可以保持预训练的参数不变。Transformer架构Encoder-only:擅长分类任务Decoder-only:擅长生成任务Encoder-only:混合情况(例如文本翻译、总结)在无监督......
  • AI_NLP以及DETR的理解-目标检测模型
    目标检测框架CNNbased以及Transformerbased。01.CNNbased通常又可以划分为以FasterRCNN和RetinaNet为代表 和以YOLO系列为代表阈值筛选(Confidencethreshold)和非极大值抑制(NMS)处理两个关键步骤02.Transformerbased目标检测:DETR......
  • 带你熟悉NLP预训练模型:BERT
    本文分享自华为云社区《【昇思技术公开课笔记-大模型】Bert理论知识》,作者:JeffDing。NLP中的预训练模型语言模型演变经历的几个阶段word2vec/Glove将离散的文本数据转换为固定长度的静态词向量,后根据下游任务训练不同的语言模型ELMo预训练模型将文本数据结合上下文信息,转换......
  • HanLP — 汉字转拼音 -- JAVA
    目录语料库训练加载语料库训练模型保存模型加载模型计算调用HanLP在汉字转拼音时,可以解决多音字问题,显示输出声调,声母、韵母,通过训练语料库,本文代码为《自然语言处理入门》配套版本HanLP-1.7.5对重载不是重任进行转拼音,效果如下:原文:重载不是重任拼音(数字音调):chong2,zai3,bu......