该文章是对TF中文手册的卷积神经网络和英文手册Convolutional Neural Networks部分所包含程序的解读,旨在展示CNN处理规模比较大的彩色图片数据集(分类问题)的完整程序模型,训练中使用交叉熵损失的同时也使用了L2范式的稀疏化约束,例子修改后就可以训练自己的数据。这篇博客按照程序工作的顺序,从cifar10_train.py开始,依次解读途径的每个重要函数,具体细节还需要自己阅读源程序。注意:运行程序前请先减小训练次数,否则训练时间太长了!!!
首先说一下例子的相关内容。CIFAR-10的数据是这样的:有10分类,每个分类6000个32*32的彩色图片,5000个用于训练,1000个用于测试,大概样子如下:
1. 例子要点
模型是一个多层架构,由卷积层和非线性层(nonlinearities)交替多次排列后构成。这些层最终通过全连通层对接到softmax分类器上。这一模型除了最顶部的几层外,基本跟Alex Krizhevsky提出的模型一致(Learning Multiple Layers of Features from Tiny Images)。在一个GPU上经过几个小时(注意时间很长!)的训练后,该模型达到了最高86%的精度。细节请查看下面的描述以及代码。模型中包含了1,068,298个学习参数,分类一副图像需要大概19.5M个乘加操作。代码的组织形式:
读入的图片经过了多种处理,都是TF自带的内部函数,另外一系列随机变换人为增加数据集的大小:
- 图片会被统一裁剪到24x24像素大小,裁剪中央区域用于评估或随机裁剪用于训练;
- 图片会进行近似的白化处理,使得模型对图片的动态范围变化不敏感。
这些基础的图像处理流程被分配在
16个线程中处理。
CNN网络的不同层的功能:
训练方法与损失的定义:
训练一个可进行N维分类的网络的常用方法是使用多项式逻辑回归(softmax 回归),Softmax 回归在网络的输出层上附加了一个softmax nonlinearity,并且计算归一化的预测值和label的1-hot encoding的交叉熵。在正则化过程中,对所有学习变量应用权重衰减损失(使用了L2范式,强调模型的参数的稀疏性),求交叉熵损失和所有权重衰减项的和,loss()函数的返回值就是这个值。
2. 数据的读取
读取的数据格式
images: Images. 4D tensor of [batch_size, IMAGE_SIZE, IMAGE_SIZE, 3] size.
labels: Labels. 1D tensor of [batch_size] size
有16个线程一直在按照指定的batch_size读取数据,放置到队列中,训练程序需要数据的时候直接从队列中获取一个batch即可。
读取:
filename_queue
=
tf
.
train
.
string_input_producer
(
filenames
)
获取文件名称队列,使用
read_cifar10()
这个自定义函数从二进制数据中获取一个样本的信息结构体(大小、数据、标签),然后使用
tf
.
cast
(
read_input
.
uint8image
,
tf
.
float32
)
把uint8变换成float32类型。
切割:
比较底层的就是:
def
read_cifar10
(
filename_queue
):,
该函数从二进制数据中读取数据并规整,每条样本都是先标签后数据,CIFAR10是一个字节标签,CIFAR100是2字节,使用切片函数
tf
.
slice
(
record_bytes
,
[
0
],
[
label_bytes
]),
tf
.
int32
)
,从输入中0开始的地方切label_bytes个字节。然后切取对应数据:
depth_major
=
tf
.
reshape
(
tf
.
slice
(
record_bytes
,
[
label_bytes
],
[
image_bytes
]),
[
result
.
depth
,
result
.
height
,
result
.
width
])
从第三个维度(深度通道数)开始规整变形。
处理原始图片:
初步获取数据后就需要变形成tensor了,1D变换成3D
tf
.
random_crop
(
reshaped_image
,
[
height
,
width
,
3
])
变换之后就是人工生成各种数据:
tf
.
image
.
random_flip_left_right
(
distorted_image
)#从左到右随机
tf.image.random_brightness(distorted_image,
max_delta
=
63
)#随机亮度变换
tf.image.random_contrast(distorted_image,
lower
=
0.2
,
upper
=
1.8
)#随机对比度变换
float_image
=
tf
.
image
.
per_image_whitening
(
distorted_image
)#最后是图像的白化:均值与方差的均衡,降低图像明暗、光照差异引起的影响
注意上述这些操作只是针对单幅图像的,至于多线程处理图片缓冲区队列,保证训练程序随时可读取batch_size的数据,是通过
tf
.
train
.
shuffle_batch
中设定队列大小、缓冲区大小,直接就保证整理好一个数据集合的队列了,这是TF内部自带的。
3. 建立网络
首先注明的是:多个GPU需要
tf.get_variable()
用于分享数据,而单个GPU只需要
tf.Variable()
。
参数设置函数:
_variable_with_weight_decay
(
name
,
shape
,
stddev
,
wd
)
功能:输入名称、形状、偏差和均值就可以定义一个参数tensor,生成数据主要分为两步,
一个是正常建立参数,另一个是添加L2范式强调稀疏化。
_variable_on_cpu中的
tf
.
get_variable
(
name
,
shape
,
initializer
=
initializer
,
dtype
=
dtype
)
,
是正常的参数建立
weight_decay
=
tf
.
mul
(
tf
.
nn
.
l2_loss
(
var
),
wd
,
name
=
'weight_loss'
)
是
增加L2范式稀疏化,其中L2范式定义为:
output = sum(t ** 2) / 2
,然后乘以一个衰减系数wd做为一个训练指标:这个值应该尽量小,以保证稀疏性。
这里使用了
tf
.
add_to_collection
(
'losses'
,
weight_decay
)
,把所有的系数作为以
losses
为标签进行收集,对应的还有下面的交叉熵。该模型通过控制wd就可以强调稀疏性在训练中的比重(wd=0就是不强调稀疏化),这个例子中只有全连接层对稀疏性有要求。
第一层是
conv1
,视野是5*5,每个图像从3通道(rgb)到64通道
shape
=[
5
,
5
,
3
,
64
]
,卷积滑动
tf
.
nn
.
conv2d
(
images
,
kernel
,
[
1
,
1
,
1
,
1
],
padding
=
'SAME'
)
,然后与偏置相加后是
relu
函数输出,对输出也有个summary用于查看稀疏性:
tf
.
scalar_summary
(
tensor_name
+
'/sparsity'
,
tf
.
nn
.
zero_fraction
(
x
))
,统计0的比例反应稀疏性。
tf
.
histogram_summary
(
tensor_name
+
'/activations'
,
x
)
,输出数值的分布直接反应神经元的活跃性,如果全是很小的值说明不活跃。
第一层后紧接着是pooling层
pool1
:
tf
.
nn
.
max_pool
(
conv1
,
ksize
=[
1
,
3
,
3
,
1
],
strides
=[
1
,
2
,
2
,
1
],
padding
=
'SAME'
,
name
=
'pool1'
)
模板是3*3,移动步长2*2,有重叠的pooling(pool有各种不同的,也有3D的)。
第一个pooling层之后有个局部响应归一化
norm1
(
tf.nn.local_response_normalization,简写为
tf.nn.lrn
),这是一篇论文里的理论(ImageNet Classification with Deep Convolutional Neural Networks):总之就是把输出归一化了一下,对训练有利。TF文档的定义是:
第一梯队之后又是个卷积层
conv2
,与第一个卷积层类似只是64通道到64通道,偏置初始是0.1,没有变化,但是之后就是归一化层
norm2
,然后才是结构一样的pooling层
pool2
。
两个标准的卷积层后是
全连接层:
local3层首先是确定2次conv、pool后的每个样本展开的维度(
注意:这里不需要知道是怎么展开的,因为到这里以后提取的都是很高维度的特征了,保证程序上连接的正确即可),展开方法:
reshape
=
tf
.
reshape
(
pool2
,
[
FLAGS
.
batch_size
,
-
1
])
,具体获取每个样本展开的维度
dim
=
reshape
.
get_shape
()[
1
].
value
,然后就是常规的定义全连接层
weights
=
_variable_with_weight_decay
(
'weights'
,
shape
=[
dim
,
384
],
stddev
=
0.04
,
wd=0.004
)
,从dim映射到384个神经元:
local3
=
tf
.
nn
.
relu
(
tf
.
matmul
(
reshape
,
weights
)
+
biases
,
name
=
scope
.
name
)
。
local4与
local3
相似只是从384全连接到192(192、384这些数字与GPU的架构有关),全连接层的wd是0.004,略微强调了一下稀疏性。
最后一个层名字是
softmax_linear
,但是并没有使用softmax:
s
oftmax_linear
=
tf
.
add
(
tf
.
matmul
(
local4
,
weights
),
biases
,
name
=
scope
.
name
)
4. 损失函数
总函数:
loss
=
cifar10
.
loss
(
logits
,
labels
)
具体使用
cross_entropy
=
tf
.
nn
.
sparse_softmax_cross_entropy_with_logits
(
logits
,
labels
,
name
=
'cross_entropy_per_example'
)
然后计算一个batch运算后的平均值:
tf
.
reduce_mean
(
cross_entropy
,
name
=
'cross_entropy'
)
与上面的收集器对应,
tf
.
add_to_collection
(
'losses'
,
cross_entropy_mean
)
同样收集进
losses
中,这样就已经包含了所有batch的交叉熵均值和所有系数的L2范式。最后使用了
tf
.
add_n
(
tf
.
get_collection
(
'losses'
),
name
=
'total_loss'
)
,这是面向多GPU的,因为一个GPU就一个
losses
,不需要add_n总损失了。
5.训练
学习率更新:
首先是根据当前的训练步数、衰减速度、之前的学习速率确定新的学习速率:
# Decay the learning rate exponentially based on the number of steps.
lr
=
tf
.
train
.
exponential_decay
(
INITIAL_LEARNING_RATE
,
global_step
,
decay_steps
,
LEARNING_RATE_DECAY_FACTOR
,
staircase
=
True
)
这个函数的解释:如果staircase是true,就取个整数。TF文档:
均值线(moving average):
等价于股票中常提到的“均值线”
:
tf
.
train
.
ExponentialMovingAverage
(
0.9
,
name
=
'avg'
)
,这个只是观察,因为按照经验:
“Some training algorithms, such as GradientDescent and Momentum often benefit from maintaining a moving average of variables during optimization. Using the moving averages for evaluations often improve results significantly.”
计算梯度:
多显卡就是麻烦:为了保障均值线观察准确,需要制定同步点:
# Compute gradients.
with
tf
.
control_dependencies
([
loss_averages_op
]):
opt
=
tf
.
train
.
GradientDescentOptimizer
(
lr
)
grads
=
opt
.
compute_gradients
(
total_loss
)
函数
tf
.
control_dependencies
只是告诉计算单元梯度计算要在统计之后,
梯度更新参数:
apply_gradient_op
=
opt
.
apply_gradients
(
grads
,
global_step
=
global_step
)
计算完了,就反向传播一次,更新被训练的参数
各种summary和句柄:
summary就不一一说明了,和之前的程序一样,句柄如下:
with
tf
.
control_dependencies
([
apply_gradient_op
,
variables_averages_op
]):
train_op
=
tf
.
no_op
(
name
=
'train'
)
最后是个nothing操作,只是返回train_op作为控制界面的句柄。
具体的训练:
# Create a saver.
saver
=
tf
.
train
.
Saver
(
tf
.
all_variables
())
# Build the summary operation based on the TF collection of Summaries.
summary_op
=
tf
.
merge_all_summaries
()
# Build an initialization operation to run below.
init
=
tf
.
initialize_all_variables
()
# Start running operations on the Graph.
sess
=
tf
.
Session
(
config
=
tf
.
ConfigProto
(
log_device_placement
=
FLAGS
.
log_device_placement
))
sess
.
run
(
init
)
启动之前建立的图片规整线程:
# Start the queue runners.
tf
.
train
.
start_queue_runners
(
sess
=
sess
)
显示和保存训练信息:
每隔10步输出:
print
(
format_str
%
(
datetime
.
now
(),
step
,
loss_value
,
examples_per_sec
,
sec_per_batch
))
每隔100步保存sumary一次
每隔1000步保存断点一次使用
saver
=
tf
.
train
.
Saver
(
tf
.
all_variables
())
保存
6. 验证模型
传入验证函数的参数:
eval_once
(
saver
,
summary_writer
,
top_k_op
,
summary_op
)
saver是用读取moving_average的
summary_writer和summary_op是保存记录的
top_k_op传入了模型和验证模型
读取检查点:
ckpt
=
tf
.
train
.
get_checkpoint_state
(
FLAGS
.
checkpoint_dir
)
从检查点恢复图和参数:
saver
.
restore
(
sess
,
ckpt
.
model_checkpoint_path
)
然后就是启动图像读取程序,组成队列,最后是使用数据验证正确率。