代码
import torch
"""
一次完整的训练:
输入:100个具有1000个特征的数据
经过隐藏层:100个具有100个特征的数据
经过输出层:100个具有10个分类结果值的数据
在得到数据结果后还可以计算损失并进行后向传播(预测结果+实际值->损失->后向传播优化参数)
一次完整的训练OVER
循环这个流程可以完成指定次数的训练,并优化模型参数
"""
batch_n = 100 # 一个批次输入100个数据
hidden_layer = 100 # 经过隐藏层后保留的数据特征个数
input_data = 1000 # 输入时每个数据的特征个数
output_data = 10 # 输出时每个数据的特征个数
# 用randn随机生成输入数据x与真实值y
x = torch.randn(batch_n, input_data) # 输入:100个具有1000个特征的数据
y = torch.randn(batch_n, output_data) # 输出:100个具有10个分类结果值的数据
# 用randn随机初始化权重参数(这里暂时不讨论权重初始化的专门方法,初始生成用随机)
w1 = torch.randn(input_data, hidden_layer) # 经过隐藏层:100个具有100个特征的数据
w2 = torch.randn(hidden_layer, output_data) # 经过输出层:100个具有10个分类结果值的数据
epoch_n = 20 # 训练的轮次数
learning_rate = 1e-6 # 学习率(用于控制梯度更新的快慢)
for epoch in range(epoch_n):
# 隐藏层
h = x.mm(w1) # (100,1000)*(1000*100)->(100,100) 100个具有100个特征的数据
h = h.clamp(min=0) # clamp剪裁,小于0的值赋为0(相当于加上ReLU激活函数)
# 输出层
y_pred = h.mm(w2) # (100,100)*(100,10)=(100,10) 100个具有10个特征的数据
# 计算损失
loss = (y_pred - y).pow(2).sum() # 平方损失
print("Epoch:{}, Loss:{:.4f}".format(epoch, loss))
# 后向传播,优化参数
grad_y_pred = 2 * (y_pred - y)
grad_w2 = h.t().mm(grad_y_pred)
grad_h = grad_y_pred.clone()
grad_h = grad_h.mm(w2.t())
grad_h.clamp(min=0)
grad_w1 = x.t().mm(grad_h)
w1 -= learning_rate*grad_w1
w2 -= learning_rate*grad_w2
后向传播
由于数学公式在代码中不好表示,下面对“后向传播,优化参数”这个过程进行详细补充说明:
要计算梯度,就要后向传播梯度,要后向传播,就要先知道前向是怎么传播的,我们已经有的公式是:
h
=
c
l
a
m
p
(
x
w
1
)
y
p
r
e
d
=
h
w
2
l
o
s
s
=
(
y
p
r
e
d
−
y
)
2
(
这里忽略
s
u
m
(
)
是因为
s
u
m
(
)
是为了把损失张量转标量,并没有其他运算意义
)
h={\rm clamp}(xw_1)\\ y_{\rm pred}=hw_2\\ loss={(y_{\rm pred}-y)}^2 \\{\rm (这里忽略sum() 是因为sum()是为了把损失张量转标量,并没有其他运算意义)}
h=clamp(xw1)ypred=hw2loss=(ypred−y)2(这里忽略sum()是因为sum()是为了把损失张量转标量,并没有其他运算意义)
据此,我们可以得到前向传播图(前向传播就是从输入到输出,把变量以及变换用拓扑图表示出来,这里我们为了计算,把
l
o
s
s
loss
loss也加上):
我们要算
w
1
w_1
w1的梯度就要先算
h
h
h的梯度,要算
h
h
h的梯度就要先算
y
p
r
e
d
y_{\rm pred}
ypred的梯度(这也体现了梯度传播链,并且“后向”),则有以下梯度计算过程(与上述代码相对应):
g
r
a
d
(
y
p
r
e
d
)
=
∂
l
o
s
s
∂
y
p
r
e
d
=
∂
(
(
y
p
r
e
d
−
y
)
2
)
∂
y
p
r
e
d
=
2
(
y
p
r
e
d
−
y
)
grad(y_{\rm pred}) =\frac{\partial loss}{\partial y_{\rm pred}} =\frac{\partial ((y_{\rm pred}-y)^2)}{\partial y_{\rm pred}} =2(y_{\rm pred}-y)
grad(ypred)=∂ypred∂loss=∂ypred∂((ypred−y)2)=2(ypred−y)
g r a d ( h ) = ∂ l o s s ∂ h = ∂ l o s s ∂ y p r e d ∂ y p r e d ∂ h = g r a d ( y p r e d ) ∂ y p r e d ∂ h = 2 ( y p r e d − y ) ∂ h w 2 ∂ h = 2 ( y p r e d − y ) w 2 grad(h) =\frac{\partial loss}{\partial h} =\frac{\partial loss}{\partial y_{\rm pred}}\frac{\partial y_{\rm pred}}{\partial h} =grad(y_{\rm pred})\frac{\partial y_{\rm pred}}{\partial h} =2(y_{\rm pred}-y)\frac{\partial hw_2}{\partial h} =2(y_{\rm pred}-y)w_2 grad(h)=∂h∂loss=∂ypred∂loss∂h∂ypred=grad(ypred)∂h∂ypred=2(ypred−y)∂h∂hw2=2(ypred−y)w2
同理得:
g
r
a
d
(
w
2
)
=
2
(
y
p
r
e
d
−
y
)
h
grad(w_2)=2(y_{\rm pred}-y)h
grad(w2)=2(ypred−y)h
g
r
a
d
(
w
1
)
=
∂
l
o
s
s
∂
w
1
=
∂
l
o
s
s
∂
h
∂
h
∂
w
1
=
g
r
a
d
(
h
)
∂
h
∂
w
1
=
2
(
y
p
r
e
d
−
y
)
w
2
∂
c
l
a
m
p
(
x
w
1
)
∂
w
1
grad(w_1) =\frac{\partial loss}{\partial w_1} =\frac{\partial loss}{\partial h}\frac{\partial h}{\partial w_1} =grad(h)\frac{\partial h}{\partial w_1} =2(y_{\rm pred}-y)w_2\frac{\partial {\rm clamp}(xw_1)}{\partial w_1}
grad(w1)=∂w1∂loss=∂h∂loss∂w1∂h=grad(h)∂w1∂h=2(ypred−y)w2∂w1∂clamp(xw1)
求到这里就出现问题了
∂
c
l
a
m
p
\partial {\rm clamp}
∂clamp要怎么处理?
c
l
a
m
p
\rm clamp
clamp相当于ReLU激活函数,长下面这个样子:
R
e
L
U
(
x
)
=
{
x
x
>
0
0
x
≤
0
{\rm ReLU}(x)=\begin{cases}x&x\gt0\\0&x\le0\end{cases}
ReLU(x)={x0x>0x≤0
发现它的导数很有特征,大于0导数就是1,小于0导数就是0,那么:
g
r
a
d
(
w
1
)
=
g
r
a
d
(
h
)
∂
h
∂
w
1
=
g
r
a
d
(
h
)
∂
c
l
a
m
p
(
x
w
1
)
∂
w
1
=
g
r
a
d
(
h
)
∂
c
l
a
m
p
(
x
w
1
)
∂
x
w
1
∂
x
w
1
∂
w
1
=
g
r
a
d
(
h
)
x
∂
c
l
a
m
p
(
x
w
1
)
∂
x
w
1
=
{
g
r
a
d
(
h
)
x
x
w
1
=
h
>
0
0
x
w
1
=
h
≤
0
grad(w_1) =grad(h)\frac{\partial h}{\partial w_1} =grad(h)\frac{\partial {\rm clamp}(xw_1)}{\partial w_1} =grad(h)\frac{\partial {\rm clamp}(xw_1)}{\partial xw_1}\frac{\partial xw_1}{\partial w_1} =grad(h)x\frac{\partial {\rm clamp}(xw_1)}{\partial xw_1}\\ =\begin{cases}grad(h)x&xw_1=h\gt 0\\0&xw_1=h\le 0\end{cases}
grad(w1)=grad(h)∂w1∂h=grad(h)∂w1∂clamp(xw1)=grad(h)∂xw1∂clamp(xw1)∂w1∂xw1=grad(h)x∂xw1∂clamp(xw1)={grad(h)x0xw1=h>0xw1=h≤0
(注意
x
x
x是常量,更不用说就算
x
x
x是变量求偏导也要看成常量)
那么这个结果其实跟
g
r
a
d
(
h
)
grad(h)
grad(h)先进行clamp()
处理再乘以
x
x
x是一样的,即:
g
r
a
d
(
w
1
)
=
{
g
r
a
d
(
h
)
x
h
>
0
0
h
≤
0
=
(
{
g
r
a
d
(
h
)
h
>
0
0
h
≤
0
)
×
x
=
c
l
a
m
p
(
g
r
a
d
(
h
)
)
×
x
grad(w_1) =\begin{cases}grad(h)x&h\gt 0\\0&h\le 0\end{cases} =(\begin{cases}grad(h)&h\gt 0\\0&h\le 0\end{cases})\times x={\rm clamp}(grad(h))\times x
grad(w1)={grad(h)x0h>0h≤0=({grad(h)0h>0h≤0)×x=clamp(grad(h))×x
也即代码中的这两行grad_h.clamp(min=0)
、grad_w1 = x.t().mm(grad_h)
。
至于 x x x它是常量,而且我们用不到它的梯度,所以就不求。那么至此,所有的梯度都求解完毕。
参数优化
讲完了“后向传播”,该讲“参数优化”了:
对于参数优化是有固定公式的:
θ
j
=
θ
j
−
η
×
∂
L
o
s
s
(
θ
j
)
θ
j
\theta_j=\theta_j-\eta\times\frac{\partial Loss(\theta_j)}{\theta_j}
θj=θj−η×θj∂Loss(θj)即
θ
j
−
=
η
×
g
r
a
d
(
θ
j
)
\theta_j-=\eta\times grad(\theta_j)
θj−=η×grad(θj)。对应代码中的这两行w1 -= learning_rate*grad_w1
、w2 -= learning_rate*grad_w2
。
好了,至此,“一个简易神经网络”就讲解完毕了。
其他
什么?你问博主为什么要写这么多,这不是很简单吗,为什么连乘法交换律这样的小学知识也要写出来?因为博主是个大笨比😢,第一次接触深度学习,每一行代码都像是大怪兽一样让我感到深深的无助,在与怪兽经历长时间搏斗后终于胜利了,这兴奋不亚于我第一次学会hello world
,贴出来给自己加油,鼓励自己即使是个大笨比也要坚持学下去。(不要在评论区喷我,博主很脆弱)