zoukankan      html  css  js  c++  java
  • 自己动手实现深度学习框架-4 使用交叉熵损失函数支持分类任务

    代码仓库: https://github.com/brandonlyg/cute-dl

    目标

    1. 增加交叉熵损失函数,使框架能够支持分类任务的模型。
    2. 构建一个MLP模型, 在mnist数据集上执行分类任务准确率达到91%。

    实现交叉熵损失函数

    数学原理

    分解交叉熵损失函数

            交叉熵损失函数把模型的输出值当成一个离散随机变量的分布列。 设模型的输出为: (hat{Y} = f(X)), 其中(f(X))表示模型。(hat{Y})是一个m X n矩阵, 如下所示:

    [egin{bmatrix} hat{y}_{11} & hat{y}_{12} & ... & hat{y}_{1n} \ hat{y}_{21} & hat{y}_{22} & ... & hat{y}_{2n} \ ... & ... & ... & ... \ hat{y}_{m1} & hat{y}_{m2} & ... & hat{y}_{mn} end{bmatrix} ]

            把这个矩阵的第i行记为(hat{y}_i), 它是一个(\R^{1Xn})向量, 它的第j个元素记为(hat{y}_{ij})
            交叉熵损失函数要求(hat{y}_i)具有如下性质:

    [egin{matrix} 0<=hat{y}_{ij}<=1 & & (1)\ sum_{j=1}^{n} hat{y}_{ij} = 1, & n=2,3,... & (2) end{matrix} ]

            特别地,当n=1时, 只需要满足第一条性质即可。我们先考虑n > 1的情况, 这种情况下n=2等价于n=1,在工程上n=1可以看成是对n=2的优化。
            模型有时候并不会保证输出值有这些性质, 这时损失函数要把(hat{y}_i)转换成一个分布列:(hat{p}_i), 转换函数的定义如下:

    [egin{matrix} S_i = sum_{j=1}^{n} e^{hat{y}_{ij}}\ hat{p}_{ij} = frac{e^{hat{y}_{ij}}}{S_i} end{matrix} ]

            这里的(hat{p}_i)是可以满足要求的。函数(e^{hat{y}_{ij}})是单调增函数,对于任意两个不同的(hat{y}_{ia} < hat{y}_{ib}), 都有:(e^{hat{y}_{ia}})(e^{hat{y}_{ib}}), 从而得到:(hat{p}_{ia} < hat{p}_{ib}). 因此这个函数把模型的输出值变成了概率值,且概率的大小关系和输出值的大小关系一致。
            设数据(x_i)的类别标签为(y_i)(\R^{1Xn}). 如果(x_i)的真实类别为t, (y_i)满足:

    [egin{matrix} y_{ij} = 1 & {如果j=t} \ y_{ij} = 0 & {如果j≠t} end{matrix} ]

            (y_i)使用的是one-hot编码。交叉熵损失函数的定义为:

    [J_i = frac{1}{m} sum_{j=1}^{n} -y_{ij}ln(hat{p}_{ij}) ]

            对于任意的(y_{ij}), 损失函数中任意一项具有如下的性质:

    [egin{matrix} -y_{ij}ln(hat{p}_{ij}) ∈ [0, ∞), & 如果: y_{ij} = 1\ -y_{ij}ln(hat{p}_{ij})=0, & 如果: y_{ij} = 0 end{matrix} ]

            可看出(y_{ij}=0)的项对损失函数的值不会产生影响,所以在计算时可以把这样的项从损失函数中忽略掉。其它(y_{ij}=1)的项当(hat{p}_{ij}=y_{ij}=1)时损失函数达到最小值0。

    梯度推导

            根据链式法则, 损失函数的梯度为:

    [frac{partial J_i}{partial hat{y}_{ij}} = frac{partial J_i}{partial hat{p}_{ij}} frac{partial hat{p}_{ij}}{partial hat{y}_{ij}}, quad (1) ]

            其中:

    [frac{partial J_i}{partial hat{p}_{ij}} = frac{1}{m} frac{-y_{ij}}{hat{p}_{ij}} quad (2) ]

    [frac{partial hat{p}_{ij}}{partial hat{y}_{ij}} = frac{e^{hat{y}_{ij}}S_i - e^{2hat{y}_{ij}}}{S_i^2} = frac{hat{y}_{ij}}{S_i} - [frac{e^{hat{y}_{ij}}}{S_i}]^2 = hat{p}_{ij} - (hat{p}_{ij})^2 = hat{p}_{ij}(1-hat{p}_{ij}) quad (3) ]

            把(2), (3)代入(1)中得到:

    [frac{partial J_i}{partial hat{y}_{ij}} = frac{1}{m} frac{-y_{ij}}{hat{p}_{ij}} hat{p}_{ij}(1-hat{p}_{ij}) = frac{1}{m}(y_{ij}hat{p}_{ij} -y_{ij}) ]

            由于当(y_{ij}=0)时, 梯度值为0, 所以这种情况可以忽略, 最终得到的梯度为:

    [frac{partial J_i}{partial hat{y}_{ij}} = frac{1}{m}(hat{p}_{ij} -y_{ij}) ]

            如果模型的输出值是一个随机变量的分布列, 损失函数就可以省略掉把(hat{y}_{ij})转换成(hat{p}_{ij})的步骤, 这个时候(hat{y}_{ij} = hat{p}_{ij}), 最终的梯度变成:

    [frac{partial J_i}{partial hat{y}_{ij}} = frac{partial J_i}{partial hat{p}_{ij}} = - frac{y_{ij}}{mhat{y}_{ij}} ]


    交叉熵损失函数的特殊情况: 只有两个类别

            现在来讨论当n=1的情况, 这个时候(hat{y}_i)(\R^{1 X 1}),可以当成标量看待。
            如果模型输出的不是分布列, 损失函数可以分解为:

    [egin{matrix} hat{p}_{i} = frac{1}{1+e^{-hat{y}_{i}}} \ \ J_i = frac{1}{m}[-y_iln(hat{p}_{i}) - (1-y_i)ln(1-hat{p}_{i})] end{matrix} ]

            损失函数关于输出值的梯度为:

    [frac{partial J_i}{partial hat{p}_i} = frac{1}{m}(-frac{y_i}{hat{p}_i} + frac{1-y_i}{1 - hat{p}_i}) = frac{hat{p}_i - y_i}{mhat{p}_i(1-hat{p}_i)}, quad (1) ]

    [frac{partial hat{p}_i}{partial hat{y}_i} = frac{e^{-hat{y}_{i}}}{(1+e^{-hat{y}_{i}})^2} = frac{1}{1+e^{-hat{y}_{i}}} frac{e^{-hat{y}_{i}}}{1+e^{-hat{y}_{i}}} = hat{p}_{i}(1- hat{p}_{i} ), quad (2) ]

    [frac{partial J_i}{partial hat{y}_i} = frac{partial J_i}{partial hat{p}_i} frac{partial hat{p}_i}{partial hat{y}_i}, quad (3) ]

            把(1),(2)代入(3)中得到:

    [frac{partial J_i}{partial hat{y}_i} = frac{hat{p}_i - y_i}{mhat{p}_i(1-hat{p}_i)} hat{p}_{i}(1- hat{p}_{i} ) = frac{1}{m}(hat{p}_i - y_i) ]

            如果模型输出值时一个随机变量的分布列, 则有:

    [frac{partial J_i}{partial hat{y}_i} = frac{partial J_i}{partial hat{p}_i} = frac{hat{y}_i - y_i}{mhat{y}_i(1-hat{y}_i)} ]


    实现代码

            这个两种交叉熵损失函数的实现代码在cutedl/losses.py中。一般的交叉熵损失函数类名为CategoricalCrossentropy, 其主要实现代码如下:

      '''
      输入形状为(m, n)
      '''
      def __call__(self, y_true, y_pred):
          m = y_true.shape[0]
          #pdb.set_trace()
          if not self.__form_logists:
              #计算误差
              loss = (-y_true*np.log(y_pred)).sum(axis=0)/m
              #计算梯度
              self.__grad = -y_true/(m*y_pred)
              return loss.sum()
    
          m = y_true.shape[0]
          #转换成概率分布
          y_prob = dlmath.prob_distribution(y_pred)
          #pdb.set_trace()
          #计算误差
          loss = (-y_true*np.log(y_prob)).sum(axis=0)/m
          #计算梯度
          self.__grad  = (y_prob - y_true)/m
    
          return loss.sum()
    

            其中prob_distribution函数把模型输出转换成分布列, 实现方法如下:

    def prob_distribution(x):
        expval = np.exp(x)
        sum = expval.sum(axis=1).reshape(-1,1) + 1e-8
    
        prob_d = expval/sum
    
        return prob_d
    

            二元分类交叉熵损失函数类名为BinaryCrossentropy, 其主要实现代码如下:

    '''
    输入形状为(m, 1)
    '''
    def __call__(self, y_true, y_pred):
        #pdb.set_trace()
        m = y_true.shape[0]
    
        if not self.__form_logists:
            #计算误差
            loss = (-y_true*np.log(y_pred)-(1-y_true)*np.log(1-y_pred))/m
            #计算梯度
            self.__grad = (y_pred - y_true)/(m*y_pred*(1-y_pred))
            return loss.sum()
    
        #转换成概率
        y_prob = dlmath.sigmoid(y_pred)
        #计算误差
        loss = (-y_true*np.log(y_prob) - (1-y_true)*np.log(1-y_prob))/m
        #计算梯度
        self.__grad = (y_prob - y_true)/m
    
        return loss.sum()
    

    在MNIST数据集上验证

            现在使用MNIST分类任务验证交叉熵损失函数。代码位于examples/mlp/mnist-recognize.py文件中. 运行这个代码前先把原始的MNIST数据集下载到examples/datasets/下并解压. 数据集下载链接为:https://pan.baidu.com/s/1CmYYLyLJ87M8wH2iQWrrFA,密码: 1rgr

            训练模型的代码如下:

    '''
    训练模型
    '''
    def fit():
        inshape = ds_train.data.shape[1]
        model = Model([
                    nn.Dense(10, inshape=inshape, activation='relu')
                ])
        model.assemble()
    
        sess = Session(model,
                loss=losses.CategoricalCrossentropy(),
                optimizer=optimizers.Fixed(0.001)
                )
    
        stop_fit = session.condition_callback(lambda :sess.stop_fit(), 'val_loss', 10)
    
        #pdb.set_trace()
        history = sess.fit(ds_train, 20000, val_epochs=5, val_data=ds_test,
                            listeners=[
                                stop_fit,
                                session.FitListener('val_end', callback=accuracy)
                            ]
                        )
    
        fit_report(history, report_path+"0.png")
    

            拟合报告:

            可以看出,通过一个小时(3699s), 将近600万步的训练,模型准确率达到了92%。同样的模型在tensorflow(CPU版)中经过十几分钟的训练即可达到91%。这说明, cute-dl框架在任务性能上是没问题的,但训练模型的速度欠佳。

    总结

            这个阶段框架实现了对分类任务的支持, 在MNIST数据集上验证模型性能达到预期。模型训练的速度并不令人满意。
            下个阶段,将会给模型添加学习率优化器, 在不损失泛化能力的同时加快模型训练速度。

  • 相关阅读:
    神秘题目4
    神秘题目3
    神秘题目2
    AC自动机
    Fence Obstacle Course 题解
    Fractal Streets
    龟速乘
    快速幂
    Stall Reservation
    Sunscreen
  • 原文地址:https://www.cnblogs.com/brandonli/p/12745859.html
Copyright © 2011-2022 走看看