1. 程式人生 > >Tensorflow 實現經典卷積神經網路AlexNet

Tensorflow 實現經典卷積神經網路AlexNet

AlexNet將CNN的基本原理應用到很深很廣的網路中,AlexNet主要使用到的新技術點如下:

(1)成功使用ReLU作為CNN的啟用函式,並驗證其效果在較深網路中超過了Sigmoid成功解決了Sigmoid在網路較深時的梯度彌散問題。

(2)訓練時使用Dropout隨機忽略一部分神經元,以避免模型過擬合。在AlexNet中主要是最後幾個全連線層使用了Dropout。

(3)在CNN中使用重疊的最大池化。此前CNN中普遍使用平均池化,ALexNet全部使用最大池化,避免平均池化的模糊化效果。並且AlexNet中提出讓步長比池化核的尺寸小,這樣池化層的輸出之間會用重疊和覆蓋,提升了模型的泛化能力。

(4)提出了LRN層,對區域性神經元的活動建立競爭機制,使得其中響應比較大的值變得相對更大,並抑制其他反饋較小的神經元,增強了模型的泛化能力。

(5)使用CUDA加速深度卷積網路的訓練,利用GPU強大的平行計算能力,處理神經網路訓練時大量的矩陣運算。

(6)資料增強,隨機地從256x256的原始影象中擷取224x224大小的區域(以及水平翻轉的映象),相當於增加了(256-224)^2*2=2048倍的資料量。使用了資料增強後可以大大減輕過擬合,提升泛化能力。

整個AlexNet有8個需要訓練引數的層(不包括池化層和LRN層),前5層為卷積層,後3層為全連線層。AlexNet最後一層是有1000類輸出的Softmax層用作分類。LRN層出現在第一個及第二個卷積層後,而最大池化層出現在兩個LRN層及最後一個卷積層後。Relu啟用函式則應用再這8層每一層的後面。

我們建立一個完整的AlexNet卷積神經網路,然後對它每個batch的前饋計算和反饋計算的速度3進行測試。

首先匯入幾個接下來會用到的幾個系統庫,包括datetime,math和time,並載入Tensorflow。

from datetime import datetime
import math
import time
import tensorflow as tf
這裡設定batch_size為32,num_batches為100,即總共測試100個batch的資料。
batch_size=32
num_batches=100
定義一個用來顯示網路每一層結構的函式print_actications,展示每一個卷積層或池化層輸出tensor的尺寸。這個函式接受一個tensor作為輸入,並顯示其名稱和尺寸。
def print_activations(t):
    print t.op.name,'',t.get_shape().as_list()
接下來設計AlexNet的網路結構。我們先定義函式inference,它接受images作為輸入,返回最後一層pool5(第5個池化層)及parameters(AlexNet中所有需要訓練的模型引數)。這個inference函式將會很大,包括多個卷積和池化層。下面將拆為幾個小段分別講解:

首先是第一個卷積層conv1,這裡使用Tensorflow中的name_scope,通過with tf.name_scope('conv1') as scope可以將scope內生成的Variable自動命名為conv1/xxx,便於區分不同卷積層之間的元件,然後定義第一個卷積層,和之前一樣使用tf.truncated_normal階段的正態分佈函式(標準差為0.1)初始化卷積核的引數為kernel。卷積核尺寸為11x11,顏色通道為3,卷積核數量為64.準備好了kernel,再使用tf.nn.conv2d對輸入images完成卷積操作,我們將strides步長設定為4x4(即在圖片上每4x4區域只取樣一次,橫向間隔是4,縱向間隔也為4,每次取樣的卷積核大小都為11X11),padding模式設為SAME。將卷積層的biases全部初始化為0,再使用tf.nn.bias_add將conv和biases加起來,並使用啟用函式tf.nn.relu對結果進行非線性處理。最後砽print_activations將這一層最後輸出的tensor conv1的結構打印出來,並將這一層可訓練的引數kernel,biases新增到parameters中。

def inference(images):
        parameters=[]
        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),trianable=True,name='biases')
                bias=tf.nn.bias_add(conv,biases)
                conv1=tf.nn.relu(bias,name=scope)
                print_activations(conv1)
                parameters+=[kernel,biases]

在第一個卷積層後再新增LRN層和最大池化層。先使用tf.nn.lrn對前面輸出的tensor conv1 進行lrn處理,這裡使用的depth_radius 設為4,bias設為1,alpha為0.001/9,beta為0.75,基本都是AlexNet論文中的推薦值。不過目前除了AlexNet,其他景點的卷積神經網路模型基本都放棄了LRN(主要是效果不明顯),而我們使用LRN也會讓前饋、反饋的速度大大下降(整體速度下降1/3)。使用tf.nn.max_pool對前面的輸出lrn1進行最大池化處理,這裡的池化尺寸為3x3,即將3x3大小的畫素塊降為1x1的畫素,取樣的步長為2x2,padding模式設為VALID,即取樣時不能超過邊框,不像SAME模式那樣可以填充邊界外的點。最後將輸出結果pool1的結構打印出來。
lrn1=tf.nn.lrn(conv1,4,bias=1.0,alpha=0.001/9,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)
接下是第二個卷積層,大部分步驟和第一個卷積層相同,只有幾個引數不同。主要區別在與我們的卷積和尺寸是5x5,輸入通道數(即上一層輸出通道數,也就是上一層卷積核數量)為64,卷積核數量為192.同時,卷積的步長也全部設為1,即掃描全影象素。
       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(pool2)
接下來對第2個卷積層的輸出conv2進行處理,同樣是先做LRN處理,再進行最大池化處理,引數和之前完全一樣。
lrn2=tf.nn.lrn(conv2,4,bias=1.0,alpha=0.001/9,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)

下面是建立第3個卷積層,基本結構和前面兩個類似,也只是引數不同。這一層的卷積核尺寸為3X3,輸入通道數為192,卷積核數量繼續擴大為384,同時卷積的步長全部為1,其他地方和前面保持一致。
      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.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)
第4個卷積層和之前也類似,這一層的卷積核尺寸為3x3,輸入通道數為384,但是卷積核數量降為256.
with tf.name_scope('conv4') as scope:
                kernel=tf.Variable(tf.truncated_normal([3,3,256,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)
                conv5=tf.nn.relu(bias,name=scope)
                parameters+=[kernel,biases]
        print_activations(conv4)
最後的第5個卷積層同樣是3X3大小的卷積核,輸入通道數為256,卷積核數量也為256
  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)
在第5個卷積層之後,還有一個最大池化層,這個池化層和前兩個卷積層後的池化層一致,最後我們返回這個池化層的輸出pool5.至此,inference函式就完成了,它可以建立AlexNet的卷積部分。在正式使用AlexNet來訓練或預測時,還需要新增3個全連線層,隱含節點數分別為4096,4096和1000。
pool5=tf.nn.max_pool(conv5,ksize=[1,3,3,1].strides=[1,2,2,1],padding='VALID',name='pool5')
print_activations(pool5)
return pool,parameters
全連線層大致如下:
reshape=tf.reshape(pool2,[batch_size,-1])
dim=reshape.get_shape()[1].value
weight=variable_with_weight_loss(shape=[dim,384],stddev=0.04,wl=0.004)
bias=tf.Variable(tf.constant(0.1,shape=[384]))
local=tf.nn.relu(tf.matmul(reshape,weight3)+bias)
下面還有2個全連線層,與上類似,這裡不再贅述。

接下來實現一個品谷AlexNet每輪計算時間的函式time_tensorflow_run。這個函式的第一個輸入就是tensorflow的SESSION,第二個變數是需要評測的運算運算元,第三個變數是測試的名稱。先定義預熱輪數num_steps_burn_in=10,它的作用是給程式熱身,頭幾輪迭代有現存載入、cache命中等問題一次可以跳過,我們只考量10輪迭代之後的計算時間。同時,也記錄總時間total_duration和平方和total_duration_squared用以計算方差。

def time_tensorflow_run(session,target,info_string):
    num_steps_burn_in=10
    total_duration=0.0
    total_duration_squared=0.0
進行num_batches+num_steps_burn_in次迭代計算,使用time.time()記錄時間,每次迭代通過session.run(target)執行。在初始熱身的Num_steps_burn_in次迭代後,每次迭代顯示迭代所需要的時間。同時每輪將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
在迴圈結束後,計算每輪迭代的平均耗時mn和標準差sd,最後將結果顯示出來,這樣就完成了計算每輪迭代耗時的評測函式time_tensorflow_run。
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)
接下來是主函式run_benchmark.首先使用with tf.Graph().as_default()定義預設的Graph方便後面使用。我們使用tf.random_normal函式構造正態分佈(標準差為0.1)的隨機tensor,第一個維度是batch_size,即每輪迭代的樣本數,第二個和第三個維度是圖片的查詢image_size=224,第四個維度是圖片的顏色通道數。接下來,使用前面定義的inference函式侯建整個AlexNet網路,得到最後一個池化層的輸出pool5和網路中需要訓練的引數的幾何parameters、接下來我們使用tf.Session()建立新的Session並通過tf.global_variable_initializer()初始化所有引數。
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))
   pool5,parameters=inference(images)
   init=tf.global_variables_initializer()
   sess=tf.Session()
   sess.run(init)

下面進行AlexNet的forward計算的評測,這裡直接使用time_tensorflow_run統計運算時間,傳入的target就是pool5,即卷積網路最後一個池化層的輸出。然後進行backward,即訓練過程的評測,這裡和forward計算有些不同,我們需要給最後的pool5設定一個優化目標loss。使用tf.nn.l2_loss計算pool5的loss,再使用tf.gradients求相對於loss的所有模型引數的梯度,這樣就模擬了一個訓練的過程。當然,訓練時還有一個根據梯度更新引數的過程,最後我們使用time_tensorflow_run統計backward的運算時間,這裡的target就是求整個網路梯度gard的操作。
time_tensorflow_run(sess,pool5,'Forward")
objective=tf.nn.l2_loss(pool5)
grad=tf.gradients(objective,parameters)
time_tensorflow_run(sess,grad,"Forward-backward")
最後執行主函式
run_benchmark()