二分类是深度学习中最常见的任务形态,但很多人第一次用Keras搭建二分类模型时会遇到一个诡异的现象:模型训练若干轮之后,预测输出永远是同一个值,比如全部输出0,或者全部输出0.5附近的数。准确率看似还不错(比如数据里90%是负样本,全预测0就有90%准确率),实际上模型什么都没学到。这个问题背后的原因往往不止一个,需要系统地排查。本文将从输出层配置、损失函数选择、数据平衡、训练细节四个方面展开分析,并给出对应的修复方案。

输出层与损失函数的匹配问题
Keras二分类最常见的错误是输出层激活函数和损失函数不配套。二分类有两种主流写法:一种是输出层用1个神经元配合sigmoid激活,损失函数用binary_crossentropy;另一种是输出层用2个神经元配合softmax激活,损失函数用sparse_categorical_crossentropy或者one-hot标签配合categorical_crossentropy。两种写法都可以,但绝对不能混搭。
比如有人写了2个神经元的输出层却用binary_crossentropy,或者写了1个神经元却用categorical_crossentropy,模型梯度方向就乱了,最终往往收敛到固定输出。正确写法如下:
from tensorflow import keras
from tensorflow.keras import layers
# 写法一:单神经元 + sigmoid(推荐,二分类首选)
model = keras.Sequential([
layers.Dense(64, activation='relu', input_shape=(20,)),
layers.Dropout(0.3),
layers.Dense(1, activation='sigmoid') # 输出概率值,大于0.5判为正类
])
model.compile(optimizer='adam',
loss='binary_crossentropy',
metrics=['accuracy'])写法二则将输出改为layers.Dense(2, activation='softmax'),标签保持整数形式并用sparse_categorical_crossentropy。两种方式数学上等价,但写法一更简洁,也方便后面做阈值调整,因此建议优先采用。
还有一个隐蔽的坑:标签的形状。如果标签是形如(n, 1)的列向量,某些情况下会被误当成多输出。用labels.shape确认标签是一维的(n,),或者与输出神经元数量严格对应,能避免很多莫名其妙的收敛问题。
类别不平衡导致模型学坏
如果正负样本比例悬殊,比如1:20,模型会发现把所有样本都预测为多数类,损失就已经很低了,梯度会把它推向这个懒惰解。这种情况下准确率指标完全失效,应该改看AUC、精确率和召回率。
解决思路有三个层次。第一层是数据层面,对多数类做下采样,或者对少数类做过采样(SMOTE等)。第二层是损失层面,给不同类别赋予不同权重,让模型惩罚少数类判错更重:
import numpy as np
from sklearn.utils.class_weight import compute_class_weight
# 假设 y_train 是 0/1 标签
classes = np.unique(y_train)
weights = compute_class_weight(class_weight='balanced',
classes=classes, y=y_train)
class_weight_dict = dict(zip(classes, weights))
print(class_weight_dict) # 例如 {0: 0.56, 1: 4.8}
model.fit(X_train, y_train,
class_weight=class_weight_dict,
epochs=30,
validation_split=0.2)第三层是决策层面,默认的0.5阈值在不平衡场景下往往不合适。可以先用model.predict拿到原始概率,再在验证集上扫描阈值,选择使F1值或业务指标最大的那个阈值,而不是盲目接受0.5的切分点。
训练配置与数据本身的陷阱
除了结构和平衡问题,一些训练细节也会造成固定输出。首先是学习率过大,Adam默认0.001通常没问题,但如果手动调大,sigmoid输出可能在几轮内被推到饱和区,梯度消失,输出死锁在0或1。可以构建keras.optimizers.Adam(learning_rate=0.0001)试试降低学习率。其次,如果没有做特征标准化,某些特征数值范围达到几千,配合sigmoid会立刻饱和,输入标准化几乎是必做的:
from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_train = scaler.fit_transform(X_train) X_test = scaler.transform(X_test) # 测试集只能transform,防止数据泄漏 # 预测阶段还原概率 probs = model.predict(X_test) preds = (probs > 0.5).astype(int)
再者,要检查标签本身是否正确。曾有不少人排查了两天模型,最后发现标签列取错了,或者标签里存在NaN。用np.bincount(y_train)查看两个类别的数量,用np.isnan(y_train).any()确认无缺失值,这两行检查应该在训练前完成。
最后,如果网络太深太窄,或者数据本身可分性极差,模型也会退化为常数输出。可以先用一个逻辑回归(单层Dense)做基线,如果逻辑回归的AUC都在0.5附近,那问题在特征而不在模型,先回头做特征工程,别急着堆网络层数。
总结一下排查顺序:确认输出层与损失函数匹配,检查标签形状与类别分布,做特征标准化,加类别权重或重采样,必要时调低学习率并换用更简单的基线模型验证数据质量。按这个流程走一遍,绝大多数只预测单一类别的问题都能定位并解决。