深度学习 - 2.TF x Keras 自定义 Loss 与 Metrics

36 篇文章 6 订阅
24 篇文章 11 订阅

上一篇利用 keras 实现了最基本的手写数字识别模型,模型编译时loss用到了交叉熵 sparse_categorical_crossentropy,metrics 针对稀疏多分类问题用到了 sparse_categorical_accuracy,这里 loss 和 metrics 也支持自己实现,只需要继承 keras.losses.Loss 和keras.metrics.Metric 类即可。以下代码基于tensorflow 2.0 版本,Conda 3.7环境。

Tips:

首先导入需要用到的类,实现和上一篇Demo一样的模型结构,并且加载后面要用到的数据集。唯一的区别就是这里的模型还没有编译,没有指定对应的optimizer,loss,metrics。

import numpy as np
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers

# 模型结构化 后续可以多次调用
def get_uncompiled_model():
    inputs = keras.Input(shape=(784,), name="digits")
    x = layers.Dense(64, activation="relu", name="dense_1")(inputs)
    x = layers.Dense(64, activation="relu", name="dense_2")(x)
    outputs = layers.Dense(10, activation="softmax", name="predictions")(x)
    model = keras.Model(inputs=inputs, outputs=outputs)
    return model

# 加载MNIST数据集
(x_train, y_train), (x_test, y_test) = keras.datasets.mnist.load_data()
print("训练集大小 {} 测试集大小 {}".format(len(x_train),len(x_test)))
# Preprocess the data (these are NumPy arrays)
x_train = x_train.reshape(60000, 784).astype("float32") / 255
x_test = x_test.reshape(10000, 784).astype("float32") / 255

y_train = y_train.astype("float32")
y_test = y_test.astype("float32")

# Reserve 10,000 samples for validation
# 0-50000 训练 50000-60000 评估
x_val = x_train[-10000:]
y_val = y_train[-10000:]
x_train = x_train[:-10000]
y_train = y_train[:-10000]
print("验证集大小 {} 验证集大小 {}".format(len(x_val),len(y_val)))

一.自定义Loss

神经网络在反向传播过程中,首先根据 y_pred 与 y_true 计算Loss值,然后根据 Loss 值计算梯度并将梯度应用到当前层的可训练参数中,更新参数后,再计算当前模型的评估指标,最终我们看到了输出的 Auc 等指标日志。这里第一步做的就是计算Loss,计算 loss 需要 预测值和真实值。在Keras下,可以直接实现损失函数在模型 compile 时传入,也可以实现Loss子类继承keras.losses.Loss。

1.无参数实现自定义 Loss 函数

实现最基本的 MSE 并在模型编译时传入

def custom_mean_squared_error(y_true, y_pred):
    return tf.math.reduce_mean(tf.square(y_true - y_pred))

model = get_uncompiled_model()
model.compile(optimizer=demo.optimizers.Adam(), loss=custom_mean_squared_error)

2.有参数实现自定义 Loss 函数

上面的方法适合实现逻辑简单的 Loss ,如果需要更灵活的实现,就采用继承 Loss 子类的方式,这里需要重写下述方法:

# __init__(self) :接受在损失函数调用期间传递的参数,这里可以加入更多自定义的参数,具体看自己的实现诉求

# call(self, y_true, y_pred) :使用目标(y_true)和模型预测(y_pred)计算模型的损失 ,这里主要是实现 loss 的计算逻辑

下面在 MSE 的基础上加入正则化因子实现 Loss:

class CustomMSE(keras.losses.Loss):
    def __init__(self, regularization_factor=0.1, name="custom_mse"):
        super().__init__(name=name)
        self.regularization_factor = regularization_factor

    def call(self, y_true, y_pred):
        mse = tf.math.reduce_mean(tf.square(y_true - y_pred))
        reg = tf.math.reduce_mean(tf.square(0.5 - y_pred))
        return mse + reg * self.regularization_factor

 以上面 Loss 函数为例做一次测试:

tips:这里官方文档给的one-hot方法有bug,one-hot函数无法对float32类型做编码,所以需要转换类型: 

y_train_one_hot = tf.one_hot(y_train, depth=10)
model = get_uncompiled_model()
model.compile(optimizer=keras.optimizers.Adam(), loss=CustomMSE(0.4))

# 官方文档中类型是tf.float32 onehot函数需要使用int32才可以,float32不可以 
y_train_one_hot = tf.one_hot(tf.cast(y_train,dtype=tf.int32), depth=10)
history = model.fit(x_train, y_train_one_hot, batch_size=64, epochs=1,validation_data=(x_val, y_val),)

history.history
   64/50000 [..............................] - ETA: 6:41 - loss: 0.1527
 1984/50000 [>.............................] - ETA: 13s - loss: 0.1384 
 3904/50000 [=>............................] - ETA: 7s - loss: 0.1262 
 5760/50000 [==>...........................] - ETA: 5s - loss: 0.1203
 7744/50000 [===>..........................] - ETA: 3s - loss: 0.1162
 9472/50000 [====>.........................] - ETA: 3s - loss: 0.1142
11456/50000 [=====>........................] - ETA: 2s - loss: 0.1123
13248/50000 [======>.......................] - ETA: 2s - loss: 0.1110
15232/50000 [========>.....................] - ETA: 2s - loss: 0.1099
16832/50000 [=========>....................] - ETA: 1s - loss: 0.1092
18688/50000 [==========>...................] - ETA: 1s - loss: 0.1084
20608/50000 [===========>..................] - ETA: 1s - loss: 0.1077
22656/50000 [============>.................] - ETA: 1s - loss: 0.1071
24640/50000 [=============>................] - ETA: 1s - loss: 0.1066
26432/50000 [==============>...............] - ETA: 1s - loss: 0.1062
28480/50000 [================>.............] - ETA: 0s - loss: 0.1057
30400/50000 [=================>............] - ETA: 0s - loss: 0.1053
32192/50000 [==================>...........] - ETA: 0s - loss: 0.1050
34048/50000 [===================>..........] - ETA: 0s - loss: 0.1046
36032/50000 [====================>.........] - ETA: 0s - loss: 0.1044
38016/50000 [=====================>........] - ETA: 0s - loss: 0.1041
40000/50000 [=======================>......] - ETA: 0s - loss: 0.1038
41984/50000 [========================>.....] - ETA: 0s - loss: 0.1036
43840/50000 [=========================>....] - ETA: 0s - loss: 0.1033
45760/50000 [==========================>...] - ETA: 0s - loss: 0.1031
47744/50000 [===========================>..] - ETA: 0s - loss: 0.1030
49664/50000 [============================>.] - ETA: 0s - loss: 0.1028
50000/50000 [==============================] - 2s 42us/sample - loss: 0.1027 - val_loss: 27.5961

3.无参数实现有参数Loss

还有一种 trick 的方法,可以不继承 keras.losses.Loss 实现带参数的自定义 Loss。还是以上面的 CustomMse 为例:

def innerLoss(y_true,y_pred,regularrization):
    mse = tf.math.reduce_mean(tf.square(y_true - y_pred))
    reg = tf.math.reduce_mean(tf.square(0.5 - y_pred))
    return mse + reg * regularrization
def CustomMSE(regularrization):
    def inner(y_true, y_pred):
        return innerLoss(y_true,y_pred,regularrization)
    return inner

model = get_uncompiled_model()
        
model.compile(optimizer=keras.optimizers.Adam(), loss=CustomMSE(0.4))

这里 CustomMSE 采用了闭包的方式,将参数传给 innerLoss,再调用 inner 。注意,在不继承 keras.losses.Loss 实现自定义 Loss 时,损失函数只能有 y_true,y_pred 两个参数。

比较一下前后两次 CustomMSE 的效果:

# 继承 Loss 子类
# loss: 0.1026 - val_loss: 27.5975
# 不继承 Loss 子类
# loss: 0.1027 - val_loss: 27.5959

符合正常的波动,且两种方法都可以正常编译模型。

二.自定义 Metrics

除了自定义 Loss 外,Metrics 也可以自定义,类似于 AUC,RECALL 等等,这里需要继承 keras.metrics.Metric,并实现下述方法:

# __init__(self) ,您将在其中创建度量标准的状态变量。

# update_state(self, y_true, y_pred, sample_weight=None) ,它使用目标y_true和模型预测y_pred来更新状态变量。

# result(self) ,它使用状态变量来计算最终结果。

# reset_states(self) ,它重新初始化度量标准的状态。

下面实现简单的0-1累加指标:

class CategoricalTruePositives(keras.metrics.Metric):
    def __init__(self, name="categorical_true_positives", **kwargs):
        super(CategoricalTruePositives, self).__init__(name=name, **kwargs)
        self.true_positives = self.add_weight(name="ctp", initializer="zeros")
        # 也可以调整shape一次计算多个指标
        self.b = self.add_weight(name="b",initializer="random_uniform")
        print(self.b)

    # 通过y_true 与 y_pred 实现指标更新
    def update_state(self, y_true, y_pred, sample_weight=None):
        y_pred = tf.reshape(tf.argmax(y_pred, axis=1), shape=(-1, 1))
        values = tf.cast(y_true, "int32") == tf.cast(y_pred, "int32")
        values = tf.cast(values, "float32")
        if sample_weight is not None:
            sample_weight = tf.cast(sample_weight, "float32")
            values = tf.multiply(values, sample_weight)
        self.true_positives.assign_add(tf.reduce_sum(values))

    def result(self):
        return self.true_positives

    def reset_states(self):
        # The state of the metric will be reset at the start of each epoch.
        self.true_positives.assign(0.0)

init 方法可以调用 add_weight 添加多个自已统计需要用到的变量,upstate 根据每轮的y _pred 与 p_true 更新指标的值,这里通过argMax函数找到分类器认为最大类的手写数字索引,与真实进行比较。

预测正确:

tf.Tensor(True, shape=(), dtype=bool)
tf.Tensor(1.0, shape=(), dtype=float32)

预测错误:

tf.Tensor(False, shape=(), dtype=bool)
tf.Tensor(0.0, shape=(), dtype=float32)

可以简单理解为预测正确 +1 分,预测错不得分,所以随着epoch增加,如果模型越来越好,则该指标越来越大就对。下面测试一下:

model = get_uncompiled_model()
model.compile(
    optimizer=keras.optimizers.RMSprop(learning_rate=1e-3),
    loss=keras.losses.SparseCategoricalCrossentropy(),
    metrics=[CategoricalTruePositives()],
)
model.fit(x_train, y_train, batch_size=64, epochs=5,validation_split=0.2,)

这里多余的就不打出来了,可以看到自定义的指标是一直增加的,和上面说的情况相符合。 

Epoch 1/5
loss: 0.3770 - categorical_true_positives: 35751.0000 - val_loss: 0.2375 - val_categorical_true_positives: 9274.0000
Epoch 2/5
loss: 0.1814 - categorical_true_positives: 37838.0000 - val_loss: 0.1873 - val_categorical_true_positives: 9440.0000
Epoch 3/5
loss: 0.1814 - categorical_true_positives: 37838.0000 - val_loss: 0.1873 - val_categorical_true_positives: 9440.0000
Epoch 4/5
loss: 0.1326 - categorical_true_positives: 38437.0000 - val_loss: 0.1658 - val_categorical_true_positives: 9506.0000
Epoch 5/5
loss: 0.1040 - categorical_true_positives: 38730.0000 - val_loss: 0.1454 - val_categorical_true_positives: 9587.0000

最基本的自定义Loss,Metrics就这些了,当然不仅仅限制于model,Layer层也可以添加损失与指标,获取更多细节可以查看tensorflow官方API

更多推荐算法相关深度学习:深度学习导读专栏 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

BIT_666

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

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

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

打赏作者

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

抵扣说明:

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

余额充值