深度学习计算——动手学深度学习5

环境:PyCharm + python3.8

1. 层和块

(block)可以描述

  • 单个层、
  • 由多个层组成的组件
  • 或整个模型本身。

使用块进行抽象的好处:

  • 可将块组合成更大的组件(这一过程通常是递归) 如 图5.1.1所示。
  • 通过定义代码来按需生成任意复杂度的块,可以通过简洁的代码实现复杂的神经网络。
图5.1.1 多个层被组合成块,形成更大的模型

从编程的角度来看,块由(class)表示。

  • 其子类都必须定义一个前向传播函数(将其输入转换为输出),且必须存储任何必需的参数(有些块不需要任何参数)。
  • 最后,为了计算梯度,块必须具有反向传播函数。在定义自己的块时,由于自动微分(在 2.5节 中引入)提供了一些后端实现,我们只需要考虑前向传播函数和必需的参数。

下面代码生成一个网络,其中包含

  1. 具有 256个单元 和 ReLU激活函数 的全连接隐藏层,
  2. 具有 10个隐藏单元 且 不带激活函数 的全连接输出层。
import torch
from torch import nn
from torch.nn import functional as Fnet = nn.Sequential(nn.Linear(20, 256),nn.ReLU(),nn.Linear(256, 10))X = torch.rand(2, 20)
print(f"随机生成的原始输入:\n{X}")
print(f"模型输出:\n{net(X)}")

该例子通过实例化nn.Sequential来构建模型,层的执行顺序是作为参数传递的。

  • nn.Sequential定义了一种特殊的Module,即在PyTorch中表示一个块的类,它维护了一个由Module组成的有序列表。
  • 注意:两个全连接层都是Linear类的实例,Linear类本身就是Module的子类。
  • net(X)实际上是net.__call__(X)的简写 (我们前面一直在通过net(X)调用我们的模型来获得模型的输出)。
    • 这个前向传播函数非常简单:它将列表中的每个块连接在一起,将每个块的输出作为下一个块的输入。

1.1. 自定义块

简要总结每个块必须提供的基本功能:

  1. 输入数据作为前向传播函数的参数
  2. 通过前向传播函数生成输出。(注意:输出的形状可能与输入的形状不同。例如,上面模型中的第一个全连接的层接收一个20维的输入,但是返回一个维度为256的输出)。
  3. 计算输出关于输入的梯度,可通过反向传播函数进行访问(通常是自动发生)。
  4. 存储和访问前向传播计算所需的参数
  5. 根据需要初始化模型参数

下面的代码片段从零开始编写一个块。(包含一个多层感知机,其具有256个隐藏单元的隐藏层和一个10维输出层)。 注意,下面的MLP类继承了表示块的类。我们的实现只需要提供我们自己的构造函数(Python中的__init__函数)和前向传播函数。

class MLP(nn.Module):# 用模型参数声明层,这里声明两个全连接的层def __init__(self):# 调用MLP的父类Module的构造函数来执行必要的初始化# 这样,在类实例化时也可以指定其他函数参数,例如模型参数params(稍后将介绍)super().__init__()self.hidden = nn.Linear(20, 256)  # 隐藏层self.out = nn.Linear(256, 10)  # 输出层# 定义模型的前向传播,即如何根据输入X返回所需的模型输出def forward(self, X):# 注意:这里使用ReLU的函数版本,其在nn.functional模块中定义。return self.out(F.relu(self.hidden(X)))

前向传播函数:

  • X作为输入,
  • 计算带有激活函数的隐藏表示,
  • 并输出其未规范化的输出值。

在这个MLP实现中,两个层都是实例变量。要了解这为什么是合理的,可以想象实例化两个多层感知机(net1net2),并根据不同的数据对它们进行训练。

接着我们实例化多层感知机的层,然后在每次调用前向传播函数时调用这些层。注意一些关键细节

  1. 首先,定制的__init__函数通过super().__init__() 调用父类的__init__函数 (避免了重复编写模版代码)。
  2. 然后,实例化两个全连接层, 分别为self.hiddenself.out
  3. 注意,除非要实现一个新的运算符,否则不必担心反向传播函数或参数初始化,系统将自动生成这些。
net = MLP()
mlp_Out = net(X)
print(f"MLP模型输出:\n{mlp_Out}")

块的一个主要优点是它的多功能性。我们可以子类化块以创建

  • 层(如全连接层的类)、
  • 整个模型(如上面的MLP类)
  • 或具有中等复杂度的各种组件。 

1.2. 顺序块

Sequential的设计是为了把其他模块串起来。

构建自己的简化的MySequential,只需定义两个关键函数

  1. 一种将块逐个追加到列表中的函数;
  2. 一种前向传播函数,用于将输入按追加块的顺序传递给块组成的“链条”。

下面的MySequential类提供了与默认Sequential类相同的功能。

# 直接将神经网络的每个层当作参数传进来
class MySequential(nn.Module):def __init__(self, *args):super().__init__() # 调用父类的构造函数for idx, module in enumerate(args):# 这里,module是Module子类的一个实例。我们把它保存在'Module'类的成员# 变量_modules中。_module的类型是OrderedDictself._modules[str(idx)] = moduledef forward(self, X):# OrderedDict保证了按照成员添加的顺序遍历它们for block in self._modules.values():X = block(X)return X
  • __init__函数将每个模块逐个添加到有序字典_modules中。
  • _modules的主要优点是:在模块的参数初始化过程中,系统知道在_modules字典中查找需要初始化参数的子块。

MySequential的前向传播函数被调用时,每个添加的块都按照它们被添加的顺序执行。下面使用MySequential类重新实现多层感知机。

net = MySequential(nn.Linear(20, 256),nn.ReLU(),nn.Linear(256, 10))
myModule_Out = net(X)
print(f"MySequential模型输出:\n{myModule_Out}")

1.3. 在前向传播函数中执行代码

当架构需要更强的灵活性时,则需要定义自己的块。 

常数参数 (constant parameter):既不是上一层的结果 也不是可更新参数 的项

  • 例如,我们需要一个计算函数 f(\mathbf{x},\mathbf{w}) = c \cdot \mathbf{w}^\top \mathbf{x} 的层,其中
    • x  是 输入, 
    • w 是 参数, 
    • c  是 某个在优化过程中没有更新的指定常量。 

因此下面实现一个FixedHiddenMLP类,如下所示:

class FixedHiddenMLP(nn.Module):def __init__(self):super().__init__()# 不计算梯度的随机权重参数。因此其在训练期间保持不变self.rand_weight = torch.rand((20, 20), requires_grad=False)self.linear = nn.Linear(20, 20)def forward(self, X):X = self.linear(X)# 使用创建的常量参数以及relu和mm函数X = F.relu(torch.mm(X, self.rand_weight) + 1)# 复用全连接层。这相当于两个全连接层共享参数X = self.linear(X)# 控制流while X.abs().sum() > 1:X /= 2return X.sum()

在这个FixedHiddenMLP模型中,

  1. 实现了一个隐藏层,其权重(self.rand_weight) 在实例化时被随机初始化,之后为常量。
  2. 这个权重不是一个模型参数,因此它永远不会被反向传播更新。
  3. 然后,神经网络将这个固定层的输出通过一个全连接层。
  4. 注意:在返回输出之前,模型做了一些不寻常的事情:
    1. 它运行了一个while循环,在 L1范数>1 的条件下,将输出向量除以2,直到它满足条件为止。
    2. 最后,模型返回了X中所有项的和。
    3. 注意:此操作可能不会常用于在任何实际任务中,这里只展示如何将任意代码集成到神经网络计算的流程中。
net = FixedHiddenMLP()
fixMLP_Out = net(X)
print(f"FixedHiddenMLP模型输出:\n{fixMLP_Out}")

 

也可以混合搭配各种组合块的方法。如下例子,我们以一些想到的方法嵌套块:(网络结构如下)

  • NestMLP(),
    • net()
      • nn.Linear(20, 64), nn.ReLU()
      • nn.Linear(64, 32), nn.ReLU()
    • nn.Linear(32, 16)
  • nn.Linear(16, 20),
  • FixedHiddenMLP()
    • nn.Linear(20, 20)
    • x*w+1,  nn.ReLU()
    • nn.Linear(20, 20)
    • x一直减半直到 其值的和>1。
class NestMLP(nn.Module):def __init__(self):super().__init__()self.net = nn.Sequential(nn.Linear(20, 64), nn.ReLU(),nn.Linear(64, 32), nn.ReLU())self.linear = nn.Linear(32, 16)def forward(self, X):return self.linear(self.net(X))chimera = nn.Sequential(NestMLP(), nn.Linear(16, 20), FixedHiddenMLP())
print(f"NestMLP模型输出:\n{chimera(X)}")

1.4. 效率

读者可能会开始担心操作效率的问题。毕竟,我们在一个高性能的深度学习库中进行了大量的字典查找、代码执行和许多其他的Python代码。Python的问题全局解释器锁 是众所周知的。在深度学习环境中,我们担心速度极快的GPU可能要等到CPU运行Python代码后才能运行另一个作业。

小结

  • 一个块可以由许多层组成;一个块可以由许多块组成。

  • 块可以包含代码。

  • 块负责大量的内部处理,包括参数初始化和反向传播。

  • 层和块的顺序连接由Sequential块处理。

2. 参数管理

选择好架构并设置好超参数后,下一阶段就是训练阶段。

训练阶段目标找到 使损失函数最小化 的模型参数值。(这些参数就是用于做出未来的预测)

本节内容:

  • 访问参数,用于调试、诊断和可视化;
  • 参数初始化;
  • 在不同模型组件间共享参数。

首先看一下具有单隐藏层的多层感知机:

import torch
from torch import nnnet = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 1))
X = torch.rand(size=(2, 4))
print(f"随机生成的原始输入:\n{X}")
print(f"模型输出:\n{net(X)}")

2.1. 参数访问

从已有模型中访问参数:

  • 当通过Sequential类定义模型时,可以通过索引来访问模型的任意层。
  • 可以把模型当作一个列表,每层的参数都在其属性中。
  • 如下所示,可以检查第二个全连接层的参数。
print(f"第二个全连接层的参数:\n{net[2].state_dict()}")

输出的结果显示:

  • 该全连接层包含两个参数,分别是该层的 权重 和 偏置。
  • 两者都存储为单精度浮点数(float32)。
  • 注意,参数名称允许唯一标识每个参数,即使在包含数百个层的网络中也是如此。

2.1.1. 目标参数

每个参数都表示为参数类的一个实例。要对参数执行任何操作,首先需要访问底层的数值。有几种方法可以做到这一点。下面的代码从第二个全连接层(即第三个神经网络层)提取偏置, 提取后返回的是一个参数类实例,并进一步访问该参数的值。

# 从第二个全连接层(即第三个神经网络层)提取偏置:
# 参数是复合的对象,包含值、梯度和额外信息,因此需要显式参数值
print(f"类型:{type(net[2].bias)}")
print(f"值(包括其形状和数据类型):\n{net[2].bias}")
print(f"数据部分(偏置参数的底层数据张量):{net[2].bias.data}")

除了值之外,还可以访问每个参数的梯度。在上面这个网络中,由于还没有调用反向传播,所以参数的梯度处于初始状态。

print(f"参数的梯度:{net[2].weight.grad == None}")

 

2.1.2. 一次性访问所有参数

  • 当需要对所有参数执行操作时,逐个访问会很麻烦。
  • 当处理更复杂的块(例如,嵌套块)时,情况会变得特别复杂,因为需要递归整个树来提取每个子块的参数。

下面,将通过演示来比较 访问第一个全连接层的参数 和 访问所有层。

print(f"---访问第一个全连接层的参数:\n{[(name, param.shape) for name, param in net[0].named_parameters()]}")
print(f"---访问所有层:\n{[(name, param.shape) for name, param in net.named_parameters()]}")
# print(*[(name, param.shape) for name, param in net[0].named_parameters()])
# print(*[(name, param.shape) for name, param in net.named_parameters()])

这为我们提供了另一种访问网络参数的方式,如下所示

# net.state_dict() 返回模型的参数字典
# ['2.bias'] 从参数字典中 获取模型中第三个模块(索引从0开始)的偏置参数
print(f"另一种访问网络参数的方式:{net.state_dict()['2.bias'].data}")

2.1.3. 从嵌套块收集参数

若将多个块相互嵌套,参数命名约定是如何工作的。首先定义一个生成块的函数(可以说是“块工厂”),然后将这些块组合到更大的块中。

def block1():return nn.Sequential(nn.Linear(4, 8), nn.ReLU(),nn.Linear(8, 4), nn.ReLU())def block2():net = nn.Sequential()for i in range(4):# 在这里嵌套net.add_module(f'block {i}', block1())return netrgnet = nn.Sequential(block2(), nn.Linear(4, 1))
rgnet(X)
print(f"嵌套块 模型输出:\n{rgnet(X)}")

设计了网络后,接下来看看它是如何工作的。

print(f"嵌套块 模型结构:\n{rgnet}")

因为层是分层嵌套的,所以我们也可以像通过嵌套列表索引一样访问它们。

访问 第一个主要的块中、第二个子块的第一层的偏置项:

print(f"第一个主要的块中、第二个子块的第一层的偏置项:\n{rgnet[0][1][0].bias.data}")

2.2. 参数初始化

  • 深度学习框架提供默认随机初始化,
  • 也允许创建自定义初始化方法,满足我们通过其他规则实现初始化权重。
  • 默认情况下,PyTorch会根据一个范围均匀地初始化权重和偏置矩阵,
    • 这个范围是根据输入和输出维度计算出的。
    • PyTorch的 nn.init模块提供了多种预置初始化方法。

2.2.1. 内置初始化

首先调用内置的初始化器。

2.2.1.1 初始化为高斯随机变量
  • 将所有权重参数初始化为标准差为0.01的高斯随机变量,且将偏置参数设置为0。
# 将所有权重参数初始化为 标准差=0.01的高斯随机变量,且将偏置参数设置为0
def init_normal(m):if type(m) == nn.Linear:nn.init.normal_(m.weight, mean=0, std=0.01)nn.init.zeros_(m.bias)
net.apply(init_normal)
print(f"权重初始化为 标准差=0.01的高斯随机变量:\n{net[0].weight.data[0]}")
print(f"偏置初始化为 常量0:\n{net[0].bias.data[0]}")

 

2.2.1.2 初始化为给定的常数
  • 将所有参数初始化为给定的常数,比如初始化为1。
# 将所有参数初始化为给定的常数,比如初始化为1
def init_constant(m):if type(m) == nn.Linear:nn.init.constant_(m.weight, 1)nn.init.zeros_(m.bias)
net.apply(init_constant)
print(f"权重初始化为 常量1:\n{net[0].weight.data[0]}")
print(f"偏置初始化为 常量0:\n{net[0].bias.data[0]}")

2.2.1.3 对不同块应用不同的初始化方法
  • 对不同块应用不同的初始化方法

例如,使用Xavier初始化方法初始化第一个神经网络层,然后将第三个神经网络层初始化为常量值42。

'''
Xavier初始化:根据输入和输出的维度自动调整初始化的范围,使得每一层的输出的方差在训练初期保持一致。(助于缓解梯度消失和梯度爆炸)
均匀分布:xavier_uniform_()使用均匀分布来初始化权重,而不是正态分布。均匀分布的范围是[-limit, limit],其中limit是根据输入和输出维度计算得出的
'''
# 对某些块应用不同的初始化方法
def init_xavier(m):if type(m) == nn.Linear:nn.init.xavier_uniform_(m.weight)
def init_42(m):if type(m) == nn.Linear:nn.init.constant_(m.weight, 42)net[0].apply(init_xavier)   # 使用Xavier初始化方法初始化第一个神经网络层
net[2].apply(init_42)       # 将第三个神经网络层初始化为常量值42
print(f"第一层网络,使用Xavier初始化权重:\n{net[0].weight.data[0]}")
print(f"第三层网络,权重初始化为 常量42:\n{net[2].weight.data}")

2.2.2. 自定义初始化

有时,深度学习框架没有提供我们需要的初始化方法。在下例中,使用以下的分布为任意权重参数w定义初始化方法:

同样,实现一个my_init函数来应用到net

def my_init(m):if type(m) == nn.Linear:print("Init", *[(name, param.shape)for name, param in m.named_parameters()][0])nn.init.uniform_(m.weight, -10, 10) # 使用均匀分布初始化权重,范围是[-10, 10]# 对权重进行阈值处理,将绝对值小于 5 的元素设置为 0。# 即 只有 绝对值>=5 的权重会被保留,其余的被置零m.weight.data *= m.weight.data.abs() >= 5
net.apply(my_init)
print(f"自定义初始化:\n{net[0].weight[:2]}")

注意,我们始终可以直接设置参数。

net[0].weight.data[:] += 1      # 将第一个模块的所有权重值加 1
net[0].weight.data[0, 0] = 42   # 将第一个模块的权重张量的 [0, 0] 位置的值设置为 42
print(f"对权重进行手动修改后:\n{net[0].weight[:2]}")

2.2.3. 参数绑定 (共享参数)

有时希望在多个层间共享参数:我们可以定义一个稠密层,然后使用它的参数来设置另一个层的参数。

# 参数绑定
# 我们需要给共享层一个名称,以便可以引用它的参数
shared = nn.Linear(8, 8)
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(),shared, nn.ReLU(),shared, nn.ReLU(),nn.Linear(8, 1))
net(X)
print(f"检查参数是否相同:\n"f"{net[2].weight.data[0] == net[4].weight.data[0]}")
net[2].weight.data[0, 0] = 100
print(f"确保它们实际上是同一个对象,而不只是有相同的值:\n"f"{net[2].weight.data[0] == net[4].weight.data[0]}")

这个例子表明第三个和第五个神经网络层的参数是绑定的。

  • 它们不仅值相等,而且由相同的张量表示。
  • 因此,若改变其中一个参数,另一个参数也会改变。
  • 当参数绑定时,由于模型参数包含梯度,因此在反向传播期间第二个隐藏层 (即第三个神经网络层)和第三个隐藏层(即第五个神经网络层)的梯度会加在一起。 

 共享参数通常可以节省内存,并在以下方面具有特定的好处:

  • 对于图像识别中的CNN,共享参数使网络能够在图像中的任何地方而不是仅在某个区域中查找给定的功能。
  • 对于RNN,它在序列的各个时间步之间共享参数,因此可以很好地推广到不同序列长度的示例。
  • 对于自动编码器,编码器和解码器共享参数。 在具有线性激活的单层自动编码器中,共享权重会在权重矩阵的不同隐藏层之间强制正交。

小结

  • 我们有几种方法可以访问、初始化和绑定模型参数。

  • 可以使用自定义初始化方法。

3. 延后初始化

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

tensorflow 版本:

def delay_initialization_tf(): # tensorflow版本import tensorflow as tfnet = tf.keras.models.Sequential([tf.keras.layers.Dense(256, activation=tf.nn.relu),tf.keras.layers.Dense(10),])print(f"访问输入层的权重参数:\n{[net.layers[i].get_weights() for i in range(len(net.layers))]}")X = tf.random.uniform((2, 20))net(X)print(f"将数据通过网络,使框架初始化参数:\n{[w.shape for w in net.get_weights()]}")
delay_initialization_tf()

def delay_initialization():net = nn.Sequential(nn.LazyLinear(64),nn.ReLU(),nn.LazyLinear(10))print(f"尚未初始化:\n{net}")print(net[0].weight)  # 尚未初始化,会报错print(f"输入层的权重参数:\n{[net[i].state_dict() for i in range(len(net))]}")X = torch.rand(2, 20)net(X) # 数据第一次通过模型传递print(f"数据第一次通过模型传递后,完成初始化:{net}")print(f"输入层的权重参数:\n{net[0].weight}")
delay_initialization()

3.1. 实例化网络

让实例化一个多层感知机:

    net = nn.Sequential(nn.LazyLinear(64),nn.ReLU(),nn.LazyLinear(10))

此时输入维数是未知的,因此网络不可能知道输入层权重的维数。因此,框架尚未初始化任何参数:

    print(f"尚未初始化:\n{net}")print(net[0].weight)  # 尚未初始化,会报错print(f"输入层的权重参数:\n{[net[i].state_dict() for i in range(len(net))]}")

 

注意:每个层对象都存在,但权重为空,参数显示未初始化。使用net.get_weights()将抛出一个错误,因为权重尚未初始化。

接下来将数据通过网络,最终使框架初始化参数:

    X = torch.rand(2, 20)net(X) # 数据第一次通过模型传递print(f"数据第一次通过模型传递后,完成初始化:{net}")print(f"输入层的权重参数:\n{net[0].weight}")

  1. 一旦知道输入维数是20,框架可以通过代入值20来识别第一层权重矩阵的形状。
  2. 识别出第一层的形状后,框架处理第二层,
  3. 依此类推,直到所有形状都已知为止。
  4. 注意:在这种情况下,只有第一层需要延迟初始化,但是框架仍是按顺序初始化的。等到知道了所有的参数形状,框架就可以初始化参数。

小结

  • 延后初始化使框架能够自动推断参数形状,使修改模型架构变得容易,避免了一些常见的错误。

  • 我们可以通过模型传递数据,使框架最终初始化参数。

练习

  • Q1: 如果指定了第一层的输入尺寸,但没有指定后续层的尺寸,会发生什么?是否立即进行初始化?
    • A1: 可以正常运行。第一层会立即初始化,但其他层同样是直到数据第一次通过模型传递才会初始化(不知道题目理解的对不对)
  • Q2: 如果指定了不匹配的维度会发生什么?
    • A2:
      • 如果在定义模型时,模块的输出尺寸和下一个模块的输入尺寸不匹配,PyTorch会在第一次调用forward时抛出错误。
      • 例如,如果你有一个LazyLinear层输出尺寸为10,但下一个LazyLinear层期望输入尺寸为20,则在第一次forward调用时会引发错误
  • Q3: 如果输入具有不同的维度,你需要做什么?提示:查看参数绑定的相关内容。
    • A3:
      • 如果输入维度比指定维度小,可以考虑使用padding填充;
      • 如果输入维度比指定维度大,可以考虑用pca等降维方法,将维度降至指定维度。

4. 自定义层

4.1. 不带参数的层

下面的CenteredLayer类要从其输入中减去均值。要构建它,只需继承基础层类实现前向传播功能

import torch
import torch.nn.functional as F
from torch import nnclass CenteredLayer(nn.Module): # 从输入中减去均值def __init__(self):super().__init__()def forward(self, X):return X - X.mean()

向该层提供一些数据,验证它是否能按预期工作:

layer = CenteredLayer()
X = torch.FloatTensor([1, 2, 3, 4, 5])
print(f"输入:{X}")
print(f"均值:{X.mean()}")
print(f"网络输出:{layer(X)}")

现在,可以将层作为组件合并到更复杂的模型中:

# 将层作为组件合并到更复杂的模型中
net = nn.Sequential(nn.Linear(8, 128), CenteredLayer())

作为额外的健全性检查,我们可以在向该网络发送随机数据后,检查均值是否为0。由于处理的是浮点数,因为存储精度的原因,我们仍然可能会看到一个非常小的非零数。

Y = net(torch.rand(4, 8)) # 向该网络发送随机数据
# print(f"将层作为组件合并到更复杂的模型中:{Y}")
print(f"检查均值是否为0 Y.mean():{Y.mean()}")

4.2. 带参数的层

定义具有参数(参数可以通过训练进行调整)的层,这些参数可以使用内置函数来创建。

  • 内置函数提供一些基本的管理功能 (比如管理访问、初始化、共享、保存和加载模型参数)。
  • 如此可以不需要为每个自定义层编写自定义的序列化程序。

下面实现自定义版本的全连接层,该层需要权重和偏置项两个参数。 这里使用修正线性单元作为激活函数。 该层需要输入参数:in_unitsunits,分别表示输入数和输出数。

class MyLinear(nn.Module):def __init__(self, in_units, units):super().__init__()# 权重和偏置项皆初始化为随机正态分布self.weight = nn.Parameter(torch.randn(in_units, units))self.bias = nn.Parameter(torch.randn(units,))def forward(self, X):linear = torch.matmul(X, self.weight.data) + self.bias.datareturn F.relu(linear)

接下来,实例化MyLinear类并访问其模型参数:

linear = MyLinear(5, 3)
print(f"自定义全连接层的权重:\n{linear.weight}")

可以使用自定义层直接执行前向传播计算:

print(f"使用自定义层直接执行前向传播计算:\n{linear(torch.rand(2, 5))}")

还可以使用自定义层构建模型,就像使用内置的全连接层一样使用自定义层:

net = nn.Sequential(MyLinear(64, 8), MyLinear(8, 1))
print(f"使用自定义全连接层构建的复杂模型,预测结果:\n{net(torch.rand(2, 64))}")

小结

  • 可以通过基本层类 设计自定义层。以此来定义 行为与深度学习框架中的任何现有层不同的 灵活的 新层。

  • 在自定义层定义完成后,就可以在任意环境和网络架构中调用该自定义层。

  • 层可以有局部参数,这些参数可以通过内置函数创建。

5. 读写文件

  • 保存训练的模型,可以 以备将来在各种环境中使用(比如在部署中进行预测)。
  • 当运行一个耗时较长的训练过程时,定期保存中间结果,可以防止在服务器电源被不小心断掉时,损失几天的计算结果。

5.1. 加载和保存张量

对于单个张量,可以直接调用loadsave函数分别读写它们。这两个函数都要求提供一个名称,save要求将要保存的变量作为输入。

import torch
from torch import nn
from torch.nn import functional as Fx = torch.arange(4)
torch.save(x, 'outputFile/x-file') # 将张量保存到文件中

然后可以将存储在文件中的数据读回内存:

x2 = torch.load('outputFile/x-file', weights_only=False) # 从文件中加载张量,并赋值
print(f"x2={x2}")

可以存储一个张量列表,然后把它们读回内存:

# 存储一个张量列表,然后把它们读回内存:
y = torch.zeros(4)
torch.save([x, y],'outputFile/x-files') # 将两个张量保存到文件中
x2, y2 = torch.load('outputFile/x-files', weights_only=False) # 加载列表中的张量,并将其解包到 x2 和 y2
print(f"x2, y2 = {x2, y2}")

也可以写入或读取从字符串映射到张量的字典。这利于读取或写入模型中的所有权重:

# 写入或读取从字符串映射到张量的字典
mydict = {'x': x, 'y': y} # 创建字典
torch.save(mydict, 'outputFile/mydict') # 将字典保存到文件 mydict 中
mydict2 = torch.load('outputFile/mydict', weights_only=False) # 加载字典,并将其赋值给 mydict2
print(f"mydict2={mydict2}")

 

5.2. 加载和保存模型参数

若想保存整个模型,并在以后加载它们,单独保存每个向量则会变得很麻烦。 毕竟,我们可能有数百个参数散布在各处。 因此,

  • 深度学习框架提供了内置函数来保存和加载整个网络。
  • 注意,保存的是 模型的参数,而不是保存整个模型。

例如,有一个3层多层感知机,我们需要单独指定架构。因为模型本身可以包含任意代码,所以模型本身难以序列化。 因此,恢复模型需要用代码生成架构,然后从磁盘加载参数。

下面以多层感知机为例:

class MLP(nn.Module):def __init__(self):super().__init__()self.hidden = nn.Linear(20, 256)self.output = nn.Linear(256, 10)def forward(self, x):return self.output(F.relu(self.hidden(x)))net = MLP()
X = torch.randn(size=(2, 20))
Y = net(X)

接下来,将模型的参数存储在一个叫做“mlp.params”的文件中:

torch.save(net.state_dict(), 'outputFile/mlp.params')

为了恢复模型,我们实例化了原始多层感知机模型的一个备份。这里不需要随机初始化模型参数,而是直接读取文件中存储的参数:

clone = MLP()
print(f"模型:\n{clone}")
# 直接读取文件中存储的参数
# clone.load_state_dict(...) 将加载的状态字典应用到clone中
clone.load_state_dict(torch.load('outputFile/mlp.params', weights_only=False))
clone.eval() # 设置模型为评估模式

由于两个实例具有相同的模型参数,在输入相同的X时, 两个实例的计算结果应该相同。 下面进行验证:

Y_clone = clone(X)
print(f"副本与读上来版本的异同:\n{Y_clone == Y}")

小结

  • saveload函数可用于张量对象的文件读写。

  • 我们可以通过参数字典保存和加载网络的全部参数。

  • 保存架构必须在代码中完成,而不是在参数中完成。

6. GPU

6.1. 计算设备

6.2. 张量与GPU

6.2.1. 存储在GPU上

6.2.2. 复制

6.2.3. 旁注

6.3. 神经网络与GPU

小结

  • 我们可以指定用于存储和计算的设备,例如CPU或GPU。默认情况下,数据在主内存中创建,然后使用CPU进行计算。

  • 深度学习框架要求计算的所有输入数据都在同一设备上,无论是CPU还是GPU。

  • 不经意地移动数据可能会显著降低性能。一个典型的错误如下:计算GPU上每个小批量的损失,并在命令行中将其报告给用户(或将其记录在NumPy ndarray中)时,将触发全局解释器锁,从而使所有GPU阻塞。最好是为GPU内部的日志分配内存,并且只移动较大的日志。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/85253.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/85253.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

NodeJS的fs模块的readFile和createReadStream区别以及常见方法

Node.js 本身没有像 Java 那样严格区分字符流和字节流,区别主要靠编码(encoding)来控制数据是以 Buffer(二进制字节)形式还是字符串(字符)形式处理。 详细解释: 方面JavaNode.js字节…

基于二进制XOR运算的机器人运动轨迹与对称图像自动生成算法

原创:项道德(daode3056,daode1212) 新的算法出现,往往能给某些行业与产业带来革命与突破。为探索机器人运动轨迹与对称图像自动生成算法,本人已经通过18种算法的测试,最终,以二进制的XOR运算为…

Spring AI 项目实战(七):Spring Boot + Spring AI Tools + DeepSeek 智能工具平台(附完整源码)

系列文章 序号文章名称1Spring AI 项目实战(一):Spring AI 核心模块入门2Spring AI 项目实战(二):Spring Boot + AI + DeepSeek 深度实战(附完整源码)3Spring AI 项目实战(三):Spring Boot + AI + DeepSeek 打造智能客服系统(附完整源码)4Spring AI 项目实战(四…

spring-webmvc @RequestHeader 典型用法

典型用法 基础用法:获取指定请求头值 GetMapping("/info") public String getInfo(RequestHeader("User-Agent") String userAgent) {return "User-Agent: " userAgent; }如果请求中包含 User-Agent 请求头,则其值将被…

Ubuntu:20.04中安装docker

是的,您列出的命令是完整的安装步骤,但为了确保100%可靠性和处理可能的问题,我建议进行以下优化和补充: 完整优化的安装脚本(包含错误处理和验证) #!/bin/bash# 1. 彻底清理旧版本和配置 sudo apt remove…

大数据实时风控引擎:Spark Streaming、Kafka、Flink与Doris的融合实践

大数据实时风控引擎:Spark Streaming、Kafka、Flink与Doris的融合实践 在数字金融、电商交易与在线服务的核心战场,风险控制能力已成为业务的生命线。传统批量风控模式在应对瞬息万变的欺诈攻击、信用风险时捉襟见肘。本文将深入探讨如何利用**Spark St…

【创龙瑞芯微 RK3576 全国产 ARM 八核 2.2GHz 工业开发板-硬件说明书】

前 言 本文主要介绍TL3576-EVM评估板硬件接口资源以及设计注意事项等内容。 RK3576J/RK3576处理器的IO电平标准一般为1.8V、3.3V,上拉电源一般不超过3.3V或1.8V,当外接信号电平与IO电平不匹配时,中间需增加电平转换芯片或信号隔离芯片。按键或接口需考虑ESD设计,ESD器件…

一文吃透ADB,从入门到精通

目录 一、ADB 简介1.1 什么是 ADB1.2 ADB 的工作原理1.3 ADB 的安装与环境配置 二、ADB 基础命令2.1 设备连接相关命令2.2 应用管理命令2.3 文件传输命令 三、ADB 高级命令3.1 ADB Shell 深入探究3.2 日志查看与分析3.3 设备信息获取3.4 屏幕操作与录制 四、ADB 常见问题与解决…

PostgreSQL高可用架构设计与实践指南

# PostgreSQL高可用架构设计与实践指南 ## 一、高可用性核心诉求 PostgreSQL作为企业级关系型数据库,高可用设计需要满足以下关键指标: - 故障恢复时间(RTO):秒级到分钟级自动切换能力 - 数据损失容忍度&#xff0…

今天我想清楚了

首先说一声抱歉,很多天没有更新了,因为在我这里,我的内心感到迷茫,从来没有这样过,不知道为什么自己一直要做的事,进度太慢了,因为我的人生是空虚的,我感觉我做的不够好,…

代码随想录day3链表1

new关键字 1.new是一个关键字,用于开辟空间,开辟的空间在堆上,而一般声明的变量存放在栈上; 2.new得到的是一段空间的首地址。所以一般需要用指针来存放这段地址 new int(10);//返回new出来这块内存的地址int *pnew int(10);//…

taro小程序如何实现新用户引导功能?

一、需求背景 1、需要实现小程序新功能引导 2、不使用第三方库(第三方组件试了几个,都是各种兼容性问题,放弃) 二、实现步骤 1、写一个公共的guide组件,代码如下 components/Guide/index.tsx文件 import React, { …

键盘动作可视化技术浅析:如何做到低延迟显示

在做屏幕录制或者操作演示的时候,你是否遇到过这样的问题:观众看不清你按了哪个键、点了哪里?这是能完美解决这个问题的小工具Keyviz。它可以把你的键盘输入和鼠标点击实时显示在屏幕上,清晰直观,特别适合教学、录屏、…

Prufer序列 学习笔记

文章目录 P r u f e r Prufer Prufer 序列对树建立 P r u f e r Prufer Prufer 序列对 P r u f e r Prufer Prufer 序列重建树 应用Cayley 公式[HNOI2004] 树的计数「雅礼集训 2017 Day8」共[THUPC 2018] 城市地铁规划CF156D Clues[ARC106F] Figures P r u f e r Prufer Pruf…

高性能场景使用Protocol Buffers/Apache Avro进行序列化怎么实现呢

我们以Protocol Buffers(Protobuf)和Apache Avro为例,分别展示高性能序列化的实现方式。 由于两者都需要定义Schema,然后生成代码,因此步骤包括: 1. 定义Schema文件 2. 使用工具生成Java类 3. 在代码中…

iOS端网页调试 debug proxy策略:项目中的工具协同实践

移动开发中的调试,一直是效率瓶颈之一。特别是当前 Web 前端与 App 原生高度耦合的背景下,页面调试不仅受限于浏览器,还要面对 WebView 实现差异、系统权限控制、设备多样性等复杂情况。 但我们是否可以构建一套**“设备无关”的调试工作流*…

springboot项目启动报错:spring boot application in default package

启动类报错: 问题: springboot的启动方法不能直接在java目录下 解决: 1.使用CompentScan 和EnableAutoConfiguration注解 2.启动类放在java目录下的package目录下

机器学习实验报告5-K-means 算法

4.1 k-means算法简介 聚类分析,作为机器学习领域中的一种无监督学习方法,在数据探索与知识发现过程中扮演着举足轻重的角色。它能够在没有先验知识或标签信息的情况下,通过挖掘数据中的内在结构和规律,将数据对象自动划分为多个类…

【已解决】yoloOnnx git工程部署

首先 yoloonnx一个VS工程下来整个工程大概1-2个g的大小因此在git的过程中总是会因为文件超过100M而触发报错,上传不上去,因此现在需要做一个过滤才能把工程重新上传上去,那么这个时候别人需要下载下来的时候确实不完整的工程,因此…

如何轻松地将照片从电脑传输到安卓手机

一些安卓用户正在寻找有效可靠的方法,将照片从电脑传输到安卓设备。如果您也想将有趣或难忘的照片导入安卓手机或平板电脑,可以参考这篇文章,它提供了 6 种可靠的方法,让您轻松传输照片。 第 1 部分:如何通过 Android …