如何用神经网络同时预测期望值与方差?求损失函数及TensorFlow实现
你描述的场景是典型的异方差回归问题——也就是噪声的方差随输入变化的回归任务,刚好我有过相关的实践经验,下面逐个解答你的疑问:
1. 如何用神经网络同时预测期望值与方差?
核心思路是让神经网络学习两个输入依赖的函数:
- $\mu(x)$:对应你说的确定性函数部分(输出的均值)
- $\sigma^2(x)$:对应依赖输入的噪声方差
网络结构上,推荐采用共享特征提取层+双输出头的设计:
- 前几层隐藏层作为共享特征提取器,负责从输入$x$中提取通用的特征表示,所有输入样本共享这部分计算,提升效率;
- 最后分支为两个独立的输出层:
- 一个输出$\mu(x)$:无需额外约束,因为均值可以是任意实数;
- 另一个输出$\log(\sigma2(x))$:用对数输出是为了确保后续转换后的方差非负(直接输出方差可能得到负数,不符合统计意义),之后通过指数运算$\sigma2(x) = \exp(\log_var)$得到合法的方差值。
2. 训练这类双输出网络的损失函数
必须使用**负对数似然(Negative Log-Likelihood, NLL)**作为损失函数,这是由你的数据生成逻辑推导出来的最优损失。
根据你的数据模型:
$$y = \mu(x) + \epsilon, \quad \epsilon \sim \mathcal{N}(0, \sigma^2(x))$$
样本$y$的似然函数是高斯分布:
$$p(y|x) = \frac{1}{\sqrt{2\pi\sigma^2(x)}} \exp\left(-\frac{(y - \mu(x))2}{2\sigma2(x)}\right)$$
取负对数后得到的损失公式为:
$$\mathcal{L} = \frac{1}{2} \log(\sigma^2(x)) + \frac{1}{2} \frac{(y - \mu(x))2}{\sigma2(x)}$$
这个损失的两个部分各有作用:
- 第一项$\frac{1}{2} \log(\sigma^2(x))$:惩罚过大的方差,避免模型“偷懒”用大方差掩盖拟合误差;
- 第二项$\frac{1}{2} \frac{(y - \mu(x))2}{\sigma2(x)}$:是被方差归一化后的均方误差,让模型更关注那些方差小(数据更可靠)的样本。
3. TensorFlow实现示例
下面是完整的可运行代码,包含模拟数据生成、模型构建、自定义损失、训练和结果可视化:
import tensorflow as tf from tensorflow.keras import layers, Model import numpy as np import matplotlib.pyplot as plt # ---------------------- 1. 生成符合要求的模拟数据 ---------------------- np.random.seed(42) x = np.linspace(-3, 3, 1000).reshape(-1, 1) # 输入特征 mu_true = np.sin(x) # 真实的确定性函数(均值) sigma_sq_true = 0.1 + 0.2 * (x + 2)**2 # 真实的输入依赖方差 epsilon = np.random.normal(0, np.sqrt(sigma_sq_true), size=x.shape) y = mu_true + epsilon # 最终观测值 # ---------------------- 2. 构建双输出神经网络 ---------------------- inputs = layers.Input(shape=(1,)) # 共享特征提取层 hidden = layers.Dense(64, activation='relu')(inputs) hidden = layers.Dense(64, activation='relu')(hidden) # 输出头1:预测均值mu mu_output = layers.Dense(1, name='mu')(hidden) # 输出头2:预测对数方差log_var(确保方差非负) log_var_output = layers.Dense(1, name='log_var')(hidden) # 定义模型 model = Model(inputs=inputs, outputs=[mu_output, log_var_output]) # ---------------------- 3. 自定义负对数似然损失函数 ---------------------- def negative_log_likelihood(y_true, y_pred): # 将双输出张量拆分为均值和对数方差 mu_pred, log_var_pred = y_pred[:, 0:1], y_pred[:, 1:2] # 转换为方差(确保非负) sigma_sq_pred = tf.exp(log_var_pred) # 计算NLL损失 loss = 0.5 * log_var_pred + 0.5 * tf.square(y_true - mu_pred) / sigma_sq_pred # 返回所有样本的平均损失 return tf.reduce_mean(loss) # 编译模型:将两个输出合并为一个张量,方便损失计算 model.compile(optimizer=tf.keras.optimizers.Adam(learning_rate=0.001), loss=negative_log_likelihood) # ---------------------- 4. 训练模型 ---------------------- # 训练时将模型的两个输出合并为一个张量 combined_output = tf.concat([mu_output, log_var_output], axis=1) history = model.fit(x, y, epochs=200, batch_size=32, verbose=1) # ---------------------- 5. 预测与结果可视化 ---------------------- mu_pred, log_var_pred = model.predict(x) sigma_sq_pred = np.exp(log_var_pred) sigma_pred = np.sqrt(sigma_sq_pred) plt.figure(figsize=(12, 6)) # 绘制均值拟合与置信区间 plt.subplot(1, 2, 1) plt.scatter(x, y, alpha=0.3, label='观测样本') plt.plot(x, mu_true, 'r--', linewidth=2, label='真实均值') plt.plot(x, mu_pred, 'b-', linewidth=2, label='预测均值') plt.fill_between(x.flatten(), mu_pred.flatten() - sigma_pred.flatten(), mu_pred.flatten() + sigma_pred.flatten(), color='blue', alpha=0.2, label='预测标准差区间') plt.xlabel('输入x') plt.ylabel('输出y') plt.title('均值拟合结果') plt.legend() # 绘制方差拟合结果 plt.subplot(1, 2, 2) plt.plot(x, sigma_sq_true, 'r--', linewidth=2, label='真实方差') plt.plot(x, sigma_sq_pred, 'b-', linewidth=2, label='预测方差') plt.xlabel('输入x') plt.ylabel('方差') plt.title('方差拟合结果') plt.legend() plt.tight_layout() plt.show()
关于损失函数中处理单个输出的说明
在上面的代码中,我们通过切片操作y_pred[:, 0:1]和y_pred[:, 1:2]将模型的双输出张量拆分为均值和对数方差两个独立的张量,再分别代入损失公式计算。
如果你更倾向于用多输出的字典形式(比如模型输出是{'mu': mu_output, 'log_var': log_var_output}),也可以这样定义损失函数:
def nll_loss_dict(y_true, y_pred_dict): mu_pred = y_pred_dict['mu'] log_var_pred = y_pred_dict['log_var'] sigma_sq_pred = tf.exp(log_var_pred) loss = 0.5 * log_var_pred + 0.5 * tf.square(y_true - mu_pred) / sigma_sq_pred return tf.reduce_mean(loss)
编译时只需指定loss={'mu': nll_loss_dict, 'log_var': None}(不过实际中合并输出的方式更简洁)。
内容的提问来源于stack exchange,提问作者Dr. Hans-Peter Störrr

