摘要:本文将评估指标、数据预处理、模型选择与调参、类别不平衡、工程化等"通用工具与基础"集中前置讲解,以避免在各算法章节中重复出现、打断学习节奏;但集中前置会导致知识脱离具体算法场景、读者只知"是什么"而不知"怎么用",因此本章为每一类知识单独设计一个简单可运行的演示模型(评估指标→逻辑回归/线性回归,数据预处理→逻辑回归,模型选择与调参→决策树/随机森林,类别不平衡→逻辑回归,工程化→Pipeline),用同一模型贯穿一类知识的讲解,使知识在真实场景中落地;演示模型仅用于展示"知识如何用",不要求理解模型内部实现,其原理将后续算法章节中正式讲解。

目录

二、机器学习工具与基础

2.1 评估指标

2.1.1 分类指标:accuracy /precision/recall / F1

2.1.2 回归指标:MSE / RMSE / MAE / R²

2.1.3 易错点:accuracy 在类别不平衡下失效

2.2 数据预处理

2.2.1 标准化 / 归一化

2.2.2 独热编码 / 标签编码

2.2.3 缺失值处理

2.2.4 数据泄漏与 train/test 划分

2.3 模型选择与调参

2.3.1 训练集 / 验证集 / 测试集

2.3.2 交叉验证 cross_val_score

2.3.3 网格搜索 GridSearchCV

2.3.4 基线模型 DummyClassifier / Regressor

2.3.5 学习曲线与验证曲线(了解)

2.4 类别不平衡

2.4.1 class_weight

2.4.2 过采样 / 欠采样

2.4.3 阈值调整与指标选择

2.5 工程化基础

2.5.1 Pipeline

2.5.2 模型保存与加载 joblib /pickle

2.5.3 随机种子与可复现

2.6 模型优化

2.6.1 优化视角:过拟合与欠拟合

2.6.2 正则化直觉:为什么能抑制过拟合

2.6.3 L1 / L2 正则化

2.6.4 正则化强度与调参

2.6.5 凸优化直觉(了解)

2.7 其它基础

2.7.1 向量化与稀疏矩阵(CountVectorizer/Tfidf)

2.7.2 距离与相似度(欧氏 / 余弦 / 曼哈顿)

2.7.3 概率基础:条件概率 / 贝叶斯 / 期望

2.7.4 集成学习


二、机器学习工具与基础

本章是算法章节的前置基础,为后续监督学习、无监督学习等算法章节提供通用工具和概念。

核心职责:

职责说明
评估指标提供衡量模型效果的标准(分类/回归)
数据预处理提供数据清洗、转换的标准方法
模型选择与调参提供选择模型、调参的工具
类别不平衡提供处理不平衡数据的方法
工程化基础提供 Pipeline、模型保存等工程能力
其它前置提供特征工程、距离度量、概率基础等铺垫

2.1 评估指标

2.1.1 分类指标:accuracy /precision/recall / F1

2.1.1.1 混淆矩阵
预测正预测负
实际正TP(真正例)FN(假负例)
实际负FP(假正例)TN(真负例)
2.1.1.2 四个核心指标
指标公式说明
准确率(Accuracy)A=(TP + TN) / (TP + TN + FP + FN)预测正确的比例
精确率(Precision)P=TP / (TP + FP)预测为正的样本中,实际为正的比例
召回率(Recall)R=TP / (TP + FN)实际为正的样本中,被正确预测的比例
F1 分数2 × P × R / (P + R)精确率和召回率的调和平均
# 分类指标演示:逻辑回归
​
import numpy as np
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import (accuracy_score, precision_score,
                             recall_score, f1_score, confusion_matrix,
                             classification_report)
​
# 生成模拟数据
X, y = make_classification(n_samples=500, n_features=10, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
​
# 训练模型
model = LogisticRegression(max_iter=1000)
model.fit(X_train, y_train)
​
# 预测
y_pred = model.predict(X_test)
​
# 混淆矩阵
print("混淆矩阵:")
print(confusion_matrix(y_test, y_pred))
​
# 四个指标
print(f"\nAccuracy:{accuracy_score(y_test, y_pred):.4f}")
print(f"Precision:{precision_score(y_test, y_pred):.4f}")
print(f"Recall:{recall_score(y_test, y_pred):.4f}")
print(f"F1:{f1_score(y_test, y_pred):.4f}")
​
# 完整报告
print(f"\n分类报告:")
print(classification_report(y_test, y_pred))
2.1.1.3 阈值选择

分类模型的预测通常可理解为输出样本属于正类的概率估计;阈值则设定了判正与判负的边界——概率不低于阈值判为正,低于阈值判为负。阈值的选择并非固定,而需结合业务代价、目标指标与数据分布综合确定。阈值影响精确率和召回率。

阈值与指标关系表

阈值精确率召回率F1适用场景
降低↓↑视情况宁可错杀(疾病检测)
提高↑↓视情况宁缺毋滥(垃圾邮件)
默认 0.5平衡平衡平衡通用

说明:对于判负(漏报)代价高的任务(如以成功检测疾病为正),降低阈值,宁可错杀;对于判正(误报)要求精准的任务,提高阈值

# 阈值选择:阈值对指标的影响
​
import numpy as np
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import precision_score, recall_score, f1_score
​
# 生成数据
X, y = make_classification(n_samples=1000, n_features=10, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
​
# 训练模型
model = LogisticRegression(max_iter=1000)
model.fit(X_train, y_train)
​
# 获取预测概率
y_prob = model.predict_proba(X_test)[:, 1]    # 正类的概率
​
# 不同阈值下的指标
print(f"{'阈值':<8}{'Precision':<12}{'Recall':<10}{'F1':<10}")
print("-" * 40)
​
for threshold in [0.3, 0.4, 0.5, 0.6, 0.7]:
    y_pred = (y_prob >= threshold).astype(int)
    precision = precision_score(y_test, y_pred)
    recall = recall_score(y_test, y_pred)
    f1 = f1_score(y_test, y_pred)
    print(f"{threshold:<8}{precision:<12.4f}{recall:<10.4f}{f1:<10.4f}")

2.1.2 回归指标:MSE / RMSE / MAE / R²

回归任务的评估指标主要有四个:MSE、RMSE、MAE、R²。它们从不同角度衡量预测值与真实值的差距。

公式说明

指标全称公式说明
MSE均方误差误差平方的平均,对大误差敏感
RMSE均方根误差MSE 的平方根,与原始数据同量纲
MAE平均绝对误差误差绝对值的平均,对异常值不敏感
R²决定系数模型解释的方差比例,越接近 1 越好

补充:

其中 ​ y_i为真实值,\hat{y}_i​ 为预测值,​ \bar{y}=\frac{1}{n}\sum_{i=1}^{n}y_i为均值;

SS_{\text{res}}=\sum_{i=1}^{n}(y_i-\hat{y}_i)^2,\qquad SS_{\text{tot}}=\sum_{i=1}^{n}(y_i-\bar{y})^2,\qquad R^2=1-\frac{SS_{\text{res}}}{SS_{\text{tot}}}

# 回归指标演示:线性回归
​
import numpy as np
from sklearn.datasets import make_regression
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score,root_mean_squared_error
​
# 生成模拟数据
X, y = make_regression(n_samples=500, n_features=10, noise=10, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
​
# 训练模型
model = LinearRegression()
model.fit(X_train, y_train)
​
# 预测
y_pred = model.predict(X_test)
​
# 回归指标
mse = mean_squared_error(y_test, y_pred)
rmse = root_mean_squared_error(y_test,y_pred)
mae = mean_absolute_error(y_test, y_pred)
r2 = r2_score(y_test, y_pred)
​
print(f"MSE:{mse:.4f}")
print(f"RMSE:{rmse:.4f}")
print(f"MAE:{mae:.4f}")
print(f"R²:{r2:.4f}")

2.1.3 易错点:accuracy 在类别不平衡下失效

# 错误:只看 accuracy
​
import numpy as np
from sklearn.metrics import accuracy_score
​
# 99% 负样本,1% 正样本
y_true = np.array([0] * 99 + [1] * 1)
y_pred = np.array([0] * 100)    # 全预测负
​
accuracy = accuracy_score(y_true, y_pred)
print(f"accuracy:{accuracy:.2%}")    # 99.00%

问题: 全预测负也有 99% 准确率,但模型毫无价值。

添加以下两行代码

from sklearn.metrics import classification_report
​
print(classification_report(y_true, y_pred))

说明:

  • 正类的 precision、recall、F1 都是 0

  • accuracy 高,但模型无效

  • 类别不平衡时,看 precision / recall / F1

2.2 数据预处理

2.2.1 标准化 / 归一化

标准化和归一化都是将特征缩放到统一范围,但方式不同。

为什么需要缩放:

问题说明
尺度不一致不同特征量纲不同(如年龄 0~100,收入 0~100000)
模型敏感梯度下降对尺度敏感,尺度大的特征主导更新
距离失效距离算法中,尺度大的特征主导距离计算

两种方法:

方法公式结果范围适用
标准化(Standardization)(x - μ) / σ均值 0,标准差 1大多数模型
归一化(Normalization)(x - min) / (max - min)[0, 1]神经网络、距离算法

标准化 / 归一化演示:

# 标准化 / 归一化
​
import numpy as np
from sklearn.preprocessing import StandardScaler, MinMaxScaler
​
# 原始数据
X = np.array([[1000000], [2000000], [3000000], [4000000], [5000000]])
​
# 标准化
scaler_std = StandardScaler()
X_std = scaler_std.fit_transform(X)
print(f"标准化:\n{X_std.flatten()}")
print(f"均值:{X_std.mean():.4f},标准差:{X_std.std():.4f}")
​
# 归一化
scaler_minmax = MinMaxScaler()
X_minmax = scaler_minmax.fit_transform(X)
print(f"\n归一化:\n{X_minmax.flatten()}")
print(f"最小值:{X_minmax.min():.4f},最大值:{X_minmax.max():.4f}")

方法说明:

方法说明
StandardScaler()标准化。fit_transform() 计算均值/标准差并转换
MinMaxScaler()归一化。fit_transform() 计算最小/最大值并转换

说明:

  • 标准化:均值 0,标准差 1

  • 归一化:缩放到 [0, 1]

  • 标准化更常用,对异常值不敏感

2.2.2 独热编码 / 标签编码

类别特征需要转为数值,常用独热编码和标签编码。

方法说明适用
独热编码(One-Hot)类别转 0/1 向量无序类别
标签编码(Label)类别转整数有序类别

为什么需要编码:

问题说明
模型只接受数值逻辑回归等模型只接受数值输入
类别不能直接比较"红"和"绿"没有大小关系
标签编码引入大小关系"红"=0,"绿"=1,模型误以为有大小关系

类别编码对逻辑回归的影响:

# 类别编码对逻辑回归的影响
​
import numpy as np
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.preprocessing import OneHotEncoder
​
# 构造含类别特征的数据
df = pd.DataFrame({
    "age": [25, 30, 28, 35, 22, 40],
    "city": ["杭州", "上海", "北京", "杭州", "上海", "北京"],
    "label": [0, 1, 0, 1, 0, 1]
})
​
# 独热编码
encoder = OneHotEncoder(sparse_output=False)
city_encoded = encoder.fit_transform(df[["city"]])
print(f"独热编码:\n{city_encoded}")
​
# 合并特征
X = np.hstack([df[["age"]].values, city_encoded])
y = df["label"].values
​
# 训练
model = LogisticRegression(max_iter=1000)
model.fit(X, y)
print(f"\nAccuracy:{model.score(X, y):.4f}")

方法说明:

方法说明
OneHotEncoder()独热编码。sparse_output=False 返回数组
LabelEncoder()标签编码。fit_transform() 转换类别为整数

说明:

  • 独热编码:无序类别,避免大小关系

  • 标签编码:有序类别,保留大小关系

2.2.3 缺失值处理

现实数据常有缺失值,需处理。

方法说明适用
删除删除含缺失值的行/列缺失少
填充用均值/中位数/众数填充缺失多
插值用前后值插值时间序列

为什么需要处理:

问题说明
模型无法处理缺失值大多数模型不接受 NaN
缺失影响训练缺失值导致样本减少或偏差
缺失模式缺失可能包含信息(如用户不愿填年龄)

缺失值处理:

# 缺失值处理
​
import numpy as np
import pandas as pd
from sklearn.impute import SimpleImputer
​
# 构造含缺失值的数据
df = pd.DataFrame({
    "age": [25, np.nan, 30, 28, np.nan],
    "score": [85, 92, np.nan, 88, 90]
})
​
print(f"原始数据:\n{df}")
print(f"\n缺失值统计:\n{df.isnull().sum()}")
​
# 删除
df_drop = df.dropna()
print(f"\n删除缺失行:\n{df_drop}")
​
# 均值填充
imputer = SimpleImputer(strategy="mean")
df_fill = pd.DataFrame(imputer.fit_transform(df), columns=df.columns)
print(f"\n均值填充:\n{df_fill}")

方法说明:

方法说明
df.dropna()删除含缺失值的行
SimpleImputer()填充缺失值。strategy 指定填充策略(mean/median/most_frequent)

说明:

  • 缺失少:删除

  • 缺失多:填充

  • 数值列:均值/中位数填充

  • 类别列:众数填充

2.2.4 数据泄漏与 train/test 划分

数据泄漏是指训练时用到了测试时不可用的信息,导致评估结果不可信(结果可能偏高也可能偏低)。

常见泄漏

泄漏类型说明示例
预处理泄漏预处理用了全部数据归一化时用了测试集
目标泄漏特征包含目标信息用"是否违约"预测"是否违约"
时间泄漏用了未来数据用未来数据预测过去

时间泄漏演示:

# 随机划分 vs 时间顺序划分
​
import numpy as np, pandas as pd
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.metrics import mean_absolute_error
​
# 构造带趋势+噪声的时间序列
n = 500
t = np.arange(n)
y = 0.1 * t + np.sin(t / 10) * 5 + np.random.normal(0, 2, n)
df = pd.DataFrame({"t": t, "lag1": np.roll(y, 1), "lag2": np.roll(y, 2), "y": y}).iloc[2:]
​
# 错误:随机打乱划分(未来样本进入训练集)
idx = np.random.RandomState(42).permutation(len(df))
tr, te = idx[:350], idx[350:]
m = GradientBoostingRegressor(random_state=42).fit(df.iloc[tr][["t","lag1","lag2"]], df.iloc[tr]["y"])
print("错误(随机划分) MAE:", mean_absolute_error(df.iloc[te]["y"], m.predict(df.iloc[te][["t","lag1","lag2"]])))
​
# 正确:按时间顺序划分(前 70% 训练,后 30% 测试)
split = int(len(df) * 0.7)
tr, te = df.iloc[:split], df.iloc[split:]
m = GradientBoostingRegressor(random_state=42).fit(tr[["t","lag1","lag2"]], tr["y"])
print("正确(时序划分) MAE:", mean_absolute_error(te["y"], m.predict(te[["t","lag1","lag2"]])))

2.3 模型选择与调参

2.3.1 训练集 / 验证集 / 测试集

数据需划分为三部分,各司其职。

数据集用途比例
训练集训练模型参数60%~80%
验证集调参、选择模型10%~20%
测试集最终评估10%~20%

为什么需要三集:

问题说明
只用训练集无法评估泛化能力
只用训练+测试反复用测试集调参 → 测试集过拟合
三集分工训练用训练集,调参用验证集,最终评估用测试集

训练集 / 验证集 / 测试集划分演示

# 训练集 / 验证集 / 测试集划分
​
import numpy as np
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score
​
# 生成数据
X, y = make_classification(n_samples=1000, n_features=10, random_state=42)
​
# 先划分训练集和临时集(70% / 30%)
X_train, X_temp, y_train, y_temp = train_test_split(X, y, test_size=0.3, random_state=42)
​
# 临时集再分为验证集和测试集(各 15%)
X_val, X_test, y_val, y_test = train_test_split(X_temp, y_temp, test_size=0.5, random_state=42)
​
print(f"训练集:{X_train.shape[0]} 条")
print(f"验证集:{X_val.shape[0]} 条")
print(f"测试集:{X_test.shape[0]} 条")
​
# 训练
model = DecisionTreeClassifier(random_state=42)
model.fit(X_train, y_train)
​
# 验证集评估
val_acc = accuracy_score(y_val, model.predict(X_val))
print(f"\n验证集准确率:{val_acc:.4f}")
​
# 测试集评估(只用一次)
test_acc = accuracy_score(y_test, model.predict(X_test))
print(f"测试集准确率:{test_acc:.4f}")

方法说明:

方法说明
train_test_split()划分数据集。test_size 指定比例,random_state 保证可复现

说明:

  • 先划分训练集和临时集

  • 临时集再分为验证集和测试集

  • 测试集只用一次

2.3.2 交叉验证 cross_val_score

交叉验证通过多次划分,充分利用数据,评估模型泛化能力。

为什么需要交叉验证:

问题说明
单次划分不稳定划分方式不同,结果不同
数据量少验证集太小,评估不可靠
充分利用数据每份数据都做验证

交叉验证:

# 交叉验证 cross_val_score
​
import numpy as np
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
​
# 生成数据
X, y = make_classification(n_samples=1000, n_features=10, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
​
# 决策树:5 折交叉验证
dt = DecisionTreeClassifier(random_state=42)
scores = cross_val_score(dt, X_train, y_train, cv=5)
print(f"决策树 5 折得分:{scores}")
print(f"平均:{scores.mean():.4f} ± {scores.std():.4f}")
​
# 随机森林:5 折交叉验证
rf = RandomForestClassifier(n_estimators=100, random_state=42)
scores_rf = cross_val_score(rf, X_train, y_train, cv=5)
print(f"\n随机森林 5 折得分:{scores_rf}")
print(f"平均:{scores_rf.mean():.4f} ± {scores_rf.std():.4f}")

方法说明:

方法说明
cross_val_score()交叉验证。cv 指定折数(计为n),返回每折得分,折数即将数据划分为多少块,每次使用n-1块训练,1块测试,共计n次

说明:

  • 随机森林比决策树更稳定(标准差更小)

  • 交叉验证评估更可靠

2.3.3 网格搜索 GridSearchCV

网格搜索自动尝试多组超参数,找到最优组合。

超参数:模型训练之前设置的参数,不通过训练获得,而是控制模型怎么学,学成什么样子。

为什么需要网格搜索:

问题说明
超参数多手动调参费时
组合爆炸多个参数组合多
自动化网格搜索自动尝试所有组合

网格搜索:

# 网格搜索 GridSearchCV
​
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split, GridSearchCV
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score
​
# 生成数据
X, y = make_classification(n_samples=1000, n_features=10, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
​
# 参数网格
param_grid = {
    "max_depth": [3, 5, 7, None],
    "min_samples_split": [2, 5, 10]
}
​
# 网格搜索
grid = GridSearchCV(
    DecisionTreeClassifier(random_state=42),
    param_grid,
    cv=5,
    scoring="accuracy"
)
grid.fit(X_train, y_train)
​
# 最优结果
print(f"最优参数:{grid.best_params_}")
print(f"最优交叉验证得分:{grid.best_score_:.4f}")
​
# 测试集评估
test_acc = accuracy_score(y_test, grid.predict(X_test))
print(f"测试集准确率:{test_acc:.4f}")

方法说明:

方法说明
GridSearchCV()网格搜索。param_grid 参数网格,cv 折数,scoring 评估指标
grid.best_params_最优参数
grid.best_score_最优交叉验证得分

说明:

  • 自动尝试所有参数组合

  • 用交叉验证评估每组

  • 选最优组合

2.3.4 基线模型 DummyClassifier / Regressor

基线模型是最简单的模型,用于判断复杂模型是否真的有效。

为什么需要基线模型:

问题说明
模型是否有效复杂模型是否比简单策略好
避免过度乐观高准确率可能只是数据不平衡
提供参照基线是改进的起点

基线模型

# 基线模型 DummyClassifier
​
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.dummy import DummyClassifier
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import accuracy_score
​
# 生成数据
X, y = make_classification(n_samples=1000, n_features=10, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
​
# 基线模型
dummy = DummyClassifier(strategy="most_frequent")
dummy.fit(X_train, y_train)
dummy_acc = accuracy_score(y_test, dummy.predict(X_test))
print(f"基线模型(最多类别):{dummy_acc:.4f}")
​
# 决策树
dt = DecisionTreeClassifier(random_state=42)
dt.fit(X_train, y_train)
dt_acc = accuracy_score(y_test, dt.predict(X_test))
print(f"决策树:{dt_acc:.4f}")
​
# 对比
print(f"\n决策树 vs 基线:{dt_acc - dummy_acc:+.4f}")

方法说明:

方法说明
DummyClassifier()基线分类器。strategy 指定策略(most_frequent/prior/uniform)

说明:

  • 基线模型提供参照

  • 复杂模型必须优于基线

  • 差距越大,模型越有效

2.3.5 学习曲线与验证曲线(了解)

学习曲线展示模型性能随数据量的变化,验证曲线展示模型性能随超参数的变化。

曲线横轴用途
学习曲线训练样本量判断欠拟合/过拟合、是否需要更多数据
验证曲线超参数选择合适参数、判断参数过强/过弱

2.4 类别不平衡

本节演示数据:不平衡分类数据

# 本节统一演示数据:不平衡分类数据
​
import numpy as np
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
​
# 生成不平衡数据(9:1)
X, y = make_classification(
    n_samples=1000, n_features=10,
    weights=[0.9, 0.1], random_state=42
)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
​
print(f"训练集:{X_train.shape[0]} 条")
print(f"训练集正负样本:{np.bincount(y_train)}")
print(f"测试集正负样本:{np.bincount(y_test)}")

本节的后续代码演示不再编写数据代码,后序代码基于此数据,将后序演示追加到此代码后即可

2.4.1 class_weight

class_weight 用于调整类别权重,让模型更关注少数类。

原理:

类别默认权重调整后权重
多数类11
少数类1自动计算(如 9)

class_weight 演示

# class_weight 演示
​
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score
​
# 默认模型
model_default = LogisticRegression(max_iter=1000)
model_default.fit(X_train, y_train)
y_pred_default = model_default.predict(X_test)
​
print("默认模型:")
print(f"  Accuracy:{accuracy_score(y_test, y_pred_default):.4f}")
print(f"  Precision:{precision_score(y_test, y_pred_default):.4f}")
print(f"  Recall:{recall_score(y_test, y_pred_default):.4f}")
print(f"  F1:{f1_score(y_test, y_pred_default):.4f}")
​
# 自动按类别比例提权(也可手动指定 {0: 1, 1: 5})
model_balanced = LogisticRegression(
    max_iter=1000,
    class_weight="balanced"
)
model_balanced.fit(X_train, y_train)
y_pred_balanced = model_balanced.predict(X_test)
​
print("\nclass_weight='balanced':")
print(f"  Accuracy:{accuracy_score(y_test, y_pred_balanced):.4f}")
print(f"  Precision:{precision_score(y_test, y_pred_balanced):.4f}")
print(f"  Recall:{recall_score(y_test, y_pred_balanced):.4f}")
print(f"  F1:{f1_score(y_test, y_pred_balanced):.4f}")

方法说明:

参数说明
class_weight类别权重。'balanced' 自动按类别比例调整权重

说明:

  • 默认模型:准确率高,但少数类召回率低

  • class_weight='balanced':召回率提升,但精确率下降

  • 类别不平衡时,看 F1 而非准确率

2.4.2 过采样 / 欠采样

重采样通过调整样本数量平衡类别。

方法说明优点缺点
过采样(Oversampling)增加少数类样本不丢失信息可能过拟合
欠采样(Undersampling)减少多数类样本减少计算可能丢失信息
SMOTE合成少数类样本不简单复制可能生成噪声

过采样

# 过采样
​
from imblearn.over_sampling import RandomOverSampler, SMOTE
​
# 随机过采样
ros = RandomOverSampler(random_state=42)
X_train_ros, y_train_ros = ros.fit_resample(X_train, y_train)
print(f"随机过采样后:{np.bincount(y_train_ros)}")
​
# SMOTE
smote = SMOTE(random_state=42)
X_train_smote, y_train_smote = smote.fit_resample(X_train, y_train)
print(f"SMOTE 后:{np.bincount(y_train_smote)}")

方法说明:

方法说明
RandomOverSampler()随机复制少数类样本
SMOTE()合成少数类样本(基于近邻插值)

欠采样

# 欠采样
​
from imblearn.under_sampling import RandomUnderSampler
​
# 随机欠采样
rus = RandomUnderSampler(random_state=42)
X_train_rus, y_train_rus = rus.fit_resample(X_train, y_train)
print(f"随机欠采样后:{np.bincount(y_train_rus)}")

对比重采样效果

# 对比重采样效果
​
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import f1_score
​
# 原始
model = LogisticRegression(max_iter=1000)
model.fit(X_train, y_train)
print(f"原始:F1={f1_score(y_test, model.predict(X_test)):.4f}")
​
# 过采样
model_ros = LogisticRegression(max_iter=1000)
model_ros.fit(X_train_ros, y_train_ros)
print(f"过采样:F1={f1_score(y_test, model_ros.predict(X_test)):.4f}")
​
# SMOTE
model_smote = LogisticRegression(max_iter=1000)
model_smote.fit(X_train_smote, y_train_smote)
print(f"SMOTE:F1={f1_score(y_test, model_smote.predict(X_test)):.4f}")
​
# 欠采样
model_rus = LogisticRegression(max_iter=1000)
model_rus.fit(X_train_rus, y_train_rus)
print(f"欠采样:F1={f1_score(y_test, model_rus.predict(X_test)):.4f}")

说明:此代码演示需要结合前面的过采样和欠采样数据处理

2.4.3 阈值调整与指标选择

阈值调整和指标选择是处理类别不平衡的补充手段。

方法说明
阈值调整降低阈值提高召回率
指标选择用 F1 / AUC 替代 accuracy

阈值调整

代码与 2.1.1.3 相同:用 predict_proba 取正类概率,再按业务代价选阈值。类别不平衡时,阈值通常低于 0.5,用精确率换召回率。

指标选择

# 指标选择
​
from sklearn.metrics import accuracy_score, f1_score, roc_auc_score
from sklearn.linear_model import LogisticRegression
​
model = LogisticRegression(max_iter=1000)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
y_prob = model.predict_proba(X_test)[:, 1]
​
print(f"Accuracy:{accuracy_score(y_test, y_pred):.4f}")      # 高但不反映少数类
print(f"F1:{f1_score(y_test, y_pred):.4f}")                  # 综合
print(f"AUC:{roc_auc_score(y_test, y_prob):.4f}")            # 不受阈值影响

说明:

  • Accuracy 高,但 F1 低 → 少数类被忽略

  • AUC 不受阈值影响,反映整体区分能力

  • 类别不平衡时,看 F1 或 AUC

2.5 工程化基础

本节演示模型:Pipeline + 逻辑回归

# 本节统一演示模型:Pipeline + 逻辑回归
​
import numpy as np
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
​
# 生成模拟数据
X, y = make_classification(n_samples=1000, n_features=10, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42)
​
print(f"训练集:{X_train.shape}")
print(f"测试集:{X_test.shape}")

说明: 后续每个小节都基于这个数据集,演示不同的工程化操作。

2.5.1 Pipeline

Pipeline 将预处理和模型串联为一个整体,避免数据泄漏,简化代码。

为什么需要 Pipeline:

问题说明
数据泄漏预处理时用了测试集数据(详见 2.2.4)
代码冗余每次训练都要手动标准化、训练模型
部署困难保存模型时,预处理步骤丢失
流程不清晰预处理和模型分离,容易出错

![img]

Pipeline 演示

# Pipeline 演示
​
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
​
# 创建 Pipeline
model = Pipeline(steps=[
    ("scaler", StandardScaler()),          # 步骤1:标准化
    ("classifier", LogisticRegression(max_iter=1000))  # 步骤2:逻辑回归
])
​
# 训练(自动标准化)
model.fit(X_train, y_train)
​
# 预测(自动标准化测试集)
y_pred = model.predict(X_test)
print(f"Pipeline 准确率:{accuracy_score(y_test, y_pred):.4f}")
​
# 查看 Pipeline 步骤
print(f"\nPipeline 步骤:{model.steps}")

方法说明:

方法说明
Pipeline()创建 Pipeline。steps 为步骤列表,每步为 (名称, 对象) 元组
model.fit()训练,依次执行每个步骤的 fit_transform(最后一步只 fit)
model.predict()预测,依次执行每个步骤的 transform(最后一步 predict)

说明:

  • Pipeline 自动管理预处理

  • 训练时:标准化 fit_transform,模型 fit

  • 预测时:标准化 transform,模型 predict

2.5.2 模型保存与加载 joblib /pickle

模型保存是将训练好的模型序列化到磁盘,模型加载是从磁盘反序列化。

为什么需要保存模型:

问题说明
训练耗时训练一次耗时,避免重复训练
部署需要生产环境加载模型
版本管理保存不同版本的模型
实验复现保存实验结果

joblib方式模型

# 模型保存与加载
​
import joblib
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
​
# 训练 Pipeline
model = Pipeline(steps=[
    ("scaler", StandardScaler()),
    ("classifier", LogisticRegression(max_iter=1000))
])
model.fit(X_train, y_train)
​
# 保存模型
joblib.dump(model, "model.pkl")
print("已保存 model.pkl")
​
# 加载模型
loaded_model = joblib.load("model.pkl")
print("已加载 model.pkl")
​
# 用加载的模型预测
y_pred = loaded_model.predict(X_test)
print(f"加载后准确率:{accuracy_score(y_test, y_pred):.4f}")

方法说明:

方法说明
joblib.dump()保存模型到文件。参数为模型和文件名
joblib.load()从文件加载模型。参数为文件名

说明: 保存的 Pipeline 包含预处理和模型,加载后可直接预测。

pickle 方式

import pickle
​
# 复用上面训练好的 model
with open("model_pickle.pkl", "wb") as f:
    pickle.dump(model, f)
​
with open("model_pickle.pkl", "rb") as f:
    loaded_model_pickle = pickle.load(f)
​
print(f"加载后准确率:{accuracy_score(y_test, loaded_model_pickle.predict(X_test)):.4f}")

说明: pickle 是 Python 原生方式;joblib 对含大 NumPy 数组的模型更快,且保存的 Pipeline 包含所有步骤。

2.5.3 随机种子与可复现

随机种子保证实验结果可复现,是实验的基础。

为什么需要随机种子:

问题说明
结果不确定不同运行结果不同
实验对比无法对比不同实验
调参复现无法复现最优参数
调试困难问题无法复现

常见随机来源

来源说明
数据划分train_test_split
模型初始化随机初始化参数
采样随机采样、SMOTE
打乱shuffle=True

随机种子与可复现

# 随机种子与可复现
​
import numpy as np
import random
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score
​
# 设置随机种子
SEED = 42
np.random.seed(SEED)
random.seed(SEED)
​
# 复用 2.5 的 X / y,重新划分,验证同一 random_state 结果一致
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=SEED
)
​
model = LogisticRegression(max_iter=1000, random_state=SEED)
model.fit(X_train, y_train)
print(f"准确率:{accuracy_score(y_test, model.predict(X_test)):.4f}")

常见问题

# 错误:只设置部分种子
​
import numpy as np
from sklearn.datasets import make_classification
​
# 只设置 NumPy 种子
np.random.seed(42)
​
# 没设置 random_state
X, y = make_classification(n_samples=100, random_state=None)    # 不可复现
​
# 正确:设置所有随机参数
X, y = make_classification(n_samples=100, random_state=42)      # 可复现

2.6 模型优化

2.6.1 优化视角:过拟合与欠拟合

过拟合和欠拟合是模型优化的核心问题(概念详见 1.1.3.1)。本节从优化视角重新审视:如何通过手段抑制过拟合、缓解欠拟合。

问题表现优化方向
过拟合训练好,测试差正则化、早停、数据增强、简化模型
欠拟合训练差,测试差增加复杂度、增加特征、减少正则化

说明: 优化的核心是找到模型复杂度的平衡点。

2.6.2 正则化直觉:为什么能抑制过拟合

核心描述: 正则化是在损失函数中加入惩罚项,限制模型参数的大小,从而抑制过拟合。

为什么能抑制过拟合

现象说明
过拟合时模型参数往往很大,对噪声敏感
正则化后参数被限制,模型更平滑
效果泛化能力提升,过拟合减少

2.6.3 L1 / L2 正则化

2.6.3.1 L2(岭)

L2 正则化在损失函数中加入参数平方和,让参数变小但不为 0。公式如下:

说明:为原始损失函数

特点

特点说明
参数平滑参数变小,但不为 0
防止过拟合限制参数大小
无特征选择所有特征都保留
凸优化有闭式解
2.6.3.2 L1(Lasso)

L1 正则化在损失函数中加入参数绝对值之和,让部分参数变为 0,实现特征选择。公式如下:

特点

特点说明
稀疏解部分参数为 0
特征选择自动选择有效特征
防止过拟合限制参数大小
非凸优化无闭式解,需迭代
2.6.3.3 弹性网络(了解)

弹性网络(Elastic Net) 结合 L1 和 L2 正则化。

特点

特点说明
结合 L1 和 L2稀疏 + 平滑
适用特征相关性强时
了解即可入门阶段不深入
2.6.3.4 L1 vs L2 对比
维度L1(Lasso)L2(Ridge)
正则项
解的特点稀疏(部分为 0)平滑(都不为 0)
特征选择自动选择不选择
适用特征多、需选择特征相关性强
优化无闭式解有闭式解
计算较慢较快

说明: L1 用于特征选择,L2 用于防止过拟合。

2.6.3.5 正则化与特征缩放

L1 / L2 正则化对特征尺度敏感:尺度大的特征参数小,惩罚不公平,所以正则化前必须先标准化(详见 2.2.1)。

2.6.4 正则化强度与调参

正则化强度 λ 控制惩罚力度,需通过交叉验证选择。

λ 值惩罚力度模型复杂度效果
λ = 0无高过拟合
λ 小弱较高轻微过拟合
λ 适中适中适中最优
λ 大强低欠拟合

说明: λ 太小过拟合,λ 太大欠拟合。从误差看:λ 过大时训练误差和测试误差都偏高(欠拟合);λ 过小时训练误差低、测试误差高(过拟合)。

2.6.5 凸优化直觉(了解)

凸优化是指损失函数是凸函数,有唯一全局最优解。

凸函数 vs 非凸函数

类型特点优化
凸函数唯一全局最优易优化
非凸函数多个局部最优难优化

常见凸优化问题

算法是否凸
线性回归凸
岭回归凸
Lasso凸
逻辑回归凸
神经网络非凸

说明: 凸优化保证找到全局最优,是机器学习的重要性质。

2.7 其它基础

2.7.1 向量化与稀疏矩阵(CountVectorizer/Tfidf)

文本数据需转为数值向量才能输入模型,常用词袋模型和 TF-IDF。

2.7.1.1 词袋模型(Bag of Words)

核心描述: 统计每个词在文档中出现的次数,忽略顺序。

公式:

词袋向量 = [count(w₁), count(w₂), ..., count(wₙ)]
​
其中:
  count(wᵢ):词 wᵢ 在文档中出现的次数
  n:词表大小
2.7.1.2 TF-IDF

核心描述: 衡量词对文档的重要性,词频高但全局常见的词权重低。

公式:

说明:

  • TF(w, d) = 词 w 在文档 d 中的频率

  • IDF(w) = log(N / df(w))

  • N:文档总数

  • df(w):包含词 w 的文档数

2.7.1.3 稀疏矩阵

核心描述: 词袋向量大多为 0,用稀疏矩阵存储节省内存。

表示说明存储
稠密矩阵存储所有元素O(n×m)
稀疏矩阵只存非零元素O(nnz)

直观理解:

稠密矩阵:
  [[1, 1, 1, 0, 0, 0, 0, 0],
   [1, 1, 0, 1, 0, 0, 0, 0]]
  → 存储 16 个元素
​
稀疏矩阵:
  (0, 0): 1, (0, 1): 1, (0, 2): 1
  (1, 0): 1, (1, 1): 1, (1, 3): 1
  → 只存 6 个非零元素

说明: sklearn 的 CountVectorizer 和 TfidfVectorizer 默认返回稀疏矩阵。

代码示例:

from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer
​
docs = ["我 爱 机器学习", "机器学习 很 有趣", "我 爱 数据"]
token_pattern = r"(?u)\b\w+\b"
​
vec = CountVectorizer(token_pattern=token_pattern)
bow = vec.fit_transform(docs)
tfidf = TfidfVectorizer(token_pattern=token_pattern).fit_transform(docs)
​
print(f"词表:{vec.get_feature_names_out()}")
print(f"词袋矩阵:{bow.shape},非零元素 {bow.nnz}")
print(f"TF-IDF 矩阵:{tfidf.shape}")

2.7.2 距离与相似度(欧氏 / 余弦 / 曼哈顿)

距离衡量两个向量的差异,相似度衡量两个向量的接近程度。

度量公式说明适用
欧氏距离两点之间的直线距离,最常用。KNN、聚类
曼哈顿距离两点在网格上的距离,也称"城市街区距离"。高维稀疏数据
余弦相似度衡量两个向量的方向相似度,与长度无关文本、embedding

说明: ​ 为点积,​ 为模。

2.7.3 概率基础:条件概率 / 贝叶斯 / 期望

概率是机器学习的基础,朴素贝叶斯等算法直接基于概率。

2.7.3.1 条件概率

说明: 在事件 B 发生的条件下,事件 A 发生的概率。

2.7.3.2 贝叶斯定理

公式:

说明:

  • P(c|x):后验概率,给定特征 x 下类别 c 的概率

  • P(x|c):似然,给定类别 c 下特征 x 的概率

  • P(c):先验概率,类别 c 的先验概率

  • P(x):证据,特征 x 的概率(对所有类别相同,可忽略)

2.7.3.3 期望

期望是随机变量取值的加权平均:​,是方差、偏差-方差分解等概念的基础。

2.7.4 集成学习

集成学习(Ensemble Learning) 是一种机器学习方法:它通过构建并组合多个学习器(称为基学习器 / 弱学习器),将多个"弱模型"的预测结果以某种规则汇总,从而得到一个性能更强、更稳健的"强学习器",用于完成分类、回归等任务。

一句话概括:"三个臭皮匠,顶个诸葛亮"——用多个模型的集体智慧,弥补单个模型的不足。

要素含义
基学习器(Base Learner)被组合的个体模型,如决策树、SVM、神经网络
生成方式如何产生多个有差异的基学习器(重采样、调权、不同算法等)
组合策略如何汇总结果(投票、平均、加权、元学习器等)
2.7.4.1 Bagging(装袋法)

全称:Bootstrap Aggregating(自助采样 + 聚合)

核心流程:

  1. 对原始训练集进行 B 次有放回抽样(Bootstrap),每次抽 N 个样本,得到 B 个大小相同的子训练集;

  2. 在每个子训练集上独立并行训练一个基学习器(通常是同质的高方差模型,如深决策树);

  3. 分类任务用投票、回归任务用平均,聚合 B 个基学习器的结果。

关键点:

  • 基学习器之间相互独立,可并行训练;

  • 通过"制造多样性 + 平均"来降低方差,对噪声和异常值鲁棒。

代表算法:Random Forest

Bagging 适用场景

场景原因
高维小样本(基因、医学诊断)特征随机子采样 + 降方差,抗过拟合
噪声大、异常值多的数据(传感器、工业质检)投票/平均对个别异常不敏感
需要特征重要性(风控筛选、生物标志物)随机森林可输出特征重要性
快速可靠 baseline调参少、鲁棒、可并行
样本宝贵、想免费估误差OOB 误差估计,无需单独验证集

典型行业:金融风控初筛、医疗辅助诊断、遥感分类、工业故障检测。

2.7.4.2 Boosting(提升法)

核心流程:

  1. 串行训练基学习器,每个新学习器都重点关注前面学习器预测错误的样本;

  2. 每轮训练后调整样本权重(AdaBoost)或拟合残差/梯度(GBDT 系列);

  3. 最后将所有基学习器加权组合成强学习器。

两条技术路线:

  • AdaBoost:提高错分样本权重,降低正确样本权重,加权投票;

  • Gradient Boosting(GBDT/XGBoost/LightGBM/CatBoost):每个新学习器拟合前一轮的负梯度(残差),逐步逼近目标。

关键点:

  • 基学习器之间存在强依赖,必须顺序训练,难并行;

  • 通过"逐步纠错"来降低偏差,把弱学习器提升为强学习器;

  • 精度高,但对噪声/异常值较敏感,需正则化、学习率、早停防过拟合。

Boosting 适用场景

场景原因
表格数据精度竞赛(Kaggle、信贷评分)GBDT 系列在表格数据长期 SOTA
点击率预估 / 推荐排序GBDT+LR、XGBoost/LightGBM 广泛用于广告推荐
搜索排序(Learning to Rank)LambdaMART 是经典 LTR 算法
信用评分与反欺诈高精度 + 可解释(SHAP、特征重要性)
大规模高维稀疏数据LightGBM 直方图 + Leaf-wise,快且省内存
类别特征多(电商、广告)CatBoost 原生处理类别特征
欠拟合、弱学习器需提升逐步纠错,擅长提升弱模型

典型行业:广告 CTR、推荐排序、金融风控评分卡、搜索排序、竞赛级表格建模。

2.7.4.3 核心区别对比
维度BaggingBoosting
训练方式并行串行
基学习器关系相互独立强依赖(后一个依赖前一个)
样本抽样有放回 Bootstrap,各子集独立全量数据,但动态调整样本权重
样本权重均等错分样本权重增大(AdaBoost)/ 拟合残差(GBDT)
基学习器类型同质,通常是高方差模型(深树)同质,通常是弱学习器(浅树/ stump)
组合方式投票 / 平均(等权)加权组合
主要作用降低方差(variance)降低偏差(bias)
过拟合风险较低,基学习器越多越稳较高,需正则化/早停
对噪声/异常值鲁棒较敏感

一句话记忆:

  • Bagging = 并行 + 投票平均 + 降方差(用"一群独立的专家投票"减少个别专家的随机误差);

  • Boosting = 串行 + 逐步纠错 + 降偏差(用"后一个专家专门补前一个专家的错"不断提升)。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐