Pytorch 編寫代碼基本思想(代碼框架與流程)


作者:憶臻 (哈工大SCIR實驗室在讀博士生)
魏福煊 哈工大英才實驗班本科生
謝天寶 哈工大英才實驗班本科生

一、前言

在我們要用pytorch構建自己的深度學習模型的時候,基本上都是下面這個流程步驟,寫在這里讓一些新手童鞋學習的時候有一個大局感覺,無論是從自己寫,還是閱讀他人代碼,按照這個步驟思想(默念4大步驟,找數據定義、找model定義、(找損失函數、優化器定義),主循環代碼邏輯),直接去找對應的代碼塊,會簡單很多。

二、基本步驟思想

所有的深度學習模型過程都可以形式化如下圖:

分為四大步驟:

1、輸入處理模塊 (X 輸入數據,變成網絡能夠處理的Tensor類型)

2、模型構建模塊 (主要負責從輸入的數據,得到預測的y^, 這就是我們經常說的前向過程)

3、定義代價函數和優化器模塊 (注意,前向過程只會得到模型預測的結果,並不會自動求導和更新,是由這個模塊進行處理)

4、構建訓練過程 (迭代訓練過程,就是上圖表情包的訓練迭代過程)

這幾個模塊分別與上圖的數字標號1,2,3,4進行一一對應!

三、實例講解

知道了上面的宏觀思想之后,后面給出每個模塊稍微具體一點的解釋和具體一個例子,再幫助大家熟悉對應的代碼!

1.數據處理

對於數據處理,最為簡單的⽅式就是將數據組織成為⼀個 。但許多訓練需要⽤到mini-batch,直 接組織成Tensor不便於我們操作。pytorch為我們提供了Dataset和Dataloader兩個類來方便的構建。

torch.utils.data.Dataset

繼承Dataset 類需要override 以下⽅法:

torch.utils.data.DataLoader

torch.utils.data.DataLoader(dataset, batch_size=1, shuffle=False)

DataLoader Batch。如果選擇shuffle = True,每⼀個epoch 后,mini-Batch batch_size 常⻅的使⽤⽅法如下:

2. 模型構建

所有的模型都需要繼承torch.nn.Module , 需要實現以下⽅法:

其中forward() ⽅法是前向傳播的過程。在實現模型時,我們不需要考慮反向傳播。

3. 定義代價函數和優化器

這部分根據⾃⼰的需求去參照doc

4、構建訓練過程

pytorch的訓練循環⼤致如下:

下面再用一個簡單例子,來鞏固一下:

slides來自https://www.bilibili.com/video/BV1Y7411d7Ys?from=search&seid=3765076366663992699 slides來自https://www.bilibili.com/video/BV1Y7411d7Ys?from=search&seid=3765076366663992699 slides來自https://www.bilibili.com/video/BV1Y7411d7Ys?from=search&seid=3765076366663992699 slides來自https://www.bilibili.com/video/BV1Y7411d7Ys?from=search&seid=3765076366663992699

四、資源推薦

希望上面的講解能幫助新手童鞋建立一個基本的代碼邏輯輪廓,這里推薦幾個我覺得很好的資源:

1、第一個是B站劉二大人的入門Pytorch視頻,這是我見過入門最好的視頻資源之一,強烈推薦,上面的例子slides也均來自於此,地址如下

https://www.bilibili.com/video/BV1Y7411d7Ys?p=6​www.bilibili.com

2、其實入門之后,就不用看太多學習資料了,你是搞哪個方向的,推薦直接去看一下相關方向頂會論文實現,從配環境、debug看懂他的code,到調參到他論文的相近結果,功力會針對性提高很多。

希望文章對一些新手朋友有幫助~

編輯於 2020-06-20
原文鏈接:https://zhuanlan.zhihu.com/p/149579648
 

CNN訓練的6大流程:

零、 函數總體說明
from torch.utils.data import Dataset 所有數據讀取的基類,自定義數據讀取會用到,返回一個列表。然后用torch.utils.data.DataLoader封裝成Tensor。最后變variable。
from torchvision.dataset import ImageFolder :ImageFolder(root,transform,loader讀片讀取的方法)讀取 root/dog/xxx.png,返回一個列表,列表中的每個值都是一個tuple,每個tuple包含的是圖像路徑和標簽信息。所以需要轉換為DataLoader將其轉換tensor,在后期轉成Variable數據類型作為網絡輸入。注意:list是不能作為模型輸入的,因此在PyTorch中需要用另一個類來封裝list,那是:torch.utils.data.DataLoader
from torch.utils.data import DataLoader DataLoader(dataset or ImageFolder,batchsize,shuffle)將Dataset可以batch、shuffle、多線程讀取,torch.utils.data.DataLoader類可以將list類型的輸入數據封裝成Tensor數據格式,以備模型使用。,返回兩個Tensor,  data = inputs, labels
from torchvision import transfrom 預處理模塊
一、數據導入
這里采用官方寫好的torchvision.datasets.ImageFolder接口實現數據導入。這個接口需要你提供圖像所在的文件夾,就是下面的data_dir=‘/data’這句,然后對於一個分類問題,這里data_dir目錄下一般包括兩個文件夾:train和val,每個文件件下面包含N個子文件夾,N是你的分類類別數,且每個子文件夾里存放的就是這個類別的圖像。這樣torchvision.datasets.ImageFolder就會返回一個列表(比如下面代碼中的image_datasets[‘train’]或者image_datasets[‘val]),列表中的每個值都是一個tuple,每個tuple包含圖像和標簽信息。

data_dir = '/data'

自定義數據讀取+多gpu運算

1. torch.utils.data.Dataset基類 or torchvision.datasets.ImageFolder 將路徑和標簽變列表
根據訓練集和驗證集預處理的不同和路徑的不同,設置一個字典。

image_datasets = {x: ImageFolder(
                    os.path.join(data_dir, x),
                    data_transforms[x]),#data_transforms是一個字典
                    for x in ['train', 'val']}

  

變成一行,看for生成的字典:

image_datasets = {x: ImageFolder(os.path.join(data_dir, x),data_transforms[x]),for x in ['train', 'val']}

data_transforms = {
    'train': transforms.Compose([  #torchvision.transforms.Compose疊加transforms操作
        transforms.RandomSizedCrop(224),#輸入對象都是PIL Image,也就是用python的PIL庫讀進來的圖像內容
        transforms.RandomHorizontalFlip(),#輸入對象都是PIL Image,也就是用python的PIL庫讀進來的圖像內容
        transforms.ToTensor(),#生成Tensor
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])#輸入對象 Tensor
    ]),
    'val': transforms.Compose([
        transforms.Scale(256),#transforms.Scale(256)其實就是resize操作,目前已經被transforms.Resize類取代了。
        transforms.CenterCrop(224),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
    ]),
}

torch.utils.data.Dataset

class TensorDataset(Dataset): """Dataset wrapping data and target tensors.
    Each sample will be retrieved by indexing both tensors along the first
    dimension.
    Arguments:
        data_tensor (Tensor): contains sample data.
        target_tensor (Tensor): contains sample targets (labels).
    """ 
    def __init__(self, data_tensor, target_tensor): 
        assert data_tensor.size(0) == target_tensor.size(0) 
        self.data_tensor = data_tensor 
        self.target_tensor = target_tensor 
    def __getitem__(self, index): 
        return self.data_tensor[index], self.target_tensor[index] 
    def __len__(self): return self.data_tensor.size(0)

  


2. torch.utils.data.DataLoader對圖像和標簽列表分別封裝成一個Tensor
前面torchvision.datasets.ImageFolder只是返回list,list是不能作為模型輸入的,因此在PyTorch中需要用另一個類來封裝list,那就是:torch.utils.data.DataLoader。torch.utils.data.DataLoader類可以將list類型的輸入數據封裝成Tensor數據格式,以備模型使用。注意,這里是對圖像和標簽分別封裝成一個Tensor。這里要提到另一個很重要的類:torch.utils.data.Dataset,這是一個抽象類,在pytorch中所有和數據相關的類都要繼承這個類來實現。比如前面說的torchvision.datasets.ImageFolder類是這樣的,以及這里的torch.util.data.DataLoader類也是這樣的。自定義一個類讀取數據,自定義的這個類必須繼承自torch.utils.data.Dataset這個基類,最后同樣用torch.utils.data.DataLoader封裝成Tensor。

dataloders = {x: torch.utils.data.DataLoader(image_datasets[x], #加入ImageFolder輸出的圖片的名字
                                            batch_size=4,
                                            shuffle=True,
                                            num_workers=4)#
                                            for x in ['train', 'val']}

 class torch.utils.data.DataLoader(dataset, batch_size=1, shuffle=False, sampler=None, batch_sampler=None, num_workers=0, collate_fn=, pin_memory=False, drop_last=False, timeout=0, worker_init_fn=None)
參數:

dataset (Dataset): 加載數據的數據集
 batch_size (int, optional): 每批加載多少個樣本
 shuffle (bool, optional): 設置為“真”時,在每個epoch對數據打亂.(默認:False)
 sampler (Sampler, optional): 定義從數據集中提取樣本的策略,返回一個樣本
 batch_sampler (Sampler, optional): like sampler, but returns a batch of indices at a time 返回一批樣本. 與atch_size, shuffle, sampler和 drop_last互斥.
 num_workers (int, optional): 用於加載數據的子進程數。0表示數據將在主進程中加載​​。(默認:0)
 collate_fn (callable, optional): 合並樣本列表以形成一個 mini-batch.  # callable可調用對象
 pin_memory (bool, optional): 如果為 True, 數據加載器會將張量復制到 CUDA 固定內存中,然后再返回它們.
 drop_last (bool, optional): 設定為 True 如果數據集大小不能被批量大小整除的時候, 將丟掉最后一個不完整的batch,(默認:False).
 timeout (numeric, optional): 如果為正值,則為從工作人員收集批次的超時值。應始終是非負的。(默認:0)
 worker_init_fn (callable, optional): If not None, this will be called on each worker subprocess with the worker id (an int in ``[0, num_workers - 1]``) as input, after seeding and before data loading. (default: None).
 
3. 將Tensor數據類型封裝成Variable數據類型。
來看下面這段代碼。dataloaders是一個字典,dataloders[‘train’]存的就是訓練的數據,下面這個for循環就是從dataloders[‘train’]中讀取batch_size個數據,batch_size在前面生成dataloaders的時候就設置了。因此這個data里面包含圖像數據(inputs)這個Tensor和標簽(labels)這個Tensor。然后用torch.autograd.Variable將Tensor封裝成模型真正可以用的Variable數據類型。
 

for data in dataloders['train']:#dataloders迭代器
   inputs, labels = data#每次取出batch_size個數據

   if use_gpu:
       inputs = Variable(inputs.cuda())#兩個Variable
       labels = Variable(labels.cuda())#
   else:
       inputs, labels = Variable(inputs), Variable(labels)

補充:Sample
class torch.utils.data.sampler.Sampler(data_source)
參數: data_source (Dataset) – dataset to sample from
作用: 創建一個采樣器, class torch.utils.data.sampler.Sampler是所有的Sampler的基類, 其中,iter(self)函數來獲取一個迭代器,對數據集中元素的索引進行迭代,len(self)方法返回迭代器中包含元素的長度.  

class RandomSampler(Sampler): """Samples elements randomly, without replacement.
Arguments:
data_source (Dataset): dataset to sample from
""" def __init__(self, data_source): self.data_source = data_source def __iter__(self): return iter(torch.randperm(len(self.data_source)).long()) def __len__(self): return len(self.data_source)
class sampler(Sampler):
def __init__(self, train_size, batch_size):
"""

  

返回一個所有的rand_num_view排序的迭代器,至於batchsize放到dataloader里面,這里面存着一個batch接着一個batch.
(1)batchsize:每批數據量的大小。DL通常用SGD的優化算法進行訓練,也就是一次(1 個iteration)一起訓練batchsize個樣本,計算它們的平均損失函數值,來更新參數。
(2)iteration:1個iteration即迭代一次,也就是用batchsize個樣本訓練一次。
"""
self.num_data = train_size # 訓練大小
self.num_per_batch = int(train_size / batch_size) #iteration 總數據/批處理大小
self.batch_size = batch_size
self.range = torch.arange(0,batch_size).view(1, batch_size).long()
self.leftover_flag = False
if train_size % batch_size:
self.leftover = torch.arange(self.num_per_batch*batch_size, train_size).long()
self.leftover_flag = True
def __iter__(self):
#torch.randperm(n, out=None) → LongTensor 給定參數n,返回一個從0 到n -1 的隨機整數排列。
rand_num = torch.randperm(self.num_per_batch).view(-1,1) * self.batch_size
self.rand_num = rand_num.expand(self.num_per_batch, self.batch_size) + self.range

self.rand_num_view = self.rand_num.view(-1)

if self.leftover_flag:
self.rand_num_view = torch.cat((self.rand_num_view, self.leftover),0)

return iter(self.rand_num_view)#返回一個所有的rand_num_view排序的迭代器

def __len__(self):
return self.num_data

  



 

二、導入你的模型
封裝好了數據后,就可以作為模型的輸入了。所以要先導入你的模型。在PyTorch中已經默認為大家准備了一些常用的網絡結構,比如分類中的VGG,ResNet,DenseNet等等,可以用torchvision.models模塊來導入。比如用torchvision.models.resnet18(pretrained=True)來導入ResNet18網絡,同時指明導入的是已經預訓練過的網絡。因為預訓練網絡一般是在1000類的ImageNet數據集上進行的,所以要遷移到你自己數據集的2分類,需要替換最后的全連接層為你所需要的輸出。因此下面這三行代碼進行的就是用models模塊導入resnet18網絡,然后獲取全連接層的輸入channel個數,用這個channel個數和你要做的分類類別數(這里是2)替換原來模型中的全連接層。這樣網絡結果也准備好。

model = models.resnet18(pretrained=True)
num_ftrs = model.fc.in_features#獲取全連接層的輸入channel個數
model.fc = nn.Linear(num_ftrs, 2)

  

三、定義損失函數criterion = nn.CrossEntropyLoss()
在PyTorch中采用torch.nn模塊來定義網絡的所有層,比如卷積、降采樣、損失層等等,這里采用交叉熵函數,因此可以這樣定義:

criterion = nn.CrossEntropyLoss()

  

四、定義優化函數optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)
在PyTorch中是通過torch.optim模塊來實現的。另外這里雖然寫的是SGD,但是因為有momentum,所以是Adam的優化方式。這個類的輸入包括需要優化的參數:model.parameters(),學習率,還有Adam相關的momentum參數。現在很多優化方式的默認定義形式就是這樣的。

optimizer = optim.SGD(model.parameters(), lr=0.001, momentum=0.9)

  

 

五、定義學習率的變化策略scheduler = lr_scheduler.StepLR(optimizer, step_size=7, gamma=0.1)
這里采用的是torch.optim.lr_scheduler模塊的StepLR類,表示每隔step_size個epoch就將學習率降為原來的gamma倍。

scheduler = lr_scheduler.StepLR(optimizer, step_size=7, gamma=0.1)

  

 

六、開始訓練
1. 更新下學習率 scheduler.step()【制定了學習率的變化策略的原因】
這是因為我們前面制定了學習率的變化策略,所以在每個epoch開始時都要更新下:

scheduler.step()

  

2. 設置模型狀態為訓練狀態 model.train(True)

model.train(True)

  

3. 所有梯度置0 model.zero_grad()
# Zero the gradients before running the backward pass.
    model.zero_grad()

model.zero_grad()

  

4. 網絡的前向傳播model(inputs)

outputs = model(inputs)

  

5. 得到損失criterion(outputs, labels)
然后將輸出的outputs和原來導入的labels作為loss函數的輸入就可以得到損失了:

loss = criterion(outputs, labels)

  


6. torch.max預測該樣本屬於哪個類別的信息
輸出的outputs也是torch.autograd.Variable格式,得到輸出后(網絡的全連接層的輸出)還希望能到到模型預測該樣本屬於哪個類別的信息,這里采用torch.max。torch.max()的第一個輸入是tensor格式,所以用outputs.data而不是outputs作為輸入;第二個參數1是代表dim的意思,也就是取每一行的最大值,其實就是我們常見的取概率最大的那個index;第三個參數loss也是torch.autograd.Variable格式。

 _, preds = torch.max(outputs.data, 1)

  

torch.max()返回的是兩個Variable,第一個Variable存的是最大值,第二個存的是其對應的位置索引index。這里我們想要得到的是索引,所以后面用[1]。

7. 梯度置0

optimizer.zero_grad()
# Before the backward pass, use the optimizer object to zero all of the
# gradients for the variables it will update (which are the learnable weights
# of the model)

  

根據pytorch中的backward()函數的計算,當網絡參量進行反饋時,梯度是被積累的而不是被替換掉;但是在每一個batch時毫無疑問並不需要將兩個batch的梯度混合起來累積,因此這里就需要每個batch設置一遍zero_grad 了。

8. loss.backward()回傳損失,過程中會計算梯度
計算得到loss后就要回傳損失。要注意的是這是在訓練的時候才會有的操作,測試時候只有forward過程。

loss.backward()

  

9. 根據這些梯度更新參數 optimizer.step()
回傳損失過程中會計算梯度,然后需要根據這些梯度更新參數,optimizer.step()就是用來更新參數的。optimizer.step()后,你就可以從optimizer.param_groups[0][‘params’]里面看到各個層的梯度和權值信息。

optimizer.step()

  

這樣一個batch數據的訓練就結束了!當你不斷重復這樣的訓練過程,最終就可以達到你想要的結果了。

0. 判斷你是否有gpu可以用use_gpu = torch.cuda.is_available()
另外如果你有gpu可用,那么包括你的數據和模型都可以在gpu上操作,這在PyTorch中也非常簡單。判斷你是否有gpu可以用可以通過下面這行代碼,如果有,則use_gpu是true。

use_gpu = torch.cuda.is_available()

  

 

完整代碼請移步:Github

整理自:https://blog.csdn.net/u014380165/article/details/78525273  
————————————————
版權聲明:本文為CSDN博主「Snoopy_Dream」的原創文章,遵循CC 4.0 BY-SA版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/e01528/article/details/83894811

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM