這篇筆記主要記錄一下學(xué)習(xí)tensorflow cifar-10圖像分類的示例代碼。
數(shù)據(jù)介紹
Cifar-10是由 Hinton 的兩個(gè)大弟子 Alex Krizhevsky、Ilya Sutskever 收集的一個(gè)用于普適物體識(shí)別的數(shù)據(jù)集。Cifar 是加拿大政府牽頭投資的一個(gè)先進(jìn)科學(xué)項(xiàng)目研究所。Hinton、Bengio和他的學(xué)生在2004年拿到了 Cifar 投資的少量資金,建立了神經(jīng)計(jì)算和自適應(yīng)感知項(xiàng)目。這個(gè)項(xiàng)目結(jié)集了不少計(jì)算機(jī)科學(xué)家、生物學(xué)家、電氣工程師、神經(jīng)科學(xué)家、物理學(xué)家、心理學(xué)家,加速推動(dòng)了 Deep Learning 的進(jìn)程。從這個(gè)陣容來看,DL 已經(jīng)和 ML 系的數(shù)據(jù)挖掘分的很遠(yuǎn)了。Deep Learning 強(qiáng)調(diào)的是自適應(yīng)感知和人工智能,是計(jì)算機(jī)與神經(jīng)科學(xué)交叉;Data Mining 強(qiáng)調(diào)的是高速、大數(shù)據(jù)、統(tǒng)計(jì)數(shù)學(xué)分析,是計(jì)算機(jī)和數(shù)學(xué)的交叉。
cifar-10分類數(shù)據(jù)集為60000張32 * 32的彩色圖片,總共有10個(gè)類別,其中50000張訓(xùn)練集,10000張測試集,官網(wǎng)地址http://www.cs.toronto.edu/~kriz/cifar.html,提供數(shù)據(jù)集下載 數(shù)據(jù)集中圖片諸如以下

這里重點(diǎn)說明數(shù)據(jù)集的存儲(chǔ)格式。下載解壓后為5個(gè)batch(圖中data_batch_1,2,3,4,5)的訓(xùn)練集,1個(gè)batch(圖中test_batch)的測試集。每個(gè)batch中10000張圖片。

每個(gè)文件中數(shù)據(jù)存儲(chǔ)格式為dict字典,鍵值為b’data’的為圖片數(shù)據(jù),是一個(gè)10000 * 3072(32 * 32 * 3)的numpy向量,10000表示圖片張數(shù),3072中前1024個(gè)表示Red通道數(shù)據(jù),中間1024個(gè)表示Green通道數(shù)據(jù),最后1024個(gè)表示Blue通道數(shù)據(jù),數(shù)據(jù)范圍是0-255,表示像素點(diǎn)灰度。鍵值為b’labels’表示對(duì)應(yīng)的標(biāo)簽,是一個(gè)長度為10000的list,數(shù)據(jù)范圍是0-9,分別表示10個(gè)類別。
另外要說明的是卷積濾波器卷積的是32 * 32 * 3格式的數(shù)據(jù),32 * 32代表圖片一個(gè)通道格式,3表示RGB 3個(gè)通道,然而依據(jù)其數(shù)據(jù)表示格式,在 reshape 3072維度的向量的時(shí)候必須首先reshape成3 * 32 * 32格式的向量,否則會(huì)破壞圖片原本格式,怎么辦呢,轉(zhuǎn)置!類似于矩陣的轉(zhuǎn)置,三維向量也有轉(zhuǎn)置,tensorflow提供transpose方法對(duì)三維向量作轉(zhuǎn)置。
網(wǎng)絡(luò)結(jié)構(gòu)
先介紹一下示例代碼中的文件構(gòu)成:
本文將使用的代碼結(jié)果和網(wǎng)絡(luò)結(jié)構(gòu):
| 文件 | 說明 |
|---|---|
| cifar10_input.py | 讀取本地CIFAR-10的二進(jìn)制文件格式的內(nèi)容 |
| cifar10.py | 建立CIFAR-10的模型 |
| cifar10_train.py | 在CPU或GPU上訓(xùn)練CIFAR-10的模型 |
| cifar10_multi_gpu_train.py | 在多GPU上訓(xùn)練CIFAR-10的模型。 |
| cifar10_eval.py | 評(píng)估CIFAR-10模型的預(yù)測性能 |
示例中的網(wǎng)絡(luò)結(jié)構(gòu)比較簡單,

模型輸入
輸入模型是通過 cifar10_input.inputs() 和 cifar10_input.distorted_inputs() 函數(shù)建立起來的,這2個(gè)函數(shù)會(huì)從 CIFAR-10 二進(jìn)制文件中讀取圖片文件,具體實(shí)現(xiàn)定義在 cifar10_input.py 中,使用的數(shù)據(jù)為 CIFAR-10 page 下的162M 的二進(jìn)制文件,由于每個(gè)圖片的存儲(chǔ)字節(jié)數(shù)是固定的,因此可以使用 tf.FixedLengthRecordReader 函數(shù)。
載入圖像數(shù)據(jù)后,通過以下流程進(jìn)行數(shù)據(jù)增廣:
- 統(tǒng)一裁剪到24x24像素大小,裁剪中央?yún)^(qū)域用于評(píng)估或隨機(jī)裁剪用于訓(xùn)練;
- 對(duì)圖像進(jìn)行隨機(jī)的左右翻轉(zhuǎn);
- 隨機(jī)變換圖像的亮度;
- 隨機(jī)變換圖像的對(duì)比度;
- 圖片會(huì)進(jìn)行近似的白化處理。
其中,白化(whitening)處理或者叫標(biāo)準(zhǔn)化(standardization)處理,是對(duì)圖片數(shù)據(jù)減去均值,除以方差,保證數(shù)據(jù)零均值,方差為1,如此降低輸入圖像的冗余性,盡量去除輸入特征間的相關(guān)性,使得網(wǎng)絡(luò)對(duì)圖片的動(dòng)態(tài)范圍變化不敏感。
# Image processing for training the network. Note the many random
# distortions applied to the image.
# Randomly crop a [height, width] section of the image.
distorted_image = tf.random_crop(reshaped_image, [height, width, 3])
# Randomly flip the image horizontally.
distorted_image = tf.image.random_flip_left_right(distorted_image)
# Because these operations are not commutative, consider randomizing
# the order their operation.
# NOTE: since per_image_standardization zeros the mean and makes
# the stddev unit, this likely has no effect see tensorflow#1458.
distorted_image = tf.image.random_brightness(distorted_image,
max_delta=63)
distorted_image = tf.image.random_contrast(distorted_image,
lower=0.2, upper=1.8)
# Subtract off the mean and divide by the variance of the pixels.
float_image = tf.image.per_image_standardization(distorted_image)
從磁盤上加載圖像并進(jìn)行變換需要花費(fèi)不少的處理時(shí)間。為了避免這些操作減慢訓(xùn)練過程,使用16個(gè)獨(dú)立的線程中并行進(jìn)行這些操作,這16個(gè)線程被連續(xù)的安排在一個(gè) TensorFlow 隊(duì)列中,最后返回預(yù)處理后封裝好的tensor,每次執(zhí)行都會(huì)生成一個(gè) batch_size 數(shù)量的樣本 [images,labels]。測試數(shù)據(jù)使用cifar10_input.inputs() 函數(shù)生成,測試數(shù)據(jù)不需要對(duì)圖片進(jìn)行翻轉(zhuǎn)或修改亮度、對(duì)比度,需要裁剪圖片正中間的24*24大小的區(qū)塊,并進(jìn)行數(shù)據(jù)標(biāo)準(zhǔn)化操作。
def _generate_image_and_label_batch(image, label, min_queue_examples,
batch_size, shuffle):
# Create a queue that shuffles the examples, and then
# read 'batch_size' images + labels from the example queue.
num_preprocess_threads = 16
if shuffle:
images, label_batch = tf.train.shuffle_batch(
[image, label],
batch_size=batch_size,
num_threads=num_preprocess_threads,
capacity=min_queue_examples + 3 * batch_size,
min_after_dequeue=min_queue_examples)
......
# Display the training images in the visualizer.
tf.summary.image('images', images)
return images, tf.reshape(label_batch, [batch_size])
模型構(gòu)建
在建立模型之前,我們構(gòu)造 weight 的構(gòu)造函數(shù) _variable_with_weight_decay(name, shape, stddev, wd),其中 wd 用于向 losses 添加L2正則化,可以防止過擬合,提高泛化能力:
def _variable_with_weight_decay(name, shape, stddev, wd):
dtype = tf.float16 if FLAGS.use_fp16 else tf.float32
var = _variable_on_cpu(
name,
shape,
tf.truncated_normal_initializer(stddev=stddev, dtype=dtype))
if wd is not None:
weight_decay = tf.multiply(tf.nn.l2_loss(var), wd, name='weight_loss')
tf.add_to_collection('losses', weight_decay)
return var
然后我們開始建立網(wǎng)絡(luò),第一層卷積層的 weight 不進(jìn)行 L2正則,因此 kernel(wd) 這一項(xiàng)設(shè)為0,建立值為0的 biases,conv1的結(jié)果由 ReLu 激活,由 _activation_summary() 進(jìn)行匯總;然后建立第一層池化層,最大池化尺寸和步長不一致可以增加數(shù)據(jù)的豐富性;最后建立 LRN 層,LRN層模仿了生物神經(jīng)系統(tǒng)的"側(cè)抑制"機(jī)制,對(duì)局部神經(jīng)元的活動(dòng)創(chuàng)建競爭環(huán)境,使得其中響應(yīng)比較大的值變得相對(duì)更大,并抑制其他反饋較小的神經(jīng)元,增強(qiáng)了模型的泛化能力,LRN 對(duì) Relu 這種沒有上限邊界的激活函數(shù)會(huì)比較有用,因?yàn)樗鼤?huì)從附近的多個(gè)卷積核的響應(yīng)中挑選比較大的反饋,但不適合 sigmoid 這種有固定邊界并且能抑制過大的激活函數(shù)。
# conv1
with tf.variable_scope('conv1') as scope:
kernel = _variable_with_weight_decay('weights',
shape=[5, 5, 3, 64],
stddev=5e-2,
wd=None)
conv = tf.nn.conv2d(images, kernel, [1, 1, 1, 1], padding='SAME')
biases = _variable_on_cpu('biases', [64], tf.constant_initializer(0.0))
pre_activation = tf.nn.bias_add(conv, biases)
conv1 = tf.nn.relu(pre_activation, name=scope.name)
_activation_summary(conv1)
# pool1
pool1 = tf.nn.max_pool(conv1, ksize=[1, 3, 3, 1], strides=[1, 2, 2, 1],
padding='SAME', name='pool1')
# norm1
norm1 = tf.nn.lrn(pool1, 4, bias=1.0, alpha=0.001 / 9.0, beta=0.75,
name='norm1')
第二層卷積層與第一層,除了輸入?yún)?shù)的改變之外,將 biases 值全部初始化為0.1,調(diào)換最大池化和 LRN 層的順序,先進(jìn)行LRN,再使用最大池化層。
# conv2
with tf.variable_scope('conv2') as scope:
kernel = _variable_with_weight_decay('weights',
shape=[5, 5, 64, 64],
stddev=5e-2,
wd=None)
conv = tf.nn.conv2d(norm1, kernel, [1, 1, 1, 1], padding='SAME')
biases = _variable_on_cpu('biases', [64], tf.constant_initializer(0.1))
pre_activation = tf.nn.bias_add(conv, biases)
conv2 = tf.nn.relu(pre_activation, name=scope.name)
_activation_summary(conv2)
# norm2
norm2 = tf.nn.lrn(conv2, 4, bias=1.0, alpha=0.001 / 9.0, beta=0.75,
name='norm2')
# pool2
pool2 = tf.nn.max_pool(norm2, ksize=[1, 3, 3, 1],
strides=[1, 2, 2, 1], padding='SAME', name='pool2')
第三層全連接層 ,需要先把前面的卷積層的輸出結(jié)果全部 flatten,使用 tf.reshape 函數(shù)將每個(gè)樣本都變?yōu)橐痪S向量,使用 get_shape 函數(shù)獲取數(shù)據(jù)扁平化之后的長度;然后對(duì)全連接層的 weights 和 biases 進(jìn)行初始化,為了防止全連接層過擬合,設(shè)置一個(gè)非零的 wd 值0.004,讓這一層的所有參數(shù)都被 L2正則所約束,最后依然使用 Relu 激活函數(shù)進(jìn)行非線性化。同理,可以建立第四層全連接層。
# local3
with tf.variable_scope('local3') as scope:
# Move everything into depth so we can perform a single matrix multiply.
reshape = tf.reshape(pool2, [images.get_shape().as_list()[0], -1])
dim = reshape.get_shape()[1].value
weights = _variable_with_weight_decay('weights', shape=[dim, 384],
stddev=0.04, wd=0.004)
biases = _variable_on_cpu('biases', [384], tf.constant_initializer(0.1))
local3 = tf.nn.relu(tf.matmul(reshape, weights) + biases, name=scope.name)
_activation_summary(local3)
# local4
with tf.variable_scope('local4') as scope:
weights = _variable_with_weight_decay('weights', shape=[384, 192],
stddev=0.04, wd=0.004)
biases = _variable_on_cpu('biases', [192], tf.constant_initializer(0.1))
local4 = tf.nn.relu(tf.matmul(local3, weights) + biases, name=scope.name)
_activation_summary(local4)
最后的 softmax_linear 層,先創(chuàng)建這一層的 weights 和 biases,不添加L2正則化。在這個(gè)模型中,不像之前的例子使用 sotfmax 輸出最后的結(jié)果,因?yàn)閷?softmax 的操作放在來計(jì)算 loss 的部分,將 softmax_linear 的線性返回值 logits 與 labels 計(jì)算 loss
# linear layer(WX + b),
# We don't apply softmax here because
# tf.nn.sparse_softmax_cross_entropy_with_logits accepts the unscaled logits
# and performs the softmax internally for efficiency.
with tf.variable_scope('softmax_linear') as scope:
weights = _variable_with_weight_decay('weights', [192, NUM_CLASSES],
stddev=1/192.0, wd=None)
biases = _variable_on_cpu('biases', [NUM_CLASSES],
tf.constant_initializer(0.0))
softmax_linear = tf.add(tf.matmul(local4, weights), biases, name=scope.name)
_activation_summary(softmax_linear)
然后計(jì)算loss:先計(jì)算交叉熵,然后在加上L2 loss
def loss(logits, labels):
# Calculate the average cross entropy loss across the batch.
labels = tf.cast(labels, tf.int64)
cross_entropy = tf.nn.sparse_softmax_cross_entropy_with_logits(
labels=labels, logits=logits, name='cross_entropy_per_example')
cross_entropy_mean = tf.reduce_mean(cross_entropy, name='cross_entropy')
tf.add_to_collection('losses', cross_entropy_mean)
# The total loss is defined as the cross entropy loss plus all of the weight
# decay terms (L2 loss).
return tf.add_n(tf.get_collection('losses'), name='total_loss')
模型訓(xùn)練
在定義 loss 之后,我們需要定義接受 loss 并返回 train op 的 train()。
首先定義學(xué)習(xí)率(learning rate),并設(shè)置隨迭代次數(shù)衰減,并進(jìn)行 summary:
# 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)
tf.summary.scalar('learning_rate', lr)
# Generate moving averages of all losses and associated summaries.
loss_averages_op = _add_loss_summaries(total_loss)
此外,我們對(duì) loss 生成滑動(dòng)均值和匯總,通過使用指數(shù)衰減,來維護(hù)變量的滑動(dòng)均值(Moving Average)。當(dāng)訓(xùn)練模型時(shí),維護(hù)訓(xùn)練參數(shù)的滑動(dòng)均值是有好處的,在測試過程中使用滑動(dòng)參數(shù)比最終訓(xùn)練的參數(shù)值本身,會(huì)提高模型的實(shí)際性能即準(zhǔn)確率。apply() 方法會(huì)添加 trained variables 的 shadow copies,并添加操作來維護(hù)變量的滑動(dòng)均值到 shadow copies。 average() 方法可以訪問 shadow variables,在創(chuàng)建 evaluation model 時(shí)非常有用。滑動(dòng)均值是通過指數(shù)衰減計(jì)算得到的,shadow variable 的初始化值和 trained variables 相同,其更新公式為 shadow_variable = decay * shadow_variable + (1 - decay) * variable。(關(guān)于ema,可參考之前的這篇tensorflow學(xué)習(xí)筆記-ExponentialMovingAverage)
def _add_loss_summaries(total_loss):
# Compute the moving average of all individual losses and the total loss.
loss_averages = tf.train.ExponentialMovingAverage(0.9, name='avg')
losses = tf.get_collection('losses')
loss_averages_op = loss_averages.apply(losses + [total_loss])
# Attach a scalar summary to all individual losses and the total loss; do the
# same for the averaged version of the losses.
for l in losses + [total_loss]:
# Name each loss as '(raw)' and name the moving average version of the loss
# as the original loss name.
tf.summary.scalar(l.op.name + ' (raw)', l)
tf.summary.scalar(l.op.name, loss_averages.average(l))
return loss_averages_op
然后,我們定義訓(xùn)練方法與目標(biāo),tf.control_dependencies 是一個(gè) context manager,控制節(jié)點(diǎn)執(zhí)行順序,先執(zhí)行[ ]中的操作,再執(zhí)行 context 中的操作。計(jì)算并應(yīng)用梯度,最后,動(dòng)態(tài)調(diào)整衰減率,返回模型參數(shù)變量的滑動(dòng)更新操作即 train op
# Compute gradients.
with tf.control_dependencies([loss_averages_op]):
opt = tf.train.GradientDescentOptimizer(lr)
grads = opt.compute_gradients(total_loss)
# Apply gradients.
apply_gradient_op = opt.apply_gradients(grads, global_step=global_step)
# Track the moving averages of all trainable variables.
variable_averages = tf.train.ExponentialMovingAverage(
MOVING_AVERAGE_DECAY, global_step)
with tf.control_dependencies([apply_gradient_op]):
variables_averages_op = variable_averages.apply(tf.trainable_variables())
return variables_averages_op
訓(xùn)練過程
def train():
"""Train CIFAR-10 for a number of steps."""
with tf.Graph().as_default():
global_step = tf.train.get_or_create_global_step()
# Get images and labels for CIFAR-10.
# Force input pipeline to CPU:0 to avoid operations sometimes ending up on
# GPU and resulting in a slow down.
with tf.device('/cpu:0'):
images, labels = cifar10.distorted_inputs()
# Build a Graph that computes the logits predictions from the
# inference model.
logits = cifar10.inference(images)
# Calculate loss.
loss = cifar10.loss(logits, labels)
# Build a Graph that trains the model with one batch of examples and
# updates the model parameters.
train_op = cifar10.train(loss, global_step)
class _LoggerHook(tf.train.SessionRunHook):
"""Logs loss and runtime."""
def begin(self):
self._step = -1
self._start_time = time.time()
def before_run(self, run_context):
self._step += 1
return tf.train.SessionRunArgs(loss) # Asks for loss value.
def after_run(self, run_context, run_values):
if self._step % FLAGS.log_frequency == 0:
current_time = time.time()
duration = current_time - self._start_time
self._start_time = current_time
loss_value = run_values.results
examples_per_sec = FLAGS.log_frequency * FLAGS.batch_size / duration
sec_per_batch = float(duration / FLAGS.log_frequency)
format_str = ('%s: step %d, loss = %.2f (%.1f examples/sec; %.3f '
'sec/batch)')
print (format_str % (datetime.now(), self._step, loss_value,
examples_per_sec, sec_per_batch))
with tf.train.MonitoredTrainingSession(
checkpoint_dir=FLAGS.train_dir, #這里會(huì)自動(dòng)保存checkpoint
hooks=[tf.train.StopAtStepHook(last_step=FLAGS.max_steps),
tf.train.NanTensorHook(loss),
_LoggerHook()],
config=tf.ConfigProto(
log_device_placement=FLAGS.log_device_placement)) as mon_sess:
while not mon_sess.should_stop():
mon_sess.run(train_op)