上一篇利用 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。
更多推荐算法相关深度学习:深度学习导读专栏