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

本章是算法章节的前置基础,为后续监督学习、无监督学习等算法章节提供通用工具和概念。
核心职责:
| 职责 | 说明 |
|---|---|
| 评估指标 | 提供衡量模型效果的标准(分类/回归) |
| 数据预处理 | 提供数据清洗、转换的标准方法 |
| 模型选择与调参 | 提供选择模型、调参的工具 |
| 类别不平衡 | 提供处理不平衡数据的方法 |
| 工程化基础 | 提供 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 越好 |
补充:
其中 为真实值,
为预测值,
为均值;
# 回归指标演示:线性回归
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 用于调整类别权重,让模型更关注少数类。
原理:
| 类别 | 默认权重 | 调整后权重 |
|---|---|---|
| 多数类 | 1 | 1 |
| 少数类 | 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(自助采样 + 聚合)
核心流程:
-
对原始训练集进行 B 次有放回抽样(Bootstrap),每次抽 N 个样本,得到 B 个大小相同的子训练集;
-
在每个子训练集上独立并行训练一个基学习器(通常是同质的高方差模型,如深决策树);
-
分类任务用投票、回归任务用平均,聚合 B 个基学习器的结果。

关键点:
-
基学习器之间相互独立,可并行训练;
-
通过"制造多样性 + 平均"来降低方差,对噪声和异常值鲁棒。
代表算法:Random Forest
Bagging 适用场景
| 场景 | 原因 |
|---|---|
| 高维小样本(基因、医学诊断) | 特征随机子采样 + 降方差,抗过拟合 |
| 噪声大、异常值多的数据(传感器、工业质检) | 投票/平均对个别异常不敏感 |
| 需要特征重要性(风控筛选、生物标志物) | 随机森林可输出特征重要性 |
| 快速可靠 baseline | 调参少、鲁棒、可并行 |
| 样本宝贵、想免费估误差 | OOB 误差估计,无需单独验证集 |
典型行业:金融风控初筛、医疗辅助诊断、遥感分类、工业故障检测。
2.7.4.2 Boosting(提升法)
核心流程:
-
串行训练基学习器,每个新学习器都重点关注前面学习器预测错误的样本;
-
每轮训练后调整样本权重(AdaBoost)或拟合残差/梯度(GBDT 系列);
-
最后将所有基学习器加权组合成强学习器。

两条技术路线:
-
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 核心区别对比
| 维度 | Bagging | Boosting |
|---|---|---|
| 训练方式 | 并行 | 串行 |
| 基学习器关系 | 相互独立 | 强依赖(后一个依赖前一个) |
| 样本抽样 | 有放回 Bootstrap,各子集独立 | 全量数据,但动态调整样本权重 |
| 样本权重 | 均等 | 错分样本权重增大(AdaBoost)/ 拟合残差(GBDT) |
| 基学习器类型 | 同质,通常是高方差模型(深树) | 同质,通常是弱学习器(浅树/ stump) |
| 组合方式 | 投票 / 平均(等权) | 加权组合 |
| 主要作用 | 降低方差(variance) | 降低偏差(bias) |
| 过拟合风险 | 较低,基学习器越多越稳 | 较高,需正则化/早停 |
| 对噪声/异常值 | 鲁棒 | 较敏感 |
一句话记忆:
-
Bagging = 并行 + 投票平均 + 降方差(用"一群独立的专家投票"减少个别专家的随机误差);
-
Boosting = 串行 + 逐步纠错 + 降偏差(用"后一个专家专门补前一个专家的错"不断提升)。
更多推荐










所有评论(0)