浅谈sigmoid函数和softmax函数

11 篇文章 3 订阅
11 篇文章 4 订阅

问题提出

最近在找文本分类问题的trick,偶尔看到夕小瑶大佬的一个回答:

在文本分类任务中,有哪些论文中很少提及却对性能有重要影响的tricks? - 夕小瑶的回答 - 知乎
https://www.zhihu.com/question/265357659/answer/578944550

里面有句话引起了我的注意:

关于二分类

二分类问题一定要用sigmoid作为输出层的激活函数?当然不是,尝试一下包含俩类别的softmax吧。可能多一条分支就多一点信息叭,虽然后者在数学形式上更丑一点,但是实践中常常带来零点几个点的提升也是比较玄学了。

我第一反应就是——二分类的softmax和sigmoid有什么区别?不是说在二分类的情况下,softmax函数就退化成sigmoid了么!

于是我找到了在刚开始学习机器学习的时候,看过的一篇博客。正是这篇博客的内容,让我知道了二分类情况下softmax与sigmoid的关系:Sigmoid 函数和 Softmax 函数的区别和关系
这篇博客,通过简单的数学推导,最终得出,在类别数为2的情况下,sigmoid函数和softmax函数的公式是可以互相转化的,从而得出该文章最后一句——“可见在二元分类的情况下,Softmax 退化为了 Sigmoid。”

那么问题来了。两个公式形式一样,真的意味着二者所做的事情就完全相同吗?我把我浅显的感悟写在这里,也可能有误,但我觉得有点道理——


问题思考

说到softmax和sigmoid二者差别,就得说说二者分别都是什么。其实很简单,网上有数以千计的优质博文去给你讲明白,我只想用我的理解来简单阐述一下:

  • sigmoid函数针对两点分布提出。神经网络的输出经过它的转换,可以将数值压缩到(0,1)之间,得到的结果可以理解成“分类成目标类别的概率P”。而不分类到该类别的概率,就是(1 - P),这也是典型的两点分布的形式;
  • softmax本身针对多项分布提出,当类别数是2时,它退化为二项分布,而它和sigmoid真正的区别就在这儿——二项分布包含两个分类类别(姑且分别称为A和B);而两点分布其实是针对一个类别的概率分布,其对应的那个类别的分布,直接由1-P粗暴得出。

据上所述,sigmoid函数,我们可以当作成它是对一个类别的“建模”。将该类别建模完成,另一个相对的类别就直接通过1减去得到;

而softmax函数,是对两个类别建模。同样的,得到两个类别的概率之和也是1.


问题解答

所以我到底要说个什么问题呢。。。(好像扯远了)我要说的是,神经网络在做二分类时,使用softmax还是sigmoid,做法其实有明显差别。由于softmax是对两个类别(正反两类,通常定义为0/1的label)建模,所以对于NLP模型而言(比如泛BERT模型),Bert输出层需要通过一个nn.Linear()全连接层压缩至2维,然后接softmax(pytorch的做法,就是直接接上torch.nn.CrossEntropyLoss);而sigmoid只对一个类别建模(通常就是正确的那个类别),所以Bert输出层需要通过一个nn.Linear()全连接层压缩至1维,然后接sigmoid(torch就是接torch.nn.BCEWithLogitsLoss

我们来看俩例子:

先看softmax的:
class AlbertForSequenceClassification(AlbertPreTrainedModel):
    def __init__(self, config):
        super(AlbertForSequenceClassification, self).__init__(config)
        self.num_labels = config.num_labels
        self.bert = AlbertModel(config)
        self.dropout = nn.Dropout(0.1 if config.hidden_dropout_prob == 0 else config.hidden_dropout_prob)
        self.classifier = nn.Linear(config.hidden_size, self.config.num_labels)
        self.use_weight = True if "use_weight" in config.__dict__.keys() else False
        self.init_weights()

    def forward(self, input_ids, attention_mask=None, token_type_ids=None,
                position_ids=None, head_mask=None, labels=None):

        outputs = self.bert(input_ids,
                            attention_mask=attention_mask,
                            token_type_ids=token_type_ids,
                            position_ids=position_ids,
                            head_mask=head_mask)

        pooled_output = outputs[1]
        pooled_output = self.dropout(pooled_output + 0.1)  # 这里为啥+0.1?
        logits = self.classifier(pooled_output)  # classifier其实就是个将312维压缩至2维的线性层
        outputs = (logits,) + outputs[2:]  # add hidden states and attention if they are here

        if labels is not None:
            if self.num_labels == 1:
                #  We are doing regression
                loss_fct = MSELoss()
                loss = loss_fct(logits.view(-1), labels.view(-1))
            else:
                class_weight = torch.FloatTensor([0.18, 0.82])
                class_weight = class_weight.to('cuda')
                if self.use_weight:  # 这里是我的个人改动,在ce上加了weight,无需关注。。。
                    ce = CrossEntropyLoss(class_weight)  # 交叉熵损失在这里!要改损失函数在这儿改!
                    loss = ce(logits.view(-1, self.num_labels), labels.view(-1))
                else:  # 对于一般分类任务而言,计算Loss在这里
                    loss_fct = CrossEntropyLoss()
                    loss = loss_fct(logits.view(-1, self.num_labels), labels.view(-1))
            outputs = (loss,) + outputs
        return outputs  # (loss), logits, (hidden_states), (attentions)

熟悉的同学一眼便知,这个例子来自老版huggingface/transformer库的modeling_albert.py文件,没记错的话,当时该库叫pytorch_pretrained_bert,现如今以及被改的面目全非了,各种封装让人欲仙欲死,但好在有其他大佬保留了原本简单易懂的代码,链接放在这儿
见code line7,此处定义self.classifier = nn.Linear(config.hidden_size, self.config.num_labels),其中self.config.num_labels=2。而后面计算Loss使用了包装了logSoftmaxNLLLossCrossEntropyLoss,即使用softmax时,最后的线性层将维度压缩至2维。

再看看sigmoid
  def _get_discriminator_output(self, inputs, discriminator, labels):
    """Discriminator binary classifier."""
    with tf.variable_scope("discriminator_predictions"):
      hidden = tf.layers.dense(
          discriminator.get_sequence_output(),
          units=self._bert_config.hidden_size,
          activation=modeling.get_activation(self._bert_config.hidden_act),
          kernel_initializer=modeling.create_initializer(
              self._bert_config.initializer_range))
      logits = tf.squeeze(tf.layers.dense(hidden, units=1), -1)   # dense-layer输出维度是1
      weights = tf.cast(inputs.input_mask, tf.float32)
      labelsf = tf.cast(labels, tf.float32)
      losses = tf.nn.sigmoid_cross_entropy_with_logits(    # 这里采用了sigmoid以及它对应的Loss函数
          logits=logits, labels=labelsf) * weights
      per_example_loss = (tf.reduce_sum(losses, axis=-1) /
                          (1e-6 + tf.reduce_sum(weights, axis=-1)))
      loss = tf.reduce_sum(losses) / (1e-6 + tf.reduce_sum(weights))
      probs = tf.nn.sigmoid(logits)
      preds = tf.cast(tf.round((tf.sign(logits) + 1) / 2), tf.int32)
      DiscOutput = collections.namedtuple(
          "DiscOutput", ["loss", "per_example_loss", "probs", "preds",
                         "labels"])
      return DiscOutput(
          loss=loss, per_example_loss=per_example_loss, probs=probs,
          preds=preds, labels=labels,)

该例子来自于google/electra,源码链接在此,对模型感兴趣的同学可见:ELECTRA论文阅读笔记
从该代码的line 10可见,ELECTRA判别器(可看作一个专做二分类任务的BERT model)的输出被tf.layers.dense压缩成1维;然后再在line 13,模型使用了tf.nn.sigmoid_cross_entropy_with_logits


总结

总而言之,sotfmax和sigmoid确实在二分类的情况下可以化为相同的数学表达形式,但并不意味着二者有一样的含义,而且二者的输入输出都是不同的。sigmoid得到的结果是“分到正确类别的概率和未分到正确类别的概率”,softmax得到的是“分到正确类别的概率和分到错误类别的概率”。
一种常见的错法(其实就是我自己的错法),即,错误地将softmax和sigmoid混为一谈,在把BERT输出 层压缩至2维的情况下,却用sigmoid对结果进行计算。这样我们得到的结果其意义是什么呢?
假设我们现在BERT输出层经nn.Linear()压缩后,得到一个二维的向量:

[-0.9419267177581787, 1.944047451019287]

对应类别分别是(0,1)。我们经过sigmoid运算得到:

tensor([0.2805, 0.8748])

前者0.2805指的是分类类别为0的概率,0.8748指的是分类类别为1的概率。二者相互独立,可看作两次独立的实验(显然在这里不适用,因为0-1类别之间显然不是相互独立的两次伯努利事件)。所以显而易见的,二者加和并不等于1.
若用softmax进行计算,可得:

tensor([0.0529, 0.9471])

这里两者加和是1,才是正确的选择。

假设存在A、B两个类别,它们类别的最终输出值为a和b。以上计算得到的结果可以如下看待:

sigmoid:
[ 1 1 + e − a , 1 1 + e − b ] \left [ \frac{1}{1 + e^{-a}} , \frac{1}{1 + e^{-b}} \right ] [1+ea1,1+eb1]
softmax:
[ e a e a + e b , e b e a + e b ] \left [\frac{e^{a}}{e^{a}+e^{b}},\frac{e^{b}}{e^{a}+e^{b}}\right ] [ea+ebea,ea+ebeb]

差别还是很明显的

所以,大佬的话果然没错,这两者之间确实有差别,而softmax的处理方式,有时候会得到相较于sigmoid处理方式的微小提升。

  • 9
    点赞
  • 36
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_illusion_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值