【深度学习入门 Day 1】手算一个神经元从矩阵乘法到反向传播本文记录我学习深度学习第 1 天的内容从矩阵乘法、线性层、sigmoid 激活函数开始手算一个单神经元的前向传播、损失函数、链式法则、梯度下降和参数更新。文章目录一、为什么从单个神经元开始二、矩阵乘法神经网络的基本计算单元三、从线性层到单个神经元四、前向传播算出模型输出五、损失函数模型错得有多严重六、反向传播用链式法则算梯度七、梯度下降真正更新一次参数八、用 NumPy 写出训练循环九、补充为什么不直接把 y 改成 0.5十、今日总结一、为什么从单个神经元开始深度学习看起来很复杂但它的核心可以先压缩成一句话深度学习就是用梯度下降优化一个由矩阵运算和非线性函数组成的复合函数。拆开来看至少包含几件事数据和参数通常表示成向量、矩阵或张量。前向传播就是一串矩阵乘法、加法和激活函数。损失函数衡量模型预测和真实答案之间的差距。反向传播用链式法则计算每个参数应该怎么改。梯度下降根据梯度更新参数让损失逐步下降。今天我们不急着上大模型也不急着写复杂网络。先把一个最小单元单个 sigmoid 神经元手算清楚。二、矩阵乘法神经网络的基本计算单元先看一个形状问题A 的形状是 (3, 4) B 的形状是 (4, 2)那么A B 的形状是 (3, 2)原因是矩阵乘法要求左矩阵的列数 右矩阵的行数也就是(3, 4) (4, 2) - (3, 2)口诀可以记成内维相同外维成形神经网络中的线性层本质上就是矩阵乘法加偏置y Wx b其中x是输入特征。W是权重矩阵。b是偏置项。y是线性变换后的输出。三、从线性层到单个神经元一个最简单的神经元可以写成z w1*x1 w2*x2 b a sigmoid(z)其中z是线性加权结果。a是经过激活函数之后的输出。sigmoid会把任意实数压缩到(0, 1)。sigmoid 函数定义为sigmoid(z) 1 / (1 exp(-z))为什么需要激活函数如果只有线性层y Wx b那么就算堆很多层本质上仍然可以合并成一个大的线性变换。也就是说没有激活函数网络再深也只是线性模型。所以线性层负责混合信息激活函数负责引入非线性。四、前向传播算出模型输出设定一个具体例子x1 2 x2 3 w1 0.5 w2 -1 b 1先计算线性部分z w1*x1 w2*x2 b 0.5*2 (-1)*3 1 1 - 3 1 -1再经过 sigmoida sigmoid(-1) 1 / (1 exp(1)) ≈ 0.269如果这是一个二分类模型并且阈值设置为0.5a 0.5 - 判断为 1 a 0.5 - 判断为 0那么当前输出a 0.269 0.5所以模型会判断为0五、损失函数模型错得有多严重假设真实标签是y 1但模型输出a 0.269说明模型预测偏低。为了衡量错得多严重先使用平方损失L (a - y)^2代入数值L (0.269 - 1)^2 (-0.731)^2 ≈ 0.534这个损失不小。因为真实答案是1而模型只输出了0.269所以接下来训练的目标就是让输出a变大。由于a sigmoid(z)而 sigmoid 是单调递增函数所以想让a变大就要让z变大。六、反向传播用链式法则算梯度现在开始计算参数应该怎么改。已知L (a - y)^2 a sigmoid(z) z w1*x1 w2*x2 b对于w1计算链条是w1 - z - a - L所以根据链式法则dL/dw1 dL/da * da/dz * dz/dw11. 计算 dL/daL (a - y)^2对a求导dL/da 2(a - y)代入dL/da 2(0.269 - 1) -1.462这个负号很有意义它表示如果a增大损失会下降。2. 计算 da/dzsigmoid 的导数是da/dz a(1 - a)代入da/dz 0.269 * (1 - 0.269) 0.269 * 0.731 ≈ 0.1973. 计算 dz/dw1、dz/dw2、dz/db因为z w1*x1 w2*x2 b所以dz/dw1 x1 2 dz/dw2 x2 3 dz/db 14. 合成梯度对w1dL/dw1 -1.462 * 0.197 * 2 ≈ -0.576对w2dL/dw2 -1.462 * 0.197 * 3 ≈ -0.864对bdL/db -1.462 * 0.197 * 1 ≈ -0.288七、梯度下降真正更新一次参数梯度下降的更新公式是参数 参数 - 学习率 * 梯度设学习率lr 0.1旧参数是w1 0.5 w2 -1 b 1梯度是dL/dw1 ≈ -0.576 dL/dw2 ≈ -0.864 dL/db ≈ -0.288更新w1w1_new 0.5 - 0.1 * (-0.576) 0.5576更新w2w2_new -1 - 0.1 * (-0.864) -0.9136更新bb_new 1 - 0.1 * (-0.288) 1.0288注意梯度为负数时参数会变大。这是因为参数 参数 - 学习率 * 负梯度也就是参数 参数 一个正数更新后模型有没有变好用新参数重新计算zz_new 0.5576*2 (-0.9136)*3 1.0288 1.1152 - 2.7408 1.0288 -0.5968旧的z_old -1新的z_new -0.5968z变大了对应的 sigmoid 输出也变大sigmoid(-1) ≈ 0.269 sigmoid(-0.5968) ≈ 0.355虽然还没超过0.5但已经朝真实标签1的方向靠近了。八、用 NumPy 写出训练循环把刚才的手算过程写成代码importnumpyasnp xnp.array([2.0,3.0])y1.0wnp.array([0.5,-1.0])b1.0lr0.1defsigmoid(z):return1/(1np.exp(-z))forstepinrange(20):# forwardznp.dot(w,x)b asigmoid(z)loss(a-y)**2# backwarddL_da2*(a-y)da_dza*(1-a)dL_dwdL_da*da_dz*x dL_dbdL_da*da_dz# updateww-lr*dL_dw bb-lr*dL_dbprint(fstep{step:02d}, floss{loss:.6f}, fa{a:.6f}, fw{w}, fb{b:.6f})其中znp.dot(w,x)b对应手算公式z w1*x1 w2*x2 b而dL_dwdL_da*da_dz*x会一次性得到[dL/dw1, dL/dw2]这就是 NumPy 向量化的好处不用分别写w1和w2而是直接对整个权重向量操作。九、补充为什么不直接把 y 改成 0.5在理解这段程序时很容易产生一个想法既然 sigmoid 输出很难真正等于 1那把y改成0.5会不会更合适答案是不一定要看任务目标是什么。如果这是一个标准二分类任务标签通常是y 1 表示正类 y 0 表示负类这时y 0.5不是更合适而是变成了另一种含义它表示一个模糊标签或者说模型希望输出“介于正类和负类之间”的概率。对于 sigmoid 来说sigmoid(z) 0.5 z 0而我们一开始z -1 a sigmoid(-1) ≈ 0.269如果设置y 1训练会推动z持续变大让a尽量接近1。如果设置y 0.5训练则会推动z从-1往0靠近让a接近0.5。所以两者不是谁更“准确”而是任务目标不同y 1希望这个样本属于正类 y 0希望这个样本属于负类 y 0.5希望模型输出一个中间概率这也引出一个很重要的概念标签不是随便取的标签定义了模型到底在学什么。当前代码只有一个样本xnp.array([2.0,3.0])y1.0因此它学到的不是通用规律而是让这个样本的输出更接近1。如果要让模型真正学习分类边界就需要多个样本x [2, 3], y 1 x [1, 1], y 0 x [3, 4], y 1 x [0, 2], y 0 ...这样训练出来的w和b才更像是在学习一条分类规则而不是只记住一个点。十、今日总结今天最重要的不是记住某个具体数字而是把训练流程串起来1. 前向传播 z w1*x1 w2*x2 b a sigmoid(z) 2. 计算损失 L (a - y)^2 3. 反向传播 dL/dw dL/da * da/dz * dz/dw 4. 参数更新 param param - lr * gradient可以把这一天的核心压缩成一句话神经网络训练就是先前向算预测再反向算每个参数对损失的影响最后沿着让损失下降的方向更新参数。下一步可以继续做两件事用 NumPy 跑 20 轮训练观察 loss 是否下降、输出 a 是否上升。把平方损失换成二分类更常用的交叉熵损失继续推导梯度。课后自测为什么没有激活函数时多层神经网络仍然只是线性模型为什么dL/dw1可以拆成dL/da * da/dz * dz/dw1当梯度是负数时为什么参数更新后会变大在本例中为什么w2从-1变成-0.9136也叫“增大”