本節內容參照小土堆的pytorch入門視訊教程。學習時建議多讀原始碼,通過原始碼中的註釋可以快速弄清楚類或函式的作用以及輸入輸出型別。
Dataset
借用Dataset
可以快速訪問深度學習需要的資料,例如我們需要訪問如下訓練資料:
其中,train
中存放的是訓練資料集,ants
和bees
既是資料夾名稱也是其包含的圖片資料的標籤,val
中存放的是驗證資料集。
假如我們希望自己的Dataset
類可以實現如下資料訪問形式:
dataset = MyDataset("root_dir", "label_dir")
img, label = dataset[0] # 通過下標訪問
我們只需要繼承Dataset
類並覆蓋其中的__getitem__()
(必須)和__len__()
(建議)方法。詳情可在互動模式中執行如下語句檢視:
from torch.utils.data import Dataset
help(Dataset)
# 或執行 Dataset??
# 或在pycharm中按住ctrl並點選Dataset直接檢視原始碼
部分輸出如下:
An abstract class representing a :class:`Dataset`.
All datasets that represent a map from keys to data samples should subclass it. All subclasses should overwrite :meth:`__getitem__`, supporting fetching a data sample for a given key. Subclasses could also optionally overwrite :meth:`__len__`, which is expected to return the size of the dataset by many :class:`~torch.utils.data.Sampler` implementations and the default options of :class:`~torch.utils.data.DataLoader`.
定義MyDataset
類:
import os
from torch.utils.data import Dataset
from torch.utils.data.dataset import T_co
from PIL import Image
class MyDataset(Dataset):
def __init__(self, root_dir, label_dir):
"""根據路徑獲取到所有的 image 檔名
:param root_dir: data路徑
:param label_dir: label
"""
self.root_dir = root_dir
self.label_dir = label_dir
self.data_dir = os.path.join(self.root_dir, self.label_dir)
self.img_names = os.listdir(self.data_dir)
def __getitem__(self, index) -> T_co:
"""overwrite 後才可以通過下標訪問 dataset
:param index: 下標
:return: img(PIL), label
"""
img_name = self.img_names[index]
img_path = os.path.join(self.data_dir, img_name)
img = Image.open(img_path)
label = self.label_dir
return img, label # 放回什麼資料由自己定義
def __len__(self):
return len(self.img_names)
使用MyDataset
類
在互動模式中匯入MyDataset
並執行如下語句:
ants_dataset = MyDataset("dataset/train", "ants") # 建立dataset
bees_dataset = MyDataset("dataset/train", "bees")
img, label = ants_dataset[47] # 通過下標訪問資料
len(bees_dataset) # 獲取dataset長度
img.show() # 顯示圖片
dataset = ants_dataset + bees_dataset # 將dataset相加
Tensorboard
使用tensorboard
可以按照日誌的形式記錄訓練模型時產生的一些資料(標量、圖片等),同時也可以對記錄的資料進行視覺化,方便我們對現有模型進行分析。例如,藉助tensorboard
記錄loss
函式隨著訓練訓練週期的下降過程並視覺化。
使用Tensorboard
首先要在conda
環境中通過如下命令進行安裝:
conda install tensorboard
記錄標量(scalar
)
記錄標量需要用到torch.utils.tensorboard
中的SummaryWriter
類的add_scalar
方法。
建立如下python
指令碼並執行:
from torch.utils.tensorboard import SummaryWriter
# 引數:log_dir,日誌存放的路徑名稱
writer = SummaryWriter("logs")
# 記錄曲線 y = x^2
# 引數一:tag,資料識別符號
# 引數二:scalar_value,標量值,y軸資料
# 引數三:global_step,步驟,x軸資料
for i in range(100):
writer.add_scalar("y = x^2", i ** 2, i)
# 呼叫close() 確保資料刷入磁碟
writer.close()
指令碼執行成功後會在指令碼檔案所在當前路徑下建立logs
資料夾並生成日誌檔案。日誌生成成功後在conda
環境中執行以下命令來啟動tensorboard
視覺化服務:
# 指明log存放的位置以及服務啟動時使用的埠,預設埠為6006
# 多人使用同一臺伺服器時應該避免以下兩個引數值相同
tensorboard --logdir=second/logs --port=6008
該命令執行成功後會給出如下圖中的提示:
在瀏覽器中開啟以上鍊接即可檢視視覺化情況:
注意:Tensorboard
是根據不同的tag
來進行視覺化的,如果多次寫入的日誌有相同tag
,可能會導致視覺化時出問題。只需刪除日誌並重新生成,然後重啟視覺化服務即可解決。
記錄影像(image
)
記錄影像可以使用torch.utils.tensorboard
中的SummaryWriter
類的add_image
方法。
該方法定義如下:
def add_image(self, tag, img_tensor, global_step=None, walltime=None, dataformats='CHW')
# tag(string) 同上
# img_tensor (torch.Tensor, numpy.array, or string/blobname): 影像資料
# global_step (int): 步驟
# dataformats (string):影像資料格式,不為CHW則需要指明
建立如下python
指令碼並執行:
from torch.utils.tensorboard import SummaryWriter
from PIL import Image
import numpy as np
writer = SummaryWriter("logs")
img_path = "E:\\code\\python\\pytorch-learn\\dataset\\train\\ants\\0013035.jpg"
# img 為 PIL.JpegImagePlugin.JpegImageFile 型別
img = Image.open(img_path)
img_array = np.array(img)
writer.add_image("ants", img_array, 0, dataformats="HWC")
writer.close()
指令碼執行成功後按之前同樣的方式啟動視覺化服務即可。
如果日誌中同一個tag
標識的資料有多個global_step
則可以如下圖所示:拖動軸來檢視global_step
之間的影像變化。
Transforms
transforms
是位於torchvision
包中的一個模組,模組中有多個類可以對將要輸入神經網路的資料進行轉換以適應網路的需要。
ToTensor
該類可以將 PIL Image
or numpy.ndarray
轉換為張量,以方便影像輸入到神經網路中,如下程式碼做了簡單示例:將PIL Image
轉換為張量後藉助tensorboard
直接寫入日誌檔案:
from torchvision import transforms
from PIL import Image
from torch.utils.tensorboard import SummaryWriter
# 建立 transforms 模組中的 ToTensor 類
trans_to_tensor = transforms.ToTensor()
# 獲取 PIL 影像
img_path = "E:/code/python/pytorch-learn/dataset/train/ants" \
"/6743948_2b8c096dda.jpg "
img = Image.open(img_path)
# 將 PIL 影像轉化為 tensor 型別
# 1. 將輸入的資料shape W,H,C ——> C,W,H
# 2. 將所有數除以255,將資料歸一化到[0,1]
# 例項像方法一樣呼叫,這裡實際呼叫的是ToTensor類中的__call__方法,
# 更多:https://zhuanlan.zhihu.com/p/370234492
img_tensor = trans_to_tensor(img)
# 使用 tensorboard 將 tensor 影像寫入日誌
writer = SummaryWriter("logs")
writer.add_image("img_tensor", img_tensor, 1)
writer.close()
啟動tensorbard
視覺化服務並在瀏覽器中訪問:
Normalize
normalize
類可以利用均值和標準差對tensor
影像進行歸一化。該類建構函式定義如下:
# mean (sequence): 代表影像每個通道均值的序列
# std (sequence): 代表影像每個通道標準差的序列
# inplace(bool,optional): 是否原地修改tensor
def __init__(self, mean, std, inplace=False):
該類的具體歸一化操作如下:
output[channel] = (input[channel] - mean[channel]) / std[channel]
拿任意通道來說,如果原值 value
為 [0, 1]
,傳入的 mean
為 0.5
,std
為 0.5
那麼歸一化操作即為:(value - 0.5) / 0.5 = 2*value - 1
,帶入value
範圍後計算得到value
的新範圍為[-1, 1]
。
建立如下指令碼並執行:
from torchvision import transforms
from PIL import Image
from torch.utils.tensorboard import SummaryWriter
# 建立 transforms 模組中的 ToTensor 類
trans_to_tensor = transforms.ToTensor()
# 建立 transforms 模組中的 Normalize 類,三個通道的mean和std都為0.5
trans_normalize = transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])
# 獲取 PIL 影像
img_path = "E:/code/python/pytorch-learn/dataset/train/ants" \
"/6743948_2b8c096dda.jpg "
img = Image.open(img_path)
# 將 PIL 影像轉化為 tensor 型別
# 1. 將輸入的資料shape W,H,C ——> C,W,H
# 2. 將所有數除以255,將資料歸一化到[0,1]
img_tensor = trans_to_tensor(img)
# 將 tensor 進行歸一化
img_normalize = trans_normalize(img_tensor)
print(type(img_normalize))
# <class 'torch.tensor'="">
# 使用 tensorboard 將 tensor 影像寫入日誌
writer = SummaryWriter("logs")
writer.add_image("img_normalize", img_normalize, 1)
writer.close()
啟動tensorbard
視覺化服務並在瀏覽器中訪問:
Resize
Resize
類可對tensor
或PIL
影像進行縮放,建構函式定義如下:
# size (sequence or int): 所需的輸出大小
# 如果size是形如(h, w)的高和寬的序列,輸出時就按此匹配;
# 如果size是一個int,則選擇小的邊來進行等比例縮放,如果 height > width,
# 則縮放後為(size * height / width, size)
def __init__(self, size, interpolation=InterpolationMode.BILINEAR, max_size=None, antialias=None):
建立如下指令碼並執行:
from torchvision import transforms
from PIL import Image
from torch.utils.tensorboard import SummaryWriter
# 建立到 transforms 模組中的 ToTensor 類
trans_to_tensor = transforms.ToTensor()
# 建立 transforms 模組中的 Normalize 類
trans_normalize = transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])
# 建立 transforms 模組中的 Resize 類
trans_resize_52x52 = transforms.Resize((80, 40))
trans_resize_52 = transforms.Resize(200)
# 獲取 PIL 影像
img_path = "E:/code/python/pytorch-learn/dataset/train/ants" \
"/6743948_2b8c096dda.jpg "
img = Image.open(img_path)
# 使用 tensorboard 記錄多個處理步驟的影像
writer = SummaryWriter("logs")
# 將 PIL 影像轉化為 tensor 型別
# 1. 將輸入的資料shape W,H,C ——> C,W,H
# 2. 將所有數除以255,將資料歸一化到[0,1]
img_tensor = trans_to_tensor(img)
# 步驟0:記錄tensor影像
writer.add_image("img-tensor-normalize-resize_52x52_resize_52", img_tensor, 0)
# 將 tensor 進行歸一化
img_normalize = trans_normalize(img_tensor)
# 步驟1:記錄normalize後的影像
writer.add_image("img-tensor-normalize-resize_52x52_resize_52",
img_normalize, 1)
# 對normalize後的tensor進行(52, 52)的resize
img_resize_52x52 = trans_resize_52x52(img_normalize)
# 步驟2:記錄(52, 52)resize後的影像
writer.add_image("img-tensor-normalize-resize_52x52_resize_52",
img_resize_52x52, 2)
# 對(52, 52)resize後的tensor再次進行(52)的resize
img_resize_52 = trans_resize_52(img_resize_52x52)
# 步驟3:記錄(52)resize後的影像
writer.add_image("img-tensor-normalize-resize_52x52_resize_52",
img_resize_52, 3)
writer.close()
啟動tensorbard
視覺化服務並在瀏覽器中訪問:
Compose
在上一節的示例中,我們對影像的處理是基於一系列transform
,為了記錄每個transform
執行後的影像,我們在每兩個transform
之間插入了記錄影像操作。如果我們不需要記錄每個transform
執行後的影像,那麼我們可以通過Compose
來順序執行一系列transform
。
Compose
類的建構函式定義如下:
# transforms (list of Transform objects):
# 需要順序執行的transfrom物件組成的list
def __init__(self, transforms):
該類的核心思想位於__call__
函式中:
def __call__(self, img):
for t in self.transforms:
img = t(img)
return img
有了compose
後,為了得到上一節最後的結果,我們可以對上一節的指令碼做如下簡化:
from torchvision import transforms
from PIL import Image
from torch.utils.tensorboard import SummaryWriter
# 建立到 transforms 模組中的 ToTensor 類
trans_to_tensor = transforms.ToTensor()
# 建立 transforms 模組中的 Normalize 類
trans_normalize = transforms.Normalize([0.5, 0.5, 0.5], [0.5, 0.5, 0.5])
# 建立 transforms 模組中的 Resize 類
trans_resize_80x40 = transforms.Resize((80, 40))
trans_resize_200 = transforms.Resize(200)
# 建立 transforms 模組中的 Compose 類
trans_compose = transforms.Compose([trans_to_tensor, trans_normalize,
trans_resize_80x40, trans_resize_200])
# 獲取 PIL 影像
img_path = "E:/code/python/pytorch-learn/dataset/train/ants" \
"/6743948_2b8c096dda.jpg "
img = Image.open(img_path)
# 執行 Compose
img_compose = trans_compose(img)
# 記錄compose後的影像
writer = SummaryWriter("logs")
writer.add_image("img-tensor-normalize-resize_80x40-resize_200-compose",
img_compose, 0)
writer.close()
啟動tensorbard
視覺化服務並在瀏覽器中訪問:
transforms
模組中還有很多其他的transform
類,這些類在需要時查詢文件即可。查詢文件時,重點要弄清楚類或函式的作用以及輸入輸出型別。
Dataset
和 Transforms
結合使用
在pytorch
官網首頁,按不同模組分別提供了文件,包括:核心模組,音訊模組,文字模組,視覺模組等:
在torchvision.datasets
中,提供了對常用資料集的支援,它可以幫助我們下載、解壓並快速使用資料集:
以CIFAR10
資料集為例,根據如下文件使用即可:
根據文件可以知道,要使用CIFAR10
資料集則使用torchvision.datasets.CIFAR10
類,該類的建構函式要求傳入如下引數:
# root (string):資料集所在目錄
# train (bool, optional):為True,建立訓練集;為false,建立測試集
# transform (callable, optional):處理 PIL image 的function/transform
# target_transform (callable, optional):處理 target(影像類別)的function/transform
# download (bool, optional):為true則下載資料集到root目錄中,如果已經存在則不會下載
def __init__(
self,
root: str,
train: bool = True,
transform: Optional[Callable] = None,
target_transform: Optional[Callable] = None,
download: bool = False,
) -> None:
根據文件中的__getitem__
函式我們知道,通過下標訪問Dataset
時會返回(image, target)
,即影像和影像的種類。
建立如下指令碼並執行(使用訓練集):
import torchvision
# 建立transform,將 PIL Image 轉換為 tensor
data_trans = torchvision.transforms.Compose([
torchvision.transforms.ToTensor()
])
# 建議download始終為True,即使你自己提前下載好了資料集
# 下載慢的話可以拷貝控制檯輸出的下載地址,然後到迅雷下載好後再將壓縮包拷貝至root下即可
# 下載地址也可以在torchvision.datasets.CIFAR10類的原始碼中檢視
train_set = torchvision.datasets.CIFAR10(root="./dataset", train=True,
transform=data_trans, download=True)
img, target = train_set[0]
print(type(img)) # <class 'torch.tensor'="">
print(target) # 6
# ['airplane', 'automobile', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck']
print(train_set.classes)
print(train_set.classes[target]) # frog
建立如下指令碼並執行(使用測試集):
import torchvision
test_set = torchvision.datasets.CIFAR10(root="./dataset", train=False,
download=True)
img, target = test_set[0]
img.show()
print(type(img)) # <class 'pil.image.image'="">
print(target) # 3
print(test_set.classes[target]) # cat
DataLoader
借用DataLoader
可以將資料輸入神經網路,檢視官方文件:
根據官方文件我們知道,在建立DataLoader
時需要的主要的引數如下:
# dataset (Dataset): Dataset型別資料集
# batch_size (int, optional): 批大小,default: 1
# shuffle (bool, optional): 每個 epoch 後是否打亂資料,default: False
# num_workers (int, optional): 載入資料時使用的子程式數量,為0表示使用主程式載入,default: 0。可設定為你的cpu核心數
# drop_last (bool, optional): 當資料個數不能被batch_size整除時,為True表示丟棄最後一批,為False表示不丟棄,default: False
def __init__(self, dataset: Dataset[T_co], batch_size: Optional[int] = 1,
shuffle: bool = False, sampler: Optional[Sampler] = None,
batch_sampler: Optional[Sampler[Sequence]] = None,
num_workers: int = 0, collate_fn: Optional[_collate_fn_t] = None,
pin_memory: bool = False, drop_last: bool = False,
timeout: float = 0, worker_init_fn: Optional[_worker_init_fn_t] = None,
multiprocessing_context=None, generator=None,
*, prefetch_factor: int = 2,
persistent_workers: bool = False):
建立如下指令碼並執行:
import torchvision
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
def train():
trans_to_tensor = torchvision.transforms.ToTensor()
train_dataset = torchvision.datasets.CIFAR10(root="./dataset",
transform=trans_to_tensor,
download=True)
# dataloader 按照batch_size打包img和target,如圖所示
train_dataloader = DataLoader(dataset=train_dataset, batch_size=64,
shuffle=False, num_workers=16,
drop_last=False)
writer = SummaryWriter("logs")
for epoch in range(2):
for batch_num, batch_data in enumerate(train_dataloader):
# print(epoch, " : ", batch_num)
batch_img, batch_target = batch_data
writer.add_images(
"CIFAR10-batch64-no_shuffle-no_drop_last_epoch{}".format(epoch)
, batch_img, batch_num)
writer.close()
if __name__ == "__main__":
train()
在windows
中如果num_workers > 1
報錯,解決方法如下:
1.修改程式碼為如下格式:
def train():
# Here was inserted the whole code that train the network ...
if __name__ == '__main__':
train()
2.如果程式碼修改後仍然報錯:OSError: [WinError 1455] 頁面檔案太小,無法完成操作
。則還需要調整你的python
環境所在盤的虛擬記憶體大小:
進入高階系統設定
:
點選:高階 -> 效能 -> 設定
:
點選:高階 -> 更改
:
按照下圖,將D
盤的虛擬記憶體調整為系統管理的大小
(重啟後生效)。任然報錯就自定義大小為一個很大的值,如:10GB-100GB
。
指令碼執行成功後,啟動tensorbard
視覺化服務並在瀏覽器中訪問:
step=170
時:(日誌比較大,瀏覽器中可能需要多次重新整理才能完全載入)
step=781
時:
由於我們在建立DataLoader
時,引數shuffle=False
使得epoch0
和epoch1
之間相同批次抽取的影像是相同的;引數drop_last=False
使得即使最後一個批次影像不足64也沒有被捨棄。
修改指令碼使得:shuffle=True
,drop_last=True
,然後再次執行指令碼並啟動tensorbard
視覺化服務。瀏覽器中訪問結果如下:
如圖所示,epoch0
和epoch1
之間相同批次(780)抽取的影像不相同,且第781個批次被捨棄。