神经网络的 Hello World - 手写数字识别

Posted on Sat 13 July 2024 in Journal

Abstract 神经网络的 Hello World - 手写数字识别
Authors Walter Fan
 Category    learning note  
Status v1.0
Updated 2024-07-13
License CC-BY-NC-ND 4.0

就象编程语言中的 hello world 那样, 手写数字识别是神经网络入门最常用的例子. 它的训练和测试数据集可通过 https://www.kaggle.com/datasets/oddrationale/mnist-in-csv 下载

它分为两部分:

  1. mnist_train.csv 训练数据集
  2. mnist_test.csv 测试数据集

mnist_train.csv 文件包含 60,000 个训练示例和标签。mnist_test.csv 包含 10,000 个测试示例和标签。每行由 785 个值组成:第一个值是标签(从 0 到 9 的数字),其余 784 个值是像素值(从 0 到 255 的数字)。

神经网络入门

神经网络(Neural Networks)是一种受人脑结构启发的计算模型,它模仿人脑中神经元的连接和交互方式来处理信息。神经网络由一系列节点(或称为“神经元”)组成,这些节点在层(或称为“层级”)中组织。每个节点可以接收输入,对输入进行加权求和,然后通过一个非线性激活函数来生成输出。

神经网络的一些关键概念有:

  1. 神经元(Neuron)
  2. 神经网络的基本单元,每个神经元接收一组输入值,进行加权求和,并可能添加一个偏置项。

  3. 层(Layer)

  4. 神经元的集合,通常分为输入层、隐藏层和输出层。
  5. 输入层接收原始数据。
  6. 隐藏层是中间层,可以有多个,用于提取特征和进行非线性变换。
  7. 输出层产生最终的预测或分类结果。

  8. 权重(Weight)

  9. 每个神经元的输入都有一个权重,这些权重决定了输入信号对神经元输出的影响大小。

  10. 偏置(Bias)

  11. 加在加权求和的输入上的一个值,用于调整神经元激活的阈值。

  12. 激活函数(Activation Function)

  13. 一个数学函数,用于在神经元的加权求和之后引入非线性,使得网络能够学习和执行更复杂的任务。
  14. 常见的激活函数包括 Sigmoid、Tanh、ReLU(Rectified Linear Unit)等。

  15. 前向传播(Forward Propagation)

  16. 数据在神经网络中的流动方向,从输入层通过隐藏层到输出层。

  17. 损失函数(Loss Function)

  18. 衡量模型预测值与实际值差异的函数,用于指导模型训练过程中的优化。

  19. 反向传播(Backpropagation)

  20. 一种训练神经网络的算法,通过计算损失函数关于网络参数的梯度,并利用这些梯度来更新网络的权重和偏置。

  21. 优化器(Optimizer)

  22. 用于在训练过程中更新网络权重的算法,如梯度下降、Adam、RMSprop 等。

  23. 训练(Training)

    • 使用大量数据和损失函数来调整网络权重和偏置,使模型能够准确地预测或分类新数据。
  24. 过拟合(Overfitting)

    • 模型在训练数据上表现很好,但在未见过的数据上表现差,即模型对训练数据过度学习。
  25. 正则化(Regularization)

    • 一种技术,用于减少过拟合,如 L1、L2 正则化或 Dropout。
  26. 学习率(Learning Rate):

    • 学习率是一个标量值,用于乘以损失函数关于模型参数的梯度。这个乘积决定了在每次迭代中更新模型参数的步长。
    • 学习率决定了在梯度下降或其他优化算法中,模型参数更新的幅度。如果步长太大,可能会越过最小值点;如果步长太小,训练过程会非常缓慢。
    • 初始学习率的设定通常是基于经验和实验。一些常见的初始学习率值包括 0.01、0.001、0.0001 等。
    • 在训练过程中,可能需要根据模型的表现调整学习率。一些常见的调整策略包括:

      • 固定学习率:在整个训练过程中保持不变。
      • 学习率衰减:随着训练的进行逐渐减小学习率。
      • 周期性调整:周期性地增加或减少学习率。
      • 自适应学习率:根据模型的损失变化自动调整学习率,如 Adam 优化器。
    • 如果学习率设置得过小,模型可能需要很长时间才能收敛,或者可能陷入局部最小值而无法达到全局最小值。

    • 如果学习率设置得过大,模型可能无法收敛,甚至发散。
    • 不同的优化器对学习率的处理方式不同。例如,SGD(随机梯度下降)使用简单的学习率乘以梯度,而 Adam 优化器则使用自适应学习率,为每个参数计算一个单独的学习率。
    • 在训练的初始阶段,逐渐增加学习率,可以帮助模型更快地收敛。
    • 使用学习率查找算法(如 Learning Rate Range Test)可以帮助确定合适的学习率范围。

以一个基本的多层感知器(MLP)为例,这是一种常见的前馈神经网络:

当然,下面是一个简化的神经网络示意图,它展示了一个基本的多层感知器(MLP),这是一种常见的前馈神经网络:

    输入层        隐藏层        输出层

    [特征1] --> [神经元1] --> [输出1]
       |           |           |
    [特征2] --> [神经元2] --> [输出2]
       |           |           |
      ...         ...         ...
    [特征N] --> [神经元M] --> [输出K]
  • 输入层:表示神经网络接收的输入特征,每个特征对应一个输入节点。在这个例子中,有N个特征。
  • 隐藏层:输入层的每个节点都连接到隐藏层的每个神经元。隐藏层可以有多个,每个隐藏层可以包含多个神经元。在这个例子中,隐藏层有M个神经元。
  • 权重和偏置:每个连接都有一个权重(W),每个神经元都有一个偏置(b)。权重和偏置是神经网络的参数,它们在训练过程中被调整。
  • 激活函数:隐藏层的每个神经元都有一个激活函数(通常是非线性的),它决定了神经元是否以及如何激活。
  • 输出层:隐藏层的输出连接到输出层。输出层的每个神经元对应一个最终的输出,在这个例子中,输出层有K个输出。

这个示意图展示了数据在神经网络中的流动过程:

  1. 输入特征被送入输入层。
  2. 数据通过权重和偏置加权求和,然后通过激活函数。
  3. 激活后的信号在隐藏层中进行处理,可能经过多个隐藏层。
  4. 最终,输出层产生模型的预测结果。

手写神经网络代码

作为一个传统程序员, 我以为我是搞不懂这些东西, 直至翻到了这本书 - "Python 神经网络编程" book

这本书对入门者太友好了, 我几乎是一口气读下来的, 很久没有这样读一本技术书籍了, 下面的代码也是我边看书边手敲的, 也就 150 行左右的代码, 就构造了一个简单的神经网络, 用来识别手写数字

#!pip install scipy
import numpy
import matplotlib.pyplot as plt
import scipy.special

%matplotlib inline

class NeuralNetwork:
    # 初始化神经网络
    def __init__(self, **kwargs):
        # 构建 n 个结点的输入层
        self.input_nodes = int(kwargs.get("input_nodes"))
        # 构建 n 个结点的隐藏层
        self.hidden_nodes = int(kwargs.get("hidden_nodes"))
        # 构建 n 个结点的输出层
        self.output_nodes = int(kwargs.get("output_nodes"))
        # 设置学习率
        self.learning_rate = float(kwargs.get("learning_rate"))

        print(f"NeuralNetwork: input_nodes: {self.input_nodes}, hidden_nodes={self.hidden_nodes}, output_nodes={self.output_nodes}, lr={self.learning_rate}")
        # 创建 m*n 的权重矩阵, 数据为 -0.5 到 +0.5 之间的随机值
        #self.wih = numpy.random.rand(self.hidden_nodes, self.input_nodes) - 0.5
        #self.who = numpy.random.rand(self.output_nodes, self.hidden_nodes) - 0.5

        # 创建 m*n 的权重矩阵, 数据为正态分布的的权重矩阵, 以 0.0 为分布中心值, 以 3^(-0.5) 为标准方差
        # weight between input layer and hidden layer
        self.wih = numpy.random.normal(0.0, pow(self.hidden_nodes, 0.5), (self.hidden_nodes, self.input_nodes))
        # weight between hidden layer and output layer
        self.who = numpy.random.normal(0.0, pow(self.hidden_nodes, 0.5), (self.output_nodes, self.hidden_nodes))

        # 设置激活函数
        self.activation_function = lambda x: scipy.special.expit(x)

    # 训练神经网络
    def train(self, input_list, target_list):
        # convert inputs list to 2d array
        inputs = numpy.array(input_list, ndmin=2).T
        targets = numpy.array(target_list, ndmin=2).T

        # calculate signals into hidden layer
        hidden_inputs = numpy.dot(self.wih, inputs)
        # calculate the signals emerging from hidden layer
        hidden_outputs = self.activation_function(hidden_inputs)

        final_inputs = numpy.dot(self.who, hidden_outputs)
        # calculate signals emerging from final output layer
        final_outputs = self.activation_function(final_inputs)

        # 计算误差 target - actual
        output_errors = targets - final_outputs
        # 隐藏层的误差 hidder_errors 可由权重 output_errors 经权重 who 进行分割得出 
        hidden_errors = numpy.dot(self.who.T, output_errors)

        # 更新隐藏层与输出层之间的权重
        self.who += self.learning_rate * numpy.dot((output_errors * final_outputs * (1.0 - final_outputs)), numpy.transpose(hidden_outputs))
        # 更新输入层与隐藏层之间的权重
        self.wih += self.learning_rate * numpy.dot((hidden_errors * hidden_outputs * (1.0 - hidden_outputs)), numpy.transpose(inputs))

    # 查询神经网络, 接收输入, 产生输出
    def query(self, input_list):
        # convert inputs list to 2d array
        inputs = numpy.array(input_list, ndmin=2).T
        # calculate signals into hidden layer
        hidden_inputs = numpy.dot(self.wih, inputs)
        # calculate the signals emerging from hidden layer
        hidden_outputs = self.activation_function(hidden_inputs)
        final_inputs = numpy.dot(self.who, hidden_outputs)
        # calculate signals emerging from final output layer
        final_outputs = self.activation_function(final_inputs)
        return final_outputs


def load_data(data_file, count=0):
    data_list = []
    line_num = 0
    with open(data_file, 'r', newline='', encoding='utf-8') as fp:
        for line in fp:
            data_list.append(line)
            line_num += 1
            if count > 0 and line_num >= count:
                break
    return data_list

# 训练数据
def train(nn, training_data_list, output_nodes):
    epochs = 5

    for e in range(epochs):
        # go through all records in the training data set
        for record in training_data_list:
            # split the record by the ',' commas
            all_values = record.split(',')
            # scale and shift the inputs
            inputs = (numpy.asfarray(all_values[1:]) / 255.0 * 0.99) + 0.01
            # create the target output values (all 0.01, except the desired label which is 0.99)
            targets = numpy.zeros(output_nodes) + 0.01
            # all_values[0] is the target label for this record
            targets[int(all_values[0])] = 0.99
            nn.train(inputs, targets)

# 测试数据
def test(nn, test_data_list):
    scorecard = []
    loop_num = 0
    # go through all the records in the test data set
    for record in test_data_list:
        # split the record by the ',' commas
        all_values = record.split(',')
        # correct answer is first value
        correct_label = int(all_values[0])
        # scale and shift the inputs
        inputs = (numpy.asfarray(all_values[1:]) / 255.0 * 0.99) + 0.01
        # query the network
        outputs = nn.query(inputs)
        # the index of the highest value corresponds to the label
        label = numpy.argmax(outputs)
        if loop_num == 0:
            draw_digit(record)
            print(f"infer to {label} from {outputs}")
        # append correct or incorrect to list
        if (label == correct_label):
            # network's answer matches correct answer, add 1 to scorecard
            scorecard.append(1)
        else:
            # network's answer doesn't match correct answer, add 0 to scorecard
            scorecard.append(0)

        loop_num += 1

    return scorecard

# 维护手写数字
def draw_digit(data):
    print(f"data format: digit 28*28=784 points, data: {data}")
    values = data.split(',')
    image_array = numpy.asfarray(values[1:]).reshape(28, 28)
    plt.imshow(image_array, cmap='Greys', interpolation='None')

if __name__ == '__main__':
    # 构造神经网络
    nn = NeuralNetwork(input_nodes=784, hidden_nodes=200, output_nodes=10, learning_rate=0.1)
    # 取 1000 条数据来训练
    data_count = 1000
    # load the mnist training data CSV file into a list - 60000 titakt
    training_data_list = load_data("mnist_train.csv", data_count)

    train(nn, training_data_list, 10)

    # load the mnist test data CSV file into a list - 10000 totally
    test_data_list = load_data("mnist_test.csv", data_count)

    scorecard = test(nn, test_data_list)
    # calculate the performance score, the fraction of correct answers
    scorecard_array = numpy.asarray(scorecard)
    print ("performance = ", scorecard_array.sum() / scorecard_array.size)

输出如下

output

我们构造了一个三层的神经网络, 输入层有 784 个节点, 隐藏层有 200 个节点, 输出层有 10 个节点 以上面的 "7" 这个数字为例, 可以看到输出的节点中第 7 个节点的值最高, 也就是 7 的可能性最大, 从而识别出这个手写数字