🧪 第16章 机器学习概要
📄 一句话概括

公式里的 Σ,落到代码就是一个 np.sum 或一个 np.dot。 同一个式子的循环写法比向量化慢几百倍,差别在「一条条算」还是「一起算」。 sklearn 的 fit 与 predict 就是「学参数」和「用参数」,没有第三条路。

📝 本章和前面几章的关系

这一篇对应教材 16.4、16.5 与 16.5.1:演算法的数学式与程式码表达、 机器学习应用的 Python 程式库、程式库与数据框架。 上一篇讲清了概念,这一篇解决一个很实际的问题:书上那些公式,抄进编辑器到底怎么写。

一、一条公式,两种写法

def loop_way():
    out = []
    for i in range(n):
        out.append(w * big[i] + b)
    return out


def vec_way():
    return w * big + b
写法 20000 条数据耗时
for 循环 7.83 ms
numpy 一行 0.022 ms

快了 361 倍,两种写法的结果完全一样。 快这么多不是因为 numpy 的算法更聪明,是它把循环挪到了 C 里,而且能一次算一批。 公式一个字没变,只是把「一条一条算」换成了「一起算」。 这也解释了为什么机器学习代码里几乎看不到 for 循环:数据动辄几十万行, Python 层的循环会成为全部耗时的来源。

二、形状:矩阵乘法怎么对齐

A = np.arange(6).reshape(2, 3)   # (2, 3)
B = np.arange(3)                  # (3,)
np.dot(A, B)                      # -> (2,),结果 [ 5 14]

前一个的列数必须等于后一个的行数。 写反了 np.dot(B, A),numpy 报的是 shapes (3,) and (2,3) not aligned: 3 (dim 0) != 2 (dim 0)。 写反了不是算错,是直接报错——这一条比深度学习里那些静默的错误友好得多, 看到 not aligned 就去改 reshape。 还有一处容易混:一维数组参与乘法时会被当成「一个向量」,所以 (3,) 在前和在后的含义不一样。 不确定的时候统一写成二维,reshape(-1, 1) 或 reshape(1, -1),别让 numpy 猜。

三、那个大写的 Σ,落到代码是什么

写法 结果 二十万个数耗时
sum(x * y for ...) 61.500000 —
np.dot(xs, ys) 61.500000 0.123 ms
np.sum(xs * ys) 61.500000 0.950 ms

三个值完全相等。 教材里那些带 Σ 的公式,落到代码就是一个 np.sum 或者一个 np.dot。看到 Σ 不用慌, 它只是在说「把这些加起来」。 np.dot 比 np.sum(x * y) 快好几倍,因为它少建一个中间数组。 但两个都在一毫秒以下——先写对,再写快。 这句听起来像口号,在数值代码里是真话:算错了不报错,只给你一个错的数。优化的前提是它算得对。

四、sklearn 的骨架:fit 和 predict

lr = LinearRegression().fit(X, price)
lr.coef_          # -> [3.219]
lr.intercept_     # -> 21.770

实测这两个数和上一篇 np.polyfit 算出来的 3.219、21.770 一样,差在小数点后几位, 来自数值方法的不同。 sklearn 没做什么神秘的事,它就是把「拟合」这件事包成了一个函数。这句话是这一篇最重要的一句—— 你手写过的那些公式,它内部算的是同一件事。 所有 sklearn 模型都是这两步:fit 学参数,predict 用参数。 从线性回归到随机森林到神经网络,接口一模一样。

这是它最值钱的地方:换模型不用改上下游代码。 第 17 章会用到这一点——把训练好的模型换个算法,API 那层一个字都不用动。

五、五个库,各管一段

库 版本 管什么
numpy 2.5.2 数组与矩阵运算,所有库的地基
pandas 3.1.0 表格:读文件、筛行、按列算
scikit-learn 1.9.1 模型与评估:fit、predict、打分
matplotlib 3.11.1 画图
scipy 1.18.1 科学计算:统计检验、优化、稀疏矩阵

这五个是本沙箱实测装着的版本,不是书上写的。 书上那份是成书时的版本,差一两个大版本很正常,接口大体没变。 教材 16.5.1 讲「程式库与数据框架」,说的就是这一层的分工。 有个说法值得记:numpy 是地基,pandas 是表格,sklearn 是模型,matplotlib 是眼睛。 还有一个这一课没装的:torch。 上一章那个物件侦测本来该用它,沙箱里装不上,所以第 13 到 15 章用的是 OpenCV 的替身。 这一章不再需要它——逻辑回归用 numpy 就能手写。

六、一份数据从 DataFrame 到模型

sub = df[ ["面积", "房龄"]].to_numpy()   # (200, 2)
one = df["面积"].to_numpy()              # (200,)

实测:取两列出来是 (200, 2),dtype 是 float64;只取一列是 (200,)。 pandas 管「表格」,numpy 管「数组」,sklearn 只吃数组。 所以真实流程永远是:读文件进 DataFrame、清洗、to_numpy()、喂给模型。 那条取两列要用双层方括号:外层是「取列」,内层是「要哪几列」。 写成一个方括号就成了「取一列」,形状从 (n, 2) 变成 (n,)——模型立刻报那个 2D 的错。

整张表一起转,pandas 会找一个能容纳所有列的共同类型。 表里混了字符串,整张表就变成 object,模型照样报错,所以要先挑列再转。

七、动手 Demo:从数学式到代码:一条公式的两种写法

这个 Demo 把书上最常见的三种记号(乘法、Σ、矩阵)各翻一遍,每次都真跑、真计时。 第一节那个倍数、第三节那两个耗时,都是这台机器上现测的。

def loop_way():
    out = []
    for i in range(n):
        out.append(w * big[i] + b)
    return out


def vec_way():
    return w * big + b

实跑输出:

--------------------------------------------------------------------
从数学式到代码:一条公式的两种写法
--------------------------------------------------------------------

  ① y = w·x + b,循环和向量化
  --------------------------------------------------------------------
  20000 条数据:for 循环 8.14 ms,numpy 一行 0.023 ms
  快了 354 倍
  两种写法结果一样 -> 是
  ! 快这么多不是因为 numpy 的算法更聪明,是它把循环挪到了 C 里,而且能一次算一批。
  公式一个字没变,只是把「一条一条算」换成了「一起算」。
  ! 这也解释了为什么机器学习代码里几乎看不到 for 循环:数据动辄几十万行,
  Python 层的循环会成为全部耗时的来源。

  ② 形状:矩阵乘法怎么对齐
  --------------------------------------------------------------------
  (2, 3) 乘 (3,) -> (2,)
  结果 -> [ 5 14]
  (3,) 乘 (2, 3) -> ValueError: shapes (3,) and (2,3) not aligned: 3 (dim 0) != 2 (dim 0)
  ! 矩阵乘法不是随便乘的:前一个的列数必须等于后一个的行数。
  写反了不是算错,是直接报错——这一条比深度学习里那些静默的错误友好得多。
  ! 一维数组参与乘法时会被当成「一个向量」,所以 (3,) 在前和在后的含义不一样。
  不确定的时候统一写成二维:reshape(-1, 1) 或 reshape(1, -1)。

  ③ 那个大写的 Σ,落到代码是什么
  --------------------------------------------------------------------
  Σ xi*yi 手写循环 -> 61.500000
  np.dot(xs, ys)   -> 61.500000
  np.sum(xs * ys)  -> 61.500000
  三个相等 -> 是
  二十万个数:np.dot 0.087 ms,np.sum(x*y) 1.057 ms
  np.dot 快 12.20 倍
  ! 教材里那些带 Σ 的公式,落到代码就是一个 np.sum 或者一个 np.dot。
  看到 Σ 不用慌,它只是在说「把这些加起来」。
  ! np.dot 快好几倍,但两个都在一毫秒以下。
  真实流程里这点差别会被读文件和清洗盖掉,先写对,再写快——这一条在数值代码里尤其重要,
  因为算错了不报错。

  ④ scikit-learn 的骨架:fit 和 predict
  --------------------------------------------------------------------
  lr.coef_ -> [3.219]
  lr.intercept_ -> 21.770
  手写 polyfit 的 w、b -> 3.219、21.770
  ! 两套写法算出来是同一个数(差在小数点后几位,来自数值方法的不同)。
  sklearn 没做什么神秘的事,它就是把「拟合」这件事包成了一个函数。
  ! 所有 sklearn 模型都是这两步:fit 学参数,predict 用参数。
  从线性回归到随机森林到神经网络,接口一模一样——这是它最值钱的地方,换模型不用改上下游代码。

  ⑤ 这几个库各管什么
  --------------------------------------------------------------------

| 库 | 版本 | 管什么 |
|---|---|---|
| numpy | 2.5.2 | 数组与矩阵运算,所有库的地基 |
| pandas | 3.1.0 | 表格:读文件、筛行、按列算 |
| scikit-learn | 1.9.1 | 模型与评估:fit、predict、打分 |
| matplotlib | 3.11.1 | 画图 |
| scipy | 1.18.1 | 科学计算:统计检验、优化、稀疏矩阵 |

  ! 这五个是本沙箱实测装着的版本,不是书上写的。
  书上那份是成书时的版本,差一两个大版本很正常,接口大体没变。
  ! 还有一个这一课没装的:torch。
  上一章那个物件侦测本来该用它,沙箱里装不上,所以第 13 章到第 15 章用的是 OpenCV 的替身。
  这一章不再需要它——逻辑回归用 numpy 就能手写。

  ⑥ 一份数据从 DataFrame 到模型
  --------------------------------------------------------------------
  df.shape -> (200, 4)
  列名 -> 面积、房龄、价格、卖掉了
  df[u'价格'].mean() -> 428.15
  取两列转 numpy -> (200, 2),dtype float64
  只取一列 df[u'面积'].to_numpy() -> (200,)
  ! pandas 管「表格」,numpy 管「数组」,sklearn 只吃数组。
  所以真实流程永远是:读文件 -> DataFrame 清洗 -> to_numpy -> 模型。
  ! 那条取两列的写法要用双层方括号:外层是「取列」,内层是「要哪几列」。
  写成一个方括号就成了「取一列」,形状从 (n, 2) 变成 (n,),模型立刻报错。

第一节 361 倍,同一份数据、同一条公式,只换了写法。 第三节三个 61.500000 完全相等——Σ 和 np.dot 是一回事。 第四节 sklearn 的两个数和 polyfit 那两个数一样,只差小数点后几位。

30 秒实验

  1. 把第一节的 n 改成 2000,看倍数会不会变小,想想为什么。
  2. 把第二节的 B 改成 reshape(3, 1),np.dot(A, B) 的形状是什么?
  3. 想一想:为什么 np.dot 比 np.sum(x * y) 快?

常见误区

  1. 在数值代码里写 for 循环:几十万行数据,Python 循环就是全部耗时。
  2. 矩阵乘法写反顺序:报的是 not aligned,不是算错,但足够卡半天。
  3. 把一维数组当二维用:numpy 不报错,但形状对不上,后面全乱。
  4. 整张表 to_numpy():混了字符串就变 object,模型直接报错。
  5. 一上来就优化性能:算错了不报错,优化得再快也是错的数。
自测三问(点开对答案) 1. 带 Σ 的公式在 Python 里怎么写? —— 一个 np.sum 或者一个 np.dot,三个写法结果相等。 2. 矩阵乘法的对齐规则是什么? —— 前一个的列数等于后一个的行数,不满足直接报 not aligned。 3. sklearn 的 fit 与 predict 各做什么? —— fit 学参数,predict 用参数;所有模型接口都一样。

记号都认识了,下一篇把它们连起来:手写一遍逻辑回归,再和 sklearn 对一遍。

相关笔记