12行实现一个简易神经网络

准备工作

现实现一个具有三层的最简单的神经网络,**函数采用sigmod函数。整体结构如下所示:
12行实现一个简易神经网络

  • x是输入的样本值。设定为5*3的矩阵,即代表5个样本值,每个样本有3个特征值。
  • y是标签,为5*1的矩阵代表每一个样本的标签。且取值范围为0或1。即完成一个简单的分类问题。
  • w0、w1为中间的权重参数。L0、L1、L2表示每一层的计算结果。
  • L2即是最终与标签比对的结果

首先是数据的初始化:

x = np.array(
    [[0, 0, 1],
    [0, 1, 1],
    [1, 0, 1],
    [1, 1, 1],
    [0, 0, 1]]
)
y = np.array([
    [0],
    [1],
    [1],
    [0],
    [0],
])
np.random.seed(1)
# w0 w1取值范围为 -1 到 1
w0 = 2 * np.random.random([3, 4]) - 1
w1 = 2 * np.random.random([4, 1]) - 1

因为神经网络可以大概分为两部分:前向传播与反向传播,且主要难点在反向传播。故要对反向传播进行公式的推导。

反向传播的推导

sigmod函数的定义

首先定义好sigmod函数:

def sigmod(x, deriv = False):
    if(deriv == True):
        return x * (1 - x)
    return 1 / (1 + np.exp(-x))

该函数的deriv参数是为了方便进行反向传播的运算。我们知道sigmod函数具有一个很有意思的特征:
f(z)=sigmod(z)=11+ezf(z) = sigmod(z) = \frac{1}{1+e^{-z}}
fz=f(z)(1f(z))\frac{\partial f}{\partial z}=f(z)(1-f(z))
故当进行反向传播时需要返回sigmod函数的导数,所以定义deriv参数方便返回值的调用。

损失函数

使用均方函数定义误差,如下:
Loss=12(l2y)2Loss = \frac{1}{2}(l_2-y)^2
分别对w1w_1w0w_0求导:
dLossdw1=(l2y)dl2dw1=(l2y)l2(1l2)l1\frac{dLoss}{dw_1} = (l_2-y)\frac{dl_2}{dw_1}=(l_2-y)l_2(1-l_2)l_1
dLossdw0=(l2y)dl2dw0=(l2y)dl2dl1dl1dw0=(l2y)l2(1l2)w1l0l1(1l0)\frac{dLoss}{dw_0} = (l_2-y)\frac{dl_2}{dw_0}=(l_2-y)\frac{dl_2}{dl_1}\frac{dl_1}{dw_0}=(l_2-y)l_2(1-l_2)w_1l_0l_1(1-l_0)
其实就是简单的复合函数求导,难度较低。
在求得梯度之后就可以进行梯度下降算法更新w1w_1w0w_0参数,进行迭代。

代码编写

设置迭代10万次,每一万次输出一下loss值

for j in range(100000):
    l0 = x
    l1 = sigmod(np.dot(l0, w0))
    l2 = sigmod(np.dot(l1, w1))
    l2_error = (y - l2) # 均方误差的导数值 
    l2_delta = l2_error * sigmod(l2, True) # 导数相乘。对应公式
    l1_error = l2_delta.dot(w1.T) 
    l1_delta = l1_error * sigmod(l1, True)
    w1 += l1.T.dot(l2_delta) # 每次更新梯度值
    w0 += l0.T.dot(l1_delta)
    if j % 10000 == 0:
        print('Error:' + str(np.mean(np.abs(l2_error))))

总结

整体其实还是蛮抽象的,关键在于梯度的计算。
代码里面的每一个变量都能在公式里找到位置,因为最终更新公式太长了所以把一些变量分开写了。