🧮 第4章 评估方法和各种数据的处理
📄 一句话概括

图像在计算机里就是一张数字表。 Demo 造了一张 16 乘 16 的灰度图,256 个像素、取值 20 到 230、均值 108.12, 然后把一个像素换成一个字符「画」了出来。 之后做三件事:除以 255 归一化(变成 0.0784 到 0.9020)、标准化(均值 0 标准差 1)、 数据增强(翻转后和原图平均差 42.00)。

📝 本节和前面几节的关系

上一节讲文本,文本要先「变成数字」;这一节讲图像,图像天生就是数字。 所以问题不是「怎么变」,而是「怎么把这些数字摆成模型喜欢的样子」。 第 2.8 节的神经网络对输入尺度特别敏感,那时用的是手工特征; 这一节处理的是原始像素——尺度问题在这里被放大了一百倍。

一、图像是一张数字表,就这么简单

一张灰度图是二维数组:高乘宽,每个数表示那个点有多亮(0 是全黑,255 是全白)。 一张彩色图是三维数组:高乘宽乘 3,第三维是红绿蓝三个通道。 再加一个批次维度就是四维:批大小乘高乘宽乘通道。 所有框架的输入形状都是这个套路。 Demo 里造了一张 16 乘 16 的图:背景是从左到右变亮的渐变(40 到 130),中间叠一个 230 的亮方块, 方块正中再挖一个 20 的暗方块。 一共 256 个数字。 16 乘 16 等于 256,所以 shape 打出来是 (16, 16),size 是 256。 这段代码最值得看的是那个「一个像素换一个字符」的打印:把亮度映射到一串由暗到亮的字符上, 逐行打出来。 这就是「图像是矩阵」最直观的样子——模型看到的就是这张字符画背后的数字,它不知道什么是「方块」, 只知道哪里数大。

二、归一化:为什么一定要做

原始像素在 0 到 255 之间。 这个范围对模型来说太大了。 神经网络的权重初始化是按「输入大概在 0 附近、方差 1 左右」设计的, 输入是 200 多的数,第一层的输出会直接爆掉,梯度也跟着爆。 所以标准做法是两步。第一步除以 255,把范围压到 0 到 1。 第二步标准化,减掉均值、除以标准差,让每个通道的分布变成均值 0、标准差 1。 Demo 里量得很清楚:原始范围 20 到 230、均值 108.12; 除以 255 之后是 0.0784 到 0.9020、均值 0.4240;再标准化之后是 −1.3302 到 1.8396、均值 0。 这三行的差别,就是「能不能训起来」的差别。 注意第二步用的均值和标准差必须来自训练集,算一次存下来,测试时用同一组数。 用测试集自己算均值,就是把测试集的信息泄漏进模型了——这是 4.1 节说的「数据泄漏」, 在图像任务里尤其常见。

三、尺寸和通道:那些不报错但会出错的地方

尺寸必须统一。 神经网络的全连接层要求输入维度固定,所以一批图必须缩放到同样大小。 常见做法是「先等比缩放到短边等于目标值,再中心裁剪」。 直接拉伸会改变物体比例——把一只瘦猫拉成胖猫,模型学到的形状就歪了。 通道顺序有 RGB 和 BGR 两种。 OpenCV 读进来是 BGR,PIL 和 matplotlib 是 RGB。 搞反了不会报错,只会让红色和蓝色互换——模型照样能训,准确率莫名其妙低一截。 这类 bug 最难查, 因为代码完全跑得通。 位深也不统一。 大多数图是 8 位(0 到 255),医学影像常见 12 位或 16 位, 直接除以 255 会饱和成全白。 拿到一批新数据,先打一张图出来看看,再 print(img.min(), img.max()) 确认范围。

四、数据增强:告诉模型哪些变化不重要

数据增强不是「造假数据」,是「告诉模型哪些变化不该影响答案」。 猫左右翻转还是猫,那就把翻转后的图也当猫训练,模型于是学会「不管朝向」。 Demo 做了三种:左右翻转(平均绝对差 42.00,说明确实变了)、 中心裁 12 乘 12(像素数从 256 降到 144)、整体调暗到 60%(平均绝对差 43.25)。 这三种都是「标签不变」的变换。 常见的增强分两大类。几何变换:翻转、旋转、缩放、裁剪、轻微平移。 颜色变换:调亮度、调对比度、加噪声、随机擦除一块。两者都不改变语义,所以都能用来扩数据。 但有一条红线:有些增强会改变标签。 手写数字 6 上下翻转就变成 9; 交通标志左右翻转可能变成另一个标志;医学影像左右翻转可能把左右肺搞反。 每加一种增强,先问一句:这个变化会改变答案吗? 会,就不能用。

五、这一章到此为止:数据进来,模型才有得算

第 4 章三节讲的是同一件事的三面:评估告诉你「怎么判断好坏」, 文本和图像告诉你「数据怎么变成一个能算的矩阵」。 回头看整本书的结构:第 1 章定框架(三种学习范式、七步流水线),第 2 章九个有监督算法, 第 3 章八个无监督算法,第 4 章评估与数据处理,第 5 章环境搭建。 十七个算法只是工具箱,真正决定成败的是第 4 章这两件事。 一句实在话: 真实项目里,七成时间花在洗数据上,两成在评估和调参,剩下一成才是选模型、调超参数。 这本书把算法放在最前面讲,是因为算法有名字、好讲;但真上手的时候,顺序是反过来的。

六、动手 Demo:造一张图,然后把它拆开看

不用真的读一张图片文件,直接用 numpy 造一张 16 乘 16 的灰度图:渐变背景 + 亮方块 + 暗方块, 三样东西叠起来,结构简单但足够说明问题。 代码分四段:造图并打印属性、把图「画」成字符画、做两级归一化、做三种数据增强。 每一段都能独立看懂。

import numpy as np

img = np.zeros((16, 16))                      # 一张 16 乘 16 的灰度图
yy, xx = np.mgrid[0:16, 0:16]
img += 40.0 + 6.0 * xx                        # 从左到右变亮
img[4:12, 4:12] = 230.0                       # 叠一个亮方块
img[6:10, 6:10] = 20.0                        # 再挖一个暗方块

print(img.shape, img.min(), img.max(), img.mean())

a = img / 255.0                               # 归一化:0~255 压到 0~1

flips = np.fliplr(img)                        # 数据增强:左右翻转
crop = img[2:14, 2:14]                        # 中心裁 12 乘 12
dark = img * 0.6                              # 整体调暗

实跑输出:

--------------------------------------------------------------------
图像:其实就是一张数字表
--------------------------------------------------------------------

  图像在计算机眼里没有「画面」,只有数字。
  **一张彩色图就是三维数组:高 × 宽 × 3(红绿蓝)。**
   每个数在 0 到 255 之间,表示那个点有多亮。

  【一】先造一张 16 乘 16 的灰度图
  属性            值
  ------------------------------------
  形状(shape)   (16, 16)
  数据类型        float64
  最小 / 最大     20.0 / 230.0
  均值 / 标准差   108.12 / 66.25
  总像素数        256

  【二】把它「画」出来:一个像素换成一个字符
  ...:::::-----===
  ...:::::-----===
  ...:::::-----===
  ...:::::-----===
  ...:%%%%%%%%-===
  ...:%%%%%%%%-===
  ...:%%    %%-===
  ...:%%    %%-===
  ...:%%    %%-===
  ...:%%    %%-===
  ...:%%%%%%%%-===
  ...:%%%%%%%%-===
  ...:::::-----===
  ...:::::-----===
  ...:::::-----===
  ...:::::-----===
  **这就是「图像是矩阵」最直观的样子。**
   神经网络看到的就是左边这张数字表,它不知道什么是「方框」,只知道哪里数大、哪里数小。

  【三】为什么一定要归一化
  状态                    取值范围          均值      标准差
  ------------------------------------------------------------
  原始(0~255)           20.0 ~ 230.0      108.12    66.25
  除以 255                0.0784 ~ 0.9020   0.4240    0.2598
  再标准化(减均值除标准差)-1.3302 ~ 1.8396  0.0000    1.0000
  **不归一化会怎样?**
   梯度下降的步长是按输入尺度调的,输入是 0~255 时梯度巨大,学习率稍大就飞出去。
  **这是 2.1 节那个坑的另一副面孔,只是这次栽在像素上而不是特征上。**

  【四】数据增强:把一张图变成很多张
  操作              结果形状      像素数    均值      和原图的平均绝对差
  ------------------------------------------------------------------------
  左右翻转          (16, 16)      256       108.12    42.00
  中心裁 12 乘 12   (12, 12)      144       126.11    不可比(尺寸变了)
  整体调暗到 60%    (16, 16)      256       64.88     43.25
  **数据增强不是「造假数据」,是「告诉模型哪些变化不该影响答案」。**
   猫左右翻转还是猫,那就把翻转后的图也当成猫来训练——模型于是学会了「不管朝向」。
  **这是小数据集上最便宜的提分手段。**
  但要小心:**有些增强会改变标签。**
   手写数字 6 上下翻转就变成 9,交通标志左右翻转可能变成另一个标志。
  **增强之前先问一句:这个变化会改变答案吗?**

属性表第一行就在纠正一个常见误解:形状是 (16, 16),不是三维。 灰度图没有通道维, 256 个像素就是 256 个数。彩色图才是 (高, 宽, 3)。 字符画那一段值得盯着看几秒。左边的渐变变成了「...::::-----===」, 中间的 230 变成了「%%%%%%%%」,暗方块变成了空格。 模型看到的就是这张图背后的 256 个数字,它没有任何「看见方块」的能力。 三级归一化那张表:20 到 230 → 0.0784 到 0.9020 → −1.3302 到 1.8396。范围一路缩小、 均值一路归零。这就是为什么神经网络吃得下像素。 增强表里「中心裁 12 乘 12」那一格写的是「不可比」。 这不是偷懒——两张图尺寸不同, 逐像素相减没有意义。评价增强效果要看「模型在验证集上的表现」,不是看像素差了多少。

30 秒实验

  1. 把图改成 32 乘 32,字符画的形状会变成什么样?像素数变成多少?
  2. 把「除以 255」去掉,直接拿 0 到 230 的数喂给一个线性模型,权重会变成什么样?
  3. 把图上下翻转(np.flipud),它和左右翻转的结果一样吗?为什么?

常见误区

  1. 不归一化就训练。输入是 0 到 255 时梯度巨大,学习率稍大就飞出去。 这是 2.1 节那个坑,只是这次栽在像素上。
  2. 用测试集算均值和标准差。这是数据泄漏,会让测试成绩虚高。 统计量只能从训练集算,存下来给验证和测试用。
  3. 把 BGR 当 RGB。代码跑得通、模型训得动,只是准确率莫名低一截。 读图之后先打一张出来看看颜色对不对。
自测三问(点开对答案) 1. 一张 224 乘 224 的彩色图,shape 是什么?一共多少个数? —— (224, 224, 3),一共 150528 个数。灰度图则没有最后那个 3。 2. 为什么归一化的均值和标准差只能用训练集算? —— 用测试集算等于把测试集的分布信息带进了模型,属于数据泄漏,会让测试成绩虚高、上线后崩掉。 3. 哪些数据增强是不能用的? —— 会改变标签的那些。 比如数字 6 上下翻转变成 9、交通标志左右翻转可能换含义、医学影像左右翻转会搞反左右。

第 4 章正文到这里结束。第 5 章不讲算法,讲最基础的一件事:把 Python 装对。

相关笔记