Sklearn中二分类问题的交叉熵计算

作者: 山阴少年 | 来源:发表于2018-07-26 20:42 被阅读19次

    二分类问题的交叉熵

      在二分类问题中,损失函数(loss function)为交叉熵(cross entropy)损失函数。对于样本点(x,y)来说,y是真实的标签,在二分类问题中,其取值只可能为集合{0, 1}. 我们假设某个样本点的真实标签为yt, 该样本点取yt=1的概率为yp, 则该样本点的损失函数为

    -log(yt|yp)=-(ytlog(yp)+(1-yt)log(1-yp))

    对于整个模型而言,其损失函数就是所有样本点的损失函数的平均值。注意到,对于该损失函数,其值应该为非负值,因为yp的取值在(0,1)之间。

    自己实现的方法有问题?

      在Python的sklearn模块中,实现二分类问题的交叉熵损失函数为log_loss()。我们尝试着运行官网中给出的例子,同时,用自己的方法实现该损失函数,其Python代码如下:

    from sklearn.metrics import log_loss
    from math import log # 自然对数为底
    
    # 二分类的交叉熵损失函数
    # 利用sklearn模块的计算结果
    y_true = [0, 0, 1, 1]
    y_pred = [[.9, .1], [.8, .2], [.3, .7], [.01, .99]]
    sk_log_loss = log_loss(y_true, y_pred)
    print('Loss by sklearn: %s.'%sk_log_loss)
    
    # 利用公式计算得到的结果
    Loss = 0
    for label, prob in zip(y_true, y_pred):
        Loss -= (label*log(prob[0])+(1-label)*log(prob[1]))
    
    Loss = Loss/len(y_true)
    print('Loss by equation: %s.'% Loss)
    

    在y_pred中,每个样本点都对应一组概率,如果我们把第一个概率作为样本分类为0的概率,第二个概率作为样本分类为1的概率,我们就会得到以下的输出结果:

    Loss by sklearn: 0.1738073366910675.
    Loss by equation: 2.430291498935543.
    

    我们惊讶的发现,两种方法得到的损失函数值竟然是不一样的。可是,貌似我们的计算公式也没有出问题啊,这到底是怎么回事呢?
      这时候,我们最好的办法是借助源代码的帮助,看看源代码是怎么实现的,与我们的计算方法有什么不一样。

    研究sklearn中的log_loss()源代码

      sklearn模块中的log_loss()函数的源代码地址为:https://github.com/scikit-learn/scikit-learn/blob/ed5e127b/sklearn/metrics/classification.py#L1576
      在具体分析源代码之前,我们应该注意以下几点(这也是从源代码中发现的):

    • 损失函数中的对数以自然常数e为底;
    • 预测概率的值有可能会出现0或1的情形,这在公式中是无意义的。因此,该代码使用了numpy中的clip()函数,将预测概率控制在[eps, 1-eps]范围内,其中eps为一个很小的数,避免了上述问题的出现。

      在log_loss()函数中,参数为:y_true, y_pred, eps, normalize, sample_weight,labels,为了分析问题的方便,我们只考虑该函数在所有默认参数取默认值时的情形。y_true为样本的真实标签,y_pred为预测概率。
      对于样本的真实标签y_true, 源代码中的处理代码为:

        lb = LabelBinarizer()
    
        if labels is not None:
            lb.fit(labels)
        else:
            lb.fit(y_true)
            
        transformed_labels = lb.transform(y_true)
    
        if transformed_labels.shape[1] == 1:
            transformed_labels = np.append(1 - transformed_labels,
                                           transformed_labels, axis=1)
    

    也就说,当我们的y_true为一维的时候,处理后的标签应当为二维的,比如说,我们输入的y_true为[0,0,1,1],那么处理后的标签应当为:

    [[1 0]
     [1 0]
     [0 1]
     [0 1]]
    

      对于预测概率,源代码中的处理过程为

        # Clipping
        y_pred = np.clip(y_pred, eps, 1 - eps)
    
        # If y_pred is of single dimension, assume y_true to be binary
        # and then check.
        if y_pred.ndim == 1:
            y_pred = y_pred[:, np.newaxis]
        if y_pred.shape[1] == 1:
            y_pred = np.append(1 - y_pred, y_pred, axis=1)
    

    也就说,当我们的y_true为一维的时候,处理后的标签应当为二维的,这跟处理样本的真实标签y_true是一样的。处理完y_true和y_pred后,之后就按照损失函数的公式得到计算值。

    自己实现二分类问题的交叉熵计算

      在我们分析完log_loss的源代码后,我们就能自己用公式来实现这个函数了,其Python代码如下:

    from sklearn.metrics import log_loss
    from math import log # 自然对数为底
    
    # 二分类的交叉熵损失函数的计算
    
    # y_true为一维,y_pred为二维
    # 用sklearn的log_loss函数计算损失函数
    y_true = [0,0,1,1]
    y_pred = [[0.1,0.9], [0.2,0.8], [0.3,0.7], [0.01, 0.99]]
    sk_log_loss = log_loss(y_true,y_pred)
    print('Loss by sklearn: %s.'%sk_log_loss)
    
    # 用公式自己实现损失函数的计算
    Loss = 0
    for label, prob in zip(y_true, y_pred):
        Loss -= ((1-label)*log(prob[0])+label*log(prob[1]))
    
    Loss = Loss/len(y_true)
    print('Loss by equation: %s.'% Loss)
    
    # y_true为一维,y_pred为一维
    # 用sklearn的log_loss函数计算损失函数
    y_true = [0,0,1,1]
    y_pred = [0.1, 0.2, 0.3, 0.01]
    sk_log_loss = log_loss(y_true,y_pred)
    print('Loss by sklearn: %s.'%sk_log_loss)
    
    # 用公式自己实现损失函数的计算
    Loss = 0
    for label, prob in zip(y_true, y_pred):
        Loss -= ((1-label)*log(1-prob)+label*log(prob))
    
    Loss = Loss/len(y_true)
    print('Loss by equation: %s.'% Loss)
    
    

    运行该函数,输出的结果为:

    Loss by sklearn: 1.0696870713050948.
    Loss by equation: 1.0696870713050948.
    Loss by sklearn: 1.5344117643215158.
    Loss by equation: 1.5344117643215158.
    

      这样我们就用公式能自己实现二分类问题的交叉熵计算了,计算结果与sklearn的log_loss()函数一致。

    感悟

      有空就得读读程序的源代码,不仅有助于我们解决问题,还能给我们很多启示,比如log_loss()函数中的np.clip()函数的应用,能很好地避免出现预测概率为0或1的情形。
      log_loss()函数的实现虽然简单,但阅读源代码的乐趣是无穷的。以后也会继续更新,希望大家多多关注。

    注意:本人现已开通两个微信公众号: 因为Python(微信号为:python_math)以及轻松学会Python爬虫(微信号为:easy_web_scrape), 欢迎大家关注哦~~

    相关文章

      网友评论

        本文标题:Sklearn中二分类问题的交叉熵计算

        本文链接:https://www.haomeiwen.com/subject/gurimftx.html