最近發(fā)現(xiàn)身邊的一些初學(xué)者朋友捧著各種pytorch指南一邊看一邊敲代碼,到最后反而變成了打字員。
敲完代碼一運(yùn)行,出來結(jié)果和書上一對(duì)比,哦,是書上的結(jié)果,就翻到下一章。
半天就能把一本書都打完,但是合上書好像什么都不記得。有的甚至看了兩三遍,都搭不出一個(gè)簡(jiǎn)單的網(wǎng)絡(luò)來,這種學(xué)習(xí)方式很不可取。
如果你剛好是這種情況,這篇文章應(yīng)該能給你一些幫助。如果你已經(jīng)是進(jìn)階的水平了,就直接關(guān)掉頁面就好了。
pytorch的網(wǎng)絡(luò)搭建,比tensorflow簡(jiǎn)單很多。格式很好理解。
如果你想做一個(gè)網(wǎng)絡(luò),需要先定義一個(gè)Class,繼承 nn.Module(這個(gè)是必須的,所以先import torch.nn as nn,nn是一個(gè)工具箱,很好用),我們把class的名字就叫成Net.
Class Net (nn.Module):
這個(gè)Class里面主要寫兩個(gè)函數(shù),一個(gè)是初始化的__init__函數(shù),另一個(gè)是forward函數(shù)。我們隨便搭一個(gè),如下:
def __init__(self):
super().__init__()
self.conv1=nn.Conv2d(1,6,5)
self.conv2=nn.Conv2d(6,16,5)
def forward(self, x):
x=F.max_pool2d(F.relu(self.conv1(x)),2)
x=F.max_pool2d(F.relu(self.conv2(x)),2)
return x
__init__里面就是定義卷積層,當(dāng)然先得super()一下,給父類nn.Module初始化一下。
(Python的基礎(chǔ)知識(shí))在這個(gè)里面主要就是定義卷積層的,比如第一層,我們叫它c(diǎn)onv1,把它定義成輸入1通道,輸出6通道,卷積核5*5的的一個(gè)卷積層。conv2同理。
神經(jīng)網(wǎng)絡(luò)“深度學(xué)習(xí)”其實(shí)主要就是學(xué)習(xí)卷積核里的參數(shù),像別的不需要學(xué)習(xí)和改變的,就不用放進(jìn)去。
比如激活函數(shù)relu(),你非要放進(jìn)去也行,再給它起個(gè)名字叫myrelu,也是可以的。forward里面就是真正執(zhí)行數(shù)據(jù)的流動(dòng)。
比如上面的代碼,輸入的x先經(jīng)過定義的conv1(這個(gè)名字是你自己起的),再經(jīng)過激活函數(shù)F.relu()(這個(gè)就不是自己起的名字了,最開始應(yīng)該先import torch.nn.functional as F,F(xiàn).relu()是官方提供的函數(shù)。
當(dāng)然如果你在__init__里面把relu定義成了我上面說的myrelu,那你這里直接第一句話就成了x=F.max_pool2d(myrelu(self.conv1(x)),2)。
下一步的F.max_pool2d池化也是一樣的,不多廢話了。在一系列流動(dòng)以后,最后把x返回到外面去。
這個(gè)Net的Class定義主要要注意兩點(diǎn)。
第一:是注意前后輸出通道和輸入通道的一致性。不能第一個(gè)卷積層輸出4通道第二個(gè)輸入6通道,這樣就會(huì)報(bào)錯(cuò)。
第二:它和我們常規(guī)的python的class還有一些不同,發(fā)現(xiàn)了沒有?我們?cè)撛趺从眠@個(gè)Net呢?
先定義一個(gè)Net的實(shí)例(畢竟Net只是一個(gè)類不能直接傳參數(shù),output=Net(input)當(dāng)然不行)
net=Net()
這樣我們就可以往里傳x了,假設(shè)你已經(jīng)有一個(gè)要往神經(jīng)網(wǎng)絡(luò)的輸入的數(shù)據(jù)“input“(這個(gè)input應(yīng)該定義成tensor類型,怎么定義tensor那就自己去看看書了。)在傳入的時(shí)候,是:
output=net(input)
看之前的定義:
def __init__(self):
…… def forward(self, x): ……
有點(diǎn)奇怪。好像常規(guī)python一般向class里面?zhèn)魅胍粋€(gè)數(shù)據(jù)x,在class的定義里面,應(yīng)該是把這個(gè)x作為形參傳入__init__函數(shù)里的,而在上面的定義中,x作為形參是傳入forward函數(shù)里面的。
其實(shí)也不矛盾,因?yàn)槟愣xnet的時(shí)候,是net=Net(),并沒有往里面?zhèn)魅雲(yún)?shù)。如果你想初始化的時(shí)候按需傳入,就把需要的傳入進(jìn)去。
只是x是神經(jīng)網(wǎng)絡(luò)的輸入,但是并非是初始化需要的,初始化一個(gè)網(wǎng)絡(luò),必須要有輸入數(shù)據(jù)嗎?
未必吧。只是在傳入網(wǎng)絡(luò)的時(shí)候,會(huì)自動(dòng)認(rèn)為你這個(gè)x是喂給forward里面的。也就是說,先定義一個(gè)網(wǎng)絡(luò)的實(shí)例net=Net(), 這時(shí)調(diào)用output=net(input), 可以理解為等同于調(diào)用output=net.forward(input), 這兩者可以理解為一碼事。
在網(wǎng)絡(luò)定義好以后,就涉及到傳入?yún)?shù),算誤差,反向傳播,更新權(quán)重…確實(shí)很容易記不住這些東西的格式和順序。
傳入的方式上面已經(jīng)介紹了,相當(dāng)于一次正向傳播,把一路上各層的輸入x都算出來了。
想讓神經(jīng)網(wǎng)絡(luò)輸出的output跟你期望的ground truth差不多,那就是不斷減小二者間的差異,這個(gè)差異是你自己定義的,也就是目標(biāo)函數(shù)(object function)或者就是損失函數(shù)。
如果損失函數(shù)loss趨近于0,那么自然就達(dá)到目的了。
損失函數(shù)loss基本上沒法達(dá)到0,但是希望能讓它達(dá)到最小值,那么就是希望它能按照梯度進(jìn)行下降。
梯度下降的公式,大家應(yīng)該都很熟悉,不熟悉的話,建議去看一下相關(guān)的理論。誰喜歡看公式呢?所以我這里不講。
只是你的輸入是由你來決定的,那神經(jīng)網(wǎng)絡(luò)能學(xué)習(xí)和決定什么呢?
自然它只能決定每一層卷積層的權(quán)重。所以神經(jīng)網(wǎng)絡(luò)只能不停修改權(quán)重,比如y=wx+b,x是你給的,它只能改變w,b讓最后的輸出y盡可能接近你希望的y值,這樣損失loss就越來越小。
如果loss對(duì)于輸入x的偏導(dǎo)數(shù)接近0了,不就意味著到達(dá)了一個(gè)極值嗎?
而l在你的loss計(jì)算方式已經(jīng)給定的情況下,loss對(duì)于輸入x的偏導(dǎo)數(shù)的減小,其實(shí)只能通過更新參數(shù)卷積層參數(shù)W來實(shí)現(xiàn)(別的它決定不了啊,都是你輸入和提供的)。
所以,通過下述方式實(shí)現(xiàn)對(duì)W的更新:(注意這些編號(hào),下面還要提)
【1】 先算loss對(duì)于輸入x的偏導(dǎo),(當(dāng)然網(wǎng)絡(luò)好幾層,這個(gè)x指的是每一層的輸入,而不是最開始的輸入input)
【2】 對(duì)【1】的結(jié)果再乘以一個(gè)步長(這樣就相當(dāng)于是得到一個(gè)對(duì)參數(shù)W的修改量)
【3】 用W減掉這個(gè)修改量,完成一次對(duì)參數(shù)W的修改。
說的不太嚴(yán)謹(jǐn),但是大致意思是這樣的。這個(gè)過程你可以手動(dòng)實(shí)現(xiàn),但是大規(guī)模神經(jīng)網(wǎng)絡(luò)怎么手動(dòng)實(shí)現(xiàn)?那是不可能的事情。所以我們要利用框架pytorch和工具箱torch.nn。
所以要定義損失函數(shù),以MSEloss為例:
compute_loss=nn.MSELoss()
明顯它也是個(gè)類,不能直接傳入輸入數(shù)據(jù),所以直接loss=nn.MSEloss(target,output)是不對(duì)的。需要把這個(gè)函數(shù)賦一個(gè)實(shí)例,叫成compute_loss。
之后就可以把你的神經(jīng)網(wǎng)絡(luò)的輸出,和標(biāo)準(zhǔn)答案target傳入進(jìn)去:
loss=compute_loss(target,output)
算出loss,下一步就是反向傳播:
loss.backward()
這一步其實(shí)就是把【1】給算完了,得到對(duì)參數(shù)W一步的更新量,算是一次反向傳播。
這里就注意了,loss.backward()是啥玩意?如果是自己的定義的loss(比如你就自己定義了個(gè)def loss(x,y):return y-x )這樣肯定直接backward會(huì)出錯(cuò)。所以應(yīng)當(dāng)用nn里面提供的函數(shù)。
當(dāng)然搞深度學(xué)習(xí)不可能只用官方提供的loss函數(shù),所以如果你要想用自己的loss函數(shù)。
必須也把loss定義成上面Net的樣子(不然你的loss不能反向傳播,這點(diǎn)要注意,注:這點(diǎn)是以前寫的,很久之前的版本不行,現(xiàn)在都可以了,基本不太需要這樣了)。
也是繼承nn.Module,把傳入的參數(shù)放進(jìn)forward里面,具體的loss在forward里面算,最后return loss。__init__()就空著,寫個(gè)super().__init__就行了。
在反向傳播之后,第【2】和第【3】怎么實(shí)現(xiàn)?就是通過優(yōu)化器來實(shí)現(xiàn)。讓優(yōu)化器來自動(dòng)實(shí)現(xiàn)對(duì)網(wǎng)絡(luò)權(quán)重W的更新。
所以在Net定義完以后,需要寫一個(gè)優(yōu)化器的定義(選SGD方式為例):
from torch import optimoptimizer=optim.SGD(net.parameters(),lr=0.001,momentum=0.9)
同樣,優(yōu)化器也是一個(gè)類,先定義一個(gè)實(shí)例optimizer,然后之后會(huì)用。
注意在optimizer定義的時(shí)候,需要給SGD傳入了net的參數(shù)parameters,這樣之后優(yōu)化器就掌握了對(duì)網(wǎng)絡(luò)參數(shù)的控制權(quán),就能夠?qū)λM(jìn)行修改了。
傳入的時(shí)候把學(xué)習(xí)率lr也傳入了。
在每次迭代之前,先把optimizer里存的梯度清零一下(因?yàn)閃已經(jīng)更新過的“更新量”下一次就不需要用了)
optimizer.zero_grad()
在loss.backward()反向傳播以后,更新參數(shù):
optimizer.step()
所以我們的順序是:
1.先定義網(wǎng)絡(luò):寫網(wǎng)絡(luò)Net的Class,聲明網(wǎng)絡(luò)的實(shí)例net=Net(),
2.定義優(yōu)化器
optimizer=optim.xxx(net.parameters(),lr=xxx),
3.再定義損失函數(shù)(自己寫class或者直接用官方的,compute_loss=nn.MSELoss()或者其他。
4.在定義完之后,開始一次一次的循環(huán):
?、傧惹蹇諆?yōu)化器里的梯度信息,optimizer.zero_grad();
?、谠賹nput傳入,output=net(input) ,正向傳播
③算損失,loss=compute_loss(target,output) ##這里target就是參考標(biāo)準(zhǔn)值GT,需要自己準(zhǔn)備,和之前傳入的input一一對(duì)應(yīng)
?、苷`差反向傳播,loss.backward()
?、莞聟?shù),optimizer.step()
這樣就實(shí)現(xiàn)了一個(gè)基本的神經(jīng)網(wǎng)絡(luò)。大部分神經(jīng)網(wǎng)絡(luò)的訓(xùn)練都可以簡(jiǎn)化為這個(gè)過程,無非是傳入的內(nèi)容復(fù)雜,網(wǎng)絡(luò)定義復(fù)雜,損失函數(shù)復(fù)雜,等等等等。
說的有問題的地方感謝指正!
編輯:黃飛
評(píng)論