1. 为什么用ResNet做表格数据回归?一个被忽略的宝藏思路
你可能已经习惯了用XGBoost、LightGBM或者简单的全连接神经网络来处理你的表格数据,去做那些销售额预测、用户评分估计或者材料性能预测的任务。我以前也是这么干的,直到有一次,我手头的一个工业传感器数据集让我栽了跟头。那个数据集有几百个特征,样本量也不算小,但特征之间存在着非常复杂的非线性交互和层级依赖关系。我用尽了各种传统树模型和浅层神经网络,模型的表现就是卡在一个瓶颈上不去,过拟合和欠拟合像跷跷板一样难以平衡。
当时我就在想,图像识别里那些动辄成百上千层、能处理极其复杂模式的深度卷积网络,能不能“跨界”来帮帮忙?毕竟,表格数据本质上也是一个二维矩阵(样本×特征),这和图像的高度×宽度在数据结构上是有相通之处的。这个想法让我把目光投向了ResNet,也就是残差网络。它最著名的就是解决了深度网络训练中的梯度消失和退化问题,让网络可以做得非常深。对于表格数据,我们虽然不需要成百上千层,但ResNet的核心思想——残差学习——却是个大宝贝。
简单来说,残差学习不让网络层直接去拟合一个复杂的底层映射H(x),而是去拟合残差F(x) = H(x) - x。你可以把它想象成学习一个“增量”或“修正值”。对于表格数据的回归问题,很多情况下特征和目标值之间并不是一个从零开始的复杂函数,而是在某个基线(比如特征本身的线性组合)上进行的非线性调整。让模型去学习这个“调整量”,往往比让它从头学习整个映射要容易、更稳定。这就是我决定把ResNet-18这个经典的图像网络架构,改造来对付二维表格数据回归任务的核心动机。实测下来,在一些特征交互复杂、存在潜在层次结构的表格数据上,这个思路的效果常常有惊喜。
2. 第一步:把表格数据“包装”成网络能吃的格式
直接丢一个Pandas的DataFrame给PyTorch,它是吃不消的。我们需要一个“翻译官”,把表格数据转换成张量(Tensor),并组织成批量的形式。这就是Dataset和DataLoader的活儿。原始文章给出了一个很好的起点,但我想结合我踩过的坑,给你展开讲讲更健壮、更实用的写法。
2.1 设计一个“聪明”的Dataset类
原始代码的AnalysisDataset假设输入已经是分离好的X和y的DataFrame。但在实际项目里,数据来源五花八门,可能是NumPy数组,也可能是从数据库读出来的一整张表。我们需要更灵活的处理。
import torch
from torch.utils.data import Dataset, DataLoader
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
class TabularRegressionDataset(Dataset):
"""
一个更通用的表格回归数据集类。
能自动处理NumPy数组和DataFrame的输入,并允许指定特征列和目标列。
"""
def __init__(self, data, feature_columns=None, target_columns=None, transform=None):
"""
Args:
data: 输入数据,可以是pd.DataFrame或np.ndarray。
如果是np.ndarray,假设最后一列是目标值。
feature_columns: 列表,指定用作特征的列名或列索引。
target_columns: 列表,指定用作目标值的列名或列索引。
transform: 可选的转换函数(如标准化),应用于特征。
"""
super().__init__()
self.transform = transform
# 处理不同类型的数据输入
if isinstance(data, pd.DataFrame):
self.df = data.copy()
else: # 假设是NumPy数组
# 如果没有指定列,我们默认最后一列是目标值
if target_columns is None and feature_columns is None:
n_features = data.shape[1] - 1
feature_columns = list(range(n_features))
target_columns = [n_features]
# 为数组创建临时DataFrame以便统一处理
col_names = [f'f_{i}' for i in range(data.shape[1])]
self.df = pd.DataFrame(data, columns=col_names)
# 将数字索引转换为字符串列名,方便后续iloc
if feature_columns is not None and all(isinstance(i, int) for i in feature_columns):
feature_columns = [col_names[i] for i in feature_columns]
if target_columns is not None and all(isinstance(i, int) for i in target_columns):
target_columns = [col_names[i] for i in target_columns]
# 确定特征列和目标列
if feature_columns is None:
# 默认所有列除了目标列都是特征
self.feature_cols = [col for col in self.df.columns if col not in target_columns]
else:
self.feature_cols = feature_columns
if target_columns is None:
# 默认最后一列是目标
self.target_cols = [self.df.columns[-1]]
else:
self.target_cols = target_columns
# 确保列存在
assert all(col in self.df.columns for col in self.feature_cols), "某些特征列不存在!"
assert all(col in self.df.columns for col in self.target_cols), "某些目标列不存在!"
self.features = self.df[self.feature_cols].values.astype(np.float32)
self.targets = self.df[self.target_cols].values.astype(np.float32)
def __len__(self):
return len(self.df)
def __getitem__(self, idx):
x = self.features[idx]
y = self.targets[idx]
if self.transform:
x = self.transform(x)
# 关键步骤:重塑为卷积网络期望的4D形状 [batch, channels, height, width]
# 对于表格数据,我们把每个样本看作一个“图像”:通道数=1,高度=特征数量,宽度=1
# 这样,一个8维的特征向量就变成了形状为(1, 8, 1)的张量。
x_tensor = torch.from_numpy(x).float().view(1, -1, 1)
y_tensor = torch.from_numpy(y).float()
return x_tensor, y_tensor
这个类的好处是它很“宽容”。你丢给它一个纯NumPy数组,它会自动把最后一列当作目标值;你丢给它一个DataFrame,你可以用列名灵活指定哪些是特征,哪些要预测。里面的注释也解释了为什么要把特征向量重塑成(1, 特征数, 1)的形状,这是为了适配后续卷积层的输入要求。
2.2 高效的数据加载与预处理管道
有了Dataset,我们还需要用DataLoader来组织小批量数据、打乱顺序,并且最好能利用多进程加速。这里我分享一个包含标准化预处理的数据划分函数,这在回归任务中至关重要,因为特征的量纲可能差异巨大,会影响模型收敛。
def create_data_loaders(data, feature_cols=None, target_cols=None, test_size=0.2, batch_size=32, random_state=42):
"""
一站式创建训练和测试的DataLoader,并自动进行训练集上的标准化。
返回:
train_loader, test_loader, scaler: 数据加载器以及拟合好的标准化器


480

被折叠的 条评论
为什么被折叠?



