☰
深度学习中的数据预处理实战指南:缺失值、缩放与编码全解析
2026/10/14 5:00:00 网站建设 项目流程

我当年第一次跑深度学习模型,把原始数据直接塞进模型,想着“反正框架会自动处理”,结果损失函数直接变成 NaN,找了一天错误,最后发现是特征里有几列缺失值没处理,加上一个特征的量级是几万,另一个是零点几,梯度直接炸了。从那以后我就明白:模型调参固然重要,但数据预处理做得不到位,后面全是白费功夫。

这篇东西就当是我的第三课笔记,讲的就是深度学习里的数据预处理。不管你是刚入门的学生,还是已经跑过几个项目想系统梳理一下的开发者,都应该能从中拿走点能直接用的东西。我会把常见的处理手段、背后的原理、实际操作的坑,以及我自己用下来的经验都写出来,尽量不说废话。

1. 为什么数据预处理在深度学习里这么关键

很多人觉得深度学习是“端到端”的,给了数据就能出结果,所以预处理不重要。这个想法不能说全错,但忽略了一个事实:模型虽然能自己学特征,但它对输入数据的格式、量纲、分布是非常敏感的。

1.1 模型训练的本质是对数值做运算

神经网络的每一层,本质上都是对输入做加权求和、激活函数变换、再传给下一层。整个过程里没有一个环节能理解“性别”这种文本,也没有一个环节能天然处理“NaN”这种缺失值。模型能做的只是数学运算,你喂给它的必须是干净、统一的数值。

我用一个生活类比解释一下:你把一堆混着沙子、石子、铁钉的原料直接扔进精密机床,机床再高级也会卡刀、崩刃。数据预处理就是清洗、分类、规整这些原料的过程,让机床能稳定运转。深度学习框架再强大,也替代不了这一步。

1.2 数据质量直接决定模型上限

有一句话说得很到位:Garbage in, garbage out。模型的上限是由数据决定的,模型结构只是逼近这个上限的方式。如果数据里充满缺失值、异常值、量纲不一致的噪声,那模型再复杂,学到的也是数据里的垃圾模式。

我之前用某公司的用户行为数据做过一个预测项目,原始数据里有一条特征“页面停留时长”,理论上应该是大于0的数值,但数据里有大量负值,还有的是0。如果不处理,模型就会学到“停留时长为负的用户转化率最高”这种完全荒谬的规律。这就是预处理的必要性:不是锦上添花,而是基础工程。

1.3 深度学习框架的数据接口给预处理留下的空间

现在主流的深度学习框架,比如 PyTorch、TensorFlow,都提供了 Dataset、DataLoader 这类数据接口。这些接口的设计初衷之一,就是让你在数据进入模型之前有一道“加工流水线”。很多人跳过这个环节直接喂原始数据,其实等于浪费了框架设计者特意留给你的处理空间。

我在实际项目里的经验是:框架层面的数据流水线不只是为了效率,更是为了让你把预处理逻辑集中管理。否则每个人预处理代码写一摊,后面维护起来非常痛苦。这个后面在实操部分我会详细展开。

2. 常见的数据问题与处理方式总览

开始动手之前,先弄清楚我们到底要处理哪些类型的问题。我把日常项目里最常见的数据问题梳理成四类,这样后面每讲一个就能对号入座。

2.1 数据问题的四个大类

问题类型典型表现后果处理手段
缺失值某些字段为 None、NaN、空字符串模型无法计算,或学到错误模式删除、填充(均值/中位数/插值/模型预测)
异常值数值远超正常范围(如年龄200岁)拉偏梯度,导致训练不稳定截断、分箱、稳健缩放
量纲不一致有的特征范围是0-1,有的是0-10000大数值特征主导梯度更新归一化、标准化
非数值数据文本、类别、日期等模型无法直接计算编码、嵌入化、时间戳转换

这四类问题很少单独出现,真实数据往往是四者混杂。我的习惯是先做一遍全局的“数据体检”,写一段代码把所有特征的缺失率、分布、量纲情况打出来,再去决定每一项怎么处理。

2.2 “先看后处理”是一条铁律

千万不要拿到数据就开始写处理代码。我见过太多人(包括当年的我)一上来就 fillna(0),结果把整个分布都搞坏了。正确做法是先了解每列数据的类型、缺失比例、分布形态、取值范围,甚至看一眼业务含义,再决定怎么处理。

具体来说,我会对每个特征问三个问题:

  • 这个特征在业务上代表什么?
  • 这个特征缺失了代表什么?是“没有此事”还是“记录遗漏”?
  • 这个特征和我们要预测的目标之间是什么关系?

这三个问题想清楚,处理方法自然就定了。比如“收入”字段缺失可能意味着用户没填,也可能意味着该用户没有收入,这两种业务含义的处理方式完全不同。前者可以用全量均值填充,后者则应该填0。

2.3 规定一个统一的处理顺序

数据预处理涉及的操作很多,如果不固定顺序,后面排查问题会很麻烦。我自己固定了一套顺序,基本涵盖了九成以上的场景:

  1. 先做数据清洗:去掉重复行、处理明显的格式错误。
  2. 再处理缺失值:先决定行删除还是列删除,再对剩余缺失做填充。
  3. 处理异常值:用描述性统计或者 IQR 规则识别并处理。
  4. 特征编码:把非数值类型转换成数值。
  5. 缩放/归一化:让所有特征处于相近的量级。
  6. 数据集划分:按比例切分训练、验证、测试集。

这套顺序不是绝对的,但大体上是对的。很多操作之间有依赖关系,比如先填充缺失才能计算均值来做标准化,先编码才能做某些缩放(比如对有序类别做归一化),所以顺序很重要。

3. 缺失值处理:不一定非要用0填

缺失值可能是最常遇到的脏数据问题。它的处理方式远比想象中复杂,因为“缺失”本身可能包含信息,也可能纯粹是噪声。

3.1 先判断缺失的三种类型

从统计学的角度,缺失值可以被分成三类,处理方式完全不同:

  • 完全随机缺失:缺失与否和其他变量无关,纯粹是记录时的偶然。这种情况直接删除缺失的样本,或者用简单的均值填充,都不会引入太大偏差。
  • 随机缺失:缺失与否和其他观测变量有关。比如女性更倾向于不填收入,那么“收入缺失”这个事件就和“性别”相关。这种情况需要用其他特征来预测缺失值。
  • 非随机缺失:缺失与否和缺失值本身有关。比如高收入的人故意不填收入。这种情况最难处理,简单的填充方法会引入系统性偏差。

我们在实际工程里不会做这么细致的统计检验(成本太高),但至少要建立这个意识:直接填0有时候是错的。

3.2 四种实用的填充策略

在实际项目中,我常用的缺失值填充方法按优先级排序如下:

  1. 业务规则填充:如果缺失有明确的业务含义,比如“未贷款用户没有贷款金额”,直接填充一个代表“无”的值,通常填0。
  2. 统计量填充:对于数值型特征,用均值、中位数或众数填充。均值适合分布对称的数据,中位数适合有偏分布,众数适合类别特征。
  3. 插值法:对时间序列或者有序数据,用线性插值、样条插值填补缺失点。
  4. 模型预测填充:用其他特征作为输入,训练一个简单模型(KNN、随机森林、线性回归等)来预测缺失值。这个方法效果好,但代价是需要额外的训练开销。

我在项目里最常用的组合是:业务规则优先,然后是统计量填充,模型预测填充只在缺失比例较高且特征间关联性强时使用。补充一点,填完之后一定加一个“是否缺失”的指示特征(is_missing),因为缺失本身往往能提供信息。

3.3 什么时候直接删行或删列

填充虽然方便,但有时候删除反而是更好的选择:

  • 缺失比例低于5%,且样本量足够大:直接删除缺失样本,对整体分布影响微乎其微。
  • 某一列缺失比例超过70%:该列信息量太少,容易引入噪声,建议整列删除。
  • 某一行多个关键特征都缺失:该样本几乎没有可用信息,删除比填充更干净。

我用一个实际经验打个样:之前处理过一个电商用户数据集,其中“会员等级”这一列缺失了82%。我一开始坚持用众数填充,后来做特征重要性分析时发现这列几乎没贡献,最后直接删掉,模型效果反而微涨。这就是“劣质特征不如没有特征”。

注意:填充缺失值必须在训练集和测试集上分别操作,但填充所用的统计量(如均值)只能用训练集计算得出,避免测试集信息泄漏到训练过程中。

4. 特征缩放:让每个特征在同一个量级上说话

特征缩放是我认为整个预处理流程里最容易出效果、却也最容易被忽视的一步。很多初学者跑模型发现 loss 降不下去,或者训练过程震荡剧烈,十有八九是特征量纲的问题。

4.1 为什么量纲不一致会导致训练不稳定

神经网络的梯度更新公式里,每个参数的梯度大小和它对应的输入特征数值大小成正比。如果一个特征的范围是0-10000,另一个是0-1,那么前者的梯度贡献会远超后者,相当于后者完全被淹没了。

梯度下降朝着大数值特征的方向大步走,小数值特征几乎不动,模型就很难收敛到最优解。这就好比两个人合伙抬一根木头,一个人身高两米壮如牛,另一个人身高一米二瘦如柴,你想让木头保持水平移动几乎不可能。特征缩放就是先把两个人的力量调整到相近的水平。

4.2 标准化与归一化的区别和选择

最常见的两种缩放方式是标准化(Standardization)和归一化(Normalization)。

标准化也叫 Z-score 标准化,公式是:

[ x' = \frac{x - \mu}{\sigma} ]

处理后的特征均值是0,标准差是1,保留了原始分布的形态信息。它适合大多数梯度类模型,尤其是数据分布接近高斯分布的时候效果最好。

归一化有两种常见形式:

  • Min-Max 归一化:[ x' = \frac{x - x_{min}}{x_{max} - x_{min}} ],把数据映射到0到1之间。
  • MaxAbs 归一化:除以最大绝对值,映射到-1到1之间。

归一化适合数据有明确边界、分布比较均匀的情况,比如像素值0-255、概率值0-1。

我的经验是:对于深度学习模型,默认推荐标准化(Standardization)。因为归一化容易受异常值影响(最大值和最小值一变,整个映射就变了),而标准化对异常值相对稳健。如果你做的是图像处理,像素归一化到0-1是惯例,那另当别论。

4.3 对稀疏数据不要用标准化

有一类数据是稀疏的,比如用 one-hot 编码后的类别特征、或者文本的 TF-IDF 矩阵,里面绝大多数是0。这时候用标准化去缩放,会让稀疏结构变得稠密,白白浪费内存和计算资源,而且缩放后的0不再是0,破坏了稀疏性。

对于这类数据,我的做法是保持稀疏,或者只做行方向的 L2 归一化。有一种专门针对稀疏特征的缩放方式是只缩放非零元素,但实现起来稍显繁琐,项目里我用得不多,提一下供参考。

4.4 缩放必须在数据划分之后进行

这可能是特征缩放里最容易犯错的地方,我专门提出来强调:计算均值和标准差(或者最大值、最小值)只能用训练集的数据,然后把这个“标准”应用到验证集和测试集上。

拿标准化举例:你先在全部数据上算均值和标准差,然后切分数据集。这样做的问题在于,测试集的信息已经通过均值和标准差混进了训练过程,模型的评估结果就会偏乐观。这在学术上叫数据泄漏(data leakage),是论文审稿人最喜欢抓的问题之一。

正确流程是这样的:

  1. 先把数据切分成训练集、验证集、测试集。
  2. 在训练集上计算均值和标准差。
  3. 用训练集的均值和标准差,分别对训练集、验证集、测试集做标准化。

代码上可以用 sklearn 的 StandardScaler 或 PyTorch 的 transforms 来实现,核心逻辑是一致的。

5. 类别特征编码:把“文字”变成模型能懂的“数字”

现实数据里,类别特征几乎是无法避免的。用户所在地域、设备类型、商品类目、星期几,这些都是类别特征。模型只能做数值运算,所以必须把类别转换成数值。但这个“转换”远不像 map 一下那么简单,不同类别特征的性质决定了不同的编码方式。

5.1 类别特征的两大类:定类与定序

在动手编码之前,先区分两个概念:

  • 定类特征:类别之间没有大小顺序关系。比如颜色、城市、设备品牌。红色和蓝色没有“谁更大”的说法。
  • 定序特征:类别之间存在顺序关系。比如学历(小学<初中<高中<本科<研究生)、用户等级(普通<银卡<金卡<钻石)。

这两种特征的编码方式完全不同。定序特征可以映射成连续整数(本科=3,研究生=4),因为模型可以利用顺序信息;定类特征如果用整数编码(红色=1,蓝色=2,绿色=3),模型就会错误地认为绿色大于蓝色,而且差值有实际意义,这完全是误导。

5.2 定类特征的编码方案对比

编码方式做法优点缺点适用场景
One-Hot 独热编码每个类别一列,命中为1否则为0无顺序误导,简单直观类别多时维度爆炸类别数量少(如<20)
哈希编码用哈希函数映射到固定维度维度可控可能碰撞,丢失可解释性类别特别多,内存受限
目标编码用该类别对应目标变量的均值编码维度低,携带监督信息容易过拟合,需要平滑类别多且有强关联性
嵌入编码训练密集向量表示最灵活,能学到相似度需要训练成本,不可解释深度学习模型内部处理

拿实际例子说明:用户设备类型这个特征,一般只有 iOS、Android、Web 等少数几个类别,用 One-Hot 编码最直接。但如果你处理的是商品类目,几万个类目,One-Hot 马上就炸了——几万列又会引入极强的稀疏性。这时候可以先用哈希编码压缩维度,或者在模型里直接加嵌入层。

5.3 One-Hot 编码的两个实操注意点

使用 One-Hot 编码时,有两个坑是我踩过之后才真正理解的。

第一个坑:训练集和测试集可能类别不一致。训练集里出现了 A、B、C 三个类别,测试集里冒出一个 D,如果你用 pandas 的 get_dummies 分别处理两个数据集,列数就会不同,模型直接报错。解决办法是先合并两个数据集一起 get_dummies,再拆分;或者用带固定类别集合的编码器,比如 sklearn 的 OneHotEncoder(handle_unknown='ignore')。

第二个坑:线性模型里要丢弃一列。One-Hot 编码后如果不丢掉一列,特征矩阵会存在多重共线性(每一列可以由其他列线性表示)。对于树模型无所谓,但对于线性回归、逻辑回归这类模型,会导致参数估计不稳定。sklearn 的 OneHotEncoder 有个参数 drop='first' 可以处理。

5.4 目标编码的正确用法

想补充一下目标编码(Target Encoding),因为它用好了效果立竿见影,用坏了会直接过拟合。基本原理就是:用某个类别下目标变量的平均值,替代这个类别本身。比如“城市A的用户转化率是5%”,就把“城市A”这个类别编码成0.05。

它的问题是目标信息泄露——你在训练自己的特征时,直接用了目标变量的信息。解决办法是交叉验证式的目标编码:在每个折内,只用该折训练数据计算类别均值,应用到该折的验证数据上。

我在用户流失预测项目里用过目标编码处理“省份”字段,效果比 One-Hot 好很多,因为省份类别有30多个,One-Hot 太稀疏,目标编码则直接把流失率信息压缩进了特征。但不做交叉验证的话,模型在训练集上表现极好,测试集上崩得稀碎,这个教训我一直记着。

6. 实操演练:完整的数据预处理流程

讲了这么多理论,我们上手写代码。下面我用一个模拟的电商用户数据集,完整示范一遍数据预处理流程。这里的代码以 PyTorch 和 pandas 为主,毕竟这是目前深度学习项目里最主流的组合。

6.1 模拟数据与任务场景

为了演示,我先造一个简单的数据集。场景是:预测用户是否会在未来30天内复购(二分类任务)。原始数据包含以下字段:

  • user_id:用户ID(需丢弃)
  • age:年龄(可能存在缺失和异常)
  • total_spent:累计消费金额(量纲大)
  • visit_freq:月均访问次数(量纲小)
  • device_type:设备类型(定类特征)
  • member_level:会员等级(定序特征)
  • churned_last_month:上个月是否流失(缺失值)
  • label:是否复购(目标值)
import pandas as pd import numpy as np # 模拟数据 np.random.seed(42) n = 10000 data = { 'user_id': range(n), 'age': np.random.randint(18, 60, size=n).astype(float), 'total_spent': np.random.lognormal(mean=5, sigma=1.5, size=n), 'visit_freq': np.random.rand(n) * 20, 'device_type': np.random.choice(['iOS', 'Android', 'Web'], size=n), 'member_level': np.random.choice(['normal', 'silver', 'gold', 'diamond'], size=n), 'label': np.random.choice([0, 1], size=n, p=[0.7, 0.3]) } df = pd.DataFrame(data) # 人为制造脏数据 df.loc[df.sample(500, random_state=1).index, 'age'] = np.nan df.loc[df.sample(300, random_state=2).index, 'total_spent'] = np.nan df.loc[df.sample(200, random_state=3).index, 'age'] = np.random.randint(100, 200, size=200) df['device_type'] = df['device_type'].astype('object') print(df.info()) print(df.describe())

这段代码故意制造了三种脏数据:年龄缺失、消费金额缺失、年龄超过100的异常值。

6.2 数据清洗与缺失值处理

拿到这个数据后,按我前面的顺序来。第一步清洗:user_id 是纯标识符,对预测毫无帮助,直接丢弃。

df = df.drop('user_id', axis=1) # 处理年龄缺失 # 业务上可以认为年龄缺失是随机的,先用中位数填充 age_median = df['age'].median() df['age'].fillna(age_median, inplace=True) # 处理total_spent缺失 # 消费金额分布通常右偏,中位数填充比均值更稳健 spent_median = df['total_spent'].median() df['total_spent'].fillna(spent_median, inplace=True) # 标记缺失 df['age_missing'] = df['age'].isna().astype(int) df['spent_missing'] = df['total_spent'].isna().astype(int)

等一下,上面的代码有个顺序问题:我先填充了缺失,然后又用 isna 检查,这时候缺失已经被填充了,isna 永远是 False。正确的做法是先记录缺失标记,再填充。我修正一下:

# 记录缺失标记 df['age_missing'] = df['age'].isna().astype(int) df['spent_missing'] = df['total_spent'].isna().astype(int) # 填充缺失 df['age'].fillna(df['age'].median(), inplace=True) df['total_spent'].fillna(df['total_spent'].median(), inplace=True)

这个顺序问题很容易被忽略,但影响很大。如果先填充再标记,你的模型就完全不知道哪些样本本来是有缺失的,也就无法利用“缺失事件”本身的信息。

6.3 异常值截断

接下来处理年龄异常。年龄在100-200之间明显不合理,直接删除会损失样本,截断更合适。常用的截断方法是用分位数:低于5%分位数或高于95%分位数的值,被截断到边界值。

p_low = df['age'].quantile(0.05) p_high = df['age'].quantile(0.95) df['age'] = df['age'].clip(lower=p_low, upper=p_high)

这里用 5%-95% 分位数截断是经验做法,不是唯一标准。如果业务上有明确的合理范围(比如年龄必须在18-65之间),应该优先用业务边界。我在实际项目里更倾向于先用业务规则,业务规则无法覆盖时再用统计分位数。

6.4 类别特征编码

按照前面讲的:

  • device_type 是定类特征,用 One-Hot 编码。
  • member_level 是定序特征,映射成有序整数。
# 定序特征映射 level_map = {'normal': 0, 'silver': 1, 'gold': 2, 'diamond': 3} df['member_level'] = df['member_level'].map(level_map) # 定类特征 One-Hot df = pd.get_dummies(df, columns=['device_type'], drop_first=True)

注意代码里的 drop_first=True,这对应我前面说的“丢弃一列避免共线性”。device_type 本来有三个类别,one-hot 后只生成两列,写成“是否为iOS”、“是否为Android”,如果都是0就代表Web。

6.5 特征缩放与数据集划分

接下来是最关键的一步:先划分,后缩放。我用 sklearn 的 train_test_split 把数据切成训练集、验证集、测试集,然后只在训练集上拟合 Scaler。

from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler X = df.drop('label', axis=1) y = df['label'] # 先划分 X_train, X_temp, y_train, y_temp = train_test_split(X, y, test_size=0.3, random_state=42) X_val, X_test, y_val, y_test = train_test_split(X_temp, y_temp, test_size=0.5, random_state=42) # 后缩放 scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_val_scaled = scaler.transform(X_val) X_test_scaled = scaler.transform(X_test)

我自己在一次复盘时发现,很多项目的预处理代码是先缩放后切分,这在传统机器学习里勉强能忍(如果你不关心评估偏差),但在深度学习里是硬伤,因为训练过程中每次迭代都会用到测试集的信息,模型的泛化能力评估就失真了。

另外,那个 X_temp 是临时变量,先把数据切出30%的临时集,再在临时集里一半一半切出验证和测试。这样训练:验证:测试就是 70:15:15,比例是比较通行的做法。

6.6 在 PyTorch 中用 Dataset 封装整个流程

预处理的结果如果只是 numpy 数组,还得转成 PyTorch 的 Dataset 才能训练。更优雅的做法是,把前面的预处理封装成一个自定义 Dataset 类,让数据在每次迭代时自动被处理,逻辑清晰且不易出错。

import torch from torch.utils.data import Dataset, DataLoader class UserDataset(Dataset): def __init__(self, X, y): self.X = torch.tensor(X, dtype=torch.float32) self.y = torch.tensor(y, dtype=torch.float32) def __len__(self): return len(self.y) def __getitem__(self, idx): return self.X[idx], self.y[idx] train_dataset = UserDataset(X_train_scaled, y_train.values) val_dataset = UserDataset(X_val_scaled, y_val.values) test_dataset = UserDataset(X_test_scaled, y_test.values) train_loader = DataLoader(train_dataset, batch_size=256, shuffle=True) val_loader = DataLoader(val_dataset, batch_size=256, shuffle=False) test_loader = DataLoader(test_dataset, batch_size=256, shuffle=False)

这样写的核心好处是:数据加载逻辑和处理逻辑解耦。以后要加新的数据增强、特征变换,只需要在 Dataset 的__getitem__里加几行,不会影响训练主流程。

7. 数据增强:你这辈子不处理增强,模型容量再大也没用

数据增强(Data Augmentation)虽然是一种特殊的预处理,但它解决的问题和前面的缺失值、缩放完全不一样。前面是“数据有毛病,我们要修正”,数据增强是“数据没毛病,但不够多,我们要造”。

7.1 数据增强为什么有效

深度神经网络动辄几十万、几百万参数,而真实标注数据的获取成本非常高。如果数据量不足以支撑这么大的模型,模型就会过拟合。数据增强通过对原始样本做合理的变换,生成多个“看起来不同但语义一致”的样本,相当于免费扩充了数据集。

以图像任务为例:一张猫的照片,水平翻转一下还是一张猫的照片,但像素分布完全不同。模型在这个新样本上学习到的特征,就对“猫在图像左侧”这种情况更有鲁棒性。少样本场景下,数据增强往往是提升模型效果最直接的手段。

7.2 常用数据增强方法速查

领域常用增强方法实例
图像翻转、旋转、裁剪、颜色扰动、加噪声水平翻转、随机裁切256x256
文本同义词替换、回译、随机删除把“开心”替换为“高兴”
时间序列加噪声、缩放、时间扭曲、通道置换对信号加高斯噪声,幅度0.02
表格数据SMOTE 过采样、特征噪声用 KNN 合成少数类样本

7.3 做增强要克制:不是所有变换都合理

数据增强有个前提,就是变换后的样本必须保持语义不变。一张 “6” 的数字图片水平翻转后就变成 “9” 了,如果拿来训练数字识别模型,就是纯纯的负样本干扰。同样,在医学影像里做大幅旋转也可能破坏诊断信息。

我的经验是:数据增强强度要循序渐进,先从弱变换开始,观察验证集表现。如果增强后的数据让训练集 loss 和验证集 loss 一起升高,说明增强强度太大了,破坏了原有语义。

在实际项目里我用过一套配置,能看出点启发:

from torchvision import transforms train_transform = transforms.Compose([ transforms.RandomHorizontalFlip(p=0.5), transforms.RandomRotation(degrees=10), transforms.ColorJitter(brightness=0.2, contrast=0.2), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ])

注意最后一步 Normalize 用了 ImageNet 的均值和标准差,这是迁移学习里的惯例。如果你是从零训练自己的模型,就需要自己统计数据集的均值和标准差。

8. 常见问题与排查技巧实录

预处理的坑非常多,我把自己踩过的一些坑整理出来,希望能帮你少走弯路。

8.1 训练 loss 为 NaN 的排查

这是深度学习新手最常遇到的问题。我建议按以下顺序排查:

  1. 检查数据里有没有 NaN 或 Inf:在模型前向传播前打印一条数据检查一下,确认预处理已经把缺失值清干净。
  2. 检查特征量纲:如果特征范围差异过大,梯度可能溢出,尝试标准化。
  3. 检查学习率:学习率过大可能导致梯度爆炸,调小一个数量级再试。
  4. 检查标签取值:回归任务标签过大会让损失函数爆炸,尝试对标签做对数变换。

我在处理一个价格预测任务时,就发现标签“房价”的范围在几十万到几千万之间。模型一开始 loss 是 NaN,我查了三天才发现不是模型问题,而是没对标签做标准化。把标签做 Log1p 变换后,loss 立刻正常了。

8.2 训练集和测试集特征不一致的坑

这个问题在比赛和实际工程里都很常见。你的特征工程在训练集上做了一堆衍生特征,比如从日期提取星期几、从文本提取长度,但测试集的特征少了一列或者列的顺序不同,模型直接报错。

解决方案是统一特征列表。我习惯在特征工程结束后,固定一份特征列清单,保存成文件,后续无论是训练还是推理都严格按这份清单取列。不要用 pandas 的隐式列匹配,要显式地选择列。

# 保存特征列清单 with open('feature_columns.txt', 'w') as f: f.write('\n'.join(feature_cols))

8.3 幂等性问题:数据预处理代码必须能重复执行

这个坑更隐蔽。很多人的预处理代码是一次性的,在 notebook 里从上到下跑一遍,数据就被改干净了。但一旦你重新运行某个单元格,比如填充逻辑被再次执行,就可能出错。典型例子:你已经把 age 列的缺失值填成中位数了,第二次执行 fillna 就找不到缺失值,但你能保证你的代码逻辑考虑了这种情况吗?

我的习惯是:把预处理流程封装成函数,并且每一步都做“断言”。比如填充前断言还有缺失值,填充后断言没有缺失值。这样代码既是自动化的,也是可重复的。

def preprocess(df): # 断言:处理前存在缺失 assert df['age'].isna().sum() > 0 df['age'].fillna(df['age'].median(), inplace=True) # 断言:处理后没有缺失 assert df['age'].isna().sum() == 0 return df

8.4 数据泄漏导致的“假高分”

数据泄漏是预处理环节里最常见、最隐蔽的问题。除了前面提到的缩放时机,目标编码、缺失值填充的统计量、特征选择的阈值,这些只要是“从全部数据计算再应用到数据上”的步骤,都可能引入泄漏。

我在做某个竞赛时曾经在验证集上拿到非常好的成绩(AUC 0.98),上了测试集直接掉到0.75。后来排查发现是目标编码的均值计算用到了全部数据。修正成交叉验证式的编码之后,验证集和测试集的表现才趋于一致。

给你一个简单的自查方法:在预处理代码里,凡是出现np.mean(df[col])、df[col].median()、df[col].max()这类全量聚合操作,都要问一句:这个操作是在训练集内做的,还是在全量数据上做的?如果是后者,大概率存在泄漏。

9. 个人实操总结与经验技巧

最后分享一些我在实践中积累的经验,不成系统,但每一条都值一些调试时间。

第一点,不要在项目一开始就追求完美的预处理方案。先用最简单的处理方式搭起一个 baseline(基线模型),然后把作为基准记录好,再逐步改进预处理方案,对比效果。很多人一上来就做复杂的目标编码、多级插补、手工特征,结果 baseline 没建,后面所有改动都没有参照系,根本看不出每一步带来了多少收益。

第二点,尽可能把预处理代码版本化。我现在的做法是每个预处理操作写一个函数,并且用 CSV 记录每次实验用的预处理配置。模型效果差了,我能准确知道是哪一步处理的改动导致的。如果所有预处理都在一个巨大的 notebook 里没法追溯,出了效果波动你也不知道锅该谁背。

第三点,很多预处理技巧的效果是和数据量、模型结构强相关的。比如数据量非常小的时候,删缺失行比复杂插补更好;数据量足够大时,模型可能自己学会处理噪声特征,过度清洗反而损伤信息。所以不要迷信某一套固定方案,拿数据多试。

第四点,关于自动化。当你的项目从研究走向工程化落地,数据预处理就不再是几个人各写各的脚本,而是会成为数据流水线的一部分。这个时候,预处理逻辑必须是确定性的、可重复的、有监控的。我见过太多模型在训练时表现良好,一到线上预测就崩,原因就是训练和推理时走了两套不同的预处理代码。这点务必留意。

最后再透露一个细节:预处理做完之后,我总会做一个“抽样人工检查”的步骤。随机抽出几张处理后的样本,直接用肉眼看看数据变成什么样了。比如图像是不是被翻转得不成形了,表格数据的数值是不是符合预期范围。这一步花不了十分钟,但能拦住一大批低级错误。我职业生涯里最贵的教训,几乎都是从这些低级错误里学到的。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询