掘金 人工智能 07月01日 11:29
从0开始手撸神经网络
index_new5.html
../../../zaker_core/zaker_tpl_static/wap/tpl_guoji1.html

 

本文是“神经网络原理与实现”系列的第四篇,也是最后一篇。通过从零开始编写代码,实现一个能够识别手写数字的神经网络。文章详细介绍了神经网络的构建,包括Network类的定义、激活函数(sigmoid)、前向传播(feedforward)、小批量随机梯度下降(SGD)以及反向传播(backprop)算法的实现。读者可以通过实际操作,加深对神经网络的理解,并提升编码能力。文章附带源码和相关知识点,方便读者学习和实践。

🧠 神经网络的构建:文章首先定义了`Network`类,该类用于表示神经网络。在初始化过程中,根据给定的`sizes`参数(表示每一层神经元的数量)创建了偏置(biases)和权重(weights)。偏置和权重通过`np.random.randn`函数随机初始化,为后续的随机梯度下降算法提供起始点。

⚡️ 前向传播与激活函数:文章介绍了`feedforward`方法,该方法用于计算给定输入的输出。它通过对每一层应用公式a′=σ(wa+b)来实现前向传播。其中,`sigmoid`函数作为激活函数,将加权输入转换为0到1之间的值,引入非线性,使神经网络能够学习复杂的模式。

📉 随机梯度下降(SGD):文章详细解释了`SGD`方法,该方法实现了小批量随机梯度下降。它通过将训练数据划分为小批量,并对每个小批量执行梯度下降步骤来更新网络的权重和偏置。`SGD`方法包括打乱数据、计算梯度、更新权重和偏置等步骤,是训练神经网络的关键。

⏪ 反向传播算法:文章深入介绍了`backprop`方法,该方法实现了反向传播算法。反向传播用于计算损失函数对每层权重和偏置的梯度。它首先执行前向传播,计算每一层的加权输入和激活值。然后从输出层开始,利用损失函数的导数和sigmoid激活函数的导数,逐层计算梯度,并使用梯度更新权重和偏置。

家人们,我准备开启一个全新的系列,来聊聊——神经网络的原理与实现。

也许你已经听过“神经网络”这个词无数次,它是深度学习的基石,是 ChatGPT、图像识别、自动驾驶背后的关键技术。但你是否真正理解过,神经网络到底是怎么“看懂”图像、听懂语言,甚至学会写代码的?这些“看似智能”的行为,背后到底发生了什么?

这个系列将从最基础的多层感知器(MLP)出发,一步步揭开神经网络的神秘面纱,带你理解:
神经网络到底是什么?它是如何“模仿人脑”的?
什么是梯度下降?为什么梯度下降能优化模型?
什么是反向传播?
如何用代码手写一个简单的神经网络,从0开始训练识别手写数字?

这个系列会兼顾原理解释工程实现,面向所有对 AI 感兴趣的朋友,不管你是小白入门,还是进阶学习,相信都能有所收获。

此外,所有相关源码示例、流程图、模型配置与知识库构建技巧,我也将持续更新在Github:LLMHub,欢迎关注收藏!

本篇文章是该系列的第四篇,也是最后一篇。在了解了什么是神经网络并且知道了神经网络的底层原理之后,我们可以通过实际动手来更加深入的理解神经网络,同时提升自己的编码能力。

下面我们就从0开始编写一个识别手写数字的代码,使用随机梯度下降方法和MNIST训练数据,我们需要做的第一件事是获取 MNIST 数据。可以通过克隆我的代码仓库获取数据git@github.com:zhangting-hit/LLMHub.git,其中同时包含源码和其他知识点。

接下来我们开始编写代码。

首先我们创建一个Network类,也就是神经网络:

class Network(object):    def __init__(self, sizes):                self.num_layers = len(sizes)        self.sizes = sizes        self.biases = [np.random.randn(y, 1) for y in sizes[1:]]        self.weights = [np.random.randn(y, x)                        for x, y in zip(sizes[:-1], sizes[1:])]

在这段代码中,列表 sizes 表示每一层中神经元的数量。例如,如果我们希望创建一个 Network 对象,其第一层有 2 个神经元,第二层有 3 个神经元,最后一层有 1 个神经元,我们可以这样写代码:

net = Network([2, 3, 1])

Network 对象中,偏置(biases)和权重(weights)都是通过 Numpy 的 np.random.randn 函数随机初始化的,该函数会生成均值为 0、标准差为 1 的高斯分布(正态分布)。这种随机初始化为后续的随机梯度下降(stochastic gradient descent, SGD)算法提供了一个起始点。

请注意,初始化代码假设网络的第一层是输入层,因此不会为这一层设置任何偏置项 —— 因为偏置项只用于计算后续层的输出

还需要注意的是,偏置和权重被存储为Numpy 矩阵组成的列表。例如,net.weights[1] 是一个 Numpy 矩阵,存储了连接第二层和第三层神经元之间的权重(注意:不是第一层和第二层,因为 Python 的列表索引是从 0 开始的)。

我们知道神经网络计算的核心公式是 a′=σ(wa+b),其中用到了激活函数,接下来我们定义激活函数 sigmoid 函数:

def sigmoid(z):    return 1.0 / (1.0 + np.exp(-z))

注意:当输入 z 是一个向量或 Numpy 数组时,Numpy 会自动逐元素地(elementwise)应用 sigmoid 函数,也就是说是向量化(vectorized)处理的。

接下来,我们给 Network 类添加一个 feedforward 方法,该方法在给定输入 a 的情况下,返回对应的输出。

def feedforward(self, a):    """Return the output of the network if "a" is input."""    for b, w in zip(self.biases, self.weights):        a = sigmoid(np.dot(w, a) + b)    return a

这个方法所做的就是:对每一层应用公式 a′=σ(wa+b)。

当然,我们希望 Network 对象最重要的能力是能够学习。为此,我们为其添加一个 SGD 方法,该方法实现了小批量随机梯度下降(mini-batch stochastic gradient descent)。以下是代码,某些部分可能有些难懂,但我会在代码后逐段解释:

def SGD(self, training_data, epochs, mini_batch_size, eta, test_data=None):    """    使用小批量随机梯度下降训练神经网络。    training_data 是一个 (x, y) 元组的列表,代表输入和期望输出。    其他参数的含义如下:    - epochs:训练轮数;    - mini_batch_size:每个小批量的大小;    - eta:学习率;    - test_data(可选):若提供,每轮训练后用测试集评估模型表现。    """    if test_data:        n_test = len(test_data)    n = len(training_data)    for j in xrange(epochs):        random.shuffle(training_data)        mini_batches = [            training_data[k:k+mini_batch_size]            for k in xrange(0, n, mini_batch_size)]        for mini_batch in mini_batches:            self.update_mini_batch(mini_batch, eta)        if test_data:            print "Epoch {0}: {1} / {2}".format(                j, self.evaluate(test_data), n_test)        else:            print "Epoch {0} complete".format(j)

工作流程如下:

    每个 epoch 开始前,对 training_data 进行随机打乱;然后将其划分为多个小批量(mini_batch);对每个小批量,调用 self.update_mini_batch(mini_batch, eta)执行一次基于该批数据的梯度下降步骤;如果提供了测试集 test_data,就输出当前模型在测试集上的准确度。

其中update_mini_batch 方法实现如下:

def update_mini_batch(self, mini_batch, eta):    """    使用反向传播算法对单个小批量更新网络的权重和偏置。    mini_batch 是一个 (x, y) 元组的列表;    eta 是学习率。    """    nabla_b = [np.zeros(b.shape) for b in self.biases]    nabla_w = [np.zeros(w.shape) for w in self.weights]    for x, y in mini_batch:        delta_nabla_b, delta_nabla_w = self.backprop(x, y)        nabla_b = [nb + dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]        nabla_w = [nw + dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]    self.weights = [w - (eta / len(mini_batch)) * nw                     for w, nw in zip(self.weights, nabla_w)]    self.biases = [b - (eta / len(mini_batch)) * nb                    for b, nb in zip(self.biases, nabla_b)]

上面的关键代码是:

delta_nabla_b, delta_nabla_w = self.backprop(x, y)

这行代码调用了一个名为 backprop 的方法,这个方法实现了反向传播算法

所以 update_mini_batch 方法的逻辑很简单:

def backprop(self, x, y):    """返回一个元组 (nabla_b, nabla_w),表示损失函数 C_x 对每层偏置和权重的梯度"""    # 初始化 nabla_b 和 nabla_w,结构与 biases 和 weights 相同,元素为 0,用于累计梯度    nabla_b = [np.zeros(b.shape) for b in self.biases]    nabla_w = [np.zeros(w.shape) for w in self.weights]    # 前向传播开始,设置初始输入 activation 为 x    activation = x    # 存储所有层的激活值(包括输入层)    activations = [x]    # 存储所有层的加权输入 z(即 z = w·a + b)    zs = []    # 遍历每层的权重 w 和偏置 b,执行前向传播    for b, w in zip(self.biases, self.weights):        z = np.dot(w, activation) + b       # 计算加权输入 z        zs.append(z)                         # 保存 z 值供后向传播使用        activation = sigmoid(z)              # 计算当前层的激活值        activations.append(activation)       # 保存激活值供后向传播使用    # 反向传播开始 —— 先计算输出层的误差 delta    delta = self.cost_derivative(activations[-1], y) * sigmoid_prime(zs[-1])        # 输出层偏置的梯度就是 delta    nabla_b[-1] = delta    # 输出层权重的梯度是 delta 与上一层激活值的转置的点积    nabla_w[-1] = np.dot(delta, activations[-2].transpose())    # 从倒数第二层开始,逐层向前传播误差    for l in xrange(2, self.num_layers):        z = zs[-l]                            # 当前层的 z        sp = sigmoid_prime(z)                # 当前层 z 的 sigmoid 导数        delta = np.dot(self.weights[-l+1].transpose(), delta) * sp  # 传播误差        nabla_b[-l] = delta                   # 当前层偏置的梯度        nabla_w[-l] = np.dot(delta, activations[-l-1].transpose())  # 当前层权重的梯度    # 返回每层偏置和权重的梯度    return (nabla_b, nabla_w)def cost_derivative(self, output_activations, y):    """返回损失函数对输出层激活值的偏导数,即 ∂C / ∂a"""    return (output_activations - y)  # 平方损失函数的导数:a - ydef sigmoid_prime(z):    """sigmoid 函数的导数,用于反向传播时计算梯度"""    return sigmoid(z) * (1 - sigmoid(z))  # 利用公式:σ'(z) = σ(z) * (1 - σ(z))

backprop 方法实现了神经网络的反向传播算法,用于高效计算代价函数对每层权重和偏置的梯度。它首先执行前向传播,将每层的加权输入(z 值)和激活值(a 值)保存下来。然后从输出层开始,利用损失函数的导数(通过 cost_derivative 计算)和 sigmoid 激活函数的导数,计算输出层的误差,并逐层向前传播这个误差。在每一层,通过误差和前一层的激活值计算出当前层权重和偏置的梯度。最终返回的 nabla_bnabla_w 分别是各层偏置和权重的梯度,它们将被用于梯度下降步骤中更新模型参数。整个过程基于链式法则,确保每个参数的梯度都能高效准确地计算出来,是神经网络学习的关键步骤。

如果对上述反向传播的过程或者公式有疑问,可以去看看本系列的什么是反向传播这篇文章。

以下是全部代码:

import randomimport numpy as npclass Network(object):    def __init__(self, sizes):        self.num_layers = len(sizes)        self.sizes = sizes        self.biases = [np.random.randn(y, 1) for y in sizes[1:]]        self.weights = [np.random.randn(y, x)                        for x, y in zip(sizes[:-1], sizes[1:])]    def feedforward(self, a):        for b, w in zip(self.biases, self.weights):            a = sigmoid(np.dot(w, a)+b)        return a    def SGD(self, training_data, epochs, mini_batch_size, eta,            test_data=None):        if test_data: n_test = len(test_data)        n = len(training_data)        for j in xrange(epochs):            random.shuffle(training_data)            mini_batches = [                training_data[k:k+mini_batch_size]                for k in xrange(0, n, mini_batch_size)]            for mini_batch in mini_batches:                self.update_mini_batch(mini_batch, eta)            if test_data:                print "Epoch {0}: {1} / {2}".format(                    j, self.evaluate(test_data), n_test)            else:                print "Epoch {0} complete".format(j)    def update_mini_batch(self, mini_batch, eta):        """Update the network's weights and biases by applying        gradient descent using backpropagation to a single mini batch.        The ``mini_batch`` is a list of tuples ``(x, y)``, and ``eta``        is the learning rate."""        nabla_b = [np.zeros(b.shape) for b in self.biases]        nabla_w = [np.zeros(w.shape) for w in self.weights]        for x, y in mini_batch:            delta_nabla_b, delta_nabla_w = self.backprop(x, y)            nabla_b = [nb+dnb for nb, dnb in zip(nabla_b, delta_nabla_b)]            nabla_w = [nw+dnw for nw, dnw in zip(nabla_w, delta_nabla_w)]        self.weights = [w-(eta/len(mini_batch))*nw                        for w, nw in zip(self.weights, nabla_w)]        self.biases = [b-(eta/len(mini_batch))*nb                       for b, nb in zip(self.biases, nabla_b)]    def backprop(self, x, y):        """返回一个元组 (nabla_b, nabla_w),表示损失函数 C_x 对每层偏置和权重的梯度"""            # 初始化 nabla_b 和 nabla_w,结构与 biases 和 weights 相同,元素为 0,用于累计梯度        nabla_b = [np.zeros(b.shape) for b in self.biases]        nabla_w = [np.zeros(w.shape) for w in self.weights]            # 前向传播开始,设置初始输入 activation 为 x        activation = x            # 存储所有层的激活值(包括输入层)        activations = [x]            # 存储所有层的加权输入 z(即 z = w·a + b)        zs = []            # 遍历每层的权重 w 和偏置 b,执行前向传播        for b, w in zip(self.biases, self.weights):            z = np.dot(w, activation) + b       # 计算加权输入 z            zs.append(z)                         # 保存 z 值供后向传播使用            activation = sigmoid(z)              # 计算当前层的激活值            activations.append(activation)       # 保存激活值供后向传播使用            # 反向传播开始 —— 先计算输出层的误差 delta        delta = self.cost_derivative(activations[-1], y) * sigmoid_prime(zs[-1])                # 输出层偏置的梯度就是 delta        nabla_b[-1] = delta            # 输出层权重的梯度是 delta 与上一层激活值的转置的点积        nabla_w[-1] = np.dot(delta, activations[-2].transpose())            # 从倒数第二层开始,逐层向前传播误差        for l in xrange(2, self.num_layers):            z = zs[-l]                            # 当前层的 z            sp = sigmoid_prime(z)                # 当前层 z 的 sigmoid 导数            delta = np.dot(self.weights[-l+1].transpose(), delta) * sp  # 传播误差            nabla_b[-l] = delta                   # 当前层偏置的梯度            nabla_w[-l] = np.dot(delta, activations[-l-1].transpose())  # 当前层权重的梯度            # 返回每层偏置和权重的梯度        return (nabla_b, nabla_w)    def evaluate(self, test_data):        """Return the number of test inputs for which the neural        network outputs the correct result. Note that the neural        network's output is assumed to be the index of whichever        neuron in the final layer has the highest activation."""        test_results = [(np.argmax(self.feedforward(x)), y)                        for (x, y) in test_data]        return sum(int(x == y) for (x, y) in test_results)    def cost_derivative(self, output_activations, y):        """返回损失函数对输出层激活值的偏导数,即 ∂C / ∂a"""        return (output_activations - y)  # 平方损失函数的导数:a - ydef sigmoid(z):    """The sigmoid function."""    return 1.0/(1.0+np.exp(-z))def sigmoid_prime(z):    """sigmoid 函数的导数,用于反向传播时计算梯度"""    return sigmoid(z) * (1 - sigmoid(z))  # 利用公式:σ'(z) = σ(z) * (1 - σ(z))

核心代码就是上面这些,下面就可以开始训练并进行测试了,代码如下:

import mnist_loaderimport network# 加载 MNIST 数据集(784维输入,10维输出one-hot)training_data, validation_data, test_data = mnist_loader.load_data_wrapper()# 创建一个 3 层神经网络:输入层784,隐藏层30,输出层10net = network.Network([784, 30, 10])# 使用小批量随机梯度下降训练:30个epoch,每批10张图,学习率为3.0net.SGD(training_data, 30, 10, 3.0, test_data=test_data)# ----------------------------# 测试部分:计算在 test_data 上的准确率# ----------------------------def test_accuracy(net, test_data):    """评估网络在测试集上的准确率"""    test_results = [(int(net.feedforward(x).argmax()), y) for (x, y) in test_data]    correct = sum(int(pred == label) for (pred, label) in test_results)    total = len(test_data)    print(f"Test Accuracy: {correct} / {total} ({100.0 * correct / total:.2f}%)")# 调用测试函数test_accuracy(net, test_data)

可以看到上图中的运行结果,测试数据集上的准确率达到了将近95%,说明神经网络在手写数字识别任务中具有很好的效果。

以上就是我们从0开始编写手写数字识别神经网络的所有内容了,到这里我们的神经网络系列也到此结束啦!我们从以MLP为例介绍什么是神经网络开始,依次介绍了什么是梯度下降,什么又是反向传播,最后我们经过实战更加深入的理解了神经网络的原理和实现。希望本系列能使大家有所收获,如果大家对什么知识点感兴趣,可以给我留言!

码字不易,如果对你有一点帮助,点个喜欢吧!

关于深度学习和大模型相关的知识和前沿技术更新,请关注公众号算法coting!

以上内容参考了

Neural Networks and Deep Learning

非常感谢,如有侵权请联系删除!

Fish AI Reader

Fish AI Reader

AI辅助创作,多种专业模板,深度分析,高质量内容生成。从观点提取到深度思考,FishAI为您提供全方位的创作支持。新版本引入自定义参数,让您的创作更加个性化和精准。

FishAI

FishAI

鱼阅,AI 时代的下一个智能信息助手,助你摆脱信息焦虑

联系邮箱 441953276@qq.com

相关标签

神经网络 手写数字识别 反向传播 梯度下降
相关文章