机器学习零基础完全指南 🤖

📚 适用人群:零基础小白、刚入门的大学生、想转行AI的工程师

🎯 学习目标:从"完全不懂"到"能独立完成ML项目"

📝 文档特色:用最通俗的语言、最多的例子、最详细的图解,带你真正搞懂机器学习

📅 最后更新:2025年11月


📋 目录


1. 什么是机器学习?

1.1 传统编程 vs 机器学习

传统编程的方式

想象你要教计算机识别"猫"和"狗":

┌─────────────────────────────────────────────┐
│          传统编程(基于规则)                 │
├─────────────────────────────────────────────┤
│                                              │
│  if 耳朵 == "尖尖的" and 胡须 == "长":       │
│      return "猫"                             │
│  elif 耳朵 == "垂下的" and 尾巴 == "摇":     │
│      return "狗"                             │
│  elif 体型 == "大" and 腿 == "长":           │
│      return "狗"                             │
│  else:                                       │
│      return "不知道"                         │
│                                              │
│  问题:                                      │
│  ❌ 规则太多,写不完                         │
│  ❌ 特殊情况处理不了(折耳猫怎么办?)       │
│  ❌ 新品种出现,规则就失效                   │
│                                              │
└─────────────────────────────────────────────┘
机器学习的方式
┌─────────────────────────────────────────────┐
│          机器学习(数据驱动)                 │
├─────────────────────────────────────────────┤
│                                              │
│  给机器1000张猫的照片 📷 🐱                 │
│  给机器1000张狗的照片 📷 🐶                 │
│                                              │
│  机器自己学习:                              │
│  "哦,猫通常这样这样..."                    │
│  "狗通常那样那样..."                        │
│                                              │
│  结果:                                      │
│  ✅ 不需要写规则                             │
│  ✅ 能识别没见过的猫狗                       │
│  ✅ 见的越多,越聪明                         │
│                                              │
└─────────────────────────────────────────────┘

1.2 机器学习的本质

本质:从数据中自动学习规律(函数)的过程。

数学表达:

传统编程:规则 + 数据 → 结果
  (你写的if-else) + (输入) → (输出)

机器学习:数据 + 结果 → 规则
  (训练数据) + (标签) → (学到的模型)

形象比喻

传统编程 = 照着菜谱做菜
  - 菜谱是人写的(规则)
  - 照着做就行
  - 换个菜,菜谱就不能用了

机器学习 = 吃过100道菜后学会做菜
  - 没有菜谱
  - 自己总结规律
  - 能做出新的菜

1.3 机器学习的应用场景

场景1:推荐系统
【抖音/B站推荐】

你的行为记录:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
看了5个游戏视频  → 喜欢游戏 (80%)
看了3个美食视频  → 喜欢美食 (60%)
看了1个科技视频  → 喜欢科技 (20%)

机器学习模型:
"这个人大概率喜欢游戏和美食类视频"

下次推荐:
🎮 游戏实况 (优先推荐)
🍔 探店美食 (优先推荐)
💻 手机评测 (偶尔推荐)
场景2:图像识别
【医疗影像诊断】

输入:X光片图像
  ┌─────────────┐
  │   ░░██░░░░  │
  │   ████████  │  ← 这是肺部X光片
  │   ░░░░░███  │
  └─────────────┘

机器学习模型分析:
- 左上角有阴影 → 可能是肺炎
- 纹理不规则  → 置信度 85%

输出:
⚠️ 疑似肺炎,建议进一步检查
场景3:自然语言处理
【智能客服】

用户:"我的快递怎么还没到?"
          ↓
机器学习分析:
1. 关键词识别:快递、没到
2. 意图分类:查询物流 (95%置信度)
3. 情感分析:焦急、不满 (情绪值: -0.3)

系统回复:
"您好,我查到您的快递在运输途中,
预计明天送达,给您带来不便深表歉意。"

1.4 机器学习能做什么、不能做什么

✅ 擅长的领域
1. 模式识别
   - 图像识别(人脸、物体)
   - 语音识别(Siri、小爱同学)
   - 手写识别(扫描文字)

2. 预测任务
   - 股票预测(虽然不一定准)
   - 天气预测
   - 销量预测

3. 推荐系统
   - 商品推荐
   - 内容推荐
   - 好友推荐

4. 自动化决策
   - 信用评分
   - 疾病诊断辅助
   - 自动驾驶
❌ 不擅长的领域
1. 小样本学习
   问题:只有10张猫的照片,能学会吗?
   现状:很难(需要成千上万的数据)

2. 因果推理
   问题:冰淇淋销量高 → 溺水人数多
   模型:认为冰淇淋导致溺水!
   真相:天气热才是共同原因

3. 常识推理
   问题:一个苹果被切成两半,有几个苹果?
   人类:1个苹果(虽然分成2块)
   AI:  2个?还是0.5个?(搞不清楚)

4. 创造性任务
   现状:AI能模仿,但原创能力有限
   (虽然GPT、Midjourney等在改变这一点)

2. 核心术语扫盲

在学习具体算法前,必须先搞懂这些"黑话"。我们用一个完整的例子贯穿讲解。

2.1 案例:预测房价

任务:根据房子的信息,预测它能卖多少钱

数据示例:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
面积   卧室数  楼层  地段评分  售价(万元)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
80     2      5     7        500
120    3      10    9        800
60     1      3     5        350
150    4      15    10       1200
...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

2.2 术语详解

数据集 (Dataset)
定义:用来训练模型的所有数据

在房价例子中:
数据集 = 所有房子的记录表(可能有1000条、10000条)

数据集通常分为三部分:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
全部数据 (100%)
    │
    ├─ 训练集 (70%) ← 用来训练模型
    │   "教科书"
    │
    ├─ 验证集 (15%) ← 用来调参
    │   "模拟考试"
    │
    └─ 测试集 (15%) ← 用来最终评估
        "高考"
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

为什么要分开?
- 如果用训练集测试,模型可能"作弊"(死记硬背)
- 就像学生不能拿练习题当高考题
特征 (Feature)
定义:描述数据的属性(输入变量)

在房价例子中:
特征1:面积(80平、120平...)
特征2:卧室数(2室、3室...)
特征3:楼层(5层、10层...)
特征4:地段评分(1-10分)

用数学表示:
X = [x₁, x₂, x₃, x₄]
  = [面积, 卧室数, 楼层, 地段评分]

某一套房子:
X = [80, 2, 5, 7]
标签 (Label)
定义:我们想要预测的结果(输出变量)

在房价例子中:
标签 = 售价(500万、800万...)

用数学表示:
y = 售价

某一套房子:
y = 500 (万元)
样本 (Sample/Instance)
定义:数据集中的一条记录

在房价例子中:
一个样本 = 一套房子的完整信息

┌────────────────────────────────────┐
│         样本 #1                     │
├────────────────────────────────────┤
│ 特征:                              │
│   - 面积: 80                       │
│   - 卧室数: 2                      │
│   - 楼层: 5                        │
│   - 地段评分: 7                    │
│                                     │
│ 标签:                              │
│   - 售价: 500万                    │
└────────────────────────────────────┘
模型 (Model)
定义:学习到的"规律"(函数)

模型就是一个函数 f:
输入特征 X → 输出预测值 ŷ

用数学表示:
ŷ = f(X)

在房价例子中(简化版):
预测售价 = 5 × 面积 + 50 × 卧室数 + 2 × 楼层 + 30 × 地段评分

举例:
X = [80, 2, 5, 7]
ŷ = 5×80 + 50×2 + 2×5 + 30×7
  = 400 + 100 + 10 + 210
  = 720 万

实际售价: 500万
预测售价: 720万
误差: 220万 ← 还需要继续训练!
训练 (Training)
定义:让模型从数据中学习的过程

过程:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 初始化:模型参数随机设定
   预测售价 = 1×面积 + 1×卧室 + ...
   (瞎猜的公式)

2. 计算误差:
   对每套房子:
   - 预测值 vs 真实值
   - 计算差距

3. 调整参数:
   "哦,面积的权重应该大一点"
   "卧室数的影响被我高估了"

4. 重复2-3步,直到误差足够小

5. 训练完成!
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

类比:
就像调试一个菜谱:
- 第1次:糖放太多了,下次少放点
- 第2次:盐放太少了,下次多放点
- ...
- 第N次:完美!
推理/预测 (Inference/Prediction)
定义:用训练好的模型预测新数据

场景:
训练完成后,来了一套新房子:

新房子特征:
- 面积: 100平
- 卧室: 3室
- 楼层: 8层
- 地段: 8分

使用训练好的模型预测:
预测售价 = 5×100 + 50×3 + 2×8 + 30×8
          = 500 + 150 + 16 + 240
          = 906 万

输出:
"这套房子大约值 906 万元"
过拟合 (Overfitting)
定义:模型在训练集上表现很好,但在新数据上很差

┌─────────────────────────────────────────┐
│           过拟合的表现                   │
├─────────────────────────────────────────┤
│                                          │
│  训练集准确率: 99% ✅                    │
│  测试集准确率: 60% ❌                    │
│                                          │
│  问题:模型把训练数据"背"下来了         │
│        而不是学到了通用规律              │
│                                          │
└─────────────────────────────────────────┘

形象比喻:

【正常学生】
上课:理解知识点
考试:灵活运用
成绩:优秀 ✅

【过拟合的学生】
上课:死记硬背题目和答案
考试:遇到原题 → 满分
      遇到变形题 → 不会
成绩:训练集100分,测试集不及格 ❌

原因:
- 模型太复杂(参数太多)
- 训练数据太少
- 训练时间太长

解决办法:
1. 增加训练数据
2. 简化模型
3. 正则化(惩罚过于复杂的模型)
4. 早停(Early Stopping)
5. Dropout(随机丢弃神经元)
欠拟合 (Underfitting)
定义:模型太简单,连训练集都学不好

┌─────────────────────────────────────────┐
│           欠拟合的表现                   │
├─────────────────────────────────────────┤
│                                          │
│  训练集准确率: 65% ❌                    │
│  测试集准确率: 63% ❌                    │
│                                          │
│  问题:模型太弱,学不到规律              │
│                                          │
└─────────────────────────────────────────┘

形象比喻:

【欠拟合的学生】
智商或努力不够:
- 书都没看懂
- 平时作业就做不好
- 考试当然也不会

原因:
- 模型太简单(用线性模型拟合复杂关系)
- 特征不够(信息不足)
- 训练不足(还没学会就停了)

解决办法:
1. 用更复杂的模型
2. 增加特征
3. 训练更长时间
泛化能力 (Generalization)
定义:模型在未见过的数据上的表现能力

好的泛化能力:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
训练集准确率: 85%
测试集准确率: 82% ← 差距小,说明没过拟合
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

目标:
不是让模型在训练集上100分
而是让它在现实世界中表现稳定

类比:
- 不要成为"考试机器"
- 要成为真正掌握知识的人

2.3 术语总结表

术语 英文 通俗解释 房价例子
数据集 Dataset 所有用来训练的数据 1000套房子的信息
样本 Sample 一条数据记录 一套房子的信息
特征 Feature 描述数据的属性(输入) 面积、卧室数、楼层
标签 Label 想要预测的值(输出) 售价
模型 Model 学习到的函数/规律 售价 = f(面积, 卧室…)
训练 Training 让模型学习的过程 调整公式参数的过程
推理 Inference 用模型预测新数据 预测新房子的价格
过拟合 Overfitting 死记硬背,不会变通 训练99%,测试60%
欠拟合 Underfitting 没学会,太笨了 训练65%,测试63%
泛化 Generalization 举一反三的能力 在新房子上也能预测准

3. 机器学习的分类

机器学习根据"学习方式"分为四大类。

3.1 监督学习 (Supervised Learning)

定义
有老师教学:数据有"标准答案"

训练数据 = 特征 + 标签

┌──────────────────────────────────┐
│         监督学习示意图            │
├──────────────────────────────────┤
│                                   │
│  输入(特征)     输出(标签)    │
│  ─────────────   ─────────────   │
│  猫的图片    →   "猫" ✅         │
│  狗的图片    →   "狗" ✅         │
│  鸟的图片    →   "鸟" ✅         │
│                                   │
│  模型学习:                       │
│  "原来这种特征的是猫"             │
│  "那种特征的是狗"                 │
│                                   │
└──────────────────────────────────┘
两大类任务

1. 回归 (Regression):预测连续数值

例子:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
任务             输入               输出
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
房价预测         房子信息           500万
股票预测         历史价格           明天涨到120元
气温预测         天气指标           明天28.5°C
学生成绩预测     学习时长           期末考85分
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

特点:输出是具体数字(可以无限细分)

2. 分类 (Classification):预测类别

例子:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
任务             输入               输出
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
垃圾邮件识别     邮件内容           垃圾/正常
疾病诊断         症状描述           健康/感冒/肺炎
图像识别         照片               猫/狗/鸟
情感分析         评论文本           正面/负面/中性
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

特点:输出是离散类别(有限的选项)

分类又分为:
- 二分类:只有2个类别(是/否,0/1)
- 多分类:多个类别(猫/狗/鸟...)
- 多标签分类:可以同时属于多个类别
  例:一张照片里既有猫又有狗
常见算法
监督学习算法:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
回归算法:
  - 线性回归 (Linear Regression)
  - 多项式回归
  - 岭回归 (Ridge)
  - Lasso 回归

分类算法:
  - 逻辑回归 (Logistic Regression)
  - 决策树 (Decision Tree)
  - 随机森林 (Random Forest)
  - 支持向量机 (SVM)
  - 朴素贝叶斯 (Naive Bayes)
  - K近邻 (KNN)
  - 神经网络 (Neural Network)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

3.2 无监督学习 (Unsupervised Learning)

定义
自学成才:数据没有"标准答案"

训练数据 = 只有特征,没有标签

┌──────────────────────────────────┐
│        无监督学习示意图           │
├──────────────────────────────────┤
│                                   │
│  输入(特征)     输出(?)      │
│  ─────────────   ─────────────   │
│  一堆图片    →   ???          │
│                                   │
│  模型自己发现:                   │
│  "这一堆长得像"(聚成一类)      │
│  "那一堆长得像"(聚成另一类)    │
│                                   │
│  虽然不知道它们叫什么名字         │
│  但知道它们是一类的               │
│                                   │
└──────────────────────────────────┘
主要任务

1. 聚类 (Clustering):把相似的东西分到一组

例子1:用户分群

某宝的1亿用户:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
没有标签,但根据购买记录聚类:

群体1:经常买显卡、键盘、手办
   → 给他们贴标签"数码宅男"
   → 推荐游戏、电脑配件

群体2:经常买口红、香水、衣服
   → 给他们贴标签"时尚达人"
   → 推荐美妆、时装

群体3:经常买奶粉、尿布、玩具
   → 给他们贴标签"宝妈宝爸"
   → 推荐母婴用品
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
例子2:图像压缩

原始图像有1600万种颜色
聚类后只保留256种主要颜色
文件大小减少,但看起来差不多

2. 降维 (Dimensionality Reduction):简化数据

问题:数据特征太多(100维、1000维)
目标:压缩到2-3维,方便可视化和计算

例子:

原始数据(100个特征):
学生的100门课成绩

降维后(2个特征):
- 主成分1:"理科能力"(数学、物理、化学...的综合)
- 主成分2:"文科能力"(语文、历史、政治...的综合)

好处:
1. 可以画在2D图上
2. 计算更快
3. 去除冗余信息

3. 异常检测 (Anomaly Detection):找出不正常的数据

例子:信用卡欺诈检测

正常消费模式:
- 每天100-500元
- 在家附近的超市
- 白天消费

异常:
- 突然一笔10000元 ⚠️
- 在国外的网站
- 凌晨3点
→ 可能被盗刷!
常见算法
无监督学习算法:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
聚类算法:
  - K-Means
  - 层次聚类 (Hierarchical)
  - DBSCAN
  - GMM (高斯混合模型)

降维算法:
  - PCA (主成分分析)
  - t-SNE
  - UMAP
  - LDA (线性判别分析)

异常检测:
  - Isolation Forest
  - One-Class SVM
  - LOF (局部异常因子)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

3.3 半监督学习 (Semi-Supervised Learning)

定义
部分有答案,部分没答案

现实场景:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
有标签数据:  100张(人工标注,成本高)
无标签数据:10000张(免费获取)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

思路:
1. 用100张有标签数据训练初步模型
2. 用模型预测10000张无标签数据
3. 挑选预测最有把握的加入训练集
4. 重复2-3,模型越来越好
应用场景
适合的场景:
- 标注成本高(医学影像、法律文档)
- 数据容易获取但标注难(网页、图片)
- 冷启动问题(新产品没有历史数据)

例子:
YouTube 视频分类
- 有标签:1万个人工标注的视频
- 无标签:10亿个用户上传的视频
→ 用半监督学习提升效果

3.4 强化学习 (Reinforcement Learning)

定义
从试错中学习:没有标准答案,只有奖励/惩罚

训练数据 = 环境反馈(做对了+1分,做错了-1分)

┌──────────────────────────────────┐
│         强化学习示意图            │
├──────────────────────────────────┤
│                                   │
│  智能体 (Agent)                   │
│      ↓                            │
│   采取动作                        │
│      ↓                            │
│   环境 (Environment)              │
│      ↓                            │
│  奖励 +1/-1                       │
│      ↓                            │
│  智能体学习                       │
│  "刚才那样做是对的/错的"         │
│                                   │
│  重复上述过程,智能体越来越聪明   │
│                                   │
└──────────────────────────────────┘
核心要素
1. 智能体 (Agent):学习者(AI)

2. 环境 (Environment):智能体所在的世界

3. 状态 (State):当前环境的情况

4. 动作 (Action):智能体可以做的事

5. 奖励 (Reward):做动作后得到的反馈
   - 正奖励:做对了,继续保持
   - 负奖励(惩罚):做错了,下次别这样

6. 策略 (Policy):智能体的决策规则
   "在状态S下,我应该做动作A"
经典例子

例子1:训练小狗

场景:教小狗"握手"

┌─────────────────────────────────┐
│  状态:主人伸出手                │
│                                  │
│  小狗的动作:                    │
│    动作1:坐下  → 奖励 0(不对) │
│    动作2:叫    → 奖励 0(不对) │
│    动作3:握手  → 奖励 +10 ✅   │
│                    (给骨头)     │
│                                  │
│  小狗学到:                      │
│  "原来主人伸手时,我应该握手!"  │
│                                  │
└─────────────────────────────────┘

例子2:AlphaGo 下围棋

状态:当前棋盘局面
动作:在某个位置落子
奖励:
  - 赢了:+1
  - 输了:-1
  - 中间步骤:0(延迟奖励)

训练过程:
1. 随机下棋(瞎下)
2. 记录哪些下法赢了、哪些输了
3. 逐渐学会"这样下赢面大"
4. 经过数百万局对战,成为高手

例子3:自动驾驶

状态:前方路况、红绿灯、行人...
动作:加速、刹车、转向...
奖励:
  - 平稳行驶:+1
  - 偏离车道:-10
  - 撞车:-1000 💥
  - 到达目的地:+100

模拟器里训练数百万次
→ 学会了安全驾驶
强化学习 vs 监督学习
┌─────────────────────────────────────────┐
│          监督学习 vs 强化学习            │
├─────────────────────────────────────────┤
│                                          │
│  【监督学习】                            │
│  老师直接告诉你答案:                    │
│  "这道题应该选A"                        │
│                                          │
│  【强化学习】                            │
│  老师不告诉答案,只说对错:              │
│  你:我选A                               │
│  老师:错了(-1分)                      │
│  你:我选B                               │
│  老师:对了(+1分)                      │
│  你:哦,以后遇到这种题选B               │
│                                          │
└─────────────────────────────────────────┘
常见算法
强化学习算法:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
经典算法:
  - Q-Learning
  - SARSA
  - Deep Q-Network (DQN)
  - Policy Gradient
  - Actor-Critic
  - PPO (Proximal Policy Optimization)
  - A3C

应用:
  - 游戏AI(Atari、星际争霸)
  - 机器人控制
  - 自动驾驶
  - 资源调度
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

3.5 四种学习方式对比

学习方式 数据特点 典型任务 生活类比 难度
监督学习 有标签 分类、回归 老师教学(有标准答案) ⭐⭐
无监督学习 无标签 聚类、降维 自己探索(没有标准答案) ⭐⭐⭐
半监督学习 部分有标签 分类 少数题目有答案,其他自己推 ⭐⭐⭐
强化学习 有奖惩反馈 决策、控制 训练小狗(做对给骨头) ⭐⭐⭐⭐

4. 经典算法详解

接下来我们详细讲解每个经典算法的原理、直觉和代码实现。

4.1 线性回归

4.1.1 什么是线性回归?
任务:预测连续数值
假设:输入和输出之间是线性关系

数学表达:
y = w₁x₁ + w₂x₂ + ... + wₙxₙ + b

简化版(一个特征):
y = wx + b

这就是一条直线!
  - w:斜率(权重)
  - b:截距(偏置)
4.1.2 直观理解
场景:根据学习时间预测考试成绩

数据散点图:

成绩(y)
  │
100│                       ●
  │                   ●
 80│               ●
  │           ●
 60│       ●
  │   ●
 40│●
  │
 20│
  └─────────────────────────→ 学习时间(x)
   0  1  2  3  4  5  6  7  8

线性回归的目标:
画一条直线 y = wx + b,让这条直线:
- 尽可能靠近所有点
- 总体误差最小

拟合后的直线:
成绩(y)
  │
100│                     ●
  │                   / ●
 80│               ● /
  │           ● /
 60│       ● /
  │   ● /
 40│● /
  │/
 20│
  └─────────────────────────→ 学习时间(x)
   0  1  2  3  4  5  6  7  8

拟合的直线:y = 12x + 30
解释:
- 不学习(x=0):基础分30分
- 每多学1小时:提高12分
4.1.3 数学原理

目标函数(损失函数)

最小化预测值和真实值的差距

均方误差 (MSE - Mean Squared Error):

MSE = (1/n) Σ(yᵢ - ŷᵢ)²
      i=1

其中:
- n:样本数量
- yᵢ:第i个样本的真实值
- ŷᵢ:第i个样本的预测值 = w·xᵢ + b

为什么用平方?
1. 平方总是正数(避免正负抵消)
2. 放大较大误差(惩罚离谱的预测)
3. 可微分(方便求导优化)

求解方法

方法1:正规方程(直接求解)

数学公式:
w = (X^T X)^(-1) X^T y

优点:一步到位,得到最优解
缺点:
- 矩阵求逆计算量大(特征多时很慢)
- 特征数量 > 10000时不实用

方法2:梯度下降(迭代优化)

思路:
一步步调整 w 和 b,让误差越来越小

过程:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 初始化:w=1, b=0(随机值)

2. 计算当前误差(MSE)

3. 计算梯度(导数):
   - ∂MSE/∂w:误差关于w的变化率
   - ∂MSE/∂b:误差关于b的变化率

4. 更新参数:
   w = w - α × ∂MSE/∂w
   b = b - α × ∂MSE/∂b
   
   (α是学习率,控制步长)

5. 重复2-4,直到收敛
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

形象比喻:
你在山上(误差高),想下到山谷(误差低)
- 梯度:告诉你哪个方向最陡
- 学习率:决定你每次迈多大步
- 收敛:到达山谷底部

可视化梯度下降

误差
  │
  │    起点●
  │       ╲
  │        ╲●
  │         ╲ ╲
  │          ╲  ●
  │           ╲  ╲
  │            ╲   ●
  │             ╲   ╲
  │              ╲    ●
  │               ╲____●最优点
  └─────────────────────────→ w的值

每次更新,误差都在下降
最终到达最低点(最优解)
4.1.4 代码实现

使用 Scikit-learn(3行代码)

from sklearn.linear_model import LinearRegression
import numpy as np

# 准备数据
X = np.array([[1], [2], [3], [4], [5]])  # 学习时间(小时)
y = np.array([42, 54, 66, 78, 90])       # 考试成绩

# 创建模型并训练
model = LinearRegression()
model.fit(X, y)

# 预测
new_X = np.array([[6], [7]])  # 新学生学了6小时、7小时
predictions = model.predict(new_X)

print(f"学习6小时,预测成绩:{predictions[0]:.1f}")
print(f"学习7小时,预测成绩:{predictions[1]:.1f}")
print(f"模型参数:y = {model.coef_[0]:.2f}x + {model.intercept_:.2f}")

输出

学习6小时,预测成绩:102.0
学习7小时,预测成绩:114.0
模型参数:y = 12.00x + 30.00

从零实现(理解原理)

import numpy as np

class SimpleLinearRegression:
    def __init__(self, learning_rate=0.01, n_iterations=1000):
        self.lr = learning_rate
        self.n_iterations = n_iterations
        self.w = None  # 权重
        self.b = None  # 偏置
        
    def fit(self, X, y):
        """训练模型"""
        n_samples = len(X)
        
        # 初始化参数
        self.w = 0
        self.b = 0
        
        # 梯度下降
        for i in range(self.n_iterations):
            # 预测
            y_pred = self.w * X + self.b
            
            # 计算梯度
            dw = -(2/n_samples) * np.sum(X * (y - y_pred))
            db = -(2/n_samples) * np.sum(y - y_pred)
            
            # 更新参数
            self.w -= self.lr * dw
            self.b -= self.lr * db
            
            # 每100次打印一次损失
            if i % 100 == 0:
                mse = np.mean((y - y_pred) ** 2)
                print(f"Iteration {i}, MSE: {mse:.4f}")
    
    def predict(self, X):
        """预测"""
        return self.w * X + self.b

# 使用示例
X = np.array([1, 2, 3, 4, 5])
y = np.array([42, 54, 66, 78, 90])

model = SimpleLinearRegression(learning_rate=0.01, n_iterations=1000)
model.fit(X, y)

print(f"\n最终模型:y = {model.w:.2f}x + {model.b:.2f}")
print(f"预测:学习6小时 → {model.predict(6):.1f}分")
4.1.5 多元线性回归
当有多个特征时:

y = w₁x₁ + w₂x₂ + w₃x₃ + ... + wₙxₙ + b

例子:房价预测
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
特征:
  x₁ = 面积
  x₂ = 卧室数
  x₃ = 楼层
  x₄ = 地段评分

模型:
售价 = 5×面积 + 50×卧室数 + 2×楼层 + 30×地段 + 100

解释:
- 面积每增加1平,房价涨5万
- 多一个卧室,房价涨50万
- 楼层每高1层,房价涨2万
- 地段评分每高1分,房价涨30万
- 基础价格:100万
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

代码示例

from sklearn.linear_model import LinearRegression
import numpy as np

# 多特征数据
X = np.array([
    [80, 2, 5, 7],   # 80平、2室、5层、地段7分
    [120, 3, 10, 9],  # 120平、3室、10层、地段9分
    [60, 1, 3, 5],    # ...
    [150, 4, 15, 10]
])

y = np.array([500, 800, 350, 1200])  # 售价(万元)

# 训练
model = LinearRegression()
model.fit(X, y)

# 查看参数
print("各特征的权重:", model.coef_)
print("截距:", model.intercept_)

# 预测新房子:100平、3室、8层、地段8分
new_house = np.array([[100, 3, 8, 8]])
price = model.predict(new_house)
print(f"预测售价:{price[0]:.1f}万元")
4.1.6 线性回归的优缺点

✅ 优点

1. 简单易懂
   - 模型就是一个线性公式
   - 可以直接看出每个特征的影响

2. 训练速度快
   - 计算量小
   - 大数据集也能快速训练

3. 可解释性强
   - 权重越大,特征越重要
   - 能直接写出预测公式

4. 不容易过拟合
   - 模型简单,泛化能力强

❌ 缺点

1. 只能拟合线性关系
   - 现实世界很多关系是非线性的
   - 无法捕捉复杂模式

例子:温度和冰淇淋销量
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
温度(°C)   销量
  -10       10   (太冷,没人买)
   0        50
   10       200
   20       500
   30       800  (热了,销量大)
   40       900
   50       850  (太热了,大家都在家吹空调)

这是非线性关系(倒U形)
线性回归拟合不好!
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

2. 对异常值敏感
   平方误差会放大离群点的影响

3. 特征需要归一化
   不同特征的量纲不同会影响结果

4. 多重共线性问题
   特征之间高度相关时,模型不稳定
4.1.7 改进版本

1. 多项式回归(Polynomial Regression)

添加高次项,拟合非线性关系

原始:y = w₁x + b
多项式:y = w₁x + w₂x² + w₃x³ + b

代码:
from sklearn.preprocessing import PolynomialFeatures
from sklearn.linear_model import LinearRegression

# 原始特征
X = [[1], [2], [3], [4], [5]]

# 扩展为多项式特征(2次)
poly = PolynomialFeatures(degree=2)
X_poly = poly.transform(X)
# [[1, x, x²], [1, x, x²], ...]

# 训练
model = LinearRegression()
model.fit(X_poly, y)

2. 岭回归(Ridge Regression)

添加L2正则化,防止过拟合

损失函数:
MSE + α × Σwᵢ²

作用:
- 惩罚过大的权重
- 让模型更"平滑"

代码:
from sklearn.linear_model import Ridge

model = Ridge(alpha=1.0)  # alpha越大,正则化越强
model.fit(X, y)

3. Lasso回归

添加L1正则化,可以做特征选择

损失函数:
MSE + α × Σ|wᵢ|

特点:
- 会把不重要的特征权重变为0
- 自动进行特征选择

代码:
from sklearn.linear_model import Lasso

model = Lasso(alpha=0.1)
model.fit(X, y)

# 查看哪些特征被选中
print("非零权重的特征:", np.where(model.coef_ != 0)[0])

4.2 逻辑回归

4.2.1 什么是逻辑回归?
⚠️ 注意:虽然叫"回归",但它是分类算法!

任务:二分类(输出0或1)
典型应用:
- 垃圾邮件识别(是/否)
- 疾病诊断(患病/健康)
- 用户流失预测(会流失/不会流失)
4.2.2 从线性回归到逻辑回归
问题:线性回归不适合分类

例子:根据学习时间预测是否通过考试

线性回归的问题:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
学习时间  通过考试
  1小时    0(不通过)
  2小时    0
  3小时    0
  4小时    1(通过)
  5小时    1
  6小时    1

用线性回归:y = wx + b
可能预测出 y = 1.5 或 y = -0.3
但我们需要的是 0 或 1!
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

解决办法:
把线性回归的输出"压缩"到(0,1)之间
→ Sigmoid函数
4.2.3 Sigmoid函数
公式:
σ(z) = 1 / (1 + e^(-z))

特点:
- 输入:任意实数 (-∞, +∞)
- 输出:(0, 1) 之间
- 可以解释为"概率"

图形:
σ(z)
  │
1.0│         ╱───────
  │       ╱
0.5│     ╱  ← 决策边界
  │   ╱
0.0│───────╱
  └────────────────→ z
     -5  0  5

性质:
σ(0) = 0.5
σ(+∞) = 1
σ(-∞) = 0

逻辑回归模型

步骤1:线性组合
z = w₁x₁ + w₂x₂ + ... + wₙxₙ + b

步骤2:Sigmoid变换
P(y=1|x) = σ(z) = 1 / (1 + e^(-z))

步骤3:分类决策
if P(y=1|x) ≥ 0.5:
    预测为类别1
else:
    预测为类别0
4.2.4 直观理解
场景:根据学习时间预测是否通过考试

数据:
学习时间(小时)  通过考试
     1            0
     2            0
     3            0
     4            1
     5            1
     6            1

逻辑回归学到的模型:
P(通过|时间) = σ(2×时间 - 7)

预测:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
时间   z=2x-7   σ(z)    预测
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
 1      -5      0.007   不通过(0)
 2      -3      0.047   不通过(0)
 3      -1      0.269   不通过(0)
 3.5     0      0.500   临界点
 4       1      0.731   通过(1)
 5       3      0.953   通过(1)
 6       5      0.993   通过(1)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

解释:
- 学习3.5小时是临界点(50%概率)
- 学习越多,通过概率越高
- 学习1小时,通过概率只有0.7%
- 学习6小时,通过概率99.3%
4.2.5 损失函数
不能用MSE!
原因:Sigmoid函数是非凸的,MSE会有很多局部最小值

使用:交叉熵损失 (Cross-Entropy Loss)

公式:
Loss = -[y·log(ŷ) + (1-y)·log(1-ŷ)]

其中:
- y:真实标签(0或1)
- ŷ:预测概率 P(y=1|x)

直觉:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
情况1:真实标签 y=1(正例)
  如果预测 ŷ=0.9(很有信心)
  → Loss = -log(0.9) = 0.105(损失小)
  
  如果预测 ŷ=0.1(判断错误)
  → Loss = -log(0.1) = 2.303(损失大)

情况2:真实标签 y=0(负例)
  如果预测 ŷ=0.1(很有信心)
  → Loss = -log(0.9) = 0.105(损失小)
  
  如果预测 ŷ=0.9(判断错误)
  → Loss = -log(0.1) = 2.303(损失大)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

总损失(所有样本):
J = (1/n) Σ Loss_i
4.2.6 代码实现

使用 Scikit-learn

from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, classification_report
import numpy as np

# 示例数据:根据学习时间预测是否通过考试
X = np.array([[1], [2], [3], [4], [5], [6], [7], [8]])
y = np.array([0, 0, 0, 1, 1, 1, 1, 1])

# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.25, random_state=42
)

# 创建并训练模型
model = LogisticRegression()
model.fit(X_train, y_train)

# 预测
y_pred = model.predict(X_test)

# 预测概率
y_pred_proba = model.predict_proba(X_test)

print("测试集预测结果:")
for i, (x, true, pred, proba) in enumerate(zip(
    X_test, y_test, y_pred, y_pred_proba
)):
    print(f"学习{x[0]}小时 - 真实:{true}, 预测:{pred}, "
          f"概率: {proba[1]:.3f}")

print(f"\n准确率: {accuracy_score(y_test, y_pred):.2%}")
print("\n分类报告:")
print(classification_report(y_test, y_pred))

# 查看模型参数
print(f"\n模型参数:")
print(f"权重 w: {model.coef_[0][0]:.3f}")
print(f"偏置 b: {model.intercept_[0]:.3f}")

从零实现

import numpy as np

class LogisticRegression:
    def __init__(self, learning_rate=0.01, n_iterations=1000):
        self.lr = learning_rate
        self.n_iterations = n_iterations
        self.w = None
        self.b = None
    
    def sigmoid(self, z):
        """Sigmoid激活函数"""
        return 1 / (1 + np.exp(-z))
    
    def fit(self, X, y):
        """训练模型"""
        n_samples, n_features = X.shape
        
        # 初始化参数
        self.w = np.zeros(n_features)
        self.b = 0
        
        # 梯度下降
        for i in range(self.n_iterations):
            # 线性组合
            z = np.dot(X, self.w) + self.b
            
            # Sigmoid变换
            y_pred = self.sigmoid(z)
            
            # 计算梯度
            dw = (1/n_samples) * np.dot(X.T, (y_pred - y))
            db = (1/n_samples) * np.sum(y_pred - y)
            
            # 更新参数
            self.w -= self.lr * dw
            self.b -= self.lr * db
            
            # 每100次打印损失
            if i % 100 == 0:
                loss = -np.mean(
                    y * np.log(y_pred + 1e-15) + 
                    (1 - y) * np.log(1 - y_pred + 1e-15)
                )
                print(f"Iteration {i}, Loss: {loss:.4f}")
    
    def predict_proba(self, X):
        """预测概率"""
        z = np.dot(X, self.w) + self.b
        return self.sigmoid(z)
    
    def predict(self, X, threshold=0.5):
        """预测类别"""
        proba = self.predict_proba(X)
        return (proba >= threshold).astype(int)

# 使用示例
X = np.array([[1], [2], [3], [4], [5], [6]])
y = np.array([0, 0, 0, 1, 1, 1])

model = LogisticRegression(learning_rate=0.1, n_iterations=1000)
model.fit(X, y)

# 测试
test_X = np.array([[2.5], [3.5], [4.5]])
probas = model.predict_proba(test_X)
preds = model.predict(test_X)

print("\n预测结果:")
for x, p, pred in zip(test_X, probas, preds):
    print(f"学习{x[0]}小时 → 通过概率: {p:.3f}, 预测: {'通过' if pred else '不通过'}")
4.2.7 多分类:Softmax回归
逻辑回归只能做二分类
如果有多个类别怎么办?

解决:Softmax回归(多分类逻辑回归)

例子:手写数字识别(0-9共10个类别)

原理:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
对每个类别k,计算得分:
z_k = w_k · x + b_k

Softmax变换(归一化为概率):
P(y=k|x) = e^(z_k) / Σ e^(z_j)
                    j=1...K

特点:
- 所有类别概率和为1
- 概率最大的类别作为预测结果
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

代码:
from sklearn.linear_model import LogisticRegression

# multi_class='multinomial' 表示使用Softmax
model = LogisticRegression(multi_class='multinomial')
model.fit(X, y)  # y可以是0,1,2,3...
4.2.8 优缺点

✅ 优点

1. 输出概率
   不仅给出预测类别,还给出置信度

2. 训练快
   凸优化问题,容易收敛

3. 可解释性好
   权重表示特征重要性

4. 对线性可分数据效果好
   如果数据本身线性可分,逻辑回归表现优秀

5. 正则化简单
   L1/L2正则化容易实现

❌ 缺点

1. 只能处理线性可分问题
   如果数据是非线性的,效果差

例子:XOR问题(无法线性分割)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    y
    │
  1 │  ●       ○
    │
  0 │  ○       ●
    └───────────── x
       0   1

● 是类别1,○ 是类别0
无法用一条直线分开!
逻辑回归失效。
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

2. 对特征工程依赖大
   需要手动构造好的特征

3. 容易欠拟合
   模型表达能力有限

4.3 决策树

4.3.1 什么是决策树?
决策树是一种基于"树形结构"的分类/回归算法

核心思想:
通过一系列if-else问题,逐步缩小范围,最终做出决策

就像玩"猜猜我是谁"游戏!
4.3.2 生活中的决策树

场景1:相亲对象选择

                      有房吗?
                    /        \
                  有          没有
                 /              \
            年薪高吗?         人品好吗?
            /    \             /      \
          高      低          好       差
         /        \          /          \
        嫁      再看看      考虑        拒绝

场景2:周末活动决策

                   天气好吗?
                  /         \
                晴天         雨天
               /               \
          有钱吗?            在家躺着
          /    \
        有      没有
       /          \
     去旅游      去公园

场景3:医生诊断流程

                 发烧吗?
                /       \
              是         否
             /             \
        体温>39°?       咳嗽吗?
        /      \         /     \
      是        否      是      否
     /          \      /         \
   去医院     多喝水  感冒药    观察
4.3.3 决策树的结构
┌─────────────────────────────────────────┐
│          决策树的组成部分                │
├─────────────────────────────────────────┤
│                                          │
│  1. 根节点 (Root Node)                  │
│     树的起点,包含所有数据              │
│                                          │
│  2. 内部节点 (Internal Node)            │
│     决策节点,包含判断条件              │
│     例如:"年龄 > 30?"                  │
│                                          │
│  3. 叶节点 (Leaf Node)                  │
│     终点,给出最终预测结果              │
│     例如:"类别A"                       │
│                                          │
│  4. 分支 (Branch)                       │
│     连接节点的路径                      │
│                                          │
└─────────────────────────────────────────┘

完整示例:预测是否打网球

数据集:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
天气     温度    湿度    风力    打球?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
晴天     热      高      弱      否
晴天     热      高      强      否
阴天     热      高      弱      是
雨天     温和    高      弱      是
雨天     凉爽    正常    弱      是
雨天     凉爽    正常    强      否
阴天     凉爽    正常    强      是
晴天     温和    高      弱      否
晴天     凉爽    正常    弱      是
雨天     温和    正常    弱      是
晴天     温和    正常    强      是
阴天     温和    高      强      是
阴天     热      正常    弱      是
雨天     温和    高      强      否
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

训练出的决策树:

                    天气
                 /   |   \
              晴天  阴天  雨天
              /      |     \
           湿度      是   风力
          /   \          /   \
        高    正常      强    弱
       /       \       /       \
      否        是     否       是

读法:
- 如果天气是阴天 → 打球
- 如果天气是晴天:
  - 如果湿度高 → 不打球
  - 如果湿度正常 → 打球
- 如果天气是雨天:
  - 如果风力强 → 不打球
  - 如果风力弱 → 打球
4.3.4 如何构建决策树?

核心问题:选哪个特征作为分裂点?

目标:选择"最好"的特征
什么是"最好"?
→ 分裂后,子节点越"纯"越好

"纯"的意思:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
不纯的节点:
[●●●○○○]  混在一起

纯的节点:
[●●●●●]  全是一类
[○○○○]  全是另一类
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

分裂目标:
父节点:[●●●●○○○]  (混乱)
        ↓ 按特征分裂
左子节点:[●●●●]    (纯)
右子节点:[○○○]    (纯)

度量"纯度"的指标

1. 信息熵 (Entropy)

定义:衡量混乱程度

公式:
H(S) = -Σ p_i × log₂(p_i)

其中:
- p_i:类别i的概率
- H(S):熵值,范围[0, 1]
- H=0:完全纯(只有一类)
- H=1:完全混乱(各类均匀分布)

例子:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
数据集1:[●●●●●●] (6个正例,0个负例)
  p(+) = 1, p(-) = 0
  H = -1×log₂(1) - 0×log₂(0) = 0
  → 完全纯!

数据集2:[●●●○○○] (3个正例,3个负例)
  p(+) = 0.5, p(-) = 0.5
  H = -0.5×log₂(0.5) - 0.5×log₂(0.5) = 1
  → 完全混乱!

数据集3:[●●●●○○] (4个正例,2个负例)
  p(+) = 4/6, p(-) = 2/6
  H = -4/6×log₂(4/6) - 2/6×log₂(2/6) = 0.918
  → 比较混乱
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

2. 信息增益 (Information Gain)

定义:分裂前后,熵的减少量

公式:
IG(S, A) = H(S) - Σ (|S_v| / |S|) × H(S_v)
                   v∈A

其中:
- S:分裂前的数据集
- A:用来分裂的特征
- S_v:特征A取值为v的子集

意义:
- IG 越大,说明分裂效果越好
- 选择 IG 最大的特征进行分裂

例子:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
原数据集 S:[●●●●○○○] (4正3负)
H(S) = 0.985

候选特征1:"年龄"
分裂后:
  年龄≤30:[●●○]   H=0.918
  年龄>30:[●●○○○] H=0.971
  
  加权平均熵 = (3/7)×0.918 + (4/7)×0.971 = 0.948
  IG(年龄) = 0.985 - 0.948 = 0.037

候选特征2:"收入"
分裂后:
  收入低:[●○○○]   H=0.811
  收入高:[●●●○]   H=0.811
  
  加权平均熵 = (4/7)×0.811 + (3/7)×0.811 = 0.811
  IG(收入) = 0.985 - 0.811 = 0.174

结论:选择"收入"作为分裂特征(IG更大)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

3. 基尼不纯度 (Gini Impurity)

定义:随机抽两个样本,类别不同的概率

公式:
Gini(S) = 1 - Σ p_i²

范围:[0, 0.5]
- Gini=0:完全纯
- Gini=0.5:完全混乱

例子:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
数据集:[●●●●●●] (6个正例)
  Gini = 1 - (1² + 0²) = 0  ✅ 完全纯

数据集:[●●●○○○] (3正3负)
  Gini = 1 - (0.5² + 0.5²) = 0.5  完全混乱

数据集:[●●●●○○] (4正2负)
  Gini = 1 - ((4/6)² + (2/6)²) = 0.444
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Gini vs 熵:
- 熵:计算涉及log,稍慢
- Gini:只有乘法,更快
- 效果相似,Gini更常用(sklearn默认)
4.3.5 决策树构建算法

ID3算法(使用信息增益)

def ID3(数据集 S, 特征集 A):
    """
    ID3 决策树算法
    """
    # 停止条件1:所有样本同一类
    if S中所有样本都是同一类别:
        return 叶节点(该类别)
    
    # 停止条件2:没有特征可用
    if A为空:
        return 叶节点(S中最多的类别)
    
    # 选择最佳分裂特征
    最佳特征 = None
    最大IG = -for 特征 in A:
        IG = 计算信息增益(S, 特征)
        if IG > 最大IG:
            最大IG = IG
            最佳特征 = 特征
    
    # 创建节点
    节点 = 内部节点(最佳特征)
    
    # 对每个特征值递归构建子树
    forin 最佳特征的所有可能值:
        子集 = S中最佳特征=值的样本
        
        if 子集为空:
            节点.添加子节点(叶节点(S中最多的类别))
        else:
            子树 = ID3(子集, A - {最佳特征})
            节点.添加子节点(子树)
    
    return 节点

C4.5算法(改进版)

改进点:
1. 使用信息增益率(防止偏向取值多的特征)
   GainRatio = IG / SplitInfo
   
2. 可以处理连续值特征
   动态选择分裂点:age ≤ 30 vs age > 30

3. 可以处理缺失值
   按概率分配到各个分支

4. 剪枝(防止过拟合)

CART算法(sklearn使用)

特点:
1. 二叉树(每次只分成两个分支)
2. 使用Gini不纯度
3. 可以做分类和回归

分类树:
- 叶节点:类别(多数投票)

回归树:
- 叶节点:数值(平均值)
- 损失函数:MSE
4.3.6 决策树的剪枝

为什么需要剪枝?

问题:决策树容易过拟合

过拟合的决策树:
                  年龄>30?
                 /        \
              是            否
             /                \
        收入>50k?          学历=本科?
        /      \            /      \
      是        否        是        否
     /          \        /          \
   已婚?       独生子?  星座=双鱼?  ...
   /  \        /   \     /    \
  ...  ...   ...  ...  ...   ...

问题:
- 树太深,太复杂
- 把训练集的噪声也学进去了
- 泛化能力差

解决:剪枝(Pruning)

两种剪枝策略

1. 预剪枝 (Pre-Pruning)

在构建过程中,提前停止

停止条件:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 最大深度 (max_depth)
   树的深度超过阈值就停止
   例如:max_depth=5

2. 最小样本分裂数 (min_samples_split)
   节点样本数少于阈值就不再分裂
   例如:节点只有2个样本,不分了

3. 最小叶节点样本数 (min_samples_leaf)
   分裂后子节点样本数太少就放弃
   例如:分裂后左边只有1个样本,放弃

4. 最小信息增益
   信息增益太小就不分裂
   说明分裂效果不明显
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

优点:训练快
缺点:可能过早停止(欠拟合)

2. 后剪枝 (Post-Pruning)

先构建完整的树,再剪掉多余的分支

方法:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 代价复杂度剪枝 (Cost Complexity Pruning)
   
   目标函数:
   Cost(T) = Error(T) + α × |T|
   
   其中:
   - Error(T):树T的误差
   - |T|:树T的叶节点数量
   - α:复杂度惩罚系数
   
   α越大,剪枝越厉害

2. 错误率降低剪枝 (REP)
   在验证集上测试,如果剪枝后错误率降低就剪

3. 悲观剪枝 (PEP)
   保守估计误差,剪掉可能过拟合的分支
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

优点:效果好
缺点:需要额外的验证集,训练慢
4.3.7 代码实现

使用 Scikit-learn

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

# 加载数据(鸢尾花数据集)
iris = load_iris()
X, y = iris.data, iris.target

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

# 创建决策树(带剪枝参数)
model = DecisionTreeClassifier(
    criterion='gini',        # 使用Gini不纯度
    max_depth=3,             # 最大深度3
    min_samples_split=5,     # 至少5个样本才分裂
    min_samples_leaf=2,      # 叶节点至少2个样本
    random_state=42
)

# 训练
model.fit(X_train, y_train)

# 预测
y_pred = model.predict(X_test)
accuracy = model.score(X_test, y_test)

print(f"准确率: {accuracy:.2%}")

# 可视化决策树
plt.figure(figsize=(20, 10))
plot_tree(
    model,
    feature_names=iris.feature_names,
    class_names=iris.target_names,
    filled=True,
    rounded=True,
    fontsize=12
)
plt.savefig('decision_tree.png', dpi=150, bbox_inches='tight')
print("决策树图已保存为 decision_tree.png")

# 查看特征重要性
feature_importance = model.feature_importances_
for name, importance in zip(iris.feature_names, feature_importance):
    print(f"{name}: {importance:.4f}")

决策树预测过程可视化

def predict_with_path(tree_model, sample):
    """
    预测并显示决策路径
    """
    # 获取决策路径
    decision_path = tree_model.decision_path([sample])
    node_indicator = decision_path.toarray()[0]
    
    # 获取叶节点ID
    leaf_id = tree_model.apply([sample])[0]
    
    # 获取树结构
    tree = tree_model.tree_
    feature_names = iris.feature_names
    
    print("决策路径:")
    print("=" * 50)
    
    node_id = 0
    while node_id != leaf_id:
        # 当前节点的分裂特征
        feature_id = tree.feature[node_id]
        threshold = tree.threshold[node_id]
        
        # 样本在该特征上的值
        feature_value = sample[feature_id]
        
        # 判断走哪边
        if feature_value <= threshold:
            direction = "左"
            next_node = tree.children_left[node_id]
            symbol = "≤"
        else:
            direction = "右"
            next_node = tree.children_right[node_id]
            symbol = ">"
        
        print(f"节点 {node_id}: {feature_names[feature_id]} {symbol} {threshold:.2f}")
        print(f"  样本值: {feature_value:.2f}")
        print(f"  → 走{direction}分支到节点 {next_node}")
        print()
        
        node_id = next_node
    
    # 叶节点预测
    prediction = tree_model.predict([sample])[0]
    proba = tree_model.predict_proba([sample])[0]
    
    print(f"到达叶节点 {leaf_id}")
    print(f"预测类别: {iris.target_names[prediction]}")
    print(f"预测概率: {proba}")
    print("=" * 50)

# 测试一个样本
test_sample = X_test[0]
print(f"测试样本特征: {test_sample}")
print()
predict_with_path(model, test_sample)

从零实现简单决策树(CART)

import numpy as np
from collections import Counter

class SimpleDecisionTree:
    """
    简单的CART决策树实现(分类)
    """
    def __init__(self, max_depth=5, min_samples_split=2):
        self.max_depth = max_depth
        self.min_samples_split = min_samples_split
        self.tree = None
    
    def gini(self, y):
        """计算Gini不纯度"""
        counter = Counter(y)
        probs = [count / len(y) for count in counter.values()]
        return 1 - sum(p**2 for p in probs)
    
    def split(self, X, y, feature_idx, threshold):
        """分裂数据集"""
        left_mask = X[:, feature_idx] <= threshold
        right_mask = ~left_mask
        
        return (
            X[left_mask], y[left_mask],
            X[right_mask], y[right_mask]
        )
    
    def find_best_split(self, X, y):
        """找到最佳分裂点"""
        best_gini = float('inf')
        best_feature = None
        best_threshold = None
        
        n_samples, n_features = X.shape
        current_gini = self.gini(y)
        
        # 遍历所有特征
        for feature_idx in range(n_features):
            # 获取该特征的所有唯一值
            thresholds = np.unique(X[:, feature_idx])
            
            # 遍历所有可能的分裂点
            for threshold in thresholds:
                # 分裂
                X_left, y_left, X_right, y_right = self.split(
                    X, y, feature_idx, threshold
                )
                
                # 如果分裂后某边为空,跳过
                if len(y_left) == 0 or len(y_right) == 0:
                    continue
                
                # 计算加权Gini
                n_left, n_right = len(y_left), len(y_right)
                weighted_gini = (
                    (n_left / n_samples) * self.gini(y_left) +
                    (n_right / n_samples) * self.gini(y_right)
                )
                
                # 更新最佳分裂
                if weighted_gini < best_gini:
                    best_gini = weighted_gini
                    best_feature = feature_idx
                    best_threshold = threshold
        
        return best_feature, best_threshold
    
    def build_tree(self, X, y, depth=0):
        """递归构建决策树"""
        n_samples = len(y)
        n_classes = len(np.unique(y))
        
        # 停止条件
        if (depth >= self.max_depth or 
            n_classes == 1 or 
            n_samples < self.min_samples_split):
            # 返回叶节点(多数类)
            return Counter(y).most_common(1)[0][0]
        
        # 找到最佳分裂
        feature_idx, threshold = self.find_best_split(X, y)
        
        if feature_idx is None:
            # 无法分裂,返回叶节点
            return Counter(y).most_common(1)[0][0]
        
        # 分裂数据
        X_left, y_left, X_right, y_right = self.split(
            X, y, feature_idx, threshold
        )
        
        # 递归构建左右子树
        left_subtree = self.build_tree(X_left, y_left, depth + 1)
        right_subtree = self.build_tree(X_right, y_right, depth + 1)
        
        # 返回决策节点
        return {
            'feature_idx': feature_idx,
            'threshold': threshold,
            'left': left_subtree,
            'right': right_subtree
        }
    
    def fit(self, X, y):
        """训练决策树"""
        self.tree = self.build_tree(X, y)
        return self
    
    def predict_sample(self, x, tree=None):
        """预测单个样本"""
        if tree is None:
            tree = self.tree
        
        # 如果是叶节点,返回类别
        if not isinstance(tree, dict):
            return tree
        
        # 否则,根据分裂条件递归
        feature_idx = tree['feature_idx']
        threshold = tree['threshold']
        
        if x[feature_idx] <= threshold:
            return self.predict_sample(x, tree['left'])
        else:
            return self.predict_sample(x, tree['right'])
    
    def predict(self, X):
        """预测多个样本"""
        return np.array([self.predict_sample(x) for x in X])

# 使用示例
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score

# 加载数据
iris = load_iris()
X, y = iris.data, iris.target

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

# 训练我们实现的决策树
tree = SimpleDecisionTree(max_depth=3, min_samples_split=5)
tree.fit(X_train, y_train)

# 预测
y_pred = tree.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)

print(f"自实现决策树准确率: {accuracy:.2%}")

# 对比sklearn
from sklearn.tree import DecisionTreeClassifier
sklearn_tree = DecisionTreeClassifier(max_depth=3, random_state=42)
sklearn_tree.fit(X_train, y_train)
sklearn_accuracy = sklearn_tree.score(X_test, y_test)

print(f"Sklearn决策树准确率: {sklearn_accuracy:.2%}")
4.3.8 决策树的优缺点

✅ 优点

1. 易于理解和解释
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 可以可视化
   - 决策过程透明
   - 非技术人员也能看懂
   
   例子:银行贷款审批
   可以直接给客户看决策树:
   "您因为收入<5000且无房产,所以被拒绝"

2. 需要的数据预处理少
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 不需要归一化
   - 不需要标准化
   - 可以处理缺失值
   - 可以处理数值型和类别型特征

3. 可以处理多输出问题
   同时预测多个目标变量

4. 使用白盒模型
   可以通过布尔逻辑解释决策
   if-else规则清晰

5. 通过统计测试验证模型
   可以评估模型的可靠性

❌ 缺点

1. 容易过拟合
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   特别是树很深时
   解决:剪枝、限制深度

2. 不稳定
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   数据的小变化可能导致完全不同的树
   
   例子:
   原始数据:
   ├─ 年龄 > 30
   │   ├─ 收入 > 5000: A
   │   └─ 收入 ≤ 5000: B
   
   增加一条数据后:
   ├─ 收入 > 5000
   │   ├─ 年龄 > 30: A
   │   └─ 年龄 ≤ 30: C
   
   整个结构都变了!

3. 贪心算法
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   每次选择局部最优分裂点
   不保证全局最优

4. 类别不平衡问题
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   如果某类样本太少,树会偏向多数类
   
   例子:
   正例:1000个
   负例:10个
   
   树可能学到:"直接预测正例"
   因为准确率已经99%了

5. 某些概念难以学习
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   XOR、奇偶性等问题
   需要非常深的树才能表示

6. 对连续值不敏感
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   只能做阶梯状的分段拟合
   线性关系拟合不好
4.3.9 决策树的应用场景
✅ 适合的场景:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 需要可解释性
   - 医疗诊断
   - 贷款审批
   - 风险评估

2. 特征混合类型
   - 既有数值特征又有类别特征
   - 不想花时间做特征工程

3. 非线性关系
   - 数据有复杂的交互作用

4. 快速原型
   - 快速建立baseline模型

❌ 不适合的场景:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 线性关系明显
   用线性模型更好

2. 高维稀疏数据
   文本分类等,决策树效果差

3. 对稳定性要求高
   结果容易因数据变化而改变

4. 需要高精度
   单棵树性能有限
   → 考虑集成方法(随机森林、GBDT)

4.4 随机森林

4.4.1 什么是随机森林?
Random Forest = 多棵决策树的"集体智慧"

核心思想:
┌─────────────────────────────────────────┐
│  三个臭皮匠,顶个诸葛亮                  │
├─────────────────────────────────────────┤
│                                          │
│  单棵决策树:                            │
│  - 不稳定                                │
│  - 容易过拟合                            │
│  - 准确率有限                            │
│                                          │
│  随机森林(100棵树):                   │
│  - 稳定(多数投票)                      │
│  - 不易过拟合(随机性)                  │
│  - 准确率高(集思广益)                  │
│                                          │
└─────────────────────────────────────────┘
4.4.2 生活中的类比

场景1:诊断疾病

单个医生诊断:
医生A:"我觉得是感冒"
→ 可能误诊

随机森林(10个医生会诊):
医生A:"感冒"  ✓
医生B:"感冒"  ✓
医生C:"肺炎"
医生D:"感冒"  ✓
医生E:"感冒"  ✓
医生F:"感冒"  ✓
医生G:"过敏"
医生H:"感冒"  ✓
医生I:"感冒"  ✓
医生J:"感冒"  ✓

最终诊断:感冒(8票)
→ 更可靠!

场景2:股票预测

单个分析师:
"我预测明天涨"
→ 主观性强,可能错

随机森林(100个分析师):
60个说"涨"
40个说"跌"

最终预测:涨(多数投票)
置信度:60%
→ 更客观!
4.4.3 随机森林的构建过程
┌─────────────────────────────────────────┐
│      随机森林训练流程                    │
├─────────────────────────────────────────┤
│                                          │
│  原始训练集:1000个样本                  │
│  [样本1, 样本2, ..., 样本1000]          │
│         │                                │
│         ├─→ 第1棵树                      │
│         │   ├ 随机抽样(有放回)         │
│         │   │  [样本3, 样本7, 样本3,...]│
│         │   ├ 随机选特征                 │
│         │   │  用3个特征(共5个特征中) │
│         │   └ 训练决策树                 │
│         │                                │
│         ├─→ 第2棵树                      │
│         │   ├ 随机抽样(不同样本)       │
│         │   │  [样本1, 样本5, 样本9,...]│
│         │   ├ 随机选特征(不同特征)     │
│         │   │  用另外3个特征             │
│         │   └ 训练决策树                 │
│         │                                │
│         ├─→ 第3棵树                      │
│         │   ...                          │
│         │                                │
│         └─→ 第100棵树                    │
│                                          │
│  预测时:100棵树投票                     │
│  ├ 分类:多数投票                       │
│  └ 回归:平均值                         │
│                                          │
└─────────────────────────────────────────┘

两个"随机"

1. Bagging(Bootstrap Aggregating)

随机抽样,构造不同的训练集

过程:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
原始数据集(10个样本):
[A, B, C, D, E, F, G, H, I, J]

第1棵树的训练集(有放回抽样):
[A, C, C, E, F, F, G, I, J, J]
注意:
- 有些样本被抽中多次(C, F, J)
- 有些样本没被抽中(B, D, H)

第2棵树的训练集:
[A, A, B, D, D, E, G, H, I, I]

第3棵树的训练集:
[B, C, D, E, E, F, H, H, J, J]
...

每棵树看到的数据不同
→ 学到的规律不同
→ 增加多样性
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

有放回抽样的结果:
平均每个样本被选中的概率 ≈ 63.2%
约36.8%的样本不会被选中(称为袋外数据 OOB)

2. 随机特征选择 (Feature Randomness)

每个分裂点只考虑部分特征

例子:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
全部特征(10个):
[年龄, 收入, 学历, 婚否, 职业, 
 地区, 信用分, 负债, 资产, 消费]

第1棵树,节点A分裂时:
随机选3个特征:[年龄, 学历, 信用分]
只在这3个中选最佳分裂特征

第1棵树,节点B分裂时:
随机选另外3个:[收入, 职业, 资产]

第2棵树:
又是不同的3个特征...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

好处:
1. 减少计算量
2. 降低树之间的相关性
3. 防止某个强特征主导所有树

随机森林 vs 决策树对比

┌─────────────────────────────────────────┐
│     决策树 vs 随机森林                   │
├─────────────────────────────────────────┤
│                                          │
│  【单棵决策树】                          │
│                                          │
│  年龄>30?                                │
│   ├─是→ 批准                            │
│   └─否→ 收入>5000?                      │
│          ├─是→ 批准                     │
│          └─否→ 拒绝                     │
│                                          │
│  特点:简单、不稳定、容易过拟合          │
│                                          │
├─────────────────────────────────────────┤
│  【随机森林(3棵树示例)】               │
│                                          │
│  树1:年龄>30? → [批准/拒绝]            │
│  树2:收入>5000? → [批准/拒绝]          │
│  树3:学历=本科? → [批准/拒绝]          │
│                                          │
│  最终决策:投票                          │
│  ├ 批准:2票  ✓                         │
│  └ 拒绝:1票                             │
│  → 批准                                  │
│                                          │
│  特点:稳定、鲁棒、准确率高              │
│                                          │
└─────────────────────────────────────────┘
4.4.4 随机森林的工作原理

训练阶段

# 伪代码
def train_random_forest(X, y, n_trees=100):
    """
    训练随机森林
    """
    forest = []
    
    for i in range(n_trees):
        # 1. Bagging:有放回抽样
        indices = random.choice(range(len(X)), size=len(X), replace=True)
        X_sample = X[indices]
        y_sample = y[indices]
        
        # 2. 训练决策树(带随机特征选择)
        tree = DecisionTree(
            max_features='sqrt'  # 每次只用sqrt(n_features)个特征
        )
        tree.fit(X_sample, y_sample)
        
        forest.append(tree)
    
    return forest

预测阶段(分类)

def predict_classification(forest, X_test):
    """
    分类预测:多数投票
    """
    predictions = []
    
    for x in X_test:
        votes = []
        
        # 每棵树投票
        for tree in forest:
            vote = tree.predict(x)
            votes.append(vote)
        
        # 统计投票结果
        from collections import Counter
        final_prediction = Counter(votes).most_common(1)[0][0]
        
        predictions.append(final_prediction)
    
    return predictions

示例

任务:预测贷款是否违约

测试样本:
年龄=25, 收入=6000, 学历=本科, 信用分=700

100棵树的预测:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
树1:  不违约 ✓
树2:  不违约 ✓
树3:  违约
树4:  不违约 ✓
树5:  不违约 ✓
...
树98: 不违约 ✓
树99: 违约
树100: 不违约 ✓
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

统计结果:
不违约:75票
违约:  25票

最终预测:不违约(75%置信度)

预测阶段(回归)

def predict_regression(forest, X_test):
    """
    回归预测:平均值
    """
    predictions = []
    
    for x in X_test:
        tree_predictions = []
        
        # 每棵树预测
        for tree in forest:
            pred = tree.predict(x)
            tree_predictions.append(pred)
        
        # 取平均值
        final_prediction = np.mean(tree_predictions)
        predictions.append(final_prediction)
    
    return predictions

示例

任务:预测房价

测试样本:
面积=100平, 卧室=3, 楼层=10, 地段=8分

100棵树的预测(万元):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
树1:   520
树2:   485
树3:   510
树4:   495
树5:   530
...
树98:  505
树99:  490
树100: 515
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

平均值:503万
标准差:15万

最终预测:503 ± 15万元
4.4.5 代码实现

使用 Scikit-learn

from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor
from sklearn.datasets import load_iris, load_boston
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, mean_squared_error
import numpy as np

# ========== 分类任务 ==========
print("=" * 50)
print("随机森林分类示例")
print("=" * 50)

# 加载数据
iris = load_iris()
X, y = iris.data, iris.target

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

# 创建随机森林分类器
rf_clf = RandomForestClassifier(
    n_estimators=100,      # 100棵树
    max_depth=5,           # 每棵树最大深度5
    min_samples_split=5,   # 至少5个样本才分裂
    max_features='sqrt',   # 每次分裂考虑sqrt(n_features)个特征
    random_state=42,
    n_jobs=-1              # 使用所有CPU核心
)

# 训练
rf_clf.fit(X_train, y_train)

# 预测
y_pred = rf_clf.predict(X_test)
y_pred_proba = rf_clf.predict_proba(X_test)

# 评估
accuracy = accuracy_score(y_test, y_pred)
print(f"\n准确率: {accuracy:.2%}")

# 查看某个样本的预测细节
sample_idx = 0
print(f"\n样本 {sample_idx} 的预测:")
print(f"真实类别: {iris.target_names[y_test[sample_idx]]}")
print(f"预测类别: {iris.target_names[y_pred[sample_idx]]}")
print(f"预测概率:")
for i, class_name in enumerate(iris.target_names):
    print(f"  {class_name}: {y_pred_proba[sample_idx][i]:.4f}")

# 特征重要性
print(f"\n特征重要性:")
feature_importance = rf_clf.feature_importances_
for name, importance in sorted(
    zip(iris.feature_names, feature_importance),
    key=lambda x: x[1],
    reverse=True
):
    print(f"  {name}: {importance:.4f}")

# 查看每棵树的预测(前10棵)
print(f"\n前10棵树的预测:")
sample_X = X_test[sample_idx:sample_idx+1]
for i, tree in enumerate(rf_clf.estimators_[:10]):
    tree_pred = tree.predict(sample_X)[0]
    print(f"  树{i+1}: {iris.target_names[tree_pred]}")

# ========== 回归任务 ==========
print("\n" + "=" * 50)
print("随机森林回归示例")
print("=" * 50)

# 加载波士顿房价数据
from sklearn.datasets import fetch_california_housing
housing = fetch_california_housing()
X, y = housing.data, housing.target

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

# 创建随机森林回归器
rf_reg = RandomForestRegressor(
    n_estimators=100,
    max_depth=10,
    min_samples_split=5,
    random_state=42,
    n_jobs=-1
)

# 训练
rf_reg.fit(X_train, y_train)

# 预测
y_pred = rf_reg.predict(X_test)

# 评估
mse = mean_squared_error(y_test, y_pred)
rmse = np.sqrt(mse)
r2 = rf_reg.score(X_test, y_test)

print(f"\nRMSE: {rmse:.4f}")
print(f"R² Score: {r2:.4f}")

# 查看预测详情
print(f"\n前5个样本的预测:")
print(f"{'真实值':<10} {'预测值':<10} {'误差':<10}")
print("-" * 30)
for i in range(5):
    true_val = y_test[i]
    pred_val = y_pred[i]
    error = abs(true_val - pred_val)
    print(f"{true_val:<10.2f} {pred_val:<10.2f} {error:<10.2f}")

# 特征重要性
print(f"\n特征重要性(Top 5):")
feature_importance = rf_reg.feature_importances_
top_features = sorted(
    zip(housing.feature_names, feature_importance),
    key=lambda x: x[1],
    reverse=True
)[:5]
for name, importance in top_features:
    print(f"  {name}: {importance:.4f}")

袋外评估(Out-of-Bag Evaluation)

"""
OOB评估:利用袋外数据评估模型
优点:不需要单独的验证集
"""

from sklearn.ensemble import RandomForestClassifier

# 创建随机森林(启用OOB评估)
rf_oob = RandomForestClassifier(
    n_estimators=100,
    oob_score=True,  # 启用OOB评估
    random_state=42
)

# 训练
rf_oob.fit(X_train, y_train)

# OOB准确率
oob_accuracy = rf_oob.oob_score_
print(f"OOB准确率: {oob_accuracy:.2%}")

# 测试集准确率
test_accuracy = rf_oob.score(X_test, y_test)
print(f"测试集准确率: {test_accuracy:.2%}")

# OOB预测
oob_predictions = rf_oob.oob_decision_function_
print(f"\nOOB预测概率矩阵形状: {oob_predictions.shape}")

特征重要性可视化

import matplotlib.pyplot as plt

def plot_feature_importance(model, feature_names, top_n=10):
    """
    可视化特征重要性
    """
    # 获取特征重要性
    importances = model.feature_importances_
    indices = np.argsort(importances)[::-1][:top_n]
    
    # 绘图
    plt.figure(figsize=(10, 6))
    plt.title(f"Top {top_n} Feature Importances")
    plt.bar(
        range(top_n),
        importances[indices],
        align='center'
    )
    plt.xticks(
        range(top_n),
        [feature_names[i] for i in indices],
        rotation=45,
        ha='right'
    )
    plt.xlabel('Feature')
    plt.ylabel('Importance')
    plt.tight_layout()
    plt.savefig('feature_importance.png', dpi=150)
    print("特征重要性图已保存为 feature_importance.png")

# 使用
plot_feature_importance(rf_clf, iris.feature_names)
4.4.6 超参数调优
from sklearn.model_selection import GridSearchCV, RandomizedSearchCV

# ========== 网格搜索 ==========
# 定义参数网格
param_grid = {
    'n_estimators': [50, 100, 200],
    'max_depth': [5, 10, 15, None],
    'min_samples_split': [2, 5, 10],
    'min_samples_leaf': [1, 2, 4],
    'max_features': ['sqrt', 'log2']
}

# 创建网格搜索
grid_search = GridSearchCV(
    RandomForestClassifier(random_state=42),
    param_grid,
    cv=5,           # 5折交叉验证
    scoring='accuracy',
    n_jobs=-1,
    verbose=1
)

# 训练
grid_search.fit(X_train, y_train)

# 最佳参数
print("最佳参数:")
print(grid_search.best_params_)
print(f"\n最佳交叉验证得分: {grid_search.best_score_:.4f}")

# 使用最佳模型
best_rf = grid_search.best_estimator_
test_score = best_rf.score(X_test, y_test)
print(f"测试集得分: {test_score:.4f}")

# ========== 随机搜索(更快)==========
from scipy.stats import randint, uniform

# 定义参数分布
param_distributions = {
    'n_estimators': randint(50, 300),
    'max_depth': [5, 10, 15, 20, None],
    'min_samples_split': randint(2, 20),
    'min_samples_leaf': randint(1, 10),
    'max_features': ['sqrt', 'log2', 0.5, 0.7]
}

# 随机搜索
random_search = RandomizedSearchCV(
    RandomForestClassifier(random_state=42),
    param_distributions,
    n_iter=100,  # 尝试100种随机组合
    cv=5,
    scoring='accuracy',
    n_jobs=-1,
    random_state=42,
    verbose=1
)

random_search.fit(X_train, y_train)

print("\n随机搜索最佳参数:")
print(random_search.best_params_)
4.4.7 随机森林的优缺点

✅ 优点

1. 准确率高
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 集成多棵树,效果好
   - 通常能达到很高的准确率
   - 在Kaggle竞赛中常用

2. 不容易过拟合
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 随机性降低了过拟合风险
   - 即使树很深也较稳定

3. 可以处理高维数据
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 特征很多时也能工作
   - 自动进行特征选择

4. 可以评估特征重要性
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 帮助理解哪些特征最重要
   - 可以进行特征筛选

5. 训练可以并行
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 每棵树独立训练
   - 多核CPU加速

6. 鲁棒性强
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 对缺失值、异常值不敏感
   - 不需要特征缩放

7. 可以输出概率
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 不仅给出预测,还给出置信度

❌ 缺点

1. 模型大
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 100棵树占用内存多
   - 保存和加载慢
   
   例如:
   单棵树:1MB
   随机森林(100棵):100MB

2. 预测慢
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 需要遍历所有树
   - 实时性要求高的场景不适合
   
   预测时间:
   决策树:0.01秒
   随机森林(100棵):1秒

3. 不易解释
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 100棵树,无法像单棵树那样可视化
   - 黑盒模型

4. 对线性关系效果一般
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 树模型本质是分段拟合
   - 线性关系用线性模型更好

5. 训练时间长
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 虽然可以并行,但仍比单棵树慢
   
   训练时间(10万样本):
   决策树:10秒
   随机森林(100棵):5分钟

6. 需要调参
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - n_estimators、max_depth等参数影响性能
   - 需要交叉验证找最佳参数
4.4.8 随机森林 vs 其他算法
┌─────────────────────────────────────────────────┐
│         算法对比(分类任务)                     │
├─────────────────────────────────────────────────┤
│                                                  │
│ 算法           准确率  速度  可解释性  过拟合风险│
│ ─────────────  ─────  ────  ────────  ─────────│
│ 逻辑回归       ⭐⭐   ⭐⭐⭐⭐ ⭐⭐⭐⭐   低      │
│ 决策树         ⭐⭐⭐  ⭐⭐⭐   ⭐⭐⭐⭐   高      │
│ 随机森林       ⭐⭐⭐⭐ ⭐⭐     ⭐⭐      中      │
│ SVM            ⭐⭐⭐⭐ ⭐⭐     ⭐        中      │
│ 神经网络       ⭐⭐⭐⭐ ⭐       ⭐        高      │
│ XGBoost        ⭐⭐⭐⭐⭐ ⭐⭐    ⭐⭐      中      │
│                                                  │
└─────────────────────────────────────────────────┘

选择建议:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
需求                    推荐算法
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
快速baseline          随机森林
追求极致准确率        XGBoost / 神经网络
需要可解释性          决策树 / 逻辑回归
实时预测              逻辑回归 / 单棵决策树
小数据集              随机森林 / SVM
大数据集              XGBoost / 神经网络
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4.4.9 实战技巧

1. 选择树的数量

"""
树越多越好?不一定!
找到准确率饱和点
"""

from sklearn.metrics import accuracy_score
import matplotlib.pyplot as plt

# 测试不同数量的树
n_trees_list = [10, 20, 50, 100, 200, 500]
train_scores = []
test_scores = []

for n_trees in n_trees_list:
    rf = RandomForestClassifier(
        n_estimators=n_trees,
        random_state=42,
        n_jobs=-1
    )
    rf.fit(X_train, y_train)
    
    train_score = rf.score(X_train, y_train)
    test_score = rf.score(X_test, y_test)
    
    train_scores.append(train_score)
    test_scores.append(test_score)
    
    print(f"n_trees={n_trees:>3}: "
          f"Train={train_score:.4f}, Test={test_score:.4f}")

# 绘图
plt.figure(figsize=(10, 6))
plt.plot(n_trees_list, train_scores, 'o-', label='Train')
plt.plot(n_trees_list, test_scores, 's-', label='Test')
plt.xlabel('Number of Trees')
plt.ylabel('Accuracy')
plt.title('Effect of Number of Trees')
plt.legend()
plt.grid(True)
plt.savefig('n_trees_effect.png', dpi=150)

# 通常100-200棵树就够了
# 再多提升不明显,反而增加计算量

2. 处理类别不平衡

"""
如果正负样本比例悬殊(如1:100)
随机森林可能偏向多数类
"""

# 方法1:调整类别权重
rf_balanced = RandomForestClassifier(
    n_estimators=100,
    class_weight='balanced',  # 自动平衡权重
    random_state=42
)

# 方法2:手动设置权重
rf_manual = RandomForestClassifier(
    n_estimators=100,
    class_weight={0: 1, 1: 10},  # 少数类权重×10
    random_state=42
)

# 方法3:采样
from imblearn.over_sampling import SMOTE

smote = SMOTE(random_state=42)
X_resampled, y_resampled = smote.fit_resample(X_train, y_train)

rf = RandomForestClassifier(n_estimators=100)
rf.fit(X_resampled, y_resampled)

3. 特征选择

"""
利用特征重要性进行特征筛选
"""

# 训练随机森林
rf = RandomForestClassifier(n_estimators=100, random_state=42)
rf.fit(X_train, y_train)

# 获取特征重要性
importances = rf.feature_importances_

# 选择重要性>阈值的特征
threshold = 0.01  # 重要性小于1%的特征去掉
selected_features = importances > threshold

print(f"原始特征数: {len(importances)}")
print(f"选择后特征数: {sum(selected_features)}")

# 使用选择的特征重新训练
X_train_selected = X_train[:, selected_features]
X_test_selected = X_test[:, selected_features]

rf_new = RandomForestClassifier(n_estimators=100)
rf_new.fit(X_train_selected, y_train)

print(f"\n全特征准确率: {rf.score(X_test, y_test):.4f}")
print(f"特征选择后准确率: {rf_new.score(X_test_selected, y_test):.4f}")

4.5 支持向量机 (SVM)

4.5.1 什么是支持向量机?
Support Vector Machine (SVM)
核心思想:找到最优的分类边界

目标:
不仅要把两类分开,还要让边界尽可能"宽"
→ 最大化"安全间隔"
4.5.2 直观理解

场景:在桌子上分豆子

桌子上有红豆●和绿豆○:

    ●  ●
  ●  ●  ●
    ●  ●
           |
           |  ← 用一根棍子分开
           |
      ○  ○
    ○  ○  ○
      ○  ○

问题:
棍子可以放很多位置,哪个最好?

SVM的答案:
选择让两边豆子距离棍子最远的位置!

可视化:线性SVM

情况1:不好的分界线(间隔小)

    ●  ●
  ●  ●  ●
    ●| ●  ← 分界线离●太近了
      |
      |○  ○
    ○  ○  ○
      ○  ○

危险!新来一颗豆子,可能分错

情况2:SVM找到的最优分界线(间隔大)

    ●  ●
  ●  ●  ●
    ●  ●
      │
      │ ← 中间的"安全地带"最宽
      │
      ○  ○
    ○  ○  ○
      ○  ○

安全!即使新豆子位置有偏差,也不容易分错
4.5.3 核心概念

1. 超平面 (Hyperplane)

定义:分割不同类别的边界

2D空间:一条直线
  y = wx + b

3D空间:一个平面
  z = w₁x + w₂y + b

高维空间:超平面
  w₁x₁ + w₂x₂ + ... + wₙxₙ + b = 0

数学表达:
  w·x + b = 0

示意图(2D):

    y
    │     ●
    │   ●  ●
    │     ●
    │─────────── w·x + b = 0 (超平面/分界线)
    │   ○
    │ ○  ○
    │   ○
    └──────────── x

2. 支持向量 (Support Vectors)

定义:距离超平面最近的那些点

示意图:

    ●  ●
  ●  ●★ ●    ← ★ 是支持向量(离边界最近)
    ●  ●
      │
      │ ← 决策边界
      │
      ○  ○
    ○★ ○  ○  ← ★ 是支持向量
      ○  ○

重要性:
1. 决策边界只由支持向量决定
2. 其他点(远离边界的)对模型无影响
3. 删除非支持向量,模型不变

例子:
100个训练样本
→ 只有5个是支持向量
→ 实际上模型只用了5个点!
→ 其他95个点可以扔掉

3. 间隔 (Margin)

定义:决策边界到最近点(支持向量)的距离

目标:最大化间隔 (Maximum Margin)

可视化:

    ●  ●
  ●  ●★ ●
    ●  ●
      ├─┤ ← 间隔
      │ │
      ├─┤
      ○  ○
    ○★ ○  ○
      ○  ○

数学表达:
间隔 = 2 / ||w||

其中 w 是超平面的法向量

目标函数:
max 间隔 = max 2/||w|| = min ||w||²/2

4. 硬间隔 vs 软间隔

硬间隔 (Hard Margin)

要求:所有点都被完美分类,没有错误

适用:数据线性可分

问题:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
如果有一个异常点(噪声):

    ●  ●
  ●  ●  ●
    ●  ●
        ○  ← 异常点(本应是●)
      │
      │
      ○  ○
    ○  ○  ○
      ○  ○

硬间隔SVM会努力完美分开所有点
→ 决策边界被异常点"拉偏"
→ 过拟合!
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

软间隔 (Soft Margin)

允许:少数点分错,或者在间隔内

引入松弛变量 ξ (xi):
允许某些点违反间隔约束

目标函数(修正):
min ||w||²/2 + C × Σξᵢ

其中:
- C:惩罚系数
  - C大:严格,不允许错误(接近硬间隔)
  - C小:宽松,允许更多错误

可视化:

    ●  ●
  ●  ●  ●
    ●  ●  ○ ← 允许这个点分错
      │
      │ ← 决策边界不受异常点影响
      │
      ○  ○
    ○  ○  ○
      ○  ○

好处:
1. 更鲁棒(对异常点不敏感)
2. 泛化能力强
3. 适用于非完美分离的数据
4.5.4 线性不可分问题:核函数

问题:数据不是线性可分的

例子:XOR问题

    y
    │
  1 │  ●       ○
    │
  0 │  ○       ●
    └───────────── x
       0   1

无法用一条直线分开!
线性SVM失效。

解决方案:核技巧 (Kernel Trick)

核心思想:
把数据映射到高维空间
→ 在高维空间中线性可分
→ 在原空间看起来是曲线

例子:XOR问题

原始空间(2D):
    y
    │
  1 │  ●       ○   无法线性分离
    │
  0 │  ○       ●
    └───────────── x
       0   1

映射到3D空间:
添加特征 z = x × y

新特征空间:
点(0,0) → (0, 0, 0)  ○
点(0,1) → (0, 1, 0)  ●
点(1,0) → (1, 0, 0)  ●
点(1,1) → (1, 1, 1)  ○

在3D空间中,可以用一个平面分开!

常用核函数

1. 线性核 (Linear Kernel)

公式:
K(x, x') = x · x'

适用:
数据线性可分

特点:
- 最简单
- 速度快
- 可解释性强

2. 多项式核 (Polynomial Kernel)

公式:
K(x, x') = (x · x' + c)^d

参数:
- d:多项式次数(通常2-5)
- c:常数项

例子(d=2):
原始特征:(x₁, x₂)
映射后:(x₁², x₂², √2·x₁x₂, √2·x₁, √2·x₂, 1)

适用:
数据有多项式关系

可视化(d=2):

原空间:     映射后:
  ●●○○        ●●
   ●○  →       ●
  ○○           ○○
               ○

从圆形分布变成线性可分

3. 高斯核/径向基函数核 (RBF Kernel) ⭐最常用

公式:
K(x, x') = exp(-γ ||x - x'||²)

参数:
- γ (gamma):控制"影响范围"
  - γ大:每个点只影响很小范围(容易过拟合)
  - γ小:每个点影响大范围(可能欠拟合)

直觉:
相似的点(距离近) → K接近1
不相似的点(距离远) → K接近0

可视化(1D数据):

γ小(影响范围大):
  ●     ●     ●     ●
  ╱─────╲─────╱─────╲  ← 平滑的决策边界
━━━━━━━━━━━━━━━━━━━━━━

γ大(影响范围小):
  ●     ●     ●     ●
  │\   /│\   /│\   /│  ← 复杂的决策边界(过拟合)
━━━━━━━━━━━━━━━━━━━━━━

适用:
- 数据非线性
- 不知道用什么核时的首选

优点:
- 效果好
- 适应性强
- 只需调γ一个参数

缺点:
- 计算量大
- 参数调优重要

4. Sigmoid核

公式:
K(x, x') = tanh(α·x·x' + c)

类似神经网络的激活函数

适用:
特定问题,较少使用

核函数的选择策略

决策流程:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 数据线性可分?
   是 → 线性核
   否 → 继续

2. 特征数量 >> 样本数量?
   是 → 线性核(避免过拟合)
   否 → 继续

3. 不知道选什么?
   默认 → RBF核(通常效果最好)

4. 有特定结构?
   多项式关系 → 多项式核
   其他 → 定制核函数
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

实战建议:
1. 先试线性核(快速baseline)
2. 再试RBF核(通常更好)
3. 调参优化(C和γ)
4.5.5 SVM的数学原理

优化问题

原始问题(硬间隔):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
min  (1/2)||w||²
w,b

subject to:  yᵢ(w·xᵢ + b) ≥ 1, ∀i

解释:
- 目标:最小化||w||²(最大化间隔)
- 约束:所有点正确分类,且在间隔外
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

软间隔版本:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
min  (1/2)||w||² + C·Σξᵢ
w,b,ξ

subject to:  
  yᵢ(w·xᵢ + b) ≥ 1 - ξᵢ, ∀i
  ξᵢ ≥ 0, ∀i

解释:
- ξᵢ:松弛变量(允许点违反间隔)
- C:惩罚系数(平衡间隔和错误)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

对偶问题 (Dual Problem)

为什么需要对偶形式?
1. 更容易求解(二次规划)
2. 可以引入核函数

对偶形式:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
max  Σαᵢ - (1/2)ΣΣαᵢαⱼyᵢyⱼK(xᵢ,xⱼ)
 α

subject to:
  0 ≤ αᵢ ≤ C, ∀i
  Σαᵢyᵢ = 0

其中:
- αᵢ:拉格朗日乘子
- K(xᵢ,xⱼ):核函数
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

决策函数:
f(x) = sign(Σαᵢyᵢ K(xᵢ, x) + b)

关键:
- 只有支持向量的αᵢ > 0
- 其他点的αᵢ = 0
- 所以只需存储支持向量!
4.5.6 代码实现

使用 Scikit-learn

from sklearn import svm
from sklearn.datasets import make_classification, make_circles
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, classification_report
import numpy as np
import matplotlib.pyplot as plt

# ========== 线性SVM ==========
print("=" * 50)
print("线性SVM示例")
print("=" * 50)

# 生成线性可分数据
X, y = make_classification(
    n_samples=100,
    n_features=2,
    n_redundant=0,
    n_informative=2,
    n_clusters_per_class=1,
    random_state=42
)

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

# 创建线性SVM
linear_svm = svm.SVC(kernel='linear', C=1.0)
linear_svm.fit(X_train, y_train)

# 预测
y_pred = linear_svm.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)

print(f"\n准确率: {accuracy:.2%}")
print(f"支持向量数量: {len(linear_svm.support_vectors_)}")
print(f"支持向量占比: {len(linear_svm.support_vectors_)/len(X_train):.2%}")

# ========== RBF SVM(非线性数据)==========
print("\n" + "=" * 50)
print("RBF SVM示例(非线性数据)")
print("=" * 50)

# 生成圆形分布数据(非线性可分)
X_circle, y_circle = make_circles(
    n_samples=200,
    noise=0.1,
    factor=0.3,
    random_state=42
)

X_train, X_test, y_train, y_test = train_test_split(
    X_circle, y_circle, test_size=0.2, random_state=42
)

# 尝试线性SVM(应该效果不好)
linear_svm = svm.SVC(kernel='linear')
linear_svm.fit(X_train, y_train)
linear_acc = linear_svm.score(X_test, y_test)

# 使用RBF核SVM
rbf_svm = svm.SVC(kernel='rbf', gamma='scale', C=1.0)
rbf_svm.fit(X_train, y_train)
rbf_acc = rbf_svm.score(X_test, y_test)

print(f"\n线性SVM准确率: {linear_acc:.2%} ❌")
print(f"RBF SVM准确率: {rbf_acc:.2%} ✅")
print(f"\nRBF SVM支持向量数量: {len(rbf_svm.support_vectors_)}")

# ========== 不同核函数对比 ==========
print("\n" + "=" * 50)
print("不同核函数对比")
print("=" * 50)

kernels = ['linear', 'poly', 'rbf', 'sigmoid']
results = {}

for kernel in kernels:
    if kernel == 'poly':
        model = svm.SVC(kernel=kernel, degree=3, gamma='scale')
    else:
        model = svm.SVC(kernel=kernel, gamma='scale')
    
    model.fit(X_train, y_train)
    acc = model.score(X_test, y_test)
    n_sv = len(model.support_vectors_)
    
    results[kernel] = {'accuracy': acc, 'n_support_vectors': n_sv}
    print(f"\n{kernel.upper()}核:")
    print(f"  准确率: {acc:.2%}")
    print(f"  支持向量数: {n_sv}")

# ========== 参数C的影响 ==========
print("\n" + "=" * 50)
print("参数C的影响")
print("=" * 50)

C_values = [0.01, 0.1, 1, 10, 100]

for C in C_values:
    model = svm.SVC(kernel='rbf', C=C, gamma='scale')
    model.fit(X_train, y_train)
    
    train_acc = model.score(X_train, y_train)
    test_acc = model.score(X_test, y_test)
    n_sv = len(model.support_vectors_)
    
    print(f"\nC={C:>6.2f}:")
    print(f"  训练准确率: {train_acc:.2%}")
    print(f"  测试准确率: {test_acc:.2%}")
    print(f"  支持向量数: {n_sv}")

# ========== 参数gamma的影响(RBF核)==========
print("\n" + "=" * 50)
print("参数gamma的影响(RBF核)")
print("=" * 50)

gamma_values = [0.001, 0.01, 0.1, 1, 10]

for gamma in gamma_values:
    model = svm.SVC(kernel='rbf', C=1.0, gamma=gamma)
    model.fit(X_train, y_train)
    
    train_acc = model.score(X_train, y_train)
    test_acc = model.score(X_test, y_test)
    
    print(f"\ngamma={gamma:>6.3f}:")
    print(f"  训练准确率: {train_acc:.2%}")
    print(f"  测试准确率: {test_acc:.2%}")

可视化决策边界

def plot_decision_boundary(model, X, y, title):
    """
    可视化SVM的决策边界
    """
    # 创建网格
    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, 6))
    
    # 绘制决策边界
    plt.contourf(xx, yy, Z, alpha=0.3, cmap='RdYlBu')
    
    # 绘制数据点
    scatter = plt.scatter(
        X[:, 0], X[:, 1],
        c=y,
        cmap='RdYlBu',
        edgecolors='black',
        s=50
    )
    
    # 绘制支持向量
    plt.scatter(
        model.support_vectors_[:, 0],
        model.support_vectors_[:, 1],
        s=200,
        facecolors='none',
        edgecolors='green',
        linewidths=2,
        label='Support Vectors'
    )
    
    plt.xlabel('Feature 1')
    plt.ylabel('Feature 2')
    plt.title(title)
    plt.legend()
    plt.colorbar(scatter)
    plt.grid(True, alpha=0.3)
    
    return plt

# 使用示例
# 线性SVM
linear_svm = svm.SVC(kernel='linear', C=1.0)
linear_svm.fit(X, y)
plot_decision_boundary(linear_svm, X, y, 'Linear SVM')
plt.savefig('linear_svm.png', dpi=150, bbox_inches='tight')

# RBF SVM
rbf_svm = svm.SVC(kernel='rbf', C=1.0, gamma='scale')
rbf_svm.fit(X_circle, y_circle)
plot_decision_boundary(rbf_svm, X_circle, y_circle, 'RBF SVM')
plt.savefig('rbf_svm.png', dpi=150, bbox_inches='tight')

print("决策边界图已保存")

网格搜索调参

from sklearn.model_selection import GridSearchCV
from sklearn.preprocessing import StandardScaler

# 数据标准化(SVM对特征尺度敏感)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

# 定义参数网格
param_grid = {
    'C': [0.1, 1, 10, 100],
    'gamma': ['scale', 'auto', 0.001, 0.01, 0.1, 1],
    'kernel': ['rbf', 'poly']
}

# 网格搜索
grid_search = GridSearchCV(
    svm.SVC(),
    param_grid,
    cv=5,
    scoring='accuracy',
    n_jobs=-1,
    verbose=1
)

grid_search.fit(X_train_scaled, y_train)

# 最佳参数
print("最佳参数:")
print(grid_search.best_params_)
print(f"\n最佳交叉验证得分: {grid_search.best_score_:.4f}")

# 使用最佳模型
best_svm = grid_search.best_estimator_
test_score = best_svm.score(X_test_scaled, y_test)
print(f"测试集得分: {test_score:.4f}")

# 查看参数对性能的影响
results_df = pd.DataFrame(grid_search.cv_results_)
print("\n参数对比(Top 5):")
print(results_df[['params', 'mean_test_score', 'rank_test_score']]
      .sort_values('rank_test_score')
      .head())
4.5.7 SVM回归 (SVR)
SVM不仅能做分类,还能做回归!

核心思想:
找一个"管道"(ε-tube),让尽可能多的点落在管道内

可视化:

    y
    │
    │    ● 
    │  ╱─────╲  ← ε-tube(允许误差范围)
    │─╱       ╲─
    │╱    ●    ╲
    │     ●     │
    │      ●    │
    │           │
    └──────────────── x

目标:
- 管道内的点:不计入损失
- 管道外的点:按距离惩罚

代码示例

from sklearn.svm import SVR
from sklearn.datasets import make_regression

# 生成回归数据
X, y = make_regression(
    n_samples=100,
    n_features=1,
    noise=10,
    random_state=42
)

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

# 创建SVR
svr_models = {
    'Linear': SVR(kernel='linear', C=1.0),
    'RBF': SVR(kernel='rbf', C=1.0, gamma='scale'),
    'Poly': SVR(kernel='poly', C=1.0, degree=2)
}

for name, model in svr_models.items():
    model.fit(X_train, y_train)
    
    train_score = model.score(X_train, y_train)
    test_score = model.score(X_test, y_test)
    
    print(f"\n{name} SVR:")
    print(f"  训练 R² = {train_score:.4f}")
    print(f"  测试 R² = {test_score:.4f}")
    print(f"  支持向量数 = {len(model.support_)}")
4.5.8 SVM的优缺点

✅ 优点

1. 高维数据效果好
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 特征数量远超样本数时仍然有效
   - 文本分类、基因数据等高维场景适用

2. 内存效率高
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 只需存储支持向量(通常很少)
   - 不需要存储所有训练数据

3. 核函数灵活
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 可以处理非线性问题
   - 可以自定义核函数

4. 理论基础扎实
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 有严格的数学证明
   - 凸优化问题,全局最优解

5. 泛化能力强
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 最大化间隔,不容易过拟合
   - 少量样本也能工作

❌ 缺点

1. 训练慢
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 大数据集(>10万样本)训练很慢
   - 时间复杂度 O(n²) 到 O(n³)
   
   例子:
   1000样本:几秒
   10000样本:几分钟
   100000样本:几小时甚至几天

2. 对参数敏感
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - C和gamma的选择很重要
   - 需要网格搜索调参(耗时)

3. 难以理解
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 黑盒模型,不直观
   - 无法像决策树那样看到决策过程

4. 对特征缩放敏感
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 必须标准化数据
   - 否则大特征会主导模型

5. 多分类问题复杂
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 原生只支持二分类
   - 多分类需要 one-vs-rest 或 one-vs-one
   - 增加计算量

6. 不输出概率
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 默认只输出类别
   - 需要额外计算才能得到概率(SVC(probability=True),更慢)
4.5.9 SVM vs 其他算法
┌─────────────────────────────────────────────────┐
│         SVM vs 其他算法对比                      │
├─────────────────────────────────────────────────┤
│                                                  │
│ 场景                   推荐算法                  │
│ ─────────────────────  ───────────────────────  │
│ 小数据集(<1000)        SVM ⭐                    │
│ 大数据集(>100K)        逻辑回归、神经网络        │
│ 高维稀疏数据           SVM、逻辑回归 ⭐          │
│ 图像分类               CNN(深度学习)⭐         │
│ 文本分类               SVM、朴素贝叶斯 ⭐        │
│ 需要概率输出           逻辑回归 ⭐               │
│ 需要快速预测           决策树、逻辑回归         │
│ 需要可解释性           决策树、逻辑回归 ⭐       │
│ 非线性复杂边界         SVM(RBF核)、神经网络 ⭐  │
│                                                  │
└─────────────────────────────────────────────────┘

性能对比(相同数据):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
算法          训练时间  预测时间  准确率
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
逻辑回归      1秒       <0.01秒   85%
决策树        2秒       <0.01秒   82%
随机森林      10秒      0.1秒     88%
SVM(linear)   5秒       0.05秒    87%
SVM(RBF)      30秒      0.1秒     90% ⭐
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4.5.10 SVM实战技巧

1. 数据预处理

"""
SVM对特征尺度敏感,必须标准化!
"""

from sklearn.preprocessing import StandardScaler

# 错误做法:直接训练
svm_bad = SVM(C=1.0, kernel='rbf')
svm_bad.fit(X_train, y_train)  # 如果特征尺度差异大,效果差

# 正确做法:先标准化
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

svm_good = SVM(C=1.0, kernel='rbf')
svm_good.fit(X_train_scaled, y_train)

2. 核函数选择

"""
快速决策树
"""

# 步骤1:先试线性核
linear_svm = SVC(kernel='linear')
linear_svm.fit(X_train, y_train)
linear_acc = linear_svm.score(X_test, y_test)

# 步骤2:如果线性核效果不好,试RBF
if linear_acc < 0.8:
    rbf_svm = SVC(kernel='rbf', gamma='scale')
    rbf_svm.fit(X_train, y_train)
    rbf_acc = rbf_svm.score(X_test, y_test)
    
    if rbf_acc > linear_acc:
        print("RBF核更好")
    else:
        print("线性核已足够")

3. 参数调优策略

"""
C和gamma的调优
"""

# 粗调:大范围搜索
param_grid_coarse = {
    'C': [0.1, 1, 10, 100],
    'gamma': [0.001, 0.01, 0.1, 1]
}

# 细调:在粗调最优值附近细搜
# 假设粗调得到 C=10, gamma=0.1
param_grid_fine = {
    'C': [5, 10, 15, 20],
    'gamma': [0.05, 0.1, 0.15, 0.2]
}

4. 大数据集加速

"""
数据量大时的技巧
"""

# 方法1:使用LinearSVC(专门针对线性核优化)
from sklearn.svm import LinearSVC

# 比SVC(kernel='linear')快很多
linear_svc = LinearSVC(C=1.0, max_iter=1000)
linear_svc.fit(X_train, y_train)

# 方法2:随机采样训练
# 如果数据太多,先用一部分训练
n_samples = min(10000, len(X_train))
indices = np.random.choice(len(X_train), n_samples, replace=False)

X_sample = X_train[indices]
y_sample = y_train[indices]

svm = SVC(kernel='rbf')
svm.fit(X_sample, y_sample)

# 方法3:使用SGDClassifier(随机梯度下降)
from sklearn.linear_model import SGDClassifier

# 类似SVM但用梯度下降,适合大数据
sgd_svm = SGDClassifier(
    loss='hinge',  # SVM损失函数
    max_iter=1000
)
sgd_svm.fit(X_train, y_train)

4.6 K近邻 (KNN)

4.6.1 什么是K近邻?
K-Nearest Neighbors (KNN)
核心思想:物以类聚,人以群分

原理:
一个样本的类别,由它周围最近的K个邻居决定
4.6.2 生活中的类比

场景1:搬家后找餐馆

你刚搬到新小区,想找好吃的餐馆

传统方法:
查攻略、看评价...

KNN方法:
1. 问周围5个邻居(K=5)
2. 3个人推荐A餐馆
   2个人推荐B餐馆
3. 结论:去A餐馆!(多数投票)

这就是KNN的思想:
相信邻居的选择

场景2:预测房价

某小区的房价:

你的房子:100平,不知道多少钱?

找3个最相似的房子(K=3):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
邻居1:98平,500万(很相似!)
邻居2:102平,510万(很相似!)
邻居3:95平,490万(比较相似)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

预测:(500 + 510 + 490) / 3 = 500万

逻辑:
相似的房子价格应该差不多
4.6.3 KNN的工作流程

分类任务

步骤1:计算距离
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
给定新样本x,计算它到所有训练样本的距离

示意图:
    
    ●                测试样本:●
  ●   ●          计算距离:d₁, d₂, d₃, ...
    ○
  ○   ○

步骤2:找K个最近邻
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
按距离排序,取前K个

K=3时:
距离排序:d₁=0.5, d₂=0.7, d₃=0.8, d₄=1.2, ...
选择:样本1, 样本2, 样本3

步骤3:投票决策
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
看这K个邻居的类别,多数获胜

例子(K=5):
邻居类别:[红, 红, 蓝, 红, 蓝]
投票结果:红3票,蓝2票
预测:红色 ✅

可视化示例

数据分布(2D):

    y
    │
    │  ●  ●
    │ ●  ●  ●
    │  ●  ●
    │         ?  ← 新样本,预测类别?
    │
    │    ○  ○
    │  ○  ○  ○
    │    ○  ○
    └──────────────── x

【K=1(1-NN)】
找1个最近邻:

    y
    │
    │  ●  ●
    │ ●  ●  ●
    │  ●  ●
    │         ●? ← 最近的是●
    │        ╱
    │    ○ ○╱ ○
    │  ○  ○  ○
    │    ○  ○
    └──────────────── x

预测:● (红色)

【K=3(3-NN)】
找3个最近邻:

    y
    │
    │  ●  ●
    │ ●  ●  ●
    │  ●  ●  ●
    │         ?
    │        ╱│╲
    │    ○ ○╱ │ ╲○
    │  ○  ○   │   ○
    │    ○  ○ │
    └──────────────── x

3个邻居:●, ●, ○
投票:● 2票,○ 1票
预测:● (红色)

【K=7(7-NN)】
找7个最近邻:

范围扩大,包含更多○
结果可能变成○

结论:K的选择很重要!
4.6.4 距离度量

常用距离公式

1. 欧氏距离 (Euclidean Distance) ⭐最常用

公式:
d(x, y) = √[(x₁-y₁)² + (x₂-y₂)² + ... + (xₙ-yₙ)²]

2D示例:
点A (1, 2)
点B (4, 6)

d = √[(4-1)² + (6-2)²]
  = √[9 + 16]
  = √25
  = 5

可视化:
    y
  6 │      B●
    │     ╱
  4 │    ╱
    │   ╱ ← 欧氏距离(直线)
  2 │ A●
    │
  0 └──────────── x
    0  2  4

特点:
- 最直观(直线距离)
- 对量纲敏感(需要标准化)

2. 曼哈顿距离 (Manhattan Distance)

公式:
d(x, y) = |x₁-y₁| + |x₂-y₂| + ... + |xₙ-yₙ|

2D示例:
点A (1, 2)
点B (4, 6)

d = |4-1| + |6-2|
  = 3 + 4
  = 7

可视化:
    y
  6 │      B●
    │      │
  4 │      │
    │      │ ← 只能横着走或竖着走
  2 │ A●───┘
    │
  0 └──────────── x
    0  2  4

名字由来:
像在曼哈顿(街区都是直角)行走
不能斜着走,只能沿街道

适用:
- 网格状数据
- 高维稀疏数据(文本)

3. 闵可夫斯基距离 (Minkowski Distance)

公式:
d(x, y) = [Σ|xᵢ-yᵢ|ᵖ]^(1/p)

特殊情况:
- p=1:曼哈顿距离
- p=2:欧氏距离
- p=∞:切比雪夫距离

灵活性:
可以通过p调整距离的"严格程度"

4. 余弦相似度 (Cosine Similarity)

公式:
similarity = (x·y) / (||x|| × ||y||)

distance = 1 - similarity

特点:
- 不关心向量长度,只关心方向
- 范围 [0, 1]

适用:
- 文本相似度(词向量)
- 推荐系统

可视化:
    y
    │
    │     B●
    │    ╱
    │   ╱ θ ← 夹角小 = 相似
    │  ╱
    │ A●
    └──────────── x

距离 = 1 - cos(θ)
4.6.5 K值的选择

K值对模型的影响

K太小(如K=1):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    y
    │  ●  ●
    │ ●  ●  ●
    │  ●  ●  ○ ← 噪声点
    │         ?
    │
    │    ○  ○
    │  ○  ○  ○
    └──────────────── x

问题:
- 对噪声敏感(过拟合)
- 决策边界不平滑

K太大(如K=N):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
相当于全局投票
→ 总是预测多数类
→ 欠拟合

最佳K值:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
通常选择:
- K = √N(N是样本数)
- 用交叉验证选择

经验:
- 小数据集:K=3-5
- 大数据集:K=10-20
- K通常选奇数(避免平票)

交叉验证选K

from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import cross_val_score
import numpy as np

# 测试不同的K值
k_range = range(1, 31)
cv_scores = []

for k in k_range:
    knn = KNeighborsClassifier(n_neighbors=k)
    scores = cross_val_score(
        knn, X_train, y_train,
        cv=5,  # 5折交叉验证
        scoring='accuracy'
    )
    cv_scores.append(scores.mean())

# 找到最佳K
best_k = k_range[np.argmax(cv_scores)]
print(f"最佳K值: {best_k}")
print(f"对应准确率: {max(cv_scores):.4f}")

# 可视化
plt.figure(figsize=(10, 6))
plt.plot(k_range, cv_scores, 'o-')
plt.xlabel('K Value')
plt.ylabel('Cross-Validation Accuracy')
plt.title('KNN: K vs Accuracy')
plt.axvline(x=best_k, color='r', linestyle='--', 
            label=f'Best K={best_k}')
plt.legend()
plt.grid(True)
plt.show()
4.6.6 代码实现

使用 Scikit-learn

from sklearn.neighbors import KNeighborsClassifier, KNeighborsRegressor
from sklearn.datasets import load_iris, load_boston
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import accuracy_score, classification_report

# ========== KNN分类 ==========
print("=" * 50)
print("KNN分类示例")
print("=" * 50)

# 加载数据
iris = load_iris()
X, y = iris.data, iris.target

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

# 标准化(重要!)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

# 创建KNN分类器
knn_clf = KNeighborsClassifier(
    n_neighbors=5,        # K=5
    weights='uniform',    # 投票方式:uniform(等权)或distance(按距离加权)
    metric='euclidean',   # 距离度量
    n_jobs=-1             # 使用所有CPU核心
)

# 训练
knn_clf.fit(X_train_scaled, y_train)

# 预测
y_pred = knn_clf.predict(X_test_scaled)

# 评估
accuracy = accuracy_score(y_test, y_pred)
print(f"\n准确率: {accuracy:.2%}")

# 查看某个样本的预测过程
sample_idx = 0
sample = X_test_scaled[sample_idx:sample_idx+1]

# 找K个最近邻
distances, indices = knn_clf.kneighbors(sample, n_neighbors=5)

print(f"\n样本 {sample_idx} 的5个最近邻:")
print(f"真实类别: {iris.target_names[y_test[sample_idx]]}")
print(f"预测类别: {iris.target_names[y_pred[sample_idx]]}")
print(f"\n邻居详情:")
for i, (dist, idx) in enumerate(zip(distances[0], indices[0])):
    neighbor_class = y_train[idx]
    print(f"  邻居{i+1}: 距离={dist:.4f}, 类别={iris.target_names[neighbor_class]}")

# 预测概率
y_pred_proba = knn_clf.predict_proba(X_test_scaled)
print(f"\n样本 {sample_idx} 的预测概率:")
for i, class_name in enumerate(iris.target_names):
    print(f"  {class_name}: {y_pred_proba[sample_idx][i]:.4f}")

# ========== 不同K值对比 ==========
print("\n" + "=" * 50)
print("不同K值对比")
print("=" * 50)

k_values = [1, 3, 5, 10, 20]

for k in k_values:
    knn = KNeighborsClassifier(n_neighbors=k)
    knn.fit(X_train_scaled, y_train)
    
    train_acc = knn.score(X_train_scaled, y_train)
    test_acc = knn.score(X_test_scaled, y_test)
    
    print(f"\nK={k:>2}:")
    print(f"  训练准确率: {train_acc:.2%}")
    print(f"  测试准确率: {test_acc:.2%}")

# ========== 不同距离度量对比 ==========
print("\n" + "=" * 50)
print("不同距离度量对比")
print("=" * 50)

metrics = ['euclidean', 'manhattan', 'minkowski']

for metric in metrics:
    knn = KNeighborsClassifier(
        n_neighbors=5,
        metric=metric,
        p=3 if metric == 'minkowski' else 2  # minkowski的p参数
    )
    knn.fit(X_train_scaled, y_train)
    
    accuracy = knn.score(X_test_scaled, y_test)
    print(f"\n{metric}距离:")
    print(f"  准确率: {accuracy:.2%}")

# ========== 加权KNN ==========
print("\n" + "=" * 50)
print("加权KNN (按距离加权)")
print("=" * 50)

# uniform: 所有邻居权重相同
knn_uniform = KNeighborsClassifier(
    n_neighbors=5,
    weights='uniform'
)
knn_uniform.fit(X_train_scaled, y_train)
uniform_acc = knn_uniform.score(X_test_scaled, y_test)

# distance: 距离越近,权重越大
knn_distance = KNeighborsClassifier(
    n_neighbors=5,
    weights='distance'  # 权重 = 1 / 距离
)
knn_distance.fit(X_train_scaled, y_train)
distance_acc = knn_distance.score(X_test_scaled, y_test)

print(f"\n等权重(uniform): {uniform_acc:.2%}")
print(f"距离加权(distance): {distance_acc:.2%}")

# ========== KNN回归 ==========
print("\n" + "=" * 50)
print("KNN回归示例")
print("=" * 50)

# 加载数据
from sklearn.datasets import fetch_california_housing
housing = fetch_california_housing()
X, y = housing.data[:1000], housing.target[:1000]  # 取部分数据

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

# 标准化
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

# 创建KNN回归器
knn_reg = KNeighborsRegressor(
    n_neighbors=5,
    weights='distance'  # 回归通常用distance权重更好
)

# 训练
knn_reg.fit(X_train_scaled, y_train)

# 预测
y_pred = knn_reg.predict(X_test_scaled)

# 评估
from sklearn.metrics import mean_squared_error, r2_score

mse = mean_squared_error(y_test, y_pred)
rmse = np.sqrt(mse)
r2 = r2_score(y_test, y_pred)

print(f"\nRMSE: {rmse:.4f}")
print(f"R² Score: {r2:.4f}")

# 查看预测详情
print(f"\n前5个样本:")
print(f"{'真实值':<10} {'预测值':<10} {'误差':<10}")
print("-" * 30)
for i in range(5):
    true_val = y_test.iloc[i] if hasattr(y_test, 'iloc') else y_test[i]
    pred_val = y_pred[i]
    error = abs(true_val - pred_val)
    print(f"{true_val:<10.2f} {pred_val:<10.2f} {error:<10.2f}")

可视化决策边界

def plot_knn_decision_boundary(k_values, X, y):
    """
    可视化不同K值的决策边界
    """
    fig, axes = plt.subplots(2, 2, figsize=(12, 12))
    axes = axes.ravel()
    
    for idx, k in enumerate(k_values):
        knn = KNeighborsClassifier(n_neighbors=k)
        knn.fit(X, y)
        
        # 创建网格
        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 = knn.predict(np.c_[xx.ravel(), yy.ravel()])
        Z = Z.reshape(xx.shape)
        
        # 绘图
        axes[idx].contourf(xx, yy, Z, alpha=0.3, cmap='RdYlBu')
        axes[idx].scatter(X[:, 0], X[:, 1], c=y, cmap='RdYlBu',
                         edgecolors='black', s=50)
        axes[idx].set_title(f'KNN with K={k}')
        axes[idx].set_xlabel('Feature 1')
        axes[idx].set_ylabel('Feature 2')
    
    plt.tight_layout()
    plt.savefig('knn_decision_boundaries.png', dpi=150)
    print("决策边界图已保存")

# 使用示例(选2个特征可视化)
X_2d = X[:, :2]  # 只用前两个特征
plot_knn_decision_boundary([1, 5, 10, 50], X_2d, y)
4.6.7 KNN的优化技巧

1. KD-Tree加速

"""
朴素KNN:计算所有点的距离 O(n)
KD-Tree:快速找最近邻 O(log n)

适用:低维数据(<20维)
"""

knn_kdtree = KNeighborsClassifier(
    n_neighbors=5,
    algorithm='kd_tree'  # 使用KD-Tree
)

# algorithm选项:
# - 'auto': 自动选择(默认)
# - 'ball_tree': Ball Tree算法
# - 'kd_tree': KD-Tree算法
# - 'brute': 暴力计算(小数据集)

2. 降维加速

"""
高维数据降维后再用KNN
"""

from sklearn.decomposition import PCA

# PCA降维
pca = PCA(n_components=10)  # 降到10维
X_train_pca = pca.fit_transform(X_train_scaled)
X_test_pca = pca.transform(X_test_scaled)

# 在降维后的数据上训练KNN
knn = KNeighborsClassifier(n_neighbors=5)
knn.fit(X_train_pca, y_train)
accuracy = knn.score(X_test_pca, y_test)

print(f"降维后KNN准确率: {accuracy:.2%}")

3. 近似最近邻(大数据)

"""
数据量很大时(>100万),精确KNN太慢
使用近似算法(牺牲一点准确性换速度)
"""

# 使用Annoy库(Approximate Nearest Neighbors)
from annoy import AnnoyIndex

# 构建索引
n_features = X_train.shape[1]
ann_index = AnnoyIndex(n_features, 'euclidean')

for i, x in enumerate(X_train):
    ann_index.add_item(i, x)

ann_index.build(10)  # 10棵树

# 查询最近邻(快很多!)
sample = X_test[0]
nearest_indices = ann_index.get_nnearest_neighbor(sample, 5)
4.6.8 KNN的优缺点

✅ 优点

1. 简单易懂
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 原理直观
   - 没有复杂的数学
   - 易于实现

2. 无需训练
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 直接记住所有数据
   - "训练"过程几乎瞬间完成
   - 属于"惰性学习"(Lazy Learning)

3. 适应性强
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 可以处理多分类
   - 可以做回归
   - 可以处理非线性问题

4. 对异常值鲁棒(K>1时)
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 多数投票机制
   - 单个异常点影响小

5. 无参数假设
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 不假设数据分布
   - 非参数模型

❌ 缺点

1. 预测慢
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 每次预测都要计算所有距离
   - 大数据集(>10万)几乎不可用
   
   预测时间:
   1000样本:<0.01秒
   10000样本:0.1秒
   100000样本:10秒  ❌
   1000000样本:几分钟  ❌❌❌

2. 内存占用大
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 需要存储所有训练数据
   - 100万样本,每样本100维
   - 需要 ~800MB 内存

3. 维度灾难
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 高维空间中,距离失去意义
   - 所有点距离都差不多
   
   例子:
   2D空间:最近和最远距离比 = 1:10
   100D空间:比值 → 1:1.1 (几乎一样!)

4. 对特征缩放敏感
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 必须标准化
   - 否则大特征主导距离计算

5. K值选择困难
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - K太小:过拟合
   - K太大:欠拟合
   - 需要交叉验证

6. 类别不平衡问题
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 多数类容易"淹没"少数类
   
   例子:
   邻居:[多, 多, 多, 多, 少]
   预测:多数类(即使少数类更相关)

4.7 朴素贝叶斯

4.7.1 什么是朴素贝叶斯?
Naive Bayes
基于贝叶斯定理的概率分类算法

核心思想:
通过计算概率来做决策
"在给定特征下,属于某类别的概率是多少?"
4.7.2 贝叶斯定理

通俗理解

场景:医生诊断

已知信息:
- 某种病的发病率:1%(先验概率)
- 检测准确率:95%(似然)

问题:
检测呈阳性,真的得病的概率是多少?

直觉答案:95%?
正确答案:不到20%!

为什么?
因为还要考虑"假阳性"

贝叶斯公式

P(A|B) = P(B|A) × P(A) / P(B)

用人话说:
P(得病|检测阳性) = P(检测阳性|得病) × P(得病) / P(检测阳性)

代入数字:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
P(得病) = 0.01                    ← 先验概率
P(检测阳性|得病) = 0.95           ← 真阳性率
P(检测阳性|不得病) = 0.05         ← 假阳性率

P(检测阳性) = P(检测阳性|得病)×P(得病) 
            + P(检测阳性|不得病)×P(不得病)
            = 0.95×0.01 + 0.05×0.99
            = 0.0095 + 0.0495
            = 0.059

P(得病|检测阳性) = 0.95 × 0.01 / 0.059
                 = 0.161
                 ≈ 16%

结论:检测阳性,真得病概率只有16%!
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4.7.3 朴素贝叶斯分类原理

分类问题

给定特征 X = (x₁, x₂, ..., xₙ)
预测类别 y

目标:计算 P(y|X) 对每个类别
选择概率最大的类别

贝叶斯公式:
P(y|X) = P(X|y) × P(y) / P(X)

其中:
- P(y):先验概率(类别y出现的概率)
- P(X|y):似然(在类别y下,X出现的概率)
- P(X):证据(X出现的概率,常数)
- P(y|X):后验概率(给定X,属于y的概率)

简化:
由于P(X)对所有类别相同,可以忽略
→ P(y|X) ∝ P(X|y) × P(y)

只需比较:P(X|y) × P(y) 的大小

"朴素"假设

问题:
P(X|y) = P(x₁, x₂, ..., xₙ|y) 太难算了

朴素假设:
特征之间相互独立

P(X|y) = P(x₁|y) × P(x₂|y) × ... × P(xₙ|y)

例子:
预测"是否打篮球"
特征:身高、体重、鞋码

朴素假设:
在打篮球的人中,身高、体重、鞋码独立
(虽然现实中它们相关,但我们"朴素"地假设独立)

为什么叫"朴素"?
因为这个假设太简单了,通常不成立
但实践中效果还不错!
4.7.4 朴素贝叶斯的三种变体

1. 高斯朴素贝叶斯 (Gaussian Naive Bayes)

适用:连续型特征(数值特征)

假设:
每个特征在给定类别下服从高斯分布(正态分布)

P(xᵢ|y) = 1/(√(2πσ²)) × exp(-(xᵢ-μ)²/(2σ²))

其中:
- μ:该特征在类别y下的均值
- σ²:该特征在类别y下的方差

例子:预测性别
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
特征:身高、体重

男性:
  身高 ~ N(175, 10)  均值175cm,标准差10
  体重 ~ N(70, 15)   均值70kg,标准差15

女性:
  身高 ~ N(160, 8)   均值160cm,标准差8
  体重 ~ N(55, 12)   均值55kg,标准差12

新样本:身高170cm,体重60kg

P(男|X) ∝ P(身高=170|男) × P(体重=60|男) × P(男)
P(女|X) ∝ P(身高=170|女) × P(体重=60|女) × P(女)

比较大小,选概率大的
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

2. 多项式朴素贝叶斯 (Multinomial Naive Bayes)

适用:离散特征(计数数据)

典型应用:文本分类

P(xᵢ|y) = (count(xᵢ, y) + α) / (count(y) + α×n)

其中:
- count(xᵢ, y):类别y中特征xᵢ出现的次数
- α:平滑参数(拉普拉斯平滑,防止概率为0)

例子:垃圾邮件过滤
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
训练数据:

垃圾邮件(100封):
  "免费" 出现 80次
  "中奖" 出现 60次
  "发票" 出现 70次
  "会议" 出现 5次

正常邮件(100封):
  "免费" 出现 5次
  "中奖" 出现 2次
  "发票" 出现 20次
  "会议" 出现 50次

新邮件:"恭喜您中奖,免费领取"

计算:
P(垃圾|"中奖","免费") 
  ∝ P("中奖"|垃圾) × P("免费"|垃圾) × P(垃圾)
  = (60/100) × (80/100) × (100/200)
  = 0.24

P(正常|"中奖","免费")
  ∝ P("中奖"|正常) × P("免费"|正常) × P(正常)
  = (2/100) × (5/100) × (100/200)
  = 0.0005

结论:垃圾邮件! ✓
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

3. 伯努利朴素贝叶斯 (Bernoulli Naive Bayes)

适用:二元特征(0/1特征)

P(xᵢ|y) = P(xᵢ=1|y)^xᵢ × P(xᵢ=0|y)^(1-xᵢ)

区别:
多项式NB:考虑特征出现次数
伯努利NB:只考虑特征是否出现

例子:文档分类
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
特征:词是否出现(不管出现几次)

文档:"免费 免费 中奖"

多项式NB:
  "免费" 出现2次,"中奖" 出现1次

伯努利NB:
  "免费" 出现,"中奖" 出现(次数忽略)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4.7.5 朴素贝叶斯实战:垃圾邮件过滤

完整示例

任务:判断邮件是否为垃圾邮件

训练数据:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
垃圾邮件:
  邮件1:"免费 中奖 恭喜"
  邮件2:"中奖 领取 免费"
  邮件3:"发票 免费 优惠"

正常邮件:
  邮件4:"会议 通知 明天"
  邮件5:"项目 进度 报告"
  邮件6:"发票 请查收"
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

步骤1:计算先验概率
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
P(垃圾) = 3/6 = 0.5
P(正常) = 3/6 = 0.5
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

步骤2:统计词频
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
垃圾邮件中:
  总词数:9
  "免费" 3次,"中奖" 2次,"发票" 1次...

正常邮件中:
  总词数:9
  "会议" 1次,"发票" 1次,"项目" 1次...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

步骤3:计算条件概率(拉普拉斯平滑)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
词汇表大小:V = 10(假设)
平滑参数:α = 1

P("免费"|垃圾) = (3+1) / (9+10) = 4/19 = 0.21
P("中奖"|垃圾) = (2+1) / (9+10) = 3/19 = 0.16
P("免费"|正常) = (0+1) / (9+10) = 1/19 = 0.05
P("中奖"|正常) = (0+1) / (9+10) = 1/19 = 0.05
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

步骤4:预测新邮件
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
新邮件:"恭喜 中奖 免费"

P(垃圾|邮件) ∝ P(垃圾) × P("恭喜"|垃圾) × 
               P("中奖"|垃圾) × P("免费"|垃圾)
             = 0.5 × 0.16 × 0.16 × 0.21
             = 0.00268

P(正常|邮件) ∝ P(正常) × P("恭喜"|正常) × 
               P("中奖"|正常) × P("免费"|正常)
             = 0.5 × 0.05 × 0.05 × 0.05
             = 0.0000625

结论:0.00268 > 0.0000625
→ 垃圾邮件!✓
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4.7.6 代码实现

使用 Scikit-learn

from sklearn.naive_bayes import (
    GaussianNB, MultinomialNB, BernoulliNB
)
from sklearn.datasets import load_iris, fetch_20newsgroups
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, classification_report
import numpy as np

# ========== 高斯朴素贝叶斯(连续特征)==========
print("=" * 50)
print("高斯朴素贝叶斯示例")
print("=" * 50)

# 加载数据
iris = load_iris()
X, y = iris.data, iris.target

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

# 创建高斯朴素贝叶斯
gnb = GaussianNB()

# 训练
gnb.fit(X_train, y_train)

# 预测
y_pred = gnb.predict(X_test)
y_pred_proba = gnb.predict_proba(X_test)

# 评估
accuracy = accuracy_score(y_test, y_pred)
print(f"\n准确率: {accuracy:.2%}")

# 查看学到的参数(均值和方差)
print(f"\n各类别各特征的均值:")
print("特征:", iris.feature_names)
for i, class_name in enumerate(iris.target_names):
    print(f"\n{class_name}:")
    print(f"  均值: {gnb.theta_[i]}")
    print(f"  方差: {gnb.var_[i]}")

# 预测单个样本
sample = X_test[0:1]
print(f"\n样本特征: {sample[0]}")
print(f"真实类别: {iris.target_names[y_test[0]]}")
print(f"预测类别: {iris.target_names[y_pred[0]]}")
print(f"预测概率:")
for i, class_name in enumerate(iris.target_names):
    print(f"  {class_name}: {y_pred_proba[0][i]:.4f}")

# ========== 多项式朴素贝叶斯(文本分类)==========
print("\n" + "=" * 50)
print("多项式朴素贝叶斯 - 文本分类")
print("=" * 50)

# 模拟垃圾邮件数据
spam_docs = [
    "免费 中奖 恭喜 领取",
    "优惠 免费 大礼包",
    "中奖 免费 发票",
    "恭喜 中奖 免费 领取"
]

ham_docs = [
    "会议 通知 明天 参加",
    "项目 进度 报告",
    "发票 请 查收",
    "工作 安排 通知"
]

# 合并数据
docs = spam_docs + ham_docs
labels = [1]*len(spam_docs) + [0]*len(ham_docs)  # 1=垃圾,0=正常

# 文本向量化(词袋模型)
vectorizer = CountVectorizer()
X = vectorizer.fit_transform(docs)

print(f"\n词汇表: {vectorizer.get_feature_names_out()}")
print(f"文档-词矩阵形状: {X.shape}")

# 划分数据(这里数据少,仅做演示)
X_train, X_test, y_train, y_test = train_test_split(
    X, labels, test_size=0.25, random_state=42
)

# 多项式朴素贝叶斯
mnb = MultinomialNB(alpha=1.0)  # alpha=1: 拉普拉斯平滑
mnb.fit(X_train, y_train)

# 预测
y_pred = mnb.predict(X_test)
accuracy = mnb.score(X_test, y_test)

print(f"\n准确率: {accuracy:.2%}")

# 测试新邮件
test_docs = [
    "恭喜 免费 中奖",
    "会议 项目 通知"
]

test_X = vectorizer.transform(test_docs)
predictions = mnb.predict(test_X)
probas = mnb.predict_proba(test_X)

for i, doc in enumerate(test_docs):
    result = "垃圾邮件" if predictions[i] == 1 else "正常邮件"
    print(f"\n邮件: \"{doc}\"")
    print(f"预测: {result}")
    print(f"概率: 垃圾={probas[i][1]:.4f}, 正常={probas[i][0]:.4f}")

# 查看特征对分类的影响
feature_names = vectorizer.get_feature_names_out()
log_probs = mnb.feature_log_prob_

print(f"\n各类别下词的log概率:")
print(f"\n正常邮件中重要的词:")
normal_idx = 0  # 正常邮件类别索引
top_normal = np.argsort(log_probs[normal_idx])[-5:]
for idx in reversed(top_normal):
    print(f"  {feature_names[idx]}: {np.exp(log_probs[normal_idx][idx]):.4f}")

print(f"\n垃圾邮件中重要的词:")
spam_idx = 1  # 垃圾邮件类别索引
top_spam = np.argsort(log_probs[spam_idx])[-5:]
for idx in reversed(top_spam):
    print(f"  {feature_names[idx]}: {np.exp(log_probs[spam_idx][idx]):.4f}")

# ========== 伯努利朴素贝叶斯 ==========
print("\n" + "=" * 50)
print("伯努利朴素贝叶斯")
print("=" * 50)

# 伯努利NB:二值化特征(是否出现)
bnb = BernoulliNB(alpha=1.0)

# 二值化数据(>0的变为1)
X_binary = (X > 0).astype(int)
X_train_bin, X_test_bin, y_train, y_test = train_test_split(
    X_binary, labels, test_size=0.25, random_state=42
)

bnb.fit(X_train_bin, y_train)
accuracy = bnb.score(X_test_bin, y_test)

print(f"\n准确率: {accuracy:.2%}")

# ========== 真实数据集:20类新闻分类 ==========
print("\n" + "=" * 50)
print("真实数据集:20 Newsgroups")
print("=" * 50)

# 加载部分类别(加快演示)
categories = ['alt.atheism', 'soc.religion.christian', 
              'comp.graphics', 'sci.med']

newsgroups_train = fetch_20newsgroups(
    subset='train',
    categories=categories,
    shuffle=True,
    random_state=42,
    remove=('headers', 'footers', 'quotes')  # 去除无关信息
)

newsgroups_test = fetch_20newsgroups(
    subset='test',
    categories=categories,
    shuffle=True,
    random_state=42,
    remove=('headers', 'footers', 'quotes')
)

print(f"\n训练样本数: {len(newsgroups_train.data)}")
print(f"测试样本数: {len(newsgroups_test.data)}")
print(f"类别: {newsgroups_train.target_names}")

# 文本向量化
from sklearn.feature_extraction.text import TfidfVectorizer

vectorizer = TfidfVectorizer(
    max_features=5000,  # 最多5000个词
    min_df=2,           # 至少出现2次
    stop_words='english'  # 去除停用词
)

X_train = vectorizer.fit_transform(newsgroups_train.data)
X_test = vectorizer.transform(newsgroups_test.data)

print(f"\n特征数量: {X_train.shape[1]}")

# 训练多项式朴素贝叶斯
mnb = MultinomialNB(alpha=0.1)
mnb.fit(X_train, newsgroups_train.target)

# 预测
y_pred = mnb.predict(X_test)
accuracy = accuracy_score(newsgroups_test.target, y_pred)

print(f"\n准确率: {accuracy:.2%}")

# 分类报告
print("\n分类报告:")
print(classification_report(
    newsgroups_test.target, 
    y_pred,
    target_names=newsgroups_train.target_names
))

# 测试一些文档
test_samples = [
    "God is love and we should follow Jesus",
    "Graphics card driver installation guide",
    "Symptoms and treatment of diabetes",
    "I don't believe in any religion"
]

sample_X = vectorizer.transform(test_samples)
predictions = mnb.predict(sample_X)

print("\n测试文档分类:")
for doc, pred in zip(test_samples, predictions):
    print(f"\n文档: {doc}")
    print(f"分类: {newsgroups_train.target_names[pred]}")

从零实现朴素贝叶斯

import numpy as np
from collections import defaultdict

class SimpleNaiveBayes:
    """
    简单的多项式朴素贝叶斯实现
    """
    def __init__(self, alpha=1.0):
        """
        alpha: 拉普拉斯平滑参数
        """
        self.alpha = alpha
        self.class_prior = {}      # 先验概率 P(y)
        self.feature_prob = {}     # 条件概率 P(xi|y)
        self.classes = None
        self.n_features = None
        
    def fit(self, X, y):
        """
        训练模型
        X: 特征矩阵 [n_samples, n_features]
        y: 标签向量 [n_samples]
        """
        self.classes = np.unique(y)
        self.n_features = X.shape[1]
        n_samples = X.shape[0]
        
        # 计算先验概率 P(y)
        for c in self.classes:
            self.class_prior[c] = np.sum(y == c) / n_samples
        
        # 计算条件概率 P(xi|y)
        for c in self.classes:
            # 该类别的所有样本
            X_c = X[y == c]
            
            # 统计每个特征的总出现次数
            feature_counts = X_c.sum(axis=0) + self.alpha
            total_count = feature_counts.sum()
            
            # 计算概率
            self.feature_prob[c] = feature_counts / total_count
        
        return self
    
    def predict(self, X):
        """
        预测
        """
        predictions = []
        
        for x in X:
            # 计算每个类别的后验概率
            posteriors = {}
            
            for c in self.classes:
                # log概率(避免下溢)
                log_prior = np.log(self.class_prior[c])
                
                # log似然
                log_likelihood = np.sum(
                    x * np.log(self.feature_prob[c])
                )
                
                posteriors[c] = log_prior + log_likelihood
            
            # 选择概率最大的类别
            pred = max(posteriors, key=posteriors.get)
            predictions.append(pred)
        
        return np.array(predictions)
    
    def predict_proba(self, X):
        """
        预测概率
        """
        probas = []
        
        for x in X:
            posteriors = {}
            
            for c in self.classes:
                log_prior = np.log(self.class_prior[c])
                log_likelihood = np.sum(
                    x * np.log(self.feature_prob[c])
                )
                posteriors[c] = log_prior + log_likelihood
            
            # 归一化为概率
            max_log = max(posteriors.values())
            exp_posteriors = {
                c: np.exp(log_prob - max_log)
                for c, log_prob in posteriors.items()
            }
            
            total = sum(exp_posteriors.values())
            proba = np.array([
                exp_posteriors[c] / total 
                for c in self.classes
            ])
            
            probas.append(proba)
        
        return np.array(probas)

# 使用示例
from sklearn.feature_extraction.text import CountVectorizer

# 训练数据
docs = [
    "免费 中奖 恭喜",
    "优惠 免费 大礼包",
    "会议 通知 明天",
    "项目 进度 报告"
]
labels = [1, 1, 0, 0]  # 1=垃圾,0=正常

# 向量化
vectorizer = CountVectorizer()
X = vectorizer.fit_transform(docs).toarray()

# 训练自实现的朴素贝叶斯
nb = SimpleNaiveBayes(alpha=1.0)
nb.fit(X, np.array(labels))

# 测试
test_docs = ["恭喜 免费", "会议 项目"]
test_X = vectorizer.transform(test_docs).toarray()

predictions = nb.predict(test_X)
probas = nb.predict_proba(test_X)

print("自实现朴素贝叶斯:")
for i, doc in enumerate(test_docs):
    result = "垃圾" if predictions[i] == 1 else "正常"
    print(f"\n文档: {doc}")
    print(f"预测: {result}")
    print(f"概率: 正常={probas[i][0]:.4f}, 垃圾={probas[i][1]:.4f}")
4.7.7 拉普拉斯平滑

为什么需要平滑?

问题:零概率

训练数据中,某个词在某类别中从未出现
→ P(词|类别) = 0
→ 整个后验概率变为0!

例子:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
垃圾邮件训练集中从未出现"Python"这个词

新邮件:"Python 免费 教程"

P(垃圾|邮件) ∝ P("Python"|垃圾) × P("免费"|垃圾) × ...
             = 0 × 0.8 × ...
             = 0  ❌

即使有"免费"这个强垃圾特征,
因为"Python"概率为0,整体就是0了!
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

拉普拉斯平滑

公式:
P(词|类别) = (count(词, 类别) + α) / (count(类别) + α×V)

其中:
- α:平滑参数(通常α=1)
- V:词汇表大小

效果:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
原始:
P("Python"|垃圾) = 0/100 = 0  ❌

平滑后(α=1, V=1000):
P("Python"|垃圾) = (0+1)/(100+1000) = 1/1100 ≈ 0.0009  ✓

虽然概率很小,但不是0
不会影响其他特征的贡献
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

α的选择:
- α=0:不平滑(可能出现零概率)
- α=1:拉普拉斯平滑(最常用)
- α>1:更强的平滑(对罕见词更宽容)
4.7.8 朴素贝叶斯的优缺点

✅ 优点

1. 简单快速
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 训练和预测都很快
   - 适合大规模数据
   - 实时系统可用

2. 小数据集效果好
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 即使训练样本少,也能工作
   - 不容易过拟合

3. 可解释性强
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 概率模型,输出有概率意义
   - 可以看出哪些特征重要

4. 多分类自然支持
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 不需要one-vs-rest
   - 直接比较所有类别概率

5. 特征独立假设的好处
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 降低计算复杂度
   - 减少所需参数

6. 对缺失值鲁棒
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 缺失的特征直接忽略
   - 用其他特征计算

❌ 缺点

1. 独立性假设太强
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 现实中特征通常相关
   - 假设不成立会影响准确率
   
   例子:
   身高和体重明显相关
   但朴素贝叶斯假设它们独立

2. 零概率问题
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 训练集中未出现的特征概率为0
   - 需要平滑技术

3. 对特征分布敏感
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 高斯NB假设正态分布
   - 如果假设不符,效果差

4. 连续特征需要离散化
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 或者假设分布(高斯)
   - 可能损失信息

5. 概率估计不准
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 分类准确,但概率值可能不准
   - 不能直接用作置信度
4.7.9 朴素贝叶斯的应用场景
✅ 特别适合:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 文本分类
   - 垃圾邮件过滤 ⭐⭐⭐⭐⭐
   - 情感分析
   - 主题分类
   - 新闻分类

2. 实时分类
   - 需要快速响应
   - 在线学习

3. 多分类问题
   - 类别数量多
   - 新闻多分类、商品分类

4. 高维数据
   - 文本(成千上万词)
   - 基因数据

❌ 不太适合:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 特征高度相关
   - 图像识别(像素相关)
   - 时间序列(前后相关)

2. 需要精确概率
   - 医疗诊断需要准确概率

3. 复杂非线性问题
   - 深度学习更合适
4.7.10 朴素贝叶斯实战技巧

1. 选择合适的变体

"""
根据数据类型选择
"""

# 连续特征(数值)
if 数据是连续值:
    model = GaussianNB()

# 离散特征(计数)
if 数据是词频:
    model = MultinomialNB()

# 二值特征(0/1)
if 数据是二值:
    model = BernoulliNB()

2. 文本预处理

"""
文本分类的预处理很重要
"""

from sklearn.feature_extraction.text import TfidfVectorizer

vectorizer = TfidfVectorizer(
    max_features=5000,      # 限制词汇量
    min_df=2,               # 至少出现2次
    max_df=0.8,             # 最多出现在80%文档中
    stop_words='english',   # 去停用词
    ngram_range=(1, 2)      # 1-gram和2-gram
)

X = vectorizer.fit_transform(documents)

3. 平滑参数调优

"""
α太小:可能零概率
α太大:过度平滑
"""

from sklearn.model_selection import GridSearchCV

param_grid = {'alpha': [0.01, 0.1, 0.5, 1.0, 2.0, 5.0]}

grid_search = GridSearchCV(
    MultinomialNB(),
    param_grid,
    cv=5,
    scoring='accuracy'
)

grid_search.fit(X_train, y_train)
best_alpha = grid_search.best_params_['alpha']

4. 特征选择

"""
高维文本数据,选择重要特征
"""

from sklearn.feature_selection import chi2, SelectKBest

# 卡方检验选择top 1000特征
selector = SelectKBest(chi2, k=1000)
X_train_selected = selector.fit_transform(X_train, y_train)
X_test_selected = selector.transform(X_test)

# 训练
nb = MultinomialNB()
nb.fit(X_train_selected, y_train)

4.8 K-Means聚类

4.8.1 什么是K-Means?
K-Means聚类
无监督学习算法

目标:
把相似的样本自动分到同一组(簇)

特点:
- 无需标签
- 自动发现数据结构
- 简单高效
4.8.2 直观理解

场景:餐厅选址

某城市有100个小区,要开3家餐厅
希望每家餐厅服务附近的小区

K-Means思路:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 随机选3个位置作为餐厅(初始中心)

2. 把每个小区分配给最近的餐厅

3. 重新计算:每组小区的中心位置

4. 把餐厅搬到新中心

5. 重复2-4,直到餐厅位置不再变化

最终:
- 每家餐厅在其服务小区的中心
- 每个小区去最近的餐厅
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

可视化过程

初始状态(K=3,随机初始化):

    y
    │
    │  ●  ●  ●
    │    ●  ●
    │  ●  ●
    │         ✕  ← 初始中心1
    │  ●  ●
    │    ●
    │      ●  ✕  ← 初始中心2
    │        ●
    │  ●  ●
    │    ✕        ← 初始中心3
    └──────────────── x

迭代1:分配样本到最近中心

    y
    │
    │  ●  ●  ●   ← 簇1(红色)
    │    ●  ●
    │  ●  ●
    │         ✕
    │  ●  ●      ← 簇2(蓝色)
    │    ●
    │      ●  ✕
    │        ●   ← 簇3(绿色)
    │  ●  ●
    │    ✕
    └──────────────── x

迭代1:更新中心(移到簇的质心)

    y
    │
    │  ●  ●  ●
    │    ✕  ●   ← 新中心1
    │  ●  ●
    │
    │  ●  ●
    │    ✕       ← 新中心2
    │      ●
    │        ●
    │  ●  ●
    │    ✕       ← 新中心3
    └──────────────── x

重复...

收敛后:

    y
    │
    │  ● ✕●  ●   ← 簇1
    │    ●  ●
    │
    │  ●  ●
    │  ● ✕●      ← 簇2
    │      ●
    │
    │  ●  ●
    │  ● ✕●      ← 簇3
    │
    └──────────────── x

每个簇内部紧密,簇间距离大
4.8.3 K-Means算法流程

算法步骤

输入:
- 数据集 X = {x₁, x₂, ..., xₙ}
- 簇数量 K

步骤:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 初始化
   随机选择K个样本作为初始中心点
   μ₁, μ₂, ..., μₖ

2. 分配簇(E步)
   for 每个样本 xᵢ:
       计算到每个中心的距离
       分配给最近的中心
       
   数学表达:
   cᵢ = argmin ||xᵢ - μⱼ||²
         j

3. 更新中心(M步)
   for 每个簇 j:
       计算簇内所有点的均值
       更新中心为均值
   
   数学表达:
   μⱼ = (1/|Cⱼ|) Σ xᵢ
                 i∈Cⱼ

4. 检查收敛
   if 中心不再变化 or 达到最大迭代次数:
       停止
   else:
       回到步骤2
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

输出:
- K个簇中心
- 每个样本的簇标签

详细示例

数据:5个点(2D)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
x₁ = (1, 2)
x₂ = (2, 1)
x₃ = (8, 8)
x₄ = (8, 9)
x₅ = (9, 8)

K = 2(分成2簇)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

【初始化】
随机选:
  中心1 μ₁ = (1, 2) = x₁
  中心2 μ₂ = (8, 8) = x₃

【迭代1 - 分配簇】
计算距离:

x₁到μ₁: ||(1,2)-(1,2)||² = 0
x₁到μ₂: ||(1,2)-(8,8)||² = 85
→ x₁属于簇1

x₂到μ₁: ||(2,1)-(1,2)||² = 2
x₂到μ₂: ||(2,1)-(8,8)||² = 85
→ x₂属于簇1

x₃到μ₁: ||(8,8)-(1,2)||² = 85
x₃到μ₂: ||(8,8)-(8,8)||² = 0
→ x₃属于簇2

x₄到μ₁: ||(8,9)-(1,2)||² = 98
x₄到μ₂: ||(8,9)-(8,8)||² = 1
→ x₄属于簇2

x₅到μ₁: ||(9,8)-(1,2)||² = 100
x₅到μ₂: ||(9,8)-(8,8)||² = 1
→ x₅属于簇2

结果:
  簇1: {x₁, x₂}
  簇2: {x₃, x₄, x₅}

【迭代1 - 更新中心】
μ₁ = ((1+2)/2, (2+1)/2) = (1.5, 1.5)
μ₂ = ((8+8+9)/3, (8+9+8)/3) = (8.33, 8.33)

【迭代2 - 分配簇】
重新计算...
发现分配不变

【收敛】
中心不再变化,算法结束!

最终结果:
  簇1: 左下角的点 {x₁, x₂}
  簇2: 右上角的点 {x₃, x₄, x₅}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4.8.4 K值的选择

肘部法则 (Elbow Method)

思路:
K越大,簇内距离越小(每个簇更紧密)
但K太大没意义(极端情况:每个点一个簇)

寻找"拐点":
- K小时,增加K显著降低误差
- K大时,增加K改进不明显
- 拐点就是最佳K

可视化:

误差
  │
  │●
  │ ●
  │  ●
  │   ●___
  │       ●___
  │           ●___●___●
  └─────────────────────→ K
   1  2  3  4  5  6  7  8

  ↑
  肘部(K=4最佳)

轮廓系数 (Silhouette Score)

定义:
衡量样本与其簇的相似度

公式:
s(i) = (b(i) - a(i)) / max(a(i), b(i))

其中:
- a(i):样本i到同簇其他点的平均距离
- b(i):样本i到最近其他簇的平均距离

范围:[-1, 1]
- s ≈ 1:样本在正确的簇(很好)
- s ≈ 0:样本在簇边界(模棱两可)
- s < 0:样本可能分错簇(不好)

平均轮廓系数越大,聚类质量越好
4.8.5 代码实现

使用 Scikit-learn

from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs
from sklearn.metrics import silhouette_score
import matplotlib.pyplot as plt
import numpy as np

# ========== 基础K-Means ==========
print("=" * 50)
print("K-Means聚类示例")
print("=" * 50)

# 生成聚类数据
X, y_true = make_blobs(
    n_samples=300,
    centers=4,
    cluster_std=0.60,
    random_state=42
)

print(f"数据形状: {X.shape}")
print(f"真实簇数: {len(np.unique(y_true))}")

# 创建K-Means
kmeans = KMeans(
    n_clusters=4,          # 簇数量
    init='k-means++',      # 初始化方法(改进的随机初始化)
    n_init=10,             # 运行10次,选最好的
    max_iter=300,          # 最大迭代次数
    random_state=42
)

# 拟合
kmeans.fit(X)

# 预测(分配簇标签)
labels = kmeans.labels_
centers = kmeans.cluster_centers_

print(f"\n簇标签: {np.unique(labels)}")
print(f"每个簇的样本数:")
for i in range(4):
    count = np.sum(labels == i)
    print(f"  簇{i}: {count}个样本")

print(f"\n簇中心坐标:")
for i, center in enumerate(centers):
    print(f"  簇{i}: {center}")

# 惯性(簇内距离平方和)
print(f"\n惯性(越小越好): {kmeans.inertia_:.2f}")

# 轮廓系数
silhouette = silhouette_score(X, labels)
print(f"轮廓系数(越大越好): {silhouette:.4f}")

# 预测新样本
new_samples = np.array([[0, 0], [10, 10]])
new_labels = kmeans.predict(new_samples)
print(f"\n新样本 {new_samples[0]} 属于簇 {new_labels[0]}")
print(f"新样本 {new_samples[1]} 属于簇 {new_labels[1]}")

# ========== 肘部法则选择K ==========
print("\n" + "=" * 50)
print("肘部法则选择最佳K值")
print("=" * 50)

inertias = []
silhouettes = []
K_range = range(2, 11)

for k in K_range:
    kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
    kmeans.fit(X)
    
    inertias.append(kmeans.inertia_)
    silhouettes.append(silhouette_score(X, kmeans.labels_))
    
    print(f"K={k}: 惯性={kmeans.inertia_:.2f}, "
          f"轮廓系数={silhouettes[-1]:.4f}")

# 可视化
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))

# 肘部法则图
ax1.plot(K_range, inertias, 'bo-')
ax1.set_xlabel('K (簇数量)')
ax1.set_ylabel('惯性')
ax1.set_title('肘部法则')
ax1.grid(True)

# 轮廓系数图
ax2.plot(K_range, silhouettes, 'ro-')
ax2.set_xlabel('K (簇数量)')
ax2.set_ylabel('轮廓系数')
ax2.set_title('轮廓系数法')
ax2.grid(True)

plt.tight_layout()
plt.savefig('kmeans_k_selection.png', dpi=150)
print("\nK值选择图已保存")

# ========== 可视化聚类结果 ==========
def plot_kmeans_result(X, labels, centers, title):
    """
    可视化K-Means聚类结果
    """
    plt.figure(figsize=(10, 6))
    
    # 绘制样本点
    scatter = plt.scatter(
        X[:, 0], X[:, 1],
        c=labels,
        cmap='viridis',
        s=50,
        alpha=0.6,
        edgecolors='black'
    )
    
    # 绘制簇中心
    plt.scatter(
        centers[:, 0], centers[:, 1],
        c='red',
        s=300,
        alpha=0.8,
        marker='X',
        edgecolors='black',
        linewidths=2,
        label='Cluster Centers'
    )
    
    plt.xlabel('Feature 1')
    plt.ylabel('Feature 2')
    plt.title(title)
    plt.colorbar(scatter, label='Cluster Label')
    plt.legend()
    plt.grid(True, alpha=0.3)
    
    return plt

# 绘制不同K值的聚类结果
fig, axes = plt.subplots(2, 2, figsize=(14, 12))
axes = axes.ravel()

for idx, k in enumerate([2, 3, 4, 5]):
    kmeans = KMeans(n_clusters=k, random_state=42, n_init=10)
    labels = kmeans.fit_predict(X)
    centers = kmeans.cluster_centers_
    
    ax = axes[idx]
    scatter = ax.scatter(
        X[:, 0], X[:, 1],
        c=labels,
        cmap='viridis',
        s=50,
        alpha=0.6
    )
    ax.scatter(
        centers[:, 0], centers[:, 1],
        c='red',
        s=200,
        marker='X',
        edgecolors='black',
        linewidths=2
    )
    ax.set_title(f'K={k}, 惯性={kmeans.inertia_:.2f}')
    ax.set_xlabel('Feature 1')
    ax.set_ylabel('Feature 2')
    ax.grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('kmeans_different_k.png', dpi=150)
print("不同K值聚类结果图已保存")

# ========== 迭代过程可视化 ==========
def plot_kmeans_iterations(X, n_clusters=3, max_iter=10):
    """
    可视化K-Means迭代过程
    """
    # 随机初始化中心
    np.random.seed(42)
    indices = np.random.choice(len(X), n_clusters, replace=False)
    centers = X[indices].copy()
    
    fig, axes = plt.subplots(2, 5, figsize=(20, 8))
    axes = axes.ravel()
    
    for iteration in range(max_iter):
        # 分配簇
        distances = np.sqrt(((X - centers[:, np.newaxis])**2).sum(axis=2))
        labels = np.argmin(distances, axis=0)
        
        # 绘图
        ax = axes[iteration]
        ax.scatter(X[:, 0], X[:, 1], c=labels, cmap='viridis', 
                  s=50, alpha=0.6)
        ax.scatter(centers[:, 0], centers[:, 1], c='red', 
                  s=200, marker='X', edgecolors='black', linewidths=2)
        ax.set_title(f'迭代 {iteration + 1}')
        ax.grid(True, alpha=0.3)
        
        # 更新中心
        new_centers = np.array([
            X[labels == i].mean(axis=0) 
            for i in range(n_clusters)
        ])
        
        # 检查收敛
        if np.allclose(centers, new_centers):
            print(f"在第 {iteration + 1} 次迭代收敛")
            break
        
        centers = new_centers
    
    plt.tight_layout()
    plt.savefig('kmeans_iterations.png', dpi=150)
    print("迭代过程图已保存")

plot_kmeans_iterations(X, n_clusters=4)

# ========== 实际应用:图像压缩 ==========
print("\n" + "=" * 50)
print("应用:使用K-Means进行图像压缩")
print("=" * 50)

from sklearn.datasets import load_sample_image
from PIL import Image

# 加载示例图像(或使用你自己的图像)
# 这里我们创建一个简单的示例
from sklearn.datasets import load_sample_image

china = load_sample_image("china.jpg")
print(f"\n原始图像形状: {china.shape}")
print(f"原始图像大小: {china.nbytes / 1024:.2f} KB")

# 重塑为2D数组(像素 × RGB)
h, w, d = china.shape
image_array = china.reshape(h * w, d)

# 使用K-Means聚类颜色(减少颜色数量)
n_colors = 64  # 从数百万种颜色减少到64种

kmeans = KMeans(n_clusters=n_colors, random_state=42, n_init=10)
labels = kmeans.fit_predict(image_array)
centers = kmeans.cluster_centers_

# 用簇中心颜色替换原始颜色
compressed_image = centers[labels].reshape(h, w, d).astype(np.uint8)

print(f"\n压缩后图像使用 {n_colors} 种颜色")
print(f"压缩比: {image_array.nbytes / (centers.nbytes + labels.nbytes):.2f}x")

# 保存结果
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6))

ax1.imshow(china)
ax1.set_title('原始图像 (数百万色)')
ax1.axis('off')

ax2.imshow(compressed_image)
ax2.set_title(f'压缩图像 ({n_colors}色)')
ax2.axis('off')

plt.tight_layout()
plt.savefig('kmeans_image_compression.png', dpi=150)
print("图像压缩结果已保存")

从零实现K-Means

import numpy as np

class SimpleKMeans:
    """
    简单的K-Means实现
    """
    def __init__(self, n_clusters=3, max_iter=300, tol=1e-4):
        """
        n_clusters: 簇数量
        max_iter: 最大迭代次数
        tol: 收敛阈值
        """
        self.n_clusters = n_clusters
        self.max_iter = max_iter
        self.tol = tol
        self.centers = None
        self.labels_ = None
        self.inertia_ = None
        
    def fit(self, X):
        """
        拟合模型
        """
        n_samples, n_features = X.shape
        
        # 随机初始化中心(选择K个随机样本)
        indices = np.random.choice(
            n_samples, 
            self.n_clusters, 
            replace=False
        )
        self.centers = X[indices].copy()
        
        for iteration in range(self.max_iter):
            # E步:分配簇
            labels = self._assign_clusters(X)
            
            # M步:更新中心
            new_centers = self._update_centers(X, labels)
            
            # 检查收敛
            center_shift = np.sqrt(
                ((new_centers - self.centers) ** 2).sum()
            )
            
            if center_shift < self.tol:
                print(f"在第 {iteration + 1} 次迭代收敛")
                break
            
            self.centers = new_centers
        
        # 最终标签和惯性
        self.labels_ = labels
        self.inertia_ = self._compute_inertia(X, labels)
        
        return self
    
    def _assign_clusters(self, X):
        """
        将样本分配到最近的簇
        """
        # 计算每个样本到每个中心的距离
        distances = np.sqrt(
            ((X - self.centers[:, np.newaxis]) ** 2).sum(axis=2)
        )
        
        # 选择最近的中心
        labels = np.argmin(distances, axis=0)
        
        return labels
    
    def _update_centers(self, X, labels):
        """
        更新簇中心为簇内样本的均值
        """
        centers = np.zeros((self.n_clusters, X.shape[1]))
        
        for k in range(self.n_clusters):
            # 该簇的所有样本
            cluster_samples = X[labels == k]
            
            if len(cluster_samples) > 0:
                centers[k] = cluster_samples.mean(axis=0)
            else:
                # 空簇:随机重新初始化
                centers[k] = X[np.random.choice(len(X))]
        
        return centers
    
    def _compute_inertia(self, X, labels):
        """
        计算惯性(簇内距离平方和)
        """
        inertia = 0
        for k in range(self.n_clusters):
            cluster_samples = X[labels == k]
            if len(cluster_samples) > 0:
                inertia += ((cluster_samples - self.centers[k]) ** 2).sum()
        
        return inertia
    
    def predict(self, X):
        """
        预测新样本的簇标签
        """
        return self._assign_clusters(X)

# 使用示例
X, _ = make_blobs(n_samples=300, centers=4, random_state=42)

# 自实现K-Means
kmeans_custom = SimpleKMeans(n_clusters=4, max_iter=100)
kmeans_custom.fit(X)

print("\n自实现K-Means:")
print(f"惯性: {kmeans_custom.inertia_:.2f}")
print(f"中心点:\n{kmeans_custom.centers}")

# 对比sklearn
from sklearn.cluster import KMeans
kmeans_sklearn = KMeans(n_clusters=4, random_state=42, n_init=10)
kmeans_sklearn.fit(X)

print(f"\nSklearn K-Means:")
print(f"惯性: {kmeans_sklearn.inertia_:.2f}")
4.8.6 K-Means的改进算法

K-Means++初始化

问题:随机初始化可能导致:
- 收敛慢
- 陷入局部最优
- 结果不稳定

K-Means++改进:
更聪明地选择初始中心

算法:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 随机选一个点作为第1个中心

2. for k = 2 to K:
     计算每个点到已选中心的最短距离 D(x)
     按概率 D(x)² 选择下一个中心
     (距离远的点被选中概率大)

3. 执行标准K-Means

好处:
- 初始中心分散
- 收敛更快
- 结果更稳定
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Mini-Batch K-Means

问题:大数据集K-Means很慢

Mini-Batch改进:
每次只用一小批数据更新中心

算法:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
for iteration:
    1. 随机采样一批数据(mini-batch)
    2. 分配这批数据到最近的簇
    3. 更新中心(只用这批数据)

优点:
- 速度快(适合百万级数据)
- 内存占用小
- 支持在线学习

缺点:
- 结果略不如标准K-Means
- 需要调batch size
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

代码示例

from sklearn.cluster import KMeans, MiniBatchKMeans
import time

# 生成大数据集
X_large, _ = make_blobs(
    n_samples=100000,
    centers=10,
    random_state=42
)

print(f"数据集大小: {X_large.shape}")

# 标准K-Means
start = time.time()
kmeans = KMeans(n_clusters=10, n_init=10)
kmeans.fit(X_large)
kmeans_time = time.time() - start

# Mini-Batch K-Means
start = time.time()
mb_kmeans = MiniBatchKMeans(
    n_clusters=10,
    batch_size=1000,
    n_init=10
)
mb_kmeans.fit(X_large)
mb_kmeans_time = time.time() - start

print(f"\n标准K-Means:")
print(f"  时间: {kmeans_time:.2f}秒")
print(f"  惯性: {kmeans.inertia_:.2f}")

print(f"\nMini-Batch K-Means:")
print(f"  时间: {mb_kmeans_time:.2f}秒")
print(f"  惯性: {mb_kmeans.inertia_:.2f}")
print(f"  加速: {kmeans_time / mb_kmeans_time:.2f}x")
4.8.7 K-Means的优缺点

✅ 优点

1. 简单易懂
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 算法直观
   - 实现简单

2. 速度快
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - O(nkt) 复杂度
   - n=样本数, k=簇数, t=迭代次数
   - 适合大数据集

3. 可扩展性好
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - Mini-Batch版本处理百万级数据
   - 并行化容易

4. 保证收敛
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 目标函数单调递减
   - 一定会收敛(虽然可能局部最优)

❌ 缺点

1. 需要预先指定K
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 不知道最佳K值
   - 需要尝试多个K

2. 对初始值敏感
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 不同初始化可能不同结果
   - 解决:运行多次取最好的

3. 对异常值敏感
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 均值受异常值影响大
   - 异常值可能"拉偏"中心

4. 只能发现球形簇
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 假设簇是凸的、各向同性的
   - 无法处理复杂形状

   例子(K-Means失效):
   
   ●●●●●      ○○○
   ●    ●    ○   ○
   ●●●●●      ○○○
   
   月牙形、环形等,K-Means无法正确分类

5. 簇大小和密度敏感
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 偏向大小相似的簇
   - 偏向密度相似的簇
4.8.8 K-Means的应用场景
✅ 适合:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 客户细分
   - 根据购买行为分群
   - 针对性营销

2. 图像分割/压缩
   - 颜色量化
   - 区域分割

3. 文档聚类
   - 新闻分类
   - 主题发现

4. 异常检测
   - 距离所有中心都远的点可能是异常

5. 特征降维预处理
   - 簇中心作为新特征

❌ 不适合:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 非凸形状簇
   - 月牙形、环形
   - 用DBSCAN等算法

2. 簇大小差异大
   - 一个簇1000点,另一个10点
   - 用层次聚类

3. 簇密度差异大
   - 一个簇很密集,另一个很稀疏
   - 用DBSCAN

4. 高维稀疏数据
   - 距离失去意义
   - 用降维后再聚类

5. 深度学习基础

5.1 神经网络原理

5.1.1 什么是神经网络?
人工神经网络 (Artificial Neural Network)
模拟人脑神经元的计算模型

核心思想:
通过大量简单神经元的连接
产生复杂的智能行为
5.1.2 生物神经元 vs 人工神经元

生物神经元

人脑有 ~860亿个神经元

单个神经元:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
       树突
      ╱ ╱ ╱
     ╱ ╱ ╱
    ╱ ╱ ╱
   ●───────→  轴突  ────→  突触
   细胞体              (连接下一个神经元)

工作原理:
1. 树突接收来自其他神经元的信号
2. 细胞体对信号加权求和
3. 如果总和超过阈值,神经元"激活"
4. 通过轴突传递信号给下一个神经元
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

人工神经元(感知机)

数学模型:

输入 →  加权求和  →  激活函数  →  输出

      x₁ ──w₁──╲
      x₂ ──w₂───●─→ σ(Σwᵢxᵢ + b) ─→ y
      x₃ ──w₃──╱
      
公式:
y = σ(w₁x₁ + w₂x₂ + ... + wₙxₙ + b)
  = σ(w·x + b)

其中:
- x:输入向量
- w:权重向量(学习的参数)
- b:偏置(bias,学习的参数)
- σ:激活函数
- y:输出

类比:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
考试成绩 = 0.4×数学 + 0.3×英语 + 0.3×语文 + 10

这就是一个简单的神经元!
- 输入:各科成绩
- 权重:各科权重
- 偏置:+10分
- 输出:总成绩
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
5.1.3 激活函数

为什么需要激活函数?

没有激活函数:
y = w·x + b  (线性函数)

堆叠多层:
y = w₃(w₂(w₁x + b₁) + b₂) + b₃
  = (w₃w₂w₁)x + (...)
  = W'x + b'

结论:多层线性=单层线性,没用!

加入激活函数:
引入非线性,才能学习复杂模式

常用激活函数

1. Sigmoid函数

公式:
σ(x) = 1 / (1 + e^(-x))

图形:
  1 │         ╱────
    │       ╱
 0.5│     ╱  ← S型曲线
    │   ╱
  0 │───╱
    └──────────── x
      -5  0  5

特点:
- 输出范围 (0, 1)
- 可解释为概率
- 早期神经网络常用

问题:
- 梯度消失(两端梯度接近0)
- 输出不以0为中心
- 计算exp较慢

适用:
- 输出层(二分类)

2. Tanh函数

公式:
tanh(x) = (e^x - e^(-x)) / (e^x + e^(-x))

图形:
  1 │      ╱────
    │    ╱
  0 │──╱──  ← 以0为中心
    │ ╱
 -1 │────╱
    └──────────── x
      -5  0  5

特点:
- 输出范围 (-1, 1)
- 以0为中心(比Sigmoid好)
- 仍有梯度消失问题

适用:
- 隐藏层(RNN常用)

3. ReLU函数 ⭐最常用

公式:
ReLU(x) = max(0, x)

图形:
    │    ╱
    │  ╱
    │╱___
    └──────── x
      0

特点:
- 计算简单(max操作)
- 缓解梯度消失
- 收敛快
- 实践中效果好

问题:
- "死亡ReLU":负值梯度为0
  神经元可能永久失活

适用:
- CNN隐藏层(最常用)
- 全连接网络

4. Leaky ReLU

公式:
Leaky ReLU(x) = max(αx, x)  (α=0.01)

图形:
    │     ╱
    │   ╱
    │ ╱___  ← 负值有小斜率
    └──────── x
      0

特点:
- 解决死亡ReLU问题
- 负值有小梯度(不会完全死)

适用:
- ReLU表现不好时尝试

5. Softmax函数

公式(输出层多分类):
softmax(xᵢ) = e^(xᵢ) / Σⱼ e^(xⱼ)

特点:
- 输出概率分布(和为1)
- 多分类问题最后一层

例子:
输入:[2.0, 1.0, 0.1]
输出:[0.659, 0.242, 0.099]

解释:
- 类别1概率:65.9%
- 类别2概率:24.2%
- 类别3概率:9.9%
5.1.4 多层神经网络(MLP)

结构

┌─────────────────────────────────────────┐
│         多层感知机 (MLP)                 │
├─────────────────────────────────────────┤
│                                          │
│  输入层    隐藏层1   隐藏层2    输出层   │
│                                          │
│   x₁      ●       ●       ●      y₁     │
│           │╲     ╱│╲     ╱│              │
│   x₂      ● ●   ● ● ●   ● ●     y₂     │
│           │╱ ╲ ╱  │╱ ╲ ╱  │              │
│   x₃      ●   ●   ●   ●   ●             │
│                                          │
│  784个   128个   64个   10个             │
│  像素    神经元  神经元  类别             │
│                                          │
└─────────────────────────────────────────┘

特点:
- 全连接(每层所有神经元连接下一层所有神经元)
- 多层堆叠
- 逐层抽象特征

前向传播示例

任务:手写数字识别(MNIST)

输入:28×28像素图像 = 784维向量
输出:10个类别(0-9)

网络结构:784 → 128 → 64 → 10

前向传播:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
层1(输入层):
  x = [像素1, 像素2, ..., 像素784]

层2(隐藏层1):
  z₁ = W₁·x + b₁          [128维]
  a₁ = ReLU(z₁)           [128维]

层3(隐藏层2):
  z₂ = W₂·a₁ + b₂         [64维]
  a₂ = ReLU(z₂)           [64维]

层4(输出层):
  z₃ = W₃·a₂ + b₃         [10维]
  y = Softmax(z₃)         [10维]

输出:[0.01, 0.02, 0.05, 0.85, 0.01, ...]
     → 预测为数字3(概率85%)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

5.2 反向传播算法

5.2.1 什么是反向传播?
Backpropagation
神经网络训练的核心算法

目标:
计算损失函数关于每个参数的梯度
用梯度下降更新参数

核心思想:
链式法则 + 从后向前传播梯度
5.2.2 直观理解

场景:调整配方

你在做蛋糕:

输入 → 面粉、糖、鸡蛋
过程 → 搅拌、烤制
输出 → 蛋糕
评价 → 太甜了!

如何改进?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
方法1(前向试错):
  随机调整面粉、糖、鸡蛋
  再做一次,看结果
  效率低!

方法2(反向分析):
  太甜 → 因为糖多
       → 减少糖的用量
  效率高!

反向传播就是"方法2":
从输出的错误,反推每个参数该如何调整
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
5.2.3 数学原理

简单网络示例

网络:x → w₁ → a → w₂ → y

前向传播:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
a = w₁ · x
y = w₂ · a

损失:
L = (y - y_true)²

数值例子:
x = 2
y_true = 10
w₁ = 1, w₂ = 2

a = 1 × 2 = 2
y = 2 × 2 = 4
L = (4 - 10)² = 36
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

反向传播:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
目标:计算 ∂L/∂w₁ 和 ∂L/∂w₂

链式法则:
∂L/∂w₂ = ∂L/∂y × ∂y/∂w₂
        = 2(y - y_true) × a
        = 2(4 - 10) × 2
        = -24

∂L/∂w₁ = ∂L/∂y × ∂y/∂a × ∂a/∂w₁
        = 2(y - y_true) × w₂ × x
        = 2(4 - 10) × 2 × 2
        = -48
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

参数更新(学习率α=0.01):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
w₂ = w₂ - α × ∂L/∂w₂
   = 2 - 0.01 × (-24)
   = 2.24  ← 增大了

w₁ = w₁ - α × ∂L/∂w₁
   = 1 - 0.01 × (-48)
   = 1.48  ← 增大了

重新前向传播:
a = 1.48 × 2 = 2.96
y = 2.24 × 2.96 = 6.63
L = (6.63 - 10)² = 11.35

损失从36降到11.35!✓
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
5.2.4 完整训练流程
┌─────────────────────────────────────────┐
│      神经网络训练流程                    │
├─────────────────────────────────────────┤
│                                          │
│  1. 初始化参数                           │
│     W, b ← 随机小值                     │
│                                          │
│  2. for epoch in range(n_epochs):       │
│                                          │
│     for batch in dataset:               │
│                                          │
│       【前向传播】                       │
│       计算预测值 ŷ                      │
│                                          │
│       【计算损失】                       │
│       L = Loss(ŷ, y)                   │
│                                          │
│       【反向传播】                       │
│       计算梯度 ∂L/∂W, ∂L/∂b            │
│                                          │
│       【参数更新】                       │
│       W = W - α × ∂L/∂W                │
│       b = b - α × ∂L/∂b                │
│                                          │
│  3. 训练完成                             │
│                                          │
└─────────────────────────────────────────┘

5.3 常见架构详解

5.3.1 卷积神经网络 (CNN)

为什么需要CNN?

问题:图像用全连接网络
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
图像:224×224×3 = 150,528个像素

全连接层(1000个神经元):
参数量 = 150,528 × 1000 = 1.5亿参数!

问题:
1. 参数太多,容易过拟合
2. 忽略了空间结构(像素间的位置关系)
3. 计算量巨大
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

CNN的解决方案:
1. 局部连接(不是全连接)
2. 权值共享(同一个卷积核扫描全图)
3. 池化降维(减少参数)

卷积操作

卷积核(Filter/Kernel):
一个小矩阵,在图像上滑动

例子:3×3卷积核检测边缘

  ┌─────────┐
  │ -1  0  1│
  │ -1  0  1│ ← 卷积核(垂直边缘检测)
  │ -1  0  1│
  └─────────┘

输入图像(5×5):
  10 10 10  0  0
  10 10 10  0  0
  10 10 10  0  0
  10 10 10  0  0
  10 10 10  0  0

卷积操作(滑动窗口):

位置(0,0):
  [-1  0  1]   [10 10 10]
  [-1  0  1] ⊗ [10 10 10]
  [-1  0  1]   [10 10 10]
  
  = -10-10-10 + 10+10+10
  = 0

位置(0,1):
  [-1  0  1]   [10 10  0]
  [-1  0  1] ⊗ [10 10  0]
  [-1  0  1]   [10 10  0]
  
  = -10-10-10 + 0+0+0
  = -30  ← 检测到边缘!

输出特征图(3×3):
   0 -30 -30
   0 -30 -30
   0 -30 -30

可视化:
原图:       特征图(边缘):
■■■□□        □■■
■■■□□   →   □■■
■■■□□        □■■

CNN典型结构

┌─────────────────────────────────────────┐
│         CNN 架构(以LeNet为例)          │
├─────────────────────────────────────────┤
│                                          │
│  输入图像    卷积1  池化1  卷积2  池化2  │
│  28×28×1  → 24×24×6 → 12×12×6 → ...    │
│                                          │
│  ┌──────┐  ┌──────┐  ┌────┐            │
│  │      │  │ ▓▓▓▓ │  │▓▓  │  全连接    │
│  │  7   │→ │ ▓▓▓▓ │→ │▓▓  │→ ●●● → 7  │
│  │      │  │ ▓▓▓▓ │  │▓▓  │  输出      │
│  └──────┘  └──────┘  └────┘            │
│                                          │
│  过程:                                  │
│  1. 卷积:提取局部特征(边缘、纹理)    │
│  2. 池化:降维、增强鲁棒性               │
│  3. 重复1-2:逐层抽象                   │
│  4. 全连接:分类决策                    │
│                                          │
└─────────────────────────────────────────┘

池化操作

最大池化(Max Pooling):

输入(4×4):
  1  3  2  4
  5  6  7  8
  3  2  1  2
  1  2  3  4

2×2池化(步长2):

  ┌──────┐
  │ 1  3 │ 2  4      取最大值:6
  │ 5  6 │ 7  8
  └──────┘
  
   3  2   1  2
   1  2   3  4

输出(2×2):
  6  8
  3  4

作用:
1. 降维(参数减少75%)
2. 保留主要特征
3. 增强平移不变性
5.3.2 循环神经网络 (RNN)
处理序列数据(时间序列、文本)

结构:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
输入:x₁  x₂  x₃  x₄  ...
       ↓   ↓   ↓   ↓
      ●→  ●→  ●→  ●→  ... ← 隐藏状态传递
       ↓   ↓   ↓   ↓
输出:y₁  y₂  y₃  y₄  ...

关键:
每个时间步共享参数
有"记忆"(隐藏状态)

应用:
- 机器翻译
- 语音识别
- 文本生成
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

问题:长期依赖难以学习
解决:LSTM、GRU

详细内容参考本目录下的其他文档
5.3.3 Transformer
2017年革命性架构
完全基于注意力机制

核心:Self-Attention
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
让序列中的每个元素
直接关注所有其他元素

优点:
1. 并行计算(比RNN快)
2. 长距离依赖(直接连接)
3. 可扩展(越大越强)

应用:
- GPT系列(ChatGPT)
- BERT(搜索引擎)
- Vision Transformer(图像)

详细内容请参考:
《Attention_Is_All_You_Need_论文精读.md》
《Transformer完全指南_小白友好版.md》
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

6. 实战完整流程

6.1 机器学习项目的标准流程

┌─────────────────────────────────────────┐
│      机器学习项目流程(7步)             │
├─────────────────────────────────────────┤
│                                          │
│  1. 问题定义                             │
│     明确目标、评估指标                   │
│                                          │
│  2. 数据收集                             │
│     获取数据、评估数据质量               │
│                                          │
│  3. 数据探索(EDA)                      │
│     可视化、统计分析、发现规律           │
│                                          │
│  4. 数据预处理                           │
│     清洗、转换、特征工程                 │
│                                          │
│  5. 模型训练                             │
│     选择算法、训练模型、调参             │
│                                          │
│  6. 模型评估                             │
│     测试集评估、交叉验证                 │
│                                          │
│  7. 模型部署                             │
│     上线、监控、迭代                     │
│                                          │
└─────────────────────────────────────────┘

6.2 案例:泰坦尼克号生存预测

完整代码实现

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import train_test_split, cross_val_score
from sklearn.preprocessing import StandardScaler, LabelEncoder
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import (
    accuracy_score, precision_score, recall_score, 
    f1_score, confusion_matrix, classification_report
)

# ========== 1. 问题定义 ==========
"""
任务:预测泰坦尼克号乘客是否生还
类型:二分类问题
评估指标:准确率、F1-Score
"""

print("=" * 50)
print("泰坦尼克号生存预测 - 完整流程")
print("=" * 50)

# ========== 2. 数据收集 ==========
# 加载数据(Kaggle泰坦尼克数据集)
# 这里使用seaborn内置数据作为示例
df = sns.load_dataset('titanic')

print(f"\n数据集大小: {df.shape}")
print(f"特征数量: {df.shape[1]}")
print(f"样本数量: {df.shape[0]}")

# ========== 3. 数据探索(EDA) ==========
print("\n" + "=" * 50)
print("数据探索")
print("=" * 50)

# 3.1 查看前几行
print("\n前5行数据:")
print(df.head())

# 3.2 数据概览
print("\n数据信息:")
print(df.info())

# 3.3 统计描述
print("\n数值特征统计:")
print(df.describe())

# 3.4 缺失值检查
print("\n缺失值统计:")
print(df.isnull().sum())

# 3.5 目标变量分布
print("\n生还情况分布:")
print(df['survived'].value_counts())
print(f"生还率: {df['survived'].mean():.2%}")

# 3.6 可视化分析
fig, axes = plt.subplots(2, 3, figsize=(15, 10))

# 生还情况
axes[0, 0].pie(
    df['survived'].value_counts(),
    labels=['未生还', '生还'],
    autopct='%1.1f%%'
)
axes[0, 0].set_title('生还比例')

# 性别 vs 生还
sns.barplot(x='sex', y='survived', data=df, ax=axes[0, 1])
axes[0, 1].set_title('性别与生还率')

# 舱位 vs 生还
sns.barplot(x='class', y='survived', data=df, ax=axes[0, 2])
axes[0, 2].set_title('舱位与生还率')

# 年龄分布
df['age'].hist(bins=30, ax=axes[1, 0])
axes[1, 0].set_title('年龄分布')
axes[1, 0].set_xlabel('年龄')

# 票价分布
df['fare'].hist(bins=30, ax=axes[1, 1])
axes[1, 1].set_title('票价分布')
axes[1, 1].set_xlabel('票价')

# 家庭人数
df['家庭人数'] = df['sibsp'] + df['parch']
df['家庭人数'].value_counts().plot(kind='bar', ax=axes[1, 2])
axes[1, 2].set_title('家庭人数分布')

plt.tight_layout()
plt.savefig('titanic_eda.png', dpi=150)
print("\nEDA图表已保存")

# ========== 4. 数据预处理 ==========
print("\n" + "=" * 50)
print("数据预处理")
print("=" * 50)

# 创建副本
df_processed = df.copy()

# 4.1 选择特征
features_to_use = [
    'pclass', 'sex', 'age', 'sibsp', 
    'parch', 'fare', 'embarked'
]
target = 'survived'

# 4.2 处理缺失值
print("\n处理缺失值...")

# 年龄:用中位数填充
df_processed['age'].fillna(df_processed['age'].median(), inplace=True)

# 登船港口:用众数填充
df_processed['embarked'].fillna(df_processed['embarked'].mode()[0], inplace=True)

# 票价:用中位数填充
df_processed['fare'].fillna(df_processed['fare'].median(), inplace=True)

print("缺失值处理完成")

# 4.3 特征工程
print("\n特征工程...")

# 家庭人数
df_processed['family_size'] = df_processed['sibsp'] + df_processed['parch'] + 1

# 是否独自一人
df_processed['is_alone'] = (df_processed['family_size'] == 1).astype(int)

# 年龄分段
df_processed['age_group'] = pd.cut(
    df_processed['age'],
    bins=[0, 12, 20, 40, 60, 100],
    labels=['child', 'teen', 'adult', 'middle', 'senior']
)

# 票价分段
df_processed['fare_group'] = pd.qcut(
    df_processed['fare'],
    q=4,
    labels=['low', 'medium', 'high', 'very_high']
)

print("特征工程完成")

# 4.4 编码分类变量
print("\n编码分类变量...")

# 性别:Label Encoding
le_sex = LabelEncoder()
df_processed['sex'] = le_sex.fit_transform(df_processed['sex'])

# 登船港口:One-Hot Encoding
df_processed = pd.get_dummies(
    df_processed,
    columns=['embarked'],
    prefix='embarked'
)

# 年龄组和票价组:Label Encoding
le_age = LabelEncoder()
df_processed['age_group'] = le_age.fit_transform(df_processed['age_group'])

le_fare = LabelEncoder()
df_processed['fare_group'] = le_fare.fit_transform(df_processed['fare_group'])

print("编码完成")

# 4.5 选择最终特征
final_features = [
    'pclass', 'sex', 'age', 'sibsp', 'parch', 'fare',
    'family_size', 'is_alone', 'age_group', 'fare_group',
    'embarked_C', 'embarked_Q', 'embarked_S'
]

X = df_processed[final_features]
y = df_processed[target]

print(f"\n最终特征数量: {X.shape[1]}")
print(f"特征列表: {list(X.columns)}")

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

print(f"\n训练集大小: {X_train.shape}")
print(f"测试集大小: {X_test.shape}")

# 4.7 标准化
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

print("数据标准化完成")

# ========== 5. 模型训练 ==========
print("\n" + "=" * 50)
print("模型训练")
print("=" * 50)

# 5.1 尝试多个模型
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.svm import SVC

models = {
    'Logistic Regression': LogisticRegression(max_iter=1000),
    'Decision Tree': DecisionTreeClassifier(random_state=42),
    'Random Forest': RandomForestClassifier(n_estimators=100, random_state=42),
    'Gradient Boosting': GradientBoostingClassifier(n_estimators=100, random_state=42),
    'SVM': SVC(kernel='rbf', probability=True)
}

results = {}

print("\n训练多个模型...")
for name, model in models.items():
    # 训练
    model.fit(X_train_scaled, y_train)
    
    # 交叉验证
    cv_scores = cross_val_score(
        model, X_train_scaled, y_train,
        cv=5, scoring='accuracy'
    )
    
    # 测试集评估
    y_pred = model.predict(X_test_scaled)
    test_acc = accuracy_score(y_test, y_pred)
    
    results[name] = {
        'model': model,
        'cv_mean': cv_scores.mean(),
        'cv_std': cv_scores.std(),
        'test_acc': test_acc
    }
    
    print(f"\n{name}:")
    print(f"  交叉验证准确率: {cv_scores.mean():.4f} (+/- {cv_scores.std():.4f})")
    print(f"  测试集准确率: {test_acc:.4f}")

# 选择最佳模型
best_model_name = max(results, key=lambda x: results[x]['test_acc'])
best_model = results[best_model_name]['model']

print(f"\n最佳模型: {best_model_name}")
print(f"测试集准确率: {results[best_model_name]['test_acc']:.4f}")

# ========== 6. 模型评估 ==========
print("\n" + "=" * 50)
print("模型评估")
print("=" * 50)

# 预测
y_pred = best_model.predict(X_test_scaled)
y_pred_proba = best_model.predict_proba(X_test_scaled)[:, 1]

# 6.1 分类指标
print("\n分类报告:")
print(classification_report(y_test, y_pred, 
                          target_names=['未生还', '生还']))

# 6.2 混淆矩阵
cm = confusion_matrix(y_test, y_pred)
print("\n混淆矩阵:")
print(cm)

# 可视化混淆矩阵
plt.figure(figsize=(8, 6))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
            xticklabels=['未生还', '生还'],
            yticklabels=['未生还', '生还'])
plt.title(f'{best_model_name} - 混淆矩阵')
plt.ylabel('真实标签')
plt.xlabel('预测标签')
plt.savefig('confusion_matrix.png', dpi=150)
print("混淆矩阵图已保存")

# 6.3 ROC曲线
from sklearn.metrics import roc_curve, auc

fpr, tpr, thresholds = roc_curve(y_test, y_pred_proba)
roc_auc = auc(fpr, tpr)

plt.figure(figsize=(8, 6))
plt.plot(fpr, tpr, color='darkorange', lw=2,
         label=f'ROC curve (AUC = {roc_auc:.2f})')
plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--')
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('ROC Curve')
plt.legend(loc="lower right")
plt.grid(True, alpha=0.3)
plt.savefig('roc_curve.png', dpi=150)
print("ROC曲线图已保存")

# 6.4 特征重要性(如果是随机森林)
if isinstance(best_model, RandomForestClassifier):
    feature_importance = pd.DataFrame({
        'feature': final_features,
        'importance': best_model.feature_importances_
    }).sort_values('importance', ascending=False)
    
    print("\n特征重要性(Top 10):")
    print(feature_importance.head(10))
    
    # 可视化
    plt.figure(figsize=(10, 6))
    top10 = feature_importance.head(10)
    plt.barh(range(len(top10)), top10['importance'])
    plt.yticks(range(len(top10)), top10['feature'])
    plt.xlabel('Importance')
    plt.title('Feature Importance (Top 10)')
    plt.gca().invert_yaxis()
    plt.tight_layout()
    plt.savefig('feature_importance.png', dpi=150)
    print("特征重要性图已保存")

# ========== 7. 模型保存 ==========
print("\n" + "=" * 50)
print("模型保存")
print("=" * 50)

import joblib

# 保存模型
joblib.dump(best_model, 'titanic_model.pkl')
print("模型已保存为 titanic_model.pkl")

# 保存预处理器
joblib.dump(scaler, 'scaler.pkl')
print("标准化器已保存为 scaler.pkl")

# 保存特征列表
with open('features.txt', 'w') as f:
    f.write('\n'.join(final_features))
print("特征列表已保存为 features.txt")

# ========== 8. 模型使用示例 ==========
print("\n" + "=" * 50)
print("模型使用示例")
print("=" * 50)

# 加载模型
loaded_model = joblib.load('titanic_model.pkl')
loaded_scaler = joblib.load('scaler.pkl')

# 预测新样本
new_passenger = pd.DataFrame({
    'pclass': [3],
    'sex': [1],  # 男性
    'age': [22],
    'sibsp': [1],
    'parch': [0],
    'fare': [7.25],
    'family_size': [2],
    'is_alone': [0],
    'age_group': [2],
    'fare_group': [0],
    'embarked_C': [0],
    'embarked_Q': [0],
    'embarked_S': [1]
})

# 标准化
new_passenger_scaled = loaded_scaler.transform(new_passenger)

# 预测
prediction = loaded_model.predict(new_passenger_scaled)[0]
probability = loaded_model.predict_proba(new_passenger_scaled)[0]

print(f"\n新乘客信息:")
print(f"  舱位: 3等舱")
print(f"  性别: 男")
print(f"  年龄: 22岁")
print(f"  票价: $7.25")
print(f"\n预测结果: {'生还' if prediction == 1 else '未生还'}")
print(f"生还概率: {probability[1]:.2%}")
print(f"未生还概率: {probability[0]:.2%}")

print("\n" + "=" * 50)
print("完整流程结束!")
print("=" * 50)

6.3 数据预处理详解

6.3.1 缺失值处理
常见方法:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 删除
   - 删除含缺失值的行
   - 删除缺失率高的列
   
   适用:缺失比例<5%

2. 填充
   - 数值型:均值、中位数、众数
   - 类别型:众数、新类别"Unknown"
   
   适用:缺失比例5%-30%

3. 预测
   - 用其他特征预测缺失值
   - 适用:重要特征缺失
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

代码示例

import pandas as pd
import numpy as np

# 示例数据
df = pd.DataFrame({
    'age': [25, 30, np.nan, 35, 40, np.nan],
    'salary': [5000, 6000, 7000, np.nan, 9000, 10000],
    'city': ['北京', '上海', np.nan, '广州', '深圳', '北京']
})

print("原始数据:")
print(df)
print(f"\n缺失值统计:")
print(df.isnull().sum())

# 方法1:删除
df_drop = df.dropna()  # 删除任何含NaN的行
print(f"\n删除后剩余: {len(df_drop)}行")

# 方法2:填充
# 数值型:中位数
df['age'].fillna(df['age'].median(), inplace=True)
df['salary'].fillna(df['salary'].mean(), inplace=True)

# 类别型:众数
df['city'].fillna(df['city'].mode()[0], inplace=True)

print("\n填充后:")
print(df)

# 方法3:高级填充
from sklearn.impute import SimpleImputer, KNNImputer

# 简单填充器
imputer = SimpleImputer(strategy='median')  # mean/median/most_frequent
X_imputed = imputer.fit_transform(X)

# KNN填充器(用邻居预测)
knn_imputer = KNNImputer(n_neighbors=5)
X_knn_imputed = knn_imputer.fit_transform(X)
6.3.2 特征缩放

为什么需要缩放?

问题:特征量纲不同

例子:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
特征1:年龄(20-60)
特征2:收入(5000-50000)

距离计算:
d = √[(年龄差)² + (收入差)²]
  = √[(5)² + (20000)²]
  = √400000025
  ≈ 20000

问题:收入完全主导了距离!
年龄的影响几乎可以忽略
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

标准化方法

1. Min-Max缩放

公式:
x' = (x - min) / (max - min)

结果:缩放到 [0, 1]

例子:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
原始数据:[10, 20, 30, 40, 50]
min = 10, max = 50

缩放后:
10 → (10-10)/(50-10) = 0.0
20 → (20-10)/(50-10) = 0.25
30 → (30-10)/(50-10) = 0.5
40 → (40-10)/(50-10) = 0.75
50 → (50-10)/(50-10) = 1.0
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

优点:保留原始分布
缺点:对异常值敏感

代码:
from sklearn.preprocessing import MinMaxScaler

scaler = MinMaxScaler()
X_scaled = scaler.fit_transform(X)

2. 标准化(Z-Score)⭐最常用

公式:
x' = (x - μ) / σ

其中:
- μ:均值
- σ:标准差

结果:均值0,标准差1

例子:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
原始数据:[10, 20, 30, 40, 50]
μ = 30, σ = 14.14

缩放后:
10 → (10-30)/14.14 = -1.41
20 → (20-30)/14.14 = -0.71
30 → (30-30)/14.14 = 0
40 → (40-30)/14.14 = 0.71
50 → (50-30)/14.14 = 1.41
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

优点:
- 对异常值较鲁棒
- 适合大多数算法

缺点:
- 数据不再在[0,1]区间

代码:
from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

3. 鲁棒缩放

使用中位数和四分位距(IQR)
对异常值更鲁棒

公式:
x' = (x - median) / IQR

代码:
from sklearn.preprocessing import RobustScaler

scaler = RobustScaler()
X_scaled = scaler.fit_transform(X)
6.3.3 特征编码

1. Label Encoding(标签编码)

把类别转为数字

例子:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
原始:['红', '绿', '蓝', '红', '绿']
编码:[ 0,   1,   2,   0,   1]

代码:
from sklearn.preprocessing import LabelEncoder

le = LabelEncoder()
df['color_encoded'] = le.fit_transform(df['color'])

注意:
只适合有序类别(如:低、中、高)
不适合无序类别(会引入假的顺序关系)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

2. One-Hot Encoding(独热编码)

每个类别变成一个二进制特征

例子:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
原始:
['红', '绿', '蓝', '红', '绿']

One-Hot编码:
        红  绿  蓝
  红     1   0   0
  绿     0   1   0
  蓝     0   0   1
  红     1   0   0
  绿     0   1   0

代码:
# Pandas方式
df_encoded = pd.get_dummies(df, columns=['color'])

# Sklearn方式
from sklearn.preprocessing import OneHotEncoder

encoder = OneHotEncoder(sparse=False)
color_encoded = encoder.fit_transform(df[['color']])
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

优点:
- 不引入顺序关系
- 适合无序类别

缺点:
- 维度爆炸(类别多时)
- 稀疏矩阵

3. 频率编码

用类别出现频率替换

例子:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
城市:['北京', '上海', '北京', '深圳', '北京']

频率统计:
北京:3次 (60%)
上海:1次 (20%)
深圳:1次 (20%)

编码:[0.6, 0.2, 0.6, 0.2, 0.6]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

适用:类别数量巨大时

4. Target Encoding

用目标变量的均值替换类别

例子:预测房价
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
城市    房价
北京    1000万
北京    1200万
上海    800万
上海    900万
深圳    700万

Target编码:
北京 → (1000+1200)/2 = 1100
上海 → (800+900)/2 = 850
深圳 → 700
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

优点:效果好,编码后维度不增加
缺点:容易过拟合,需要交叉验证
6.3.4 特征工程

创建新特征

"""
特征工程是提升模型效果的关键!
"""

# 1. 组合特征
df['BMI'] = df['weight'] / (df['height'] / 100) ** 2

# 2. 多项式特征
from sklearn.preprocessing import PolynomialFeatures

poly = PolynomialFeatures(degree=2, include_bias=False)
X_poly = poly.fit_transform(X)
# 从 [x1, x2] 变成 [x1, x2, x1², x1·x2, x2²]

# 3. 分箱(离散化)
df['age_group'] = pd.cut(
    df['age'],
    bins=[0, 18, 35, 60, 100],
    labels=['青少年', '青年', '中年', '老年']
)

# 4. 日期特征提取
df['year'] = df['date'].dt.year
df['month'] = df['date'].dt.month
df['day_of_week'] = df['date'].dt.dayofweek
df['is_weekend'] = df['day_of_week'].isin([5, 6]).astype(int)

# 5. 文本特征
df['text_length'] = df['text'].str.len()
df['word_count'] = df['text'].str.split().str.len()
df['has_url'] = df['text'].str.contains('http').astype(int)

# 6. 比例特征
df['debt_to_income'] = df['debt'] / df['income']

# 7. 聚合特征
df['avg_purchase'] = df.groupby('user_id')['amount'].transform('mean')

7. 模型评估与优化

7.1 评估指标详解

7.1.1 分类指标

混淆矩阵 (Confusion Matrix)

              预测
          正例  负例
真  正例   TP   FN
实  负例   FP   TN

其中:
- TP (True Positive):真阳性(预测对了)
- TN (True Negative):真阴性(预测对了)
- FP (False Positive):假阳性(误判为正)
- FN (False Negative):假阴性(漏判了正)

例子:疾病诊断
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
100个病人,10个真的有病

混淆矩阵:
              预测
          有病  没病
真  有病    8     2   ← 漏诊2个
实  没病    5    85   ← 误诊5个

TP=8:  正确诊断出8个病人
FN=2:  漏诊2个病人
FP=5:  误诊5个健康人
TN=85: 正确判断85个健康人
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

准确率 (Accuracy)

定义:预测正确的比例

公式:
Accuracy = (TP + TN) / (TP + TN + FP + FN)

例子:
Accuracy = (8 + 85) / 100 = 93%

优点:直观
缺点:类别不平衡时误导

反例:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
100个人,只有1个得病

模型:全部预测"没病"
结果:
  TP=0, FN=1
  FP=0, TN=99
  
准确率 = 99%!✅

但完全没用!这个模型啥也没学到
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

精确率 (Precision)

定义:预测为正的样本中,真正为正的比例

公式:
Precision = TP / (TP + FP)

例子:
Precision = 8 / (8 + 5) = 8/13 = 61.5%

解释:
模型说"有病"的13个人中
真正有病的只有8个
误诊率 = 5/13 = 38.5%

何时重要?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
垃圾邮件过滤:
- 宁可漏掉垃圾邮件
- 也不能把正常邮件当垃圾
→ 追求高Precision

商品推荐:
- 推荐的商品要准
- 宁可少推荐
→ 追求高Precision
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

召回率 (Recall)

定义:真正为正的样本中,被预测为正的比例

公式:
Recall = TP / (TP + FN)

例子:
Recall = 8 / (8 + 2) = 8/10 = 80%

解释:
10个真病人中
模型找出了8个
漏诊了2个

何时重要?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
疾病诊断:
- 宁可误诊(FP高)
- 也不能漏诊(FN要低)
→ 追求高Recall

欺诈检测:
- 宁可误报
- 也要抓出所有欺诈
→ 追求高Recall
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Precision vs Recall的权衡

矛盾:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
提高Recall → Precision下降
提高Precision → Recall下降

可视化:

Precision
    │
 1.0│●
    │ ╲
 0.8│  ╲___
 0.6│      ╲___
 0.4│          ╲___
 0.2│              ╲___
  0 └────────────────────●─ Recall
    0   0.2  0.4  0.6  0.8  1.0

例子:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
模型A:只预测最有把握的
  Precision = 95%(很准)
  Recall = 30%(漏了很多)

模型B:大范围预测
  Precision = 60%(不够准)
  Recall = 90%(抓得多)

如何选择?
看具体需求!
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

F1-Score

定义:Precision和Recall的调和平均

公式:
F1 = 2 × (Precision × Recall) / (Precision + Recall)

例子:
Precision = 61.5%, Recall = 80%
F1 = 2 × (0.615 × 0.8) / (0.615 + 0.8) = 0.695 = 69.5%

特点:
- 平衡Precision和Recall
- 类别不平衡时比Accuracy更可靠

推广:Fβ-Score
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Fβ = (1+β²) × (Precision × Recall) / (β²×Precision + Recall)

β=0.5:更重视Precision
β=1:  F1-Score(平衡)
β=2:  更重视Recall
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

代码示例

from sklearn.metrics import (
    accuracy_score, precision_score, recall_score,
    f1_score, confusion_matrix, classification_report
)

y_true = [0, 1, 0, 1, 0, 1, 0, 1]
y_pred = [0, 1, 0, 0, 0, 1, 1, 1]

# 计算指标
acc = accuracy_score(y_true, y_pred)
precision = precision_score(y_true, y_pred)
recall = recall_score(y_true, y_pred)
f1 = f1_score(y_true, y_pred)

print(f"准确率: {acc:.2%}")
print(f"精确率: {precision:.2%}")
print(f"召回率: {recall:.2%}")
print(f"F1分数: {f1:.2%}")

# 混淆矩阵
cm = confusion_matrix(y_true, y_pred)
print(f"\n混淆矩阵:\n{cm}")

# 完整报告
print("\n分类报告:")
print(classification_report(y_true, y_pred))

ROC曲线和AUC

ROC (Receiver Operating Characteristic)
评估分类器在不同阈值下的表现

横轴:FPR (False Positive Rate) = FP / (FP + TN)
纵轴:TPR (True Positive Rate) = TP / (TP + FN) = Recall

曲线:
TPR
  1│     ╱─────  ← 理想模型(AUC=1.0)
   │   ╱
   │  ●  ← 实际模型(AUC=0.85)
 0.5│ ╱
   │╱ - - -  ← 随机猜测(AUC=0.5)
  0└──────────── FPR
   0    0.5    1

AUC (Area Under Curve):
- 曲线下面积
- 范围 [0, 1]
- 越大越好

解释:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
AUC = 0.5:随机猜测(没用)
AUC = 0.7-0.8:一般
AUC = 0.8-0.9:好
AUC = 0.9+:优秀
AUC = 1.0:完美(可能过拟合)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

代码示例

from sklearn.metrics import roc_curve, roc_auc_score
import matplotlib.pyplot as plt

# 预测概率
y_proba = model.predict_proba(X_test)[:, 1]

# 计算ROC曲线
fpr, tpr, thresholds = roc_curve(y_test, y_proba)

# 计算AUC
auc = roc_auc_score(y_test, y_proba)

# 绘制ROC曲线
plt.figure(figsize=(8, 6))
plt.plot(fpr, tpr, lw=2, label=f'ROC curve (AUC = {auc:.2f})')
plt.plot([0, 1], [0, 1], 'k--', lw=2, label='Random')
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.05])
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('ROC Curve')
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()

# 找最佳阈值
optimal_idx = np.argmax(tpr - fpr)
optimal_threshold = thresholds[optimal_idx]
print(f"最佳阈值: {optimal_threshold:.4f}")
7.1.2 回归指标

1. MSE (均方误差)

公式:
MSE = (1/n) Σ(yᵢ - ŷᵢ)²

特点:
- 对大误差敏感(平方)
- 单位是目标变量的平方

代码:
from sklearn.metrics import mean_squared_error

mse = mean_squared_error(y_true, y_pred)

2. RMSE (均方根误差)

公式:
RMSE = √MSE

优点:
- 单位与目标变量相同
- 更直观

例子:
预测房价,RMSE=50万
→ 平均误差50万元

3. MAE (平均绝对误差)

公式:
MAE = (1/n) Σ|yᵢ - ŷᵢ|

特点:
- 对异常值不敏感(不平方)
- 更鲁棒

例子:
真实:[100, 200, 300, 1000]
预测:[110, 190, 310, 500]

MSE  = 65025(被1000的误差主导)
MAE  = 130(更平衡)

4. R² (决定系数)

公式:
R² = 1 - (SS_res / SS_tot)

其中:
- SS_res = Σ(yᵢ - ŷᵢ)²  残差平方和
- SS_tot = Σ(yᵢ - ȳ)²   总平方和

范围:(-∞, 1]
- R² = 1:完美预测
- R² = 0:和预测均值一样
- R² < 0:比预测均值还差

解释:
R² = 0.85 表示模型解释了85%的方差

7.2 交叉验证

为什么需要交叉验证?

问题:单次划分train/test不够可靠

原因:
- 可能划分不均(碰巧简单样本都在测试集)
- 浪费数据(测试集不参与训练)
- 结果不稳定(换个随机种子结果就变)

K折交叉验证

思想:
把数据分成K份
轮流用一份做测试,其余做训练

可视化(K=5):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
全部数据:[■■■■■■■■■■]

第1折:[■■■■■■■■░░] ← 后20%测试
        训练集    测试

第2折:[■■■■■■░░■■] ← 中间20%测试
        训练  测试 训练

第3折:[■■■■░░■■■■]
        训练 测试 训练

第4折:[■■░░■■■■■■]

第5折:[░░■■■■■■■■]

结果:
5次准确率:[0.82, 0.85, 0.83, 0.87, 0.84]
平均:0.842 ± 0.018
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

优点:
1. 更可靠(5次评估)
2. 充分利用数据
3. 可以看到稳定性(标准差)

代码示例

from sklearn.model_selection import cross_val_score, cross_validate

# 简单交叉验证
scores = cross_val_score(
    model, X, y,
    cv=5,                  # 5折
    scoring='accuracy'     # 评估指标
)

print(f"交叉验证准确率: {scores}")
print(f"平均准确率: {scores.mean():.4f} (+/- {scores.std():.4f})")

# 多指标交叉验证
scoring = ['accuracy', 'precision', 'recall', 'f1']

scores = cross_validate(
    model, X, y,
    cv=5,
    scoring=scoring,
    return_train_score=True
)

print("\n多指标交叉验证:")
for metric in scoring:
    train_scores = scores[f'train_{metric}']
    test_scores = scores[f'test_{metric}']
    print(f"{metric}:")
    print(f"  训练: {train_scores.mean():.4f} (+/- {train_scores.std():.4f})")
    print(f"  测试: {test_scores.mean():.4f} (+/- {test_scores.std():.4f})")

分层K折 (Stratified K-Fold)

问题:类别不平衡时,普通K折可能不均

解决:保持每折中类别比例一致

例子:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
总数据:100个样本
  正例:20个(20%)
  负例:80个(80%)

普通K折(可能):
  第1折:正例30%,负例70%  ❌ 不均
  第2折:正例10%,负例90%  ❌ 不均

分层K折:
  第1折:正例20%,负例80%  ✓ 均衡
  第2折:正例20%,负例80%  ✓ 均衡
  ...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

代码:
from sklearn.model_selection import StratifiedKFold

skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

for train_idx, test_idx in skf.split(X, y):
    X_train, X_test = X[train_idx], X[test_idx]
    y_train, y_test = y[train_idx], y[test_idx]
    
    # 训练和评估...

7.3 超参数调优

网格搜索 (Grid Search)

穷举所有参数组合

例子:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
参数空间:
  C = [0.1, 1, 10]
  gamma = [0.01, 0.1, 1]

总组合:3 × 3 = 9种

逐一尝试:
  (C=0.1, gamma=0.01): 准确率80%
  (C=0.1, gamma=0.1):  准确率82%
  (C=0.1, gamma=1):    准确率78%
  (C=1, gamma=0.01):   准确率85%
  ...

最佳组合:(C=1, gamma=0.01)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

优点:保证找到最优
缺点:慢(组合爆炸)

随机搜索 (Random Search)

随机采样参数组合

例子:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
参数空间:
  C = [0.1, 1, 10, 100]
  gamma = [0.001, 0.01, 0.1, 1]

总组合:4 × 4 = 16种

随机搜索(尝试10次):
  随机选10种组合测试

优点:快,通常够用
缺点:可能miss最优解
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

贝叶斯优化 (Bayesian Optimization)

更智能的搜索策略

思想:
根据历史结果,预测哪些参数更可能好
优先尝试"有希望"的参数

代码:
from skopt import BayesSearchCV

param_space = {
    'C': (0.001, 100, 'log-uniform'),
    'gamma': (0.0001, 1, 'log-uniform')
}

bayes_search = BayesSearchCV(
    SVC(),
    param_space,
    n_iter=50,
    cv=5
)

bayes_search.fit(X_train, y_train)

7.4 过拟合与欠拟合的诊断

7.4.1 诊断方法

学习曲线 (Learning Curve)

绘制:训练集大小 vs 性能

可视化:

准确率
  │
1.0│
   │  ───────  ← 训练集准确率(总是很高)
0.9│     ╱───
   │    ╱     ← 验证集准确率
0.8│   ╱
   │  ╱
0.7│ ╱
   └──────────────── 训练样本数
    100  500  1000

【过拟合特征】
训练集准确率:99%
验证集准确率:70%
→ 差距大!

【欠拟合特征】
训练集准确率:70%
验证集准确率:68%
→ 都不高!

【正常特征】
训练集准确率:88%
验证集准确率:85%
→ 差距小,都还不错

代码实现

from sklearn.model_selection import learning_curve

# 计算学习曲线
train_sizes, train_scores, val_scores = learning_curve(
    model, X, y,
    train_sizes=np.linspace(0.1, 1.0, 10),
    cv=5,
    scoring='accuracy',
    n_jobs=-1
)

# 计算均值和标准差
train_mean = train_scores.mean(axis=1)
train_std = train_scores.std(axis=1)
val_mean = val_scores.mean(axis=1)
val_std = val_scores.std(axis=1)

# 绘图
plt.figure(figsize=(10, 6))
plt.plot(train_sizes, train_mean, 'o-', label='Training score')
plt.plot(train_sizes, val_mean, 's-', label='Validation score')

plt.fill_between(
    train_sizes,
    train_mean - train_std,
    train_mean + train_std,
    alpha=0.1
)
plt.fill_between(
    train_sizes,
    val_mean - val_std,
    val_mean + val_std,
    alpha=0.1
)

plt.xlabel('Training Set Size')
plt.ylabel('Accuracy')
plt.title('Learning Curve')
plt.legend()
plt.grid(True)
plt.show()

验证曲线 (Validation Curve)

绘制:参数值 vs 性能

可视化:

准确率
  │
0.9│      训练      
   │       ╱╲   ← 验证集
0.8│      ╱  ╲
   │     ╱    ╲
0.7│    ╱      ╲___
   │   ╱
0.6│  ╱
   └──────────────── 参数复杂度
    简单        复杂

左边:欠拟合(模型太简单)
中间:刚刚好
右边:过拟合(模型太复杂)

代码实现

from sklearn.model_selection import validation_curve

# 例子:决策树的max_depth
param_range = np.arange(1, 21)

train_scores, val_scores = validation_curve(
    DecisionTreeClassifier(random_state=42),
    X, y,
    param_name='max_depth',
    param_range=param_range,
    cv=5,
    scoring='accuracy'
)

train_mean = train_scores.mean(axis=1)
val_mean = val_scores.mean(axis=1)

plt.figure(figsize=(10, 6))
plt.plot(param_range, train_mean, 'o-', label='Training')
plt.plot(param_range, val_mean, 's-', label='Validation')
plt.xlabel('max_depth')
plt.ylabel('Accuracy')
plt.title('Validation Curve')
plt.legend()
plt.grid(True)
plt.show()

# 找最佳max_depth
best_depth = param_range[np.argmax(val_mean)]
print(f"最佳max_depth: {best_depth}")
7.4.2 解决过拟合

方法汇总

┌─────────────────────────────────────────┐
│        解决过拟合的10种方法              │
├─────────────────────────────────────────┤
│                                          │
│ 1. 增加训练数据                          │
│    - 收集更多数据                        │
│    - 数据增强(Data Augmentation)      │
│                                          │
│ 2. 简化模型                              │
│    - 减少特征数量                        │
│    - 降低模型复杂度                      │
│      (如:决策树降低深度)              │
│                                          │
│ 3. 正则化 (Regularization)              │
│    - L1正则(Lasso)                    │
│    - L2正则(Ridge)                    │
│    - Elastic Net                         │
│                                          │
│ 4. 早停 (Early Stopping)                │
│    - 监控验证集误差                      │
│    - 误差不再下降时停止                  │
│                                          │
│ 5. Dropout(神经网络)                  │
│    - 随机丢弃神经元                      │
│    - 防止过度依赖某些神经元              │
│                                          │
│ 6. 集成方法                              │
│    - Bagging(随机森林)                │
│    - 多模型投票                          │
│                                          │
│ 7. 交叉验证                              │
│    - 检测过拟合                          │
│    - 选择泛化性能好的参数                │
│                                          │
│ 8. 特征选择                              │
│    - 去除不相关特征                      │
│    - 降维(PCA)                         │
│                                          │
│ 9. 增加噪声                              │
│    - 训练数据加噪声                      │
│    - 增强鲁棒性                          │
│                                          │
│ 10. 批归一化 (Batch Normalization)      │
│     - 稳定训练过程                       │
│     - 有正则化效果                       │
│                                          │
└─────────────────────────────────────────┘

L1和L2正则化对比

┌─────────────────────────────────────────┐
│         L1 vs L2 正则化                  │
├─────────────────────────────────────────┤
│                                          │
│  L1正则化(Lasso)                      │
│  ─────────────────                      │
│  损失函数:MSE + α·Σ|wᵢ|               │
│                                          │
│  特点:                                  │
│  - 产生稀疏解(很多权重变为0)          │
│  - 自动特征选择                          │
│  - 鲁棒性强                              │
│                                          │
│  可视化权重:                            │
│  [2.5, 0, 0.8, 0, 1.2, 0, 0, 3.1]       │
│    ↑       ↑    ↑                        │
│  重要特征(其他被置为0)                │
│                                          │
├─────────────────────────────────────────┤
│  L2正则化(Ridge)                      │
│  ─────────────────                      │
│  损失函数:MSE + α·Σwᵢ²                │
│                                          │
│  特点:                                  │
│  - 权重趋向小值(但不会为0)            │
│  - 保留所有特征                          │
│  - 数值更稳定                            │
│                                          │
│  可视化权重:                            │
│  [1.2, 0.3, 0.8, 0.1, 0.9, 0.2, 0.1, 1.5]│
│    ↑                                     │
│  所有权重都小,但都保留                  │
│                                          │
└─────────────────────────────────────────┘

如何选择?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
特征很多且冗余 → L1(自动筛选)
所有特征都重要 → L2(保留所有)
不确定 → Elastic Net(L1+L2混合)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

代码示例

from sklearn.linear_model import Lasso, Ridge, ElasticNet

# L1正则化(Lasso)
lasso = Lasso(alpha=0.1)
lasso.fit(X_train, y_train)

print("Lasso回归(L1):")
print(f"非零权重数量: {np.sum(lasso.coef_ != 0)}")
print(f"权重: {lasso.coef_}")

# L2正则化(Ridge)
ridge = Ridge(alpha=0.1)
ridge.fit(X_train, y_train)

print("\nRidge回归(L2):")
print(f"权重: {ridge.coef_}")

# Elastic Net(L1 + L2)
elastic = ElasticNet(alpha=0.1, l1_ratio=0.5)  # l1_ratio=0.5表示各占50%
elastic.fit(X_train, y_train)

print("\nElastic Net:")
print(f"非零权重数量: {np.sum(elastic.coef_ != 0)}")
print(f"权重: {elastic.coef_}")

# 可视化权重
fig, axes = plt.subplots(1, 3, figsize=(15, 4))

axes[0].bar(range(len(lasso.coef_)), lasso.coef_)
axes[0].set_title('Lasso (L1)')
axes[0].set_xlabel('Feature Index')
axes[0].set_ylabel('Weight')

axes[1].bar(range(len(ridge.coef_)), ridge.coef_)
axes[1].set_title('Ridge (L2)')

axes[2].bar(range(len(elastic.coef_)), elastic.coef_)
axes[2].set_title('Elastic Net')

plt.tight_layout()
plt.show()

8. 常见问题解答

8.1 基础概念

Q1: 机器学习、深度学习、人工智能的关系?

关系图:
┌─────────────────────────────────────────┐
│          人工智能 (AI)                   │
│  ┌───────────────────────────────────┐  │
│  │      机器学习 (ML)                 │  │
│  │  ┌─────────────────────────────┐  │  │
│  │  │   深度学习 (DL)              │  │  │
│  │  │   - CNN                      │  │  │
│  │  │   - RNN                      │  │  │
│  │  │   - Transformer              │  │  │
│  │  └─────────────────────────────┘  │  │
│  │  - 决策树                         │  │
│  │  - SVM                            │  │
│  │  - 随机森林                       │  │
│  └───────────────────────────────────┘  │
│  - 专家系统                             │
│  - 规则引擎                             │
└─────────────────────────────────────────┘

简单说:
人工智能 ⊃ 机器学习 ⊃ 深度学习

深度学习是机器学习的一个分支
专注于神经网络

Q2: 监督学习、无监督学习、强化学习怎么选?

决策树:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 有标签数据吗?
   有 → 监督学习
       - 预测类别 → 分类
       - 预测数值 → 回归
   
   无 → 无监督学习
       - 分组 → 聚类
       - 降维 → PCA
       - 异常检测

2. 需要序列决策吗?
   是 → 强化学习
       - 游戏AI
       - 机器人控制
       - 自动驾驶
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Q3: 如何选择机器学习算法?

快速决策流程:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 数据量多大?
   <1000样本 → SVM, 朴素贝叶斯
   1000-10万 → 随机森林, XGBoost
   >10万 → 神经网络, XGBoost

2. 数据类型?
   表格数据 → XGBoost, 随机森林 ⭐
   图像 → CNN ⭐
   文本 → 朴素贝叶斯, Transformer ⭐
   时间序列 → LSTM, Prophet
   图数据 → GNN ⭐

3. 需要可解释性吗?
   是 → 决策树, 逻辑回归, 线性回归
   否 → XGBoost, 神经网络

4. 实时性要求?
   高 → 逻辑回归, 单棵决策树
   低 → 深度学习, 集成方法

5. 有GPU吗?
   有 → 深度学习
   无 → 传统机器学习
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

实战建议:
1. 先试简单模型(逻辑回归/决策树)
2. 再试集成方法(随机森林/XGBoost)
3. 最后考虑深度学习

Q4: 准确率都99%了,为什么还说模型不好?

原因:类别不平衡

例子:欺诈检测
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
数据:10000笔交易
  正常:9900笔(99%)
  欺诈:100笔(1%)

模型:全部预测"正常"
结果:
  准确率 = 9900/10000 = 99%!

但完全没用:
  一个欺诈都没抓到!
  Recall = 0%
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

解决办法:
1. 用F1-Score代替Accuracy
2. 用PR曲线(Precision-Recall)
3. 调整样本权重
4. 重采样(SMOTE等)

8.2 模型选择

Q5: 随机森林 vs XGBoost,选哪个?

随机森林:
✅ 训练快
✅ 参数少,容易调
✅ 不容易过拟合
❌ 性能通常略逊XGBoost

XGBoost:
✅ 性能强(Kaggle冠军常用)
✅ 内置缺失值处理
✅ 支持GPU加速
❌ 参数多,需要调优
❌ 容易过拟合

选择:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
快速baseline → 随机森林
追求极致性能 → XGBoost
比赛 → XGBoost + 深度学习集成
生产环境(稳定性) → 随机森林
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Q6: 什么时候用深度学习?

✅ 用深度学习:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 图像/视频
   - 物体检测、图像分类
   - 用CNN

2. 文本/NLP
   - 机器翻译、文本生成
   - 用Transformer

3. 语音
   - 语音识别
   - 用RNN/Transformer

4. 数据量巨大
   - >10万样本
   - 深度学习能充分利用

5. 特征工程难
   - 自动学习特征
   - 端到端训练
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

❌ 不用深度学习:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 表格数据(结构化数据)
   XGBoost/随机森林通常更好

2. 小数据集(<1000样本)
   会过拟合

3. 需要可解释性
   黑盒模型

4. 没有GPU
   训练太慢

5. 实时性要求高
   推理延迟大
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Q7: 为什么我的模型训练集99%,测试集60%?

原因:严重过拟合!

可能的原因:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 模型太复杂
   - 决策树太深
   - 神经网络层数太多
   - 参数太多

2. 训练数据太少
   - 样本不够
   - 模型把训练集"背"下来了

3. 特征太多
   - 维度灾难
   - 特征数 > 样本数

4. 训练太久
   - 迭代次数太多
   - 没有早停

5. 数据泄露
   - 测试集信息泄露到训练集
   - 特征中包含目标变量信息
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

解决步骤:
1. 检查数据泄露
2. 简化模型(减少深度/参数)
3. 增加正则化
4. 增加训练数据
5. 特征选择/降维
6. 交叉验证

8.3 实战问题

Q8: 训练很慢怎么办?

加速技巧:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 减少数据量
   - 采样训练(随机选部分数据)
   - 在线学习(逐批训练)

2. 减少特征
   - 特征选择
   - PCA降维

3. 简化模型
   - 用更简单的算法
   - 减少层数/深度

4. 并行计算
   - n_jobs=-1(使用所有CPU核心)
   - GPU加速(深度学习)

5. 更快的算法
   - LinearSVC代替SVC
   - LightGBM代替XGBoost
   - Mini-Batch K-Means

6. 早停
   - 监控验证集
   - 不再提升就停

7. 参数优化
   - 减少迭代次数
   - 增大batch size
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

代码示例:
# 采样训练
indices = np.random.choice(len(X_train), 10000)
X_sample = X_train[indices]
y_sample = y_train[indices]

# 快速模型
from sklearn.svm import LinearSVC  # 比SVC快
from lightgbm import LGBMClassifier  # 比XGBoost快

# 并行
model = RandomForestClassifier(n_jobs=-1)

# 早停
model = XGBClassifier(
    early_stopping_rounds=10,
    eval_set=[(X_val, y_val)]
)

Q9: 如何处理类别不平衡?

问题:
正例1000个,负例100个(比例10:1)
→ 模型倾向预测多数类

解决方法:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 重采样
   
   过采样(Over-sampling):
   增加少数类样本
   - 重复采样
   - SMOTE(合成新样本)
   
   欠采样(Under-sampling):
   减少多数类样本
   - 随机删除
   - Tomek Links

2. 调整权重
   
   class_weight='balanced'
   少数类权重自动增大

3. 改变评估指标
   
   不用Accuracy
   用F1-Score、AUC

4. 阈值调整
   
   默认阈值0.5
   调整到0.3(更容易预测为正例)

5. 异常检测方法
   
   把少数类当作"异常"
   用One-Class SVM等
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

代码示例:
from imblearn.over_sampling import SMOTE
from imblearn.under_sampling import RandomUnderSampler
from imblearn.pipeline import Pipeline

# SMOTE过采样
smote = SMOTE(random_state=42)
X_resampled, y_resampled = smote.fit_resample(X_train, y_train)

print(f"原始数据分布: {Counter(y_train)}")
print(f"SMOTE后分布: {Counter(y_resampled)}")

# 组合(先过采样,再欠采样)
pipeline = Pipeline([
    ('over', SMOTE(sampling_strategy=0.5)),  # 少数类增加到50%
    ('under', RandomUnderSampler(sampling_strategy=0.8))  # 多数类减到80%
])

X_res, y_res = pipeline.fit_resample(X_train, y_train)

# 调整权重
model = RandomForestClassifier(class_weight='balanced')

# 或手动设置
model = RandomForestClassifier(
    class_weight={0: 1, 1: 10}  # 少数类权重×10
)

Q10: 如何避免数据泄露?

数据泄露:测试集信息泄露到训练集

常见错误:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
错误1:先标准化,再划分数据

# 错误❌
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)  # 用了全部数据!
X_train, X_test = train_test_split(X_scaled, ...)

问题:测试集的均值/方差影响了标准化

# 正确✓
X_train, X_test = train_test_split(X, ...)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)  # 只用训练集
X_test_scaled = scaler.transform(X_test)  # 用训练集的参数
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

错误2:特征中包含未来信息

例子:预测明天股价
特征:明天的成交量  ❌ 这是作弊!

错误3:特征中包含目标变量

例子:预测用户流失
特征:最后登录时间  ❌ 流失了才没有登录
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

检查数据泄露:
1. 特征是否在预测时可获得?
2. 特征是否包含目标信息?
3. 测试集性能是否"太好"?
   (可能是泄露了)

8.4 调参技巧

Q11: 学习率应该设多少?

学习率(Learning Rate)
控制参数更新的步长

太大:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    Loss
     │
     │  ╲  ╱
     │   ╲╱  ← 震荡,不收敛
     │   ╱╲
     └──────── iteration

太小:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    Loss
     │
     │╲
     │ ╲_____ ← 收敛太慢
     │
     └──────── iteration

刚刚好:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    Loss
     │
     │╲
     │ ╲___
     │     ╲___
     └──────────── iteration

经验值:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
传统ML:0.01 - 0.1
深度学习:0.001 - 0.01
Adam优化器:0.001(默认)

调整策略:
1. 从0.001开始
2. 如果收敛慢,增大10倍
3. 如果不收敛,减小10倍
4. 使用学习率调度(逐渐减小)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Q12: Batch Size应该设多少?

Batch Size:每次更新用多少样本

影响:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
小Batch(如32):
✅ 泛化能力好(噪声帮助跳出局部最优)
✅ 内存占用小
❌ 训练慢(更新次数多)
❌ 收敛不稳定

大Batch(如512):
✅ 训练快(GPU利用率高)
✅ 收敛稳定
❌ 泛化能力差(容易过拟合)
❌ 内存占用大
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

经验值:
- 小数据集:32-64
- 大数据集:128-256
- 超大数据集:512-1024

实验:
尝试[32, 64, 128, 256]
用交叉验证选最佳

Q13: 如何判断模型已经训练好了?

停止训练的信号:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 验证集误差不再下降
   
   Validation Loss
     │
     │╲
     │ ╲___
     │     ╲___ ← 已经稳定,可以停了
     │         ╲___
     └──────────────── epoch
   
2. 验证集误差开始上升(过拟合)
   
   Loss
     │
     │╲        ╱ ← 验证集开始上升
     │ ╲___  ╱
     │     ╲╱
     │    训练集继续下降
     └──────────────── epoch
     
   在这里停止!

3. 达到目标性能
   
   准确率达到95% → 停止

4. 时间/资源耗尽
   
   训练24小时 → 停止
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Early Stopping实现:
from sklearn.ensemble import GradientBoostingClassifier

model = GradientBoostingClassifier(
    n_estimators=1000,
    validation_fraction=0.1,  # 10%数据做验证
    n_iter_no_change=10,      # 10轮不改进就停
    tol=1e-4
)

8.5 深度学习问题

Q14: 神经网络的深度和宽度如何选择?

深度:网络层数
宽度:每层神经元数量

深度的影响:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
浅层网络(2-3层):
- 学习简单模式
- 训练快
- 不容易过拟合

深层网络(10+层):
- 学习复杂模式
- 表达能力强
- 容易梯度消失/爆炸
- 需要更多数据
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

宽度的影响:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
窄网络(16-32神经元):
- 参数少
- 欠拟合风险

宽网络(256-512神经元):
- 表达能力强
- 参数多
- 过拟合风险
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

经验配置:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
小任务(MNIST):
  784 → 128 → 64 → 10

中等任务:
  输入 → 256 → 128 → 64 → 输出

大任务(ImageNet):
  ResNet-50:50层
  每层64-2048个神经元

通用建议:
1. 先试浅层网络(3-5层)
2. 如果欠拟合,增加层数或宽度
3. 如果过拟合,减少或加正则化
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Q15: 梯度消失/爆炸是什么?怎么解决?

梯度消失:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
深层网络反向传播时:
梯度 = ∂L/∂w₁ × ∂w₁/∂w₂ × ... × ∂wₙ/∂wₙ₊₁

如果每项 < 1(如0.5):
0.5 × 0.5 × 0.5 × ... (10次) = 0.001

梯度越传越小 → 前面的层学不到东西

原因:
- Sigmoid/Tanh激活函数(梯度<1)
- 网络太深

表现:
- 前面的层权重几乎不更新
- 训练loss不下降
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

梯度爆炸:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
如果每项 > 1(如2):
2 × 2 × 2 × ... (10次) = 1024

梯度越传越大 → 参数更新太猛

表现:
- Loss变成NaN
- 权重爆炸
- 训练崩溃
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

解决办法:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 换激活函数
   Sigmoid → ReLU(缓解梯度消失)

2. 批归一化(Batch Normalization)
   每层输出归一化

3. 残差连接(Residual Connection)
   跳过某些层(ResNet)

4. 梯度裁剪(Gradient Clipping)
   限制梯度最大值(防爆炸)

5. 更好的权重初始化
   Xavier初始化、He初始化

6. 使用LSTM/GRU(RNN的改进版)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

代码:
# 梯度裁剪
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

for batch in data_loader:
    loss = criterion(model(X), y)
    loss.backward()
    
    # 裁剪梯度
    torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)
    
    optimizer.step()
    optimizer.zero_grad()

Q16: 什么是迁移学习?怎么用?

迁移学习:
使用在大数据集上预训练的模型
在小数据集上微调(Fine-tune)

思想:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
大公司(Google/Meta):
- 用ImageNet(100万图像)训练模型
- 学到通用的图像特征(边缘、纹理、形状...)

你(只有1000张猫狗图片):
- 直接用预训练模型
- 只训练最后几层(适应你的任务)
- 效果远超从头训练!
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

可视化:

预训练模型(ImageNet):
┌────────────────────────────┐
│ 卷积层1:提取边缘           │ ← 冻结(不训练)
│ 卷积层2:提取纹理           │ ← 冻结
│ 卷积层3:提取形状           │ ← 冻结
│ 卷积层4:提取物体部件       │ ← 微调
│ 全连接层:分类1000类        │ ← 替换成2类(猫/狗)
└────────────────────────────┘

你的模型(猫狗分类):
┌────────────────────────────┐
│ 卷积层1:提取边缘(复用)   │
│ 卷积层2:提取纹理(复用)   │
│ 卷积层3:提取形状(复用)   │
│ 卷积层4:提取物体部件(微调)│
│ 全连接层:分类2类(新训练) │ ← 猫/狗
└────────────────────────────┘

优点:
✅ 小数据集也能训练好
✅ 训练快(只训练部分层)
✅ 效果好(站在巨人肩膀上)

代码:
import torch
import torchvision.models as models

# 加载预训练模型
model = models.resnet50(pretrained=True)

# 冻结前面的层
for param in model.parameters():
    param.requires_grad = False

# 替换最后的分类层
num_features = model.fc.in_features
model.fc = torch.nn.Linear(num_features, 2)  # 2类输出

# 只训练最后一层
optimizer = torch.optim.Adam(model.fc.parameters(), lr=0.001)

9. 学习路线与资源

9.1 零基础学习路线

完整路线(6-12个月):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

【阶段1:Python基础】(2-4周)
├─ 变量、数据类型、控制流
├─ 函数、类、模块
└─ NumPy数组操作

推荐资源:
- 廖雪峰Python教程
- Python官方教程

【阶段2:数据分析】(4-6周)
├─ NumPy:数值计算
├─ Pandas:数据处理
├─ Matplotlib:数据可视化
└─ 实战:分析真实数据集

推荐资源:
- 《利用Python进行数据分析》
- Kaggle Learn(免费课程)

【阶段3:机器学习基础】(8-12周)
├─ 理论:线性回归、逻辑回归
├─ 理论:决策树、随机森林、SVM
├─ 实践:Scikit-learn库
└─ 实战:Kaggle入门赛(泰坦尼克、房价预测)

推荐资源:
- 吴恩达《机器学习》课程 ⭐⭐⭐⭐⭐
- 周志华《机器学习》(西瓜书)
- 《Hands-On Machine Learning》

【阶段4:深度学习入门】(8-12周)
├─ 神经网络原理
├─ PyTorch/TensorFlow基础
├─ CNN:图像分类
├─ RNN/Transformer:NLP
└─ 实战:MNIST、CIFAR-10

推荐资源:
- 吴恩达《深度学习》专项课程
- 《动手学深度学习》(d2l.ai)
- Fast.ai课程

【阶段5:专项深入】(持续)
选择方向:
├─ 计算机视觉(CV)
│   ├─ 目标检测(YOLO、Faster R-CNN)
│   ├─ 图像分割
│   └─ 生成模型(GAN、Diffusion)
│
├─ 自然语言处理(NLP)
│   ├─ 大模型(GPT、BERT)
│   ├─ 机器翻译
│   └─ 问答系统
│
├─ 推荐系统
│   ├─ 协同过滤
│   ├─ 深度推荐
│   └─ 图神经网络(GNN)
│
└─ 强化学习
    ├─ DQN、PPO
    ├─ 多智能体
    └─ 机器人控制
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

9.2 必读论文(从易到难)

入门级:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. ImageNet Classification with Deep CNNs (AlexNet, 2012)
   开启深度学习时代

2. Dropout: A Simple Way to Prevent Overfitting (2014)
   简单有效的正则化

3. Batch Normalization (2015)
   训练深层网络的关键技术

进阶级:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4. ResNet: Deep Residual Learning (2015)
   残差连接,训练超深网络

5. Attention Is All You Need (2017) ⭐
   Transformer,现代NLP基石
   详见:《Attention_Is_All_You_Need_论文精读.md》

6. BERT (2018)
   双向预训练,NLP里程碑

高级:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
7. GPT-3 (2020)
   大模型涌现能力

8. Vision Transformer (ViT, 2020)
   Transformer用于图像

9. AlphaGo/AlphaZero
   强化学习经典
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

9.3 实战项目推荐

Kaggle竞赛(入门)

1. Titanic: Machine Learning from Disaster
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   任务:预测泰坦尼克号乘客生存
   类型:二分类
   难度:⭐
   学到:数据清洗、特征工程、模型选择

2. House Prices: Advanced Regression
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   任务:预测房价
   类型:回归
   难度:⭐⭐
   学到:特征工程、回归模型、调参

3. Digit Recognizer
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   任务:手写数字识别
   类型:图像分类
   难度:⭐⭐
   学到:CNN、深度学习基础

4. Natural Language Processing with Disaster Tweets
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   任务:推文分类
   类型:文本分类
   难度:⭐⭐⭐
   学到:NLP、文本预处理、BERT

个人项目创意

1. 垃圾邮件过滤器
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   数据:自己的邮箱
   算法:朴素贝叶斯
   部署:浏览器插件

2. 电影推荐系统
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   数据:MovieLens数据集
   算法:协同过滤、矩阵分解
   部署:Web应用

3. 图像风格迁移
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   算法:神经风格迁移
   框架:PyTorch
   部署:微信小程序

4. 智能客服机器人
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   算法:BERT、对话系统
   框架:Hugging Face
   部署:网页聊天窗口

5. 股票价格预测
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   数据:Yahoo Finance API
   算法:LSTM、Transformer
   部署:Dashboard

9.4 工具生态系统

Python机器学习全家桶

┌─────────────────────────────────────────┐
│         Python ML/DL 生态系统            │
├─────────────────────────────────────────┤
│                                          │
│ 【核心计算库】                           │
│ NumPy        数值计算                    │
│ Pandas       数据处理                    │
│ SciPy        科学计算                    │
│                                          │
│ 【机器学习】                             │
│ Scikit-learn 经典ML算法(最常用)⭐     │
│ XGBoost      梯度提升树                  │
│ LightGBM     微软出品,更快              │
│ CatBoost     处理类别特征                │
│                                          │
│ 【深度学习框架】                         │
│ PyTorch      学术界首选 ⭐               │
│ TensorFlow   工业界常用                  │
│ Keras        高层API(简单)             │
│ JAX          Google新框架                │
│                                          │
│ 【预训练模型】                           │
│ Hugging Face Transformers ⭐             │
│ Timm         视觉模型库                  │
│ OpenAI API   GPT系列                     │
│                                          │
│ 【可视化】                               │
│ Matplotlib   基础绘图                    │
│ Seaborn      统计可视化                  │
│ Plotly       交互式图表                  │
│                                          │
│ 【数据获取】                             │
│ Kaggle       竞赛数据集 ⭐               │
│ UCI          机器学习数据仓库            │
│ Papers with Code 论文+代码+数据集       │
│                                          │
│ 【部署】                                 │
│ Flask/FastAPI Web服务                   │
│ Streamlit    快速原型                    │
│ ONNX         模型转换                    │
│ TensorRT     推理加速                    │
│                                          │
└─────────────────────────────────────────┘

9.5 避坑指南

新手最容易犯的10个错误

❌ 错误1:直接上深度学习
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
表格数据用CNN?
→ 应该先试XGBoost/随机森林

✅ 正确做法:
   结构化数据 → 传统ML
   图像/文本 → 深度学习

❌ 错误2:忽略数据探索
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
拿到数据直接训练模型
→ 不了解数据分布、异常值、相关性

✅ 正确做法:
   先做EDA(探索性数据分析)
   画图、统计、相关性分析

❌ 错误3:训练测试集不分
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
全部数据训练和测试
→ 根本不知道泛化能力

✅ 正确做法:
   70%训练,15%验证,15%测试
   或用交叉验证

❌ 错误4:不做特征缩放
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
SVM/KNN不缩放数据
→ 大特征主导模型

✅ 正确做法:
   StandardScaler标准化

❌ 错误5:只看准确率
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
类别不平衡时准确率误导

✅ 正确做法:
   看F1-Score、Precision、Recall、AUC

❌ 错误6:参数都用默认值
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
不调参,效果不好就换算法
→ 可能只是参数不对

✅ 正确做法:
   网格搜索/随机搜索调参

❌ 错误7:忽略特征工程
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
直接用原始特征
→ 损失很多信息

✅ 正确做法:
   特征工程通常比调参更重要
   组合特征、多项式特征、分箱...

❌ 错误8:过度追求复杂模型
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
简单问题用深度学习
→ 杀鸡用牛刀,还效果差

✅ 正确做法:
   从简单模型开始
   够用就好

❌ 错误9:不看文档和报错
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
参数乱设,报错不看
→ 浪费时间

✅ 正确做法:
   多看官方文档
   理解参数含义

❌ 错误10:不保存实验结果
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
试了100种参数组合,忘了哪个最好
→ 重复劳动

✅ 正确做法:
   记录实验(MLflow、Weights & Biases)
   或至少用Excel表格记录
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

9.6 推荐学习资源

在线课程

【入门级】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. Andrew Ng - Machine Learning (Coursera)
   ⭐⭐⭐⭐⭐ 机器学习必修课!
   - 通俗易懂
   - 数学不多
   - 有编程作业
   - 免费审计

2. Andrew Ng - Deep Learning Specialization
   ⭐⭐⭐⭐⭐ 深度学习入门首选
   - 5门课程
   - CNN、RNN、调参、部署
   - 有证书

3. Fast.ai - Practical Deep Learning
   ⭐⭐⭐⭐ 注重实践
   - 代码优先
   - 快速上手
   - 免费

4. Kaggle Learn
   ⭐⭐⭐⭐ 免费实战教程
   - Pandas、ML、DL等
   - 有练习和证书

【中文资源】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 李宏毅 - 机器学习(B站)
   ⭐⭐⭐⭐⭐ 中文最佳!
   - 幽默风趣
   - 讲解深入
   - 免费

2. 动手学深度学习 (d2l.ai)
   ⭐⭐⭐⭐⭐ 理论+代码
   - 中英双语
   - PyTorch/TensorFlow版本
   - 免费

3. 吴恩达课程的中文笔记
   网易云课堂、B站搬运
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

书籍推荐

【入门】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 《Hands-On Machine Learning》
   - Scikit-learn + TensorFlow
   - 大量代码示例
   - 实战导向 ⭐⭐⭐⭐⭐

2. 《Python机器学习》
   - Scikit-learn详解
   - 适合入门

3. 《机器学习实战》
   - 代码实现
   - 偏工程

【进阶】
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4. 《机器学习》(周志华,西瓜书)
   - 理论扎实
   - 数学较多
   - 中文经典 ⭐⭐⭐⭐⭐

5. 《深度学习》(花书,Goodfellow)
   - 深度学习圣经
   - 数学硬核
   - 适合研究

6. 《统计学习方法》(李航)
   - 数学证明严谨
   - 适合考研/面试
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

实战平台

1. Kaggle
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   ⭐⭐⭐⭐⭐ 最推荐!
   - 免费GPU/TPU
   - 真实比赛
   - 大神代码(Notebooks)
   - 讨论区学习
   
   网址:kaggle.com

2. Google Colab
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   ⭐⭐⭐⭐ 免费GPU
   - Jupyter Notebook环境
   - 无需配置
   
   网址:colab.research.google.com

3. Hugging Face
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   ⭐⭐⭐⭐⭐ NLP必用
   - 预训练模型库
   - 一键部署
   
   网址:huggingface.co

4. Papers with Code
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   ⭐⭐⭐⭐ 论文+代码
   - 最新论文
   - 开源实现
   - 排行榜
   
   网址:paperswithcode.com

9.7 职业发展建议

机器学习工程师技能树

必备技能(⭐⭐⭐⭐⭐):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✓ Python编程
✓ NumPy、Pandas
✓ Scikit-learn
✓ 数据预处理
✓ 模型评估
✓ Git版本控制

进阶技能(⭐⭐⭐⭐):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✓ PyTorch/TensorFlow
✓ SQL数据库
✓ Linux命令行
✓ Docker容器
✓ 云平台(AWS/GCP/Azure)

加分技能(⭐⭐⭐):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✓ Spark大数据
✓ MLOps(模型部署运维)
✓ 模型压缩加速
✓ 分布式训练
✓ 论文阅读能力

软技能:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✓ 问题分析能力
✓ 沟通表达能力
✓ 业务理解能力
✓ 持续学习能力

学习时间规划

每天学习时间:2-3小时

时间分配:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
看视频/读文档   40%(1小时)
  - 理解原理
  - 记笔记

写代码实践     50%(1.5小时)⭐最重要!
  - 跟着教程敲代码
  - 做练习题
  - Kaggle比赛

复习总结       10%(20分钟)
  - 整理笔记
  - 总结经验
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

周末:
- 完成一个小项目
- 参加Kaggle比赛
- 看论文/博客

关键:
动手 > 看视频
少就是多(深入学透一门课,胜过浅尝十门)

10. 深度学习进阶

10.1 神经网络训练技巧

10.1.1 权重初始化
为什么重要?
初始化不好 → 梯度消失/爆炸

常用方法:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. Xavier初始化(也叫Glorot)
   
   适用:Sigmoid、Tanh激活
   
   公式:
   w ~ U(-√(6/(n_in + n_out)), √(6/(n_in + n_out)))
   
   或:
   w ~ N(0, √(2/(n_in + n_out)))

2. He初始化
   
   适用:ReLU激活 ⭐最常用
   
   公式:
   w ~ N(0, √(2/n_in))
   
   原因:ReLU会让一半神经元失活
         需要更大的初始值

3. 随机初始化(不推荐)
   
   w ~ N(0, 0.01)
   
   问题:深层网络会梯度消失
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

PyTorch代码:
import torch.nn as nn

# Xavier初始化
nn.init.xavier_uniform_(layer.weight)

# He初始化
nn.init.kaiming_uniform_(layer.weight, nonlinearity='relu')
10.1.2 优化器对比
┌─────────────────────────────────────────┐
│         常用优化器对比                   │
├─────────────────────────────────────────┤
│                                          │
│ SGD(随机梯度下降)                      │
│ ───────────────────                     │
│ w = w - α·∇L                            │
│                                          │
│ 特点:                                   │
│ - 最基础                                 │
│ - 可能陷入局部最优                       │
│ - 学习率难调                             │
│                                          │
├─────────────────────────────────────────┤
│ SGD + Momentum(动量)                  │
│ ──────────────────────                  │
│ v = β·v + ∇L                            │
│ w = w - α·v                             │
│                                          │
│ 特点:                                   │
│ - 加速收敛(惯性)                       │
│ - 减少震荡                               │
│ - β通常取0.9                            │
│                                          │
├─────────────────────────────────────────┤
│ Adam(自适应学习率)⭐最常用            │
│ ────────────────────────                │
│ 结合Momentum和RMSprop                   │
│                                          │
│ 特点:                                   │
│ ✅ 自适应学习率(每个参数不同)         │
│ ✅ 收敛快                                │
│ ✅ 鲁棒性强                              │
│ ✅ 适合大多数问题                        │
│                                          │
│ 默认参数:                               │
│ lr=0.001, β1=0.9, β2=0.999             │
│                                          │
├─────────────────────────────────────────┤
│ AdamW                                   │
│ ─────                                   │
│ Adam + 权重衰减                          │
│                                          │
│ 特点:                                   │
│ - 解决Adam的过拟合问题                  │
│ - Transformer训练常用                   │
│                                          │
└─────────────────────────────────────────┘

选择建议:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
不知道用什么 → Adam(默认)
训练不收敛 → 试试SGD+Momentum
Transformer → AdamW
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

代码:
import torch.optim as optim

# SGD
optimizer = optim.SGD(model.parameters(), lr=0.01)

# SGD + Momentum
optimizer = optim.SGD(model.parameters(), lr=0.01, momentum=0.9)

# Adam
optimizer = optim.Adam(model.parameters(), lr=0.001)

# AdamW
optimizer = optim.AdamW(model.parameters(), lr=0.001, weight_decay=0.01)
10.1.3 Batch Normalization
批归一化:神经网络训练的"银弹"

问题:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
深层网络中,每层输入分布不断变化
→ "内部协变量偏移"
→ 训练不稳定

解决:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
每层输出归一化(均值0,方差1)

位置:激活函数之前

流程:
输入 → 线性变换 → BN → 激活函数 → 输出

公式:
μ = (1/m)Σxᵢ              # batch均值
σ² = (1/m)Σ(xᵢ - μ)²     # batch方差

x̂ = (x - μ) / √(σ² + ε)  # 归一化

y = γ·x̂ + β               # 可学习的缩放和平移

优点:
✅ 加快训练(可以用更大学习率)
✅ 减少对初始化的依赖
✅ 有正则化效果(类似Dropout)
✅ 允许更深的网络

代码:
import torch.nn as nn

model = nn.Sequential(
    nn.Linear(784, 256),
    nn.BatchNorm1d(256),  # Batch Normalization
    nn.ReLU(),
    nn.Linear(256, 128),
    nn.BatchNorm1d(128),
    nn.ReLU(),
    nn.Linear(128, 10)
)
10.1.4 Dropout
Dropout:随机"丢弃"神经元

训练时:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
      ●  ●  ●  ●
       ╲ │ ╱│ ╱
        ●✗●  ●   ← ✗ 被随机丢弃
         ││ ╱│
         ●●  ●

每次forward,随机丢弃p%的神经元
(常用p=0.5)

测试时:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
不丢弃,但输出乘以(1-p)
保持期望一致

原理:
- 防止神经元之间过度依赖
- 类似集成学习(每次训练不同子网络)
- 增强泛化能力

代码:
import torch.nn as nn

model = nn.Sequential(
    nn.Linear(784, 512),
    nn.ReLU(),
    nn.Dropout(0.5),  # 50% dropout
    nn.Linear(512, 256),
    nn.ReLU(),
    nn.Dropout(0.3),  # 30% dropout
    nn.Linear(256, 10)
)

注意:
- 只在训练时使用
- 测试时自动关闭
model.eval()  # 测试模式(关闭dropout)
model.train()  # 训练模式(启用dropout)

10.2 CNN详解

10.2.1 卷积层详解
卷积核的作用:特征检测器

3×3卷积核示例:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
垂直边缘检测:         水平边缘检测:
 -1  0  1              -1 -1 -1
 -1  0  1               0  0  0
 -1  0  1               1  1  1

对角线检测:           模糊:
  0  1  0              1/9 1/9 1/9
  1  0  1              1/9 1/9 1/9
  0  1  0              1/9 1/9 1/9
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

卷积参数:
- kernel_size:卷积核大小(常用3×3, 5×5)
- stride:步长(常用1)
- padding:填充(same padding保持尺寸)
- in_channels:输入通道数
- out_channels:输出通道数(卷积核数量)

输出尺寸计算:
output_size = (input_size + 2×padding - kernel_size) / stride + 1

例子:
输入:28×28
卷积:3×3, stride=1, padding=1
输出:(28 + 2×1 - 3) / 1 + 1 = 28×28 ✓(保持)

输入:28×28
卷积:3×3, stride=2, padding=0
输出:(28 + 0 - 3) / 2 + 1 = 13×13

多通道卷积

输入:RGB图像(3通道)

    R通道         G通道         B通道
  ┌──────┐    ┌──────┐    ┌──────┐
  │ 255  │    │ 128  │    │ 64   │
  │  ... │    │  ... │    │  ... │
  └──────┘    └──────┘    └──────┘

卷积核(3×3×3):
  对应3个通道,每通道一个3×3矩阵

输出:1个通道(所有输入通道求和)

多个卷积核(如64个):
  → 输出64个通道(特征图)

可视化:
输入:32×32×3(高×宽×通道)
卷积层:64个3×3卷积核
输出:32×32×64

每个输出通道检测一种特征:
- 通道1:检测垂直边缘
- 通道2:检测水平边缘
- 通道3:检测圆形
- ...
- 通道64:检测某种复杂纹理
10.2.2 池化层详解
池化:降维 + 增强鲁棒性

最大池化(Max Pooling):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
输入(4×4):
  1  3  2  4
  5  6  7  8
  3  2  1  2
  1  2  3  4

2×2 Max Pooling(stride=2):
  
  ┌────┐
  │1  3│ 2  4  → max=6
  │5  6│ 7  8
  └────┘
  
   3  2   1  2
   1  2   3  4
   
输出(2×2):
  6  8
  3  4

作用:
1. 降维(4×4 → 2×2)
2. 保留最显著特征
3. 位置不变性(小范围移动不影响)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

平均池化(Average Pooling):
取平均而非最大值

全局平均池化(Global Average Pooling):
整个特征图取平均
→ 输出1个数(常用于全连接层前)
10.2.3 经典CNN架构

LeNet-5(1998)

最早的CNN之一

结构:
输入(32×32) 
  → Conv(6个5×5) → Pooling 
  → Conv(16个5×5) → Pooling
  → FC(120) → FC(84) → FC(10)

应用:手写数字识别
参数量:约6万

历史意义:
证明了CNN在图像识别上的潜力

AlexNet(2012)

深度学习革命的开端
ImageNet冠军(Top-5错误率15.3%)

创新:
1. 更深(8层)
2. ReLU激活(替代Sigmoid)
3. Dropout正则化
4. 数据增强
5. GPU训练

结构:
11×11 Conv → Pool 
  → 5×5 Conv → Pool
  → 3×3 Conv → 3×3 Conv → 3×3 Conv → Pool
  → FC(4096) → FC(4096) → FC(1000)

参数量:6000万

影响:
开启了深度学习时代
之后几年,CNN统治图像识别

VGGNet(2014)

更深更简单

特点:
- 全部使用3×3卷积
- 网络很深(16/19层)
- 结构规整

VGG-16结构:
输入(224×224×3)
  → [Conv3×3]×2 → Pool
  → [Conv3×3]×2 → Pool
  → [Conv3×3]×3 → Pool
  → [Conv3×3]×3 → Pool
  → [Conv3×3]×3 → Pool
  → FC(4096) → FC(4096) → FC(1000)

参数量:1.38亿(太多!)

优点:结构简单
缺点:参数太多,训练慢

ResNet(2015)

残差网络:解决深层网络退化问题

核心:残差连接(Skip Connection)

传统网络:
  x → [层] → [层] → [层] → y

ResNet:
  x → [层] → [层] → [层] → y
  └─────────────────────┘
       残差连接(跳过)

数学:
传统:y = F(x)
ResNet:y = F(x) + x

优点:
✅ 可以训练超深网络(152层、甚至1000层)
✅ 梯度直接传播(通过skip connection)
✅ 不会退化(最坏情况,学到恒等映射)

代码:
class ResidualBlock(nn.Module):
    def __init__(self, channels):
        super().__init__()
        self.conv1 = nn.Conv2d(channels, channels, 3, padding=1)
        self.bn1 = nn.BatchNorm2d(channels)
        self.conv2 = nn.Conv2d(channels, channels, 3, padding=1)
        self.bn2 = nn.BatchNorm2d(channels)
        
    def forward(self, x):
        residual = x  # 保存输入
        
        out = self.conv1(x)
        out = self.bn1(out)
        out = F.relu(out)
        
        out = self.conv2(out)
        out = self.bn2(out)
        
        out += residual  # 加上residual
        out = F.relu(out)
        
        return out

10.3 循环神经网络 (RNN)

10.3.1 RNN原理
处理序列数据的神经网络

核心特点:有"记忆"

传统神经网络:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
输入1 → ● → 输出1
输入2 → ● → 输出2  (独立处理,无记忆)
输入3 → ● → 输出3

RNN:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
输入1 → ●─→ 隐藏状态1 → 输出1
        ↓
输入2 → ●─→ 隐藏状态2 → 输出2
        ↓                (记住前面的信息)
输入3 → ●─→ 隐藏状态3 → 输出3
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

RNN展开图

时间步:  t=1      t=2      t=3
        
输入:    x₁       x₂       x₃
          ↓        ↓        ↓
        ┌─●─┐    ┌─●─┐    ┌─●─┐
h₀ ────→│RNN│───→│RNN│───→│RNN│───→ h₃
        └─●─┘    └─●─┘    └─●─┘
          ↓        ↓        ↓
输出:    y₁       y₂       y₃

公式:
hₜ = tanh(Wₕₕ·hₜ₋₁ + Wₓₕ·xₜ + bₕ)
yₜ = Wₕᵧ·hₜ + bᵧ

其中:
- hₜ:时刻t的隐藏状态(记忆)
- xₜ:时刻t的输入
- yₜ:时刻t的输出

应用场景

1. 机器翻译
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   输入:"我 爱 机器 学习"
   输出:"I love machine learning"

2. 文本生成
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   输入:"从前有座山"
   输出:续写故事

3. 情感分析
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   输入:"这部电影真的很棒"
   输出:正面情感

4. 时间序列预测
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   输入:过去7天股价
   输出:明天股价
10.3.2 LSTM(长短期记忆网络)
RNN的问题:长期依赖难以学习
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
句子:"法国的首都,这个美丽的城市,是巴黎"

RNN处理到"是"时,已经忘了"法国"
→ 无法正确预测"巴黎"
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

LSTM解决方案:
引入"细胞状态"(长期记忆)+ 三个门

结构:
┌─────────────────────────────────────────┐
│          LSTM单元                        │
├─────────────────────────────────────────┤
│                                          │
│         细胞状态(长期记忆)             │
│  Cₜ₋₁ ──────────────────────→ Cₜ       │
│         ↓               ↓                │
│      遗忘门           输入门             │
│      (忘掉)           (记住)             │
│         ↓               ↓                │
│  ┌──────────────────────────┐           │
│  │      LSTM核心计算         │           │
│  └──────────────────────────┘           │
│         ↓                                │
│      输出门                              │
│      (输出)                              │
│         ↓                                │
│        hₜ (隐藏状态)                   │
│                                          │
└─────────────────────────────────────────┘

三个门:
1. 遗忘门(Forget Gate):
   决定丢弃哪些旧信息
   fₜ = σ(Wf·[hₜ₋₁, xₜ] + bf)

2. 输入门(Input Gate):
   决定存储哪些新信息
   iₜ = σ(Wi·[hₜ₋₁, xₜ] + bi)
   C̃ₜ = tanh(WC·[hₜ₋₁, xₜ] + bC)

3. 输出门(Output Gate):
   决定输出什么
   oₜ = σ(Wo·[hₜ₋₁, xₜ] + bo)

更新细胞状态:
Cₜ = fₜ * Cₜ₋₁ + iₜ * C̃ₜ
     ↑          ↑
   遗忘旧的    加入新的

输出:
hₜ = oₜ * tanh(Cₜ)

代码示例

import torch
import torch.nn as nn

# 使用PyTorch内置LSTM
class LSTMModel(nn.Module):
    def __init__(self, input_size, hidden_size, num_layers, num_classes):
        super().__init__()
        self.hidden_size = hidden_size
        self.num_layers = num_layers
        
        # LSTM层
        self.lstm = nn.LSTM(
            input_size,
            hidden_size,
            num_layers,
            batch_first=True,  # 输入形状 (batch, seq, feature)
            dropout=0.2        # 层间dropout
        )
        
        # 全连接层
        self.fc = nn.Linear(hidden_size, num_classes)
    
    def forward(self, x):
        # 初始化隐藏状态
        h0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size)
        c0 = torch.zeros(self.num_layers, x.size(0), self.hidden_size)
        
        # LSTM前向传播
        out, (hn, cn) = self.lstm(x, (h0, c0))
        
        # 取最后一个时间步的输出
        out = self.fc(out[:, -1, :])
        
        return out

# 示例:情感分析
vocab_size = 10000  # 词汇表大小
embed_size = 128    # 词嵌入维度
hidden_size = 256   # LSTM隐藏层大小
num_layers = 2      # LSTM层数
num_classes = 2     # 正面/负面

model = nn.Sequential(
    nn.Embedding(vocab_size, embed_size),  # 词嵌入层
    nn.LSTM(embed_size, hidden_size, num_layers, batch_first=True),
    nn.Linear(hidden_size, num_classes)
)

print(model)

10.4 Transformer架构

详细内容请参考本目录下的文档:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📄 Attention_Is_All_You_Need_论文精读.md
📄 Transformer完全指南_小白友好版.md
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

核心概念:
1. Self-Attention机制
   - Query、Key、Value
   - 注意力权重计算

2. Multi-Head Attention
   - 多个注意力头
   - 捕获不同关系

3. 位置编码
   - 正弦/余弦编码
   - 保留位置信息

4. Encoder-Decoder结构
   - 6层Encoder
   - 6层Decoder

应用:
- GPT系列(ChatGPT)
- BERT(Google搜索)
- T5(通用NLP)
- ViT(图像识别)

11. 完整实战案例集

11.1 房价预测(回归)

数据集:波士顿房价

import pandas as pd
import numpy as np
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LinearRegression, Ridge, Lasso
from sklearn.ensemble import RandomForestRegressor, GradientBoostingRegressor
from sklearn.metrics import mean_squared_error, r2_score
import matplotlib.pyplot as plt

# 加载数据
housing = fetch_california_housing()
X, y = housing.data, housing.target

print("=" * 50)
print("加州房价预测")
print("=" * 50)
print(f"\n数据集大小: {X.shape}")
print(f"特征: {housing.feature_names}")
print(f"目标: 房价中位数(单位:10万美元)")

# 转换为DataFrame
df = pd.DataFrame(X, columns=housing.feature_names)
df['price'] = y

# 数据探索
print("\n数据统计:")
print(df.describe())

# 相关性分析
correlation = df.corr()['price'].sort_values(ascending=False)
print("\n与房价相关性:")
print(correlation)

# 可视化
fig, axes = plt.subplots(2, 4, figsize=(16, 8))
axes = axes.ravel()

for idx, col in enumerate(housing.feature_names):
    axes[idx].scatter(df[col], df['price'], alpha=0.3, s=1)
    axes[idx].set_xlabel(col)
    axes[idx].set_ylabel('Price')
    axes[idx].set_title(f'{col} vs Price')

plt.tight_layout()
plt.savefig('housing_eda.png', dpi=150)

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

# 特征缩放
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

# 训练多个模型
models = {
    'Linear Regression': LinearRegression(),
    'Ridge': Ridge(alpha=1.0),
    'Lasso': Lasso(alpha=0.01),
    'Random Forest': RandomForestRegressor(n_estimators=100, random_state=42),
    'Gradient Boosting': GradientBoostingRegressor(n_estimators=100, random_state=42)
}

results = {}

print("\n" + "=" * 50)
print("模型训练与评估")
print("=" * 50)

for name, model in models.items():
    # 训练
    if 'Linear' in name or 'Ridge' in name or 'Lasso' in name:
        model.fit(X_train_scaled, y_train)
        y_pred = model.predict(X_test_scaled)
    else:
        model.fit(X_train, y_train)
        y_pred = model.predict(X_test)
    
    # 评估
    mse = mean_squared_error(y_test, y_pred)
    rmse = np.sqrt(mse)
    r2 = r2_score(y_test, y_pred)
    
    results[name] = {'RMSE': rmse, 'R2': r2}
    
    print(f"\n{name}:")
    print(f"  RMSE: {rmse:.4f}")
    print(f"  R² Score: {r2:.4f}")

# 可视化对比
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))

# RMSE对比
rmse_values = [results[name]['RMSE'] for name in models.keys()]
ax1.barh(list(models.keys()), rmse_values)
ax1.set_xlabel('RMSE')
ax1.set_title('模型RMSE对比(越小越好)')

# R²对比
r2_values = [results[name]['R2'] for name in models.keys()]
ax2.barh(list(models.keys()), r2_values)
ax2.set_xlabel('R² Score')
ax2.set_title('模型R²对比(越大越好)')
ax2.axvline(x=0, color='gray', linestyle='--')

plt.tight_layout()
plt.savefig('housing_model_comparison.png', dpi=150)
print("\n模型对比图已保存")

# 选择最佳模型进行预测
best_model_name = max(results, key=lambda x: results[x]['R2'])
print(f"\n最佳模型: {best_model_name}")

# 预测展示
best_model = models[best_model_name]
if 'Linear' in best_model_name or 'Ridge' in best_model_name:
    y_pred_best = best_model.predict(X_test_scaled[:10])
else:
    y_pred_best = best_model.predict(X_test[:10])

print(f"\n前10个预测结果:")
print(f"{'真实值':<12} {'预测值':<12} {'误差':<12}")
print("-" * 36)
for i in range(10):
    true_val = y_test[i]
    pred_val = y_pred_best[i]
    error = abs(true_val - pred_val)
    print(f"{true_val:<12.3f} {pred_val:<12.3f} {error:<12.3f}")

11.2 图像分类(CNN实战)

MNIST手写数字识别

import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
import matplotlib.pyplot as plt

# ========== 数据准备 ==========
print("=" * 50)
print("MNIST手写数字识别")
print("=" * 50)

# 数据增强
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.1307,), (0.3081,))
])

# 加载数据
train_dataset = datasets.MNIST(
    root='./data',
    train=True,
    download=True,
    transform=transform
)

test_dataset = datasets.MNIST(
    root='./data',
    train=False,
    transform=transform
)

# 数据加载器
train_loader = DataLoader(
    train_dataset,
    batch_size=64,
    shuffle=True
)

test_loader = DataLoader(
    test_dataset,
    batch_size=1000,
    shuffle=False
)

print(f"\n训练集大小: {len(train_dataset)}")
print(f"测试集大小: {len(test_dataset)}")

# 可视化样本
fig, axes = plt.subplots(2, 5, figsize=(12, 5))
for i, ax in enumerate(axes.flat):
    image, label = train_dataset[i]
    ax.imshow(image.squeeze(), cmap='gray')
    ax.set_title(f'Label: {label}')
    ax.axis('off')
plt.tight_layout()
plt.savefig('mnist_samples.png', dpi=150)

# ========== 定义CNN模型 ==========
class SimpleCNN(nn.Module):
    def __init__(self):
        super().__init__()
        # 卷积层
        self.conv1 = nn.Conv2d(1, 32, kernel_size=3, padding=1)
        self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
        
        # 全连接层
        self.fc1 = nn.Linear(64 * 7 * 7, 128)
        self.fc2 = nn.Linear(128, 10)
        
        # Dropout
        self.dropout = nn.Dropout(0.25)
        
    def forward(self, x):
        # 卷积层1 + ReLU + MaxPool
        x = F.relu(self.conv1(x))
        x = F.max_pool2d(x, 2)  # 28x28 → 14x14
        
        # 卷积层2 + ReLU + MaxPool
        x = F.relu(self.conv2(x))
        x = F.max_pool2d(x, 2)  # 14x14 → 7x7
        
        # 展平
        x = x.view(-1, 64 * 7 * 7)
        
        # 全连接层1
        x = F.relu(self.fc1(x))
        x = self.dropout(x)
        
        # 全连接层2(输出)
        x = self.fc2(x)
        
        return F.log_softmax(x, dim=1)

# 创建模型
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = SimpleCNN().to(device)

print(f"\n模型结构:")
print(model)

# 统计参数量
total_params = sum(p.numel() for p in model.parameters())
print(f"\n总参数量: {total_params:,}")

# ========== 训练 ==========
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
criterion = nn.NLLLoss()

def train(epoch):
    model.train()
    train_loss = 0
    correct = 0
    total = 0
    
    for batch_idx, (data, target) in enumerate(train_loader):
        data, target = data.to(device), target.to(device)
        
        optimizer.zero_grad()
        output = model(data)
        loss = criterion(output, target)
        loss.backward()
        optimizer.step()
        
        train_loss += loss.item()
        pred = output.argmax(dim=1)
        correct += pred.eq(target).sum().item()
        total += target.size(0)
        
        if batch_idx % 100 == 0:
            print(f'Epoch: {epoch} [{batch_idx * len(data)}/{len(train_loader.dataset)}'
                  f' ({100. * batch_idx / len(train_loader):.0f}%)]'
                  f'\tLoss: {loss.item():.6f}')
    
    avg_loss = train_loss / len(train_loader)
    accuracy = 100. * correct / total
    
    return avg_loss, accuracy

def test():
    model.eval()
    test_loss = 0
    correct = 0
    
    with torch.no_grad():
        for data, target in test_loader:
            data, target = data.to(device), target.to(device)
            output = model(data)
            test_loss += criterion(output, target).item()
            pred = output.argmax(dim=1)
            correct += pred.eq(target).sum().item()
    
    test_loss /= len(test_loader)
    accuracy = 100. * correct / len(test_loader.dataset)
    
    print(f'\nTest set: Average loss: {test_loss:.4f}, '
          f'Accuracy: {correct}/{len(test_loader.dataset)} '
          f'({accuracy:.2f}%)\n')
    
    return test_loss, accuracy

# 训练循环
n_epochs = 5
train_losses, train_accs = [], []
test_losses, test_accs = [], []

print("\n开始训练...")
for epoch in range(1, n_epochs + 1):
    train_loss, train_acc = train(epoch)
    test_loss, test_acc = test()
    
    train_losses.append(train_loss)
    train_accs.append(train_acc)
    test_losses.append(test_loss)
    test_accs.append(test_acc)

# 可视化训练过程
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 5))

ax1.plot(train_losses, label='Train Loss')
ax1.plot(test_losses, label='Test Loss')
ax1.set_xlabel('Epoch')
ax1.set_ylabel('Loss')
ax1.set_title('Training and Test Loss')
ax1.legend()
ax1.grid(True)

ax2.plot(train_accs, label='Train Accuracy')
ax2.plot(test_accs, label='Test Accuracy')
ax2.set_xlabel('Epoch')
ax2.set_ylabel('Accuracy (%)')
ax2.set_title('Training and Test Accuracy')
ax2.legend()
ax2.grid(True)

plt.tight_layout()
plt.savefig('mnist_training_curves.png', dpi=150)
print("训练曲线已保存")

# 保存模型
torch.save(model.state_dict(), 'mnist_cnn.pth')
print("模型已保存")

11.3 情感分析(NLP)

使用预训练BERT

from transformers import BertTokenizer, BertForSequenceClassification
from transformers import Trainer, TrainingArguments
import torch
from torch.utils.data import Dataset

# ========== 准备数据 ==========
class SentimentDataset(Dataset):
    def __init__(self, texts, labels, tokenizer, max_len=128):
        self.texts = texts
        self.labels = labels
        self.tokenizer = tokenizer
        self.max_len = max_len
    
    def __len__(self):
        return len(self.texts)
    
    def __getitem__(self, idx):
        text = self.texts[idx]
        label = self.labels[idx]
        
        encoding = self.tokenizer(
            text,
            max_length=self.max_len,
            padding='max_length',
            truncation=True,
            return_tensors='pt'
        )
        
        return {
            'input_ids': encoding['input_ids'].flatten(),
            'attention_mask': encoding['attention_mask'].flatten(),
            'labels': torch.tensor(label, dtype=torch.long)
        }

# 示例数据
texts_train = [
    "这部电影真的很棒,我非常喜欢!",
    "太无聊了,浪费时间。",
    "演员演技很好,剧情也不错。",
    "完全不推荐,很差劲。"
]

labels_train = [1, 0, 1, 0]  # 1=正面,0=负面

# 加载预训练模型和分词器
model_name = 'bert-base-chinese'
tokenizer = BertTokenizer.from_pretrained(model_name)
model = BertForSequenceClassification.from_pretrained(
    model_name,
    num_labels=2
)

# 创建数据集
train_dataset = SentimentDataset(texts_train, labels_train, tokenizer)

# 训练参数
training_args = TrainingArguments(
    output_dir='./results',
    num_train_epochs=3,
    per_device_train_batch_size=8,
    logging_dir='./logs',
)

# 训练器
trainer = Trainer(
    model=model,
    args=training_args,
    train_dataset=train_dataset
)

# 训练
trainer.train()

# 预测新文本
test_texts = [
    "非常精彩的一部电影!",
    "这是我看过最烂的电影。"
]

model.eval()
predictions = []

for text in test_texts:
    encoding = tokenizer(
        text,
        max_length=128,
        padding='max_length',
        truncation=True,
        return_tensors='pt'
    )
    
    with torch.no_grad():
        outputs = model(**encoding)
        logits = outputs.logits
        pred = torch.argmax(logits, dim=1).item()
        probs = F.softmax(logits, dim=1)[0]
    
    sentiment = "正面" if pred == 1 else "负面"
    confidence = probs[pred].item()
    
    print(f"\n文本: {text}")
    print(f"情感: {sentiment} (置信度: {confidence:.2%})")

12. 总结与展望

12.1 机器学习知识图谱

┌─────────────────────────────────────────────────────┐
│              机器学习完整知识体系                     │
├─────────────────────────────────────────────────────┤
│                                                      │
│  【理论基础】                                        │
│  ├─ 概率论与统计                                    │
│  ├─ 线性代数                                        │
│  ├─ 最优化理论                                      │
│  └─ 信息论                                          │
│                                                      │
│  【经典算法】                                        │
│  ├─ 监督学习                                        │
│  │   ├─ 回归:线性回归、岭回归、Lasso              │
│  │   ├─ 分类:逻辑回归、SVM、朴素贝叶斯            │
│  │   └─ 树模型:决策树、随机森林、XGBoost          │
│  │                                                   │
│  ├─ 无监督学习                                      │
│  │   ├─ 聚类:K-Means、层次聚类、DBSCAN            │
│  │   ├─ 降维:PCA、t-SNE、UMAP                     │
│  │   └─ 异常检测                                    │
│  │                                                   │
│  └─ 强化学习                                        │
│      ├─ Q-Learning                                  │
│      ├─ Policy Gradient                             │
│      └─ Actor-Critic                                │
│                                                      │
│  【深度学习】                                        │
│  ├─ 基础架构                                        │
│  │   ├─ MLP(全连接网络)                          │
│  │   ├─ CNN(卷积神经网络)                        │
│  │   ├─ RNN/LSTM(循环神经网络)                   │
│  │   └─ Transformer(注意力机制)                  │
│  │                                                   │
│  ├─ 计算机视觉                                      │
│  │   ├─ 图像分类:ResNet、ViT                      │
│  │   ├─ 目标检测:YOLO、Faster R-CNN               │
│  │   ├─ 图像分割:U-Net、Mask R-CNN                │
│  │   └─ 生成模型:GAN、Diffusion                   │
│  │                                                   │
│  ├─ 自然语言处理                                    │
│  │   ├─ 预训练模型:BERT、GPT                      │
│  │   ├─ 机器翻译:Transformer                      │
│  │   └─ 对话系统:ChatGPT原理                      │
│  │                                                   │
│  └─ 其他应用                                        │
│      ├─ 语音识别:Wav2Vec                          │
│      ├─ 推荐系统:Deep FM                           │
│      └─ 图神经网络:GCN、GAT                       │
│                                                      │
│  【工程实践】                                        │
│  ├─ 数据处理:Pandas、SQL                          │
│  ├─ 特征工程:自动化特征工程                        │
│  ├─ 模型部署:Flask、Docker、云服务                │
│  ├─ 模型监控:MLflow、Wandb                        │
│  └─ 优化加速:模型剪枝、量化、蒸馏                  │
│                                                      │
└─────────────────────────────────────────────────────┘

12.2 从入门到精通的里程碑

阶段1:新手村(1-2个月)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
□ 完成Python基础学习
□ 掌握NumPy、Pandas基本操作
□ 理解机器学习基本概念
□ 实现简单的线性回归
□ 完成1个Kaggle入门项目

标志:能用Scikit-learn解决简单问题

阶段2:进阶(3-6个月)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
□ 掌握5种以上ML算法(原理+代码)
□ 完成数据清洗、特征工程
□ 学会调参(网格搜索、交叉验证)
□ Kaggle排名前50%
□ 读懂3篇以上经典论文

标志:能独立完成中等难度ML项目

阶段3:深度学习(6-12个月)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
□ 掌握PyTorch/TensorFlow
□ 实现CNN图像分类(准确率>95%)
□ 实现RNN文本生成
□ 理解Transformer原理
□ 使用预训练模型(BERT、GPT)
□ Kaggle排名前10%

标志:能用深度学习解决复杂问题

阶段4:专家(1-2年持续)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
□ 选择专精方向(CV/NLP/推荐等)
□ 读论文并复现代码
□ 参与开源项目
□ 发表论文/写技术博客
□ Kaggle获奖(前3%)

标志:在某个领域有深入研究

12.3 持续学习建议

机器学习是快速发展的领域
需要持续学习!

学习习惯:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 每天阅读
   ├─ Papers with Code(最新论文)
   ├─ arXiv.org(论文预印本)
   └─ 技术博客(Medium、知乎)

2. 每周实践
   ├─ Kaggle新比赛
   ├─ 复现论文代码
   └─ 个人项目

3. 每月总结
   ├─ 整理笔记
   ├─ 写博客分享
   └─ 回顾进步

4. 持续关注
   ├─ 顶会论文(NeurIPS、ICML、CVPR)
   ├─ 大公司博客(OpenAI、DeepMind、Google AI)
   └─ GitHub Trending
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

推荐订阅:
- Papers with Code Newsletter
- Andrew Ng的deeplearning.ai
- 机器之心、AI科技评论(中文)

12.4 最后的话

恭喜你读完这份文档!🎉

你已经学到:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
✅ 机器学习的核心概念
✅ 7种经典算法的原理和实现
   - 线性回归
   - 逻辑回归
   - 决策树
   - 随机森林
   - SVM
   - KNN
   - 朴素贝叶斯
   - K-Means

✅ 深度学习基础
   - 神经网络原理
   - CNN卷积网络
   - RNN/LSTM
   - Transformer架构

✅ 实战技能
   - 数据预处理
   - 特征工程
   - 模型评估
   - 超参数调优
   - 完整项目流程

✅ 工程能力
   - Scikit-learn使用
   - PyTorch基础
   - 模型保存和加载
   - 可视化分析
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
接下来该做什么?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 立即行动!
   别只看不练
   打开Jupyter Notebook
   敲下第一行代码:import sklearn

2. 选一个项目
   Kaggle泰坦尼克
   或你感兴趣的任何问题

3. 遇到困难?
   Google、Stack Overflow
   Kaggle Discussions
   GitHub Issues

4. 记录学习
   写笔记、写博客
   教是最好的学

5. 加入社区
   Kaggle、GitHub
   机器学习论坛
   找到志同道合的伙伴

🚀祝你天天开心!
最后更新: 2025年11月23日
作者:Echo


附录 A:常用代码片段

A.1 数据处理

# ========== 读取数据 ==========
import pandas as pd

# CSV
df = pd.read_csv('data.csv')

# Excel
df = pd.read_excel('data.xlsx')

# JSON
df = pd.read_json('data.json')

# SQL
import sqlite3
conn = sqlite3.connect('database.db')
df = pd.read_sql('SELECT * FROM table', conn)

# ========== 数据探索 ==========
# 查看前几行
print(df.head())

# 数据信息
print(df.info())

# 统计描述
print(df.describe())

# 缺失值
print(df.isnull().sum())

# 数据类型
print(df.dtypes)

# 唯一值
print(df['column'].unique())
print(df['column'].nunique())

# ========== 数据清洗 ==========
# 删除缺失值
df_clean = df.dropna()  # 删除任何含NaN的行
df_clean = df.dropna(subset=['col1', 'col2'])  # 只看特定列
df_clean = df.dropna(thresh=3)  # 至少3个非NaN值

# 填充缺失值
df['age'].fillna(df['age'].median(), inplace=True)
df['city'].fillna('Unknown', inplace=True)

# 删除重复
df_clean = df.drop_duplicates()

# 异常值处理(IQR方法)
Q1 = df['column'].quantile(0.25)
Q3 = df['column'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
df_clean = df[(df['column'] >= lower_bound) & (df['column'] <= upper_bound)]

# ========== 特征工程 ==========
# 类别编码
df['gender_encoded'] = df['gender'].map({'男': 0, '女': 1})

# One-Hot编码
df_encoded = pd.get_dummies(df, columns=['city'])

# 分箱
df['age_group'] = pd.cut(df['age'], bins=[0, 18, 35, 60, 100])

# 日期特征
df['date'] = pd.to_datetime(df['date'])
df['year'] = df['date'].dt.year
df['month'] = df['date'].dt.month
df['day_of_week'] = df['date'].dt.dayofweek

# ========== 数据划分 ==========
from sklearn.model_selection import train_test_split

X = df.drop('target', axis=1)
y = df['target']

X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

A.2 模型训练模板

from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import cross_val_score, GridSearchCV
from sklearn.metrics import classification_report
import joblib

# ========== 快速训练 ==========
model = RandomForestClassifier(n_estimators=100, random_state=42)
model.fit(X_train, y_train)

# 评估
y_pred = model.predict(X_test)
print(classification_report(y_test, y_pred))

# ========== 交叉验证 ==========
scores = cross_val_score(model, X, y, cv=5, scoring='accuracy')
print(f"交叉验证准确率: {scores.mean():.4f} (+/- {scores.std():.4f})")

# ========== 网格搜索调参 ==========
param_grid = {
    'n_estimators': [50, 100, 200],
    'max_depth': [5, 10, None],
    'min_samples_split': [2, 5, 10]
}

grid_search = GridSearchCV(
    RandomForestClassifier(random_state=42),
    param_grid,
    cv=5,
    scoring='accuracy',
    n_jobs=-1,
    verbose=1
)

grid_search.fit(X_train, y_train)

print(f"最佳参数: {grid_search.best_params_}")
print(f"最佳得分: {grid_search.best_score_:.4f}")

best_model = grid_search.best_estimator_

# ========== 保存模型 ==========
joblib.dump(best_model, 'model.pkl')

# 加载模型
loaded_model = joblib.load('model.pkl')

A.3 深度学习训练模板

import torch
import torch.nn as nn
import torch.optim as optim
from torch.utils.data import DataLoader, TensorDataset

# ========== 定义模型 ==========
class SimpleNet(nn.Module):
    def __init__(self, input_size, hidden_size, num_classes):
        super().__init__()
        self.fc1 = nn.Linear(input_size, hidden_size)
        self.bn1 = nn.BatchNorm1d(hidden_size)
        self.fc2 = nn.Linear(hidden_size, hidden_size)
        self.bn2 = nn.BatchNorm1d(hidden_size)
        self.fc3 = nn.Linear(hidden_size, num_classes)
        self.dropout = nn.Dropout(0.5)
        
    def forward(self, x):
        x = F.relu(self.bn1(self.fc1(x)))
        x = self.dropout(x)
        x = F.relu(self.bn2(self.fc2(x)))
        x = self.dropout(x)
        x = self.fc3(x)
        return x

# ========== 准备数据 ==========
# 转换为Tensor
X_train_tensor = torch.FloatTensor(X_train)
y_train_tensor = torch.LongTensor(y_train)
X_test_tensor = torch.FloatTensor(X_test)
y_test_tensor = torch.LongTensor(y_test)

# 创建Dataset和DataLoader
train_dataset = TensorDataset(X_train_tensor, y_train_tensor)
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)

test_dataset = TensorDataset(X_test_tensor, y_test_tensor)
test_loader = DataLoader(test_dataset, batch_size=64, shuffle=False)

# ========== 初始化 ==========
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model = SimpleNet(input_size=X_train.shape[1], hidden_size=128, num_classes=10).to(device)

criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

# ========== 训练 ==========
n_epochs = 20
best_acc = 0

for epoch in range(n_epochs):
    # 训练模式
    model.train()
    train_loss = 0
    train_correct = 0
    
    for batch_X, batch_y in train_loader:
        batch_X, batch_y = batch_X.to(device), batch_y.to(device)
        
        # 前向传播
        outputs = model(batch_X)
        loss = criterion(outputs, batch_y)
        
        # 反向传播
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        
        # 统计
        train_loss += loss.item()
        pred = outputs.argmax(dim=1)
        train_correct += pred.eq(batch_y).sum().item()
    
    # 评估模式
    model.eval()
    test_loss = 0
    test_correct = 0
    
    with torch.no_grad():
        for batch_X, batch_y in test_loader:
            batch_X, batch_y = batch_X.to(device), batch_y.to(device)
            outputs = model(batch_X)
            loss = criterion(outputs, batch_y)
            
            test_loss += loss.item()
            pred = outputs.argmax(dim=1)
            test_correct += pred.eq(batch_y).sum().item()
    
    # 打印结果
    train_acc = 100. * train_correct / len(train_loader.dataset)
    test_acc = 100. * test_correct / len(test_loader.dataset)
    
    print(f'Epoch {epoch+1}/{n_epochs}:')
    print(f'  Train Loss: {train_loss/len(train_loader):.4f}, Acc: {train_acc:.2f}%')
    print(f'  Test Loss: {test_loss/len(test_loader):.4f}, Acc: {test_acc:.2f}%')
    
    # 保存最佳模型
    if test_acc > best_acc:
        best_acc = test_acc
        torch.save(model.state_dict(), 'best_model.pth')
        print(f'  ✓ 保存最佳模型(准确率: {best_acc:.2f}%)')

print(f'\n训练完成!最佳测试准确率: {best_acc:.2f}%')


4.9 集成学习深入

4.9.1 什么是集成学习?
Ensemble Learning
组合多个模型的预测,得到更好的结果

核心思想:"三个臭皮匠,顶个诸葛亮"

分类:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. Bagging
   - 并行训练多个模型
   - 数据采样(有放回)
   - 投票/平均
   - 代表:随机森林

2. Boosting
   - 串行训练多个模型
   - 逐步纠正错误
   - 加权组合
   - 代表:AdaBoost、XGBoost、LightGBM

3. Stacking
   - 分层结构
   - 用模型预测作为新特征
   - 元学习器综合决策
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
4.9.2 Boosting详解

AdaBoost(自适应提升)

核心思想:
关注被分错的样本,逐步提升

算法流程:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
初始化:所有样本权重相同

迭代(T轮):
  1. 训练弱分类器(如浅决策树)
  2. 计算分类器权重(准确率高→权重大)
  3. 更新样本权重:
     - 分错的样本:权重增加 ↑
     - 分对的样本:权重减少 ↓
  4. 下一轮关注权重大的样本

最终模型 = 加权组合所有弱分类器
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

可视化:

第1轮:
样本:● ● ● ● ○ ○ ○ ○
权重:均等(都是0.125)

训练弱分类器1:
      分类线
      ↓
● ● ● | ● ○ ○ ○ ○
正确   错误!
       ↑
     权重增加

第2轮:
样本:● ● ● ● ○ ○ ○ ○
权重:0.1 0.1 0.1 0.3 0.1 0.1 0.1 0.1
                 ↑ 被分错的权重大

训练弱分类器2:
重点关注权重大的样本
...

最终组合:
Final = α₁×弱分类器1 + α₂×弱分类器2 + ...

代码示例

from sklearn.ensemble import AdaBoostClassifier
from sklearn.tree import DecisionTreeClassifier

# 基学习器(弱分类器)
base_estimator = DecisionTreeClassifier(max_depth=1)  # 决策树桩

# AdaBoost
adaboost = AdaBoostClassifier(
    base_estimator=base_estimator,
    n_estimators=50,      # 50个弱分类器
    learning_rate=1.0,    # 学习率
    random_state=42
)

adaboost.fit(X_train, y_train)

# 查看每个弱分类器的权重
print("弱分类器权重:")
print(adaboost.estimator_weights_[:10])  # 前10个

# 准确率
print(f"训练集准确率: {adaboost.score(X_train, y_train):.4f}")
print(f"测试集准确率: {adaboost.score(X_test, y_test):.4f}")

Gradient Boosting(梯度提升)

核心思想:
每次拟合前一轮的残差(梯度方向)

算法流程:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 初始预测:F₀(x) = 平均值

2. for t = 1 to T:
   
   a. 计算残差(梯度):
      rₜᵢ = yᵢ - Fₜ₋₁(xᵢ)
   
   b. 训练树拟合残差:
      hₜ(x) ≈ rₜ
   
   c. 更新模型:
      Fₜ(x) = Fₜ₋₁(x) + η·hₜ(x)
      
      η是学习率(防止过拟合)

3. 最终模型:
   F(x) = F₀(x) + η·Σhₜ(x)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

示例:
真实值 y = [10, 8, 12]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第1轮:
初始预测 F₀ = 10(平均值)
残差 r₁ = [0, -2, 2]

训练树1拟合残差:
h₁预测 = [0, -1.8, 1.9]

更新:F₁ = 10 + 0.1×h₁
     = [10, 9.82, 10.19]

第2轮:
残差 r₂ = [0, -1.82, 1.81]
训练树2拟合残差...

逐步逼近真实值!
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

代码示例

from sklearn.ensemble import GradientBoostingClassifier, GradientBoostingRegressor

# 分类
gb_clf = GradientBoostingClassifier(
    n_estimators=100,      # 树的数量
    learning_rate=0.1,     # 学习率(η)
    max_depth=3,           # 每棵树的深度
    min_samples_split=5,
    random_state=42
)

gb_clf.fit(X_train, y_train)

# 查看训练过程
train_scores = gb_clf.train_score_
print(f"训练误差变化: {train_scores[:10]}")  # 前10轮

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

# 部分依赖图(Partial Dependence)
from sklearn.inspection import plot_partial_dependence

fig, ax = plt.subplots(figsize=(12, 4))
plot_partial_dependence(
    gb_clf, X_train, features=[0, 1, 2],
    feature_names=feature_names,
    ax=ax
)
plt.tight_layout()
plt.savefig('partial_dependence.png', dpi=150)
4.9.3 XGBoost(极限梯度提升)
XGBoost = eXtreme Gradient Boosting
Kaggle竞赛神器!

改进点:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 正则化
   - 防止过拟合
   - L1/L2正则化

2. 并行化
   - 特征选择并行
   - 训练加速

3. 树剪枝
   - 深度优先剪枝
   - 更高效

4. 处理缺失值
   - 自动学习缺失值的最优分裂方向

5. 列采样
   - 类似随机森林
   - 减少过拟合

6. 内置交叉验证
   - 方便调参
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

代码示例

import xgboost as xgb
from sklearn.metrics import accuracy_score

# ========== 使用Scikit-learn API ==========
from xgboost import XGBClassifier, XGBRegressor

# 分类
xgb_clf = XGBClassifier(
    n_estimators=100,
    max_depth=5,
    learning_rate=0.1,
    subsample=0.8,           # 行采样比例
    colsample_bytree=0.8,    # 列采样比例
    gamma=0,                 # 最小分裂损失
    reg_alpha=0,             # L1正则化
    reg_lambda=1,            # L2正则化
    random_state=42
)

# 训练(带Early Stopping)
xgb_clf.fit(
    X_train, y_train,
    eval_set=[(X_test, y_test)],
    early_stopping_rounds=10,  # 10轮不提升就停
    verbose=True
)

# 预测
y_pred = xgb_clf.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
print(f"\n准确率: {accuracy:.4f}")

# 特征重要性(3种类型)
print("\n特征重要性:")
for importance_type in ['weight', 'gain', 'cover']:
    print(f"\n{importance_type}:")
    xgb_clf.get_booster().feature_names = feature_names
    importance = xgb_clf.get_booster().get_score(importance_type=importance_type)
    for feat, score in sorted(importance.items(), key=lambda x: x[1], reverse=True)[:5]:
        print(f"  {feat}: {score:.4f}")

# 可视化决策树(第1棵)
import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(20, 10))
xgb.plot_tree(xgb_clf, num_trees=0, ax=ax)
plt.savefig('xgboost_tree_0.png', dpi=150, bbox_inches='tight')

# 可视化特征重要性
fig, ax = plt.subplots(figsize=(10, 6))
xgb.plot_importance(xgb_clf, ax=ax, max_num_features=10)
plt.tight_layout()
plt.savefig('xgboost_importance.png', dpi=150)

# ========== 调参 ==========
from sklearn.model_selection import GridSearchCV

param_grid = {
    'max_depth': [3, 5, 7],
    'learning_rate': [0.01, 0.1, 0.3],
    'n_estimators': [50, 100, 200],
    'subsample': [0.6, 0.8, 1.0],
    'colsample_bytree': [0.6, 0.8, 1.0]
}

grid_search = GridSearchCV(
    XGBClassifier(random_state=42),
    param_grid,
    cv=5,
    scoring='accuracy',
    n_jobs=-1,
    verbose=1
)

grid_search.fit(X_train, y_train)

print(f"\n最佳参数: {grid_search.best_params_}")
print(f"最佳得分: {grid_search.best_score_:.4f}")

# ========== 使用原生API(更灵活)==========
# 转换数据格式
dtrain = xgb.DMatrix(X_train, label=y_train)
dtest = xgb.DMatrix(X_test, label=y_test)

# 参数字典
params = {
    'max_depth': 5,
    'eta': 0.1,              # learning_rate
    'objective': 'binary:logistic',  # 二分类
    'eval_metric': 'logloss'
}

# 训练
evallist = [(dtrain, 'train'), (dtest, 'eval')]
num_round = 100

bst = xgb.train(
    params,
    dtrain,
    num_round,
    evallist,
    early_stopping_rounds=10,
    verbose_eval=10
)

# 预测
y_pred_proba = bst.predict(dtest)
y_pred = (y_pred_proba > 0.5).astype(int)

print(f"\n准确率: {accuracy_score(y_test, y_pred):.4f}")

# 保存模型
bst.save_model('xgboost_model.json')
4.9.4 LightGBM
LightGBM(Light Gradient Boosting Machine)
微软开源,更快的GBDT实现

创新点:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 基于直方图的算法
   - 连续值离散化成bins
   - 加速分裂点查找

2. 带深度限制的Leaf-wise生长
   - 不是逐层生长
   - 而是选损失减少最大的叶子分裂

3. 直接支持类别特征
   - 不需要One-Hot编码

4. 优化并行和缓存
   - 更快的训练速度

5. 大数据集友好
   - 内存占用小
   - 速度快
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

XGBoost vs LightGBM:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
              XGBoost   LightGBM
速度          较快      更快
内存          较大      更小
小数据集      更好      一般
大数据集      较慢      更好
类别特征      需编码    原生支持
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

代码示例

import lightgbm as lgb
from sklearn.metrics import accuracy_score

# ========== 使用Scikit-learn API ==========
from lightgbm import LGBMClassifier, LGBMRegressor

lgb_clf = LGBMClassifier(
    n_estimators=100,
    max_depth=5,
    learning_rate=0.1,
    num_leaves=31,           # 叶子数(LightGBM特有)
    subsample=0.8,
    colsample_bytree=0.8,
    random_state=42,
    n_jobs=-1
)

# 训练(带Early Stopping)
lgb_clf.fit(
    X_train, y_train,
    eval_set=[(X_test, y_test)],
    early_stopping_rounds=10,
    verbose=20
)

# 预测
y_pred = lgb_clf.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
print(f"\n准确率: {accuracy:.4f}")

# 特征重要性
import matplotlib.pyplot as plt

lgb.plot_importance(lgb_clf, max_num_features=10, figsize=(10, 6))
plt.title('LightGBM Feature Importance')
plt.tight_layout()
plt.savefig('lightgbm_importance.png', dpi=150)

# ========== 使用原生API ==========
# 创建数据集
train_data = lgb.Dataset(X_train, label=y_train)
test_data = lgb.Dataset(X_test, label=y_test, reference=train_data)

# 参数
params = {
    'objective': 'binary',
    'metric': 'binary_logloss',
    'boosting_type': 'gbdt',
    'num_leaves': 31,
    'learning_rate': 0.05,
    'feature_fraction': 0.8,
    'bagging_fraction': 0.8,
    'bagging_freq': 5,
    'verbose': -1
}

# 训练
gbm = lgb.train(
    params,
    train_data,
    num_boost_round=100,
    valid_sets=[test_data],
    early_stopping_rounds=10,
    verbose_eval=10
)

# 预测
y_pred_proba = gbm.predict(X_test)
y_pred = (y_pred_proba > 0.5).astype(int)

print(f"\n准确率: {accuracy_score(y_test, y_pred):.4f}")

# 保存模型
gbm.save_model('lightgbm_model.txt')
4.9.5 CatBoost
CatBoost(Categorical Boosting)
Yandex开源,专注类别特征

特点:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 原生支持类别特征
   - 自动处理,无需手动编码
   - 效果好

2. 有序提升(Ordered Boosting)
   - 减少过拟合
   - 提高泛化能力

3. 对称树
   - 平衡的树结构
   - 预测快

4. GPU加速
   - 支持GPU训练
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

代码示例

from catboost import CatBoostClassifier, Pool

# 指定类别特征(序号或名称)
cat_features = [0, 2]  # 第0和第2列是类别特征

# 创建模型
cat_clf = CatBoostClassifier(
    iterations=100,
    learning_rate=0.1,
    depth=6,
    cat_features=cat_features,  # 指定类别特征
    random_seed=42,
    verbose=20
)

# 训练
cat_clf.fit(
    X_train, y_train,
    eval_set=(X_test, y_test),
    early_stopping_rounds=10,
    plot=True  # 绘制训练曲线
)

# 预测
y_pred = cat_clf.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
print(f"\n准确率: {accuracy:.4f}")

# 特征重要性
feature_importance = cat_clf.get_feature_importance()
print("\n特征重要性:")
for name, importance in zip(feature_names, feature_importance):
    print(f"  {name}: {importance:.4f}")
4.9.6 Stacking(堆叠)
Stacking:多层模型组合

结构:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
原始特征 X
    ↓
┌────────────────────────────┐
│      第1层(基学习器)      │
├────────────────────────────┤
│  模型1    模型2    模型3    │
│  (RF)    (SVM)    (XGB)    │
│   ↓        ↓        ↓       │
│  预测1    预测2    预测3    │
└────────────────────────────┘
    ↓        ↓        ↓
  [预测1, 预测2, 预测3] ← 新特征
           ↓
┌────────────────────────────┐
│      第2层(元学习器)      │
├────────────────────────────┤
│     逻辑回归/神经网络       │
│           ↓                 │
│       最终预测              │
└────────────────────────────┘
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

优点:
✅ 结合多个模型优势
✅ 通常性能最好

缺点:
❌ 复杂,训练慢
❌ 容易过拟合

代码示例

from sklearn.ensemble import StackingClassifier
from sklearn.linear_model import LogisticRegression

# 基学习器
estimators = [
    ('rf', RandomForestClassifier(n_estimators=100, random_state=42)),
    ('svm', SVC(kernel='rbf', probability=True)),
    ('xgb', XGBClassifier(n_estimators=100, random_state=42))
]

# 元学习器
meta_learner = LogisticRegression()

# Stacking
stacking = StackingClassifier(
    estimators=estimators,
    final_estimator=meta_learner,
    cv=5,  # 交叉验证生成新特征
    n_jobs=-1
)

# 训练
stacking.fit(X_train, y_train)

# 预测
y_pred = stacking.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)
print(f"Stacking准确率: {accuracy:.4f}")

# 对比单个模型
print("\n单个模型性能:")
for name, model in estimators:
    model.fit(X_train, y_train)
    acc = model.score(X_test, y_test)
    print(f"  {name}: {acc:.4f}")

print(f"\nStacking: {accuracy:.4f}")  # 通常最好
4.9.7 Voting(投票)
Voting:简单的集成方法

硬投票(Hard Voting):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
多数投票

例子:
模型1预测:类别A
模型2预测:类别A
模型3预测:类别B

最终:类别A(2票)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

软投票(Soft Voting):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
平均概率

例子:
模型1:P(A)=0.9, P(B)=0.1
模型2:P(A)=0.8, P(B)=0.2
模型3:P(A)=0.3, P(B)=0.7

平均:P(A)=0.67, P(B)=0.33

最终:类别A(概率更高)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

代码示例

from sklearn.ensemble import VotingClassifier

# 创建多个模型
clf1 = LogisticRegression(max_iter=1000)
clf2 = RandomForestClassifier(n_estimators=100, random_state=42)
clf3 = SVC(kernel='rbf', probability=True)

# 硬投票
voting_hard = VotingClassifier(
    estimators=[('lr', clf1), ('rf', clf2), ('svm', clf3)],
    voting='hard'
)

voting_hard.fit(X_train, y_train)
hard_acc = voting_hard.score(X_test, y_test)

# 软投票
voting_soft = VotingClassifier(
    estimators=[('lr', clf1), ('rf', clf2), ('svm', clf3)],
    voting='soft'
)

voting_soft.fit(X_train, y_train)
soft_acc = voting_soft.score(X_test, y_test)

print(f"硬投票准确率: {hard_acc:.4f}")
print(f"软投票准确率: {soft_acc:.4f}")

# 对比单个模型
for name, clf in [('Logistic', clf1), ('RF', clf2), ('SVM', clf3)]:
    clf.fit(X_train, y_train)
    print(f"{name}: {clf.score(X_test, y_test):.4f}")

附录 B:数学公式速查表

B.1 基础公式

【梯度下降】
w = w - α·∇L(w)

【均方误差(MSE)】
MSE = (1/n)Σ(yᵢ - ŷᵢ)²

【交叉熵损失】
L = -Σyᵢ·log(ŷᵢ)

【Sigmoid函数】
σ(x) = 1/(1 + e^(-x))

【Softmax函数】
softmax(xᵢ) = e^(xᵢ)/Σⱼe^(xⱼ)

【ReLU】
ReLU(x) = max(0, x)

B.2 评估指标

【分类】
Accuracy = (TP+TN)/(TP+TN+FP+FN)
Precision = TP/(TP+FP)
Recall = TP/(TP+FN)
F1 = 2·Precision·Recall/(Precision+Recall)

【回归】
MSE = (1/n)Σ(yᵢ-ŷᵢ)²
RMSE = √MSE
MAE = (1/n)Σ|yᵢ-ŷᵢ|
R² = 1 - SS_res/SS_tot

附录 C:术语中英对照表

中文 英文 缩写
机器学习 Machine Learning ML
深度学习 Deep Learning DL
人工智能 Artificial Intelligence AI
监督学习 Supervised Learning -
无监督学习 Unsupervised Learning -
强化学习 Reinforcement Learning RL
神经网络 Neural Network NN
卷积神经网络 Convolutional Neural Network CNN
循环神经网络 Recurrent Neural Network RNN
长短期记忆网络 Long Short-Term Memory LSTM
支持向量机 Support Vector Machine SVM
决策树 Decision Tree DT
随机森林 Random Forest RF
梯度提升 Gradient Boosting GB
K近邻 K-Nearest Neighbors KNN
主成分分析 Principal Component Analysis PCA
反向传播 Backpropagation BP
梯度下降 Gradient Descent GD
交叉验证 Cross Validation CV
过拟合 Overfitting -
欠拟合 Underfitting -
正则化 Regularization -
批归一化 Batch Normalization BN

附录 D:推荐阅读清单

本目录下的相关文档

进阶学习推荐阅读顺序:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. ✓ Machine_Learning_Guide_for_Beginners.md
   (本文档)

2. → Transformer完全指南_小白友好版.md
   深入学习Transformer和注意力机制

3. → 图神经网络GNN完全指南.md
   学习处理图结构数据

4. → Attention_Is_All_You_Need_论文精读.md
   精读AI领域最重要的论文之一

5. → AI运行环境搭建完全指南.md
   搭建完整的开发环境

6. → DCVC-RT论文详解_通俗版.md
   了解视频压缩中的深度学习应用
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

附录 E:高级主题速览

E.1 自动机器学习 (AutoML)

AutoML:自动化机器学习流程

核心:自动完成:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 数据预处理
2. 特征工程
3. 模型选择
4. 超参数调优
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

主流工具:
- Auto-sklearn
- H2O AutoML
- TPOT
- AutoKeras(神经网络)
- Google Cloud AutoML

代码示例:
```python
from autosklearn.classification import AutoSklearnClassifier

# 自动机器学习
automl = AutoSklearnClassifier(
    time_left_for_this_task=3600,  # 1小时
    per_run_time_limit=300,         # 每个模型最多5分钟
    memory_limit=3072                # 内存限制
)

automl.fit(X_train, y_train)

# 查看找到的最佳模型
print(automl.show_models())

# 预测
y_pred = automl.predict(X_test)

E.2 模型解释性 (Explainable AI)

为什么需要解释性?
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 信任:理解模型决策依据
2. 调试:发现模型问题
3. 合规:医疗、金融等领域要求
4. 改进:指导特征工程
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

方法:
1. SHAP (SHapley Additive exPlanations)
2. LIME (Local Interpretable Model-agnostic Explanations)
3. Permutation Importance
4. Partial Dependence Plots

SHAP示例

import shap

# 训练模型
model = XGBClassifier()
model.fit(X_train, y_train)

# 创建SHAP解释器
explainer = shap.TreeExplainer(model)
shap_values = explainer.shap_values(X_test)

# 可视化
# 1. 单个样本解释
shap.force_plot(
    explainer.expected_value,
    shap_values[0],
    X_test.iloc[0],
    feature_names=X.columns
)

# 2. 特征重要性
shap.summary_plot(shap_values, X_test, plot_type="bar")

# 3. 依赖图
shap.dependence_plot("age", shap_values, X_test)

# 4. 决策图
shap.decision_plot(
    explainer.expected_value,
    shap_values[0],
    X_test.iloc[0],
    feature_names=X.columns
)

E.3 模型压缩与加速

目标:让模型更小、更快

方法:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 剪枝 (Pruning)
   - 去除不重要的权重/神经元
   - 减少参数量

2. 量化 (Quantization)
   - FP32 → INT8
   - 模型大小减少4倍
   - 速度提升2-4倍

3. 知识蒸馏 (Knowledge Distillation)
   - 大模型(教师)教小模型(学生)
   - 保留性能,减少参数

4. 神经架构搜索 (NAS)
   - 自动设计网络结构
   - 找到高效架构
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

示例:模型量化
```python
import torch

# 训练好的模型
model = trained_model

# 动态量化(推理时自动)
quantized_model = torch.quantization.quantize_dynamic(
    model,
    {torch.nn.Linear},  # 量化线性层
    dtype=torch.qint8    # INT8
)

# 对比
print(f"原模型大小: {os.path.getsize('model.pth') / 1024:.2f} KB")
torch.save(quantized_model.state_dict(), 'quantized_model.pth')
print(f"量化后大小: {os.path.getsize('quantized_model.pth') / 1024:.2f} KB")

# 速度对比
import time

# 原模型
start = time.time()
for _ in range(1000):
    _ = model(test_input)
original_time = time.time() - start

# 量化模型
start = time.time()
for _ in range(1000):
    _ = quantized_model(test_input)
quantized_time = time.time() - start

print(f"\n原模型推理时间: {original_time:.4f}秒")
print(f"量化模型推理时间: {quantized_time:.4f}秒")
print(f"加速比: {original_time / quantized_time:.2f}x")

E.4 联邦学习 (Federated Learning)

隐私保护的分布式机器学习

场景:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
多个医院想合作训练疾病诊断模型
但病人数据不能共享(隐私)

传统方法:
医院1、2、3 → 汇总数据 → 中央服务器训练
问题:数据泄露风险!

联邦学习:
医院1 → 本地训练 → 上传模型参数
医院2 → 本地训练 → 上传模型参数
医院3 → 本地训练 → 上传模型参数
       ↓
中央服务器 → 聚合参数 → 全局模型
       ↓
下发给各医院

优点:
✅ 数据不出本地(隐私)
✅ 利用分布式数据
✅ 减少通信(只传参数)

应用:
- 手机输入法(Google Gboard)
- 医疗数据分析
- 金融风控
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

E.5 元学习 (Meta-Learning)

Meta-Learning:"学会学习"

核心思想:
不是学习某个具体任务
而是学习"如何快速学习新任务"

应用:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
少样本学习(Few-Shot Learning):
- 只给5张图片,学会识别新物体
- 类似人类的学习方式

例子:
训练时:见过猫、狗、鸟(每类1000张)
测试时:给5张"熊猫"的图片
目标:立即学会识别熊猫!

经典算法:
- MAML (Model-Agnostic Meta-Learning)
- Prototypical Networks
- Matching Networks
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

E.6 图神经网络 (GNN)

处理图结构数据

详细内容请参考:
📄 图神经网络GNN完全指南.md

核心概念:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 图的表示
   - 节点(Node)
   - 边(Edge)

2. 消息传递
   - 节点聚合邻居信息
   - 逐层传播

3. 应用
   - 社交网络分析
   - 推荐系统
   - 分子性质预测
   - 交通预测
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

主流架构:
- GCN(图卷积网络)
- GraphSAGE
- GAT(图注意力网络)

E.7 生成模型

生成式AI:创造新内容

分类:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. GAN (Generative Adversarial Network)
   
   生成器 vs 判别器
   对抗训练
   
   应用:
   - 图像生成(StyleGAN)
   - 图像翻译(Pix2Pix)
   - 超分辨率

2. VAE (Variational Autoencoder)
   
   编码器-解码器结构
   潜在空间连续
   
   应用:
   - 图像生成
   - 数据压缩
   - 异常检测

3. Diffusion Model
   
   逐步去噪
   SOTA图像生成
   
   应用:
   - Stable Diffusion
   - DALL-E 2
   - Midjourney

4. 自回归模型
   
   逐步生成
   
   应用:
   - GPT(文本生成)
   - PixelCNN(图像)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

附录 F:面试常见问题

F.1 算法原理类

Q: 解释偏差-方差权衡 (Bias-Variance Trade-off)

答:

偏差(Bias):
- 模型的预测值与真实值的差距
- 高偏差 = 欠拟合
- 模型太简单,学不到规律

方差(Variance):
- 模型在不同数据集上预测的变化程度
- 高方差 = 过拟合
- 模型太复杂,对训练数据过度敏感

可视化:
    总误差
      │
      │    ╱ ← 方差
      │  ╱
      │╱______ ← 偏差
      │╲
      │ ╲
      │  ╲
      └──────────── 模型复杂度
      简单        复杂

权衡:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
简单模型:高偏差、低方差(欠拟合)
复杂模型:低偏差、高方差(过拟合)
目标:找到最佳平衡点
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Q: L1和L2正则化的区别

答:

L1正则化(Lasso):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
损失 = MSE + α·Σ|wᵢ|

特点:
- 产生稀疏解(很多权重=0)
- 特征选择
- 鲁棒性强

权重分布:
[2.5, 0, 0.8, 0, 1.2, 0, 0, 3.1]
     ↑       ↑            ↑
  重要特征(其他置为0)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

L2正则化(Ridge):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
损失 = MSE + α·Σwᵢ²

特点:
- 权重倾向小值(但不为0)
- 保留所有特征
- 数值稳定

权重分布:
[1.2, 0.3, 0.8, 0.1, 0.9, 0.2, 0.1, 1.5]
 ↑
所有权重都小但非零
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

几何解释:
L1:菱形约束(容易碰到坐标轴→稀疏)
L2:圆形约束(权重均匀缩小)

选择:
特征冗余多 → L1
所有特征重要 → L2
不确定 → Elastic Net(L1+L2)

Q: Bagging和Boosting的区别

答:

┌─────────────────────────────────────────┐
│       Bagging vs Boosting               │
├─────────────────────────────────────────┤
│                                          │
│ 维度        Bagging      Boosting       │
│ ──────────  ─────────    ─────────      │
│ 训练方式    并行         串行            │
│ 样本权重    相同         动态调整        │
│ 基学习器    独立         有依赖          │
│ 目标        降低方差     降低偏差        │
│ 代表算法    随机森林     XGBoost         │
│ 过拟合风险  低           中              │
│ 训练速度    快(并行)   慢(串行)      │
│                                          │
└─────────────────────────────────────────┘

Bagging流程:
数据 → [采样] → 模型1 ╲
     → [采样] → 模型2 ─ 投票/平均 → 结果
     → [采样] → 模型3 ╱

Boosting流程:
数据 → 模型1 → 更新权重 → 模型2 → 更新权重 → 模型3 → 加权组合

适用场景:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
高方差(过拟合) → Bagging
高偏差(欠拟合) → Boosting
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

F.2 实战经验类

Q: 如何处理特征数量远大于样本数的情况?

答:

场景:基因数据
- 样本数:100个病人
- 特征数:20000个基因
- 特征数 >> 样本数

问题:
- 严重过拟合
- 维度灾难
- 计算困难

解决方法:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 特征选择
   
   方法:
   - 过滤法(Filter):卡方检验、互信息
   - 包装法(Wrapper):递归特征消除
   - 嵌入法(Embedded):Lasso、树模型

   代码:
   from sklearn.feature_selection import SelectKBest, chi2
   
   selector = SelectKBest(chi2, k=100)  # 选100个特征
   X_selected = selector.fit_transform(X, y)

2. 降维
   
   PCA降维:
   from sklearn.decomposition import PCA
   
   pca = PCA(n_components=50)
   X_reduced = pca.fit_transform(X)

3. 正则化
   
   L1正则化自动特征选择:
   from sklearn.linear_model import LassoCV
   
   lasso = LassoCV(cv=5)
   lasso.fit(X, y)
   
   # 非零权重的特征
   selected = np.where(lasso.coef_ != 0)[0]

4. 集成方法
   
   随机森林的随机特征选择:
   - 每次只用部分特征
   - 降低过拟合风险
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Q: 数据量很小(<100样本)怎么办?

答:

策略:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 数据增强
   
   图像:
   - 旋转、翻转、裁剪
   - 颜色抖动
   - 随机擦除
   
   文本:
   - 同义词替换
   - 回译(中→英→中)
   - 随机插入/删除

2. 迁移学习
   
   使用预训练模型:
   - ImageNet预训练(图像)
   - BERT预训练(文本)
   - 只需少量数据微调

3. 简单模型
   
   避免深度学习:
   - 用SVM、随机森林
   - 参数少,不易过拟合

4. 交叉验证
   
   K折交叉验证:
   - 充分利用数据
   - K=5或10

5. 正则化
   
   强正则化:
   - 大的L1/L2系数
   - Dropout(神经网络)
   - Early Stopping

6. 合成数据
   
   SMOTE:合成少数类样本
   GAN:生成新样本

7. 半监督学习
   
   利用无标签数据:
   - Self-Training
   - Co-Training
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Q: 模型在训练集上完美,测试集很差,怎么办?

答:

典型的过拟合!

诊断:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
训练集准确率:99%
测试集准确率:65%
差距:34%(太大!)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

解决步骤:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 检查数据泄露
   □ 测试集信息是否泄露到训练集?
   □ 特征是否包含未来信息?
   □ 数据划分是否正确?

2. 增加数据
   □ 收集更多训练样本
   □ 数据增强

3. 简化模型
   □ 减少层数/深度
   □ 减少参数量
   □ 特征选择/降维

4. 正则化
   □ 增大L1/L2系数
   □ Dropout(从0.3增加到0.5)
   □ Early Stopping

5. 交叉验证
   □ 5折交叉验证
   □ 确保结果稳定

6. 集成方法
   □ 随机森林/XGBoost
   □ 多个模型投票
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

代码:
# 检查学习曲线
from sklearn.model_selection import learning_curve

train_sizes, train_scores, val_scores = learning_curve(
    model, X, y, cv=5, scoring='accuracy'
)

# 如果训练和验证曲线差距大 → 过拟合

Q: GPU内存不足怎么办?

答:

常见错误:
RuntimeError: CUDA out of memory

解决方法:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 减小batch size
   
   从64 → 32 → 16
   内存占用减半

2. 梯度累积
   
   小batch多次累积 = 大batch
   
   代码:
   accumulation_steps = 4
   
   for i, (X, y) in enumerate(data_loader):
       loss = model(X, y) / accumulation_steps
       loss.backward()
       
       if (i + 1) % accumulation_steps == 0:
           optimizer.step()
           optimizer.zero_grad()

3. 混合精度训练
   
   FP16代替FP32
   内存减半,速度提升
   
   from torch.cuda.amp import autocast, GradScaler
   
   scaler = GradScaler()
   
   with autocast():
       loss = model(X)
   
   scaler.scale(loss).backward()
   scaler.step(optimizer)
   scaler.update()

4. 梯度检查点
   
   不保存中间激活值
   需要时重新计算
   
   import torch.utils.checkpoint as checkpoint
   
   output = checkpoint.checkpoint(layer, input)

5. 模型并行
   
   不同层放不同GPU
   
   model.layer1.to('cuda:0')
   model.layer2.to('cuda:1')

6. 减小模型
   
   - 减少层数
   - 减少隐藏层大小
   - 使用轻量级模型
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

F.3 项目经验类

Q: 如何构建机器学习项目的Pipeline?

答:

完整Pipeline示例:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.ensemble import RandomForestClassifier

# 构建Pipeline
pipeline = Pipeline([
    ('scaler', StandardScaler()),       # 步骤1:标准化
    ('pca', PCA(n_components=10)),     # 步骤2:降维
    ('classifier', RandomForestClassifier())  # 步骤3:分类
])

# 训练(自动执行所有步骤)
pipeline.fit(X_train, y_train)

# 预测(自动执行所有步骤)
y_pred = pipeline.predict(X_test)

# Pipeline + GridSearch
param_grid = {
    'pca__n_components': [5, 10, 20],
    'classifier__n_estimators': [50, 100, 200],
    'classifier__max_depth': [5, 10, None]
}

grid_search = GridSearchCV(pipeline, param_grid, cv=5)
grid_search.fit(X_train, y_train)

# 优点:
✅ 防止数据泄露(每折独立处理)
✅ 代码简洁
✅ 易于部署
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Q: 如何处理时间序列数据?

答:

时间序列特点:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- 时间依赖性
- 趋势性
- 季节性
- 不能随机打乱!
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

数据划分:
❌ 错误:随机划分
   可能用未来预测过去!

✅ 正确:时间序列划分
   训练集:2020-2022
   测试集:2023

方法1:滑动窗口
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
过去7天 → 预测第8天

时间:1  2  3  4  5  6  7  8  9
样本1:[1  2  3  4  5  6  7] → 8
样本2:   [2  3  4  5  6  7  8] → 9
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

方法2:时间特征工程
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
提取:
- 年、月、日、星期几
- 是否周末
- 是否节假日
- 季度
- sin/cos编码(周期性)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

方法3:专用模型
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
- ARIMA(统计方法)
- Prophet(Facebook)
- LSTM/GRU(深度学习)
- Transformer(最新)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

代码:
from sklearn.model_selection import TimeSeriesSplit

# 时间序列交叉验证
tscv = TimeSeriesSplit(n_splits=5)

for train_idx, test_idx in tscv.split(X):
    X_train, X_test = X[train_idx], X[test_idx]
    y_train, y_test = y[train_idx], y[test_idx]
    
    model.fit(X_train, y_train)
    score = model.score(X_test, y_test)
    print(f"Fold score: {score:.4f}")

Q: 如何评估推荐系统?

答:

推荐系统特殊指标:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 准确性指标

   Precision@K:
   前K个推荐中,相关的比例
   
   例:推荐10个商品
   用户喜欢3个
   Precision@10 = 3/10 = 30%

   Recall@K:
   所有相关项中,被推荐的比例
   
   例:用户实际喜欢20个商品
   推荐中命中3个
   Recall@10 = 3/20 = 15%

   MAP (Mean Average Precision):
   考虑推荐顺序的平均精确率

2. 排序指标

   NDCG (Normalized Discounted Cumulative Gain):
   - 考虑相关性程度(不只是0/1)
   - 考虑位置(排前面的更重要)

3. 多样性指标

   Coverage:
   推荐覆盖多少商品
   
   Diversity:
   推荐的商品是否多样化

4. 业务指标

   - 点击率 (CTR)
   - 转化率
   - 用户满意度
   - 停留时间
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

代码:
def precision_at_k(recommendations, relevant_items, k=10):
    """计算Precision@K"""
    top_k = recommendations[:k]
    relevant_in_top_k = set(top_k) & set(relevant_items)
    return len(relevant_in_top_k) / k

def recall_at_k(recommendations, relevant_items, k=10):
    """计算Recall@K"""
    top_k = recommendations[:k]
    relevant_in_top_k = set(top_k) & set(relevant_items)
    return len(relevant_in_top_k) / len(relevant_items)

def ndcg_at_k(recommendations, relevances, k=10):
    """计算NDCG@K"""
    dcg = sum(
        rel / np.log2(idx + 2)
        for idx, rel in enumerate(relevances[:k])
    )
    
    ideal_relevances = sorted(relevances, reverse=True)
    idcg = sum(
        rel / np.log2(idx + 2)
        for idx, rel in enumerate(ideal_relevances[:k])
    )
    
    return dcg / idcg if idcg > 0 else 0

附录 G:常见数据集

G.1 经典数据集

┌─────────────────────────────────────────────────────┐
│              机器学习常用数据集                       │
├─────────────────────────────────────────────────────┤
│                                                      │
│ 【分类/回归】                                        │
│                                                      │
│ 1. Iris(鸢尾花)                                   │
│    - 样本数:150                                     │
│    - 特征数:4                                       │
│    - 类别数:3                                       │
│    - 用途:分类入门                                  │
│                                                      │
│ 2. Wine(葡萄酒)                                   │
│    - 样本数:178                                     │
│    - 特征数:13                                      │
│    - 类别数:3                                       │
│    - 用途:分类、特征选择                            │
│                                                      │
│ 3. Breast Cancer(乳腺癌)                          │
│    - 样本数:569                                     │
│    - 特征数:30                                      │
│    - 类别数:2(良性/恶性)                          │
│    - 用途:医疗诊断、二分类                          │
│                                                      │
│ 4. Boston Housing(波士顿房价)                     │
│    - 样本数:506                                     │
│    - 特征数:13                                      │
│    - 用途:回归入门                                  │
│    - 注意:现已弃用,用California Housing替代        │
│                                                      │
│ 5. California Housing(加州房价)                   │
│    - 样本数:20640                                   │
│    - 特征数:8                                       │
│    - 用途:回归                                      │
│                                                      │
│ 【图像】                                             │
│                                                      │
│ 6. MNIST(手写数字)                                │
│    - 样本数:70000(60000训练+10000测试)           │
│    - 图像大小:28×28灰度图                          │
│    - 类别数:10(0-9)                              │
│    - 用途:CNN入门                                  │
│                                                      │
│ 7. CIFAR-10(小图像)                               │
│    - 样本数:60000                                   │
│    - 图像大小:32×32彩色图                          │
│    - 类别数:10(飞机、汽车、鸟...)                │
│    - 用途:CNN进阶                                  │
│                                                      │
│ 8. ImageNet                                         │
│    - 样本数:1400万+                                │
│    - 类别数:1000                                    │
│    - 用途:预训练、迁移学习                          │
│                                                      │
│ 【文本】                                             │
│                                                      │
│ 9. IMDB(电影评论)                                 │
│    - 样本数:50000                                   │
│    - 任务:情感分类(正面/负面)                    │
│    - 用途:文本分类、情感分析                        │
│                                                      │
│ 10. 20 Newsgroups(新闻组)                         │
│     - 样本数:约20000                               │
│     - 类别数:20                                     │
│     - 用途:文本分类、主题分类                      │
│                                                      │
│ 【推荐系统】                                         │
│                                                      │
│ 11. MovieLens                                       │
│     - 用户:数万                                     │
│     - 电影:数万                                     │
│     - 评分:数百万                                   │
│     - 用途:协同过滤、推荐系统                      │
│                                                      │
│ 【竞赛】                                             │
│                                                      │
│ 12. Titanic(Kaggle)                              │
│     - 样本数:891                                    │
│     - 任务:生存预测                                │
│     - 用途:入门竞赛                                │
│                                                      │
│ 13. House Prices(Kaggle)                         │
│     - 任务:房价预测                                │
│     - 特征数:79                                     │
│     - 用途:特征工程练习                            │
│                                                      │
└─────────────────────────────────────────────────────┘

加载数据集代码

from sklearn import datasets

# Iris
iris = datasets.load_iris()
X, y = iris.data, iris.target

# Wine
wine = datasets.load_wine()

# Breast Cancer
cancer = datasets.load_breast_cancer()

# California Housing
housing = datasets.fetch_california_housing()

# MNIST(需要下载)
from sklearn.datasets import fetch_openml
mnist = fetch_openml('mnist_784', version=1)

# 或使用torchvision
from torchvision import datasets
mnist = datasets.MNIST('./data', download=True)

# CIFAR-10
cifar10 = datasets.CIFAR10('./data', download=True)

# IMDB
from tensorflow.keras.datasets import imdb
(X_train, y_train), (X_test, y_test) = imdb.load_data()

附录 H:调试技巧

H.1 常见错误和解决

错误1:维度不匹配
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
RuntimeError: size mismatch

原因:输入维度和模型不符

解决:
# 检查维度
print(f"输入形状: {X.shape}")
print(f"模型期望: {model.input_shape}")

# 调整输入或模型


错误2:数据类型不对
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
TypeError: can't convert np.ndarray of type numpy.object_

原因:数据中有字符串等非数值类型

解决:
# 检查数据类型
print(df.dtypes)

# 转换
df = df.astype(float)

# 或编码类别特征
le = LabelEncoder()
df['category'] = le.fit_transform(df['category'])


错误3:标签格式错误
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
ValueError: Unknown label type

原因:分类标签不是整数

解决:
# 标签编码
from sklearn.preprocessing import LabelEncoder

le = LabelEncoder()
y = le.fit_transform(y_original)


错误4:特征缩放忘记
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
SVM/KNN效果差

解决:
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)


错误5:数据泄露
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
测试集准确率100%(太好了,可疑!)

检查:
1. 是否用全部数据做了预处理?
2. 特征是否包含目标信息?
3. 时间序列是否打乱了?


错误6:过拟合
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
训练99%,测试60%

解决:
1. 增加数据
2. 简化模型
3. 正则化
4. 交叉验证
5. Early Stopping


错误7:欠拟合
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
训练和测试都只有60%

解决:
1. 增加特征
2. 用更复杂的模型
3. 减少正则化
4. 训练更久


错误8:类别不平衡
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
准确率99%但F1-Score很低

解决:
1. 重采样(SMOTE)
2. 调整权重(class_weight='balanced')
3. 换指标(F1、AUC)
4. 阈值调整


错误9:内存不足
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
MemoryError

解决:
1. 减小batch size
2. 使用生成器(逐批加载)
3. 降采样
4. 数据类型优化(float64→float32)


错误10:训练太慢
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
一个epoch要几小时

解决:
1. 使用GPU
2. 减少数据量(采样)
3. 简化模型
4. 并行计算(n_jobs=-1)
5. 使用更快的算法(LightGBM)

附录 I:实用技巧集锦

I.1 提升模型性能的10个技巧

1. 特征工程 ⭐⭐⭐⭐⭐
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 组合特征
   - 多项式特征
   - 领域知识
   
   效果:通常比调参更有效!

2. 数据增强
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 图像:旋转、翻转、裁剪
   - 文本:同义词替换
   - 增加数据多样性

3. 集成方法
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 多个模型投票
   - Stacking
   - 通常能提升2-5%

4. 超参数调优
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 网格搜索
   - 贝叶斯优化
   - 关注重要参数

5. 交叉验证
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 5折或10折
   - 确保结果可靠

6. 处理不平衡数据
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - SMOTE
   - class_weight
   - 换评估指标

7. 异常值处理
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - IQR方法
   - Z-Score
   - 鲁棒缩放

8. 特征选择
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 去除无用特征
   - 减少过拟合
   - 加快训练

9. 迁移学习
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   - 使用预训练模型
   - 小数据集救星

10. 正则化
    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    - L1/L2
    - Dropout
    - Early Stopping

I.2 加快训练速度的技巧

1. 使用GPU
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   深度学习:10-100倍加速
   
   代码:
   device = torch.device('cuda')
   model.to(device)

2. 并行计算
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   n_jobs=-1(使用所有CPU核心)
   
   RandomForestClassifier(n_jobs=-1)

3. 数据采样
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   大数据集:先用部分数据试验
   
   indices = np.random.choice(len(X), 10000)
   X_sample = X[indices]

4. 更快的算法
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   LinearSVC代替SVC
   LightGBM代替XGBoost
   SGDClassifier代替逻辑回归

5. 减小batch size
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   内存受限时
   batch_size: 128 → 64 → 32

6. 混合精度训练
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   FP16代替FP32
   速度提升50%+

7. 特征选择/降维
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   减少特征数量
   PCA降维

8. Early Stopping
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   验证集不再提升就停
   避免无用迭代

9. 预计算
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   缓存中间结果
   避免重复计算

10. 增大学习率
    ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
    从0.001 → 0.01
    配合warmup和decay

I.3 调试神经网络的Checklist

□ 数据检查
  □ 数据加载正确?
  □ 标签对应正确?
  □ 数据类型正确(float/long)?
  □ 数据范围合理(归一化了吗)?

□ 模型检查
  □ 输入输出维度对吗?
  □ 激活函数选对了吗?
  □ 损失函数选对了吗?
  □ 最后一层对吗(分类用Softmax)?

□ 训练检查
  □ Loss在下降吗?
  □ 梯度是否为NaN/Inf?
  □ 学习率合适吗?
  □ Batch size合适吗?

□ 过拟合检查
  □ 训练集和验证集差距大吗?
  □ 需要正则化吗?
  □ 需要Early Stopping吗?

□ 欠拟合检查
  □ 训练集性能够好吗?
  □ 模型够复杂吗?
  □ 特征够吗?

□ 性能检查
  □ 和baseline对比?
  □ 和SOTA对比?
  □ 还有提升空间吗?

I.4 代码规范和工程最佳实践

1. 项目结构
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   project/
   ├── data/
   │   ├── raw/           # 原始数据
   │   └── processed/     # 处理后数据
   ├── notebooks/         # Jupyter notebooks
   ├── src/
   │   ├── data/          # 数据处理
   │   ├── features/      # 特征工程
   │   ├── models/        # 模型定义
   │   └── utils/         # 工具函数
   ├── models/            # 保存的模型
   ├── reports/           # 结果报告
   ├── requirements.txt   # 依赖
   └── README.md

2. 代码规范
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   # 清晰的命名
   model_random_forest = RandomForestClassifier()  # ✓
   m = RandomForestClassifier()  # ✗
   
   # 模块化
   def preprocess_data(df):
       """数据预处理"""
       # 处理缺失值
       # 特征编码
       # 标准化
       return X, y
   
   # 配置文件
   config = {
       'model': 'random_forest',
       'n_estimators': 100,
       'max_depth': 10
   }

3. 版本控制
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   git init
   git add .
   git commit -m "Initial model"
   
   # .gitignore
   data/
   *.pyc
   __pycache__/
   models/*.pkl

4. 实验记录
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   使用MLflow/Wandb
   
   import mlflow
   
   with mlflow.start_run():
       mlflow.log_param("n_estimators", 100)
       mlflow.log_metric("accuracy", 0.95)
       mlflow.log_model(model, "model")

5. 文档
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   README.md包含:
   - 项目描述
   - 环境配置
   - 使用方法
   - 结果展示

6. 单元测试
   ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   import unittest
   
   class TestModel(unittest.TestCase):
       def test_prediction_shape(self):
           pred = model.predict(X_test)
           self.assertEqual(len(pred), len(y_test))
   
   if __name__ == '__main__':
       unittest.main()

附录 J:更多学习资源

J.1 YouTube频道

英文频道:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 3Blue1Brown
   - 数学可视化
   - 神经网络系列

2. StatQuest with Josh Starmer
   - 统计和ML概念讲解
   - 通俗易懂

3. Sentdex
   - Python + ML教程
   - 实战项目多

4. Two Minute Papers
   - 最新论文解读
   - 2分钟快速了解

中文频道:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 李宏毅(B站)
   - 机器学习课程
   - 中文最佳

2. 跟李沐学AI(B站)
   - 论文精读
   - 动手学深度学习作者

3. AI科技评论
   - 最新AI资讯
   - 论文解读

J.2 博客和网站

英文:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. Towards Data Science (Medium)
   - 最大ML社区
   - 各种教程和实战

2. Machine Learning Mastery
   - 系统化教程
   - 代码示例丰富

3. Distill.pub
   - 可视化论文
   - 质量极高

4. Fast.ai Blog
   - 实践经验分享

中文:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. 机器之心
   - AI新闻
   - 论文解读

2. 知乎 - 机器学习话题
   - 问答社区
   - 经验分享

3. CSDN博客
   - 代码教程
   - 问题解答

J.3 GitHub优秀项目

必看项目:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
1. scikit-learn
   github.com/scikit-learn/scikit-learn
   - 经典ML库
   - 代码规范

2. PyTorch
   github.com/pytorch/pytorch
   - 深度学习框架

3. TensorFlow
   github.com/tensorflow/tensorflow

4. Hugging Face Transformers
   github.com/huggingface/transformers
   - NLP必备

5. fastai
   github.com/fastai/fastai
   - 高层API

6. d2l-ai(动手学深度学习)
   github.com/d2l-ai/d2l-zh
   - 中文教程

7. Machine Learning Yearning
   github.com/ajaymache/machine-learning-yearning
   - Andrew Ng的实战指南

8. Awesome Machine Learning
   github.com/josephmisiti/awesome-machine-learning
   - 资源大全

9. ML From Scratch
   github.com/eriklindernoren/ML-From-Scratch
   - 从零实现各种算法

10. Papers with Code
    github.com/paperswithcode
    - 论文+代码复现

附录 K:数学基础补充

K.1 线性代数核心概念

向量(Vector):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
v = [1, 2, 3]  (一维数组)

向量运算:
- 加法:[1,2] + [3,4] = [4,6]
- 数乘:2×[1,2] = [2,4]
- 点积:[1,2]·[3,4] = 1×3 + 2×4 = 11

矩阵(Matrix):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
A = [[1, 2],
     [3, 4]]  (二维数组)

矩阵运算:
- 转置:A^T
- 乘法:A×B
- 逆矩阵:A^(-1)

在ML中的应用:
- 特征矩阵 X:[样本数, 特征数]
- 权重矩阵 W:[输入维度, 输出维度]
- y = X·W  (线性变换)

K.2 概率论核心概念

概率(Probability):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
P(A):事件A发生的概率
范围:[0, 1]

条件概率:
P(A|B):在B发生的条件下,A发生的概率

贝叶斯定理:
P(A|B) = P(B|A) × P(A) / P(B)

期望(Expected Value):
E[X] = Σ xᵢ·P(xᵢ)  (加权平均)

方差(Variance):
Var(X) = E[(X - E[X])²]  (离散程度)

正态分布(Gaussian):
N(μ, σ²)
- μ:均值
- σ²:方差

在ML中的应用:
- 朴素贝叶斯:贝叶斯定理
- 高斯分布:很多算法假设数据服从正态分布
- 最大似然估计:参数估计

K.3 微积分核心概念

导数(Derivative):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
f'(x) = df/dx
表示:函数在x点的变化率

几何意义:切线斜率

常用导数:
- (x^n)' = n·x^(n-1)
- (e^x)' = e^x
- (ln x)' = 1/x
- (sin x)' = cos x

链式法则(重要!):
(f(g(x)))' = f'(g(x)) × g'(x)

在ML中的应用:
- 梯度下降:∂L/∂w
- 反向传播:链式法则
- 优化:找最小值(导数=0)

梯度(Gradient):
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
多元函数的导数(向量)

∇f = [∂f/∂x₁, ∂f/∂x₂, ..., ∂f/∂xₙ]

指向:函数增长最快的方向
梯度下降:沿负梯度方向(减小)

最后的最后

机器学习是一场马拉松,不是百米冲刺。

不要期望一周就精通,但也不要怀疑自己的能力。

每天进步一点点,一年后你会惊讶于自己的成长。

现在,关掉这个文档,打开Jupyter Notebook,

敲下你的第一行机器学习代码吧!

💻 import sklearn

你的AI之旅,从这里开始!🚀


🎉祝你天天开心,我将更新更多有意思的内容,欢迎关注!

最后更新:2025年11月
作者:Echo

Logo

DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。

更多推荐