在之前的文章中,我們利用silm工具和谷歌訓練好的inception-v3模型完成了一個花朵圖像分類問題,但代碼還是比較繁瑣。為了更精簡的代碼和提高可讀性,這一次我們利用TensorFlow提供的高階API Estimator來解決同樣的問題。同時,在最后,我們會把訓練過程中的參數變化通過TensorBoard展示出來。
Estimator
Estimator是TensorFlow官方提供的一個高層API,它更好的整合了原生態TensorFlow提供的功能。它可以極大簡化機器學習編程。下面來看一下TensorFlow API結構:
在官方文檔中,有這么一句話:
We strongly recommend writing TensorFlow programs with the following APIs:
- Estimators, which represent a complete model. The Estimator API provides methods to train the model, to judge the model's accuracy, and to generate predictions.
- Datasets for Estimators, which build a data input pipeline. The Dataset API has methods to load and manipulate data, and feed it into your model. The Dataset API meshes well with the Estimators API.
可以看到Estimator和Dataset這兩個API是官方強烈推薦的。Estimator提供了預創建的DNN模型,使用起來非常方便。具體怎么使用Estimator預創建模型,官方文檔里面也有寫,有興趣的可以去看Estimator官方。
但是預先定義的Estimator功能有限,比如目前無法很好的實現卷積神經網絡和循環神經網絡,也沒有辦法支持自定義的損失函數,所以為了更好的使用Estimator,這篇文章會教大家怎么用Estimator自定義CNN模型,以及如何配合Dataset讀取圖片數據。
數據準備
在這里我們可以使用之前的谷歌提供的花朵分類數據集,也可以使用其它的。為了區分上次結果這次我們使用新的數據集。在這里我使用百度挑桃分類數據集。下載解壓后可以看到是這樣的目錄:數據集已經幫我們劃分好了是訓練還是測試。每一個文件夾代表一種桃子,總共有4種桃子(這個數據集肉眼很難辨別,可能是因為我不夠專業-_-)。
數據預處理
我們還是像之前一樣對數據預處理。在工程目錄下新建select_peach_data.py文件。跟之前處理花朵分類的時候一樣所以這里直接粘貼代碼:
import glob
import os.path
import numpy as np
import tensorflow as tf
from tensorflow.python.platform import gfile
#輸入圖片地址
INPUT_ALL_DATA = './select_peach'
INPUT_TRAIN_DATA = './select_peach/train'
INPUT_TEST_DATA = './select_peach/test'
OUTPUT_TRAIN_FILE = './path/to/output_train.tfrecords'
OUTPUT_TEST_FILE = './path/to/output_test.tfrecords'
def _int64_feature(value):
return tf.train.Feature(int64_list=tf.train.Int64List(value=[value]))
#生成字符串的屬性
def _bytes_feature(value):
return tf.train.Feature(bytes_list=tf.train.BytesList(value=[value]))
#檢索目錄并提取目錄圖片文件生成TFRecords
def get_img_data(sub_dirs,writer,INPUT_DATA,sess):
current_label = 0
is_root_dir = True
print("文件地址: "+INPUT_DATA)
for sub_dir in sub_dirs:
if is_root_dir:
is_root_dir = False
continue
file_list = []
dir_name = os.path.basename(sub_dir)
file_glob = os.path.join(INPUT_DATA, dir_name, '*.' + "png")
# extend合并兩個數組
# glob模塊的主要方法就是glob,該方法返回所有匹配的文件路徑列表(list)
# 比如:glob.glob(r’c:*.txt’) 這里就是獲得C盤下的所有txt文件
file_list.extend(glob.glob(file_glob))
if not file_list: continue
# print('file_list',current_label)
# 處理圖片數據
index = 0
for file_name in file_list:
# 讀取并解析圖片 講圖片轉化成299*299方便模型處理
image_raw_data = gfile.FastGFile(file_name, 'rb').read()
image = tf.image.decode_png(image_raw_data)
if image.dtype != tf.float32:
image = tf.image.convert_image_dtype(image, dtype=tf.float32)
image = tf.image.resize_images(image, [299, 299])
image_value = sess.run(image)
pixels = image_value.shape[1]
image_raw = image_value.tostring()
# 存到features
example = tf.train.Example(features=tf.train.Features(feature={
'pixels': _int64_feature(pixels),
'label': _int64_feature(current_label),
'image_raw': _bytes_feature(image_raw)
}))
chance = np.random.randint(100)
# 寫入訓練集
writer.write(example.SerializeToString())
index = index + 1
if index == 400:
break
print("處理文件索引%d index%d"%(current_label,index))
current_label += 1
#讀取數據并將數據分割成訓練數據、驗證數據和測試數據
def create_image_lists(sess):
#首先處理訓練數據集
sub_dirs = [x[0] for x in os.walk(INPUT_TRAIN_DATA)]
writer_train = tf.python_io.TFRecordWriter(OUTPUT_TRAIN_FILE)
get_img_data(sub_dirs,writer_train,INPUT_TRAIN_DATA,sess)
sub_test_dirs = [x[0] for x in os.walk(INPUT_TEST_DATA)]
writer_test = tf.python_io.TFRecordWriter(OUTPUT_TEST_FILE)
get_img_data(sub_test_dirs,writer_test,INPUT_TEST_DATA,sess)
writer_train.close()
writer_test.close()
def main():
with tf.Session() as sess:
create_image_lists(sess)
print('success')
if __name__ == '__main__':
main()
這里因為test和train已經在文件夾上作了區分,所以這里我利用兩個TFRecordWriter來把數據分別寫入兩個TFRecord。為了節省時間在這里我并沒有利用全部的訓練數據,只是加載了其中的400份。當然在真實的訓練場景下你是需要加載全部的數據的。
代碼沒有詳盡的注釋,因為和之前的處理大部分都是一樣的,不清楚的可以去看我之前的文章。inception-v3。
自定義Estimator
下面我們開始步入主題。先看一張Estimator類組成圖。
以下源自官方文檔的一段話:
Pre-made Estimators are fully baked. Sometimes though, you need more control over an Estimator's behavior. That's where custom Estimators come in. You can create a custom Estimator to do just about anything. If you want hidden layers connected in some unusual fashion, write a custom Estimator. If you want to calculate a unique metric for your model, write a custom Estimator. Basically, if you want an Estimator optimized for your specific problem, write a custom Estimator.
A model function (or model_fn
) implements the ML algorithm. The only difference between working with pre-made Estimators and custom Estimators is:
- With pre-made Estimators, someone already wrote the model function for you.
- With custom Estimators, you must write the model function.
Your model function could implement a wide range of algorithms, defining all sorts of hidden layers and metrics. Like input functions, all model functions must accept a standard group of input parameters and return a standard group of output values. Just as input functions can leverage the Dataset API, model functions can leverage the Layers API and the Metrics API.
大概意思是:預創建的 Estimator 是 tf.estimator.Estimator
基類的子類,而自定義 Estimator 是 tf.estimator.Estimator 的實例。
Pre-made Estimators和custom Estimators差異主要在于tensorflow中是否有它們可以直接使用的模型函數(model function or model_fn)的實現。對于前者,tensorflow中已經有寫好的model function,因而直接調用即可;而后者的model function需要自己編寫。因此,Pre-made Estimators使用方便,但使用范圍小,靈活性差;custom Estimators則正好相反。
總體來說,模型是由三部分構成:Input functions、Model functions 和Estimators(評估控制器,main function)。
- Input functions:主要是由Dataset API組成,可以分為train_input_fn和eval_input_fn。前者的任務(行為)是接受參數,輸出數據訓練數據,后者的任務(行為)是接受參數,并輸出驗證數據和測試數據。
- Model functions:是由模型(the Layers API )和監控模塊( the Metrics API)組成,主要是實現模型的訓練、測試(驗證)和監控顯示模型參數狀況的功能。
- Estimators:在模型中的作用類似于計算機中的操作系統。它將各個部分“粘合”起來,控制數據在模型中的流動與變換,同時控制模型的的各種行為(運算)。
在得知以上知識以后,我們可以開始動手編碼起來。通過以上內容得知,首先我們需要先創建自定義的Model functions。下面新建my_estimator文件。
由于我們這里是實現自定義的model_fn函數,而model_fn主要功能是定義模型的結構,損失函數以及優化器。還會對預測和評測進行處理。綜上我們來完成model_fn的編寫。
自定義model_fn
#導入相關庫
import numpy as np
import tensorflow as tf
import tensorflow.contrib.slim as slim
# 加載通過TensorFlow-Silm定義好的 inception_v3模型
import tensorflow.contrib.slim.python.slim.nets.inception_v3 as inception_v3
#圖片數據地址
TRAIN_DATA = './path/to/output_train.tfrecords'
TEST_DATA = './path/to/output_test.tfrecords'
shuffle_buffer = 10000
BATCH = 64
#打開 estimator 日志
tf.logging.set_verbosity(tf.logging.INFO)
#自定義模型
#這里我們提供了兩種方案。一種是直接通過slim工具定義已有模型
#另一種是通過tf.layer更加靈活地定義神經網絡結構
def inception_v3_model(image,is_training):
with slim.arg_scope(inception_v3.inception_v3_arg_scope()):
predictions,_ = inception_v3.inception_v3(image,num_classes=5)
return predictions
#定義lenet5模型
def lenet5(x,is_training):
net = tf.layers.conv2d(x,32,5,activation=tf.nn.relu)
net = tf.layers.max_pooling2d(net,2,2)
net = tf.layers.conv2d(net,64,3,activation=tf.nn.relu)
net = tf.layers.max_pooling2d(net,2,2)
net = tf.contrib.layers.flatten(net)
net = tf.layers.dense(net,1024)
net = tf.layers.dropout(net,rate=0.4,training=is_training)
return tf.layers.dense(net,5)
#自定義Estimator中使用的模型。定義的函數有4個收入,
#features給出在輸入函數中會提供的輸入層張量。這是個字典
#字典通過input_fn提供。如果是系統的輸入
#系統會提供tf.estimator.inputs.numpy_input_fn中的x參數指定內容
#labels是正確答案,通過numpy_input_fn的y參數給出
#在這里我們用dataset來自定義輸入函數。
#mode取值有3種可能,分別對應Estimator的train,evaluate,predict這三個函數
#mode參數可以判斷當前是訓練,預測還是驗證模式。
#最有一個參數param也是字典,里面是有關于這個模型的相關任何超參數(學習率)
def model_fn(features,labels,mode,params):
predict = lenet5(features,mode == tf.estimator.ModeKeys.TRAIN)
#如果是預測模式,直接返回結果
if mode == tf.estimator.ModeKeys.PREDICT:
return tf.estimator.EstimatorSpec(
mode=mode,
predictions={"result":tf.argmax(predict,1)}
)
#定義損失函數,這里使用tf.losses可以直接從tf.losses.get_total_loss()拿到損失
tf.losses.softmax_cross_entropy(tf.one_hot(labels, 5), predict, weights=1.0)
#優化器
optimizer = tf.train.GradientDescentOptimizer(learning_rate=params["learning_rate"])
#定義訓練過程。傳入global_step的目的,為了在TensorBoard中顯示圖像的橫坐標
train_op = optimizer.minimize(
loss=tf.losses.get_total_loss(),
global_step=tf.train.get_global_step()
)
#定義評測標準
#這個函數會在調用Estimator.evaluate的時候調用
accuracy = tf.metrics.accuracy(
predictions=tf.argmax(predict,1),
labels=labels,
name="acc_op"
)
eval_metric_ops = {
"my_metric":accuracy
}
#用于向TensorBoard輸出準確率圖像
#如果你不需要使用TensorBoard可以不添加這行代碼
tf.summary.scalar('accuracy', accuracy[1])
#model_fn會返回一個EstimatorSpec
#EstimatorSpec必須包含模型損失,訓練函數。其它為可選項
#eval_metric_ops用于定義調用Estimator.evaluate()時候所指定的函數
return tf.estimator.EstimatorSpec(
mode=mode,
loss=tf.losses.get_total_loss(),
train_op=train_op,
eval_metric_ops=eval_metric_ops
)
自定義Input functions
定義完了model functions接下來我們通過Dataset API來定義input functions:
#解析tfrecords
def parse(record):
features = tf.parse_single_example(
record,
features={
'image_raw': tf.FixedLenFeature([], tf.string),
'label': tf.FixedLenFeature([], tf.int64),
'pixels': tf.FixedLenFeature([], tf.int64)
}
)
decoded_image = tf.decode_raw(features['image_raw'], tf.float16)
label = features['label']
return decoded_image, label
#從dataset中讀取訓練數據,這里和之前處理花朵分類的時候一樣
def my_input_fn(file):
dataset = tf.data.TFRecordDataset([file])
dataset = dataset.map(parse)
dataset = dataset.shuffle(shuffle_buffer).batch(BATCH)
dataset = dataset.repeat(10)
iterator = dataset.make_one_shot_iterator()
batch_img,batch_labels = iterator.get_next()
with tf.Session() as sess:
batch_sess_img,batch_sess_labels = sess.run([batch_img,batch_labels])
#這里需要特別注意 由于batch_sess_img這里是轉成了string后在原有長度上增加了8倍
#所以在這里我們要先轉成numpy然后再reshape要不然會報錯
batch_sess_img = np.fromstring(batch_sess_img, dtype=np.float32)
#numpy轉換成Tensor
batch_sess_img = tf.reshape(batch_sess_img, [BATCH, 299, 299, 3])
return batch_sess_img,batch_sess_labels
在這里要注意,Estimator輸入函數要求每次被調用可以得到一個batch的數據,包括所有的輸入層數據和正確答案標注。而且my_input_fn函數并不能帶有參數。稍后我們會用lambda表達式解決這個問題。
最后我們通過main函數來啟動訓練過程:
def main():
#定義超參數
model_params = {"learning_rate":0.001}
#定義訓練的相關配置參數
#keep_checkpoint_max=1表示在只在目錄下保存一份模型文件
#log_step_count_steps=50表示每訓練50次輸出一次損失的值
run_config = tf.estimator.RunConfig(keep_checkpoint_max=1,log_step_count_steps=50)
#通過tf.estimator.Estimator來生成自定義模型
#把我們自定義的model_fn和超參數傳進去
#這里我們還傳入了持久化模型的目錄
#estimator會自動幫我們把模型持久化到這個目錄下
estimator = tf.estimator.Estimator(model_fn=model_fn,params=model_params,model_dir="./path/model",config=run_config)
#開始訓練模型,這里說一下lambda表達式
#lambda表達式會把函數原本的輸入參數變成0個或它指定的參數。可以理解為函數的默認值
#這里傳入自定義輸入函數,和訓練的輪數
estimator.train(input_fn=lambda :my_input_fn(TRAIN_DATA),steps=300)
#訓練完后進行驗證,這里傳入我們的測試數據
test_result = estimator.evaluate(input_fn=lambda :my_input_fn(TEST_DATA))
#輸出測試驗證結果
accuracy_score = test_result["my_metric"]
print("\nTest accuracy:%g %%"%(accuracy_score*100))
if __name__ == '__main__':
main()
運行程序,可以看到如下輸出。因為我這里是從367步以后繼續訓練,所以我們在日志中看到我這里是直接加載了第367步保存的模型。
每隔一定時間,Estimator會自動創建模型文件。另外如果訓練中斷,下一次再啟動訓練的話,Estimator會自動從模型目錄下加載最新的模型并且用于訓練,非常方便。這就是為什么谷歌推薦我們用Estimator來訓練模型,因為它封裝了很多開發者并不需要關心的操作,大大提升了我們的開發效率。
INFO:tensorflow:Done calling model_fn.
INFO:tensorflow:Create CheckpointSaverHook.
INFO:tensorflow:Graph was finalized.
INFO:tensorflow:Restoring parameters from ./path/model/model.ckpt-367
INFO:tensorflow:Running local_init_op.
INFO:tensorflow:Done running local_init_op.
INFO:tensorflow:Saving checkpoints for 368 into ./path/model/model.ckpt.
INFO:tensorflow:loss = 0.2994086, step = 368
INFO:tensorflow:global_step/sec: 0.116191
INFO:tensorflow:loss = 0.2086069, step = 418 (430.326 sec)
INFO:tensorflow:Saving checkpoints for 438 into ./path/model/model.ckpt.
INFO:tensorflow:global_step/sec: 0.115405
INFO:tensorflow:loss = 0.17857286, step = 468 (433.259 sec)
INFO:tensorflow:Saving checkpoints for 506 into ./path/model/model.ckpt.
INFO:tensorflow:global_step/sec: 0.111342
INFO:tensorflow:loss = 0.107850984, step = 518 (449.065 sec)
INFO:tensorflow:global_step/sec: 0.115999
INFO:tensorflow:loss = 0.08592671, step = 568 (431.040 sec)
INFO:tensorflow:Saving checkpoints for 575 into ./path/model/model.ckpt.
INFO:tensorflow:global_step/sec: 0.112465
INFO:tensorflow:loss = 0.05861471, step = 618 (444.587 sec)
INFO:tensorflow:Saving checkpoints for 643 into ./path/model/model.ckpt.
TensorBoard
為了更加直觀的看到訓練過程,接下來我們將使用谷歌提供的一個工具TensorBoard來可視化我們的訓練過程。
要啟動TensorBoard,執行下面的命令:
#PATH替換為你模型保存的目錄。要注意在這里用的是絕對路徑。
tensorboard --logdir=PATH
執行命令后可以看到如下信息,說明TensorBoard已經跑起來了。
TensorBoard 1.8.0 at http://bogon:6006 (Press CTRL+C to quit)
W0817 16:14:27.129659 Reloader tf_logging.py:121] Found more than one graph event per run, or there was a metagraph containing a graph_def, as well as one or more graph events. Overwriting the graph with the newest event.
W0817 16:14:27.650306 Reloader tf_lo
所有預創建的 Estimator 都會自動將大量信息記錄到 TensorBoard 上。不過,對于自定義 Estimator,TensorBoard 只提供一個默認日志(損失圖)以及您明確告知 TensorBoard 要記錄的信息。對于我們剛剛創建的自定義 Estimator,并且明確說明要繪制正確率的圖,所以TensorBoard 會生成以下內容:
TensorBoard生成了三個圖。分別表示正確率,訓練處理的批次,訓練輪數所對應的損失值
簡而言之,下面是三張圖顯示的內容:
- global_step/sec:這是一個性能指標,顯示我們在進行模型訓練時每秒處理的批次數(梯度更新)。
- loss:所報告的損失。
- accuracy:準確率由下列兩行記錄:
- eval_metric_ops={'my_accuracy': accuracy}(評估期間)。
- tf.summary.scalar('accuracy', accuracy[1])(訓練期間)。
這些 Tensorboard 圖是務必要將 global_step 傳遞給優化器的 minimize 方法的主要原因之一。如果沒有它,模型就無法記錄這些圖的 x 坐標。
我們來看下TensorBoard的輸出。可以看到隨著訓練步驟的增加,loss在相應的減少,accuracy也在慢慢增加。這是一個健康的訓練過程。可以看到LeNet5在這個數據集上的正確率達到了95%左右。
eval
因為我自定義的Estimator在訓練結束之后并沒有輸出正確率(暫時沒找到原因),所以這里我們另外寫一個程序來測試這個模型的正確率。這里我們命名為eval.py。
import tensorflow as tf
import Estimator1
import numpy as np
TEST_DATA = './path/to/output_test.tfrecords'
CKPT_PATH = './path/model'
EVAL_BATCH = 20
def getValidationData():
dataset = tf.data.TFRecordDataset([TEST_DATA])
dataset = dataset.map(Estimator1.parse)
dataset = dataset.batch(EVAL_BATCH)
iterator = dataset.make_one_shot_iterator()
batch_img, batch_labels = iterator.get_next()
# batch_img作處理
return batch_img, batch_labels
def my_eval():
#estimator的eval方法不好使 用傳統方法試試
batch_img,batch_labels = getValidationData()
x = tf.placeholder(tf.float32, [None, 299,299,3], name='x-input')
y_ = tf.placeholder(tf.int64, [None], name='y-input')
y = Estimator1.lenet5(x, False)
correct_prediction = tf.equal(tf.argmax(y, 1), y_)
accuracy = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))
saver = tf.train.Saver()
with tf.Session() as sess:
while True:
try:
ckpt = tf.train.get_checkpoint_state(CKPT_PATH)
if ckpt and ckpt.model_checkpoint_path:
saver.restore(sess,ckpt.model_checkpoint_path)
#通過文件名得到模型保存時迭代的輪數
global_step = ckpt.model_checkpoint_path.split('/')[-1].split('-')[-1]
batch_sess_img, batch_sess_labels = sess.run([batch_img, batch_labels])
batch_sess_img = np.fromstring(batch_sess_img, dtype=np.float32)
batch_sess_img = tf.reshape(batch_sess_img, [EVAL_BATCH, 299, 299, 3])
batch_sess_img = sess.run(batch_sess_img)
print(sess.run([tf.argmax(y,1),y_],feed_dict={x:batch_sess_img,y_:batch_sess_labels}))
accuracy_score = sess.run(accuracy,feed_dict={x:batch_sess_img,y_:batch_sess_labels})
print("After %s training step(s),validation accuracy = %g"%(global_step,accuracy_score))
else:
print('No checkpoint file found')
return
except tf.errors.OutOfRangeError:
break
def main():
my_eval()
if __name__ == '__main__':
main()
這個程序大概的作用是:
1.讀取測試數據,把測試數據打包成batch。然后定義神經網絡輸入變量x和正確答案的標簽y_。
2.把x通過神經網絡得到的前向傳播結果y和y_作比較來計算正確率。
3.讀取之前訓練好的模型。
4.用一個while循環來輸出在訓練好的模型上每一個batch的正確率,直到數據讀取完畢。
運行這個程序可以得到以下輸出:
INFO:tensorflow:Restoring parameters from ./path/model/model.ckpt-643
[array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1]), array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1])]
After 643 training step(s),validation accuracy = 1
INFO:tensorflow:Restoring parameters from ./path/model/model.ckpt-643
[array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2]), array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2])]
After 643 training step(s),validation accuracy = 1
INFO:tensorflow:Restoring parameters from ./path/model/model.ckpt-643
[array([2, 2, 2, 2, 2, 2, 2, 3, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]), array([2, 2, 2, 2, 2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3])]
After 643 training step(s),validation accuracy = 0.95
INFO:tensorflow:Restoring parameters from ./path/model/model.ckpt-643
嗯。60個數據中只有1個判斷錯誤,也符合我們之前得到的正確率。
寫在最后
Estimator是TensorFlow官方強烈推薦的API,通過上述程序大家也能看到相比傳統的TensorFlow API,Estimator封裝了大部分與業務邏輯無關的操作,然而通過Custom Estimator,Estimator也不失靈活性。
我們之前還通過slim定義了一個inception-v3模型,但是由于inception-v3結構比較復雜,訓練的時間比較久所以這里我們就以LeNet-5作演示了。但是在復雜的圖像分類問題上,比如ImageNet數據集中,LeNet-5的分類效果就不是很好。如果是復雜的圖像分類問題,就要選擇更加復雜的神經網絡模型來訓練才能達到較高的準確率。
另外這篇文章主要是以使用Estimator為主,對于其中的一些細節沒有很好的闡述。之后的文章會對一些技術細節做探究。
歡迎廣大喜歡AI的開發者互相交流,有問題也可以在評論區里留言,大家互相討論,一起進步。