目录
模型构造
层和块
首先,我们回顾一下多层感知机
import torch
from torch import nn
from torch.nn import functional as F
# 一个简单的单层神经网络
net = nn.Sequential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))
# 生成一个随机的input
X = torch.rand(2, 20)
net(x) # 得到output
functional里面定义了一些没有包括参数的一些函数,之后会讲如何使用,刨个坑在这~
nn.Sequential定义了一种特殊的Module,在PyTorch中,Module是一个很重要的概念
自定义块
任何一个层,任何一个神经网络,都应该是Module的一个子类
下面自定义了一个MLP,实现了和刚刚一样的函数
class MLP(nn.Module): # 是nn.Module的子类,继承之后会得到很多好用的函数
def __init__(self):
super().__init__() # 调用父类,这样在初始化weight等的时候就可以直接全部弄好
self.hidden = nn.Linear(20, 256)
self.out == nn.Linear(256, 10)
def forward(self, X):
return self.out(F.relu(self.hidden(X))) # 直接通过nn.module里面实现的ReLU来激活
所有的Module都有两个比较重要的函数:
- __init__:定义需要哪些类、哪些参数
- forward:前向函数,用来写给定输入,如何操作来前向运算传输
实例化多层感知机的层,然后在每次调用正向传播函数时调用这些层
net = MLP() # 实例化我们刚刚创建的类
net(X) # 把输入丢进去,最后得出一个2*10的矩阵
顺序块
让我们来实现以下nn.Sequential,其实很简单
class MySequential(nn.Module):
def __init__(self, *args): # 接收一个list of input arguments
super().__init__() # 调用父类的初始化函数
for block in args: # 对所有传入的层(称为block)放入一个特殊的容器里面
# 放进_modules[],pytorch就知道里面都是我们需要的层,其实是一个按序排的字典,所以我们按序给它插入,将本身作为key
self._modules[block] = block
def forward(self, X):
for block in self._modules.values(): # 因为是排过序的,所以会和我们调用的顺序是一样的
X = block(X)
return X
net = MySquential(nn.Linear(20, 256), nn.ReLU(), nn.Linear(256, 10))
net(X)
在正向传播函数中执行代码
当Sequential无法满足我们的时候,我们还可以做更多灵活的定义,比如如下代码所示
class FixedHiddenMLP(nn.Module):
def __init__(self):
super().__init__()
# 这个随机的weight不参与训练,因为不计算梯度
self.rand_weight = torch.rand((20, 20), requires_grad = false)
self.linear = nn.Linear(20, 20)
# 里面可以写任何你想做的函数! 任何!比Sequential更灵活!!
def forward(self, X):
X = self.linear(X)
# mm:矩阵乘法
X = F.relu(torch.mm(X, self.rand_weight) + 1)
# 回头二次调用线性函数
X = self.linear(X)
while X.abs().sum() > 1:
X /= 2 # 一直除2,除到你很小为止
return X.sum()
net = FixedHiddenMLP()
net(X)
混合搭配各种组合块的方法
另一种灵活的方法就是:嵌套使用!因为无论是层、Sequential等都是nn.Module的子类,其实是可以很灵活的嵌套使用的
没任何意义,只是一个举例
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)) # 一个Sequential和Linear的嵌套
chimera = nn.Sequentisl(NestMLP(), nn.Linear(16, 20), FixedHiddenMLP()) # 再套一层!
chimera(X)
参数管理
当我们把模型定义好之后我们要如何访问我们的参数呢?
我们首先关注具有单隐藏层的多层感知机
import torch
from torch import nn
# 一个单隐藏层MLP
net = nn.Sequential(nn.Linear(4, 8), nn.ReLU(), nn.Linear(8, 1))
X = torch.rand(size = (2, 4))
net(X)
参数访问
# stare_dict():理论上他就是_Modules
print(net[2].statc_dict())
我们会看到这样的输出:
OrderedDict([('weight', tensor([[-0.1351, -0.0821, 0.2084, 0.2804, -0.2748, 0.0061,
0.1729, -0.3424]])), ('bias', tensor([-0.3389]))])
可以简单把nn.Sequential当成python的一个list,那我们的net[2]就会拿到我们刚刚定义net时候的第三个层,也就是nn.Lunear(8, 1),最后的输出层
目标参数
我们也可以直接去访问一个具体的参数
print(type(net[2],bias)) # 打印最后一层偏移
print(net[2].bias)
print(net[2].bias.data) # 只访问值,他还有个梯度,可以通过.grad来访问梯度值
输出:
<class 'torch.nn.parameter.Parameter'> # 类型是Parameter:是一个可以优化的参数
Parameter containing
tensor([-0.3389], required_grad = Trye)
tensot([-0.3389])
一次性访问所有参数
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]))
因为1是ReLU,是没有参数的,所以拿不出来
当我们有名字之后,我们可以通过名字来获取参数
net.state_dict()['2.bias'].data
输出:
tensor([-0.3389])
从嵌套块收集参数
当我们模型里面出现了嵌套我们要如何访问参数呢?
def block1():
return nn.Sequential(nn.Linear(4,8), nn.ReLU(), nn.Linear(8, 4),
nn.ReLU())
# 嵌套了四个block1
def block2():
net = nn.Sequential()
for i in range(4):
net.add_module(f'bock {i}', block1()) # 我自己传了个字符串给你,就不是0123了
return net
rgnet = nn.Sequential(block2(), nn.Linear(4, 1))
rgnet(X)
print(rgnet)
输出:
首先外层是一个Sequential,第0个元素也是Sequential(我们的block2),block2里面的block 0元素又是一个Sequential(block1),所以整个网络是一个三个Sequential的嵌套
所以可以通过print大概看出网络长什么样子 。当然这是个简单网络,如果是复杂网络的话,print看起来就没有那么方便
复杂模型可以一个block一个block看,这样对于模型结构也有好处
初始化参数
如何修改默认的参数
内置初始化
def init_normal(m): # m:每次传入一个Module
if type(m) == nn.Linear:
nn.init.normal_(m.weight, mean = 0, std = 0.01) # 对weight初始化,下划线跟在后面表示这是一个替换函数,不是返回一个值
nn.init.zeros_(m.bias) # bias赋0
net.apply(init_normal) # apply:对net里面所有的Module去调用这个函数,遍历一遍
apply:给你一个方式让你去遍历整个神经网络去做一些事情
对某些块应用不同的初始化方法
def xavier(m):
if type(m) == nn.Linear:
nn.init.xavier_uniform_(m.weight) # 一个xavier初始化
def init_42(m):
if type(m) == nn.Linear:
nn.init.constant_(m.weight, 42)
net[0].apply(xavier)
ner[2].applu(init_42)
自定义初始化
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)
m.weight.data *= m.weight.data.abs() >= 5
net.apply(my_init)
当然还有一些更暴力的办法:
# 直接拎出来改值
net[0].weight.data[:] += 1
net[0].weight.data[0, 0] = 42
参数绑定
讲一个小应用,有时候我们会希望在不同层里面共享权重的时候,可以做一个参数绑定
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]) # 比较shared
net[2].weight.data[0, 0] = 100
print(net[2].weight.data[0] == net[4].weight.data[0]) # 改第三层,第五层也被修改
# 输出
tensor([True, True, True, True, True, True, True, True])
tensor([True, True, True, True, True, True, True, True])
自定义层
构造一个没有任何参数的自定义层
class CenteredLayer(nn.Module):
def __init__(self): # python3不写这个函数会自己加上
super().__init__()
def forward(self, X):
return X - X.mean()
layer = CenteredLayer()
layer(torch.FloatTensor([1, 2, 3, 4, 5]))
# 输出
tensor({[-2., -1., 0., 1., 2.])
将层作为组件合并构建更复杂的模型中
net = nn.Sequential(nn.Linear(8, 128), CenteredLayer())
Y = net(torch.rand(4, 8))
Y.mean()
# 输出
tensor(-2.7940e-09, grad_fn = <MeanBackward0>)
带参数的层
通过nn.Parameter()把初始化的值包起来就行了
class MyLinear(nn.Module):
def __init__(self, in_units, units): # in_units:输入大小 units:输出大小
super().__init__()
# Parameter:给你梯度加上,再给你一个合适的名字,就干这点事情
self.weight = nn.Parameter(torch.randn(in_units, units))
self.bias = nn.Parameter(torch.randn(units,))
def forward(self, X):
# matmul:矩阵乘法
linear = torch.matmul(X, self.weight.data) + self.bias.data
return F.relu(linear)
dense = MyLinear(5, 3)
dense.weight
# 输出
Parameter containing:
tensor([[1.1537, -0.2809, 1.2778],
[-0.7972, 0.8573, -0.6472],
[0.6014, -2.0051, -0.3640],
[1.3730, 0.5488, 1.1121],
[-1.0814, 1.5180, -0.2057]], requires_grad = True)
读写文件
简而言之就是,你训练好的东西怎么存下来
加载和保存张量
x = torch.arange(4)
torch.save(x, 'x-file') # 把这个向量x,存入当前目录下,文件名为x-file
x2 = torch.load("x-file")
x2
# 输出
tensor([0, 1, 2, 3])
存储一个张量列表,然后把他们读回内存
我们不单单可以只存一个向量,还可以存一个list,然后读取
y = torch.zeros(4)
torch.save([x, y], 'x-files')
x2, y2 = torch.load('x-files')
(x2, y2)
# 输出
(tensor([0, 1, 2, 3]), tensor([0., 0., 0., 0.]))
写入或读取从字符串映射到张量的字典
可以存list,那我们存一个字典也很正常叭!
mydict = {'x' : x, 'y' : y}
torch.save(mydict, 'mydict')
mydict2 = torch.load('mydict')
mydict2
# 输出
{'x': tensor([0, 1, 2, 3]), 'y': tensor([0., 0., 0., 0.])}
加载和保存模型参数
那我有一个MLP我要怎么办?
首先我们要明白,对于一个神经网络,我们应该存什么东西?
因为我们PyTorch是没有办法把整个网络模型存下来的,相较于Tensorflow有一定局限性
其实我们要存的是模型的权重参数!
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)
那我们要怎么存呢?
记得我们可以用state_dict()来得到我们所有的参数的字符串对吧!
那我们将模型的参数存储为一个叫做”mlp.params“的文件
torch.save(net.state_dict(), 'mlp.params')
那我们怎么load回来呢??
实例化了原始多层感知机模型的一个备份。直接读取文件中存储的参数
如果你真的要在别的地方load这个模型的话,你不光要把.params文件带走,你还得把网络的定义带走!!
因为你首先要声明生成一个网络出来
声明一个新的clone实例化MLP,这个时候网络参数已经被随机初始化过了,但是我们不用管
用load_state_dict来overwrite掉我们初始化的参数
clone = MLP()
# load():从本地文件读取参数;load_state_dict():把参数加载到模型
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)
)