天天看點

SoftMax與交叉熵損失

快速跳轉位置

文章目錄

        • 先來一份代碼, 支援IDE斷點調試
        • SoftMax計算公式
        • 交叉熵損失
        • SoftMax交叉熵損失
        • SoftMax交叉熵損失梯度求導

作為分類任務重最常用的激活函數, SoftMax是如何進行計算, 以及對應的交叉熵損失如何計算?

先來一份代碼, 支援IDE斷點調試

# -*- coding: utf-8 -*-
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import os
import time
import argparse
import math

import tensorflow as tf
from tensorflow.keras.utils import Sequence
import numpy as np

"""兩種方式加載的資料集不同圖像部分資料是不同的,
official.mnist: 加載的圖像是uint8資料類型編碼, /255. 需要歸一化
tensorflow.examples.tutorials.mnist 是float類型編碼, 無需歸一化操作
"""
from tensorflow.keras.datasets import mnist
from tensorflow.keras.utils import to_categorical
from tensorflow.keras.models import Model
from tensorflow.keras.layers import MaxPooling2D, Conv2D, Input, Flatten, Dense, Dropout, Layer

# 立即執行模式
tf.enable_eager_execution()

"""
Sequence生成器的方法
__iter__()
__getitem__(index)
支援batch
"""


class DataGenerator(Sequence):
    # 自定義資料集加載方式,
    # 傳入資料可以是檔案清單或是其他格式,實作相應的加載和預處理方法即可
    def __init__(self, x, y, batch_size=32, shuffle=True):
        self.batch_size = batch_size
        self.x, self.y = x, y
        # 索引重排
        self.indexes = np.arange(len(self.x))
        self.shuffle = shuffle

    def __len__(self):
        # 計算每一個epoch的疊代次數
        return math.ceil(len(self.x) / float(self.batch_size))

    def __getitem__(self, index):
        # 生成每個batch資料,這裡就根據自己對資料的讀取方式進行發揮了
        # 生成batch_size個索引
        batch_indexs = self.indexes[index * self.batch_size:(index + 1) * self.batch_size]
        # 根據索引擷取資料集合中的資料
        x, y = self.data_generation(batch_indexs)
        return x, y

    def on_epoch_end(self):
        # 在每一次epoch結束是否需要進行一次随機,重新随機一下index
        if self.shuffle == True:
            np.random.shuffle(self.indexes)

    def data_generation(self, batch_indexs):
        x = self.x[batch_indexs]
        y = self.y[batch_indexs]
        return x, y


class MySoftMaxLayer(Layer):
    def __init__(self, **kwargs):
        super(MySoftMaxLayer, self).__init__(**kwargs)

    def build(self, input_shape):
        # SoftMax層不需要定義任何參數
        # 是以這裡不需要定義任何訓練參數

        # 一定調用父類方法 在最後!!!
        super(MySoftMaxLayer, self).build(input_shape)

    def call(self, logits, **kwargs):
        # 前向傳播計算
        # (200,10) / (200, 1)
        # tf.expand_dims 擴充次元
        # tf.squeeze 壓縮次元
        # return tf.exp(inputs) / tf.expand_dims(tf.reduce_sum(tf.exp(inputs), axis=-1), axis=-1)
        # 等同于
        return tf.exp(logits) / tf.reduce_sum(tf.exp(logits), axis=-1, keep_dims=True)

    def compute_output_shape(self, input_shape):
        # 在指定輸入下的輸出
        # TensorShape類型
        return input_shape


class LeNet(Model):
    def __init__(self, input_shape=(28, 28, 1), num_classes=10):
        # super(LeNet, self).__init__(name="LeNet")
        self.num_classes = num_classes
        ''' 定義要用到的層 layers '''
        # 輸入層
        img_input = Input(shape=input_shape)

        # Conv => ReLu => Pool
        x = Conv2D(filters=20, kernel_size=5, padding="same", activation="relu", name='block1_conv1')(img_input)
        x = MaxPooling2D(pool_size=(2, 2), strides=(2, 2), name='block1_pool')(x)
        # Conv => ReLu => Pool
        x = Conv2D(filters=50, kernel_size=5, padding="same", activation="relu", name='block1_conv2')(x)
        x = MaxPooling2D(pool_size=(2, 2), strides=(2, 2), name='block1_poo2')(x)
        # 壓成一維
        x = Flatten(name='flatten')(x)
        # 全連接配接層
        x = Dense(units=500, activation="relu", name="f1")(x)
        # softmax分類器
        # x = Dense(units=num_classes, activation="softmax", name="prediction")(x)
        x = Dense(units=num_classes)(x)
        x = MySoftMaxLayer(name="my_softmax_layer")(x)
        # 調用Model類的Model(input, output, name="***")構造方法
        super(LeNet, self).__init__(img_input, x, name="LeNet")

def my_softmax_cross_entropy_with_logits(labels, logits):
    # L = -sum(Yj*ln(Pj) = - ln(Pj)
    # Yj 中取值為 [0,1]
    # 标準的交叉熵損失為E = -sum(Yi * ln(Pi)
    return -tf.log(tf.reduce_sum(labels*logits, axis=1))
# 自定義損失函數
def loss(logits, labels):
    return  tf.reduce_mean(my_softmax_cross_entropy_with_logits(labels, logits))
    # softmax_cross_entropy_with_logits 為每一個輸入項結果計算一個損失, 傳回值arrayList, 長度N=Batch
    # reduce_mean 再求均值
    # return tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits_v2(labels=labels, logits=logits))


# 自定義評估函數
def compute_accuracy(logits, labels):
    predictions = tf.argmax(input=logits, axis=1, output_type=tf.int64)
    labels = tf.argmax(input=labels, axis=1, output_type=tf.int64)  # tf.cast(x=labels, dtype=tf.int64)
    batch_size = int(logits.shape[0])
    return tf.reduce_sum(tf.cast(tf.equal(predictions, labels), dtype=tf.float32)) / batch_size


def run_mnist_eager(cfg):
    # 自動選擇裝置
    (device, data_format) = ('/gpu:0', 'channels_last')
    if not tf.test.is_gpu_available():
        (device, data_format) = ('/cpu:0', 'channels_first')

    print('Using device %s, and data format %s.' % (device, data_format))
    # 載入資料集
    train_ds, test_ds = load_mnist()  # shape = (?, 768) / (?)

    # 建立 model and optimizer
    model = LeNet()
    optimizer = tf.train.MomentumOptimizer(cfg.lr, cfg.momentum)
    model.compile(optimizer=optimizer,
                  loss=loss,  # 'categorical_crossentropy',
                  metrics=[compute_accuracy])  # ['accuracy']


    # Generator 使用生成器方式提供資料,支援eager模式
    train_ds = DataGenerator(train_ds[0], train_ds[1], batch_size=200)
    test_ds = DataGenerator(test_ds[0], test_ds[1], batch_size=200)
    # Train and evaluate for a set number of epochs.
    with tf.device(device):  # 使用GPU必須有此一句
        for _ in range(cfg.train_epochs):
            start = time.time()

            model.fit_generator(generator=train_ds, epochs=1)
            # model.fit(train_ds[0], train_ds[1], batch_size=200, epochs=1)
            # verbose=0 不顯示
            # 生成器
            # _loss, _acc = model.evaluate(test_ds[0], test_ds[1], batch_size=100, verbose=0)
            # _loss, _acc = model.evaluate_generator(generator=test_ds, steps=10000)
            #
            _loss, _acc = model.evaluate_generator(generator=test_ds, verbose=1)
            print("test dataset loss: %f acc: %f" % (_loss, _acc))
            # train(model, optimizer, train_ds, step_counter, cfg.log_interval)
            end = time.time()
            print('\nTrain time for epoch #%d (%d total steps): %f' %
                  (_, len(train_ds), end - start))



def arg_parse():
    """參數定義"""
    parser = argparse.ArgumentParser(description="Lenet-5 MNIST 模型")
    parser.add_argument("--lr", dest="lr", help="學習率", default=0.01, type=float)
    parser.add_argument("--momentum", dest="momentum", help="SGD momentum.", default=0.5)

    parser.add_argument("--data_dir", dest="data_dir", help="資料集下載下傳/儲存目錄", default="data/mnist/input_data/")
    parser.add_argument("--model_dir", dest="model_dir", help="模型儲存目錄", default="data/mnist/checkpoints/")
    parser.add_argument("--batch_size", dest="batch_size", help="訓練或測試時 Batch Size", default=100, type=int)
    parser.add_argument("--train_epochs", dest="train_epochs", help="訓練時epoch疊代次數", default=4, type=int)
    parser.add_argument("--log_interval", dest="log_interval", help="日志列印間隔", default=10, type=int)

    # 傳回轉換好的結果
    return parser.parse_args()


def load_mnist():
    # 加載資料,轉換編碼格式并歸一化
    (x_train, y_train), (x_test, y_test) = mnist.load_data()
    x_train = x_train.astype("float32") / 255.
    x_test = x_test.astype("float32") / 255.

    # 擴充1維, 等效寫法
    x_train = x_train[:, :, :, None]
    x_test = x_test[:, :, :, None]

    print(x_train.shape, "train shape")
    print(x_test.shape, "test shape")

    y_train = to_categorical(y_train, 10)
    y_test = to_categorical(y_test, 10)
    train_ds = (x_train, y_train)
    test_ds = (x_test, y_test)

    return train_ds, test_ds


if __name__ == '__main__':
    args = arg_parse()
    run_mnist_eager(args)


                

其中: 我們關注的核心的是

my_softmax_cross_entropy_with_logits

函數和

MySoftMaxLayer

SoftMax計算公式

對于次元

(T,1)

的輸入, SoftMax的計算

第j類

機率(

0-1之間的一個浮點值

)公式

S j = e a j ∑ k = 1 T e a k {S_j} = \frac{{{e^{{a_j}}}}}{{\sum\limits_{k = 1}^T {{e^{{a_k}}}} }} Sj​=k=1∑T​eak​eaj​​

MySoftMaxLayer

build

函數中是批量輸入計算

softmax

分類實作, 最核心的代碼如下

return tf.exp(logits) / tf.reduce_sum(tf.exp(logits), axis=-1, keep_dims=True)
                

交叉熵損失

對于标準的交叉熵損失, 可以用下面的公式去表示

L = − ∑ j = 1 T Y j ln ⁡ P j L = - \sum\limits_{j = 1}^T {{Y_j}\ln {P_j}} L=−j=1∑T​Yj​lnPj​

其中

Pj

表示屬于第j類的機率,

Yj

表示第j類對應的參數值

SoftMax交叉熵損失

SoftMax的交叉熵損失标準形式如下,

L = − ∑ j = 1 T Y j ln ⁡ P j L = - \sum\limits_{j = 1}^T {{Y_j}\ln{P_j}} L=−j=1∑T​Yj​lnPj​

其中

Y

是一組多個0和一個1組成的

one-hot編碼

,

P

是預測屬于每個類别的機率, 因為

Y

隻有一個位置處為1,假設為

j

, 是以,公式可以寫成如下形式

L = − ln ⁡ P j L = - \ln {P_j} L=−lnPj​

核心代碼如下(批輸入損失計算)

def my_softmax_cross_entropy_with_logits(labels, logits):
    # L = -sum(Yj*Ln(Pj) = - Ln(Pj)
    # Yj 中取值為 [0,1]
    # 标準的交叉熵損失為E = -sum(Yi * ln(Pi)
    return -tf.log(tf.reduce_sum(labels*logits, axis=1))
                

SoftMax交叉熵損失梯度求導

轉載自: https://blog.csdn.net/qian99/article/details/78046329

首先,我們要明确一下我們要求什麼,我們要求的是我們的loss對于神經元輸出(zi)的梯度,即:

∂ C ∂ z i \frac{{\partial C}}{{\partial {z_i}}} ∂zi​∂C​

根據複合函數求導法則:

∂ C ∂ z i = ∂ C ∂ s j ∂ s j ∂ z i \frac{{\partial C}}{{\partial {z_i}}} = \frac{{\partial C}}{{\partial {s_j}}} \frac{{\partial {s_j}}}{{\partial {z_i}}} ∂zi​∂C​=∂sj​∂C​∂zi​∂sj​​

有個人可能有疑問了,這裡為什麼是   s j \ {{s_j}}  sj​而不是   s i \ {{s_i}}  si​(s為經過softmax運算後的結果, 即歸屬每一類的機率),這裡要看一下softmax的公式了,因為

softmax

公式的特性,它的分母包含了所有神經元的輸出,是以,對于不等于i的

其他輸出

裡面,也包含着   z i \ {{z_i}}  zi​,所有的s都要納入到計算範圍中,并且後面的計算可以看到需要分為

i=j

i≠j

兩種情況求導。

下面我們一個一個推:

∂ C ∂ s j = ∂ ( − ∑ j y j ln ⁡ s j ) ∂ s j = − ∑ j y j 1 s j \frac{{\partial C}}{{\partial {s_j}}} = \frac{{\partial ( - \sum\limits_j {{y_j}\ln } {s_j})}}{{\partial {s_j}}} = - \sum\limits_j {{y_j}\frac{1}{{{s_j}}}} ∂sj​∂C​=∂sj​∂(−j∑​yj​lnsj​)​=−j∑​yj​sj​1​

第二部分稍微複雜一點,我們先把它分為兩種情況:

①如果i=j:

∂ s i ∂ z i = ∂ ( e z i ∑ k e z k ) ∂ z i = ∑ k e z k e z i − ( e z i ) 2 ( ∑ k e z k ) 2 = ( e z i ∑ k e z k ) ( 1 − e z i ∑ k e z k ) = s i ( 1 − s i ) {{\partial \mathop {\rm{s}}\nolimits_{\rm{i}} } \over {\partial \mathop z\nolimits_i }} = {{\partial ({{\mathop e\nolimits^{\mathop z\nolimits_i } } \over {\sum\nolimits_k {\mathop e\nolimits^{\mathop z\nolimits_k } } }})} \over {\partial \mathop z\nolimits_i }} = {{\sum\nolimits_k {\mathop e\nolimits^{\mathop z\nolimits_k } } \mathop e\nolimits^{\mathop z\nolimits_i } - \mathop {(\mathop e\nolimits^{\mathop z\nolimits_i } )}\nolimits^2 } \over {\mathop {(\sum\nolimits_k {\mathop e\nolimits^{\mathop z\nolimits_k } } )}\nolimits^2 }} = ({{\mathop e\nolimits^{\mathop z\nolimits_i } } \over {\sum\nolimits_k {\mathop e\nolimits^{\mathop z\nolimits_k } } }})(1 - {{\mathop e\nolimits^{\mathop z\nolimits_i } } \over {\sum\nolimits_k {\mathop e\nolimits^{\mathop z\nolimits_k } } }}) = \mathop s\nolimits_i (1 - \mathop s\nolimits_i ) ∂zi​∂si​​=∂zi​∂(∑k​ezk​ezi​​)​=(∑k​ezk​)2∑k​ezk​ezi​−(ezi​)2​=(∑k​ezk​ezi​​)(1−∑k​ezk​ezi​​)=si​(1−si​)

②如果i≠j:

∂ s j ∂ z i = ∂ ( e z j ∑ k e z k ) ∂ z i = − z j ( 1 ∑ k e z k ) 2 e z i = − s i s j {{\partial {{\rm{s}}_j}} \over {\partial {\rm{ }}{z_i}}} = {{\partial ({{{\rm{ }}{e^{{\rm{ }}\mathop z\nolimits_j }}} \over {\sum\nolimits_k {{\rm{ }}{e^{{\rm{ }}\mathop z\nolimits_k }}} }})} \over {\partial {\rm{ }}{z_i}}} = - {\rm{ }}{z_j}{({1 \over {\sum\nolimits_k {{\rm{ }}{e^{{\rm{ }}\mathop z\nolimits_k }}} }})^2}{\rm{ }}{e^{{z_i}}} = - {\rm{ }}{s_i}{\rm{ }}{s_j} ∂zi​∂sj​​=∂zi​∂(∑k​ezk​ezj​​)​=−zj​(∑k​ezk​1​)2ezi​=−si​sj​

ok,接下來我們隻需要把上面的組合起來:

∂ C ∂ z i = ( − ∑ j y j 1 s j ) ∂ s i ∂ z i = − y i s i s i ( 1 − s i ) + ∑ j ≠ i y i s j s i s j = − y i + y i s i + ∑ j ≠ i y j s i = − y i + s i ∑ j y j \frac{{\partial C}}{{\partial {z_i}}} = ( - \sum\limits_j {{y_j}\frac{1}{{{s_j}}}} )\frac{{\partial {s_i}}}{{\partial {z_i}}} = - \frac{{{y_i}}}{{{s_i}}}{s_i}(1 - {s_i}) + \sum\limits_{j \ne i} {\frac{{{y_i}}}{{{s_j}}}} {s_i}{s_j} = - {y_i} + {y_i}{s_i} + \sum\limits_{j \ne i} {{y_j}{s_i} = - } {y_i} + {s_i}\sum\limits_j {{y_j}} ∂zi​∂C​=(−j∑​yj​sj​1​)∂zi​∂si​​=−si​yi​​si​(1−si​)+j̸​=i∑​sj​yi​​si​sj​=−yi​+yi​si​+j̸​=i∑​yj​si​=−yi​+si​j∑​yj​

最後的結果看起來簡單了很多,最後,針對分類問題,我們給定的結果   y i \ {{y_i}}  yi​最終隻會有一個類别是1,其他類别都是0,是以,對于分類問題,這個梯度等于:

∂ C ∂ z i = s i − y i \frac{{\partial C}}{{\partial {z_i}}} = {{ s_i}} - {{y_i}} ∂zi​∂C​=si​−yi​

繼續閱讀