1. 程式人生 > >Tensorflow-遷移學習程式碼及註解

Tensorflow-遷移學習程式碼及註解

# -*- coding:utf-8 -*-

# 先對待操作的檔案的進行介紹
# 通過下載連線下載後解壓的資料夾中包含5個子資料夾,每一個子資料夾的名稱為一種花的名稱,
# 代表了不同的類別,即一種花對應一個類別,對應一個子資料夾。
# 平均每一種花有734張圖片,每一張圖片都是RGB色彩模式的,大小也不相同。
# 所以特別需要注意的是,這和之前處理的影象不同,這裡的程式將處理的是沒有整理過的影象資料

import glob  # 用於獲取檔案目錄的模組
import os.path
import random
import numpy as np
import tensorflow as tf
from tensorflow.python.platform import gfile  # 沒有執行緒鎖的檔案IO包裝器 用於儲存和載入檢查點

# Inception-v3模型瓶頸層的節點個數
# 因為是進行遷移學習,使用的是已經訓練好的Inception-v3模型,
# 所以僅僅需要替換最後一層全連線層,在此之前的都為瓶頸層bottleneck,
# 所以當前的目標是為了得到一副影象經過已經訓練好的Inception-v3模型的瓶頸層的輸出

BOTTLENECK_TENSOR_SIZE = 2048

# 在使用的Inception-v3模型中代表bottleneck輸出結果的節點的張量名稱
BOTTLENECK_TENSOR_NAME = 'pool_3/_reshaped:0'

# 在使用的Inception-v3模型中代表影象輸入的節點或者說張量的名稱
JPEG_DATA_TENSOR_NAME = 'DecodeJpeg/contents:0'

# 存放所使用的Inception-v3模型的檔案目錄
MODEL_DIR = '/path/to/model'

# 所使用的Inception-v3模型的檔名
MODEL_FILE = 'classify_image_graph_def.pb'

# 因為一個訓練資料會被使用多次,所以可以將原始影象通過Inception-v3模型計算得到的
# 影象的特徵向量儲存在檔案中,以避免重複的計算。CACHE_DIR表示存放這些檔案的地址
CACHE_DIR = '/tmp/bottleneck'

# 圖片資料資料夾。在這個資料夾中,每一個子資料夾代表一個類別,
# 每一個子資料夾中存放了相應類別的圖片
INPUT_DATA = 'path/to/flower_data'

# 因為需要將擁有的圖片集劃分為訓練集、驗證集、測試集,
# 所以需要確定各個集合所佔的比例以用於劃分

# 驗證集中影象數目佔所有影象數目的比重
VALIDATION_PERCENTAGE = 10
# 測試集中影象數目佔所有影象數目的比重
TEST_PERCENTAGE = 10
# 定義了驗證集和測試集的比例後,剩下的就是訓練集的比例

# 定義神經網路的一些配置 包括學習率、訓練週期、Batch大小
LEARNING_RATE = 0.01
STEPS = 4000
BATCH = 100


# create_image_lists()函式用於從影象資料集中讀取所有圖片列表並將其劃分為訓練、驗證、測試三個集合


def create_image_dict(testing_percentage, validation_percentage):
    # 將讀取的所有圖片存放在result這個字典中,這個字典的key為類別的名稱,value為一個字典,
    # 存放相應類別下所有圖片的名稱
    result = {}
    # 獲取當前目錄下所有的子目錄
    # os.walk():http://www.runoob.com/python/os-walk.html
    # 對目錄下的檔案進行遍歷,返回(ROOT, DIRS, FILES)
    # 因為os.walk()返回的是一個三元組,所以x[0]表示元組的第一個元素,表示當前資料夾的地址
    sub_dirs = [x[0] for x in os.walk(INPUT_DATA)]
    # 因為是遍歷資料夾樹,且有遍歷優先順序的選擇選項。所以當前是根目錄,
    # 之後隨著遍歷過程的進行,當前目錄就會變成檔案樹中的子節點的目錄,
    # 所以需要通過設定is_root_dir來作為是否是檔案樹中根目錄的標誌符。
    # 又因為os.walk()返回的三元組中的元素有資料夾和檔案之分,files項為檔案路徑,
    # 而其他兩項都為資料夾路徑,不包含檔案路徑和各自的子檔案路徑,所以只要用一個x[0]即可
    # 得到的第一個目錄是當前目錄,該目錄無需考慮,因為想得到的是子資料夾的目錄
    is_root_dir = True
    for sub_dir in sub_dirs:
        # 注意接下來的內容都是在這個sub_dir下
        if is_root_dir:
            is_root_dir = False
            continue
        # 獲取當前目錄下所有的有效圖片檔案。extensions為拓展名的列表
        extensions = ['jpg', 'jpeg', 'JPG', 'JPEG']
        file_list = []
        # os.path.basename()返回的是path最後的檔名
        # 所以dir_name中存放的是sub_dirs中的對應與類別的子資料夾的名稱
        dir_name = os.path.basename(sub_dir)
        for extension in extensions:
            # 注意:INPUT_DATA是整個圖片資料的資料夾 dir_name是子資料夾名稱
            # 這裡file_glob中存放的是待通過glob方法匹配的檔名的路徑
            # 這裡得到的是某一個類別對應的子資料夾中所有的有效的圖片
            # 將glob(file_glob)返回的檔案路徑加入file_list中
            file_glob = os.path.join(INPUT_DATA, dir_name, '*.' + extension)
            # glob模組下的glob方法返回的是與傳入引數匹配的檔案的路徑的列表
            # 傳入的引數為字串,用來指定說需要匹配的路徑的字串,可以是相對路徑,也可以是絕對路徑
            # 相對路徑 : ../
            # glob:https://blog.csdn.net/u010472607/article/details/76857493/
            file_list.extend(glob.glob(file_glob))
        if not file_list: continue
        # 當file_list為空時跳過此過程

        # dir_name中存放的是類別的名稱,使用lower()方法使之全部為小寫字元賦予label_name
        label_name = dir_name.lower()
        # 初始化當前類別的訓練、測試、驗證資料集,注意是當前的類別的資料集,因為這是在某一個sub_dir下的內容
        # 當前目錄的資料集合下存放的是圖片檔案的名稱
        training_images = []
        testing_images = []
        validation_images = []
        for file_name in file_list:
            # 對當前類別的所有有效圖片的路徑進行遍歷
            # 通過os.path.basename()方法得到圖片檔案的名稱
            base_name = os.path.basename(file_name)
            # 通過隨機數和之前確定好的三個集合佔的比例來進行資料集的劃分
            chance = np.random.randint(100)
            if chance < validation_percentage:
                validation_images.append(base_name)
            elif chance < (testing_percentage + validation_percentage):
                testing_images.append(base_name)
            else:
                training_images.append(base_name)

        # 對result這個字典的value進行賦值,
        # 之前提到過result的key為某一類別,即label_name;value為一個字典,
        # 存放的是dir_name類別的名稱和三個存放圖片名稱的資料集列表
        result[label_name] = {
            'dir': dir_name,
            'training': training_images,
            'testing': testing_images,
            'validation': validation_images
        }
    #
    return result


# 通過類別名稱、所屬資料集和圖片編號獲取一張圖片的地址,根據之前create_image_lists函式,
# 返回的result中,三個資料集列表的元素均為圖片本身的名稱,base_name;
# image_lists給出了圖片的所有資訊,就是返回的result;
# image_dir給出了根目錄。因為result中的內容最上層只到子資料夾的名稱,即類別的名稱,
# 注意存放圖片資料的根目錄和存放圖片特徵向量的根目錄地址不同;
# label_name給定了需要獲取的圖片的類別的名稱;
# index給定了需要獲取的圖片的編號,注意圖片編號並非圖片檔案的名稱,圖片的編號僅表示圖片在列表中的位置;
# category指定了需要獲取的圖片是在訓練、測試還是驗證資料集上;


def get_image_path(image_dict, image_dir, label_name, index, category):
    # 在某一個目錄下的value。
    # 注意這裡image_dict是個字典,其key為類別,value同樣為一個字典,參考result
    label_dict = image_dict[label_name]
    # 得到三個資料集中某個資料集的列表,列表元素為相應資料集下的圖片檔案的名稱
    category_list = label_dict[category]
    # index為索引,對應圖片檔案的編號,表示圖片檔案在列表中的位置
    mod_index = index % len(category_list)
    # 通過圖片編號獲取圖片檔案的名稱
    # 因為最終想要得到圖片檔案存放的路徑,所以要得到的是'名稱':圖片檔案的名稱,類別的名稱即子資料夾名...
    base_name = category_list[mod_index]
    sub_dir = label_dict['dir']
    # 圖片的絕對路徑可由'根目錄'、'子資料夾目錄'即類別名稱、'圖片檔名稱'三者組合而成
    full_path = os.path.join(image_dir, sub_dir, base_name)
    return full_path


# 通過類別名稱、所屬資料集和圖片編號獲取經過Inception-v3模型處理後的特徵向量的檔案地址


def get_bottleneck_path(image_dict, label_name, index, category):
    # 注意圖片檔案存放的地址和圖片特徵向量存放的地址的根目錄是不同的,其他一致
    # 所以在這裡,傳入的根目錄是CACHE_DIR,即特徵向量說存放的地址的根目錄
    # 傳入image_dict是為了得到相應影象的各部分的檔名稱:'類別名稱'即'子資料夾名稱'等等
    return get_image_path(image_dict, CACHE_DIR, label_name, index, category) + '.txt'


# 使用載入已經訓練好的Inception-v3模型去處理一張圖片,得到該圖片的特徵向量,即瓶頸層的輸出結果,
# 這個結果也就是這個影象的新的特徵向量。也正是因為找不到已經存在的相應圖片的特徵向量,
# 才會重新通過Inception-v3模型對影象資料進行處理,得到一個新的特徵向量


def run_bottleneck_on_image(sess, image_data, image_data_tensor, bottleneck_tensor):
    # 這個image_data_tensor是一個placeholder,將image_data傳入image_data_tensor中,
    # 並且計算bottleneck_tensor得到相應的瓶頸層的輸出結果
    bottleneck_values = sess.run(bottleneck_tensor, {image_data_tensor: image_data})
    # 因為瓶頸層的輸出結果是一個4維資料,所以需要將其進行壓縮,得到一個1維向量,即特徵向量
    bottleneck_values = np.squeeze(bottleneck_values)
    return bottleneck_values


# 輸入一個圖片資料,通過該函式輸出該影象的Inception-v3模型瓶頸層的處理結果;
# 這個函式和run_bottleneck_on_image函式不同之處在於:這個函式不知道是否已經存在該影象的特徵向量,
# 總之向該函式傳入一個影象資料就能得到一個特徵向量,無論是通過重新計算還是拿已經存在的特徵向量;
# 而run_bottleneck_on_image函式是明確不存在已經計算得到的特徵向量,
# 就是重新去計算得到一個圖片資料的特徵向量;
# 所以在get_or_create_bottleneck函式中,會先試圖去尋找已經計算且儲存下來的特徵向量,
# 如果找不到則去計算這個特徵向量,然後儲存到檔案。


def get_or_create_bottleneck(sess, image_dict, label_name, index, category,
                             jpeg_data_tensor, bottleneck_tensor):
    #
    label_dict = image_dict[label_name]  # label_dict也是一個字典,參考result的value
    sub_dir = label_dict['dir']
    sub_dir_path = os.path.join(CACHE_DIR, sub_dir)  # CACHE_DIR存放的是特徵向量的根目錄
    # 通過os.path.exists判斷傳入引數路徑是否存在,返回bool值,
    # 如果不存在,則通過os.makedirs建立一個目錄
    # os.makedir():http://www.runoob.com/python/os-makedirs.html
    # 在這裡判斷、建立的是子資料夾的目錄
    if not os.path.exists(sub_dir_path): os.makedirs(sub_dir_path)

    bottleneck_path = get_bottleneck_path(image_dict, label_name, index, category)
    # 在這裡判斷、建立的是bottleneck的輸出,即特徵向量檔案的目錄
    if not os.path.exists(bottleneck_path):
        # 當特徵向量不存在時,通過get_image_path()獲取圖片檔案的原始路徑並存放進image_path中
        image_path = get_image_path(image_dict, INPUT_DATA, label_name, index, category)
        # 通過tf.gfile.FastGFile().read()進行影象的讀取
        # tf.gfile.FastGFile():https://blog.csdn.net/william_hehe/article/details/78821715
        # tf.gfile.FastGFile().read():https://www.jianshu.com/p/d8f5357b95b3
        # 在這裡tf.gfile.FastGFile()像是open()方法一樣,為了得到檔案的控制代碼
        # 再呼叫read()方法對檔案物件進行相關操作
        image_data = gfile.FastGFile(image_path, 'rb').read()
        # 通過Inception-v3模型的瓶頸層對影象檔案進行計算,
        # 其中引數jpeg_data_tensor, bottleneck_tensor分別為模型中的計算節點或者說tensor
        bottleneck_values = run_bottleneck_on_image(sess, image_data,
                                                    jpeg_data_tensor, bottleneck_tensor)
        # 將得到的Inception-v3模型計算出的特徵向量存入檔案中,檔案內容為bottleneck_string
        bottleneck_string = ','.join(str(x) for x in bottleneck_values)
        # 通過檔案控制代碼的方法將檔案內容存放進bottleneck_path中的檔案中
        with open(bottleneck_path, 'w') as bottleneck_file:
            bottleneck_file.write(bottleneck_string)

    else:
        # 因為存在Inception-v3的計算結果的檔案,所以直接從檔案中讀取相應的計算結果
        # 存入bottleneck_values中。注意bottleneck的輸出結果檔案內容是以','相間隔的
        with open(bottleneck_path, 'r') as bottleneck_file:
            bottleneck_string = bottleneck_file.read()
        bottleneck_values = [float(x) for x in bottleneck_string.split(',')]
    # 返回Inception_v3模型輸出結果的列表,即瓶頸層的輸出結果的列表形式
    return bottleneck_values


# 該函式隨機獲取一個batch的圖片作為訓練資料
# n_classes:類別數
# batch_size:batch值
# category:訓練、驗證和測試三個資料集中的哪一個
# jpeg_data_tensor, bottleneck_tensor:Inception-v3模型檔案中的計算節點


def get_random_cached_bottlenecks(sess, n_classes, image_dict, batch_size, category,
                                  jpeg_data_tensor, bottleneck_tensor):
    # bottlenecks列表存放的元素是一個影象資料經過Inception-v3處理的結果:
    # bottleneck_values同樣為一個列表。所以bottlenecks是用於存放一個batch內的圖片
    # 經過Inception-v3模型處理後的輸出結果的列表
    # ground_truths列表中的元素則是one-hot編碼的1維向量,對應為1的索引表示所屬的類別
    bottlenecks = []
    ground_truths = []
    for i in range(batch_size):
        # 通過random.randrange()得到0-n_classes範圍內的隨機整數,作為類別的索引值,
        # 再根據索引值,從所有影象類別得到的列表中得到通過隨機得到的類別的名稱
        label_index = random.randrange(n_classes)
        label_name = list(image_dict.keys())[label_index]
        # 再根據random.random()隨機得到某一類別下的影象的編號,以此來隨機獲取影象資料
        image_index = random.random(65536)
        # get_or_create_bottleneck返回的是bottleneck的計算結果的列表:bottleneck_values
        bottleneck = get_or_create_bottleneck(sess, image_dict, label_name, image_index,
                                              category, jpeg_data_tensor, bottleneck_tensor)
        ground_truth = np.zeros(n_classes, dtype=np.float32)
        ground_truth[label_index] = 1.0
        bottlenecks.append(bottleneck)
        ground_truths.append(ground_truth)
    return bottlenecks, ground_truths


# 這個函式獲取全部的測試資料。在最終測試的時候需要在所有的測試資料上計算正確率


def get_test_bottlenecks(sess, image_dict, n_classes, jpeg_data_tensor, bottleneck_tensor):
    bottlenecks = []
    ground_truths = []
    # 使用label_name_list陣列來存放傳入的image_dict的所有關鍵字,image_dict的關鍵字即類別名稱
    label_name_list = list(image_dict.keys())
    # enumerate:http://www.runoob.com/python/python-func-enumerate.html
    # 可知,enumerate(label_name_list)返回的元組的兩個元素分別為類別在列表中的索引值和類別名稱
    for label_index, label_name in enumerate(label_name_list):
        category = 'testing'
        # 使用key=category表示要獲取的是測試資料集;image_dict的結構參考result
        # enumerate(image_dict[label_name][category])返回的二元組的兩個元素為
        # 測試集中影象資料檔案的索引值和測試集中影象資料檔案的名稱 分別賦予index和unused_base_name
        for index, unused_base_name in enumerate(
                image_dict[label_name][category]):
            # 將當前遍歷到的影象資料檔案傳入get_or_create_bottleneck()函式中,
            # 得到影象資料經過Inception-v3模型的bottleneck層的輸出結果
            bottleneck = get_or_create_bottleneck(sess, image_dict, label_name,
                                                  index, category, jpeg_data_tensor,
                                                  bottleneck_tensor)
            # 建立對應的one-hot編碼
            ground_truth = np.zeros(n_classes, dtype=np.float32)
            ground_truth[label_index] = 1.0
            # 將測試集影象資料經過bottleneck層處理後的結構加入bottlenecks列表中
            bottlenecks.append(bottleneck)
            ground_truths.append(ground_truth)
        # 返回兩個列表
        return bottlenecks, ground_truths


# 主函式


def main():
    # 通過向create_image_dict()函式傳入測試集和驗證集佔整個資料集的比例,
    # 返回一個劃分好的字典image_dict,字典的結構參考result,將整個資料集劃分為測試、驗證、訓練集
    image_dict = create_image_dict(TEST_PERCENTAGE, VALIDATION_PERCENTAGE)
    # image_dict.keys()返回的是元素為字典中所有key的列表,image_dict的key值表示的是類別
    n_classes = len(image_dict.keys())
    # 讀取已經訓練好的Inception-v3模型。這個模型儲存在GraphDef Protocol Buffer中,
    # 其中儲存了每一個節點取值的計算方法以及constant的取值。
    with gfile.FastGFile(os.path.join(MODEL_DIR, MODEL_FILE, 'rb')) as f:
        # GraphDef可以理解為一種資料結構
        # GraphDef: https://blog.csdn.net/qq_39124762/article/details/83857252
        # gfile.FastGFile()像是open()得到檔案的控制代碼,
        # graph_def = tf.GraphDef();graph_def.ParseFromString(f.read());
        # 則是通過pb檔案呼叫已被儲存的模型:https://blog.csdn.net/zj360202/article/details/78539464
        graph_def = tf.GraphDef()
        graph_def.ParseFromString(f.read())
        # tf.import_graph_def():https://www.w3cschool.cn/tensorflow_python/tensorflow_python-vhtj2f4p.html
        # 將圖形從graph_def匯入當前的預設的graph(as_default...)
        # 即該函式提供了一種匯入序列化的Graph:GraphDef Protocol Buffer的方法,
        # 將GraphDef中的各個物件提取為tf.Tensor和tf.Operation物件,這些物件都將放入預設圖中
        # tf.import_graph_def()中的引數:graph_def:將匯入的GraphDef原型;
        # return_elements:整個tf.import_graph_def()函式返回的GraphDef中tf.Tensor物件和tf.Operation物件的名稱的字串的列表
        # tf.import_graph_def()最終返回的是GraphDef中由return_elements傳入的名稱所對應的的物件,以張量Tensor的形式返回
        bottleneck_tensor, jpeg_data_tensor = tf.import_graph_def(graph_def,
                                                                  return_elements=[BOTTLENECK_TENSOR_NAME,
                                                                                   JPEG_DATA_TENSOR_NAME])
        # 佔位符,對應的是新的神經網路的輸入。這個輸入就是影象經過Inception-v3模型前向傳播
        # 到達bottleneck層時的節點取值。可將這個過程理解為特徵提取
        bottleneck_output = tf.placeholder(tf.float32, [None, BOTTLENECK_TENSOR_SIZE],
                                           name="BottleneckOutputPlaceholder")
        # 佔位符,對應的是新的標準答案的輸入
        ground_truth_output = tf.placeholder(tf.float32, [None, n_classes],
                                             name="GroundTruthOutput")
        # 定義一個全連線層來處理影象資料經過Inception-v3模型的bottleneck層的輸出,
        # 以得到最終的遷移學習模型的輸出。
        # 在'final_training_ops'這個名稱空間下定義網路最後一層全連線層的一些引數
        with tf.name_scope('final_training_ops'):
            weights = tf.Variable(tf.truncated_normal_initializer(
                [BOTTLENECK_TENSOR_SIZE, n_classes], stddev=0.1))
            biases = tf.Variable(tf.zeros([n_classes]))
            # 這裡是bottleneck_output作為全連線層的輸入,
            # 即影象資料經過bottleneck層處理的輸出結果作為全連線層的輸入
            logits = tf.matmul(bottleneck_output, weights) + biases
            final_tensor = tf.nn.softmax(logits)

        # 定義損失函式等內容
        cross_entropy = tf.nn.softmax_cross_entropy_with_logits(logits,
                                                                ground_truth_output)
        cross_entropy_mean = tf.reduce_mean(cross_entropy)
        train_step = tf.train.GradientDescentOptimizer(LEARNING_RATE). \
            minimize(cross_entropy_mean)

        # 計算正確率
        with tf.name_scope('evaluation'):
            correct_prediction = tf.equal(tf.argmax(final_tensor, 1),
                                          tf.argmax(ground_truth_output, 1))
            evaluation_step = tf.reduce_mean(tf.cast(correct_prediction, tf.float32))

        # 執行模型
        with tf.Session as sess:
            init_op = tf.global_variables_initializer()
            sess.run(init_op)
            # 開始訓練
            for i in range(STEPS):
                # get_random_cached_bottlenecks()函式用於得到由隨機獲取的一個Batch的影象資料
                # 經過Inception-v3模型的bottleneck層的輸出結果和標準答案
                train_bottlenecks, train_ground_truth = \
                    get_random_cached_bottlenecks(sess, n_classes, image_dict,
                                                  BATCH, 'training', jpeg_data_tensor, bottleneck_tensor)
                sess.run(train_step, feed_dict={bottleneck_output: train_bottlenecks,
                                                ground_truth_output: train_ground_truth})

                # 在驗證集上測試正確率
                if i % 100 == 0 or i + 1 == STEPS:
                    validation_bottlenecks, validation_ground_truth = \
                        get_random_cached_bottlenecks(sess, n_classes, image_dict,
                                                      BATCH, 'validation', jpeg_data_tensor, bottleneck_tensor)
                    validation_accuracy = sess.run(evaluation_step,
                                                   feed_dict={bottleneck_output: validation_bottlenecks,
                                                              ground_truth_output: validation_ground_truth})
                    print ('Step: %d: Validation accuracy on random sampled '
                           '%d examples = %.1f%%' %
                           (i, BATCH, validation_accuracy * 100))

                # 在測試集上測試正確率
                test_bottlenecks, test_ground_truth = \
                    get_test_bottlenecks(sess, image_dict,
                                         n_classes, jpeg_data_tensor, bottleneck_tensor)
                test_accuracy = sess.run(evaluation_step, feed_dict={bottleneck_output: test_bottlenecks,
                                                                     ground_truth_output: test_ground_truth})
                print ('Final test accuracy = %.1f%%' % (test_accuracy * 100))


if __name__ == '__main__':
    tf.app.run()