PAYATHON 2026

如何在 Keras 中用缩放目标训练,并输出原始尺度的预测值?

技术老齐

可以,训练模型和预测模型可以共享同一个回归网络

可以把输入缩放和输出反缩放封装进模型,并将所需参数一起保存。一个直接的做法是建立两个共享权重的模型:

  • 训练模型:原始输入 → 输入缩放 → 回归网络 → 缩放后的预测值。
  • 预测模型:原始输入 → 输入缩放 → 同一个回归网络 → 输出反缩放。

训练时,用训练模型的输出和缩放后的目标值计算损失;部署时保存预测模型。这样调用 predict() 就能得到原始尺度的结果,无需另行读取均值和缩放系数。

对于输出数量级差异很大的多输出回归,保留目标缩放通常是合理的,尤其是在使用 MSE 并希望各输出的相对误差尺度比较接近时。

为什么不能直接在训练输出后面加反缩放层?

原问题中,部分输出在 ±10 附近,部分输出则在十万量级。如果直接计算原始尺度的 MSE,绝对误差较大的输出可能主导损失。

设第 j 个输出的缩放方式为:

[ y’_j = \frac{y_j-\mu_j}{s_j} ]

对应的反缩放为:

[ \hat y_j = s_j\hat y’_j+\mu_j ]

如果模型最后输出的已经是原始尺度的值,却仍把缩放后的目标值传给普通 mse,损失两端的单位就不一致。

如果改用原始目标值计算普通 MSE,单位虽然一致了,损失却回到了原始尺度,各输出的权重关系也会改变。因此,要保持使用缩放目标训练的行为,可以在反缩放之前计算损失。

实现示例:保存一个直接接收原始输入的预测模型

下面使用 tensorflow.keras,假设环境支持 .keras 保存格式。示例中的 x_train 和 y_train 是已经划分好的训练集,形状分别为 (样本数, 输入变量数) 和 (样本数, 输出变量数),数据均为有限数值。隐藏层结构仅用于演示。

固定仿射层执行 inputs * factor + offset,参数通过层配置随模型保存:

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


@keras.utils.register_keras_serializable(package="Regression")
class FixedAffine(layers.Layer):
    def __init__(self, factor, offset, **kwargs):
        super().__init__(**kwargs)
        self.factor_values = np.asarray(
            factor, dtype=np.float32
        ).tolist()
        self.offset_values = np.asarray(
            offset, dtype=np.float32
        ).tolist()

    def call(self, inputs):
        factor = tf.cast(self.factor_values, inputs.dtype)
        offset = tf.cast(self.offset_values, inputs.dtype)
        return inputs * factor + offset

    def get_config(self):
        config = super().get_config()
        config.update({
            "factor": self.factor_values,
            "offset": self.offset_values,
        })
        return config


x_train = np.asarray(x_train, dtype=np.float32)
y_train = np.asarray(y_train, dtype=np.float32)

# Compute statistics per variable, using training data only.
x_mean = x_train.mean(axis=0)
y_mean = y_train.mean(axis=0)
x_scale = np.max(np.abs(x_train), axis=0)
y_scale = np.max(np.abs(y_train), axis=0)

# Prevent division by zero for all-zero variables.
x_scale = np.where(x_scale == 0, 1.0, x_scale)
y_scale = np.where(y_scale == 0, 1.0, y_scale)

y_train_scaled = (y_train - y_mean) / y_scale

inputs = keras.Input(shape=(x_train.shape[1],))
x_scaled = FixedAffine(
    factor=1.0 / x_scale,
    offset=-x_mean / x_scale,
    name="input_scaling",
)(inputs)

h = layers.Dense(128, activation="relu")(x_scaled)
h = layers.Dense(64, activation="relu")(h)
y_scaled = layers.Dense(
    y_train.shape[1], name="scaled_prediction"
)(h)

# Train against scaled targets.
train_model = keras.Model(inputs, y_scaled)
train_model.compile(optimizer="adam", loss="mse")
train_model.fit(x_train, y_train_scaled, epochs=20)

# Reuse the trained graph and append inverse scaling.
y_original = FixedAffine(
    factor=y_scale,
    offset=y_mean,
    name="output_inverse_scaling",
)(y_scaled)
predict_model = keras.Model(inputs, y_original)

predict_model.save("regressor.keras")

loaded = keras.models.load_model("regressor.keras")
predictions = loaded.predict(
    np.asarray(x_test, dtype=np.float32)
)

predictions 已经是原始尺度的输出。输入缩放参数和输出反缩放参数都保存在模型中,无需另外维护统计量文件。

两个模型引用同一组网络层和权重,创建预测模型时不需要重新训练。在新进程中加载模型时,需要先导入定义并注册 FixedAffine 的模块,让 Keras 能够找到这个自定义层。

原来的公式不保证数值落在 [-1,1]

原问题使用的公式是:

x i ' = (x i - mean(X)) / max(|X|).

它可以使训练数据中每个变量的均值在数值精度允许的范围内接近零,但不保证缩放结果位于 [-1,1]。

例如,一个变量的训练值为 [0, 0, 10],均值约为 3.33,分母为 10,缩放结果在该范围内。但如果训练值为 [-10, 10, 10, 10],均值为 5,-10 就会被变换为 -1.5。

如果要求训练集在减去均值后落入 [-1,1],可以把分母改为中心化后的最大绝对值:

[ s_j = \max_i |x_{ij}-\mu_j| ]

对应代码为:

x_scale = np.max(np.abs(x_train - x_mean), axis=0)
y_scale = np.max(np.abs(y_train - y_mean), axis=0)

x_scale = np.where(x_scale == 0, 1.0, x_scale)
y_scale = np.where(y_scale == 0, 1.0, y_scale)

其余缩放和反缩放逻辑不变。不过,这只能约束用于计算统计量的训练数据;验证集、测试集和未来输入仍可能超出该范围。回归模型的输出也不应仅为匹配训练范围而默认使用 tanh,否则会限制可预测的范围。

输出缩放是否等于让所有输出同等重要?

输出缩放可以减少量纲和数值范围造成的影响,但“同等重要”仍取决于损失函数的定义。

对于相同的缩放参数,缩放空间中的平方误差等价于:

[ (\hat y’_j-y’_j)^2 = \frac{(\hat y_j-y_j)^2}{s_j^2} ]

也就是说,缩放后的 MSE 相当于对原始误差使用 1 / s_j² 的权重。它衡量的是相对于各变量缩放尺度的误差,不保证各输出对最终损失的贡献完全相同。

如果业务更关心各变量相对于自身变化范围的预测精度,可以继续使用缩放后的目标训练;如果业务明确按原始单位评价误差,则应据此设计损失权重。最终评估时,建议逐个输出查看原始尺度的 MAE 或 RMSE,避免汇总指标掩盖某些输出预测较差的问题。

均值和缩放系数应只用训练集计算,并固定用于验证、测试和部署。如果训练时传入验证数据,训练模型对应的验证目标也必须使用同一组参数缩放。

备注:内容仅供参考。