对教材《深度学习框架pytorch:入门与实践》和一些技术博客及实践过程中的总结,比较精炼。
网络模型定义
一般通过继承pytoch实现的torch.nn.Module
(以下简称Module),然后定义自己的网络的每层。
这里我们先通过继承Module
构建一个抽象的网络BasicModule
实现了对Module
中.save/.load
的重载,方便保存和加载已训练完毕的模型(持久化
)。
class BasicModule(t.nn.Module):
def __init__(self):
super(BasicModule, self).__init__() # 初始化父类
self.model_name = 'model' #
print(self.model_name)
def load(self, path):
self.load_state_dict(t.load(path))
def save(self, name=None):
if name is None:
abspath = os.getcwd()
prefix = abspath + '/data/checkpoint/' + self.model_name + '_'
name = time.strftime(prefix + '%m%d_%H:%M:%S.pth')
else:
abspath = os.getcwd()
prefix = abspath + '/data/checkpoint/' + name + '_'
name = time.strftime(prefix + '%m%d_%H:%M:%S.pth')
t.save(self.state_dict(), name)
return name
以上是对模型Module
的持久化,对于tensor的持久化只需要t.load(obj,filename)/t.save(obj,filename)
。而对于Module和Optimizer
建议保存其state_dict()
(以上就是这么做的)而不是直接保存整个模型或优化器,可以保存其参数和动量信息。
基于以上的BasicModule
实现自己的Module就可以具有加载和保存参数的功能
class BriefNet(BM.BasicModule):
def __init__(self):
super(BriefNet, self).__init__()
# nn.ReLU 和 functional.relu的差别(大部分nn.layer 都对应一个nn.functional中的函数):
# 前者是nn.Module的子类,后者是nn.functional的子类
# nn.中许多layer和激活函数都是继承与nn.Module的可以自动提取派生类中的可学习参数(通过继承Module实现的__getattr__和__setattr__)
# functional是没有可学习参数的,可以用在激活函数、池化等地方
self.fc = nn.Sequential(
nn.Linear(784, 256), # 数据集图片为28*28单通道,此处直接用多层感知机而不是用CNN
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(256, 128),
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(128, 64),
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(64, 10),
nn.LogSoftmax(dim=1)
)
def forward(self, x):
# 定义前向传播函数
x = x.view(x.shape[0], -1) # 将输入图片变成列向量输入
x = self.fc(x)
return x
定义起来还是很简单的,网络定义有两种比较常见的格式(无性能差别)
一种是使用
nn.Sequantial
(推荐),其也是Module的子类,相对于ModuleList
而言可以直接输入如 self.fc(x)
。nn.Sequantial
中定义了一些列的nn.Module
的子类如:Linear(全连接、仿射层)、ReLu(激活函数)、Dropout、SoftMax、Conv
等常用的layers,使用Sequential
可以很方便的把这些layer的输入输出自动连接起来,自己来定义layer间的组合而得到一个复合layer(代码结构意义上的一个,只需要给定整个sequential一个输入,就能得出一个输出,而不需要一层一层的输入输出和第二种方法有差别),这样的好处是定义简洁、且通过对本model的控制如models.train()、models.eval()
就可以自动的设置其内部那些训练和验证具有不同表现
的layer所处的状态或model.zero_grad()实现对所有叶节点参数梯度清空操作
。更有一体感,方便宏观掌控。-
另一种是通过在
__init__
中自己声明一些nn中实现的layer
的实例属性(相较于之前这是比较分散的)。class Classifier(nn.Module): def __init__(self): super().__init__() self.fc1 = nn.Linear(784, 256) self.fc2 = nn.Linear(256, 128) self.fc3 = nn.Linear(128, 64) self.fc4 = nn.Linear(64, 10) def forward(self, x): # make sure input tensor is flattened x = x.view(x.shape[0], -1) x = F.relu(self.fc1(x)) x = F.relu(self.fc2(x)) x = F.relu(self.fc3(x)) x = F.log_softmax(self.fc4(x), dim=1) return x
nn
中实现的大多数layers在nn.functional
中也有对应的实现(一般为全小写函数如nn.functinal.relu、softmax、F.conv
)。介绍到这,注意一下代码中注释提示:用Module实现的子类layer能自动提取内部的类型为nn.parameter
可学习参数(需要变化的参数),而nn.functional
中的函数更像是纯函数调用时必须给输入如F.relu(input)、F.linear(input,model.weight,model.bias)
。且functional这种纯函数的用法更适合那些没有可学习参数的层如pooling、ReLU、sigmoid、tanh
,他们参数都是固定的,不需要输入需要学习的参数到functional.xxx()
的输入中,可以保持完整感。但是对于dropout
建议使用Module,否则需要手动给出当前训练状态F.dropout(input,training=self.training)
。 然后就是实现
forward
。给网络一个输入即output=model(input)
其内部会调用model.__call__
(nn.Module实现),call中主要是调用了forward
。forward主要负责计算图的构建,然后我们就能根据计算图进行自动求导计算。-
这里说一下以上内容的实现原理:
为何
nn.Module
可以自动提取子类内部的可学习参数:
因为nn.Module
中实现了两个魔法方法.__getattr__和.__setattr__
。而obj.name=value 等价于setattr(obj,'name',value)
和obj.name 等价于 getattr(obj,'name')
,当定义了自定义方法后getattr和setattr
会调用自定义的.__getattr__和.__setattr__
。然后,当用户在子类定义实例属性时候就会触发.__setattr__(obj,'name',value)
,在此函数中会判断obj
类型是否是Parameter或nn.Module
对象,如果是就存放到_parameters和_models
两个私有字典中;如果是其他对象,则调用默认操作:添加到__dict__
字典中。而_models
中的都是子models,他们在定义的时候也会在其内部的._models
字典中存放其子models。最终可能是会对本自定义models内的所有子models(含迭代)的parameter进行一些管理。是如何实现自动求导的:
在这之前先了解一下torch的一些数据结构:nn.Parameter、autograd.variable、torch.tensor
。
a. 其中tensor
是numpy中array类似的多维数组。
b.variable
是以tensor作为核心data对象并进行了一些 扩充(如grad梯度值(目标函数对本变量的偏导,也是variable类)、grad_fn梯度计算函数、requires_grad是否需要自动求导功能
),使其能进行自动求导功能。且model(网络模型)的输入都是以variable形式输入的。如果数据集经过变换to_tensor
在输入前还需要input=Variable(input)
。注释:autograd只实现了标量目标函数对叶节点求导
,对目标函数进行backward()
后叶节点变量x
就可以得到目标函数对x偏导x.grad
,但是叶节点有标记AccumulateGrad
会自动累加历次backward的梯度值。因此调用反向传播时需要清零梯度x.grad.data.zero_()inplace操作清零
。
c.Parameter
是variable
的子类,且默认是requires_grad=True
。能在定义Model时候自动被基类nn.Module
管理。-
什么是计算图(pytorch是动态图):计算图是一种
有向无环图
且图中有两种节点:- 一种是○表示的变量(variable);
- 另一种是□表示的算子(如已实现重载的
+、-、*、/、**n等
或.sum()、t.exp等已实现函数
或基于Function类自定义运算符
)。
其中,用户声明的
Variable
为叶节点(需要requires_grad的)是依赖于其他变量了;其他节点都是通过对叶节点及其运算结果进行复合运算得到的一些中间Variable
和最终的目标函数。我们定义用户Variable
,拿他们
或他们之间运算结果
做运算时,会自动根据重载或以实现的运算函数(都是算子)进行边的连接(输入输出关系,是有向的)(并在算子内储存一份输入输出拷贝用来梯度计算时使用),然后生成一个图。图中任意一点都可以利用链式法则实现!!因此当对requires_grad
的叶子节点求导时,中间变量Variable
都会自动.requires_grad=True
,但非叶子节点计算完后会清空梯度。此外是利用以下几个规则来进行链式法则的:当y是标量的时候可以进行y.backward()
,但是y是向量(认为是中间variable)其求梯度必须输入一个参数y.backward(grad_y)
否则会提示只能进行标量对向量求梯度,其中grad_y=标量目标函数z对向量y求偏导
。 -
目前大多数算子都可以使用autograd的反向求导,自己写的复杂函数没有,如何解决:自己实现的复杂函数(算子)不具备自动求导功能(
比如y=x**2+x*2支持,但y=func(x)的func不支持
)。可以利用autograd.Function
对autograd功能进行拓展,其中Function
对应计算图中的□。需要自己实现一个继承与autograd.Function的算子,并自己实现求导。这样就可以在计算图使用它并使得链式法则进行下去了。其中forward
输入输出都是tensor
,而backward
的输入和输出都是variable
。backward函数的输入对应forwad的输出,backward的输出对应forward的输入。然后用Function.apply(variable)
即可调用实现的Function。如z=func(V(...),V(...),...)
会自动调用forward并提取Variable中的data(tensor类型)作为输入然后把输出的tensor包装成Variable,z.backward()
就会自动调用func.backward()
class func(Function): @staticmethod def forward(ctx,input1,input2,...): ctx.save_for_backward(input1,input2,...,) output = # some opertation with implemently factor return output @staticmethod def backward(ctx,grad_z_input) #grad_z_input对应y.backward()中输入的前链dz/dy input1,input2,... = ctx.saved_variables grad_input1_x = grad_z_input * (...) grad_input2_x = grad_z_input * (...) return grad_input1_x,grad_input2_x,...
数据集加载
训练
训练除了需要数据集,还需要损失函数/目标函数、优化器。其中损失函数是标量函数,将网络的输出(多维向量)进行某种评估得分。我们训练的方法就是BP即通过求取目标函数对输入(叶节点)的梯度,然后通过优化器来求去网络可学习参数的变化量的。
损失函数
torch.nn
中实现了许多损失函数如:NLLLoss、CrossEntropyLoss
等常用损失函数。
如果网络训练需要用GPU则需要使用对应的损失函数即.cuda()。因为为了用GPU进行训练,网络参数在GPU上、输入也需要在GPU上才能进行前向传播、网络输出的向量也是在GPU上的,为了评估输出,损失函数计算时候的参数也是GPU上的才能和网络输出进行运算,因此损失函数求出的score也是GPU上的。
if option.use_gpu:
criterion = t.nn.NLLLoss().cuda()
else:
criterion = t.nn.NLLLoss()
优化器
优化器根据criterion.backward()
得到model.parameters().grad
。然后得到的梯度进行一些优化策略从而计算出网络中可学习参数的更新值。并能通过model.zero_grad()或optimizer.zero_grad()
清空对叶节点(网络可学习参数)梯度。
torch.optim
中实现了许多常用的优化器如:SGD(根据参数觉得是否用动量)、Adam...
这里注意:学习率和weight_decay(可能导致学习率为0)要小心设计否则优化结果导致训练发散或不变。
optimizer = optim.SGD(model.parameters(), lr=option.lr, momentum=0.8) # , weight_decay=option.lr_decay) # 慎用,
训练
网络设置为训练模式 -> 梯度清零 -> 输入网络(Variable输入) -> 根据实际值和输出值计算损失函数输出评分 -> 求评分对网络可学习参数梯度 -> 优化器更新网络可学习参数
model.train()
optimizer.zero_grad()
ps = model(x)
loss = criterion(ps, target)
loss.backward()
optimizer.step()
训练完需要model.save(path)
保存网络
测试
需要先model.load(path)加载训练后的参数
把网络设置成测试模型 -> 输入 -> (optional:计算损失函数 -> ) 获得输出 -> 输出和已知标签对比 -> 计算准确度
model.eval()
ps = model(val_input)
test_loss = criterion(ps, val_labels)
top_p, top_class = t.exp(ps).topk(1, dim=1)
equals = top_class == val_labels.view(*top_class.shape)
accuracy += t.mean(equals.type(t.FloatTensor))
结语
至此,就简要的记完了这几天学习pytorch的结果了。