机器学习🤖
智能
人工智能发展:
- 1956年:“人工智能”(Ariticial Intelligence)概念正式提出。
- 1997年:IBM深蓝依靠强大算力击败国际象棋冠军。
- 2016年:DeepMind AlphaGo通过策略网络+强化学习击败人类围棋冠军。
- 2020年:广泛落地于人脸识别、智能驾驶等领域。
⚠️局限性:专用人工智能(ANI)
- 它们在特定任务上表现卓越,但很难跨任务、跨场景迁移能力。
什么是智能?
智能的现象:举一反三
- 👶孩子与开水:被烫过一次后,不仅避开那个杯子,还会警惕所有冒热气的物体。不需要把“所有会烫的东西”都试一遍,就能学会避险。
这种“从有限经验中抽取规律,并在陌生环境中做出正确判断”的能力,被称为泛化(Generalization)。
智能 = 学习能力 + 迁移能力
通用人工智能(AGI):一种具备类人理解、推理与学习能力,并能在不同领域中解决问题的系统。
仿生学,学习人的智能:
认识苹果的过程(见多识广,举一反三)
在反复见过无数次苹果后,大脑自动从海量样本中:
- 提取特征:轮廓的弧度、表皮的光泽、果柄的位置、切面的纹理...
- 迁移学习:面对从未见过的品种(如深紫色、扁平的),依然能瞬间认出。
神经元的连接在不断的刺激中自行调整,最终在脑海中形成了一个模糊却精准的概念————“苹果”。
这就是现代AI模仿的学习方式。
AI发展的两条路线
路线1
规则驱动(符号主义):早期AI走的是“专家系统”路线,工程师以“上帝视角”写代码定义世界。
失败原因:规则写不完,被咬了一口、烂了一半、光影变化,这些算不算苹果?
路线2
学习驱动(联结主义):让机器像人一样去“看”、去“学”,1959年计算机科学家亚瑟·塞缪尔定义机器学习(Machine learning is the field of study that gives computers the ability to learn without being explicitly programmed.)
成功路径:机器不再依赖人类硬编码的规则,而是开始从海量数据中自主归纳特征。
为什么AI直到最近十年(2016-至今)才迎来大爆发?
经过数百万年演化的人类大脑,能以仅约20瓦的功率高效完成复杂的认知任务。而机器要模拟这种智能,本质上是一场依赖“暴力计算”的模拟游戏。这种“大力出奇迹”的做法,直到最近才集齐了两个关键拼图:
- 海量数据(Data):数据是模型训练的养料。如同人类通过经验学习,AI需要通过海量样本来提取规律。互联网时代沉淀的文本、图像与视频————从维基百科到社交网络,从数字化典籍到网页记录————构成了人类文明的数字化全景。这些数据为AI提供了规模空前的训练集,打破了早期人工智能面临的“数据匮乏”瓶颈。
- 超强算力(Compute):深度神经网络的训练涉及数十亿甚至万亿级参数的优化,每一次权重更新都需要海量的矩阵运算。得益于GPU并行计算架构的成熟、摩尔定律的红利以及专用AI芯片(如TPU)的突破,现代硬件终于能够支撑起如此庞大的算力需求。
数据与算力,是智能得以落地的物理基础,二者缺一不可。正是它们在近十年的同步跃升,才把人工智能从实验室里的论文与原型,推向了每个人的手机屏幕。
模型的本质
在数学中,我们通过一个函数曲线来拟合坐标系中的数据点。机器学习本质上也是在拟合一个函数,使其可以:
- 输入一张图片x --> 输出其代表的分类y
- 输入一段语音x --> 输出对应的文字y
- 输入前半句话x --> 输出后半句y
机器学习“找到”的这个函数,就是模型,训练一个AI模型,本质上就是在海量的参数空间里,通过不断试错与优化,搜索出那个最能解释数据背后规律的函数。
机器学习探索:机器学习将这种探索规模化、可重复。在庞大的参数空间里,通过海量计算持续逼近那个能完美描述世界规律的“函数”。
人工智能发展的三个阶段
机器学习(统计学的胜利)
- 数据驱动:不再依赖预设规则,从数据中学习概率。
- 典型案例:垃圾邮件分类(统计词频组合)。
- 局限:依赖人工特征工程,在“人画的框”里优化。
深度学习(感知智能的觉醒)
- 自动特征:神经网络模拟人脑,学会“看”和“听”。
- 典型案例:AlphaGo、人脸识别(直觉式策略)。
- 局限:专用智能,缺乏跨任务迁移能力。
大语言模型(生成式AI的兴起)
- 生成范式:从做“选择题”变为写“作文”(推理/创造)。
- 核心机制:Transformer + Next Token Prediction。
- 展望:跨领域通用能力,迈向AGI。
为什么是语言模型率先通向了通用智能?
维特根斯坦认为:语言是世界的压缩。人类几千年的文字记录,编码了我们对世界全量的认知。
说“因为...所以...” --> 编码了因果律
说“虽然...但是...” --> 编码了逻辑转折
讲述一个故事 --> 编码了时间、空间、社会规范
语言模型(预测下一个字,就是理解世界)
LLM的训练目标看似简单:预测下一个字(Next Token Prediction)
但为了准确预测,模型被迫去“理解”深层逻辑:
- 要预测物理题的答案 --> 必须学会物理规律
- 要预测代码的下一行 --> 必须学会编程逻辑
- 要预测故事的结局 --> 必须学会人物动机
通过学习人类所有的文字,LLM实际上是在学习人类对这个世界的全量认知。进而“涌现”出惊人的能力————理解逻辑、因果和常识,能够进行推理、创作代码、甚至模仿人类的情感。
当数据与经验可以被大规模获取时,关键问题变成了:
如何把这些数据变成可学习的人物,让机器在反复试错中自己学出规律?
机器学习概念
人工智能(Artificial Intelligence,AI)是计算机科学的一个分支,致力于创建能够模拟、延伸和扩展人类智能的理论、方法、技术及应用系统。
简单来说,就是让机器具备感知、学习、推理和决策的能力。
机器学习:AI的核心实现路径
机器学习(Machine Learning,ML)研究如何让计算机在无需显式编程的情况下,通过数据自动学习规律,对未来进行预测。
- 人类的常识:发现数据背后的规律(例如:戒烟 + 运动 --> 健康状况改善)。
- 机器学习的意义正是:由计算机来自动完成寻找规律的过程。
传统编程的困境
传统编程必须清楚描述每个步骤,这只适合简单的任务。对于复杂任务(如视觉识别)则束手无策。
难题:如何编写程序识别图像中是否包含苹果?
“写程序覆盖苹果的几何参数、纹理方程或颜色阈值...。这个任务几乎不可能。”
从认知到机器学习
人类如何认识苹果:
- 反复观察无数实物、照片、视频。
- 大脑自动提取关键特征:轮廓的弧度、表皮、光泽、纹理。
- 神经元自行调整,最终形成苹果的概念。
机器学习如何解决:
- 向计算机展示大量图像,并标注是否包含苹果。
- 重复训练,直到计算机自动捕捉模式。
- 面对新图像,使用学习到的模式做出判断。
我们依然需要编写程序,但不是编写最终答案,只是在告诉计算机应该如何学习。
深度学习:基于神经网络的ML
深度学习(DL)本质上仍是机器学习,但它使用了多层神经网络架构,能够使提取更深层次的抽象特征。
传统机器学习
- 依赖人工告诉模型要关注哪些特征。
- 擅长处理结构化数据(表格等)。
- 算力需求较小,在金融风控、商品推荐中广泛应用。
深度学习
- 输入原始数据,模型自动逐层抽象(边缘 --> 纹理 --> 整体语义)。
- 擅长处理非结构化数据(图/文/音)。
- 需及其海量的数据和庞大算力(大语言模型、自动驾驶)。
AI、ML与DL的包含关系

人工智能(AI)
最宽泛概念:让机器表现出类似人类的智能。
机器学习(ML)
不靠人工规则,从数据中自动寻找规律。
深度学习(DL)
使用神经网络,提取深层抽象模式。
什么是模型(Model)
广义上的模型:
- 对现实世界复杂事物的简化与抽象。
- 目的:更清晰地理解、分析、预测或控制错综复杂的现象。
机器学习领域的模型:
- 本质上是计算机通过学习大量数据后,自主总结出的一套规则集合,用于对未来的新数据进行预测或决策。
算法与模型
算法(Algorithm):
- “学习的方法”
- 用于解决问题或执行计算的一套明确逻辑步骤。
- 指导计算机去观测并找出共同特征。
模型(Model):
- “学习的结果”
- 计算机学习大量数据后自主总结出的一套规则集合。
- 对现实规则极简表示,用于预测新数据。
“算法负责构建模型,而模型负责预测未来。”
建立模型:垃圾邮件分类
场景:周末收到朋友小张的邮件,如何在不打开的情况下判断是否是垃圾邮件。
示例1:基于历史概率的盲猜
回忆历史数据:最近10封邮件,就有6封垃圾邮件。
模型1.0:小张每发送10封邮件,就有6封是垃圾邮件。
预测结构:新邮件有60%的可能是垃圾邮件,盲猜其为垃圾邮件。
示例2:寻找单一特征规律
深入分析邮件的发送时间特征:
| 星期一 | ✅ 正常 | 星期六 | ❌ 垃圾 |
|---|---|---|---|
| 星期二 | ✅ 正常 | 星期六 | ❌ 垃圾 |
| 星期三/四 | ✅ 正常 | 星期日 | ❌ 垃圾 |
模型2.0:工作日发送 = 正常邮件,周末发送 = 垃圾邮件
预测结果:今天是周末,非常自信地看都不看直接扔进垃圾桶!
示例3:多维特征组合
小张周末发的“徒步通知”被误杀!只看时间太绝对,引入新特征:文件大小。
| 1~7 KB | ✅ 正常 | 16 KB | ❌ 垃圾 |
|---|---|---|---|
| 3 KB | ✅ 正常 | 20~35 KB | ❌ 垃圾 |
模型3.0:如果是周末发送且大小>10KB --> 垃圾邮件(否则正常)
预测结果:成功拦截周末发的大附件广告,并顺利接收了周末3KB的徒步通知。
机器学习本质:寻找最优权重
“发送时间”、“邮件大小”被称为预测的特征(Feature)。
人类一次智能处理少量特征,计算机能快速分析数百万邮件和上万特征。算法会为不同特征分配权重(Weight)。
- 如果包含“免费”,权重极高(+50分)
- 如果大于10KB,权重较高(+30分)
- 若总得分 > 60分 --> 判定为垃圾邮件
如何精准确定每一个特征的权重?
让计算机通过大量历史数据,不断自动调整寻找这组最优权重,这就是机器学习。
机器学习的核心思想:
- 用数据寻找规律。
- 用模型表达规律。
- 用损失函数衡量误差。
- 用梯度下降优化参数。
- 不断训练让模型变得更准确。
几乎所有的机器学习和深度学习模型,都可以看作是对上述基本范式的扩展和升级。
机器学习类型
数据是用于训练和测试机器学习模型的原始信息。在日常应用中,我们最常遇到的是结构化数据,即以表格形式(如Excel或数据库)存储的数据:
- 每一行代表一个具体的实体。
- 每一列代表一项特定的属性。
结构化数据集(具体的房价数据集表格):
| 面积 (㎡) | 卧室数量 | 浴室数量 | 地段评分 | 房龄 (年) | 房价 (万元) |
|---|---|---|---|---|---|
| 85 | 2 | 1 | 6 | 15 | 180 |
| 110 | 3 | 1 | 7 | 8 | 320 |
| 142 | 4 | 2 | 9 | 3 | 510 |
| 73 | 2 | 1 | 4 | 20 | 120 |
| 128 | 3 | 2 | 8 | 5 | 450 |
基础概念解析:
- 数据集(Dataset):通常指上面这一整张数据表格。
- 数据点(Data Point):也称样本,数据集中的单条记录,表格的每一行就是一个数据点。
- 特征(Feature):表示数据的属性或性质。在表格中,特征就是表格的列。例如,每个房屋有5个特征,我们就称这个数据集的维度(Dimension)为5。
- 标签(Label):模型需要重点预测的“目标变量”(即表格最后一列)。所谓预测,本质上就是让机器通过学习特征,去推算数据中的标签。
机器学习的三大分支
根据数据集是否带有标签,以及学习方式的不同,机器学习衍生出三大主要分支:
- 监督学习(Supervied Learning)
- 数据集中的每个数据点都包含标签(如已知房价),机器通过学习特征与标签的关系进行预测。
- 无监督学习(Unsupervised Learning)
- 数据点不包含标签,机器需要自己在数据中寻找隐藏的结构或规律。
- 强化学习(Reinforcement Learning)
- 在动态环境中通过不断试错,学习如何做出更优决策以获得最大奖励。
监督学习
监督学习:处理标签数据
监督学习的应用十分广泛,涵盖图像识别、文本处理、推荐系统等众多领域。
- 其核心目标是根据已有数据预测标签。
在房价数据集中,“房价”就是我们要预测的标签。模型通过学习这些现有的历史数据找出规律,进而预测新数据的标签。这意味着,如果我们向模型输入一套缺失价格信息的新房子的数据,模型就能自动预测出它的价格。
监督学习的两类核心模型
针对不同类型的标签数据,监督学习衍生出了两类模型:
回归模型(Regression)
用于预测连续的数值型数据。模型的输出是一个具体的数值。
- 应用场景:预测房屋价格、股票走势。
- 常见算法:线性回归、决策树、随机森林、XGBoost。
分类模型(Classification)
用于预测离散的分类型数据(如“猫/狗”、“晴天/雨天”)。
- 应用场景:图像识别、垃圾邮件检测、客户群体划分。
- 常见算法:感知器、逻辑回归、朴素贝叶斯、K近邻(KNN)、支持向量机(SVM)。
线性回归
线性回归(Linear Regression)是机器学习中最经典的模型之一,它通过构建线性方程来拟合数据的内在分布规律,从而实现对连续数值的预测。
在数学上,线性描述的是输入与输出之间按比例变化、可叠加的关系。一个映射
- 齐次性:
- 可加性:
几何解释:在一维空间中,线性关系表现为一条直线;在高维空间中,表现为平面或超平面。
高尔顿的“回归均值”
“回归”(Regression)一词源于统计学家佛朗西斯·高尔顿(Francis Galton)的经典研究。他观察到,身高极高或极矮的父母,其子女的身高往往会向群体平均值靠拢,这一现象被称为“回归均值”。
在机器学习领域中,回归的概念被进一步引申:旨在通过数据建模变量间的映射关系,进而实现对连续数值的预测。
模型公式
最简单的线性回归模型只有一个特征(如房屋面积),预测公式为:
:模型的预测值(如预测房价)。 :输入特征(如面积)。 :权重(weight,斜率)————面积每增加1 ,房价变化多少 。 :偏置(bias,截距)————当面积为0时的理论基准值。 这里的w和b就是模型需要学习的参数。训练模型本质上就是搜索一组最合适的w和b。
损失函数:衡量模型准不准
模型要找到最好的参数,首先得回答一个问题:当前这组参数到底好不好?
我们需要一种方法来度量预测值与真实值之间的偏差————这就是损失函数(Loss Function)。
均方误差(Mean Squared Error,MSE):
- 对每个样本,计算预测值 - 真实值(即误差)
- 将误差取平方(消除正负号、放大较大误差)
- 对所有样本求平均
- 预测值离真实值越远,损失越大。模型训练的目标,就是让损失函数的值尽可能小。
使用MSE的两个优点:
- 消除正负号的影响:让误差绝对值化,防止求和之后正负误差互相抵消。
- 放大误差便于优化:例如误差为3和0.4,平方后变为9和0.16,差异更明显。(惩罚更大)
梯度下降:寻找最优解
知道当前模型有多不准之后,如何自动调整w和b?
最通用的算法是梯度下降(Gradient Descent):沿着损失函数下降最快的方向,逐步调整参数,直至找到最优解。
形象比喻:蒙眼下山
- 感知坡度(计算梯度):盲人用脚探知当前所处的坡度方向,确定哪里是下坡。
- 迈出一小步(更新参数):顺着最陡峭的下坡方向(负梯度方向),迈出一步。
- 循环往复(迭代优化):在新位置继续感知坡度,继续向下走。
- 到达谷底(模型收敛):当走到再也没有下坡路的地方,即认为到达了最低点。
模型训练的核心循环
把下山的比喻和机器学习概念对应起来:
- 山的当前高度= 损失函数的值(Loss)
- 下坡的方向 = 梯度(Gradient,即导数)
- 每一步迈多大 = 学习率(Learning Rate)
训练流程:
输入数据 -> 计算预测 -> 计算损失 -> 计算梯度 -> 更新参数
每完成一轮循环参数就更接近一点,经过多轮迭代模型逐渐收敛到最优解。
多特征线性回归
影响房价的因素包括:面积、楼层、距地铁距离、房龄等。
| 面积 (㎡) | 楼层 | 距地铁 (km) | 房龄 (年) | 总价 (万元) |
|---|---|---|---|---|
| 50 | 3 | 0.5 | 5 | 180 |
| 70 | 15 | 1.2 | 10 | 210 |
| 90 | 8 | 0.3 | 2 | 320 |
| 110 | 20 | 2.0 | 15 | 280 |
| 130 | 12 | 0.8 | 8 | 380 |
现在有d个特征,模型自然地扩展为:
(面积):➕正,面积越大,房价越高。 (楼层):➕正,中高楼层通常更贵。 (距地铁):➖负,离地铁越远,房价越低。 (房龄):➖负,房子越老,房价越低。
代码实现:
使用Python的scikit-learn库可以非常方便地完成多特征线性回归的训练和预测:
import numpy as np
from sklearn.linear_model import LinearRegression
# 1. 准备数据:4个特征(面积、楼层、距地铁、房龄)
X = np.array([
[50, 3, 0.5, 5],
[70, 15, 1.2, 10],
[90, 8, 0.3, 2],
[110, 20, 2.0, 15],
[130, 12, 0.8, 8],
])
y = np.array([180, 210, 320, 280, 380]) # 总价(万元)
# 2. 训练模型
model = LinearRegression()
model.fit(X, y)
# 3. 查看学到的权重
feature_names = ['面积', '楼层', '距地铁', '房龄']
print("各特征权重:")
for name, w in zip(feature_names, model.coef_):
print(f" {name}: {w:+.2f}")
print(f" 偏置b: {model.intercept_:.2f}")
# 4. 预测新数据:[100, 10, 0.6, 5]
new_house = np.array([[100, 10, 0.6, 5]])
pred = model.predict(new_house)
print(f"\n预测房价:{pred[0]:.1f}万元")输出结果:
各特征权重:
面积: +2.63
楼层: +1.77
距地铁: -1.23
房龄: -8.60
偏置b: 86.91
预测房价:323.7万元用向量简化表达
当特征很多时,把公式全部展开写会非常冗长。借助向量的记号,可以将其写得更简洁:
其中:
- 不管是1个特征还是100个特征,模型的本质结构完全一样————都是特征的加权求和再加偏置。
多项式回归:当直线不够用时
现实中的数据并不总是沿着一条直线分布。比如当房价随面积的增长先慢后快时,直线就无法很好地拟合了。这时可以引入多项式回归:
- 当d = 1时,就是普通的线性回归。
- 当d = 2时,拟合出的是一条抛物线;阶数越高,曲线就越灵活。
尽管公式中出现了
无监督学习
无监督学习:处理无标签的数据
在现实的业务场景中,获取带有标签的数据往往成本很高,我们手中绝大多数的数据其实是没有标签的。
- 无监督学习的目标不再是去预测某个具体的数值或类别,而是让模型自行探索,发现数据内部隐藏的结构、模式或规律。
无监督学习在实际业务中同样十分常见,主要包括三个方向:聚类、降维与生成算法。
聚类
核心作用:将数据集分组
面对海量数据时分类无从下手?聚类算法会计算数据之间的相似度,将行为模式相似的数据自动归拢到一个组(簇)里,实现物以类聚。
代表算法:K-means算法
案例:电商百万用户精准营销
面对海量浏览记录,人工无法打标签。通过聚类,平台能在无干预情况下,自动梳理出“价格敏感型”或“品牌忠诚型”等清晰用户画像,方便精准推荐。
降维
核心作用:信息整合与特征重构
在处理复杂数据时,特征过载会导致“维度灾难”。降维的目标是在尽量不丢失关键信息的前提下,极大地简化数据集。
代表算法:主成分分析(PCA)
案例:房价预测中的周边环境特征
“距离地铁站米数”、“学校数量”、“商场个数”等几十个特征会让计算缓慢且冗余。降维算法能捕捉这些特征的相关性,将它们合并压缩成一个新特征,如“综合地段评分”。
生成
生成算法(AIGC)
核心作用:从数据中创造全新内容
当面临数据匮乏(如医疗AI隐私限制)或创作瓶颈时,生成算法通过深度学习真实数据的分布规律,生成现实中不存在的全新数据点。
代表算法:生成对抗网络(GAN)
应用场景:
- 生成极其逼真的虚拟人脸或病理切片,低成本扩充模型训练集。
- AIGC应用:AI绘画工具仅凭文本提示生成高质量画作。
强化学习
强化学习:在动态环境中试错
在复杂的现实场景中(如自动驾驶实时决策、围棋落子),我们面临的是高度动态的挑战,并没有现成的静态历史数据集。
- 强化学习的核心是:在不断试错的过程中,学习如何做出更优决策。
运行机制:设定一个智能体(Agent)置身于环境中。智能体采取动作,环境根据动作的好坏实时反馈奖励(Reward)或惩罚(Penalty)。终极目标是探索出一套能获得最大长期累计奖励的策略。
强化学习的里程碑与应用
最广为认知的里程碑是2016年击败人类世界冠军的AlphaGo,其通过左右互搏、在数百万局的试错中进化出了超越人类的策略。
关键技术:
经典的Q-Learning算法,以及结合了神经网络的深度强化学习(DRL)技术,正在让AI学会像人类一样思考与行动。
广泛应用领域:
- 复杂棋牌与电子游戏
- 机器人姿态控制
- 工业机械臂调度
- 量化交易策略制定
- 大型语言模型的人类意图对齐
线性回归核心知识
梯度下降
损失函数与调参方向
损失函数用来度量模型预测值
- 损失函数能告诉我们当前参数有多不准。
- 训练的目标:让损失函数的值尽可能小。
- 问题:如何决定是把参数调大一点,还是调小一点?
💡我们需要一种自动调参的算法————梯度下降(Gradient Desecent)。
偏导数:多变量函数的变化率
损失函数通常依赖于多个参数(如w和b)。我们需要固定其他变量不变,只对其中一个变量求导。
梯度下降核心原理
把所有变量的偏导数组合成一个向量,就得到了梯度(Gradient)。
- 梯度的方向,是函数值增长最快的方向。
- 梯度的反方向,是函数值下降最快的方向。
梯度下降的迭代公式
基于梯度下降的性质,参数更新公式为:
:当前位置。 :脚下最陡的上坡方向。 :学习率(Learning Rate),控制每一步迈多大。 :朝下坡方向迈一步。
🤔为什么要减去梯度?
🤓因为我们要最小化损失函数,所以需要往反方向走。
线性回归的梯度推导
损失函数(MSE):
使用链式法则分别对w和b球偏导:
房价与面积线性回归
假设房价与面积呈线性关系
我们的目标是通过梯度下降算法,从数据中学习出参数w和b。
1.生成模拟数据
我们生成100套房屋的数据,面积范围90-150千米。
为了使梯度下降更稳定,我们将面积单位转换为百平米,这样数据范围变为0.9-1.5。
真实规律:
等价于:
import numpy as np
import matplotlib.pyplot as plt
plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
np.random.seed(42)
n_samples = 100
areas_m2 = np.random.uniform(90, 150, n_samples)
areas = areas_m2 / 100
prices = 250 * areas + 30 + np.random.randn(n_samples) * 15
print(f"房屋数量: {len(areas)} 套")
print(f"面积范围: {areas_m2.min():.1f} - {areas_m2.max():.1f} 平米 ({areas.min():.2f} - {areas.max():.2f} 百平米)")
print(f"价格范围: {prices.min():.1f} - {prices.max():.1f} 万")
print(f"真实规律: 房价 = 2.5 × 面积(平米) + 30")2.梯度下降算法
梯度下降通过计算损失函数对参数的梯度,不断更新参数:
其中
# 梯度下降
def gradient_descent(X, y, lr = 0.1, epochs = 200):
# 初始化参数
w, b = 0.0, 0.0
n = len(X)
# 初始化历史记录,使用记录初始参数和损失
history = {'w': [w], 'b': [b], 'loss': [np.mean((w * X + b - y) ** 2)]}
# 进行迭代训练,更新参数
for epoch in range(epochs):
# 计算预测值和损失
y_pred = w * X + b
loss = np.mean((y_pred - y) ** 2)
# 计算梯度
dw = (2/n) * np.sum((y_pred - y) * X)
db = (2/n) * np.sum(y_pred - y)
# 更新参数
w -= lr * dw
b -= lr * db
# 记录历史参数和损失
history['w'].append(w)
history['b'].append(b)
history['loss'].append(loss)
# 每隔 1% 的训练周期打印一次当前参数和损失
if epoch % (epochs // 100) == 0:
print(f"Epoch {epoch} - w: {w:.2f}, b: {b:.2f}, Loss: {loss:.2f}")
return history3.可视化结果
左图展示拟合效果,中图展示参数收敛过程,右图展示损失函数下降过程。

学习率的作用
学习率(Learning Rate)控制参数更新的步长:
- 学习率太小:收敛速度慢,需要更多迭代。
- 学习率太大:可能震荡甚至发散,无法收敛。
- 合适的学习率:快速稳定地收敛到最优解。
下面我们对比不同学习率的效果。


梯度下降的优化算法
标准梯度下降存在收敛慢、易陷入局部最优等局限。业界发展出了多种优化器:
- 动量法(Momentum):借鉴物理学惯性,给梯度加上“速度”,减少狭长山谷中的震荡。
- AdaGrad:为每个参数自动维护独立的学习率,适合处理稀疏数据。
- RMSProp:AdaGrad的改良,关注近期的梯度变化,避免学习率降到零,适合非平稳目标(如RNN)。
- Adam(自适应矩估计):结合了动量(方向加速)和RMSProp(自适应步幅)。它同时维护梯度的方向趋势和大小变化,既跑得快又稳。收敛快且对学习率不敏感,是目前深度学习中最广泛使用的默认首选优化器。
使用Adam优化器加速训练
def adam_optimizer(X, y, lr = 0.1, epochs = 200, beta1 = 0.9, beta2 = 0.999, epsilon = 1e-8):
# 初始化参数
w, b = 0.0, 0.0
n = len(X)
# 初始化一阶矩和二阶矩
m_w, m_b = 0.0, 0.0
v_w, v_b = 0.0, 0.0
# 初始化历史记录,使用记录初始参数和损失
history = {'w': [w], 'b': [b], 'loss': [np.mean((w * X + b - y) ** 2)]}
# 进行迭代训练,更新参数
for epoch in range(epochs):
# 计算预测值和损失
y_pred = w * X + b
loss = np.mean((y_pred - y) ** 2)
# 计算梯度
dw = (2/n) * np.sum((y_pred - y) * X)
db = (2/n) * np.sum(y_pred - y)
# 更新一阶矩和二阶矩
m_w = beta1 * m_w + (1 - beta1) * dw
m_b = beta1 * m_b + (1 - beta1) * db
v_w = beta2 * v_w + (1 - beta2) * dw ** 2
v_b = beta2 * v_b + (1 - beta2) * db ** 2
# 偏置修正
m_w_hat = m_w / (1 - beta1 ** (epoch + 1))
m_b_hat = m_b / (1 - beta1 ** (epoch + 1))
v_w_hat = v_w / (1 - beta2 ** (epoch + 1))
v_b_hat = v_b / (1 - beta2 ** (epoch + 1))
# 更新参数
w -= lr * m_w_hat / (np.sqrt(v_w_hat) + epsilon)
b -= lr * m_b_hat / (np.sqrt(v_b_hat) + epsilon)
# 记录历史参数和损失
history['w'].append(w)
history['b'].append(b)
history['loss'].append(loss)
# 每隔 1% 的训练周期打印一次当前参数和损失
if epoch % (epochs // 100) == 0:
print(f"Epoch {epoch} - w: {w:.2f}, b: {b:.2f}, Loss: {loss:.2f}")
return history
# ==================== 运行测试 ====================
# 提示:Adam 的收敛速度通常比普通梯度下降快很多。
# 原来需要 100,000 次,现在可能 10,000 ~ 50,000 次就足够收敛了。
# 学习率对 Adam 来说通常设为 0.1,0.01 或 0.001 都可以自适应调整。
history = adam_optimizer(areas, prices, lr=0.1, epochs=50000)
print(f"最终结果:房价 = {history['w'][-1]/100:.3f} × 面积(平米) + {history['b'][-1]:.2f}")梯度下降的变体
每次更新参数时,该用多少数据来计算梯度?
| 变体 | 数据量 | 特点 |
|---|---|---|
| 全量 (BGD) | 全部数据 | 梯度精确,稳定,但计算开销极大。 |
| 随机 (SGD) | 1 条数据 | 轻量,有随机性,但噪声大、震荡剧烈。 |
| 小批量 (Mini-batch) | 一小批 (如 64) | 折中方案。能利用 GPU 并行,兼顾精度与效率。 |
Mini-batch已经成为深度学习训练的业界主流选择。
数据预处理
真实世界中的数据往往充满各种问题:
- 存在缺失值和异常值。
- 包含文本、类别等非数值类型。
- 量纲差异巨大(房价是百万级,房间是个位数)。
数据处理的四大常用工具:
Pandas:
- 表格数据处理的瑞士军刀,提供DataFrame等核心数据结构。
Matplotlib:
- Python最基础的数据可视化库。
Seaborn:
- 基于Matplotlib的高级可视化库,语法更简洁、图标更美观。
Scikit-Learn(sklearn):
- 机器学习标准库,包含丰富的预处理工具和常用算法实现。
准备环境与数据
准备环境与数据
以经典的Titanic(泰坦尼克号)生存预测数据集为例:
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
# 设置绘画风格
sns.set_style("whitegrid")
# 中文标签
plt.rcParams['font.sans-serif'] = ['SimHei', 'Arial Unicode MS', 'DejaVu Sans'] # 设置中文字体为SimHei
plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
# 从网络读取泰坦尼克号数据集
url = "https://raw.githubusercontent.com/datasciencedojo/datasets/master/titanic.csv"
df = pd.read_csv(url)
# 查看数据概览
print("数据集形状:", df.shape)
print("\n数据集前五行:")
display(df.head())
print("\n数据类型与缺失值情况:")
print(df.info())观察df.info()的输出会发现Age(年龄)、Cabin(客舱号)和Embarked(登船港口)存在缺失值,且包含文本类型的列,这正是我们需要处理的“脏数据”。
数据集划分
正确的做法是先划分数据集,再做预处理。通常将数据集划分为三个部分:
- 训练集(60%-80%):训练模型,学习数据中的规律。
- 验证集(10%-20%):调整超参数,评估模型在未见过数据上的表现。
- 测试集(10%-20%):模拟真实世界数据,最终评估模型泛化能力。
防止数据泄漏(Data Leakage)原则:
所有fit操作只在训练集上进行,测试集只做transform。
from sklearn.model_selection import train_test_split
# 定义特征X和目标标签y(生存情况)
X = df.drop('Survived', axis=1)
y = df['Survived']
# 将数据集划分为训练集和测试集,比例为80%训练集和20%测试集
X_train, X_test, y_train, y_test = train_test_split(
X, y,
test_size=0.2,
random_state=42, # 固定随机种子,以确保结果可复现
stratify=y # 按照目标标签y进行分层抽样,确保训练集和测试集中生存情况的比例相同
)
print(f"训练集:{X_train.shape[0]}条数据")
print(f"测试集:{X_test.shape[0]}条数据")
print(f"训练集中的生存率:{y_train.mean():.2%}")
print(f"测试集中的生存率:{y_test.mean():.2%}")设置stratify=y后,sklearn会确保两者中正负样本比例与原始数据保持一致。接下来的所有探索和规则制定都必须只在X_train上进行!
数据探索分析
数据探索分析(EDA,Exploratory Data Analysis)就像是给数据做一次全面体检,直观的发现数据中的问题和规律。
基本信息
使用.info()查看数据集的数据类型和缺失值情况:
# 查看训练集数据类型和缺失值情况
print("\n训练集数据类型与缺失值情况:")
print(X_train.info())# 缺失值统计
missing = X_train.isnull().sum()
missing_pct = ((missing / len(X_train)) * 100).round(1)
pd.DataFrame({
'缺失值数量': missing,
'缺失值比例(%)': missing_pct
}).query('缺失值数量 > 0')Cabin缺失77.5% ———— 缺失极多,信息量匮乏。Age缺失19.2% ———— 有明显缺失,需要填充。Embarked缺失0.3% ———— 仅2条记录缺失。
数值特征统计
X_train.describe().style.format("{:.1f}")
| PassengerId | Pclass | Age | SibSp | Parch | Fare | |
|---|---|---|---|---|---|---|
| count | 712.0 | 712.0 | 575.0 | 712.0 | 712.0 | 712.0 |
| mean | 444.4 | 2.3 | 29.8 | 0.5 | 0.4 | 31.8 |
| std | 257.5 | 0.8 | 14.5 | 1.1 | 0.8 | 48.1 |
| min | 1.0 | 1.0 | 0.4 | 0.0 | 0.0 | 0.0 |
| 25% | 222.8 | 2.0 | 21.0 | 0.0 | 0.0 | 7.9 |
| 50% | 439.5 | 3.0 | 28.5 | 0.0 | 0.0 | 14.5 |
| 75% | 667.2 | 3.0 | 39.0 | 1.0 | 0.0 | 31.0 |
| max | 891.0 | 3.0 | 80.0 | 8.0 | 6.0 | 512.3 |
- 注意到
Fare最大512,最小0,量纲差异巨大,后续需要缩放。
数据可视化
使用Matplotlib和Seaborn对数据进行可视化,看出更多端倪:

通过以上可视化,我们得出六条关键发现,它们将直接指导后续的预处理策略:
| 发现 | 对应处理 |
|---|---|
| 女性存活率远高于男性 (~74% vs ~19%) | Sex 是强特征,需编码为数值 |
| 一等舱存活率最高 (~63%) | Pclass 是强特征,考虑保留或独热编码 |
| Age 缺失约 20% | 需要填充,分布略偏态,选用中位数 |
| Cabin 缺失 77% | 直接填充意义不大,考虑转化为"有/无客舱信息" |
| Fare 严重右偏 (均值 > 中位数,长尾分布) | 可能需要对数变换或截断处理 |
| PassengerId、Name、Ticket 对预测无直接帮助 | 考虑删除,或从 Name 中提取有用信息 |
数据清洗
处理重复值:数据清洗的核心环节通常涵盖重复值剔除、缺失值填补以及异常值检测与处理。
对于数据中重复的行直接删除即可:
# 检查重复行
duplicate_count = X_train.duplicated().sum()
print(f"训练集中重复行的数量:{duplicate_count}")
# 如果有重复行,删除
if duplicate_count > 0:
X_train = X_train.drop_duplicates()
y_train = y_train[X_train.index] # 同步删除对应的标签行
print(f"删除重复行后,训练集形状:{X_train.shape}")处理缺失值:当数据集中出现缺失值时,处理策略需根据缺失比例及特征类型灵活选择:
按缺失比例决定:
- 缺失极低(< 5%):
- 且为随机缺失 -> 直接删除对应样本行
- 缺失极高(> 70%):
- 该特征信息量匮乏,通常直接删除整列
按特征类型选择:
- 数值型特征:
- 用均值或中位数填充。中位数对极端值不敏感,在数据偏态时更加稳健。
- 类别型特征:
- 用众数填充,或新增“Unknown”类别保留业务信息。
使用sklearn.impute.SimpleImputer处理缺失值:
from sklearn.impute import SimpleImputer
# —— Age: 中位数填充(因为年龄分布可能有偏,使用中位数更稳健)——
age_imputer = SimpleImputer(strategy='median')
age_imputer.fit(X_train[['Age']]) # 只拟合训练集的Age列
X_train['Age'] = age_imputer.transform(X_train[['Age']]).ravel() # 填充训练集的Age列
X_test['Age'] = age_imputer.transform(X_test[['Age']]).ravel() # 同样用训练集的中位数填充测试集的Age列
# —— Embarked: 众数填充(因为Embarked是分类变量且只缺2个,使用众数填充最合理)——
embarked_imputer = SimpleImputer(strategy='most_frequent')
embarked_imputer.fit(X_train[['Embarked']]) # 只拟合训练集的Embarked列
X_train['Embarked'] = embarked_imputer.transform(X_train[['Embarked']]).ravel() # 填充训练集的Embarked列
X_test['Embarked'] = embarked_imputer.transform(X_test[['Embarked']]).ravel() # 同样用训练集的众数填充测试集的Embarked列
# —— Cabin: 缺失 77%,先转化为“有无客舱信息”,再删除原列 ——
X_train['Has_Cabin'] = X_train['Cabin'].notna().astype(int) # 有客舱信息为1,无为0
X_test['Has_Cabin'] = X_test['Cabin'].notna().astype(int) # 同样处理测试集
X_train.drop(columns=['Cabin'], inplace=True) # 删除原Cabin列
X_test.drop(columns=['Cabin'], inplace=True) # 同样删除测试集的Cabin列
# 查看训练集和测试集的情况
print("\n处理缺失值后训练集数据类型与缺失值情况:")
print(X_train.info())
print("\n处理缺失值后测试集数据类型与缺失值情况:")
print(X_test.info())异常值检测
异常值(Outliers)是指显著偏离其他观测值的极端值。可以使用箱线图检测:

- Age(年龄):集中在20-40岁,60-80岁有离群点(高龄乘客,真实存在)。
- Fare(票价):极度右偏。绝大多数票价极低,极少数超过500美元。不处理会让模型过度关注高价票。
- SibSp(亲属数):大部分是0或1,少数大家庭(3-8个亲属)也是真实的异常值。
处理异常值
使用统计学方法(IQR)识别并处理票价(Fare)中的极端异常值。采用截断策略,将过高的票价强行拉回到合理范围内。
# 用IQR方法检测
def detect_outliers_iqr(data, column, factor=1.5):
Q1 = data[column].quantile(0.25) # 计算25分位数
Q3 = data[column].quantile(0.75) # 计算75分位数
IQR = Q3 - Q1
lower = max(0, Q1 - factor * IQR) # 下界不能小于0
upper = Q3 + factor * IQR
outliers = data[(data[column] < lower) | (data[column] > upper)]
print(f"{column}: Q1={Q1:.2f}, Q3={Q3:.2f}, IQR={IQR:.2f}")
print(f"{column}: 下界={lower:.2f}, 上界={upper:.2f}")
print(f"{column}: 异常值数量={len(outliers)}(占比{len(outliers)/len(data)*100:.2f}%)")
return lower, upper
lower, upper = detect_outliers_iqr(X_train, 'Fare')
# 对于Fare,我们选择阶段而非删除
# 因为高票价可能是真实的,头等舱确实很贵
X_train['Fare'] = X_train['Fare'].clip(lower=lower, upper=upper) # 将Fare限制在合理范围内
X_test['Fare'] = X_test['Fare'].clip(lower=lower, upper=upper) # 同样处理测试集的Fare列特征编码
目的:将文本映射为数值,使模型能进行数学运算。
核心区别:是否保留顺序、是否引入数值大小。
- 序数编码:保留严格顺序。
- 独热编码:无大小顺序之分。
- 标签编码:引入数值大小(不保证顺序)。
序数编码(Ordinal Encoding)
将类别映射为整数,严格保留顺序关系(如:低=0,中=1,高=2)。
适用场景:特征本身具有明确等级(如学历、满意度、舱位)。
from sklearn.preprocessing import OrdinalEncoder, OneHotEncoder
# 构造模拟数据:['Low', 'Medium', 'High']
oe = OrdinalEncoder(categories=[['Low', 'Medium', 'High']]) # 显式指定类别顺序
df_ordinal['fare_cat_encoded'] = oe.fit_transform(df_ordinal[['fare_category']])
"""
顺序定义:Low < Medium < High -> 0 < 1 < 2
fare_category fare_cat_encoded
0 Low 0.0
1 Medium 1.0
2 High 2.0
"""独热编码(One-Hot Encoding)
核心思想:把每个类别编程独立列,用0和1表示,消除虚假的大小关系。
使用场景:无序分类特征(如颜色、城市)。
| 样本 | 颜色 | 编码 |
|---|---|---|
| A | 🔴 红 | 1 |
| B | 🟢 绿 | 2 |
| C | 🔵 蓝 | 3 |
模型会误以为:蓝 = 红 × 3
from sklearn.preprocessing import OneHotEncoder
# 初始化 OneHotEncoder
# handle_unknown='ignore': 遇到训练集中未出现的类别时,不会报错而是忽略(置为全0)
# sparse_output=False: 输出为密集数组 (numpy array),而不是稀疏矩阵
ohe = OneHotEncoder(handle_unknown='ignore', sparse_output=False)
# 对 'embark_town' 列进行拟合并转换
# 注意:输入必须是二维数组,所以使用双括号 [['embark_town']]
ohe_result = ohe.fit_transform(df_ohe[['embark_town']])
# --- 将结果转换为 DataFrame 以便查看和拼接 ---
# 1. 获取新生成的列名 (scikit-learn 1.0+ 推荐使用 get_feature_names_out)
ohe_columns = ohe.get_feature_names_out(['embark_town'])
# 2. 构建 DataFrame,并保持与原表相同的索引
ohe_df = pd.DataFrame(ohe_result, columns=ohe_columns, index=df_ohe.index)
# 此时 ohe_df 包含了独热编码后的新特征,可以进一步与主表拼接
# final_df = pd.concat([df_ohe, ohe_df], axis=1)
# 生成的新列:
# ['embark_town_Cherbourg', 'embark_town_Queenstown', 'embark_town_Southampton']
# 表格数据:
# survived sex pclass embark_town_Cherbourg embark_town_Queenstown embark_town_Southampton
# 0 0 male 3 0.0 0.0 1.0
# 1 1 female 1 1.0 0.0 0.0缺点(维度爆炸):N种类别会增加N-1列。类别极多时会导致数据稀疏,建议该用Embedding。
标签编码(Lable Encoding)
简单映射为整数(0,1,2...),不保证顺序,但引入了数值大小。
✅适用场景
- 二分类特征(如性别,0/1等价于独热)。
- 分类任务的目标变量/标签列。
- 树模型(如XGBoost、随机森林)。
❌严禁使用
严禁用于线性模型和多分类无序特征,此类情况必须改用独热编码或序数编码。
from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
X_train_encoded['Sex'] = le.fit_transform(X_train['Sex']) # 映射关系特征构造与删除
原始数据只是原材料,接下来我们将基于对泰坦尼克号背景的理解,对数据集做三个关键处理,挖掘出数据背后真正的规律:
- 合并特征:将分散的亲属数量合并为“家庭规模”,并标记出“独自一人”的乘客(家庭结构可能直接影响逃生策略)。
- 提取信息:从看似无用的姓名中提取出头衔(如Mr,Mrs,Master),帮助识别社会地位和特殊群体。
- 删减噪音:果断删除缺失太多、格式混乱或会导致数据泄漏的列,只保留最核心的特征。
目标:去粗取精,将杂乱的原始记录转化为模型更容易理解的高质量特征。
def create_features(df):
df = df.copy() # 避免修改原始数据
# 1. 家庭成员总数
df['FamilySize'] = df['SibSp'] + df['Parch'] + 1 # +1 是为了包括乘客本人
# 2. 是否独自一人
df['IsAlone'] = (df['FamilySize'] == 1).astype(int) # 独自一人则为1,否则为0
# 3. 从姓名提取头衔并归并稀有头衔
df['Title'] = df['Name'].str.extract(' ([A-Za-z]+)\.', expand=False) # 提取头衔
rare_titles = ['Lady', 'Countess', 'Capt', 'Col', 'Don', 'Dr',
'Major', 'Rev', 'Sir', 'Jonkheer', 'Dona']
df['Title'] = df['Title'].replace(rare_titles, 'Rare')
df['Title'] = df['Title'].replace({'Ms': 'Miss', 'Mlle': 'Miss', 'Mme': 'Mrs'}) # 将Mlle和Ms归并为Miss,Mme归并为Mrs
# 4. 删除无用特征
drop_cols = ['PassengerId', 'Name', 'Ticket'] # 这些特征对模型没有帮助,且可能引入噪声
df = df.drop(columns=[c for c in drop_cols if c in df.columns]) # 只删除存在的列
return df
X_train_fe = create_features(X_train)
X_test_fe = create_features(X_test)
# 查看训练集情况
print("\n特征工程后训练集数据类型与缺失值情况:")
print(X_train_fe.shape)
print(X_train_fe[['FamilySize', 'IsAlone', 'Title']].head())特征放缩
在数据集中,部分特征的量纲差异可能极大:
Age: 0 ~ 80 (十位数)
Fare: 0 ~ 100+ (百位数)
SibSp: 0 ~ 8 (个位数)
Embarked_C: 0 or 1 (0-1)如果不进行缩放,模型会错误地认为数值较大的特征更重要,赋予其过大的权重。
导致梯度下降在更新参数时反复震荡,需要极多的迭代次数才能找到最优解,甚至可能无法收敛。
标准化(Standardization)
将数据转换为均值为0、标准差为1的分布。最常用的方法,尤其适用于数据分布近似正态分布的场景。
特点与影响:
- 不会强制压缩到特定区间(可以是负数)。
- 基于均值和标准差计算,易受异常值影响。
- 得益于前面的异常值清洗,整体相对稳定。
适用场景:
- 线性模型
- 逻辑回归
- 神经网络
- 支持向量机(SVM)
from sklearn.preprocessing import StandardScaler
# 选取需要缩放的数值型列(例如 'Age' 和 'Fare')
numeric_cols = ['Age', 'Fare']
# 标准化
scaler = StandardScaler()
# 只在训练集上拟合,获取均值和标准差
scaler.fit(X_train[numeric_cols])
# 使用训练集的均值和标准差来转换训练集和测试集
X_train_scaled = X_train.copy()
X_test_scaled = X_test.copy()
X_train_scaled[numeric_cols] = scaler.transform(X_train[numeric_cols])
X_test_scaled[numeric_cols] = scaler.transform(X_test[numeric_cols])
print(X_train_scaled[numeric_cols].head())
# 均值和标准差
print("训练集数值型特征的均值:")
print(X_train_scaled[numeric_cols].mean())
print("\n训练集数值型特征的标准差:")
print(X_train_scaled[numeric_cols].std())归一化(Normalization)
将数据线性变换到一个固定的区间,通常是[0, 1]
特点与影响:
- 严格限定数据范围,确保结果不会出现负数。
- 受异常值影响极其明显(极大离群点会破坏正常数值分布)。
适用场景:
- 图像数据处理(像素天然有0~255的边界)。
- 神经网络的输入(Sigmoid等激活函数是有界的)。
- 算法强制要求数据范围时。
工程实践
工程最佳实践(Pipeline)
适用Pipeline和ColumnTransformer将整个流程串起来,防止数据泄漏,代码更简洁,便于部署。
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import OneHotEncoder
# 1. 数值列流水线:填补缺失值 -> 标准化
numeric_transformer = Pipeline(steps=[
('imputer', SimpleImputer(strategy='median')), # 用中位数填补数值列的缺失值
('scaler', StandardScaler()) # 标准化数值列
])
# 2. 类别列流水线:填补缺失值 -> 独热编码
categorical_transformer = Pipeline(steps=[
('imputer', SimpleImputer(strategy='most_frequent')), # 用众数填补类别列的缺失值
('onehot', OneHotEncoder(handle_unknown='ignore', drop='first')) # 独热编码
])
# 3. 组合流水线
preprocessor = ColumnTransformer(
transformers=[
('num', numeric_transformer, ['Age', 'Fare', 'SibSp', 'Parch']), # 数值列
('cat', categorical_transformer, ['Sex', 'Pclass', 'Embarked']) # 类别列
])
# 4. 创建完整的流水线(预处理 + 模型)
X_train_ready = preprocessor.fit_transform(X_train)
X_test_ready = preprocessor.transform(X_test)
# 现在 X_train_ready 和 X_test_ready 已经是预处理后的特征矩阵,可以直接用于模型训练和预测
print("预处理后训练集特征矩阵形状:", X_train_ready.shape)
print("预处理后测试集特征矩阵形状:", X_test_ready.shape)
# 预处理后训练集特征矩阵形状: (712, 9)
# 预处理后测试集特征矩阵形状: (179, 9)模型评估
拟合的前提,是正确判断
基线模型
Baseline(基线模型)是指首个快速实现、相对简单但具备完整流程的模型。
它的核心价值不在于极致精度,而在于建立参照系————后续的所有优化尝试,都需以此为基准进行对比。
先把Baseline跑起来!在深入评估之前,我们需要一个可靠的起点。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.preprocessing import StandardScaler
# 加载数据
housing = fetch_california_housing()
X = pd.DataFrame(housing.data, columns=housing.feature_names)
y = pd.Series(housing.target, name='MedHouseVal')
print("数据集基本信息:")
print(f"样本数量: {X.shape[0]}, 特征数量: {X.shape[1]}")
# 房价上界和下界和均值
print(f"房价范围[{y.min():.2f}, {y.max():.2f}], 房价均值: {y.mean():.2f}")
print(X.head().round(2))- 样本规模:20,640条记录
- 特征维度:8个数值型特征
- 预测目标:房屋价格中位数
训练基线模型
我们对特征进行标准化,划分训练集和测试集,并使用线性回归作为基线模型。
# 标准化 + 划分数据集
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)
X_train, X_test, y_train, y_test = train_test_split(
X_scaled, y, test_size=0.2, random_state=42)
print(f"训练集样本数量: {X_train.shape[0]}, 测试集样本数量: {X_test.shape[0]}")
# 训练baseline模型
model = LinearRegression()
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
for i in range(5):
print(f"真实值: {y_test.iloc[i]:.3f}, 预测值: {y_pred[i]:.3f}")评估指标
评估指标
模型训练完成后,我们需要在测试集上评估其泛化能力。
MAE(平均绝对误差)
- 核心思想:计算预测值与真实值偏差的绝对值,再求平均。
- 优点:直观易懂,单位与目标变量完全一致;对异常值相对不敏感,不会过度放大极端错误。
- 缺点:在数学上绝对值在零点不可导,作为损失函数优化时不如平方项方便。
MSE(均方误差)
- 核心思想:计算预测值与真实值偏差的平方,再求平均。
- 特点:平方操作会呈指数级放大大误差的影响。如果业务场景对大偏差零容忍(医疗、自动驾驶),MSE是很好的指标。
- 缺点:单位被平方了(例如“平方万美元”),失去了直观的业务含义。
RMSE(均方根误差)
- 核心思想:对MSE开根号。
- 单位还原:量纲回到了和目标变量一致,业务上容易解释。
- 惩罚大误差:继承了MSE对大偏差敏感的特性。
- 在实际工程和数据科学竞赛(如Kaggle)中,RMSE是回归任务的默认标准。
核心问题:我们的模型比“瞎猜”(直接用均值预测)好多少?
- 无量纲指标:衡量模型消除了总方差中多大比例的不确定性,可在不同数据集间横向比较。
:完美预测,没有任何误差。 :模型表现和直接预测所有样本的平均值一样,等于没学到有效特征。 :模型预测得比直接猜平均值还差(通常是因为选错了模型或存在严重Bug)。
from sklearn.metrics import mean_squared_error, r2_score
from sklearn.metrics import mean_absolute_error
mae = mean_absolute_error(y_test, y_pred)
mse = mean_squared_error(y_test, y_pred)
rmse = np.sqrt(mse)
r2 = r2_score(y_test, y_pred)
# 输出评估指标
print(f"MAE: {mae:.4f} (平均偏差 {mae*10:.2f}万美元)")
print(f"MSE: {mse:.4f}")
print(f"RMSE: {rmse:.4f} (平均偏差 {rmse*10:.2f}万美元)")
print(f"R²: {r2:.4f} (解释了{r2*100:.1f}%的方差)")- RMSE > MAE:说明存在一些偏差较大的样本把RMSE拉高了。
:解释了约57.6%的房价方差,剩余42.4%未捕获。 - 对于只用8个特征的线性模型,这是个合理的起点,但有提升空间。
残差分析
残差分析:数字之外的诊断
指标只给出全局汇总,残差分析能告诉我们模型在哪里表现好、哪里差。

- 预测值与真实值对比图:
- 真实值较高时,预测值普遍偏低,说明模型倾向于低估高价房产。右侧的垂直聚集现象是数据阶段效应(最高50万美元)。
- 残差与预测值关系图:
- 随着预测值增大,残差的发散程度也在变大(漏斗形),说明模型预测高房价时非常不稳定。
- 残差分布直方图:
- 残差整体呈现右偏且带有长尾————再次印证了模型对部分高房价样本严重低估的问题。
交叉验证
交叉验证:剔除随机性带来的偏差
单次train/test split 的评估结果可能依赖于运气。为了获得更稳定、更具统计显著性的评估结果,我们采用K-Fold交叉验证。
- 将数据均匀分成K份。每一轮取1份作为测试集,剩余K-1份作为训练集。重复K轮,最终报告K次评估结果的均值和标准差。
from sklearn.model_selection import cross_val_score
# 注意:cross_val_score 会在内部完成fit和predict,不需要手动划分
scores = cross_val_score(
LinearRegression(), X_scaled, y,
cv=5, # 5 折交叉验证
scoring='neg_mean_squared_error'
)
# 将负MSE转换回RMSE
rmse_scores = np.sqrt(-scores)
print("每折 RMSE:")
for i, score in enumerate(rmse_scores):
print(f" Fold {i + 1}: {score:.4f}")
print(f"\n平均 RMSE: {rmse_scores.mean():.4f} "
f"± {rmse_scores.std():.4f}")标准差极小(约5.9%),说明线性回归在不同数据划分下表现非常稳定。
深入诊断:训练集 vs测试集
from sklearn.model_selection import cross_validate
cv_results = cross_validate(
LinearRegression(), X_scaled, y,
cv=5,
scoring={
'rmse': 'neg_root_mean_squared_error',
'mae': 'neg_mean_absolute_error',
'r2': 'r2'
},
return_train_score=True # 同时返回训练集上的得分(用于诊断过拟合)
)
print(f"{'指标':<2} {'训练集':>12} {'测试集':>10} {'差距':>8}")
print("-" * 48)
for metric in ['rmse', 'mae', 'r2']:
train_key = f'train_{metric}'
test_key = f'test_{metric}'
if metric == 'r2':
train_mean = cv_results[train_key].mean()
test_mean = cv_results[test_key].mean()
else:
train_mean = -cv_results[train_key].mean()
test_mean = -cv_results[test_key].mean()
gap = abs(train_mean - test_mean)
print(f"{metric.upper():<6} {train_mean:>12.4f} "
f"{test_mean:>12.4f} {gap:>10.4f}")核心发现:
- 未过拟合:训练集和测试集指标非常接近。
- 存在欠拟合:训练集上的表现本身也不算出色(
只有0.61),说明模型可能还没有充分挖掘数据中的信息。
欠拟合与过拟合
训练模型的困境
机器学习目标是训练出一个能够捕捉数据背后潜在规律的模型,使其不仅能够很好地拟合训练数据,还能对从未见过的新数据做出准确预测,这种能力被称为泛化能力。
例子
然而,模型并不总能具备良好的泛化能力,就像做菜时的火候————
🔥火太小,食材夹生(学得太少)
🔥火太大,外焦里糊(学得太多)
模型训练也是一样:学得太少或学得太多,都会导致预测效果不佳。
欠拟合
欠拟合(Underfitting)
模型过于简单
未能充分学习训练数据中的规律,导致其在训练集和测试集上的表现都很差。
生动比喻
就像一个学生只背了课本的目录,连课堂上讲过的例题都做不对,考试遇到新题更是无从下手。他不是粗心,而是压根没学进去。
过拟合
过拟合(Overfitting)
模型过于复杂
不仅学到了数据中的真实规律,还将噪声也一并记住了,导致其在训练集上表现优异,但在测试集上表现明显下降。
生动比喻
就像一个学生死记硬背了所有课后习题的标准答案,题目稍加变化就束手无策————他记住的是答案本身,而非解题方法。
欠拟合vs过拟合
| 状态 | 训练集表现 | 测试集表现 | 核心问题 |
|---|---|---|---|
| 恰好拟合 | ✅ 好 | ✅ 好 | 真正学到了规律 |
| 欠拟合 | ❌ 差 | ❌ 差 | 没学到规律 |
| 过拟合 | ✅ 很好 | ❌ 差 | 把噪声也当成了规律 |
学习曲线
诊断拟合情况的工具:学习曲线
学习取消可以展示误差随样本增加的变化趋势,假设数据的真实规律是
- degree = 1:线性模型,无法捕捉二次关系,导致欠拟合。
- degree = 2:二次多项式,与真实数据生成过程匹配,良好拟合。
- degree = 15:高次多项式,过度拟合训练数据中的噪声,导致过拟合。
三种情况实现:

结果分析:
- 图1(欠拟合):直线无法捕捉数据的弯曲趋势。
- 图2(良好拟合):二次多项式精准捕捉抛物线趋势,不被噪声带偏。
- 图3(过拟合):15次多项式精确穿过每个受干扰的样本,曲线扭曲怪异。
绘制学习曲线

结果分析:
- 欠拟合:训练和验证误差都高,且差距不大,加数据也没用。
- 良好拟合:训练和验证误差都低,逐渐收敛。
- 过拟合:训练误差极低,验证误差极高,差距大,加数据可能有帮助。
解决欠拟合
核心思路:让模型更有表达力(做加法)
1.特征工程
- 多项式特征:添加高次项。
- 特征交叉:组合多个特征。
- 数学变换:对数、指数等。
- 领域特征:结合业务知识。
2.增加训练轮数
增加epochs,给模型更多学习时间,指导损失不再下降。
3.增加模型复杂度
从简单模型(如线性回归)切换到复杂模型(如随机森林、决策树)。
from sklearn.tree import DecisionTreeRegressor
from sklearn.ensemble import RandomForestRegressor
from sklearn.preprocessing import StandardScaler
# 增加模型复杂度
models = {
"线性回归": make_pipeline(StandardScaler(), LinearRegression()),
"决策树(max_depth=5)": DecisionTreeRegressor(max_depth=5, random_state=42),
"随机森林(100棵树)": RandomForestRegressor(
n_estimators = 100, max_depth = 10, random_state = 42, n_jobs = -1
),
}
results = []
for name, model in models.items():
model.fit(X_train, y_train)
train_r2 = r2_score(y_train, model.predict(X_train))
test_r2 = r2_score(y_test, model.predict(X_test))
results.append({
"模型": name,
"训练 R²": train_r2,
"测试 R²": test_r2,
"差距 (R²)": train_r2 - test_r2,
})
df_results = pd.DataFrame(results)
print(df_results.to_string(index=False, float_format="%.4f"))
# 输出结果:
# 模型 训练 R² 测试 R² 差距 (R²)
# 线性回归 0.6107 0.5957 0.0150
# 决策树(max_depth=5) 0.7086 0.6905 0.0181
# 随机森林(100棵树) 0.9136 0.8068 0.1068解决过拟合
核心思路:让模型别太极端(做减法)
- 正则化(最通用):给模型复杂度加成法,限制权重大小,防止死记硬背。
- 增加训练数据:更多数据让噪声被稀释(数据获取成本低时)。
- 降低模型复杂度:使用更简单的模型(如减少多项式阶数或树的深度)。
- 特征选择:主动去掉无关或冗余的特征,减少干扰信号。
- 早停(Early Stopping):在验证集误差开始上升时,及时停止训练过程。
- Dropout / 数据增强:深度学习中的常用防过拟合高级技巧。
正则化
核心思想:在损失函数中额外加一个惩罚项,系数越大罚得越重,让模型尽可能用小的系数拟合。
L2正则化(Ridge/岭回归)
惩罚所有权重的平方和:
将权重均匀压缩到接近0的小值,但不会恰好等于0。表现稳定。
L1正则化(Lasso)
惩罚所有权重的绝对值之和:
将不重要特征的权重直接压缩到0,相当于自动进行特征选择。
名词溯源:
正则化(regulation)
本意是使事物变得规则、整齐、正常。没加正则化时,模型为了迎合噪声会疯狂扭曲(不正常);加上正则化后,强制让模型“别太极端”,回归到平滑、正常的简单形态。
L2(Ridge)
没有正则化时,损失函数像一个深邃的峡谷。加上L2惩罚后,损失函数的集合形状会在原点附近隆起一道“山脊(Ridge)”,把参数“顶住”,限制在较小范围内,防止它们在峡谷深处失控。
Lasso的由来
Lasso是缩写(最小绝对值收缩与选择因子,Least Absolute Shrinkage and Selection Operator)。恰好与英文lasso(套索)同形,非常形象————它就像牛仔的套索,把不重要的无用特征直接“勒”到0,天然具备特征选择能力。
-2.0143x^1 +21.5388x^2 +30.3905x^3 +-297.6976x^4 +-304.5768x^5 +1753.8255x^6 +1812.4747x^7 +-5043.0462x^8 +-5718.8704x^9 +7569.2743x^10 +9363.0127x^11 +-5676.3405x^12 +-7504.0952x^13 +1676.5888x^14 +2325.6142x^15
-0.0869x^1 +1.4014x^2 +-0.0688x^3 +0.8328x^4 +-0.0097x^5 +0.4300x^6 +0.0421x^7 +0.1767x^8 +0.0737x^9 +0.0174x^10 +0.0924x^11 +-0.0878x^12 +0.1048x^13 +-0.1619x^14 +0.1149x^15
2.4803x^2 +0.0809x^4 +0.0064x^13 +0.0729x^15
逻辑回归核心知识
分类问题
线性回归能预测房价、股票价格这样的连续数值。但现实业务中,有大量的问题本质上是在做选择题:
- 这封邮件是不是垃圾邮件?
- 这个肿瘤是良性还是恶性?
- 用户会不会点击广告?
我们把预测离散标签、输出有限个互斥类别的任务称为分类(Classification)。
总结:
线性回归解决是多少的问题,逻辑回归解决属于哪一类的问题。
为什么需要逻辑回归?
为什么不能直接用线性回归预测,大于0.5算1、小于0.5算0?
1.输出范围失控
线性回归的输出是
2.极易受异常值影响
假设数据里多了一个极其巨大的异常样本,整条回归线就会被严重拉偏,导致原本预测准确的普通样本被错误分类。
💡解决方案:为线性回归叠加一个变形器————这就是逻辑回归。
Sigmoid函数
为了将任意实数映射到0到1之间的概率值,逻辑回归引入了Sigmoid函数:
把线性回归结果
- 逻辑回归 = 线性回归 + Sigmoid。前半段算分数,Sigmoid把分数转成概率。

使用Sigmoid的好处
它完美契合了分类任务的三个核心需求:
- 输出天然具有概率含义
- 严格限制在(0, 1)之间,0.8就代表模型有80%的把握认为是正类。
- 自带决策分界点
。 判为正类, 判为负类。决策边界就是
- 处处可导,且导数极简
- 相比阶跃函数,sigmoid连续平滑,导数公式极其简洁:
,极大提升梯度下降效率。
- 相比阶跃函数,sigmoid连续平滑,导数公式极其简洁:
分类问题的损失函数
为什么不使用MSE作为损失函数?
线性回归 + MSE = 凸函数
像一个光滑的碗,无论初始参数怎么设置,梯度下降都能找到全局最优解。
逻辑回归 + MSE = 非凸函数
引入非线性的Sigmoid后,损失函数变得坑坑洼洼,极易陷入局部最优,且存在梯度消失问题。
分类问题损失的特征
对于二分类任务,模型输出的是概率p。一个合理的损失函数应该满足:
- 如果真实标签 y = 1,那么p越接近1越好,越接近0惩罚越大。
- 如果真实标签 y = 0,那么p越接近0越好,越接近1惩罚越大。
WARNING
⚠️常见误区:p = 0.5并不是损失最大,而是模型最不确定。
真正损失最大的时候,是模型非常自信但预测错了(如真实y=1,却预测
为了设计这样的函数,我们借用数学工具:对数函数(log)、伯努利分布 和 最大似然估计。
对数函数
对数函数(Logarithm)
log的优势:
- 单调性保持一致
- 对数函数是严格单调递增的。最大化某个概率,完全等价于最大化它的对数。这让优化过程变得简单。
- 防止数值下溢出
- 多个介于0和1之间的概率相乘,结果会迅速趋近于0,导致计算机无法精确表示。取对数后,乘法变成了加法:
- 多个介于0和1之间的概率相乘,结果会迅速趋近于0,导致计算机无法精确表示。取对数后,乘法变成了加法:

💡观察
当x -> 1时, y -> 0
当x -> 0时, y ->
这完美符合我们对“惩罚”的定义!
伯努利分布
二分类问题的本质,就是一个只有两种结果的随机试验(比如抛不均匀的硬币)。
- 统一概率公式
假设结果为1的概率p,结果为0的概率是1-p:
数学家巧妙地用一个公式把两种情况统一了起来:
- 结合逻辑回归
在逻辑回归中,模型预测的
将
最大似然估计(MLE)
最大似然估计:既然事情已经发生,那么让这件事情发生的概率最大的参数,就是最好的参数。
我们要找到一组参数
- 似然函数
所有样本预测正确的概率乘积。
- 取对数(Log-Likelihood)
将连乘变为连加,方便求导。
💡在统计学中希望最大化似然函数,但在机器学习中需要最小化损失函数。因此只需在前面加个负号并求平均,这就得到了交叉熵损失函数!
交叉熵损失
交叉熵损失(Cross-Entropy Loss)
| 真实标签 | 损失项 | 含义 |
|---|---|---|
| 预测概率越接近 1,损失越小;接近 0 时损失 | ||
| 预测概率越接近 0,损失越小;接近 1 时损失 |
交叉熵是一个严格的凸函数,无论初始权重在哪里,都能引导模型快速且稳定地收敛到全局最优点。
梯度下降
逻辑回归的梯度推导过程中,Sigmoid的导数和交叉熵的导数恰好大量约分抵消,最终结果和线性回归的形式一模一样。
| 线性回归 | 逻辑回归 | |
|---|---|---|
| 权重梯度 | ||
| 偏置梯度 | ||
分类问题实战
使用sklearn内置乳腺癌数据集演示完整训练流程。
目标:二分类————恶性(0)/良性(1)。
训练集:455条数据
测试集:114条数据
训练集癌变率(恶性占比):62.64%
测试集癌变率(恶性占比):63.16%
测试集准确率:0.9825
模型评估
分类任务不能只看准确率。混淆矩阵把所有预测结果按「真实类别 x 预测类别」排列,是分类评估的基础。

| 预测为正 | 预测为负 | |
|---|---|---|
| 实际为正 | TP (真正例)✅ | FN (假负例)❌ 漏报 |
| 实际为负 | FP (假正例)❌ 误报 | TN (真负例)✅ |
WARNING
⚠️FN(漏报恶性)在医疗场景中危害最大,患者可能因此延误治疗。
准确率、召回率、F1
从混淆矩阵推导出三个核心指标:
准确率(Precision)
预测为正类的样本中,真正是正类的比例(预测质量)。
召回率(Recall)
所有真正的正类中,被成功预测出来的比例(覆盖能力)。
F1 Score
准确率和召回率的调和平均,两者都高,F1才高。
💡权衡:降低决策阈值可以提高召回率(漏报更少),但会降低精确率(误报更多)。在乳腺癌场景中,恶性的召回率是最重要的————宁可误报,不能漏报。
ROC曲线
准确率/召回率依赖于决策阈值。ROC曲线通过遍历所有阈值,完整展示模型表现:
- 横轴(FPR):假正率,负类被误判为正类的比例。
- 纵轴(TPR):真正率(召回率),正类被正确识别的比例。
AUC(Area Under Curve)
ROC曲线下面积,取值0~1。AUC越大,模型区分正负类的能力越强,且与阈值无关,适合模型间横向对比。

多分类与深度学习
多分类进阶:Softmax
逻辑回归(Sigmoid)解决的是非黑即白的二分类问题。如果面临多个类别(如猫、狗、猪),我们需要升级为Softmax回归:
- 为每个类别计算一个独立得分。
- 通过Softmax函数,将所有得分转化为概率分布。
- 所有类别的概率总和严格为1。
通向深度学习的桥梁
逻辑回归不仅是传统的机器学习算法,它与深度学习有着极深的渊源:
本质
逻辑回归本质上就是一个没有隐藏层的单层神经网络!
- 输入层:特征
- 权重与偏置:神经网络的连接权重w和偏执b
- 激活函数:Sigmoid / Softmax
- 输出层A:逻辑回归 + Softmax 正是现代神经网络最常用的输出层结构。
线性回归vs逻辑回归
| 维度 | 线性回归 | 逻辑回归 |
|---|---|---|
| 解决什么问题 | 回归(预测数值) | 分类(预测类别) |
| 模型输出 | 任意实数 | 0~1 的概率 |
| 损失函数 | MSE | 交叉熵 (Cross-Entropy) |
| 评估指标 | MSE / RMSE / R² | Accuracy / F1 / AUC |
- 逻辑回归本质上是在线性回归的基础上加了一个 Sigmoid。
- 分类问题不能只看准确率,要根据业务场景选择 Precision / Recall / F1。
- 逻辑回归虽然简单,但可解释性强、训练快、不容易过拟合,工业界依然大量使用。
- 扩展到多分类时,使用 Softmax Regression(神经网络中最常见的输出层)。
集成学习重点知识
决策树
例子:
小张这周末去打球吗?
小张是否出门打球,很大程度上取决于天气情况。下表纪录了过去10个周末的天气数据:
| Outlook (天气) | Temperature (温度) | Humidity (湿度) | Windy (刮风) | Play (打球) |
|---|---|---|---|---|
| Overcast | Hot | High | True | Yes |
| Overcast | Mild | Normal | False | Yes |
| Rain | Mild | High | False | Yes |
| Rain | Cool | Normal | False | Yes |
| Sunny | Cool | Normal | False | Yes |
| Sunny | Mild | High | True | No |
| Rain | Mild | High | True | No |
| Overcast | Cool | Normal | True | Yes |
| Sunny | Hot | High | False | No |
| Sunny | Mild | High | False | No |
像人类一样提问:
假设今天天气是“晴天、炎热、正常湿度、不刮风”,你的大脑会自动进行如下逻辑推理:
- 先看整体天气:只要是多云,风雨无阻,一定会去。
- 如果是晴天:看湿度。湿度高不去,湿度正常才去。
- 如果是雨天:看风力。刮风不去,不刮风才去。
决策树(Decision Tree)是一种模仿人类决策时层层提问方式的机器学习算法。它将数据按照不同特征进行递归分裂,最终形成倒立的树状结构。
- 内部节点:代表对某个特征的判断问题(如“天气是晴天吗?)
- 树枝分支:代表判断后的不同答案(如:是/否)
- 叶节点:代表最终的决策结果(如:去打球/不去)
💡决策树是最具可解释性的机器学习模型之一,决策路径完全透明。
衡量纯度的指标
决策树做预测的过程非常简单,问题的关键在于如何从众多特征中挑选出在哪个特征上进行分裂。
模型怎么知道第一步该问天气而不是温度?
核心概念是:纯度(Purity)。让结果越来越纯
❌如果按“温度”分组(纯度低)
每个温度分组下,去与不去的结果混杂在一起,非常混乱。
| Temp | Play |
|---|---|
| Hot | No, Yes |
| Mild | Yes, Yes, No, No, Yes |
| Cool | Yes, Yes, Yes |
✅如果按“天气”分组(纯度高)
多云分组所有标签都是Yes,这种情况我们就说这个节点的纯度极高。
| Outlook | Play |
|---|---|
| Sunny | No, No, Yes, No |
| Overcast | Yes, Yes, Yes |
| Rain | Yes, Yes, No |
熵与信息增益
决策树在选择最佳分裂特征时,需要一个数学指标来量化节点的混乱程度或纯度。
1. 熵(Entropy)
概念来自于物理和信息论,用来衡量一个节点中各类别分布的混乱程度。
- 一半去打球、一半不去(50% vs 50%),悬念最大,熵值最高,最混乱。
- 全部去或全部不去(100% vs 0%),毫无悬念,熵值为0,最纯净。
2. 信息增益(Information Gain)
用某个特征分裂之后,混乱程度下降了多少。
用这个特征提问后,消除了多少不确定性。信息增益越大,说明特征区分能力越强。
基尼系数
为了解决熵计算复杂的问题,工业界更倾向于使用基尼系数(Gini Impurity)。
直观理解:随机从节点中抽取两个样本,它们属于不同类别的概率。
- 基尼系数越小,说明节点越纯(完全纯净时为0)。
- 计算极快:只有乘法和加法,没有耗时的对数运算。
- 工业标准:是scikit-learn库中决策树(CART算法)的默认指标。
决策树的生长过程
构建决策树是一个不断寻找最优特征进行划分的递归流程:
- 计算指标:计算当前节点使用各项特征划分后的基尼系数。
- 选择最优:选择划分后基尼系数最小(纯度提升最大)的特征。
- 分裂数据:按该特征的不同取值,将数据分裂成子节点。
- 递归生长:对每个子节点重复上述过程,继续向下生长。
- 生成叶节点:满足停止条件时停止分裂,确定最终预测结果。
使用scikit-learn自带的经典鸢尾花(Iris)数据集(150朵花,4个特征,3个品种)。
import pandas as pd
from sklearn.datasets import load_iris
# 1. 加载数据集
iris = load_iris()
# 2. 为了方便阅读,我们将英文特征名和类别名翻译成中文
feature_names_cn = ['花萼长度(cm)', '花萼宽度(cm)', '花瓣长度(cm)', '花瓣宽度(cm)']
target_names_cn = ['山鸢尾', '变色鸢尾', '维吉尼亚鸢尾']
# 3. 转换成表格(DataFrame)形式展示
df = pd.DataFrame(iris.data, columns=feature_names_cn)
df['最终分类结果'] = [target_names_cn[i] for i in iris.target]
print("============ 1. 数据集前5行预览 ==============")
print(df.head())
print("============ 2. 数据集包含的分类及数量 =========== ")
print(df['最终分类结果'].value_counts())决策树训练及展示
决策树模型的预测准确率为:100.00% 
节点信息解读:
- samples:节点包含的总样本数。
- value:各类别样本分布。
- gini:基尼不纯度(0表示完全纯净)。
- class:当前节点的默认类别。
通过可视化,我们可以清晰地看到模型是如何一步步通过花瓣长度和花瓣宽度来将三种鸢尾花区分开的。
结论:花瓣尺寸决定了种类
这是根据图片内容转换的 Markdown 表格:
| 鸢尾花类别 | 关键阈值 | 节点纯度 |
|---|---|---|
| 山鸢尾 | 花瓣长度 ≤ 2.45 | 完全纯净 (gini=0) |
| 变色鸢尾 | 2.45 < 花瓣长度 ≤ 4.75 and 宽度 ≤ 1.6 | 完全纯净 (gini=0) |
| 变色鸢尾 | 花瓣长度 > 4.75 and 1.6 < 宽度 ≤ 1.75 | 混合较多 (gini=0.5, 与维吉尼亚鸢尾有重叠) |
| 维吉尼亚鸢尾 | 2.45 < 花瓣长度 ≤ 4.75 and 宽度 > 1.6 | 完全纯净 (gini=0) |
| 维吉尼亚鸢尾 | 花瓣长度 > 4.75 and 宽度 > 1.75 | 整体较纯 (gini≈0.06, 含少量杂质) |
- 花瓣越短 --> 越可能是山鸢尾
- 花瓣中等 --> 多为变色鸢尾
- 花瓣最长 --> 多为维吉尼亚鸢尾
- ⚠️注意:花瓣长度 >4.75、1.6 < 宽度
1.75 时变色鸢尾与维吉尼亚有重叠,这是模型容易误判的地方。
剪枝
剪枝(pruning)
防止过拟合
如果任由决策树肆意生长,它会为了把每一个异常值都分对,长出无数繁琐的分支,导致过拟合。我们需要剪掉多余细节,保留核心主干。
1. 预剪枝(Pre-pruning)
在树往下生长的过程中,提前设定规则,触及规则强制停止。
- 限制最大深度(max_depth)
- 限制叶节点最少样本数
✅实现简单、计算量小,工业广泛使用。
2. 后剪枝(Post-pruning)
先让树毫无保留地生长到最大,然后再从下往上检查,剪掉不影响准确率的分支。
✅理论效果更好,但大数据下计算成本高。
决策树总结
决策树直观、易解释,但单棵树稳定性差、容易过拟合。既然一棵树容易犯错,那我们就种一整片森林!这就是集成学习(Ensemble Learning)。
🌳 随机森林(Random Forest)
构建很多棵相互独立的决策树。每次随机抽取部分数据和特征。面对位置数据时,所有树独立预测,投票表决,少数服从多数,极大降低误判风险。
🚀 梯度提升树家族(GBDT / XGBoost / LightGBM)
树与树之间不再独立,而是前赴后继。第二棵树专门纠正第一棵树的错误,第三棵树纠正第二棵树...通过不断迭代,把模型精度推向极致。
随机森林
决策树的困境
✅优势:极佳的可解释性
模拟人类思维路径,通过一连串“如果...那么...“的逻辑判断,将复杂数据层层剥离。这种白盒特性使其过程清晰透明。
❌劣势:天然的脆弱性
倾向于“死记硬背”细节与噪声,导致严重的过拟合。对数据极其敏感,微小变动即可引起树结构剧烈震荡,缺乏稳健型。
- 为了处理决策树不稳定的风险,Leo Breiman于2001年正式提出了随机森林。
核心思想:集成学习
随机森林属于集成学习(Ensemble Learning):将多个弱学习器合成一个强学习器,以获得比单一模型更好的泛化性能。
专家会诊模式
- 不再依赖一棵树做决定,而是通过随机采样建成百上千棵各不相同的决策树。
- 预测时让所有树独立给出判断,最终汇总结果(分类投票 / 回归求平均)。
- 有效中和了单棵树的偏见与错误,让模型既准确又文件。
为什么叫随机森林?
顾名思义,这个名字包含两个核心词汇:“随机”和“森林”。
- 森林(Forest):由大量决策树组成。决策树是基础模型,通过一系列 IF-THEN 规则对数据逐层切分。
- 随机(Random):算法的灵魂。如果所有树都用完全一样的数据和特征训练,投票就失去了意义。
为了保证每棵树的多样性,算法引入了双重随机性。
双重随机性
样本随机
Bootstrap(有放回抽样)
假设训练集有N个样本,每次训练一棵树时:
- 从原数据集中随机放回地抽取N个样本,构成该树专属的训练子集。
- 由于是有放回的,部分样本会被重复抽到,部分样本则完全不会出现。
- 结果:使得每棵树使用的样本都略有不同,保证了基础的差异性。
特征随机
Random Subspace(特征子空间)
- 在决策树的每个节点进行分裂时,并不考虑全部特征。
- 而是随机抽取其中一个子集(通常取
个,d为总特征数),仅在这些候选特征中寻找最优分裂点。
为什么关键?
如果没有特征随机,所有树都会优先选择最强的特征(如“收入”)作为根节点,导致树结构高度相似。
加入特征随机后,有些树被迫用其他特征分裂,提供了不同的视角。个体不必完美,多样性才是力量。
OBB样本价值
免费的验证集:袋外估计(OBB)
每次Bootstrap采样时,某个样本在N次抽取中始终未被选中的概率为:
- 每棵树大约有1/3的样本没参与训练,这些被称为袋外样本(Out-of-Bag,OOB)。
- 零浪费机制:天然的验证集,省去了额外划分验证集和交叉验证的繁琐开销。
- 所有数据都能直接投入训练并同步支持超参数调优,在小数据场景下尤为珍贵。
随机森林实战
算法工作流程:
- 构建专家团:
- 设定森林中树的数量
n_estimators(如100、500)。
- 设定森林中树的数量
- 逐棵生成决策树
- Boostrap抽取N个样本作为训练集。
- 节点分裂时,随机选取m个候选特征找最佳切分点。
- 让树充分生长(通常不剪枝)。
- 集成输出
- 分类任务:多数表决
- 回归任务:简单平均
- 分类任务:多数表决
使用100棵决策树组成随机森林,对鸢尾花数据集做训练与验证
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
# 加载数据
X, y = load_iris(return_X_y=True)
X_train, X_test, y_train, y_test = train_test_split(
X, y,
test_size=0.3,
random_state=42, # 固定随机种子,以确保结果可复现
)
# 构建随机森林
rf = RandomForestClassifier(
n_estimators=100, # 树的数量
max_features='sqrt', # 每次分裂时随机选取的特征树
oob_score=True, # 启用OOB估计
random_state=42,
n_jobs=-1 # 并行训练
)
rf.fit(X_train, y_train)
# 评估
print(f"OOB 准确率:{rf.oob_score_:.4f}")
print(f"测试集准确率:{accuracy_score(y_test, rf.predict(X_test)):.4f}")
# OOB 准确率:0.9429
# 测试集准确率:1.0000- OOB 准确率 0.9429:模型仅利用训练中产生的袋外数据,就达到了94%的准确率。
- 测试集准确率100%:得益于鸢尾花数据集较小且特征明显。
特征重要性
工业界最常用的能力之一:帮助理解哪些因素在起作用,哪些特征可以舍弃
基于不纯度减少(MDI)
统计特征在所有节点带来的不纯度下降量之和。计算快(sklearn默认),但对高基数特征(如ID列)有偏好。
基于排列的重要性(Permutation)
将特征值随机打乱,观察模型性能下降程度。
下降越多越重要。更可靠,不受基数影响,目前更推荐。

随机森林优劣
核心优势:
用样本随机 + 特征随机 制造多样性,再用集体投票消除个体偏差。
它未必是精度最高的模型,但几乎总是最值得首先尝试的那一个。
- 抗过拟合能力强:极大降低了模型的方差。
- 极简的数据预处理:无需归一化,能处理缺失值,对异常值不敏感。
- 高维数据友好:从容应对成千上万特征,无需显式降维。
- 附赠特征重要性:自然输出特征贡献度,利于业务归因分析。
- 天然支持并行计算:树的生成完全独立,完美利用多核CPU加速。
局限性:
WARNING
黑盒模型:虽然单棵决策树可解释性极强,但成百上千棵树组合在一起后,就变成了难以直观解释的黑盒。
计算与内存开销大:保存海量的树结构需要较大的内存空间;预测时每条数据需穿过所有树,预测速度相对较慢,不适合极其追求毫米级延迟的在线场景。
极端噪音敏感:在噪音极大的分类任务中,仍然有一定概率出现过拟合。
Bootsing模型
Bagging vs Boosting
集成学习有两大主流思想,目标都是把一群弱模型组合成一个强模型,但实现路径截然不同。
Bagging:少数服从多数
- 有放回采样:给每棵树准备一份不同的训练数据;随机选特征,让每棵树学到不同的视角。
- 最终投票/平均:综合所有树意见给出结果。
- 代表算法:随机森林。
Boosting:接力纠错
- 串行训练:树依次训练,一棵接一棵。
- 专注错误:每棵新树专注于前面树犯的错。
- 累加预测:最终所有树的预测累加起来。
- 代表算法:XGBoost、LightGBM、CatBoost。
Boosting:接力纠错
INFO
打高尔夫球的例子(目标:350米)
- 🏌️♂️第一杆(第1棵树):大力开球,打了300米。离球洞还差:350 - 300 = 50米。
- 🏌️♂️第二杆(第2棵树):目标不是350米,而是“补上这50米”。打了45米。离球洞还差:5米。
- 🏌️♂️第三杆(第3棵树):目标是“补上这5米”。打了4.5米。离球洞还差:0.5米。
- 🏌️♂️第四杆(第4棵树):推杆,补上最后0.5米。进洞!⛳️
最终结果 = 300 + 45 + 4.5 + 0.5 = 350 米
每个新模型都在拟合前面模型的残差(错误),接力合作,逐步逼近真实答案。
| 维度 | Bagging(随机森林) | Boosting(梯度提升树) |
|---|---|---|
| 训练方式 | 多棵树并行独立训练 | 多棵树串行依次训练 |
| 每棵树的目标 | 学习原始目标 | 学习前面模型的残差 |
梯度提升决策树
GBDT(Gradient Boosting Decision Tree)核心原理
前面说每棵新树学习上一轮的残差,为什么偏偏要叫梯度提升树?
(用梯度下降的方式来让能力越来越强)
GBDT的模型是多棵树的累加:
它不像线性回归有固定维度的参数
这是根据图片内容转换的 Markdown 表格:
| 经典梯度下降 | GBDT | |
|---|---|---|
| 优化对象 | 参数 | 函数 |
| 下降方向 | 负梯度 | 负梯度 |
| 更新方式 |
GBDT每一轮在做什么?
- 计算每个样本上的负梯度,即当前模型最应该修正的方向。
- 训练一棵决策树去拟合这些负梯度值。
- 把这棵树乘以学习率加到现有模型熵,相当于沿着下坡方向走一小步。
当损失函数是均方无擦好(MSE)时:
对
在MSE下,负梯度恰好就是残差。这就是为什么前面说每棵新树拟合残差。
损失函数与负梯度
换成其他损失函数,负梯度就不再是简单的残差了:
| 损失函数 | 负梯度(每棵新树拟合的目标) |
|---|---|
| 均方误差(MSE) | |
| 绝对误差(MAE) | |
| 对数损失(分类) |
- Gradient(梯度):用负梯度来指引每棵新树的学习方向。
- Boosting(提升):多棵弱决策树逐步增加,模型能力不断提升。
手动实现一个简易版GBDT,并可视化逐步纠错的过程:

残差逐轮缩小的过程:

GBDT的优缺点
优点:
- 预测精度高:在结构化/表格数据上长期霸占各类竞赛和业务场景。
- 处理混合特征:数值型、类别型都能用。
- 自带特征重要性:帮助理解哪些特征影响最大。
- 对特征放缩不敏感:不需要标准化或归一化。
- 损失函数灵活:适配各种业务需求。
缺点:
- 训练速度较慢:串行训练,树之间无法并行。
- 容易过拟合:数据量少、噪声大时需仔细调参。
- 对超参数敏感:学习率、树的数量、深度等需认真调节。
- 不擅长超高维稀疏数据:如文本的one-hot编码。
- 类别特征处理不够原生:经典GBDT需手动编码。
XGBoost
2014年陈天奇开发的XGBoost(eXtreme Gradient Boosting)可能是机器学习历史上最具影响力的开源项目之一。
核心改进:
- 正则化:在目标函数中显式加入树复杂度的惩罚项(叶节点数量 + 叶节点权重的L2正则),有效防止过拟合。
- 二阶梯度信息:经典GBDT只用一阶梯度,XGBoost同时利用二阶梯度,使得分裂点选择更精准。
- 工程优化:支持列采样、缓存感知访问、核外计算等,使得单机处理上亿样本称为可能。
- 内置正则化和Early Stopping:训练过程更可控。
LightGBM
2017年微软推出的LightGBM主打一个字:快(大数据集训练速度是XGBoost的5~10倍)。
核心改进:
- 直方图算法:XGBoost默认会去遍历所有数据点寻找分裂点;而LightGBM将连续的浮点特征分段(分桶)成离散的直方图。寻找分裂点时只需要遍历这些分桶,计算量呈指数级下降,且内存占用极小。
- 带深度限制的Leaf-wise生长策略:传统决策树是一层一层整齐生长的(Level-wise)。LightGBM则是谁的残差大就顺着谁往下长(Leaf-wise),这导致树的结构不对称,但在同等叶子节点下能达到更高的精度。
- 引入了单边梯度采样(GOSS)和互斥特征捆绑(EFB)两大黑科技,进一步将速度推向极限。
CatBoost
2017年俄罗斯互联网巨头Tandex针对类别特征的处理推出的CatBoost(Category Boosting)。在处理具有大量类别型特征的业务场景中,它是当之无愧的王者。
核心改进:
- 原生支持类别特征:以前用XGBoost,开发者必须手动把类别特征转换成One-hot编码。CatBoost可以直接把类别型特征扔进去,它内部有一套极强的方法(Target Statistics)自动处理。
- 排序提升:GBDT家族有一个隐患叫预测偏移,即当前训练数据的目标值影响了当前模型的构建。CatBoost通过一种极其严谨的排序策略,确保在训练每一个样本时,只使用排在它前面的样本信息,极大地提升了泛化能力。
- 对称树结构:它强制生成的决策树是完全对称的,这使得预测时的速度(推理速度)比XGBoost和LightGBM都要快得多。
Boost模型对比
| 场景 | 推荐框架 |
|---|---|
| Kaggle 竞赛,追求极致精度 | XGBoost 或 LightGBM |
| 大数据集(>100万样本),需要快速训练 | LightGBM |
| 数据中有大量类别特征 | CatBoost |
==================================================
指标 随机森林 XGBoost
==================================================
MSE 0.2951 0.2132
R^2 Score 0.7748 0.8373
训练时间(秒) 0.99 0.22XGBoost的早停机制:
[0] validation_0-rmse:1.11974
[50] validation_0-rmse:0.58075
[100] validation_0-rmse:0.51449
[150] validation_0-rmse:0.49533
.....
[1200] validation_0-rmse:0.44035
[1206] validation_0-rmse:0.44039
最佳迭代轮数:1186
最佳 MSE:0.19387836065052816Early Stopping 让你不用纠结到底该设多少棵树————设一个足够大的上线,让算法自己找到最佳停止点。
XGB训练可视化:

验证集的RMSE先快速下降,然后趋于平缓,最后可能略微上升(过拟合信号),Early Stopping恰好在最低点附近停下来。
总结
这是根据图片内容转换的 Markdown 表格及下方的总结文本:
| 维度 | 决策树 | 随机森林 | GBDT / XGBoost |
|---|---|---|---|
| 模型数量 | 1棵 | 多棵(并行) | 多棵(串行) |
| 集成策略 | 无 | Bagging:投票/平均 | Boosting:逐步累加 |
| 单棵树特点 | 可深可浅 | 深树(低偏差高方差) | 浅树(高偏差低方差) |
| 核心目标 | 基线模型 | 降低方差 | 降低偏差 |
| 过拟合风险 | 高 | 低 | 中等 |
总结:
- 决策树是基础模型——简单符合直觉,但容易过拟合。
- 随机森林用 Bagging 思想,让多棵树并行投票,降低方差。
- GBDT用 Boosting 思想,让多棵树串行纠错,降低偏差。
- XGBoost / LightGBM / CatBoost是工业级进化,更快、更准、更易用。
无监督学习重点知识
无监督聚类问题
现实中我们经常会遇到没有标签的数据。数据本身不会直接告诉我们答案,但隐含着某种结果或规律,需要通过算法自动发现。
- 用户画像与分群:根据购买频次、消费金额等,将用户划分为高价值、潜力或流失风险用户。
- 图像压缩:从数十万种颜色种自动找出若干典型代表,减少存储空间。
- 异常检测:识别偏离常规的数据点(如异常访问、恶意攻击),智能通过数据分布特征去识别。
聚类
所谓聚类,就是在没有标签的前提下,根据样本之间的相似性,将数据自动划分为若干组,使得同组内样本尽可能相似,不同组之间尽可能有差异。
K-Means
K-Means是一种经典的聚类算法,用于将一批无标签数据自动划分为K个簇。
- K:预先指定的簇数。
- Means:均值,即每个簇的中心位置由该簇内所有样本的均值来表示。

目标:
找到K个具有代表性的中心点,使每个样本归属于离自己最近的中心,并尽可能减小样本到中心的距离。
质心与距离度量
- 质心(Centroid)
质心就是一个簇的中心位置,通常取该簇所有点在各个维度上的均值。
例如,点(1, 2), (2, 3), (3, 4)的质心为:
- 距离度量
最常用的距离度量方式是欧氏距离(直线距离)。
二维平面中A、B两点的距离:
当维度扩展到n维时:
簇内相似度
簇内相似度
K-Means追求的是:同一个簇中的样本尽可能彼此接近,并且尽量靠近该簇的质心。
通常通过簇内平方和SSE(Sum of Squared Errors)来衡量:
表示第i个簇, 表示第i个簇的质心。 表示样本点到质心的平方距离。 - SSE越小,说明簇内样本越紧凑,聚类效果通常越好。
K-Means的迭代流程
K-Means的核心就是不断循环:选中心 -> 分簇 -> 更新中心 -> 再分簇 -> 直到稳定。
- 初始化质心:预先指定K,随机选取K个初始中心点。
- 分配样本:计算每个样本到各质心的距离,划分到距离最近的簇中。
- 更新质心:根据当前簇中的所有样本,更新计算该簇的中心点(均值)。
- 重复迭代:重新分配样本并更新质心,不断循环。
- 收敛停止:当质心位置不在变化、样本归属不再变化,或达到最大迭代次数时停止。

算法在第5次迭代后完全收敛!中心点不在移动。 
K-Means的优化
优化1:初始质心选择(K-Means++)
随机选择初始质心可能导致算法陷入较差的局部最优解。不同的初始化方式,最终得到的聚类结果可能存在明显差异。
解决方案:K-Means++
先随机选取一个样本点作为第一个质心;之后,在选择下一个质心时,优先考虑那些距离当前已有质心较远的样本点。重复这一过程,知道选出K个初始质心。
SSE: 362.47
迭代次数: 3 
优化2:肘部法则选择K值
K-Means最大的问题时簇数K需要预先制定。如何找到性价比最优的K值?
- 依次尝试不同的K值,计算对应的SSE。
- 随着K增大,SSE会持续下降。
- 当K超过某个值,SSE下降幅度明显减小,曲线趋于平缓。
- 这个斜率突然减小、曲线由陡转平的拐点(肘部),就是较优的K值选择。

K-Means总结
✅K-Means的优点
- 思想简单,容易理解:流程非常清晰。
- 计算效率高:适合大规模数据和工程应用。
- 易于实现:最常见的聚类入门算法。
- 结果可解释性强:每个簇都有明确的中心。
⚠️K-Means的局限性
- 需要预设K:选择不当会严重影响结果。
- 对初始值敏感:容易陷入局部最优。
- 只能发现球形簇:无法处理环形灯非凸形状。
- 对异常值敏感:极端值会显著拉偏质心。
- 特征尺度敏感:使用前需要进行标准化。
其他聚类模型
K-Medoids(中心点聚类)
与 K-Means 取均值不同,K-Medoids 强制选取实际存在的数据点作为质心。这使得它对噪声和极端异常值具有更强的鲁棒性。
DBSCAN(基于密度的聚类)
不需要预先指定 K 值!它通过样本的密度来划分簇,能够发现任意形状(如环形、月牙形)的簇,并且自带异常点检测能力。
层次聚类(Hierarchical Clustering)
通过计算节点之间的距离,自底向上(或自顶向下)构建一棵聚类树。同样不需要预先指定 K 值,适合需要多层次分类的场景。
聚类模型常见应用
聚类算法常作为数据探索、特征工程和系统冷启动等环节的重要组成部份。
- 客户分群:电商用户RFM分层,自动识别用户价值层级,支撑精准运营。
- 图像处理:颜色量化、图像分割,压缩颜色空间,划分图像语义区域。
- 异常检测:网络入侵、信用卡欺诈,将原理所有簇中心的样本标记为潜在异常。
- 推荐系统:协同过滤的冷启动,将新用户/物品归入相似簇,快速生成初始推荐。
图像颜色压缩
使用K-Means把彩色图片里成千上万种颜色,压缩成4种、16种、64种颜色:

