目錄
文章目錄
- 目錄
- 前言
- 一、構造一個極簡網路
- 二、初始化部分介紹
- 三、添加一個卷積層的簡單網路
- 四、通過簡單網路洞察nn.Module的其他屬性和方法
- 總結
前言
一、構造一個極簡網路
import torch
import torch.nn as nn
from collections.abc import Iterable, Iterator
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
def forward(self,x):
...
if __name__ == '__main__':
net = Net()
? 首先Net繼承自nn.Module,通過super(python中的超類)完成父類的初始化,
二、初始化部分介紹
? 在nn.Module()代碼中,完成初始化方法通過python的魔法函式__setattr__完成,簡單介紹下該魔法函式:setattr (object,name,value)用于設定當前物件(object)的屬性(name)值(value),當然,name屬性不一定存在,簡單有個概念即可,不理解魔法函式也沒關系,
? 看下nn.Module中初始化部分的代碼,
class Module:
def __init__(self):
"""
Initializes internal Module state, shared by both nn.Module and ScriptModule.
"""
# 初始化的屬性
self.training = True
self._parameters = OrderedDict() # 存盤模型引數,參與BP
self._buffers = OrderedDict() # 中間變數,不參與BP
self._non_persistent_buffers_set = set()
self._backward_hooks = OrderedDict()
self._forward_hooks = OrderedDict()
self._forward_pre_hooks = OrderedDict()
self._state_dict_hooks = OrderedDict()
self._load_state_dict_pre_hooks = OrderedDict()
self._modules = OrderedDict() # 添加模塊:比如conv/bn等
def __setattr__(self, name: str, value: Union[Tensor, 'Module']) -> None:
def remove_from(*dicts_or_sets):
for d in dicts_or_sets:
if name in d:
if isinstance(d, dict):
del d[name]
else:
d.discard(name)
params = self.__dict__.get('_parameters')
if isinstance(value, Parameter):
if params is None:
raise AttributeError(
"cannot assign parameters before Module.__init__() call")
remove_from(self.__dict__, self._buffers, self._modules, self._non_persistent_buffers_set)
self.register_parameter(name, value) #注冊進self._parameters
elif params is not None and name in params:
if value is not None:
raise TypeError("cannot assign '{}' as parameter '{}' "
"(torch.nn.Parameter or None expected)"
.format(torch.typename(value), name))
self.register_parameter(name, value) #注冊進self._parameters
else:
modules = self.__dict__.get('_modules')
if isinstance(value, Module):
if modules is None:
raise AttributeError(
"cannot assign module before Module.__init__() call")
remove_from(self.__dict__, self._parameters, self._buffers, self._non_persistent_buffers_set)
modules[name] = value # 注冊進modules
elif modules is not None and name in modules:
if value is not None:
raise TypeError("cannot assign '{}' as child module '{}' "
"(torch.nn.Module or None expected)"
.format(torch.typename(value), name))
modules[name] = value # 注冊進modules
else:
buffers = self.__dict__.get('_buffers')
if buffers is not None and name in buffers:
if value is not None and not isinstance(value, torch.Tensor):
raise TypeError("cannot assign '{}' as buffer '{}' "
"(torch.Tensor or None expected)"
.format(torch.typename(value), name))
buffers[name] = value # 注冊進buffers
else:
object.__setattr__(self, name, value)
? 這里簡單介紹下流程:以完成“self._parameters()屬性”注冊為例,當程式遇見self._parameters() 屬性時,將自動執行def setattr(self,name,value)函式:此處self表示當前類本身,在初始化中,name = self._parameters(); value =OrderDict(); 之后在setattr中,會依次判斷當前屬性name是否是Parameter類/Module類/buffer,由于當前OrderDict都不是,因此,執行最底下代碼:object.setattr(self,name,value)完成屬性 self._parameters()的添加,
? 由于本代碼中初始化均是OrderDict(),因此,實質上在初始化這個極簡網路時,均是執行了 最后一行代碼,即object.setattr(self,name,value),
三、添加一個卷積層的簡單網路
?在上一節中主要介紹了通過super借助__setattr__完成了怎樣的初始化,假如現在添加一個卷積層,初始化部分會有何不同呢?
import torch
import torch.nn as nn
from collections.abc import Iterable, Iterator
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.lele= nn.Conv2d(1, 1, 1, 1, 0)
def forward(self,x):
...
if __name__ == '__main__':
net = Net()
?在第1小節中,實質上執行完了super(Net,self).init()陳述句,現在程式添加了一個卷積層self.lele=nn.Conv2d(1,1,1,1,0):當然,本節我們只關注Module的學習,并不關注卷積層是如何初始化的,即Conv2d類如何創建本文不討論,本文默認已經實體了一個self.lele物件,現在核心是:如何將這個self.lele物件添加進Module初始化中呢?由于往類中添加了新的屬性,因此,首先執行__setattr__函式,object = self , name=lele, value= nn.Conv2d類,
觀察此時函式內部的運行情況,
?這里簡單介紹下nn.Conv2d類,在pytorch中,Conv2d類繼承自_ConvNd類,而_ConvNd類繼承自nn.Module(); _ConvNd主要完成卷積核的一些初始化引數作業,而具體到Conv2d/Conv1d類時,主要負責前向傳播的運算任務,
?Okay,言歸正傳,在執行到self.lele=nn.Conv2d(1,1,1,1,0)這句代碼時:首先肯定完成卷積初始化作業,即完成_ConvNd中引數作業,以Conv2d中in_channel引數為例,首先也是通過__setattr__方法判斷in_channel是否是Parameter/Module/Buffer,肯定均不屬于,因此,同樣也是執行object.setattr(self,name,value)這句代碼,即將in_channel這種引數往卷積核類中添加了in_channel屬性,
?當然,_ConvNd中還有好多引數:同樣采用上段中的方法完成了引數注冊:
in_channels: int,
out_channels: int,
kernel_size: _size_1_t,
stride: _size_1_t,
padding: _size_1_t,
dilation: _size_1_t,
transposed: bool,
output_padding: _size_1_t,
groups: int,
bias: Optional[Tensor],
padding_mode: str) -> None:
?Conv2d中有weights(卷積核權重)和bias(卷積核偏置)兩個Parameter類,即Conv2d中待學習的引數有兩個weights和bias,在torch原始碼中長這樣:
那么,如何將這兩個Parameter類注冊進self._parameters()?
同理:在__setattr__函式中:
def __setattr__(self, name: str, value: Union[Tensor, 'Module']) -> None:
...
if isinstance(value, Parameter): #判斷若value是Parameter類
self.register_parameter(name, value) # 則注冊進self._parameter
?簡單說就是判斷下value的型別,若為Parameter,則注冊進_parameters(),這里在看下register_parameter()函式代碼:
def register_parameter(self, name: str, param: Optional[Parameter]) -> None:
r"""Adds a parameter to the module.
The parameter can be accessed as an attribute using given name.
Args:
name (string): name of the parameter. The parameter can be accessed
from this module using the given name
param (Parameter): parameter to be added to the module.
"""
if '_parameters' not in self.__dict__:
raise AttributeError(
"cannot assign parameter before Module.__init__() call")
elif not isinstance(name, torch._six.string_classes):
raise TypeError("parameter name should be a string. "
"Got {}".format(torch.typename(name)))
elif '.' in name:
raise KeyError("parameter name can't contain \".\"")
elif name == '':
raise KeyError("parameter name can't be empty string \"\"")
elif hasattr(self, name) and name not in self._parameters:
raise KeyError("attribute '{}' already exists".format(name))
if param is None:
self._parameters[name] = None
elif not isinstance(param, Parameter):
raise TypeError("cannot assign '{}' object to parameter '{}' "
"(torch.nn.Parameter or None required)"
.format(torch.typename(param), name))
elif param.grad_fn:
raise ValueError(
"Cannot assign non-leaf Tensor to parameter '{0}'. Model "
"parameters must be created explicitly. To express '{0}' "
"as a function of another Tensor, compute the value in "
"the forward() method.".format(name))
else:
self._parameters[name] = param
?從上述代碼可以看出:實質上就是判斷Parameter是否符合各種規范,比如命名啥的,若全都符合,則執行最后一句代碼,完成注冊,
?當然:理解完上述完全ok,還有一個細節涉及到python的又一個高級操作:就是標黃的部分:hasattr,python用來判斷物件是否具有某個屬性,在pytorch中重寫了這個魔法方法:即hasattr實質上跳轉到了__getattr__中,看下__getattr__(object_name,name)—用來獲取物件的屬性:
def __getattr__(self, name: str) -> Union[Tensor, 'Module']:
if '_parameters' in self.__dict__:
_parameters = self.__dict__['_parameters']
if name in _parameters:
return _parameters[name]
if '_buffers' in self.__dict__:
_buffers = self.__dict__['_buffers']
if name in _buffers:
return _buffers[name]
if '_modules' in self.__dict__:
modules = self.__dict__['_modules']
if name in modules:
return modules[name]
raise ModuleAttributeError("'{}' object has no attribute '{}'".format(
type(self).__name__, name))
?邏輯也比較簡單:若具有這個屬性,則通過字典進行回傳:_parameters[name],通過上述方式,就完成了Conv2d中weights和bias兩個Parameter類的注冊,同時也完成了Conv2d中所有引數的注冊,
?上述實質上僅僅完成了Conv2d中引數的初始化,現在,實質上實體化出了一個名為lele的卷積核物件,屬于一個Module類,因此,還需要通過__setattr__將lele這個Module類注冊進self._modules(),具體通過:
def add_module(self, name: str, module: Optional['Module']) -> None:
r"""Adds a child module to the current module.
The module can be accessed as an attribute using the given name.
Args:
name (string): name of the child module. The child module can be
accessed from this module using the given name
module (Module): child module to be added to the module.
"""
if not isinstance(module, Module) and module is not None:
raise TypeError("{} is not a Module subclass".format(
torch.typename(module)))
elif not isinstance(name, torch._six.string_classes):
raise TypeError("module name should be a string. Got {}".format(
torch.typename(name)))
elif hasattr(self, name) and name not in self._modules:
raise KeyError("attribute '{}' already exists".format(name))
elif '.' in name:
raise KeyError("module name can't contain \".\"")
elif name == '':
raise KeyError("module name can't be empty string \"\"")
self._modules[name] = module
?類似于注冊register_parameter,此出不在贅述,
?對于lele來說:因為lele從nn.Conv2d實體化的物件而來,所以,lele作為一個module(容器)包裹了待優化的引數(weights,bias),即可以通過module._parameters查看待優化的引數,
四、通過簡單網路洞察nn.Module的其他屬性和方法
?上述其實已經完成了一個網路的構建,只要實作forward方法,就能運行了,但是在現有大多數pytorch參考書中:會告訴你諸如“呼叫.children()方法”,我就奇了怪了,咋呼叫的?因此,本節主要分析nn.Module中如何實作查看一個網路的module,parameter,buffer等,
?截取nn.Module中查看module等屬性的代碼:還是簡單網路為例,
def parameters(self, recurse: bool = True) -> Iterator[Parameter]:
for name, param in self.named_parameters(recurse=recurse):
yield param
def named_parameters(self, prefix: str = '', recurse: bool = True) -> Iterator[Tuple[str, Tensor]]:
gen = self._named_members(
lambda module: module._parameters.items(),
prefix=prefix, recurse=recurse)
for elem in gen:
yield elem
?上述代碼用來查看parameters,通過yield回傳了一個生成器,生成器不理解請看另一篇博客,就是列印每個模塊的引數,
?同時,nn.Module還提供了查看其它引數的方法,代碼類似,
def buffers(self, recurse: bool = True) -> Iterator[Tensor]:
for name, buf in self.named_buffers(recurse=recurse):
yield buf
def named_buffers(self, prefix: str = '', recurse: bool = True) -> Iterator[Tuple[str, Tensor]]:
gen = self._named_members(
lambda module: module._buffers.items(),
prefix=prefix, recurse=recurse)
for elem in gen:
yield elem
def children(self) -> Iterator['Module']:
for name, module in self.named_children():
yield module
def named_children(self) -> Iterator[Tuple[str, 'Module']]:
memo = set()
for name, module in self._modules.items():
if module is not None and module not in memo:
memo.add(module)
yield name, module
def modules(self) -> Iterator['Module']:
for name, module in self.named_modules():
yield module
def named_modules(self, memo: Optional[Set['Module']] = None, prefix: str = ''):
if memo is None:
memo = set()
if self not in memo:
memo.add(self)
yield prefix, self
for name, module in self._modules.items():
if module is None:
continue
submodule_prefix = prefix + ('.' if prefix else '') + name
for m in module.named_modules(memo, submodule_prefix):
yield m
?上述全部都是生成器,下面我給出一個如何使用這些方法的示例代碼(寫了我好久,太麻煩了,,,):
# -*- coding: utf-8 -*-
# ======================================================
# @Time : 2021/01/04
# @Author : lele wu
# @Email : 2541612007@qq.com
# @File : lele_module.py
# @Comment: 研究nn.Module屬性和方法
# ======================================================
import torch
import torch.nn as nn
from collections.abc import Iterable, Iterator
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.lele = nn.Conv2d(1, 1, 1, 1, 0)
#self.bn = nn.BatchNorm2d(1)
#self.relu = nn.ReLU(inplace=True)
def forward(self,x):
#out = self.conv(x)
#out = self.bn(out)
#out = self.relu(out)
#return out
...
if __name__ == '__main__':
# input = torch.Tensor([[[[-1,0],[1,2]]]])
net = Net()
# 查看網路的子模塊
print('net.children回傳一個迭代器嗎?:', isinstance(net.children(), Iterator)) # 因為使用了yeild生成器運算式,故是生成器
print('***********************************************************************************************************')
print('通過呼叫children()方法:')
for module in net.children():
print('當前module類為:',module)
print('***********************************************************************************************************')
print('通過呼叫named_children()方法:')
for name,module in net.named_children():
print('類名:',name,'運算模塊:',module) # name: 就是自己定義網路中 conv,bn,bn
print('***********************************************************************************************************')
print('通過呼叫modules()方法:')
for module in net.modules():
print('當前module類為:',module)
print('***********************************************************************************************************')
print('通過呼叫named_modules()方法:')
for module in net.named_modules():
print('當前module類為:',module)
print('***********************************************************************************************************')
print('總結: named_modules或者modules方法遞回呼叫所有子類;而children和named_children方法則僅呼叫第一層子類:\n'
'參考資料: https://zhuanlan.zhihu.com/p/65105409?utm_source=wechat_session')
print('***********************************************************************************************************')
print('通過呼叫buffers()方法:')
for buffer in net.buffers():
print('當前buffer為:',buffer)
print('***********************************************************************************************************')
print('通過呼叫named_buffers()方法:')
for buffer in net.named_buffers():
print('當前buffer(元祖)為:',buffer)
print('***********************************************************************************************************')
?此處我放張運行效果圖得了:

?當然,由于僅僅有一個卷積核,因此buffers為空,即卷積核在訓練程序中不產生快取,
總結
? 這是第一篇,寫的有點兒多,后續會介紹hook,apply等,當然,下節會首先介紹如何凍結引數,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/247592.html
標籤:AI
