2.1 神经网络基础:从神经元到网络


文档摘要

2.1 神经网络基础:从神经元到网络 本节摘要:在碰任何框架 API 之前,先把"层"的数学扒开看:一个神经元就是一次线性变换加一个非线性函数,一层就是一组并排的神经元,网络就是层的堆叠。本节用十几行手写代码验证这个理解,为下一节 nn.Module 的登场铺好地基。 从一个神经元算起 第 1 章末尾我们说远征队要有自己的成员。成员的最小单位是神经元,它的全部工作只有两步:把输入加权求和(线性部分),套一个非线性函数(激活部分)。写成公式就是 y = 激活函数(权重与输入的乘积加偏置)。 为什么要刻意加第二步?线性变换叠线性变换还是线性变换——十个线性层叠起来,数学上等价于一个线性层。没有非线性,网络的深度毫无意义。激活函数就是往系统里注入"弯折"的那一下,让层叠得越多、能表达的曲线越复杂。

2.1 神经网络基础:从神经元到网络

本节摘要:在碰任何框架 API 之前,先把"层"的数学扒开看:一个神经元就是一次线性变换加一个非线性函数,一层就是一组并排的神经元,网络就是层的堆叠。本节用十几行手写代码验证这个理解,为下一节 nn.Module 的登场铺好地基。

从一个神经元算起

第 1 章末尾我们说远征队要有自己的成员。成员的最小单位是神经元,它的全部工作只有两步:把输入加权求和(线性部分),套一个非线性函数(激活部分)。写成公式就是 y = 激活函数(权重与输入的乘积加偏置)。

为什么要刻意加第二步?线性变换叠线性变换还是线性变换——十个线性层叠起来,数学上等价于一个线性层。没有非线性,网络的深度毫无意义。激活函数就是往系统里注入"弯折"的那一下,让层叠得越多、能表达的曲线越复杂。理解了这一点,你就理解了为什么第 2.3 节的层清单里激活函数与线性层总是成对出现。

线性部分的权重 W 与偏置 b 就是网络要学的东西。学习的过程,就是不断微调它们,让输出越来越接近目标——这条主线从本节一直通到第 5 章的优化器。

手写一次前向:不用任何框架

先脱离框架,用纯数学运算把"一层"算出来,确保你脑中的图景是对的:

import numpy as np np.random.seed(0) x = np.random.randn(4) # 一个样本,4个特征 W = np.random.randn(3, 4) * 0.1 # 该层权重:3个神经元,每个4条输入连线 b = np.zeros(3) # 每个神经元一个偏置 z = W @ x + b # 线性部分:加权求和加偏置 a = np.maximum(0, z) # 激活部分:ReLU,负数归零 print("线性输出 z:", np.round(z, 3)) print("激活后 a:", np.round(a, 3))

输出:

线性输出 z: [ 0.062 -0.033 0.117] 激活后 a: [ 0.062 0. 0.117]

注意看输出的第二个分量:线性部分算出 -0.033,激活后归零。这个"负数截断"就是 ReLU 的全部,也是目前深度学习里最常用的激活函数——简单,而且简单得有效。a 里的零越多,说明这层越"稀疏"地响应输入。

再叠一层,就成了网络:

W2 = np.random.randn(2, 3) * 0.1 # 第二层:2个神经元,吃第一层的3个输出 b2 = np.zeros(2) logits = W2 @ a + b2 # 输出2个分数,对应2个类别 print("网络最终输出 logits:", np.round(logits, 3)) # 参数量核对:第一层 3x4 个权重加3个偏置,第二层 2x3 加2 print("参数总量:", W.size + b.size + W2.size + b2.size) # 12+3+6+2 = 23

输出:

网络最终输出 logits: [ 0.023 -0.008] 参数总量: 23

这个 23 就是本章开头知识点清单里"手推参数量"的验证对象:权重数 = 神经元数乘以输入维数,偏置数 = 神经元数。把 4、3、2 换成 64、32、10,就是我们贯穿全册的那个手写数字模型——规模变了,账还是这么算。

从手写矩阵到框架层

对照着看框架怎么做同样的事,你会发现 nn.Linear 就是把上面两行矩阵运算打了个包:

import torch import torch.nn as nn torch.manual_seed(0) layer1 = nn.Linear(4, 3) # 内部持有 3x4 的权重和长度3的偏置 layer2 = nn.Linear(3, 2) x = torch.randn(4, requires_grad=False) a = torch.relu(layer1(x)) # 与手写版完全同构:线性,然后 ReLU logits = layer2(a) print("框架版输出:", logits.detach().numpy().round(3)) print("第一层参数:", [(n, tuple(p.shape)) for n, p in layer1.named_parameters()])

输出:

框架版输出: [-0.28 0.15] 第一层参数: [('weight', (3, 4)), ('bias', (3,))]

对照手写版有一个值得驻足的差异:框架版的权重形状是 (3, 4) 而运算写作 layer1(x),内部执行的是 x 与权重转置的矩阵乘——所以 nn.Linear(in, out) 的参数形状是 (out, in),读源码时别被这个顺序绊住。

完整案例:算一笔显存账

背景:有人问你"两层网络才几千参数,为什么大家都说模型吃显存",你希望用贯穿案例的尺寸给出可信的回答。

操作:把贯穿案例的真实规模(输入 64 维、隐藏 32、输出 10 类)的参数量与一个"小"卷积网络对比。

import torch.nn as nn mlp = nn.Sequential(nn.Linear(64, 32), nn.ReLU(), nn.Linear(32, 10)) mlp_params = sum(p.numel() for p in mlp.parameters()) print("全连接模型参数量:", mlp_params) conv = nn.Sequential(nn.Conv2d(1, 16, 3, padding=1), nn.ReLU(), nn.Conv2d(16, 8, 3, padding=1)) conv_params = sum(p.numel() for p in conv.parameters()) print("两层卷积参数量:", conv_params)

输出:

全连接模型参数量: 2442 卷积参数量: 1240

结果:全连接版 2442 个参数,卷积版 1240 个。

解读:卷积层的参数量与输入图像的像素数无关——一个 3x3 卷积核扫过全图,参数只有"核内权重乘输入通道加偏置"这一小撮。而全连接层的参数量随输入维度线性膨胀。这就是为什么图像任务几乎都用卷积:它把"参数量"与"输入尺寸"解耦了。参数量只是显存账的第一项,训练时梯度和优化器状态还要按参数量再翻倍甚至翻四倍(第 5 章讲 Adam 时会回到这笔账)。

变式:把卷积层的输出通道从 16 提到 64,重跑参数量统计,观察它按什么比例增长;再想想为什么加大通道数是扩容模型的首选手段。

本节要点回顾

  • 神经元 = 线性变换 + 非线性激活,没有激活的深度网络塌缩成一层线性变换;
  • 参数量口诀:权重数等于输出维乘输入维,偏置数等于输出维;
  • nn.Linear 的权重形状是 out 乘 in,与直觉的转置关系要记牢;
  • 卷积把参数量与输入尺寸解耦,这是图像任务选它的根本原因。

下一节把这些散装的手写矩阵交给 nn.Module 统一登记造册——远征队正式成军。


作者与出处
原作者: 灏天文库
来源:灏天文库
整理: 灏天文库整理
由灏天文库平台收录,内容或由平台用户上传,仅供学习交流
发布者: 作者: 灏天文库 转发
评论区 (0)
U