BertForSequenceClassification 如何在 CLS 向量上进行分类?

Kev*_*vin 5 python transformer-model bert-language-model huggingface-transformers

背景:

遵循这个问题,当使用 bert 对序列进行分类时,模型使用表示分类任务的“[CLS]”标记。据该论文称:

每个序列的第一个标记始终是一个特殊的分类标记([CLS])。与该标记对应的最终隐藏状态用作分类任务的聚合序列表示。

查看 Huggingfaces 存储库,他们的 BertForSequenceClassification 使用 bert pooler 方法:

class BertPooler(nn.Module):
    def __init__(self, config):
        super().__init__()
        self.dense = nn.Linear(config.hidden_size, config.hidden_size)
        self.activation = nn.Tanh()

    def forward(self, hidden_states):
        # We "pool" the model by simply taking the hidden state corresponding
        # to the first token.
        first_token_tensor = hidden_states[:, 0]
        pooled_output = self.dense(first_token_tensor)
        pooled_output = self.activation(pooled_output)
        return pooled_output
Run Code Online (Sandbox Code Playgroud)

我们可以看到他们采用第一个标记(CLS)并将其用作整个句子的表示。具体来说,他们执行的操作hidden_states[:, 0]看起来很像从每个状态中获取第一个元素,而不是获取第一个标记隐藏状态?

我的问题:

我不明白的是他们如何将整个句子的信息编码到这个标记中?CLS 标记是一个常规标记,它有自己的嵌入向量来“学习”句子级别表示吗?为什么我们不能只使用隐藏状态的平均值(编码器的输出)并用它来分类?

编辑:经过一番思考:因为我们使用 CLS 令牌隐藏状态来预测,所以 CLS 令牌嵌入是否正在接受分类任务的训练,因为这是用于分类的令牌(因此是导致错误的主要因素)哪个会传播到它的权重?)

cro*_*oik 6

CLS 标记是一个常规标记,它有自己的嵌入向量来“学习”句子级别表示吗?

是的:

from transformers import BertTokenizer, BertModel

tokenizer = BertTokenizer.from_pretrained('bert-base-uncased')
model = BertModel.from_pretrained('bert-base-uncased')

clsToken = tokenizer.convert_tokens_to_ids('[CLS]') 
print(clsToken)
#or
print(tokenizer.cls_token, tokenizer.cls_token_id)

print(model.get_input_embeddings()(torch.tensor(clsToken)))
Run Code Online (Sandbox Code Playgroud)

输出:

101
[CLS] 101
tensor([ 1.3630e-02, -2.6490e-02, -2.3503e-02, -7.7876e-03,  8.5892e-03,
        -7.6645e-03, -9.8808e-03,  6.0184e-03,  4.6921e-03, -3.0984e-02,
         1.8883e-02, -6.0093e-03, -1.6652e-02,  1.1684e-02, -3.6245e-02,
         ...
         5.4162e-03, -3.0037e-02,  8.6773e-03, -1.7942e-03,  6.6826e-03,
        -1.1929e-02, -1.4076e-02,  1.6709e-02,  1.6860e-03, -3.3842e-03,
         8.6805e-03,  7.1340e-03,  1.5147e-02], grad_fn=<EmbeddingBackward>)
Run Code Online (Sandbox Code Playgroud)

您可以通过以下方式获取您的模型的所有其他特殊令牌的列表:

101
[CLS] 101
tensor([ 1.3630e-02, -2.6490e-02, -2.3503e-02, -7.7876e-03,  8.5892e-03,
        -7.6645e-03, -9.8808e-03,  6.0184e-03,  4.6921e-03, -3.0984e-02,
         1.8883e-02, -6.0093e-03, -1.6652e-02,  1.1684e-02, -3.6245e-02,
         ...
         5.4162e-03, -3.0037e-02,  8.6773e-03, -1.7942e-03,  6.6826e-03,
        -1.1929e-02, -1.4076e-02,  1.6709e-02,  1.6860e-03, -3.3842e-03,
         8.6805e-03,  7.1340e-03,  1.5147e-02], grad_fn=<EmbeddingBackward>)
Run Code Online (Sandbox Code Playgroud)

输出:

['[CLS]', '[UNK]', '[PAD]', '[SEP]', '[MASK]']
Run Code Online (Sandbox Code Playgroud)

我不明白的是他们如何将整个句子的信息编码到这个标记中?

因为我们使用 CLS 令牌隐藏状态来预测,所以 CLS 令牌嵌入是否正在接受分类任务的训练,因为这是用于分类的令牌(因此是传播到其权重的误差的主要贡献者?)

也是的。正如您在问题中已经指出的那样,BertForSequenceClassification利用BertPooler来训练 Bert 之上的线性层:

print(tokenizer.all_special_tokens)
Run Code Online (Sandbox Code Playgroud)

为什么我们不能只使用隐藏状态的平均值(编码器的输出)并用它来分类?

我一般无法真正回答这个问题,但为什么你认为作为线性层会更容易或更好?您还需要训练隐藏层以生成平均值映射到您的类别的输出。因此,你还需要一个“平均层”作为你损失的主要贡献者。一般来说,当你可以证明它比当前的方法能带来更好的结果时,没有人会拒绝它。

  • 谢谢,这是完全有道理的!我不认为它比使用线性层更容易或更好,我只是好奇我们可以表示句子宽(或在我的情况下是图像宽)表示的其他方式 (2认同)