bp神经网络实现

380 阅读4分钟

bp神经网络实现

1.什么是梯度

以f(x,y) 举例

方向导数:对于多元函数f(x,y)f(x,y)的来说,若其在P0(x0,y0)P_0(x_0,y_0)可微分 ,f沿着单位向量u=u1i+u2ju = u_1 i + u_2 j的方向导数是

(dfds)u,p0=lims0f(x0+su1,y0+su2)f(x0,y0)s(\frac{df}{ds}){_u},{_{p_0}} = \lim\limits_{s \to 0}\frac{f(x_0 +su_1,y_0+ su_2) - f(x_0,y_0)}{s}(极限存在时)

也可表示成(Duf)p0(D_uf)_{p_0}, 表示多元函数f(x,y)f(x,y) 在经过P0P_0点沿向量uu方向的变化率

梯度:对于点P0(x0,y0)P_0(x_0,y_0) 来说 f=fxi+fyj\nabla f = \frac{\partial f}{\partial x} i + \frac{\partial f}{\partial y}j (\nabla 为梯度算子),单位向量u沿着梯度指向的方向时,沿uu方向的求得的方向导数有最大值,沿梯度方向函数值增长最快

为什么沿梯度方向函数增长最快?这里引入证明

f(x,y)f(x,y) 在点(x0,y0)(x_0,y_0)可微分,则函数f(x,y)f(x,y) 沿着 向量u=(tcosα,tsinα)u = (tcos\alpha,tsin\alpha)的方向导数为

limt0+f(x0+tcosα,y+tsinα)f(x0,y0)t=limt0+[f(x0+tcosα,y0+tsinα)f(x0,y0+tsinα)t+(x0,y0+tsinα)f(x0,y0)t]\lim\limits_{t \to 0+} \frac{f(x_0 +tcos\alpha,y+tsin\alpha) - f(x_0,y_0)}{t} = \lim\limits_{t \to 0+}[\frac{f(x_0 +tcos\alpha,y_0+tsin\alpha) - f(x_0 ,y_0+tsin\alpha)}{t} + \frac{(x_0,y_0+tsin\alpha) - f(x_0,y_0)}{t}]

使用拉格朗日中值定理ξx(x0,x0+tcosα),ξy(y0,y0+tsinα) \exist \xi _x \in (x_0,x_0 + tcos\alpha) , \xi_y \in (y_0,y_0 + tsin\alpha) 使得 limt0+[f(x0+tcosα,y0+tsinα)f(x0,y0+tsinα)t+(x0,y0+tsinα)f(x0,y0)t]\lim\limits_{t \to 0+}[\frac{f(x_0 +tcos\alpha,y_0+tsin\alpha) - f(x_0 ,y_0+tsin\alpha)}{t} + \frac{(x_0,y_0+tsin\alpha) - f(x_0,y_0)}{t}]= limt0+[fx(ξx,y0+tsinα)tcosαt+fy(x0,ξy)tsinαt]\lim\limits_{t \to 0+} [\frac{f_x(\xi_x,y_0 + tsin\alpha) tcos\alpha}{t} +\frac{f_y(x_0,\xi_y)tsin\alpha}{t}] =fx(x0,y0)cosα+fy(x0,y0)sinα f_x(x_0,y_0)cos\alpha + f_y(x_0,y_0)sin\alpha

e=(cosα,sinα)e = (cos\alpha,sin\alpha)是u 的单位向量,Duf=(fx,fy)(cosα,sinα)=feD_uf = \vec{(f_x,f_y)}\cdot \vec{(cos \alpha,sin\alpha)} = \nabla f \cdot e

当 e 与梯度同方向的时候方向导数增加最快,与梯度反方向则其方向导数减少最快

2.何为梯度下降算法

Wnew=Woldαf(Wold)αW_{new} = W_{old} - α \cdot ∇f(W_{old}) , \alpha 为学习率,沿梯度方向时函数增长最快,反之与梯度方向相反函数减少最快所以这里是负号,我们用的函数是误差函数,每次都希望它以最快的速度减少,更新方向与梯度相反所以名字叫梯度下降

其实这里我们可以用一个简单的函数举个例子L=(x1)2,dLdx=2(x1) L = (x-1)^2,\frac{dL}{dx} = 2(x -1),我们想要通过误差函数让x到最小误差点的话,可以如下建模

假设我们选择的初始点在 (5,16),位于函数L上,学习率α=0.2\alpha = 0.2

第一次计算Δx=0.22(51)=1.6\Delta x = -0.2*2*(5-1) = -1.6,更新后x=3.4x = 3.4

第二次计算Δx=0.22(3.41)=0.96\Delta x = -0.2*2*(3.4 -1) = -0.96,更新后x=2.44x = 2.44

第三次计算Δx=0.22(2.441)=0.576\Delta x = -0.2*2(2.44 -1) = -0.576,更新后x=1.864x = 1.864

通过多轮次的迭代,我们可以通过误差函数让x逐渐靠近 x= 1(就是误差最小点,也是目标点)

另一方面,初始态x 的随机初始化和学习率的调整也十分重要

3.为什么要非线性函数作为激活函数

这个时候我们就要引入一个新的概念激活函数,所有的东西并非单一的线性拟合就可以完成,而是需要非线性的去拟合。比如σ\sigma函数。为什么非线性不能解决所有问题呢,比如同或门(XNOR)需要非线性方法解决,下面是同或门的返回值

(0,0)(0,1)(1,0)(1,1)
truefalsefalsetrue

如果用(x,y) 表示两个输入,用w1,w2w_1,w_2表示两个输入节点的权重Output=w1x+w2yb Output = w_1x + w_2y - b

对于比较简单的与门和非门 通过调节w1,w2bw_1,w_2,b 的值,我们可以通过Output>0Output>0 是否成立来拟合,在xOyxOy平面上有直线w1x+w2yb=0w_1x + w_2y - b = 0作为返回true和返回false的分界线,以下是python简单实现的一个与门

def AND(x,y):
    input = numpy.array([x,y])
    w = numpy.array([0.5,0,5])
    b = -0.6
    output = numpu.sum(w*input) - b
    if(output>0):
        return 1
    else:
        retunr 0  

对于同或门,在xOyxOy 平面上,难以找到线性函数将其划分两个输出为True 和False的两个区域,这个时候我们就需要非线性的函数参与让分界线变为非线性的,从而使得它符合这种刁钻的局面,使得分类变得更准确

另一方面,人的神经元直之间也有类似激活函数的物理结构。比如膜电位存在 “阈值” 超过某个值之后神经元的激活不可抑制的,比如σ函数:σ(x)=11ex\sigma 函数 :\sigma(x) = \frac{1}{1-e^{-x}} 它的斜率在0附近突然猛烈增加,这就是它的阈值点,这个点存在一个难以抑制增加趋势,膜电位超过阈值电位之后也会难以抑制的逆转,它的图像也类似于σ\sigma 函数(不同电位于神经元被激发的概率的图)

4.不同层之间的连接

基本上包含输入层,隐藏层,输出层

网图侵删

在这里,我们可以用一些比较简单的方法实现,比如定义一个dense类来表示,这是一种比较好的抽象方式

Oj=IiNeti,j+BaisO_j = I_i*Net_{i,j} + Bais

Aj=σ(Oj)A_j=\sigma(O_j) 表示激活后的输出 其求导关系如下 σ(Oj)Oj=σ(Oj)(1σ(Oj))\frac{\partial \sigma(O_j)}{O_j} = \sigma(O_j) (1- \sigma(O_j))

选定误差函数为Lj=(AjTj)2L_j = (A_j - T_j)^2 这里TjT_j是目标输出

f=LjNetij=LjAjAjNetij=LjAjσ(Oj)Netij=LjAjσ(Oj)OjOjNetij\nabla f =\frac{\partial L_j}{\partial Net_{ij}} =\frac{\partial L_j}{\partial A_j} \cdot \frac{\partial A_j}{\partial Net_{ij}} = \frac{\partial L_j}{\partial A_j} \cdot \frac{\sigma(O_j)}{\partial Net_{ij}} = \frac{\partial L_j}{\partial A_j} \cdot \frac{\sigma(O_j)}{\partial O_j}\cdot \frac{\partial O_j}{\partial Net_{ij}}

=2(AjTj)σ(Oj)(1σ(Oj))Ii= 2\cdot (A_j - T_j) \cdot\sigma(O_j) (1- \sigma(O_j)) \cdot I_i

由于系数可由学习率调整,省略系数2

Wnew=Woldαf(Wold)W_{new} = W_{old} - α \cdot ∇f(W_{old})

ΔW=αIT(AT)σ(O)(1σ(O))\Delta W = -\alpha \cdot I^{T} (A - T)\sigma(O) (1- \sigma(O))

这里的转置是由于WW实际上是与原先矩阵大小相同的,同样是(i,j)(i,j)大小的,对于向量 II 取转置才能得到实际需要的矩阵

5 .如何编程实现

这里我们使用pytorch,不使用自动求导进行演示

定义一个Dense类实现链接信息存储

class Dense:
    def __init__(self, input_size, output_size, bias=0.0):
        self.net = pow(output_size, -0.5) * torch.randn(input_size, output_size)
        # 不随机初始化会出现大问题
        self.input = None
        self.output = None
        self.active_output = None
        self.dW = None
        self.error = None
        self.bias = torch.ones((1, output_size)) * bias

    def forward(self, input_data):
        self.input = input_data
        self.output = torch.matmul(self.input, self.net) + self.bias
        self.active_output = torch.sigmoid(self.output)
        return self.active_output

    def cacu_error(self, error):
        # error = output - target
        self.error = error
        prev_error = torch.matmul(self.error, self.net.T)
        return prev_error

    def backward(self, lr):
        s = self.active_output
        self.dW = torch.matmul(self.input.T, self.error * s * (1 - s))
        self.net -= lr * self.dW

接下来封装一下

class NeuralNet:
    def __init__(self):
        self.target_data = None
        self.input_data = None
        self.denses: list[Dense] = []
        self.lr = 0.2

    def add_dense(self, input_size, output_size):
        current_dense = Dense(input_size, output_size)
        self.denses.append(current_dense)

    def set_data(self, input_data, target_data):
        self.input_data = input_data
        self.target_data = target_data

    def set_lr(self, learning_rate):
        self.lr = learning_rate

    def forward(self):
        fwd = self.input_data
        for current_dense in self.denses:
            fwd = current_dense.forward(fwd)

    def query(self, query_data):
        fwd = query_data
        for current_dense in self.denses:
            fwd = current_dense.forward(fwd)
        return fwd

    def backward(self):
        current_error = (self.denses[-1].active_output - self.target_data)
        for idx in range(len(self.denses) - 1, -1, -1):
            current_error = self.denses[idx].cacu_error(current_error)
            self.denses[idx].backward(self.lr)

使用的话,输入是(1,inputSize) 大小的,不是(inputSize,1),不同人可能习惯不同

network = NeuralNet()
network.addDense(784, 200)
network.addDense(200, 10)
​
network.setLr(0.2)
​
for x, y in train_iterator:
    network.setData(x, y)
    network.forward()
    network.backward()