在介紹這一節之前,需要你對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
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()
我們可以看到20輪下來准確率大概在55%,效果並不是很好。而使用vgg16微調的效果明顯更高。
參考文章
[1]【Tensorflow】輔助工具篇——tensorflow slim(TF-Slim)介紹
[2]TF-Slim簡介