公式里的 Σ,落到代码就是一个 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 秒实验
- 把第一节的
n改成 2000,看倍数会不会变小,想想为什么。 - 把第二节的
B改成reshape(3, 1),np.dot(A, B)的形状是什么? - 想一想:为什么
np.dot比np.sum(x * y)快?
常见误区
- 在数值代码里写 for 循环:几十万行数据,Python 循环就是全部耗时。
- 矩阵乘法写反顺序:报的是
not aligned,不是算错,但足够卡半天。 - 把一维数组当二维用:numpy 不报错,但形状对不上,后面全乱。
- 整张表
to_numpy():混了字符串就变 object,模型直接报错。 - 一上来就优化性能:算错了不报错,优化得再快也是错的数。
自测三问(点开对答案)
1. 带 Σ 的公式在 Python 里怎么写? —— 一个 np.sum 或者一个 np.dot,三个写法结果相等。 2. 矩阵乘法的对齐规则是什么? —— 前一个的列数等于后一个的行数,不满足直接报 not aligned。 3. sklearn 的 fit 与 predict 各做什么? —— fit 学参数,predict 用参数;所有模型接口都一样。记号都认识了,下一篇把它们连起来:手写一遍逻辑回归,再和 sklearn 对一遍。
- ☐ (本篇完,下一篇 16.3 逻辑回归:NumPy 手写与 sklearn 对照)