【实战复盘】从Kaggle房价预测看深度学习数据预处理与模型调优
1. 数据预处理:从原始数据到模型输入
Kaggle房价预测比赛的数据集通常包含大量异构特征,包括数值型、类别型甚至文本描述。处理这些数据时,我踩过的第一个坑就是直接套用标准预处理流程。实际上,不同类型特征需要差异化的处理方式。
对于数值特征,常见的操作包括缺失值填充和标准化。但这里有个细节容易被忽略:某些数值特征的分布可能极度偏斜。比如房屋面积、成交价格这类特征,直接标准化效果往往不佳。我的经验是先用对数变换(np.log1p)压缩数值范围,再进行标准化:
# 处理偏态分布的数值特征
large_scale_cols = ['Lot', 'Total interior livable area', 'Tax assessed value']
for col in large_scale_cols:
train_data[col] = np.log1p(train_data[col])
test_data[col] = np.log1p(test_data[col])
# 标准化处理
numeric_features = all_features.select_dtypes(include=['float64']).columns
all_features[numeric_features] = all_features[numeric_features].apply(
lambda x: (x - x.mean()) / x.std())
类别特征的处理更考验经验。最初我试图对所有类别特征进行One-Hot编码,结果导致特征维度爆炸(从19维膨胀到470维)。后来通过分析类别基数发现,像"Appliances included"这样的特征有上万个不同取值,直接编码显然不合理。最终解决方案是:
- 只保留出现频率高的类别(如前20个)
- 对低频类别统一归为"其他"类
- 对基数小的特征(如房屋类型)保留完整编码
# 智能类别特征处理示例
categorical_features = all_features.select_dtypes(include=['object']).columns
for feat in categorical_features:
# 统计类别出现频率
freq = all_features[feat].value_counts(normalize=True)
# 只保留占比>1%的类别
valid_categories = freq[freq > 0.01].index
all_features[feat] = all_features[feat].where(
all_features[feat].isin(valid_categories), 'Other')
# One-Hot编码
all_features = pd.get_dummies(all_features, dummy_na=True)
2. 模型架构设计与实现细节
在房价预测任务中,MLP(多层感知机)往往比复杂模型表现更好,这点可能出乎很多人意料。经过多次实验,我发现关键在于网络深度和宽度的平衡。太深的网络容易过拟合,而太宽的网络则难以训练。
这是我最终采用的网络结构:
class HousePriceMLP(nn.Module):
def __init__(self, in_features):
super().__init__()
self.layer1 = nn.Linear(in_features, 256)
self.bn1 = nn.BatchNorm1d(256)
self.layer2 = nn.Linear(256, 64)
self.bn2 = nn.BatchNorm1d(64)
self.out = nn.Linear(64, 1)
def forward(self, x):
x = F.relu(self.bn1(self.layer1(x)))
x = F.relu(self.bn2(self.layer2(x)))
return self.out(x)
几个关键设计点:
- 每层后都加入BatchNorm,这对稳定训练至关重要
- 使用ReLU激活函数而非LeakyReLU,实测效果更好
- 输出层不使用激活函数,直接回归预测值
在实现时有个容易忽略的细节:GPU显存管理。当特征维度很高时(如470维),即使batch_size=256也可能导致显存不足。我的解决方案是:
- 使用梯度累积:每4个mini-batch更新一次参数
- 混合精度训练:显著减少显存占用
# 混合精度训练示例
scaler = torch.cuda.amp.GradScaler()
for X, y in train_iter:
X, y = X.to(device), y.to(device)
with torch.cuda.amp.autocast():
outputs = net(X)
loss = criterion(outputs, y)
scaler.scale(loss).backward()
if (i+1) % 4 == 0:
scaler.step(optimizer)
scaler.update()
optimizer.zero_grad()
3. 训练过程中的调优技巧
学习率设置是模型训练中最关键的参数之一。在房价预测任务中,我发现学习率需要随着训练动态调整。固定学习率要么导致收敛缓慢,要么引发梯度爆炸。
我的调优策略是:
- 初始阶段使用较大学习率(如0.01)快速下降
- 中期切换为中等学习率(如0.001)精细调参
- 后期使用小学习率(如0.0001)稳定收敛
实现代码:
# 分层学习率调度器
optimizer = torch.optim.Adam([
{'params': net.layer1.parameters(), 'lr': 0.01},
{'params': net.layer2.parameters(), 'lr': 0.001},
{'params': net.out.parameters(), 'lr': 0.0001}
], weight_decay=0.05)
# 配合ReduceLROnPlateau
scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(
optimizer, mode='min', factor=0.5, patience=10)
另一个重要技巧是早停机制(Early Stopping)。在Kaggle比赛中,我观察到验证集RMSE会在约350轮后开始上升,这时继续训练只会导致过拟合。实现方法:
best_loss = float('inf')
patience = 20
counter = 0
for epoch in range(500):
train_loss = train_epoch(...)
val_loss = validate(...)
if val_loss < best_loss:
best_loss = val_loss
torch.save(net.state_dict(), 'best_model.pth')
counter = 0
else:
counter += 1
if counter >= patience:
print(f'Early stopping at epoch {epoch}')
break
4. 结果分析与模型集成
单个模型的表现往往有提升空间。在最终提交方案中,我采用了模型集成策略:
- 时间维度集成:保存训练过程中不同checkpoint的模型
- 空间维度集成:训练多个不同初始化的模型
- 多样性集成:结合MLP、LightGBM等不同模型
集成预测结果的代码示例:
# 加载多个模型
models = []
for ckpt in ['model_300.pth', 'model_350.pth', 'model_400.pth']:
model = HousePriceMLP(input_dim).to(device)
model.load_state_dict(torch.load(ckpt))
models.append(model)
# 生成集成预测
all_preds = []
for model in models:
with torch.no_grad():
pred = model(test_features).cpu().numpy()
all_preds.append(pred)
final_pred = np.mean(all_preds, axis=0)
在结果分析阶段,我发现模型在某些特定房源类型上表现不佳。通过错误分析,发现主要是因为这些房源的特征模式与主流差异较大。解决方案是:
- 对这些样本进行数据增强
- 在损失函数中增加样本权重
- 使用专门针对这些样本的辅助模型
最终我的方案在Kaggle私有排行榜上取得了前10%的成绩。虽然不如某些自动机器学习方案,但整个调优过程让我对深度学习在结构化数据上的应用有了更深理解。特别是在特征工程与模型架构的协同优化方面,积累了不少实战经验。
更多推荐
所有评论(0)