什么是机器学习
机器学习是教会计算机从数据里找规律,而不是靠手写规则。
类型: Learn 语言: Python 前置要求: 阶段 1(数学基础) 预计时间: ~45 分钟
学习目标
- 说清监督学习、无监督学习和强化学习的区别,并判断某个问题属于哪一类
- 从零实现一个最近质心分类器,并和随机基线对比评估
- 区分分类任务和回归任务,为各自挑选合适的损失函数
- 判断一个业务问题到底适合用 ML,还是用确定性规则解决更好
问题背景
你想做一个垃圾邮件过滤器。传统做法是:坐下来写几百条规则。"邮件里出现 'FREE MONEY' 就标垃圾。超过 3 个感叹号就标垃圾。"你花几周写规则。然后垃圾邮件发送者换了说法,你的规则失效。你再写更多规则。这个循环永远停不下来。
机器学习把这件事反过来。你不写规则,而是给计算机几千封标好的邮件("垃圾"或"非垃圾"),让它自己琢磨出规则。计算机找出的规律是你压根想不到的。当垃圾邮件发送者改变套路时,你用新数据重新训练,而不是改代码。
从"编写规则"到"从数据学习"的这一转变,正是机器学习的核心。每一个推荐引擎、语音助手、自动驾驶汽车和语言模型,都是这么工作的。
核心概念
从数据学习,而非从规则
传统编程和机器学习解决问题的方向正好相反。
flowchart LR
subgraph Traditional["传统编程"]
direction LR
R[规则] --> P1[程序]
D1[数据] --> P1
P1 --> O1[输出]
end
subgraph ML["机器学习"]
direction LR
D2[数据] --> P2[学习算法]
O2[期望输出] --> P2
P2 --> M[模型 / 规则]
end传统编程:你写规则,程序把规则应用到数据上产生输出。
机器学习:你提供数据和期望输出,算法去发现规则。
训练得到的"模型"本身就是规则,只不过编码成了数字(权重、参数)。它从见过的样本中归纳出规律,对从没见过的数据做预测。
机器学习的三大类型
flowchart TD
ML[机器学习] --> SL[监督学习]
ML --> UL[无监督学习]
ML --> RL[强化学习]
SL --> C["分类"]
SL --> R["回归"]
UL --> CL["聚类"]
UL --> DR["降维"]
RL --> PO["策略优化"]
RL --> VL["价值学习"]监督学习:你有输入-输出对,模型学习把输入映射到输出。
- "这是 10000 张标了猫或狗的照片,学会分辨它们。"
- "这是房屋特征和价格,学会预测价格。"
无监督学习:你只有输入,没有标签,模型自己找结构。
- "这是 10000 条客户购买记录,找出天然的分组。"
- "这是 1000 维的数据点,在保留结构的同时降到 2 维。"
强化学习:一个 agent 在环境里采取行动,获得奖励或惩罚。它学习一套策略(policy),让总奖励最大化。
- "玩这个游戏。赢 +1,输 -1。自己琢磨策略。"
- "控制这只机械臂。抓起物体 +1,每浪费一秒 -0.01。"
你在实践中构建的大部分东西用的是监督学习。无监督学习常用于预处理和探索。强化学习驱动着游戏 AI、机器人,以及语言模型的 RLHF。
三大类之外
上面这三类划分得很干净,但现实中的 ML 常常把界限搞模糊。
半监督学习用一小批有标签数据加一大批无标签数据。你可能有 100 张标好的医学影像和 100000 张没标的。常见技术包括:
- 标签传播: 建一张图把相似的数据点连起来。标签从有标签的节点沿着图扩散到无标签的邻居。
- 伪标签: 先在有标签数据上训一个模型,用它给无标签数据预测标签,再用全部数据重新训练。模型自举出自己的训练集。
- 一致性正则化: 对于一个输入和它的轻微扰动版本,模型应该给出相同的预测。这一招连标签都不需要。
自监督学习从数据自身造出监督信号,完全不需要人工标注。模型从数据的结构里给自己造出一个预测任务。
- 掩码语言建模(BERT): 遮住句子里 15% 的词,训练模型预测被遮住的词。"标签"来自原始文本。
- 对比学习(SimCLR): 拿一张图,做出两个增强版本。训练模型识别它们来自同一张图,同时把它们和其他图的增强版本区分开。
- 下一个 token 预测(GPT): 给定前面所有词,预测下一个词。每篇文本都变成一个训练样本。
这些并不是和三大类并列的独立类别,而是把监督和无监督思路结合起来的策略。自监督学习严格说是监督学习(模型在预测某个东西),但标签是自动生成的,不是人标的。
分类 vs 回归
这是监督学习的两大主要任务。
| 维度 | 分类 | 回归 |
|---|---|---|
| 输出 | 离散类别 | 连续数值 |
| 例子 | "这封邮件是垃圾吗?" | "房价会是多少?" |
| 输出空间 | {猫, 狗, 鸟} | 任意实数 |
| 损失函数 | 交叉熵、准确率 | 均方误差、MAE |
| 决策 | 类别之间的边界 | 一条拟合数据的曲线 |
分类回答"哪一类?"回归回答"多少?"
有些问题两种框法都行。预测股票涨还是跌是分类,预测具体价格是回归。
ML 工作流
不管用什么算法,每个机器学习项目都走同一套流程。
flowchart LR
A[采集数据] --> B[清洗 & 探索]
B --> C[特征工程]
C --> D[划分数据]
D --> E[训练模型]
E --> F[评估]
F -->|不够好| C
F -->|够好了| G[部署]
G --> H[监控]
H -->|性能下滑| A采集数据:收集原始数据。数据多几乎总是更好,但质量比数量更重要。
清洗 & 探索:处理缺失值、去重、可视化分布、发现异常。这一步常常吃掉整个项目 60-80% 的时间。
特征工程:把原始数据变成模型能用的特征。把日期变成星期几,把数值列归一化,把类别变量编码。好特征比花哨算法更重要。
划分数据:分成训练集、验证集和测试集。模型在训练数据上训练,你在验证数据上调超参数,最后在测试数据上报告最终性能。
训练模型:把训练数据喂给算法。算法调整内部参数,让损失函数最小。
评估:在验证/测试数据上衡量性能。如果性能不达标,就回头换特征、换算法或换超参数。
部署:把模型放到生产环境,对新数据做预测。
监控:长期跟踪性能。数据分布会变(数据漂移),模型会退化。性能下滑时就重新训练。
训练集、验证集和测试集
这是新手最容易搞错的概念。你必须在模型训练时从没见过的数据上评估它,否则你衡量的是死记硬背,而不是学习。
flowchart LR
subgraph Dataset["完整数据集 (100%)"]
direction LR
TR["训练集 (70%)"]
VA["验证集 (15%)"]
TE["测试集 (15%)"]
end
TR -->|训练模型| M[模型]
M -->|调超参数| VA
VA -->|最终评估| TE| 划分 | 用途 | 何时使用 | 典型占比 |
|---|---|---|---|
| 训练集 | 模型从这部分数据学习 | 训练期间 | 60-80% |
| 验证集 | 调超参数、比较模型 | 每次训练后 | 10-20% |
| 测试集 | 最终的无偏性能估计 | 仅在最后用一次 | 10-20% |
测试集是神圣的,你只看它一次。如果你不停地根据测试性能去调模型,那等于在测试集上训练,你报告的数字就毫无意义。
对于小数据集,用 k 折交叉验证:把数据分成 k 份,在 k-1 份上训练,在剩下那份上验证,轮换,再把结果平均。
过拟合 vs 欠拟合
flowchart LR
subgraph UF["欠拟合"]
U1["模型太简单"]
U2["高偏差"]
U3["抓不住规律"]
end
subgraph GF["拟合恰当"]
G1["复杂度合适"]
G2["平衡"]
G3["泛化良好"]
end
subgraph OF["过拟合"]
O1["模型太复杂"]
O2["高方差"]
O3["把噪声也背下来"]
end
UF -->|提升复杂度| GF
GF -->|复杂度过头| OF欠拟合:模型太简单,抓不住数据里的规律。比如用一条直线去拟合弯曲的关系。训练误差高,测试误差也高。
过拟合:模型太复杂,把训练数据连同噪声一起背了下来。比如一条扭来扭去的曲线穿过每一个训练点,却在新数据上崩盘。训练误差低,测试误差高。
拟合恰当:模型抓住了真实规律,又没把噪声背下来。训练误差和测试误差都相当低。
过拟合的信号:
- 训练准确率远高于验证准确率
- 模型在训练数据上表现很好,在新数据上很差
- 加更多训练数据能提升性能(说明模型之前是在背,不是在学)
过拟合的解法:
- 拿更多训练数据
- 降低模型复杂度(更少参数、更简单的架构)
- 正则化(给大权重加惩罚)
- Dropout(训练时随机把神经元置零)
- 提前停止(验证误差开始上升时就停训)
欠拟合的解法:
- 用更复杂的模型
- 加更多特征
- 减少正则化
- 训练更久
偏差-方差权衡
这是过拟合和欠拟合背后的数学框架。
偏差:模型假设错误带来的误差。当真实关系是非线性时,线性模型就有高偏差。高偏差导致欠拟合。
方差:模型对训练数据小幅波动过于敏感带来的误差。高方差的模型在不同数据子集上训练时,给出的预测差异很大。高方差导致过拟合。
| 模型复杂度 | 偏差 | 方差 | 结果 |
|---|---|---|---|
| 太低(用线性模型拟合弯曲数据) | 高 | 低 | 欠拟合 |
| 恰到好处 | 中 | 中 | 泛化良好 |
| 太高(用 20 次多项式拟合 10 个点) | 低 | 高 | 过拟合 |
总误差 = 偏差^2 + 方差 + 不可约噪声
不可约噪声你没法减少(它是数据本身的随机性)。你要找的是让 偏差^2 + 方差 最小的那个甜点位。
没有免费午餐定理
没有哪个算法对所有问题都最好。在某一类问题上表现好的算法,在另一类上会表现差。这就是为什么数据科学家会试多个算法再对比结果。
实践中,选择取决于:
- 你有多少数据
- 有多少特征
- 关系是线性还是非线性
- 你是否需要可解释性
- 你能负担多少算力
什么时候不该用机器学习
ML 很强大,但并不总是对的工具。在动手用模型之前,先问问你是不是真的需要它。
不该用 ML 的情形:
- 规则简单且定义明确。 报税计算、排序算法、单位换算。如果逻辑几个 if 语句就能写完,上模型只是徒增复杂度、毫无收益。
- 你没有数据,或数据极少。 ML 需要样本来学习。只有 10 个数据点,你训不出任何有意义的东西。先去收集数据。
- 出错代价是灾难性的,而你需要保证正确。 医疗剂量计算、核反应堆控制、密码学验证。ML 模型是概率性的,它有时会错。如果"有时会错"不可接受,就用确定性方法。
- 查表或启发式就能解决问题。 如果一个简单阈值或表格能覆盖 99% 的情况,上 ML 只会增加维护成本,却没有实质改进。
- 你无法解释决策,而又必须可解释。 受监管的行业(信贷、保险、刑事司法)有时要求每个决策都能完全解释。有些 ML 模型是可解释的(线性回归、小决策树),大多数不是。
- 问题变化得比你能重新训练的速度还快。 如果规则每天都在变,而重新训练要一周,那模型永远是过时的。
用这张决策流程图:
flowchart TD
A["你有数据吗?"] -->|没有| B["先收集数据,或用规则"]
A -->|有| C["你能把规则明确写出来吗?"]
C -->|"能,而且很简单"| D["用规则,跳过 ML。"]
C -->|"不能,或太复杂"| E["出错的代价能接受吗?"]
E -->|"不能,需要保证正确"| F["用确定性方法"]
E -->|能接受| G["你需要可解释性吗?"]
G -->|"需要,严格要求"| H["只用可解释的模型"]
G -->|"不需要,或部分需要"| I["用 ML"]
I --> J["你有足够的标注数据吗?"]
J -->|有| K["监督学习"]
J -->|"有一些标签"| L["半监督学习"]
J -->|"没有标签"| M["无监督或自监督"]f3-learning-boundary动手构建
code/ml_intro.py 里的代码从零实现了一个最近质心分类器,这是最简单的 ML 算法。它演示了核心思想:从数据学习,再对新数据预测。
第 1 步:从零实现最近质心分类器
最近质心分类器计算训练数据里每个类的中心(均值)。预测时,它把每个新点分给中心离它最近的那个类。
class NearestCentroid:
def fit(self, X, y):
self.classes = np.unique(y)
self.centroids = np.array([
X[y == c].mean(axis=0) for c in self.classes
])
def predict(self, X):
distances = np.array([
np.sqrt(((X - c) ** 2).sum(axis=1))
for c in self.centroids
])
return self.classes[distances.argmin(axis=0)]整个算法就这么多。fit 算两个均值,predict 算距离。没有梯度下降,没有迭代,没有超参数。
第 2 步:在合成数据上训练
我们生成一个二维分类数据集,两个类略有重叠。质心分类器在两个类中心之间画出一条线性决策边界。
rng = np.random.RandomState(42)
X_class0 = rng.randn(100, 2) + np.array([1.0, 1.0])
X_class1 = rng.randn(100, 2) + np.array([-1.0, -1.0])
X = np.vstack([X_class0, X_class1])
y = np.array([0] * 100 + [1] * 100)第 3 步:和基线对比
每个 ML 模型都应该和一个平凡的基线对比。这里的基线随机预测一个类。如果你的 ML 模型连随机猜都赢不了,那肯定有问题。
baseline_preds = rng.choice([0, 1], size=len(y_test))
baseline_acc = np.mean(baseline_preds == y_test)在这个干净的数据集上,质心分类器应该能拿到 90%+ 的准确率。随机基线大约 50%。
为什么这很重要
最近质心分类器简单到不能再简单。它没有超参数、没有迭代、没有梯度下降。但它抓住了 ML 的根本模式:
- 从训练数据学习一个表示(质心)
- 用这个表示对新数据预测(最近距离)
- 和基线评估对比(随机猜)
从逻辑回归到 transformer,每个 ML 算法都遵循这同样的三步模式。表示会越来越复杂,但工作流不变。
第 4 步:质心分类器做不到什么
最近质心分类器假设每个类是一团。它画的是线性决策边界。它在以下情况会失败:
- 类有多个簇(比如数字 "1" 可以有好几种写法)
- 决策边界是非线性的(比如一个类绕着另一个类)
- 特征尺度差异很大(距离会被尺度最大的那个特征主导)
这些局限正是你将要学的每个其他算法的动机。K 近邻能处理多个簇。决策树能处理非线性边界。特征缩放解决尺度问题。每一节课都建立在前一节的局限之上。
实际使用
sklearn 提供了 NearestCentroid 和合成数据生成器:
from sklearn.neighbors import NearestCentroid
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
X, y = make_classification(
n_samples=500, n_features=2, n_redundant=0,
n_clusters_per_class=1, random_state=42
)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3)
clf = NearestCentroid()
clf.fit(X_train, y_train)
print(f"Accuracy: {clf.score(X_test, y_test):.3f}")拿去用
本节课产出 outputs/prompt-ml-problem-framer.md —— 一个把模糊业务问题变成具体 ML 任务的提示词。给它一段问题描述("我们想降低流失率"或"预测下季度需求"),它会识别学习类型、定义预测目标、列出候选特征、挑选成功指标、确立基线,并标出数据泄漏或类别不平衡这类坑。在任何 ML 项目开头用它,能避免你造错东西。
关键术语
| 术语 | 大家怎么说 | 它实际是什么 |
|---|---|---|
| 模型 | "那个 AI" | 一个带可学习参数的数学函数,把输入映射到输出 |
| 训练 | "教 AI" | 运行优化算法调整模型参数,让预测匹配已知输出 |
| 特征 | "一个输入列" | 数据的一个可测量属性,模型用它来做预测 |
| 标签 | "答案" | 训练样本的已知输出,用来算误差信号 |
| 超参数 | "你拧的一个设置" | 训练前设定、控制学习过程的参数(学习率、层数) |
| 损失函数 | "模型错得多离谱" | 衡量预测和真实输出之间差距的函数,训练就是去最小化它 |
| 过拟合 | "它把测试题背下来了" | 模型学到的是训练数据特有的噪声而非通用规律,所以在新数据上失败 |
| 欠拟合 | "它啥也没学到" | 模型太简单,抓不住数据里真正的规律 |
| 泛化 | "它在新数据上也好使" | 模型在没训练过的数据上做出准确预测的能力 |
| 交叉验证 | "在不同块上测试" | 反复把数据切成训练/测试折并平均结果,得到更稳健的性能估计 |
| 正则化 | "让权重保持小" | 给损失函数加一个惩罚项,抑制过于复杂的模型 |
| 数据漂移 | "世界变了" | 进来的数据的统计分布随时间偏移,导致模型性能退化 |
练习
- 拿任意一个数据集(比如 Iris、Titanic),按 70/15/15 切成训练/验证/测试。解释为什么不能在测试集上调超参数。
- 列出三个现实问题。对每一个,判断它是分类、回归还是聚类,以及它是监督还是无监督。
- 一个模型在训练数据上拿到 99% 准确率,但在测试数据上只有 60%。诊断问题,并列出三件你会尝试去修它的事。
延伸阅读
- An Introduction to Statistical Learning - 免费教材,涵盖所有经典 ML 方法并配有实战示例
- Google's Machine Learning Crash Course - 简洁的可视化 ML 概念入门
- Scikit-learn User Guide - 用 Python 实现 ML 的实用参考