Tensorflow_05_从头构建 CNN 神经网络框架 - part 3: 优化结果与观察过程

Brief. Review of the low accuracy

当使用完了所有论文提及到的招式后,看到低下的正确率时,脑中的疑点肯定已经足以在几秒钟内盖过诧异的神色,从卷积框架的搭建,到数据处理方法的使用,再到超参数的初始化设定,最后检查在缓存不过载的情况下,把框架执行上千次的训练后得出一个结果,每个环节需要更为深刻的理解才足以支持实际应用。下面是低正确率的训练结果回顾,同时也是上一篇文章的结尾:

optimize(4000, batch_size=192, pp=True)
acc()
100%|██████████| 4000/4000 [1:35:22<00:00,  1.40s/it]
Took 5.827e+03 sec to run "optimize" func
Accuracy on Test Set: 57.84%

Characteristics of Data 数据特征

机器学习最核心的问题莫过于特征的提取,如何使用一个有效且实际的方式把藏在每一批数据中的特征找出来,并泛化成大多数数据都适用的评判依据,那就是整个问题的核心,而深度学习靠的就是逐层提取信息,不像传统的浅层神经网络一般,使用非常宽扁的结构,排列组合不够多的情况下,就会使得最终的分类结果死板且不知变通。

提取特征的过程用人类的角度来说就是一个抽象化的概念,举一个小朋友学数学的心路历程作为抽象话过程的例子,步骤如下:

  1. 数数的训练,理解数字与个数的关系
  2. 从数数演变到加减法,第一阶的抽象化
  3. 从加减法演变到乘法,第二阶段的抽象化
  4. 从乘法反推回除法,第三阶段的抽象化
  5. 从除法进阶到分数小数,第四阶段的抽象化
  6. 分数小数的加减乘除重新来一遍,横向的知识拓展

从人类的理解路径来看,下一个阶段的理解和观念肯定是基于上一个阶段的积累与铺垫,用统计学的方式说,下一个阶段永远是上一个阶段的子集,不会有超出上一个阶段边界的例外,利用这个观点回推深度学习上也是同一个道理,因此做特征提取的时候,第一步要做的就是尽力搜寻到所有有用的特征,因为接下来几层的特征再提取都是基于上一层已经有的特征去做变化,如果一开始的特征不够多,那么对整体的分类来说就是白搭,继续基于这个观点的论证,我们可以了解到深层网络中前面几层的网络设计也就有着无足轻重的地位。

Parameters in Funnel Shape 头重脚轻的参数分布

理解了上面描述的观念后,在建构神经网络时我们就要非常小心的考虑参数在每一层神经中的消长过程,如果让神经数量经过每层的迭代越变越多,用人的角度来看那就是对目标的迷失,没有方向感的判断将让最后全连接层无法顺利缩小到固定的标签数量上。

一个良性的神经网络应该是在最一开始就抽离出我们认为合理的特征数量,然后依据此特征数量开始往后更深层的方向抽象之,抽象的过程逐渐减少参数量,到最后全连接层顺利地把富含高特征密度的值嫁接到标签上,这么一来准确率就能够顺利提升。而应用此方法后,每层神经网络包含的参数量纵观而论就会像是一个漏斗的分布,前面很多特征,最后逐渐把特征收合到对应的类上去。

Advantages of Convolution 卷积的优势

卷积这个方法无非也是围绕着同一个话题:特征剥离,一张图片的最基本组成单元即为像素点,任何的形状都是基于不同颜色像素点抽象化出来的结果,因此卷积是一个非常适合寻找不同图片之间形状组合的共同原则,截取出来的额特征如下示意图:

第一层提取出来的内容都是非常简单的图案,甚至连几何图形都还不是,但可以肯定的是这些简单的图案就是后面拼凑出复杂图形的最基本原料,只有这些图案足够齐全,后面的拼凑过程才会完整。每一层的图案都会是基于上一层给的原料依照一定的权重拼凑出来的结果,而这些权重也正是深度学习中必须使用梯度下降求得的未知数,这也是为什么神经网络的单元越多,就越容易拟合复杂的内容,而最最重要的就是要有一个参数量大的起头。

使用卷积的另一大好处就是其 「权值共享」,一个小小的卷积核就可以提取整幅图画,只要卷积核数量够多,那么提取特征图像的效果就能够非常好,同时减少了指数量级的参数量,实为一石二鸟之举。

 

Hands on Modification 手动调整

思考部分理解透彻后,接下来就是把思考付诸实践并观察观念对结果的提升有多大的效用,执行层面分为四个部分,分别对应到上面三个观点,另外再多加一个我们对缓存的处理。

Important Dataset

在上面的代码设定中,每次启动一次 optimize() 函数的时候才会重新创造出一批增强后的数据,如果只呼叫一次函数的话,那么事实上在训练的过程中等于没有数据增强的效果,因为一张图片还是被训练了只有一次,即便我们看来是增强了,但是从计算机的角度来说,他经历过的该图片次数只有一次。

就结果而言,如果只有呼叫一次函数,那么不论怎么训练,结果就是在正确率 60% 的位置饱和,如果添加了多个维度的同一数据,那么正确率可以直接提升 15% 以上到达更为令人满意的结果: 75~80% 正确率,结果分别如下:

  • 只有呼叫一次函数的训练结果,训练次数为 20k 次循环:
  • 呼叫了五次同一函数的训练结果,训练次数分别为 32k 次循环:

数据增强的手法对计算机抽象化的过程有着至关重要的影响,提升正确率的同时还可以增加泛化能力,使用两个正确率测试方法,就可以非常清楚的看出差异,两个方法分别是从训练集中割出一小块不参与训练但经过数据处理的数据,和原始测试集数据,结果训练集割出来的部分正确率还比测试集低了约 8% 。

Structure 结构

设计 CNN 架构的时候需要注意的是其卷积核的感受野大小,一个感受野越大同时又没有太多参数的卷积核,越是我们追求的状态,因此这边使用了四层都是 3x3 大小的卷积核,使其达到两层 5x5 大小卷积核的感受野,同时可以缩减参数量,而每两层给一个最大池化处理,让参数总量逐渐缩减,向标签数靠近。

最后使用全连接层,把所有的参数都接上一个点计算每个点的权重后,逐渐舍弃到最后的 10 个类中。同上面原理所述,调整越靠前层的神经网络参数,越容易导致最终结果的偏差,偏差范围大到 15% ,而越后面的神经网络就只有 1~3% 的浮动影响力。

相较于后来的结构设计,原始设计在最易开始处理不好图形的最底层概念,也就是特征提取的不够系统化,不足以支撑后面在进一步抽象化的过程中所需的元素,因此分类的正确率低落。

Hyperparameters 超参数

虽然这只是一个简单的神经网络,但是参数量的排列组合已经是一个不可估量的数字,下面是超参数的总共位置:

  • 卷积核的权重,偏值
  • 全连接层的权重,偏值
  • 归一化中的权重,偏值
  • 卷积核的大小,步长,个数
  • 最大池化的大小,步长
  • 卷积层的层数
  • 全连接层的层数
  • 学习效率
  • 训练次数
  • 每个簇的大小

其中敏感程度最高的所属归一化的参数了,只要调整一点点大小,很可能就直接让结果飞出不存在低谷的方程式地形中去,这使得结果不论多少训练回合都是徒劳无功。最理想的初始化就是设定尽可能接近 0 和 1 ,至少需要在小数点后两位开始偏差才不会有不可转还的影响。

卷积层和全连接层的参数初始化和结果的关系则沿着两个维度变化,第一是只要添加上归一化层,则他们的初始化值可以随便一点没有影响,反正分散大一点的参数也会被后来归一化的步骤抓回一定的合理范围中。第二是随着层数的加深,该层的初始化函数影响也会变得越来越没有地位可言,是数学上的合理推演结果。

池化的大小和使用时机则需要配合总参数的缩放而定,在还没有用卷积核让图像特征足够明显和丰富之前如果就直接使用池化一刀砍落一大部分的参数,肯定是不会有太理想的预期结果。

每个算法框架在设定好的那一刻都会有一个我们未知的损失函数曲线,如果曲线陡峭,表示其训练回合是非常有效率的,如果曲线像是一个平滑的下坡,则表示必须慎重的重新修改框架里面的环节。当然也有可能是在训练过程中遇到了一个非常平坦的地段把参数困死在当下,那又将是另一个故事的开端。

RAM Management 缓存管控

每次 python 在赋值给一个变量名时,都是对缓存的一次占用,平时我们写代码的时候看似缓存用尽的问题不存在,那是因为占用量如同湖泊里面的一碗水是可以忽视的资源,然而到了大数据的运算工作上时,那就是一个非常需要重视的环节,因为一旦赋值一个变量,那么占用的缓存可能就是几百 MB 的量。

这里使用的数据集是 cifar10 ,从官网上面下载下来的样貌就是分做不同的簇储存,目的就是为了规避缓存用完的风险和窘境。不过由于这个数据集还小,我们尚且可以一次全部导入进行运算,下面章节会继续介绍如何使用 Tensorflow 内置更为优雅的数据导入方式。

缓存不够时,第一是把被赋值的变量名释放,和尽可能的把必要和不必要的数据依次导入模型中,减少过载的肯能性。第二是调整算法模型的结构,降低参数的蕴含量进而空出位置给更需要空间的数据。

 

Train again

经过了上面内容对知识的整理后,接下来开始第二次训练,在优化函数 optimize 中添加了是否分割验证集的功能,并让正确率函数 acc 有监视验证集和测试集两个不同数据来源的准确性。

由于多次测试的结果显示只要训练次数达到 8000 后,整个正确率就会停滞在类似 saddle point 的 "平原" 上无法进一步前进,因此这里使用多个不同的随机图像处理数据集来让变量产生改变,试图达到跳出平原地带的效果,让准确率进一步提升,而事实证明是有效的,如下代码:

optimize(6000, batch_size=64, pp=True, val=False)
acc(dataset='test')
optimize(4000, batch_size=64, pp=False, val=False)
optimize(8000, batch_size=64, pp=True, val=False)
acc(dataset='test')
optimize(8000, batch_size=64, pp=False, val=False)
optimize(10000, batch_size=64, pp=True, val=False)
acc(dataset='test')

p.s. 由于个人电脑效能有限,运行训练过程都是在云端完成,截图如上面 78.29% 的正确率。

 

Results Observation 观察结果

经过一系列的计算后,产生了一个正确率的结果背后却是一个类似黑箱的操作,在正确率不断上升的同时,我们更希望能够有一个好的方法来观察这些参数不断迭代之间的变化过程,观察的目标如下陈列:

  1. 权重值的切片陈列
  2. 经过卷积运算后的图像数据
  3. 错误预判的数据陈列

经过对每一层的观察,我们就可以了解神经网络在千万次计算中变化的方向和规律性,进而有机会调整框架的结构,推高准确率的结果。

1. 权重值切片

下面函数设定要显示多少个切片数量,是第几层神经的卷积核,并且是该卷积核的第几层要被展示出来。

def plot_conv_filter(filter, floor=0, size=[3, 3]):
    # The filter is still a tensor and it has to be executed so that
    # it would become a certain value.
    f = sess.run(filter)
    
    # To set up a ceil of matrixes which are about to be drawn,
    # find the value first
    f_min = np.min(f)
    f_max = np.max(f)
    abs_max = max(abs(f_min), abs(f_max))
    print("min: {:.3f}, max: {:.3f}".format(f_min, f_max))
    
    # To find out how many weight cubes do we have and we want to 
    # randomly pick up the weight cube without repeated
    f_num = f.shape[-1]
    num = np.arange(f_num)
    np.random.shuffle(num)
    
    fig, axes = plt.subplots(size[0], size[1])
    
    for i, axis in enumerate(axes.flat):        
        # A filter is a stack of 3D tensors, if we want to plot them,
        # we can only plot one layer of the filter cube composed of numbers
        img = f[:, :, floor, num[i]]
        
        axis.imshow(img, vmin=-abs_max, vmax=abs_max, 
                    interpolation='nearest', cmap='seismic')
        
        # To remove the tick of x and y axis
        axis.set_xticks([])
        axis.set_yticks([])
        
    plt.show()
plot_conv_filter(filter1, floor=2, size=[8, 8])
min: -0.434, max: 0.463

 

plot_conv_filter(filter3, floor=2, size=[6, 6])
min: -0.465, max: 0.470

plot_conv_filter(filter5, floor=2, size=[8, 8])
min: -0.465, max: 0.473

2. 卷积运算后的图像数据

在此函数定义时,我们考虑的并非图片本身的样式,因此只选择一张图片做展示,陈列出来的是该图片经过多个不同的卷积核扫描出来的结果陈列:

def plot_conv_layer(layer, floor=0, size=[3, 3]):
    # The layer is still a tensor before execution
    # So run it first and a feed_dict is necessary
    p = sess.run(layer, feed_dict={Input: two_img})
    
    p_min = np.min(p)
    p_max = np.max(p)
    
    # To find out how many channels do we have in the processed image,
    # we find the shape and pick some of the channels randomly without repeated
    p_num = p.shape[-1]
    num = np.arange(p_num)
    np.random.shuffle(num)
    
    fig, axes = plt.subplots(size[0], size[1])
    title = 'The pic class: {}'.format(cifar.get_class_names[two_lab[1]])
    fig.suptitle(title)
    
    for i, axis in enumerate(axes.flat):
        img = p[1, :, :, num[i]]
        
        axis.imshow(img, vmin=p_min, vmax=p_max,
                    interpolation='nearest')#, cmap='binary')
        
        # To clean up the ticks on x and y axis
        axis.set_xticks([])
        axis.set_yticks([])
    
    plt.show()
plot_conv_layer(layer1, floor=0, size=[3, 4])

plot_conv_layer(layer3, floor=0, size=[3, 4])

plot_conv_layer(layer5, floor=0, size=[3, 4])

3. 错误预判的数据陈列

最后一个函数则是定义经过算法计算后,错误判断的函数结果,可选择错误判断的无题种类做针对性的观察:

def wrong_pred_images(size=[3, 3], lab_class=None):
    img_test = cifar.img_test
    lab_test = cifar.lab_test
    format_img = cifar.format_images(img_test)

    test_dict = {
        Input: format_img,
        Label_oh: one_hot(lab_test, class_num=10),
        Label: lab_test
    }
    
    correct_pred, pred_lab = sess.run([t_or_f, p_lab], feed_dict=test_dict)
    
    wrong_pred = (correct_pred == False)
    
    if lab_class is not None:
        try:
            class_num = cifar.get_class_names.index(lab_class)
            wrong_lab = lab_test[wrong_pred]
            # print(wrong_lab, class_num)
            # Mind that np.where return a "tuple" that should be indexing.
            wrong_pred = np.where(wrong_lab==class_num)[0]
        except:
            print('This name is not one of the class.')
    
    plot_images(format_img[wrong_pred], lab_test[wrong_pred], 
                cifar.get_class_names, size=size, 
                pred_labels=pred_lab[wrong_pred], random=True, smooth=True)
wrong_pred_images(size=[3, 4], lab_class='automobile')

实际操作过上面所有的步骤虽然需要很多时间,但是确实是一个非常大的挑战和非常好的实践经验,后面章节还有许多方法可以提升神经网络预测的准确率,稍后续集。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值