1.AlexNet
2012年,AlexNet横空出世,该模型的名字源于论文第一作者的姓名Alex Krizhevsky 。AlexNet使用了8层卷积神经网络,以很大的优势赢得了ImageNet 2012图像识别挑战赛。它首次证明了学习到的特征可以超越手工设计的特征,从而一举打破计算机视觉研究的方向。

该网络的特点是:
- AlexNet包含8层变换,有5层卷积和2层全连接隐藏层,以及1个全连接输出层
- AlexNet第一层中的卷积核形状是1111 。第二层中的卷积核形状减小到55 ,之后全采用3*3 。所有的池化层窗口大小为 、步幅为2的最大池化。
- AlexNet将sigmoid激活函数改成了ReLU激活函数,使计算更简单,网络更容易训练
- AlexNet通过dropOut来控制全连接层的模型复杂度。
- AlexNet引入了大量的图像增强,如翻转、裁剪和颜色变化,从而进一步扩大数据集来缓解过拟合。
在tf.keras中实现AlexNet模型:
python
# 构建AlexNet模型
net = tf.keras.models.Sequential([
# 卷积层:96个卷积核,卷积核为11*11,步幅为4,激活函数relu
tf.keras.layers.Conv2D(filters=96,kernel_size=11,strides=4,activation='relu'),
# 池化:窗口大小为3*3、步幅为2
tf.keras.layers.MaxPool2D(pool_size=3, strides=2),
# 卷积层:256个卷积核,卷积核为5*5,步幅为1,padding为same,激活函数relu
tf.keras.layers.Conv2D(filters=256,kernel_size=5,padding='same',activation='relu'
# 池化:窗口大小为3*3、步幅为2
tf.keras.layers.MaxPool2D(pool_size=3, strides=2),
# 卷积层:384个卷积核,卷积核为3*3,步幅为1,padding为same,激活函数relu
tf.keras.layers.Conv2D(filters=384,kernel_size=3,padding='same',activation='relu'
# 卷积层:384个卷积核,卷积核为3*3,步幅为1,padding为same,激活函数relu
tf.keras.layers.Conv2D(filters=384,kernel_size=3,padding='same',activation='relu'
# 卷积层:256个卷积核,卷积核为3*3,步幅为1,padding为same,激活函数relu
tf.keras.layers.Conv2D(filters=256,kernel_size=3,padding='same',activation='relu'
# 池化:窗口大小为3*3、步幅为2
tf.keras.layers.MaxPool2D(pool_size=3, strides=2),
# 伸展为1维向量
tf.keras.layers.Flatten(),
# 全连接层:4096个神经元,激活函数relu
tf.keras.layers.Dense(4096,activation='relu'),
# 随机失活
tf.keras.layers.Dropout(0.5),
# 全链接层:4096个神经元,激活函数relu
tf.keras.layers.Dense(4096,activation='relu'),
# 随机失活
tf.keras.layers.Dropout(0.5),
# 输出层:10个神经元,激活函数softmax
tf.keras.layers.Dense(10,activation='softmax')
])
python
# 我们构造一个高和宽均为227的单通道数据样本来看一下模型的架构:
# 构造输入X,并将其送入到net网络中
X = tf.random.uniform((1,227,227,1)
y = net(X)
# 通过net.summay()查看网络的形状
net.summay()

代码实现:
python
# 获取手写数字数据集
(train_images, train_labels), (test_images, test_labels) = mnist.load_data()
# 训练集数据维度的调整:N H W C
train_images = np.reshape(train_images,(train_images.shape[0],train_images.shape[1],train_images.shape[2],1))
# 测试集数据维度的调整:N H W C
test_images = np.reshape(test_images,(test_images.shape[0],test_images.shape[1],test_images.shape[2],1))
# 由于使用全部数据训练时间较长,我们定义两个方法获取部分数据,并将图像调整为227*227大小,进行模型训练:
# 定义两个方法随机抽取部分样本演示
# 获取训练集数据
def get_train(size):
# 随机生成要抽样的样本的索引
index = np.random.randint(0, np.shape(train_images)[0], size)
# 将这些数据resize成227*227大小
resized_images = tf.image.resize_with_pad(train_images[index],227,227,)
# 返回抽取的
return resized_images.numpy(), train_labels[index]
# 获取测试集数据
def get_test(size):
# 随机生成要抽样的样本的索引
index = np.random.randint(0, np.shape(test_images)[0], size)
# 将这些数据resize成227*227大小
resized_images = tf.image.resize_with_pad(test_images[index],227,227,)
# 返回抽样的测试样本
return resized_images.numpy(), test_labels[index]
# 获取训练样本和测试样本
train_images,train_labels = get_train(256)
test_images,test_labels = get_test(128)
# 数据展示:将数据集的前九个数据集进行展示
for i in range(9):
plt.subplot(3,3,i+1)
# 以灰度图显示,不进行插值
plt.imshow(train_images[i].astype(np.int8).squeeze(), cmap='gray',interpolation='none')
# 设置图片的标题:对应的类别
plt.title("数字{}".format(train_labels[i]))
# 模型编译
# 指定优化器,损失函数和评价指标
optimizer = tf.keras.optimizers.SGD(learning_rate=0.01, momentum=0.0,nesterov=False)
net.compile(optimizer=optimizer,loss='sparse_categorical_crossentropy',metrics=['accuracy'])
# 模型训练:指定训练数据,batchsize,epoch,验证集
net.fit(train_images,train_labels,batch_size=128,epochs=3,verbose=1,validation_sp
# 指定测试数据
net.evaluate(test_images,test_labels,verbose=1)
# 这边只使用了部分图片进行训练,测试集正确率不到10%,如果使用整个数据训练,结果会好很多
2.VGGNet
2014年,牛津大学计算机视觉组(Visual Geometry Group)和Google DeepMind公司的研究员一起研发出了新的深度卷积神经网络:VGGNet,并取得了ILSVRC2014比赛分类项目的第二名,主要贡献是使用很小的卷积核(3×3)构建卷积神经网络结构,能够取得较好的识别精度,常用来提取图像特征的VGG-16和VGG-19。
VGG可以看成是加深版的AlexNet,整个网络由卷积层和全连接层叠加而成,和AlexNet不同的是,VGG中使用的都是小尺寸的卷积核(3×3),其网络架构如下图所示:

VGGNet使用的全部都是3x3的小卷积核和2x2的池化核,通过不断加深网络来提升性能。VGG可以通过重复使用简单的基础块来构建深度模型。

核心代码:
python
# 定义VGG网络中的卷积块:卷积层的个数,卷积层中卷积核的个数
def vgg_block(num_convs, num_filters):
# 构建序列模型
blk = tf.keras.models.Sequential()
# 遍历所有的卷积层
for _ in range(num_convs):
# 每个卷积层:num_filter个卷积核,卷积核大小为3*3,padding是same,激活函数是
relu
blk.add(tf.keras.layers.Conv2D(num_filters,kernel_size=3,
VGG16网络有5个卷积块,前2块使用两个卷积层,而后3块使用三个卷积层。第一块的输出通道
是64,之后每次对输出通道数翻倍,直到变为512。
因为这个网络使用了13个卷积层和3个全连接层,所以经常被称为VGG-16,通过制定conv_arch得
到模型架构后构建VGG16:
我们构造一个高和宽均为224的单通道数据样本来看一下模型的架构:
网络架构如下:
padding='same',activation='relu'))
# 卷积块最后是一个最大池化,窗口大小为2*2,步长为2
blk.add(tf.keras.layers.MaxPool2D(pool_size=2, strides=2))
return blk
# 定义5个卷积块,指明每个卷积块中的卷积层个数及相应的卷积核个数
conv_arch = ((2, 64), (2, 128), (3, 256), (3, 512), (3, 512))
# 定义VGG网络
def vgg(conv_arch):
# 构建序列模型
net = tf.keras.models.Sequential()
# 根据conv_arch生成卷积部分
for (num_convs, num_filters) in conv_arch:
net.add(vgg_block(num_convs, num_filters))
# 卷积块序列后添加全连接层
net.add(tf.keras.models.Sequential([
# 将特征图展成一维向量
tf.keras.layers.Flatten(),
# 全连接层:4096个神经元,激活函数是relu
tf.keras.layers.Dense(4096, activation='relu'),
# 随机失活
tf.keras.layers.Dropout(0.5),
# 全连接层:4096个神经元,激活函数是relu
tf.keras.layers.Dense(4096, activation='relu'),
# 随机失活
tf.keras.layers.Dropout(0.5),
# 全连接层:10个神经元,激活函数是softmax
tf.keras.layers.Dense(10, activation='softmax')]))
return net
# 网络实例化
net = vgg(conv_arch)
# 定义VGG网络
def vgg(conv_arch):
# 构建序列模型
net = tf.keras.models.Sequential()
# 根据conv_arch生成卷积部分
for (num_convs, num_filters) in conv_arch:
net.add(vgg_block(num_convs, num_filters))
# 卷积块序列后添加全连接层
net.add(tf.keras.models.Sequential([
# 将特征图展成一维向量
tf.keras.layers.Flatten(),
# 全连接层:4096个神经元,激活函数是relu
tf.keras.layers.Dense(4096, activation='relu'),
# 随机失活
tf.keras.layers.Dropout(0.5),
# 全连接层:4096个神经元,激活函数是relu
tf.keras.layers.Dense(4096, activation='relu'),
# 随机失活
tf.keras.layers.Dropout(0.5),
# 全连接层:10个神经元,激活函数是softmax
tf.keras.layers.Dense(10, activation='softmax')]))
return net
# 网络实例化
net = vgg(conv_arch)
# 构造输入X,并将其送入到net网络中
X = tf.random.uniform((1,224,224,1))
y = net(X)
# 通过net.summay()查看网络的形状
net.summay()
3.GoogleNet

GoogLeNet的名字不是GoogleNet,而是GoogLeNet,这是为了致敬LeNet。GoogLeNet和AlexNet/VGGNet这类依靠加深网络结构的深度的思想不完全一样。GoogLeNet在加深度的同时做了结构上的创新,引入了一个叫做Inception的结构来代替之前的卷积加激活的经典组件。
GoogLeNet在ImageNet分类比赛上的Top-5错误率降低到了6.7%。。
3.1 Inception块
GoogLeNet中的基础卷积块叫作Inception块,得名于同名电影《盗梦空间》(Inception)。
Inception块在结构比较复杂,如下图所示:

Inception块里有4条并行的线路。前3条线路使用窗口大小分别是1 × 1、3 × 3和5 × 5的卷积层来抽取不同空间尺寸下的信息,其中中间2个线路会对输入先做1 × 1卷积来减少输入通道数,以降低模型复杂度。第4条线路则使用3 × 3最大池化层,后接1 × 1卷积层来改变通道数。4条线路都使用了合适的填充来使输入与输出的高和宽一致。最后我们将每条线路的输出在通道维上连结,并向后进行传输。

以inception模块为例,来说明1x1的卷积如何来减少模型参数:

(a)是未加入1x1卷积的inception模块,(b)是加入了1x1 卷积的inception模块。
我们以3x3卷积线路为例,假设输入的特征图大小为(28x28x192),输出特征图的通道数是128:
(a)图中该线路的参数量为:3x3x192x128 = 221184
(b)图中加入1x1卷积后通道为96,再送入3x3卷积中的参数量为:(1x1x192x96)+(3x3x96x128)=129024.
对比可知,加入1x1卷积后参数量减少了。
在tf.keras中实现Inception模块:
python
# 定义Inception模块
class Inception(tf.keras.layers.Layer):
# 输入参数为各个卷积的卷积核个数
def __init__(self, c1, c2, c3, c4):
super().__init__()
# 线路1:1 x 1卷积层,激活函数是RELU,padding是same
self.p1_1 = tf.keras.layers.Conv2D(
c1, kernel_size=1, activation='relu', padding='same')
# 线路2,1 x 1卷积层后接3 x 3卷积层,激活函数是RELU,padding是same
self.p2_1 = tf.keras.layers.Conv2D(
c2[0], kernel_size=1, padding='same', activation='relu')
self.p2_2 = tf.keras.layers.Conv2D(c2[1], kernel_size=3,
padding='same',
activation='relu')
# 线路3,1 x 1卷积层后接5 x 5卷积层,激活函数是RELU,padding是same
self.p3_1 = tf.keras.layers.Conv2D(
c3[0], kernel_size=1, padding='same', activation='relu')
self.p3_2 = tf.keras.layers.Conv2D(c3[1], kernel_size=5,
padding='same',
activation='relu')
# 线路4,3 x 3最大池化层后接1 x 1卷积层,激活函数是RELU,padding是same
self.p4_1 = tf.keras.layers.MaxPool2D(
pool_size=3, padding='same', strides=1)
self.p4_2 = tf.keras.layers.Conv2D(
c4, kernel_size=1, padding='same', activation='relu')
# 完成前向传播过程
def call(self, x):
# 线路1
p1 = self.p1_1(x)
# 线路2
p2 = self.p2_2(self.p2_1(x))
# 线路3
p3 = self.p3_2(self.p3_1(x))
# 线路4
p4 = self.p4_2(self.p4_1(x))
# 在通道维上concat输出
outputs = tf.concat([p1, p2, p3, p4], axis=-1)
return outputs
Inception(64, (96, 128), (16, 32), 32)
3.2 B1~B5模块

3.2.1 B1模块
第一模块使用一个64通道的7 × 7卷积层。
python
# 定义模型的输入
inputs = tf.keras.Input(shape=(224,224,3),name = "input")
# b1 模块
# 卷积层7*7的卷积核,步长为2,pad是same,激活函数RELU
x = tf.keras.layers.Conv2D(64, kernel_size=7, strides=2, padding='same',
activation='relu')(inputs)
# 最大池化:窗口大小为3*3,步长为2,pad是same
x = tf.keras.layers.MaxPool2D(pool_size=3, strides=2, padding='same')(x)
3.2.2 B2模块
第二模块使用2个卷积层:首先是64通道的1 × 1卷积层,然后是将通道增大3倍的3 × 3卷积层。
python
# b2 模块
# 卷积层1*1的卷积核,步长为2,pad是same,激活函数RELU
x = tf.keras.layers.Conv2D(64, kernel_size=1, padding='same',
activation='relu')(x)
# 卷积层3*3的卷积核,步长为2,pad是same,激活函数RELU
x = tf.keras.layers.Conv2D(192, kernel_size=3, padding='same',
activation='relu')(x)
# 最大池化:窗口大小为3*3,步长为2,pad是same
x = tf.keras.layers.MaxPool2D(pool_size=3, strides=2, padding='same')(x)
3.2.3 B3模块
第三模块串联2个完整的Inception块。
第一个Inception块的输出通道数为 64+ 128+32+32 = 256.
第二个Inception块输出通道数增至 128 + 192 + 96 + 64 = 480.
python
# b3 模块
# Inception
x = Inception(64, (96, 128), (16, 32), 32)(x)
# Inception
x = Inception(128, (128, 192), (32, 96), 64)(x)
# 最大池化:窗口大小为3*3,步长为2,pad是same
x = tf.keras.layers.MaxPool2D(pool_size=3, strides=2, padding='same')(x)
3.2.4 B4模块
第四模块更加复杂。它串联了5个Inception块,其输出通道数分别是
192 + 208 + 48 + 64 = 512 ,160 + 224 + 64 + 64 = 512,
128 + 256 + 64 + 64 = 512, 112 + 288 + 64 + 64 = 528 和 256 + 320 + 128 + 128 = 832。
并且增加了辅助分类器,根据实验发现网络的中间层具有很强的识别能力,为了利用中间层抽象的特征,在某些中间层中添加含有多层的分类器,如下图所示:

python
def aux_classifier(x, filter_size):
#x:输入数据,filter_size:卷积层卷积核个数,全连接层神经元个数
# 池化层
x = tf.keras.layers.AveragePooling2D(
pool_size=5, strides=3, padding='same')(x)
# 1x1 卷积层
x = tf.keras.layers.Conv2D(filters=filter_size[0], kernel_size=1,
strides=1,
padding='valid', activation='relu')(x)
# 展平
x = tf.keras.layers.Flatten()(x)
# 全连接层1
x = tf.keras.layers.Dense(units=filter_size[1], activation='relu')(x)
# softmax输出层
x = tf.keras.layers.Dense(units=10, activation='softmax')(x)
return x
# b4 模块
# Inception
x = Inception(192, (96, 208), (16, 48), 64)(x)
# 辅助输出1
aux_output_1 = aux_classifier(x, [128, 1024])
# Inception
x = Inception(160, (112, 224), (24, 64), 64)(x)
# Inception
x = Inception(128, (128, 256), (24, 64), 64)(x)
# Inception
x = Inception(112, (144, 288), (32, 64), 64)(x)
# 辅助输出2
aux_output_2 = aux_classifier(x, [128, 1024])
# Inception
x = Inception(256, (160, 320), (32, 128), 128)(x)
# 最大池化
x = tf.keras.layers.MaxPool2D(pool_size=3, strides=2, padding='same')(x)
3.2.5 B5模块
第五模块有输出通道数为 256 + 320 + 128 + 128 = 832 和 384 + 384 + 128 + 128 = 1024 的两个Inception块。后面紧跟输出层,该模块使用全局平均池化层(GAP)来将每个通道的高和宽变成1。最后输出变成二维数组后接输出个数为标签类别数的全连接层。
全局平均池化层(GAP)
用来替代全连接层前的Flatten,将特征图每一通道中所有像素值相加后求平均,得到就是GAP的结果,在将其送入后续网络中进行计算

python
# b5 模块
# Inception
x = Inception(256, (160, 320), (32, 128), 128)(x)
# Inception
x = Inception(384, (192, 384), (48, 128), 128)(x)
# GAP
x = tf.keras.layers.GlobalAvgPool2D()(x)
# 输出层
main_outputs = tf.keras.layers.Dense(10,activation='softmax')(x)
最终使用model来创建模型:
python
# 使用Model来创建模型,指明输入和输出
model = tf.keras.Model(inputs=inputs, outputs=[main_outputs,aux_output_1,
aux_output_2])
model.summary()
4.ResNet
⽹络越深,获取的信息就越多,特征也越丰富。但是在实践中,随着⽹络的加深,优化效果反⽽越差,测试数据和训练数据的准确率反⽽降低了。

针对这⼀问题,何恺明等⼈提出了残差⽹络(ResNet)在2015年的ImageNet图像识别挑战赛夺魁,并深刻影响了后来的深度神经⽹络的设计。
4.1 残差块
假设 F(x) 代表某个只包含有两层的映射函数, x 是输⼊, F(x)是输出。假设他们具有相同的维度。在训练的过程中我们希望能够通过修改⽹络中的 w和b去拟合⼀个理想的 H(x)(从输⼊到输出的⼀个理想的映射函数)。也就是我们的⽬标是修改F(x) 中的 w和b逼近 H(x) 。如果我们改变思路,⽤F(x) 来逼近 H(x)-x ,那么我们最终得到的输出就变为 F(x)+x(这⾥的加指的是对应位置上的元素相加,也就是element-wise addition),这⾥将直接从输⼊连接到输出的结构也称为shortcut,那整个结构就是残差块,ResNet的基础模块。

ResNet沿⽤了VGG全3 × 3卷积层的设计。残差块⾥⾸先有2个有相同输出通道数的3 × 3卷积层。每个卷积层后接BN层和ReLU激活函数,然后将输⼊直接加在最后的ReLU激活函数前,这种结构⽤于层数较少的神经⽹络中,⽐如ResNet34。若输⼊通道数⽐较多,就需要引⼊1 × 1卷积层来调整输⼊的通道数,这种结构也叫作瓶颈模块,通常⽤于⽹络层数较多的结构中。如下图所⽰:

上图左中的残差块的实现如下,可以设定输出通道数,是否使⽤1*1的卷积及卷积层的步幅。

python
# 导入相关的工具包
import tensorflow as tf
from tensorflow.keras import layers, activations
# 定义ResNet的残差块
class Residual(tf.keras.Model):
# 指明残差块的通道数,是否使用1*1卷积,步长
def __init__(self, num_channels, use_1x1conv=False, strides=1):
super(Residual, self).__init__()
# 卷积层:指明卷积核个数,padding,卷积核大小,步长
self.conv1 = layers.Conv2D(num_channels,padding='same', kernel_size=3, strides=strides)
# 卷积层:指明卷积核个数,padding,卷积核大小,步长
self.conv2 = layers.Conv2D(num_channels, kernel_size=3, padding='same')
if use_1x1conv:
self.conv3 = layers.Conv2D(num_channels, kernel_size=1, strides=strides)
else:
self.conv3 = None
# 指明BN层
self.bn1 = layers.BatchNormalization()
self.bn2 = layers.BatchNormalization()
# 定义前向传播过程
def call(self, X):
# 卷积,BN,激活
Y = activations.relu(self.bn1(self.conv1(X)))
# 卷积,BN
Y = self.bn2(self.conv2(Y))
# 对输入数据进行1*1卷积保证通道数相同
if self.conv3:
X = self.conv3(X)
# 返回与输入相加后激活的结果
return activations.relu(Y + X)
1*1卷积⽤来调整通道数。
4.2 ResNet模型

ResNet⽹络中按照残差块的通道数分为不同的模块。第⼀个模块前使⽤了步幅为2的最⼤池化层,所以⽆须减⼩⾼和宽。之后的每个模块在第⼀个残差块⾥将上⼀个模块的通道数翻倍,并将⾼和宽减半。
下⾯我们来实现这些模块。注意,这⾥对第⼀个模块做了特别处理。
python
# ResNet网络中模块的构成
class ResnetBlock(tf.keras.layers.Layer):
# 网络层的定义:输出通道数(卷积核个数),模块中包含的残差块个数,是否为第一个模块
def __init__(self,num_channels, num_residuals, first_block=False):
super(ResnetBlock, self).__init__()
# 模块中的网络层
self.listLayers=[]
# 遍历模块中所有的层
for i in range(num_residuals):
# 若为第一个残差块并且不是第一个模块,则使用1*1卷积,步长为2(目的是减小特征图,并增大通道数)
if i == 0 and not first_block:
self.listLayers.append(Residual(num_channels,use_1x1conv=True, strides=2))
# 否则不使用1*1卷积,步长为1
else:
self.listLayers.append(Residual(num_channels))
# 定义前向传播过程
def call(self, X):
# 所有层依次向前传播即可
for layer in self.listLayers.layers:
X = layer(X)
return X
ResNet的前两层跟之前介绍的GoogLeNet中的⼀样:在输出通道数为64、步幅为2的 卷积层后接步幅为2的 的最⼤池化层。不同之处ResNet每个卷积层后增加了BN层,接着是所有残差模块,最后,与GoogLeNet⼀样,加⼊全局平均池化层(GAP)后接上全连接层输出。
python
# 构建ResNet网络
class ResNet(tf.keras.Model):
# 初始化:指定每个模块中的残差快的个数
def __init__(self,num_blocks):
super(ResNet, self).__init__()
# 输入层:7*7卷积,步长为2
self.conv=layers.Conv2D(64, kernel_size=7, strides=2, padding='same')
# BN层
self.bn=layers.BatchNormalization()
# 激活层
self.relu=layers.Activation('relu')
# 最大池化层
self.mp=layers.MaxPool2D(pool_size=3, strides=2, padding='same')
# 第一个block,通道数为64
self.resnet_block1=ResnetBlock(64,num_blocks[0], first_block=True)
# 第二个block,通道数为128
self.resnet_block2=ResnetBlock(128,num_blocks[1])
# 第三个block,通道数为256
self.resnet_block3=ResnetBlock(256,num_blocks[2])
# 第四个block,通道数为512
self.resnet_block4=ResnetBlock(512,num_blocks[3])
# 全局平均池化
self.gap=layers.GlobalAvgPool2D()
# 全连接层:分类
self.fc=layers.Dense(units=10,activation=tf.keras.activations.softmax)
# 前向传播过程
def call(self, x):
# 卷积
x=self.conv(x)
# BN
x=self.bn(x)
# 激活
x=self.relu(x)
# 最大池化
x=self.mp(x)
# 残差模块
x=self.resnet_block1(x)
x=self.resnet_block2(x)
x=self.resnet_block3(x)
x=self.resnet_block4(x)
# 全局平均池化
x=self.gap(x)
# 全链接层
x=self.fc(x)
return x
# 模型实例化:指定每个block中的残差块个数
mynet=ResNet([2,2,2,2])
这⾥每个模块⾥有4个卷积层(不计算 1×1卷积层),加上最开始的卷积层和最后的全连接层,共计18层。这个模型被称为ResNet-18。通过配置不同的通道数和模块⾥的残差块数可以得到不同的ResNet模型,例如更深的含152层的ResNet-152。虽然ResNet的主体架构GoogLeNet的类似,但ResNet结构更简单,修改也更⽅便。这些因素都导致了ResNet迅速被⼴泛使⽤。 在训练ResNet之前,我们来观察⼀下输⼊形状在ResNe的架构:
python
X = tf.random.uniform(shape=(1, 224, 224 , 1))
y = mynet(X)
mynet.summary()
