《Python AI入门》第6章 特征工程与模型调优——从60分到90分的秘密
本章,我们将从“写代码”进阶到“构建系统”。我们将引入 Pipeline(管道) 技术,搭建一条自动化的机器学习流水线,并学习如何通过“暴力搜索”找到模型的最优参数。
章节导语
“数据和特征决定了机器学习的上限,而模型和算法只是在逼近这个上限。” —— 机器学习界名言
在前两章,我们学会了如何把数据丢给模型,不管是预测房价(回归)还是预测生存(分类),流程似乎都很顺畅。但你可能会发现,无论怎么换模型,准确率卡在70%~80%就死活上不去了。
是模型不够先进吗?通常不是。原因往往在于“喂”给模型的数据不够好。
-
特征工程(Feature Engineering):就像烹饪前的备菜。顶级的食材(特征)只需要简单的烹饪(模型)就能做出美味;而糟糕的食材,再好的厨师也救不了。
-
模型调优(Model Tuning):就像调节火候。你需要找到那个完美的参数组合,让模型发挥出最大潜力。
本章,我们将从“写代码”进阶到“构建系统”。我们将引入 Pipeline(管道) 技术,搭建一条自动化的机器学习流水线,并学习如何通过“暴力搜索”找到模型的最优参数。
6.1 学习目标
在学完本章后,你将能够:
-
数据整形:理解为什么需要归一化(Normalization)和标准化(Standardization),解决“身高1.8米”和“工资20000元”量级不同导致的权重偏差。
-
工程化封装:掌握 Scikit-learn Pipeline,将数据预处理和模型训练打包成一个整体,彻底杜绝“数据泄露”。
-
处理混合数据:学会使用
ColumnTransformer,同时处理表格中的数值列和文本列。 -
自动调参:使用 GridSearchCV(网格搜索) 自动寻找随机森林的最佳超参数。
-
实战落地:构建一个能够预测二手车价格的复杂模型系统。
6.2 为什么数据需要“整容”?
6.2.1 量纲的陷阱
想象你要预测一个人的信用评分。你有两个特征:
-
年龄:范围 20 ~ 60。
-
年薪:范围 50,000 ~ 500,000。
如果你直接用线性回归或KNN算法,模型会认为“年薪”比“年龄”重要10000倍,仅仅因为年薪的数字更大。这显然是不合理的。我们需要把它们拉到同一个起跑线上。
6.2.2 归一化 vs 标准化
这是面试中最常问的两个概念,在工程应用中各有优劣:
-
归一化 (Min-Max Scaling):
-
把数据强行压缩到
[0, 1]之间。 -
公式:
-
适用场景:图像处理(像素本身就是0-255),或者你不希望有负数。
-
-
标准化 (Z-Score Standardization):
-
把数据变成均值为0,标准差为1的分布(符合正态分布)。
-
公式:
-
适用场景:绝大多数机器学习算法(如SVM、逻辑回归、神经网络)的首选。因为它对异常值(Outliers)不那么敏感。
-
代码演示:
from sklearn.preprocessing import MinMaxScaler, StandardScaler
import numpy as np
data = np.array([[25, 50000], [40, 120000], [30, 80000]])
# 1. 归一化
scaler_minmax = MinMaxScaler()
print("归一化结果:\n", scaler_minmax.fit_transform(data))
# 2. 标准化 (推荐)
scaler_std = StandardScaler()
print("标准化结果:\n", scaler_std.fit_transform(data))
【小白避坑】严禁在测试集上Fit! 这是新手最容易犯的致命错误:分别对训练集和测试集做标准化。
错误做法:
scaler.fit(X_train)然后scaler.fit(X_test)。这样会导致两边的缩放标准不一样(比如训练集的最高分是100,测试集最高分是80,缩放后都变成了1,这就不对了)。正确做法:
scaler.fit(X_train)计算出训练集的均值和方差,然后用这些参数去transform(X_train)和transform(X_test)。 测试集必须完全模拟“未来数据”,不能让模型提前偷看它的统计信息。
6.3 工程化思维:Pipeline(管道)
如果在做项目时,你需要先填补缺失值,再做标准化,再做独热编码,最后训练模型。对训练集做一遍,对测试集做一遍,对未来新数据再做一遍……这太容易出错了。
Scikit-learn 提供了 Pipeline,它把这一连串步骤封装成一个对象。你只需要调用一次 fit,它就会自动按顺序执行所有步骤。
“就像工厂流水线:原材料进去,成品出来。”
6.4 实战案例:二手车价格预测系统(进阶版)
在这个案例中,我们将挑战一个真实的复杂场景:数据中既有数字(年份、公里数),又有类别(品牌、燃料类型)。
我们需要构建一个“分流处理”的流水线:
-
数值特征
缺失值填充
标准化。
-
类别特征
缺失值填充
独热编码 (One-Hot)。
-
合并特征
输入模型 (随机森林)。
6.4.1 第一步:生成模拟数据
为了保证代码可运行,我们先生成一份包含混合特征的二手车数据。
import pandas as pd
import numpy as np
# 模拟 1000 条二手车数据
np.random.seed(42)
n_samples = 1000
data = {
'Brand': np.random.choice(['Toyota', 'Honda', 'BMW', 'Ford', 'Tesla'], n_samples),
'Age': np.random.randint(1, 20, n_samples), # 车龄
'Mileage': np.random.randint(5000, 200000, n_samples), # 里程
'Fuel_Type': np.random.choice(['Petrol', 'Diesel', 'Electric', np.nan], n_samples), # 包含缺失值
'Owner_Count': np.random.choice([1, 2, 3], n_samples)
}
df = pd.DataFrame(data)
# 构造真实价格 (价格 = 基础价 - 折旧 + 品牌溢价 + 随机波动)
# 这是一个非线性关系,适合用随机森林
base_price = 30000
brand_premium = {'Toyota': 0, 'Honda': 1000, 'Ford': -2000, 'BMW': 15000, 'Tesla': 20000}
fuel_premium = {'Petrol': 0, 'Diesel': 1000, 'Electric': 5000, np.nan: 0}
prices = []
for i in range(n_samples):
brand = df.loc[i, 'Brand']
fuel = df.loc[i, 'Fuel_Type']
age = df.loc[i, 'Age']
mileage = df.loc[i, 'Mileage']
# 简单的定价公式
p = base_price + brand_premium[brand] + fuel_premium.get(fuel, 0)
p = p * (0.9 ** age) # 每年折旧10%
p = p - (mileage * 0.05) # 每公里贬值0.05元
p += np.random.normal(0, 2000) # 加上噪音
prices.append(max(1000, p)) # 价格不能低于1000
df['Price'] = prices
print("--- 数据预览 ---")
print(df.head())
print(df.info())
6.4.2 第二步:切分数据集
from sklearn.model_selection import train_test_split
X = df.drop('Price', axis=1)
y = df['Price']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
6.4.3 第三步:构建预处理流水线 (ColumnTransformer)
这是本章最核心的代码段。我们需要分别定义对“数字”和“文本”的处理逻辑。
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler, OneHotEncoder
# 1. 定义数值型特征的处理步骤
# 逻辑:先用平均值填补空缺 -> 再做标准化
numeric_features = ['Age', 'Mileage', 'Owner_Count']
numeric_transformer = Pipeline(steps=[
('imputer', SimpleImputer(strategy='mean')),
('scaler', StandardScaler())
])
# 2. 定义类别型特征的处理步骤
# 逻辑:先用"missing"字符串填补空缺 -> 再做One-Hot编码
categorical_features = ['Brand', 'Fuel_Type']
categorical_transformer = Pipeline(steps=[
('imputer', SimpleImputer(strategy='constant', fill_value='missing')),
('onehot', OneHotEncoder(handle_unknown='ignore')) # 遇到新类别忽略,防止报错
])
# 3. 组合成一个大处理器
preprocessor = ColumnTransformer(
transformers=[
('num', numeric_transformer, numeric_features),
('cat', categorical_transformer, categorical_features)
])
print("预处理流水线构建完成!")
6.4.4 第四步:连接模型与训练
我们将使用 随机森林(Random Forest)。它是一种集成算法,由很多棵决策树组成,效果通常比线性回归好得多。
from sklearn.ensemble import RandomForestRegressor
# 构建最终的完整管道:预处理 -> 模型
full_pipeline = Pipeline(steps=[
('preprocessor', preprocessor),
('regressor', RandomForestRegressor(random_state=42))
])
# 训练 (直接调用 fit,Pipeline 会自动帮我们处理数据)
full_pipeline.fit(X_train, y_train)
# 评估
score = full_pipeline.score(X_test, y_test)
print(f"默认参数下的 R2 分数: {score:.4f}")
你可能会得到一个 0.90 左右的分数,已经很不错了。但我们能做得更好吗?
6.5 模型调优:GridSearch 网格搜索
随机森林有很多超参数(Hyperparameters),比如:
-
n_estimators:种多少棵树?(树越多越准,但越慢) -
max_depth:树最深长到多少层?(太深会过拟合,太浅欠拟合)
我们不可能手动一个个去试。GridSearchCV 可以帮我们把所有组合都跑一遍,找出最强的那一组。
from sklearn.model_selection import GridSearchCV
# 1. 定义我们要尝试的参数组合
# 注意:参数名前面要加 'regressor__',因为它是 pipeline 里的步骤名
param_grid = {
'regressor__n_estimators': [50, 100, 200],
'regressor__max_depth': [None, 10, 20],
'regressor__min_samples_split': [2, 5]
}
# 2. 建立搜索任务
# cv=5 表示使用 5折交叉验证 (K-Fold),让评估更稳健
grid_search = GridSearchCV(full_pipeline, param_grid, cv=5, scoring='r2', n_jobs=-1)
print("开始炼丹(自动调参中,请稍候)...")
grid_search.fit(X_train, y_train)
# 3. 输出结果
print(f"最佳参数组合: {grid_search.best_params_}")
print(f"最佳模型得分: {grid_search.best_score_:.4f}")
# 4. 拿到最优模型
best_model = grid_search.best_estimator_
【专业提示】什么是 K-Fold 交叉验证? 如果我们只切分一次训练集/测试集,结果可能有运气成分。 GridSearch 默认使用 K-Fold (K折):它把训练数据切成 K 份(比如5份)。 轮流拿其中1份做验证,剩下4份做训练,跑5次取平均分。 这就像为了测试一个学生的真实水平,让他做5套不同的卷子取平均分,而不是只看一次考试成绩。
6.6 章节小结
本章通过一个二手车价格预测的案例,让你从“会写代码”升级到了“会做工程”。
-
标准化:让不同量级的特征平等对话。
-
ColumnTransformer:解决了数值与文本混合处理的难题。
-
Pipeline:我们的“防呆设计”,确保了预处理步骤在训练和预测时严格一致,防止了数据泄露。
-
GridSearch:用算力换智力,自动寻找模型的最优解。
现在的你,已经掌握了传统机器学习(Machine Learning)最标准、最扎实的“数据处理 + 模型训练 + 调优”全套连招。
在接下来的篇章中,我们将离开表格数据的舒适区,进入更加神奇的领域——深度学习。我们将通过PyTorch,去教计算机像人眼一样“看”图片,像人脑一样“读”文字。
6.7 思考与扩展练习
-
更高效的搜索: GridSearch 会尝试所有组合,如果参数很多,计算会非常慢。请查阅
RandomizedSearchCV的文档。它是如何通过随机抽样来加速调参过程的?试着将代码中的 GridSearch 替换为 RandomizedSearch。 -
数据泄露思考: 为什么我们要在
imputer(缺失值填充)里使用mean(平均值)?如果我们在填补测试集的缺失值时,使用了测试集自己的平均值,这算不算数据泄露? (答案:算!Pipeline 能够避免这个问题,因为它会记录训练集的平均值,并用这个固定的数去填充测试集。) -
特征筛选: 并不是特征越多越好。有些特征可能是噪音。尝试在 Pipeline 中加入
SelectKBest,让模型自动挑选最有效的前 3 个特征进行训练,看看效果是否会下降?
更多推荐




所有评论(0)