pytorch學(xué)習(xí)

對教材《深度學(xué)習(xí)框架pytorch:入門與實(shí)踐》和一些技術(shù)博客及實(shí)踐過程中的總結(jié),比較精煉。

網(wǎng)絡(luò)模型定義

一般通過繼承pytoch實(shí)現(xiàn)的torch.nn.Module(以下簡稱Module),然后定義自己的網(wǎng)絡(luò)的每層。
這里我們先通過繼承Module構(gòu)建一個抽象的網(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()(以上就是這么做的)而不是直接保存整個模型或優(yōu)化器,可以保存其參數(shù)和動量信息。

基于以上的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)一個nn.functional中的函數(shù)):
        # 前者是nn.Module的子類,后者是nn.functional的子類
        # nn.中許多l(xiāng)ayer和激活函數(shù)都是繼承與nn.Module的可以自動提取派生類中的可學(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的輸入輸出自動連接起來,自己來定義layer間的組合而得到一個復(fù)合layer(代碼結(jié)構(gòu)意義上的一個,只需要給定整個sequential一個輸入,就能得出一個輸出,而不需要一層一層的輸入輸出和第二種方法有差別),這樣的好處是定義簡潔、且通過對本model的控制如models.train()、models.eval()就可以自動的設(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能自動提取內(nèi)部的類型為nn.parameter可學(xué)習(xí)參數(shù)(需要變化的參數(shù)),而nn.functional中的函數(shù)更像是純函數(shù)調(diào)用時必須給輸入如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)前訓(xùn)練狀態(tài)F.dropout(input,training=self.training)。

  • 然后就是實(shí)現(xiàn)forward。給網(wǎng)絡(luò)一個輸入即output=model(input)其內(nèi)部會調(diào)用model.__call__(nn.Module實(shí)現(xiàn)),call中主要是調(diào)用了forward。forward主要負(fù)責(zé)計算圖的構(gòu)建,然后我們就能根據(jù)計算圖進(jìn)行自動求導(dǎo)計算。

  • 這里說一下以上內(nèi)容的實(shí)現(xiàn)原理:

    1. 為何nn.Module可以自動提取子類內(nèi)部的可學(xué)習(xí)參數(shù):
      因?yàn)?code>nn.Module中實(shí)現(xiàn)了兩個魔法方法.__getattr__和.__setattr__。而obj.name=value 等價于setattr(obj,'name',value)obj.name 等價于 getattr(obj,'name'),當(dāng)定義了自定義方法后getattr和setattr會調(diào)用自定義的.__getattr__和.__setattr__。然后,當(dāng)用戶在子類定義實(shí)例屬性時候就會觸發(fā).__setattr__(obj,'name',value),在此函數(shù)中會判斷obj類型是否是Parameter或nn.Module對象,如果是就存放到_parameters和_models兩個私有字典中;如果是其他對象,則調(diào)用默認(rèn)操作:添加到__dict__字典中。而_models中的都是子models,他們在定義的時候也會在其內(nèi)部的._models字典中存放其子models。最終可能是會對本自定義models內(nèi)的所有子models(含迭代)的parameter進(jìn)行一些管理。

    2. 是如何實(shí)現(xiàn)自動求導(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梯度計算函數(shù)、requires_grad是否需要自動求導(dǎo)功能),使其能進(jìn)行自動求導(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會自動累加歷次backward的梯度值。因此調(diào)用反向傳播時需要清零梯度x.grad.data.zero_()inplace操作清零。
      c. Parametervariable的子類,且默認(rèn)是requires_grad=True。能在定義Model時候自動被基類nn.Module管理。

    3. 什么是計算圖(pytorch是動態(tài)圖):計算圖是一種有向無環(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)算時,會自動根據(jù)重載或以實(shí)現(xiàn)的運(yùn)算函數(shù)(都是算子)進(jìn)行邊的連接(輸入輸出關(guān)系,是有向的)(并在算子內(nèi)儲存一份輸入輸出拷貝用來梯度計算時使用),然后生成一個圖。圖中任意一點(diǎn)都可以利用鏈?zhǔn)椒▌t實(shí)現(xiàn)??!因此當(dāng)對requires_grad的葉子節(jié)點(diǎn)求導(dǎo)時,中間變量Variable都會自動.requires_grad=True,但非葉子節(jié)點(diǎn)計算完后會清空梯度。此外是利用以下幾個規(guī)則來進(jìn)行鏈?zhǔn)椒▌t的:當(dāng)y是標(biāo)量的時候可以進(jìn)行y.backward(),但是y是向量(認(rèn)為是中間variable)其求梯度必須輸入一個參數(shù)y.backward(grad_y)否則會提示只能進(jìn)行標(biāo)量對向量求梯度,其中grad_y=標(biāo)量目標(biāo)函數(shù)z對向量y求偏導(dǎo)。

    4. 目前大多數(shù)算子都可以使用autograd的反向求導(dǎo),自己寫的復(fù)雜函數(shù)沒有,如何解決:自己實(shí)現(xiàn)的復(fù)雜函數(shù)(算子)不具備自動求導(dǎo)功能(比如y=x**2+x*2支持,但y=func(x)的func不支持)。可以利用autograd.Function對autograd功能進(jìn)行拓展,其中Function對應(yīng)計算圖中的□。需要自己實(shí)現(xiàn)一個繼承與autograd.Function的算子,并自己實(shí)現(xiàn)求導(dǎo)。這樣就可以在計算圖使用它并使得鏈?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(...),...)會自動調(diào)用forward并提取Variable中的data(tensor類型)作為輸入然后把輸出的tensor包裝成Variable,z.backward()就會自動調(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ù)計算時候的參數(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)化策略從而計算出網(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ù)覺得是否用動量)、Adam...
這里注意:學(xué)習(xí)率和weight_decay(可能導(dǎo)致學(xué)習(xí)率為0)要小心設(shè)計否則優(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í)際值和輸出值計算損失函數(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:計算損失函數(shù) -> ) 獲得輸出 -> 輸出和已知標(biāo)簽對比 -> 計算準(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é)果了。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容