使用神经网络解决二分类问题:从回归到分类
在前面的神经网络学习中,我们通常用模型解决回归问题 ,例如预测房价、销量、温度等连续数值。现在要进入另一个非常常见的机器学习任务:分类问题。
分类任务与回归任务在网络结构上有很多相似之处,例如仍然可以使用全连接层、激活函数、优化器和早停机制。但二者最大的区别在于:
- 输出层希望得到的结果不同
- 损失函数不同
- 评估指标不同
本文以二分类 Binary Classification为例,介绍神经网络如何完成分类任务。
1. 什么是二分类问题?
二分类指的是将样本划分到两个类别之一。
常见例子包括:
- 判断客户是否会购买商品
- 判断信用卡交易是否为欺诈
- 判断雷达信号是否探测到目标
- 判断医学检测结果是否显示患病
- 判断酒店订单是否会被取消
这些问题的共同点是:结果只有两种可能。
例如:
text
购买 / 不购买
欺诈 / 正常
有病 / 无病
取消 / 不取消
猫 / 狗
在原始数据中,类别可能是字符串,例如:
text
Yes / No
Dog / Cat
good / bad
但神经网络不能直接处理这些文本标签,因此需要将它们转换成数字标签。
例如:
python
df['Class'] = df['Class'].map({'good': 0, 'bad': 1})
也就是:
text
good -> 0
bad -> 1
这样模型才能学习输入特征与类别标签之间的关系。
2. 分类问题中的准确率 Accuracy
在分类任务中,最直观的评估指标是准确率 Accuracy。
准确率定义为:
text
accuracy = 预测正确的样本数 / 总样本数
如果模型全部预测正确,则准确率为:
text
accuracy = 1.0
例如有 100 个样本,模型预测对了 88 个,那么准确率就是:
text
accuracy = 88 / 100 = 0.88
也就是 88%。
准确率适合用在类别比较均衡的数据集中。例如正样本和负样本数量差不多时,准确率通常是一个比较合理的指标。
但是,如果类别极度不均衡,准确率可能会产生误导。
例如,某疾病检测数据中:
text
99% 的人没有病
1% 的人有病
如果模型永远预测"没有病",它也能达到 99% 的准确率,但这个模型显然没有实际价值。
所以在实际项目中,除了准确率,还经常关注:
- Precision 精确率
- Recall 召回率
- F1-score
- ROC-AUC
- PR-AUC
- 混淆矩阵 Confusion Matrix
3. 为什么不能直接用准确率作为损失函数?
训练神经网络时,需要一个损失函数 Loss Function来告诉模型当前预测有多差。
在回归问题中,我们常使用:
- MAE:平均绝对误差
- MSE:均方误差
这些损失函数是连续、平滑变化的,适合梯度下降算法优化。
但准确率不适合作为损失函数。
原因是准确率是一个"计数比例",它的变化是不连续的。
例如模型输出概率从:
text
0.51 -> 0.99
如果真实标签是 1,这两个预测最终都被判定为类别 1,因此准确率没有变化。
但显然,0.99 比 0.51 更有信心,也更接近理想预测。
反过来,如果模型输出从:
text
0.49 -> 0.51
只变化了一点点,但分类结果却从 0 变成了 1,准确率发生突变。
这种"不平滑"的特性不适合梯度下降算法。因此,分类任务通常使用另一种损失函数:交叉熵 Cross-Entropy。
4. 交叉熵 Cross-Entropy
对于分类任务,我们希望模型输出的是某个类别的概率。
例如对于二分类问题,模型可以输出:
text
0.8
表示模型认为该样本属于类别 1 的概率是 80%。
如果真实标签是 1,那么这个预测比较好。
如果真实标签是 0,那么这个预测就很差。
交叉熵的核心思想是:
当模型给正确类别分配的概率越高,损失越小;
当模型给正确类别分配的概率越低,损失越大。
对于二分类问题,常用损失函数是:
text
binary_crossentropy
其数学形式可以写成:
Loss=−y⋅log(p)+(1−y)⋅log(1−p) \text{Loss} = -\big y \\cdot \\log(p) + (1 - y) \\cdot \\log(1 - p) \\big Loss=−y⋅log(p)+(1−y)⋅log(1−p)
其中:
- yyy 是真实标签,取值为 0 或 1
- ppp 是模型预测为类别 1 的概率
- log\loglog 是对数函数(通常指自然对数)
如果真实标签是 1,希望 ppp 越接近 1 越好。
如果真实标签是 0,希望 ppp 越接近 0 越好。
举个例子,真实标签为 1:
text
预测概率 p = 0.99 -> 损失很小
预测概率 p = 0.60 -> 损失较大
预测概率 p = 0.01 -> 损失非常大
这就很好地惩罚了"自信但错误"的预测。
5. Sigmoid 函数:把输出变成概率
普通神经网络的 Dense 层输出可以是任意实数,例如:
text
-10, -2.5, 0, 3.7, 20
但二分类任务需要的是概率,也就是范围在:
text
[0, 1]
之间的数。
因此,在二分类模型的最后一层,通常使用 sigmoid 激活函数。
Sigmoid 函数可以将任意实数映射到 0 到 1 之间:
sigmoid(x)=11+e−x \text{sigmoid}(x) = \frac{1}{1 + e^{-x}} sigmoid(x)=1+e−x1
它的特点是:
- 输入 xxx 很大时,输出接近 1
- 输入 xxx 很小时,输出接近 0
- 输入 xxx 为 0 时,输出为 0.5
因此它非常适合用来表示二分类概率。
例如:
python
layers.Dense(1, activation='sigmoid')
这里的含义是:
- 输出层只有 1 个神经元
- 该神经元输出类别 1 的概率
- 使用 sigmoid 将输出限制在 0 到 1 之间
6. 如何从概率得到最终类别?
模型输出的是概率,而不是直接输出类别。
例如:
text
0.82
表示模型认为样本属于类别 1 的概率是 82%。
通常会设置一个阈值 threshold,例如:
text
0.5
判断规则为:
text
预测概率 < 0.5 -> 类别 0
预测概率 >= 0.5 -> 类别 1
例如:
text
0.12 -> 0
0.48 -> 0
0.51 -> 1
0.93 -> 1
Keras 中的 binary_accuracy 默认也使用 0.5 作为分类阈值。
不过在实际业务中,阈值不一定非要是 0.5。
例如在医学检测、欺诈检测等场景中,漏判代价很高,可能会降低阈值来提高召回率。
7. 示例:Ionosphere 数据集二分类
Ionosphere 数据集包含来自雷达信号的特征,任务是判断信号是否显示存在某种物体,还是只是空信号。
原始数据中的类别是:
text
good
bad
需要先映射为数字标签:
python
df['Class'] = df['Class'].map({'good': 0, 'bad': 1})
8. 数据划分与归一化
示例中将数据划分为训练集和验证集:
python
df_train = df.sample(frac=0.7, random_state=0)
df_valid = df.drop(df_train.index)
含义是:
- 70% 数据作为训练集
- 剩余 30% 数据作为验证集
random_state=0保证结果可复现
然后进行归一化:
python
max_ = df_train.max(axis=0)
min_ = df_train.min(axis=0)
df_train = (df_train - min_) / (max_ - min_)
df_valid = (df_valid - min_) / (max_ - min_)
归一化后,特征值被缩放到大致 0 到 1 之间。
这样做的好处是:
- 加快模型收敛
- 避免某些数值范围过大的特征主导训练
- 提高梯度下降的稳定性
需要注意的是,验证集归一化时使用的是训练集的 min_ 和 max_,而不是验证集自己的统计值。
这是为了避免数据泄漏。
9. 构建二分类神经网络
模型结构如下:
python
from tensorflow import keras
from tensorflow.keras import layers
model = keras.Sequential([
layers.Dense(4, activation='relu', input_shape=[33]),
layers.Dense(4, activation='relu'),
layers.Dense(1, activation='sigmoid'),
])
这个模型包含:
- 输入层:33 个特征
- 隐藏层 1:4 个神经元,ReLU 激活
- 隐藏层 2:4 个神经元,ReLU 激活
- 输出层:1 个神经元,Sigmoid 激活
这里输出层使用:
python
layers.Dense(1, activation='sigmoid')
是二分类模型的关键。
如果是回归任务,最后一层通常不使用 sigmoid。
如果是多分类任务,最后一层通常会使用 softmax。
10. 编译模型
模型编译代码如下:
python
model.compile(
optimizer='adam',
loss='binary_crossentropy',
metrics=['binary_accuracy'],
)
各参数含义如下:
optimizer='adam'
使用 Adam 优化器。
Adam 是深度学习中非常常用的优化算法,通常比普通 SGD 更容易训练,收敛速度也更稳定。
loss='binary_crossentropy'
使用二分类交叉熵作为损失函数。
这是二分类问题中最常见的选择。
metrics='binary_accuracy'
训练过程中同时记录二分类准确率。
注意:
loss用于模型优化metrics用于观察模型表现
模型真正优化的是 binary_crossentropy,不是 binary_accuracy。
11. 使用 Early Stopping 防止过拟合
训练代码中使用了早停机制:
python
early_stopping = keras.callbacks.EarlyStopping(
patience=10,
min_delta=0.001,
restore_best_weights=True,
)
参数含义如下:
patience=10
如果验证集指标连续 10 个 epoch 没有明显改善,就停止训练。
min_delta=0.001
只有改善幅度大于 0.001,才认为是真的改善。
restore_best_weights=True
训练结束后,恢复到验证集表现最好的那一轮模型参数。
这个参数非常重要。
因为模型最后一轮的权重不一定是最好的,可能已经开始过拟合。启用该参数后,可以自动保留验证集表现最佳的模型。
12. 训练模型
训练代码如下:
python
history = model.fit(
X_train, y_train,
validation_data=(X_valid, y_valid),
batch_size=512,
epochs=1000,
callbacks=[early_stopping],
verbose=0,
)
这里设置了最多训练 1000 个 epoch,但由于使用了 Early Stopping,模型通常不会真的训练满 1000 轮。
参数解释:
X_train, y_train:训练数据validation_data:验证数据batch_size=512:每次用 512 个样本更新模型epochs=1000:最多训练 1000 轮callbacks=[early_stopping]:启用早停verbose=0:不显示训练过程日志
13. 查看训练曲线
训练完成后,可以将 history.history 转换为 DataFrame:
python
history_df = pd.DataFrame(history.history)
然后绘制损失曲线:
python
history_df.loc[5:, ['loss', 'val_loss']].plot()
以及准确率曲线:
python
history_df.loc[5:, ['binary_accuracy', 'val_binary_accuracy']].plot()
这里从第 5 个 epoch 开始画图,是为了避开训练初期波动较大的阶段,让趋势更清晰。
输出示例:
text
Best Validation Loss: 0.3534
Best Validation Accuracy: 0.8857
表示验证集上最好的结果为:
- 最佳验证损失:0.3534
- 最佳验证准确率:0.8857
也就是模型在验证集上的准确率约为 88.57%。
14. 二分类模型的标准配置
对于神经网络二分类任务,常见配置可以总结如下:
python
model = keras.Sequential([
layers.Dense(若干神经元, activation='relu', input_shape=[特征数量]),
layers.Dense(若干神经元, activation='relu'),
layers.Dense(1, activation='sigmoid'),
])
model.compile(
optimizer='adam',
loss='binary_crossentropy',
metrics=['binary_accuracy'],
)
核心要点是:
| 任务类型 | 输出层 | 激活函数 | 损失函数 |
|---|---|---|---|
| 回归 | 1 个或多个神经元 | 通常无激活 | MAE / MSE |
| 二分类 | 1 个神经元 | sigmoid | binary_crossentropy |
| 多分类 | 类别数量个神经元 | softmax | categorical_crossentropy / sparse_categorical_crossentropy |
15. Sigmoid 与 Softmax 的区别
二分类通常使用:
python
Dense(1, activation='sigmoid')
多分类通常使用:
python
Dense(num_classes, activation='softmax')
二者区别如下:
Sigmoid
适合二分类或多标签分类。
输出每个类别独立成立的概率。
例如多标签任务:
text
一张图片可以同时包含:猫、狗、车
每个标签可以独立为真。
Softmax
适合单标签多分类。
输出所有类别的概率分布,且概率和为 1。
例如:
text
一张图片只能是:猫 / 狗 / 鸟 中的一类
16. 实践中的注意事项
1. 类别标签必须是数字
神经网络不能直接使用字符串类别,需要先编码:
python
good -> 0
bad -> 1
2. 特征需要归一化
对于神经网络,数值归一化通常很重要,可以提升训练稳定性。
3. 验证集不能参与训练统计
归一化参数应该从训练集计算,再应用到验证集和测试集。
4. 准确率不是万能指标
类别不平衡时,准确率可能误导判断。此时应该结合 Precision、Recall、F1、AUC 等指标。
5. 阈值可以根据业务调整
默认阈值是 0.5,但实际项目中可以根据业务需求调整。
例如:
- 想减少漏检:降低阈值
- 想减少误报:提高阈值
17. 总结
本文介绍了如何使用神经网络解决二分类问题。
核心知识点如下:
- 二分类问题的目标是将样本分为两个类别之一
- 原始类别标签需要转换为 0 和 1
- 准确率可以用于评估模型,但不适合作为损失函数
- 二分类常用损失函数是
binary_crossentropy - 输出层通常使用
sigmoid激活函数 - Sigmoid 可以将模型输出转换为 0 到 1 之间的概率
- 默认情况下,概率大于等于 0.5 判为类别 1,否则判为类别 0
- Adam 优化器同样适用于分类任务
- Early Stopping 可以减少过拟合并节省训练时间
一句话概括:
二分类神经网络的典型组合是:
sigmoid输出概率,binary_crossentropy作为损失函数,binary_accuracy作为评估指标。
完整流程可以概括为:
text
准备数据
-> 标签编码
-> 划分训练集和验证集
-> 特征归一化
-> 构建神经网络
-> 输出层使用 sigmoid
-> 使用 binary_crossentropy 编译
-> 训练模型
-> 查看 loss 和 accuracy 曲线
-> 根据验证集表现评估模型
掌握了这一流程之后,就可以将类似方法应用到更多实际二分类任务中,例如客户流失预测、订单取消预测、欺诈检测和医学辅助诊断等场景。