KNN到随机森林:常用机器学习算法的直观理解与实战

玄同 765

大语言模型 (LLM) 开发工程师 | 中国传媒大学 · 数字媒体技术(智能交互与游戏设计)

CSDN · 个人主页 | GitHub · Follow


关于作者

  • 深耕领域:大语言模型开发 / RAG 知识库 / AI Agent 落地 / 模型微调
  • 技术栈:Python | RAG (LangChain / Dify + Milvus) | FastAPI + Docker
  • 工程能力:专注模型工程化部署、知识库构建与优化,擅长全流程解决方案

「让 AI 交互更智能,让技术落地更高效」
欢迎技术探讨与项目合作,解锁大模型与智能交互的无限可能!


KNN到随机森林:常用机器学习算法的直观理解与实战

引言:为什么需要这么多种算法?

在上一篇文章中,我们介绍了机器学习的基本概念,理解了什么是监督学习、损失函数和梯度下降。现在你可能会想:既然原理都差不多,为什么需要这么多种不同的算法?

想象一下,你要买一辆车。不同的场景下,你需要不同的车:城市通勤可能需要小巧灵活的小轿车;全家出游可能需要空间大的SUV;越野探险则需要底盘高的吉普车。没有哪种车是"最好的",只有最适合特定场景的。

机器学习算法也是一样的道理。每种算法都有它擅长的领域和局限性。KNN简单直观,但对大数据不友好;决策树容易解释,但容易过拟合;随机森林强大稳定,但有时候过于复杂难以解释。

今天,让我们深入理解每种算法的"脾性",学会在不同场景下选择最合适的武器。我会用大量的图解和代码示例,让你不仅"知道"这些算法是什么,更要"理解"它们为什么有效。


零、前置知识:上篇文章核心回顾

0.1 监督学习的核心框架

在深入算法之前,让我们快速回顾一下监督学习的核心框架。如果你对这部分还不太熟悉,建议先阅读《从猜数游戏到模型训练:机器学习核心概念的无痛入门》

监督学习的本质是:根据输入数据 X X X 和对应的正确答案 y y y,学习一个从 X X X y y y 的映射函数。训练时,模型预测 y ^ \hat{y} y^,计算预测误差 y − y ^ y - \hat{y} yy^,然后通过优化算法(如梯度下降)调整模型参数,让误差越来越小。

优化

预测

模型

数据

输入特征

真实标签

待训练模型

预测值

损失函数

梯度

参数更新

0.2 分类与回归的区别

在开始算法讲解之前,我们需要区分两类最基本的任务:分类回归

分类是预测一个离散的类别标签。比如判断邮件是"垃圾"还是"正常"、图片是"猫"还是"狗"。输出是有限的几个类别。

回归是预测一个连续的值。比如预测房价(可以是100万、150万、203.5万)、预测明天的温度(可能是23.5度、24.2度)。

回归任务

输入房屋特征

输出房价数值

连续数值

分类任务

输入邮件特征

输出垃圾或正常

离散标签

这两种任务虽然目标不同,但背后的学习原理是相通的——都是通过调整模型参数,让预测值尽可能接近真实值。


一、KNN:最直观也最"懒"的算法

1.1 "物以类聚"的数学表达

K近邻(K-Nearest Neighbors,简称KNN)是我见过的最符合直觉的机器学习算法之一。它的核心思想用一句话就能概括:如果你周围的邻居大多属于某个类别,那你也大概率属于这个类别

这听起来像是一句废话,但正是这种朴素的直觉,构成了一个完整机器学习算法的核心。

让我用一个具体的例子来说明。假设我们在二维平面上有一些点,有红色和蓝色两类。现在有一个新的点(绿色)需要分类。

投票结果

距离计算

待分类点

已知类别点

红点1

红点2

红点3

蓝点1

蓝点2

绿点

计算到各点距离

红2票蓝1票判为红色

1.2 距离度量:如何定义"近"?

在KNN中,“近"是由距离决定的。最常用的距离是欧氏距离,也就是我们平常意义上"两点之间的直线距离”。

对于两个点 A ( x 1 , y 1 ) A(x_1, y_1) A(x1,y1) B ( x 2 , y 2 ) B(x_2, y_2) B(x2,y2),欧氏距离是:

d ( A , B ) = ( x 1 − x 2 ) 2 + ( y 1 − y 2 ) 2 d(A, B) = \sqrt{(x_1 - x_2)^2 + (y_1 - y_2)^2} d(A,B)=(x1x2)2+(y1y2)2

距离计算

两点坐标

点A坐标x1y1

点B坐标x2y2

计算x差值

计算y差值

平方后相加

开方得距离

除了欧氏距离,还有一些其他常用的距离度量:

曼哈顿距离:像在城市街区中开车,只能沿格子状的街道行驶,不能斜穿:

d ( A , B ) = ∣ x 1 − x 2 ∣ + ∣ y 1 − y 2 ∣ d(A, B) = |x_1 - x_2| + |y_1 - y_2| d(A,B)=x1x2+y1y2

余弦相似度:衡量两个向量方向的相似程度,与绝对距离无关:

cos ⁡ ( θ ) = A ⋅ B ∣ A ∣ × ∣ B ∣ \cos(\theta) = \frac{A \cdot B}{|A| \times |B|} cos(θ)=A×BAB

余弦相似度

方向比数值重要

文本分类场景

曼哈顿距离

适用高维数据

特征含义不同

欧氏距离

适用连续变量

特征尺度相近

1.3 K值的选择:邻居数量的影响

KNN中的K代表"选择多少个最近邻居"来投票。K值的选择对结果有很大影响。

K值太小(比如K=1):模型会变得非常敏感,容易受到个别异常点的影响。这就像只听从离你最近的那个人的意见,风险很大。

K值太大:会考虑太多邻居,可能把一些不太相关的点也纳入考虑,导致决策边界变得模糊。

K太大

看20个邻居

可能欠拟合

K适中

看5个邻居

抗噪声能力强

K太小

看1个邻居

容易过拟合

在实际应用中,通常通过交叉验证来选择最优的K值。常见的做法是尝试K=1, 3, 5, 7, 9, …等奇数(奇数是为了避免平票),选择验证集上表现最好的K值。

1.4 KNN的优缺点与适用场景

不适用场景

大数据集

实时预测

适用场景

小数据集

需要可解释性

缺点

预测速度慢

对大数据不友好

对异常值敏感

优点

简单直观

无需训练

适合多分类

为什么KNN预测速度慢? 因为它没有显式的"训练"过程。训练时只是把数据存储起来,预测时需要计算待分类点与所有训练点的距离,时间复杂度是O(n)。

特征尺度为什么重要? 假设我们用两个特征预测房价:面积(几十到几百)和房间数(1到5)。面积的范围远大于房间数,如果不做标准化,距离计算会被面积主导。

1.5 KNN实战代码

import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import classification_report, confusion_matrix

# 1. 生成模拟数据
# make_classification是sklearn提供的模拟数据生成函数
# n_samples=500: 生成500个样本
# n_features=2: 2个特征(方便可视化)
# n_informative=2: 2个有信息量的特征
# n_redundant=0: 没有冗余特征
# n_classes=2: 2个类别
X, y = make_classification(
    n_samples=500, 
    n_features=2,
    n_informative=2,
    n_redundant=0,
    n_classes=2,
    random_state=42
)

# 2. 数据划分:80%训练,20%测试
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

# 3. 特征标准化(对KNN非常重要!)
# 因为KNN依赖距离计算,如果特征尺度不统一,
# 尺度大的特征会主导距离计算
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)  # 用训练集fit,transform训练集和测试集
X_test_scaled = scaler.transform(X_test)

# 4. 使用交叉验证选择最优K值
k_values = range(1, 31, 2)  # 尝试K=1,3,5,...,29
cv_scores = []

for k in k_values:
    # n_jobs=-1: 使用所有CPU核心加速
    # cv=5: 5折交叉验证
    knn = KNeighborsClassifier(n_neighbors=k, n_jobs=-1)
    scores = cross_val_score(knn, X_train_scaled, y_train, cv=5, scoring='accuracy')
    cv_scores.append(scores.mean())
    print(f"K={k:2d}: 平均准确率 = {scores.mean():.4f} (+/- {scores.std()*2:.4f})")

# 找到最优K值
best_k = k_values[np.argmax(cv_scores)]
print(f"\n最优K值: {best_k}")

# 5. 用最优K值训练最终模型
final_knn = KNeighborsClassifier(n_neighbors=best_k)
final_knn.fit(X_train_scaled, y_train)

# 6. 在测试集上评估
y_pred = final_knn.predict(X_test_scaled)

print("\n分类报告:")
print(classification_report(y_test, y_pred))

print("\n混淆矩阵:")
print(confusion_matrix(y_test, y_pred))

# 7. 可视化决策边界
def plot_decision_boundary(model, X, y, title):
    """绘制决策边界的辅助函数"""
    h = 0.02  # 网格步长
    x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
    y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
    
    xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
                         np.arange(y_min, y_max, h))
    
    Z = model.predict(np.c_[xx.ravel(), yy.ravel()])
    Z = Z.reshape(xx.shape)
    
    plt.figure(figsize=(10, 8))
    plt.contourf(xx, yy, Z, alpha=0.4)
    plt.scatter(X[:, 0], X[:, 1], c=y, alpha=0.8)
    plt.title(title)
    plt.show()

plot_decision_boundary(final_knn, X_test_scaled, y_test, 
                       f'KNN Decision Boundary (K={best_k})')

二、线性回归与逻辑回归:从猜数字到分类

2.1 线性回归:找到最佳拟合直线

线性回归是最基础也是最重要的回归算法。它的目标很简单:找到一条直线,使得所有数据点到这条直线的"距离之和"最小

想象你在白纸上撒了一把米粒,现在要用一条直线来概括这些米粒的分布趋势。线性回归就是找到那条"最合适"的直线。

残差计算

拟合直线

散点数据

数据点1

数据点2

数据点3

数据点4

最佳拟合直线

计算各点误差

误差平方和最小

线性回归的数学形式是:

y = w 1 x 1 + w 2 x 2 + . . . + w n x n + b y = w_1 x_1 + w_2 x_2 + ... + w_n x_n + b y=w1x1+w2x2+...+wnxn+b

其中 w 1 , w 2 , . . . , w n w_1, w_2, ..., w_n w1,w2,...,wn 是权重, b b b 是偏置。对于只有一个特征的情况,简化为 y = w x + b y = wx + b y=wx+b

2.2 梯度下降求解线性回归

在线性回归中,我们通过最小化均方误差(MSE)来找到最优参数:

M S E = 1 n ∑ i = 1 n ( y i − y ^ i ) 2 MSE = \frac{1}{n} \sum_{i=1}^{n}(y_i - \hat{y}_i)^2 MSE=n1i=1n(yiy^i)2

收敛

迭代2

迭代1

初始化

随机参数

计算梯度

更新参数

计算梯度

更新参数

最优参数

2.3 逻辑回归:分类而非回归

逻辑回归的名字里有"回归",但实际上是一个分类算法。它之所以叫"回归",是因为它最初源于回归模型,后来被扩展用于分类。

逻辑回归的核心思想是:将线性回归的输出压缩到0-1之间,作为属于正类的概率

这个压缩操作是通过Sigmoid函数完成的:

σ ( z ) = 1 1 + e − z \sigma(z) = \frac{1}{1 + e^{-z}} σ(z)=1+ez1

二分类决策

Sigmoid曲线

输入任意值

输出0到1之间

大于0.5判为类别1

小于0.5判为类别0

为什么需要Sigmoid函数?

因为线性回归的输出是 ( − ∞ , + ∞ ) (-\infty, +\infty) (,+) 的任意值,但我们需要的是概率,必须在 [ 0 , 1 ] [0, 1] [0,1] 范围内。Sigmoid函数恰好完成了这个映射: z z z 很大时输出接近1, z z z 很小时输出接近0。

2.4 线性回归与逻辑回归对比

逻辑回归

输入特征

线性组合

Sigmoid压缩

输出概率

线性回归

输入特征

线性组合

输出连续值


三、决策树:自动生成if-else规则

3.1 用决策树理解世界

决策树是我最喜欢的算法之一,因为它模拟了人类做决策的思维方式。你可以把决策树理解为一连串的if-else规则,只不过这些规则不是人工设计的,而是算法从数据中自动学习出来的。

想象一个医生诊断病人的过程:医生会问"发烧吗?"如果发烧,再问"咳嗽吗?“如果咳嗽,再问"持续多久?”…这个不断提问、不断缩小范围的过程,就是决策树的核心思想。

收入大于5000

信用分大于700

拒绝

批准

有房产

批准

人工审核

3.2 信息增益:如何选择最佳分割特征?

决策树的核心问题之一是:每一步应该选择哪个特征来分割?

直觉告诉我们,应该选择那个"最能区分数据"的特征。比如在贷款审批中,"收入"比"最喜欢的颜色"更能区分申请人是否应该获批。

为了量化这个"区分能力",我们引入信息熵的概念。

衡量的是一个集合的"混乱程度"。如果集合中所有元素都属于同一类,熵为0(最有序);如果各类均匀分布,熵最大(最混乱)。

H ( S ) = − ∑ i = 1 c p i log ⁡ 2 ( p i ) H(S) = -\sum_{i=1}^{c} p_i \log_2(p_i) H(S)=i=1cpilog2(pi)

集合C

8红2蓝

熵约0.72

集合B

5红5蓝

熵为1最混乱

集合A

10个红球

熵为0最有序

信息增益是分割前的熵减去分割后各子集熵的加权和。信息增益越大,说明这个分割让数据变得更纯净。

I G ( S , A ) = H ( S ) − ∑ v ∈ V a l u e s ( A ) ∣ S v ∣ ∣ S ∣ H ( S v ) IG(S, A) = H(S) - \sum_{v \in Values(A)} \frac{|S_v|}{|S|} H(S_v) IG(S,A)=H(S)vValues(A)SSvH(Sv)

信息增益

分割后

分割前

整体熵值

子集1占比60%

子集2占比40%

分割前后熵差

3.3 决策树的三大问题与解决方案

问题1:树太深 → 过拟合

如果决策树无限生长,它可能会记住每一个训练样本的细节,包括噪声。这就像一个学生背下了教科书上的每一道题,但不会做新题。

解决方案:剪枝

  • 预剪枝:设置树的最大深度、节点最小样本数等,提前停止生长
  • 后剪枝:先让树充分生长,再从叶节点向上剪除

后剪枝

先生长到最大

自底向上剪枝

预剪枝

限制最大深度

限制最小样本数

问题2:特征多时容易偏向取值多的特征

如果某个特征有100个取值(像身份证号),按它分割会得到很多单样本的节点,看起来信息增益很大,但实际上这是过拟合。

解决方案:使用信息增益率而非信息增益

G a i n R a t i o ( A ) = I G ( S , A ) S p l i t I n f o ( A ) GainRatio(A) = \frac{IG(S, A)}{SplitInfo(A)} GainRatio(A)=SplitInfo(A)IG(S,A)

其中 S p l i t I n f o ( A ) SplitInfo(A) SplitInfo(A) 是分割信息,惩罚取值多的特征。

问题3:连续值特征如何处理?

对于"面积 > 100㎡"这样的连续值条件,需要先离散化。常见做法是尝试所有可能的分割点,选择信息增益最大的。

3.4 决策树实战代码

from sklearn.datasets import load_iris
from sklearn.tree import DecisionTreeClassifier, plot_tree
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, classification_report
import matplotlib.pyplot as plt

# 1. 加载鸢尾花数据集
# 经典数据集,包含3种鸢尾花的4个特征
iris = load_iris()
X, y = iris.data, iris.target
feature_names = iris.feature_names  # ['sepal length (cm)', 'sepal width (cm)', ...]
target_names = iris.target_names   # ['setosa', 'versicolor', 'virginica']

# 2. 数据划分
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

# 3. 训练决策树(带预剪枝防止过拟合)
# criterion='entropy' 使用信息增益
# max_depth=4 限制树深度
# min_samples_split=5 内部节点最小样本数
# min_samples_leaf=2 叶节点最小样本数
tree_clf = DecisionTreeClassifier(
    criterion='entropy',  # 或 'gini'(基尼系数)
    max_depth=4,
    min_samples_split=5,
    min_samples_leaf=2,
    random_state=42
)
tree_clf.fit(X_train, y_train)

# 4. 评估
y_pred = tree_clf.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
print(f"测试集准确率: {accuracy:.4f}")
print("\n分类报告:")
print(classification_report(y_test, y_pred, target_names=target_names))

# 5. 可视化决策树
plt.figure(figsize=(20, 10))
plot_tree(
    tree_clf,
    feature_names=feature_names,
    class_names=target_names,
    filled=True,  # 用颜色填充
    rounded=True,  # 圆角节点
    fontsize=10
)
plt.title('Decision Tree for Iris Classification')
plt.tight_layout()
plt.savefig('decision_tree.png', dpi=150)
plt.show()

# 6. 特征重要性
print("\n特征重要性:")
for name, importance in zip(feature_names, tree_clf.feature_importances_):
    print(f"  {name}: {importance:.4f}")

四、集成学习:三个臭皮匠赛过诸葛亮

4.1 集成学习的核心思想

"三个臭皮匠赛过诸葛亮"这句话蕴含着深刻的智慧。在机器学习中,集成学习就是将多个模型的预测组合起来,得到一个更好的预测结果。

为什么集成能提升性能?关键在于多样性和减少过拟合

组合预测

独立预测

多个模型

模型1

模型2

模型3

预测A

预测B

预测C

投票或平均

想象你要决定是否投资一只股票。你会怎么做?你可能会咨询多个分析师,而不是只听一个人的意见。即使每个分析师都可能有偏见或犯错,综合多个意见往往能得到更可靠的判断。

4.2 Bagging vs Boosting:两种集成策略

集成学习主要有两种策略:BaggingBoosting

Bagging(Bootstrap Aggregating):并行训练多个模型,每个模型独立决策,最后投票。核心理念是"减少方差"。

Boosting:串行训练多个模型,每个模型专注于纠正前一个模型的错误。核心理念是"减少偏差"。

Boosting串行

串行训练

逐步纠错

加权投票

Bagging并行

Bootstrap采样

独立训练

投票平均

4.3 随机森林:Bagging的代表作

随机森林是Bagging策略最成功的实现之一。它的"随机"体现在两个地方:

  1. 行采样:每个决策树只用到一部分样本(有放回抽样)
  2. 列采样:每个决策树只用到一部分特征

投票决策

训练决策树

数据采样

原始数据

子集1

子集2

子集3

树1

树2

树3

最终结果

为什么随机森林不容易过拟合?

因为每棵树只看到了一部分数据,而且只用到了一部分特征。这种"盲人摸象"式的学习,使得每棵树都是有偏的,但当很多有偏的树组合起来时,偏差会相互抵消,最终得到一个低方差(稳定)的预测。

4.4 XGBoost:Boosting的代表作

XGBoost(eXtreme Gradient Boosting)是Boosting策略的巅峰之作,在Kaggle等竞赛中几乎统治了表格数据任务长达数年。

XGBoost的核心思想是:每一棵新树都去学习前面所有树的预测误差,逐步减少残差。

第3棵树

第2棵树

第1棵树

初始预测

学习残差

计算残差

纠正残差

再次计算残差

继续纠正

XGBoost之所以强大,还因为它做了大量工程优化:

  • 正则化:防止过拟合
  • 缺失值处理:自动学习最优方向
  • 特征并行:高效处理高维数据
  • 缓存感知:优化内存访问

4.5 随机森林与XGBoost对比

XGBoost

Boosting策略

串行训练

精度更高

随机森林

Bagging策略

并行训练

不易过拟合

4.6 集成学习实战代码

from sklearn.datasets import make_classification
from sklearn.model_selection import cross_val_score
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from xgboost import XGBClassifier
from sklearn.metrics import accuracy_score
import numpy as np

# 1. 生成模拟数据
X, y = make_classification(
    n_samples=1000,
    n_features=20,
    n_informative=15,
    n_redundant=5,
    n_classes=2,
    random_state=42
)

# 2. 对比三种算法
models = {
    'RandomForest': RandomForestClassifier(n_estimators=100, random_state=42),
    'GradientBoosting': GradientBoostingClassifier(n_estimators=100, random_state=42),
    'XGBoost': XGBClassifier(n_estimators=100, random_state=42, use_label_encoder=False, eval_metric='logloss')
}

print("=" * 50)
print("集成学习算法对比")
print("=" * 50)

for name, model in models.items():
    # 5折交叉验证评估
    cv_scores = cross_val_score(model, X, y, cv=5, scoring='accuracy')
    print(f"\n{name}:")
    print(f"  交叉验证准确率: {cv_scores.mean():.4f} (+/- {cv_scores.std()*2:.4f})")
    
    # 训练并测试
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
    model.fit(X_train, y_train)
    y_pred = model.predict(X_test)
    test_acc = accuracy_score(y_test, y_pred)
    print(f"  测试集准确率: {test_acc:.4f}")

# 3. XGBoost调参示例
print("\n" + "=" * 50)
print("XGBoost超参数调优")
print("=" * 50)

# 常用XGBoost参数
param_grid = {
    'n_estimators': [50, 100, 200],
    'max_depth': [3, 5, 7],
    'learning_rate': [0.01, 0.1, 0.2],
    'subsample': [0.8, 1.0]
}

# 简单网格搜索(实际项目中建议用随机搜索或贝叶斯优化)
best_score = 0
best_params = {}

for n_est in param_grid['n_estimators']:
    for max_d in param_grid['max_depth']:
        for lr in param_grid['learning_rate']:
            for ss in param_grid['subsample']:
                xgb = XGBClassifier(
                    n_estimators=n_est,
                    max_depth=max_d,
                    learning_rate=lr,
                    subsample=ss,
                    random_state=42,
                    use_label_encoder=False,
                    eval_metric='logloss'
                )
                scores = cross_val_score(xgb, X, y, cv=3, scoring='accuracy')
                mean_score = scores.mean()
                
                if mean_score > best_score:
                    best_score = mean_score
                    best_params = {
                        'n_estimators': n_est,
                        'max_depth': max_d,
                        'learning_rate': lr,
                        'subsample': ss
                    }

print(f"\n最优参数: {best_params}")
print(f"最优交叉验证准确率: {best_score:.4f}")

五、实战项目:客户流失预测全流程

5.1 项目背景

让我用一个完整的实战项目来串联所有学到的算法。假设你在一家电信公司工作,老板让你预测哪些客户可能会流失(取消服务)。

这是一个经典的二分类问题:你需要根据客户的历史行为数据,预测他们是否会流失。

5.2 数据探索

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.datasets import make_classification

# 模拟电信客户数据(实际项目中用真实数据)
# make_classification生成模拟的二分类数据
X, y = make_classification(
    n_samples=1000,
    n_features=10,
    n_informative=8,
    n_redundant=2,
    n_classes=2,
    weights=[0.7, 0.3],  # 70%不流失,30%流失(类别不平衡)
    random_state=42
)

# 创建特征名
feature_names = [
    '月消费', '在网时长', '投诉次数', '套餐等级',
    '使用流量', '国际通话时长', '客服联系次数',
    '欠费次数', '转套餐次数', '年龄'
]

df = pd.DataFrame(X, columns=feature_names)
df['流失'] = y

print("=" * 50)
print("数据概览")
print("=" * 50)
print(f"样本数: {len(df)}")
print(f"特征数: {len(feature_names)}")
print(f"\n流失分布:")
print(df['流失'].value_counts())
print(f"\n流失率: {df['流失'].mean()*100:.1f}%")

# 查看基本统计信息
print("\n" + "=" * 50)
print("特征统计")
print("=" * 50)
print(df.describe().round(2))

5.3 数据预处理

from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split

# 1. 处理类别不平衡
# 由于流失客户只占30%,我们需要处理类别不平衡问题
# 方法1: 过采样少数类(SMOTE)
# 方法2: 欠采样多数类
# 方法3: 调整分类阈值

# 2. 特征标准化
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# 3. 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(
    X_scaled, y, test_size=0.2, random_state=42, stratify=y
)

print(f"训练集: {len(X_train)} 样本")
print(f"测试集: {len(X_test)} 样本")
print(f"训练集流失率: {y_train.mean()*100:.1f}%")
print(f"测试集流失率: {y_test.mean()*100:.1f}%")

5.4 多模型对比

from sklearn.neighbors import KNeighborsClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from xgboost import XGBClassifier
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score

# 定义模型
models = {
    'KNN': KNeighborsClassifier(n_neighbors=5),
    '逻辑回归': LogisticRegression(max_iter=1000),
    '决策树': DecisionTreeClassifier(max_depth=5, random_state=42),
    '随机森林': RandomForestClassifier(n_estimators=100, max_depth=5, random_state=42),
    '梯度提升': GradientBoostingClassifier(n_estimators=100, max_depth=5, random_state=42),
    'XGBoost': XGBClassifier(n_estimators=100, max_depth=5, random_state=42, 
                             use_label_encoder=False, eval_metric='logloss')
}

# 训练和评估
print("=" * 80)
print("模型对比结果")
print("=" * 80)
print(f"{'模型':<15} {'准确率':<10} {'精确率':<10} {'召回率':<10} {'F1分数':<10} {'AUC':<10}")
print("-" * 80)

results = []
for name, model in models.items():
    # 训练
    model.fit(X_train, y_train)
    
    # 预测
    y_pred = model.predict(X_test)
    y_prob = model.predict_proba(X_test)[:, 1]  # 流失的概率
    
    # 计算指标
    acc = accuracy_score(y_test, y_pred)
    prec = precision_score(y_test, y_pred)
    rec = recall_score(y_test, y_pred)
    f1 = f1_score(y_test, y_pred)
    auc = roc_auc_score(y_test, y_prob)
    
    results.append({
        'model': name,
        'accuracy': acc,
        'precision': prec,
        'recall': rec,
        'f1': f1,
        'auc': auc
    })
    
    print(f"{name:<15} {acc:.4f}      {prec:.4f}      {rec:.4f}      {f1:.4f}      {auc:.4f}")

# 找出最佳模型
best_model = max(results, key=lambda x: x['f1'])
print("\n" + "=" * 80)
print(f"最佳模型: {best_model['model']} (F1={best_model['f1']:.4f})")

5.5 业务解读

# 假设XGBoost是最佳模型,分析特征重要性
best_xgb = models['XGBoost']
feature_importance = pd.DataFrame({
    '特征': feature_names,
    '重要性': best_xgb.feature_importances_
}).sort_values('重要性', ascending=False)

print("\n" + "=" * 50)
print("特征重要性排名(业务解读)")
print("=" * 50)
for i, row in feature_importance.iterrows():
    bar = "█" * int(row['重要性'] * 50)
    print(f"{row['特征']:<12}: {row['重要性']:.4f} {bar}")

# 业务建议
print("\n" + "=" * 50)
print("业务建议")
print("=" * 50)
top3 = feature_importance.head(3)['特征'].tolist()
if '投诉次数' in top3:
    print("• 重点关注投诉次数多的客户,及时跟进处理")
if '客服联系次数' in top3:
    print("• 频繁联系客服的客户可能有问题,需主动关怀")
if '月消费' in top3:
    print("• 高消费客户流失风险也高,提供VIP服务挽留")
if '在网时长' in top3:
    print("• 新客户流失风险高,加强入网前3个月的服务")

六、总结与展望

六句话核心总结

第一句:KNN是最直观的算法,"近朱者赤近墨者黑"是其核心哲学。但预测速度慢,不适合大数据。

第二句:线性回归找最佳拟合直线,逻辑回归用Sigmoid将输出转为概率。前者用于回归,后者用于分类。

第三句:决策树自动学习if-else规则,信息增益帮助选择最优分割特征。但容易过拟合,需要剪枝。

第四句:集成学习将多个模型组合,“三个臭皮匠赛过诸葛亮”。Bagging减少方差,Boosting减少偏差。

第五句:随机森林是Bagging的代表作,通过数据采样和特征采样让每棵树独立学习。XGBoost是Boosting的巅峰,在表格数据上几乎无敌。

第六句:没有"最好的算法",只有"最适合的算法"。根据数据量、特征类型、任务需求选择合适的模型。

算法选择决策树

小规模

大规模

普通

二分类

多分类

普通

表格数据

文本图像

开始选择算法

数据集大小

需要可解释性

实时性要求

决策树或逻辑回归

类别数

线性模型或KNN

精度要求

特征类型

随机森林

XGBoost或LightGBM

深度学习

部署模型


下篇预告

在下一篇文章中,我们将进入深度学习的世界,从神经元开始,一步步理解神经网络的工作原理。你将了解到:

  • 人工神经元是如何模拟生物神经元的
  • 激活函数为什么能带来非线性
  • 前向传播和反向传播是如何工作的
  • 如何用PyTorch实现第一个神经网络

敬请期待:《从神经元到神经网络:深度学习的本质》


参考资料

  • Scikit-learn官方文档:https://scikit-learn.org/stable/
  • XGBoost官方文档:https://xgboost.readthedocs.io/
  • 机器学习实战:Peter Harrington
  • Kaggle入门:https://www.kaggle.com/learn

如果觉得有帮助,欢迎转发给需要的朋友!

KNN到随机森林:常用机器学习算法的直观理解与实战

引言:为什么需要这么多种算法?

在上一篇文章中,我们介绍了机器学习的基本概念,理解了什么是监督学习、损失函数和梯度下降。现在你可能会想:既然原理都差不多,为什么需要这么多种不同的算法?

想象一下,你要买一辆车。不同的场景下,你需要不同的车:城市通勤可能需要小巧灵活的小轿车;全家出游可能需要空间大的SUV;越野探险则需要底盘高的吉普车。没有哪种车是"最好的",只有最适合特定场景的。

机器学习算法也是一样的道理。每种算法都有它擅长的领域和局限性。KNN简单直观,但对大数据不友好;决策树容易解释,但容易过拟合;随机森林强大稳定,但有时候过于复杂难以解释。

今天,让我们深入理解每种算法的"脾性",学会在不同场景下选择最合适的武器。我会用大量的图解和代码示例,让你不仅"知道"这些算法是什么,更要"理解"它们为什么有效。


零、前置知识:上篇文章核心回顾

0.1 监督学习的核心框架

在深入算法之前,让我们快速回顾一下监督学习的核心框架。如果你对这部分还不太熟悉,建议先阅读《从猜数游戏到模型训练:机器学习核心概念的无痛入门》

监督学习的本质是:根据输入数据 X X X 和对应的正确答案 y y y,学习一个从 X X X y y y 的映射函数。训练时,模型预测 y ^ \hat{y} y^,计算预测误差 y − y ^ y - \hat{y} yy^,然后通过优化算法(如梯度下降)调整模型参数,让误差越来越小。

优化

预测

模型

数据

输入特征

真实标签

待训练模型

预测值

损失函数

梯度

参数更新

0.2 分类与回归的区别

在开始算法讲解之前,我们需要区分两类最基本的任务:分类回归

分类是预测一个离散的类别标签。比如判断邮件是"垃圾"还是"正常"、图片是"猫"还是"狗"。输出是有限的几个类别。

回归是预测一个连续的值。比如预测房价(可以是100万、150万、203.5万)、预测明天的温度(可能是23.5度、24.2度)。

回归任务

输入房屋特征

输出房价数值

连续数值

分类任务

输入邮件特征

输出垃圾或正常

离散标签

这两种任务虽然目标不同,但背后的学习原理是相通的——都是通过调整模型参数,让预测值尽可能接近真实值。


一、KNN:最直观也最"懒"的算法

1.1 "物以类聚"的数学表达

K近邻(K-Nearest Neighbors,简称KNN)是我见过的最符合直觉的机器学习算法之一。它的核心思想用一句话就能概括:如果你周围的邻居大多属于某个类别,那你也大概率属于这个类别

这听起来像是一句废话,但正是这种朴素的直觉,构成了一个完整机器学习算法的核心。

让我用一个具体的例子来说明。假设我们在二维平面上有一些点,有红色和蓝色两类。现在有一个新的点(绿色)需要分类。

投票结果

距离计算

待分类点

已知类别点

红点1

红点2

红点3

蓝点1

蓝点2

绿点

计算到各点距离

红2票蓝1票判为红色

1.2 距离度量:如何定义"近"?

在KNN中,“近"是由距离决定的。最常用的距离是欧氏距离,也就是我们平常意义上"两点之间的直线距离”。

对于两个点 A ( x 1 , y 1 ) A(x_1, y_1) A(x1,y1) B ( x 2 , y 2 ) B(x_2, y_2) B(x2,y2),欧氏距离是:

d ( A , B ) = ( x 1 − x 2 ) 2 + ( y 1 − y 2 ) 2 d(A, B) = \sqrt{(x_1 - x_2)^2 + (y_1 - y_2)^2} d(A,B)=(x1x2)2+(y1y2)2

距离计算

两点坐标

点A坐标x1y1

点B坐标x2y2

计算x差值

计算y差值

平方后相加

开方得距离

除了欧氏距离,还有一些其他常用的距离度量:

曼哈顿距离:像在城市街区中开车,只能沿格子状的街道行驶,不能斜穿:

d ( A , B ) = ∣ x 1 − x 2 ∣ + ∣ y 1 − y 2 ∣ d(A, B) = |x_1 - x_2| + |y_1 - y_2| d(A,B)=x1x2+y1y2

余弦相似度:衡量两个向量方向的相似程度,与绝对距离无关:

cos ⁡ ( θ ) = A ⋅ B ∣ A ∣ × ∣ B ∣ \cos(\theta) = \frac{A \cdot B}{|A| \times |B|} cos(θ)=A×BAB

余弦相似度

方向比数值重要

文本分类场景

曼哈顿距离

适用高维数据

特征含义不同

欧氏距离

适用连续变量

特征尺度相近

1.3 K值的选择:邻居数量的影响

KNN中的K代表"选择多少个最近邻居"来投票。K值的选择对结果有很大影响。

K值太小(比如K=1):模型会变得非常敏感,容易受到个别异常点的影响。这就像只听从离你最近的那个人的意见,风险很大。

K值太大:会考虑太多邻居,可能把一些不太相关的点也纳入考虑,导致决策边界变得模糊。

K太大

看20个邻居

可能欠拟合

K适中

看5个邻居

抗噪声能力强

K太小

看1个邻居

容易过拟合

在实际应用中,通常通过交叉验证来选择最优的K值。常见的做法是尝试K=1, 3, 5, 7, 9, …等奇数(奇数是为了避免平票),选择验证集上表现最好的K值。

1.4 KNN的优缺点与适用场景

不适用场景

大数据集

实时预测

适用场景

小数据集

需要可解释性

缺点

预测速度慢

对大数据不友好

对异常值敏感

优点

简单直观

无需训练

适合多分类

为什么KNN预测速度慢? 因为它没有显式的"训练"过程。训练时只是把数据存储起来,预测时需要计算待分类点与所有训练点的距离,时间复杂度是O(n)。

特征尺度为什么重要? 假设我们用两个特征预测房价:面积(几十到几百)和房间数(1到5)。面积的范围远大于房间数,如果不做标准化,距离计算会被面积主导。

1.5 KNN实战代码

import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.preprocessing import StandardScaler
from sklearn.neighbors import KNeighborsClassifier
from sklearn.metrics import classification_report, confusion_matrix

# 1. 生成模拟数据
# make_classification是sklearn提供的模拟数据生成函数
# n_samples=500: 生成500个样本
# n_features=2: 2个特征(方便可视化)
# n_informative=2: 2个有信息量的特征
# n_redundant=0: 没有冗余特征
# n_classes=2: 2个类别
X, y = make_classification(
    n_samples=500, 
    n_features=2,
    n_informative=2,
    n_redundant=0,
    n_classes=2,
    random_state=42
)

# 2. 数据划分:80%训练,20%测试
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

# 3. 特征标准化(对KNN非常重要!)
# 因为KNN依赖距离计算,如果特征尺度不统一,
# 尺度大的特征会主导距离计算
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)  # 用训练集fit,transform训练集和测试集
X_test_scaled = scaler.transform(X_test)

# 4. 使用交叉验证选择最优K值
k_values = range(1, 31, 2)  # 尝试K=1,3,5,...,29
cv_scores = []

for k in k_values:
    # n_jobs=-1: 使用所有CPU核心加速
    # cv=5: 5折交叉验证
    knn = KNeighborsClassifier(n_neighbors=k, n_jobs=-1)
    scores = cross_val_score(knn, X_train_scaled, y_train, cv=5, scoring='accuracy')
    cv_scores.append(scores.mean())
    print(f"K={k:2d}: 平均准确率 = {scores.mean():.4f} (+/- {scores.std()*2:.4f})")

# 找到最优K值
best_k = k_values[np.argmax(cv_scores)]
print(f"\n最优K值: {best_k}")

# 5. 用最优K值训练最终模型
final_knn = KNeighborsClassifier(n_neighbors=best_k)
final_knn.fit(X_train_scaled, y_train)

# 6. 在测试集上评估
y_pred = final_knn.predict(X_test_scaled)

print("\n分类报告:")
print(classification_report(y_test, y_pred))

print("\n混淆矩阵:")
print(confusion_matrix(y_test, y_pred))

# 7. 可视化决策边界
def plot_decision_boundary(model, X, y, title):
    """绘制决策边界的辅助函数"""
    h = 0.02  # 网格步长
    x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
    y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
    
    xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
                         np.arange(y_min, y_max, h))
    
    Z = model.predict(np.c_[xx.ravel(), yy.ravel()])
    Z = Z.reshape(xx.shape)
    
    plt.figure(figsize=(10, 8))
    plt.contourf(xx, yy, Z, alpha=0.4)
    plt.scatter(X[:, 0], X[:, 1], c=y, alpha=0.8)
    plt.title(title)
    plt.show()

plot_decision_boundary(final_knn, X_test_scaled, y_test, 
                       f'KNN Decision Boundary (K={best_k})')

二、线性回归与逻辑回归:从猜数字到分类

2.1 线性回归:找到最佳拟合直线

线性回归是最基础也是最重要的回归算法。它的目标很简单:找到一条直线,使得所有数据点到这条直线的"距离之和"最小

想象你在白纸上撒了一把米粒,现在要用一条直线来概括这些米粒的分布趋势。线性回归就是找到那条"最合适"的直线。

残差计算

拟合直线

散点数据

数据点1

数据点2

数据点3

数据点4

最佳拟合直线

计算各点误差

误差平方和最小

线性回归的数学形式是:

y = w 1 x 1 + w 2 x 2 + . . . + w n x n + b y = w_1 x_1 + w_2 x_2 + ... + w_n x_n + b y=w1x1+w2x2+...+wnxn+b

其中 w 1 , w 2 , . . . , w n w_1, w_2, ..., w_n w1,w2,...,wn 是权重, b b b 是偏置。对于只有一个特征的情况,简化为 y = w x + b y = wx + b y=wx+b

2.2 梯度下降求解线性回归

在线性回归中,我们通过最小化均方误差(MSE)来找到最优参数:

M S E = 1 n ∑ i = 1 n ( y i − y ^ i ) 2 MSE = \frac{1}{n} \sum_{i=1}^{n}(y_i - \hat{y}_i)^2 MSE=n1i=1n(yiy^i)2

收敛

迭代2

迭代1

初始化

随机参数

计算梯度

更新参数

计算梯度

更新参数

最优参数

2.3 逻辑回归:分类而非回归

逻辑回归的名字里有"回归",但实际上是一个分类算法。它之所以叫"回归",是因为它最初源于回归模型,后来被扩展用于分类。

逻辑回归的核心思想是:将线性回归的输出压缩到0-1之间,作为属于正类的概率

这个压缩操作是通过Sigmoid函数完成的:

σ ( z ) = 1 1 + e − z \sigma(z) = \frac{1}{1 + e^{-z}} σ(z)=1+ez1

二分类决策

Sigmoid曲线

输入任意值

输出0到1之间

大于0.5判为类别1

小于0.5判为类别0

为什么需要Sigmoid函数?

因为线性回归的输出是 ( − ∞ , + ∞ ) (-\infty, +\infty) (,+) 的任意值,但我们需要的是概率,必须在 [ 0 , 1 ] [0, 1] [0,1] 范围内。Sigmoid函数恰好完成了这个映射: z z z 很大时输出接近1, z z z 很小时输出接近0。

2.4 线性回归与逻辑回归对比

逻辑回归

输入特征

线性组合

Sigmoid压缩

输出概率

线性回归

输入特征

线性组合

输出连续值


三、决策树:自动生成if-else规则

3.1 用决策树理解世界

决策树是我最喜欢的算法之一,因为它模拟了人类做决策的思维方式。你可以把决策树理解为一连串的if-else规则,只不过这些规则不是人工设计的,而是算法从数据中自动学习出来的。

想象一个医生诊断病人的过程:医生会问"发烧吗?"如果发烧,再问"咳嗽吗?“如果咳嗽,再问"持续多久?”…这个不断提问、不断缩小范围的过程,就是决策树的核心思想。

收入大于5000

信用分大于700

拒绝

批准

有房产

批准

人工审核

3.2 信息增益:如何选择最佳分割特征?

决策树的核心问题之一是:每一步应该选择哪个特征来分割?

直觉告诉我们,应该选择那个"最能区分数据"的特征。比如在贷款审批中,"收入"比"最喜欢的颜色"更能区分申请人是否应该获批。

为了量化这个"区分能力",我们引入信息熵的概念。

衡量的是一个集合的"混乱程度"。如果集合中所有元素都属于同一类,熵为0(最有序);如果各类均匀分布,熵最大(最混乱)。

H ( S ) = − ∑ i = 1 c p i log ⁡ 2 ( p i ) H(S) = -\sum_{i=1}^{c} p_i \log_2(p_i) H(S)=i=1cpilog2(pi)

集合C

8红2蓝

熵约0.72

集合B

5红5蓝

熵为1最混乱

集合A

10个红球

熵为0最有序

信息增益是分割前的熵减去分割后各子集熵的加权和。信息增益越大,说明这个分割让数据变得更纯净。

I G ( S , A ) = H ( S ) − ∑ v ∈ V a l u e s ( A ) ∣ S v ∣ ∣ S ∣ H ( S v ) IG(S, A) = H(S) - \sum_{v \in Values(A)} \frac{|S_v|}{|S|} H(S_v) IG(S,A)=H(S)vValues(A)SSvH(Sv)

信息增益

分割后

分割前

整体熵值

子集1占比60%

子集2占比40%

分割前后熵差

3.3 决策树的三大问题与解决方案

问题1:树太深 → 过拟合

如果决策树无限生长,它可能会记住每一个训练样本的细节,包括噪声。这就像一个学生背下了教科书上的每一道题,但不会做新题。

解决方案:剪枝

  • 预剪枝:设置树的最大深度、节点最小样本数等,提前停止生长
  • 后剪枝:先让树充分生长,再从叶节点向上剪除

后剪枝

先生长到最大

自底向上剪枝

预剪枝

限制最大深度

限制最小样本数

问题2:特征多时容易偏向取值多的特征

如果某个特征有100个取值(像身份证号),按它分割会得到很多单样本的节点,看起来信息增益很大,但实际上这是过拟合。

解决方案:使用信息增益率而非信息增益

G a i n R a t i o ( A ) = I G ( S , A ) S p l i t I n f o ( A ) GainRatio(A) = \frac{IG(S, A)}{SplitInfo(A)} GainRatio(A)=SplitInfo(A)IG(S,A)

其中 S p l i t I n f o ( A ) SplitInfo(A) SplitInfo(A) 是分割信息,惩罚取值多的特征。

问题3:连续值特征如何处理?

对于"面积 > 100㎡"这样的连续值条件,需要先离散化。常见做法是尝试所有可能的分割点,选择信息增益最大的。

3.4 决策树实战代码

from sklearn.datasets import load_iris
from sklearn.tree import DecisionTreeClassifier, plot_tree
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, classification_report
import matplotlib.pyplot as plt

# 1. 加载鸢尾花数据集
# 经典数据集,包含3种鸢尾花的4个特征
iris = load_iris()
X, y = iris.data, iris.target
feature_names = iris.feature_names  # ['sepal length (cm)', 'sepal width (cm)', ...]
target_names = iris.target_names   # ['setosa', 'versicolor', 'virginica']

# 2. 数据划分
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

# 3. 训练决策树(带预剪枝防止过拟合)
# criterion='entropy' 使用信息增益
# max_depth=4 限制树深度
# min_samples_split=5 内部节点最小样本数
# min_samples_leaf=2 叶节点最小样本数
tree_clf = DecisionTreeClassifier(
    criterion='entropy',  # 或 'gini'(基尼系数)
    max_depth=4,
    min_samples_split=5,
    min_samples_leaf=2,
    random_state=42
)
tree_clf.fit(X_train, y_train)

# 4. 评估
y_pred = tree_clf.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
print(f"测试集准确率: {accuracy:.4f}")
print("\n分类报告:")
print(classification_report(y_test, y_pred, target_names=target_names))

# 5. 可视化决策树
plt.figure(figsize=(20, 10))
plot_tree(
    tree_clf,
    feature_names=feature_names,
    class_names=target_names,
    filled=True,  # 用颜色填充
    rounded=True,  # 圆角节点
    fontsize=10
)
plt.title('Decision Tree for Iris Classification')
plt.tight_layout()
plt.savefig('decision_tree.png', dpi=150)
plt.show()

# 6. 特征重要性
print("\n特征重要性:")
for name, importance in zip(feature_names, tree_clf.feature_importances_):
    print(f"  {name}: {importance:.4f}")

四、集成学习:三个臭皮匠赛过诸葛亮

4.1 集成学习的核心思想

"三个臭皮匠赛过诸葛亮"这句话蕴含着深刻的智慧。在机器学习中,集成学习就是将多个模型的预测组合起来,得到一个更好的预测结果。

为什么集成能提升性能?关键在于多样性和减少过拟合

组合预测

独立预测

多个模型

模型1

模型2

模型3

预测A

预测B

预测C

投票或平均

想象你要决定是否投资一只股票。你会怎么做?你可能会咨询多个分析师,而不是只听一个人的意见。即使每个分析师都可能有偏见或犯错,综合多个意见往往能得到更可靠的判断。

4.2 Bagging vs Boosting:两种集成策略

集成学习主要有两种策略:BaggingBoosting

Bagging(Bootstrap Aggregating):并行训练多个模型,每个模型独立决策,最后投票。核心理念是"减少方差"。

Boosting:串行训练多个模型,每个模型专注于纠正前一个模型的错误。核心理念是"减少偏差"。

Boosting串行

串行训练

逐步纠错

加权投票

Bagging并行

Bootstrap采样

独立训练

投票平均

4.3 随机森林:Bagging的代表作

随机森林是Bagging策略最成功的实现之一。它的"随机"体现在两个地方:

  1. 行采样:每个决策树只用到一部分样本(有放回抽样)
  2. 列采样:每个决策树只用到一部分特征

投票决策

训练决策树

数据采样

原始数据

子集1

子集2

子集3

树1

树2

树3

最终结果

为什么随机森林不容易过拟合?

因为每棵树只看到了一部分数据,而且只用到了一部分特征。这种"盲人摸象"式的学习,使得每棵树都是有偏的,但当很多有偏的树组合起来时,偏差会相互抵消,最终得到一个低方差(稳定)的预测。

4.4 XGBoost:Boosting的代表作

XGBoost(eXtreme Gradient Boosting)是Boosting策略的巅峰之作,在Kaggle等竞赛中几乎统治了表格数据任务长达数年。

XGBoost的核心思想是:每一棵新树都去学习前面所有树的预测误差,逐步减少残差。

第3棵树

第2棵树

第1棵树

初始预测

学习残差

计算残差

纠正残差

再次计算残差

继续纠正

XGBoost之所以强大,还因为它做了大量工程优化:

  • 正则化:防止过拟合
  • 缺失值处理:自动学习最优方向
  • 特征并行:高效处理高维数据
  • 缓存感知:优化内存访问

4.5 随机森林与XGBoost对比

XGBoost

Boosting策略

串行训练

精度更高

随机森林

Bagging策略

并行训练

不易过拟合

4.6 集成学习实战代码

from sklearn.datasets import make_classification
from sklearn.model_selection import cross_val_score
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from xgboost import XGBClassifier
from sklearn.metrics import accuracy_score
import numpy as np

# 1. 生成模拟数据
X, y = make_classification(
    n_samples=1000,
    n_features=20,
    n_informative=15,
    n_redundant=5,
    n_classes=2,
    random_state=42
)

# 2. 对比三种算法
models = {
    'RandomForest': RandomForestClassifier(n_estimators=100, random_state=42),
    'GradientBoosting': GradientBoostingClassifier(n_estimators=100, random_state=42),
    'XGBoost': XGBClassifier(n_estimators=100, random_state=42, use_label_encoder=False, eval_metric='logloss')
}

print("=" * 50)
print("集成学习算法对比")
print("=" * 50)

for name, model in models.items():
    # 5折交叉验证评估
    cv_scores = cross_val_score(model, X, y, cv=5, scoring='accuracy')
    print(f"\n{name}:")
    print(f"  交叉验证准确率: {cv_scores.mean():.4f} (+/- {cv_scores.std()*2:.4f})")
    
    # 训练并测试
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
    model.fit(X_train, y_train)
    y_pred = model.predict(X_test)
    test_acc = accuracy_score(y_test, y_pred)
    print(f"  测试集准确率: {test_acc:.4f}")

# 3. XGBoost调参示例
print("\n" + "=" * 50)
print("XGBoost超参数调优")
print("=" * 50)

# 常用XGBoost参数
param_grid = {
    'n_estimators': [50, 100, 200],
    'max_depth': [3, 5, 7],
    'learning_rate': [0.01, 0.1, 0.2],
    'subsample': [0.8, 1.0]
}

# 简单网格搜索(实际项目中建议用随机搜索或贝叶斯优化)
best_score = 0
best_params = {}

for n_est in param_grid['n_estimators']:
    for max_d in param_grid['max_depth']:
        for lr in param_grid['learning_rate']:
            for ss in param_grid['subsample']:
                xgb = XGBClassifier(
                    n_estimators=n_est,
                    max_depth=max_d,
                    learning_rate=lr,
                    subsample=ss,
                    random_state=42,
                    use_label_encoder=False,
                    eval_metric='logloss'
                )
                scores = cross_val_score(xgb, X, y, cv=3, scoring='accuracy')
                mean_score = scores.mean()
                
                if mean_score > best_score:
                    best_score = mean_score
                    best_params = {
                        'n_estimators': n_est,
                        'max_depth': max_d,
                        'learning_rate': lr,
                        'subsample': ss
                    }

print(f"\n最优参数: {best_params}")
print(f"最优交叉验证准确率: {best_score:.4f}")

五、实战项目:客户流失预测全流程

5.1 项目背景

让我用一个完整的实战项目来串联所有学到的算法。假设你在一家电信公司工作,老板让你预测哪些客户可能会流失(取消服务)。

这是一个经典的二分类问题:你需要根据客户的历史行为数据,预测他们是否会流失。

5.2 数据探索

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.datasets import make_classification

# 模拟电信客户数据(实际项目中用真实数据)
# make_classification生成模拟的二分类数据
X, y = make_classification(
    n_samples=1000,
    n_features=10,
    n_informative=8,
    n_redundant=2,
    n_classes=2,
    weights=[0.7, 0.3],  # 70%不流失,30%流失(类别不平衡)
    random_state=42
)

# 创建特征名
feature_names = [
    '月消费', '在网时长', '投诉次数', '套餐等级',
    '使用流量', '国际通话时长', '客服联系次数',
    '欠费次数', '转套餐次数', '年龄'
]

df = pd.DataFrame(X, columns=feature_names)
df['流失'] = y

print("=" * 50)
print("数据概览")
print("=" * 50)
print(f"样本数: {len(df)}")
print(f"特征数: {len(feature_names)}")
print(f"\n流失分布:")
print(df['流失'].value_counts())
print(f"\n流失率: {df['流失'].mean()*100:.1f}%")

# 查看基本统计信息
print("\n" + "=" * 50)
print("特征统计")
print("=" * 50)
print(df.describe().round(2))

5.3 数据预处理

from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split

# 1. 处理类别不平衡
# 由于流失客户只占30%,我们需要处理类别不平衡问题
# 方法1: 过采样少数类(SMOTE)
# 方法2: 欠采样多数类
# 方法3: 调整分类阈值

# 2. 特征标准化
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# 3. 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(
    X_scaled, y, test_size=0.2, random_state=42, stratify=y
)

print(f"训练集: {len(X_train)} 样本")
print(f"测试集: {len(X_test)} 样本")
print(f"训练集流失率: {y_train.mean()*100:.1f}%")
print(f"测试集流失率: {y_test.mean()*100:.1f}%")

5.4 多模型对比

from sklearn.neighbors import KNeighborsClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from xgboost import XGBClassifier
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score

# 定义模型
models = {
    'KNN': KNeighborsClassifier(n_neighbors=5),
    '逻辑回归': LogisticRegression(max_iter=1000),
    '决策树': DecisionTreeClassifier(max_depth=5, random_state=42),
    '随机森林': RandomForestClassifier(n_estimators=100, max_depth=5, random_state=42),
    '梯度提升': GradientBoostingClassifier(n_estimators=100, max_depth=5, random_state=42),
    'XGBoost': XGBClassifier(n_estimators=100, max_depth=5, random_state=42, 
                             use_label_encoder=False, eval_metric='logloss')
}

# 训练和评估
print("=" * 80)
print("模型对比结果")
print("=" * 80)
print(f"{'模型':<15} {'准确率':<10} {'精确率':<10} {'召回率':<10} {'F1分数':<10} {'AUC':<10}")
print("-" * 80)

results = []
for name, model in models.items():
    # 训练
    model.fit(X_train, y_train)
    
    # 预测
    y_pred = model.predict(X_test)
    y_prob = model.predict_proba(X_test)[:, 1]  # 流失的概率
    
    # 计算指标
    acc = accuracy_score(y_test, y_pred)
    prec = precision_score(y_test, y_pred)
    rec = recall_score(y_test, y_pred)
    f1 = f1_score(y_test, y_pred)
    auc = roc_auc_score(y_test, y_prob)
    
    results.append({
        'model': name,
        'accuracy': acc,
        'precision': prec,
        'recall': rec,
        'f1': f1,
        'auc': auc
    })
    
    print(f"{name:<15} {acc:.4f}      {prec:.4f}      {rec:.4f}      {f1:.4f}      {auc:.4f}")

# 找出最佳模型
best_model = max(results, key=lambda x: x['f1'])
print("\n" + "=" * 80)
print(f"最佳模型: {best_model['model']} (F1={best_model['f1']:.4f})")

5.5 业务解读

# 假设XGBoost是最佳模型,分析特征重要性
best_xgb = models['XGBoost']
feature_importance = pd.DataFrame({
    '特征': feature_names,
    '重要性': best_xgb.feature_importances_
}).sort_values('重要性', ascending=False)

print("\n" + "=" * 50)
print("特征重要性排名(业务解读)")
print("=" * 50)
for i, row in feature_importance.iterrows():
    bar = "█" * int(row['重要性'] * 50)
    print(f"{row['特征']:<12}: {row['重要性']:.4f} {bar}")

# 业务建议
print("\n" + "=" * 50)
print("业务建议")
print("=" * 50)
top3 = feature_importance.head(3)['特征'].tolist()
if '投诉次数' in top3:
    print("• 重点关注投诉次数多的客户,及时跟进处理")
if '客服联系次数' in top3:
    print("• 频繁联系客服的客户可能有问题,需主动关怀")
if '月消费' in top3:
    print("• 高消费客户流失风险也高,提供VIP服务挽留")
if '在网时长' in top3:
    print("• 新客户流失风险高,加强入网前3个月的服务")

六、总结与展望

六句话核心总结

第一句:KNN是最直观的算法,"近朱者赤近墨者黑"是其核心哲学。但预测速度慢,不适合大数据。

第二句:线性回归找最佳拟合直线,逻辑回归用Sigmoid将输出转为概率。前者用于回归,后者用于分类。

第三句:决策树自动学习if-else规则,信息增益帮助选择最优分割特征。但容易过拟合,需要剪枝。

第四句:集成学习将多个模型组合,“三个臭皮匠赛过诸葛亮”。Bagging减少方差,Boosting减少偏差。

第五句:随机森林是Bagging的代表作,通过数据采样和特征采样让每棵树独立学习。XGBoost是Boosting的巅峰,在表格数据上几乎无敌。

第六句:没有"最好的算法",只有"最适合的算法"。根据数据量、特征类型、任务需求选择合适的模型。

算法选择决策树

小规模

大规模

普通

二分类

多分类

普通

表格数据

文本图像

开始选择算法

数据集大小

需要可解释性

实时性要求

决策树或逻辑回归

类别数

线性模型或KNN

精度要求

特征类型

随机森林

XGBoost或LightGBM

深度学习

部署模型


下篇预告

在下一篇文章中,我们将进入深度学习的世界,从神经元开始,一步步理解神经网络的工作原理。你将了解到:

  • 人工神经元是如何模拟生物神经元的
  • 激活函数为什么能带来非线性
  • 前向传播和反向传播是如何工作的
  • 如何用PyTorch实现第一个神经网络

敬请期待:《从神经元到神经网络:深度学习的本质》


参考资料

  • Scikit-learn官方文档:https://scikit-learn.org/stable/
  • XGBoost官方文档:https://xgboost.readthedocs.io/
  • 机器学习实战:Peter Harrington
  • Kaggle入门:https://www.kaggle.com/learn

如果觉得有帮助,欢迎转发给需要的朋友!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值