📗 第1章 绪论
📄 一句话概括

同一个隐层,接三个不同的头:回归输出 1 个数用平方损失,二分类输出 1 个概率用 sigmoid, 多分类输出类别数用 softmax。 前面的层一模一样,只有最后一层和损失函数不同。 实测三个任务:回归均方误差 0.000168、二分类准确率 1.0000、多分类 1.0000。 这就是本书的组织方式:骨架讲一次,各种变体各讲一次。

📝 本节和前面几节的关系

这一节是全书的地图,本来可以只列个目录。 但目录看不出「为什么这样分」,所以用一个 Demo 把八章的关系串起来。 核心观察是:深度学习里大部分「不同的任务」,差别只在最后一层。 同一个隐层表示,接上不同的头和不同的损失,就变成回归、二分类、多分类。 这个观察能省掉很多重复学习——你只要把骨架搞懂一次。 所以本书的顺序是:先讲骨架(第 2 章),再讲三种给骨架换形状的办法 (第 3 章卷积、第 4 章能量模型、第 5 章自编码器)。 然后讲怎么让它不过拟合(第 6 章)、怎么落到工具上(第 7 章)。

一、三个头,一套骨架

先看三种输出层:

任务 输出个数 最后一层 损失
回归 1 不激活 平方误差
二分类 1 sigmoid 交叉熵
多分类 类别数 softmax 交叉熵
「不激活」的意思是输出层直接给出实数,要多大就多大。
sigmoid 把实数压到 0 和 1 之间,当成概率。
softmax 把一组实数变成一组加起来等于 1 的概率。
实测三个任务在同一份数据上(三条螺旋,360 个点):
任务 指标 结果
回归(预测到原点的距离) 均方误差 0.000168
二分类(是不是第一类) 准确率 1.0000
多分类(属于哪一条螺旋) 准确率 1.0000
三个任务的隐层结构完全一样,都是 2 到 16。
换句话说:同一层特征,既能支撑数值预测,也能支撑分类。

二、为什么 softmax 之后还要交叉熵

这是一个初学者常卡住的地方:既然 softmax 已经把输出变成概率了,为什么不用平方误差, 非要换成交叉熵? 原因是梯度。 平方误差配 softmax 时,输出层梯度里会多乘一个 softmax 的导数, 而这个导数在输出「很确信但错得很离谱」时接近零——也就是说,错得越狠,学得越慢。 交叉熵配 softmax 时,两者的一堆导数恰好约掉,输出层梯度直接变成「预测减目标」。 错得越狠,梯度越大,学得越快。 这个「巧合」其实是设计出来的:交叉熵就是对数似然,而 softmax 是对数线性的,两者天然配对。 第 2.6 和 2.7 节会把这件事从似然函数的角度再讲一遍。

三、八章各自的位置

章 讲什么 在全书的角色
1 绪论:从哪来、为什么、是什么 建立直觉
2 神经网络:前向、反向、梯度下降 地基,后面全部靠它
3 卷积神经网络:换一套更省参数的结构 给图像专用的骨架
4 受限玻尔兹曼机:能量模型与逐层预训练 无监督那条线的起点
5 自编码器:学压缩,也学到特征 无监督那条线的延续
6 提高泛化能力:样本、预处理、激活、Dropout 让它别过拟合
7 深度学习工具:Theano 到 TensorFlow 落到工程上
8 现在和未来:应用与局限 收尾与展望
第 2 章是唯一必须逐字读的一章。
后面每一章都在它上面加东西:第 3 章换了层的连接方式,第 4、5 章换了训练目标,
第 6 章换了正则手段——但没有一章换掉前向、反向、更新这三步。
第 4、5 章讲的无监督方法,在今天已经不太常用了。
但它们的历史地位很重要:2006 年正是靠逐层预训练,深层网络才第一次训得动。
而且它们提出的「用重构误差当训练目标」这个思路,后来的自监督学习一直在用。

四、怎么读这本书

如果只想快速上手:第 2 章加第 7 章,够你把一个网络跑起来。 如果想理解为什么:第 2 章逐字读,第 3 章认真读,第 6 章反复读。 第 6 章讲的是「为什么你的模型在测试集上不行」,这是实战里最常遇到的问题。 第 4、5 章可以快读。 它们是这本书写作年代(2015 年前后)的热点,现在的地位已经变了,但里面的数学(能量、 配分函数、对比散度)在生成模型里还能见到。 每章的 Demo 都建议亲手跑一遍。 这本书的 Demo 全部用 numpy 从零写,不依赖框架——因为框架会把「发生了什么」藏起来, 而初学阶段最需要的恰恰是看见每一步。

五、动手 Demo:本书结构:同一个隐层,接三个不同的头

这段代码用三条螺旋的数据,让同一个 2 到 16 的隐层分别接上回归、二分类、多分类三个头, 看三种任务能不能共用同一套特征。 重点看两处。第二到四段的三个结果 0.000168、1.0000、1.0000; 第五段那张表——三种任务的差别只在最后一层和损失。

# -*- coding: utf-8 -*-
import numpy as np


def spiral(n=120, C=3, seed=1, noise=0.12):
    rg = np.random.default_rng(seed)
    X, Y = [], []
    for c in range(C):
        t = np.linspace(0.4, 4.2, n)
        ang = t + c * 2.0 * np.pi / C
        X.append(np.column_stack([t * np.cos(ang), t * np.sin(ang)])
                 + rg.normal(0.0, noise, (n, 2)))
        Y.append(np.full(n, c))
    return np.vstack(X), np.concatenate(Y)


def init(sizes, seed=0):
    rg = np.random.default_rng(seed)
    W, b = [], []
    for i in range(len(sizes) - 1):
        W.append(rg.normal(0.0, np.sqrt(2.0 / sizes[i]), (sizes[i], sizes[i + 1])))
        b.append(np.zeros(sizes[i + 1]))
    return {"W": W, "b": b}


def fwd(p, X):
    h = X
    n = len(p["W"])
    A = [X]
    Z = []
    for i in range(n):
        z = h @ p["W"][i] + p["b"][i]
        Z.append(z)
        h = z if i == n - 1 else np.maximum(z, 0.0)
        A.append(h)
    return A, Z


def softmax(S):
    S = S - S.max(axis=1, keepdims=True)
    E = np.exp(S)
    return E / E.sum(axis=1, keepdims=True)


def sigmoid(z):
    return 1.0 / (1.0 + np.exp(-np.clip(z, -60.0, 60.0)))


def head_of(kind, S):
    """同一个隐层输出,接三种不同的头。"""
    if kind == "reg":
        return S
    if kind == "bin":
        return sigmoid(S)
    return softmax(S)


def dhead(kind, P, T):
    """损失对这层输入的导数,三种头各不相同。"""
    if kind == "reg":
        return 2.0 * (P - T) / T.size
    if kind == "bin":
        return (P - T) / len(T)
    return (P - T) / len(T)


def back(p, X, T, kind):
    A, Z = fwd(p, X)
    n = len(p["W"])
    d = dhead(kind, head_of(kind, A[-1]), T)
    gW = [None] * n
    gb = [None] * n
    for i in range(n - 1, -1, -1):
        gW[i] = A[i].T @ d
        gb[i] = d.sum(axis=0)
        if i > 0:
            d = (d @ p["W"][i].T) * (Z[i - 1] > 0.0)
    return {"W": gW, "b": gb}


def train(p, X, T, kind, lr=0.1, epochs=400, batch=32, seed=0):
    rg = np.random.default_rng(seed)
    n = len(X)
    for _ in range(epochs):
        idx = rg.permutation(n)
        for s in range(0, n, batch):
            j = idx[s:s + batch]
            g = back(p, X[j], T[j], kind)
            for i in range(len(p["W"])):
                p["W"][i] -= lr * g["W"][i]
                p["b"][i] -= lr * g["b"][i]
    return p


def out(p, X, kind):
    return head_of(kind, fwd(p, X)[0][-1])


X, y = spiral(120, 3, seed=1)

print('[1] 同一个隐层,接三个不同的头')
print('  数据:三条螺旋线上共 %d 个点' % len(y))
print('  三个任务的输入完全一样,隐层结构也完全一样,只有最后一层和损失不同')
print('')

# 任务一:回归,预测这条点到原点的距离
r = np.sqrt((X ** 2).sum(axis=1))[:, None] / 4.0
p = train(init([2, 16, 1], seed=1), X, r, "reg", lr=0.05, epochs=400)
e = float(np.mean((out(p, X, "reg") - r) ** 2))
print('[2] 回归:输出 1 个数,用平方损失')
print('  目标是每个点到原点的距离,输出层直接给出数值')
print('  训练 %d 轮后,均方误差 %.6f' % (400, e))
print('')

# 任务二:二分类,判断这个点在第一类还是其他
y2 = (y == 0).astype(float)[:, None]
p = train(init([2, 16, 1], seed=1), X, y2, "bin", lr=0.1, epochs=400)
a = float(np.mean((out(p, X, "bin") > 0.5) == (y2 > 0.5)))
print('[3] 二分类:输出 1 个概率,用 sigmoid 把它压到 0 和 1 之间')
print('  训练 %d 轮后,准确率 %.4f' % (400, a))
print('')

# 任务三:多分类,判断属于哪一条螺旋
T = np.zeros((len(y), 3))
T[np.arange(len(y)), y] = 1.0
p = train(init([2, 16, 3], seed=1), X, T, "ce", lr=0.1, epochs=400)
a3 = float(np.mean(np.argmax(out(p, X, "ce"), axis=1) == y))
print('[4] 多分类:输出 3 个分数,用 softmax 变成三个加起来为 1 的概率')
print('  训练 %d 轮后,准确率 %.4f' % (400, a3))
print('')

print('[5] 三种任务,差别只在最后一层')
print('  任务      输出个数   最后一层   损失')
print('  回归         1        不激活     平方误差')
print('  二分类       1        sigmoid    交叉熵')
print('  多分类      类别数     softmax    交叉熵')
print('  前面的层一模一样。这也是本书的组织方式:骨架讲一次,各种变体各讲一次')
print('')

print('[6] 全书八章各自管什么')
for line in (
    '  第 1 章  绪论:这些东西是怎么一步步走到今天的',
    '  第 2 章  神经网络:前向、反向、梯度下降,全书的地基',
    '  第 3 章  卷积神经网络:给图像换一套更省参数的结构',
    '  第 4 章  受限玻尔兹曼机:无监督的能量模型,第一次「预训练」',
    '  第 5 章  自编码器:让网络学压缩,也学会了特征',
    '  第 6 章  提高泛化能力:过拟合怎么防',
    '  第 7 章  深度学习工具:把上面这些装进框架',
    '  第 8 章  现在和未来:它已经用在哪,还差什么',
):
    print(line)

实跑输出:

--------------------------------------------------------------------
本书结构:同一个隐层,接三个不同的头
--------------------------------------------------------------------
[1] 同一个隐层,接三个不同的头
  数据:三条螺旋线上共 360 个点
  三个任务的输入完全一样,隐层结构也完全一样,只有最后一层和损失不同

[2] 回归:输出 1 个数,用平方损失
  目标是每个点到原点的距离,输出层直接给出数值
  训练 400 轮后,均方误差 0.000168

[3] 二分类:输出 1 个概率,用 sigmoid 把它压到 0 和 1 之间
  训练 400 轮后,准确率 1.0000

[4] 多分类:输出 3 个分数,用 softmax 变成三个加起来为 1 的概率
  训练 400 轮后,准确率 1.0000

[5] 三种任务,差别只在最后一层
  任务      输出个数   最后一层   损失
  回归         1        不激活     平方误差
  二分类       1        sigmoid    交叉熵
  多分类      类别数     softmax    交叉熵
  前面的层一模一样。这也是本书的组织方式:骨架讲一次,各种变体各讲一次

[6] 全书八章各自管什么
  第 1 章  绪论:这些东西是怎么一步步走到今天的
  第 2 章  神经网络:前向、反向、梯度下降,全书的地基
  第 3 章  卷积神经网络:给图像换一套更省参数的结构
  第 4 章  受限玻尔兹曼机:无监督的能量模型,第一次「预训练」
  第 5 章  自编码器:让网络学压缩,也学会了特征
  第 6 章  提高泛化能力:过拟合怎么防
  第 7 章  深度学习工具:把上面这些装进框架
  第 8 章  现在和未来:它已经用在哪,还差什么

第一段那句「输入完全一样,隐层结构也完全一样」是全篇的立论。 如果三个任务的网络结构不同,就说明不了「骨架可以共用」。 第二段回归的均方误差 0.000168 看着很小,但要注意目标值也被缩放了(除以 4)。 不缩放的话数值会大得多,学习率也得跟着改——这是实践中很常见的一步预处理。 第三、四段都是 1.0000,因为三条螺旋在这个设定下确实容易分。 这两个 1.0000 的价值不在于「准确率高」,而在于「同一层特征两边都能用」。 第五段那张表可以背下来。 实际做项目时,先判断任务属于哪一类,然后直接照着表配最后一层和损失,能省掉大量试错。 代码里 head_of 和 dhead 是成对的两个函数。 前向用什么变换,反向就要用对应的导数——这是自己实现网络时最容易错的地方。 写错一个,训练仍然会跑,只是效果莫名其妙地差。 第六段那八行是本书的目录,但换了个说法:不是说「第几章讲什么」, 而是说「每章在整体里干什么活」。 读技术书时先建立这个视角,比按顺序啃有效得多。

30 秒实验

  1. 把回归的目标从「到原点的距离」换成「角度的正弦」,重跑。 均方误差会变差一些,因为正弦在跨越正负 pi 的地方不连续。 这说明「回归」这个头不是万能的,目标本身的连续性会限制效果。
  2. 把二分类的隐层从 16 改成 2,重跑。准确率会掉。 同一份数据上,回归任务可能还凑合,分类任务先垮——不同任务对容量的要求不一样。
  3. 把多分类的损失从交叉熵换成平方误差(dhead 里改成 2*(P-T)/T.size),重跑。 收敛会明显变慢。 这就是「softmax 配平方误差」的梯度消失问题,亲手跑一次比看公式印象深。

常见误区

  1. 「不同任务要用完全不同的网络结构」。 本节三个任务的隐层完全一样,只是最后一层不同。 回归、二分类、多分类共用同一套特征表示,这在实践中很常见(叫多任务学习的基础)。
  2. 「softmax 之后用平方误差也行,反正都是损失」。 能跑,但学得慢。 因为平方误差配 softmax 时,梯度里多一个 softmax 导数,在「很确信但错得离谱」时接近零。 交叉熵配 softmax 时这个因子恰好约掉。
  3. 「第 4、5 章讲的东西已经过时,可以跳过」。 方法本身确实不常用了,但思路没有:用重构误差做无监督训练、逐层预训练、 能量模型——这些在今天的自监督学习和生成模型里还在用。 而且 2006 年正是靠它们,深层网络才第一次训得动。
  4. 「用框架就行,不用懂原理」。 跑通一个例子确实不用懂。 但模型不收敛、准确率上不去、换个数据集就崩的时候,能救你的只有「知道每一步在算什么」。
自测三问(点开对答案) 1. 回归、二分类、多分类的最后一层分别是什么? —— 回归不激活,直接输出实数;二分类用 sigmoid 压到 0 和 1 之间; 多分类用 softmax 变成一组加起来为 1 的概率。损失上回归用平方误差,两种分类都用交叉熵。 2. 为什么分类任务要用交叉熵而不是平方误差? —— 因为平方误差配 softmax 时,输出层梯度里会多乘 softmax 的导数, 在「很确信但错得离谱」时这个导数接近零,导致错得越狠学得越慢。 交叉熵配 softmax 时两者导数恰好约掉,梯度直接是「预测减目标」。 3. 第 2 章为什么是唯一必须逐字读的一章? —— 因为后面每章都在它上面加东西,但没有一章换掉前向、反向、更新这三步。 第 3 章换层的连接方式,第 4、5 章换训练目标,第 6 章换正则手段,骨架始终是第 2 章那一套。

地图看完了,从下一章开始正式动工。 第一个要拆开的是最基础的那个单元:一个神经元怎么把若干输入变成一个输出,它的权重又该怎么定。 这个问题从 1943 年问到今天,答案换过好几次。

相关笔记