BERT模型的输出格式探究以及提取出BERT 模型的CLS表示,last_hidden_state[:, 0, :]用于提取每个句子的CLS向量表示

说在前面

最近使用自己的数据集对bert-base-uncased进行了二次预训练,只使用了MLM任务,发现在加载训练好的模型进行输出CLS表示用于下游任务时,同一个句子的输出CLS表示都不一样,并且控制台输出以下警告信息。说是没有这些权重。

Some weights of BertModel were not initialized from the model checkpoint at ./model/test-model and are newly initialized: ['bert.pooler.dense.bias', 'bert.pooler.dense.weight']
You should probably TRAIN this model on a down-stream task to be able to use it for predictions and inference.

BertModel 的某些权重在 ./model/test-model 的模型检查点时未被初始化,现在是新初始化的: ['bert.pooler.dense.bias','bert.pooler.dense.weight']。
您可能应该在下流任务中训练该模型,以便将其用于预测和推理。

问题原因

我刚开始获取CLS token表示使用的是以下方式,池化后的CLS token表示。

# 获取池化后的[CLS] token 表示
pooler_output = outputs.pooler_output 

但是我使用的模型是以下加载方式,它的输出并不包含 pooler_output,因为这个模型是为 掩码语言模型(Masked Language Model)任务设计的,而不是用于分类任务。因此,模型的输出包括 last_hidden_state,而不包括 pooler_output

model = AutoModelForMaskedLM.from_pretrained(BERT_PATH).to('cuda')
# 或者
model = BertForMaskedLM.from_pretrained(BERT_PATH).to('cuda')

所以我加载使用训练后的模型总是对同一个句子的CLS输出总是变化,就是没有pooler_output层的权重参数,它加载模型推理时自己随机初始化了pooler_output层权重参数。

解决方法

一共有两个解决方法。

1. 直接使用last_hidden_state[:,0,:]获取每个句子的cls token的表示。缺点:cls 表示在句子级表示方面差于pooler_output表示。

2. 修改训练的模型架构,添加池化层。pooler_output表示的优点是它对 [CLS] token 的表示进行了池化处理,它通常是更好的句子级别表示。

1. BERT模型的输出格式探究

last_hidden_state

last_hidden_state:这是一个张量,形状为 [batch_size, sequence_length, hidden_size]

  • batch_size:一次传入模型的样本数。
  • sequence_length:输入序列的长度(即输入文本中的 token 数量)。
  • hidden_size:每个 token 的隐藏状态向量的维度,通常是 768(BERT-base)或 1024(BERT-large)。

形象生动图形

真实示例

以下是1个batch_size的真实数据,每一行就是一个token,共有512行。每行里面的数值共有768个。该last_hidden_state 的形状是 [1, 512, 768]

last_hidden_state:  

tensor([[[-0.1043,  0.0966, -0.2970,  ..., -0.3728,  0.2120,  0.5492],
         [ 0.0131, -0.0778,  0.0908,  ..., -0.1869,  1.0111,  0.1027],
         [-0.8840,  0.3916,  0.3881,  ..., -0.5864,  0.3374,  0.1069],
         ...,
         [-0.2845, -0.8075,  0.6715,  ..., -0.5281,  0.5046, -0.6814],
         [-0.4623, -0.6836, -0.8556,  ...,  0.1499,  0.1142,  0.0486],
         [ 0.5701, -0.1264, -0.2348,  ...,  0.2635, -0.4314, -0.1724]]])

2. 获取每个句子的CLS token向量表示

last_hidden_state[:, 0, :]的含义=CLS向量表示

  • 表示我们选择了所有的批次样本。batch_size 为 1 时,选择所有样本,即 [1, 512, 768] 中的所有样本。
  • 0:表示我们选择了每个句子序列中的第一个 token(索引 0),在 BERT 中,输入序列的第一个 token 通常是 [CLS] token。因此,0 索引指向 [CLS] token 对应的隐藏状态。
  • 表示我们选择了所有的隐藏维度(即每个 token 的隐藏状态),也就是每个 token 的向量表示,通常为 768 维(对于 BERT-base)。

因此该操作的含义是:每个句子有512个token,提取每个句子里面的第1个token向量,人话说就是每个句子的的第1个token向量就是每个句子的CLS向量表示。

结果:这将会返回一个形状为 [1, 768] 的张量,它包含了 [CLS] token 的表示。由于 batch_size=1,最终的张量只有一个样本。

为什么last_hidden_state[:, 0, :]提取的就是每个句子的CLS token表示呢?

在 BERT 模型中,[CLS] token 是一个特殊的 token,通常用于表示整个句子的嵌入(embedding),特别是在分类任务中,[CLS] token 的输出被用作整个输入句子的向量表示。

  • 分类任务 中(如情感分析、文本分类等),通常使用 [CLS] token 的表示 作为整个句子的特征向量输入到分类器中。
  • 其他任务 中(如命名实体识别、问答系统等),[CLS] token 的表示也常常被用作输入的高层特征。

3. last_hidden_state 和 pooler_output的含义区别

outputs.last_hidden_stateoutputs.pooler_output 是 BERT 模型的两个重要输出,二者之间有明显的区别。它们分别代表了不同层级的模型输出,具体如下:

outputs.last_hidden_state

  • 定义last_hidden_state 是 BERT 模型中每一层的输出,包含了模型对于输入文本中每个 token 的隐藏表示。

  • 形状last_hidden_state 的形状通常是 [batch_size, sequence_length, hidden_size],即:

    • batch_size:批次中样本的数量。
    • sequence_length:输入序列(即文本)的长度,通常是 token 的个数(包括 [CLS][SEP] token)。
    • hidden_size:每个 token 的隐藏状态向量的维度(通常是 768,对于 bert-base-uncased)。
  • 用途last_hidden_state 是 BERT 对每个 token 的表示,包含了输入文本中每个 token 在其上下文中被表示出来的隐藏状态。它包含了完整的上下文信息。

    例如,对于输入文本 "Hello, how are you?",last_hidden_state 包含了 "Hello"",""how" 等每个 token 的上下文嵌入(表示)。你可以根据这个输出提取每个 token 的表示或使用 [CLS] token 的表示(last_hidden_state[:, 0, :])作为整个句子的表示。

outputs.pooler_output

  • 定义pooler_output 是一个经过额外处理的 [CLS] token 的表示。BERT 的 pooler 是一个简单的全连接层,它接收 last_hidden_state[CLS] token 的输出,然后对其进行处理(通常是通过一个 tanh 激活函数)以得到一个句子级别的特征表示。
  • 形状pooler_output 的形状通常是 [batch_size, hidden_size],即:
    • batch_size:批次中的样本数。
    • hidden_size:每个样本的 pooler_output 的维度(通常是 768)。
  • 用途pooler_output[CLS] token 的经过进一步处理后的表示,通常用于分类任务中。pooler_output 是通过对 last_hidden_state[CLS] token 的输出应用池化操作(通常是 tanh 激活函数)得到的最终句子级别的表示。这个表示通常用于下游任务,如分类任务。

两者的区别

  • last_hidden_state

    • 是 BERT 的每个 token 的 上下文表示。它是来自模型的 所有 token 的输出,形状为 [batch_size, sequence_length, hidden_size]
    • 它包含了对输入文本中每个 token 的隐藏状态表示,可以通过它提取每个 token(包括 [CLS])的表示。
  • pooler_output

    • 仅包含 [CLS] token 的表示,并且是经过池化处理(通过 tanh)后的结果,形状为 [batch_size, hidden_size]
    • 它通常用于 句子级别的表示,尤其在分类任务中,pooler_output 是更常见的输入特征。

何时使用哪一个?

  • last_hidden_state:如果你需要每个 token 的表示(如进行命名实体识别、文本生成等任务),你应该使用 last_hidden_state
    • 例如:对于文本分类任务中的 BERT 模型,你通常会使用 [CLS] token 的 last_hidden_state 来提取句子的表示,last_hidden_state[:, 0, :]
  • pooler_output:如果你只是进行 句子级别的分类任务(如情感分析、文本分类等),通常会直接使用 pooler_output,因为它已经对 [CLS] token 的表示进行了处理,通常是更好的句子级别表示。
    • 例如:对于情感分析,你会使用 pooler_output 作为整个句子的向量表示进行分类。

代码示例

假设你正在使用 BERT 模型进行文本分类,你可以使用以下代码来区分这两个输出:

import torch
from transformers import BertModel, BertTokenizer

# 加载模型和分词器
model = BertModel.from_pretrained("bert-base-uncased")
tokenizer = BertTokenizer.from_pretrained("bert-base-uncased")

# 输入文本
text = "Hello, how are you?"

# 编码文本
inputs = tokenizer(text, return_tensors="pt", truncation=True, padding=True)

# 获取模型的输出
with torch.no_grad():
    outputs = model(**inputs)

# 获取每个 token 的表示
last_hidden_state = outputs.last_hidden_state  # [batch_size, sequence_length, hidden_size]

# 获取[CLS] token 的表示(从 last_hidden_state 中提取)
cls_last_hidden = last_hidden_state[:, 0, :]  # [batch_size, hidden_size]

# 获取池化后的[CLS] token 表示
pooler_output = outputs.pooler_output  # [batch_size, hidden_size]

print("CLS token's last hidden state:", cls_last_hidden)
print("CLS token's pooler output:", pooler_output)

总结

  • last_hidden_state:包含了每个 token 的 上下文表示,你可以用它来获取每个 token 的隐藏状态(包括 [CLS])。
  • pooler_output:仅包含 [CLS] token 的表示,并且经过了池化处理,通常用于句子级别的任务,如文本分类。

4. 疑惑:pooler_output表示问题

我对bert进行了二次预训练后保存的模型输出发现并没有pooler_output权重参数(也就是文章开始处给出的警告信息),但是我又想要使用训练后的模型进行情感分析,我是直接使用lasthiddenstate的cls token表示呢,还是对其加一个池化处理呢?

cls_output = outputs.last_hidden_state[:, 0, :]  
print(cls_output)

pooler_output = outputs.pooler_output
print("pooler_output: ", pooler_output)

没有pooler_output权重参数的原因分析

因为我使用的是 BertForMaskedLM 模型,它的输出并不包含 pooler_output,因为这个模型是为 掩码语言模型(Masked Language Model)任务设计的,而不是用于分类任务。因此,模型的输出包括 last_hidden_state,而不包括 pooler_output

解决方案

1. 直接使用 last_hidden_state[:, 0, :]([CLS] token 的表示)

cls_representation = outputs.last_hidden_state[:, 0, :]

2. [CLS] token 的表示进行池化或全连接层处理

我觉得仅使用 CLS token 的输出还不够,可以加一个简单的全连接层(例如,用 tanh 激活函数)来进一步池化或优化 CLS token 的表示。

import torch.nn as nn

class SentimentAnalysisModel(nn.Module):
    def __init__(self, model):
        super(SentimentAnalysisModel, self).__init__()
        self.bert = model
        self.fc = nn.Linear(768, 2)  # 假设是二分类任务(情感分析)

    def forward(self, input_ids, attention_mask):
        outputs = self.bert(input_ids=input_ids, attention_mask=attention_mask)
        cls_rep = outputs.last_hidden_state[:, 0, :]  # 获取 [CLS] token 的表示
        logits = self.fc(cls_rep)  # 使用全连接层进行分类
        return logits

# 使用训练后的模型来构建情感分析模型
sentiment_model = SentimentAnalysisModel(model)

在这个例子中,cls_rep[CLS] token 的表示,之后我们将其传入一个全连接层进行二分类(情感分析)。当然,如果你有更多类别,你可以根据任务需要修改输出层。

总结

  • 没有 pooler_output:由于我使用的是 BertForMaskedLM,该模型的输出没有 pooler_output,但你可以使用 last_hidden_state[:, 0, :](即 [CLS] token 的表示)作为句子级别的特征来进行情感分析。
  • 是否加池化:我觉得仅使用 [CLS] token 的表示不够好,可以加一个池化层或全连接层来进一步处理该表示,通常这种方式会提高分类性能。

因此,可以继续使用 last_hidden_state 中的 [CLS] token 作为情感分析的特征,如果需要提高模型效果,可以尝试在其基础上加一个池化层或全连接层。

5. 修改模型加载器方法探究

有没有其他解决方法呢,让我二次预训练后的模型有pooler_output。可以修改模型加载器吗,但是我使用的是MLM任务来二次预训练bert模型。我的训练任务是对bert模型进行二次预训练。

为了在训练后的 BERT 模型中有 pooler_output,需要稍作修改,因为默认的 BertForMaskedLM 模型并不包含 pooler_output(该输出通常用于分类任务的 BERT 模型,例如 BertForSequenceClassification)。因此,可以通过修改模型的架构来实现这一目标。

解决方案:修改模型架构

在加载模型时,选择使用 BertModel 或继承 BertForMaskedLM 的新模型,并手动添加一个池化层(如全连接层)。这种方法允许继续使用 MLM 任务,同时为分类任务提供所需的 pooler_output

修改代码的步骤

第一步:继承 BertForMaskedLM 并添加 pooler_output

  • 修改 BertForMaskedLM 模型,使其输出 pooler_output
  • 可以通过添加一个全连接层来模拟池化过程。
class BertWithPoolerOutput(BertForMaskedLM):
    def __init__(self, config):
        super().__init__(config)
        # 定义pooler层
        self.pooler = torch.nn.Linear(config.hidden_size, config.hidden_size)
        self.tanh = torch.nn.Tanh()
        # 使用BertModel来获得last_hidden_state
        self.bert = BertModel(config)

    def forward(self, input_ids=None, attention_mask=None, token_type_ids=None, labels=None):
        # 使用BertModel来获取last_hidden_state
        bert_outputs = self.bert(input_ids=input_ids, attention_mask=attention_mask,
                                 token_type_ids=token_type_ids)
        last_hidden_state = bert_outputs.last_hidden_state  # 获取last_hidden_state

        # 提取[CLS]的输出
        cls_token_representation = last_hidden_state[:, 0, :]  # 获取[CLS] token的表示
        # 通过池化层(全连接层)进行处理
        pooler_output = self.tanh(self.pooler(cls_token_representation))

        # 获取BERT的MaskedLM输出
        lm_outputs = super().forward(input_ids=input_ids, attention_mask=attention_mask,
                                     token_type_ids=token_type_ids, labels=labels)

        # 返回字典,包含loss, logits, pooler_output
        return {
            'loss': lm_outputs.loss,
            'logits': lm_outputs.logits,
            'pooler_output': pooler_output
        }

在这个新类 BertWithPoolerOutput 中,我们继承了 BertForMaskedLM,并增加了一个池化层 (self.pooler) 和激活函数(tanh),用于生成 pooler_output。此修改确保在进行二次预训练时仍然可以使用池化后的表示。为了正确获取 last_hidden_state,我们应该调用 BertModel 来获取 last_hidden_state,而不是直接从 BertForMaskedLM 获取。

第2步:加载并训练这个模型

通过这种方式,加载并训练模型时,模型将返回 pooler_output,就可以使用它进行情感分析或其他分类任务。

from transformers import BertTokenizer, Trainer, TrainingArguments
from datasets import load_dataset
from transformers import DataCollatorForLanguageModeling

# 训练和数据集代码保持不变,只是模型加载部分更换为我们自定义的模型
tokenizer = BertTokenizer.from_pretrained(BERT_PATH)
model = BertWithPoolerOutput.from_pretrained(BERT_PATH)  # 使用我们自定义的模型

# 其余的训练部分和数据处理代码不变

第三步:在训练过程中使用 pooler_output

训练结束后,模型将返回 pooler_output,你可以直接使用它进行分类任务。

# 假设输出为 `outputs`,你可以访问 `pooler_output`
pooler_output = outputs.pooler_output

总结

  • 可以通过 自定义模型 来为 BertForMaskedLM 添加 pooler_output,方法是在原有的 BertForMaskedLM 上增加一个池化层。
  • 这样,模型仍然可以用于 MLM 任务,同时也能输出 pooler_output,而它通常是更好的句子级别表示,便于后续情感分析等任务。
  • 训练完成后,就能使用 pooler_output,它是通过池化 [CLS] token 的表示得到的句子级别的特征。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/928953.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Linux操作系统】多线程控制(创建,等待,终止、分离)

目录 一、线程与轻量级进程的关系二、进程创建1.线程创建线程创建函数(pthread)查看和理解线程id主线程与其他线程之间的关系 三、线程等待(回收)四、线程退出线程退出情况线程退出方法 五、线程分离线程的优点线程的缺点 一、线程…

Android ConstraintLayout 约束布局的使用手册

目录 前言 一、ConstraintLayout基本介绍 二、ConstraintLayout使用步骤 1、引入库 2、基本使用,实现按钮居中。相对于父布局的约束。 3、A Button 居中展示,B Button展示在A Button正下方(距离A 46dp)。相对于兄弟控件的约束…

【论文复现】隐式神经网络实现低光照图像增强

📝个人主页🌹:Eternity._ 🌹🌹期待您的关注 🌹🌹 ❀ 隐式神经网络实现低光照图像增强 引言那么目前低光照图像增强还面临哪些挑战呢? 挑战1. 不可预测的亮度降低和噪声挑战2.度量友好…

【机器学习】机器学习的基本分类-监督学习-决策树-C4.5 算法

C4.5 是由 Ross Quinlan 提出的决策树算法,是对 ID3 算法的改进版本。它在 ID3 的基础上,解决了以下问题: 处理连续型数据:支持连续型特征,能够通过划分点将连续特征离散化。处理缺失值:能够在特征值缺失的…

Spring和SpringBoot的关系和区别?

大家好,我是锋哥。今天分享关于【Spring和SpringBoot的关系和区别?】面试题。希望对大家有帮助; Spring和SpringBoot的关系和区别? 1000道 互联网大厂Java工程师 精选面试题-Java资源分享网 Spring和Spring Boot是两种相关但有所…

Scrapy 中的配置笔记

概述 scrapy在命令启动之前,先设置好了各种配置文件。其中包括系统自带的默认配置文件,还有用户自定义的settings.py。其中还有一个日常开发中不怎么用的scrapy.cfg文件,这个文件是用来告诉scrapy用户自定义的settings.py文件在哪里的 关键…

代码随想录算法训练营day49|动态规划part11

最长公共子序列 这个与上篇笔记最大的不同就是子序列里的数可以不相邻,那么只需加入一个dp[i][j]的上和左的更新方向即可 class Solution { public:int longestCommonSubsequence(string text1, string text2) {vector<vector<int>> dp(text1.size()1,vector<…

Python知识分享第十九天-网络编程

网络编程 概述用来实现 网络互联 不同计算机上运行的程序间可以进行数据交互也叫Socket编程 套接字编程 三要素IP地址概述设备在网络中的唯一标识分类IPV4城域网13广域网22局域网31IPV6八字节 十六进制相关dos命令查看ipwindows: ipconfigmac和linux: ifconfig测试网络ping 域…

CAN接口设计

CAN总线的拓扑结构 CAN总线的拓扑结构有点像485总线,都是差分的传输方式,总线上都可以支持多个设备,端接匹配电阻都是120Ω。 485和CAN通信方面最大的区别:网络特性。485是一主多从的通讯方式,CAN是多主通讯,多个设备都可以做主机。那多个设备都相要控制总线呢?…

Latex转word(docx)或者说PDF转word 一个相对靠谱的方式

0. 前言 投文章过程中总会有各种各样的要求&#xff0c;其中提供word格式的手稿往往是令我头疼的一件事。尤其在多公式的文章中&#xff0c;其中公式转换是一个头疼的地方&#xff0c;还有很多图表&#xff0c;格式等等&#xff0c;想想就让人头疼欲裂。实践中摸索出一条相对靠…

数据结构——单调队列

这篇博客我们来讨论一下单调队列的问题&#xff0c;其实和之前学的单调栈都是一种上通过改变操作来解决问题的一种数据结构 我们先来回忆一下单调栈的内容&#xff0c;这样方便将其和单调队列做区分 单调栈&#xff1a;(单调性从栈底到栈顶&#xff09; 1.单调栈是一种栈数据…

解决 Maven 部署中的 Artifact 覆盖问题:实战经验分享20241204

&#x1f6e0;️ 解决 Maven 部署中的 Artifact 覆盖问题&#xff1a;实战经验分享 &#x1f4cc; 引言 在软件开发过程中&#xff0c;持续集成和持续部署&#xff08;CI/CD&#xff09;是提高开发效率和代码质量的关键手段。Hudson 和 Maven 是两种广泛使用的工具&#xff0…

[go-redis]客户端的创建与配置说明

创建redis client 使用go-redis库进行创建redis客户端比较简单&#xff0c;只需要调用redis.NewClient接口创建一个客户端 redis.NewClient(&redis.Options{Addr: "127.0.0.1:6379",Password: "",DB: 0, })NewClient接口只接收一个参数red…

Solving the Makefile Missing Separator Stop Error in VSCode

1. 打开 Makefile 并转换缩进 步骤 1: 在 VSCode 中打开 Makefile 打开 VSCode。使用文件浏览器或 Ctrl O&#xff08;在 Mac 上是 Cmd O&#xff09;打开你的 Makefile。 步骤 2: 打开命令面板 按 Ctrl Shift P&#xff08;在 Mac 上是 Cmd Shift P&#xff09;&…

交换机四大镜像(端口镜像、流镜像、VLAN镜像、MAC镜像)应用场景、配置实例及区别对比

在网络管理中&#xff0c;端口镜像、流镜像、VLAN镜像和MAC镜像都是用于监控和分析网络流量的重要技术。 端口镜像&#xff08;Port Mirroring&#xff09; 定义&#xff1a;端口镜像是将一个或多个源端口的流量复制到一个目标端口&#xff0c;以便于网络管理员能够监控和分析…

Unity数据持久化

二进制数据持久化的好处&#xff1a;安全、效率高、利于网络通信 文章目录 补充文件夹相关EditorResourcesSteammingAsset 序列化和反序列化序列化反序列化 二进制数据持久化转换为字节数据文件操作写入字节&#xff1a;读取字节安全关闭文件夹操作操作文件夹目录信息和文件信息…

【机器学习】机器学习的基本分类-监督学习-随机森林(Random Forest)

随机森林是一种基于集成学习&#xff08;Ensemble Learning&#xff09;思想的算法&#xff0c;由多个决策树构成。它通过结合多棵决策树的预测结果来提升模型的泛化能力和准确性&#xff0c;同时减少过拟合的风险。 1. 随机森林的核心思想 多样性&#xff1a; 随机森林通过引…

中国矿业大学《2024年868自动控制原理真题》 (完整版)

本文内容&#xff0c;全部选自自动化考研联盟的&#xff1a;《中国矿业大学868自控考研资料》的真题篇。后续会持续更新更多学校&#xff0c;更多年份的真题&#xff0c;记得关注哦~ 目录 2024年真题 Part1&#xff1a;2024年完整版真题 2024年真题

SQL SERVER 2016 AlwaysOn 无域集群+负载均衡搭建与简测

之前和很多群友聊天发现对2016的无域和负载均衡满心期待&#xff0c;毕竟可以简单搭建而且可以不适用第三方负载均衡器&#xff0c;SQL自己可以负载了。windows2016已经可以下载使用了&#xff0c;那么这回终于可以揭开令人憧憬向往的AlwaysOn2016 负载均衡集群的神秘面纱了。 …

生产看板到底在看什么?

说起生产看板&#xff0c;可能很多人脑海里冒出来的画面是&#xff1a;车间里一块挂在墙上的大板子&#xff0c;上面贴满了各式各样的卡片、表格&#xff0c;甚至还有几个闪闪发光的指示灯。但是&#xff0c;无论是精益生产方式代表——丰田&#xff0c;还是当下以“智能制造”…