本专栏是记录作者学习TensorFlow深度学习的相关内容,参考教材:《动手学深度学习》第二版

得益于强大的深度学习开源框架,神经网络研究人员已经从考虑单个人工神经元的行为转变为从层的角度构思网络, 通常在设计架构时考虑的是更粗糙的块(block)。

在本节中,我们将深入探索深度学习计算的关键组件, 即模型构建、参数访问与初始化、设计自定义层和块、将模型读写到磁盘, 以及利用GPU实现显著的加速。

本节的 Jupyter 笔记本文件已上传至gitee以供大家学习交流:我的gitee仓库

文章目录

1 层和块1.1 tf.keras.models.Sequential1.2 自定义块1.3 随意混合搭配各种组合块!1.4 自定义层

2 参数访问与初始化2.1 参数访问2.2 参数初始化 keras.initializers2.2.1 框架初始化的机制:延后初始化2.2.2自定义参数初始化

3 模型参数的存取3.1 保存参数save_weights3.2 获取参数load_weights

4 GPU加速

1 层和块

前几个章节我们介绍的模型都由“层”构成,例如:隐藏层,输出层。事实上,多个层可以构成神经网络块,使用块就可以以块为单位组合成更大的组件。在计算机视觉中广泛流行的ResNet-152架构就有数百层。这些架构由多个层以各种重复模式排列构成。

如下图所示,多个层构成了块,快的组合构成了最终的网络。

1.1 tf.keras.models.Sequential

tf.keras.models.Sequential是 TensorFlow 中用于构建顺序(Sequential)模型的类。它继承自 tf.keras.Model类。

输入:可以通过向Sequential类构造函数传递一个层的列表来创建模型。每个元素代表一个神经网络层,按照列表中的顺序依次添加到模型中。

__call__函数:上段代码,net是Sequential的一个实例,net(X)会调用Sequential实例的__call__函数,通过前向传播计算输出(y_hat)。

我们可以通过实例化keras.models.Sequential类,来构建我们的块。在TensorFlow中称作模型,我们简单地构建如下顺序模型。一个模型通常包含层的组合。

import tensorflow as tf

# 创建一个序列模型

net = tf.keras.models.Sequential([

# 添加一个具有256个神经元的全连接层,激活函数为ReLU

tf.keras.layers.Dense(256, activation=tf.nn.relu),

# 添加一个具有10个神经元的全连接层

tf.keras.layers.Dense(10),

])

# 生成一个形状为(2, 20)的随机张量作为输入

X = tf.random.uniform((2, 20))

# 使用模型对输入进行预测

net(X)

结果:

array([[ 0.11223674, 0.0570154 , 0.24488513, 0.19016925, 0.16087194,

-0.41190267, 0.02919962, 0.23194662, -0.1918173 , -0.03502434],

[ 0.19962178, 0.23649202, 0.20880091, 0.22732121, 0.09754458,

-0.13185 , -0.01996534, 0.11025474, -0.21684128, -0.3742783 ]],

dtype=float32)>

1.2 自定义块

由此我们可以简单自定义自己的’模型’类,类比了解Sequential的工作方式。通过执行层的call方法实现了前向传播。

tf.keras.layers.Dense继承自tf.keras.layers.Layer。在 TensorFlow 的 Keras API 中,Layer是构建神经网络的基本组件,而Dense则是一种全连接层,用于实现线性变换(包括权重和偏置)以及激活函数。

__init__定义了必要的初始化,如下初始化时定义了构成该块的layer。

call方法执行了线性变换和激活函数的操作。见下面代码第12行。

class Dense_3(tf.keras.Model):

def __init__(self):

# 调用父类Model的构造函数来执行必要的初始化。

super().__init__()

# 用模型参数声明层。这里,我们声明三个个全连接的层

hidden_1 = tf.keras.layers.Dense(units=256, activation=tf.nn.relu)

hidden_2 = tf.keras.layers.Dense(units=128, activation=tf.nn.relu)

hidden_3 = tf.keras.layers.Dense(units=32, activation=tf.nn.relu)

# 定义块的前向传播,即如何根据输入X返回块输出

def call(self, X):

return self.hidden_3(self.hidden_2(self.hidden_1((X))))#调用Dense对象的call方法求输出

net = Dense_3()

net(X)

结果:

array([[0. , 0.23247738, 0. , 0.03089234, 0. ,

0. , 0.38066423, 0.2292603 , 0. , 0.17844239,

0.12169323, 0.23782215, 0. , 0.05544428, 0. ,

0. , 0.24541184, 0. , 0. , 0. ,

0. , 0. , 0. , 0. , 0. ,

0.05869596, 0.05059667, 0. , 0.05235524, 0.26484865,

0.15438756, 0.21849719],

[0. , 0.11601003, 0.20752 , 0.13710164, 0. ,

0. , 0.30639148, 0. , 0. , 0. ,

0.3036584 , 0.17476179, 0. , 0.22209452, 0. ,

0. , 0.0819096 , 0.07936593, 0. , 0. ,

0. , 0.03377184, 0. , 0. , 0.02346063,

0. , 0.0978866 , 0. , 0.03887625, 0.04847652,

0.19021733, 0.2981867 ]], dtype=float32)>

进一步,我们可以接受tf.keras.layers.Layer对象作为参数,创建顺序列表,并且相应地依顺序调用计算输出。

class MySequential(tf.keras.Model):

def __init__(self, *args):

super().__init__()

self.modules = [] # 用于存储层的列表

for block in args:

# 对于每个参数block,将其添加到self.modules列表中

self.modules.append(block)

def call(self, X):

for module in self.modules: # 遍历self.modules中的每个层

X = module(X) # 使用层对输入X进行操作

return X # 返回经过所有层操作后的结果

net = MySequential(

tf.keras.layers.Dense(units=256, activation=tf.nn.relu),

tf.keras.layers.Dense(10))

net(X)

结果:

array([[-0.06453231, 0.27186775, 0.1817614 , 0.18816313, -0.02284257,

-0.27613 , 0.23772688, -0.35325792, -0.06396832, -0.11730409],

[-0.14121777, 0.26647487, 0.23432402, -0.10953505, 0.2088038 ,

0.0187881 , 0.04929116, -0.2591108 , -0.09224377, 0.05071623]],

dtype=float32)>

1.3 随意混合搭配各种组合块!

现在我们可以自行定义模型进行组合!

net = tf.keras.Sequential()

net.add(Dense_3())

net.add(MySequential(

tf.keras.layers.Dense(units=16, activation=tf.nn.relu),

tf.keras.layers.Dense(2)))

net(X)

结果:

array([[ 0.14945824, -0.23502888],

[-0.00933508, -0.07106275]], dtype=float32)>

1.4 自定义层

以上我们利用框架提供的层进行任意组合,我们也可以定义自己的层。

当你第一次调用模型的call方法时,TensorFlow会根据输入的X动态调用build方法,并将输入数据的形状传递给build方法。因此我们并不需要关心输入的维度。这也是框架的机制,可以看2.2.1节。

class MyDense(tf.keras.Model):

def __init__(self, units):

super().__init__()

self.units = units

def build(self, X_shape):

"""

构建模型,根据输入数据的形状创建权重和偏置变量

参数:

X_shape:输入数据的形状

返回值:

"""

self.weight = self.add_weight(name='weight',

shape=[X_shape[-1], self.units],

initializer=tf.random_normal_initializer())

self.bias = self.add_weight(

name='bias', shape=[self.units],

initializer=tf.zeros_initializer())

def call(self, X):

"""

前向传播函数,计算线性运算和激活函数的输出

参数:

X:输入数据

返回值:

线性运算和激活函数的输出

"""

linear = tf.matmul(X, self.weight) + self.bias

return tf.nn.relu(linear)

实例化类,进行前向传播,并查看权重

dense = MyDense(3)

dense(tf.random.uniform((2, 5)))

dense.get_weights()

结果:

[array([[-0.00619106, 0.03606476, -0.06667487],

[ 0.02008205, -0.00195551, 0.03292775],

[-0.02063993, -0.03763751, -0.03919746],

[-0.01398631, 0.03046289, 0.0016272 ],

[ 0.01619437, 0.02225424, -0.06965501]], dtype=float32),

array([0., 0., 0.], dtype=float32)]

定义的层也可以组成块

net = tf.keras.models.Sequential([MyDense(8), MyDense(1)])

net(tf.random.uniform((2, 64)))

结果:

array([[0. ],

[0.00642088]], dtype=float32)>

2 参数访问与初始化

简单定义一个模型,以供参数访问

import tensorflow as tf

##定义两层全连接层模型

net = tf.keras.models.Sequential([

tf.keras.layers.Dense(4, activation=tf.nn.relu),

tf.keras.layers.Dense(2, activation=tf.nn.relu),

tf.keras.layers.Dense(1),

])

X = tf.random.uniform((2, 4))

net(X)

结果:

array([[0.11630532],

[0.25339502]], dtype=float32)>

2.1 参数访问

对于Sequential对象,我们可以通过索引访问模型的任意层,并且可以访问其属性。如下我们可以看最后一层的参数。

第一个权重张量net.layers[-1].weights[0]是连接输入和隐藏层的权重矩阵,而第二个权重张量net.layers[-1].weights[1]是隐藏层的偏置向量。

获取所有参数

net.layers[-1].weights

结果:

[

array([[ 0.93102634],

[-0.36053765]], dtype=float32)>,

]

获取权重矩阵

net.layers[-1].weights[0]

结果:

array([[ 0.93102634],

[-0.36053765]], dtype=float32)>

获取所有的参数

net.get_weights()

结果:

[array([[ 0.7828732 , -0.7399799 , -0.835389 , -0.1791215 ],

[ 0.14022547, 0.7993271 , 0.26908928, 0.1886558 ],

[ 0.7538975 , 0.15449113, 0.43292075, -0.46523562],

[-0.25687873, 0.49252445, 0.5199073 , -0.23493934]],

dtype=float32),

array([0., 0., 0., 0.], dtype=float32),

array([[-0.02003312, -0.84314585],

[ 0.44456553, -0.5659945 ],

[ 0.7054217 , -0.30972958],

[-0.28872013, 0.39585948]], dtype=float32),

array([0., 0.], dtype=float32),

array([[ 0.93102634],

[-0.36053765]], dtype=float32),

array([0.], dtype=float32)]

同样我们使用下面的方式也可以访问到特定的参数

net.get_weights()[0]

结果:

array([[ 0.7828732 , -0.7399799 , -0.835389 , -0.1791215 ],

[ 0.14022547, 0.7993271 , 0.26908928, 0.1886558 ],

[ 0.7538975 , 0.15449113, 0.43292075, -0.46523562],

[-0.25687873, 0.49252445, 0.5199073 , -0.23493934]],

dtype=float32)

同样我们可以嵌套多层,该网络嵌套了3层顺序模型。

def block1(name):

return tf.keras.Sequential([

tf.keras.layers.Flatten(),

tf.keras.layers.Dense(4, activation=tf.nn.relu)],

name=name)

def block2():

net = tf.keras.Sequential()

for i in range(4):

# 在这里嵌套

net.add(block1(name=f'block-{i}'))

return net

rgnet = tf.keras.Sequential()

rgnet.add(block2())

rgnet.add(tf.keras.layers.Dense(1))

rgnet(X)

结果:

array([[-0.02181714],

[-0.00484836]], dtype=float32)>

该网络的第一层是一个顺序模型,输出的形状为(2,4),一共85个参数

第二层是一个全连接输出层,输出的形状为(2,1),一共5个参数

print(rgnet.summary())

Model: "sequential_4"

_________________________________________________________________

Layer (type) Output Shape Param #

=================================================================

sequential_5 (Sequential) (2, 4) 80

dense_16 (Dense) (2, 1) 5

=================================================================

Total params: 85

Trainable params: 85

Non-trainable params: 0

_________________________________________________________________

None

同样我们可以通过所以查询对应参数

rgnet.layers[0].layers[1].layers[1].weights[1]

结果:

2.2 参数初始化 keras.initializers

默认情况下,Keras会根据一个范围均匀地初始化权重矩阵, 这个范围是根据输入和输出维度计算出的。偏置参数设置为0。

TensorFlow在根模块和keras.initializers模块中提供了各种初始化方法。

net = tf.keras.models.Sequential([

tf.keras.layers.Dense(

8, activation=tf.nn.relu, # 第一个全连接层,8个神经元,使用ReLU激活函数

# 使用Xavier初始化方法初始化

kernel_initializer=tf.keras.initializers.GlorotUniform(),

bias_initializer=tf.zeros_initializer()), # 偏置初始化为0

tf.keras.layers.Dense(

4, activation=tf.nn.relu, # 第二个全连接层,4个神经元,使用ReLU激活函数

# 权重初始化为均值为1,标准差为0.01的正态分布

kernel_initializer=tf.random_normal_initializer(mean=1, stddev=0.01),

bias_initializer=tf.zeros_initializer()), # 偏置初始化为0

tf.keras.layers.Dense(

2, activation=tf.nn.relu, # 第三个全连接层,2个神经元,使用ReLU激活函数

# 权重初始化为常数1

kernel_initializer=tf.keras.initializers.Constant(1),

bias_initializer=tf.zeros_initializer()), # 偏置初始化为0

tf.keras.layers.Dense(1) # 输出层,1个神经元

])

net(X) # 使用构建的神经网络对X进行预测

结果:

array([[15.272718],

[19.372719]], dtype=float32)>

net.layers[0].weights #第一层权重矩阵

结果:

[

array([[ 0.1894015 , 0.5312361 , 0.6059064 , -0.6156314 , 0.5940756 ,

-0.04593736, 0.12908906, -0.41465536],

[ 0.28501892, -0.09111625, 0.10588145, -0.687394 , -0.6627563 ,

-0.4076561 , 0.41302592, -0.15980196],

[-0.5251165 , -0.06503427, 0.6931526 , 0.679614 , 0.1149466 ,

0.44401962, -0.15350068, -0.47491068],

[ 0.36718386, 0.5753067 , -0.15203142, 0.6642664 , -0.6725273 ,

0.15356624, 0.54792875, 0.01905608]], dtype=float32)>,

]

net.layers[1].weights #第二层权重矩阵

结果:

[

array([[1.0002614 , 1.0153509 , 0.99366397, 0.99379915],

[1.0195246 , 1.0079677 , 0.98768413, 0.9858873 ],

[0.9831471 , 0.99515057, 1.0097063 , 1.0003647 ],

[1.007163 , 0.9951799 , 0.9895254 , 1.0052459 ],

[0.989326 , 1.0118208 , 0.99288726, 0.99346656],

[1.0004822 , 1.0110354 , 0.99120724, 0.98923993],

[0.9917146 , 0.9914353 , 0.996987 , 0.99826473],

[1.0169553 , 0.99790084, 0.98912305, 0.99505866]], dtype=float32)>,

]

net.layers[2].weights #第三层权重矩阵

结果:

[

array([[1., 1.],

[1., 1.],

[1., 1.],

[1., 1.]], dtype=float32)>,

]

2.2.1 框架初始化的机制:延后初始化

我们使用框架设计网络架构时,并没有指定输入维度,也没有指定网络层的输出维度。也没有规定每一层参数的维度。那框架是如何初始化参数的呢?

框架会采用延后初始化(defers initialization)的机制,数据第一次通过模型传递后,框架才会动态地推断出每个层的大小。

我们通过下列实验进行验证,经过前向传播之前,框架未对框架进行初始化。

## 定义一个简单的网络

net = tf.keras.models.Sequential([

tf.keras.layers.Dense(4, activation=tf.nn.relu),

tf.keras.layers.Dense(2),

])

net.layers[0].get_weights(),net.layers[1].get_weights()

结果:

([], [])

经过模型前向传播时,模型的权重才得到了初始化。

X = tf.random.uniform((2, 5))

net(X)

net.layers[0].get_weights(),net.layers[1].get_weights()

结果:

([array([[-0.25728935, 0.2756946 , 0.6772716 , -0.18902028],

[-0.6141169 , 0.14123023, -0.55098295, 0.1385709 ],

[ 0.01887792, 0.09123522, 0.6648692 , -0.5260417 ],

[ 0.47965014, -0.756184 , 0.6921437 , -0.6299395 ],

[-0.4874957 , -0.6404319 , 0.19716501, -0.3970687 ]],

dtype=float32),

array([0., 0., 0., 0.], dtype=float32)],

[array([[ 0.30519867, 0.04916096],

[-0.6803074 , 0.2633133 ],

[-0.69551635, -0.5037539 ],

[-0.76904774, 0.5488248 ]], dtype=float32),

array([0., 0.], dtype=float32)])

2.2.2自定义参数初始化

自定义初始化需要定义一个Initializer的子类,并定义__call__函数,实现初始化。 该函数返回给定形状和数据类型的所需张量。

初始化方式如下:

w

{

U

(

5

,

10

)

 可能性 

1

4

0

 可能性 

1

2

U

(

10

,

5

)

 可能性 

1

4

\begin{aligned} w \sim \begin{cases} U(5, 10) & \text{ 可能性 } \frac{1}{4} \\ 0 & \text{ 可能性 } \frac{1}{2} \\ U(-10, -5) & \text{ 可能性 } \frac{1}{4} \end{cases} \end{aligned}

w∼⎩

⎧​U(5,10)0U(−10,−5)​ 可能性 41​ 可能性 21​ 可能性 41​​​

class MyInit(tf.keras.initializers.Initializer):

def __call__(self, shape, dtype=None):

# 生成一个范在-10到10之间的随机数

data=tf.random.uniform(shape, -10, 10, dtype=dtype)

# factor为系数,绝对值大于等于5的值的系数为1,其余为0

factor=(tf.abs(data) >= 5)

# 将布尔值转换为浮点数

factor=tf.cast(factor, tf.float32)

# 对数据乘以系数

return data * factor

# 创建一个Sequential模型

net = tf.keras.models.Sequential([

tf.keras.layers.Flatten(), # 展平层

tf.keras.layers.Dense(

4,

activation=tf.nn.relu,

kernel_initializer=MyInit()), # 具有自定义初始化器的全连接层

tf.keras.layers.Dense(1), # 全连接层

])

# 使用模型对数据进行预测

net(X)

# 打印第二层模型的权重

print(net.layers[1].weights[0])

结果:

array([[-0. , -0. , 0. , 0. ],

[-9.531331 , -0. , 9.090769 , -0. ],

[ 0. , 0. , 0. , -7.2820926],

[-0. , -0. , -0. , 0. ],

[-0. , -5.1779747, 0. , 0. ]], dtype=float32)>

3 模型参数的存取

深度学习框架提供了内置函数来保存和加载整个网络,这将保存模型的参数而不是保存整个模型。本节用到的代码是多层感知机章最后一节的代码。

【TensorFlow深度学习】八、多层感知机(隐藏层、ReLU)

import tensorflow as tf

from d2l import tensorflow as d2l

net = tf.keras.models.Sequential([

tf.keras.layers.Flatten(),#展开层

tf.keras.layers.Dense(256, activation='relu'),#隐藏层(256个单元)

tf.keras.layers.Dense(10)])#输出层(10个类别)

batch_size, lr, num_epochs = 256, 0.1, 10 #配置超参数

loss = tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True) #损失函数定义为交叉熵

trainer = tf.keras.optimizers.SGD(learning_rate=lr) #优化方法选择SGD

train_iter, test_iter = d2l.load_data_fashion_mnist(batch_size)

d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer) #训练

模型最终准确率

d2l.evaluate_accuracy(net, test_iter)

结果:

0.8566

3.1 保存参数save_weights

我们将该模型的参数保存到文件mlp.params中

net.save_weights('mlp.params')

在同级目录下生成了文件,其中mlp.params.index文件包含了模型的索引信息,它指示权重存储在哪个.data文件中。

mlp.params.data-00000-of-000001文件包含了模型的具体权重数值。

3.2 获取参数load_weights

我们新建一个模型实例,net_new并测试其准确率。

net_new = tf.keras.models.Sequential([

tf.keras.layers.Flatten(),#展开层

tf.keras.layers.Dense(256, activation='relu'),#隐藏层(256个单元)

tf.keras.layers.Dense(10)])#输出层(10个类别)

d2l.evaluate_accuracy(net_new, test_iter)

结果:

0.1235

现在我们为模型加载已存储的参数

net_new.load_weights('mlp.params')

结果:

再次测试模型在测试集上的准确率

d2l.evaluate_accuracy(net_new, test_iter)

结果:

0.8566

4 GPU加速

TensorFlow_gpu版本的配置详见【TensorFlow深度学习】一、TensorFlow_gpu 2.10.0安装(Win11、Anaconda3、Python3.9、Pycharm配置、Jupyter及免费公网部署方案)

验证tf版本,gpu是否可用

import tensorflow as tf

print("TensorFlow version is :",tf.__version__)

print("Keras version is :",tf.keras.__version__)

print("GPU is","available" if tf.config.list_physical_devices('GPU')else "NOT AVAILABLE")

TensorFlow version is : 2.10.0

Keras version is : 2.10.0

GPU is available

该环境下,我们定义的模型和张量均运行在GPU上

x = tf.constant([1, 2, 3])

x.device

结果:

'/job:localhost/replica:0/task:0/device:GPU:0'

定义一个网络

net = tf.keras.models.Sequential([

tf.keras.layers.Dense(1)])

X = tf.random.uniform((2, 5))

net(X)

结果:

array([[-0.1214669],

[ 0.9222388]], dtype=float32)>

查看网络所在设备名字

net.layers[0].weights[0].device, net.layers[0].weights[1].device

结果:

('/job:localhost/replica:0/task:0/device:GPU:0',

'/job:localhost/replica:0/task:0/device:GPU:0')

参考文章

评论可见,请评论后查看内容,谢谢!!!
 您阅读本篇文章共花了: