机器学习模型评估宝典:训练/测试切分与交叉验证深度解析
本文介绍了机器学习建模的核心流程,包括数据准备、数据切分、模型训练和评估。首先展示了一个员工离职预测的数据示例,说明特征(X)和标签(y)的结构。接着详细讲解了三种数据切分方法:随机切分(推荐使用train_test_split)、交叉验证和时间序列切分。最后演示了如何使用训练集训练逻辑回归模型,并查看模型参数。整个过程强调数据预处理的重要性,并提供了完整的Python代码实现,适合机器学习初学者
·
一、核心流程图解
1.1 整体流程
原始数据
↓
【步骤1】数据准备
↓
【步骤2】数据切分 → 训练集 + 测试集
↓
【步骤3】用训练集训练模型
↓
【步骤4】用测试集评估模型
↓
结果分析
让我们逐步详细拆解每个步骤。
二、步骤1:数据准备
2.1 数据长什么样?
示例:员工离职预测数据
| 员工ID | 年龄 | 工资 | 工作年限 | 满意度 | 是否离职 |
|---|---|---|---|---|---|
| 1 | 35 | 8000 | 5 | 7 | 0(留任) |
| 2 | 28 | 6000 | 2 | 4 | 1(离职) |
| 3 | 42 | 12000 | 10 | 8 | 0(留任) |
| … | … | … | … | … | … |
| 1000 | 31 | 7500 | 3 | 6 | 0(留任) |
数据结构:
- 特征(X):年龄、工资、工作年限、满意度(用来预测)
- 标签(y):是否离职(要预测的目标)
2.2 Python数据准备
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
# 1. 读取数据
data = pd.read_csv('employees.csv')
# 查看数据
print(data.head())
print(f"数据形状:{data.shape}") # 例如:(1000, 6)
# 2. 分离特征和标签
X = data[['年龄', '工资', '工作年限', '满意度']] # 特征
y = data['是否离职'] # 标签
print(f"特征矩阵形状:{X.shape}") # (1000, 4)
print(f"标签向量形状:{y.shape}") # (1000,)
关键概念:
- X(大写):特征矩阵,每行是一个样本,每列是一个特征
- y(小写):标签向量,每个元素对应一个样本的标签
三、步骤2:数据切分
3.1 方法A:简单随机切分(最常用)
原理图解
原始数据(1000条)
[1][2][3][4]...[1000]
↓
随机打乱
↓
[523][12][789][456]...[234]
↓
按比例切分
↓
├─ 训练集(700条,70%)
│ [523][12][789]...[第700条]
│
└─ 测试集(300条,30%)
[第701条]...[234]
Python实现
# 方法1:使用sklearn的train_test_split(推荐)
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(
X, # 特征
y, # 标签
test_size=0.3, # 测试集比例30%
random_state=42, # 随机种子(保证可重复)
stratify=y # 分层抽样(保持类别比例)
)
# 查看切分结果
print(f"训练集特征形状:{X_train.shape}") # (700, 4)
print(f"训练集标签形状:{y_train.shape}") # (700,)
print(f"测试集特征形状:{X_test.shape}") # (300, 4)
print(f"测试集标签形状:{y_test.shape}") # (300,)
# 检查类别比例
print(f"原始数据离职率:{y.mean():.2%}") # 例如:15%
print(f"训练集离职率:{y_train.mean():.2%}") # 15%(相同)
print(f"测试集离职率:{y_test.mean():.2%}") # 15%(相同)
参数详解
| 参数 | 作用 | 推荐值 |
|---|---|---|
test_size |
测试集比例 | 0.2-0.3(即20%-30%) |
random_state |
随机种子 | 任意整数(如42),保证结果可重复 |
stratify |
分层抽样 | 设为y,保持类别比例一致 |
shuffle |
是否打乱 | 默认True,通常保持 |
方法2:手动切分(理解原理)
# 手动实现train_test_split的逻辑
# 1. 确定切分点
n_samples = len(X) # 1000
split_point = int(n_samples * 0.7) # 700
# 2. 生成随机索引
np.random.seed(42) # 设置随机种子
indices = np.random.permutation(n_samples) # 随机打乱0-999
# 3. 切分索引
train_indices = indices[:split_point] # 前700个索引
test_indices = indices[split_point:] # 后300个索引
# 4. 根据索引切分数据
X_train = X.iloc[train_indices]
X_test = X.iloc[test_indices]
y_train = y.iloc[train_indices]
y_test = y.iloc[test_indices]
print(f"训练集大小:{len(X_train)}") # 700
print(f"测试集大小:{len(X_test)}") # 300
3.2 方法B:交叉验证切分
3折交叉验证示例
from sklearn.model_selection import KFold
# 创建3折交叉验证对象
kf = KFold(n_splits=3, shuffle=True, random_state=42)
# 遍历每一折
fold = 1
for train_index, test_index in kf.split(X):
print(f"\n===== 第{fold}折 =====")
# 切分数据
X_train = X.iloc[train_index]
X_test = X.iloc[test_index]
y_train = y.iloc[train_index]
y_test = y.iloc[test_index]
print(f"训练集大小:{len(X_train)}") # 约667条
print(f"测试集大小:{len(X_test)}") # 约333条
print(f"训练集索引范围:{train_index[:5]}...{train_index[-5:]}")
print(f"测试集索引范围:{test_index[:5]}...{test_index[-5:]}")
fold += 1
输出示例:
===== 第1折 =====
训练集大小:667
测试集大小:333
训练集索引范围:[0 1 2 3 4]...[995 996 997 998 999]
测试集索引范围:[334 335 336 337 338]...[662 663 664 665 666]
===== 第2折 =====
训练集大小:667
测试集大小:333
...
3.3 方法C:时间序列切分
from sklearn.model_selection import TimeSeriesSplit
# 假设数据按时间排序
# 创建时间序列5折交叉验证
tscv = TimeSeriesSplit(n_splits=5)
for fold, (train_index, test_index) in enumerate(tscv.split(X), 1):
print(f"\n===== 第{fold}折 =====")
print(f"训练集:索引 {train_index[0]} 到 {train_index[-1]}")
print(f"测试集:索引 {test_index[0]} 到 {test_index[-1]}")
X_train, X_test = X.iloc[train_index], X.iloc[test_index]
y_train, y_test = y.iloc[train_index], y.iloc[test_index]
输出示例:
===== 第1折 =====
训练集:索引 0 到 166
测试集:索引 167 到 333
===== 第2折 =====
训练集:索引 0 到 333
测试集:索引 334 到 500
===== 第3折 =====
训练集:索引 0 到 500
测试集:索引 501 到 666
...
特点:训练集逐步扩大,测试集始终在训练集之后
四、步骤3:用训练集训练模型
4.1 训练的本质
训练 = 让模型从数据中学习规律
训练前:
模型:一张白纸,不知道任何规律
训练过程:
模型看到:35岁、8000工资、满意度7 → 留任
模型看到:28岁、6000工资、满意度4 → 离职
模型看到:42岁、12000工资、满意度8 → 留任
...(看完700条训练数据)
训练后:
模型学到:年轻+低工资+低满意度 → 容易离职
年长+高工资+高满意度 → 容易留任
4.2 Python训练代码
基础训练
from sklearn.linear_model import LogisticRegression
# 1. 创建模型对象
model = LogisticRegression(random_state=42)
# 2. 训练模型(fit = 拟合 = 学习)
model.fit(X_train, y_train)
print("模型训练完成!")
# 3. 查看模型学到的参数
print(f"截距:{model.intercept_}")
print(f"系数:{model.coef_}")
# 例如:系数 = [-0.05, 0.0003, 0.1, 0.2]
# 含义:年龄每增加1岁,离职概率降低0.05
# 工资每增加1元,离职概率增加0.0003
# ...
训练过程可视化
# 查看训练集上的表现
train_score = model.score(X_train, y_train)
print(f"训练集准确率:{train_score:.2%}") # 例如:87%
# 预测几个训练样本
sample_predictions = model.predict(X_train[:5])
sample_true = y_train.iloc[:5].values
print("\n训练集前5个样本:")
for i in range(5):
print(f"样本{i+1}:预测={sample_predictions[i]}, 真实={sample_true[i]}")
输出示例:
训练集准确率:87%
训练集前5个样本:
样本1:预测=0, 真实=0 ✓
样本2:预测=1, 真实=1 ✓
样本3:预测=0, 真实=0 ✓
样本4:预测=0, 真实=1 ✗
样本5:预测=1, 真实=1 ✓
4.3 不同模型的训练
# 逻辑回归
from sklearn.linear_model import LogisticRegression
model1 = LogisticRegression()
model1.fit(X_train, y_train)
# 决策树
from sklearn.tree import DecisionTreeClassifier
model2 = DecisionTreeClassifier(max_depth=5)
model2.fit(X_train, y_train)
# 随机森林
from sklearn.ensemble import RandomForestClassifier
model3 = RandomForestClassifier(n_estimators=100)
model3.fit(X_train, y_train)
# 支持向量机
from sklearn.svm import SVC
model4 = SVC(kernel='rbf')
model4.fit(X_train, y_train)
print("所有模型训练完成!")
关键点:
- 所有模型的训练接口都是
.fit(X_train, y_train) - 训练只使用训练集,测试集完全不能碰
五、步骤4:用测试集评估模型
5.1 评估的本质
评估 = 用模型从未见过的数据考试
测试前:
模型:已经用700条训练数据学习完毕
测试集:300条全新数据,模型从未见过
测试过程:
模型看到测试样本1:30岁、7000工资、满意度5
模型预测:离职(概率65%)
真实情况:离职 ✓
模型看到测试样本2:40岁、10000工资、满意度8
模型预测:留任(概率80%)
真实情况:留任 ✓
...(预测完300条)
测试后:
统计:预测对了255条,错了45条
准确率:255/300 = 85%
5.2 Python评估代码
基础评估
# 1. 计算测试集准确率
test_score = model.score(X_test, y_test)
print(f"测试集准确率:{test_score:.2%}") # 例如:85%
# 2. 进行预测
y_pred = model.predict(X_test)
# 3. 对比预测结果和真实标签
print("\n前10个测试样本的预测结果:")
comparison = pd.DataFrame({
'真实值': y_test.iloc[:10].values,
'预测值': y_pred[:10],
'是否正确': y_test.iloc[:10].values == y_pred[:10]
})
print(comparison)
输出示例:
测试集准确率:85%
前10个测试样本的预测结果:
真实值 预测值 是否正确
0 0 0 True
1 1 1 True
2 0 0 True
3 1 0 False
4 0 0 True
5 1 1 True
6 0 1 False
7 1 1 True
8 0 0 True
9 0 0 True
详细评估指标
from sklearn.metrics import classification_report, confusion_matrix
import seaborn as sns
import matplotlib.pyplot as plt
# 1. 混淆矩阵
cm = confusion_matrix(y_test, y_pred)
print("混淆矩阵:")
print(cm)
print("\n解释:")
print(f"真负例(正确预测留任):{cm[0,0]}")
print(f"假正例(错误预测离职):{cm[0,1]}")
print(f"假负例(错误预测留任):{cm[1,0]}")
print(f"真正例(正确预测离职):{cm[1,1]}")
# 2. 可视化混淆矩阵
plt.figure(figsize=(8, 6))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues')
plt.xlabel('预测值')
plt.ylabel('真实值')
plt.title('混淆矩阵')
plt.show()
# 3. 详细分类报告
print("\n分类报告:")
print(classification_report(y_test, y_pred,
target_names=['留任', '离职']))
输出示例:
混淆矩阵:
[[215 10]
[ 35 40]]
解释:
真负例(正确预测留任):215
假正例(错误预测离职):10
假负例(错误预测留任):35
真正例(正确预测离职):40
分类报告:
precision recall f1-score support
留任 0.86 0.96 0.90 225
离职 0.80 0.53 0.64 75
accuracy 0.85 300
macro avg 0.83 0.74 0.77 300
weighted avg 0.84 0.85 0.84 300
预测概率分析
# 获取预测概率
y_proba = model.predict_proba(X_test)
print("前5个测试样本的预测概率:")
for i in range(5):
print(f"样本{i+1}:")
print(f" 留任概率:{y_proba[i,0]:.2%}")
print(f" 离职概率:{y_proba[i,1]:.2%}")
print(f" 预测结果:{'离职' if y_pred[i]==1 else '留任'}")
print(f" 真实结果:{'离职' if y_test.iloc[i]==1 else '留任'}")
print()
输出示例:
前5个测试样本的预测概率:
样本1:
留任概率:85%
离职概率:15%
预测结果:留任
真实结果:留任 ✓
样本2:
留任概率:30%
离职概率:70%
预测结果:离职
真实结果:离职 ✓
样本3:
留任概率:92%
离职概率:8%
预测结果:留任
真实结果:留任 ✓
...
5.3 对比训练集和测试集表现
# 计算两个数据集的准确率
train_acc = model.score(X_train, y_train)
test_acc = model.score(X_test, y_test)
print(f"训练集准确率:{train_acc:.2%}")
print(f"测试集准确率:{test_acc:.2%}")
print(f"差异:{abs(train_acc - test_acc):.2%}")
# 判断模型状态
if train_acc > 0.95 and test_acc < 0.70:
print("\n⚠️ 警告:严重过拟合!")
print("模型在训练集上表现很好,但在测试集上很差")
print("建议:简化模型、增加正则化、收集更多数据")
elif train_acc < 0.70 and test_acc < 0.70:
print("\n⚠️ 警告:欠拟合!")
print("模型在两个数据集上都表现不好")
print("建议:使用更复杂的模型、增加特征、调整超参数")
elif abs(train_acc - test_acc) < 0.05:
print("\n✓ 模型状态良好!")
print("训练集和测试集表现接近,泛化能力强")
else:
print("\n⚠️ 轻微过拟合")
print("可以接受,但有改进空间")
六、完整实战案例
6.1 简单切分完整流程
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, classification_report
import matplotlib.pyplot as plt
# ========== 步骤1:数据准备 ==========
print("=" * 50)
print("步骤1:数据准备")
print("=" * 50)
# 模拟数据(实际应用中从文件读取)
np.random.seed(42)
n_samples = 1000
data = pd.DataFrame({
'年龄': np.random.randint(22, 60, n_samples),
'工资': np.random.randint(4000, 15000, n_samples),
'工作年限': np.random.randint(0, 20, n_samples),
'满意度': np.random.randint(1, 11, n_samples)
})
# 生成标签(简化规则:年轻+低工资+低满意度 → 离职)
data['是否离职'] = (
(data['年龄'] < 30) &
(data['工资'] < 7000) &
(data['满意度'] < 5)
).astype(int)
print(f"数据形状:{data.shape}")
print(f"离职率:{data['是否离职'].mean():.2%}")
print("\n前5行数据:")
print(data.head())
# 分离特征和标签
X = data[['年龄', '工资', '工作年限', '满意度']]
y = data['是否离职']
# ========== 步骤2:数据切分 ==========
print("\n" + "=" * 50)
print("步骤2:数据切分(70%训练,30%测试)")
print("=" * 50)
X_train, X_test, y_train, y_test = train_test_split(
X, y,
test_size=0.3,
random_state=42,
stratify=y
)
print(f"训练集大小:{len(X_train)} ({len(X_train)/len(X)*100:.1f}%)")
print(f"测试集大小:{len(X_test)} ({len(X_test)/len(X)*100:.1f}%)")
print(f"训练集离职率:{y_train.mean():.2%}")
print(f"测试集离职率:{y_test.mean():.2%}")
# ========== 步骤3:训练模型 ==========
print("\n" + "=" * 50)
print("步骤3:训练模型")
print("=" * 50)
model = LogisticRegression(random_state=42, max_iter=1000)
model.fit(X_train, y_train)
print("✓ 模型训练完成!")
print(f"模型参数:")
print(f" 截距:{model.intercept_[0]:.4f}")
for i, feature in enumerate(X.columns):
print(f" {feature}系数:{model.coef_[0][i]:.4f}")
# ========== 步骤4:评估模型 ==========
print("\n" + "=" * 50)
print("步骤4:评估模型")
print("=" * 50)
# 训练集表现
train_pred = model.predict(X_train)
train_acc = accuracy_score(y_train, train_pred)
print(f"训练集准确率:{train_acc:.2%}")
# 测试集表现
test_pred = model.predict(X_test)
test_acc = accuracy_score(y_test, test_pred)
print(f"测试集准确率:{test_acc:.2%}")
print(f"\n准确率差异:{abs(train_acc - test_acc):.2%}")
# 详细报告
print("\n测试集详细报告:")
print(classification_report(y_test, test_pred,
target_names=['留任', '离职']))
# ========== 可视化 ==========
from sklearn.metrics import confusion_matrix
import seaborn as sns
cm = confusion_matrix(y_test, test_pred)
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# 混淆矩阵
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', ax=axes[0])
axes[0].set_xlabel('预测值')
axes[0].set_ylabel('真实值')
axes[0].set_title('混淆矩阵')
# 准确率对比
metrics = ['训练集', '测试集']
accuracies = [train_acc, test_acc]
axes[1].bar(metrics, accuracies, color=['skyblue', 'lightcoral'])
axes[1].set_ylim([0, 1])
axes[1].set_ylabel('准确率')
axes[1].set_title('训练集 vs 测试集准确率')
for i, v in enumerate(accuracies):
axes[1].text(i, v + 0.02, f'{v:.2%}', ha='center')
plt.tight_layout()
plt.show()
print("\n✓ 完整流程执行完毕!")
6.2 交叉验证完整流程
from sklearn.model_selection import cross_val_score, cross_validate
import numpy as np
print("=" * 50)
print("5折交叉验证完整流程")
print("=" * 50)
# 创建模型
model = LogisticRegression(random_state=42, max_iter=1000)
# 方法1:简单交叉验证(只返回准确率)
scores = cross_val_score(model, X, y, cv=5, scoring='accuracy')
print("\n5次交叉验证准确率:")
for i, score in enumerate(scores, 1):
print(f" 第{i}折:{score:.2%}")
print(f"\n平均准确率:{scores.mean():.2%}")
print(f"标准差:{scores.std():.3f}")
print(f"95%置信区间:{scores.mean():.2%} ± {1.96*scores.std():.2%}")
# 方法2:详细交叉验证(返回多个指标)
scoring = {
'accuracy': 'accuracy',
'precision': 'precision',
'recall': 'recall',
'f1': 'f1'
}
cv_results = cross_validate(model, X, y, cv=5, scoring=scoring)
print("\n详细交叉验证结果:")
for metric_name in ['accuracy', 'precision', 'recall', 'f1']:
scores = cv_results[f'test_{metric_name}']
print(f"{metric_name.capitalize()}:{scores.mean():.3f} ± {scores.std():.3f}")
# 可视化
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(10, 6))
metrics = ['Accuracy', 'Precision', 'Recall', 'F1']
means = [cv_results[f'test_{m.lower()}'].mean() for m in metrics]
stds = [cv_results[f'test_{m.lower()}'].std() for m in metrics]
x = np.arange(len(metrics))
ax.bar(x, means, yerr=stds, capsize=5, alpha=0.7, color='steelblue')
ax.set_xticks(x)
ax.set_xticklabels(metrics)
ax.set_ylabel('分数')
ax.set_title('5折交叉验证各指标表现')
ax.set_ylim([0, 1])
for i, (m, s) in enumerate(zip(means, stds)):
ax.text(i, m + s + 0.02, f'{m:.3f}', ha='center')
plt.tight_layout()
plt.show()
七、常见问题与解决方案
问题1:训练集和测试集准确率差异很大
# 诊断代码
train_acc = model.score(X_train, y_train)
test_acc = model.score(X_test, y_test)
print(f"训练集准确率:{train_acc:.2%}")
print(f"测试集准确率:{test_acc:.2%}")
print(f"差异:{abs(train_acc - test_acc):.2%}")
if train_acc - test_acc > 0.15:
print("\n诊断:过拟合")
print("解决方案:")
print("1. 增加训练数据")
print("2. 简化模型(减少特征、降低复杂度)")
print("3. 增加正则化")
print("4. 使用交叉验证")
# 示例:增加正则化
from sklearn.linear_model import LogisticRegression
model_regularized = LogisticRegression(C=0.1, random_state=42)
model_regularized.fit(X_train, y_train)
new_train_acc = model_regularized.score(X_train, y_train)
new_test_acc = model_regularized.score(X_test, y_test)
print(f"\n正则化后:")
print(f"训练集准确率:{new_train_acc:.2%}")
print(f"测试集准确率:{new_test_acc:.2%}")
问题2:数据泄露检查
# 错误示例:在切分前标准化
from sklearn.preprocessing import StandardScaler
# ❌ 错误做法
scaler = StandardScaler()
X_scaled_wrong = scaler.fit_transform(X) # 用了全部数据的统计量
X_train, X_test = train_test_split(X_scaled_wrong, test_size=0.3)
# ✅ 正确做法
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train) # 只用训练集统计量
X_test_scaled = scaler.transform(X_test) # 用训练集的统计量转换测试集
# 更好的做法:使用Pipeline
from sklearn.pipeline import Pipeline
pipeline = Pipeline([
('scaler', StandardScaler()),
('model', LogisticRegression())
])
# Pipeline自动处理,不会泄露
pipeline.fit(X_train, y_train)
test_score = pipeline.score(X_test, y_test)
问题3:保存和加载训练好的模型
import joblib
# 训练模型
model = LogisticRegression()
model.fit(X_train, y_train)
# 保存模型
joblib.dump(model, 'employee_churn_model.pkl')
print("✓ 模型已保存")
# 加载模型
loaded_model = joblib.load('employee_churn_model.pkl')
# 使用加载的模型预测
predictions = loaded_model.predict(X_test)
accuracy = loaded_model.score(X_test, y_test)
print(f"加载模型的准确率:{accuracy:.2%}")
八、总结:训练/测试流程速查表
核心4步骤
# 步骤1:准备数据
X = data[特征列]
y = data[标签列]
# 步骤2:切分数据
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.3, random_state=42, stratify=y
)
# 步骤3:训练模型
model = LogisticRegression()
model.fit(X_train, y_train)
# 步骤4:评估模型
test_score = model.score(X_test, y_test)
print(f"测试集准确率:{test_score:.2%}")
关键原则
| 原则 | 说明 |
|---|---|
| 训练集专用 | 只用训练集训练模型 |
| 测试集保密 | 训练时完全不能看测试集 |
| 先切分后处理 | 先切分数据,再做标准化等处理 |
| 关注测试集 | 测试集表现才是真实能力 |
| 对比两者 | 训练集和测试集差异反映过拟合程度 |
常用参数
train_test_split(
X, y,
test_size=0.3, # 测试集30%
random_state=42, # 随机种子
stratify=y, # 分层抽样
shuffle=True # 打乱数据(默认)
)
从数据准备到模型评估,从简单切分到交叉验证,从基础代码到完整案例,这份指南涵盖了实际应用中的所有关键步骤。记住核心原则:训练集用来学习,测试集用来考试,两者严格分离!
更多推荐



所有评论(0)