Keras实战:用波士顿房价数据集手把手教你搭建第一个深度学习模型(附完整代码)
从零到一:用Keras构建你的第一个房价预测模型
还记得第一次接触深度学习时,那种既兴奋又无从下手的感觉吗?面对满屏的数学公式和抽象概念,很多初学者往往在第一步就卡住了。今天,我想带你绕过那些复杂的理论迷宫,直接动手搭建一个能实际运行的深度学习模型。我们不用什么高深莫测的数据集,就用经典的波士顿房价数据,一步步把代码写出来,看着模型从无到有地“学会”预测房价。
这篇文章是写给谁的?如果你对Python有基本了解,知道什么是数组和函数,但对神经网络还感到陌生,那么你来对地方了。我们将完全从实践出发,不空谈理论,专注于代码和结果。你会发现,深度学习的入门门槛并没有想象中那么高。通过这个完整的项目,你不仅能得到一个可以运行的模型,更能理解模型背后的每一步决策——为什么这样设计网络结构?为什么要标准化数据?训练过程中那些曲线又代表了什么?
准备好了吗?让我们打开编辑器,开始这次实战之旅。
1. 环境搭建与数据初探
在开始写任何模型代码之前,确保你的工作环境已经就绪是第一步。我推荐使用Anaconda来管理Python环境,它能帮你轻松处理各种依赖包冲突。如果你还没有安装,去Anaconda官网下载对应版本,安装过程基本是“下一步”到底。
创建一个专门用于本项目的环境是个好习惯:
conda create -n keras_demo python=3.8
conda activate keras_demo
接下来安装核心库。我们将使用TensorFlow 2.x作为后端,因为它现在与Keras深度集成:
pip install tensorflow==2.8.0
pip install pandas numpy matplotlib scikit-learn
注意:TensorFlow版本不必完全一致,2.5以上版本通常都能良好运行。如果遇到安装问题,可以尝试使用清华或阿里云的镜像源加速下载。
环境准备好后,让我们先看看今天要用的数据。波士顿房价数据集是机器学习领域的“Hello World”,它包含了20世纪70年代波士顿郊区506个区域的住房信息,每个区域有13个特征指标,比如犯罪率、房间数量、税率等,目标值是这些区域住房的中位数价格。
# 导入必要的库
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from tensorflow import keras
# 设置中文字体(如果图表需要显示中文)
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
# 加载数据
from tensorflow.keras.datasets import boston_housing
(train_data, train_targets), (test_data, test_targets) = boston_housing.load_data()
print(f"训练数据形状: {train_data.shape}")
print(f"训练标签形状: {train_targets.shape}")
print(f"测试数据形状: {test_data.shape}")
print(f"测试标签形状: {test_targets.shape}")
运行这段代码,你会看到输出显示训练集有404个样本,测试集102个样本,每个样本有13个特征。房价标签的单位是千美元,所以数值20代表20,000美元。
数据加载后,我习惯先快速浏览一下特征的含义和分布:
# 特征名称(根据官方文档)
feature_names = [
'CRIM', # 城镇人均犯罪率
'ZN', # 住宅用地超过25,000平方英尺的比例
'INDUS', # 城镇非零售业务用地比例
'CHAS', # 查尔斯河虚拟变量(临河=1)
'NOX', # 氮氧化物浓度(千万分之一)
'RM', # 每栋住宅的平均房间数
'AGE', # 1940年以前建造的自住单位比例
'DIS', # 到波士顿五个就业中心的加权距离
'RAD', # 径向公路可达性指数
'TAX', # 每10,000美元的全额财产税税率
'PTRATIO', # 城镇师生比例
'B', # 1000*(Bk-0.63)^2,其中Bk是城镇黑人比例
'LSTAT' # 人口中低收入阶层百分比
]
# 创建DataFrame以便查看
train_df = pd.DataFrame(train_data, columns=feature_names)
train_df['MEDV'] = train_targets # 添加房价列
print("前5行数据预览:")
print(train_df.head())
print("\n基本统计信息:")
print(train_df.describe())
这个初步探索能让你对数据有个直观感受。比如,你会发现CRIM(犯罪率)的均值是3.6,但最大值高达88.9,说明数据中存在一些极端值。RM(房间数)的均值约6.28,大部分在5-7之间。这些观察会在后续的预处理中用到。
2. 数据预处理:为模型准备“食材”
原始数据很少能直接喂给神经网络。就像做菜前要洗菜切菜一样,我们需要对数据进行预处理。对于数值型回归问题,标准化是最关键的一步。为什么?因为不同特征的量纲差异巨大——犯罪率可能是0-100,而房间数只有3-9。如果不处理,模型会过分关注数值大的特征,忽视数值小但可能重要的特征。
标准化通常指将数据转换为均值为0、标准差为1的分布:
# 数据标准化
mean = train_data.mean(axis=0)
std = train_data.std(axis=0)
train_data = (train_data - mean) / std
test_data = (test_data - mean) / std
print("标准化后训练数据统计:")
print(f"均值: {train_data.mean(axis=0)[:3]}...") # 显示前三个特征
print(f"标准差: {train_data.std(axis=0)[:3]}...")
这里有个重要细节:我们只用训练集的均值和标准差来标准化测试集。为什么?因为在真实场景中,测试数据是“未来”的数据,我们不应该提前知道它的分布。用训练集计算的统计量来转换测试集,模拟了真实部署时的情况。
接下来看看目标值(房价)是否需要处理:
# 查看房价分布
plt.figure(figsize=(10, 4))
plt.subplot(1, 2, 1)
plt.hist(train_targets, bins=30, edgecolor='black', alpha=0.7)
plt.xlabel('房价(千美元)')
plt.ylabel('频数')
plt.title('房价分布直方图')
plt.subplot(1, 2, 2)
plt.scatter(range(len(train_targets)), sorted(train_targets))
plt.xlabel('样本索引(排序后)')
plt.ylabel('房价')
plt.title('房价排序散点图')
plt.tight_layout()
plt.show()
房价的分布大致正常,范围在5到50之间。对于回归问题,如果目标值范围很大,有时也会进行标准化或对数变换。但这里范围适中,我们可以暂时保持原样。
数据预处理的另一个考虑是特征工程。对于深度学习,特别是全连接网络,简单的特征工程有时也能带来提升。我们可以尝试创建一些交互特征:
# 简单的特征工程示例:创建房间数与低收入人口比例的交互特征
# 直觉:房间数多且低收入人口少的区域可能房价更高
train_data_engineered = np.zeros((train_data.shape[0], train_data.shape[1] + 1))
train_data_engineered[:, :-1] = train_data
# RM是第5个特征(索引5),LSTAT是第12个(索引12)
train_data_engineered[:, -1] = train_data[:, 5] / (train_data[:, 12] + 1e-6) # 避免除零
test_data_engineered = np.zeros((test_data.shape[0], test_data.shape[1] + 1))
test_data_engineered[:, :-1] = test_data
test_data_engineered[:, -1] = test_data[:, 5] / (test_data[:, 12] + 1e-6)
print(f"工程化后特征维度: {train_data_engineered.shape[1]}")
这个新特征反映了“每个房间对应的低收入人口比例倒数”,理论上与房价正相关。我们将在后续的建模中对比使用原始特征和工程化特征的效果。
3. 构建神经网络模型
现在来到最核心的部分——构建模型。对于波士顿房价这样的结构化数据,全连接神经网络(Dense Network)是最直接的选择。我们的模型不会太复杂,毕竟数据量只有几百条,复杂模型很容易过拟合。
让我先展示一个基础版本,然后逐步添加优化技巧:
from tensorflow.keras import models
from tensorflow.keras import layers
def build_basic_model(input_shape):
"""构建基础的三层神经网络"""
model = models.Sequential([
layers.Dense(64, activation='relu', input_shape=(input_shape,)),
layers.Dense(32, activation='relu'),
layers.Dense(1) # 输出层,线性激活,因为我们是回归问题
])
return model
# 使用原始特征
basic_model = build_basic_model(train_data.shape[1])
basic_model.summary()
运行summary()方法,你会看到模型的架构详情:
Model: "sequential"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
dense (Dense) (None, 64) 896
dense_1 (Dense) (None, 32) 2080
dense_2 (Dense) (None, 1) 33
=================================================================
Total params: 3,009
Trainable params: 3,009
Non-trainable params: 0
_________________________________________________________________
这个简单的模型已经有3009个参数需要学习。参数数量是怎么算出来的?第一层:13个输入特征 × 64个神经元 + 64个偏置 = 896;第二层:64 × 32 + 32 = 2080;第三层:32 × 1 + 1 = 33。
但基础模型往往不是最优的。在实践中,我通常会考虑以下几个优化方向:
- 批标准化(Batch Normalization):加速训练,提高稳定性
- Dropout:防止过拟合
- 正则化:约束权重,提高泛化能力
- 学习率调度:动态调整学习率
让我们构建一个更健壮的模型:
def build_enhanced_model(input_shape, dropout_rate=0.3, l2_weight=0.001):
"""构建增强版神经网络,包含多种优化技术"""
model = models.Sequential([
# 输入层
layers.Dense(128, activation='relu', input_shape=(input_shape,),
kernel_regularizer=keras.regularizers.l2(l2_weight)),
layers.BatchNormalization(),
layers.Dropout(dropout_rate),
# 隐藏层1
layers.Dense(64, activation='relu',
kernel_regularizer=keras.regularizers.l2(l2_weight)),
layers.BatchNormalization(),
layers.Dropout(dropout_rate),
# 隐藏层2
layers.Dense(32, activation='relu',
kernel_regularizer=keras.regularizers.l2(l2_weight)),
layers.BatchNormalization(),
layers.Dropout(dropout_rate * 0.5), # 最后一层dropout率低一些
# 输出层
layers.Dense(1)
])
return model
enhanced_model = build_enhanced_model(train_data_engineered.shape[1])
enhanced_model.summary()
这个模型看起来复杂多了,但每个组件都有其作用:
- 批标准化:在激活函数之前对每一层的输入进行标准化,使得网络对初始权重不那么敏感,允许使用更高的学习率。
- Dropout:在训练过程中随机"丢弃"一部分神经元,强制网络学习冗余表示,防止过拟合。
- L2正则化:在损失函数中添加权重平方和作为惩罚项,防止权重变得过大。
提示:Dropout率通常设置在0.2-0.5之间。太低的dropout效果不明显,太高的dropout可能导致欠拟合。对于小型网络,我一般从0.3开始尝试。
4. 模型训练与调优策略
模型构建好后,我们需要配置训练过程。这包括选择损失函数、优化器,以及设置回调函数。对于回归问题,均方误差(MSE)是最常用的损失函数:
from tensorflow.keras import optimizers
from tensorflow.keras.callbacks import EarlyStopping, ReduceLROnPlateau
# 编译模型
enhanced_model.compile(
optimizer=optimizers.Adam(learning_rate=0.001), # Adam优化器,学习率0.001
loss='mse', # 均方误差
metrics=['mae'] # 同时监控平均绝对误差,更易解释
)
# 设置回调函数
callbacks = [
EarlyStopping(
monitor='val_loss', # 监控验证集损失
patience=30, # 如果30个epoch验证损失没有改善,就停止训练
restore_best_weights=True # 恢复最佳权重
),
ReduceLROnPlateau(
monitor='val_loss',
factor=0.5, # 学习率减半
patience=10, # 10个epoch没有改善就降低学习率
min_lr=1e-6 # 最小学习率
)
]
这里我引入了两个重要的回调函数:
- EarlyStopping:防止过拟合的利器。当验证集损失连续多个epoch不再下降时,自动停止训练,并恢复最佳权重。
- ReduceLROnPlateau:动态调整学习率。当模型陷入局部最优时,降低学习率有助于找到更优解。
现在开始训练。我们将数据分为训练集和验证集(注意:测试集要留到最后评估):
# 训练模型
history = enhanced_model.fit(
train_data_engineered,
train_targets,
epochs=200, # 设置较大的epoch数,靠EarlyStopping提前停止
batch_size=16, # 小批量大小,适合小数据集
validation_split=0.2, # 20%训练数据作为验证集
callbacks=callbacks,
verbose=1 # 显示进度条
)
# 绘制训练历史
def plot_training_history(history):
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(12, 4))
# 绘制损失曲线
ax1.plot(history.history['loss'], label='训练损失')
ax1.plot(history.history['val_loss'], label='验证损失')
ax1.set_xlabel('Epoch')
ax1.set_ylabel('损失 (MSE)')
ax1.set_title('训练与验证损失')
ax1.legend()
ax1.grid(True, alpha=0.3)
# 绘制MAE曲线
ax2.plot(history.history['mae'], label='训练MAE')
ax2.plot(history.history['val_mae'], label='验证MAE')
ax2.set_xlabel('Epoch')
ax2.set_ylabel('平均绝对误差 (千美元)')
ax2.set_title('训练与验证MAE')
ax2.legend()
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
plot_training_history(history)
观察训练曲线,你能看到模型学习的过程。理想情况下,训练损失和验证损失都应该稳步下降,然后趋于平稳。如果出现训练损失持续下降但验证损失开始上升,那就是过拟合的迹象。
除了调整模型结构,超参数调优也是提升性能的关键。对于小型项目,手动调参结合交叉验证是可行的:
from sklearn.model_selection import KFold
import time
def kfold_cross_validation(data, targets, model_builder, n_splits=5, epochs=100):
"""执行K折交叉验证"""
kfold = KFold(n_splits=n_splits, shuffle=True, random_state=42)
fold_no = 1
cv_scores = []
training_times = []
for train_idx, val_idx in kfold.split(data, targets):
print(f'\n训练第 {fold_no} 折...')
# 分割数据
train_fold = data[train_idx]
val_fold = data[val_idx]
train_targets_fold = targets[train_idx]
val_targets_fold = targets[val_idx]
# 构建并训练模型
model = model_builder(data.shape[1])
model.compile(optimizer='adam', loss='mse', metrics=['mae'])
start_time = time.time()
history = model.fit(
train_fold, train_targets_fold,
epochs=epochs,
batch_size=16,
validation_data=(val_fold, val_targets_fold),
verbose=0
)
training_time = time.time() - start_time
training_times.append(training_time)
# 评估模型
scores = model.evaluate(val_fold, val_targets_fold, verbose=0)
cv_scores.append(scores[1]) # 取MAE作为评估指标
print(f'第 {fold_no} 折 MAE: {scores[1]:.2f}千美元 | 训练时间: {training_time:.1f}秒')
fold_no += 1
print(f'\n{"="*50}')
print(f'交叉验证结果:')
print(f'平均MAE: {np.mean(cv_scores):.2f} ± {np.std(cv_scores):.2f}千美元')
print(f'平均训练时间: {np.mean(training_times):.1f}秒')
return cv_scores
# 执行交叉验证
cv_results = kfold_cross_validation(
train_data_engineered,
train_targets,
build_enhanced_model,
n_splits=5
)
交叉验证能更可靠地评估模型性能,避免因单次数据划分带来的偶然性。通过对比不同模型架构或超参数的交叉验证结果,你可以做出更明智的选择。
5. 模型评估与结果分析
训练完成后,我们需要在独立的测试集上评估模型性能。这是检验模型泛化能力的最终关卡:
# 在测试集上评估
test_loss, test_mae = enhanced_model.evaluate(test_data_engineered, test_targets, verbose=0)
print(f"测试集 MSE: {test_loss:.2f}")
print(f"测试集 MAE: {test_mae:.2f}千美元")
# 生成预测值
predictions = enhanced_model.predict(test_data_engineered).flatten()
# 可视化预测结果
plt.figure(figsize=(10, 6))
# 实际值 vs 预测值散点图
plt.subplot(2, 2, 1)
plt.scatter(test_targets, predictions, alpha=0.6)
plt.plot([test_targets.min(), test_targets.max()],
[test_targets.min(), test_targets.max()],
'r--', lw=2, label='完美预测线')
plt.xlabel('实际房价(千美元)')
plt.ylabel('预测房价(千美元)')
plt.title('实际值 vs 预测值')
plt.legend()
plt.grid(True, alpha=0.3)
# 残差图
residuals = test_targets - predictions
plt.subplot(2, 2, 2)
plt.scatter(predictions, residuals, alpha=0.6)
plt.axhline(y=0, color='r', linestyle='--')
plt.xlabel('预测房价(千美元)')
plt.ylabel('残差(实际-预测)')
plt.title('残差分析')
plt.grid(True, alpha=0.3)
# 误差分布
plt.subplot(2, 2, 3)
plt.hist(residuals, bins=20, edgecolor='black', alpha=0.7)
plt.xlabel('预测误差(千美元)')
plt.ylabel('频数')
plt.title('误差分布')
plt.grid(True, alpha=0.3)
# 排序对比
plt.subplot(2, 2, 4)
sorted_idx = np.argsort(test_targets)
plt.plot(test_targets[sorted_idx], 'o-', label='实际值', alpha=0.7)
plt.plot(predictions[sorted_idx], 's-', label='预测值', alpha=0.7)
plt.xlabel('样本索引(按实际值排序)')
plt.ylabel('房价(千美元)')
plt.title('实际值与预测值对比')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
这些图表能告诉你很多信息:
- 散点图:点越接近红色对角线,预测越准确。如果点呈喇叭形分布,说明模型在某些值范围内预测不准。
- 残差图:残差应该随机分布在0线周围,没有明显模式。如果有趋势,说明模型有系统性偏差。
- 误差分布:理想情况下应该接近正态分布,均值为0。
- 排序对比:直观展示模型在整个值域上的表现。
除了整体指标,我们还可以分析单个样本的预测情况:
# 分析预测误差最大的样本
errors = np.abs(test_targets - predictions)
worst_idx = np.argmax(errors)
best_idx = np.argmin(errors)
print("预测最差的样本分析:")
print(f"样本索引: {worst_idx}")
print(f"实际房价: {test_targets[worst_idx]:.1f}千美元")
print(f"预测房价: {predictions[worst_idx]:.1f}千美元")
print(f"绝对误差: {errors[worst_idx]:.1f}千美元")
print(f"特征值:")
for i, (name, value) in enumerate(zip(feature_names, test_data[worst_idx])):
# 注意:这里显示的是标准化前的原始值
original_value = value * std[i] + mean[i] if i < len(std) else value
print(f" {name}: {original_value:.2f}")
print(f"\n预测最好的样本绝对误差: {errors[best_idx]:.1f}千美元")
分析预测误差大的样本能帮助你理解模型的局限性。也许这些样本有异常的特征组合,或者数据本身有噪声。
6. 模型解释与特征重要性
深度学习模型常被批评为"黑箱",但我们可以用一些技术来理解模型的决策过程。SHAP(SHapley Additive exPlanations)是当前最流行的模型解释工具之一:
# 安装shap库(如果未安装)
# !pip install shap
import shap
# 创建解释器
# 使用训练数据的一个子集作为背景分布
background = train_data_engineered[np.random.choice(train_data_engineered.shape[0],
100, replace=False)]
# 由于DeepExplainer对TensorFlow 2.x的支持问题,我们使用KernelExplainer作为替代
# 这是一个简化版本,计算成本较低
explainer = shap.KernelExplainer(
model=lambda x: enhanced_model.predict(x).flatten(),
data=background[:50] # 使用更小的背景数据集加速计算
)
# 计算测试集前20个样本的SHAP值
shap_values = explainer.shap_values(test_data_engineered[:20], nsamples=100)
# 可视化单个样本的解释
sample_idx = 0
print(f"\n样本 {sample_idx} 的预测解释:")
print(f"实际房价: {test_targets[sample_idx]:.1f}千美元")
print(f"预测房价: {predictions[sample_idx]:.1f}千美元")
# 获取特征重要性(全局)
shap.summary_plot(shap_values, test_data_engineered[:20],
feature_names=feature_names + ['RM/LSTAT'])
# 单个样本的力导向图
shap.force_plot(
explainer.expected_value,
shap_values[sample_idx],
test_data_engineered[sample_idx],
feature_names=feature_names + ['RM/LSTAT'],
matplotlib=True
)
SHAP图能显示每个特征对最终预测的贡献程度。红色表示正向影响(推高房价),蓝色表示负向影响(拉低房价)。通过分析这些贡献,你可以:
- 验证模型的逻辑是否符合业务常识
- 发现重要的特征,用于后续的特征工程
- 向非技术人员解释模型的决策依据
除了SHAP,我们还可以通过更简单的方法分析特征重要性:
# 方法1:排列重要性
from sklearn.inspection import permutation_importance
# 由于permutation_importance需要sklearn估计器,我们包装一下Keras模型
class KerasRegressorWrapper:
def __init__(self, model):
self.model = model
def predict(self, X):
return self.model.predict(X).flatten()
wrapped_model = KerasRegressorWrapper(enhanced_model)
# 计算排列重要性(在小型数据集上)
perm_importance = permutation_importance(
wrapped_model,
test_data_engineered[:50], # 使用部分数据加速计算
test_targets[:50],
n_repeats=10,
random_state=42
)
# 整理结果
importance_df = pd.DataFrame({
'feature': feature_names + ['RM/LSTAT'],
'importance_mean': perm_importance.importances_mean,
'importance_std': perm_importance.importances_std
}).sort_values('importance_mean', ascending=False)
print("\n特征重要性排序(排列重要性):")
print(importance_df.head(10))
# 可视化
plt.figure(figsize=(10, 6))
plt.barh(range(10),
importance_df['importance_mean'].head(10)[::-1],
xerr=importance_df['importance_std'].head(10)[::-1])
plt.yticks(range(10), importance_df['feature'].head(10)[::-1])
plt.xlabel('特征重要性(MAE增加量)')
plt.title('Top 10 重要特征')
plt.grid(True, alpha=0.3, axis='x')
plt.tight_layout()
plt.show()
特征重要性分析不仅能帮助理解模型,还能指导特征工程。如果某些特征重要性很低,可以考虑移除它们以简化模型。如果发现某些特征组合很重要,可以尝试创建新的交互特征。
7. 模型部署与生产化考虑
一个在Jupyter Notebook里运行良好的模型,离实际应用还有一段距离。在实际项目中,我们需要考虑模型的部署、监控和维护。这里我分享几个关键点:
模型保存与加载:
# 保存整个模型(包括架构、权重和优化器状态)
enhanced_model.save('boston_housing_model.h5')
# 保存模型架构为JSON
model_json = enhanced_model.to_json()
with open('model_architecture.json', 'w') as json_file:
json_file.write(model_json)
# 仅保存权重
enhanced_model.save_weights('model_weights.h5')
# 加载模型
from tensorflow.keras.models import load_model
loaded_model = load_model('boston_housing_model.h5')
# 验证加载的模型
test_loss_loaded, test_mae_loaded = loaded_model.evaluate(
test_data_engineered, test_targets, verbose=0
)
print(f"加载模型测试MAE: {test_mae_loaded:.2f}千美元")
创建预测API:
在实际部署中,我们通常会将模型封装成API服务。这里是一个简单的Flask示例:
"""
# app.py - 简单的模型服务API
from flask import Flask, request, jsonify
import numpy as np
from tensorflow.keras.models import load_model
import joblib # 用于保存标准化参数
app = Flask(__name__)
# 加载模型和标准化参数
model = load_model('boston_housing_model.h5')
scaler_params = joblib.load('scaler_params.pkl') # 保存的mean和std
@app.route('/predict', methods=['POST'])
def predict():
data = request.json
# 提取特征并转换为数组
features = np.array([[
data['CRIM'], data['ZN'], data['INDUS'], data['CHAS'],
data['NOX'], data['RM'], data['AGE'], data['DIS'],
data['RAD'], data['TAX'], data['PTRATIO'], data['B'],
data['LSTAT']
]])
# 应用相同的标准化
features_scaled = (features - scaler_params['mean']) / scaler_params['std']
# 添加工程特征
rm_lstat_ratio = features_scaled[0, 5] / (features_scaled[0, 12] + 1e-6)
features_final = np.append(features_scaled, [[rm_lstat_ratio]], axis=1)
# 预测
prediction = model.predict(features_final)[0][0]
return jsonify({
'predicted_price': float(prediction * 1000), # 转换为美元
'status': 'success'
})
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)
"""
模型监控与更新:
生产环境中的模型需要持续监控。你需要跟踪:
- 预测性能衰减:随着时间推移,数据分布可能变化(概念漂移),模型性能会下降
- 预测延迟:API响应时间是否在可接受范围内
- 输入数据质量:是否有异常值或缺失值突然增多
我通常会在预测服务中添加简单的监控逻辑:
"""
# monitoring.py - 简单的模型监控
import numpy as np
import pandas as pd
from datetime import datetime, timedelta
class ModelMonitor:
def __init__(self, window_size=1000):
self.predictions = []
self.response_times = []
self.window_size = window_size
def log_prediction(self, features, prediction, response_time):
self.predictions.append({
'timestamp': datetime.now(),
'features': features,
'prediction': prediction,
'response_time': response_time
})
# 保持固定窗口大小
if len(self.predictions) > self.window_size:
self.predictions.pop(0)
def check_anomalies(self):
if len(self.predictions) < 100:
return None
recent_preds = [p['prediction'] for p in self.predictions[-100:]]
avg_pred = np.mean(recent_preds)
std_pred = np.std(recent_preds)
# 检查最近预测是否显著偏离历史均值
latest_pred = self.predictions[-1]['prediction']
z_score = abs(latest_pred - avg_pred) / std_pred if std_pred > 0 else 0
anomalies = []
if z_score > 3: # 3个标准差以外
anomalies.append(f"异常预测值: {latest_pred:.2f} (z-score: {z_score:.2f})")
# 检查响应时间
recent_times = [p['response_time'] for p in self.predictions[-100:]]
if np.mean(recent_times) > 1.0: # 平均响应时间超过1秒
anomalies.append(f"高延迟: {np.mean(recent_times):.2f}秒")
return anomalies if anomalies else None
"""
模型版本管理:
当模型需要更新时,要有完整的版本管理流程:
"""
# model_registry.py - 简单的模型注册表
import json
import hashlib
from datetime import datetime
class ModelRegistry:
def __init__(self, registry_file='model_registry.json'):
self.registry_file = registry_file
self.registry = self.load_registry()
def load_registry(self):
try:
with open(self.registry_file, 'r') as f:
return json.load(f)
except FileNotFoundError:
return {'models': [], 'current_version': None}
def register_model(self, model_path, test_mae, features_used, notes=''):
# 计算模型文件的哈希值作为唯一标识
with open(model_path, 'rb') as f:
model_hash = hashlib.md5(f.read()).hexdigest()
model_info = {
'version': f"v{len(self.registry['models']) + 1}.0",
'model_hash': model_hash,
'test_mae': test_mae,
'features': features_used,
'registration_date': datetime.now().isoformat(),
'model_path': model_path,
'notes': notes,
'is_active': False
}
self.registry['models'].append(model_info)
self.save_registry()
return model_info['version']
def activate_model(self, version):
for model in self.registry['models']:
model['is_active'] = (model['version'] == version)
self.registry['current_version'] = version
self.save_registry()
def save_registry(self):
with open(self.registry_file, 'w') as f:
json.dump(self.registry, f, indent=2)
def get_active_model(self):
for model in self.registry['models']:
if model['is_active']:
return model
return None
"""
这些代码示例展示了如何将一个实验性的模型转化为可维护、可监控的生产系统。在实际项目中,你可能还需要考虑A/B测试、灰度发布、回滚机制等更复杂的部署策略。
8. 进阶优化与实验设计
如果你对这个基础模型的表现还不满意,或者想进一步探索深度学习的可能性,这里有几个进阶方向:
1. 尝试不同的网络架构:
def build_residual_block(x, units, dropout_rate=0.3):
"""构建残差块"""
shortcut = x
# 主路径
x = layers.Dense(units, activation='relu')(x)
x = layers.BatchNormalization()(x)
x = layers.Dropout(dropout_rate)(x)
x = layers.Dense(units, activation='relu')(x)
x = layers.BatchNormalization()(x)
# 如果维度不匹配,调整shortcut
if shortcut.shape[-1] != units:
shortcut = layers.Dense(units)(shortcut)
# 残差连接
x = layers.Add()([x, shortcut])
x = layers.Activation('relu')(x)
return x
def build_residual_network(input_shape):
"""构建残差网络"""
inputs = keras.Input(shape=(input_shape,))
# 初始层
x = layers.Dense(64, activation='relu')(inputs)
x = layers.BatchNormalization()(x)
# 残差块
x = build_residual_block(x, 64)
x = build_residual_block(x, 64)
x = build_residual_block(x, 32)
x = build_residual_block(x, 32)
# 输出层
outputs = layers.Dense(1)(x)
model = keras.Model(inputs=inputs, outputs=outputs)
return model
# 构建并编译残差网络
residual_model = build_residual_network(train_data_engineered.shape[1])
residual_model.compile(
optimizer=optimizers.Adam(learning_rate=0.0005),
loss='mse',
metrics=['mae']
)
residual_model.summary()
2. 集成学习:
单个模型可能有过拟合的风险,集成多个模型能提高鲁棒性:
from sklearn.ensemble import VotingRegressor
from tensorflow.keras.wrappers.scikit_learn import KerasRegressor
# 创建多个不同架构的模型
def create_model_1():
model = models.Sequential([
layers.Dense(32, activation='relu', input_shape=(train_data_engineered.shape[1],)),
layers.Dense(16, activation='relu'),
layers.Dense(1)
])
model.compile(optimizer='adam', loss='mse')
return model
def create_model_2():
model = models.Sequential([
layers.Dense(64, activation='relu', input_shape=(train_data_engineered.shape[1],)),
layers.Dropout(0.2),
layers.Dense(32, activation='relu'),
layers.Dropout(0.2),
layers.Dense(1)
])
model.compile(optimizer='adam', loss='mse')
return model
def create_model_3():
model = models.Sequential([
layers.Dense(128, activation='relu', input_shape=(train_data_engineered.shape[1],)),
layers.BatchNormalization(),
layers.Dense(64, activation='relu'),
layers.BatchNormalization(),
layers.Dense(1)
])
model.compile(optimizer='adam', loss='mse')
return model
# 创建集成模型
ensemble_models = [
('model_1', KerasRegressor(build_fn=create_model_1, epochs=100, verbose=0)),
('model_2', KerasRegressor(build_fn=create_model_2, epochs=100, verbose=0)),
('model_3', KerasRegressor(build_fn=create_model_3, epochs=100, verbose=0)),
]
# 注意:这里需要将Keras模型包装成sklearn兼容的格式
# 实际使用时可能需要调整参数
3. 超参数自动调优:
手动调参效率低,可以尝试自动化方法:
import keras_tuner as kt
def build_model_hp(hp):
"""定义可调超参数的模型构建函数"""
model = models.Sequential()
# 可调的超参数
hp_units1 = hp.Int('units1', min_value=16, max_value=128, step=16)
hp_units2 = hp.Int('units2', min_value=8, max_value=64, step=8)
hp_dropout = hp.Float('dropout', min_value=0.1, max_value=0.5, step=0.1)
hp_learning_rate = hp.Choice('learning_rate', values=[1e-2, 1e-3, 1e-4])
# 构建模型
model.add(layers.Dense(units=hp_units1, activation='relu',
input_shape=(train_data_engineered.shape[1],)))
model.add(layers.Dropout(hp_dropout))
model.add(layers.Dense(units=hp_units2, activation='relu'))
model.add(layers.Dropout(hp_dropout))
model.add(layers.Dense(1))
# 编译模型
model.compile(
optimizer=optimizers.Adam(learning_rate=hp_learning_rate),
loss='mse',
metrics=['mae']
)
return model
# 创建调优器
tuner = kt.RandomSearch(
build_model_hp,
objective='val_mae',
max_trials=20,
executions_per_trial=2,
directory='keras_tuner',
project_name='boston_housing'
)
# 搜索最佳超参数
tuner.search(
train_data_engineered, train_targets,
epochs=50,
validation_split=0.2,
verbose=0
)
# 获取最佳模型
best_model = tuner.get_best_models(num_models=1)[0]
best_hps = tuner.get_best_hyperparameters(num_trials=1)[0]
print(f"最佳超参数:")
print(f" 第一层神经元数: {best_hps.get('units1')}")
print(f" 第二层神经元数: {best_hps.get('units2')}")
print(f" Dropout率: {best_hps.get('dropout')}")
print(f" 学习率: {best_hps.get('learning_rate')}")
4. 实验跟踪与管理:
当尝试多种方法时,系统化地跟踪实验很重要:
import mlflow
import mlflow.keras
# 设置MLflow跟踪
mlflow.set_tracking_uri("file:./mlruns") # 本地存储
mlflow.set_experiment("boston_housing_experiments")
def run_experiment(model_name, model_builder, params):
"""运行单个实验并记录结果"""
with mlflow.start_run(run_name=model_name):
# 记录参数
mlflow.log_params(params)
# 构建和训练模型
model = model_builder(**params)
history = model.fit(
train_data_engineered, train_targets,
validation_split=0.2,
epochs=100,
verbose=0
)
# 评估模型
test_loss, test_mae = model.evaluate(
test_data_engineered, test_targets, verbose=0
)
# 记录指标
mlflow.log_metric("test_mae", test_mae)
mlflow.log_metric("test_loss", test_loss)
# 记录模型
mlflow.keras.log_model(model, "model")
# 记录训练历史图表
fig = plot_training_history(history)
mlflow.log_figure(fig, f"training_history_{model_name}.png")
return test_mae
# 运行不同配置的实验
experiments = [
{
'name': 'baseline_2layer',
'builder': build_basic_model,
'params': {'input_shape': train_data_engineered.shape[1]}
},
{
'name': 'enhanced_3layer',
'builder': build_enhanced_model,
'params': {
'input_shape': train_data_engineered.shape[1],
'dropout_rate': 0.3,
'l2_weight': 0.001
}
}
]
results = {}
for exp in experiments:
mae = run_experiment(exp['name'], exp['builder'], exp['params'])
results[exp['name']] = mae
print(f"{exp['name']}: 测试MAE = {mae:.2f}千美元")
通过这些进阶实验,你不仅能找到性能更好的模型,还能更深入地理解不同技术选择对结果的影响。记住,模型优化是一个迭代过程,需要结合业务理解、数据分析和实验验证。
走到这里,你已经完成了一个完整的深度学习项目——从数据加载、预处理,到模型构建、训练、评估,再到解释和部署。波士顿房价预测虽然是个经典的小数据集问题,但它涵盖了深度学习应用的完整流程。在实际工作中,你面对的数据可能更复杂、更混乱,需要更多的清洗和特征工程,模型可能需要更复杂的架构,但核心思路是相通的。
我自己的经验是,第一个能跑通的完整项目最有价值。它给了你继续探索的信心和基础。接下来,你可以用同样的流程去尝试其他数据集,比如预测股票价格、用户购买行为,或者图像分类、文本情感分析。每个新项目都会带来新的挑战,也会让你对深度学习的理解更深一层。
最后分享一个小技巧:保存好这个项目的所有代码和笔记。未来当你遇到新问题时,回头看看这个项目,很多解决方案的灵感就藏在那些你曾经写过的代码里。深度学习不是魔法,而是 craftsmanship——需要不断练习、试错和积累。现在,去创建你的第二个、第三个项目吧,每一次实践都会让你离掌握这项技术更近一步。
更多推荐


所有评论(0)