Python Keras Metrics 源码深度解析:有状态与无状态机制

桃夭
桃夭
发布时间:2026-10-16 | 7人浏览过
原创

本文深入解析 Keras 中 metrics 模块的源码实现,重点对比有状态(stateful)与无状态(stateless)度量的区别。以 tf.keras.metrics.Mean 为例,详解 total、count 及 update_state 等核心方法的运作机制,并展示如何继承 Metric 类创建自定义有状态指标,以及利用 Layer 的 add_metric 方法记录层统计信息。

前言

在模型评估中,metrics 用于衡量模型性能。度量函数的作用类似于损失函数,但其计算结果不参与模型的梯度更新或训练过程。在模型训练期间,通过 Tensorboard 监控 metrics 是最佳实践。

官方提供的 metrics 核心概念在于其拥有状态(stateful)变量。通过不断更新这些状态变量,metrics 可以累积统计数据,并随时输出基于状态变量计算的结果。这是 metrics 与 losses 的关键区别,因为 losses 通常是无状态(stateless)的。

本文部分内容参考了:

Keras-Metrics官方文档

代码运行环境为:tf.__version__==2.6.2 。

metrics原理解析(以metrics.Mean为例)

metrics 具有状态(stateful)特性,即 Metric 实例会存储、记录并返回已累积的结果,从而为后续操作提供信息支持。下面以 tf.keras.metrics.Mean() 为例进行解释:

创建 tf.keras.metrics.Mean 的实例:

m = tf.keras.metrics.Mean()

通过 help(m) 可以看到 MRO 为:

Mean Reduce Metric keras.engine.base_layer.Layer ...

可见 Metric 和 Mean 是 keras.layers.Layer 的子类。相比于基类 Layer,其子类 Mean 额外提供了几个关键方法:

  • result: 计算并返回标量度量值(tensor 形式)或标量字典,即对状态变量进行简单计算以得到度量值。例如,m.result(),就是计算均值并返回。
  • total: 状态变量 m 目前累积的数字总和
  • count: 状态变量 m 目前累积的数字个数(m.total/m.count 就是 m.result() 的返回值)
  • update_state: 累积统计数字用于计算指标。每次调用 m.update_state 都会更新 m.total 和 m.count;
  • reset_state: 将状态变量重置到初始化状态;
  • reset_states: 等价于 reset_state,参见 keras 源代码 metrics.py L355
  • reduction: 目前来看,没什么用。

这也决定了 Mean 的特殊性质。其使用参见如下代码:

# 创建状态变量m,由于m未刚初始化,
# 所以total,count和result()均为0
m = tf.keras.metrics.Mean()
print("m.total:",m.total)
print("m.count:",m.count)
print("m.result():",m.result())

""" # 输出: m.total: <tf.Variable 'total:0' shape=() dtype=float32, numpy=0.0> m.count: <tf.Variable 'count:0' shape=() dtype=float32, numpy=0.0> m.result(): tf.Tensor(0.0, shape=(), dtype=float32) """

# 更新状态变量,可以看到total累加了总和,
# count累积了个数,result()返回total/count
m.update_state([1,2,3])
print("m.total:",m.total)
print("m.count:",m.count)
print("m.result():",m.result())

""" # 输出: m.total: <tf.Variable 'total:0' shape=() dtype=float32, numpy=6.0> m.count: <tf.Variable 'count:0' shape=() dtype=float32, numpy=3.0> m.result(): tf.Tensor(2.0, shape=(), dtype=float32) """

# 重置状态变量, 重置到初始化状态
m.reset_state()
print("m.total:",m.total)
print("m.count:",m.count)
print("m.result():",m.result())

""" # 输出: m.total: <tf.Variable 'total:0' shape=() dtype=float32, numpy=0.0> m.count: <tf.Variable 'count:0' shape=() dtype=float32, numpy=0.0> m.result(): tf.Tensor(0.0, shape=(), dtype=float32) """

创建自定义metrics

创建无状态 metrics

与损失函数类似,任何带有类似于 metric_fn(y_true, y_pred)、返回损失数组(如输入一个 batch 的数据,会返回一个 batch 的损失标量)的函数,都可以作为 metric 传递给 compile():

import tensorflow as tf
import numpy as np
inputs = tf.keras.Input(shape=(3,))
x = tf.keras.layers.Dense(4, activation=tf.nn.relu)(inputs)
outputs = tf.keras.layers.Dense(1, activation=tf.nn.softmax)(x)
model1 = tf.keras.Model(inputs=inputs, outputs=outputs)
def my_metric_fn(y_true, y_pred):
    squared_difference = tf.square(y_true - y_pred)
    return tf.reduce_mean(squared_difference, axis=-1) # shape=(None,)
model1.compile(optimizer='adam', loss='mse', metrics=[my_metric_fn])
x = np.random.random((100, 3))
y = np.random.random((100, 1))
model1.fit(x, y, epochs=3)

输出:

Epoch 1/3 4/4 [==============================] - 0s 667us/step - loss: 0.0971 - my_metric_fn: 0.0971 Epoch 2/3 4/4 [==============================] - 0s 667us/step - loss: 0.0958 - my_metric_fn: 0.0958 Epoch 3/3 4/4 [==============================] - 0s 1ms/step - loss: 0.0946 - my_metric_fn: 0.0946

注意,因为本例创建的是无状态的度量,所以上面跟踪的度量值 (my_metric_fn 后面的值) 是每个 batch 的平均度量值,并不是一个 epoch(完整数据集)的累积值。(这一点需要理解,这也是为什么要使用有状态度量的原因!)

值得一提的是,如果上述代码使用

model1.compile(optimizer='adam', loss='mse', metrics=["mse"])

进行 compile,则输出的结果是累积的,在每个 epoch 结束时的结果就是整个数据集的结果,因为 metrics=["mse"] 是直接调用了标准库的有状态度量。

通过继承 Metric 创建有状态 metrics

如果想查看整个数据集的指标,就需要传入有状态的 metrics,这样就会在一个 epoch 内累加,并在 epoch 结束时输出整个数据集的度量值。

创建有状态度量指标,需要创建 Metric 的子类,它可以跨 batch 维护状态,步骤如下:

  • 在 __init__ 中创建状态变量 (state variables)
  • 更新 update_state() 中 y_true 和 y_pred 的变量
  • 在 result() 中返回标量度量结果
  • 在 reset_states() 中清除状态
class BinaryTruePositives(tf.keras.metrics.Metric):
    def __init__(self, name='binary_true_positives', **kwargs):
        super(BinaryTruePositives, self).__init__(name=name, **kwargs)
        self.true_positives = self.add_weight(name='tp', initializer='zeros')
    def update_state(self, y_true, y_pred, sample_weight=None):
        y_true = tf.cast(y_true, tf.bool)
        y_pred = tf.cast(y_pred, tf.bool)
        values = tf.logical_and(tf.equal(y_true, True), tf.equal(y_pred, True))
        values = tf.cast(values, self.dtype)
        if sample_weight is not None:
            sample_weight = tf.cast(sample_weight, self.dtype)
            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):
        self.true_positives.assign(0)
m = BinaryTruePositives()
m.update_state([0, 1, 1, 1], [0, 1, 0, 0])
print('Intermediate result:', float(m.result()))
m.update_state([1, 1, 1, 1], [0, 1, 1, 0])
print('Final result:', float(m.result()))

add_metric()方法

add_metric 方法是 tf.keras.layers.Layer 类添加的方法,Layer 的父类 tf.Module 并没有这个方法,因此在编写 Layer 子类如包括自定义层、官方提供的层 (Dense) 或模型 (tf.keras.Model 也是 Layer 的子类) 时,可以使用 add_metric() 来与层相关的统计量。比如,将类似 Dense 的自定义层的激活平均值记录为 metric。可以执行以下操作:

class DenseLike(Layer):
    """y = w.x + b"""
    ...
    def call(self, inputs):
        output = tf.matmul(inputs, self.w) + self.b
        self.add_metric(tf.reduce_mean(output), aggregation='mean', name='activation_mean')
        return output

将在名称为 activation_mean 的度量下跟踪 output,跟踪的值为每个批次度量值的平均值。

更详细的信息,参阅官方文档 The base Layer class - add_metric method。

参考

Keras-Metrics官方文档

到此这篇关于 Python keras.metrics 源代码分析的文章就介绍到这了,更多相关 Python keras.metrics 内容请搜索本站以前的文章或继续浏览下面的相关文章希望大家以后多多支持本站!

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表