CIFAR10攻略:基于TensorFlow2.1

CIFAR10数据介绍

数据集官网

CIFAR (Canadian Institute for Advanced Research)系列有两个数据集:CIFAR-10和CIFAR-100,后面的数字表示数据集中包含的类别数目。作为深度学习入门数据集,CIFAR-10更加流行一些。
数据集官网如下:
CIFAR-10 and CIFAR-100 datasets

数据集内容简介

CIFAR-10数据集包含10个类别:airplane, automobile, bird, cat, deer, dog, frog, horse, ship, truck
类别与标签号码的对应关系如下:

classlabel
airplane0
automobile1
bird2
cat3
deer4
dog5
frog6
horse7
ship8
truck9

每个类别包含6000张图片,其中随机选取5000张作为训练集,其余1000张作为测试集,所以训练集共有5万张图片,测试集共1万张图片。
图片大小全部是32*32,彩图,所以是3通道。
数据集提供了三个类型以供下载:

本文档以TensorFlow为基础进行训练,所以在python中使用,因此需要下载python version。下载到的数据集名字为cifar-10-python.tar.gz,解压后包含以下文件:

data_batch_1
data_batch_2
data_batch_3
data_batch_4
data_batch_5
test_batch
batches.meta
readme.html

data_batch_1到data_batch_5是训练集数据,每个文件中包含1万个样本,test_batch是测试集数据,包含1万个样本。

数据集八卦

CIFAR数据集虽然不大,但是如MNIST(手写字数据集)一样,在深度学习的历史上也起到了相当重要的作用。

2012年之前,也就是Alex Krizhevsky发表他那篇里程碑式的文章《ImageNet Classification with Deep Convolutional Neural Networks》之前,深度学习处于一个相对沉寂的阶段,当时SVM(Support Vector Machine,支持向量机)更加流行。技术方法流行于否很多时候对于吃瓜群众而言不是太有所谓,谁效果好就用谁呗。但是对于坚持某个方向的研究者而言就很有所谓了。Geoffrey Hinton,深度学习的三大马车头之一,Alex Krizhevsky的师傅,当时一直在坚持神经网络理论或深度学习的研究,并且已经有二三十个年头了。因为深度学习的相对沉寂,Geoffrey Hinton不太容易获取到足够的科研资金的支持(可能’不太足够’已经是比较含蓄的说法,不客气点讲应该是很难获取到充足的资金支持)。

现在我们已经知道了,深度学习之所以这么火爆是因为数据和算力的长足发展,两者缺一不可。早期在算力不足的情况下,研究者还可以通过耐心和等待来解决,现在服务器集群训练一个小时就可以完成的任务以前可能需要几周时间,但耐得住性子的研究者毕竟还是可以通过时间来解决这个问题。但是数据就是硬伤了,没有足够的数据就没有研究的基础。至少到目前为止(2020年),监督学习是深度学习研究和应用的主流方向。监督学习就意味着数据必需要有标签,而标签的获取,也就是数据标注,是要钱的。

这时MIT和NYU有一个项目,采集了数目众多的小图片(80 Million Tiny Images),不过这些小图片是没有标签的(各位看官可以登录这个网站去贡献标签,不过图片太小了,以现在的数据积累情况,估计大家也都看不上这么低质量的图片)。Alex Krizhevsky从这里面抽了一些图片,然后雇了一帮学生,付费让学生帮忙给做了一下标注,最后Alex Krizhevsky他们对标好的数据进行了清洗,去重和检查,形成了CIFAR-10和CIFAR-100两个数据集。因为Geoffrey Hinton团队似乎并没有特别多的资金,所以能做到这种程度也是不容易了。

这些数据在很长一段时间支持了Geoffrey Hinton团队在基础理论方面的研究,比如当时的RBMs(Restricted Boltzmann Machines)和DBNs(Deep Belief Networks)。CIFAR数据官网下面有一篇Alex Krizhevsky基于该数据的研究报告:Learning Multiple Layers of Features from Tiny Images, Alex Krizhevsky, 2009,各位可以瞄一下,里面的研究手法和2012年那篇里程碑式的文章非常相似,只是数据集不同而已。所以可以认为如果没有基于CIFAR的这些研究积累,2012年那篇文章也难以横空出世。

CIFAR-10数据解析

在python3中解析这些数据文件的代码可以在官网找到,搬运如下:

def unpickle(file):
    import pickle
    with open(file, 'rb') as fo:
        data = pickle.load(fo, encoding='bytes')
    return data

解析后,返回的data是一个dict,里面包含四个变量,内容分别如下:

keytypesizevalue
batch_labelbytes1‘training batch 1 of 5’
dataArray of uint8(10000, 3072)[[59, 43, 50, …, 140, 84, 72],
[154, 126, 105, …, 139, 142, 144],
…]
filenameslist10000‘leptodactylus_pentadactylus_s_000004.png’
‘camion_s_000148.png’
labelslist10000[6, 9, 9, 4, 1, 1, …]
  • batch_label是对数据文件的简要说明
  • data是数据文件,每一行是一张图片,3072 = 3*32*32,数据的排列方式是RRRR...GGGG....BBBB....,所以如果要对数据做reshape的话,首先应当reshape成(3, 32, 32)的形状,然后如果有其他需求的话,比如维度顺序调整(transpose),颜色通道顺序调整等,再根据需求做操作。
  • filenames是文件名,如果有需要保存成图片的话,建议保存成bmp格式,因为bmp读取和解析的速度远快于png(参考:python读图命令与效率汇总
  • labels是标签,数值范围[0, 9]

将数据保存成图片

根据上面的解析,我们可以将数据保存成图片,方便观察。代码如下:

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

import os
import cv2
import numpy as np

DATA_INPUT_FOLDER = 'F:/CIFAR-10/'
TRAIN_OUTPUT_FOLDER = 'F:/CIFAR-10/train/'
TEST_OUTPUT_FOLDER = 'F:/CIFAR-10/test/'


def unpickle(file):
    """
    parse cifar-10 data.
    return a dict containing {data, filenames, labels, batch_label}
    """
    import pickle
    with open(file, 'rb') as fo:
        data = pickle.load(fo, encoding='bytes')
    return data


def makedirs(directory):
    """
    create a directory recursively, that is, all intermediate paths will also
    be created
    """
    try:
        os.makedirs(directory)
    except FileExistsError:
        print(directory + ' already exists.')


def save_list(filename, data_list):
    """
    save a string list to file
    """
    with open(filename, 'w') as fid:
        for item in data_list:
            fid.write(item + '\n')


# save images of train set
makedirs(TRAIN_OUTPUT_FOLDER)
train_list = []
for k in range(1, 6):
    data_dict = unpickle(DATA_INPUT_FOLDER + 'data_batch_' + str(k))
    # type of key in data_dict is not 'str' but 'bytes', a prefix of 'b' should
    # be ahead of key in order to get value
    data = data_dict.get(b'data')
    filenames = data_dict.get(b'filenames')
    labels = data_dict.get(b'labels')
    # default cifar-10 data encoding is channel first
    images = np.reshape(data, [-1, 3, 32, 32])
    # transpose to channel last
    images = np.transpose(images, [0, 2, 3, 1])
    # turn from 'RGB' to 'BGR' for the using of opencv (cv2)
    images = images[:, :, :, ::-1]
    # decode the type of filenames from 'bytes' to 'str'
    filenames = [x.decode() for x in filenames]

    # save images to disk
    for i in range(images.shape[0]):
        filename = filenames[i].replace('.png', '.%d.bmp' % labels[i])
        train_list.append('%s %d' % (filename, labels[i]))
        cv2.imwrite(TRAIN_OUTPUT_FOLDER + filename, images[i])
save_list(TRAIN_OUTPUT_FOLDER.rstrip('/') + '.txt', train_list)

# save images of test set
makedirs(TEST_OUTPUT_FOLDER)
test_list = []
data_dict = unpickle(DATA_INPUT_FOLDER + 'test_batch')
data = data_dict.get(b'data')
filenames = data_dict.get(b'filenames')
labels = data_dict.get(b'labels')
images = np.reshape(data, [-1, 3, 32, 32])
images = np.transpose(images, [0, 2, 3, 1])
images = images[:, :, :, ::-1]
filenames = [x.decode() for x in filenames]
for i in range(images.shape[0]):
    filename = filenames[i].replace('.png', '.%d.bmp' % labels[i])
    test_list.append('%s %d' % (filename, labels[i]))
    cv2.imwrite(TEST_OUTPUT_FOLDER + filename, images[i])
save_list(TEST_OUTPUT_FOLDER.rstrip('/') + '.txt', test_list)

接下来要讲训练,其中一种训练策略是从硬盘边读图片边训练,采用这种方式需要尽可能提高读图的速度,因为bmp格式的图片读取效率远高于png格式,所以我在这里没有采用CIFAR-10原本提供的png文件名,而是将后缀改成了bmp。

另外,保存下来的文件名中有两级后缀,第一级是’bmp’表示图片格式,第二级是标签号,这纯属个人喜好,我只是希望在看图的时候仅通过文件名就能知道图片的类别。

上述代码除了保存图片外,还保存了两个文件列表:train.txttest.txt,里面内容是图片文件名 + 空格 + 标签号的格式,如下:

leptodactylus_pentadactylus_s_000004.6.bmp 6
camion_s_000148.9.bmp 9
tipper_truck_s_001250.9.bmp 9
american_elk_s_001521.4.bmp 4

直接加载数据(不保存图片)

新建个文件起名为load_data.py,然后把上面的代码整理整理,将保存数据的部分删除掉,剩余部分写成函数形式放进load_data.py中即可,内容如下:

# -*- coding: utf-8 -*-
import numpy as np


def unpickle(file):
    """
    parse cifar-10 data.
    return a dict containing {data, filenames, labels, batch_label}
    """
    import pickle
    with open(file, 'rb') as fo:
        data = pickle.load(fo, encoding='bytes')
    return data


def load_train_data(data_folder):
    data_folder = data_folder.rstrip('/') + '/'
    train_images = None
    train_labels = None
    for k in range(1, 6):
        data_dict = unpickle(data_folder + 'data_batch_' + str(k))
        # type of key in data_dict is not 'str' but 'bytes', a prefix of 'b'
        # should be ahead of key in order to get value
        data = data_dict.get(b'data')
        labels = data_dict.get(b'labels')
        # default cifar-10 data encoding is channel first
        images = np.reshape(data, [-1, 3, 32, 32])
        # transpose to channel last
        images = np.transpose(images, [0, 2, 3, 1])
        # turn from 'RGB' to 'BGR' for the using of opencv (cv2)
        images = images[:, :, :, ::-1]
        if train_images is None:
            train_images = images
            train_labels = labels
        else:
            train_images = np.concatenate((train_images, images), axis=0)
            train_labels += labels
    return train_images, np.array(train_labels)


def load_test_data(data_folder):
    data_folder = data_folder.rstrip('/') + '/'
    data_dict = unpickle(data_folder + 'test_batch')
    data = data_dict.get(b'data')
    test_labels = data_dict.get(b'labels')
    test_images = np.reshape(data, [-1, 3, 32, 32])
    test_images = np.transpose(test_images, [0, 2, 3, 1])
    test_images = test_images[:, :, :, ::-1]
    return test_images, np.array(test_labels)

基于TensorFlow2.1的训练

TF2开始已经直接集成了keras,并且TF官方也建议使用keras来搭建模型,所以这里也只讲如何使用keras来搭建模型,我会由简入繁介绍多种搭建模型的方法,对应实际工作中可能碰到的一些情况。

一次性将数据加载入内存的训练方法

尽管深度学习很吃数据,常常要求巨大的数据量,但有时候我们仍然会碰到训练数据比较少的情况,比如现在我们正在使用的CIFAR-10。此时可以一次性将其全部加载入内存,这样就可以避免硬盘和内存之间的IO,加快训练速度。

Sequential Model

Sequential Model的优点是搭建模型非常简单,我们只需要在网络最开始的接口处指定输入的shape就可以了,甚至不需要指定层与层之间的输入输出,因为在Sequential模式下,上一层的输出就是下一层的输入。但也正因如此,Sequential方法只能搭建非常简单的模型,没有办法搭建像ResNet,Inception这种具有多通路的模型,正如其名字“Sequential”所表达的那样——序列化。下面我们使用Sequential Model搭建一个具有三个卷积层的神经网络来训练CIFAR-10,卷积核的大小全部是3*3,通道数分别是32,64,128,之后使用全局AveragePooling,然后接全连接层和softmax进行分类,优化器使用了Adam,loss使用交叉熵。

训练代码中有几点需要注意的事项:

  • label要转为one_hot形式,简单的原因是tf.keras就是要么要求的,稍微深层次一点的原因可参考Softmax以及Cross Entropy Loss求导
  • input_shape=(None, None, 3)input_shape是一个图片的shape,前两个维度表示height和width,可以省略,第三个维度表示channel,必需指定
  • MaxPool2D()的默认参数是pool_size=(2, 2),也就是2倍下采样
  • load_data模块是自己写的,在本文档上面一部分

代码中还有几个与训练过程没有紧密关系的注意点:

  • import ipykernel不是必需的。当model.fit()中的verbose=1时,keras的训练日志应当是一个进度条,但是在我的电脑上这个进度条不停地换行,使用import ipykernel可以解决这个问题,如果ipykernel未安装的话请使用pip install ipykernel安装。
  • DATA_INPUT_FOLDER是存放原始解压的数据:data_batch_1~5,test_batch的文件夹路径

训练代码如下:

# -*- coding: utf-8 -*-
import ipykernel
import tensorflow as tf
from tensorflow.keras.layers import Conv2D, Dense
from tensorflow.keras.layers import MaxPool2D, GlobalAveragePooling2D
from load_data import load_train_data, load_test_data

DATA_INPUT_FOLDER = 'F:/CIFAR-10/'

# reset layer name numbers of model.summary()
tf.keras.backend.clear_session()
gpus = tf.config.list_physical_devices('GPU')
tf.config.experimental.set_memory_growth(gpus[0], True)

if __name__ == '__main__':
    # load data
    train_images, train_labels = load_train_data(DATA_INPUT_FOLDER)
    test_images, test_labels = load_test_data(DATA_INPUT_FOLDER)
    train_labels = tf.one_hot(train_labels, depth=10)
    test_labels = tf.one_hot(test_labels, depth=10)
    # build model
    model = tf.keras.Sequential()
    model.add(Conv2D(32, 3, padding='same', activation='relu',
                     input_shape=(None, None, 3)))
    model.add(MaxPool2D())
    model.add(Conv2D(64, 3, padding='same', activation='relu'))
    model.add(MaxPool2D())
    model.add(Conv2D(128, 3, padding='same', activation='relu'))
    model.add(GlobalAveragePooling2D())
    model.add(Dense(10, activation='softmax'))
    model.summary()
    # model compile
    optimizer = tf.keras.optimizers.Adam(lr=1e-3)
    loss = tf.keras.losses.CategoricalCrossentropy()
    model.compile(optimizer=optimizer, loss=loss, metrics=['accuracy'])
    # train
    model.fit(
        x=train_images,
        y=train_labels,
        batch_size=32,
        epochs=10,
        validation_data=[test_images, test_labels],
        validation_freq=2,
        verbose=1)

上面训练代码中,build model部分也可以更加简洁地使用如下方式:

model = tf.keras.Sequential([
    Conv2D(32, 3, padding='same', activation='relu',
           input_shape=(None, None, 3)),
    MaxPool2D(),
    Conv2D(64, 3, padding='same', activation='relu'),
    MaxPool2D(),
    Conv2D(128, 3, padding='same', activation='relu'),
    GlobalAveragePooling2D(),
    Dense(10, activation='softmax')])
model.summary()

model.summary()的作用是打印网络的结构和参数:

Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d (Conv2D)              (None, None, None, 32)    896       
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, None, None, 32)    0         
_________________________________________________________________
conv2d_1 (Conv2D)            (None, None, None, 64)    18496     
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, None, None, 64)    0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, None, None, 128)   73856     
_________________________________________________________________
global_average_pooling2d (Gl (None, 128)               0         
_________________________________________________________________
dense (Dense)                (None, 10)                1290      
=================================================================
Total params: 94,538
Trainable params: 94,538
Non-trainable params: 0
_________________________________________________________________

这里简单手算一下卷积层的参数,卷积层的参数计算方式是:
卷积核高 * 卷积层宽 * 上一层通道数 * 当前层通道数 + 当前层通道数(bias数目)
所以三个卷积层参数如下:
第一个:3*3*3*32 + 32 = 896
第二个:3*3*32*64 + 64 = 18496
第三个:3*3*64*128 + 128 = 73856
全连接层的参数:128*10 + 10= 1290

训练日志如下:

Train on 50000 samples, validate on 10000 samples
Epoch 1/10
50000/50000 [=========] - 14s 272us/sample - loss: 1.6634 - accuracy: 0.4384
Epoch 2/10
50000/50000 [=========] - 15s 301us/sample - loss: 1.1417 - accuracy: 0.5946 - val_loss: 1.0881 - val_accuracy: 0.6218
Epoch 3/10
50000/50000 [=========] - 12s 245us/sample - loss: 0.9775 - accuracy: 0.6580
Epoch 4/10
50000/50000 [=========] - 13s 267us/sample - loss: 0.8666 - accuracy: 0.6983 - val_loss: 0.9410 - val_accuracy: 0.6780
Epoch 5/10
50000/50000 [=========] - 12s 238us/sample - loss: 0.7828 - accuracy: 0.7296
Epoch 6/10
50000/50000 [=========] - 15s 304us/sample - loss: 0.7156 - accuracy: 0.7536 - val_loss: 0.9301 - val_accuracy: 0.6835
Epoch 7/10
50000/50000 [=========] - 13s 256us/sample - loss: 0.6584 - accuracy: 0.7707
Epoch 8/10
50000/50000 [=========] - 14s 271us/sample - loss: 0.6025 - accuracy: 0.7913 - val_loss: 0.8300 - val_accuracy: 0.7272
Epoch 9/10
50000/50000 [=========] - 11s 228us/sample - loss: 0.5565 - accuracy: 0.8071
Epoch 10/10
50000/50000 [=========] - 14s 270us/sample - loss: 0.5034 - accuracy: 0.8253 - val_loss: 0.8551 - val_accuracy: 0.7311

一般的模型创建方法

如果我们想要创建类似ResNet这样的多通路模型该怎么办?此时Sequential Model就无法胜任了,我们需要改用tf.keras.layers中的API进行搭建,这也是最一般的模型创建方法。搭建的语法是:

outputs = tf.keras.layers.API(parameters) (inputs)

API是当前层的函数名,比如ConvD,Dense等,后面跟了两个括号:第一个是API的参数,如通道数,卷积核大小等;第二个括号里的inputs是当前层的输入,outputs是当前层的输出。

下面我们使用这种方式搭建一个非常短小的残差网络:

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

import ipykernel
import tensorflow as tf
from tensorflow.keras.layers import Conv2D, Dense
from tensorflow.keras.layers import Add
from tensorflow.keras.layers import MaxPool2D, GlobalAveragePooling2D
from load_data import load_train_data, load_test_data

DATA_INPUT_FOLDER = 'F:/CIFAR-10/'

# reset layer name numbers of model.summary()
tf.keras.backend.clear_session()
gpus = tf.config.list_physical_devices('GPU')
tf.config.experimental.set_memory_growth(gpus[0], True)

if __name__ == '__main__':
    # load data
    train_images, train_labels = load_train_data(DATA_INPUT_FOLDER)
    test_images, test_labels = load_test_data(DATA_INPUT_FOLDER)
    train_labels = tf.one_hot(train_labels, depth=10)
    test_labels = tf.one_hot(test_labels, depth=10)
    # build model
    inputs = tf.keras.layers.Input(shape=(None, None, 3))
    conv_1 = Conv2D(32, 3, padding='same', activation='relu')(inputs)
    pool1 = MaxPool2D()(conv_1)
    conv2_1 = Conv2D(64, 1, padding='same')(pool1)
    conv2_1a = Conv2D(32, 3, padding='same', activation='relu')(pool1)
    conv2_1b = Conv2D(64, 3, padding='same')(conv2_1a)
    conv2_2 = Add()([conv2_1, conv2_1b])
    conv2_2 = tf.keras.layers.Activation('relu')(conv2_2)
    conv2_2a = Conv2D(32, 3, padding='same', activation='relu')(conv2_2)
    conv2_2b = Conv2D(64, 3, padding='same')(conv2_2a)
    conv2_3 = Add()([conv2_2, conv2_2b])
    conv2_3 = tf.keras.layers.Activation('relu')(conv2_3)
    pool2 = MaxPool2D()(conv2_3)
    conv3_1 = Conv2D(128, 1, padding='same')(pool2)
    conv3_1a = Conv2D(64, 3, padding='same', activation='relu')(pool2)
    conv3_1b = Conv2D(128, 3, padding='same')(conv3_1a)
    conv3_2 = Add()([conv3_1, conv3_1b])
    conv3_2 = tf.keras.layers.Activation('relu')(conv3_2)
    conv3_2a = Conv2D(64, 3, padding='same', activation='relu')(conv3_2)
    conv3_2b = Conv2D(128, 3, padding='same')(conv3_2a)
    conv3_3 = Add()([conv3_2, conv3_2b])
    conv3_3 = tf.keras.layers.Activation('relu')(conv3_3)
    global_pool = GlobalAveragePooling2D()(conv3_3)
    outputs = Dense(10, activation='softmax')(global_pool)

    model = tf.keras.Model(inputs=inputs, outputs=outputs)
    model.summary()
    # model compile
    optimizer = tf.keras.optimizers.Adam(lr=1e-3)
    loss = tf.keras.losses.CategoricalCrossentropy()
    model.compile(optimizer=optimizer, loss=loss, metrics=['accuracy'])
    # train
    model.fit(
        x=train_images,
        y=train_labels,
        batch_size=32,
        epochs=10,
        validation_data=[test_images, test_labels],
        validation_freq=2,
        verbose=1)

模型参数量以及训练日志如下:

Total params: 335,754
Trainable params: 335,754
Non-trainable params: 0
__________________________________________________________________________________________________
Train on 50000 samples, validate on 10000 samples
Epoch 1/10
50000/50000 [=========] - 19s 380us/sample - loss: 1.6835 - accuracy: 0.4422
Epoch 2/10
50000/50000 [=========] - 18s 351us/sample - loss: 1.1295 - accuracy: 0.5998 - val_loss: 0.9964 - val_accuracy: 0.6569
Epoch 3/10
50000/50000 [=========] - 16s 325us/sample - loss: 0.9344 - accuracy: 0.6726
Epoch 4/10
50000/50000 [=========] - 18s 353us/sample - loss: 0.8035 - accuracy: 0.7200 - val_loss: 0.8484 - val_accuracy: 0.7164
Epoch 5/10
50000/50000 [=========] - 16s 316us/sample - loss: 0.7198 - accuracy: 0.7497
Epoch 6/10
50000/50000 [=========] - 18s 358us/sample - loss: 0.6396 - accuracy: 0.7767 - val_loss: 0.9410 - val_accuracy: 0.6899
Epoch 7/10
50000/50000 [=========] - 16s 322us/sample - loss: 0.5890 - accuracy: 0.7953
Epoch 8/10
50000/50000 [=========] - 17s 348us/sample - loss: 0.5376 - accuracy: 0.8131 - val_loss: 0.8065 - val_accuracy: 0.7342
Epoch 9/10
50000/50000 [=========] - 16s 318us/sample - loss: 0.4835 - accuracy: 0.8309
Epoch 10/10
50000/50000 [=========] - 17s 349us/sample - loss: 0.4493 - accuracy: 0.8416 - val_loss: 0.8392 - val_accuracy: 0.7354

准确率似乎并没有比上面Sequential Model搭的三层简单模型高多少。

边读边训的方法

这部分的代码有问题,不建议看了,以后有机会再验一下。)

CIFAR-10数据非常小,所以可以一次性将其加载到内存中去。但实际上近些年来深度学习的数据量开始变得越来越庞大,如ImageNet,COCO等,这些都还是公开数据库而已,工业界用于产品开发的数据库更是大到不可思议,所以基本不可能一次性将数据都读入内存中去。

此时就需要一种边读边训练的方法,现在的深度学习框架一般都提供了这种能力:CPU负责读取,GPU负责训练。在TensorFlow中我们可以通过Dataset API实现这种功能。其中起主要作用的是from_tensor_slicesmap两个接口,下面配合这两个接口讲述边读边训的简单原理:

  • 首先我们需要制作数据列表。图片的总数据量可能会非常大以至于超出内存的装载能力,但是数据列表通常不会(如果你的数据列表都大到内存无法装载,那就买内存条吧)。所以我们可以将数据列表送给from_tensor_slices,此时Dataset中的每一个元素都是一个数据的地址。
  • 然后可以使用map接口来读取图片。由于TF2.1使用了动态图或动态tensor(tf.Tensor),所以读图命令我们可以任意选择,只要读取的结果是numpy.array或者tf.Tensor即可。可以参考"python读图命令与效率汇总"来选择合适的读取命令。

下面我们仍然以CIFAR-10为例。emmm,我们可以假装CIFAR-10的数据量“很大”。

首先制作两个数据列表:train.txt和test.txt,列表内容如下,以空格作为分隔,前面是图片文件名,后面是类别号。

leptodactylus_pentadactylus_s_000004.6.bmp 6
camion_s_000148.9.bmp 9
tipper_truck_s_001250.9.bmp 9
american_elk_s_001521.4.bmp 4
station_wagon_s_000293.1.bmp 1
coupe_s_001735.1.bmp 1
cassowary_s_001300.2.bmp 2
cow_pony_s_001168.7.bmp 7
sea_boat_s_001584.8.bmp 8
tabby_s_001355.3.bmp 3
......

训练代码如下,需注意以下几点:

  • tf.data.Dataset.from_tensor_slices(train_list)以列表作为最初的数据入口
  • dataset_train.map配合tf.py_function读取列表中的数据
  • tf.py_function的参数说明:
    • func=_map:映射函数的名称是_map,即_map(inp: tf.Tensor)
    • inp=[x]_map的输入是lambda x中的x,它表示目前dataset_train中的一条记录,如在不shuffle的情况下,第一条记录就是leptodactylus_pentadactylus_s_000004.6.bmp 6
    • Tout=[tf.float32, tf.float32]指定了_map函数的输出类型
  • _map(inp: tf.Tensor):如果不加: tf.Tensor的话,_map函数的输入是静态Tensor,那就没办法转成普通的python string,也就无法使用opencv来读图,加上: tf.Tensor的话可以起到类型强转的作用,inp就是一个动态Tensor,而后可以使用inp.numpy().decode()转成普通python string做进一步操作。另外,特别注意,在_map函数中,读图后可以做一些自己喜欢的数据增强
  • 陈年BUG(至少我认为是个BUG):当一个Tensor或者numpy array经tf.py_function作用后,model.fit()将无法推断其shape,导致训练报错( ValueError: Cannot take the length of shape with unknown rank.),处理方式是再加一个不适用tf.py_function的map函数重新设置一下shape,也就是下面代码中的_map_set_shape(image, label),感觉就是TF自己没搞好所以导致了这种麻烦。

此代码训练极其缓慢,肯定是有问题的,可能有一些绕弯弯的方法能够搞定这种以列表模式边读边训的问题,不过最近比较忙,没有时间去摸索,先留个坑,以后再填。

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

import ipykernel
import os
import cv2
import numpy as np
import tensorflow as tf
from tensorflow.keras.layers import Conv2D, Dense
from tensorflow.keras.layers import MaxPool2D, GlobalAveragePooling2D

# reset layer name numbers of model.summary()
tf.keras.backend.clear_session()
gpus = tf.config.list_physical_devices('GPU')
tf.config.experimental.set_memory_growth(gpus[0], True)


# build a 3-layers model
def build_model(input_shape=(None, None, 3)):
    inputs = tf.keras.layers.Input(shape=input_shape)
    conv_1 = Conv2D(32, 3, padding='same', activation='relu')(inputs)
    pool1 = MaxPool2D()(conv_1)
    conv_2 = Conv2D(64, 3, padding='same', activation='relu')(pool1)
    pool2 = MaxPool2D()(conv_2)
    conv_3 = Conv2D(128, 3, padding='same', activation='relu')(pool2)
    global_pool = GlobalAveragePooling2D()(conv_3)
    outputs = Dense(10, activation='softmax')(global_pool)
    model = tf.keras.Model(inputs=inputs, outputs=outputs)
    return model


def read_file_list(filename):
    file_list = []
    if os.path.exists(filename) and os.path.isfile(filename):
        for line in open(filename, 'r'):
            file_list.append(line.rstrip())
    return file_list


def join_root_folder(root_folder, file_list):
    for i, line in enumerate(file_list):
        file_list[i] = os.path.join(root_folder, line)
    return file_list


# map function for tf.Dataset
def _map(inp: tf.Tensor):
    file, label = inp.numpy().decode().split(' ')
    image = cv2.imread(file)
    label = int(label)
    label = tf.one_hot(label, depth=10)
    return image, label


def _map_set_shape(image, label):
    image.set_shape([None, None, 3])
    label.set_shape([10, ])
    return image, label


if __name__ == '__main__':
    # train parameters
    EPOCH = 10
    BATCH_SIZE = 50
    # load data
    DATA_INPUT_FOLDER = 'F:/CIFAR-10'
    TRAIN_LIST_FILE = 'F:/CIFAR-10/train.txt'
    TEST_LIST_FILE = 'F:/CIFAR-10/test.txt'
    train_list = join_root_folder(os.path.join(DATA_INPUT_FOLDER, 'train'),
                                  read_file_list(TRAIN_LIST_FILE))
    test_list = join_root_folder(os.path.join(DATA_INPUT_FOLDER, 'test'),
                                 read_file_list(TEST_LIST_FILE))
    # train dataset
    dataset_train = tf.data.Dataset.from_tensor_slices(train_list)
    dataset_train = dataset_train.shuffle(
        buffer_size=len(train_list), reshuffle_each_iteration=True)
    dataset_train = dataset_train.map(lambda x: tf.py_function(
        func=_map, inp=[x], Tout=[tf.float32, tf.float32]))
    dataset_train = dataset_train.map(_map_set_shape)
    dataset_train = dataset_train.repeat(EPOCH).batch(BATCH_SIZE)
    # test dataset
    dataset_test = tf.data.Dataset.from_tensor_slices(test_list)
    dataset_test = dataset_test.map(lambda x: tf.py_function(
        func=_map, inp=[x], Tout=[tf.float32, tf.float32]))
    dataset_test = dataset_test.map(_map_set_shape)
    dataset_test = dataset_test.repeat().batch(BATCH_SIZE)

    # build model
    model = build_model(input_shape=(None, None, 3))
    model.summary()
    # model compile
    optimizer = tf.keras.optimizers.Adam(lr=1e-3)
    loss = tf.keras.losses.CategoricalCrossentropy()
    model.compile(optimizer=optimizer, loss=loss, metrics=['accuracy'])
    model.fit(
        dataset_train,
        validation_data=dataset_test,
        validation_steps=200,
        validation_freq=2,
        verbose=1)

参考资料

CIFAR-10 and CIFAR-100 datasets
80 Million Tiny Images
Learning Multiple Layers of Features from Tiny Images, Alex Krizhevsky, 2009
ImageNet Classification with Deep Convolutional Neural Networks, Alex Krizhevsky et al., 2012
MNIST

  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值