ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

手写神经元:从加权求和到梯度下降,彻底搞懂深度学习的最小积木

手写神经元:从加权求和到梯度下降,彻底搞懂深度学习的最小积木 我一直觉得深度学习入门最大的坎不是模型、不是框架而是最底层的“神经元”这三个字。很多人上来就敲model.add(Dense(64, activationrelu))敲得飞起但要是问一句“这个神经元到底在算什么它凭什么能学习”十有八九会卡壳。我自己学的时候也是这样看了好多教程要么直接甩公式要么把生物神经科学翻出来讲一堆离子通道结果越看越懵。所以这篇学习笔记我不打算堆公式而是用一种“从问题出发”的方式把神经元的工作原理掰开了讲清楚配合一点简单的numpy代码让你看完能自己动手验证而不是光记结论。这篇笔记适合什么人首先是那些已经会用TensorFlow或PyTorch跑通几个Demo、但对底层原理还觉得虚的学习者其次是和曾经的我一样被“神经元”“激活函数”“反向传播”这些词吓住的初学者。读完你不需要马上懂所有数学细节但你应该能回答三个问题单个神经元在做什么计算它怎么调整自己的参数它为什么解决不了稍微复杂一点的问题这三个问题串起来就是整个深度学习大厦的起点。1. 从生物神经元到数学模型的映射为什么叫“神经元”1.1 生物神经元的三要素与启发先聊点背景。19世纪末卡哈尔等人用染色法画出了大脑神经元的形态一个细胞体伸出很多树突还有一根长长的轴突。树突接收来自其他神经元的信号细胞体对信号做整合超过某个阈值时轴突就会产生一个脉冲传给下一个神经元。这就是著名的“全或无”法则——信号要么引发一次放电要么不引发没有中间状态。但人工神经元并没有真的去模拟这种生物电化学过程它只借鉴了三个最核心的抽象多条输入通道对应树突。对这些输入做加权求和对应细胞体的整合作用。一个非线性函数把求和的连续值映射到某个输出范围对应“阈值激发”或者不激发。这三条抽象被麦卡洛克和皮茨在1943年用数学严格表达了出来就是我们后来看到的MP模型。MP模型的伟大之处在于它第一次用逻辑运算的角度把神经元形式化了只要给每个输入配一个权重再通过一个阈值判断输出是0还是1就能模拟AND、OR这些逻辑运算。换句话说神经元不是一个生物概念而是一个计算单元。1.2 人工神经元的两个核心计算现在我们把手头的“人工神经元”彻底数学化。假设一个神经元接收n个输入(x_1, x_2, ..., x_n)每个输入对应一个权重(w_1, w_2, ..., w_n)另外还有一个偏置(b)。那么神经元内部顺序做两件事线性加权求和(z \sum_{i1}^{n} w_i x_i b)非线性激活(a f(z))这个(z)常被称为神经元“净输入”或“logits”(a)才是这个神经元真正对外输出的结果。权重(w)决定了每个输入对神经元的影响程度偏置(b)则像一个调节阈值灵敏度的旋钮——它决定了当所有输入都为0的时候神经元处于什么状态。理解这两步非常关键因为后面所有的神经网络结构——全连接层、卷积层、甚至注意力机制——本质上都是在设计“输入怎么加权求和”以及“求和结果怎么变换”。区别只是把(x_i)换成了图像像素、词向量或者其他特征。这里有个特别容易忽略的点没有偏置(b)的话神经元只能拟合通过原点的直线/平面表达能力一下就砍掉一截。我在初学的时候就犯过这个糊涂总觉得偏置可有可无后来自己用单神经元拟合(y 3x 2)时才发现去掉偏置怎么调权重都拟合不出截距。所以现在看到任何Dense层我都会下意识确认它是否默认启用了bias。2. 感知机到神经元激活函数才是“神经”的体现2.1 阶跃函数与线性不可分问题早期的MP模型里激活函数用的是阶跃函数[ f(z) \begin{cases} 1 z 0 \ 0 otherwise \end{cases} ]这种神经元的输出是离散的对应“激发/不激发”。用它做一个二分类器就是大名鼎鼎的感知机Perceptron。感知机在1960年代火过一阵但随后马文·明斯基在1969年用一本《感知机》指出了它的致命短板单个感知机只能解决线性可分问题连一个简单的异或XOR都学不出来。为什么因为异或问题的数据分布不像AND和OR那样可以画一条直线分开。XOR的四个点(0,0)-0(0,1)-1(1,0)-1(1,1)-0你永远找不到一条直线能把这四个点正确分成两类。而单层感知机的决策边界就是一条直线超平面天然没能力处理这种非线性关系。这个发现直接导致了神经网络第一次寒冬。现在回过头看问题的根源不在“神经元”本身而在“单层”和“阶跃函数”这两个限制上。后来大家搞明白了只要把多个神经元堆成多层再把阶跃函数换成连续可导的非线性函数这个死结就解开了。2.2 Sigmoid和ReLU的选择逻辑既然阶跃函数不可导没法用梯度方法学习那就换一个平滑的、可导的、输出范围有限的函数。最早被广泛采用的是Sigmoid[ \sigma(z) \frac{1}{1 e^{-z}} ]它的输出落在(0,1)可以解释成概率也能提供平滑的梯度。但Sigmoid有两个让人头疼的问题一是当(z)的绝对值很大时梯度几乎为0会造成梯度消失二是输出不以0为中心会让后续层接收到“全正”或“全负”的信号导致优化变慢。后来ReLU横空出世[ \text{ReLU}(z) \max(0, z) ]它简单到粗暴却在实践中效果惊人。ReLU在正区间梯度恒为1不会让梯度缩水计算开销几乎为零而且它天然产生稀疏激活神经元输出为0的比例很高。代价是负区间梯度为0一旦某个神经元落入负区它可能永远得不到更新——这就是“神经元死亡”。我在自己的实验里遇到过好几次ReLU死亡问题训练到一半某些神经元的输出恒为0特征图全黑loss也降不下来了。后来排查才知道是学习率设太大把权重的初始更新推入了负区。解决办法包括用小一点的学习率、改用Leaky ReLU或者用合理的初始化方法。所以你看激活函数的选择不只是“哪个准确率高一点”的问题它直接决定了参数更新的健康程度。这里我想多说一句很多人把激活函数当装饰品觉得随便换一个无伤大雅。实际上激活函数是神经网络能拟合非线性函数的关键。没有激活函数叠加再多层也不过是一个线性变换的复合最后还是线性变换等于白堆。可以说非线性激活函数才是“神经网络”能逼近任意复杂函数的核心原因。3. 神经元的参数学习损失函数与梯度下降如何驱动“学习”3.1 损失函数如何定义“错得多离谱”神经元里的权重和偏置不会自己变好得靠一个明确的目标来指导。这个目标就是损失函数Loss Function。在单个神经元的场景里最直观的损失函数是均方误差MSE[ L \frac{1}{m} \sum_{j1}^{m} (y_j - a_j)^2 ]其中(y_j)是第(j)个样本的真实值(a_j)是神经元预测值。MSE的几何含义很简单预测和真实之间距离平方的平均误差越大惩罚越重。但在分类任务里我们更常用交叉熵损失。因为它和概率分布挂钩还能避免MSE在Softmax场景下梯度太弱的问题。说到这你可能发现了损失函数并不是随便选的它和输出层的激活函数是配套的。比如二分类输出层用Sigmoid配合二元交叉熵BCE多分类输出层用Softmax配合多元交叉熵。它们的组合在数学上推导出来的梯度形式特别干净不像MSE配Sigmoid那样求导后会多乘一个接近0的(\sigma(z))导致参数学不动。我自己刚开始写代码时习惯随意换损失函数直到有一次用MSE训练一个分类模型发现loss一直在0.25附近不下降换成交叉熵以后很快收敛。后面才理解这不是优化器的问题是损失函数和激活函数搭配的问题。你不需要背所有组合的公式但至少要知道当你把输出层激活函数改了损失函数大概率也要跟着改。3.2 梯度下降在单个神经元上的几何直觉有了损失函数我们想找到一组权重(w)和偏置(b)让损失(L)最小。怎么找微积分告诉我们沿着梯度偏导数组成的向量的方向函数增长最快那沿着负梯度方向走函数下降就最快。于是就有了梯度下降的更新规则[ w_i \leftarrow w_i - \eta \frac{\partial L}{\partial w_i} ] [ b \leftarrow b - \eta \frac{\partial L}{\partial b} ]这里的(\eta)是学习率也就是每次沿着负梯度方向迈多大的步子。为了直观理解可以把(L)想象成一座山的高度权重是你脚下的坐标。你在山上看不到整座山只能感受到脚下最陡的上升方向。要下山就朝最陡的下降方向迈一步然后反复。是不是感觉像蒙着眼睛在山上摸索对梯度下降就是一个局部贪心的算法它只保证每一步都在下降不保证你最终走到的是全局最低点。在单个神经元上(L)关于(w_i)的偏导数可以通过链式法则求出来[ \frac{\partial L}{\partial w_i} \frac{\partial L}{\partial a} \cdot \frac{\partial a}{\partial z} \cdot \frac{\partial z}{\partial w_i} ]如果我们用MSE损失、Sigmoid激活那么这三项分别是(\frac{\partial L}{\partial a} \frac{2}{m} \sum (a - y))(\frac{\partial a}{\partial z} a(1-a))Sigmoid的导数(\frac{\partial z}{\partial w_i} x_i)三项乘起来再乘以(-\eta)就是权重更新的方向。这个过程对每一个训练样本或一小批样本重复权重就会一点一点调整loss也跟着下降。3.3 学习率的选择与常见陷阱学习率可能是超参数里最敏感的一个。设太大参数会在最优解附近震荡甚至发散设太小训练半天loss纹丝不动。我见过最典型的案例是把学习率从0.1改成1.0loss直接变成NaN。原因是梯度更新步长过大权重爆炸数值溢出。一个比较稳妥的做法是使用带自适应学习率的优化器比如Adam。它给每个参数维护自己的学习率能在训练初期大步探索、后期小步收敛。但这不代表普通梯度下降没用了——理解SGD是理解所有优化器的基础Adam本质上也是SGD加上一阶二阶动量修正。另外还要注意批大小batch size和学习率之间的配合。实践中有个经验法则当你把batch size增大到原来的k倍学习率可以相应调大一些比如乘以(\sqrt{k})或(k)的某种比例。这不是玄学因为更大的batch意味着梯度估计更稳定可以承受更大的步长。关于梯度下降有个绝佳的生活类比你在完全黑暗的球场上找最低点只能用脚尖感受地面坡度每步迈多大、什么时候从大步改小步这些决策都会影响你能不能找到真正的坑底。没有万能的参数组合但可以先跑一小段训练观察loss曲线的形状来调。4. 单个神经元的能力边界与多层网络的进化4.1 单神经元是线性分类器不管激活函数是Sigmoid还是ReLU只要输入输出之间只有一个加权求和再加一个固定的非线性变换那么它的决策边界本质上仍然是一个线性超平面只不过被激活函数压弯了输出值而已。为什么这么说你仔细观察就能发现对于一个二分类神经元它的预测结果只取决于(z w^T x b)的正负。(w^T x b 0)在二维平面上就是一条直线在三维空间中是一个平面在高维空间中是一个超平面。所以单神经元能完美区分线性可分的数据但对线性不可分数据无能为力。但奇怪的是实验又表明即使数据集线性不可分普通的单个神经元照样能训练只是正确率上不去。这是因为损失函数在优化时会把决策边界放在一个“使大多数样本分类正确”的位置少数线性不可分的点会表现为持续存在的损失。4.2 从神经元到层参数共享与特征复用既然一个神经元能力有限那就多放几个神经元并行排列成一层。多个神经元各自学会不同的权重模式就相当于在不同维度上对这个输入做特征提取。拿图像识别来说一层里的某些神经元可能对横线响应强另外一些对竖线响应强。把多层堆叠起来后续神经元就可以组合前一层学到的基础特征形成更抽象的概念。这里有一个关键设计层与层之间是全连接的意味着每一个输入特征都会连接到下一层的每一个神经元。这种设计的优点是信息不丢失缺点是参数数量大。所以后来CNN引入了局部连接和权值共享大幅压缩了参数规模。但CNN本质上也是由一个个“神经元”组成的只不过每个神经元的输入从全局向量变成了局部感受野。我建议你在学习深层网络之前一定要先把“单层内含多个神经元”这件事从数学上吃透。一个全连接层本质上就是输入向量乘以一个权重矩阵再加偏置向量[ a f(W x b) ]其中(W)的每一行就是一个神经元的权重向量。一次性更新一整层的(W)就是对这层所有神经元同时做梯度下降。理解这个矩阵乘法的视角后再看Transformer里的QKV矩阵、线性投影就会觉得异常亲切。5. 动手复现一个神经元的完整流程基于numpy5.1 手写单神经元训练代码光看公式理解得再到位也比不上亲手跑一遍。我们用Python和numpy写一个最简单的二元分类神经元。任务区分平面上的两类点一类在原点附近一类在右上方。数据是线性可分的目的是看神经元怎么在训练中调整决策边界。先造数据import numpy as np import matplotlib.pyplot as plt np.random.seed(42) # 生成两个类别类别0在左下类别1在右上 cls0 np.random.randn(50, 2) * 0.5 np.array([0.0, 0.0]) cls1 np.random.randn(50, 2) * 0.5 np.array([2.0, 2.0]) X np.vstack([cls0, cls1]) y np.concatenate([np.zeros(50), np.ones(50)])再初始化一个神经元输入是二维特征权重和偏置都设成0w np.zeros(2) b 0.0 lr 0.1 epochs 100训练循环里我们不用现成的框架手动计算梯度并更新参数。为了简化损失采用均方误差激活函数我们用Sigmoid因为输出是0到1便于和标签对齐def sigmoid(z): return 1 / (1 np.exp(-z)) for epoch in range(epochs): # 前向传播 z X w b # 线性加权 a sigmoid(z) # 激活 # 计算损失MSE loss np.mean((y - a) ** 2) # 反向传播 da 2 * (a - y) / len(X) # dL/da dz da * (a * (1 - a)) # dL/dz dw X.T dz # dL/dw db np.sum(dz) # dL/db # 更新参数 w - lr * dw b - lr * db if epoch % 20 0: print(fepoch {epoch}, loss {loss:.4f}, w {w}, b {b})跑完100轮你会看到loss不断下降w和b慢慢稳定在某个值。这个w就是那条分类直线的法向量b决定直线的偏移。终于神经元“学会”了把两类点分开。5.2 验证决策边界为了更直观我们可以把学到的决策边界画出来。决策边界的方程是(w_1 x_1 w_2 x_2 b 0)即(x_2 (-w_1 x_1 - b) / w_2)。加上下面这段绘图代码xx np.linspace(-1, 3, 100) yy (-w[0] * xx - b) / w[1] plt.scatter(cls0[:, 0], cls0[:, 1], labelclass 0) plt.scatter(cls1[:, 0], cls1[:, 1], labelclass 1) plt.plot(xx, yy, r--, labeldecision boundary) plt.legend() plt.show()你会看到一条红色的虚线把两类点完整分开。这条线的位置就是神经元在“学习”之后形成的分类规则。有意思的是随机画法不同最终得到的直线可能会略有不同——这也解释了为什么神经网络每次训练结果都不完全一样。5.3 常见错误排查跑代码的过程中新手最容易遇到几个问题我挨个说loss变成NaN。通常是因为学习率太大导致梯度过大更新后exp(-z)溢出。解决办法是降低学习率或者把输入X做标准化比如减均值除标准差让数值范围保持在温和区间。loss完全不下降。先检查是不是权重初始化太小比如全为0把梯度憋住了或者输入特征量纲太大导致梯度消失。试试把w初始化为一个小的随机数并检查a的值是不是始终在0.5附近。Sigmoid导致梯度消失。如果在深层网络里用Sigmoid这个问题会被放大。所以现代神经网络的隐藏层更推荐ReLU。我们这里为了演示方便才用Sigmoid。还有一个小坑数据样本顺序。如果数据是按类别排好的训练时梯度会在不同类别间剧烈摇摆。最好先打乱数据或者用随机小批量梯度下降。这不是理论问题是实际工程中经常被忽略的细节。6. 学习笔记中的几个关键经验与易错点6.1 神经元误解的“词源陷阱”现在很多人把神经网络想得过于神秘开口闭口“仿生”“模拟大脑”。实际上人工神经元更像一个线性加权累加器加一个按钮。它和真正的生物神经元差了十万八千里连简化模型都算不上。理解这一点有个好处当你碰到“脉冲神经网络SNN”这类更接近生物机制的模型时就会意识到它是另一套体系和深度学习里的传统神经元不是一回事。我在学习时也踩过一个“词源陷阱”总以为“激活”意味着神经元真的像生物那样“兴奋”或“抑制”。其实激活函数只是一个数学映射比如Sigmoid把任意实数压到(0,1)ReLU直接把负数变成0。它和生物放电没有对应关系。把概念去神秘化以后心态就轻松很多。6.2 画计算图是理解反向传播的捷径如果让我给一个建议那就是遇到复杂的神经网络结构先把它拆成计算图逐个节点标出输入输出和局部梯度。手动算一遍单个权重在反向传播中收到什么梯度比看十篇讲反向传播的文章都更有用。计算图的规则特别简单正向传播时从输入到输出传播数值反向传播时从损失开始沿着正向路径反过来传播梯度在每一个“节点”处用链式法则相乘。神经元的“节点”里只有加法和乘法所以梯度计算本质上就是“乘法分配律”。我第一次亲手写出dw X.T dz的时候突然就明白代码里的backward()函数在干什么了。6.3 从“神经元”出发怎么继续进阶学完单个神经元下一个自然的问题是如果是多个神经元组合成两层、三层反向传播的链式法则会怎么链得更长那一瞬间你就能理解为什么框架要给你自动求导为什么会有梯度消失/梯度爆炸以及为什么需要残差连接、BatchNorm这些“插在层与层之间的保险丝”。我的路线是先用手写神经元理解核心然后用numpy手写一层全连接网络最后再过渡到PyTorch。这个节奏比直接上框架要踏实得多。建议你也试试花一个下午把上面这段代码扩展到10个神经元的隐藏层你会亲眼看到原本单神经元解决不了的异或问题居然被一个带ReLU的隐藏层轻松解决了——那一瞬间的惊喜抵得上十节理论课。最后多说一句学习笔记和正式教程不太一样它允许我保留一些“尚未完全理解”的坑。比如“深度学习的万能逼近定理到底需要多少个神经元”我自己也还在琢磨。但正是这些问题让我一点点把地基打牢。神经元是深度学习最小的积木理解它不需要高深的数学背景只需要愿意动手跑代码、画图、调参然后在某个时刻突然“啊哈”一下。希望你也能在动手实验里找到属于自己的那个“啊哈”瞬间。
返回列表