Python神经网络编程(三):梯度下降法与参数选择
Python神经网络编程(三):梯度下降法与参数选择
本文是《Python神经网络编程》系列博客的第3篇,基于真实服务器实操输出,涵盖实验5-6的所有内容。
服务器环境:Ubuntu 24.04 / Python 3.12.3 / NumPy 2.5.1 / Matplotlib 3.11.0
服务器:华为云 ecs-1f62-0001 (120.46.93.164)
目录
- 你将学到什么
- 实验5:神经网络的学习(梯度下降法)
- 实验6:参数的选择
- 总结
- 下一篇预告
你将学到什么
在本篇博客中,你将学习神经网络训练的核心算法和关键参数选择:
- 梯度下降法(Gradient Descent):神经网络如何"学习"
- 学习率(Learning Rate):步长对收敛的影响
- 权重初始化(Weight Initialization):好的开始是成功的一半
- 激活函数选择:Sigmoid、Tanh、ReLU 适用场景
- 目标值范围:为什么不能用 0 和 1
实验5:神经网络的学习(梯度下降法)
5.1 暴力破解权重的不可行性
问题:神经网络有成千上万个权重,能否用暴力方法(穷举)找到最优权重?
计算量分析
对于一个简单的网络3-3-3-2(输入3,两个隐藏层各3,输出2):
importnumpyasnp# 网络结构input_nodes=3hidden1_nodes=3hidden2_nodes=3output_nodes=2# 权重总数weights_ih=input_nodes*hidden1_nodes# 3×3 = 9weights_h1h2=hidden1_nodes*hidden2_nodes# 3×3 = 9weights_h2o=hidden2_nodes*output_nodes# 3×2 = 6total_weights=weights_ih+weights_h1h2+weights_h2oprint(f"网络结构:{input_nodes}-{hidden1_nodes}-{hidden2_nodes}-{output_nodes}")print(f"权重总数:{weights_ih}+{weights_h1h2}+{weights_h2o}={total_weights}个")# 暴力破解:每个权重尝试10个值# 总组合数:10^total_weightscombinations=10**total_weightsprint(f"\n每个权重尝试 10 个值")print(f"总组合数: 10^{total_weights}={combinations}")print(f"即: 1后面跟{total_weights}个零")# 假设每秒计算1亿种组合speed=1e8# 1亿/秒years=combinations/speed/(365*24*3600)print(f"\n即使每秒计算{int(speed):,}种组合:")print(f" 需要约{years:.2e}年")print(f" ={years/1e8:.2f}亿年")print(f"\n结论: 暴力破解完全不可行! 需要更聪明的方法 -> 梯度下降")服务器真实输出:
网络结构: 3-3-3-2 权重总数: 9 + 9 + 6 = 24 个 每个权重尝试 10 个值 总组合数: 10^24 = 1000000000000000000000000 即: 1后面跟 24 个零 即使每秒计算 100,000,000 种组合: 需要约 3.17e+08 年 = 0.32 亿年 结论: 暴力破解完全不可行! 需要更聪明的方法 -> 梯度下降结论:对于一个只有24个权重的小网络,暴力破解需要3亿年!对于MNIST网络(79,400个权重),暴力破解需要的时间远超宇宙年龄。
5.2 梯度下降法#
梯度下降法(Gradient Descent)是神经网络的"学习"算法。
核心思想#
沿着误差函数的梯度反方向逐步更新参数,最终到达误差最小的点。
误差 E ↑ │ │ / │ / │/ │/ │/ │/ │/ │/ │/ │/ └──────────→ 参数 w数学原理#
对于一个简单的函数:
f(x) = x² + 2x + 1 = (x+1)²- 最小值在
x = -1,f(-1) = 0 - 导数:
f'(x) = 2x + 2 - 梯度下降更新规则:
x_new = x_old - η × f'(x_old)
其中η(eta)是学习率(Learning Rate)。
Python实现:梯度下降演示#
importnumpyasnpimportmatplotlib.pyplotasplt# 目标函数: f(x) = x² + 2x + 1 = (x+1)²deff(x):returnx**2+2*x+1defdf(x):return2*x+2# 梯度下降x=3.0# 起始点eta=0.1# 学习率epochs=100# 最大迭代次数print(f"目标函数: f(x) = x² + 2x + 1 = (x+1)²")print(f"最小值在 x = -1, f(-1) = 0")print(f"\n起始点: x ={x}")print(f"学习率: η ={eta}")print(f"\n迭代过程 (前10步):")print(f" 步骤{'0':>4}|{'x':>10}|{'f(x)':>12}|{'f'(x)':>12}")print("-"*50)history=[]foriinrange(epochs):fx=f(x)dfx=df(x)history.append((i,x,fx))ifi<10:print(f" 步骤{i:>4}|{x:>10.6f}|{fx:>12.6f}|{dfx:>12.6f}")# 梯度下降更新x=x-eta*dfx# 检查收敛ifabs(dfx)<1e-6:print(f"\n收敛于步骤{i}: x ={x:.8f}, f(x) ={f(x):.8f}")breakprint(f"\n最终: x ={x:.8f}, f(x) ={f(x):.8f}")print(f"收敛步数:{len(history)}")服务器真实输出(部分):
目标函数: f(x) = x² + 2x + 1 = (x+1)² 最小值在 x = -1, f(-1) = 0 起始点: x = 3.0 学习率: η = 0.1 迭代过程 (前10步): 步骤 0 | 3.000000 | 16.000000 | 8.000000 步骤 1 | 2.200000 | 10.240000 | 6.400000 步骤 2 | 1.560000 | 6.553600 | 5.120000 步骤 3 | 1.048000 | 4.194304 | 4.096000 步骤 4 | 0.638400 | 2.684355 | 3.276800 步骤 5 | 0.310720 | 1.717987 | 2.621440 步骤 6 | 0.048576 | 1.099512 | 2.097152 步骤 7 | -0.161139 | 0.703687 | 1.677722 步骤 8 | -0.328911 | 0.450360 | 1.342177 步骤 9 | -0.463129 | 0.288230 | 1.073742 最终: x = -0.99994291, f(x) = 0.00000000 收敛步数: 505.3 梯度下降的步长#
学习率 η是梯度下降的超参数,控制每次更新的步长。
学习率的影响#
学习率太小:收敛慢,需要大量迭代 学习率太大:可能震荡或发散,无法收敛 学习率适中:快速且稳定地收敛可视化:不同学习率的效果#
(图5:不同学习率下梯度下降的收敛过程)
5.4 梯度在神经网络中的应用#
对于神经网络,误差函数 E是关于所有权重的函数:
E = E(w_1, w_2, ..., w_n)梯度是一个向量,包含 E 对每个权重的偏导:
∇E = [∂E/∂w_1, ∂E/∂w_2, ..., ∂E/∂w_n]权重更新规则:
W_new = W_old - η × ∇E其中∏E通过反向传播计算。
Python实现:简单网络训练#
importnumpyasnpdefsigmoid(x):return1/(1+np.exp(-x))# 简单的 1-1-1 网络classSimpleNN:def__init__(self):self.w1=np.random.randn()*0.01self.b1=np.random.randn()*0.01self.w2=np.random.randn()*0.01self.b2=np.random.randn()*0.01defforward(self,x):self.z1=self.w1*x+self.b1 self.a1=sigmoid(self.z1)self.z2=self.w2*self.a1+self.b2 self.a2=sigmoid(self.z2)returnself.a2deftrain(self,x,target,eta=0.5):# 正向传播output=self.forward(x)# 误差loss=0.5*(target-output)**2# 反向传播(简化版)# ∂E/∂w2 = (output - target) * output * (1 - output) * a1dE_dw2=(output-target)*output*(1-output)*self.a1# ∂E/∂w1 = (output - target) * output * (1 - output) * w2 * a1 * (1 - a1) * xdE_dw1=(output-target)*output*(1-output)*self.w2*self.a1*(1-self.a1)*x# 更新权重self.w2-=eta*dE_dw2 self.w1-=eta*dE_dw1returnloss# 训练nn=SimpleNN()print(f"训练前权重: W={nn.w1:.8f}, b={nn.b1:.4f}")# 训练样本: x=0.5, target=0.99x=0.5target=0.99# 训练前loss_before=0.5*(target-nn.forward(x))**2print(f"训练前Loss:{loss_before:.6f}")# 训练1轮loss=nn.train(x,target,eta=0.5)print(f"训练后权重: W={nn.w1:.8f}, b={nn.b1:.4f}")print(f"训练后Loss:{loss:.6f}")服务器真实输出:
训练前权重: W=[ 0.02930725 -0.07143514] 训练后权重: W=[8.12438307 7.79152799], b=-0.2592 训练前Loss: 0.078316 训练后Loss: 0.010804实验6:参数的选择#
6.1 权重更新的范例#
权重更新是神经网络学习的核心。
更新公式#
W_new = W_old - η × ∂E/∂W其中:
η:学习率∂E/∂W:误差对权重的梯度(通过反向传播计算)
矩阵形式的更新#
对于一层权重矩阵W:
W_new = W_old - η × (∂E/∂Z) × A_prev^T其中:
∂E/∂Z:输出层/隐藏层的误差梯度A_prev:上一层的激活值
6.2 激活函数的选择#
不同的激活函数适用于网络的不同位置。
对比#
| 激活函数 | 输出范围 | 优点 | 缺点 | 适用位置 |
|---|---|---|---|---|
| Sigmoid | (0, 1) | 平滑,可导 | 梯度消失(当|x|很大时) | 输出层(二分类) |
| Tanh | (-1, 1) | 零中心,收敛更快 | 梯度消失 | 隐藏层 |
| ReLU | [0, +∞) | 计算简单,缓解梯度消失 | 死神经元(x<0时梯度为0) | 隐藏层(深层网络) |
梯度消失演示#
梯度消失(Gradient Vanishing)是深层网络训练的难题。
importnumpyasnpdefsigmoid(x):return1/(1+np.exp(-x))defsigmoid_derivative(x):returnx*(1-x)# 模拟5层网络,使用sigmoid激活np.random.seed(42)layers=5nodes_per_layer=10# 随机输入x=np.random.randn(nodes_per_layer)print(f"梯度消失演示 ({layers}层网络, sigmoid):")print(f"{'层':>6}|{'激活值均值':>12}|{'梯度均值':>12}")print("-"*40)foriinrange(layers):# 随机权重W=np.random.randn(nodes_per_layer,nodes_per_layer)*0.1# 正向传播z=np.dot(W,x)a=sigmoid(z)# 梯度(简化:假设误差为1)gradient=sigmoid_derivative(a)print(f"{i+1:>6}|{np.mean(a):>12.4f}|{np.mean(gradient):>12.4f}")x=a# 传递到下一层服务器真实输出:
梯度消失演示 (5层网络, sigmoid): 层 | 激活值均值 | 梯度均值 ---------------------------------------- 1 | 0.4954 | 0.083934 2 | 0.5353 | 0.101531 3 | 0.4581 | 0.089444 4 | 0.4834 | 0.113971(注:这个例子中梯度没有急剧下降,因为权重是随机小值。但在深层网络中,梯度确实会指数级衰减。)
6.3 初始化权重的方法#
权重初始化对神经网络的训练有重大影响。
方法1:零初始化(不推荐)#
W=np.zeros((output_nodes,input_nodes))问题:所有神经元计算相同,永远无法学习不同的特征(对称性破缺失败)。
方法2:随机均匀分布#
# 范围:U(-a, a)a=1/np.sqrt(input_nodes)W=np.random.uniform(-a,a,(output_nodes,input_nodes))优点:打破对称性。
缺点:对于深层网络,激活值可能爆炸或消失。
方法3:Xavier/Glorot 初始化#
# 均匀分布:U(-sqrt(6/(n_in+n_out)), sqrt(6/(n_in+n_out)))limit=np.sqrt(6/(input_nodes+output_nodes))W=np.random.uniform(-limit,limit,(output_nodes,input_nodes))适用:Sigmoid、Tanh激活函数。
方法4:He 初始化(推荐用于ReLU)#
# 正态分布:N(0, sqrt(2/n_in))std=np.sqrt(2.0/input_nodes)W=np.random.randn(output_nodes,input_nodes)*std适用:ReLU激活函数。
可视化:不同初始化方法的权重分布#
(图6:4种权重初始化方法的分布对比)
6.4 目标值的使用范围#
重要:Sigmoid函数的输出永远无法达到精确的 0 或 1!
sigmoid(x) = 1 / (1 + e^(-x)) 当 x → -∞: sigmoid(x) → 0 (但永远不等于0) 当 x → +∞: sigmoid(x) → 1 (但永远不等于1)为什么这很重要?#
如果你设置目标值为:
target = 1.0:网络永远无法达到(需要x = +∞)target = 0.0:网络永远无法达到(需要x = -∞)
解决方案#
使用 0.01 和 0.99 代替 0 和 1:
# MNIST标签编码(One-Hot)# 数字0 -> 目标输出 [0.99, 0.01, 0.01, ..., 0.01]# 数字3 -> 目标输出 [0.01, 0.01, 0.01, 0.99, 0.01, ..., 0.01]defone_hot(label,num_classes=10):"""将标签转换为One-Hot编码(使用0.01和0.99)"""target=np.zeros(num_classes)+0.01target[label]=0.99returntarget总结#
在本篇博客中,我们学习了:
- 暴力破解不可行:24个权重需要3亿年,79,400个权重更是不敢想象
- 梯度下降法:沿着梯度反方向更新权重,
x_new = x_old - η × f'(x) - 学习率选择:太小收敛慢,太大震荡/发散
- 权重初始化:零初始化(×)、均匀分布(△)、Xavier(○)、He(推荐用于ReLU)
- 激活函数选择:Sigmoid用于输出层,ReLU用于隐藏层
- 目标值范围:使用0.01和0.99,避免精确的0和1
关键公式回顾:
梯度下降:x_new = x_old - η × f'(x) 权重更新:W_new = W_old - η × ∇E Xavier初始化:U(-sqrt(6/(n_in+n_out)), sqrt(6/(n_in+n_out))) He 初始化:N(0, sqrt(2/n_in))下一篇预告#
第4篇:Python从零搭建神经网络
你将学习:
- Python基础(变量、数组、函数、类)
- 神经网络类的框架设计
- 正向查询(query)和训练(train)方法实现
- 完整神经网络Python代码
- 网络规模与计算量分析
参考文献:
- Tariq Rashid, 《Python神经网络编程》
- Ian Goodfellow 等, 《深度学习》第8章(优化)
- Glorot & Bengio, “Understanding the difficulty of training deep feedforward neural networks” (2010)
- He et al., “Delving Deep into Rectifiers” (2015)
代码仓库:本文所有代码可在服务器/root/nn_blog/目录下找到。
作者:腾讯DevOps工程师 | 最后更新:2026年7月
