【深度学习入门 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再经过 sigmoid:
a = sigmoid(-1) = 1 / (1 + exp(1)) ≈ 0.269如果这是一个二分类模型,并且阈值设置为0.5:
a >= 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/da
L = (a - y)^2对a求导:
dL/da = 2(a - y)代入:
dL/da = 2(0.269 - 1) = -1.462这个负号很有意义:它表示如果a增大,损失会下降。
2. 计算 da/dz
sigmoid 的导数是:
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. 合成梯度
对w1:
dL/dw1 = -1.462 * 0.197 * 2 ≈ -0.576对w2:
dL/dw2 = -1.462 * 0.197 * 3 ≈ -0.864对b:
dL/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更新w1:
w1_new = 0.5 - 0.1 * (-0.576) = 0.5576更新w2:
w2_new = -1 - 0.1 * (-0.864) = -0.9136更新b:
b_new = 1 - 0.1 * (-0.288) = 1.0288注意:梯度为负数时,参数会变大。
这是因为:
参数 = 参数 - 学习率 * 负梯度也就是:
参数 = 参数 + 一个正数更新后模型有没有变好?
用新参数重新计算z:
z_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 x=np.array([2.0,3.0])y=1.0w=np.array([0.5,-1.0])b=1.0lr=0.1defsigmoid(z):return1/(1+np.exp(-z))forstepinrange(20):# forwardz=np.dot(w,x)+b a=sigmoid(z)loss=(a-y)**2# backwarddL_da=2*(a-y)da_dz=a*(1-a)dL_dw=dL_da*da_dz*x dL_db=dL_da*da_dz# updatew=w-lr*dL_dw b=b-lr*dL_dbprint(f"step={step:02d}, "f"loss={loss:.6f}, "f"a={a:.6f}, "f"w={w}, "f"b={b:.6f}")其中:
z=np.dot(w,x)+b对应手算公式:
z = w1*x1 + w2*x2 + b而:
dL_dw=dL_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:希望模型输出一个中间概率这也引出一个很重要的概念:
标签不是随便取的,标签定义了模型到底在学什么。
当前代码只有一个样本:
x=np.array([2.0,3.0])y=1.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也叫“增大”?