resnet-18 网络 训练cifar100数据-tf2.0-详细注释

resnet.py

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



class BasicBlock(layers.Layer):
    # 继承自layers.layer 这个父类。实现2个基本函数:__init__ 初始化函数  call 函数

    def __init__(self, filter_num, stride=1):
        # __init__ 中定义网络层,做初始化。call中使用__init__中定义好的网络,进行前向传播(计算,得到输出)
        # filter_num :通道数量 贯穿始终。  stride:=1时,如果做padding 则图片大小不变。如果stride !=1 则会对输入做padding,使其可以对stride整除。比如stride=2,输入为32*32,则输出为16*16

        super(BasicBlock, self).__init__()

        self.conv1 = layers.Conv2D(filter_num, (3, 3), strides=stride, padding="same")
        # 选用小的卷积核,准确度不会下降,所以一般选择 1*1 3*3 这样的卷积核 两个卷积层中,第一个卷积层strides=stride 有可能执行下采样 第二个stride=1,即第二个卷积层肯定不会执行下采样,这样调用BasicBlock时,即使stride不为1,下采样只会执行一次。
        self.bn1 = layers.BatchNormalization()
        self.relu = layers.Activation('relu')

        self.conv2 = layers.Conv2D(filter_num, (3, 3), strides=1, padding='same')
        #                                              第二个stride=1
        self.bn2 = layers.BatchNormalization()

        if stride != 1:
            self.downsample = Sequential()
            self.downsample.add(layers.Conv2D(filter_num, (1, 1), strides=stride))
            #     .add :向Sequential中添加层       通过1*1的卷积核 和strides改变其形状。使其与通过卷积层后的数据形状相同。
        else:
            self.downsample = lambda x:x
            # lambda:             参数:函数体。举例:b=lambda x: x+1 相当于 def g(x):return x+1

    def call(self, inputs, training=None):
        # call函数有2个基本参数 inputs 和 training。 这是两个函数的标准的写法

        # [b, h, w, c]
        out = self.conv1(inputs)
        out = self.bn1(out, training=training)
        out = self.relu(out)

        out = self.conv2(out)
        out = self.bn2(out, training=training)

        identify = self.downsample(inputs)

        output = layers.add([out, identify])
        # 两个输出直接做相加,得到output  layers下面有一个add,把这2个层添加进来相加。
        output = tf.nn.relu(output)

        return output

# Res Block 模块。继承keras.Model或者keras.Layer都可以
class ResNet(keras.Model):

    # 第一个参数layer_dims:[2, 2, 2, 2] 4个Res Block,每个包含2个Basic Block
    # 第二个参数num_classes:我们的全连接输出,取决于输出有多少类。
    def __init__(self, layer_dims, num_classes=100):

        super(ResNet, self).__init__()

        # resnet-18的第一层:预处理层。实现起来比较灵活可以加 MAXPool2D,可以没有。
        self.stem = Sequential([layers.Conv2D(64, (3,3), strides=(1, 1)),
                               layers.BatchNormalization(),
                               layers.Activation('relu'),
                               layers.MaxPool2D(pool_size=(2, 2),strides=(1, 1), padding='same')
                                ])

        # 创建4个Res Block;注意第1项不一定以2倍形式扩张,都是比较随意的,这里都是经验值。
        self.layers1 = self.build_resblock(64, layer_dims[0])
        self.layers2 = self.build_resblock(128, layer_dims[1], stride=2)
        self.layers3 = self.build_resblock(256, layer_dims[2], stride=2)
        self.layers4 = self.build_resblock(512, layer_dims[3], stride=2)

        # 残差网络输出output: [b, 512, h, w];长宽无法确定,上面的需要运算一下,如果这里没有办法确定的话。
        # 用这个层可以自适应的确定输出。表示不管你的长和宽是多少,我会在某一个channel上面,所有的长和宽像素值加起来
        # 求一个均值,比如:有512个3*3的feature map,[512, 3, 3],每个feature map为3*3,9个像素值,我做一个这样的
        # average,得到一个平均的像素值是多少。下面这里处理之后得到一个512的vector,准确来说为[512, 1, 1],这个512的
        # vector就可以送到先形成进行分类。
        # [b, 512, h, w]  GlobalAveragePooling2D 作用是把一个feature map 上的像素值做一个均值 输出是[b, 512]
        self.avgpool = layers.GlobalAveragePooling2D()
        # 全连接层 输出类别是num_classes
        self.fc = layers.Dense(num_classes)

    def call(self, inputs, training=None):
        # __init__中准备工作完毕;下面完成前向运算过程。
        x = self.stem(inputs)

        x = self.layers1(x)
        x = self.layers2(x)
        x = self.layers3(x)
        x = self.layers4(x)

        # 做一个global average pooling,得到之后只会得到一个channel,不需要做reshape操作了。
        # shape为 [batchsize, channel]
        # [b, 512]
        x = self.avgpool(x)
        # [b, 100]
        x = self.fc(x)

        return x

    # 实现 Res Block; 创建一个Res Block  filter_num:通道数量    blocks: block数量,即堆叠的BasicBlock数量。
    def build_resblock(self, filter_num, blocks, stride=1):

        res_blocks = Sequential()
        # 添加第一个BasicBlock
        # may down sample 也许进行下采样。
        # 对于当前Res Block中的Basic Block,我们要求每个Res Block只有一次下采样的能力。
        res_blocks.add(BasicBlock(filter_num, stride))
        # 添加后续的BasicBlock 设置stride=1 不让它执行下采样。
        for _ in range(1, blocks):
            res_blocks.add(BasicBlock(filter_num, stride=1))  # 这里stride设置为1,只会在第一个Basic Block做一个下采样。

        return res_blocks


def resnet18():

    return ResNet([2, 2, 2, 2])
# 如果我们要使用 ResNet-34 的话,那34是怎样的配置呢?只需要改一下这里就可以了。对于56,152去查一下配置
def resnet34():

    return ResNet([3, 4, 6, 3])  #4个Res Block,第1个包含3个Basic Block,第2为4,第3为6,第4为3




# 补充:下面需要使用的。
#
# 介绍一下global average pooling ,这个概念出自于 network in network;global average pooling 与 average pooling 的差别就在 “global” 这一个字眼上。global 与 local 在字面上都是用来形容 pooling 窗口区域的。 local 是取 feature map 的一个子区域求平均值,然后滑动这个子区域; global 显然就是对整个 feature map 求平均值了。
# 主要是用来解决全连接的问题,其主要是是将最后一层的特征图进行整张图的一个均值池化,形成一个特征点,将这些特征点组成最后的特征向量进行softmax中进行计算
# 举个例子:假如,最后的一层的数据是10个6×6的特征图,global average pooling是将每一张特征图计算所有像素点的均值,输出一个数据值。这样10 个特征图就会输出10个数据点,将这些数据点组成一个1×10的向量的话,就成为一个特征向量,就可以送入到softmax的分类中计算了;

# 如果在training时出现out of memory,就是显卡的显存比较小网络参数比较大,可以适当调小batchsize
# 可以尝试修改resnet。标准的是有4个resblock 每个包含2个basicblock,可以把它改成[1, 1, 1, 1] 这样每个resnet 包含一个basicblock,参数大概减少了一半
# 可以尝试google的colab,提供了一个k80的显卡,11G的显存。

resnet-18-train.py

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

os.environ["CUDA_VISIBLE_DEVICES"] = "1"
os.environ["TF_CPP_MIN_LOG_LEVEL"] = "2"
tf.random.set_seed(2345)

# 数据预处理,仅仅是类型的转换。    [-1~1]
def preprocess(x, y):

    x = 2 * tf.cast(x, dtype=tf.float32) / 255. - 1.
    y = tf.cast(y, dtype=tf.int32)

    return x, y

# 加载cifar100数据 如果本地没有,会自动下载。
(x, y), (x_test, y_test) = datasets.cifar100.load_data()
# print(x.shape, y.shape, x_test.shape, y_test.shape)
# 形状:(50000, 32, 32, 3) (50000, 1)这里要注意,y的形状,后面要去掉为1的维度  (10000, 32, 32, 3) (10000, 1)

y = tf.squeeze(y, axis=1)           # 或者tf.squeeze(y, axis=1)把1维度的squeeze掉。 去掉为1 的维度 [50000, 1] => [5000]
y_test = tf.squeeze(y_test, axis=1) # 去掉为1 的维度 [50000, 1] => [5000]

# 加载数据集
train_db = tf.data.Dataset.from_tensor_slices((x, y))
# 对数据的处理       打乱数据        批预处理         设置batch大小
train_db = train_db.shuffle(10000).map(preprocess).batch(32)

test_db = tf.data.Dataset.from_tensor_slices((x_test, y_test))
test_db = test_db.map(preprocess).batch(32)

# 这里只是为了查看数据形状,在整个程序中并无意义
sample = next(iter(train_db))
print("sample:", sample[0].shape, sample[1].shape, tf.reduce_min(sample[0]), tf.reduce_max(sample[0]))



def main():

    # 输入[b, 32, 32, 3]
    model = resnet18()
    # build()
    model.build(input_shape=(None, 32, 32, 3))
    model.summary() # 查看网络结构和参数量
    # 优化器:Adam
    optimizer = optimizers.Adam(lr=1e-3)

    # 训练流程:
    for epoch in range(50):

        for step, (x, y) in enumerate(train_db):

            # 从数据进入网络,到计算损失函数,都要放到with tf.GradientTape() as tape: 中,方便计算梯度
            with tf.GradientTape() as tape:

                # [b, 32, 32, 3] => [b, 100]
                logits = model(x)
                # 对y做one_hot处理 [b] => [b, 100]
                y_one_hot = tf.one_hot(y, depth=100)
                # 计算loss 结果维度[b]   交叉熵损失函数      one_hot之后的y  输出结果  这里要设置为True
                loss = tf.losses.categorical_crossentropy(y_one_hot, logits, from_logits=True)
                loss = tf.reduce_mean(loss)

            # 计算梯度  variables:所有变量
            grads = tape.gradient(loss, model.trainable_variables)
            # 梯度更新     grads 要和 变量一一对应,所以用zip
            optimizer.apply_gradients(zip(grads, model.trainable_variables))

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


        # 以下是测试部分--
        # 测试流程:
        # 1、数据放入网络得到数据。
        # 2、 softmax得到概率。
        # 3、 argmax 得到最大值的位置。
        # 4、equal 与最大值比较, 得到True False。 cast转化为int格式(0, 1)
        # 5、reduce_sum 计算预测正确的数量
        # 6、计算总的预测正确的数量(每个batch预测正确的相加) 和 总的数据数量(每个batch的总数量)
        # 7、计算准确率 acc
        total_num = 0
        total_correct = 0
        for x, y in test_db:

            # 数据放入网络,得到输出 [b, 100]
            logits = model(x, training=False)

            # softmax 处理 每个数据处理成概率
            prob = tf.nn.softmax(logits, axis=1)
            # argmax 返回指定维度的最大值的位置 int64类型
            pred = tf.argmax(prob, axis=1)
            # 转化数据格式 int64 -> int32
            pred = tf.cast(pred, dtype=tf.int32)

            # tf.cast:把true False转化成 int格式:0或者1   tf.equal: 比较预测值和真实值,结果是True或者False
            correct = tf.cast(tf.equal(pred, y), dtype=tf.int32)
            # 计算所有正确的总数(在上一步,预测正确的已经转化为1)
            correct = tf.reduce_sum(correct)

            # 总数量,是把每一批的数量加起来
            total_num += x.shape[0]
            # 预测正确的总数量,把每次预测正确的总数量加起来
            total_correct += int(correct)

        # 计算准确率
        acc = total_correct / total_num
        print(epoch, 'acc:', acc)



if __name__ == '__main__':
    main()
  • 2
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值