深度學習框架PyTorch一書的學習-第五章-常用工具模塊


https://github.com/chenyuntc/pytorch-book/blob/v1.0/chapter5-常用工具/chapter5.ipynb

希望大家直接到上面的網址去查看代碼,下面是本人的筆記

 

在訓練神經網絡過程中,需要用到很多工具,其中最重要的三部分是:數據、可視化和GPU加速。本章主要介紹Pytorch在這幾方面的工具模塊,合理使用這些工具能夠極大地提高編碼效率。

1.數據處理

PyTorch提供了幾個高效便捷的工具,以便使用者進行數據處理或增強等操作,同時可通過並行化加速數據加載。

基本原理就是使用Dataset提供數據集的封裝,再使用Dataloader實現數據並行加載

1)Dataset

在PyTorch中,數據加載可通過自定義的數據集對象。數據集對象被抽象為Dataset類,實現自定義的數據集需要繼承Dataset,並實現兩個Python魔法方法:

  • __getitem__:返回一條數據,或一個樣本。obj[index]等價於obj.__getitem__(index)
  • __len__:返回樣本的數量。len(obj)等價於obj.__len__()

這里我們以Kaggle經典挑戰賽"Dogs vs. Cat"的數據為例,來詳細講解如何處理數據。"Dogs vs. Cats"是一個分類問題,判斷一張圖片是狗還是貓,其所有圖片都存放在一個文件夾下,根據文件名的前綴判斷是狗還是貓。

import torch as t
from torch.utils import data

自定義數據集加載函數:

import os
from PIL import Image
import numpy as np

#加載數據
class DogCat(data.Dataset):
    def __init__(self, root):
        imgs = os.listdir(root)
        #所有圖片的絕對路徑
        #這里不實際加載圖片,只是指定路徑,當調用__getitem__時才會真正讀圖片
        self.imgs = [os.path.join(root, img) for img in imgs]
        
    def __getitem__(self, index): #生成圖片數據及其標簽數據
        img_path = self.imgs[index]
        #dog則label為1,cat則為0
        label = 1 if 'dog' in img_path.split('/')[-1] else 0
        pil_img = Image.open(img_path)
        array = np.asarray(pil_img)
        data = t.from_numpy(array)
        return data, label
    
    def __len__(self): #得到數據大小信息
        return len(self.imgs)

測試:

dataset = DogCat('./data/dogcat/')
img, label = dataset[0] #相當於調用dataset.__getitem__(0)
for img, label in dataset:
    print(img.size(), img.float().mean(), label)

返回:

torch.Size([375, 499, 3]) tensor(150.5079) 1
torch.Size([500, 497, 3]) tensor(106.4915) 0
torch.Size([499, 379, 3]) tensor(171.8085) 0
torch.Size([375, 499, 3]) tensor(116.8138) 1
torch.Size([374, 499, 3]) tensor(115.5177) 0
torch.Size([236, 289, 3]) tensor(130.3004) 0
torch.Size([377, 499, 3]) tensor(151.7174) 1
torch.Size([400, 300, 3]) tensor(128.1550) 1

通過上面的代碼,我們學習了如何自定義自己的數據集,並可以依次獲取。但這里返回的數據不適合實際使用,因其具有如下兩方面問題:

  • 返回樣本的形狀不一,因每張圖片的大小不一樣,這對於需要取batch訓練的神經網絡來說很不友好
  • 返回樣本的數值較大,未歸一化至[-1, 1]

針對上述問題,PyTorch提供了torchvision1。它是一個視覺工具包,提供了很多視覺圖像處理的工具,其中transforms模塊提供了對PIL Image對象和Tensor對象的常用操作。

對PIL Image的操作包括:

  • Scale:調整圖片尺寸,長寬比保持不變
  • CenterCropRandomCropRandomResizedCrop: 裁剪圖片
  • Pad:填充
  • ToTensor:將PIL Image對象轉成Tensor,會自動將[0, 255]歸一化至[0, 1]

對Tensor的操作包括:

  • Normalize:標准化,即減均值,除以標准差
  • ToPILImage:將Tensor轉為PIL Image對象

如果要對圖片進行多個操作,可通過Compose函數將這些操作拼接起來,類似於nn.Sequential。注意,這些操作定義后是以函數的形式存在,真正使用時需調用它的__call__方法,這點類似於nn.Module。例如要將圖片調整為$224\times 224$,首先應構建這個操作trans = Resize((224, 224)),然后調用trans(img)

下面我們就用transforms的這些操作來優化上面實現的dataset

import os
from PIL import Image
import numpy as np
from torchvision import transforms as T

transform = T.Compose([
    T.Resize(224), #縮放圖片,保持長寬比不變,最短邊為224像素
    T.CenterCrop(224), #從圖片中間切出224*224的圖片
    T.ToTensor(), #將圖片(Image)轉成Tensor,歸一化至[0, 1]
    T.Normalize(mean=[.5, .5, .5], std=[.5, .5, .5]) # 標准化至[-1, 1],規定均值和標准差,即減均值,除以標准差
])

#加載數據
class DogCat(data.Dataset):
    def __init__(self, root, transforms=None):
        imgs = os.listdir(root)
        #所有圖片的絕對路徑
        #這里不實際加載圖片,只是指定路徑,當調用__getitem__時才會真正讀圖片
        self.imgs = [os.path.join(root, img) for img in imgs]
        self.transforms = transform
        
    def __getitem__(self, index): #生成圖片數據及其標簽數據
        img_path = self.imgs[index]
        #dog則label為1,cat則為0
        label = 0 if 'dog' in img_path.split('/')[-1] else 1
        data = Image.open(img_path)
        if self.transforms:
            data = self.transforms(data)
        return data, label
    
    def __len__(self): #得到數據大小信息
        return len(self.imgs)
    
dataset = DogCat('./data/dogcat/', transforms=transform)
img, label = dataset[0]
for img, label in dataset:
    print(img.size(), label)

返回:

torch.Size([3, 224, 224]) 0
torch.Size([3, 224, 224]) 1
torch.Size([3, 224, 224]) 1
torch.Size([3, 224, 224]) 0
torch.Size([3, 224, 224]) 1
torch.Size([3, 224, 224]) 1
torch.Size([3, 224, 224]) 0
torch.Size([3, 224, 224]) 0

 

除了上述操作之外,transforms還可通過Lambda封裝自定義的轉換策略。例如想對PIL Image進行隨機旋轉,則可寫成這樣:

trans=T.Lambda(lambda img: img.rotate(random()*360))

torchvision已經預先實現了常用的Dataset,包括前面使用過的CIFAR-10,以及ImageNet、COCO、MNIST、LSUN等數據集,可通過諸如torchvision.datasets.CIFAR10來調用,具體使用方法請參看官方文檔1。在這里介紹一個會經常使用到的Dataset——ImageFolder,它的實現和上述的DogCat很相似。

ImageFolder假設所有的文件按文件夾保存,每個文件夾下存儲同一個類別的圖片,文件夾名為類名,其構造函數如下:

ImageFolder(root, transform=None, target_transform=None, loader=default_loader)

它主要有四個參數:

  • root:在root指定的路徑下尋找圖片
  • transform:對PIL Image進行的轉換操作,transform的輸入是使用loader讀取圖片的返回對象
  • target_transform:對label的轉換
  • loader:給定路徑后如何讀取圖片,默認讀取為RGB格式的PIL Image對象

label是按照文件夾名順序排序后存成字典,即{類名:類序號(從0開始)},一般來說最好直接將文件夾命名為從0開始的數字,這樣會和ImageFolder實際的label一致,如果不是這種命名規范,建議看看self.class_to_idx屬性以了解label和文件夾名的映射關系。

有以下成員變量:

  • self.classes - 用一個list保存 類名
  • self.class_to_idx - 類名對應的 索引
  • self.imgs - 保存(圖片路徑, 圖片class) tuple的list

比如文件:

root/dog/xxx.png
root/dog/xxy.png
root/dog/xxz.png

root/cat/123.png
root/cat/nsdf3.png
root/cat/asd932_.png

調用為:

dset.ImageFolder(root="root folder path", [transform, target_transform])

 

開始操作:

from torchvision.datasets import ImageFolder
dataset = ImageFolder('data/dogcat_2/')

查看:

# cat文件夾的圖片對應label 0,dog對應1
dataset.class_to_idx

返回:

{'cat': 0, 'dog': 1}

 

#圖片對應的類別
dataset.classes

返回:

['cat', 'dog']

 

# 所有圖片的路徑和對應的label
dataset.imgs

返回:

[('data/dogcat_2/cat/cat.12484.jpg', 0),
 ('data/dogcat_2/cat/cat.12485.jpg', 0),
 ('data/dogcat_2/cat/cat.12486.jpg', 0),
 ('data/dogcat_2/cat/cat.12487.jpg', 0),
 ('data/dogcat_2/dog/dog.12496.jpg', 1),
 ('data/dogcat_2/dog/dog.12497.jpg', 1),
 ('data/dogcat_2/dog/dog.12498.jpg', 1),
 ('data/dogcat_2/dog/dog.12499.jpg', 1)]

 

# 沒有任何的transform,所以返回的還是PIL Image對象
print(dataset[0][1]) # 第一維是第幾張圖,第二維為1返回label,返回0
dataset[0][0] # 為0返回圖片數據

圖示:

定義transform:

#加上transform
#給定均值:(R,G,B) 方差:(R,G,B),將會把Tensor正則化。
#即:Normalized_image=(image-mean)/std
normalize = T.Normalize(mean=[0.4, 0.4, 0.4], std=[0.2, 0.2, 0.2])
transform = T.Compose([
    T.RandomResizedCrop(224), #先將給定的PIL.Image隨機切,然后再resize成給定的size大小
    T.RandomHorizontalFlip(), #隨機水平翻轉,概率為0.5。即:一半的概率翻轉,一半的概率不翻轉
    #把一個取值范圍是[0,255]的PIL.Image或者shape為(H,W,C)的numpy.ndarray,
    #轉換成形狀為[C,H,W],取值范圍是[0,1.0]的torch.FloadTensor    
    T.ToTensor(),
    normalize,
])

獲取圖片數據:

dataset = ImageFolder('data/dogcat_2/', transform=transform)

查看圖片數據大小:

## 深度學習中圖片數據一般保存成CxHxW,即通道數x圖片高x圖片寬
dataset[0][0].size()

返回:

torch.Size([3, 224, 224])

將tensor圖片數據變回圖片:

to_img = T.ToPILImage()
#將dataset[0][0]的圖片數據通過和標准差和均值的計算重新返回成圖片
to_img(dataset[0][0]*0.2+0.4)

圖示:

 

2.DataLoader
Dataset
只負責數據的抽象,一次調用__getitem__只返回一個樣本。前面提到過,在訓練神經網絡時,最好是對一個batch的數據進行操作,同時還需要對數據進行shuffle和並行加速等。對此,PyTorch提供了DataLoader幫助我們實現這些功能。

 

DataLoader的函數定義如下: 

DataLoader(dataset, batch_size=1, shuffle=False, sampler=None, num_workers=0, collate_fn=default_collate, pin_memory=False, drop_last=False)
  • dataset:加載的數據集(Dataset對象)
  • batch_size:batch size
  • shuffle::是否將數據打亂
  • sampler: 樣本抽樣,后續會詳細介紹
  • num_workers:使用多進程加載的進程數,0代表不使用多進程
  • collate_fn: 如何將多個樣本數據拼接成一個batch,一般使用默認的拼接方式即可
  • pin_memory:是否將數據保存在pin memory區,pin memory中的數據轉到GPU會快一些
  • drop_last:dataset中的數據個數可能不是batch_size的整數倍,drop_last為True會將多出來不足一個batch的數據丟棄

 下面舉例說明:

from torch.utils.data import DataLoader

 

#上面對數據處理后得到dataset,下面使用DataLoader實現其他操作
dataloader = DataLoader(dataset, batch_size=3, shuffle=True, num_workers=0, drop_last=False)

 

dataiter = iter(dataloader)
imgs, labels = next(dataiter)
imgs.size() # batch_size, channel, height, weight

返回:

torch.Size([3, 3, 224, 224])

dataloader是一個可迭代的對象,意味着我們可以像使用迭代器一樣使用它,例如:

for batch_datas, batch_labels in dataloader:
    train()

dataiter = iter(dataloader)
batch_datas, batch_labesl = next(dataiter)

 

1》圖片出問題時的解決方法:

1)將出錯的樣本剔除

在數據處理中,有時會出現某個樣本無法讀取等問題,比如某張圖片損壞。這時在__getitem__函數中將出現異常,此時最好的解決方案即是將出錯的樣本剔除。如果實在是遇到這種情況無法處理,則可以返回None對象,然后在Dataloader中實現自定義的collate_fn,將空對象過濾掉。但要注意,在這種情況下dataloader返回的batch數目會少於batch_size。

下面舉例說明:

class NewDogCat(DogCat): #繼承前面實現的DogCat數據集
    def __getitem__(self, index):
        try:
            # 調用父類的獲取函數,即 DogCat.__getitem__(self, index)
            return super(NewDogCat, self).__getitem__(index)
        except:
            return None, None
from torch.utils.data.dataloader import default_collate #導入默認的拼接方式
#自定義
def my_collate_fn(batch):
    '''
    batch中每個元素形如(data, label)
    '''
    # 過濾為None的數據
    batch = list(filter(lambda x : x[0] is not None, batch))
    if len(batch) == 0: return t.Tensor()
    return default_collate(batch) # 用默認方式拼接過濾后的batch數據

獲取dataset:

#會去調用父類DogCat生成dataset
dataset = NewDogCat('data/dogcat_wrong/', transforms=transform) 

查看:

dataset[5]

返回:

(tensor([[[ 0.9804,  1.0196,  1.0980,  ..., -1.1765, -1.1373, -1.1176],
          [ 0.9804,  1.0196,  1.0980,  ..., -1.1765, -1.1373, -1.1176],
          [ 1.0588,  1.0980,  1.1765,  ..., -1.1961, -1.1569, -1.1373],
          ...,
          [ 2.2941,  2.2941,  2.3137,  ...,  2.4902,  2.5098,  2.5098],
          [ 2.2941,  2.2941,  2.3137,  ...,  2.4902,  2.4902,  2.4902],
          [ 2.2941,  2.2941,  2.3137,  ...,  2.4902,  2.4902,  2.4902]],
 
         [[ 0.8824,  0.9216,  1.0000,  ..., -1.2157, -1.1765, -1.1569],
          [ 0.8824,  0.9216,  1.0000,  ..., -1.2157, -1.1765, -1.1569],
          [ 0.9608,  1.0000,  1.0784,  ..., -1.2353, -1.1961, -1.1765],
          ...,
          [ 2.1961,  2.1961,  2.2157,  ...,  2.4902,  2.4902,  2.4902],
          [ 2.1961,  2.1961,  2.2157,  ...,  2.4902,  2.4902,  2.4902],
          [ 2.1961,  2.1961,  2.2157,  ...,  2.4902,  2.4902,  2.4902]],
 
         [[ 0.8235,  0.8627,  0.9412,  ..., -1.1961, -1.1569, -1.1373],
          [ 0.8235,  0.8627,  0.9412,  ..., -1.1961, -1.1569, -1.1373],
          [ 0.9020,  0.9412,  1.0196,  ..., -1.2157, -1.1765, -1.1569],
          ...,
          [ 2.0784,  2.0784,  2.0980,  ...,  2.3529,  2.3529,  2.3529],
          [ 2.0784,  2.0784,  2.0980,  ...,  2.3333,  2.3333,  2.3333],
          [ 2.0784,  2.0784,  2.0980,  ...,  2.3333,  2.3333,  2.3333]]]), 1)

得到dataloader:

#然后對dataset進行處理,使用自定義的collate_fn
dataloader = DataLoader(dataset, 2, collate_fn=my_collate_fn, num_workers=1,shuffle=True)
for batch_datas, batch_labels in dataloader:
    print(batch_datas.size(), batch_labels.size())
    

返回:

torch.Size([2, 3, 224, 224]) torch.Size([2])
torch.Size([2, 3, 224, 224]) torch.Size([2])
torch.Size([1, 3, 224, 224]) torch.Size([1])
torch.Size([2, 3, 224, 224]) torch.Size([2])
torch.Size([1, 3, 224, 224]) torch.Size([1])

來看一下上述batch_size的大小。其中第3個的batch_size為1,這是因為有一張圖片損壞,導致其無法正常返回。而最后1個的batch_size也為1,這是因為共有9張(包括損壞的文件)圖片,無法整除2(batch_size),因此最后一個batch的數據會少於batch_szie,可通過指定drop_last=True來丟棄最后一個不足batch_size的batch。

 

2)隨機取一張圖片代替

對於諸如樣本損壞或數據集加載異常等情況,還可以通過其它方式解決。例如但凡遇到異常情況,就隨機取一張圖片代替:

import random
class NewDogCat(DogCat): #繼承前面實現的DogCat數據集
    def __getitem__(self, index):
        try:
            # 調用父類的獲取函數,即 DogCat.__getitem__(self, index)
            return super(NewDogCat, self).__getitem__(index)
        except:
            #更改這里成隨機選取一個圖片替代
            new_index = random.randint(0,len(self)-1)
            return self[new_index]
from torch.utils.data.dataloader import default_collate #導入默認的拼接方式
#自定義
def my_collate_fn(batch):
    '''
    batch中每個元素形如(data, label)
    '''
    # 過濾為None的數據
    batch = list(filter(lambda x : x[0] is not None, batch))
    if len(batch) == 0: return t.Tensor()
    return default_collate(batch) # 用默認方式拼接過濾后的batch數據

生成dataset:

#會去調用父類DogCat生成dataset
dataset = NewDogCat('data/dogcat_wrong/', transforms=transform) 

生成dataloader並調用:

#然后對dataset進行處理,使用自定義的collate_fn
dataloader = DataLoader(dataset, 2, collate_fn=my_collate_fn, num_workers=1,shuffle=True)
for batch_datas, batch_labels in dataloader:
    print(batch_datas.size(), batch_labels.size())
    

返回:

torch.Size([2, 3, 224, 224]) torch.Size([2])
torch.Size([2, 3, 224, 224]) torch.Size([2])
torch.Size([2, 3, 224, 224]) torch.Size([2])
torch.Size([2, 3, 224, 224]) torch.Size([2])
torch.Size([1, 3, 224, 224]) torch.Size([1]) #這是因為只有9張數據

相比較丟棄異常圖片而言,這種做法會更好一些,因為它能保證每個batch的數目仍是batch_size

 

3)徹底清洗

 但在大多數情況下,最好的方式還是對數據進行徹底清洗。

 

2》DataLoader的多進程-multiprocessing
DataLoader里面並沒有太多的魔法方法,它封裝了Python的標准庫multiprocessing,使其能夠實現多進程加速。在此提幾點關於Dataset和DataLoader使用方面的建議:

  1. 高負載的操作放在__getitem__中,如加載圖片等。
  2. dataset中應盡量只包含只讀對象,避免修改任何可變對象,利用多線程進行操作。

第一點是因為多進程會並行的調用__getitem__函數,將負載高的放在__getitem__函數中能夠實現並行加速。

第二點是因為dataloader使用多進程加載,如果在Dataset實現中使用了可變對象,可能會有意想不到的沖突。在多線程/多進程中,修改一個可變對象,需要加鎖,但是dataloader的設計使得其很難加鎖(在實際使用中也應盡量避免鎖的存在),因此最好避免在dataset中修改可變對象。

例如下面就是一個不好的例子,在多進程處理中self.num可能與預期不符,這種問題不會報錯,因此難以發現。如果一定要修改可變對象,建議使用Python標准庫Queue中的相關數據結構。

class BadDataset(Dataset):
    def __init__(self):
        self.datas = range(100)
        self.num = 0 # 取數據的次數
    def __getitem__(self, index):
        self.num += 1
        return self.datas[index]

 

使用Python multiprocessing庫的另一個問題是,在使用多進程時,如果主程序異常終止(比如用Ctrl+C強行退出),相應的數據加載進程可能無法正常退出。這時你可能會發現程序已經退出了,但GPU顯存和內存依舊被占用着,或通過topps aux依舊能夠看到已經退出的程序,這時就需要手動強行殺掉進程。建議使用如下命令:

ps x | grep <cmdline> | awk '{print $1}' | xargs kill
  • ps x:獲取當前用戶的所有進程
  • grep <cmdline>:找到已經停止的PyTorch程序的進程,例如你是通過python train.py啟動的,那你就需要寫grep 'python train.py'
  • awk '{print $1}':獲取進程的pid
  • xargs kill:殺掉進程,根據需要可能要寫成xargs kill -9強制殺掉進程

在執行這句命令之前,建議先打印確認一下是否會誤殺其它進程

ps x | grep <cmdline> | ps x

 

3》sampler——采樣(shuffle=True時使用)

PyTorch中還單獨提供了一個sampler模塊,用來對數據進行采樣。

常用的有隨機采樣器:RandomSampler,當dataloader的shuffle參數為True時,系統會自動調用這個采樣器,實現打亂數據。默認的是采用SequentialSampler,它會按順序一個一個進行采樣。

這里介紹另外一個很有用的采樣方法: WeightedRandomSampler,它會根據每個樣本的權重選取數據,在樣本比例不均衡的問題中,可用它來進行重采樣。

構建WeightedRandomSampler時需提供兩個參數:

  • 每個樣本的權重weights
  • 共選取的樣本總數num_samples

以及一個可選參數replacement

replacement用於指定是否可以重復選取某一個樣本,默認為True,即允許在一個epoch中重復采樣某一個數據。如果設為False,則當某一類的樣本被全部選取完,但其樣本數目仍未達到num_samples時,sampler將不會再從該類中選擇數據,此時可能導致weights參數失效。

權重越大的樣本被選中的概率越大,待選取的樣本數目一般小於全部的樣本數目。

下面舉例說明:

dataset = DogCat('data/dogcat/', transforms=transform)

# 狗的圖片被取出的概率是貓的概率的兩倍,狗是1,貓是0
# 兩類圖片被取出的概率與weights的絕對大小無關,只和比值有關
weights = [2 if label == 1 else 1 for data, label in dataset]
weights

返回:

[1, 2, 2, 1, 2, 2, 1, 1]

 

from torch.utils.data.sampler import WeightedRandomSampler
sampler = WeightedRandomSampler(weights, num_samples=9,replacement=True)
dataloader = DataLoader(dataset, batch_size=3,sampler=sampler)
for datas, labels in dataloader:
    print(labels.tolist())

返回:

[1, 1, 0]
[0, 1, 1]
[1, 1, 0]

可見狗貓樣本比例約為2:1,另外一共只有8個樣本,但是卻返回了9個,說明肯定有被重復返回的,這就是replacement參數的作用,下面將replacement設為False試試:

sampler = WeightedRandomSampler(weights, 8, replacement=False)
dataloader = DataLoader(dataset, batch_size=4, sampler=sampler)
for datas, labels in dataloader:
    print(labels.tolist())

返回:

[1, 0, 0, 1]
[1, 1, 0, 0]

在這種情況下,num_samples等於dataset的樣本總數,為了不重復選取,sampler會將每個樣本都返回,這樣就失去weight參數的意義了。

從上面的例子可見sampler在樣本采樣中的作用:如果指定了sampler,shuffle將不再生效,並且sampler.num_samples會覆蓋dataset的實際大小,即一個epoch返回的圖片總數取決於sampler.num_samples

 

3.計算機視覺工具包:torchvision

計算機視覺是深度學習中最重要的一類應用,為了方便研究者使用,PyTorch團隊專門開發了一個視覺工具包torchvion,這個包獨立於PyTorch,需通過pip instal torchvision安裝。

在之前的例子中我們已經見識到了它的部分功能,這里再做一個系統性的介紹。torchvision主要包含三部分:

  • models:提供深度學習中各種經典網絡的網絡結構以及預訓練好的模型,包括AlexNet、VGG系列、ResNet系列、Inception系列等。
  • datasets: 提供常用的數據集加載,設計上都是繼承torhc.utils.data.Dataset,主要包括MNISTCIFAR10/100ImageNetCOCO等。
  • transforms:提供常用的數據預處理操作,主要包括對Tensor以及PIL Image對象的操作。

1)models

from torchvision import models
from torch import nn
# 加載預訓練好的模型,如果不存在會進行下載
# 預訓練好的模型保存在 ~/.torch/models/下面
resnet34 = models.squeezenet1_1(pretrained=True, num_classes=1000)

# 修改最后的全連接層為10分類問題(默認是ImageNet上的1000分類)
resnet34.fc=nn.Linear(512, 10)

然后會開始下載模型:

Downloading: "https://download.pytorch.org/models/squeezenet1_1-f364aa15.pth" to /Users/user/.torch/models/squeezenet1_1-f364aa15.pth
4966400.0 bytes

 

2)datasets

下載數據:

from torchvision import datasets
# 指定數據集路徑為data,如果數據集不存在則進行下載
# 通過train=False獲取測試集
dataset = datasets.MNIST('data/', download=True, train=False, transform=transform)

返回:

Downloading http://yann.lecun.com/exdb/mnist/train-images-idx3-ubyte.gz to data/MNIST/raw/train-images-idx3-ubyte.gz
100.1%
Extracting data/MNIST/raw/train-images-idx3-ubyte.gz
Downloading http://yann.lecun.com/exdb/mnist/train-labels-idx1-ubyte.gz to data/MNIST/raw/train-labels-idx1-ubyte.gz
113.5%
Extracting data/MNIST/raw/train-labels-idx1-ubyte.gz
Downloading http://yann.lecun.com/exdb/mnist/t10k-images-idx3-ubyte.gz to data/MNIST/raw/t10k-images-idx3-ubyte.gz
100.4%
Extracting data/MNIST/raw/t10k-images-idx3-ubyte.gz
Downloading http://yann.lecun.com/exdb/mnist/t10k-labels-idx1-ubyte.gz to data/MNIST/raw/t10k-labels-idx1-ubyte.gz
180.4%
Extracting data/MNIST/raw/t10k-labels-idx1-ubyte.gz
Processing...
Done!

 

3)transforms

Transforms中涵蓋了大部分對Tensor和PIL Image的常用處理,這些已在上文提到,這里就不再詳細介紹。

需要注意的是轉換分為兩步

  • 第一步:構建轉換操作,例如transf = transforms.Normalize(mean=x, std=y)
  • 第二步:執行轉換操作,例如output = transf(input)

另外還可將多個處理操作用Compose拼接起來,形成一個處理轉換流程。

from torchvision import transforms
to_pil = transforms.ToPILImage()
to_pil(t.randn(3, 64, 64))

圖示:

 

4)常用函數

torchvision還提供了兩個常用的函數。

一個是make_grid,它能將多張圖片拼接成一個網格中;

另一個是save_img,它能將Tensor保存成圖片。

dataloader = DataLoader(dataset, shuffle=True, batch_size=16)
from torchvision.utils import make_grid, save_image
dataiter = iter(dataloader)
img = make_grid(next(dataiter)[0], 4) #[0]是圖片,[1]是label,拼成4*4網格圖片,且會轉成3通道
save_image(img,'a.png')

出錯:

RuntimeError: output with shape [1, 224, 224] doesn't match the broadcast shape [3, 224, 224]

好像make_grid並不能轉成3通道啊?????

如果換成使用的是彩色圖像,就成功運行:

dataset = DogCat('data/dogcat/', transforms=transform)
dataloader = DataLoader(dataset, shuffle=True, batch_size=16)
from torchvision.utils import make_grid, save_image
dataiter = iter(dataloader)
img = make_grid(next(dataiter)[0], 4) #[0]是圖片,[1]是label,拼成4*4網格圖片,且會轉成3通道
save_image(img,'a.png')

存儲得到的a.png為:

可以使用下面語句查看圖像:

Image.open('a.png')

 

4.可視化工具

在訓練神經網絡時,我們希望能更直觀地了解訓練情況,包括損失曲線、輸入圖片、輸出圖片、卷積核的參數分布等信息。這些信息能幫助我們更好地監督網絡的訓練過程,並為參數優化提供方向和依據。最簡單的辦法就是打印輸出,但其只能打印數值信息,不夠直觀,同時無法查看分布、圖片、聲音等。在本節,我們將介紹兩個深度學習中常用的可視化工具:Tensorboard和Visdom

1)Tensorboard

Tensorboard最初是作為TensorFlow的可視化工具迅速流行開來。作為和TensorFlow深度集成的工具,Tensorboard能夠展現你的TensorFlow網絡計算圖,繪制圖像生成的定量指標圖以及附加數據。但同時Tensorboard也是一個相對獨立的工具,只要用戶保存的數據遵循相應的格式,tensorboard就能讀取這些數據並進行可視化。這里我們將主要介紹如何在PyTorch中使用tensorboardX1進行訓練損失的可視化。 TensorboardX是將Tensorboard的功能抽取出來,使得非TensorFlow用戶也能使用它進行可視化,幾乎支持原生TensorBoard的全部功能。

在這里選擇了其中一個來學習,選擇了下面的Visdom

 

2)Visdom

可見pytorch visdom可視化工具學習—1—安裝和使用

 

 

5.使用GPU加速:cuda

這部分內容在前面介紹Tensor、Module時大都提到過,這里將做一個總結,並深入介紹相關應用。

在PyTorch中以下數據結構分為CPU和GPU兩個版本:

  • Tensor
  • nn.Module(包括常用的layer、loss function,以及容器Sequential等)

它們都帶有一個.cuda方法,調用此方法即可將其轉為對應的GPU對象。

注意,tensor.cuda會返回一個新對象,這個新對象的數據已轉移至GPU,而之前的tensor還在原來的設備上(CPU)。

module.cuda則會將所有的數據都遷移至GPU,並返回自己。所以module = module.cuda()module.cuda()所起的作用一致。

nn.Module在GPU與CPU之間的轉換,本質上還是利用了Tensor在GPU和CPU之間的轉換。nn.Module的cuda方法是將nn.Module下的所有parameter(包括子module的parameter)都轉移至GPU,而Parameter本質上也是tensor(Tensor的子類)。

下面將舉例說明,這部分代碼需要你具有兩塊GPU設備。但是因為我的機器中只有一塊GPU,所以只能運行看看感覺,有些地方將會有點不同

 

P.S. 為什么將數據轉移至GPU的方法叫做.cuda而不是.gpu,就像將數據轉移至CPU調用的方法是.cpu?這是因為GPU的編程接口采用CUDA,而目前並不是所有的GPU都支持CUDA,只有部分Nvidia的GPU才支持。PyTorch未來可能會支持AMD的GPU,而AMD GPU的編程接口采用OpenCL,因此PyTorch還預留着.cl方法,用於以后支持AMD等的GPU。

user@home:/opt/user$ python
Python 3.6.3 |Anaconda custom (64-bit)| (default, Oct 13 2017, 12:02:49)
[GCC 7.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import torch as t
>>> tensor = t.Tensor(3,4)
>>> tensor.cuda(0) #返回一個新的tensor,保存在第0塊GPU上,但原來的tensor並沒有改變
tensor([[-4.5677e+20,  3.0805e-41,  3.7835e-44,  0.0000e+00],
        [        nan,  4.5817e-41,  1.3733e-14,  6.4069e+02],
        [ 4.3066e+21,  1.1824e+22,  4.3066e+21,  6.3828e+28]], device='cuda:0')
>>> tensor.is_cuda
False

 

>>> tensor1 = tensor.cuda() # 不指定所使用的GPU設備,將默認使用第1塊GPU
>>> tensor1.is_cuda
True
>>> tensor1.get_device()
0
>>> tensor2 = tensor.cuda(1) 
#因為我的機器上只有一個GPU,所以如果想要設置第二塊GPU則會報錯
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
RuntimeError: CUDA error: invalid device ordinal

 

>>> from torch import nn
>>> module = nn.Linear(3,4)
#將模塊設置為.cuda(),這樣模塊中的參數也會自動使用GPU
>>> module.cuda(device=0)
Linear(in_features=3, out_features=4, bias=True)
>>> module.weight.is_cuda
True

 

在定義網絡時也可以直接在聲明參數的時候指定運行的GPU:

#-*- coding: utf-8 -*-
from __future__ import print_function
import torch as t

class VeryBigModule(nn.Module):
        def __init__(self):
                super(VeryBigModule, self).__init__()
                self.GiantParameter1 = t.nn.Parameter(t.randn(100000, 20000)).cuda(0)
                self.GiantParameter2 = t.nn.Parameter(t.randn(20000, 100000)).cuda(1)

        def forward(self, x):
                x = self.GiantParameter1.mm(x.cuda(0))
                x = self.GiantParameter2.mm(x.cuda(1))
                return x    

上面最后一部分中,兩個Parameter所占用的內存空間都非常大,大概是8個G,如果將這兩個都同時放在一塊GPU上幾乎會將顯存占滿,無法再進行任何其它運算。此時可通過這種方式將不同的計算分布到不同的GPU中。

關於使用GPU的一些建議:

  • GPU運算很快,但對於很小的運算量來說,並不能體現出它的優勢,因此對於一些簡單的操作可直接利用CPU完成
  • 數據在CPU和GPU之間,以及GPU與GPU之間的傳遞會比較耗時,應當盡量避免
  • 在進行低精度的計算時,可以考慮HalfTensor,它相比於FloatTensor能節省一半的顯存,但需千萬注意數值溢出的情況。

另外這里需要專門提一下,大部分的損失函數也都屬於nn.Module,但在使用GPU時,很多時候我們都忘記使用它的.cuda方法,這在大多數情況下不會報錯,因為損失函數本身沒有可學習的參數(learnable parameters)。

但在某些情況下會出現問題,為了保險起見同時也為了代碼更規范,應記得調用criterion.cuda

下面舉例說明:

# 交叉熵損失函數,帶權重
>>> criterion = t.nn.CrossEntropyLoss(weight=t.Tensor([1,3]))
>>> input = t.randn(4,2).cuda()
>>> target = t.Tensor([1,0,0,1]).long().cuda(0)

# 下面這行會報錯,因weight未被轉移至GPU
# loss = criterion(input, target)

>>> criterion.cuda(0)
CrossEntropyLoss()
>>> loss = criterion(input, target)
>>> criterion._buffers
OrderedDict([('weight', tensor([1., 3.], device='cuda:0'))])

 


而除了調用對象的.cuda方法之外,還可以使用torch.cuda.device,來指定默認使用哪一塊GPU:

>>> with t.cuda.device(0):
...     a = t.cuda.FloatTensor(2,3)
...     b = t.FloatTensor(2,3).cuda() #如果沒有顯示聲明,就會報錯
...     print(a.get_device() == b.get_device() == 0)
...
...     c = a + b
...     print(c.get_device() == 0)
...
True
Traceback (most recent call last):
  File "<stdin>", line 6, in <module>
RuntimeError: CUDA error: invalid device ordinal

從上面的情況發現最好還是顯示指明使用的GPU,所以改成下面這樣就可以了:

>>> with t.cuda.device(0):
...     a = t.cuda.FloatTensor(2,3)
...     b = t.FloatTensor(2,3).cuda(0)
...     print(a.get_device() == b.get_device() == 0)
...     c = a + b
...     print(c.get_device() == 0)
...     d = t.randn(2,3).cuda()
...     print(d.get_device())
...
True
True
0

還可以使用torch.set_default_tensor_type使程序默認使用GPU,不需要手動調用cuda

>>> t.set_default_tensor_type('torch.cuda.FloatTensor')
>>> a = t.ones(2,3)
>>> a.is_cuda
True

 


如果服務器具有多個GPU,tensor.cuda()方法會將tensor保存到第一塊GPU上,等價於tensor.cuda(0)。此時如果想使用第二塊GPU,需手動指定tensor.cuda(1),而這需要修改大量代碼,很是繁瑣。

這里有兩種替代方法:

  • 一種是先調用t.cuda.set_device(1)指定使用第二塊GPU,后續的.cuda()都無需更改,切換GPU只需修改這一行代碼。
  • 更推薦的方法是設置環境變量CUDA_VISIBLE_DEVICES,例如當export CUDA_VISIBLE_DEVICE=1(下標是從0開始,1代表第二塊GPU),只使用第二塊物理GPU,但在程序中這塊GPU會被看成是第一塊邏輯GPU,因此此時調用tensor.cuda()會將Tensor轉移至第二塊物理GPU。CUDA_VISIBLE_DEVICES還可以指定多個GPU,如export CUDA_VISIBLE_DEVICES=0,2,3,那么第一、三、四塊物理GPU會被映射成第一、二、三塊邏輯GPU,tensor.cuda(1)會將Tensor轉移到第三塊物理GPU上。

設置CUDA_VISIBLE_DEVICES有兩種方法:

  • 一種是在命令行中CUDA_VISIBLE_DEVICES=0,1 python main.py
  • 一種是在程序中import os;os.environ["CUDA_VISIBLE_DEVICES"] = "2"。如果使用IPython或者Jupyter notebook,還可以使用%env CUDA_VISIBLE_DEVICES=1,2來設置環境變量。

從 0.4 版本開始,pytorch新增了tensor.to(device)方法,能夠實現設備透明,便於實現CPU/GPU兼容。這部份內容已經在第三章講解過了。

從PyTorch 0.2版本中,PyTorch新增分布式GPU支持。分布式是指有多個GPU在多台服務器上,而並行一般指的是一台服務器上的多個GPU。分布式涉及到了服務器之間的通信,因此比較復雜,PyTorch封裝了相應的接口,可以用幾句簡單的代碼實現分布式訓練。分布式對普通用戶來說比較遙遠,因為搭建一個分布式集群的代價十分大,使用也比較復雜。相比之下一機多卡更加現實。對於分布式訓練,這里不做太多的介紹,感興趣的讀者可參考文檔1

 

單機多卡並行

要實現模型單機多卡十分容易,直接使用 new_module = nn.DataParallel(module, device_ids), 默認會把模型分布到所有的卡上。多卡並行的機制如下:

  • 將模型(module)復制到每一張卡上
  • 將形狀為(N,C,H,W)的輸入均等分為 n份(假設有n張卡),每一份形狀是(N/n, C,H,W),然后在每張卡前向傳播,反向傳播,梯度求平均。要求batch-size 大於等於卡的個數(N>=n)

在絕大多數情況下,new_module的用法和module一致,除了極其特殊的情況下(RNN中的PackedSequence)。另外想要獲取原始的單卡模型,需要通過new_module.module訪問。

 

6.持久化

在PyTorch中,以下對象可以持久化到硬盤,並能通過相應的方法加載到內存中:

  • Tensor
  • Variable
  • nn.Module
  • Optimizer

本質上上述這些信息最終都是保存成Tensor。Tensor的保存和加載十分的簡單,使用t.save和t.load即可完成相應的功能。在save/load時可指定使用的pickle模塊,在load時還可將GPU tensor映射到CPU或其它GPU上。

我們可以通過t.save(obj, file_name)等方法保存任意可序列化的對象到file_name文件中,然后通過obj = t.load(file_name)方法加載保存的數據file_name

對於Module和Optimizer對象,這里建議保存對應的state_dict,而不是直接保存整個Module/Optimizer對象。Optimizer對象保存的主要是參數,以及動量信息,通過加載之前的動量信息,能夠有效地減少模型震盪

下面舉例說明:

 

>>> a = t.Tensor(3,4)
>>> if t.cuda.is_available():
...     a = a.cuda(0) # 把a轉為GPU0上
...     t.save(a, 'a.pth') #保存a的值到'a.pth'
...     b = t.load('a.pth') #加載'a.pth'為b, 因為保存時tensor在GPU0上,所以b也會存儲與GPU0
...     c = t.load('a.pth', map_location=lambda storage, loc:storage) #加載為c, 存儲於CPU
...
>>> b
tensor([[1.0373e-22, 9.1637e-41, 1.0373e-22, 9.1637e-41],
        [2.5318e-12, 3.2127e+21, 0.0000e+00, 0.0000e+00],
        [0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00]])
>>> a
tensor([[1.0373e-22, 9.1637e-41, 1.0373e-22, 9.1637e-41],
        [2.5318e-12, 3.2127e+21, 0.0000e+00, 0.0000e+00],
        [0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00]])
>>> a.get_device() == b.get_device()
True
>>> c.is_cuda #可見c為CPU
False
>>> c #三者的值是相同的
tensor([[1.0373e-22, 9.1637e-41, 1.0373e-22, 9.1637e-41],
        [2.5318e-12, 3.2127e+21, 0.0000e+00, 0.0000e+00],
        [0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00]], device='cpu')

 

存儲模型和優化器參數:

>>> t.set_default_tensor_type('torch.FloatTensor')
>>> from torchvision.models import SqueezeNet
>>> model = SqueezeNet()
# module的state_dict是一個字典
>>> model.state_dict().keys()
odict_keys(['features.0.weight', 'features.0.bias', 'features.3.squeeze.weight', 'features.3.squeeze.bias', 'features.3.expand1x1.weight', 'features.3.expand1x1.bias', 'features.3.expand3x3.weight', 'features.3.expand3x3.bias', 'features.4.squeeze.weight', 'features.4.squeeze.bias', 'features.4.expand1x1.weight', 'features.4.expand1x1.bias', 'features.4.expand3x3.weight', 'features.4.expand3x3.bias', 'features.5.squeeze.weight', 'features.5.squeeze.bias', 'features.5.expand1x1.weight', 'features.5.expand1x1.bias', 'features.5.expand3x3.weight', 'features.5.expand3x3.bias', 'features.7.squeeze.weight', 'features.7.squeeze.bias', 'features.7.expand1x1.weight', 'features.7.expand1x1.bias', 'features.7.expand3x3.weight', 'features.7.expand3x3.bias',
'features.8.squeeze.weight', 'features.8.squeeze.bias', 'features.8.expand1x1.weight', 'features.8.expand1x1.bias', 'features.8.expand3x3.weight', 'features.8.expand3x3.bias', 'features.9.squeeze.weight', 'features.9.squeeze.bias', 'features.9.expand1x1.weight', 'features.9.expand1x1.bias', 'features.9.expand3x3.weight', 'features.9.expand3x3.bias', 'features.10.squeeze.weight', 'features.10.squeeze.bias', 'features.10.expand1x1.weight', 'features.10.expand1x1.bias', 'features.10.expand3x3.weight',
'features.10.expand3x3.bias', 'features.12.squeeze.weight', 'features.12.squeeze.bias', 'features.12.expand1x1.weight', 'features.12.expand1x1.bias', 'features.12.expand3x3.weight', 'features.12.expand3x3.bias', 'classifier.1.weight', 'classifier.1.bias'])

# Module對象的保存與加載
>>> t.save(model.state_dict(), 'squeezenet.pth')
>>> model.load_state_dict(t.load('squeezenet.pth'))

#優化器
>>> optimizer = t.optim.Adam(model.parameters(), lr=0.1)
>>> t.save(optimizer.state_dict(), 'optimizer.pth')
>>> optimizer.load_state_dict(t.load('optimizer.pth'))  

#將兩者的狀態存放在一起
>>> all_data = dict(
...     optimizer = optimizer.state_dict(),
...     model = model.state_dict(),
...     info = 'the all parameters of model and optimizer'
... )
>>> t.save(all_data, 'all.pth')
>>> load_all_data = t.load('all.pth')

>>> load_all_data.keys()
dict_keys(['optimizer', 'model', 'info'])

>>> load_all_data['model'].keys()
odict_keys(['features.0.weight', 'features.0.bias', 'features.3.squeeze.weight', 'features.3.squeeze.bias', 'features.3.expand1x1.weight', 'features.3.expand1x1.bias', 'features.3.expand3x3.weight', 'features.3.expand3x3.bias', 'features.4.squeeze.weight', 'features.4.squeeze.bias', 'features.4.expand1x1.weight', 'features.4.expand1x1.bias', 'features.4.expand3x3.weight', 'features.4.expand3x3.bias', 'features.5.squeeze.weight', 'features.5.squeeze.bias', 'features.5.expand1x1.weight', 'features.5.expand1x1.bias', 'features.5.expand3x3.weight', 'features.5.expand3x3.bias', 'features.7.squeeze.weight', 'features.7.squeeze.bias', 'features.7.expand1x1.weight', 'features.7.expand1x1.bias', 'features.7.expand3x3.weight', 'features.7.expand3x3.bias', 'features.8.squeeze.weight', 'features.8.squeeze.bias', 'features.8.expand1x1.weight', 'features.8.expand1x1.bias', 'features.8.expand3x3.weight', 'features.8.expand3x3.bias', 'features.9.squeeze.weight', 'features.9.squeeze.bias', 'features.9.expand1x1.weight', 'features.9.expand1x1.bias', 'features.9.expand3x3.weight', 'features.9.expand3x3.bias', 'features.10.squeeze.weight', 'features.10.squeeze.bias', 'features.10.expand1x1.weight', 'features.10.expand1x1.bias', 'features.10.expand3x3.weight', 'features.10.expand3x3.bias', 'features.12.squeeze.weight', 'features.12.squeeze.bias', 'features.12.expand1x1.weight', 'features.12.expand1x1.bias', 'features.12.expand3x3.weight', 'features.12.expand3x3.bias', 'classifier.1.weight', 'classifier.1.bias'])

>>> load_all_data['optimizer'].keys()
dict_keys(['state', 'param_groups'])

 


免責聲明!

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



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