【深度學(xué)習(xí)】基于PyTorch深度學(xué)習(xí)框架的序列圖像數(shù)據(jù)裝載器
作者 | Harsh Maheshwari
編譯 | VK
來源 | Towards Data Science
如今,深度學(xué)習(xí)和機器學(xué)習(xí)算法正在統(tǒng)治世界。PyTorch是最常用的深度學(xué)習(xí)框架之一,用于實現(xiàn)各種深度學(xué)習(xí)算法。另一方面,基于學(xué)習(xí)的方法本質(zhì)上需要一些帶注釋的訓(xùn)練數(shù)據(jù)集,這些數(shù)據(jù)集可以被模型用來提取輸入數(shù)據(jù)和標簽之間的關(guān)系。為了給神經(jīng)網(wǎng)絡(luò)提供數(shù)據(jù),我們定義了一個數(shù)據(jù)加載器。
在這個博客中,我們將看到如何在PyTorch框架中為不同的數(shù)據(jù)集編寫一個數(shù)據(jù)加載器。
圖像數(shù)據(jù)集的數(shù)據(jù)加載器
我們將致力于狗與貓的圖像分類問題。我們需要對給定的圖像進行分類,數(shù)據(jù)集可以從這里下載:https://www.kaggle.com/c/dogs-vs-cats。訓(xùn)練數(shù)據(jù)集總共包含25000個圖像。因為這是一個分類問題,所以dog的標簽是“0”,cat的標簽是“1”。
讓我們從導(dǎo)入所有必需的庫開始。
import os
from PIL import Image
import torch
from torch.utils.data import DataLoader, Dataset
import torchvision.transforms as transforms
import torch.nn as nn
PyTorch框架的dataset類被定義為一個類,其基本結(jié)構(gòu)如下
class data(Dataset):
def __init__(self, param1, param2):
# 函數(shù)在此處初始化
def __len__(self):
# 函數(shù)返回數(shù)據(jù)的長度
def __getitem__(self, index):
# 一次提供一個項目
這個類的最終目的是使用函數(shù)
__getitem__每次提供一個數(shù)據(jù)點。這是通過使用內(nèi)部傳遞給函數(shù)的索引完成的,使用Dataloader中定義的sampler函數(shù)(將在接下來的博客中討論)。初始化數(shù)據(jù)集的對象時,會調(diào)用函數(shù)
__init__。在這里,你可以傳遞多個參數(shù),這些參數(shù)對于編寫__getitem__非常有用。函數(shù)用于返回數(shù)據(jù)集的總長度。在此基礎(chǔ)上,將生成索引,然后將其提供給
getitem。
dog vs cat數(shù)據(jù)集的格式如下-:
data/
- dog_1.jpg
- dog_2.jpg
...
...
...
- cat_1.jpg
- cat_2.jpg
...
...
...
現(xiàn)在我們已經(jīng)了解了編寫數(shù)據(jù)加載器所需的組件,讓我們深入研究一下我們的用例。
class data(Dataset):
def __init__(self, path, transform):
self.files = os.listdir(path)
self.transform = transform
self.path = path def __len__(self):
return len(self.files) def __getitem__(self, index):
filename = self.files[index]
input = Image.open(os.path.join(self.path, filename))
label = 0 if filename.find("dog")>=0 else 1
img_as_tensor = self.transform(input)
return img_as_tensor, labeltransformations = transforms.Compose(
[transforms.Resize((224,224)),transforms.ToTensor()]
)
path = "./data"
train_dataset = data(path, transformations)
dataloader = DataLoader(train_dataset, batch_size=Train_Batch_Size, shuffle=True)
首先讓我們了解函數(shù)
__init__。類數(shù)據(jù)用兩個參數(shù)path和transform初始化,這兩個參數(shù)作為參數(shù)傳遞給__init__。當我們聲明這個類的一個對象時,它會在內(nèi)部調(diào)用__init__。由于使用了len來返回整個數(shù)據(jù)集的長度,所以我使用len(self.files)來返回相同的長度。
函數(shù)getitem是最關(guān)鍵的,它加載圖像,然后調(diào)整其大小,然后將其轉(zhuǎn)換為張量。這里需要注意的一點是,提供給神經(jīng)網(wǎng)絡(luò)的數(shù)據(jù)應(yīng)該總是標準化的。我們使用transforms.ToTensor處理規(guī)范化。最后,
getitem返回兩個結(jié)果,image作為張量,label作為對應(yīng)的數(shù)據(jù)點。
在初始化類數(shù)據(jù)之后,我們使用DataLoader函數(shù)自動將整個數(shù)據(jù)批處理成一個定義的批大小。因此,如果你的原始數(shù)據(jù)點大小是(3,224,224)(你從__getitem__獲得),那么dataloader的每個項都將具有大小(batch_size,3,224,224),即它會自動對數(shù)據(jù)點的batch_size數(shù)進行采樣。
這在我們的例子中是可能的,因為圖像的大小是恒定的,所以DataLoader函數(shù)能夠自動創(chuàng)建批處理。然而,在自然語言處理這樣的情況下,當大小不是常數(shù)時,我們需要編寫自己的批處理函數(shù)。
序列數(shù)據(jù)集的數(shù)據(jù)加載器
現(xiàn)在讓我們來處理序列數(shù)據(jù)集,即句子、時間序列、音頻等。這里的__getitem__將不再提供相同大小的數(shù)據(jù)點。例如,考慮情緒分類的任務(wù)(在這里解釋),那么一句話可以是“The flight service was very good”,另一句話可以是“I did not get my baggage on the belt, pathetic service.”在這里,兩句話的長度是不同的。
為了解決這個問題,讓我們先回答三個問題。
什么是batch?-批處理是指將多個數(shù)據(jù)點的張量合并成一個張量
為什么我們需要分批處理?批處理可以用于加快計算速度,因為批處理可以同時處理多個數(shù)據(jù)點,而不是一次只處理一個數(shù)據(jù)點。
如何進行batch化?因為我們在這里合并多個張量,所以張量的每個維度的大小都需要相同。由于輸出的數(shù)據(jù)點大小不一,我們手中就有一個問題。
我們現(xiàn)在主要要解決batch化問題。
為了便于我們在這里討論,我們將使用IMDB數(shù)據(jù)集,它是一個評論數(shù)據(jù)集。因為我們在這里處理的是句子,所以處理數(shù)據(jù)集的方法會有所不同。
因為神經(jīng)網(wǎng)絡(luò)只懂數(shù)字,不懂單詞,所以我們必須把每個單詞轉(zhuǎn)換成一個數(shù)字。為了做到這一點,我們必須構(gòu)建一個詞匯表,如下代碼所述。
import os
import gensim
from collections import Counter
import json
train_path = "./aclImdb/train"
test_path = "./aclImdb/test"
# simple函數(shù)從目錄讀取數(shù)據(jù)并返回數(shù)據(jù)和標簽
# 你可以為其他數(shù)據(jù)集制作自己的讀取器。
def reader(path):
pos_path = os.path.join(path, "pos")
neg_path = os.path.join(path, "neg")
data = []
label = []
for file in os.listdir(pos_path):
f = open(os.path.join(pos_path, file))
data.append(f.read())
label.append(1)
for file in os.listdir(neg_path):
f = open(os.path.join(neg_path, file))
data.append(f.read())
label.append(0)
# print(data[:1])
return data, label
def build_vocab(data, min_word_count = 5):
counter = Counter()
for line in data:
l = gensim.utils.simple_preprocess(line)
counter.update(l)
# 初始化一個字典或查找表
word2id = {}
word2id['<pad>'] = 0
word2id['<unk>'] = 1
# 只包括那些在字典中出現(xiàn)超過min次的單詞。
words = [word for word, count in counter.items() if count>min_word_count]
for i, word in enumerate(words):
word2id[word] = i+2
with open("word2id.json", 'w') as f:
json.dump(word2id, f)
return word2id
data, label = reader(train_path)
word2id = build_vocab(data)
print("Dictionary Formed and saved. The length of dictionary is-: ", len(word2id))
函數(shù)讀取器用于讀取整個數(shù)據(jù),它返回所有句子的列表,標簽“0”表示消極評論,“1”表示積極評論。
函數(shù)build_vocab將數(shù)據(jù)和最小字數(shù)作為輸入,并將每個字的映射(稱為“word2id”)作為輸出,映射到一個唯一的數(shù)字。對于每個向前的未知單詞,對應(yīng)的數(shù)字將是1。
繼續(xù)為序列數(shù)據(jù)集編寫數(shù)據(jù)集類。我們的目標是在給定索引的情況下,一次輸出一個item。
import torch
from torch.utils.data import Dataset, DataLoader
import numpy as np
import os
import gensim
class Dataset_seq(Dataset):
def __init__(self, word2id, train_path):
self.word2id = word2id
self.train_path = train_path
# 讀取數(shù)據(jù)和標簽
self.data, self.label = reader(train_path)
def __getitem__(self, index):
# 返回seq和標簽
seq = self.preprocess(self.data[index])
label = self.label[index]
return seq, label
def __len__(self):
return(len(self.data))
def preprocess(self, text):
# 用于將line轉(zhuǎn)換為token,然后使用word2id將其轉(zhuǎn)換為相應(yīng)的數(shù)字值
line = gensim.utils.simple_preprocess(text)
seq = []
for word in line:
if word in self.word2id:
seq.append(self.word2id[word])
else:
seq.append(self.word2id['<unk>'])
# 將list轉(zhuǎn)換成張量
seq = torch.from_numpy(np.array(seq))
return seq
由于上面已經(jīng)討論了不同函數(shù)的功能,我將簡要地回顧一下。
函數(shù)
__init__采用word2id映射和train路徑。然后,init調(diào)用reader獲取與句子對應(yīng)的數(shù)據(jù)和標簽。函數(shù)
__len__返回整個數(shù)據(jù)集的長度,即self.data。函數(shù)preprocess將輸入句子轉(zhuǎn)換成數(shù)字張量,其中每個數(shù)字對應(yīng)于句子中的單詞。
函數(shù)
getitem用于在索引的幫助下輸出一個經(jīng)過處理的數(shù)據(jù)點。
下面的代碼定義了collate_fn。
train_dataset = Dataset_seq(word2id, train_path)
train_dataloader = DataLoader(dataset=train_dataset, batch_size=batch_size, shuffle=True,collate_fn=collate_fn)
def collate_fn(data):
'''
我們應(yīng)該構(gòu)建一個自定義的collate_fn,而不是使用默認的collate_fn,
因為每個句子的大小不同,并且默認不支持合并序列。
Args:
data: 元組列表 (training sequence, label)
Return:
padded_seq - 填充序列,形狀 (batch_size, padded_length)
length - 每個序列的原始長度(沒有填充), 形狀(batch_size)
label - 張量形狀 (batch_size)
'''
data.sort(key=lambda x: len(x[0]), reverse=True)
sequences, label = zip(*data)
length = [len(seq) for seq in sequences]
padded_seq = torch.zeros(len(sequences), max(length)).long()
for i, seq in enumerate(sequences):
end = length[i]
padded_seq[i,:end] = seq
return padded_seq, torch.from_numpy(np.array(length)), torch.from_numpy(np.array(label))
這里需要注意的一點是,在一個元組列表中,每個元組可以有不同的大小,但在張量中,所有維度的大小都必須相同才能合并它們。
collate_fn自動獲得一個名為data的輸入,這是一個長度等于batch size的元組列表。每個元組包含數(shù)字張量及其相應(yīng)的標簽。
為了簡單起見,我們將它們分別稱為sequence和label。所以最終我們必須以這樣一種方式轉(zhuǎn)換每個序列,使它們的大小保持不變。
為了實現(xiàn)這一點,我們執(zhí)行零填充,如上面的代碼所示。由于對整個數(shù)據(jù)集統(tǒng)一使用零填充,因此模型了解到它沒有多大用處,它只是表示浪費值。
我們肯定已經(jīng)找到了解決辦法,但問題是,這是一個最佳的解決辦法嗎?如果所有序列的原始大小都有很大的差異,或者換言之有很大的差異,那么我們最終會浪費大量的GPU內(nèi)存,而這些內(nèi)存是零填充的,這最終是沒有用的。必須有一個更好的方法來最小化零填充的要求!
這個問題的解決請關(guān)注后續(xù)文章!
往期精彩回顧 本站qq群851320808,加入微信群請掃碼:
