Tensorflow_05_从头构建 CNN 神经网络框架 - part 2: 框架主体
Brief 概述
上一个环节的内容呈现中,我们主要是在做框架搭建前的准备工作,把手边的数据集与即将要使用上的函数尽可能交代清楚,而这节主要的内容是来探讨建构神经网络的过程与步骤,并尽可能的使用函数预留接口的形式把重复书写的代码整合得整洁且易懂,就如同 keras 的模块包一般,建构神经网络的过程好比堆积木,一层网络一行代码,只要能够设计好函数的长相,就可以达到与 keras 模块类似的效果,非常简便的同时还不失设计上的弹性调整空间。
CNN Structure 卷积神经网络架构
接下来定义以 CNN 为单元函数,函数内部包含了几个元素,如下陈列:
- kernal 卷积核: 是一个用来提取图像特征的张量,通常卷积核的个数会随着层数的增多而增加,原因是每经过一层卷积核的扫描,单位面积就会逐渐缩减,如果要保有与输入数据同样多的参数信息下,只能把层数加深来维持平衡。
- bias 偏值: 同样每次做完卷积后,每个值可能会有一个平移过程用来更好的拟合结果,但是在做反向传播的时候,这个值会在一次微分的过程中做为常数项被砍掉,因此在建构模型的时候只需要一个常数的起始值即可。
- conv2d: 接下来使用 tf 框架提供的卷积方法,一般卷积核设定会在 3x3 ~ 11x11 之间依照不同的目的和测试结果的好坏决定,卷积出来后的结果就会是一个较为纤瘦但是细长的数据形态,提取了原数据每个部分的特征而组成的一个张量。
- max pooling 最大池化: 把特征值无条件直接缩减,取最大值出来,也有人使用平均的方式,不过目前流行的框架看来,取最大值的方式会产生比较好的结果,常用的池化大小有 2x2(步长为 2)或是 3x3(步长为 2)的参数。
- activation 激励函数: 接下来把经过上述 1~4 步骤处理的数据放入激励函数中,通常使用的是 relu(如果小于 0 则为 0,如果大于 0 则维持原本数据大小)
p.s. 步骤 4 与 5 的先后顺序都有人用,不过我认为一个值被放入了激励函数后,得出来的结果与它的邻居被做过同样的运算做比较并不会改变其大小顺序,因此我比较推荐先使用最大池化,把运算负重抽离 75% 后,再做激励函数运算,更能提升效率。
Normalization 归一化
而在上述五点步骤中,还需要添加此最为重要的一点,它不仅可以在训练过程中减少损失函数的抖动现象,让过程更为平滑和流畅,甚至可以适时的加速训练的进程因此有必要特别提出来先说明其内部构造,那就是:normalization 归一化,能够达到上述效果的原因就是因其解决了 internal covariance shift 问题,使得前一层和后一层在训练调整参数过程中彼此更能够好的配合对方。
-
Batch Normalization : 此一方法是在 2015 年被发表,用意在于维持数据集彼此之间特征的条件下,帮他们重新 "挪移" 了数据之间的位置,让整体数据分布更向中间靠拢,这么一来就不容易造成提督弥散等深层网络遇到的问题。需要的参数如下:
- 输入数据,即将要被做 BN 的数据集
- 平均值,即将要被 BN 的那些数据依照指定维度算出来的平均值
- 方差,即将要被 BN 的那些数据依照指定维度算出来的方差
- beta,类似神经元的 bias 功能,提供一个截距上的变化
- gama,类似神经元的 weight 功能,提供一个斜率上的变化
- epsilon,是一个 Hyperparameter,是无法被训练的一个部分,初始设 0.001
实现 BN 的代码如下:
# In order to map the 4D tensor dataset into this function, we have to be
# careful for the shape of these variables.
def BN(layer, epsilon):
size = layer.shape[-1]
# beta stands for "offset" term
beta = tf.Variable(tf.constant(0.001, shape=[size], dtype=tf.float32))
# gama stands for "scale" term
gama = tf.Variable(tf.constant(0.98, shape=[size], dtype=tf.float32))
mean, var = tf.nn.moments(layer, axes=[0], keep_dims=False)
layer = tf.nn.batch_normalization(layer, mean, var, beta, gama, epsilon)
size, beta, gama, mean, var = [None for _ in range(5)]
return layer
添加了 BN 归一化处理后好处非常明显且直接,但是添加此一绝招到神经网络中时还需要了解下面几个注意事项,方可更完好的让归一化的威力发挥出来:
- 增大 learning_rate 学习速率(五倍到三十倍都是可以尝试的范围),增大衰减速度
- 去除 Dropoup 环节,并减轻 L2 正则影响,因为 BN 本身已经有使用正则时的效果
- 原本如果有 LRN 归一化的函数,需要删除,不需要用不同的方法归一化两次
- 减少数据增强的范围和效果,因为加了 BN 后,每个样本被训练的次数相对少的情况下就可以得出高正确率的结果,因此太夸张的增强效果,反而成为了影响判断的阻碍
- 增加样本的随机分布程度,归一化的算法取决于非常良好的随机性,其对结果的影响达到 1% 的准确率
一般的模型里,数据处理的顺序是不变的,变化的只有每一层之间的卷积核大小
,卷积的步长
,核的数量
,最大池化的大小
等,因此这些变化的参数必须做为函数的接口,定义在参数的位置,这么一来就可以非常直观的使用定义好的函数,有条理切有效率的搭建出一个完整的神经网络,同时可以在修改和除错上降低重新梳理逻辑的时间,其过程如下说明。
1. CNN Layer 卷积层
- 设定两个最重要的参数,分别是卷积核与偏值
- 使用 Tensorflow 框架中的 conv2d 方法运算卷积核
- 卷积出来的结果套入上面设定好的 BN 函数做归一化
- 在函数中可以自行设定是否添加池化
- 通过激励函数后回传结果
p.s. 为了在后面好方便观察卷积核的变化,回传值的时候也把卷积核带上
不过根据文献和他人的分享结果,归一化的步骤顺序在最大池化之前与之后的做法都有人做过,归一化在激励函数之前还是之后的顺序问题,则后来被普遍接受放在激励之前了,大家可以根据自己特定模型需求去实现不同的顺序架构,最后验证结果差异。
定义函数的代码如下:
def conv_layer(Input, fs, fn, fsd=[1, 1], pd='SAME', pool=True):
filters = tf.Variable(tf.truncated_normal(shape=[fs[0], fs[1], fs[2], fn],
stddev=0.2, mean=0.0,
dtype=tf.float32))
biases = tf.Variable(tf.constant(0.05, shape=[fn], dtype=tf.float32))
layer = tf.nn.conv2d(Input, filters,
strides=[1, fsd[0], fsd[1], 1],
padding=pd, data_format='NHWC')
layer += biases
layer = BN(layer, 0.001)
if pool:
layer = tf.nn.max_pool(layer, [1, 2, 2, 1],
strides=[1, 2, 2, 1],
padding=pd, data_format='NHWC')
layer = tf.nn.relu(layer)
return layer, filters
点击查看官方网址: https://www.angtk.com
2. Fully Connected Layer 全联接层
经过卷积层的运算结果出来后的数据还是 4D 的数据尺寸,但是全联接层只能是 2D 准备对接分类的类别数,分别对应的是 row: 一个 batch 的数据量; col: 有几个类别,而这也是此函数需要漏出来的接口部位。
为了让整个最后搭建的顺序有强的可读性,全联接层我定义了两个函数,功能分别如下:
- 把输入的 4D tensor 编程一个 2D tensor,转变的方式不需要根据之前的图像原路转回去,原因在于对所有的数据而言,他们都被同样处理,并且像素值的总量并不会变化。
- 接下来被塑身的 2D tensor 进一步要和标签数量拟合,需要使用一个现行回归的全联接层完成,同样设定权重和偏值,最后加上一个激励函数,同样回传全重值为方便观察。
此二函数代码如下:
# layer can only be the value produced by tensorflow or numpy.
def flat_input(layer):
cols = layer.shape[1] * layer.shape[2] * layer.shape[3]
flat_layer = tf.reshape(layer, shape=[-1, int(cols)])
layer = None
return flat_layer
# Fully connected network works the same way as linear regression.
# The reason for adding activation func is to make more non-linearity variety.
def fcn(flat_layer, class_num, activation=True):
rows = flat_layer.shape[1]
weights = tf.Variable(tf.truncated_normal(shape=[int(rows), class_num],
stddev=0.2, mean=0.0,
dtype=tf.float32))
biases = tf.Variable(tf.constant(0.05, shape=[class_num], dtype=tf.float32))
legits = tf.matmul(flat_layer, weights) + biases
legits = BN(legits, 0.001)
if activation:
legits = tf.nn.relu(legits)
return legits, weights
Construct a model 建构模型
事前准备都做好后,接下来就是非常轻松的建构模型步骤,只要使用前面定义好的函数,一排一排的定义下来就可以,同样分为几个步骤,如下陈列:
- 数据导入,使用
tf.placeholder()
来制造一个数据导入的入口,让数据流图能够反复被使用 - 建构卷积神经网络的框架,层数因建构几次而决定
- 拉直从卷积层出来的输出数据
- 开始全联接层架构,把张量的行数和最后面标签个数对上
- 计算损失函数值
- 使用梯度下降方法最小化损失函数
- 计算优化后的预测结果准确度
图像采用随机抓取的方式来组成一个簇,这样好处是我们可以直接忽略掉分整齐批量抓取簇的过程,并且不用考虑 epoch 的次数,直接让随机抓取的量和数据集的总量相除就能够与设定 epoch 次数同样的效果。
代码如下,供参考:
H, W, C = cifar.image_size, cifar.image_size, cifar.image_channels
# ---------- Step No.1 ---------- #
Input = tf.placeholder(shape=[None, H, W, C], dtype=tf.float32)
Label_oh = tf.placeholder(shape=[None, 10], dtype=tf.float32)
Label = tf.placeholder(shape=[None], dtype=tf.int32)
# ---------- Step No.2 ---------- #
layer1, filter1 = conv_layer(Input, [3, 3, 3], 64, fsd=[1, 1], pd='VALID', pool=False)
layer2, filter2 = conv_layer(layer1, [3, 3, 64], 64, fsd=[1, 1], pd='VALID', pool=False)
layer3, filter3 = conv_layer(layer2, [3, 3, 64], 48, fsd=[1, 1], pd='VALID', pool=False)
layer4, filter4 = conv_layer(layer3, [3, 3, 48], 64, fsd=[1, 1], pd='VALID', pool=True)
layer5, filter5 = conv_layer(layer4, [3, 3, 64], 64, fsd=[1, 1], pd='VALID', pool=False)
layer6, filter6 = conv_layer(layer5, [3, 3, 64], 128, fsd=[1, 1], pd='VALID', pool=True)
layer7, filter7 = conv_layer(layer6, [3, 3, 128], 64, fsd=[1, 1], pd='VALID', pool=True)
# ---------- Step No.3 ---------- #
flat_layer = flat_input(layer7)
# ---------- Step No.4 ---------- #
legits1, weights1 = fcn(flat_layer, 576, activation=True)
legits2, weights2 = fcn(legits1, 256, activation=False)
legits3, weights3 = fcn(legits2, 10, activation=False)
p_lab_soh = tf.nn.softmax(legits3)
p_lab = tf.argmax(p_lab_soh, axis=1, output_type=tf.int32)
# ---------- Step No.5 ---------- #
cross_entropy = tf.nn.softmax_cross_entropy_with_logits_v2(logits=legits3,
labels=Label_oh)
loss = tf.reduce_mean(cross_entropy)
# ---------- Step No.6 ---------- #
optimizer = tf.train.AdamOptimizer(learning_rate=0.01).minimize(loss)
# ---------- Step No.7 ---------- #
t_or_f = tf.equal(p_lab, Label)
accuracy = tf.reduce_mean(tf.cast(t_or_f, dtype=tf.float32))
sess = tf.Session()
sess.run(tf.global_variables_initializer())
# saver = tf.train.Saver(max_to_keep=2)
Training part 训练部分
和上一个章节的函数定义方式类似,定一个迭代函数,把上面数据流图中的 "optimizer" 放入其中开始循环,并喂入他所需要的两个输入数据,从刚刚预留的入口 tf.placeholder()
,使用字典的方式给入,如下代码:
# This module is used to show the percentage bar and the time we left.
from tqdm import tqdm # It's necessary when training!
@time_counter
def optimize(iteration, batch_size=64, pp=False, val=False):
global lab_train
lab_train = cifar.lab_train
format_img = cifar.format_images(cifar.img_train)
global two_img, two_lab
rd = np.random.randint(9000)
two_img = format_img[[rd, rd+1]]
two_lab = lab_train[[rd, rd+1]]
if pp:
format_img = tf.map_fn(lambda img: image_preprocessing(
img, crop=[24, 24]), format_img)
# To make the result into np.array again, it has to be run.
format_img = sess.run(format_img)
if val:
global format_img_val
format_img, format_img_val = cifar.set_validation(format_img)
global lab_train_val
lab_train, lab_train_val = cifar.set_validation(lab_train)
for i in tqdm(range(iteration)):
random = np.random.randint(0, len(lab_train), size=batch_size)
train_dict = {
Input: format_img[random],
Label_oh: one_hot(lab_train[random], class_num=10),
}
sess.run(optimizer, feed_dict=train_dict)
# if i % 500 == 0:
# saver.save(sess, './checkpoints/CIFAR10CNN.ckpt', global_step=i)
# To erase the buffering variables, saving RAM space
lab_train, format_img, train_dict = [None for _ in range(3)]
# Mind that when we are testing the accuracy of a model, the input data
# should be changed as a testing set.
def acc(dataset='test'):
if dataset == 'test':
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
}
elif dataset == 'validation':
global format_img_val
global lab_train_val
test_dict = {
Input: format_img_val,
Label_oh: one_hot(lab_train_val, class_num=10),
Label: lab_train_val
}
else:
print('Only "test" and "validation" can be put after dataset argument.')
try:
Acc = sess.run(accuracy, feed_dict=test_dict)
print('Accuracy on {0} Set: {1:.2%}'.format(dataset, Acc))
except:
print('The validation set is not created yet.')
# To erase the buffering variables, saving RAM space
img_test, lab_test, lab_train_val,\
format_img_val, format_img, test_dict = [None for _ in range(6)]
有一点非常需要注意的是,每启动一次 optimize
函数,图像预处理的步骤也会跟着重新启动,因此会新制造出一整批微调的训练集供这个批次的训练使用。启动训练代码如下:
optimize(4000, batch_size=192, pp=False, val=False)
acc(dataset='test')
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%
经过 CPU 缓慢处理出来的四千次迭代中,却给出了一个不理想的数据,下一个章节将继续根据此模型更正,达到更优的结果!
上一篇: HDU 3533 Escape
下一篇: LeetCode 买卖股票的最佳时机II