深度学习计算笔记

层和块

首次介绍神经网络时,关注的是具有单一输出的线性模型,整个模型只有一个输出。单个神经网络(1)接受一些输入;(2)生成相应的标量输出;(3)具有一组相关参数(parameters),更新这些参数可以优化目标函数。
对于具有多个输出的神经网络,我们利用矢量化算法来描述正层神经元。像单个神经元一样,层(1)接受一组输入;(2)生成相应的输出;(3)由一组可调整参数描述。
对于多层感知机,整个模型(1)接受原始输入(特征);(2)生成输出(预测);(3)并包含一些参数(所有组成层的参数集合)。同样,每个单独的层接受输入(由前一层提供),生成输出(到下一层输入),并且具有一组可调参数,这些参数根据从下一层反向传播的信号更新。
事实证明,研究讨论‘‘比单个层大’’但‘‘比整个模型小’’的组件更有价值。例如,在计算机视觉中广泛流行的ResNet-152架构就是有数百层,这些层是由层组(groups of layers)的重复模型组成。这个ResNet架构仍然是许多视觉任务的首选架构。在其他的领域,如自然语言处理,层组以各种重复模型排列的类似架构现在也是普遍存在。
为了实现这些复杂的网络,我们引入了神经网络块的概念。块(block)可以描述单个层、由多个层组成的组件或整个模型本身。抽象成块的好处是可以将一些块组合成更大的组件。这一过程通常是递归的。通过定义代码来按需生成任意复杂度的块,可以通过简洁的代码实现复杂的神经网络。
深度学习计算笔记_第1张图片
从编程的角度来看,块由类(class)表示。它的任何子类都必须定义一个将其输入转换为输出的前向传播函数,并且必须存储任何必须的参数(有些块不需要任何参数)。最后,为了计算梯度,块必须具有反向传播函数。

自定义块

每个块必须提供的基本功能:

  • 输入数据,作为其前向传播函数的输入参数。
  • 输出数据,通过前向传播函数生成的输出参数。
  • 计算其输出关于输入的梯度,可通过其反向传播函数进行访问。
  • 存储和访问前向传播计算所需的参数。
  • 根据需要初始化模型参数
from torch import nn
from torch import functional as F
class MLP(nn.Module):
	def __init__(self):
		super().__init__()
		self.hidden = nn.Linear(784,256)
		self.out = nn.Linear(256,10)
	def forward(self, X):
		return self.out(F.relu(self.hidden(X)))
import torch
X = torch.rand(10,784)
net = MLP()
net(X)

顺序块

class SelfSequential(nn.Module):
	def __init__(self, *args):
		super().__init__()
		for idx, module in enumerate(args):
			# class Sequential(Module):
			# 	_modules: Dict[str, Module]  # type: ignore[assignment]
			self._modules[str(idx)] = module
		
	def forward(self, X):
		for block in self._modules.values():
			X = block(X)
		return X

参数管理

本节将介绍,(1)访问参数,用于调试、诊断和可视化;(2)参数初始化;(3)在不同模型组件间共享参数。

import torch
from torch import nn
net = nn.Sequential(nn.Linear(4,8),nn.ReLU(),nn.Linear(8,1))
X = torch.rand(size=(2,4))
net(X)
# tensor([[ 0.0034],
#        [-0.0161]], grad_fn=)

参数访问

通过Sequential类定义模型时,可以通过索引来访问模型的任意层。模型就像是一个列表一样,每层的参数都在其属性中。
由输出结果可知,首先,这个全连接层包含两个参数,分别是权重和偏置。两者都存储为单精度浮点数(float32)。注意,参数名称允许唯一标识每个参数,即使在包含数百层的网络中。

net[2].state_dict()
# OrderedDict([('weight',
#               tensor([[ 0.2673,  0.0125,  0.1899,  0.2948, -0.1987, -0.2978,  0.2894,  0.3379]])),
#             ('bias', tensor([-0.2959]))])

目标参数

每个参数都表示为参数类的一个实例。要对参数执行任何操作,首先需要访问该参数。
参数是复合的对象,包含值、梯度和额外信息,这就是需要显示访问参数的原因。除值之外,还可以访问每个参数的梯度。

print(type(net[2].bias))
print(net[2].bias)
print(net[2].bias.data)
# 
# Parameter containing:
# tensor([-0.2959], requires_grad=True)
# tensor([-0.2959])

print(net[2].weight.grad == None)
# True

访问所有参数

当需要对所有参数执行操作时,逐个访问可能会很麻烦。当处理更复杂的块(例如,嵌套块)时,情况可能会变得特别复杂,因为我们需要递归整个树来提取每个子块的参数。下面,我们将通过演示来比较访问第一个全连接层的参数和访问所有层

print(*[(name, param.shape) for name, param in net[0].named_parameters()])
print(*[(name, param.shape) for name, param in net.named_parameters()])
# ('weight', torch.Size([8, 4])) ('bias', torch.Size([8]))
# ('0.weight', torch.Size([8, 4])) ('0.bias', torch.Size([8])) ('2.weight', torch.Size([1, 8])) ('2.bias', torch.Size([1]))

另一种访问网络参数的方式

net.state_dict()['2.bias'].data

从嵌套块收集参数

生成自定义组合块

def block1():
	return nn.Sequential(nn.Linear(4,8), nn.ReLU(),nn.Linear(4,8), nn.ReLU())
def block2():
	net = nn.Sequential()
	for i in range(4):
		net.add_module(f'block {i}', block1())
	return net
rgnet = nn.Sequential(block2(), nn.Linear(4,1))
rgnet(X)
# tensor([[-0.7421],
#        [-0.7421]], grad_fn=)
print(rgnet)
# Output exceeds the size limit. Open the full output data in a text editor
# Sequential(
#   (0): Sequential(
#     (block 0): Sequential(
#       (0): Linear(in_features=4, out_features=8, bias=True)
#       (1): ReLU()
#       (2): Linear(in_features=8, out_features=4, bias=True)
#       (3): ReLU()
#     )
#     (block 1): Sequential(
#       (0): Linear(in_features=4, out_features=8, bias=True)
#       (1): ReLU()
#       (2): Linear(in_features=8, out_features=4, bias=True)
#       (3): ReLU()
#     )
#     (block 2): Sequential(
#       (0): Linear(in_features=4, out_features=8, bias=True)
#       (1): ReLU()
#       (2): Linear(in_features=8, out_features=4, bias=True)
#       (3): ReLU()
#     )
#     (block 3): Sequential(
#       (0): Linear(in_features=4, out_features=8, bias=True)
#       (1): ReLU()
#       (2): Linear(in_features=8, out_features=4, bias=True)
#       (3): ReLU()
# ...
#     )
#   )
#   (1): Linear(in_features=4, out_features=1, bias=True)
# )

层是分层嵌套的,可以像通过嵌套列表索引一样访问参数。

# 访问第一个主要块中第二个子块的第一层的偏置项
rgnet[0][1][0].bias.data
# tensor([ 0.4368, -0.4383, -0.2301,  0.1096, -0.0355,  0.2192, -0.4385,  0.0027])

初始化参数

默认情况,PyTorch根据一个范围均匀地初始化权重和偏置矩阵,范围由根据输入和输出维度计算得到。PyTorch的nn.init模块提供多种预置初始化方法。

import torch
from torch import nn
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 1))X = torch.rand(size=(2, 4))
X = torch.rand(size=(2, 4))

内置初始化

通过内置初始化器进行初始化

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)
net[0].weight.data[0], net[0].bias.data[0]
# (tensor([0.0041, 0.0010, 0.0083, 0.0200]), tensor(0.))
def init_constant(m):
	if type(m) == nn.Linear:
		nn.init.constant_(m.weight, 1)
		nn.init.zeros_(m.bias)
net.apply(init_constant)
net[0].weight.data[0], net[0].bias.data[0]
# (tensor([1., 1., 1., 1.]), tensor(0.))

Xavier初始化

def init_xavier(m):
	if type(m) == nn.Linear:
		nn.init.xavier_uniform_(m.weight)
def init_constant_42(m):
	if type(m) == nn.Linear:
		nn.init.constant_(m.weight, 42)
net[0].apply(init_xavier)
net[2].apply(init_constant_42)
print(net[0].weight.data[0])
print(net[2].weight.data)
# tensor([-0.4327, -0.5751, -0.5363,  0.2745])
# tensor([[42., 42., 42., 42., 42., 42., 42., 42.]])

自定义初始化

深度学习计算笔记_第2张图片

def my_init(m):
	if type(m) == nn.Linear:
		nn.init.uniform_(m.weight, -10, 10)
		m.weight.data *= m.weight.data.abs() >= 5
net.apply(my_init)
net[0].weight[:2]
# tensor([[-0.0000,  9.2375,  6.7505, -0.0000],
#        [ 0.0000, -6.8064,  0.0000,  8.1219]], grad_fn=)
  1. torch.nn.init.uniform_(tensor, a=0, b=1),从均匀分布U(a, b)中采样,填充输入的张量或变量。
  2. torch.nn.init.normal_(tensor, mean=0.0, std=1.0),从给定的均值和标准差的正态分布 中生成值,初始化张量。
  3. torch.nn.init.constant_(tensor, val),用常数 val 的值填充输入的张量或变量。
  4. torch.nn.init.xavier_normal_(tensor, gain=1.0),使用Glorot 初始化方法正态分布生成值,生成随机数填充张量。
  5. torch.nn.init.xavier_uniform_(tensor, gain=1.0),使用Glorat初始化方法均匀分布生成值,生成随机数填充张量。
  6. torch.nn.init.kaiming_uniform_(tensor, a=0, mode=‘fan_in’, nonlinearity=‘leaky_relu’),使用HE初始化方法均匀分布生成值,生成随机数填充张量。
  7. torch.nn.init.kaiming_normal_(tensor, a=0, mode=‘fan_in’, nonlinearity=‘leaky_relu’),使用HE初始化方法正态分布生成值,生成随机数填充张量。

直接设置参数

net[0].weight.data[:] += 1
net[0].weight.data[0, 0] = 42
net[0].weight.data[0]
# tensor([42.0000, 10.2375,  7.7505,  1.0000])

参数绑定

在多个层间共享参数,定义稠密层,使用该层参数初始化另一层的参数

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(net[2].weight.data[0] == net[4].weight.data[0])
net[2].weight.data[0, 0] = 100
# 确保它们实际上是同⼀个对象,⽽不只是有相同的值
print(net[2].weight.data[0] == net[4].weight.data[0])

自定义层

不带参数的层

定义CenteredLayer类,从其输⼊中减去均值

def CenteredLayer(nn.Module):
	def __init__(self):
		super().__init__()
	def forward(self, X):
		return X - X.mean()
# 测试
layer = CenteredLayer()
layer(torch.FloatTensor([_ for _ in range(5)]))
# tensor([-2., -1.,  0.,  1.,  2.])

以将自定义层作为组件合并到更复杂的模型中。

net = nn.Sequential(nn.Linear(8,128), CenteredLayer())

# 测试
Y = net(torch.rand(4,8))
Y.mean()
# tensor(-1.8626e-09, grad_fn=)

带参数的层

自定义全连接层,包含两个参数:权重和偏置。使⽤修正线性单元作为激活函数。输⼊参数:in_units和units,分别表⽰输⼊数和输出数。

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.data
		return F.relu(linear)
linear = MyLinear(5, 3)
linear.weight
# Parameter containing:
# tensor([[-0.1539,  0.7556,  0.5985],
#         [-0.4069,  1.0454, -0.3202],
#         [-1.2697,  2.2512, -0.1835],
#         [ 1.5846,  0.3276, -0.8318],
#         [-1.2247, -0.5247,  0.4574]], requires_grad=True)

像使⽤内置的全连接层⼀样使⽤⾃定义层

net = nn.Sequential(MyLinear(64, 8), MyLinear(8, 1))
net(torch.rand(2, 64))
# tensor([[0.],
#         [0.]])

小结

  • 可以通过基本层类灵活地设计自定义层,新定义层的行为与深度学习框架中的现有层相同。
  • 自定义层可以在任何环境和网络框架中调用。
  • 层可以有局部参数,这些参数可以通过内置函数创建。

读写文件

import torch 
from torch import nn
from torch.nn import functional as F
x = torch.arange(5)
x
# tensor([0, 1, 2, 3, 4])

torch.arange(end: Number, step: Number,dtype:Optional[_dtype]=None),从0开始到end结束,步长为step
torch.arange(start: Number, end: Number, step: Number,dtype:Optional[_dtype]=None),默认步长为1,默认数据类型为整数

加载和保存张量

读写单个张量

torch.save(X, 'x-file')
x2 = torch.load('x-file')
x2
# tensor([0, 1, 2, 3, 4])

读写一个张量列表

y = torch.zeros(4)
torch.save([x, y],'x-files')
x2, y2 = torch.load('x-files')
(x2, y2)
# (tensor([0, 1, 2, 3, 4]), tensor([0., 0., 0., 0.]))

读写字典

mydict = {'x': x, 'y': y}
torch.save(mydict, 'mydict')
mydict2 = torch.load('mydict')
mydict2
# {'x': tensor([0, 1, 2, 3, 4]), 'y': tensor([0., 0., 0., 0.])}

加载和保存模型参数

定义模型,实例化,并初始化

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)

保存模型参数

torch.save(net.state_dict(), 'mlp-params')

读取模型参数

clone = MLP()
clone.load_state_dict(torch.load('mlp-params'))
clone.eval()
# MLP(
#  (hidden): Linear(in_features=20, out_features=256, bias=True)
#  (output): Linear(in_features=256, out_features=10, bias=True)
# )

由于两个实例具有相同的模型参数,在输⼊相同的X时,两个实例的计算结果应该相同

Y_clone = clone(X)
Y_clone == Y
# tensor([[True, True, True, True, True, True, True, True, True, True],
# [True, True, True, True, True, True, True, True, True, True]])

你可能感兴趣的:(动手学深度学习v2)