Pytorch入門上 —— Dataset、Tensorboard、Transforms、Dataloader

WINLSR發表於2021-12-15

本節內容參照小土堆的pytorch入門視訊教程。學習時建議多讀原始碼,通過原始碼中的註釋可以快速弄清楚類或函式的作用以及輸入輸出型別。

Dataset

借用Dataset可以快速訪問深度學習需要的資料,例如我們需要訪問如下訓練資料:

image-20211207102403188

其中,train中存放的是訓練資料集,antsbees既是資料夾名稱也是其包含的圖片資料的標籤,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

該命令執行成功後會給出如下圖中的提示:

image-20211207152421725

在瀏覽器中開啟以上鍊接即可檢視視覺化情況:

image-20211207152721046

注意: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之間的影像變化。

tensorboard-image

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視覺化服務並在瀏覽器中訪問:

image-20211208115853366

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],傳入的 mean0.5std0.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視覺化服務並在瀏覽器中訪問:

image-20211208154236631

Resize

Resize類可對tensorPIL影像進行縮放,建構函式定義如下:

# 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視覺化服務並在瀏覽器中訪問:

transforms-image

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視覺化服務並在瀏覽器中訪問:

image-20211208214032582

transforms模組中還有很多其他的transform類,這些類在需要時查詢文件即可。查詢文件時,重點要弄清楚類或函式的作用以及輸入輸出型別。

DatasetTransforms結合使用

pytorch官網首頁,按不同模組分別提供了文件,包括:核心模組,音訊模組,文字模組,視覺模組等:

image-20211209093100907

torchvision.datasets中,提供了對常用資料集的支援,它可以幫助我們下載、解壓並快速使用資料集:

image-20211209093940579

CIFAR10資料集為例,根據如下文件使用即可:

image-20211209094225286

根據文件可以知道,要使用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可以將資料輸入神經網路,檢視官方文件:

image-20211209115807173 image-20211209120029889

根據官方文件我們知道,在建立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()
image-20211209140650121

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環境所在盤的虛擬記憶體大小:

進入高階系統設定

image-20211209152952729

點選:高階 -> 效能 -> 設定

image-20211209153043930

點選:高階 -> 更改

image-20211209153121627

按照下圖,將D盤的虛擬記憶體調整為系統管理的大小(重啟後生效)。任然報錯就自定義大小為一個很大的值,如:10GB-100GB

image-20211209153220914

指令碼執行成功後,啟動tensorbard視覺化服務並在瀏覽器中訪問:

step=170時:(日誌比較大,瀏覽器中可能需要多次重新整理才能完全載入)

image-20211209154356868

step=781時:

image-20211209154529704

由於我們在建立DataLoader時,引數shuffle=False使得epoch0epoch1之間相同批次抽取的影像是相同的;引數drop_last=False使得即使最後一個批次影像不足64也沒有被捨棄。

修改指令碼使得:shuffle=Truedrop_last=True,然後再次執行指令碼並啟動tensorbard視覺化服務。瀏覽器中訪問結果如下:

image-20211209160213512

如圖所示,epoch0epoch1之間相同批次(780)抽取的影像不相同,且第781個批次被捨棄。

相關文章