對教材《深度學(xué)習(xí)框架pytorch:入門與實(shí)踐》和一些技術(shù)博客及實(shí)踐過程中的總結(jié),比較精煉。
網(wǎng)絡(luò)模型定義
一般通過繼承pytoch實(shí)現(xiàn)的torch.nn.Module
(以下簡稱Module)濒蒋,然后定義自己的網(wǎng)絡(luò)的每層。
這里我們先通過繼承Module
構(gòu)建一個(gè)抽象的網(wǎng)絡(luò)BasicModule
實(shí)現(xiàn)了對Module
中.save/.load
的重載,方便保存和加載已訓(xùn)練完畢的模型(持久化
)蹦掐。
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()
(以上就是這么做的)而不是直接保存整個(gè)模型或優(yōu)化器卧抗,可以保存其參數(shù)和動(dòng)量信息。
基于以上的BasicModule
實(shí)現(xiàn)自己的Module就可以具有加載和保存參數(shù)的功能
class BriefNet(BM.BasicModule):
def __init__(self):
super(BriefNet, self).__init__()
# nn.ReLU 和 functional.relu的差別(大部分nn.layer 都對應(yīng)一個(gè)nn.functional中的函數(shù)):
# 前者是nn.Module的子類鳖粟,后者是nn.functional的子類
# nn.中許多l(xiāng)ayer和激活函數(shù)都是繼承與nn.Module的可以自動(dòng)提取派生類中的可學(xué)習(xí)參數(shù)(通過繼承Module實(shí)現(xiàn)的__getattr__和__setattr__)
# functional是沒有可學(xué)習(xí)參數(shù)的颗味,可以用在激活函數(shù)、池化等地方
self.fc = nn.Sequential(
nn.Linear(784, 256), # 數(shù)據(jù)集圖片為28*28單通道牺弹,此處直接用多層感知機(jī)而不是用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):
# 定義前向傳播函數(shù)
x = x.view(x.shape[0], -1) # 將輸入圖片變成列向量輸入
x = self.fc(x)
return x
定義起來還是很簡單的浦马,網(wǎng)絡(luò)定義有兩種比較常見的格式(無性能差別)
一種是使用
nn.Sequantial
(推薦),其也是Module的子類张漂,相對于ModuleList
而言可以直接輸入如 self.fc(x)
晶默。nn.Sequantial
中定義了一些列的nn.Module
的子類如:Linear(全連接、仿射層)航攒、ReLu(激活函數(shù))磺陡、Dropout、SoftMax漠畜、Conv
等常用的layers币他,使用Sequential
可以很方便的把這些layer的輸入輸出自動(dòng)連接起來,自己來定義layer間的組合而得到一個(gè)復(fù)合layer(代碼結(jié)構(gòu)意義上的一個(gè)憔狞,只需要給定整個(gè)sequential一個(gè)輸入蝴悉,就能得出一個(gè)輸出,而不需要一層一層的輸入輸出和第二種方法有差別)瘾敢,這樣的好處是定義簡潔拍冠、且通過對本model的控制如models.train()尿这、models.eval()
就可以自動(dòng)的設(shè)置其內(nèi)部那些訓(xùn)練和驗(yàn)證具有不同表現(xiàn)
的layer所處的狀態(tài)或model.zero_grad()實(shí)現(xiàn)對所有葉節(jié)點(diǎn)參數(shù)梯度清空操作
。更有一體感庆杜,方便宏觀掌控射众。-
另一種是通過在
__init__
中自己聲明一些nn中實(shí)現(xiàn)的layer
的實(shí)例屬性(相較于之前這是比較分散的)。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
中實(shí)現(xiàn)的大多數(shù)layers在nn.functional
中也有對應(yīng)的實(shí)現(xiàn)(一般為全小寫函數(shù)如nn.functinal.relu晃财、softmax叨橱、F.conv
)。介紹到這断盛,注意一下代碼中注釋提示:用Module實(shí)現(xiàn)的子類layer能自動(dòng)提取內(nèi)部的類型為nn.parameter
可學(xué)習(xí)參數(shù)(需要變化的參數(shù))罗洗,而nn.functional
中的函數(shù)更像是純函數(shù)調(diào)用時(shí)必須給輸入如F.relu(input)、F.linear(input,model.weight,model.bias)
郑临。且functional這種純函數(shù)的用法更適合那些沒有可學(xué)習(xí)參數(shù)的層如pooling栖博、ReLU、sigmoid厢洞、tanh
仇让,他們參數(shù)都是固定的,不需要輸入需要學(xué)習(xí)的參數(shù)到functional.xxx()
的輸入中躺翻,可以保持完整感丧叽。但是對于dropout
建議使用Module,否則需要手動(dòng)給出當(dāng)前訓(xùn)練狀態(tài)F.dropout(input,training=self.training)
公你。 然后就是實(shí)現(xiàn)
forward
踊淳。給網(wǎng)絡(luò)一個(gè)輸入即output=model(input)
其內(nèi)部會(huì)調(diào)用model.__call__
(nn.Module實(shí)現(xiàn)),call中主要是調(diào)用了forward
陕靠。forward主要負(fù)責(zé)計(jì)算圖的構(gòu)建迂尝,然后我們就能根據(jù)計(jì)算圖進(jìn)行自動(dòng)求導(dǎo)計(jì)算。-
這里說一下以上內(nèi)容的實(shí)現(xiàn)原理:
為何
nn.Module
可以自動(dòng)提取子類內(nèi)部的可學(xué)習(xí)參數(shù):
因?yàn)?code>nn.Module中實(shí)現(xiàn)了兩個(gè)魔法方法.__getattr__和.__setattr__
剪芥。而obj.name=value 等價(jià)于setattr(obj,'name',value)
和obj.name 等價(jià)于 getattr(obj,'name')
垄开,當(dāng)定義了自定義方法后getattr和setattr
會(huì)調(diào)用自定義的.__getattr__和.__setattr__
。然后税肪,當(dāng)用戶在子類定義實(shí)例屬性時(shí)候就會(huì)觸發(fā).__setattr__(obj,'name',value)
溉躲,在此函數(shù)中會(huì)判斷obj
類型是否是Parameter或nn.Module
對象,如果是就存放到_parameters和_models
兩個(gè)私有字典中益兄;如果是其他對象锻梳,則調(diào)用默認(rèn)操作:添加到__dict__
字典中。而_models
中的都是子models净捅,他們在定義的時(shí)候也會(huì)在其內(nèi)部的._models
字典中存放其子models疑枯。最終可能是會(huì)對本自定義models內(nèi)的所有子models(含迭代)的parameter進(jìn)行一些管理。是如何實(shí)現(xiàn)自動(dòng)求導(dǎo)的:
在這之前先了解一下torch的一些數(shù)據(jù)結(jié)構(gòu):nn.Parameter灸叼、autograd.variable神汹、torch.tensor
庆捺。
a. 其中tensor
是numpy中array類似的多維數(shù)組古今。
b.variable
是以tensor作為核心data對象并進(jìn)行了一些 擴(kuò)充(如grad梯度值(目標(biāo)函數(shù)對本變量的偏導(dǎo)屁魏,也是variable類)、grad_fn梯度計(jì)算函數(shù)捉腥、requires_grad是否需要自動(dòng)求導(dǎo)功能
)氓拼,使其能進(jìn)行自動(dòng)求導(dǎo)功能。且model(網(wǎng)絡(luò)模型)的輸入都是以variable形式輸入的抵碟。如果數(shù)據(jù)集經(jīng)過變換to_tensor
在輸入前還需要input=Variable(input)
桃漾。注釋:autograd只實(shí)現(xiàn)了標(biāo)量目標(biāo)函數(shù)對葉節(jié)點(diǎn)求導(dǎo)
,對目標(biāo)函數(shù)進(jìn)行backward()
后葉節(jié)點(diǎn)變量x
就可以得到目標(biāo)函數(shù)對x偏導(dǎo)x.grad
拟逮,但是葉節(jié)點(diǎn)有標(biāo)記AccumulateGrad
會(huì)自動(dòng)累加歷次backward的梯度值撬统。因此調(diào)用反向傳播時(shí)需要清零梯度x.grad.data.zero_()inplace操作清零
。
c.Parameter
是variable
的子類敦迄,且默認(rèn)是requires_grad=True
恋追。能在定義Model時(shí)候自動(dòng)被基類nn.Module
管理。-
什么是計(jì)算圖(pytorch是動(dòng)態(tài)圖):計(jì)算圖是一種
有向無環(huán)圖
且圖中有兩種節(jié)點(diǎn):- 一種是○表示的變量(variable)罚屋;
- 另一種是□表示的算子(如已實(shí)現(xiàn)重載的
+苦囱、-、*脾猛、/撕彤、**n等
或.sum()、t.exp等已實(shí)現(xiàn)函數(shù)
或基于Function類自定義運(yùn)算符
)猛拴。
其中羹铅,用戶聲明的
Variable
為葉節(jié)點(diǎn)(需要requires_grad的)是依賴于其他變量了;其他節(jié)點(diǎn)都是通過對葉節(jié)點(diǎn)及其運(yùn)算結(jié)果進(jìn)行復(fù)合運(yùn)算得到的一些中間Variable
和最終的目標(biāo)函數(shù)愉昆。我們定義用戶Variable
职员,拿他們
或他們之間運(yùn)算結(jié)果
做運(yùn)算時(shí),會(huì)自動(dòng)根據(jù)重載或以實(shí)現(xiàn)的運(yùn)算函數(shù)(都是算子)進(jìn)行邊的連接(輸入輸出關(guān)系撼唾,是有向的)(并在算子內(nèi)儲存一份輸入輸出拷貝用來梯度計(jì)算時(shí)使用)廉邑,然后生成一個(gè)圖。圖中任意一點(diǎn)都可以利用鏈?zhǔn)椒▌t實(shí)現(xiàn)5构取蛛蒙!因此當(dāng)對requires_grad
的葉子節(jié)點(diǎn)求導(dǎo)時(shí),中間變量Variable
都會(huì)自動(dòng).requires_grad=True
渤愁,但非葉子節(jié)點(diǎn)計(jì)算完后會(huì)清空梯度牵祟。此外是利用以下幾個(gè)規(guī)則來進(jìn)行鏈?zhǔn)椒▌t的:當(dāng)y是標(biāo)量的時(shí)候可以進(jìn)行y.backward()
,但是y是向量(認(rèn)為是中間variable)其求梯度必須輸入一個(gè)參數(shù)y.backward(grad_y)
否則會(huì)提示只能進(jìn)行標(biāo)量對向量求梯度抖格,其中grad_y=標(biāo)量目標(biāo)函數(shù)z對向量y求偏導(dǎo)
诺苹。 -
目前大多數(shù)算子都可以使用autograd的反向求導(dǎo)咕晋,自己寫的復(fù)雜函數(shù)沒有,如何解決:自己實(shí)現(xiàn)的復(fù)雜函數(shù)(算子)不具備自動(dòng)求導(dǎo)功能(
比如y=x**2+x*2支持收奔,但y=func(x)的func不支持
)掌呜。可以利用autograd.Function
對autograd功能進(jìn)行拓展坪哄,其中Function
對應(yīng)計(jì)算圖中的□质蕉。需要自己實(shí)現(xiàn)一個(gè)繼承與autograd.Function的算子,并自己實(shí)現(xiàn)求導(dǎo)翩肌。這樣就可以在計(jì)算圖使用它并使得鏈?zhǔn)椒▌t進(jìn)行下去了模暗。其中forward
輸入輸出都是tensor
,而backward
的輸入和輸出都是variable
念祭。backward函數(shù)的輸入對應(yīng)forwad的輸出兑宇,backward的輸出對應(yīng)forward的輸入。然后用Function.apply(variable)
即可調(diào)用實(shí)現(xiàn)的Function粱坤。如z=func(V(...),V(...),...)
會(huì)自動(dòng)調(diào)用forward并提取Variable中的data(tensor類型)作為輸入然后把輸出的tensor包裝成Variable隶糕,z.backward()
就會(huì)自動(dòng)調(diào)用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īng)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,...
數(shù)據(jù)集加載
訓(xùn)練
訓(xùn)練除了需要數(shù)據(jù)集,還需要損失函數(shù)/目標(biāo)函數(shù)比规、優(yōu)化器若厚。其中損失函數(shù)是標(biāo)量函數(shù),將網(wǎng)絡(luò)的輸出(多維向量)進(jìn)行某種評估得分蜒什。我們訓(xùn)練的方法就是BP即通過求取目標(biāo)函數(shù)對輸入(葉節(jié)點(diǎn))的梯度测秸,然后通過優(yōu)化器來求去網(wǎng)絡(luò)可學(xué)習(xí)參數(shù)的變化量的。
損失函數(shù)
torch.nn
中實(shí)現(xiàn)了許多損失函數(shù)如:NLLLoss灾常、CrossEntropyLoss
等常用損失函數(shù)霎冯。
如果網(wǎng)絡(luò)訓(xùn)練需要用GPU則需要使用對應(yīng)的損失函數(shù)即.cuda()。因?yàn)闉榱擞肎PU進(jìn)行訓(xùn)練钞瀑,網(wǎng)絡(luò)參數(shù)在GPU上沈撞、輸入也需要在GPU上才能進(jìn)行前向傳播、網(wǎng)絡(luò)輸出的向量也是在GPU上的雕什,為了評估輸出缠俺,損失函數(shù)計(jì)算時(shí)候的參數(shù)也是GPU上的才能和網(wǎng)絡(luò)輸出進(jìn)行運(yùn)算,因此損失函數(shù)求出的score也是GPU上的贷岸。
if option.use_gpu:
criterion = t.nn.NLLLoss().cuda()
else:
criterion = t.nn.NLLLoss()
優(yōu)化器
優(yōu)化器根據(jù)criterion.backward()
得到model.parameters().grad
壹士。然后得到的梯度進(jìn)行一些優(yōu)化策略從而計(jì)算出網(wǎng)絡(luò)中可學(xué)習(xí)參數(shù)的更新值。并能通過model.zero_grad()或optimizer.zero_grad()
清空對葉節(jié)點(diǎn)(網(wǎng)絡(luò)可學(xué)習(xí)參數(shù))梯度偿警。
torch.optim
中實(shí)現(xiàn)了許多常用的優(yōu)化器如:SGD(根據(jù)參數(shù)覺得是否用動(dòng)量)躏救、Adam...
這里注意:學(xué)習(xí)率和weight_decay(可能導(dǎo)致學(xué)習(xí)率為0)要小心設(shè)計(jì)否則優(yōu)化結(jié)果導(dǎo)致訓(xùn)練發(fā)散或不變。
optimizer = optim.SGD(model.parameters(), lr=option.lr, momentum=0.8) # , weight_decay=option.lr_decay) # 慎用,
訓(xùn)練
網(wǎng)絡(luò)設(shè)置為訓(xùn)練模式 -> 梯度清零 -> 輸入網(wǎng)絡(luò)(Variable輸入) -> 根據(jù)實(shí)際值和輸出值計(jì)算損失函數(shù)輸出評分 -> 求評分對網(wǎng)絡(luò)可學(xué)習(xí)參數(shù)梯度 -> 優(yōu)化器更新網(wǎng)絡(luò)可學(xué)習(xí)參數(shù)
model.train()
optimizer.zero_grad()
ps = model(x)
loss = criterion(ps, target)
loss.backward()
optimizer.step()
訓(xùn)練完需要model.save(path)
保存網(wǎng)絡(luò)
測試
需要先model.load(path)加載訓(xùn)練后的參數(shù)
把網(wǎng)絡(luò)設(shè)置成測試模型 -> 輸入 -> (optional:計(jì)算損失函數(shù) -> ) 獲得輸出 -> 輸出和已知標(biāo)簽對比 -> 計(jì)算準(zhǔn)確度
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))
結(jié)語
至此盒使,就簡要的記完了這幾天學(xué)習(xí)pytorch的結(jié)果了崩掘。