基于梯度下降法的手写数字识别Python实例整理(超详细版)

1.背景

  本博客是本人学习了网易云课堂上龙曲良老师的《深度学习与TensorFlow 2入门实战》课程后总结出来的,这里表示感谢(非广告)。该程序的实现方式是通过建立全连接层,利用tensorflow,只需百行python代码,便可以简单高效地实现目标。
  此文初衷是为了巩固自己的学习成果,输出倒逼输入,如果能有幸帮助到网友,就更值了!

2.适用条件

  1. 电脑已安装Pycharm软件、tensorflow2以及常用的python第三方库,如numpy等。
  2. 由于计算量不大,CPU版本的tensorflow2即可。
  3. 无需自己准备mnist手写数字数据集,程序可自动下载,且无需科学上网。
  4. 该程序同样适用于FashionMnist数据集。

3.效果总览

  这里先给出所有程序代码以及输出结果,下一节将对细节内容做进一步详细介绍,以期能深入了解背后的原理。

#Mnist实战(层)
import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'#不输出无用信息

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import datasets, layers, optimizers, Sequential, metrics

assert tf.__version__.startswith('2.')#测试tensorflow版本号是否2.0开头

# 预处理函数
def preprocess(x,y):

    x=tf.cast(x,dtype=tf.float32)/255.
    y=tf.cast(y,dtype=tf.int32)
    return x,y

# 数据下载
(x,y),(x_test,y_test)=datasets.mnist.load_data()
print(x.shape,y.shape,x_test.shape,y_test.shape)

batchsz=128

# 训练集
db=tf.data.Dataset.from_tensor_slices((x,y))
db=db.map(preprocess).shuffle(10000).batch(batchsz)

# 测试集
db_test=tf.data.Dataset.from_tensor_slices((x_test,y_test))
db_test=db_test.map(preprocess).batch(batchsz)

# 检查样本
db_iter=iter(db)
sample=next(db_iter)
print('batch:',sample[0].shape,sample[1].shape)


#全连接层
model=Sequential([
    layers.Dense(256,activation=tf.nn.relu),
    layers.Dense(128,activation=tf.nn.relu),
    layers.Dense(64,activation=tf.nn.relu),
    layers.Dense(32,activation=tf.nn.relu),
    layers.Dense(10,)
])
model.build(input_shape=[None,28*28])
model.summary()

#w = w - lr*grad
optimizer = optimizers.Adam(lr=1e-3)

def main():
    for epoch in range(30):#循环三十次
        for step,(x,y) in enumerate(db):
            x = tf.reshape(x,[-1,28*28])#x[b,784]  ->   x[b,10]
            y_onehot = tf.one_hot(y, depth=10)#y[b]   ->  y[b,10]
            with tf.GradientTape() as tape:
                logits = model(x)
                loss_mse = tf.reduce_mean(tf.losses.MSE(y_onehot,logits))
                loss_ce = tf.reduce_mean(tf.losses.categorical_crossentropy(y_onehot,logits, from_logits=True))

            #求梯度
            # grads = tape.gradient(loss_ce,model.trainable_variables)
            grads = tape.gradient(loss_mse, model.trainable_variables)
            #参数更新
            optimizer.apply_gradients(zip(grads,model.trainable_variables))
            #zip(A,B):对应打包成元组,optimizer.apply_gradients:利用梯度对参数进行更新
            #打印信息
            if step % 100 ==0:
                print(epoch,step,'loss:',float(loss_ce),float(loss_mse))

        #test
        total_correct = 0
        total_num = 0
        for x,y in db_test:

            #输入x[b,28,28]
            x = tf.reshape(x,[-1,28*28])#x[b,784]
            logits = model(x)#x[b,10]
            prob = tf.nn.softmax(logits,axis=1)#求概率
            pred = tf.cast(tf.argmax(prob,axis=1),dtype=tf.int32)#x[b]

            correct = tf.equal(pred,y)#bool:x[b]
            correct = tf.reduce_sum(tf.cast(correct,dtype=tf.int32))#int:x[b]

            total_correct += int(correct)
            total_num += x.shape[0]

        acc = total_correct / total_num
        print(epoch,'test acc:', acc)

if __name__ == '__main__':
    main()

  上面程序大体上可分为数据加载、数据预处理、建立全连接层、梯度下降和数据测试5个子模块,下面给出输出结果。这里同时给出交叉熵、均方根误差两种误差计算方式(对应下面每行后两个值),并用均方根误差对参数求梯度来更新参数值,可以看出在训练数据循环完一遍后,测试数据的成功率已达到96%。下节将按照上述5个子模块,逐一细致地梳理数据处理过程。

0 0 loss: 2.3302528858184814 0.1209840327501297
0 100 loss: 1.6496442556381226 0.01957141049206257
0 200 loss: 1.5355703830718994 0.007448272779583931
0 300 loss: 1.5914044380187988 0.012856109999120235
0 400 loss: 1.527681827545166 0.010478023439645767
0 test acc: 0.9667

4.功能细分

4.1数据加载

  首先是数据加载子模块,这一部分主要包含以下代码:

import os
os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2'#仅输出error和fatal信息

import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import datasets, layers, optimizers, Sequential, metrics

assert tf.__version__.startswith('2.')#测试tensorflow版本号是否2.0开头

(x,y),(x_test,y_test)=datasets.mnist.load_data()#加载数据
print(x.shape,y.shape,x_test.shape,y_test.shape)#数据维度
print(type(x),type(y))#数据类型
print(tf.reduce_min(x),tf.reduce_max(x),tf.reduce_min(y),tf.reduce_max(y))#数据最值

  1、2行代码限制了python console窗口输出warnning等无用信息,可帮助我们快速识别输出结果(或者bug类型哈哈哈);4-6行代码用于导入tensorflow、keras等深度学习需要用到的库;10-13行代码通过datasets下载mnist数据集,该数据集包含了60k张28×28像素的图片训练集(以及对应的60k个标签)和10k张28×28像素的图片测试集(以及对应的10k个标签),它们的数据类型为’numpy.ndarray’,图片最小值为0,最大值为255(单通道灰度图),标签最小值为0,最大值为9(表示数字0到9),元素数据类型为uint8。如下为print的输出结果:

(60000, 28, 28) (60000,) (10000, 28, 28) (10000,)
<class 'numpy.ndarray'> <class 'numpy.ndarray'>
tf.Tensor(0, shape=(), dtype=uint8) tf.Tensor(255, shape=(), dtype=uint8) tf.Tensor(0, shape=(), dtype=uint8) tf.Tensor(9, shape=(), dtype=uint8)

用该方法可同样下载FashionMnist、Cifa10等数据集。

4.2数据预处理

  数据加载后,并不可直接输入给模型,首先需要经过数据预处理,该子模块的代码如下:

# 预处理函数
def preprocess(x,y):

    x=tf.cast(x,dtype=tf.float32)/255.
    y=tf.cast(y,dtype=tf.int32)
    return x,y
    
batchsz=128
# 训练集
db=tf.data.Dataset.from_tensor_slices((x,y))
db=db.map(preprocess).shuffle(10000).batch(batchsz)

# 测试集
db_test=tf.data.Dataset.from_tensor_slices((x_test,y_test))
db_test=db_test.map(preprocess).batch(batchsz)

# 检查样本
db_iter=iter(db)
sample=next(db_iter)
print('batch:',sample[0].shape,sample[1].shape)

  这里首先定义了一个数据预处理函数(1-6行代码),用于完成训练数据和测试数据的类型转换和归一化;之后,将60k的数据量分批,batchsz变量定义了每一批数据的个数,这里设置为128,意味着60k的数据被分为了469批(最后一批数据量小于128);接着将x、y组合成训练数据库db,同时利用map()调用预处理函数,利用shuffle打乱训练数据并将其按照batchsz分组,测试数据可做同样操作,但无需shuffle打乱;最后3行代码用于检验数据预处理结果是否正确,这里利用iter()创建迭代器,并通过next()取出第一批db,如下为print的输出结果:

batch: (128, 28, 28) (128,)

4.3建立全连接层

  数据预处理后,相应的模型也需建立好,全连接层子模块的代码如下:

#全连接层
model=Sequential([
    layers.Dense(256,activation=tf.nn.relu),
    layers.Dense(128,activation=tf.nn.relu),
    layers.Dense(64,activation=tf.nn.relu),
    layers.Dense(32,activation=tf.nn.relu),
    layers.Dense(10,)
])
model.build(input_shape=[None,28*28])
model.summary()

#w = w - lr*grad
optimizer = optimizers.Adam(lr=1e-3)

  这里利用Squential()构建了一个五层的全连接模型,经过该模型后,输出维度为10,前4层的输出激活函数选为relu函数,并设置该模型的输入数据的第二个维度为784(第一个维度由总张量元素个数除以784(28×28),这里其实就是128),然后利用summary()输出模型各层的参数状况,结果如下:

Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
dense (Dense)                multiple                  200960    
_________________________________________________________________
dense_1 (Dense)              multiple                  32896     
_________________________________________________________________
dense_2 (Dense)              multiple                  8256      
_________________________________________________________________
dense_3 (Dense)              multiple                  2080      
_________________________________________________________________
dense_4 (Dense)              multiple                  330       
=================================================================
Total params: 244,522
Trainable params: 244,522
Non-trainable params: 0

可以看出,5层全连接将模型共使用了244522个参数。最后适用优化器optimizers.Adam()来动态调整学习率,实现最优的结果(学习得既快又不会直接越过最优点)。

4.4梯度下降法

  当模型和数据都准备就绪后,就可以利用本博客的核心梯度下降来做优化了,在进行优化之前,首先需要设置两个循环(也可见总程序),第一层循环是

for epoch in range(30):#循环三十次

该层循环是将整个60k的训练数据循环30次(可根据情况调整),以求得更优的模型参数。第二层循环即是按照先前设置的128为一批,一批批处理,以循环整个60k的数据集,循环次数为469次。

for step,(x,y) in enumerate(db):

  进入两层循环内,即取出一批训练数据后,便可进入梯度下降子模块,该子模块的代码如下,使用tensorflow实现十分简单:

x = tf.reshape(x,[-1,28*28])#x[b,784]  ->   x[b,10]
y_onehot = tf.one_hot(y, depth=10)#y[b]   ->  y[b,10]

with tf.GradientTape() as tape:
    logits = model(x)

    loss_mse = tf.reduce_mean(tf.losses.MSE(y_onehot,logits))
    loss_ce = tf.reduce_mean(tf.losses.categorical_crossentropy(y_onehot,logits, from_logits=True))

#求梯度
# grads = tape.gradient(loss_ce,model.trainable_variables)
grads = tape.gradient(loss_mse, model.trainable_variables)
#参数更新
optimizer.apply_gradients(zip(grads,model.trainable_variables))
#zip(A,B):对应打包成元组,optimizer.apply_gradients:利用梯度对参数进行更新
#打印信息
if step % 100 ==0:
    print(epoch,step,'loss:',float(loss_ce),float(loss_mse))

  首先利用tf.reshape()函数将二维图片重组为一维向量,以符合model的输入维度(shape:[128,784]);其次,为了将标签值与估计值(模型输出值)相比较,需利用tf.one_hot()函数将标签第二维度个改为10(shape:[128,10]),即如果原标签值为3,则one_hot后为(0001000000)。
  在准备好输入数据x和真实值y(标签)后,就要进入梯度下降法的核心部分了,这里利用tensorflow自带自动求导工具:GradientTape,将估计值计算和误差计算均放在GradientTape中,之后便可通过tape.gradient()来计算误差对各个参数的导数,并利用optimizer.apply_gradients()对参数进行更新,而无需人为列出所有参数,并一一手动更新,该函数大大简化了工作,之后便按照前述两个循环进行一步步迭代。
  最后每间隔100batch数据,输出一次信息,可观察均方根误差和交叉熵误差是否都随着循环而减小,以判断结果是否朝着更优的方向前进,具体可见本博客第二块代码图。

4.5数据测试

  从误差结果看,似乎其并非随着迭代次数而呈现线性的下降趋势,为了更加全面地评判模型估计结果的准确性,可利用测试数据来做测试,具体代码如下:

#test
total_correct = 0
total_num = 0
for x,y in db_test:

    #输入x[b,28,28]
    x = tf.reshape(x,[-1,28*28])#x[b,784]
    logits = model(x)#x[b,10]
    prob = tf.nn.softmax(logits,axis=1)#求概率
    pred = tf.cast(tf.argmax(prob,axis=1),dtype=tf.int32)#x[b]

    correct = tf.equal(pred,y)#bool:x[b]
    correct = tf.reduce_sum(tf.cast(correct,dtype=tf.int32))#int:x[b]

    total_correct += int(correct)
    total_num += x.shape[0]

acc = total_correct / total_num
print(epoch,'test acc:', acc)

  首先,该代码块是在循环完一整块训练数据后,才利用最新的模型参数计算一次测试数据的识别值,因为此时的准确率可能更高,更值得测试并输出。当然为了全程监控模型的情况,也可以每循环100批数据,做一次测试。
  测试时,首先定义两个变量,分别放置测试成功的数据个数,和总的数据个数,两者相除便可得到测试成功率。之后与训练数据类似:取出测试数据的一个batch、维度重排、代入模型计算,这里可不像训练数据那样对标签做one_hot,可利用tf.nn.softmax()函数将估计值(shape:[128,10])转为概率,并通过tf.argmax()函数取出概率最大值的索引,即为本模型的最终识别值(shape:[128])。
  为了得到估计成功的数据个数,将估计值与标签值进行比较(通过tf.equal()函数,相等为1,不等为0)并求和(tf.reduce_sum()函数)即可,而总数据个数其实也就是x的第一个维度值。待循环完整个测试数据时,计算成功数据个数与总个数的比值就可得到模型识别成功率。具体结果同样可见本博客第二块代码图。

5.总结

  以上便是本博客的所有内容,希望耗时4.5天写出,后续还将进一步优化,希望对自己对大家有所帮助。下面是待改进内容:

  1. 还需添加数据类型转换详解
  2. 每行实现功能最好以注释方式体现,在代码块下方用文字讲解费时费力
  3. 加入可视化结果使得数据处理更清晰
  • 5
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值