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()

猜你喜欢

转载自blog.csdn.net/weixin_39721347/article/details/86219570
今日推荐