ResNet回归实战:从二维表格数据到端到端预测

1. 为什么用ResNet做表格数据回归?一个被忽略的宝藏思路

你可能已经习惯了用XGBoost、LightGBM或者简单的全连接神经网络来处理你的表格数据,去做那些销售额预测、用户评分估计或者材料性能预测的任务。我以前也是这么干的,直到有一次,我手头的一个工业传感器数据集让我栽了跟头。那个数据集有几百个特征,样本量也不算小,但特征之间存在着非常复杂的非线性交互和层级依赖关系。我用尽了各种传统树模型和浅层神经网络,模型的表现就是卡在一个瓶颈上不去,过拟合和欠拟合像跷跷板一样难以平衡。

当时我就在想,图像识别里那些动辄成百上千层、能处理极其复杂模式的深度卷积网络,能不能“跨界”来帮帮忙?毕竟,表格数据本质上也是一个二维矩阵(样本×特征),这和图像的高度×宽度在数据结构上是有相通之处的。这个想法让我把目光投向了ResNet,也就是残差网络。它最著名的就是解决了深度网络训练中的梯度消失和退化问题,让网络可以做得非常深。对于表格数据,我们虽然不需要成百上千层,但ResNet的核心思想——残差学习——却是个大宝贝。

简单来说,残差学习不让网络层直接去拟合一个复杂的底层映射H(x),而是去拟合残差F(x) = H(x) - x。你可以把它想象成学习一个“增量”或“修正值”。对于表格数据的回归问题,很多情况下特征和目标值之间并不是一个从零开始的复杂函数,而是在某个基线(比如特征本身的线性组合)上进行的非线性调整。让模型去学习这个“调整量”,往往比让它从头学习整个映射要容易、更稳定。这就是我决定把ResNet-18这个经典的图像网络架构,改造来对付二维表格数据回归任务的核心动机。实测下来,在一些特征交互复杂、存在潜在层次结构的表格数据上,这个思路的效果常常有惊喜。

2. 第一步:把表格数据“包装”成网络能吃的格式

直接丢一个Pandas的DataFrame给PyTorch,它是吃不消的。我们需要一个“翻译官”,把表格数据转换成张量(Tensor),并组织成批量的形式。这就是DatasetDataLoader的活儿。原始文章给出了一个很好的起点,但我想结合我踩过的坑,给你展开讲讲更健壮、更实用的写法。

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: 数据加载器以及拟合好的标准化器
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值