深度學(xué)習(xí)框架PyTorch入門與實(shí)踐:第四章 神經(jīng)網(wǎng)絡(luò)工具箱nn

autograd實(shí)現(xiàn)了自動微分系統(tǒng)壶硅,然而對深度學(xué)習(xí)來說過于底層谁不,本章將介紹的nn模塊坐梯,是構(gòu)建于autograd之上的神經(jīng)網(wǎng)絡(luò)模塊。除了nn之外刹帕,我們還會介紹神經(jīng)網(wǎng)絡(luò)中常用的工具吵血,比如優(yōu)化器optim、初始化init等偷溺。

4.1 nn.Module

第3章中提到蹋辅,使用autograd可實(shí)現(xiàn)深度學(xué)習(xí)模型,但其抽象程度較低挫掏,如果用其來實(shí)現(xiàn)深度學(xué)習(xí)模型侦另,則需要編寫的代碼量極大。在這種情況下尉共,torch.nn應(yīng)運(yùn)而生褒傅,其是專門為深度學(xué)習(xí)設(shè)計(jì)的模塊。torch.nn的核心數(shù)據(jù)結(jié)構(gòu)是Module袄友,它是一個(gè)抽象的概念殿托,既可以表示神經(jīng)網(wǎng)絡(luò)中的某個(gè)層(layer),也可以表示一個(gè)包含很多層的神經(jīng)網(wǎng)絡(luò)剧蚣。在實(shí)際使用中支竹,最常見的做法繼承nn.Module,撰寫自己的網(wǎng)絡(luò)/層鸠按。下面先來看看如何使用nn.Module實(shí)現(xiàn)自己的全連接層礼搁。全連接層,又名仿射層目尖,輸入y和輸入x滿足y=Wx+b馒吴,W和b是可學(xué)習(xí)的參數(shù)。

import torch as t
from torch import nn
from torch.autograd import Variable as V

# 定義線性模型:y = w * x + b
class Linear(nn.Module):    # 繼承nn.Module
    def __init__(self,in_features,out_features):
        super(Linear,self).__init__()    # 等價(jià)于nn.Module.__init__(self)
        self.w = nn.Parameter(t.randn(in_features,out_features))
        self.b = nn.Parameter(t.randn(out_features))
        
    def forward(self,x):
        xw = x.mm(self.w)
        y = xw + self.b.expand_as(xw)
        return y

net = Linear(4,3)
x = V(t.randn(2,4))
y = net(x)
y

輸出如下:

tensor([[ 0.2732,  1.8660,  1.3620],
        [ 0.6374, -0.3646,  1.0089]], grad_fn=<AddBackward0>)
for name,parameter in layer.named_parameters():
    print(name,parameter)    # w and b

輸出如下:

w Parameter containing:
tensor([[-0.3146,  2.5440,  0.0063],
        [ 0.6632, -1.5358,  0.1820],
        [-1.5990,  0.7136,  0.2463],
        [-1.8826,  1.4418,  0.7892]], requires_grad=True)
b Parameter containing:
tensor([ 0.1779,  0.2043, -0.3796], requires_grad=True)

可見瑟曲,全連接層的實(shí)現(xiàn)非常簡單饮戳,其代碼量不超過10行,但需注意以下幾點(diǎn):

  • 自定義層Linear必須繼承nn.Module测蹲,并且在其構(gòu)造函數(shù)中需調(diào)用nn.Module的構(gòu)造函數(shù),即super(Linear,self).init()或nn.Module.init(self)鬼吵。
  • 在構(gòu)造函數(shù)init中必須自己定義可學(xué)習(xí)的參數(shù)扣甲,并封裝成Parameter,如在本例中我們把w和b封裝成Parameter。Parameter是一種特殊的Variable琉挖,但其默認(rèn)需要求導(dǎo)(requires_grad=True)启泣,感興趣的讀者可以通過nn.Parameter??查看Parameter類的源代碼。
  • forward函數(shù)實(shí)現(xiàn)前向傳播過程示辈,其輸入可以是一個(gè)或多個(gè)variable寥茫,對x的任何操作也必須是variable支持的操作。
  • 無須寫反向傳播函數(shù)矾麻,因其前向傳播都是對variable進(jìn)行操作纱耻,nn.Module能夠利用autograd自動實(shí)現(xiàn)反向傳播,這一點(diǎn)比Function簡單許多险耀。
  • 使用時(shí)弄喘,直觀上可將net看成數(shù)學(xué)概念中的函數(shù),調(diào)用net(x)即可得到x對應(yīng)的結(jié)果甩牺。它等價(jià)于net.call(x)蘑志,在call函數(shù)中,主要調(diào)用的是net.forward(x)贬派,另外還對鉤子做了一些處理急但。所以在實(shí)際使用中應(yīng)盡量使用net(x)而不是使用net.forward(x),關(guān)于鉤子技術(shù)的具體內(nèi)容將在下文講到搞乏。
  • Module中的可學(xué)習(xí)參數(shù)可以通過named_parameters()或者parameters()返回迭代器波桩,前者會給每個(gè)parameter附上名字,使其更具有辨識度查描。

可見突委,利用Module實(shí)現(xiàn)的全連接層,比利用Function實(shí)現(xiàn)的更簡單冬三,因其不再需要寫反向傳播函數(shù)匀油。

Module能夠自動檢測到自己的parameter,并將其作為學(xué)習(xí)參數(shù)勾笆。除了parameter敌蚜,Module還包含子Module,主Module能夠遞歸查找子Module中的parameter窝爪。下面再來看看稍微復(fù)雜一點(diǎn)的網(wǎng)絡(luò):多層感知機(jī)弛车。

多層感知機(jī)的網(wǎng)絡(luò)結(jié)構(gòu)如圖所示。它由兩個(gè)全連接層組成蒲每,采用sigmoid函數(shù)作為激活函數(shù)(圖中沒有畫出)纷跛。

image.png
class Perceptron(nn.Module):
    def __init__(self,in_features,hidden_features,out_features):
        nn.Module.__init__(self)
        self.layer1 = Linear(in_features,hidden_features)    # 此處的Linear是前面自定義的全連接層
        self.layer2 = Linear(hidden_features,out_features)
        
    def forward(self,x):
        x = self.layer1(x)
        x = t.sigmoid(x)
        x = self.layer2(x)
        return x
        
perceptron = Perceptron(3,4,1)
for name,param in perceptron.named_parameters():
    print(name,param.size())

輸出如下:

layer1.w torch.Size([3, 4])
layer1.b torch.Size([4])
layer2.w torch.Size([4, 1])
layer2.b torch.Size([1])

可見,即使是稍復(fù)雜的多層感知機(jī)邀杏,其實(shí)現(xiàn)依舊很簡單贫奠。這里需要注意以下兩個(gè)知識點(diǎn)唬血。

  • 構(gòu)造函數(shù)init中,可利用前面自定義的Linear層(Module)作為當(dāng)前Module對象的一個(gè)子Module唤崭,它的可學(xué)習(xí)參數(shù)拷恨,也會成為當(dāng)前Module的可學(xué)習(xí)參數(shù)。
  • 在前向傳播函數(shù)中谢肾,我們有意識地將輸出變量都命名為x腕侄,是為了能讓Python回收一些中間層的輸出,從而節(jié)省內(nèi)存芦疏。但并不是所有的中間結(jié)果都會被回收冕杠,有些variable雖然名字被覆蓋,但其在反向傳播時(shí)仍需要用到眯分,此時(shí)Python的內(nèi)存回收模塊將通過檢查引用計(jì)數(shù)拌汇,不會回收這一部分內(nèi)存。

Module中parameter的全局命名規(guī)范如下:

  • Parameter直接命名弊决。例如self.param_name = nn.Parameter(t.randn(3,4))噪舀,命名為param_name。
  • 子Module中的parameter飘诗,會在其名字之前加上當(dāng)前Module的名字与倡。例如self.sub_module = SubModule(),SubModule中有個(gè)parameter的名字也叫作param_name昆稿,那么二者拼接而成的parameter name就是sub_module.param_name纺座。

為了方便用戶使用颓遏,PyTorch實(shí)現(xiàn)了神經(jīng)網(wǎng)絡(luò)中絕大多數(shù)的layer素征,這些layer都繼承于nn.Module,封裝了可學(xué)習(xí)參數(shù)parameter白翻,并實(shí)現(xiàn)了forward函數(shù)喳瓣,且專門針對GPU運(yùn)算進(jìn)行了CuDNN優(yōu)化馋贤,其速度和性能都十分優(yōu)異。本書不準(zhǔn)備對nn.Module中的所有層進(jìn)行詳細(xì)介紹畏陕,具體內(nèi)容讀者可參照官方文檔或在IPython/Jupyter中使用nn.layer?查看配乓。閱讀文檔時(shí)應(yīng)主要關(guān)注以下幾點(diǎn)。

  • 構(gòu)造函數(shù)的參數(shù)惠毁,如nn.Linear(in_features,out_features,bias)犹芹,需關(guān)注這三個(gè)參數(shù)的作用。
  • 屬性鞠绰、可學(xué)習(xí)參數(shù)和子Module腰埂。如nn.Linear中有weight和bias兩個(gè)可學(xué)習(xí)參數(shù),不包含子Module蜈膨。
  • 輸入輸出的形狀屿笼,如nn.Linear的輸入形狀是(N荒给,input_features),輸出形狀為(N刁卜,output_features),N是batch_size曙咽。

這些自定義layer對輸入形狀都有假設(shè):輸入的不是單個(gè)數(shù)據(jù)蛔趴,而是一個(gè)batch。若想輸入一個(gè)數(shù)據(jù)例朱,必須調(diào)用unsqueeze(0)函數(shù)將數(shù)據(jù)偽裝成batch_size=1的batch孝情。

下面將從應(yīng)用層面出發(fā),對一些常用的layer做簡單介紹洒嗤,更詳細(xì)的用法請查看官方文檔箫荡。

4.2 常用的神經(jīng)網(wǎng)絡(luò)層

4.2.1 圖像相關(guān)層

圖像相關(guān)層主要包括卷積層(Conv)、池化層(Pool)等渔隶,這些層在實(shí)際使用中分為一維(1D)羔挡、二維(2D)和三維(3D),池化方式又分為平均池化(AvgPool)间唉、最大值池化(MaxPool)绞灼、自適應(yīng)池化(AdaptiveAvgPool)等。卷積層除了常用的前向卷積外呈野,還有逆卷積(TransposeConv)低矮。下面舉例說明。

from PIL import Image
from torchvision.transforms import ToTensor, ToPILImage
to_tensor = ToTensor() # img -> tensor
to_pil = ToPILImage()
lena = Image.open('imgs/lena.png')
lena

輸出如下:

image.png
# 輸入是一個(gè)batch被冒,batch_size=1
input = to_tensor(lena).unsqueeze(0) 

# 銳化卷積核
kernel = t.ones(3, 3)/-9.
kernel[1][1] = 1
conv = nn.Conv2d(1, 1, (3, 3), 1, bias=False)
conv.weight.data = kernel.view(1, 1, 3, 3)

out = conv(input)
to_pil(out.data.squeeze(0))

處理后的Lena圖如下:

image.png

圖像的卷積操作還有各種變體军掂,有關(guān)各種變體的介紹具體可以參照此處的介紹

池化層可以看成是一種特殊的卷積層昨悼,用來下采樣蝗锥。但池化層沒有可學(xué)習(xí)的參數(shù),其weight是固定的幔戏。

pool = nn.AvgPool2d(2,2)
list(pool.parameters())

輸出如下:

[]
out = pool(input)
to_pil(out.data.squeeze(0))

處理后的Lena圖如下:

image.png

除了卷積層和池化層玛追,深度學(xué)習(xí)中還將常用到以下幾個(gè)層。

  • Linear:全連接層闲延。
  • BatchNorm:批規(guī)范化層痊剖,分為1D、2D和3D垒玲。除了標(biāo)準(zhǔn)的BatchNorm之外陆馁,還有在風(fēng)格遷移中常用到的InstanceNorm層。
  • Dropout:dropout層合愈,用于防止過擬合叮贩,同樣分為1D击狮、2D和3D。

下面通過例子講解它們的使用方法益老。

# 輸入 batch_size=2彪蓬,維度3
input = t.randn(2, 3)
linear = nn.Linear(3, 4)
h = linear(input)
h

輸出:

tensor([[-0.3437,  0.3086,  0.3261, -1.3908],
        [ 0.3508, -0.7137,  0.8659, -0.5121]], grad_fn=<AddmmBackward>)
# 4 channel,初始化標(biāo)準(zhǔn)差為4捺萌,均值為0
bn = nn.BatchNorm1d(4)
bn.weight.data = t.ones(4) * 4
bn.bias.data = t.zeros(4)

bn_out = bn(h)
# 注意輸出的均值和方差
# 方差是標(biāo)準(zhǔn)差的平方档冬,計(jì)算無偏方差分母會減1
# 使用unbiased=False 分母不減1
bn_out.mean(0), bn_out.var(0, unbiased=False)

輸出:

(tensor([ 0.0000e+00,  0.0000e+00, -2.3842e-07,  0.0000e+00],
        grad_fn=<MeanBackward2>),
 tensor([15.9987, 15.9994, 15.9978, 15.9992], grad_fn=<VarBackward1>))
# 每個(gè)元素以0.5的概率舍棄
dropout = nn.Dropout(0.5)
o = dropout(bn_out)
o # 有一半左右的數(shù)變?yōu)?

輸出:

tensor([[-7.9997,  0.0000, -0.0000, -7.9998],
        [ 0.0000, -0.0000,  7.9995,  7.9998]], grad_fn=<MulBackward0>)

以上很多例子中都對Module的屬性直接操作,其大多數(shù)是可學(xué)習(xí)參數(shù)桃纯,一般會隨著學(xué)習(xí)的進(jìn)行而不斷改變酷誓。實(shí)際使用中除非需要使用特殊的初始化,否則應(yīng)盡量不要直接修改這些參數(shù)态坦。

4.2.2 激活函數(shù)

PyTorch實(shí)現(xiàn)了常見的激活函數(shù)盐数,其具體的接口信息可參見官方文檔。這些激活函數(shù)可作為獨(dú)立的layer使用伞梯。這里將介紹最常用的激活函數(shù)ReLU玫氢,其數(shù)學(xué)表達(dá)式為:

ReLU(x) = max(0,x)

relu = nn.ReLU(inplace=True)
input = t.randn(2, 3)
print(input)
output = relu(input)
print(output) # 小于0的都被截?cái)酁?
# 等價(jià)于input.clamp(min=0)

輸出:

tensor([[-1.1303,  0.9884,  1.8299],
        [-1.0804,  1.4700, -1.0847]])
tensor([[0.0000, 0.9884, 1.8299],
        [0.0000, 1.4700, 0.0000]])

ReLU函數(shù)有個(gè)inplace參數(shù),如果設(shè)為True谜诫,它會把輸出直接覆蓋到輸入中琐旁,這樣可以節(jié)省內(nèi)存/顯存。之所以可以覆蓋是因?yàn)樵谟?jì)算ReLU的反向傳播時(shí)猜绣,只需根據(jù)輸出就能推算出反向傳播的梯度灰殴。但是只有少數(shù)的autograd操作支持inplace操作(如variable.sigmoid_()),除非你明確地知道自己在做什么掰邢,否則一般不要使用inplace操作牺陶。在以上例子中,都是將每一層的輸出直接作為下一層的輸入辣之,這種網(wǎng)絡(luò)稱為前饋傳播網(wǎng)絡(luò)(Feedforward Neural Network)掰伸。對于此類網(wǎng)絡(luò),如果每次都寫復(fù)雜的forward函數(shù)會有些麻煩怀估,在此就有兩種簡化方式狮鸭,ModuleList和Sequential。其中Sequential是一個(gè)特殊的Module多搀,它包含幾個(gè)子Module歧蕉,前向傳播時(shí)會將輸入一層接一層地傳遞下去。ModuleList也是一個(gè)特殊的Module康铭,可以包含幾個(gè)子Module惯退,可以像用list一樣使用它,但不能直接把輸入傳給ModuleList从藤。下面我們舉例說明催跪。

# Sequential的三種寫法
net1 = nn.Sequential()
net1.add_module('conv', nn.Conv2d(3, 3, 3))
net1.add_module('batchnorm', nn.BatchNorm2d(3))
net1.add_module('activation_layer', nn.ReLU())

net2 = nn.Sequential(
        nn.Conv2d(3, 3, 3),
        nn.BatchNorm2d(3),
        nn.ReLU()
        )

from collections import OrderedDict
net3= nn.Sequential(OrderedDict([
          ('conv1', nn.Conv2d(3, 3, 3)),
          ('bn1', nn.BatchNorm2d(3)),
          ('relu1', nn.ReLU())
        ]))
print('net1:', net1)
print('net2:', net2)
print('net3:', net3)

輸出:

net1: Sequential(
  (conv): Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1))
  (batchnorm): BatchNorm2d(3, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  (activation_layer): ReLU()
)
net2: Sequential(
  (0): Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1))
  (1): BatchNorm2d(3, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  (2): ReLU()
)
net3: Sequential(
  (conv1): Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1))
  (bn1): BatchNorm2d(3, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
  (relu1): ReLU()
)
# 可根據(jù)名字或序號取出子module
net1.conv, net2[0], net3.conv1

輸出:

(Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1)),
 Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1)),
 Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1)))
input = t.rand(1, 3, 4, 4)
output = net1(input)
output = net2(input)
output = net3(input)
output = net3.relu1(net1.batchnorm(net1.conv(input)))
modellist = nn.ModuleList([nn.Linear(3,4), nn.ReLU(), nn.Linear(4,2)])
input = t.randn(1, 3)
for model in modellist:
    input = model(input)
# 下面會報(bào)錯(cuò),因?yàn)閙odellist沒有實(shí)現(xiàn)forward方法
# output = modelist(input)

看到這里锁蠕,讀者可能會問,為何不直接使用Python中自帶的list懊蒸,而非要多此一舉呢荣倾?這是因?yàn)镸oduleList是Module的子類,當(dāng)在Module中使用它時(shí)骑丸,就能自動識別為子Module逃呼。

下面我們舉例說明。

class MyModule(nn.Module):
    def __init__(self):
        super(MyModule, self).__init__()
        self.list = [nn.Linear(3, 4), nn.ReLU()]
        self.module_list = nn.ModuleList([nn.Conv2d(3, 3, 3), nn.ReLU()])
    def forward(self):
        pass
model = MyModule()
model

輸出:

MyModule(
  (module_list): ModuleList(
    (0): Conv2d(3, 3, kernel_size=(3, 3), stride=(1, 1))
    (1): ReLU()
  )
)
for name, param in model.named_parameters():
    print(name, param.size())

輸出:

module_list.0.weight torch.Size([3, 3, 3, 3])
module_list.0.bias torch.Size([3])

可見者娱,list中的子Module并不能被主Module識別,而ModuleList中的子Module能夠被主Module識別苏揣。這意味著如果用list保存子Module黄鳍,將無法調(diào)整其參數(shù),因其未加入到主Module的參數(shù)中平匈。

除ModuleList之外還有ParameterList框沟,它是一個(gè)可以包含多個(gè)parameter的類list對象。在實(shí)際使用中增炭,使用方式與ModuleList類似忍燥。在構(gòu)造函數(shù)init中用到list、tuple隙姿、dict等對象時(shí)梅垄,一定要思考是否應(yīng)該用ModuleList或ParameterList代替。

4.2.3 循環(huán)神經(jīng)網(wǎng)絡(luò)層

近些年输玷,隨著深度學(xué)習(xí)和自然語言處理的結(jié)合加深队丝,循環(huán)神經(jīng)網(wǎng)絡(luò)(RNN)的使用也越來越多,關(guān)于RNN的基礎(chǔ)知識欲鹏,推薦閱讀colah的文章
入門机久。PyTorch中實(shí)現(xiàn)了如今最常用的三種RNN:RNN(vanilla RNN)、LSTM和GRU赔嚎。此外還有對應(yīng)的三種RNNCell膘盖。

RNN和RNNCell層的區(qū)別在于前者能夠處理整個(gè)序列,而后者一次只處理序列中一個(gè)時(shí)間點(diǎn)的數(shù)據(jù)尤误,前者封裝更完備更易于使用侠畔,后者更具靈活性。RNN層可以通過組合調(diào)用RNNCell來實(shí)現(xiàn)损晤。

t.manual_seed(1000)
# 輸入:batch_size=3践图,序列長度都為2,序列中每個(gè)元素占4維
input = t.randn(2, 3, 4)
# lstm輸入向量4維沉馆,隱藏元3码党,1層
lstm = nn.LSTM(4, 3, 1)
# 初始狀態(tài):1層德崭,batch_size=3,3個(gè)隱藏元
h0 = t.randn(1, 3, 3)
c0 = t.randn(1, 3, 3)
out, hn = lstm(input, (h0, c0))
out

輸出:

tensor([[[-0.3610, -0.1643,  0.1631],
         [-0.0613, -0.4937, -0.1642],
         [ 0.5080, -0.4175,  0.2502]],

        [[-0.0703, -0.0393, -0.0429],
         [ 0.2085, -0.3005, -0.2686],
         [ 0.1482, -0.4728,  0.1425]]], grad_fn=<StackBackward>)
t.manual_seed(1000)
input = t.randn(2, 3, 4)
# 一個(gè)LSTMCell對應(yīng)的層數(shù)只能是一層
lstm = nn.LSTMCell(4, 3)
hx = t.randn(3, 3)
cx = t.randn(3, 3)
out = []
for i_ in input:
    hx, cx=lstm(i_, (hx, cx))
    out.append(hx)
t.stack(out)

輸出:

tensor([[[-0.3610, -0.1643,  0.1631],
         [-0.0613, -0.4937, -0.1642],
         [ 0.5080, -0.4175,  0.2502]],

        [[-0.0703, -0.0393, -0.0429],
         [ 0.2085, -0.3005, -0.2686],
         [ 0.1482, -0.4728,  0.1425]]], grad_fn=<StackBackward>)

詞向量在自然語言中應(yīng)用十分廣泛揖盘,PyTorch同樣提供了Embedding層眉厨。

# 有4個(gè)詞,每個(gè)詞用5維的向量表示
embedding = nn.Embedding(4, 5)
# 可以用預(yù)訓(xùn)練好的詞向量初始化embedding
embedding.weight.data = t.arange(0,20).view(4,5)
input = t.arange(3, 0, -1).long()
output = embedding(input)
output

輸出:

tensor([[15, 16, 17, 18, 19],
        [10, 11, 12, 13, 14],
        [ 5,  6,  7,  8,  9]], grad_fn=<EmbeddingBackward>)
4.2.4 損失函數(shù)

在深度學(xué)習(xí)中藥用到各種各樣的損失函數(shù)(Loss Function)兽狭,這些損失函數(shù)可看作是一種特殊的layer憾股,PyTorch也將這些損失函數(shù)實(shí)現(xiàn)為nn.Module的子類。然而在實(shí)際使用中通常將這些孫淑函數(shù)專門提取出來箕慧,作為獨(dú)立的一部分服球。詳細(xì)的loss使用請參考官方文檔。這里以分類中最常用的交叉熵?fù)p失CrocsEntropyLoss為例講解颠焦。

# batch_size=3斩熊,計(jì)算對應(yīng)每個(gè)類別的分?jǐn)?shù)(只有兩個(gè)類別)
score = t.randn(3, 2)
# 三個(gè)樣本分別屬于1,0伐庭,1類粉渠,label必須是LongTensor
label = t.Tensor([1, 0, 1]).long()

# loss與普通的layer無差異
criterion = nn.CrossEntropyLoss()
loss = criterion(score, label)
loss

輸出:

tensor(0.5944)

4.3 優(yōu)化器

PyTorch將深度學(xué)習(xí)中常用的優(yōu)化方法全部封裝在torch.optim中,其設(shè)計(jì)十分靈活圾另,能夠很方便地?cái)U(kuò)展城自定義的優(yōu)化方法霸株。

所有的優(yōu)化方法都是繼承自類optim.Optimizer,并實(shí)現(xiàn)了自己的優(yōu)化步驟集乔。下面就以最基本的優(yōu)化方法——隨機(jī)梯度下降法(SGD)舉例說明去件。這里需要重點(diǎn)掌握:

  • 優(yōu)化方法的基本使用方法。
  • 如何對模型的不同部分設(shè)置不同的學(xué)習(xí)率扰路。
  • 如何調(diào)整學(xué)習(xí)率箫攀。
# 首先定義一個(gè)LeNet網(wǎng)絡(luò)
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.features = nn.Sequential(
                    nn.Conv2d(3, 6, 5),
                    nn.ReLU(),
                    nn.MaxPool2d(2,2),
                    nn.Conv2d(6, 16, 5),
                    nn.ReLU(),
                    nn.MaxPool2d(2,2)
        )
        self.classifier = nn.Sequential(
            nn.Linear(16 * 5 * 5, 120),
            nn.ReLU(),
            nn.Linear(120, 84),
            nn.ReLU(),
            nn.Linear(84, 10)
        )

    def forward(self, x):
        x = self.features(x)
        x = x.view(-1, 16 * 5 * 5)
        x = self.classifier(x)
        return x

net = Net()
from torch import  optim
optimizer = optim.SGD(params=net.parameters(), lr=1)
optimizer.zero_grad() # 梯度清零,等價(jià)于net.zero_grad()

input = t.randn(1, 3, 32, 32)
output = net(input)
output.backward(output) # fake backward

optimizer.step() # 執(zhí)行優(yōu)化
# 為不同子網(wǎng)絡(luò)設(shè)置不同的學(xué)習(xí)率幼衰,在finetune中經(jīng)常用到
# 如果對某個(gè)參數(shù)不指定學(xué)習(xí)率靴跛,就使用最外層的默認(rèn)學(xué)習(xí)率
optimizer =optim.SGD([
                {'params': net.features.parameters()}, # 學(xué)習(xí)率為1e-5
                {'params': net.classifier.parameters(), 'lr': 1e-2}
            ], lr=1e-5)
optimizer

輸出:

SGD (
Parameter Group 0
    dampening: 0
    lr: 1e-05
    momentum: 0
    nesterov: False
    weight_decay: 0

Parameter Group 1
    dampening: 0
    lr: 0.01
    momentum: 0
    nesterov: False
    weight_decay: 0
)
# 只為兩個(gè)全連接層設(shè)置較大的學(xué)習(xí)率,其余層的學(xué)習(xí)率較小
special_layers = nn.ModuleList([net.classifier[0], net.classifier[3]])
special_layers_params = list(map(id, special_layers.parameters()))
base_params = filter(lambda p: id(p) not in special_layers_params,
                     net.parameters())

optimizer = t.optim.SGD([
            {'params': base_params},
            {'params': special_layers.parameters(), 'lr': 0.01}
        ], lr=0.001 )
optimizer

輸出:

SGD (
Parameter Group 0
    dampening: 0
    lr: 0.001
    momentum: 0
    nesterov: False
    weight_decay: 0

Parameter Group 1
    dampening: 0
    lr: 0.01
    momentum: 0
    nesterov: False
    weight_decay: 0
)

調(diào)整學(xué)習(xí)率主要有兩種做法渡嚣。一種是修改optimizer.param_groups中對應(yīng)的學(xué)習(xí)率梢睛,另一種是新建優(yōu)化器(更簡單也是更推薦的做法),由于optimize十分輕量級识椰,構(gòu)建開銷很小绝葡,故可以構(gòu)建新的optimize。但是新建優(yōu)化器會重新初始化動量等狀態(tài)信息腹鹉,這對使用動量的優(yōu)化器來說(如帶momentum的sgd)藏畅,可能會造成損失函數(shù)在收斂過程中出現(xiàn)震蕩。

# 方法1: 調(diào)整學(xué)習(xí)率,新建一個(gè)optimizer
old_lr = 0.1
optimizer1 =optim.SGD([
                {'params': net.features.parameters()},
                {'params': net.classifier.parameters(), 'lr': old_lr*0.1}
            ], lr=1e-5)
optimizer1

輸出:

SGD (
Parameter Group 0
    dampening: 0
    lr: 1e-05
    momentum: 0
    nesterov: False
    weight_decay: 0

Parameter Group 1
    dampening: 0
    lr: 0.010000000000000002
    momentum: 0
    nesterov: False
    weight_decay: 0
)
# 方法2: 調(diào)整學(xué)習(xí)率, 手動decay, 保存動量
for param_group in optimizer.param_groups:
    param_group['lr'] *= 0.1 # 學(xué)習(xí)率為之前的0.1倍
optimizer

輸出:

SGD (
Parameter Group 0
    dampening: 0
    lr: 0.0001
    momentum: 0
    nesterov: False
    weight_decay: 0

Parameter Group 1
    dampening: 0
    lr: 0.001
    momentum: 0
    nesterov: False
    weight_decay: 0
)

4.4 nn.functional

nn中還有一個(gè)很常用的模塊:nn.functional愉阎。nn中的大多數(shù)layer在functional中都有一個(gè)與之對應(yīng)的函數(shù)绞蹦。nn.functional中的函數(shù)和nn.Module主要區(qū)別在于,用nn.Module實(shí)現(xiàn)的layers是一個(gè)特殊的類榜旦,都是由class Layer(nn.Module)定義幽七,會自動提取科學(xué)系參數(shù);而nn.functional中的函數(shù)更像是純函數(shù)溅呢,由def function(input)定義澡屡。下面舉例說明functional的使用,并對比二者的不同咐旧。

input = t.randn(2, 3)
model = nn.Linear(3, 4)
output1 = model(input)
output2 = nn.functional.linear(input, model.weight, model.bias)
output1 == output2

輸出:

tensor([[1, 1, 1, 1],
        [1, 1, 1, 1]], dtype=torch.uint8)
b = nn.functional.relu(input)
b2 = nn.ReLU()(input)
b == b2

輸出:

tensor([[1, 1, 1],
        [1, 1, 1]], dtype=torch.uint8)

此時(shí)讀者可能會問驶鹉,應(yīng)該什么時(shí)候使用nn.Module,什么時(shí)候使用nn.functional呢铣墨?答案很簡單室埋,如果模型有可學(xué)習(xí)的參數(shù),最好用nn.Module踏兜,否則既可以使用nn.functional也可以使用nn.Module,二者在性能上沒有太大差異八秃,具體的使用取決于個(gè)人的喜好碱妆。如激活函數(shù)(ReLU、sigmoid昔驱、tanh)疹尾,池化(MaxPool)等層由于沒有可學(xué)習(xí)參數(shù),則可以使用對應(yīng)的functional函數(shù)代替骤肛,而對于卷積纳本、全連接等具有可學(xué)習(xí)參數(shù)的網(wǎng)絡(luò)建議使用nn.Module。下面舉例說明腋颠,如何在模型中搭配使用nn.Module和nn.functional繁成。另外雖然dropout操作也沒有可學(xué)習(xí)操作,但建議還是使用nn.Dropout而不是nn.functional.dropout淑玫,因?yàn)閐ropout在訓(xùn)練和測試兩個(gè)階段的行為有所差別巾腕,使用nn.Module對象能夠通過model.eval操作加以區(qū)分。

from torch.nn import functional as F
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        self.conv1 = nn.Conv2d(3, 6, 5)
        self.conv2 = nn.Conv2d(6, 16, 5)
        self.fc1 = nn.Linear(16 * 5 * 5, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)

    def forward(self, x):
        x = F.pool(F.relu(self.conv1(x)), 2)
        x = F.pool(F.relu(self.conv2(x)), 2)
        x = x.view(-1, 16 * 5 * 5)
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x

對于不具備可學(xué)習(xí)參數(shù)的層(激活層絮蒿、池化層等)尊搬,將它們用函數(shù)代替,這樣則可以不用放置在構(gòu)造函數(shù)init中土涝。對于有可學(xué)習(xí)參數(shù)的模塊佛寿,也可以用functional來代替,只不過實(shí)現(xiàn)起來較為繁瑣但壮,需要手動定義參數(shù)parameter冀泻,如前面實(shí)現(xiàn)自定義的全連接層常侣,就可將weight和bias兩個(gè)參數(shù)單獨(dú)拿出來,在構(gòu)造函數(shù)中初始化為parameter腔长。

class MyLinear(nn.Module):
    def __init__(self):
        super(MyLinear, self).__init__()
        self.weight = nn.Parameter(t.randn(3, 4))
        self.bias = nn.Parameter(t.zeros(3))
    def forward(self):
        return F.linear(input, weight, bias)

關(guān)于nn.functional的設(shè)計(jì)初衷袭祟,以及它和nn.Module更多的比較說明,可參看論壇的討論和作者說明捞附。

4.5 初始化策略

在深度學(xué)習(xí)中參數(shù)的初始化十分重要巾乳,良好的初始化能讓模型更快收斂,并達(dá)到更高水平鸟召,而糟糕的初始化則可能使得模型迅速癱瘓胆绊。PyTorch中nn.Module的模塊參數(shù)都采取了較為合理的初始化策略,因此一般不用我們考慮欧募,當(dāng)然我們也可以用自定義初始化去代替系統(tǒng)的默認(rèn)初始化压状。而當(dāng)我們在使用Parameter時(shí),自定義初始化則尤為重要跟继,因t.Tensor()返回的是內(nèi)存中的隨機(jī)數(shù)种冬,很可能會有極大值,這在實(shí)際訓(xùn)練網(wǎng)絡(luò)中會造成溢出或者梯度消失舔糖。PyTorch中nn.init模塊就是專門為初始化而設(shè)計(jì)娱两,如果某種初始化策略nn.init不提供,用戶也可以自己直接初始化金吗。

# 利用nn.init初始化
from torch.nn import init
linear = nn.Linear(3, 4)

t.manual_seed(1)
# 等價(jià)于 linear.weight.data.normal_(0, std)
init.xavier_normal_(linear.weight)

輸出:

Parameter containing:
tensor([[ 0.3535,  0.1427,  0.0330],
        [ 0.3321, -0.2416, -0.0888],
        [-0.8140,  0.2040, -0.5493],
        [-0.3010, -0.4769, -0.0311]], requires_grad=True)
# 直接初始化
import math
t.manual_seed(1)

# xavier初始化的計(jì)算公式
std = math.sqrt(2)/math.sqrt(7.)
linear.weight.data.normal_(0,std)

輸出:

tensor([[ 0.3535,  0.1427,  0.0330],
        [ 0.3321, -0.2416, -0.0888],
        [-0.8140,  0.2040, -0.5493],
        [-0.3010, -0.4769, -0.0311]])
# 對模型的所有參數(shù)進(jìn)行初始化
for name, params in net.named_parameters():
    if name.find('linear') != -1:
        # init linear
        params[0] # weight
        params[1] # bias
    elif name.find('conv') != -1:
        pass
    elif name.find('norm') != -1:
        pass

4.6 nn.Module深入分析

如果想要更深入地理解nn.Module十兢,究其原理是很有必要的。首先來看看nn.Module基類的構(gòu)造函數(shù):

def __init__(self):
    self._parameters = OrderedDict()
    self._modules = OrderedDict()
    self._buffers = OrderedDict()
    self._backward_hooks = OrderedDict()
    self._forward_hooks = OrderedDict()
    self.training = True

其中每個(gè)屬性的解釋如下:

  • _parameters:字典摇庙,保存用戶直接設(shè)置的parameter旱物,self.param1 = nn.Parameter(t.randn(3, 3))會被檢測到,在字典中加入一個(gè)key為'param'卫袒,value為對應(yīng)parameter的item宵呛。而self.submodule = nn.Linear(3, 4)中的parameter則不會存于此。
  • _modules:子module夕凝,通過self.submodel = nn.Linear(3, 4)指定的子module會保存于此烤蜕。
  • _buffers:緩存。如batchnorm使用momentum機(jī)制迹冤,每次前向傳播需用到上一次前向傳播的結(jié)果讽营。
  • _backward_hooks與_forward_hooks:鉤子技術(shù),用來提取中間變量泡徙,類似variable的hook橱鹏。
  • training:BatchNorm與Dropout層在訓(xùn)練階段和測試階段中采取的策略不同,通過判斷training值來決定前向傳播策略。

上述幾個(gè)屬性中莉兰,_parameters挑围、_modules和_buffers這三個(gè)字典中的鍵值,都可以通過self.key方式獲得糖荒,效果等價(jià)于self._parameters['key'].

下面舉例說明杉辙。

class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        # 等價(jià)與self.register_parameter('param1' ,nn.Parameter(t.randn(3, 3)))
        self.param1 = nn.Parameter(t.rand(3, 3))
        self.submodel1 = nn.Linear(3, 4) 
    def forward(self, input):
        x = self.param1.mm(input)
        x = self.submodel1(x)
        return x
net = Net()
net

輸出:

Net(
  (submodel1): Linear(in_features=3, out_features=4, bias=True)
)
net._modules

輸出:

OrderedDict([('submodel1', Linear(in_features=3, out_features=4, bias=True))])
net._parameters

輸出:

OrderedDict([('param1', Parameter containing:
              tensor([[0.3398, 0.5239, 0.7981],
                      [0.7718, 0.0112, 0.8100],
                      [0.6397, 0.9743, 0.8300]], requires_grad=True))])
net.param1 # 等價(jià)于net._parameters['param1']

輸出:

Parameter containing:
tensor([[0.3398, 0.5239, 0.7981],
        [0.7718, 0.0112, 0.8100],
        [0.6397, 0.9743, 0.8300]], requires_grad=True)
for name, param in net.named_parameters():
    print(name, param.size())

輸出:

param1 torch.Size([3, 3])
submodel1.weight torch.Size([4, 3])
submodel1.bias torch.Size([4])
for name, submodel in net.named_modules():
    print(name, submodel)

輸出:

 Net(
  (submodel1): Linear(in_features=3, out_features=4, bias=True)
)
submodel1 Linear(in_features=3, out_features=4, bias=True)
bn = nn.BatchNorm1d(2)
input = t.rand(3, 2)
output = bn(input)
bn._buffers

輸出:

OrderedDict([('running_mean', tensor([0.0514, 0.0749])),
             ('running_var', tensor([0.9116, 0.9068])),
             ('num_batches_tracked', tensor(1))])

nn.Module在實(shí)際使用中可能層層嵌套,一個(gè)module包含若干個(gè)子module捶朵,每一個(gè)子module又包含了更多的子module蜘矢。為方便用戶訪問各個(gè)子module,nn.Module實(shí)現(xiàn)了很多方法综看,如函數(shù)children可以查看直接子module品腹,函數(shù)module可以查看所有的子module(包括當(dāng)前module)。與之相對應(yīng)的還有函數(shù)named_childen和named_modules红碑,其能夠在返回module列表的同時(shí)返回它們的名字舞吭。

x = t.arange(0, 12).view(3, 4).float()
model = nn.Dropout()
# 在訓(xùn)練階段,會有一半左右的數(shù)被隨機(jī)置為0
model(x)

輸出:

tensor([[ 0.,  0.,  0.,  0.],
        [ 8.,  0.,  0., 14.],
        [ 0., 18., 20.,  0.]])
model.training  = False
# 在測試階段析珊,dropout什么都不做
model(x)

輸出:

tensor([[ 0.,  1.,  2.,  3.],
        [ 4.,  5.,  6.,  7.],
        [ 8.,  9., 10., 11.]])

對于batchnorm羡鸥、dropout、instancenorm等在訓(xùn)練和測試階段行為差距巨大的層忠寻,如果在測試時(shí)不將其training值設(shè)為True惧浴,則可能會有很大影響,這在實(shí)際使用中要千萬注意锡溯。雖然可通過直接設(shè)置training屬性赶舆,來將子module設(shè)為train和eval模式哑姚,但這種方式較為繁瑣祭饭,因如果一個(gè)模型具有多個(gè)dropout層,就需要為每個(gè)dropout層指定training屬性叙量。更為推薦的做法是調(diào)用model.train()函數(shù)倡蝙,它會將當(dāng)前module及其子module中的所有training屬性都設(shè)為True,相應(yīng)的绞佩,model.eval()函數(shù)會把training屬性都設(shè)為False寺鸥。

print(net.training, net.submodel1.training)
net.eval()
net.training, net.submodel1.training

輸出:

True True

(False, False)
list(net.named_modules())

輸出:

[('', Net(
    (submodel1): Linear(in_features=3, out_features=4, bias=True)
  )), ('submodel1', Linear(in_features=3, out_features=4, bias=True))]

register_forward_hook與register_backward_hook,這兩個(gè)函數(shù)的功能類似于variable函數(shù)的register_hook品山,可在module前向傳播或反向傳播時(shí)注冊鉤子胆建。每次前向傳播執(zhí)行結(jié)束后會執(zhí)行鉤子函數(shù)(hook)。前向傳播的鉤子函數(shù)具有如下形式:hook(module, input, output) -> None肘交,而反向傳播則具有如下形式:hook(module, grad_input, grad_output) -> Tensor or None笆载。鉤子函數(shù)不應(yīng)修改輸入和輸出,并且在使用后應(yīng)及時(shí)刪除,以避免每次都運(yùn)行鉤子增加運(yùn)行負(fù)載凉驻。鉤子函數(shù)主要用在獲取某些中間結(jié)果的情景腻要,如中間某一層的輸出或某一層的梯度。這些結(jié)果本應(yīng)寫在forward函數(shù)中涝登,但如果在forward函數(shù)中專門加上這些處理雄家,可能會使處理邏輯比較復(fù)雜,這時(shí)候使用鉤子技術(shù)就更合適一些胀滚。下面考慮一種場景趟济,有一個(gè)預(yù)訓(xùn)練好的模型,需要提取模型的某一層(不是最后一層)的輸出作為特征進(jìn)行分類蛛淋,但又不希望修改其原有的模型定義文件咙好,這時(shí)就可以利用鉤子函數(shù)。下面給出實(shí)現(xiàn)的偽代碼褐荷。

model = VGG()
features = t.Tensor()
def hook(module, input, output):
    '''把這層的輸出拷貝到features中'''
    features.copy_(output.data)

handle = model.layer8.register_forward_hook(hook)
_ = model(input)
# 用完hook后刪除
handle.remove()

nn.Module對象在構(gòu)造函數(shù)中的行為看起來有些怪異勾效,如果想要真正掌握其原理,就需要看兩個(gè)魔法方法getattrsetattr叛甫。在Python中有兩個(gè)常用的buildin方法getattr和setattr层宫,getattr(obj, 'attr1')等價(jià)于obj.attr,如果getattr函數(shù)無法找到所需屬性其监,Python會轉(zhuǎn)而調(diào)用obj.getattr('attr1')方法萌腿,即getattr函數(shù)無法找到的交給getattr函數(shù)處理,沒有實(shí)現(xiàn)getattr或者getattr也無法處理的就會raise AttributeError抖苦。setattr(obj, 'name', value)等價(jià)于obj.name=value毁菱,如果obj對象實(shí)現(xiàn)了setattr方法,setattr會直接調(diào)用obj.setattr('name', value)锌历,否則調(diào)用buildin方法贮庞。總結(jié)一下:

  • result = obj.name會調(diào)用buildin函數(shù)getattr(obj, 'name')究西,如果該屬性找不到窗慎,會調(diào)用obj.getattr('name')
  • obj.name = value會調(diào)用buildin函數(shù)setattr(obj, 'name', value),如果obj對象實(shí)現(xiàn)了setattr方法卤材,setattr會直接調(diào)用obj.setattr('name', value')

nn.Module實(shí)現(xiàn)了自定義的setattr函數(shù)遮斥,當(dāng)執(zhí)行module.name=value時(shí),會在setattr中判斷value是否為Parameter或nn.Module對象扇丛,如果是則將這些對象加到_parameters和_modules兩個(gè)字典中术吗,而如果是其它類型的對象,如Variable帆精、list较屿、dict等材蹬,則調(diào)用默認(rèn)的操作,將這個(gè)值保存在dict中吝镣。

module = nn.Module()
module.param = nn.Parameter(t.ones(2, 2))
module._parameters

輸出:

OrderedDict([('param', Parameter containing:
              tensor([[1., 1.],
                      [1., 1.]], requires_grad=True))])
submodule1 = nn.Linear(2, 2)
submodule2 = nn.Linear(2, 2)
module_list =  [submodule1, submodule2]
# 對于list對象堤器,調(diào)用buildin函數(shù),保存在__dict__中
module.submodules = module_list
print('_modules: ', module._modules)
print("__dict__['submodules']:",module.__dict__.get('submodules'))

輸出:

_modules:  OrderedDict()
__dict__['submodules']: [Linear(in_features=2, out_features=2, bias=True), Linear(in_features=2, out_features=2, bias=True)]
module_list = nn.ModuleList(module_list)
module.submodules = module_list
print('ModuleList is instance of nn.Module: ', isinstance(module_list, nn.Module))
print('_modules: ', module._modules)
print("__dict__['submodules']:", module.__dict__.get('submodules'))

輸出:

ModuleList is instance of nn.Module:  True
_modules:  OrderedDict([('submodules', ModuleList(
  (0): Linear(in_features=2, out_features=2, bias=True)
  (1): Linear(in_features=2, out_features=2, bias=True)
))])
__dict__['submodules']: None

因_modules和_parameters中的item未保存在dict中末贾,所以默認(rèn)的getattr方法無法獲取它闸溃,因而nn.Module實(shí)現(xiàn)了自定義的getattr方法,如果默認(rèn)的getattr無法處理拱撵,就調(diào)用自定義的getattr方法辉川,嘗試從_modules、_parameters和_buffers這三個(gè)字典中獲取拴测。

getattr(module, 'training') # 等價(jià)于module.training
# error
# module.__getattr__('training')

輸出:

True
module.attr1 = 2
getattr(module, 'attr1')
# 報(bào)錯(cuò)
# module.__getattr__('attr1')

輸出:

2
# 即module.param, 會調(diào)用module.__getattr__('param')
getattr(module, 'param')

輸出:

Parameter containing:
tensor([[1., 1.],
        [1., 1.]], requires_grad=True)

在PyTorch中保存模型十分簡單乓旗,所有的Module對象都具有state_dict()函數(shù),返回當(dāng)前Module所有的狀態(tài)數(shù)據(jù)集索。將這些狀態(tài)數(shù)據(jù)保存后屿愚,下次使用模型時(shí)即可利用model.load_state_dict()函數(shù)將狀態(tài)加載進(jìn)來。優(yōu)化器(optimizer)也有類似的機(jī)制务荆,不過一般并不需要保存優(yōu)化器的運(yùn)行狀態(tài)妆距。

# 保存模型
t.save(net.state_dict(), 'net.pth')

# 加載已保存的模型
net2 = Net()
net2.load_state_dict(t.load('net.pth'))

輸出:

IncompatibleKeys(missing_keys=[], unexpected_keys=[])

實(shí)際上還有另外一種保存方法,但因其嚴(yán)重依賴模型定義方式及文件路徑結(jié)構(gòu)等函匕,很容易出問題娱据,因而不建議使用。

t.save(net, 'net_all.pth')
net2 = t.load('net_all.pth')
net2

輸出:

Net(
  (submodel1): Linear(in_features=3, out_features=4, bias=True)
)

將Module放在GPU上運(yùn)行也十分簡單盅惜,只需兩步:

  • model = model.cuda():將模型的所有參數(shù)轉(zhuǎn)存到GPU
  • input.cuda():將輸入數(shù)據(jù)也放置到GPU上

至于如何在多個(gè)GPU上并行計(jì)算中剩,PyTorch也提供了兩個(gè)函數(shù),可實(shí)現(xiàn)簡單高效的并行GPU計(jì)算

  • nn.parallel.data_parallel(module, inputs, device_ids=None, output_device=None, dim=0, module_kwargs=None)
  • class torch.nn.DataParallel(module, device_ids=None, output_device=None, dim=0)

可見二者的參數(shù)十分相似抒寂,通過device_ids參數(shù)可以指定在哪些GPU上進(jìn)行優(yōu)化结啼,output_device指定輸出到哪個(gè)GPU上。唯一的不同就在于前者直接利用多GPU并行計(jì)算得出結(jié)果蓬推,而后者則返回一個(gè)新的module妆棒,能夠自動在多GPU上進(jìn)行并行加速澡腾。

# method 1
new_net = nn.DataParallel(net, device_ids=[0, 1])
output = new_net(input)

# method 2
output = nn.parallel.data_parallel(new_net, input, device_ids=[0, 1])

DataParallel并行的方式沸伏,是將輸入一個(gè)batch的數(shù)據(jù)均分成多份,分別送到對應(yīng)的GPU進(jìn)行計(jì)算动分,各個(gè)GPU得到的梯度累加毅糟。與Module相關(guān)的所有數(shù)據(jù)也都會以淺復(fù)制的方式復(fù)制多份,在此需要注意澜公,在module中屬性應(yīng)該是只讀的姆另。

4.7 nn和autograd的關(guān)系

nn.Module利用的也是autograd技術(shù)喇肋,其主要工作是實(shí)現(xiàn)前向傳播。在forward函數(shù)中迹辐,nn.Module對輸入的tensor進(jìn)行的各種操作蝶防,本質(zhì)上都是用到了autograd技術(shù)。這里需要對比autograd.Function和nn.Module之間的區(qū)別:

  • autograd.Function利用了Tensor對autograd技術(shù)的擴(kuò)展明吩,為autograd實(shí)現(xiàn)了新的運(yùn)算op间学,不僅要實(shí)現(xiàn)前向傳播還要手動實(shí)現(xiàn)反向傳播。
  • nn.Module利用了autograd技術(shù)印荔,對nn的功能進(jìn)行擴(kuò)展低葫,實(shí)現(xiàn)了深度學(xué)習(xí)中更多的層。只需實(shí)現(xiàn)前向傳播功能仍律,autograd即會自動實(shí)現(xiàn)反向傳播嘿悬。
  • nn.functional是一些autograd操作的集合,是經(jīng)過封裝的函數(shù)水泉。

作為兩大類擴(kuò)充PyTorch接口的方法善涨,我們在實(shí)際使用中應(yīng)該如何選擇呢?如果某一個(gè)操作草则,在autograd中尚未支持躯概,那么只能實(shí)現(xiàn)Function接口對應(yīng)的前向傳播和反向傳播。如果某些時(shí)候利用autograd接口比較復(fù)雜畔师,則可以利用Function將多個(gè)操作聚合娶靡,實(shí)現(xiàn)優(yōu)化,正如第三章所實(shí)現(xiàn)的Sigmoid一樣看锉,比直接利用autograd低級別的操作要快姿锭。而如果只是想在深度學(xué)習(xí)中增加某一層,使用nn.Module進(jìn)行封裝則更為簡單高效伯铣。

4.8 小試牛刀:用50行代碼搭建ResNet

Kaiming He的深度殘差網(wǎng)絡(luò)(ResNet)在深度學(xué)習(xí)的發(fā)展中起到了很重要的作用呻此,ResNet不僅一舉拿下了當(dāng)年CV下多個(gè)比賽項(xiàng)目的冠軍,更重要的是這一結(jié)構(gòu)解決了訓(xùn)練極深網(wǎng)絡(luò)時(shí)的梯度消失問題腔寡。

首先來看看ResNet的網(wǎng)絡(luò)結(jié)構(gòu)焚鲜,這里選取的是ResNet的一個(gè)變種:ResNet34。ResNet的網(wǎng)絡(luò)結(jié)構(gòu)如圖4-2所示放前,可見除了最開始的卷積池化和最后的池化全連接之外忿磅,網(wǎng)絡(luò)中有很多結(jié)構(gòu)相似的單元,這些重復(fù)單元的共同點(diǎn)就是有個(gè)跨層直連的shortcut凭语。ResNet中將一個(gè)跨層直連的單元稱為Residual block葱她,其結(jié)構(gòu)如圖4-3所示,左邊部分是普通的卷積網(wǎng)絡(luò)結(jié)構(gòu)似扔,右邊是直連吨些,但如果輸入和輸出的通道數(shù)不一致搓谆,或其步長不為1,那么就需要有一個(gè)專門的單元將二者轉(zhuǎn)成一致豪墅,使其可以相加泉手。

另外我們可以發(fā)現(xiàn)Residual block的大小也是有規(guī)律的,在最開始的pool之后有連續(xù)的幾個(gè)一模一樣的Residual block單元偶器,這些單元的通道數(shù)一樣螃诅,在這里我們將這幾個(gè)擁有多個(gè)Residual block單元的結(jié)構(gòu)稱之為layer,注意和之前講的layer區(qū)分開來状囱,這里的layer是幾個(gè)層的集合术裸。

考慮到Residual block和layer出現(xiàn)了多次,我們可以把它們實(shí)現(xiàn)為一個(gè)子Module或函數(shù)。這里我們將Residual block實(shí)現(xiàn)為一個(gè)子moduke,而將layer實(shí)現(xiàn)為一個(gè)函數(shù)敛助。下面是實(shí)現(xiàn)代碼,規(guī)律總結(jié)如下:

  • 對于模型中的重復(fù)部分猾编,實(shí)現(xiàn)為子module或用函數(shù)生成相應(yīng)的modulemake_layer。
  • nn.Module和nn.Functional結(jié)合使用升敲。
  • 盡量使用nn.Seqential答倡。
image.png
image.png
class ResidualBlock(nn.Module):
    '''
    實(shí)現(xiàn)子module: Residual Block
    '''
    def __init__(self, inchannel, outchannel, stride=1, shortcut=None):
        super(ResidualBlock, self).__init__()
        self.left = nn.Sequential(
                nn.Conv2d(inchannel,outchannel,3,stride, 1,bias=False),
                nn.BatchNorm2d(outchannel),
                nn.ReLU(inplace=True),
                nn.Conv2d(outchannel,outchannel,3,1,1,bias=False),
                nn.BatchNorm2d(outchannel) )
        self.right = shortcut

    def forward(self, x):
        out = self.left(x)
        residual = x if self.right is None else self.right(x)
        out += residual
        return F.relu(out)

class ResNet(nn.Module):
    '''
    實(shí)現(xiàn)主module:ResNet34
    ResNet34 包含多個(gè)layer,每個(gè)layer又包含多個(gè)residual block
    用子module來實(shí)現(xiàn)residual block驴党,用_make_layer函數(shù)來實(shí)現(xiàn)layer
    '''
    def __init__(self, num_classes=1000):
        super(ResNet, self).__init__()
        # 前幾層圖像轉(zhuǎn)換
        self.pre = nn.Sequential(
                nn.Conv2d(3, 64, 7, 2, 3, bias=False),
                nn.BatchNorm2d(64),
                nn.ReLU(inplace=True),
                nn.MaxPool2d(3, 2, 1))
        
        # 重復(fù)的layer瘪撇,分別有3,4港庄,6倔既,3個(gè)residual block
        self.layer1 = self._make_layer( 64, 64, 3)
        self.layer2 = self._make_layer( 64, 128, 4, stride=2)
        self.layer3 = self._make_layer( 128, 256, 6, stride=2)
        self.layer4 = self._make_layer( 256, 512, 3, stride=2)

        #分類用的全連接
        self.fc = nn.Linear(512, num_classes)
    
    def _make_layer(self,  inchannel, outchannel, block_num, stride=1):
        '''
        構(gòu)建layer,包含多個(gè)residual block
        '''
        shortcut = nn.Sequential(
                nn.Conv2d(inchannel,outchannel,1,stride, bias=False),
                nn.BatchNorm2d(outchannel))
        
        layers = []
        layers.append(ResidualBlock(inchannel, outchannel, stride, shortcut))
        
        for i in range(1, block_num):
            layers.append(ResidualBlock(outchannel, outchannel))
        return nn.Sequential(*layers)
        
    def forward(self, x):
        x = self.pre(x)
        
        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)

        x = F.avg_pool2d(x, 7)
        x = x.view(x.size(0), -1)
        return self.fc(x)
model = ResNet()
input  = t.randn(1, 3, 224, 224)
o = model(input)

感興趣的讀者可以嘗試實(shí)現(xiàn)Google的Inception網(wǎng)絡(luò)結(jié)構(gòu)或ResNet的其它變體,看看如何能夠簡潔明了地實(shí)現(xiàn)它鹏氧,實(shí)現(xiàn)代碼盡量控制在80行以內(nèi)(本例去掉空行和注釋總共不超過50行)渤涌。另外,與PyTorch配套的圖像工具包torchvision已經(jīng)實(shí)現(xiàn)了深度學(xué)習(xí)中大多數(shù)經(jīng)典的模型把还,其中就包括ResNet34实蓬,讀者可以通過下面兩行代碼使用:

from torchvision import models
model = models.resnet34()

本例中ResNet34的實(shí)現(xiàn)就是參考了torchvision中的實(shí)現(xiàn)并做了簡化,感興趣的讀者可以閱讀相應(yīng)的源碼吊履,比較這里的實(shí)現(xiàn)和torchvision中實(shí)現(xiàn)的不同安皱。

通過本章的學(xué)習(xí),讀者可以掌握PyTorch中神經(jīng)網(wǎng)絡(luò)工具箱中大部分類和函數(shù)的用法率翅。關(guān)于這部分的更多內(nèi)容练俐,讀者可以參考官方文檔袖迎,文檔中有更多詳細(xì)的說明冕臭。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末腺晾,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子辜贵,更是在濱河造成了極大的恐慌悯蝉,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,509評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件托慨,死亡現(xiàn)場離奇詭異鼻由,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)厚棵,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評論 3 394
  • 文/潘曉璐 我一進(jìn)店門蕉世,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人婆硬,你說我怎么就攤上這事狠轻。” “怎么了彬犯?”我有些...
    開封第一講書人閱讀 163,875評論 0 354
  • 文/不壞的土叔 我叫張陵向楼,是天一觀的道長。 經(jīng)常有香客問我谐区,道長湖蜕,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,441評論 1 293
  • 正文 為了忘掉前任宋列,我火速辦了婚禮昭抒,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘炼杖。我一直安慰自己戈鲁,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,488評論 6 392
  • 文/花漫 我一把揭開白布嘹叫。 她就那樣靜靜地躺著婆殿,像睡著了一般。 火紅的嫁衣襯著肌膚如雪罩扇。 梳的紋絲不亂的頭發(fā)上婆芦,一...
    開封第一講書人閱讀 51,365評論 1 302
  • 那天,我揣著相機(jī)與錄音喂饥,去河邊找鬼消约。 笑死,一個(gè)胖子當(dāng)著我的面吹牛员帮,可吹牛的內(nèi)容都是我干的或粮。 我是一名探鬼主播,決...
    沈念sama閱讀 40,190評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼捞高,長吁一口氣:“原來是場噩夢啊……” “哼氯材!你這毒婦竟也來了渣锦?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,062評論 0 276
  • 序言:老撾萬榮一對情侶失蹤氢哮,失蹤者是張志新(化名)和其女友劉穎袋毙,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體冗尤,經(jīng)...
    沈念sama閱讀 45,500評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡听盖,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,706評論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了裂七。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片皆看。...
    茶點(diǎn)故事閱讀 39,834評論 1 347
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖背零,靈堂內(nèi)的尸體忽然破棺而出悬蔽,到底是詐尸還是另有隱情,我是刑警寧澤捉兴,帶...
    沈念sama閱讀 35,559評論 5 345
  • 正文 年R本政府宣布蝎困,位于F島的核電站,受9級特大地震影響倍啥,放射性物質(zhì)發(fā)生泄漏禾乘。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,167評論 3 328
  • 文/蒙蒙 一虽缕、第九天 我趴在偏房一處隱蔽的房頂上張望始藕。 院中可真熱鬧,春花似錦氮趋、人聲如沸伍派。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽诉植。三九已至,卻和暖如春昵观,著一層夾襖步出監(jiān)牢的瞬間晾腔,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評論 1 269
  • 我被黑心中介騙來泰國打工啊犬, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留灼擂,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 47,958評論 2 370
  • 正文 我出身青樓觉至,卻偏偏與公主長得像剔应,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,779評論 2 354

推薦閱讀更多精彩內(nèi)容