python–基于三层神经网络实现手写数字分类

背景知识

一个完整的神经网络通常由多个基本的网络层堆叠而成。本实验中的三层神经网络由三个全连接层构成,在每两个全连接层之间会插入ReLU激活函数引入非线性变换,最后使用Softmax层计算交叉嫡损失,如图所示。因此本实验中使用的基本单元包括全连接层、ReLU激活函数、Softmax损失函数,在本节中将分别进行介绍。

全连接层以一维向量作为输入,输入与权重相乘后再与偏置相加得到输出向量。假设全连接层的输入为一维向量x,维度为m;输出为一维向量y,维度为n;权重W是二维矩阵,维度为mxn,偏置b是一维向量,维度为n。前向传播时,全连接层的输出的计算公式:
y = W T x + b y = W^{T}x + b y =W T x +b
在计算全连接层的反向传播时,给定神经网络损失函数L对当前全连接层的输出y的偏导:
∇ y L = ∂ L ∂ y \nabla_yL = \dfrac{\partial L}{\partial y}∇y ​L =∂y ∂L ​
其维度与全连接层的输出y相同,均为n。根据链式法则,全连接层的权重和偏置的梯度
∇ W L = ∂ L ∂ W \nabla_WL = \dfrac{\partial L}{\partial W}∇W ​L =∂W ∂L ​
∇ b L = ∂ L ∂ b \nabla_bL = \dfrac{\partial L}{\partial b}∇b ​L =∂b ∂L ​
以及损失函数对输入的偏导
∇ x L = ∂ L ∂ x \nabla_xL = \dfrac{\partial L}{\partial x}∇x ​L =∂x ∂L ​
计算公式分别为:
∇ W L = x ∇ y L T \nabla_WL =x \nabla_yL^{T}∇W ​L =x ∇y ​L T
∇ b L = ∇ y L \nabla_bL = \nabla_yL ∇b ​L =∇y ​L
∇ x L = W T ∇ y L \nabla_xL = W^{T}\nabla_yL ∇x ​L =W T ∇y ​L
实际应用中通常使用批量随机梯度下降算法进行反向传播计算,即选择若干个样本同时计算。假设选择的样本量为p,此时输入变为二维矩阵X,维度为p×m,每行代表一个样本。输出也变为二维矩阵Y,维度为p×n。此时全连接层的前向传播计算公式为:
Y = X W + b Y = XW + b Y =X W +b
其中的+代表广播运算,表示偏置b中的元素会被加到XW的乘积矩阵对应的一行元素中。权重和偏置的梯度以及损失函数对输入的偏导的计算公式变为:
∇ W L = X T ∇ Y L \nabla_WL =X^{T} \nabla_YL ∇W ​L =X T ∇Y ​L
∇ b L = 1 ∇ Y L \nabla_bL = 1 \nabla_YL ∇b ​L =1 ∇Y ​L
∇ X L = ∇ Y L W T \nabla_XL = \nabla_YLW^{T}∇X ​L =∇Y ​L W T
其中计算偏置的梯度时,为确保维度正确,用与维度为1×p的全1向量1相乘。

ReLU激活函数是按元素运算操作,输出向量y的维度与输入向量x的维度相同。在前向传播中,如果输入x中的元素小于0,输出为0,否则输出等于输入。因此ReLU的计算公式为:
y ( i ) = m a x ( 0 , x ( i ) ) y(i) = max(0,x(i))y (i )=m a x (0 ,x (i ))
其中x(i) 和y(i)分别代表x和y在位置i的值。
由于ReLU激活函数不包含参数,在反向传播计算过程中仅需根据损失函数对输出的偏导计算损失函数对输入的偏导。设i代表输入x的某个位置,则损失函数对本层的第i个输入的偏导的计算公式为
∇ x ( i ) L = { ∇ y ( i ) L if x ( i ) > 0 0 if x ( i ) < 0 \nabla_x(i)L = \begin{cases} \nabla_y(i)L &\text{if } x(i) >0 \ 0 &\text{if } x(i)

Softmax损失层是目前多分类问题中最常用的损失函数层。假设Softmax 损失层的输入为向量x,维度为k。其中k对应分类的类别数,如对手写数字0至9进行分类时,类别数k = 10。在前向传播的计算过程中,对r计算e指数并进行归一化,即得到Softmax分类概率。假设x对应 i位置的值为x(i),y^(i)为i位置的Softmax 分类概率,i ∈[1,k]且为整数,则到y^(i)的计算公式为:
y ^ = e x ( i ) ∑ j = 1 n e x ( j ) \hat{y} =\frac{e^{x_(i)}}{\textstyle\sum_{j=1}^n e^{x_(j)}}y ^​=∑j =1 n ​e x (​j )e x (​i )​
在前向计算时,对Softmax 分类概率,取最大概率对应的类别作为预测的分类类别。损失层在计算前向传播时还需要根据给定的标记(label,也称为真实值或实际值)y计算总的损失函数值。在分类任务中,标记y通常表示为一个维度为k的one-hot向量,该向量中对应真实类别的分量值为1,其他值为0。Softmax损失层使用交叉嫡计算损失值,其损失值L的计算公式为
L = − ∑ i y ( i ) l n y ^ ( i ) L = – \sum_{\mathclap{i}} y_{(i)}ln\hat{y}(i)L =−i ∑​y (i )​l n y ^​(i )
在反向传播的计算过程中,可直接利用标记数据和损失层的输出计算本层输入的损失。对于Softmax损失层,损失函数对输入的偏导vxL的计算公式为
∇ x L = ∂ L ∂ x = y ^ − y \nabla_xL = \dfrac{\partial L}{\partial x} = \hat{y} -y ∇x ​L =∂x ∂L ​=y ^​−y
由于工程实现中使用批量随机梯度下降算法,假设选择的样本量为p,Softmax 损失层的输入变为二维矩阵X,维度为p×k,的每个行向量代表一个样本,则对每个输入计算e指数并进行行归一化得到
y ^ = e x ( i ) − m a x X ( i , n ) ∑ j = 1 n e x ( j ) − m a x X ( j , n ) \hat{y} =\frac{e^{x_(i)-max X(i,n)}}{\textstyle\sum_{j=1}^n e^{x_(j)-maxX(j,n)}}y ^​=∑j =1 n ​e x (​j )−m a x X (j ,n )e x (​i )−m a x X (i ,n )​
在前向计算时,对Softmax分类概率的每个样本(即每个行向量)取最大概率对应的类别作为预测的分类类别。此时标记Y通常表示为一组one-hot向量,维度为p×k,其中每行是一个one-hot向量,对应一个样本的标记。则计算损失值的公式变为
L = − 1 p ∑ i , j y ( i , j ) l n y ^ ( i , j ) L = – \dfrac{1}{p}\sum_{\mathclap{i,j}} y_{(i,j)}ln\hat{y}(i,j)L =−p 1 ​i ,j ​∑​y (i ,j )​l n y ^​(i ,j )
其中损失值是所有样本的平均损失,因此对样本数量p取平均。
在反向传播时,当选择的样本量为p时,损失函数对输入的偏导的计算公式变为:
∇ X L = ∂ L ∂ X = 1 p ( y ^ − y ) \nabla_XL = \dfrac{\partial L}{\partial X} = \dfrac{1}{p}( \hat{y} -y)∇X ​L =∂X ∂L ​=p 1 ​(y ^​−y )
类似地,损失vxL是所有样本的平均损失,因此对样本数量p取平均。

神经网络训练

神经网络训练通过调整网络层的参数来使神经网络计算出来的结果与真实结果(标记)尽量接近。神经网络训练通常使用随机梯度下降算法,通过不断的迭代计算每层参数的梯度,利用梯度对每层参数进行更新。具体而言,给定当前迭代的训练样本〈包含输入数据及标记信息),首先进行神经网络的前向传播处理,输入数据和权重相乘再经过激活函数计算出隐层,隐层与下一层的权重相乘再经过激活函数得到下一个隐层,通过逐层迭代计算出神经网络的输出结果。随后利用输出结果和标记信息计算出损失函数值。然后进行神经网络的反向传播处理,从损失函数开始逆序逐层计算损失函数对权重和偏置的偏导(即梯度),最后利用梯度对相应的参数进行更新。更新参数W的计算公式为
W = W − η ∇ W L W = W – \eta\nabla_WL W =W −η∇W ​L

在图像分类任务中,通常使用测试集的平均分类正确率来判断分类结果的精度。假设共有N个图像样本(MNIST手写数据集中共包含10000张测试图像,此时N = 10000),pi为神经网络输出的第i张图像的推断结果,p;为一个向量,取其中最大分量对应的类别作为推断类别。假设第i张图像的标记为y;,即第i张图像属于类别y;,则计算平均分类正确率R的公式为
L = 1 N ∑ y ( i , j ) a r g m a x ( p i ) = y i L = \dfrac{1}{N}\sum_{} y_{(i,j)}argmax(p_i) = y_i L =N 1 ​∑​y (i ,j )​a r g m a x (p i ​)=y i ​

实验内容

设计一个三层神经网络实现手写数字图像分类。该网络包含两个隐层和一个输出层,其中输入神经元个数由输入数据维度决定,输出层的神经元个数由数据集包含的类别决定,两个隐层的神经元个数可以作为超参数自行设置。对于手写数字图像的分类问题,输入数据为手写数字图像﹐原始图像一般可表示为二维矩阵(灰度图像)或三维矩阵(彩色图像),在输入神经网络前会将图像矩阵调整为一维向量作为输入。待分类的类别数一般是提前预设的,如手写数字包含0至9共10个类别,则神经网络的输出神经元个数为10。
为了便于迭代开发,工程实现时采用模块化的方式来实现整个神经网络的处理。目前绝大多数神经网络的工程实现通常划分为5大模块:

1)数据加载模块:从文件中读取数据,并进行预处理,其中预处理包括归一化、维度变换等处理。如果需要人为对数据进行随机数据扩增,则数据扩增处理也在数据加载模块中实现。

2)基本单元模块:实现神经网络中不同类型的网络层的定义、前向传播计算、反向传播计算等功能。

3)网络结构模块:利用基本单元模块建立一个完整的神经网络。

4)网络训练(training)模块:该模块实现用训练集进行神经网络训练的功能。在已建立的神经网络结构基础上,实现神经网络的前向传播、神经网络的反向传播、对神经网络进行参数更新、保存神经网络参数等基本操作,以及训练函数主体。

5)网络推断(inference)模块:该模块实现使用训练得到的网络模型,对测试样本进行预测的过程。具体实现的操作包括训练得到的模型参数的加载、神经网络的前向传播等。

下面给出所有代码:
layers.py


import numpy as np
import struct
import os
import time

class FullyConnectedLayer(object):
    def __init__(self, num_input, num_output):
        self.num_input = num_input
        self.num_output = num_output
        print('\tFully connected layer with input %d, output %d.' % (self.num_input, self.num_output))
    def init_param(self, std=0.01):
        self.weight = np.random.normal(loc=0, scale=std, size=(self.num_input, self.num_output))
        self.bias = np.zeros([1, self.num_output])
    def forward(self, input):
        start_time = time.time()
        self.input = input

        self.output = np.dot(self.input,self.weight)+self.bias
        return self.output
    def backward(self, top_diff):

        self.d_weight =np.dot(self.input.T,top_diff)
        self.d_bias = top_diff
        bottom_diff = np.dot(top_diff,self.weight.T)
        return bottom_diff
    def update_param(self, lr):

        self.weight = self.weight - lr * self.d_weight
        self.bias = self.bias - lr * self.d_bias
    def load_param(self, weight, bias):
        assert self.weight.shape == weight.shape
        assert self.bias.shape == bias.shape
        self.weight = weight
        self.bias = bias
    def save_param(self):
        return self.weight, self.bias

class ReLULayer(object):
    def __init__(self):
        print('\tReLU layer.')
    def forward(self, input):
        start_time = time.time()
        self.input = input

        output = np.maximum(self.input,0)
        return output
    def backward(self, top_diff):

        b = self.input
        b[b>0] =1
        b[b<0] = 0
        bottom_diff = np.multiply(b,top_diff)

        return bottom_diff

class SoftmaxLossLayer(object):
    def __init__(self):
        print('\tSoftmax loss layer.')
    def forward(self, input):

        input_max = np.max(input, axis=1, keepdims=True)
        input_exp = np.exp(input- input_max)
        partsum = np.sum(input_exp,axis=1)
        sum = np.tile(partsum,(10,1))
        self.prob = input_exp / sum.T

        return self.prob
    def get_loss(self, label):
        self.batch_size = self.prob.shape[0]
        self.label_onehot = np.zeros_like(self.prob)

        self.label_onehot[np.arange(self.batch_size), label] = 1.0

        loss = -np.sum(self.label_onehot*np.log(self.prob)) / self.batch_size
        return loss
    def backward(self):

        bottom_diff = (self.prob - self.label_onehot)/self.batch_size
        return bottom_diff

main_mnist.py

import time

def evaluate(mlp):
    pred_results = np.zeros([mlp.test_data.shape[0]])
    for idx in range(mlp.test_data.shape[0]//mlp.batch_size):
        batch_images = mlp.test_data[idx*mlp.batch_size:(idx+1)*mlp.batch_size, :-1]
        prob = mlp.forward(batch_images)
        pred_labels = np.argmax(prob, axis=1)
        pred_results[idx*mlp.batch_size:(idx+1)*mlp.batch_size] = pred_labels
    if mlp.test_data.shape[0] % mlp.batch_size >0:
        last_batch =mlp.test_data.shape[0] % mlp.batch_size
        batch_images = mlp.test_data[-last_batch:, :-1]
        prob = mlp.forward(batch_images)
        pred_labels = np.argmax(prob, axis=1)
        pred_results[-last_batch:] = pred_labels
    accuracy = np.mean(pred_results == mlp.test_data[:,-1])
    print('Accuracy in test set: %f' % accuracy)

if __name__ == '__main__':
    mlp = build_mnist_mlp()
    evaluate(mlp)

mnist_mlp_cpu.py

coding=utf-8
import numpy as np
import struct
import os
import time
import matplotlib.pyplot as plt
from layers_1 import FullyConnectedLayer, ReLULayer, SoftmaxLossLayer

MNIST_DIR = "mnist_data"
TRAIN_DATA = "train-images.idx3-ubyte"
TRAIN_LABEL = "train-labels.idx1-ubyte"
TEST_DATA = "t10k-images.idx3-ubyte"
TEST_LABEL = "t10k-labels.idx1-ubyte"

def show_matrix(mat, name):
    #print(name + str(mat.shape) + ' mean %f, std %f' % (mat.mean(), mat.std()))
    pass

class MNIST_MLP(object):
    def __init__(self, batch_size=100, input_size=784, hidden1=32, hidden2=16, out_classes=10, lr=0.01, max_epoch=1, print_iter=100):
        self.batch_size = batch_size
        self.input_size = input_size
        self.hidden1 = hidden1
        self.hidden2 = hidden2
        self.out_classes = out_classes
        self.lr = lr
        self.max_epoch = max_epoch
        self.print_iter = print_iter

    def load_mnist(self, file_dir, is_images = 'True'):
        # Read binary data
        bin_file = open(file_dir, 'rb')
        print(file_dir)
        bin_data = bin_file.read()
        bin_file.close()
        # Analysis file header
        if is_images:
            # Read images
            fmt_header = '>iiii'
            magic, num_images, num_rows, num_cols = struct.unpack_from(fmt_header, bin_data, 0)
        else:
            # Read labels
            fmt_header = '>ii'
            magic, num_images = struct.unpack_from(fmt_header, bin_data, 0)
            num_rows, num_cols = 1, 1
        data_size = num_images * num_rows * num_cols
        mat_data = struct.unpack_from('>' + str(data_size) + 'B', bin_data, struct.calcsize(fmt_header))
        mat_data = np.reshape(mat_data, [num_images, num_rows * num_cols])
        print('Load images from %s, number: %d, data shape: %s' % (file_dir, num_images, str(mat_data.shape)))
        return mat_data

    def load_data(self):
        # TODO: &#x8C03;&#x7528;&#x51FD;&#x6570; load_mnist &#x8BFB;&#x53D6;&#x548C;&#x9884;&#x5904;&#x7406; MNIST &#x4E2D;&#x8BAD;&#x7EC3;&#x6570;&#x636E;&#x548C;&#x6D4B;&#x8BD5;&#x6570;&#x636E;&#x7684;&#x56FE;&#x50CF;&#x548C;&#x6807;&#x8BB0;
        print('Loading MNIST data from files...')
        train_images = self.load_mnist(os.path.join(MNIST_DIR, TRAIN_DATA), True)
        train_labels = self.load_mnist(os.path.join(MNIST_DIR, TRAIN_LABEL), False)
        test_images = self.load_mnist(os.path.join(MNIST_DIR, TEST_DATA), True)
        test_labels = self.load_mnist(os.path.join(MNIST_DIR, TEST_LABEL), False)
        self.train_data = np.append(train_images, train_labels, axis=1)

        self.test_data = np.append(test_images, test_labels, axis=1)
        # self.test_data = np.concatenate((self.train_data, self.test_data), axis=0)

    def shuffle_data(self):
        print('Randomly shuffle MNIST data...')
        np.random.shuffle(self.train_data)

    def build_model(self):  # &#x5EFA;&#x7ACB;&#x7F51;&#x7EDC;&#x7ED3;&#x6784;
        # TODO&#xFF1A;&#x5EFA;&#x7ACB;&#x4E09;&#x5C42;&#x795E;&#x7ECF;&#x7F51;&#x7EDC;&#x7ED3;&#x6784;
        print('Building multi-layer perception model...')
        self.fc1 = FullyConnectedLayer(self.input_size, self.hidden1)
        self.relu1 = ReLULayer()
        self.fc2 = FullyConnectedLayer(self.hidden1, self.hidden2)      #
        self.relu2 =  ReLULayer()   #
        self.fc3 = FullyConnectedLayer(self.hidden2, self.out_classes)
        self.softmax = SoftmaxLossLayer()
        self.update_layer_list = [self.fc1, self.fc2, self.fc3]

    def init_model(self):
        print('Initializing parameters of each layer in MLP...')
        for layer in self.update_layer_list:
            layer.init_param()

    def load_model(self, param_dir):
        print('Loading parameters from file ' + param_dir)
        params = np.load(param_dir).item()
        self.fc1.load_param(params['w1'], params['b1'])
        self.fc2.load_param(params['w2'], params['b2'])
        self.fc3.load_param(params['w3'], params['b3'])

    def save_model(self, param_dir):
        print('Saving parameters to file ' + param_dir)
        params = {}
        params['w1'], params['b1'] = self.fc1.save_param()
        params['w2'], params['b2'] = self.fc2.save_param()
        params['w3'], params['b3'] = self.fc3.save_param()
        np.save(param_dir, params)

    def forward(self, input):  # &#x795E;&#x7ECF;&#x7F51;&#x7EDC;&#x7684;&#x524D;&#x5411;&#x4F20;&#x64AD;
        # TODO&#xFF1A;&#x795E;&#x7ECF;&#x7F51;&#x7EDC;&#x7684;&#x524D;&#x5411;&#x4F20;&#x64AD;
        h1 = self.fc1.forward(input)
        h1 = self.relu1.forward(h1)
        h2 = self.fc2.forward(h1)  #
        h2 = self.relu2.forward(h2)  #
        h3 = self.fc3.forward(h2)  #
        self.prob = self.softmax.forward(h3)
        return self.prob

    def backward(self):  # &#x795E;&#x7ECF;&#x7F51;&#x7EDC;&#x7684;&#x53CD;&#x5411;&#x4F20;&#x64AD;
        # TODO&#xFF1A;&#x795E;&#x7ECF;&#x7F51;&#x7EDC;&#x7684;&#x53CD;&#x5411;&#x4F20;&#x64AD;
        dloss = self.softmax.backward()
        dh2 = self.fc3.backward(dloss)  #
        dh2 = self.relu2.backward(dh2)  #
        dh1 = self.fc2.backward(dh2)
        dh1 = self.relu1.backward(dh1)#
        dh1 = self.fc1.backward(dh1)

    def update(self, lr):
        for layer in self.update_layer_list:
            layer.update_param(lr)

    def train(self):
        max_batch = int(self.train_data.shape[0] / self.batch_size)
        print('Start training...')
        for idx_epoch in range(self.max_epoch):
            self.shuffle_data()
            for idx_batch in range(max_batch):
                batch_images = self.train_data[idx_batch*self.batch_size:(idx_batch+1)*self.batch_size, :-1]
                batch_labels = self.train_data[idx_batch*self.batch_size:(idx_batch+1)*self.batch_size, -1]
                #plt.imshow(batch_images[:1,:].reshape(28,28))
                #print(batch_labels[:10])
                #plt.show()
                prob = self.forward(batch_images)
                #(batch_labels[:1])
                #print(self.prob)
                loss = self.softmax.get_loss(batch_labels)
                self.backward()

                self.update(self.lr)
                if idx_batch % self.print_iter == 0:
                    print('Epoch %d, iter %d, loss: %.6f' % (idx_epoch, idx_batch, loss))

    def evaluate(self):
        pred_results = np.zeros([self.test_data.shape[0]])
        for idx in range(int(self.test_data.shape[0]/self.batch_size)):
            batch_images = self.test_data[idx*self.batch_size:(idx+1)*self.batch_size, :-1]
            start = time.time()
            prob = self.forward(batch_images)
            end = time.time()
            print("inferencing time: %f"%(end-start))
            pred_labels = np.argmax(prob, axis=1)
            pred_results[idx*self.batch_size:(idx+1)*self.batch_size] = pred_labels
        accuracy = np.mean(pred_results == self.test_data[:,-1])
        print('Accuracy in test set: %f' % accuracy)

def build_mnist_mlp(param_dir='weight.npy'):
    h1, h2, e = 32, 16, 10
    mlp = MNIST_MLP(hidden1=h1, hidden2=h2, max_epoch=e)
    mlp.load_data()
    mlp.build_model()
    mlp.init_model()
    mlp.train()
    mlp.save_model('mlp-%d-%d-%depoch.npy' % (h1, h2, e))
    # mlp.load_model('mlp-%d-%d-%depoch.npy' % (h1, h2, e))
    return mlp

if __name__ == '__main__':
    mlp = build_mnist_mlp()
    mlp.evaluate()

Original: https://blog.csdn.net/qq_46102127/article/details/113618650
Author: 玮雨君
Title: python–基于三层神经网络实现手写数字分类

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/666908/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球