🕐 预计用时:3-4 小时 | 🎯 目标:掌握三种核心算法 + 交叉验证
决策树就像你每天做的选择题。比如今天穿什么衣服?
今天冷吗?
/ \
是 否
/ \
下雨吗? 穿短袖
/ \
是 否
/ \
穿外套+带伞 穿外套机器学习中的决策树也一样——它通过一系列是/否问题,把数据逐步分类。每个"问题"就是一个节点,每个回答就是一条分支。
from sklearn.tree import DecisionTreeClassifier, export_text
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, classification_report
# 加载数据
iris = load_iris()
X_train, X_test, y_train, y_test = train_test_split(
iris.data, iris.target, test_size=0.2, random_state=42
)
# 创建决策树(限制最大深度为3,防止过拟合)
tree = DecisionTreeClassifier(max_depth=3, random_state=42)
tree.fit(X_train, y_train)
# 预测
y_pred = tree.predict(X_test)
print(f"准确率: {accuracy_score(y_test, y_pred):.2%}")
# 打印决策规则(文本版)
print("\n决策规则:")
print(export_text(tree, feature_names=iris.feature_names))准确率: 100.00%
决策规则:
|--- petal length (cm) <= 2.45
| |--- class: 0
|--- petal length (cm) > 2.45
| |--- petal width (cm) <= 1.75
| | |--- petal length (cm) <= 4.95
| | | |--- class: 1
| | |--- petal length (cm) > 4.95
| | | |--- class: 2
| |--- petal width (cm) > 1.75
| | |--- class: 2# 哪个特征最重要?
importances = tree.feature_importances_
for name, imp in sorted(zip(iris.feature_names, importances), key=lambda x: -x[1]):
bar = "█" * int(imp * 30)
print(f" {name:>20s}: {imp:.3f} {bar}") petal length (cm): 0.950 ████████████████████████████
petal width (cm): 0.050 █
sepal length (cm): 0.000
sepal width (cm): 0.000💡 决策树优点:可解释性极强——你可以直接看到模型是怎么做决策的,就像看一棵"问题树"。缺点是容易过拟合(死记硬背训练数据),通过限制 max_depth 可以缓解。
from sklearn.tree import plot_tree
plt.figure(figsize=(16, 8))
plot_tree(tree,
feature_names=iris.feature_names,
class_names=iris.target_names,
filled=True, # 颜色填充
rounded=True, # 圆角框
fontsize=11)
plt.title('鸢尾花分类决策树')
plt.tight_layout()
plt.savefig('decision_tree.png', dpi=150)
plt.show()一棵决策树容易犯错,那种一片森林呢?
随机森林的核心思想:三个臭皮匠,赛过诸葛亮。训练很多棵决策树,每棵树看不同的数据子集,最后投票决定结果。
from sklearn.ensemble import RandomForestClassifier
# 用乳腺癌数据集(569个样本,30个特征)
from sklearn.datasets import load_breast_cancer
data = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(
data.data, data.target, test_size=0.2, random_state=42
)
# 创建随机森林(100棵树)
rf = RandomForestClassifier(n_estimators=100, random_state=42)
rf.fit(X_train, y_train)
y_pred = rf.predict(X_test)
print(f"准确率: {accuracy_score(y_test, y_pred):.2%}")
print(f"\n分类报告:")
print(classification_report(y_test, y_pred, target_names=data.target_names))# 随机森林可以告诉你哪些特征最重要
importances = rf.feature_importances_
indices = np.argsort(importances)[::-1]
print("特征重要性排名:")
for i in range(10): # 前10个
print(f" {i+1}. {data.feature_names[indices[i]]}: {importances[indices[i]]:.4f}")
# 可视化
plt.figure(figsize=(12, 6))
top_n = 15
plt.barh(range(top_n), importances[indices[:top_n]], color='#07c160')
plt.yticks(range(top_n), [data.feature_names[i] for i in indices[:top_n]])
plt.xlabel('重要性')
plt.title('随机森林 - 特征重要性 Top 15')
plt.gca().invert_yaxis()
plt.tight_layout()
plt.savefig('feature_importance.png', dpi=150)
plt.show()🔍 为什么随机森林更好?每棵树只看随机的一部分数据和特征,所以它们各自有"偏见"。但把它们的投票汇总起来,偏见就互相抵消了。这就是集成学习的力量。
前面的算法都是监督学习——有标签(正确答案)。聚类是无监督学习——没有标签,让机器自己找规律。
K-Means 的思路很直觉:
想象你有 1000 个客户的消费数据,你想把他们分成 3 类(高/中/低消费),但你不知道谁属于哪类——K-Means 帮你搞定。
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
# 生成模拟数据(3个簇)
X, y_true = make_blobs(n_samples=300, centers=3,
cluster_std=0.8, random_state=42)
# K-Means 聚类
kmeans = KMeans(n_clusters=3, random_state=42, n_init=10)
kmeans.fit(X)
# 聚类结果
y_pred = kmeans.labels_
centers = kmeans.cluster_centers_
print(f"聚类中心:")
for i, center in enumerate(centers):
print(f" 簇{i+1}: ({center[0]:.2f}, {center[1]:.2f})")
# 可视化
plt.figure(figsize=(10, 6))
colors = ['#07c160', '#1890ff', '#ff4d4f']
for i in range(3):
mask = y_pred == i
plt.scatter(X[mask, 0], X[mask, 1], c=colors[i], alpha=0.5, label=f'簇 {i+1}')
# 画中心点
plt.scatter(centers[:, 0], centers[:, 1], c='black', marker='X', s=200, linewidths=2, label='中心点')
plt.xlabel('特征 1')
plt.ylabel('特征 2')
plt.title('K-Means 聚类结果')
plt.legend()
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.savefig('kmeans_clustering.png', dpi=150)
plt.show()K-Means 需要你提前指定 K(簇的数量),但怎么知道该分几组?肘部法则来帮忙:
# 用鸢尾花数据集
iris = load_iris()
X = iris.data
# 测试不同的 K 值
inertias = []
K_range = range(1, 10)
for k in K_range:
km = KMeans(n_clusters=k, random_state=42, n_init=10)
km.fit(X)
inertias.append(km.inertia_)
# 画肘部图
plt.figure(figsize=(8, 5))
plt.plot(K_range, inertias, 'bo-', linewidth=2)
plt.xlabel('K (簇的数量)')
plt.ylabel('惯性 (Inertia)')
plt.title('肘部法则')
plt.grid(True, alpha=0.3)
# 标注"肘部"
plt.annotate('肘部 (K=3)', xy=(3, inertias[2]),
xytext=(5, inertias[0] * 0.7),
arrowprops=dict(arrowstyle='->', color='red'),
fontsize=12, color='red')
plt.tight_layout()
plt.savefig('elbow_method.png', dpi=150)
plt.show()🎯 肘部法则:惯性(Inertia)衡量的是"数据点到中心的平均距离"。K 越大,惯性越小(分得更细)。当 K 增大到某个值后,惯性下降变缓——这个"拐点"就是最佳 K。图形看起来像胳膊肘,所以叫"肘部法则"。
# 客户分群实战
np.random.seed(42)
n_customers = 200
# 特征:月消费金额、月访问次数
spending = np.concatenate([
np.random.normal(500, 100, 80), # 低消费
np.random.normal(1500, 200, 70), # 中消费
np.random.normal(3000, 300, 50), # 高消费
])
visits = np.concatenate([
np.random.normal(3, 1, 80),
np.random.normal(8, 2, 70),
np.random.normal(15, 3, 50),
])
X_customers = np.column_stack([spending, visits])
# 聚类
kmeans = KMeans(n_clusters=3, random_state=42, n_init=10)
labels = kmeans.fit_predict(X_customers)
# 分析每组特征
for i in range(3):
mask = labels == i
avg_spending = X_customers[mask, 0].mean()
avg_visits = X_customers[mask, 1].mean()
count = mask.sum()
print(f"簇 {i+1}: {count}人, 平均消费{avg_spending:.0f}元, 平均访问{avg_visits:.1f}次")
# 给每组命名
group_names = {}
for i in range(3):
avg = X_customers[labels == i, 0].mean()
if avg < 800:
group_names[i] = "低价值客户"
elif avg < 2000:
group_names[i] = "中价值客户"
else:
group_names[i] = "高价值客户"
print("\n客户分群结果:")
for i, name in group_names.items():
print(f" 簇{i+1} → {name}")昨天我们用 train_test_split 把数据分成训练集和测试集。但这样有个问题:你只测了一次。如果你运气不好,测试集刚好特别简单或特别难呢?
交叉验证(Cross Validation)的解决方案:把数据切成 K 份,轮流用每一份当测试集,其余当训练集,最后取平均分。
数据分成5份:
第1轮: [测试] [训练] [训练] [训练] [训练] → 得分 0.95
第2轮: [训练] [测试] [训练] [训练] [训练] → 得分 0.92
第3轮: [训练] [训练] [测试] [训练] [训练] → 得分 0.97
第4轮: [训练] [训练] [训练] [测试] [训练] → 得分 0.93
第5轮: [训练] [训练] [训练] [训练] [测试] → 得分 0.96
平均: 0.946 ± 0.018from sklearn.model_selection import cross_val_score
# 鸢尾花数据集
iris = load_iris()
X, y = iris.data, iris.target
# 决策树 + 5折交叉验证
tree = DecisionTreeClassifier(max_depth=3, random_state=42)
scores = cross_val_score(tree, X, y, cv=5, scoring='accuracy')
print("5折交叉验证结果:")
for i, score in enumerate(scores):
print(f" 第{i+1}折: {score:.4f}")
print(f" 平均: {scores.mean():.4f} ± {scores.std():.4f}")
# 随机森林 + 5折交叉验证
rf = RandomForestClassifier(n_estimators=100, random_state=42)
scores_rf = cross_val_score(rf, X, y, cv=5, scoring='accuracy')
print(f"\n随机森林: {scores_rf.mean():.4f} ± {scores_rf.std():.4f}")from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.svm import SVC
from sklearn.neighbors import KNeighborsClassifier
# 对比多种算法
models = {
'决策树(depth=3)': DecisionTreeClassifier(max_depth=3, random_state=42),
'决策树(depth=5)': DecisionTreeClassifier(max_depth=5, random_state=42),
'随机森林(100)': RandomForestClassifier(n_estimators=100, random_state=42),
'KNN(k=5)': KNeighborsClassifier(n_neighbors=5),
'SVM': SVC(random_state=42),
}
print("算法对比 (5折交叉验证):")
print("-" * 50)
results = {}
for name, model in models.items():
scores = cross_val_score(model, X, y, cv=5, scoring='accuracy')
results[name] = scores.mean()
print(f" {name:>20s}: {scores.mean():.4f} ± {scores.std():.4f}")
best = max(results, key=results.get)
print(f"\n🏆 最佳模型: {best} ({results[best]:.4f})")# 完整对比:在乳腺癌数据集上测试
from sklearn.datasets import load_breast_cancer
from sklearn.linear_model import LogisticRegression
data = load_breast_cancer()
X, y = data.data, data.target
models = {
'逻辑回归': LogisticRegression(max_iter=10000),
'决策树(depth=3)': DecisionTreeClassifier(max_depth=3),
'决策树(depth=5)': DecisionTreeClassifier(max_depth=5),
'随机森林(100)': RandomForestClassifier(n_estimators=100),
'KNN(k=5)': KNeighborsClassifier(n_neighbors=5),
'SVM': SVC(),
}
print("=" * 55)
print(f"{'模型':>20s} | {'准确率':>8s} | {'标准差':>8s}")
print("-" * 55)
for name, model in models.items():
scores = cross_val_score(model, X, y, cv=5, scoring='accuracy')
print(f"{name:>20s} | {scores.mean():>7.4f} | {scores.std():>7.4f}")
print("=" * 55)⚠️ 没有"万能算法":不同数据集适合不同的算法。决策树可解释性强,随机森林性能稳定,SVM 在高维数据上表现好。实际项目中,要多试几种,用交叉验证选最好的。
注意:KNN 和 SVM 对特征量纲非常敏感,使用前务必做 StandardScaler 标准化,否则准确率会明显偏低。
用 datasets.load_digits() 数据集,分别训练决策树和随机森林,对比准确率。
自己生成 200 个客户的消费数据(月消费 + 访问次数),用 K-Means 分成 4 群,分析每群特征。
用交叉验证测试随机森林不同的 n_estimators(10, 50, 100, 200),找出最佳值。
🎯 一句话总结:决策树是"一棵树的智慧",随机森林是"一群树的投票",K-Means 是"没有答案的分组",交叉验证是"多考几次取平均"。
🔮 明天预告:Day88 我们将运用所学知识完成一个完整的房价预测项目——从数据探索、特征工程到模型训练、评估、预测,走完一个机器学习项目的全流程!