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

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 的数字)。

神经网络的背景知识

神经网络有两种

  1. 生物神经网络,由真实的生物神经元组成
  2. 人工神经网络,旨在识别模式和解决业务问题。

人工神经网络试图模仿人类大脑中的生物神经网络, 以模仿人类的智能, 也就是人工智能。

典型的大脑包含近 1000 亿个称为神经元的微小细胞。每个神经元由一个细胞体和许多连接组成:众多树突dendrite(细胞的输入 - 将信息传送到细胞体)和单个轴突 axon(细胞的输出 - 将信息带走)。
树突从神经元细胞体延伸并接收来自其他神经元的信息。当神经元接收或发送信息时,它们会沿着轴突传输电脉冲,帮助执行诸如存储记忆、控制肌肉等功能。

nerual network

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

ai nn

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

  1. 神经元(Neuron)

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

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

    • 每个神经元的输入都有一个权重,这些权重决定了输入信号对神经元输出的影响大小。
  4. 偏置(Bias)

    • 加在加权求和的输入上的一个值,用于调整神经元激活的阈值。
  5. 激活函数(Activation Function)

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

    • 数据在神经网络中的流动方向,从输入层通过隐藏层到输出层。
  7. 损失函数(Loss Function)

    • 衡量模型预测值与实际值差异的函数,用于指导模型训练过程中的优化。
  8. 反向传播(Backpropagation)

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

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

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

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

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

    • 学习率是一个标量值,用于乘以损失函数关于模型参数的梯度。这个乘积决定了在每次迭代中更新模型参数的步长。

    • 学习率决定了在梯度下降或其他优化算法中,模型参数更新的幅度。如果步长太大,可能会越过最小值点;如果步长太小,训练过程会非常缓慢。

    • 初始学习率的设定通常是基于经验和实验。一些常见的初始学习率值包括 0.01、0.001、0.0001 等。

    • 在训练过程中,可能需要根据模型的表现调整学习率。一些常见的调整策略包括:

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

    • 如果学习率设置得过大,模型可能无法收敛,甚至发散。

    • 不同的优化器对学习率的处理方式不同。例如,SGD(随机梯度下降)使用简单的学习率乘以梯度,而 Adam 优化器则使用自适应学习率,为每个参数计算一个单独的学习率。

    • 在训练的初始阶段,逐渐增加学习率,可以帮助模型更快地收敛。

    • 使用学习率查找算法(如 Learning Rate Range Test)可以帮助确定合适的学习率范围。

下面是一个简化的神经网络示意图,它展示了一个基本的多层感知器(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 的可能性最大, 从而识别出这个手写数字

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容