那麼好,我們今天來看一看優化器和參數更新

在進入這一部分之前,我覺得有必要說明自動求導和反向傳播的數學定義以及在編程中實現的原理, 在網上找到了這一篇文章覺得很不錯,

【Autograd】深入理解BP與自動求導?

okcd00.oschina.io
圖標

前文中說到,backward會求出loss的導數,這裡補充一個上一篇文章中沒有提到的點:

在engine求導過程中,所有的節點都會依次求出導數,對於之前的參數層,則會求出梯度向量(或者說Jacobian矩陣,因為目標函數是一個標量),這是一個關於參數的一階導數矩陣(自變數為輸入的數據,輸出為圖的輸出)。 然後計算出hessian矩陣(的一部分,完整的hessian矩陣無法被計算並表述),這是一個關於參數的二階偏導數矩陣,得到的數值將會被放進Variable.grad.data中,接下來,我們回到python。

參數獲取(nn.Module.parameters())

parameters方法會獲得當前的參數列表,它將是一個迭代器,包含了你定義的模塊中的所有的參數, 而print parmeters對象則會給你返回字元串樣式的提示

In[2]: model.parameters
Out[2]:
<bound method Module.parameters of Net(
(conv1): Conv2d(1, 20, kernel_size=(5, 5), stride=(1, 1))
(conv2): Conv2d(20, 50, kernel_size=(5, 5), stride=(1, 1))
(fc1): Linear(in_features=800, out_features=500, bias=True)
(fc2): Linear(in_features=500, out_features=10, bias=True)
)>
In[3]: model.parameters()
Out[3]: <generator object Module.parameters at 0x0000019F23B80678>

具體實現邏輯如下:

def parameters(self, recurse=True):
for name, param in self.named_parameters(recurse=recurse):
yield param

def named_parameters(self, prefix=, recurse=True):
gen = self._named_members(
lambda module: module._parameters.items(),
prefix=prefix, recurse=recurse)
for elem in gen:
yield elem

def _named_members(self, get_members_fn, prefix=, recurse=True):
r"""Helper method for yielding various names + members of modules."""
memo = set()
modules = self.named_modules(prefix=prefix) if recurse else [(prefix, self)]
for module_prefix, module in modules:
members = get_members_fn(module)
for k, v in members:
if v is None or v in memo:
continue
memo.add(v)
name = module_prefix + (. if module_prefix else ) + k
yield name, v

邏輯很簡單,之前也有一個add_modules方法,這裡我沒有貼出來,這裡完成了模型的參數註冊,說到參數,我覺得可以多說一句, conv的源代碼中有專門對參數的描寫,而且也有專門的Parameter類,這裡附一下conv的parameters實現和類的實現

if transposed:
self.weight = Parameter(torch.Tensor(
in_channels, out_channels // groups, *kernel_size))
else:
self.weight = Parameter(torch.Tensor(
out_channels, in_channels // groups, *kernel_size))
if bias:
self.bias = Parameter(torch.Tensor(out_channels))
else:
self.register_parameter(bias, None)
self.reset_parameters()

# 可以看到,這裡的參數使用了Parameter類,我們自己寫一些操作需要參數的時候,可以直接使用這個類,免去了註冊之類的麻煩。

class Parameter(torch.Tensor):
def __new__(cls, data=None, requires_grad=True):
if data is None:
data = torch.Tensor()
return torch.Tensor._make_subclass(cls, data, requires_grad)

def __deepcopy__(self, memo):
if id(self) in memo:
return memo[id(self)]
else:
result = type(self)(self.data.clone(), self.requires_grad)
memo[id(self)] = result
return result

def __repr__(self):
return Parameter containing:
+ super(Parameter, self).__repr__()

def __reduce_ex__(self, proto):
# See Note [Dont serialize hooks]
return (
torch._utils._rebuild_parameter,
(self.data, self.requires_grad, OrderedDict())
)

# 這個類的實現邏輯還是非常pythonic的,全部使用的魔術方法(運算符重載),雖然說邏輯並不難,就是註冊了一些需要求導的張量罷了。

ok,我們現在知道優化器裡面輸入的是什麼了,接下來,我們說一會數學:

參數更新的數學原理

首先,需要明確的是,pt的求導是對實值執行求導而不是對表達式進行求導,這一點的差別會產生誤差的問題,這是為了動態性而做的犧牲,不過,這樣也帶來了一個好處, 在計算參數應該下降的數值時,可以直接用 
abla p 	imes lr (p為參數矩陣,lr為學習率)進行近似,畢竟在這個學習率的尺度下可以做一個線性近似而不用做積分(或者對原始函數帶入後求值),即 p leftarrow p + (
abla p 	imes lr) 。 因為這兩點,pt與tf的實現上的差別還是蠻大的,tf是對整張圖求出形式導數,然後再帶入數值,但是,這樣就只適用於靜態圖了,不過,快是真的快,但是犧牲了靈活性。

值得一提的是: 有很多人都說同樣一個網路,pt和tf訓練的結果有區別,其實這裡就是導致區別的一個點。另外還有一個點則是初始參數的問題,不過並不是主要的。

mnist中,優化器用的是SGD,這裡面也包括了動量之類的一系列的東西,具體公式再官網文檔裡面有寫,這裡就不在贅述了,計算之後的參數會直接更新在之前的參數向量 中,到這裡,一次迭代就全部完成了。

CUDA

接下來說一點額外的把,就是cuda和cudnn,關於並行計算的,其實在前面的forward計算中,pt會直接調用c的代碼,如果你在之前指定過model.cuda(),那麼這個時候就會 直接調用cuda或者cudnn的代碼。

我們之前寫pt的c或者cuda代碼的時候,通常都是轉換成numpy或者py的列表,然後通過swig轉換為c的數據類型,不過,看了源碼發現,pt有自己的cuda tensor實現,可以直接include相關的頭文件,會省事很多,不過這個是一個大坑,後一篇文章再講吧,我們下期再見吧。

推薦閱讀:

相關文章