【机器学习算法】为什么交叉熵可以用作逻辑回归(分类模型)的损失函数 ?

逻辑回归的损失函数

有两种方式可以推导出二分类交叉熵损失函数,一个是通过极大似然估计法,另一个则是信息熵。

以MLE角度理解交叉熵

参考之前如何推导多元线性回归的损失函数,我们可以总结一下这个思想:那就是,一个预测模型的背后,一定都会假设预测结果服从某种分布,然后通过对该分布构建似然函数,紧接着使用极大似然估计,最后求解出线性模型中的参数。可不是嘛,多元线性回归假设数据的误差服从高斯分布,而逻辑回归假设预测的结果服从伯努利分布

损失函数要做的,就是优化模型学习出内部的参数,使得模型在这个参数给出的预测值的前提下,预测结果和标签相同的概率最大。这个条件概率可以表示如下:
P ( x 1 , x 2 , x 3 , x 4 , … , x n ∣ f ( W , X ) ) P\left(x_{1}, x_{2}, x_{3}, x_{4}, \ldots, x_{n} \mid f(W,X)\right) P(x1,x2,x3,x4,,xnf(W,X))
这里的W就是指模型的参数,f(W,X)就是表示模型的预测结果y_hat,即参数W和输入X的函数,x1~xn就是数据的真实标签。

对于多元线性回归而言,这里的f(W)就是θᵀx,并且我们假设这整个概率P(·)是服从高斯分布的,那么这个高斯分布哪来的呢,其实就源自于误差epsilon。所以损失函数要做的,就是使得这个条件概率最大化(预测结果最准确),说到这,这个概率P的含义就很清楚了,它实则是一个似然函数。这样一来,不就可以通过极大似然估计去求解了吗。我们也的确是这么做的,通过MLE,就推得了多元线性回归的损失函数MSE。

同理逻辑回归,我们假设预测的结果服从伯努利分布(0,1分布)

伯努利分布的概率计算:
P ⁡ ( x i = 1 ) = y i , P ⁡ ( x i = 0 ) = 1 − y i , 0 < p < 1 \operatorname{P}(x_i=1)=y_i, \operatorname{P}(x_i=0)=1-y_i, 0<p<1 P(xi=1)=yi,P(xi=0)=1yi,0<p<1
其中,xi表示每条样本的真值(标签0,1),yi表示模型对该样本的预测结果。

将上式整合成一个式子表达就是:
P ( x i ) = y i x i ( 1 − y i ) 1 − x i , x i ∈ { 0 , 1 } ; P(x_i)=y_{i}^{x_{i}}\left(1-y_{i}\right)^{1-x_{i}},\boldsymbol{x}_{i} \in\{\mathbf{0}, \mathbf{1}\}; P(xi)=yixi(1yi)1xixi{ 0,1};
此时似然函数就是:
∏ i = 1 n y i x i ( 1 − y i ) 1 − x i \prod_{i=1}^{n} y_{i}^{x_{i}}\left(1-y_{i}\right)^{1-x_{i}} i=1nyixi(1yi)1xi
转化为对数似然函数就是:
log ⁡ ( ∏ i = 1 n y i x i ( 1 − y i ) 1 − x i ) = ∑ i = 1 n log ⁡ ( y i x i ( 1 − y i ) 1 − x i ) = ∑ i = 1 n ( x i ⋅ log ⁡ y i + ( 1 − x i ) ⋅ log ⁡ ( 1 − y i ) ) \begin{aligned} &\log \left(\prod_{i=1}^{n} y_{i}^{x_{i}}\left(1-y_{i}\right)^{1-x_{i}}\right) \\ &=\sum_{i=1}^{n} \log \left(y_{i}^{x_{i}}\left(1-y_{i}\right)^{1-x_{i}}\right) \\ &=\sum_{i=1}^{n}\left(x_{i} \cdot \log y_{i}+\left(1-x_{i}\right) \cdot \log \left(1-y_{i}\right)\right) \end{aligned} log(i=1nyixi(1yi)1xi)=i=1nlog(yixi(1yi)1xi)=i=1n(xilogyi+(1xi)log(1yi))
所以最后我们要做的就是极大化这个对数似然函数:

arg ⁡ max ⁡ θ = ∑ i = 1 n ( x i ⋅ log ⁡ y i + ( 1 − x i ) ⋅ log ⁡ ( 1 − y i ) ) \underset{\theta}{\arg \max }\quad=\sum_{i=1}^{n}\left(x_{i} \cdot \log y_{i}+\left(1-x_{i}\right) \cdot \log \left(1-y_{i}\right)\right) θargmax=i=1n(xilogyi+(1xi)log(1yi))
但是损失函数一般都是来最小化的,小问题,对似然函数加个负号即可:
arg ⁡ min ⁡ θ = − ∑ i = 1 n ( x i ⋅ log ⁡ y i + ( 1 − x i ) ⋅ log ⁡ ( 1 − y i ) ) \underset{\theta}{\arg \min }\quad=-\sum_{i=1}^{n}\left(x_{i} \cdot \log y_{i}+\left(1-x_{i}\right) \cdot \log \left(1-y_{i}\right)\right) θargmin=i=1n(xilogyi+(1xi)log(1yi))
所以,逻辑回归的损失函数就是:
l o s s = − ∑ i = 1 n ( x i ⋅ log ⁡ y i + ( 1 − x i ) ⋅ log ⁡ ( 1 − y i ) ) loss=-\sum_{i=1}^{n}\left(x_{i} \cdot \log y_{i}+\left(1-x_{i}\right) \cdot \log \left(1-y_{i}\right)\right) loss=i=1n(xilogyi+(1xi)log(1yi))
这样,我们就用极大似然估计,推导出了逻辑回归的损失函数,但到这里其实还没有结束,因为,这个损失函数还有一个名称,那就是交叉熵。所以接下来,我将顺便以熵的角度,帮助大家理解一波这个损失函数的由来。

以信息论角度理解交叉熵

既然是交叉熵,那么公式的定义当中必然含有熵的概念,事实上,交叉熵里的熵,指的就是信息熵,因此我们必须先搞懂,信息熵是如何定义的:

信息熵

我们知道,熵在热力学当中,表示一个体系的混乱程度。克劳德·香农将熵的概念引入了信息论中,作为信息熵的定义者,他给出了信息熵的三个性质:

  1. 单调性,发生概率越高的事件,其携带的信息量越低;
  2. 非负性,信息熵可以看作为一种广度量,非负性是一种合理的必然;
  3. 累加性,即多随机事件同时发生存在的总不确定性的量度是可以表示为各事件不确定性的量度的和,这也是广度量的一种体现;
  4. 确定性,对于确定事件(概率为0或1),它的信息熵是0.

可见,信息熵与事件发生的概率大小有关。因此我们可以先将信息熵定义为事件概率的某个函数f:
H ( x ) : = f ( p ( x ) ) H(x) := f(p(x)) H(x):=f(p(x))
再根据信息熵的累加性定义,两个事件发生的信息熵是这两个事件各自发生的信息熵之和。我们假设事件是独立的,则有
f ( p ( x ) p ( y ) ) = f ( p ( x ) ) + f ( p ( y ) ) f(p(x)p(y)) = f(p(x))+f(p(y)) f(p(x)p(y))=f(p(x))+f(p(y))
那么,有没有一种函数,能将连乘的形式转换为连加呢,有,就是对数函数,所以信息熵就可以表示成:
H ( x ) : = C l o g a ( p ( x ) ) H(x) := Clog_a(p(x)) H(x):=Cloga(p(x))
现在我们还不知道这个对数的系数和底数到底是多少,所以先暂且用C和a表示了

然后再根据定义中的非负性,即信息熵一定是非负的,但是我们定义式中的概率实则是在(0,1)之间的一个值,而对数在这个范围内是一个负值,因此我们将系数定义为-1:
H ( x ) : = − l o g a ( p ( x ) ) H(x) := -log_a(p(x)) H(x):=loga(p(x))
(最后公式里的底数可以是e或者2.其实底数只会影响信息熵的单位,即导致量纲的不同,并不会影响计算信息熵的本质思想,因此这里就不再详细说明了)

这时候,我们便推得了体系中只有一个事件下的信息熵:

请添加图片描述

如果体系中包含多个事件,每个事件发生的概率不同,那么信息熵可以定义为系统中所有事件信息熵的期望值,即:
H ( X ) : = − ∑ x ∈ χ p ( x ) log ⁡ a p ( x ) H(X):=-\sum_{x \in \chi} p(x) \log_a p(x) H(X):=xχp(x)logap(x)

H ( X ) : = E ( H ( x i ) ) H(X):=E(H(x_i)) H(X):=E(H(xi))

到此,信息熵的定义我们就已经知道了,可以看到,系统中一个事件的信息发生的概率越低,它的信息熵也就越高,或者说,一个事件的信息熵越高,该事件的不确定度就越高。这就表明,低概率事件将蕴含着巨大的信息量。举个例子吧,我们说太阳是从东边升起的,这是一个不争的事实,你今天告诉我了,也不会给我的人生带来多大变化。所以说这个事件的信息熵很低。但是如果有一天,你告诉我,太阳从西边升起了,那这其中包含的信息可就复杂了,可能是太阳系发生了巨变,或者是地球的自转转向了等等,因此这个事件的信息熵很高。总之就是这个意思。

我们这时候不妨思考一下,分类算法(逻辑回归属于分类算法的一种,在深度学习的分类网络也需要使用交叉熵作为损失函数)和信息熵有什么联系吗,为什么分类算法的损失函数能和信息论扯上关系呢?

你要这么问,那还真就有点关系。我们知道(上文有提过),分类算法其实是在优化一个条件概率,这个条件概率中包含一组随机变量(数据集),数据集包含两个分布,一个是真实分布,我们记为p(x),一个是非真实分布,我们记为q(x),真实分布好理解,就是自然界本来的样子,在机器学习中就是输入x和真实标签y的分布,而非真实分布,也好理解,就是输入x和模型预测y_hat的分布。

为什么说是两个分布呢,因为预测和真实情况总会有偏差嘛,因此它们之间的分布参数并不是完全相同的,那么损失函数这时就要干一件事,那就是优化非真实分布q(y_hat),使其尽可能接近真实分布p(y)。这时候,我们就可以使用相对熵这种方法来实现这一度量。

K-L散度(相对熵)

请添加图片描述

相对熵,顾名思义,即一定是一个相对的概念。在信息论中,相对熵等价于两个概率分布的信息熵的差值。在机器学习中,相对熵表示使用理论分布拟合真实分布时产生的信息损耗,因此当相对熵越小时,预测值越能够拟合真实样本。

假设我们以真实分布为度量基准,相对熵的定义如下:
D K L ( P ∥ Q ) : = ∑ i = 1 m p i ⋅ ( H Q ( q i ) − H P ( p i ) ) \begin{aligned}&\boldsymbol{D}_{K L}(\boldsymbol{P} \| \boldsymbol{Q}) :=\sum_{i=1}^{m} p_{i} \cdot\left(H_{Q}\left(q_{i}\right)-H_{P}\left(p_{i}\right)\right) \\ \end{aligned} DKL(PQ):=i=1mpi(HQ(qi)HP(pi))
可以看到,相对熵和信息熵的差别就在于,信息熵中的每一项只关于一个分布系统中每一个事件的信息熵,而相对熵则是两个系统对应事件熵的差值,并且前面的权重也只基于其中一个系统的系数,所以这也导致了相对熵不能作为距离度量,因为它是不对称的,即:
D K L ( P ∥ Q ) ≠ D K L ( Q ∥ P ) \boldsymbol{D}_{K L}(\boldsymbol{P} \| \boldsymbol{Q}) \neq \boldsymbol{D}_{K L}(\boldsymbol{Q} \| \boldsymbol{P}) DKL(PQ)=DKL(QP)
将相对熵的定义继续展开:

D K L ( P ∥ Q ) = ∑ i = 1 m p i ⋅ ( ( − log ⁡ q i ) − ( − log ⁡ p i ) ) = ∑ i = 1 m p i ⋅ ( − log ⁡ q i ) − ∑ i = 1 m p i ⋅ ( − log ⁡ p i ) \begin{aligned} &\boldsymbol{D}_{K L}(\boldsymbol{P} \| \boldsymbol{Q}) \\ &=\sum_{i=1}^{m} p_{i} \cdot\left(\left(-\log q_{i}\right)-\left(-\log p_{i}\right)\right) \\ &=\sum_{i=1}^{m} p_{i} \cdot\left(-\log q_{i}\right)-\sum_{i=1}^{m} p_{i} \cdot\left(-\log p_{i}\right) \end{aligned} DKL(PQ)=i=1mpi((logqi)(logpi))=i=1mpi(logqi)i=1mpi(logpi)
这时候等式右边其实就是真实分布的信息熵,在分类问题中,真实分布实则就是数据集+标签,因此它是一个定值。

所以相对熵也可以表示成:
D K L ( P ∥ Q ) = H ( p , q ) − H ( p ) \boldsymbol{D}_{K L}(\boldsymbol{P} \| \boldsymbol{Q})=H(p, q)-H(p) DKL(PQ)=H(p,q)H(p)
既然我们知道了相对熵可以用来衡量两个分布系统的相似函数,那这不就说明我们可以用它来作为分类模型的损失函数吗,相对熵越小,说明两个系统的分布就越相似,这其实就可以等价于最小化损失函数为0。所以,我们其实就可以将相对熵作为损失函数。

但是到这里还没完,既然右边的H(p)是个定值,我们可否就拿左边的那部分来作为我们的损失呢?如果这么做,就需要一个前提,那就是要求相对熵必须恒为一个非负数,因为只有非负数,我们仅仅优化H(p,q)趋于0也可以保证整个相对熵最小化。如果相对熵可能出现负值,这时候仅仅优化H(p,q)趋于0就不能保证相对熵是最小了。

事实上,相对熵的确是非负的,这部分可以由吉布斯不等式证明。我这里就不深入了。

所以结论是,如果H§是定值,我们就可以仅通过优化H(p, q)来替代优化相对熵,那么H(p,q)又是什么呢。哈哈,它就是交叉熵呀

交叉熵(Cross Entropy)

把相对熵中的第一项单独拿出来,就是交叉熵:
H ( p , q ) = − ∑ i = 1 m p i ⋅ ( log ⁡ q i ) H(p,q)=-\sum_{i=1}^{m} p_{i} \cdot\left(\log q_{i}\right) H(p,q)=i=1mpi(logqi)
这时候,我们就可以把它和分类问题中的各个参数一一对应起来,m指的就是数据集数量;pi指的就是标签,如果是二分类,就是0或者1;qi指的就是模型预测出的某条样本对应的概率,范围是(0,1)。

但是仔细想想,感觉一些细节还有些问题。我们知道,第i条样本的交叉熵,应该对应第i个事件发生的概率,但是在二分类中,样本负例的标签为0,这显然不应该是负例样本发生的概率。同样的,逻辑回归模型预测出的结果也只包含它是正例的概率,即如果有一个样本预测为0.2,则应该表明它是负例的概率是0.8。这时候如果仅仅使用上述的交叉熵作为损失就是不完整的,应该再加上一项:
− ∑ i = 1 m ( 1 − p i ) ⋅ ( log ⁡ ( 1 − q i ) ) -\sum_{i=1}^{m} (1-p_{i}) \cdot\left(\log (1-q_{i})\right) i=1m(1pi)(log(1qi))
所以,对于二分类,完整的交叉熵损失就是(哈哈,感觉还是有凑出来的成分):
H ( p , q ) = − ( ∑ i = 1 m p i ⋅ log ⁡ q i + ∑ i = 1 m ( 1 − p i ) ⋅ log ⁡ ( 1 − q i ) ) = − ∑ i = 1 n ( y i ⋅ log ⁡ y ^ i + ( 1 − y i ) ⋅ log ⁡ ( 1 − y ^ i ) ) \begin{aligned} H(p,q)=-(\sum_{i=1}^{m} p_{i} \cdot\log q_{i}+\sum_{i=1}^{m} (1-p_{i}) \cdot\log (1-q_{i})) \\ =-\sum_{i=1}^{n}\left(y_{i} \cdot \log \hat{y}_{i}+\left(1-y_{i}\right) \cdot \log\left(1-\hat{y}_{i}\right)\right) \end{aligned} H(p,q)=(i=1mpilogqi+i=1m(1pi)log(1qi))=i=1n(yilogy^i+(1yi)log(1y^i))

推导逻辑回归损失对参数的梯度

我们先来推导一下sigmoid函数的导函数,方便接下来直接使用:
g ( z ) = 1 1 + e − z g ′ ( z ) = d d z 1 1 + e − z = 1 ( 1 + e − z ) 2 ( e − z ) = 1 ( 1 + e − z ) ⋅ ( 1 − 1 ( 1 + e − z ) ) = g ( z ) ( 1 − g ( z ) ) \begin{aligned} &g(z)=\frac{1}{1+e^{-z}} \\\\\\ &g^{\prime}(z) =\frac{d}{d z} \frac{1}{1+e^{-z}} \\ &=\frac{1}{\left(1+e^{-z}\right)^{2}}\left(e^{-z}\right) \\ &=\frac{1}{\left(1+e^{-z}\right)} \cdot\left(1-\frac{1}{\left(1+e^{-z}\right)}\right) \\ &=g(z)(1-g(z)) \end{aligned} g(z)=1+ez1g(z)=dzd1+ez1=(1+ez)21(ez)=(1+ez)1(1(1+ez)1)=g(z)(1g(z))
把逻辑回归的交叉熵损失写成带参数的形式:
l o s s ( θ ) = − ∑ i = 1 n ( y i ⋅ log ⁡ g ( θ T x i ) + ( 1 − y i ) ⋅ log ⁡ ( 1 − g ( θ T x i ) ) ) \begin{aligned} loss(\theta)=-\sum_{i=1}^{n}\left(y_{i} \cdot \log g(\theta^Tx_i)+\left(1-y_{i}\right) \cdot \log\left(1-g(\theta^Tx_i)\right)\right) \end{aligned} loss(θ)=i=1n(yilogg(θTxi)+(1yi)log(1g(θTxi)))
求导的过程我直接写纸上了(假设log以e为底):
请添加图片描述

所以损失函数关于参数θ的梯度就是:
g r a d . = 1 m ∑ i = 1 m ( g ( θ T x i ) − y i ) ) x i grad.=\frac{1}{m} \sum_{i=1}^{m}(g(\theta^Tx_i)-y_{i})) x_{i} grad.=m1i=1m(g(θTxi)yi))xi
啊哈,是不是感觉又很眼熟,这不和多元线性回归损失的梯度一样嘛,只不过是y_hat换了个形式而已,毕竟都是它们都是属于广义线性回归的一种。

使用逻辑回归实现乳腺癌数据集二分类

数据集简介:

    The breast cancer dataset is a classic and very easy binary classification dataset.
    =================   ==============
    类别数                           2
    每个类别下的样本数     212(M),357(B)
    总样本数                       569
    每条样本的维度                   30
    =================   ==============

(哈哈,直接在多元线性回归代码基础上改的,懒得重写了):

import sklearn.datasets as datasets # 数据集模块
import numpy as np
from sklearn.model_selection import train_test_split # 划分训练集和验证集
import sklearn.metrics # sklearn评估模块
from sklearn.preprocessing import StandardScaler # 标准归一化
from sklearn.metrics import accuracy_score

# 设置超参数
LR= 1e-5         # 学习率
EPOCH = 200000   # 最大迭代次数
BATCH_SIZE = 300  # 批大小
THRESHOLD = 1e-6 # 判断收敛条件

# 导入数据集
X, y = datasets.load_breast_cancer(return_X_y=True) # 乳腺癌二分类数据集
r= X.shape[0]
y = y.reshape(-1,1)
# 标准归一化
scaler = StandardScaler()
X = scaler.fit_transform(X)
# y = scaler.fit_transform(y)


X = np.concatenate((np.ones((r, 1)), X), axis=1)
# 划分训练集和验证集,使用sklearn中的方法
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.33)
m, n = X_train.shape[0], X_train.shape[1]
# 每个epoch包含的批数
NUM_BATCH = m//BATCH_SIZE+1
# print(m,n)
# 1, 随机初始化W参数
W = np.random.rand(n, 1)

train_loss = []
test_loss = []
train_acc = []
test_acc = []
count = 0




def sigmoid(x):
    return 1/(1 + np.exp(-x))

# 交叉熵损失
def cross_entropy(y_true, y_pred):
    crossEntropy = -np.sum(y_true * np.log(y_pred) + (1 - y_true) * np.log(1 - y_pred)) / (1 + y_true.shape[0])
    return crossEntropy


for i in range(EPOCH):

    # 这部分代码打乱数据集,保证每次小批量迭代更新使用的数据集都有所不同
    # 产生一个长度为m的顺序序列
    index = np.arange(m)
    # shuffle方法对这个序列进行随机打乱
    np.random.shuffle(index)
    # 打乱
    X_train = X_train[index]
    y_train =y_train[index]

    pred_y_test = sigmoid(np.dot(X_test, W))
    test_loss.append(cross_entropy(y_true=y_test, y_pred=pred_y_test))
    test_acc.append(accuracy_score(y_true=y_test, y_pred=(pred_y_test > 0.5).astype(int)))

    pred_y_train = sigmoid(np.dot(X_train, W))
    train_loss.append(cross_entropy(y_true=y_train, y_pred=pred_y_train))
    train_acc.append(accuracy_score(y_true=y_train, y_pred=(pred_y_train > 0.5).astype(int)))

    if i % 100 == 0:
        print("eopch: %d | train loss: %.6f | test loss: %.6f | train acc.:%.4f | test acc.:%.4f" % (i, train_loss[i], test_loss[i], train_acc[i], test_acc[i]))


    for batch in range(NUM_BATCH-1):
        # 切片操作获取对应批次训练数据(允许切片超过列表范围)
        X_batch = X_train[batch*BATCH_SIZE: (batch+1)*BATCH_SIZE]
        y_batch = y_train[batch*BATCH_SIZE: (batch+1)*BATCH_SIZE]

        # 2, 求梯度,需要用上多元线性回归对应的损失函数对W求导的导函数
        previous_y = sigmoid(np.dot(X_batch, W))
        grad = np.dot(X_batch.T, previous_y - y_batch)
        # print(X_batch.T.shape,previous_y.shape)
        # 3, 更新参数,利用梯度下降法的公式
        W = W - LR * grad

        # 4, 判断收敛
        current_y = sigmoid(np.dot(X_batch, W))
        previous_loss = cross_entropy(y_true=y_batch, y_pred=previous_y)
        current_loss = cross_entropy(y_true=y_batch, y_pred=current_y)
        d_loss = previous_loss - current_loss
        if 0 < d_loss < THRESHOLD:
            count += 1
        else:
            count = 0
        # 如果连续10次loss变化的幅度小于设定的阈值,让for循环退出
        if count >= 10:
            for loop in range(32):
                print('===', end='') 
            print("\ntotal iteration is : {}".format(i))
            break

    if count >= 10:
        break


# 打印最终结果
y_hat_train = sigmoid(np.dot(X_train, W))
loss_train = cross_entropy(y_true=y_train, y_pred=y_hat_train)
print("train loss:{}".format(loss_train))
y_hat_test = sigmoid(np.dot(X_test, W))
loss_test = cross_entropy(y_true=y_test, y_pred=y_hat_test)
print("test loss:{}".format(loss_test))
print("train acc.:{}".format(train_acc[-1]))
print("test acc.:{}".format(test_acc[-1]))

# 保存权重
# np.save("Weight.npy",W)
np.save("train_loss.npy",train_loss)
np.save("test_loss.npy",test_loss)
np.save("train_acc.npy",train_acc)
np.save("test_acc.npy",test_acc)

测试结果:

... ...
eopch: 18600 | train loss: 0.071966 | test loss: 0.059439 | train acc.:0.9843 | test acc.:0.9840
eopch: 18700 | train loss: 0.071909 | test loss: 0.059360 | train acc.:0.9843 | test acc.:0.9840
================================================================================================
total iteration is : 18717
train loss:0.07189857389459127
test loss:0.05934627841056932
train acc.:0.984251968503937
test acc.:0.9840425531914894

最终在测试集上的准确率达到了98%

猜你喜欢

转载自blog.csdn.net/SESESssss/article/details/121449771