​ 在这学期开始学习深度学习,找学长要了一些书籍,包括《d2l-zh-pytorch》《深度学习入门:基于Python的理论与实现》等,这两本书作为入门书籍十分友好,只需一些python、numpy的基础即可。相比于我上学期硬看周志华的西瓜书,看了一半实在看不进去了,数学推导太多太繁琐,大部分时候不说为什么,更像是上课用的教材,在没有老师的指导时难以自学。以前我还很担心机器学习学不会的话无法开展深度学习,实际上,0基础的人也可以从深度学习开始,注重动手实践从而理解算法本质。

1. 引言

为什么学习神经网络?
  • 神经网络是一种强大的人工智能工具,近年来已经成为推动技术革新的核心力量。神经网络模拟人脑的结构和功能,通过大量的计算单元(神经元)进行数据处理和模式识别。实践已经证明,神经网络在性能上已经远远超过传统的统计方法。
神经网络的未来发展
  • 随着计算资源的提升(尤其是GPU的应用)、大数据的积累和优化算法的发展,神经网络的应用越来越广泛,从学术研究到工业应用都产生了深远的影响。如今,深度学习已经成为人工智能领域的核心技术之一,未来有望在更多领域带来创新性突破。

2. 什么是神经网络?

神经网络的基本结构
  • 神经元(Neuron):输入x,权重w,偏置bcita,激活函数f

    在生物神经网中, 每个神经元与其他神经元相连, 当它“兴奋”时,就会问相连的神经元发送化学物质, 从而改变这些神经元内的电位; 如果神经元的电位超过了一个“阈值”(threshold), 那么它会被激活。

    在这里插入图片描述

  • 参数:权重和偏置(Weight and Bias):神经网络中用于调整输入信息的参数

    对于每个神经元,包含权重w,偏置bcita

    在这里插入图片描述

  • 激活函数(Activation Function):Sigmoid、ReLU、Tanh等

    即上图中的f
    在这里插入图片描述
    理想的激活函数为上图(a)的阶跃函数,但它不够平滑,导数为0,以至于我们无法用它的梯度(导数)来传递参数的更新。
    常用激活函数为Sigmoid、ReLU

Sigmoid:如上图(b)所示

在这里插入图片描述
优点:它的图像最接近阶跃函数,将结果限制在[0, 1]之间,且处处可导。

缺点:1.梯度消失问题:Sigmoid函数的导数在极端值(如很大的正数或负数)时非常小,导致梯度消失问题。这意味着在深层网络中,反向传播时梯度会变得非常小,导致学习速度极慢甚至停滞。2. 计算开销较大:Sigmoid的计算涉及指数运算,计算开销较大,尤其在深层网络中可能会影响性能。

下面的ReLU激活函数可改善这些问题:

ReLU:ReLU函数在输入大于0时,直接输出该值;在输入小于等于0时,输出0
在这里插入图片描述

在这里插入图片描述
优点:1.解决了Sigmod函数的两个缺点。2.稀疏激活:ReLU会将负数输出为0,产生稀疏激活(即大量神经元的输出为0)。这使得网络更具稀疏性和一定的正则化效果。

缺点:1.死神经元问题:当输入为负数时,ReLU的输出为0,这意味着如果在训练过程中某个神经元的输入始终为负数,那么它的梯度也会是0,导致该神经元永远不会被激活,进而无法对网络产生有效的贡献。这个问题被称为"死神经元"问题。

2.输出不被限制:ReLU没有上界限制,输出可以变得非常大,这可能导致梯度爆炸等问题,尤其是在深层网络中。

对比之下,ReLU常用在各种网络中间的激活函数,Sigmod常用于网络的末端,将输入(一般是分类问题)限制在0,1之间

  • 层(Layer):输入层,隐藏层,输出层
    多个神经元并行在一行,即为一层。输入层即为自变量

    此时,把该层的神经元统一计算输出,就可以使用矩阵运算: Y = WX + B

    输入X的维度为 d X 1, 权重参数W的维度为 n X d(即这一层有n个神经元,每个神经元有d个权重w,1个偏执b)输出Y的维度为 n X 1,我们把这个隐层的结构类写出来:

    class Layer:
        def __init__(self, W, b):
            self.W = W
            self.b = b
    

    初始化时给定参数

    在这里插入图片描述

3. 神经网络的工作原理

前向传播(Forward Propagation)

即上面说的矩阵运算: Y = WX + B,这一个层的输入即下一个层的输出。

我们在上面的类里加入forward方法:

    def forward(self, x):
        self.x = x
        return np.dot(x, self.W) + self.b

这样调用Layer.forward(x)即可输出这层的结果

反向传播(Backpropagation)

反向传播,即从最右端的输出向左端传播梯度,告诉每一层它们的参数要变化多少。参考《深度学习入门:基于Python的理论与实现》第五章,讲的非常浅显易懂。

在这里插入图片描述

这里不展开详细的求导推导,总结起来就是,如果我们想让损失函数减少1,对于这一层的某个参数来说要变化多少呢?
对此,根据链式法则传递梯度(导数),我们要获得这个层的下一层的输入(就是本层的输出)要变化多少(梯度),然后再计算本层的参数要变化多少,同时把本层的输入(x)要变化多少传给上一层,然后上一层再做同样的事情。这里注意我们在算梯度时要用到x(比如y=x**2, 导数为2x),因此forward时把x先存起来,后面backward时用。

我们在反向传播时

把backward方法写入刚才的类里:

    def backward(self, dout):
        dx = np.dot(dout, self.W.T)
        self.dW = np.dot(self.x.T, dout)
        self.db = np.sum(dout, axis=0)
        return dx

我们获得的梯度(dw db)当然要用来更新本来的参数,因此要先保存起来(self.dw, self,db,故这些变量也应在init里声明),返回dx

因此完整的层如下:

class Layer:
    def __init__(self, W, b):
        self.W = W
        self.b = b
        self.x = None
        self.dW = None
        self.db = None

    def forward(self, x):
        self.x = x
        return np.dot(x, self.W) + self.b

    def backward(self, dout):
        dx = np.dot(dout, self.W.T)
        self.dW = np.dot(self.x.T, dout)
        self.db = np.sum(dout, axis=0)
        return dx

我在动手实现时,还加了一个这样的函数:

def update(self, rate):
    self.W -= rate * self.dW
    self.b -= rate * self.db

以为能方便更新参数了,但后面的学习发现有许多不同的参数更新策略,不单单是简单的梯度下降。因此无需这样的函数,而是将这个层的参数返回,在类的外面选择合适的策略更新。(果然书上的东西都有它的道理)

4. 激活函数

上面的层只实现了线性变换: Y = WX + B, 神经元的激活与否还要使用第2部分的激活函数

下面给出实现 Sigmod:

class Sigmoid:
    def __init__(self):
        self.out = None

    def forward(self, x):
        self.out = sigmoid(x)
        return self.out

    def backward(self, dout):
        return dout * (1 - self.out) * self.out

ReLU:

class ReLU:
    def __init__(self):
        self.x = None

    def forward(self, x):
        self.x = x
        return x * (x > 0)

    def backward(self, dout):
        return dout * (self.x > 0)

注意:这些激活函数中并没有可学习的参数,仅需保证完成前向传播和梯度反向传播

softmax函数用于在分类任务中使神经网络的输出变为概率分布,一般仅加在网络的最后一层的输出上:

在这里插入图片描述

在指数里面加上 C 是为了防止溢出(分母接近0),并不改变原数值大小。容易看出,softmax函数的输出是0.0到1.0之间的实数。并且,softmax 函数的输出值的总和是1。输出总和为1是softmax函数的一个重要性质。正 因为有了这个性质,我们才可以把softmax函数的输出解释为“概率”。带有Loss的实现如下(因为我们只要结果的时候,也就是预测的时候,取网络最后一层输出的最大值即可,softmax不改变大小关系,但在训练时,我们需要每种预测的概率来构成交叉熵损失,进行学习):
在这里插入图片描述

class SoftmaxWithLoss:
    def __init__(self):
        self.loss = None # 损失
        self.y = None    # softmax的输出
        self.t = None    # 监督数据(one-hot vector)

    def forward(self, x, t):
        self.t = t
        self.y = softmax(x)
        self.loss = cross_entropy_error(self.y, self.t)  #交叉熵损失,也可以是其他
        return self.loss

    def backward(self, dout=1):
        batch_size = self.t.shape[0]
        dx = (self.y - self.t) / batch_size
        return dout * dx

5. 神经网络的训练过程

损失函数(Loss Function)

常见的如下:

  • 均方误差(MSE):

    在这里插入图片描述

  • 交叉熵(Cross-Entropy):

    在这里插入图片描述

    具体细节不展开了,这就是我们要最小化的量。

优化算法(Optimization Algorithms: optimizer)

梯度下降算法及其变种,如批量梯度下降、随机梯度下降(SGD)、AdaGrad等,Adam优化器等。

最简单的随机梯度下降(SGD):
在这里插入图片描述

class SGD:
    def __init__(self, lr=0.001):
        self.lr = lr

    def update(self, params, grads):
        for key in params.keys():
            params[key] -= self.lr * grads[key]
学习率(Learning Rate: lr)

如何选择合适的学习率通常要试验得出,选用进阶的梯度下降方法如动量法、Adam等可减轻因选取的学习率不当引起的训练异常。

运用以上我们实现的小模块,就可以组成一个完整的多层的神经网络(上文的Layer改名为Affine,因为不加激活函数的它不是一个完整的层):

class Network3Layer:
    def __init__(self, input_size, hidden_size1, output_size):
        self.params = {}
        # 参数初始化为高斯分布
        self.params['W1'] = np.sqrt(2 / input_size) * np.random.randn(input_size, hidden_size1)
        self.params['b1'] = np.random.randn(hidden_size1)
        self.params['W2'] = np.sqrt(2 / hidden_size1) * np.random.randn(hidden_size1, output_size)
        self.params['b2'] = np.random.randn(output_size)
        
        self.Layer = {}
        self.Layer['Affine1'] = Affine(self.params['W1'], self.params['b1'])
        self.Layer['ReLU1'] = ReLU()
        self.Layer['Affine2'] = Affine(self.params['W2'], self.params['b2'])
        self.lastLayer = SoftmaxWithLoss()

    def predict(self, x):
        for i in self.Layer.values():
            x = i.forward(x)

        # return self.lastLayer.forward(x)
        return x
        
    ef gradient(self, x, t):
        # forword
        for i in self.Layer.values():
            x = i.forward(x)
        self.lastLayer.forward(x, t)
        # backward
        grad = self.lastLayer.backward(1)
        LayerList = list(self.Layer.values())
        LayerList.reverse()
        for l in LayerList:
            grad = l.backward(grad)

        grads = {}
        grads['W1'] = self.Layer['Affine1'].dW
        grads['b1'] = self.Layer['Affine1'].db
        grads['W2'] = self.Layer['Affine2'].dW
        grads['b2'] = self.Layer['Affine2'].db
        return grads

    def accuracy(self, x, t):
        y = self.predict(x)
        y = np.argmax(y, axis=1)
        if t.ndim != 1 : t = np.argmax(t, axis=1)
        accuracy = np.sum(y == t) / float(x.shape[0])
        return accuracy

    def loss(self, y, t):
        d = 1e-7
        return self.lastLayer.forward(y,t)

假设已有输入输出数据x,y(可以在网上下载MNIST手写数据集),则训练过程如下:

network = Network3Layer(784, 100, 10)
batch_size = 100 # 批数量
loss_ave = 0
optimizer = SGD(0.001)
for j in range(100):
    for i in range(0, len(x), batch_size):
        x_batch = x[i:i+batch_size]
        t_batch = t[i:i + batch_size]
        y_batch = network.predict(x_batch)  # score
        loss_ave = network.loss(y_batch, t_batch)
        grads = network.gradient(x_batch, t_batch)
        optimizer.update(network.params, grads)
    print(j, loss_ave)
    print('accuracy: {}'.format(network.accuracy(x, t)))

到此为止,我们不依赖任何库(除numpy)完成了一个可扩充的神经网络,上面的3层网络(一层隐层)在MNIST手写数据集中准确率可达96.23%.

代码在:https://github.com/PUBGprofessor/DeepLearning.git

参考文献

  • 《深度学习入门:基于Python的理论与实现》
  • 《机器学习—周志华》
Logo

魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。

更多推荐