☰
PyTorch重写神经元与层实验:张量形状、参数归属与激活位置
2026/10/27 11:24:55 网站建设 项目流程

很多人第一次打开这个实验的原始版本时,会觉得"就几个神经元,有什么好讲的",然后半小时后卡在一行形状报错上出不来。这个实验的名字里带着"神经元与层",看起来是深度学习最基础的一课,但它真正训练的是你对张量形状、参数归属、激活位置这三件事的直觉。原版实验用的是那套声明式的接口,一层一行,写起来很爽,但爽的代价是你看不见内部发生了什么。换成 PyTorch 重写之后,前向传播必须自己一行行写出来,形状对不对、激活加没加、权重从哪来,全部摊在明面上。这篇文章就是把这个中英对照的 PyTorch 版本从头拆一遍:单神经元怎么手搓、Dense 层怎么翻译成 Linear、两层堆起来之后形状怎么变、以及迁移过程中最容易翻车的那几个点。适合刚学完这门课想换框架练手的人,也适合已经会用 PyTorch 但没认真想过"一个神经元到底对应几行代码"的人。

1. 先搞清楚这个实验在教什么,而不是急着敲代码

1.1 一个神经元就是"加权和加激活",没有别的魔法

这个实验的第一部分永远是从单个神经元开始,输入两三个特征,输出一个值。数学上它简单到有点寒酸:把每个输入乘上对应的权重,全部加起来,再加上一个偏置项,最后过一个激活函数。写成式子就是 z 等于 w 转置乘 x 加 b,输出等于 g(z)。这里 g 就是激活函数,如果 g 是恒等映射,那这个神经元就退化成一个线性回归。很多人学到这里会觉得"这也太简单了",但恰恰是这个简单结构,堆一百层之后就成了能识别图像的东西,原因不在于单个神经元复杂,而在于组合方式和参数规模。

我在带人看这段代码的时候,习惯让他们先在纸上算一遍:输入是 1 和 2,权重是 1 和 2,偏置是 3,输出是多少。算出来是 8,然后再去看代码跑出来是不是 8。这一步看着幼稚,但它建立了一个非常重要的锚点——后面所有形状错误、维度对不上、结果诡异的问题,你都可以退回到这个手算版本去对照。实验里给出的那几个数值例子,本质上就是在给你这个锚点。别跳过它,直接去看后面的多层网络,那样你只是复制粘贴了一遍代码,什么都没学到。

还有一点值得强调:这个实验刻意把"线性部分"和"激活部分"分开讲。先是纯线性的神经元,然后才引入 sigmoid 或者 ReLU。这个顺序不是随便排的,它想让你意识到激活函数是外挂上去的,不是神经元自带的。这个认知在后面调模型的时候特别有用——当你发现模型怎么训都是线性的效果,第一反应应该是去检查激活函数是不是漏了或者被写成了恒等。

1.2 层不是新东西,就是一堆神经元并排站着

实验的第二部分把单个神经元扩展成一层。很多教程在这里会用"层"这个抽象词一笔带过,但你要是把它拆开看,一层有 n 个单元的 Dense,本质就是 n 个独立的神经元,每个都有自己的权重向量和偏置,只不过它们共享同一份输入。所以一层的权重矩阵形状是"输入维度 × 单元数",偏置是一个长度为单元数的向量。前向传播的时候,输入向量和这个矩阵相乘,一次性算出所有单元的结果。

这里有个特别容易混淆的点:矩阵乘法的顺序。数学课上写的是 W 转置乘 x,而在实际框架里,输入通常是按批次来的,形状是"批大小 × 特征数",所以实际做的是 x 乘 W,W 的形状是"特征数 × 单元数"。这两种写法描述的是同一件事,只是把权重矩阵转置了一下。实验里给的图示是按数学写法画的,代码里是按批处理写法实现的,如果你不注意到这个差别,看代码的时候就会觉得"这个矩阵形状怎么和图上不一样"。

我个人的经验是,在这一节一定要动手打印一次形状。把输入设成形状 1×2,层设成 3 个单元,然后打印输入、权重、输出三个张量的形状,你会发现输入是 (1, 2),权重是 (2, 3),输出是 (1, 3)。这个 (1, 3) 里的 1 是批次维度,3 是单元数。理解了这个,后面所有关于 batch 的困惑基本都能自解。

1.3 实验里那三个不起眼的变量,才是真正的考点

翻这个实验的原始代码,你会发现它反复在设置三个东西:权重、偏置、激活函数。而且它会刻意用一组固定值去初始化权重,而不是让它随机。为什么?因为如果权重随机,你根本没法判断输出对不对。固定权重之后,输出就是一个确定值,你可以手算验证。这是教学实验和真实训练最大的区别——真实训练里你巴不得权重随机一点,教学实验里你必须让它是确定的。

这件事延伸出一个实践中的习惯:在写任何新的网络结构时,先用固定权重跑通一次前向,确认形状和数值都对,再去接数据、接损失、接优化器。我自己踩过太多次坑,直接在复杂模型上改结构,跑出来 loss 不降,排查半天最后发现是某一层的输出维度写错了,输入被静默广播成了一个奇怪形状。如果一开始就用固定权重加小张量做一次前向校验,这个问题两分钟就能发现。

激活函数这一块也有个细节:实验里会对比"有激活"和"无激活"两种情况。无激活时,两层线性网络在数学上等价于一层线性网络,因为两个线性变换复合还是线性变换。这个结论看着像废话,但它是理解"为什么需要非线性"的关键。你在 PyTorch 里把 ReLU 去掉跑一次,会发现不管堆多少层,表达能力都停留在线性回归的水平,这就是最直观的证明。

2. 把原版的 Dense 翻译成 PyTorch 的 Linear,坑比想象的多

2.1 权重矩阵的方向是反的,这是第一个硬坑

原版实验里的全连接层,权重矩阵的形状是"输入维度 × 输出维度"。而 PyTorch 的nn.Linear(in_features, out_features),内部权重weight的形状是"输出维度 × 输入维度"。也就是说,同样是一层 2 输入 1 输出的结构,原版权重是 2×1,PyTorch 权重是 1×2。这个差异不是命名习惯问题,而是因为 PyTorch 底层用的是 x 乘 W 转置的形式。

如果你只是自己搭网络从头训练,这个差异完全不影响你,因为权重是随机初始化的。但如果你想复现实验里那组固定权重的数值结果,就必须手动把权重按 PyTorch 的形状塞进去,否则你会得到一串对不上的数,然后怀疑自己公式推错了。我见过不少人在这里卡半小时,反复检查前向公式,其实公式没错,就是矩阵转置了。

手动赋值的标准写法是这样,注意一定要包在torch.no_grad()里,否则会破坏计算图或者在参数带梯度的时候报错:

import torch from torch import nn layer = nn.Linear(2, 1) with torch.no_grad(): layer.weight.copy_(torch.tensor([[1.0, 2.0]])) layer.bias.copy_(torch.tensor([3.0])) x = torch.tensor([[1.0, 2.0]]) print(layer(x)) # tensor([[8.]])

这段代码跑出来是 8,和手算一致。注意copy_后面的下划线,它表示原地操作。用copy_而不是直接赋值layer.weight = ...,是因为后者会替换掉参数对象,导致优化器里注册的引用失效,虽然在这个纯前向的实验里看不出问题,但养成用copy_的习惯能避免以后接优化器时出诡异 bug。张量的 dtype 也要匹配,默认是 float32,如果你硬塞一个整数张量会报类型错误。

2.2 默认初始化策略不一样,别指望两边随机出来一样

原版框架的全连接层默认用 Glorot 均匀分布初始化,而 PyTorch 的nn.Linear默认用的是 Kaiming 均匀分布来初始化权重,偏置则是在正负 fan_in 平方根分之一之间均匀取值。这两套策略的出发点和数值范围都不同。这意味着,即使你把网络结构完全对齐,两边的初始输出也不会一样,训练曲线也不会一样。

这个差异在教学实验里不构成问题,因为实验用的是固定权重。但在你做迁移验证的时候,如果你拿原版的输出和 PyTorch 的输出对比,发现数值差了一截,先别急着怀疑自己写错了,很可能就是初始化不同导致的。判断方法很简单:把两边的权重都手动设成同一组值,再看输出是否一致。如果一致,说明结构翻译没问题,剩下的差异都是初始化带来的。

如果你想在 PyTorch 里模拟原版的初始化,可以手动调用nn.init.xavier_uniform_(layer.weight),偏置用nn.init.zeros_(layer.bias)。不过我个人不建议在这种教学实验里花太多时间对齐初始化,因为真实项目里初始化策略是要根据激活函数和网络深度来选的,Kaiming 配 ReLU、Xavier 配 tanh 或 sigmoid,这才是你要记住的对应关系。实验的价值在于让你理解"初始化会影响训练起点",而不是让你背诵某个框架的默认值。

顺带说一个实操细节:nn.Linear的参数是有偏置的,构造时传bias=False可以关掉。有些网络结构里偏置是冗余的(比如后面紧跟 BatchNorm 的卷积层),关掉能省一点参数。但在这个实验里,偏置是教学内容的一部分,必须保留,别手贱关掉。

2.3 手写一个最小可用的 Neuron 类,比调包更有价值

PyTorch 里没有叫"Neuron"的现成组件,最接近的就是nn.Linear。但为了让这个实验的教学意图落地,我建议你手写一个极简的类,把线性变换和激活显式分开:

class SingleNeuron(nn.Module): def __init__(self, n_features, activation=None): super().__init__() self.linear = nn.Linear(n_features, 1) self.activation = activation def forward(self, x): z = self.linear(x) if self.activation is None: return z return self.activation(z)

这个类只有十几行,但它把三件事讲清楚了:参数在线性层里、激活是独立对象、前向过程是显式两步。对比一下原版那种一行Dense(1, activation='sigmoid')的写法,你会发现 PyTorch 逼着你把"激活加在哪"这件事写明白。这个显式性在简单模型里显得啰嗦,在网络复杂的时候就是救命的东西——比如你想在激活前后取中间值做可视化,或者想给某一层加个残差连接,显式前向让你有地方插代码。

我在实际项目里也保留了这个习惯:哪怕最后会用nn.Sequential把层拼起来,前期调试阶段一定先写成显式的forward,等结构确认没问题了再考虑要不要简化。因为Sequential一旦拼错顺序,报错信息往往指向一个你根本没注意的层,排查成本比多写几行代码高得多。

3. 从单神经元到两层网络,一步步跑通并验证

3.1 环境准备和可复现性设置,别省这几行

开始写代码之前,把随机种子固定住。这个实验虽然主流程用的是固定权重,但后面演示"随机初始化后多次前向结果不同"的时候会用到随机数,不固定种子的话每次跑出来都不一样,你就没法判断是自己改错了还是随机性导致的。标准写法是torch.manual_seed(42),如果涉及 numpy 的部分,再补一句np.random.seed(42)。

环境上没什么特殊要求,PyTorch 的 CPU 版本足够跑完整个实验,因为模型小到可以忽略不计。但有两个版本相关的细节值得注意:第一,nn.Linear的输入可以是二维的"批 × 特征",也可以是一维的"特征",前者输出是"批 × 单元数",后者输出是一维。这两种行为在旧版本里有过细微变化,如果你看到输出形状和自己预期不符,先打印一下x.dim()。第二,从某个版本开始,PyTorch 默认开启了更严格的形状检查,某些以前能靠广播混过去的形状错误现在会直接报错,这其实是好事,报错比静默算错强。

还有一点,如果你在带 GPU 的机器上跑,记得把模型和数据放到同一个设备上。这个实验规模太小,用 GPU 纯属浪费,但养成model.to(device)和x.to(device)成对出现的习惯,能让你以后迁移到大模型时少踩一类经典错误。我见过最典型的翻车现场是模型在 GPU 上、输入在 CPU 上,报错信息只说设备不一致,不告诉你是哪一层,新手要愣很久。

3.2 单神经元前向:把数值校验做成固定动作

单神经元这一节的核心动作是"算一遍、跑一遍、比一遍"。我建议你这样组织代码:先定义一组输入和权重,手算出期望输出写在注释里,然后跑代码,用torch.allclose或者直接打印对比。比如输入[1.0, 2.0]、权重[1.0, 2.0]、偏置3.0,期望是 8.0。代码跑出 8.0,这一节就算过了。

接下来换成 sigmoid 激活再试一次。sigmoid 的定义是 1 除以 1 加 e 的负 z 次方,z 等于 8 的时候,输出约等于 0.99966。这个数你可以用计算器验证,也可以直接看代码输出。这一步的意义在于确认激活函数接的位置对——它是接在加权和之后,不是接在单个输入上。听起来很傻,但我确实见过有人把激活写在了每个输入元素上,然后困惑为什么结果和公式对不上。

还有个细节:PyTorch 里 sigmoid 有好几种写法,torch.sigmoid、nn.Sigmoid()、torch.nn.functional.sigmoid。前两个在数值上等价,第三个已经被标记为弃用,不建议用。在forward里我倾向于用函数式写法torch.sigmoid(z),因为它不占模块树的位置,序列化的时候也不会多出一层。当然如果你习惯用模块式,只要保持一致就行,别在一个项目里混着用。

3.3 用 Linear 拼一层,重点观察形状怎么变

从单神经元过渡到一层,最直观的方式是把 1 个输出换成 3 个输出。nn.Linear(2, 3)意味着输入两个特征,输出三个值,这三个值对应三个"神经元"各自的加权和。如果你不做任何激活,这三个输出就是三个独立的线性组合,彼此之间没有交互——这一点很多人会误解,以为同一层里的神经元会互相影响,其实同层之间是平行的,交互发生在层与层之间。

代码上这样写:

layer = nn.Linear(2, 3) x = torch.tensor([[1.0, 2.0]]) with torch.no_grad(): layer.weight.copy_(torch.tensor([ [1.0, 0.5, -1.0], [2.0, 1.0, 0.0], ])) layer.bias.copy_(torch.tensor([0.0, 1.0, -1.0])) print(layer(x).shape) # torch.Size([1, 3]) print(layer(x))

第一行输出等于 1 乘 1 加 2 乘 2 加 0,等于 5;第二行是 0.5 乘 1 加 1 乘 2 加 1,等于 3.5;第三行是 -1 乘 1 加 0 乘 2 减 1,等于 -2。跑出来对着这三个数,你对"一层"的理解就从抽象变成了具体的。

这里有个高频困惑点值得单独说:为什么输出形状是 (1, 3) 而不是 (3,)。那个 1 是批次维度,表示你一次喂了一个样本。如果你传进去的是形状 (2,) 的一维张量,输出就是形状 (3,),批次维度消失了。这两种行为 PyTorch 都支持,但会让你的代码在不同场景下表现不一致,所以统一约定所有输入都带批次维度,哪怕只有一个样本也写成[[1.0, 2.0]]而不是[1.0, 2.0]。这个习惯能帮你避开后面接 DataLoader 时的绝大多数形状问题。

3.4 两层堆起来,形状链要走一遍

实验的最后一部分是把两层堆起来,中间加激活。对应的 PyTorch 代码可以是显式的,也可以是nn.Sequential的。先看显式版本:

class TwoLayerNet(nn.Module): def __init__(self): super().__init__() self.fc1 = nn.Linear(2, 3) self.act = nn.ReLU() self.fc2 = nn.Linear(3, 1) self.out = nn.Sigmoid() def forward(self, x): x = self.fc1(x) # (batch, 2) -> (batch, 3) x = self.act(x) # (batch, 3) -> (batch, 3) x = self.fc2(x) # (batch, 3) -> (batch, 1) return self.out(x) # (batch, 1) -> (batch, 1)

每一行后面我都标了形状变化,这不是为了好看,而是这个实验最值得带走的习惯。你在写任何网络的时候,只要做到"每一层的输入输出形状心里有数",百分之八十的调试时间都能省掉。

有个小问题值得停下来想一想:最后那个 sigmoid 是必须的吗?如果这是个回归任务,输出一个实数,那就不该加 sigmoid,因为它会把输出压到 0 到 1 之间。如果这是个二分类任务,sigmoid 就是把 logits 转成概率的标准做法。实验里加 sigmoid 是为了演示激活函数的用法,不代表任何任务都该这么加。这个区分在实际项目里非常关键,我见过有人做房价预测,最后一层挂了 sigmoid,模型怎么调都只能预测 0 到 1 之间的数,排查了好久才反应过来是激活用错了。

用nn.Sequential写就是另一种风格:

model = nn.Sequential( nn.Linear(2, 3), nn.ReLU(), nn.Linear(3, 1), nn.Sigmoid(), )

简洁是简洁,但你失去了在中间插调试代码的位置。我的建议是:教学阶段和调试阶段用显式写法,结构稳定并且需要部署的时候再考虑Sequential。这不是风格偏好问题,而是可维护性问题。你能一眼看出Sequential里第四层是 sigmoid,但当网络有二十层的时候,靠数位置来定位某一层是很痛苦的。

4. 中英对照版本怎么读才不浪费时间

4.1 先建立术语映射表,别边读边查

这个实验的中英对照版最容易出现的低效场景,就是读一句英文查一个词,读完一段已经忘了前面在讲什么。比较靠谱的做法是先花十分钟把核心术语的对应关系过一遍,形成一个稳定的映射,后面读起来就顺了。这个实验涉及的核心术语其实不多,我整理成下面这张表:

英文术语中文对应在本实验里的具体含义
neuron神经元一个线性变换加一个激活的最小单元
layer层若干个神经元并联组成的结构
dense / fully connected全连接每个输入都连接到每个输出单元
weight / kernel权重线性变换里的乘数矩阵
bias偏置线性变换里的加数
activation激活作用在加权和上的非线性函数
forward propagation前向传播输入逐层算到输出的过程
unit单元层里的一个神经元
logits未归一化得分激活函数之前的原始输出
shape形状张量各维度的大小

把这些词过一遍之后再去读原文,你会发现阅读速度明显不一样。特别是logits这个词,中文材料里有时译成"逻辑值"有时直接不译,第一次遇到很容易懵,其实它就是指"还没过 softmax 或 sigmoid 的原始输出"。理解了这一点,你看很多代码里变量名叫logits就知道它后面大概率还要接一个激活。

4.2 注释和输出信息的双语组织,影响的是复现效率

中英对照版本在代码注释上的处理,直接决定了你半年后回来看还能不能看懂。我自己的做法是:关键公式和形状变化用中文注释,变量名和函数名保持英文原样,打印输出用中英双语。变量名不要翻译,weight就写weight,翻译成权重反而会让代码和框架文档对不上,搜索的时候也搜不到。

打印输出这一块值得多说两句。实验里会打印形状、打印数值,如果你只打印中文,对照原始英文材料的时候会有点错位;如果只打印英文,读起来又慢。双语打印的成本其实很低,比如print(f"输入形状 input shape: {x.shape}")这样一行,两边都能对得上。我在做跨语言学习材料的时候一直用这个方式,效果比想象中好,因为你在两种语言之间切换的时候,大脑会自动做一次校验,反而更容易发现理解偏差。

还有个小技巧:把每个代码块的中文说明放在代码上方,英文原文的关键句放在代码下方的引用块里。这样从上到下读是一个完整的中文叙述流,需要对照原文的时候往下扫一眼就行,不用来回翻页。

4.3 阅读顺序建议:先跑通再精读

我的建议是反着来。先不要逐字读英文原文,直接把 Python 代码跑一遍,看输出,建立"这个实验大概在干什么"的整体印象。然后回头读中文说明,把每个代码块对应到"线性变换""激活""形状变化"这三个概念上。最后再读英文原文,这时候你是在验证自己的理解,而不是在解码陌生的表达,效率完全不一样。

这个顺序的好处在于,英文原文里有些表述是按它那套框架的语境写的,比如"层会自动处理批次维度"这种话,在 PyTorch 语境下需要重新理解。如果你先读原文再看代码,很容易把原文的框架假设带进来,然后在 PyTorch 里找不到对应的东西干着急。先跑代码建立起 PyTorch 的心智模型,再去读原文,你就能自动做语境转换。

5. 迁移过程中真正会卡住人的几个地方

5.1 形状报错怎么倒推:从报错信息里读出期望值

PyTorch 的形状报错信息其实写得很清楚,只是新手不知道怎么读。典型的一条是mat1 and mat2 shapes cannot be multiplied (1x3 and 2x1),这里面已经告诉你了:左边矩阵是 1 乘 3,右边是 2 乘 1,3 和 2 对不上所以乘不了。你需要的动作是找到这两个矩阵分别是谁——左边通常是你的输入或者上一层的输出,右边是当前层的权重。

排查链路我一般是这么走的:第一步,在报错的那一层前面插一句print(x.shape),确认实际传进来的是什么形状。第二步,打印这一层的weight.shape,确认它期望什么形状。第三步,对比两者,看差在哪个维度。第四步,往回找是哪一步导致形状变成这样的。这四步走完,问题基本就定位了。

最常见的两种成因:一是忘了批次维度,传了个一维张量进去,某些层能兼容所以没报错,到某一层突然不兼容了;二是上一层的输出单元数和下一层的输入特征数写得不一致,比如上一层是nn.Linear(2, 3),下一层写成了nn.Linear(4, 1)。第二种错误在层数多的时候特别容易发生,因为你改了一层的输出维度,忘了同步改下一层的输入维度。这也是我推荐显式写forward的原因之一——你把形状注释写在每行后面,改的时候一眼就能看到哪里需要同步。

5.2 dtype 和 device 不一致,报错信息往往指不到根因

copy_权重的时候,如果源张量是 float64 而目标参数是 float32,会直接报类型不匹配。从 numpy 转过来的张量默认是 float64,从torch.tensor([1.0, 2.0])建的默认是 float32,混用就会出问题。解决办法是在建张量的时候显式指定dtype=torch.float32,或者用.float()转一下。

设备不一致的表现更隐蔽一些。如果模型在 GPU 上、输入在 CPU 上,报错会说设备不一样,但不会告诉你具体是哪一步引入的。在这个小实验里根本用不到 GPU,所以最简单的做法是全程不碰.to(device),让所有东西都在 CPU 上,省掉一整类问题。等你要跑真正的训练任务时再引入设备管理,那时候你会更清楚该怎么组织代码。

还有一个容易被忽略的点:torch.no_grad()的作用范围。手动改权重、做纯推理验证的时候一定要包在里面,否则 PyTorch 会为这些操作构建计算图,浪费内存不说,在某些场景下还会报"试图对需要梯度的张量做原地操作"的错误。这个错误信息挺吓人的,但根因往往就是漏了一个with torch.no_grad():。

5.3 随机种子固定了,结果还是不一样是怎么回事

固定了torch.manual_seed之后结果还是每次不同,通常有三个原因。第一,你用了 numpy 的随机函数但没固定 numpy 的种子,这两套随机数生成器是独立的。第二,你用了 DataLoader 并且开了多进程,每个 worker 有自己的随机状态,需要额外设置worker_init_fn。第三,你在代码里某处调用了不依赖种子的操作,比如某些哈希相关的行为。

在这个实验里,前两个原因基本不会出现,因为数据是手写的小张量。但第三个值得注意:如果你发现两次运行的输出在小数点后很多位才有差异,那可能不是随机性问题,而是浮点累加顺序不同导致的。CPU 上单线程的话一般不会,多线程归约的时候会出现。这种差异通常在 1e-7 量级,不影响教学验证,用torch.allclose比较的时候记得把容差设合理一点,默认的 1e-8 有时候太严了。

5.4 激活函数选错或者漏加,是这类实验最高频的"静默错误"

形状错了会报错,dtype 错了会报错,但激活漏了不会报错,它只是让模型悄悄地退化成线性模型。这是最难查的一类问题,因为没有错误信息,只有"效果不对"。

自查方法很直接:打印每一层的输出,看数值范围。如果某一层输出全部落在负无穷到正无穷且分布很宽,说明没激活;如果全部落在 0 到 1,说明过了 sigmoid;如果一半是 0 一半是正常值,说明过了 ReLU。这个小检查在网络很深的时候尤其有用,你能快速看出激活是不是按预期接上了。

再补一个判断依据:如果你堆了三层以上,训练效果和单层线性回归差不多,那基本可以确定中间没有有效激活。这个判断在真实项目里也成立,我遇到过有人抱怨"深层网络还不如浅层",最后发现是激活函数被写成了一个恒等映射的自定义函数,注释里写着"临时替换测试",然后忘了改回来。

6. 把这个实验的价值用足:几个可以自己加的练习

跑完实验给的标准流程之后,如果就停在这里,收获其实有限。我建议加三个小练习,每个都不超过二十分钟,但能把理解推进一大截。

第一个练习:把两层的网络改成三层,自己决定每层的单元数,然后打印每一层的输出形状,确认形状链条对得上。这个练习的目的是让你从"照着抄"过渡到"自己设计",重点不是效果好不好,而是形状能不能自洽。

第二个练习:把中间的 ReLU 换成 tanh,再换成不激活,同一个输入跑三次,对比输出的数值范围。你会直观地看到三种情况下输出分布的区别,这比看任何解释都管用。做完这个练习,你对"激活函数决定了信号的范围和形态"这件事会有体感。

第三个练习:手动把两层网络的权重全部设成同一个常数值,然后看输出。你会发现所有单元的输出完全一样,这就是所谓的对称性问题——如果所有权重相同,多个神经元就退化成一个。这个观察解释了为什么初始化必须随机,也解释了为什么真实的训练过程需要打破对称。这个知识点在很多教程里只是一句话带过,但你自己动手验证一次,印象会深得多。

这三个练习我都实际做过,做完之后再看那些讲"深层网络表达能力"的材料,理解会完全不一样。实验本身很小,但它是那种"麻雀虽小五脏俱全"的类型,你往里加多少东西,它就能承载多少东西。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询