本章將介紹AlexNet另玖,VGGNet饼灿,Google Inception Net其掂,和ResNet
AlexNet
其主要使用到的新技術(shù):
(1)成功使用ReLU作為CNN的激活函數(shù)缤底,解決了Sigmoid在網(wǎng)絡(luò)較深時的梯度彌散問題;
(2)訓練使用Dropout隨機忽略一部分神經(jīng)元自娩,以避免過擬合用踩;
(3)在CNN中使用重疊的最大池化,避免平均池化的模糊化效果忙迁。并且提出讓步長比池化核的尺寸小脐彩,這樣池化層的輸出之間會有重疊和覆蓋,提升了特征的豐富性姊扔;
(4)提出了LRN層惠奸,對局部神經(jīng)元的活動創(chuàng)建競爭機制,使得其中響應(yīng)比較大的值變得相對更大恰梢,并抑制其他反饋較小的神經(jīng)元佛南,增加了模型泛化能力;
(5)使用CUDA加速深度卷積網(wǎng)絡(luò)的訓練嵌言,同時AlexNet的設(shè)計讓GPU之間的通信只在網(wǎng)絡(luò)某些曾進行嗅回,控制通信的性能損耗;
(6)數(shù)據(jù)增強摧茴,大大減輕過擬合绵载,提升泛化能力;
由于訓練時間過長,本章將不設(shè)計實際數(shù)據(jù)的訓練娃豹,只對它每個batch的前饋計算(forward)和反饋計算(backward)的速度進行測試焚虱。這里使用隨機圖片來計算。
首先載入幾個庫懂版,然后定義主要參數(shù):
from datetime import datetime
import math
import time
import tensorflow as tf
batch_size=32
num_batches=100
顯示網(wǎng)絡(luò)每一層結(jié)構(gòu)鹃栽,展示其姓名和尺寸:
def print_activations(t):
print(t.op.name, ' ', t.get_shape().as_list())
接下來設(shè)計AlexNet的網(wǎng)絡(luò)結(jié)構(gòu)。先定義inference定续,接受images作為輸入谍咆,返回最后一層pool5(第五個池化層)及parameters(需要訓練的參數(shù))禾锤。首先是第一個卷積層conv1私股。tf.name_scope()可以將scope內(nèi)生成的Variable自動命名為conv1/xxx。然后定義第一個卷積層恩掷,先初始化卷積核參數(shù)kernel倡鲸。然后進行卷積操作,每隔4x4取樣一次黄娘,卷積核大小11x11峭状。將卷積層的biases全部初始化為0,再加起來得到bias逼争,并使用激活函數(shù)進行非線性處理优床,最后打印conv1,并且添加kernel和biases到parameters:
def inference(images):
parameters = []
# conv1
with tf.name_scope('conv1') as scope:
kernel = tf.Variable(tf.truncated_normal([11, 11, 3, 64], dtype=tf.float32,
stddev=1e-1), name='weights')
conv = tf.nn.conv2d(images, kernel, [1, 4, 4, 1], padding='SAME')
biases = tf.Variable(tf.constant(0.0, shape=[64], dtype=tf.float32),
trainable=True, name='biases')
bias = tf.nn.bias_add(conv, biases)
conv1 = tf.nn.relu(bias, name=scope)
print_activations(conv1)
parameters += [kernel, biases]
添加LRN層和最大池化層誓焦。參數(shù)基本是AlexNet論文中的推薦值胆敞。然后進行最大池化處理。VALID意思為取樣不能超過邊框杂伟,不像pool那樣填充邊界外的點:
# pool1
lrn1 = tf.nn.lrn(conv1, 4, bias=1.0, alpha=0.001 / 9.0, beta=0.75, name='lrn1')
pool1 = tf.nn.max_pool(lrn1,
ksize=[1, 3, 3, 1],
strides=[1, 2, 2, 1],
padding='VALID',
name='pool1')
print_activations(pool1)
接下來設(shè)計第二個卷積層移层,只有幾個參數(shù)不同:
# conv2
with tf.name_scope('conv2') as scope:
kernel = tf.Variable(tf.truncated_normal([5, 5, 64, 192], dtype=tf.float32,
stddev=1e-1), name='weights')
conv = tf.nn.conv2d(pool1, kernel, [1, 1, 1, 1], padding='SAME')
biases = tf.Variable(tf.constant(0.0, shape=[192], dtype=tf.float32),
trainable=True, name='biases')
bias = tf.nn.bias_add(conv, biases)
conv2 = tf.nn.relu(bias, name=scope)
parameters += [kernel, biases]
print_activations(conv2)
接下來同樣先LRN處理,再進行最大池化處理赫粥,參數(shù)和之前完全一樣:
# pool2
lrn2 = tf.nn.lrn(conv2, 4, bias=1.0, alpha=0.001 / 9.0, beta=0.75, name='lrn2')
pool2 = tf.nn.max_pool(lrn2,
ksize=[1, 3, 3, 1],
strides=[1, 2, 2, 1],
padding='VALID',
name='pool2')
print_activations(pool2)
第三個卷積層观话,同樣是參數(shù)不同:
# conv3
with tf.name_scope('conv3') as scope:
kernel = tf.Variable(tf.truncated_normal([3, 3, 192, 384],
dtype=tf.float32,
stddev=1e-1), name='weights')
conv = tf.nn.conv2d(pool2, kernel, [1, 1, 1, 1], padding='SAME')
biases = tf.Variable(tf.constant(0.0, shape=[384], dtype=tf.float32),
trainable=True, name='biases')
bias = tf.nn.bias_add(conv, biases)
conv3 = tf.nn.relu(bias, name=scope)
parameters += [kernel, biases]
print_activations(conv3)
第四層和第五層也是修改參數(shù):
# conv4
with tf.name_scope('conv4') as scope:
kernel = tf.Variable(tf.truncated_normal([3, 3, 384, 256],
dtype=tf.float32,
stddev=1e-1), name='weights')
conv = tf.nn.conv2d(conv3, kernel, [1, 1, 1, 1], padding='SAME')
biases = tf.Variable(tf.constant(0.0, shape=[256], dtype=tf.float32),
trainable=True, name='biases')
bias = tf.nn.bias_add(conv, biases)
conv4 = tf.nn.relu(bias, name=scope)
parameters += [kernel, biases]
print_activations(conv4)
# conv5
with tf.name_scope('conv5') as scope:
kernel = tf.Variable(tf.truncated_normal([3, 3, 256, 256],
dtype=tf.float32,
stddev=1e-1), name='weights')
conv = tf.nn.conv2d(conv4, kernel, [1, 1, 1, 1], padding='SAME')
biases = tf.Variable(tf.constant(0.0, shape=[256], dtype=tf.float32),
trainable=True, name='biases')
bias = tf.nn.bias_add(conv, biases)
conv5 = tf.nn.relu(bias, name=scope)
parameters += [kernel, biases]
print_activations(conv5)
下面是一個最大池化層:
# pool5
pool5 = tf.nn.max_pool(conv5,
ksize=[1, 3, 3, 1],
strides=[1, 2, 2, 1],
padding='VALID',
name='pool5')
print_activations(pool5)
return pool5, parameters
至此函數(shù)就完成了,它可以創(chuàng)建AlexNet的卷積部分越平。還需要添加3個全連接層频蛔,隱含層節(jié)點數(shù)分別為4096,4096,1000。
接下來實現(xiàn)一個評估AlexNet每輪計算時間的函數(shù)time_tensorflow_run秦叛。第二個變量是評測的運算算子晦溪,第三個變量是測試的名稱:
def time_tensorflow_run(session, target, info_string):
# """Run the computation to obtain the target tensor and print timing stats.
#
# Args:
# session: the TensorFlow session to run the computation under.
# target: the target Tensor that is passed to the session's run() function.
# info_string: a string summarizing this run, to be printed with the stats.
#
# Returns:
# None
# """
num_steps_burn_in = 10
total_duration = 0.0
total_duration_squared = 0.0
我們進行num_batches+num_step_burn_in次迭代計算,使用time.time()計算時間书闸,每次迭代通過session.run(target)執(zhí)行尼变。每10輪迭代顯示當前迭代所需要的時間。同時每輪將total_duration和total_duration_squared累加,以便后面計算每輪耗時的均值和標準差:
for i in range(num_batches + num_steps_burn_in):
start_time = time.time()
_ = session.run(target)
duration = time.time() - start_time
if i >= num_steps_burn_in:
if not i % 10:
print ('%s: step %d, duration = %.3f' %
(datetime.now(), i - num_steps_burn_in, duration))
total_duration += duration
total_duration_squared += duration * duration
循環(huán)結(jié)束后計算平均耗時mn和標準差sd:
mn = total_duration / num_batches
vr = total_duration_squared / num_batches - mn * mn
sd = math.sqrt(vr)
print ('%s: %s across %d steps, %.3f +/- %.3f sec / batch' %
(datetime.now(), info_string, num_batches, mn, sd))
接下來是主函數(shù)嫌术。首先使用with tf.Graph().as_default()定義默認的Graph方便后面使用哀澈。然后使用預(yù)先定義的inference函數(shù)構(gòu)建整個AlexNet網(wǎng)絡(luò),得到最后一個池化層的輸出pool5和網(wǎng)絡(luò)中需要訓練的參數(shù)集合parameters度气。然后初始化所有參數(shù):
def run_benchmark():
# """Run the benchmark on AlexNet."""
with tf.Graph().as_default():
# Generate some dummy images.
image_size = 224
# Note that our padding definition is slightly different the cuda-convnet.
# In order to force the model to start with the same activations sizes,
# we add 3 to the image_size and employ VALID padding above.
images = tf.Variable(tf.random_normal([batch_size,
image_size,
image_size, 3],
dtype=tf.float32,
stddev=1e-1))
# Build a Graph that computes the logits predictions from the
# inference model.
pool5, parameters = inference(images)
# Build an initialization operation.
init = tf.global_variables_initializer()
# Start running operations on the Graph.
config = tf.ConfigProto()
config.gpu_options.allocator_type = 'BFC'
sess = tf.Session(config=config)
sess.run(init)
下面進行forward評測割按,這里直接使用time_tensorflow_run統(tǒng)計運算時間,傳入的target就是pool5磷籍。然后進行backward即訓練過程的評測适荣。grad求相對于所有模型參數(shù)的梯度,這樣就模擬了訓練過程院领。最后執(zhí)行主函數(shù):
# Run the forward benchmark.
time_tensorflow_run(sess, pool5, "Forward")
# Add a simple objective so we can calculate the backward pass.
objective = tf.nn.l2_loss(pool5)
# Compute the gradient with respect to all the parameters.
grad = tf.gradients(objective, parameters)
# Run the backward benchmark.
time_tensorflow_run(sess, grad, "Forward-backward")
run_benchmark()
應(yīng)用CNN的主要瓶頸還是在訓練弛矛,用CNN做預(yù)測問題不大。
VGGNet
VGGNet論文中全部使用了3x3的卷積核和2x2的池化核比然,通過不斷加深網(wǎng)絡(luò)結(jié)構(gòu)來提升性能丈氓。其中經(jīng)常出現(xiàn)多個完全一樣的3x3卷積層,兩個3x3的卷積層串聯(lián)相當于1個5x5的卷積層强法,即一個像素會跟5x5的像素產(chǎn)生關(guān)聯(lián)万俗,可以說感受野5x5。
VGG訓練的時候有小技巧饮怯,先訓練級別A的簡單網(wǎng)絡(luò)闰歪,再復用A網(wǎng)絡(luò)的權(quán)重來初始化后面的幾個復雜模型,這樣訓練模型的收斂速度更快蓖墅。
作者總結(jié)了一下結(jié)論:
①LRN層作用不大库倘;
②越深的網(wǎng)絡(luò)效果越好;
③1x1的卷積也是很有效的置媳,但沒有3x3的卷積好于樟,大一些的卷積核可以學習更大的空間特征
(下面僅記錄和其他模型不一樣的,或者有特點的代碼)
用來創(chuàng)建卷積層并把本層的參數(shù)存入?yún)?shù)列表拇囊。get_shape()[-1].value獲取輸入input_op的通道數(shù)迂曲。
def conv_op(input_op, name, kh, kw, n_out, dh, dw, p):
n_in = input_op.get_shape()[-1].value
with tf.name_scope(name) as scope:
kernel = tf.get_variable(scope+"w",
shape=[kh, kw, n_in, n_out],
dtype=tf.float32,
initializer=tf.contrib.layers.xavier_initializer_conv2d())
conv = tf.nn.conv2d(input_op, kernel, (1, dh, dw, 1), padding='SAME')
bias_init_val = tf.constant(0.0, shape=[n_out], dtype=tf.float32)
biases = tf.Variable(bias_init_val, trainable=True, name='b')
z = tf.nn.bias_add(conv, biases)
activation = tf.nn.relu(z, name=scope)
p += [kernel, biases]
return activation
下面是全連接層創(chuàng)建函數(shù)fc_op()。先獲取輸入input_op的通道數(shù)寥袭,再使用tf.get_variable創(chuàng)建全連接層的參數(shù):
def fc_op(input_op, name, n_out, p):
n_in = input_op.get_shape()[-1].value
with tf.name_scope(name) as scope:
kernel = tf.get_variable(scope+"w",
shape=[n_in, n_out],
dtype=tf.float32,
initializer=tf.contrib.layers.xavier_initializer())
biases = tf.Variable(tf.constant(0.1, shape=[n_out], dtype=tf.float32), name='b')
activation = tf.nn.relu_layer(input_op, kernel, biases, name=scope)
p += [kernel, biases]
return activation
我們將第5段卷積網(wǎng)絡(luò)的輸出結(jié)果進行扁平化路捧,使用tf.reshape函數(shù)將每個樣本化為長度為 7x7x512=25088的一維向量:
shp = pool5.get_shape()
flattened_shape = shp[1].value * shp[2].value * shp[3].value
resh1 = tf.reshape(pool5, [-1, flattened_shape], name="resh1")
下面定義評測的主函數(shù)run_benchmark,我們的目標依然是僅評測forward和backward的運算性能传黄,并不進行實質(zhì)的訓練和預(yù)測杰扫。首先生成224x224的隨機圖片,方法和AlexNet中一樣膘掰,通過tf.random_normal函數(shù)生成標準差為0.1的正態(tài)分布的隨機數(shù):
def run_benchmark():
with tf.Graph().as_default():
image_size = 224
images = tf.Variable(tf.random_normal([batch_size,
image_size,
image_size, 3],
dtype=tf.float32,
stddev=1e-1))
接下來創(chuàng)建keep_prob的placeholder章姓,并調(diào)用inference_op函數(shù)構(gòu)建VGGNet-16的網(wǎng)絡(luò)結(jié)構(gòu)佳遣,獲得predictions、softmax凡伊、fc8和參數(shù)列表p:
keep_prob = tf.placeholder(tf.float32)
predictions, softmax, fc8, p = inference_op(images, keep_prob)
Google Inception Net
Inception V1降低參數(shù)量的目的有兩點零渐,第一:參數(shù)越多模型越龐大,需要供模型學習的數(shù)據(jù)量就越大系忙;第二:參數(shù)越多诵盼,耗費的計算資源也會更大。Inception V1參數(shù)少但效果好的原因除了模型層數(shù)更深银还、表達能力更強外风宁,還有兩點。一是除了最后的全連接層蛹疯,用全局平均池化層來取代他戒财。二是Inception V1中精心設(shè)計的Inception Module提高了參數(shù)的利用效率。
Inception V1比NIN更進一步的是增加了分支網(wǎng)絡(luò)苍苞,NIN則主要是級聯(lián)的卷積層和MLPConv層固翰。
IM的基本結(jié)構(gòu)有4個分支狼纬。第一個分支是1x1卷積羹呵,是一個非常優(yōu)秀的結(jié)構(gòu)。它可以對輸出通道升維和降維疗琉。第二個分支線使用1x1卷積冈欢,然后連接3x3卷積,相當于進行了兩次特征變換盈简。第三個分支類似凑耻。
因為1x1的卷積性價比高,用很小的計算量就能增加一層特征變換和非線性化柠贤。IM中的4個分支在最后通過一個聚合操作合并香浩。IM中包含看3個不同尺寸的卷積和1個最大池化,增加了網(wǎng)絡(luò)對不同尺度的適應(yīng)性臼勉。
如果數(shù)據(jù)集的概率分布可以被一個很大很稀疏的神經(jīng)網(wǎng)絡(luò)所表達邻吭,那么構(gòu)筑這個網(wǎng)絡(luò)的最佳方法是逐層構(gòu)筑網(wǎng)絡(luò):將上一層高度相關(guān)的節(jié)點聚類,并將聚類出來的每一個小簇連接到一起宴霸。
Inception V2學習了VGGNet囱晴,用兩個3x3的卷積代替5x5的大卷積(可以降低參數(shù)量并且減輕過擬合)。BN在用于神經(jīng)網(wǎng)絡(luò)的某層時瓢谢,會對mini-batch內(nèi)部進行標準化處理畸写,使輸出規(guī)范化到N(0,1)的正態(tài)分布,減少Internal Covariate Shift(內(nèi)部神經(jīng)元的改變)氓扛。
單純使用BN獲得的增益還不明顯枯芬,還需要一些相應(yīng)的調(diào)整:增大學習速率并加快學習衰減以適應(yīng)BN規(guī)范化后的數(shù)據(jù);去除Dropout并減輕L2正則;去除LRN千所;更徹底的進行shuffle翅楼;減少數(shù)據(jù)增強中的光學畸變。
Inception V3網(wǎng)絡(luò)則主要是兩方面的改造真慢,一是引入Factorization into small convolutions的思想毅臊,將一個較大的二維卷積拆成兩個較小的一維卷積。一方面節(jié)約了大量參數(shù)黑界,加速運算并減輕了過擬合管嬉。同時增加了一層非線性擴展模型表達能力。
另一方面朗鸠,Inception V3優(yōu)化了Inception Module的結(jié)構(gòu)蚯撩。
Inception V4相比V3主要是結(jié)合了微軟的ResNet。
下面僅記錄不同的代碼烛占。
首先介紹tf.contrib.slim胎挎。
slim = tf.contrib.slim
他可以給函數(shù)的參數(shù)自動賦予某些默認值。例如weights_regularizer=slim.l2_regularizer(weight_decay))會對[slim.conv2d, slim.fully_connected]這兩個函數(shù)的參數(shù)自動賦值忆家,將參數(shù)weights_regularizer的值默認設(shè)為slim.l2_regularizer(weight_decay)犹菇。使用slim.arg_scope后就不需要每次都重復設(shè)置參數(shù)了,只要在有修改時設(shè)置芽卿。接下來嵌套一個slim.arg_scope揭芍,對卷積層生成函數(shù)slim.sonv2d的參數(shù)賦予默認值。
with slim.arg_scope([slim.conv2d, slim.fully_connected],
weights_regularizer=slim.l2_regularizer(weight_decay)):
with slim.arg_scope(
[slim.conv2d],
weights_initializer=trunc_normal(stddev),
activation_fn=tf.nn.relu,
normalizer_fn=slim.batch_norm,
normalizer_params=batch_norm_params) as sc:
return sc
同時卸例,Inception V3論文中也提出了Factorization into Module思想称杨,利用兩個一維卷積模擬大尺寸的二維卷積,減少參數(shù)量同時是增加非線性筷转。前面幾層姑原,卷積中還有一層1x1卷積,這也是前面提到的Inception Module中經(jīng)常使用的結(jié)構(gòu)之一呜舒,可以低成本的跨通道的對特征進行組合锭汛。
ResNet
Residual Neural Network由微軟研究院Kaiming He等4名華人提出。ResNet的結(jié)構(gòu)可以極快的加速超神神經(jīng)網(wǎng)絡(luò)的訓練阴绢,模型的準確率也有非常大的提升店乐。Highway Network的目標就是解決極深的神經(jīng)網(wǎng)絡(luò)難以訓練的問題。前一層的信息呻袭,有一定比例可以不經(jīng)過矩陣乘法和非線性變換眨八,直接傳輸?shù)较乱粚印esNet最初的靈感出自問題:在不斷加深神經(jīng)網(wǎng)絡(luò)深度時左电,會出現(xiàn)Degradation的問題廉侧,即準確率會先上升然后達到飽和页响,再持續(xù)增加深度則會導致準確率下降。這并不是過擬合的問題段誊,因為不光在測試集上誤差增大闰蚕,訓練集本身也會增大。假設(shè)某段神經(jīng)網(wǎng)絡(luò)的輸入是x连舍,期望輸出是H(x)没陡,如果我們直接把輸入x傳到輸出作為初始結(jié)果,那么此時我們需要學習的目標就是F(x)=H(x)-x索赏。
傳統(tǒng)的卷積層或全連接層在信息傳遞時盼玄,或多或少會存在信息丟失、損耗等問題潜腻。ResNet在某種程度上解決了這個問題埃儿,通過直接將輸入信息繞道到輸出,保護信息的完整性融涣,這個網(wǎng)絡(luò)則只需要學習輸入童番、輸出差別的那一部分,簡化學習目標和難度威鹿。
以下同只記錄與眾不同的代碼剃斧。
class Block(collections.namedtuple('Block', ['scope', 'unit_fn', 'args'])):
使用collections.namedtuple設(shè)計ResNet基本Block模塊組的named tuple,并用它創(chuàng)建Block類专普,但只包含數(shù)據(jù)結(jié)構(gòu)悯衬,不包含具體方法。
下面定義一個降采樣subsample的方法檀夹,參數(shù)包括inputs(輸入),factor(采樣因子)和scope策橘。如果factor是1炸渡,則不做修改直接返回inputs;如果不為1丽已,則使用slim.max_pool2d最大池化實現(xiàn):
def subsample(inputs, factor, scope=None):
if factor == 1:
return inputs
else:
return slim.max_pool2d(inputs, [1, 1], stride=factor, scope=scope)
接下來定義堆疊Blocks的函數(shù)蚌堵,參數(shù)中net即為輸入,而outputs_collections則是用來收集各個end_points的collections沛婴。下面使用兩層循環(huán)吼畏,逐個Block,逐個Residual Unit地堆疊嘁灯,先使用兩個tf.variable_scope將殘差學習單元命名為block/unit_1的形式泻蚊。在第二層循環(huán)中,我們拿到每個Block中每個Residual Unit的args丑婿,并展開為depth性雄、depth_bottleneck和stribe没卸,其含義在前面定義Blocks類時已經(jīng)講解過。然后使用unit_fn函數(shù)(即殘差學習單元的生成函數(shù))順序地創(chuàng)建并連接所有殘差單元:
@slim.add_arg_scope
def stack_blocks_dense(net, blocks,
outputs_collections=None):
for block in blocks:
with tf.variable_scope(block.scope, 'block', [net]) as sc:
for i, unit in enumerate(block.args):
with tf.variable_scope('unit_%d' % (i + 1), values=[net]):
unit_depth, unit_depth_bottleneck, unit_stride = unit
net = block.unit_fn(net,
depth=unit_depth,
depth_bottleneck=unit_depth_bottleneck,
stride=unit_stride)
net = slim.utils.collect_named_outputs(outputs_collections, sc.name, net)
return net