1. 从房价预测切入:为什么回归任务是深度学习的另一条腿
很多人入门深度学习,第一个接触的都是分类任务——手写数字识别、猫狗分类、情感判断,输出的是离散的类别标签。但真实世界里大量问题要的不是“属于哪一类”,而是“值是多少”。房价、销量、温度、股价、设备剩余寿命,这些连续数值的预测,才是工业场景中更常见的需求。波士顿房价数据集就是用来练这个的经典入口。
这个数据集在Keras里内置,加载一行代码就能用,看起来简单到不像话。但恰恰因为它简单,很多人跑完一个模型拿到一个MSE数值就结束了,完全没搞明白回归任务和分类任务在建模思路上的根本差异。我见过不少做了半年深度学习的人,换个回归场景就不知道损失函数该选什么、输出层要不要激活函数、评估指标怎么看。所以这篇东西不打算只给你一段能跑的代码,而是把回归任务从数据理解到模型调优的完整链路拆开讲。
适合谁看?如果你已经跑过Keras的分类Demo,想搞清楚回归任务到底哪里不一样,这篇能帮你把认知补齐。如果你完全没碰过Keras,跟着走也能跑通,但建议先把Sequential模型的基本用法过一遍。整篇内容围绕波士顿房价数据集展开,但讲的方法论可以迁移到任何tabular数据的回归场景。
2. 波士顿房价数据集到底长什么样
2.1 506条数据、13个特征的真实含义
Keras内置的boston_housing数据集来自上世纪70年代波士顿周边城镇的房价统计,一共506条样本,每条有13个特征,目标值是该区域自有住房价格的中位数,单位是千美元。13个特征覆盖了城镇的犯罪率、住宅用地比例、是否临河、空气质量指标、平均房间数、到就业中心的距离、公路可达性、房产税率、师生比等维度。
先加载看一眼:
from tensorflow.keras.datasets import boston_housing (train_data, train_targets), (test_data, test_targets) = boston_housing.load_data() print(train_data.shape) # (404, 13) print(test_data.shape) # (102, 13) print(train_targets[:10]) # [15.2 42.3 50. 21.1 17.7 ...]训练集404条,测试集102条,Keras已经帮你切好了。目标值范围大概在5到50之间,单位千美元。注意这里的目标值不是原始价格,而是中位数,所以不会出现极端值把分布拉爆的情况。
2.2 回归任务和分类任务在数据层面的关键差异
分类任务里,标签是类别编号,比如0到9十个数字。回归任务里,标签是连续实数。这个差异看起来只是标签类型不同,但它会连锁影响到后面每一个环节。
第一,标签的尺度变得重要。分类任务的标签是离散的,模型输出经过softmax后比较的是概率分布。回归任务的标签是连续的,如果目标值范围是5到50,而你的模型初始输出可能在0附近波动,那梯度信号会非常不稳定。所以回归任务通常需要对目标值做标准化。
第二,特征尺度的影响被放大。分类任务里特征尺度不一致虽然也有影响,但类别边界相对鲁棒。回归任务里,一个特征范围是0到1,另一个是0到1000,模型在梯度下降时会被大尺度特征主导,收敛极慢甚至不收敛。所以特征标准化在回归任务里几乎是必做项。
第三,评估指标完全不同。分类看准确率,回归看MSE、MAE、RMSE。MSE对异常值敏感,MAE更鲁棒,RMSE和原始目标同量纲更直观。选哪个指标取决于你对误差的容忍方式。
2.3 数据标准化:为什么必须做,怎么做
前面说了回归任务对尺度敏感,具体操作就是对每个特征减去均值再除以标准差:
mean = train_data.mean(axis=0) std = train_data.std(axis=0) train_data = (train_data - mean) / std test_data = (test_data - mean) / std注意这里的关键细节:均值和标准差只能用训练集算,然后应用到测试集。如果你用全量数据算均值和标准差,测试集的信息就泄漏到了训练过程中,评估结果会偏乐观。这是很多人容易忽略的一个坑。
目标值要不要标准化?在这个数据集里,目标值范围是5到50,不算特别大,但标准化之后训练会更稳。如果做标准化,预测完之后记得反变换回来才能解释实际价格。我个人的习惯是目标值也做标准化,因为这样损失函数的初始值不会太大,学习率好设。
3. 搭建回归模型的几个关键决策点
3.1 输出层为什么不能加激活函数
这是回归任务里最容易被搞错的地方。分类任务的输出层通常用softmax或sigmoid,把输出压缩到概率范围。但回归任务预测的是连续值,如果加了sigmoid,输出被限制在0到1之间,而房价可能是5到50,模型永远预测不对。如果加了relu,输出非负,但房价理论上不可能是负数,这个约束倒不算错,但没必要。
所以回归任务的输出层就是一个Dense(1),不加任何激活函数,让它线性输出。Keras里Dense层默认activation就是linear,所以直接写Dense(1)就行。
from tensorflow.keras import models, layers def build_model(): model = models.Sequential([ layers.Dense(64, activation='relu', input_shape=(13,)), layers.Dense(64, activation='relu'), layers.Dense(1) ]) model.compile(optimizer='rmsprop', loss='mse', metrics=['mae']) return model3.2 损失函数选MSE还是MAE
MSE是均方误差,对误差取平方,大误差的惩罚被放大。MAE是平均绝对误差,对每个误差一视同仁。选哪个取决于你的业务场景。
如果你预测房价,偶尔出现一个偏差很大的预测,MSE会狠狠惩罚这个样本,迫使模型去拟合那些难样本。但如果你数据里有异常值,MSE会被异常值带偏。MAE更鲁棒,但梯度在零点不可导,训练时收敛可能慢一些。
实践中,MSE是回归任务的默认选择,因为它的梯度性质好,优化起来稳定。MAE通常作为评估指标而不是训练损失。我一般用MSE做loss,同时监控MAE,这样既保证训练稳定,又能看到实际误差的平均水平。
3.3 隐藏层规模和层数的取舍
这个数据集只有506条样本、13个特征,属于小规模tabular数据。隐藏层不需要太深,两层64单元的全连接网络足够了。层数太多、单元太多,参数量上去之后,几百条样本根本喂不饱,直接过拟合。
我试过一层32、两层64、三层128几种配置,在这个数据集上两层64的表现最稳。一层32欠拟合,训练loss和验证loss都降不下去。三层128训练loss能降得很低,但验证loss在几个epoch之后就开始回升,典型的过拟合。
对于tabular数据,经验法则是:先从一个隐藏层开始,逐步加层加宽,直到验证loss不再改善为止。不要一上来就堆深层网络,小数据集撑不住。
4. 训练过程中必须盯住的几个信号
4.1 用K折交叉验证代替单次划分
506条数据,如果只切一次训练集和验证集,验证集可能只有几十条,评估结果的方差很大。同样的模型,换个随机种子,验证loss可能差很多。这种情况下,K折交叉验证是更可靠的做法。
import numpy as np k = 4 num_val_samples = len(train_data) // k num_epochs = 100 all_scores = [] for i in range(k): print(f'Processing fold #{i}') val_data = train_data[i * num_val_samples: (i + 1) * num_val_samples] val_targets = train_targets[i * num_val_samples: (i + 1) * num_val_samples] partial_train_data = np.concatenate( [train_data[:i * num_val_samples], train_data[(i + 1) * num_val_samples:]], axis=0) partial_train_targets = np.concatenate( [train_targets[:i * num_val_samples], train_targets[(i + 1) * num_val_samples:]], axis=0) model = build_model() model.fit(partial_train_data, partial_train_targets, epochs=num_epochs, batch_size=16, verbose=0) val_mse, val_mae = model.evaluate(val_data, val_targets, verbose=0) all_scores.append(val_mae) print(f'MAE scores: {all_scores}') print(f'Mean MAE: {np.mean(all_scores):.2f}')4折交叉验证,每折验证集约101条,比单次划分稳定得多。跑完之后看平均MAE,而不是某一折的结果。如果各折之间MAE差异很大,说明数据分布不均匀或者模型不稳定,需要进一步排查。
4.2 训练loss下降但验证loss上升意味着什么
这是过拟合的典型信号。训练loss持续下降,说明模型在记忆训练数据。验证loss在某个epoch之后开始上升,说明模型学到的模式在新数据上不适用了。
遇到这种情况,几个处理方向:减小网络容量(减少层数或单元数)、加Dropout、加L2正则化、提前停止(EarlyStopping)。在这个数据集上,因为样本少,加Dropout效果比较明显。在隐藏层后面加layers.Dropout(0.5),训练时随机丢弃一半神经元,能有效抑制过拟合。
model = models.Sequential([ layers.Dense(64, activation='relu', input_shape=(13,)), layers.Dropout(0.5), layers.Dense(64, activation='relu'), layers.Dropout(0.5), layers.Dense(1) ])注意Dropout只在训练时生效,评估和预测时会自动关闭。所以加了Dropout之后,训练loss会比不加时高一些,这是正常的,不要误以为模型变差了。
4.3 学习率设多少合适
RMSprop的默认学习率是0.001,在这个数据集上通常够用。如果你发现训练loss震荡厉害或者下降太慢,可以调整。学习率太大,loss会震荡甚至发散;学习率太小,收敛太慢,100个epoch可能还没降到合理范围。
我一般会跑一次默认学习率,看loss曲线。如果前10个epoch loss下降很慢,说明学习率偏小,可以调到0.01试试。如果loss上下跳动,说明学习率偏大,调到0.0001。这个数据集规模小,试错成本低,跑几次就能找到合适的值。
5. 模型评估与预测结果的反标准化
5.1 MAE数值怎么解读
假设你跑完交叉验证,平均MAE是2.5。这个2.5的单位是千美元,意思是模型预测房价平均偏差2500美元。目标值范围是5到50千美元,MAE 2.5大概相当于平均偏差5%左右,对于这个数据集来说算不错的水平。
如果你对目标值做了标准化,MAE的单位就变成了标准差倍数,需要乘以目标值的标准差才能还原成原始单位。所以评估的时候要么不做目标标准化,要么记得反变换。
5.2 用训练好的模型做实际预测
训练完模型之后,拿测试集跑一遍预测:
model = build_model() model.fit(train_data, train_targets, epochs=80, batch_size=16, verbose=0) predictions = model.predict(test_data) print(predictions[:10].flatten()) print(test_targets[:10])对比预测值和真实值,能直观感受模型的表现。如果某些样本偏差特别大,可以单独看这些样本的特征,分析是哪些特征导致了误判。这种案例分析比只看一个MSE数值有价值得多。
5.3 反标准化与结果解释
如果训练时对目标值做了标准化,预测结果需要反变换:
# 假设训练时做了 target = (target - target_mean) / target_std predictions_original = predictions * target_std + target_mean反变换之后,预测值的单位回到千美元,可以直接和真实房价对比。这一步很容易忘,忘了的话预测值会在0附近波动,看起来完全不对。
6. 从波士顿房价数据集能迁移出什么通用经验
6.1 小规模tabular数据的建模套路
波士顿房价数据集虽然小,但它代表了一类非常常见的场景:几百到几千条样本、十几个到几十个特征、目标是连续值。这类场景在工业界大量存在,比如设备参数预测、销量预估、能耗预测。
这类数据的建模套路可以总结为:特征标准化是必做项,网络不要超过三层,每层单元数控制在32到128之间,Dropout和EarlyStopping是防过拟合的主力,K折交叉验证用来获得可靠的评估结果。不要试图用深层网络去拟合小数据,参数量远超样本量的时候,正则化手段只能缓解过拟合,不能根治。
6.2 回归任务中特征工程的不可替代性
深度学习在图像、文本、语音领域能自动提取特征,但在tabular数据上,特征工程的价值依然很大。波士顿房价数据集里的13个特征已经是经过筛选的,如果你拿到的是原始数据,可能需要做缺失值处理、类别特征编码、特征交叉、异常值处理。
我做过一个能耗预测的项目,原始特征只有温度、湿度、时间戳,直接扔给全连接网络效果一般。后来加了温度湿度交互项、时间的周期编码(sin/cos变换)、过去几小时的滑动平均,MAE直接降了30%。所以不要迷信“深度学习不需要特征工程”,在tabular场景下,好的特征比复杂的模型更管用。
6.3 常见踩坑清单
最后列几个我在这个数据集上踩过的坑,供你参考:
- 忘记对测试集用训练集的均值和标准差做标准化,导致评估结果不可信。
- 输出层加了激活函数,模型怎么训都训不好。
- 目标值做了标准化但预测时忘了反变换,预测值全在0附近。
- 用全部数据算标准化参数,造成数据泄漏。
- 网络层数太深,训练loss很低但验证loss很高,过拟合严重。
- 学习率用默认值但epoch设太少,模型还没收敛就停了。
- 只看MSE不看MAE,MSE被个别大误差样本主导,误以为模型很差。
这些坑都不复杂,但每一个都会让你多花几个小时甚至几天去排查。提前知道,能省不少时间。