一、前言

1.1、数据集介绍

​ CIFAR-10数据集由10个类的60000个32x32彩色图像组成,每个类有6000个图像。有50000个训练图像和10000个测试图像。
​ 数据集分为五个训练批次和一个测试批次,每个批次有10000个图像。测试批次包含来自每个类别的恰好1000个随机选择的图像。训练批次以随机顺序包含剩余图像,但一些训练批次可能包含来自一个类别的图像比另一个更多。总体来说,五个训练集之和包含来自每个类的正好5000张图像。
​ 以下是数据集中的类,以及来自每个类的10个随机图像:
在这里插入图片描述

​ CIFAR-100 数据集就像CIFAR-10,除了它有100个类,每个类包含600个图像。,每类各有500个训练图像和100个测试图像。CIFAR-100 中的100个类被分成20个超类。每个图像都带有一个“精细”标签(它所属的类)和一个“粗糙”标签(它所属的超类)。以下是 CIFAR-100 中的类别列表:

在这里插入图片描述

1.2、本次网络介绍

​ 本次共搭建18个网络层:10个卷积层、5个最大池化层以及3个全连接层,其中2个卷积层+1个最大池化层为1个单元。具体网络参数如下图所示:

在这里插入图片描述

二、导入库

​ 本次使用的Tensorflow版本为2.0.0,为方便搭建网络,利用 kears 构建网络,因此,导入 kears 的 layers、Sequential、optimizers、datasets。

import tensorflow as tf
from tensorflow.keras import layers,Sequential,optimizers,datasets

三、导入Cifar-100数据集并查看数据集格式

​ 利用 kearas.datasets 函数直接导入数据集,并分为训练集与测试集,并查看数据集形状:

(x,y),(x_test,y_test) = datasets.cifar100.load_data() # 加载数据集
print('x:',x.shape,'y:',y.shape,'x_test:',x_test.shape,"y_test:",y_test.shape)

​ 得到的数据格式如下:

在这里插入图片描述

​ 因为 y 与 y_test 都是标签集,因此,其维度均应为1维,而加载后的却是二维张量,所以要进行数据处理。

y = tf.reshape(y,[50000])
y_test = tf.reshape(y_test,[10000])
print('y:',y.shape,'y_test:',y_test.shape)

在这里插入图片描述

四、数据处理

4.1、数据预处理函数

​ 通过数据与处理函数,将 numpy 类型数据变为 tensorflow 类型数据。

def preprocess(x,y):                      # 预处理函数
    # [0-1]
    x = tf.cast(x,dtype=tf.float32) / 255 # 归一化处理
    y = tf.cast(y,dtype=tf.int64)         # y必须是整数型,因为onehot里输入不能为float
    return x,y

​ 通过数据预处理函数便可以将别的类型的数据变为 tensorflow 类型。因为像素均为 0 -255 值,所以进行归一化处理,将数据值分布变为 0 -1。由后面要进行 one_hot 编码,所以标签集数据必须为 tfint64类型。

4.2、进行数据处理

train_db = tf.data.Dataset.from_tensor_slices((x,y)) # 将输入的张量的第一个维度看做样本的个数,沿其第一个维度将tensor切片,得到的每个切片是一个样本数据。实现了输入张量的自动切片。
train_db = train_db.shuffle(10000).map(preprocess).batch(64) # 64个样本为一个 batch
test_db = tf.data.Dataset.from_tensor_slices((x_test,y_test))
test_db = test_db.map(preprocess).batch(64)

​ 首先对于训练集,对变量进行切片处理,然后利用 shuffle 函数打乱数据,利用与处理函数进行数据转换,并64个样本为一个batch;而对于测试集,并不需要来打乱样本顺序。

五、搭建网络

​ 为便于区分,本次搭建分为两步:一是卷积层——包括卷积与池化两个操作,选择最大池化操作;二是全连接层,最后一层输出神经元为100(本次分类的类数)且最后一层不添加激活函数。

5.1、卷积层搭建

​ 卷积层共 5 个单元,每个单元都是“2+1”组合:2个卷积层,一个池化层,具体形式如下:

conv_layers = [
    # unit 1
    layers.Conv2D(64,kernel_size=[3,3],padding='same',activation=tf.nn.relu),
    layers.Conv2D(64,kernel_size=[3,3],padding='same',activation=tf.nn.relu),
    layers.MaxPool2D(pool_size=[2,2],strides=2,padding='same'),

    # unit 2
    layers.Conv2D(128,kernel_size=[3,3],padding='same',activation=tf.nn.relu),
    layers.Conv2D(128,kernel_size=[3,3],padding='same',activation=tf.nn.relu),
    layers.MaxPool2D(pool_size=[2,2],strides=2,padding='same'),

    # unit 3
    layers.Conv2D(256,kernel_size=[3,3],padding='same',activation=tf.nn.relu),
    layers.Conv2D(256,kernel_size=[3,3],padding='same',activation=tf.nn.relu),
    layers.MaxPool2D(pool_size=[2,2],strides=2,padding='same'),

    # unit 4
    layers.Conv2D(512,kernel_size=[3,3],padding='same',activation=tf.nn.relu),
    layers.Conv2D(512,kernel_size=[3,3],padding='same',activation=tf.nn.relu),
    layers.MaxPool2D(pool_size=[2,2],strides=2,padding='same'),

    # unit 5
    layers.Conv2D(512,kernel_size=[3,3],padding='same',activation=tf.nn.relu),
    layers.Conv2D(512,kernel_size=[3,3],padding='same',activation=tf.nn.relu),
    layers.MaxPool2D(pool_size=[2,2],strides=2,padding='same')

]

5.2、全连接层搭建

​ 全连接层共三层,其参数如下:

fc_net = Sequential([
        layers.Dense(256, activation=tf.nn.relu),     # 输出为 256 
        layers.Dense(128, activation=tf.nn.relu),	  # 输出为 128
        layers.Dense(100, activation=None),			  # 输出为 100,且不加激活函数
    ])

六、设立各部分网络输入形式、优化器以及定义所有变量

​ 因为照片是 32 × 32 × 3 32\times32\times3 32×32×3 的,因此第一部分的输入是 [ N o n e , 32 , 32 , 3 ] [None,32,32,3] [None,32,32,3] ,其中None 代表样本数;第二部分全连接层输入为 [ N o n e , 512 ] [None,512] [None,512] ,因此,为保证输入格式正确,需要对第一部分网络的输出进行 r e s h a p e reshape reshape 操作。

​ 选择 A d a m Adam Adam 优化器,学习率设置为 1 e − 4 1e-4 1e4网络的总变量 = 第一部分变量 + 第二部分变量。

conv_net.build(input_shape=[None, 32, 32, 3])
fc_net.build(input_shape=[None,512])

optimizer = optimizers.Adam(lr=1e-4)
variables = conv_net.variables + fc_net.variables

七、前向传播与误差计算

​ 训练 50 次, b a t c h s i z e = 64 batchsize = 64 batchsize=64 ,每训练 100 个批次,输出打印准确率与损失函数值。

for epoch in range(50):
    for step,(x,y) in enumerate(train_db):        # enumerate() 函数用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,同时列出数据和数据下标,一般用在 for 循环当中
        with tf.GradientTape() as tape:
            # [b,32,32,3] => [b,1,1,512]
            out = conv_net(x)
            # flaten
             out = tf.reshape(out,[-1,512])
            # [b,512] => [b,100]
             logits = fc_net(out)
            # onehot编码:[b] => [b,100]
            y_onehotcode = tf.one_hot(y,100) # y不能为float
            # compute loss
            loss = tf.losses.categorical_crossentropy(y_onehotcode,logits,from_logits=True)
            loss = tf.reduce_mean(loss)

八、梯度求解与权重更新

grads = tape.gradient(loss,variables)           # 求解梯度
optimizer.apply_gradients(zip(grads,variables)) # 梯度更新
if step % 100 ==0:
    print(epoch,step,'loss:',float(loss))

九、测试训练集

for x,y in test_deb:
    out = conv_net(x)
    out = tf.reshape(out,[-1,512]) # 保证全连接层输入正确
	logits = fc_net(out)
    prob = tf.nn.softmax(logits,axis=1) 
    pred = tf.argmax(prob,axis= 1)	# 把全连接层的输出进行分类
    # 求解准确率
    correct = tf.cast(tf.equal(pred,y),dtype=tf.int32)
    total_num += x.shape[0]
    total_correct += int(correct)
    
acc = total_correct / total_num
print(epoch,'acc:',acc)

十、附录

10.1、注意事项

  • 数据集的形状十分重要,无论是加载后数据集还是要预处理的数据集,都应确保其 s h a p e shape shape 准确,否则无法代入网络进行训练;
  • 利用 tf.one_hot() 函数进行编码时,要确保数据为 tf.int64

10.2、完整代码

import os
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"  # 屏蔽tensorflow的输出日志信息,必须放在导入TF库前


import tensorflow as tf
from tensorflow.keras import layers,Sequential,optimizers,datasets

conv_layers = [
    # unit 1
    layers.Conv2D(64,kernel_size=[3,3],padding='same',activation=tf.nn.relu),
    layers.Conv2D(64,kernel_size=[3,3],padding='same',activation=tf.nn.relu),
    layers.MaxPool2D(pool_size=[2,2],strides=2,padding='same'),

    # unit 2
    layers.Conv2D(128,kernel_size=[3,3],padding='same',activation=tf.nn.relu),
    layers.Conv2D(128,kernel_size=[3,3],padding='same',activation=tf.nn.relu),
    layers.MaxPool2D(pool_size=[2,2],strides=2,padding='same'),

    # unit 3
    layers.Conv2D(256,kernel_size=[3,3],padding='same',activation=tf.nn.relu),
    layers.Conv2D(256,kernel_size=[3,3],padding='same',activation=tf.nn.relu),
    layers.MaxPool2D(pool_size=[2,2],strides=2,padding='same'),

    # unit 4
    layers.Conv2D(512,kernel_size=[3,3],padding='same',activation=tf.nn.relu),
    layers.Conv2D(512,kernel_size=[3,3],padding='same',activation=tf.nn.relu),
    layers.MaxPool2D(pool_size=[2,2],strides=2,padding='same'),

    # unit 5
    layers.Conv2D(512,kernel_size=[3,3],padding='same',activation=tf.nn.relu),
    layers.Conv2D(512,kernel_size=[3,3],padding='same',activation=tf.nn.relu),
    layers.MaxPool2D(pool_size=[2,2],strides=2,padding='same')

]

def preprocess(x,y):  # 预处理函数
    # [0-1]
    x = tf.cast(x,dtype=tf.float32) / 255
    y = tf.cast(y,dtype=tf.int64)         # y必须是整数型,因为onehot里输入不能为float
    return x,y

(x,y),(x_test,y_test) = datasets.cifar100.load_data() # 加载数据集
y = tf.reshape(y,[50000])
y_test = tf.reshape(y_test,[10000])

train_db = tf.data.Dataset.from_tensor_slices((x,y)) # 将输入的张量的第一个维度看做样本的个数,沿其第一个维度将tensor切片,得到的每个切片是一个样本数据。实现了输入张量的自动切片。
train_db = train_db.shuffle(10000).map(preprocess).batch(64)
test_db = tf.data.Dataset.from_tensor_slices((x_test,y_test))
test_db = test_db.map(preprocess).batch(64)

def main():
    # [b,32,32,3] => [b,1,1,512]
    conv_net = Sequential(conv_layers)
    # x =tf.random.normal([4,32,32,3])
    # out = conv_net(x)
    # print(out.shape)
    fc_net = Sequential([
        layers.Dense(256,activation=tf.nn.relu),
        layers.Dense(128,activation=tf.nn.relu),
        layers.Dense(100,activation=None)             # 最后一个全连接层,因为有100个种类,所以输出为100
    ])
    conv_net.build(input_shape=[None, 32, 32, 3])
    fc_net.build(input_shape=[None,512])

    optimizer = optimizers.Adam(lr=1e-4)
    variables = conv_net.variables + fc_net.variables

    for epoch in range(50):
        for step,(x,y) in enumerate(train_db):        # enumerate() 函数用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,同时列出数据和数据下标,一般用在 for 循环当中
            with tf.GradientTape() as tape:
                # [b,32,32,3] => [b,1,1,512]
                out = conv_net(x)
                # flaten
                out = tf.reshape(out,[-1,512])
                # [b,512] => [b,100]
                logits = fc_net(out)
                # onehot编码:[b] => [b,100]
                y_onehotcode = tf.one_hot(y,100) # y不能为float
                # compute loss
                loss = tf.losses.categorical_crossentropy(y_onehotcode,logits,from_logits=True)
                loss = tf.reduce_mean(loss)

            grads = tape.gradient(loss,variables)           # 求解梯度
            optimizer.apply_gradients(zip(grads,variables)) # 梯度更新 zip() 函数用于将可迭代的对象作为参数,将对象中对应的元素打包成一个个元组,然后返回由这些元组组成的列表。
                                                            # 如果各个迭代器的元素个数不一致,则返回列表长度与最短的对象相同。

            if step % 100 == 0:
                print(epoch,step,'loss:',float(loss))


        total_num = 0
        total_correct = 0

        for x,y in test_db:
            out = conv_net(x)
            out = tf.reshape(out,[-1,512])
            logits = fc_net(out)
            prob = tf.nn.softmax(logits,axis=1) # 进行预测
            pred = tf.argmax(prob,axis=1)
            correct = tf.cast(tf.equal(pred,y),dtype=tf.int32)
            total_num += x.shape[0]
            total_correct += int(correct)

        acc = total_correct / total_num
        print(epoch,'acc:',acc)

if __name__ == '__main__':
    main()
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐