【机器学习】手把手带你从零开始学机器学习(超详细),十万字篇幅拉满,一般人看不完!
机器学习零基础完全指南 🤖
📚 适用人群:零基础小白、刚入门的大学生、想转行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
最佳特征 = 特征
# 创建节点
节点 = 内部节点(最佳特征)
# 对每个特征值递归构建子树
for 值 in 最佳特征的所有可能值:
子集 = 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
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐
所有评论(0)