6月5日 18:15

TensorFlow 损失函数怎么选?一张决策图搞定回归、分类和不平衡数据

损失函数决定了模型往哪个方向优化——选错了,训练再久也是白费。TensorFlow 内置了十几种损失函数,加上自定义能力,选择面很广,但真正常用且需要搞清楚的也就那么几类。

先搞清楚你的任务类型

选损失函数的第一步不是看哪个函数厉害,而是明确你的任务:

  • 回归(预测连续值,比如房价、温度)→ MSE / MAE / Huber
  • 二分类(是或否,比如垃圾邮件检测)→ Binary Crossentropy
  • 多分类(多个互斥类别,比如手写数字识别)→ Categorical / Sparse Categorical Crossentropy
  • 特殊场景(类别不平衡、图像分割、生成模型)→ Focal Loss / Dice Loss / KL Divergence

这个分类不是随便列的——同一类里的函数互相之间有明确的取舍逻辑,下面挨个说清楚。

回归损失:MSE、MAE 和 Huber 的取舍

三个函数各有脾气,选谁取决于你的数据长什么样。

MSE(均方误差)—— 默认选择,但对异常值过敏

python
model.compile(optimizer="adam", loss="mse")

MSE 对大误差施加二次惩罚——预测偏差 10 的样本,惩罚是偏差 1 的 100 倍。这意味着如果你的数据里有几个极端异常值(比如房价数据里突然混入一栋别墅),MSE 会拼命去拟合它们,结果把整体预测带偏。

什么时候用:数据干净、分布均匀,且你确实想对大误差更严格。

MAE(平均绝对误差)—— 异常值多时的保底方案

python
model.compile(optimizer="adam", loss="mae")

MAE 的惩罚和误差成线性关系,异常值不会像 MSE 那样获得不成比例的影响力。代价是在 0 点处不可导,梯度始终相同,收敛可能比 MSE 慢一些。

什么时候用:数据有明显异常值,或者你不想让少数极端样本主导训练方向。

Huber —— 两者的折中

python
from tensorflow.keras.losses import Huber model.compile(optimizer="adam", loss=Huber(delta=1.0))

Huber 的设计很直觉:误差小于 delta 时按 MSE 算(收敛快),误差大于 delta 时切换成 MAE(不被异常值绑架)。delta 就是那个分界线,调大调小直接影响模型对异常值的容忍度。

什么时候用:数据有异常值但你仍然想保留 MSE 在小误差时的收敛优势。实际上大多数回归任务 Huber 都是个比 MSE 更稳的选择,只是很多人不知道。

一个实际例子:预测用户付费金额时,90% 的用户付费在 0-100 元,但有个别用户付费过万。用 MSE 会导致模型过度关注那些大额用户,预测结果偏高;用 Huber(delta=10)就能忽略极端值的影响,同时保证小额预测的精度。

分类损失:交叉熵家族

分类任务几乎都用交叉熵(Crossentropy),区别在于标签格式和分类数量。

二分类 → Binary Crossentropy

python
model.compile(optimizer="adam", loss="binary_crossentropy")

输出层用 sigmoid 激活,标签是 0 或 1。这是二分类的标准配置,没什么好犹豫的。

注意:如果你的正负样本比例悬殊(比如欺诈检测,正常交易 99.9%,欺诈 0.1%),直接用 Binary Crossentropy 会让模型倾向于全预测为多数类。这时候需要 Focal Loss 或加权交叉熵。

多分类 → 两种 Crossentropy

这是很多人搞混的地方:

Categorical CrossentropySparse Categorical Crossentropy
标签格式one-hot 编码,如 [0, 1, 0]整数,如 1
输出层激活softmaxsoftmax
适合场景类别少、标签已 one-hot类别多、不想手动 one-hot
python
# 标签是 one-hot y_train = [[0, 1, 0], [1, 0, 0], [0, 0, 1]] model.compile(optimizer="adam", loss="categorical_crossentropy") # 标签是整数 y_train = [1, 0, 2] model.compile(optimizer="adam", loss="sparse_categorical_crossentropy")

经验:类别超过 10 个时,Sparse 版本更省内存也更好用。功能上完全等价,只是输入格式不同。

特殊场景的损失函数

Focal Loss —— 类别不平衡的杀手锏

python
def focal_loss(gamma=2.0, alpha=0.25): def loss(y_true, y_pred): y_true = tf.cast(y_true, tf.float32) epsilon = tf.keras.backend.epsilon() y_pred = tf.clip_by_value(y_pred, epsilon, 1.0 - epsilon) cross_entropy = -y_true * tf.math.log(y_pred) weight = alpha * tf.pow(1.0 - y_pred, gamma) return tf.reduce_mean(tf.reduce_sum(weight * cross_entropy, axis=1)) return loss model.compile(optimizer="adam", loss=focal_loss(gamma=2.0, alpha=0.25))

Focal Loss 的核心思想:模型已经分对的样本,少花点力气;分不对的样本,加大火力。gamma 控制对易分类样本的抑制程度(越大抑制越强),alpha 控制正类的权重。

什么时候用:目标检测、欺诈检测、罕见病诊断——任何正负样本比例超过 10:1 的场景。

Dice Loss —— 图像分割标配

python
def dice_loss(smooth=1.0): def loss(y_true, y_pred): y_true = tf.cast(y_true, tf.float32) y_pred = tf.cast(y_pred, tf.float32) intersection = tf.reduce_sum(y_true * y_pred) union = tf.reduce_sum(y_true) + tf.reduce_sum(y_pred) dice = (2.0 * intersection + smooth) / (union + smooth) return 1.0 - dice return loss model.compile(optimizer="adam", loss=dice_loss(smooth=1.0))

图像分割任务中,前景像素通常远少于背景像素。Dice Loss 基于 Dice 系数(衡量两个区域的重叠度),对小目标分割更友好。实际项目中经常和 Crossentropy 组合使用:total_loss = bce + dice_loss

KL Divergence —— 生成模型专用

python
model.compile(optimizer="adam", loss="kld")

KL 散度衡量两个概率分布的差异,在 VAE(变分自编码器)中让编码分布逼近标准正态分布,在知识蒸馏中让学生模型模仿教师模型的输出分布。日常分类回归任务用不上它。

自定义损失函数

内置函数覆盖不了所有场景。两种写法:

函数式——简单直接

python
def custom_loss(y_true, y_pred): mse = tf.reduce_mean(tf.square(y_true - y_pred)) reg = tf.reduce_mean(tf.square(y_pred)) return mse + 0.01 * reg model.compile(optimizer="adam", loss=custom_loss)

类式——需要传参数时

python
class WeightedMSE(tf.keras.losses.Loss): def __init__(self, weight=1.0, name="weighted_mse"): super().__init__(name=name) self.weight = weight def call(self, y_true, y_pred): return self.weight * tf.reduce_mean(tf.square(y_true - y_pred)) model.compile(optimizer="adam", loss=WeightedMSE(weight=2.0))

类式写法的好处是参数可以在 __init__ 中初始化,不用 functools.partial 那种绕弯路的方式。而且保存模型时能正确序列化。

多任务学习中的损失组合

一个模型同时预测多个目标时,需要组合多个损失函数。关键问题是权重怎么设——最简单的做法是手动调,更科学的方法是用 Uncertainty Weighting:

python
def multi_task_loss(y_true, y_pred): cls_pred, reg_pred = y_pred[:, :10], y_pred[:, 10:] cls_true, reg_true = y_true[:, :10], y_true[:, 10:] cls_loss = tf.keras.losses.categorical_crossentropy(cls_true, cls_pred) reg_loss = tf.keras.losses.mse(reg_true, reg_pred) return 0.5 * cls_loss + 0.5 * reg_loss model.compile(optimizer="adam", loss=multi_task_loss)

手动设 0.5/0.5 是起点,实际项目里通常需要根据各任务的收敛速度调整——哪个任务 loss 下降太快就降低权重,反之加大,保持各任务梯度量级大致相当。

损失函数调试要点

选完损失函数不代表万事大吉,训练过程中需要关注几个信号:

  • Loss 值是否在合理范围:MSE 在房价预测时可能几百,在手写数字分类时可能 0.01,这都正常。但如果 Binary Crossentropy 跑到负数,说明标签或预测值有问题。
  • 训练/验证 Loss 的差距:训练 Loss 持续下降但验证 Loss 开始上升,不是损失函数的问题,是过拟合——该加正则化或早停,不是换损失函数。
  • Loss 突然变 NaN:学习率太大或交叉熵里预测值出现了 0——加上 epsilon 裁剪:tf.clip_by_value(y_pred, 1e-7, 1 - 1e-7)

快速选择参考

你的任务数据特点推荐损失函数
回归数据干净MSE
回归有异常值Huber(delta 根据异常值大小设)
回归异常值很多MAE
二分类样本均衡Binary Crossentropy
二分类样本不平衡Focal Loss
多分类标签 one-hotCategorical Crossentropy
多分类标签整数Sparse Categorical Crossentropy
图像分割前景小Dice Loss + BCE 组合
生成模型VAE/GANKL Divergence

选损失函数不需要一步到位——先用最简单的(回归用 MSE,分类用 Crossentropy),跑出基线结果,再根据训练曲线和业务需求调整。大部分情况下换个损失函数带来的提升远不如调数据和特征。

标签:Tensorflow