🧠 第四章 机器学习算法
📄 一句话概括

24×24 的图,576 维像素经过一次 Sobel 卷积和 2×2 池化,压成 5 维特征, 三类图形识别率都是 1.000。单张图从像素算到特征要 4.1 毫秒。

📝 本节和前面几节的关系

上一节讲完六步流程,这一节看第一个应用场景。 图像这条链是所有视觉任务的地基,人脸、检索、检测都从它长出来。

一、像素不是特征

一张 24×24 的灰度图是 576 个数。 直接把这 576 个数丢给分类器,有两个问题:维数太高、而且平移一个像素就全变了。 卷积解决的就是这个问题。 用一个 3×3 的小核在图上滑动,每个位置算一次加权和——它学的不是「第 5 行第 7 列有多亮」, 而是「这里有没有一条边」。 Demo 用了两个经典核:横向 Sobel 和纵向 Sobel。一个抓竖边,一个抓横边。 然后取绝对值相加,就得到一张「边缘强度图」。 这一步把「亮不亮」变成了「有没有边界」——这才是能跨图复用的特征。

二、池化:把图缩小,把结论留下

卷积后是 22×22,还是太大。Demo 做 2×2 最大池化:每四个格子取最大的那个,变成 11×11。 最大池化的意思是「这一小块里有没有强边缘」,不关心具体在哪个像素。 这正是我们要的:位置稍微动一动,池化后的结果基本不变。 最后从池化图里抽 5 个数:边缘均值、最大响应、强响应占比、横竖边缘比、亮区面积占比。 576 维压成 5 维。 这 5 维不是随便选的——每一个都对应一种「形状的性质」。 圆、方、三角的横竖边缘比和亮区面积占比明显不同,靠这两条就能分开。

三、分类器:在 5 维空间里画三条线

有了 5 维特征,用 softmax 回归(本质是多项逻辑回归)学三类的分界。 Demo 自己写了梯度下降,400 轮收敛。 结果三类识别率都是 1.000。 这个数不能当证据——图是我用公式画的,噪声是我加的高斯噪声,三类在特征空间里本来就隔得远。 能当证据的是形状的变化:576 维像素 → 卷积 → 池化 → 5 维特征。 后面所有图像模型,不管多深,走的都是这条链。 真实人脸识别的区别只在于:卷积核不是手写的 Sobel,而是从数据里学出来的;层数从 1 层变成几十层; 特征从 5 维变成 512 维。链条本身没变。

四、耗时:为什么会卡在预处理上

Demo 量了单张图的特征提取耗时:20 张共 82.72 毫秒,折合每张 4.1 毫秒。 4 毫秒听起来不多,但要做 10 万张标注数据的质检,就是 400 秒; 要在车载摄像头上跑 30 帧每秒,就是每帧只有 33 毫秒预算,特征提取吃掉 4 毫秒已经不少了。 这个 Demo 里的卷积是纯 Python 双层循环乘加,慢得离谱。 真实的卷积用矩阵乘法或专用硬件,同样的活快几百倍。 所以「这个算法能不能上线」这个问题,一半答案在精度,另一半在这一步要花多少毫秒。

五、动手 Demo:图像分类的最小完整链条

用公式画三类 24×24 图形(圆、方、三角)各 30 张,加高斯噪声; 每张做横向与纵向 Sobel 卷积、2×2 最大池化,抽出 5 个特征; 标准化后按 7:3 切分,用自己写的 softmax 回归训练; 最后打每类识别率、整体准确率、混淆口径与单张耗时。

KX = np.array((-1, 0, 1, -2, 0, 2, -1, 0, 1), float).reshape(3, 3)
KY = np.array((-1, -2, -1, 0, 0, 0, 1, 2, 1), float).reshape(3, 3)

def feats(img):
    gx, gy = conv3(img, KX), conv3(img, KY)
    g = np.abs(gx) + np.abs(gy)
    p = g.reshape(11, 2, 11, 2).max(axis=(1, 3))    # 2×2 池化
    return np.array([g.mean(), p.max(), (p > p.mean()).mean(),
                     np.abs(gx).sum() / (np.abs(gy).sum() + 1e-9),
                     (img > 0.5).mean()])

W, b = softmax_fit(Z[tr], y[tr])
pred = H.softmax(Z[te] @ W + b).argmax(1)

实跑输出:

--------------------------------------------------------------------
图像分类的最小完整链条:卷积 → 池化 → 分类
--------------------------------------------------------------------
  图像尺寸                 24×24 灰度
  每类样本                 30 张(训练 66/测试 24)
  卷积核                   3×3 Sobel 横向 + 纵向,各做一次
  池化                     2×2 最大池化,22×22 → 11×11
  最终特征维度             5 维

  真实类别   测试样本   该类识别率
  -------------------------------
  圆                7        1.000
  方               11        1.000
  三角              6        1.000

  整体准确率               1.000
  只当「圆」是正类         精度 1.000 召回 1.000 F1 1.000

  单张图从像素到特征的耗时:
  20 张共                  82.72 毫秒
  折合每张                 4.136 毫秒

  这个 Demo 里的三张「图」是自己画的,噪声是自己加的,
  所以 1.000 的准确率不能当作真实图像任务的证据。
  能当作证据的是【形状变了】:576 维像素 → 卷积 → 池化 → 5 维特征,
  后面所有图像模型,走的都是这条链。

形状变化是这条链的骨架:576 维像素 → 卷积得 22×22 → 池化得 11×11 → 抽 5 维特征。 三类识别率全 1.000,整体也 1.000。 这是自造数据的必然结果,所以我在输出里直接写明「不能当作真实图像任务的证据」。 「只当圆是正类」那行还给了精度/召回/F1 各 1.000——三分类也能拆成三个二分类来评, 这是多分类评估的标准做法。 单张耗时 4.1 毫秒是纯 Python 卷积的结果。真实推理引擎同样一步只要几十微秒,差两三个数量级。 所以这个数只能用来看「比例」,不能用来看「水平」。

30 秒实验

  1. 把噪声标准差从 0.18 加到 0.45,重跑,看识别率掉到多少。
  2. 把池化从 2×2 改成 4×4,看特征维度变不变、准确率变不变。
  3. 想一想:为什么「平移一个像素」会毁掉直接拿像素当特征的做法?

常见误区

  1. 「像素就是特征」——576 维原始像素既不稳又太多。
  2. 「卷积核必须手写」——真实网络里的核是从数据学出来的,Sobel 只是入门的例子。
  3. 「1.000 说明模型很强」——自造数据分得开,满分是必然。
自测三问(点开对答案) 1. 卷积把「亮度」变成了什么? —— 变成了「边缘强度」——位置无关,能跨图复用。 2. 2×2 最大池化有什么好处? —— 缩小尺寸,同时让结果对小幅平移不敏感。 3. 这个 Demo 里 1.000 为什么不算证据? —— 图是公式画的、噪声是自己加的,三类本来就分得开。

图像之后是文本。下一节处理自然语言,顺带看看没有分词库怎么办。

相关笔记