1. 數據處理
數據加載
在Pytorch 中,數據加載可以通過自己定義的數據集對象來實現。數據集對象被抽象為Dataset類,實現自己定義的數據集需要繼承Dataset,並實現兩個Python魔法方法。
__getitem__
: 返回一條數據或一個樣本。obj[index]
等價於obj.__getitem__(index)
.__len__
: 返回樣本的數量。len(obj)
等價於obj.__len__()
.
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->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.image) dataset=DogCat('N:/百度網盤/kaggle/DogCat') img,label=dataset[0]#相當於調用dataset.__getitem__(0) for img,label in dataset: print(img.size(),img.float().mean(),label)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
結果:
torch.Size([280, 300, 3]) tensor(71.6653) 0 torch.Size([396, 312, 3]) tensor(131.8400) 0 torch.Size([414, 500, 3]) tensor(156.6921) 0 torch.Size([375, 499, 3]) tensor(96.8243) 0 torch.Size([445, 431, 3]) tensor(103.8582) 1 torch.Size([373, 302, 3]) tensor(160.0512) 1 torch.Size([240, 288, 3]) tensor(95.1983) 1 torch.Size([499, 375, 3]) tensor(90.5196) 1
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
問題:結果大小不一,這對於batch訓練的神經網絡來說很不友好。
返回的樣本數值交大,未歸一化至【-1,1】
針對上述問題,pytorch提供了torchvision。它是一個視覺工具包,提供了很多視覺圖像處理的工具。
其中transforms模塊提供了對PIL Image對象和Tensor對象的常用操作。
對PIL Image的常見操作如下:
- Scale/Resize: 調整尺寸,長寬比保持不變; #Resize
- CenterCrop、RandomCrop、RandomSizedCrop:裁剪圖片;
- Pad: 填充;
- ToTensor: 將PIL Image對象轉換成Tensor,會自動將【0,255】歸一化至【0,1】。
對Tensor的常見操作如下:
- Normalize: 標准化,即減均值,除以標准差;
-
ToPILImage:將Tensor轉為PIL Image.
如果要對圖片進行多個操作,可通過Compose將這些操作拼接起來,類似於nn.Sequential.
這些操作定義之后是以對象的形式存在,真正使用時需要調用它的__call__
方法,類似於nn.Mudule.
例如:要將圖片調整為224*224,首先應構建操作trans=Scale((224,224))
,然后調用trans(img)
.
import os from PIL import Image import numpy as np from torchvision import transforms as T transforms=T.Compose([ T.Resize(224), #縮放圖片(Image),保持長寬比不變,最短邊為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) self.imgs=[os.path.join(root, img) for img in imgs] self.transforms=transforms def __getitem__(self, index): img_path=self.imgs[index] #dog->1, cat->0 label=1 if 'dog' in img_path.split("/")[-1] else 0 data=Image.open(img_path) if self.transforms: data=self.transforms(data) return data,label def __len__(self): return len(self.imgs) dataset=DogCat('N:/百度網盤/kaggle/DogCat/', transforms=transforms) img,label=dataset[0]#相當於調用dataset.__getitem__(0) for img,label in dataset: print(img.size(),label)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
結果:
torch.Size([3, 224, 224]) 0 torch.Size([3, 224, 224]) 0 torch.Size([3, 224, 224]) 0 torch.Size([3, 224, 224]) 0 torch.Size([3, 224, 224]) 1 torch.Size([3, 224, 224]) 1 torch.Size([3, 224, 224]) 1 torch.Size([3, 224, 224]) 1
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
除了上述操作外,transforms還可以通過Lambda
封裝自定義的轉換策略.
例如相對PIL Image進行隨機旋轉,則可以寫成trans=T.Lambda(lambda img: img.rotate(random()*360))
.
ImageFolder
下面介紹一個會經常使用到的Dataset——ImageFolder,它的實現和上述DogCat很相似。
ImageFolder假設所有的文件按文件夾保存,每個文件夾下存儲同一個類別的圖片,文件夾名為類名,其構造函數如下:
ImageFolder(root, transform=None, target_transform = None, loader = default_loader)
- 1
- 2
它主要有四個參數:
- root :在root指定的路徑下尋找圖片
- transform: 對PIL Image進行轉換操作, transform的輸入是使用loader讀取圖片返回的對象;
- target_transform :對label的轉換;
- loader: 指定加載圖片的函數,默認操作是讀取為PIL Image對象。
label是按照文件夾名順序排序后存成字典的,即{類名:類序號(從0開始)}
,一般來說最好直接將文件夾命名為從0開始的數字,這樣會和ImageFolder實際的label一直,如果不是這種命名規則,建議通過self.class_to_idx
屬性了解label和文件夾名的映射關系。
from torchvision.datasets import ImageFolder dataset=ImageFolder('N:\\data\\') dataset.class_to_idx
- 1
- 2
- 3
運行結果:
{'cat': 0, 'dog': 1}
- 1
輸入:
#所有圖片的路徑和對應的label dataset.imgs
- 1
- 2
輸出:
[('N:\\data\\cat\\cat.1.jpg', 0), ('N:\\data\\cat\\cat.2.jpg', 0), ('N:\\data\\cat\\cat.3.jpg', 0), ('N:\\data\\cat\\cat.4.jpg', 0), ('N:\\data\\dog\\dog.9131.jpg', 1), ('N:\\data\\dog\\dog.9132.jpg', 1), ('N:\\data\\dog\\dog.9133.jpg', 1), ('N:\\data\\dog\\dog.9134.jpg', 1)]
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
#沒有任何的transform,所以返回的還是PIL Image對象 dataset[0][1] #第一維是第幾張圖,第二維為1返回label
- 1
- 2
輸出:0
dataset[0][0] #第一維是第幾張圖,第二維為0返回圖片數據,返回的Image對象如圖所示:
- 1
輸出:
加上transform:
normilize=T.Normalize(mean=[0.4,0.4,0.4],std=[0.2,0.2,0.2]) transform=T.Compose([ T.RandomResizedCrop (224), T.RandomHorizontalFlip(), T.ToTensor(), normilize, ])
- 1
- 2
- 3
- 4
- 5
- 6
- 7
dataset=ImageFolder('N:\\data\\',transform=transform)
#深度學習中圖片數據一般保存為CxHxWx,即通道數x圖片高x圖片寬 dataset[0][0].size()
- 1
- 2
- 3
輸出:
torch.Size([3, 224, 224])
- 1
to_img=T.ToPILImage()
#0.2和0.4是標准差和均值的近似 to_img(dataset[0][0]*0.2+0.4)
- 1
- 2
- 3
輸出:
DataLoader加載數據
Dateset只負責數據的抽象,一次調用__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)
- 1
- 2
- dataset: 加載的數據集)Dataset對象;
- 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 dataloader=DataLoader(dataset, batch_size=3, shuffle=True, num_workers=0, drop_last=False) dataiter=iter(dataloader) imgs,labels=next(dataiter) imgs.size()
- 1
- 2
- 3
- 4
- 5
- 6
輸出:
torch.Size([3, 3, 224, 224])
- 1
dataloader是一個可迭代的對象,我們可以像使用迭代器一樣使用它,例如:
for batch_datas,batch_labels in dataloader: train()
- 1
- 2
或
dataiter=iter(dataloader) batch_datas,batch_labels =next(dataiter)
- 1
- 2
sampler:采樣模塊
Pytorch 中還提供了一個sampler
模塊,用來對數據進行采樣。
常用的有隨機采樣器RandonSampler
,當dataloader
的shuffle
參數為True
時,系統會自動調用這個采樣器 ,實現打亂數據。
默認的采樣器是SequentialSampler, 它會按順序一個一個進行采樣。
這里介紹另外一個很有用的采樣方法:它會根據每個樣本的權重選取數據,在樣本比例不均衡的問題中,可用它進行重采樣。
構建WeightedRandomSampler
時需提供兩個參數:每個樣本的權重weights
、共選取的樣本總數num_samples
,以及一個可選參數replacement
。權重越大的樣本被選中的概率越大,待選取的樣本數目一般小於全部樣本數目。
replacement用於指定是否可以重復選取某一個樣本,默認為True,即允許在一個epoch中重復采樣某一個數據。
如果設為False,則當某一類樣本被全部選取完,但樣本數目仍為達到num_samples時,sampler將不會再從該類中選取數據,此時可能導致weights參數失效。
下面舉例說明:
1)
#dataset=DogCat('N:/百度網盤/kaggle/DogCat/',transforms=transforms) dataset=DogCat('N:/百度網盤/kaggle/DogCat/', transforms=transforms) #img,label=dataset[0]#相當於調用dataset.__getitem__(0) #狗的圖片取出的概率是貓的概率的兩倍 #兩類取出的概率與weights的絕對值大小無關,之和比值有關 weights=[2 if label==1 else 1 for data ,label in dataset] weights
- 1
- 2
- 3
- 4
- 5
- 6
- 7
輸出:
[1, 1, 1, 1, 2, 2, 2, 2]
- 1
2)
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
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
輸出:
[1, 0, 1] [1, 0, 1] [1, 0, 1]
- 1
- 2
- 3
可見貓狗樣本比例約為1:2,另外一共有8個樣本,卻返回了9個樣本,說明樣本有被重復返回的,這就是replacement參數的作用。
下面我們將replacement設置為False.
from torch.utils.data.sampler import WeightedRandomSampler sampler=WeightedRandomSampler(weights,num_samples=8,replacement=False) dataloader=DataLoader(dataset,batch_size=4,sampler=sampler) for datas,labels in dataloader: print(labels.tolist())
- 1
- 2
- 3
- 4
- 5
輸出:
[0, 0, 1, 0] [1, 0, 1, 1]
- 1
- 2
在這種情況下,num_samples等於dataset的樣本總數,為了 不重復選取,sampler會將每個樣本都返回,這樣就失去了weight的意義。
從上面的例子可見sampler
在采樣中的作用:如果指定了sampler
,shuffle
將不再生效,並且sampler.num_smples
會覆蓋dataset
的實際大小,即一個epoch返回的圖片總數取決於sampler.num_samples
.
總結:
完整代碼:
import os from PIL import Image from torch.utils import data #import numpy as np from torchvision import transforms as T from torch.utils.data import DataLoader from torch.utils.data.sampler import WeightedRandomSampler transforms=T.Compose([ T.Resize(224), #縮放圖片(Image),保持長寬比不變,最短邊為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) self.imgs=[os.path.join(root, img) for img in imgs] self.transforms=transforms def __getitem__(self, index): img_path=self.imgs[index] #dog->1, cat->0 label=1 if 'dog' in img_path.split("/")[-1] else 0 data=Image.open(img_path) if self.transforms: data=self.transforms(data) return data,label def __len__(self): return len(self.imgs) dataset=DogCat('N:/百度網盤/kaggle/DogCat/', transforms=transforms) img,label=dataset[0]#相當於調用dataset.__getitem__(0) print("******dataset*************") print("dataset") for img,label in dataset: print(img.size(),label) dataset=DogCat('N:/百度網盤/kaggle/DogCat/', transforms=transforms) #狗的圖片取出的概率是貓的概率的兩倍 #兩類取出的概率與weights的絕對值大小無關,之和比值有關 weights=[2 if label==1 else 1 for data ,label in dataset] print("******weights**************") print("weight:{}".format(weights)) print("******sampler**************") sampler=WeightedRandomSampler(weights,num_samples=8,replacement=False) dataloader=DataLoader(dataset,batch_size=4,sampler=sampler) for datas,labels in dataloader: print(labels.tolist())
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
輸出:
******dataset************* dataset torch.Size([3, 224, 224]) 0 torch.Size([3, 224, 224]) 0 torch.Size([3, 224, 224]) 0 torch.Size([3, 224, 224]) 0 torch.Size([3, 224, 224]) 1 torch.Size([3, 224, 224]) 1 torch.Size([3, 224, 224]) 1 torch.Size([3, 224, 224]) 1 ******weights************** weight:[1, 1, 1, 1, 2, 2, 2, 2] ******sampler************** [0, 0, 1, 1] [1, 0, 1, 0]