第二十四節,TensorFlow下slim庫函數的使用以及使用VGG網絡進行預訓練、遷移學習(附代碼)


在介紹這一節之前,需要你對slim模型庫有一些基本了解,具體可以參考第二十二節,TensorFlow中的圖片分類模型庫slim的使用、數據集處理,這一節我們會詳細介紹slim模型庫下面的一些函數的使用。

一 簡介

slim被放在tensorflow.contrib這個庫下面,導入的方法如下:

import tensorflow.contrib.slim as slim

這樣我們就可以使用slim了,既然說到了,先來了解tensorflow.contrib這個庫,tensorflow官方對它的描述是:此目錄中的任何代碼未經官方支持,可能會隨時更改或刪除。每個目錄下都有指定的所有者。它旨在包含額外功能和貢獻,最終會合並到核心TensorFlow中,但其接口可能仍然會發生變化,或者需要進行一些測試,看是否可以獲得更廣泛的接受。所以slim依然不屬於原生tensorflow。

那么什么是slim?slim到底有什么用?

上一節已經講到slim是一個使構建,訓練,評估神經網絡變得簡單的庫。它可以消除原生tensorflow里面很多重復的模板性的代碼,讓代碼更緊湊,更具備可讀性。另外slim提供了很多計算機視覺方面的著名模型(VGG, AlexNet等),我們不僅可以直接使用,甚至能以各種方式進行擴展。

slim的子模塊及功能介紹:

  • arg_scope: provides a new scope named arg_scope that allows a user to define default arguments for specific operations within that scope.

除了基本的name_scope,variabel_scope外,又加了arg_scope,它是用來控制每一層的默認超參數的。(后面會詳細說)

  • data: contains TF-slim's dataset definition, data providers, parallel_reader, and decoding utilities.

貌似slim里面還有一套自己的數據定義,這個跳過,我們用的不多。

  • evaluation: contains routines for evaluating models.

評估模型的一些方法,用的也不多。

  • layers: contains high level layers for building models using tensorflow.

這個比較重要,slim的核心和精髓,一些復雜層的定義。

  • learning: contains routines for training models.

一些訓練規則。

  • losses: contains commonly used loss functions.

一些loss。

  • metrics: contains popular evaluation metrics.

評估模型的度量標准。

  • nets: contains popular network definitions such as VGG and AlexNet models.

包含一些經典網絡,VGG等,用的也比較多。

  • queues: provides a context manager for easily and safely starting and closing QueueRunners.

文本隊列管理,比較有用。

  • regularizers: contains weight regularizers.

包含一些正則規則。

  • variables: provides convenience wrappers for variable creation and manipulation.

這個比較有用,我很喜歡slim管理變量的機制。

 

二.slim定義模型

在slim中,組合使用variables, layers和scopes可以簡潔的定義模型。

1.variable

定義於模型變量。生成一個weight變量, 用truncated normal初始化它, 並使用l2正則化,並將其放置於CPU上, 只需下面的代碼即可:

#定義模型變量
weights = slim.model_variable('weights', shape=[10, 10, 3 , 3], initializer=tf.truncated_normal_initializer(stddev=0.1), regularizer=slim.l2_regularizer(0.05), device='/CPU:0') model_variables = slim.get_model_variables()

原生tensorflow包含兩類變量:普通變量和局部變量。大部分變量都是普通變量,它們一旦生成就可以通過使用saver存入硬盤,局部變量只在session中存在,不會保存。

  • slim進一步的區分了變量類型,定義了model_variables(模型變量),這種變量代表了模型的參數。模型變量通過訓練或者微調而得到學習,或者在評測或前向傳播中可以從ckpt文件中載入。
  • 非模型參數在實際前向傳播中不需要的參數,比如global_step。同樣的,移動平均反應了模型參數,但它本身不是模型參數。如下:
#常規變量
my_var = slim.variable('my_var',shape=[20, 1], initializer=tf.zeros_initializer()) #get_variables()得到模型參數和常規參數
regular_variables_and_model_variables = slim.get_variables()

當我們通過slim的layers或着直接使用slim.model_variable創建變量時,tf會將此變量加入tf.GraphKeys.MODEL_VARIABLES這個集合中,當你需要構建自己的變量時,可以通過以下代碼
將其加入模型參數。

#Letting TF-Slim know about the additional variable.
slim.add_model_variable(my_var)

 2.layers

抽象並封裝了常用的層,並且提供了repeat和stack操作,使得定義網絡更加方便。
首先讓我們看看tensorflow怎么實現一個層,例如卷積層:

#在tensorflow下實現一個層
input_x = tf.placeholder(dtype=tf.float32,shape=[None,224,224,3]) with tf.name_scope('conv1_1') as scope: weight = tf.Variable(tf.truncated_normal([3, 3, 3, 64], dtype=tf.float32, stddev=1e-1), name='weights') conv = tf.nn.conv2d(input_x, weight, [1, 1, 1, 1], padding='SAME') bias = tf.Variable(tf.constant(0.0, shape=[64], dtype=tf.float32), trainable=True, name='biases') conv1 = tf.nn.relu(tf.nn.bias_add(conv, bias), name=scope)

然后slim的實現:

#在slim實現一層
net = slim.conv2d(input_x, 64, [3, 3], scope='conv1_1')  

但這個不是重要的,因為tenorflow目前也有大部分層的簡單實現,這里比較吸引人的是slim中的repeat和stack操作:

假設定義三個相同的卷積層:

net = ... net = slim.conv2d(net, 256, [3, 3], scope='conv2_1') net = slim.conv2d(net, 256, [3, 3], scope='conv2_2') net = slim.conv2d(net, 256, [3, 3], scope='conv2_3') net = slim.max_pool2d(net, [2, 2], scope='pool2')  

在slim中的repeat操作可以減少代碼量:

net = slim.repeat(net, 3, slim.conv2d, 256, [3, 3], scope='conv2') net = slim.max_pool2d(net, [2, 2], scope='pool2')  

repeat不僅只實現了相同操作相同參數的重復,它還將scope進行了展開,例子中的scope被展開為 'conv2/conv2_1', 'conv2/conv2_2' and 'conv2/conv2_3'。

而stack是處理卷積核或者輸出不一樣的情況,假設定義三層FC:

#stack的使用 stack是處理卷積核或者輸出不一樣的情況,
x = tf.placeholder(dtype=tf.float32,shape=[None,784]) x = slim.fully_connected(x, 32, scope='fc/fc_1') x = slim.fully_connected(x, 64, scope='fc/fc_2') x = slim.fully_connected(x, 128, scope='fc/fc_3')  
#使用stack操作:
x = slim.stack(x, slim.fully_connected, [32, 64, 128], scope='fc')  

同理卷積層也一樣:

# 普通方法: 
net = slim.conv2d(input_x, 32, [3, 3], scope='core/core_1') net = slim.conv2d(net, 32, [1, 1], scope='core/core_2') net = slim.conv2d(net, 64, [3, 3], scope='core/core_3') net = slim.conv2d(net, 64, [1, 1], scope='core/core_4') # 簡便方法: 
net = slim.stack(input_x, slim.conv2d, [(32, [3, 3]), (32, [1, 1]), (64, [3, 3]), (64, [1, 1])], scope='core')  

 3.scope

除了tensorflow中的name_scope和variable_scope, tf.slim新增了arg_scope操作,這一操作符可以讓定義在這一scope中的操作共享參數,即如不指定參數的話,則使用默認參數。且參數可以被局部覆蓋。

如果你的網絡有大量相同的參數,如下:

net = slim.conv2d(input_x, 64, [11, 11], 4, padding='SAME', weights_initializer=tf.truncated_normal_initializer(stddev=0.01), weights_regularizer=slim.l2_regularizer(0.0005), scope='conv1') net = slim.conv2d(net, 128, [11, 11], padding='VALID', weights_initializer=tf.truncated_normal_initializer(stddev=0.01), weights_regularizer=slim.l2_regularizer(0.0005), scope='conv2') net = slim.conv2d(net, 256, [11, 11], padding='SAME', weights_initializer=tf.truncated_normal_initializer(stddev=0.01), weights_regularizer=slim.l2_regularizer(0.0005), scope='conv3')  

然后我們用arg_scope處理一下:

#使用arg_scope
with slim.arg_scope([slim.conv2d], padding='SAME', weights_initializer=tf.truncated_normal_initializer(stddev=0.01), weights_regularizer=slim.l2_regularizer(0.0005)): net = slim.conv2d(input_x, 64, [11, 11], scope='conv1') net = slim.conv2d(net, 128, [11, 11], padding='VALID', scope='conv2') net = slim.conv2d(net, 256, [11, 11], scope='conv3')  

如上倒數第二行代碼,對padding進行了重新賦值。那如果除了卷積層還有其他層呢?那就要如下定義:

with slim.arg_scope([slim.conv2d, slim.fully_connected], activation_fn=tf.nn.relu, weights_initializer=tf.truncated_normal_initializer(stddev=0.01), weights_regularizer=slim.l2_regularizer(0.0005)): with slim.arg_scope([slim.conv2d], stride=1, padding='SAME'): net = slim.conv2d(input_x, 64, [11, 11], 4, padding='VALID', scope='conv1') net = slim.conv2d(net, 256, [5, 5], weights_initializer=tf.truncated_normal_initializer(stddev=0.03), scope='conv2') net = slim.fully_connected(net, 1000, activation_fn=None, scope='fc')  

寫兩個arg_scope就行了。采用如上方法,定義一個VGG也就十幾行代碼的事了。

#定義一個vgg16網絡
def vgg16(inputs): with slim.arg_scope([slim.conv2d, slim.fully_connected], activation_fn=tf.nn.relu, weights_initializer=tf.truncated_normal_initializer(0.0, 0.01), weights_regularizer=slim.l2_regularizer(0.0005)): net = slim.repeat(inputs, 2, slim.conv2d, 64, [3, 3], scope='conv1') net = slim.max_pool2d(net, [2, 2], scope='pool1') net = slim.repeat(net, 2, slim.conv2d, 128, [3, 3], scope='conv2') net = slim.max_pool2d(net, [2, 2], scope='pool2') net = slim.repeat(net, 3, slim.conv2d, 256, [3, 3], scope='conv3') net = slim.max_pool2d(net, [2, 2], scope='pool3') net = slim.repeat(net, 3, slim.conv2d, 512, [3, 3], scope='conv4') net = slim.max_pool2d(net, [2, 2], scope='pool4') net = slim.repeat(net, 3, slim.conv2d, 512, [3, 3], scope='conv5') net = slim.max_pool2d(net, [2, 2], scope='pool5') net = slim.fully_connected(net, 4096, scope='fc6') net = slim.dropout(net, 0.5, scope='dropout6') net = slim.fully_connected(net, 4096, scope='fc7') net = slim.dropout(net, 0.5, scope='dropout7') net = slim.fully_connected(net, 1000, activation_fn=None, scope='fc8') return net  

 

三.訓練模型

這里直接選用經典網絡。

import tensorflow as tf vgg = tf.contrib.slim.nets.vgg # Load the images and labels. 
images, labels = ... # Create the model. 
predictions, _ = vgg.vgg_16(images) # Define the loss functions and get the total loss. 
loss = slim.losses.softmax_cross_entropy(predictions, labels)  

關於loss,要說一下定義自己的loss的方法,以及注意不要忘記加入到slim中讓slim看到你的loss。

還有正則項也是需要手動添加進loss當中的,不然最后計算的時候就不優化正則目標了。

# Load the images and labels. 
images, scene_labels, depth_labels, pose_labels = ... # Create the model. 
scene_predictions, depth_predictions, pose_predictions = CreateMultiTaskModel(images) # Define the loss functions and get the total loss. 
classification_loss = slim.losses.softmax_cross_entropy(scene_predictions, scene_labels) sum_of_squares_loss = slim.losses.sum_of_squares(depth_predictions, depth_labels) pose_loss = MyCustomLossFunction(pose_predictions, pose_labels) slim.losses.add_loss(pose_loss) # Letting TF-Slim know about the additional loss. 
  
# The following two ways to compute the total loss are equivalent: 
regularization_loss = tf.add_n(slim.losses.get_regularization_losses()) total_loss1 = classification_loss + sum_of_squares_loss + pose_loss + regularization_loss # (Regularization Loss is included in the total loss by default). 
total_loss2 = slim.losses.get_total_loss()  

slim在learning.py中提供了一個簡單而有用的訓練模型的工具。我們只需調用slim.learning.create_train_op 和slim.learning.train就可以完成優化過程。

slim.learning.train函數被用來訓練神經網絡,函數定義如下:

def slim.learning.train(train_op,
          logdir,
          train_step_fn=train_step,
          train_step_kwargs=_USE_DEFAULT,
          log_every_n_steps=1,
          graph=None,
          master='',
          is_chief=True,
          global_step=None,
          number_of_steps=None,
          init_op=_USE_DEFAULT,
          init_feed_dict=None,
          local_init_op=_USE_DEFAULT,
          init_fn=None,
          ready_op=_USE_DEFAULT,
          summary_op=_USE_DEFAULT,
          save_summaries_secs=600,
          summary_writer=_USE_DEFAULT,
          startup_delay_steps=0,
          saver=None,
          save_interval_secs=600,
          sync_optimizer=None,
          session_config=None,
          trace_every_n_steps=None):

其中部分參數的說明如下:

  • train_op: A `Tensor` that, when executed, will apply the gradients and return the loss value.
  • logdir: The directory where training logs are written to. If None, model checkpoints and summaries will not be written.檢查點文件和日志文件的保存路徑。
  • number_of_steps: The max number of gradient steps to take during training,as measured by 'global_step': training will stop if global_step is greater than 'number_of_steps'. If the value is left as None, training proceeds indefinitely.默認是一致循環訓練。
  • save_summaries_secs: How often, in seconds, to save summaries.
  • summary_writer: `SummaryWriter` to use. Can be `None` to indicate that no summaries should be written. If unset, we create a SummaryWriter.
  • startup_delay_steps: The number of steps to wait for before beginning. Note that this must be 0 if a sync_optimizer is supplied.
  • saver: Saver to save checkpoints. If None, a default one will be created and used.
  • save_interval_secs: How often, in seconds, to save the model to `logdir`.
g = tf.Graph() # Create the model and specify the losses...
... total_loss = slim.losses.get_total_loss() optimizer = tf.train.GradientDescentOptimizer(learning_rate) # create_train_op ensures that each time we ask for the loss, the update_ops # are run and the gradients being computed are applied too.
train_op = slim.learning.create_train_op(total_loss, optimizer) logdir = ... # Where checkpoints are stored.
 slim.learning.train( train_op, logdir, number_of_steps=1000,     #迭代次數
          save_summaries_secs=300,        #存summary間隔秒數
          save_interval_secs=600)             #存模型間隔秒數

 

四.讀取保存模型變量

在遷移學習中,我們經常會用到別人已經訓練好的網絡和模型參數,這時候我們可能需要從檢查點文件中加載部分變量,下面我就會講解如何加載指定變量。以及當前圖的變量名和檢查點文件中變量名不一致時怎么辦。

1. 從檢查恢復部分變量

通過以下功能我們可以載入模型的部分變量:

# Create some variables.
v1 = tf.Variable(..., name="v1")
v2 = tf.Variable(..., name="v2")
...
# Add ops to restore all the variables.
restorer = tf.train.Saver()

# Add ops to restore some variables.
restorer = tf.train.Saver([v1, v2])

# Later, launch the model, use the saver to restore variables from disk, and
# do some work with the model.
with tf.Session() as sess:
  # Restore variables from disk.
  restorer.restore(sess, "/tmp/model.ckpt")
  print("Model restored.")
  # Do some work with the model
  ...

通過這種方式我們可以加載不同變量名的變量!

 2 從從檢查點恢復部分變量還可以采用其他方法

# Create some variables.
v1 = slim.variable(name="v1", ...)
v2 = slim.variable(name="nested/v2", ...)
...

# Get list of variables to restore (which contains only 'v2'). These are all
# equivalent methods:
#從檢查點文件中恢復name='v2'的變量
variables_to_restore = slim.get_variables_by_name("v2")     
# or 從檢查點文件中恢復name帶有2的所有變量
variables_to_restore = slim.get_variables_by_suffix("2")     
# or 從檢查點文件中恢復命名空間scope='nested'的所有變量
variables_to_restore = slim.get_variables(scope="nested")    
# or 恢復命名空間scope='nested'的所有變量
variables_to_restore = slim.get_variables_to_restore(include=["nested"])  
# or 除了命名空間scope='v1'的變量
variables_to_restore = slim.get_variables_to_restore(exclude=["v1"])      

# Create the saver which will be used to restore the variables.
restorer = tf.train.Saver(variables_to_restore)

with tf.Session() as sess:
  # Restore variables from disk.
  restorer.restore(sess, "/tmp/model.ckpt")
  print("Model restored.")
  # Do some work with the model
  ...

3.當圖的變量名與checkpoint中的變量名不同時,恢復模型參數

當從checkpoint文件中恢復變量時,Saver在checkpoint文件中定位到變量名,並且把它們映射到當前圖中的變量中。之前的例子中,我們創建了Saver,並為其提供了變量列表作為參數。這時,在checkpoint文件中定位的變量名,是隱含地從每個作為參數給出的變量的var.op.name而獲得的。這一方式在圖與checkpoint文件中變量名字相同時,可以很好的工作。而當名字不同時,必須給Saver提供一個將checkpoint文件中的變量名映射到圖中的每個變量的字典。

假設我們定義的網絡變量是conv1/weights,而從VGG檢查點文件加載的變量名為vgg16/conv1/weights,正常load肯定會報錯(找不到變量名),但是可以這樣:例子見下:

# Assuming that 'conv1/weights' should be restored from 'vgg16/conv1/weights'
def name_in_checkpoint(var):
  return 'vgg16/' + var.op.name

# Assuming that 'conv1/weights' and 'conv1/bias' should be restored from 'conv1/params1' and 'conv1/params2'
def name_in_checkpoint(var):
  if "weights" in var.op.name:
    return var.op.name.replace("weights", "params1")
  if "bias" in var.op.name:
    return var.op.name.replace("bias", "params2")

variables_to_restore = slim.get_model_variables()
variables_to_restore = {name_in_checkpoint(var):var for var in variables_to_restore}
restorer = tf.train.Saver(variables_to_restore)

with tf.Session() as sess:
  # Restore variables from disk.
  restorer.restore(sess, "/tmp/model.ckpt")

4.在一個不同的任務上對網絡進行微調

比如我們要將1000類的imagenet分類任務應用於20類的Pascal VOC分類任務中,我們只導入部分層,見下例:

image, label = MyPascalVocDataLoader(...)
images, labels = tf.train.batch([image, label], batch_size=32)

# Create the model,20類
predictions = vgg.vgg_16(images,num_classes=20)

train_op = slim.learning.create_train_op(...)

# Specify where the Model, trained on ImageNet, was saved.
model_path = '/path/to/pre_trained_on_imagenet.checkpoint'

# Specify where the new model will live:
log_dir = '/path/to/my_pascal_model_dir/'

# Restore only the convolutional layers: 從檢查點載入除了fc6,fc7,fc8層之外的參數
variables_to_restore = slim.get_variables_to_restore(exclude=['fc6', 'fc7', 'fc8'])
init_fn = assign_from_checkpoint_fn(model_path, variables_to_restore)

# Start training.
slim.learning.train(train_op, log_dir, init_fn=init_fn)

 下面會顯示一個具體遷移學習的案例。

 

五 預訓練

如果我們仍然是對1000類的數據集進行分類,此時我們可以利用訓練好的模型參數進行初始化,然后繼續訓練。

文件夾結構如下,不懂得話,可以參考第二十二節,TensorFlow中的圖片分類模型庫slim的使用、數據集處理,其中vgg預訓練模型下載地址:https://github.com/tensorflow/models/tree/master/research/slim/#Pretrained

代碼如下:

def retrain():
    '''
    演示一個VGG16網絡的例子
    從頭開始訓練
    '''    
    batch_size = 128
    
    learning_rate = 1e-4
    
    #用於保存微調后的檢查點文件和日志文件路徑
    train_log_dir = './log/vgg16/slim_retrain'    
    
    #官方下載的檢查點文件路徑
    checkpoint_file = './log/vgg16/vgg_16.ckpt'
    
    if not tf.gfile.Exists(train_log_dir):
        tf.gfile.MakeDirs(train_log_dir)
    
    #創建一個圖,作為當前圖
    with tf.Graph().as_default():
        
        #加載數據
        train_images, train_labels = ....


        #創建vgg16網絡  如果想凍結所有層,可以指定slim.conv2d中的 trainable=False
        logits,end_points =  vgg.vgg_16(train_images, is_training=True)        
        
        #交叉熵代價函數
        slim.losses.softmax_cross_entropy(logits, onehot_labels=train_labels)
        total_loss = slim.losses.get_total_loss()
      
        #設置寫入到summary中的變量
        tf.summary.scalar('losses/total_loss', total_loss)
    
        '''
        設置優化器 這里不能指定成Adam優化器,因為我們的官方模型文件中使用的就是GradientDescentOptimizer優化器,
        因此我們要和官方模型一致,如果想使用AdamOptimizer優化器,我們可以在調用完vgg16()網絡后,就執行恢復模型。
        而把執行恢復模型的代碼放在后面,會由於我們在當前圖中定義了一些檢查點中不存在變量,恢復時在檢查點文件找不
        到變量,因此會報錯。
        '''
        optimizer = tf.train.GradientDescentOptimizer(learning_rate)
        #optimizer = tf.train.AdamOptimizer(learning_rate)
        # create_train_op that ensures that when we evaluate it to get the loss,
        # the update_ops are done and the gradient updates are computed.
        train_tensor = slim.learning.create_train_op(total_loss, optimizer)
        
        # Restore only the convolutional layers: 從檢查點載入除了fc8層之外的參數到當前圖             
        variables_to_restore = slim.get_variables_to_restore(exclude=['vgg_16/fc8'])        
        init_fn = slim.assign_from_checkpoint_fn(checkpoint_file, variables_to_restore)
        

        print('開始訓練!')
        #開始訓練網絡        
        slim.learning.train(train_tensor,
                            train_log_dir,
                            number_of_steps=100,             #迭代次數 一次迭代batch_size個樣本
                            save_summaries_secs=300,         #存summary間隔秒數
                            save_interval_secs=300,          #存模模型間隔秒數                         
                            init_fn=init_fn)

 

六 微調

有時候我們數據集比較少的時候,可能使用已經訓練的網絡模型。比如我們想對flowers數據集進行分類。該數據集分成了兩部分,訓練集數據有3320張,校驗集數據有350張。我們使用slim庫下已經寫好的vgg16網絡,並下載對應的模型參數文件。由於模型參數是針對ImageNet數據集訓練的得到的,而我們Flower數據集只有5類,因此需要把vgg16最后一層分類數改為5。

這里我們仍然先使用TensorFlow的網絡架構來實現微調功能,后面我們再演示一個使用slim庫簡化之后的代碼。

1.TensorFlow實現代碼

# -*- coding: utf-8 -*-
"""
Created on Wed Jun  6 11:56:58 2018

@author: zy
"""

'''
利用已經訓練好的vgg16網絡對flowers數據集進行微調
把最后一層分類由2000->5 然后重新訓練,我們也可以凍結其它所有層,只訓練最后一層
'''

from nets import vgg
import matplotlib.pyplot as plt
import tensorflow as tf
import numpy as np
import input_data
import os

slim = tf.contrib.slim

DATA_DIR = './datasets/data/flowers'
#輸出類別
NUM_CLASSES = 5
    
#獲取圖片大小
IMAGE_SIZE = vgg.vgg_16.default_image_size



def flowers_fine_tuning():
    '''
    演示一個VGG16的例子 
    微調 這里只調整VGG16最后一層全連接層,把1000類改為5類 
    對網絡進行訓練
    '''
        
    '''
    1.設置參數,並加載數據
    '''
    #用於保存微調后的檢查點文件和日志文件路徑
    train_log_dir = './log/vgg16/fine_tune'
    train_log_file = 'flowers_fine_tune.ckpt'
    
    #官方下載的檢查點文件路徑
    checkpoint_file = './log/vgg16/vgg_16.ckpt'
    
    #設置batch_size
    batch_size  = 256
    
    learning_rate = 1e-4
    
    #訓練集數據長度
    n_train = 3320
    #測試集數據長度
    #n_test = 350
    #迭代輪數
    training_epochs = 3
    
    display_epoch = 1
    
    if not tf.gfile.Exists(train_log_dir):
        tf.gfile.MakeDirs(train_log_dir)
                        
    #加載數據
    train_images, train_labels = input_data.get_batch_images_and_label(DATA_DIR,batch_size,NUM_CLASSES,True,IMAGE_SIZE,IMAGE_SIZE)          
    test_images, test_labels = input_data.get_batch_images_and_label(DATA_DIR,batch_size,NUM_CLASSES,False,IMAGE_SIZE,IMAGE_SIZE)          

    #獲取模型參數的命名空間
    arg_scope = vgg.vgg_arg_scope()

    #創建網絡
    with  slim.arg_scope(arg_scope):
        
        '''
        2.定義占位符和網絡結構
        '''        
        #輸入圖片
        input_images = tf.placeholder(dtype=tf.float32,shape = [None,IMAGE_SIZE,IMAGE_SIZE,3])
        #圖片標簽
        input_labels = tf.placeholder(dtype=tf.float32,shape = [None,NUM_CLASSES])        
        #訓練還是測試?測試的時候棄權參數會設置為1.0
        is_training = tf.placeholder(dtype = tf.bool)
        
        #創建vgg16網絡  如果想凍結所有層,可以指定slim.conv2d中的 trainable=False
        logits,end_points =  vgg.vgg_16(input_images, is_training=is_training,num_classes = NUM_CLASSES)
        #print(end_points)  每個元素都是以vgg_16/xx命名
            
        
        '''
        #從當前圖中搜索指定scope的變量,然后從檢查點文件中恢復這些變量(即vgg_16網絡中定義的部分變量)  
        #如果指定了恢復檢查點文件中不存在的變量,則會報錯 如果不知道檢查點文件有哪些變量,我們可以打印檢查點文件查看變量名
        params = []
        conv1 = slim.get_variables(scope="vgg_16/conv1")
        params.extend(conv1)            
        conv2 = slim.get_variables(scope="vgg_16/conv2")
        params.extend(conv2)
        conv3 = slim.get_variables(scope="vgg_16/conv3")
        params.extend(conv3)
        conv4 = slim.get_variables(scope="vgg_16/conv4")
        params.extend(conv4)
        conv5 = slim.get_variables(scope="vgg_16/conv5")
        params.extend(conv5)
        fc6 = slim.get_variables(scope="vgg_16/fc6")
        params.extend(fc6)
        fc7 = slim.get_variables(scope="vgg_16/fc7")
        params.extend(fc7)          
        '''

        # Restore only the convolutional layers: 從檢查點載入當前圖除了fc8層之外所有變量的參數
        params = slim.get_variables_to_restore(exclude=['vgg_16/fc8'])
        #用於恢復模型  如果使用這個保存或者恢復的話,只會保存或者恢復指定的變量
        restorer = tf.train.Saver(params) 
        
        #預測標簽
        pred = tf.argmax(logits,axis=1)

        '''
        3 定義代價函數和優化器
        '''                
        #代價函數
        cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=input_labels,logits=logits))
        
        #設置優化器
        optimizer = tf.train.AdamOptimizer(learning_rate).minimize(cost)
        
        #預測結果評估        
        correct = tf.equal(pred,tf.argmax(input_labels,1))                    #返回一個數組 表示統計預測正確或者錯誤 
        accuracy = tf.reduce_mean(tf.cast(correct,tf.float32))                #求准確率
                            
        num_batch = int(np.ceil(n_train / batch_size))
        
        #用於保存檢查點文件 
        save = tf.train.Saver(max_to_keep=1) 

        #恢復模型
        with tf.Session() as sess:      
            sess.run(tf.global_variables_initializer())
                    
            #檢查最近的檢查點文件
            ckpt = tf.train.latest_checkpoint(train_log_dir)
            if ckpt != None:
                save.restore(sess,ckpt)
                print('從上次訓練保存后的模型繼續訓練!')
            else:
                restorer.restore(sess, checkpoint_file)                
                print('從官方模型加載訓練!')

                                   
            #創建一個協調器,管理線程
            coord = tf.train.Coordinator()  
            
            #啟動QueueRunner, 此時文件名才開始進隊。
            threads = tf.train.start_queue_runners(sess=sess,coord=coord)                      

            '''
            4 查看預處理之后的圖片
            '''            
            imgs, labs = sess.run([train_images, train_labels])                  
            print('原始訓練圖片信息:',imgs.shape,labs.shape)
            show_img = np.array(imgs[0],dtype=np.uint8)
            plt.imshow(show_img)                                
            plt.title('Original train image')   
            plt.show()

                                    
            imgs, labs = sess.run([test_images, test_labels])                  
            print('原始測試圖片信息:',imgs.shape,labs.shape)
            show_img = np.array(imgs[0],dtype=np.uint8)
            plt.imshow(show_img)                                
            plt.title('Original test image')   
            plt.show()
            
                        
            print('開始訓練!')
            for epoch in range(training_epochs):                
                total_cost = 0.0                
                for i in range(num_batch):
                    imgs, labs = sess.run([train_images, train_labels])                                                
                    _,loss = sess.run([optimizer,cost],feed_dict={input_images:imgs,input_labels:labs,is_training:True})
                    total_cost += loss
            
                #打印信息
                if epoch % display_epoch == 0:
                    print('Epoch {}/{}  average cost {:.9f}'.format(epoch+1,training_epochs,total_cost/num_batch))
                    
                #進行預測處理
                imgs, labs = sess.run([test_images, test_labels])                                                                    
                cost_values,accuracy_value = sess.run([cost,accuracy],feed_dict = {input_images:imgs,input_labels:labs,is_training:False})
                print('Epoch {}/{}  Test cost {:.9f}'.format(epoch+1,training_epochs,cost_values))
                print('准確率:',accuracy_value)
                
                
                #保存模型
                save.save(sess,os.path.join(train_log_dir,train_log_file),global_step = epoch)
                print('Epoch {}/{}  模型保存成功'.format(epoch+1,training_epochs))
                
            print('訓練完成')
                    
            #終止線程
            coord.request_stop()
            coord.join(threads)  
            
def flowers_test():
    '''
    使用微調好的網絡進行測試
    '''          
    '''
    1.設置參數,並加載數據
    '''
    #微調后的檢查點文件和日志文件路徑
    save_dir = './log/vgg16/fine_tune'
    
    #設置batch_size
    batch_size  = 128
    
                        
    #加載數據
    train_images, train_labels = input_data.get_batch_images_and_label(DATA_DIR,batch_size,NUM_CLASSES,True,IMAGE_SIZE,IMAGE_SIZE)          
    test_images, test_labels = input_data.get_batch_images_and_label(DATA_DIR,batch_size,NUM_CLASSES,False,IMAGE_SIZE,IMAGE_SIZE)          

    #獲取模型參數的命名空間
    arg_scope = vgg.vgg_arg_scope()

    #創建網絡
    with  slim.arg_scope(arg_scope):
        
        '''
        2.定義占位符和網絡結構
        '''        
        #輸入圖片
        input_images = tf.placeholder(dtype=tf.float32,shape = [None,IMAGE_SIZE,IMAGE_SIZE,3])     
        #訓練還是測試?測試的時候棄權參數會設置為1.0
        is_training = tf.placeholder(dtype = tf.bool)
        
        #創建vgg16網絡
        logits,end_points =  vgg.vgg_16(input_images, is_training=is_training,num_classes = NUM_CLASSES)
         
        #預測標簽
        pred = tf.argmax(logits,axis=1)             
        
        restorer = tf.train.Saver() 
        
        #恢復模型
        with tf.Session() as sess:      
            sess.run(tf.global_variables_initializer())
            ckpt = tf.train.latest_checkpoint(save_dir)
            if ckpt != None:
                #恢復模型
                restorer.restore(sess,ckpt)
                print("Model restored.")
                                   
            #創建一個協調器,管理線程
            coord = tf.train.Coordinator()  
            
            #啟動QueueRunner, 此時文件名才開始進隊。
            threads = tf.train.start_queue_runners(sess=sess,coord=coord)                      

            '''
            查看預處理之后的圖片
            '''            
            imgs, labs = sess.run([test_images, test_labels])                  
            print('原始測試圖片信息:',imgs.shape,labs.shape)
            show_img = np.array(imgs[0],dtype=np.uint8)
            plt.imshow(show_img)                                
            plt.title('Original test image')   
            plt.show()
                         
            pred_value = sess.run(pred,feed_dict = {input_images:imgs,is_training:False})
            print('預測結果為:',pred_value)
            print('實際結果為:',np.argmax(labs,1))
            correct = np.equal(pred_value,np.argmax(labs,1))
            print('准確率為:',  np.mean(correct))
                
            #終止線程
            coord.request_stop()
            coord.join(threads)  
            
            
            
    
if __name__ == '__main__':
    tf.reset_default_graph()    
    flowers_fine_tuning()
    flowers_test()

 這里我在訓練的時候,凍結了出輸出層之外的所有層,運行結果如下:

 

三輪之后,我們可以看到准確率大概在60%。

 如果我們不凍結其它層,(訓練所有層,速度慢),3輪下來,准確率可以達到90%左右。

 

2.Slim庫實現代碼

使用slim庫簡化上面的代碼:

           
def flowers_simple_fine_tuning():
    '''
    演示一個VGG16的例子 
    微調 這里只調整VGG16最后一層全連接層,把1000類改為5類 
    對網絡進行訓練   使用slim庫簡化代碼
    '''    
    batch_size = 128
    
    learning_rate = 1e-4
    
    #用於保存微調后的檢查點文件和日志文件路徑
    train_log_dir = './log/vgg16/slim_fine_tune'    
    
    #官方下載的檢查點文件路徑
    checkpoint_file = './log/vgg16/vgg_16.ckpt'
    
    if not tf.gfile.Exists(train_log_dir):
        tf.gfile.MakeDirs(train_log_dir)
    
    #創建一個圖,作為當前圖
    with tf.Graph().as_default():
        
        #加載數據
        train_images, train_labels = input_data.get_batch_images_and_label(DATA_DIR,batch_size,NUM_CLASSES,True,IMAGE_SIZE,IMAGE_SIZE)          


        #創建vgg16網絡  如果想凍結所有層,可以指定slim.conv2d中的 trainable=False
        logits,end_points =  vgg.vgg_16(train_images, is_training=True,num_classes = NUM_CLASSES)        
        
        #交叉熵代價函數
        slim.losses.softmax_cross_entropy(logits, onehot_labels=train_labels)
        total_loss = slim.losses.get_total_loss()
      
        #設置寫入到summary中的變量
        tf.summary.scalar('losses/total_loss', total_loss)
    
        '''
        設置優化器 這里不能指定成Adam優化器,因為我們的官方模型文件中使用的就是GradientDescentOptimizer優化器,
        因此我們要和官方模型一致,如果想使用AdamOptimizer優化器,我們可以在調用完vgg16()網絡后,就執行恢復模型。
        而把執行恢復模型的代碼放在后面,會由於我們在當前圖中定義了一些檢查點中不存在變量,恢復時在檢查點文件找不
        到變量,因此會報錯。
        '''
        optimizer = tf.train.GradientDescentOptimizer(learning_rate)
        #optimizer = tf.train.AdamOptimizer(learning_rate)
        # create_train_op that ensures that when we evaluate it to get the loss,
        # the update_ops are done and the gradient updates are computed.
        train_tensor = slim.learning.create_train_op(total_loss, optimizer)
        
        #檢查最近的檢查點文件
        ckpt = tf.train.latest_checkpoint(train_log_dir)
        if ckpt != None:
            variables_to_restore = slim.get_model_variables()
            init_fn = slim.assign_from_checkpoint_fn(ckpt,variables_to_restore)
            print('從上次訓練保存后的模型繼續訓練!')
        else:
            # Restore only the convolutional layers: 從檢查點載入除了fc8層之外的參數到當前圖             
            variables_to_restore = slim.get_variables_to_restore(exclude=['vgg_16/fc8']) 
            init_fn = slim.assign_from_checkpoint_fn(checkpoint_file, variables_to_restore)
            print('從官方模型加載訓練!')
        

        print('開始訓練!')
        #開始訓練網絡        
        slim.learning.train(train_tensor,
                            train_log_dir,
                            number_of_steps=100,             #迭代次數 一次迭代batch_size個樣本
                            save_summaries_secs=300,         #存summary間隔秒數
                            save_interval_secs=300,          #存模模型間隔秒數                         
                            init_fn=init_fn)

 

上面的代碼中我們用到了input_data.py文件,主要負責加載數據集,程序如下:

# -*- coding: utf-8 -*-
"""
Created on Fri Jun  8 08:52:30 2018

@author: zy
"""

'''
導入flowers數據集
'''

from datasets import download_and_convert_flowers
from preprocessing import vgg_preprocessing
from datasets import flowers
import tensorflow as tf


slim = tf.contrib.slim


def read_flower_image_and_label(dataset_dir,is_training=False):
    '''
    下載flower_photos.tgz數據集  
    切分訓練集和驗證集
    並將數據轉換成TFRecord格式  5個訓練數據文件(3320),5個驗證數據文件(350),還有一個標簽文件(存放每個數字標簽對應的類名)
            
    args:
        dataset_dir:數據集所在的目錄
        is_training:設置為TRue,表示加載訓練數據集,否則加載驗證集
    return:
        image,label:返回隨機讀取的一張圖片,和對應的標簽
    '''    
    download_and_convert_flowers.run(dataset_dir)    
    '''
    利用slim讀取TFRecord中的數據
    '''
    #選擇數據集train
    if is_training:        
        dataset = flowers.get_split(split_name = 'train',dataset_dir=dataset_dir)
    else:
        dataset = flowers.get_split(split_name = 'validation',dataset_dir=dataset_dir)
    
    #創建一個數據provider
    provider = slim.dataset_data_provider.DatasetDataProvider(dataset)
    
    #通過provider的get隨機獲取一條樣本數據 返回的是兩個張量
    [image,label] = provider.get(['image','label'])

    return image,label



def get_batch_images_and_label(dataset_dir,batch_size,num_classes,is_training=False,output_height=224, output_width=224,num_threads=10):
    '''
    每次取出batch_size個樣本
    
    注意:這里預處理調用的是slim庫圖片預處理的函數,例如:如果你使用的vgg網絡,就調用vgg網絡的圖像預處理函數
          如果你使用的是自己定義的網絡,則可以自己寫適合自己圖像的預處理函數,比如歸一化處理也可以使用其他網絡已經寫好的預處理函數
    
    args:
         dataset_dir:數據集所在的目錄
         batch_size:一次取出的樣本數量
         num_classes:輸出的類別 用於對標簽one_hot編碼
         is_training:設置為TRue,表示加載訓練數據集,否則加載驗證集
         output_height:輸出圖片高度
         output_width:輸出圖片寬
         
     return:
        images,labels:返回隨機讀取的batch_size張圖片,和對應的標簽one_hot編碼
    '''
    #獲取單張圖像和標簽
    image,label = read_flower_image_and_label(dataset_dir,is_training)   
    # 圖像預處理 這里要求圖片數據是tf.float32類型的
    image = vgg_preprocessing.preprocess_image(image, output_height, output_width,is_training=is_training)
    
    #縮放處理
    #image = tf.image.convert_image_dtype(image, dtype=tf.float32)  
    #image = tf.image.resize_image_with_crop_or_pad(image, output_height, output_width)
    
    #  shuffle_batch 函數會將數據順序打亂
    #  bacth 函數不會將數據順序打亂    
    images, labels = tf.train.batch(
                [image, label],
                batch_size = batch_size,
                capacity=5 * batch_size, 
                num_threads = num_threads)    
        
    #one-hot編碼
    labels = slim.one_hot_encoding(labels,num_classes)
    
    return images,labels
View Code

3.CNN網絡代碼,與vgg16微調效果對比

我們這里使用三層的cnn網絡對flower數據集進行分類,測試一下其效果如何:

# -*- coding: utf-8 -*-
"""
Created on Fri Jun  8 08:51:45 2018

@author: zy
"""

'''
使用卷積神經網絡訓練flowers數據集
用來和微調后的VGG網絡對比
'''

import tensorflow as tf
import input_data
import numpy as np

slim = tf.contrib.slim


def cnn(inputs,num_classes=5):
    '''
    定義一個cnn網絡結構
    
    args:
        inputs:輸入形狀為[batch_size,in_height,in_width,in_channel]    
        輸入圖片大小為224 x 224 x3
        num_classes:類別數
    
    '''
    with tf.variable_scope('cnn'):
        with slim.arg_scope([slim.conv2d,slim.fully_connected,slim.max_pool2d,slim.avg_pool2d],
                            padding='SAME',  
                            ):
            net = slim.conv2d(inputs,64,[5,5],4,weights_initializer=tf.truncated_normal_initializer(stddev=0.01),scope='conv1')    #batch_size x 56 x 56 x64
            net = slim.max_pool2d(net,[2,2],scope='pool1')        #batch_size x 28 x 28 x64
            net = slim.conv2d(net,64,[3,3],2,weights_initializer=tf.truncated_normal_initializer(stddev=0.01),scope='conv2')    #batch_size x 14 x 14 x64
            net = slim.max_pool2d(net,[2,2],scope='pool2')        #batch_size x 7 x 7 x64            
            #net = slim.conv2d(net,num_classes,[7,7],7,weights_initializer=tf.truncated_normal_initializer(stddev=0.01),scope='conv3')    #batch_size x 1 x 1 x num_classes
            net = slim.conv2d(net,num_classes,[1,1],1,weights_initializer=tf.truncated_normal_initializer(stddev=0.01),scope='conv3')    #batch_size x7 x 7 xnum_classes
            net = slim.avg_pool2d(net,[7,7],7,scope='pool3')                     #全局平均池化層            
            net = tf.squeeze(net,[1,2])                            #batch_size x num_classes            
            return net
        
                
DATA_DIR = './datasets/data/flowers'
#輸出類別
NUM_CLASSES = 5
IMAGE_SIZE = 224


def flower_cnn():
    '''
    使用CNN網絡訓練flower數據集
    '''
    #設置batch_size
    batch_size  = 128
    
    learning_rate = 1e-4
    
    #訓練集數據長度
    n_train = 3320
    #測試集數據長度
    #n_test = 350
    #迭代輪數
    training_epochs = 20
    
    display_epoch = 1

    #加載數據
    train_images, train_labels = input_data.get_batch_images_and_label(DATA_DIR,batch_size,NUM_CLASSES,True,IMAGE_SIZE,IMAGE_SIZE)          
    test_images, test_labels = input_data.get_batch_images_and_label(DATA_DIR,batch_size,NUM_CLASSES,True,IMAGE_SIZE,IMAGE_SIZE)          
    
        
    #定義占位符
    input_images = tf.placeholder(dtype=tf.float32,shape = [None,IMAGE_SIZE,IMAGE_SIZE,3])
    input_labels = tf.placeholder(dtype=tf.float32,shape = [None,NUM_CLASSES])
    is_training = tf.placeholder(dtype = tf.bool)
        
    #創建cnn網絡
    logits =  cnn(input_images,num_classes = NUM_CLASSES)
     
    #預測標簽
    pred = tf.argmax(logits,axis=1)
     
    #代價函數
    cost = tf.reduce_mean(tf.nn.softmax_cross_entropy_with_logits(labels=input_labels,logits=logits))
   
    #設置優化器
    optimizer = tf.train.AdamOptimizer(learning_rate).minimize(cost)
    
    #預測結果評估        
    correct = tf.equal(pred,tf.argmax(input_labels,1))                    #返回一個數組 表示統計預測正確或者錯誤 
    accuracy = tf.reduce_mean(tf.cast(correct,tf.float32))                #求准確率
                        
    num_batch = int(np.ceil(n_train / batch_size))
    
    '''
    啟動會話,開始訓練
    '''
    with tf.Session() as sess:      
        sess.run(tf.global_variables_initializer())
                
        #創建一個協調器,管理線程
        coord = tf.train.Coordinator()  
        
        #啟動QueueRunner, 此時文件名才開始進隊。
        threads=tf.train.start_queue_runners(sess=sess,coord=coord)                      
                                
        print('開始訓練!')
        for epoch in range(training_epochs):
            total_cost = 0.0
            for i in range(num_batch):
                imgs, labs = sess.run([train_images, train_labels])                                                
                _,loss = sess.run([optimizer,cost],feed_dict={input_images:imgs,input_labels:labs,is_training:True})
                total_cost += loss
        
            #打印信息
            if epoch % display_epoch == 0:
                print('Epoch {}/{}  Train average cost {:.9f}'.format(epoch+1,training_epochs,total_cost/num_batch))
                #進行預測處理
                imgs, labs = sess.run([test_images, test_labels])                                                
                cost_values,accuracy_value = sess.run([cost,accuracy],feed_dict = {input_images:imgs,input_labels:labs,is_training:False})
                print('Epoch {}/{}  Test cost {:.9f}'.format(epoch+1,training_epochs,cost_values))
                print('准確率:',accuracy_value)

        print('訓練完成')                
        #終止線程
        coord.request_stop()
        coord.join(threads)  
            
                
if __name__ == '__main__':
    tf.reset_default_graph()    
    flower_cnn()
View Code

 

我們可以看到20輪下來准確率大概在55%,效果並不是很好。而使用vgg16微調的效果明顯更高。

參考文章

[1]【Tensorflow】輔助工具篇——tensorflow slim(TF-Slim)介紹

[2]TF-Slim簡介


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM