磐创AI分享
作者 | Harsh Maheshwari
编译 | VK 来源 | Towards Data Science
如今,深度学习和机器学习算法正在统治世界。PyTorch是最常用的深度学习框架之一,用于实现各种深度学习算法。另一方面,基于学习的方法本质上需要一些带注释的训练数据集,这些数据集可以被模型用来提取输入数据和标签之间的关系。为了给神经网络提供数据,我们定义了一个数据加载器。
在这个博客中,我们将看到如何在PyTorch框架中为不同的数据集编写一个数据加载器。
图像数据集的数据加载器
我们将致力于狗与猫的图像分类问题。我们需要对给定的图像进行分类,数据集可以从这里下载:https://www.kaggle.com/c/dogs-vs-cats。训练数据集总共包含25000个图像。因为这是一个分类问题,所以dog的标签是“0”,cat的标签是“1”。
让我们从导入所有必需的库开始。
代码语言:javascript复制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类被定义为一个类,其基本结构如下
代码语言:javascript复制class data(Dataset):
def __init__(self, param1, param2):
# 函数在此处初始化
def __len__(self):
# 函数返回数据的长度
def __getitem__(self, index):
# 一次提供一个项目
- 这个类的最终目的是使用函数
__getitem__
每次提供一个数据点。这是通过使用内部传递给函数的索引完成的,使用Dataloader中定义的sampler函数(将在接下来的博客中讨论)。 - 初始化数据集的对象时,会调用函数
__init__
。在这里,你可以传递多个参数,这些参数对于编写__getitem__
非常有用。 - 函数用于返回数据集的总长度。在此基础上,将生成索引,然后将其提供给
getitem
。
dog vs cat数据集的格式如下-:
代码语言:javascript复制data/
- dog_1.jpg
- dog_2.jpg
...
...
...
- cat_1.jpg
- cat_2.jpg
...
...
...
现在我们已经了解了编写数据加载器所需的组件,让我们深入研究一下我们的用例。
代码语言:javascript复制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)
- 首先让我们了解函数
__init__
。类数据用两个参数path和transform初始化,这两个参数作为参数传递给__init__
。当我们声明这个类的一个对象时,它会在内部调用__init__
。 - 由于使用了len来返回整个数据集的长度,所以我使用len(self.files)来返回相同的长度。
- 函数getitem是最关键的,它加载图像,然后调整其大小,然后将其转换为张量。这里需要注意的一点是,提供给神经网络的数据应该总是标准化的。我们使用transforms.ToTensor处理规范化。最后,
getitem
返回两个结果,image作为张量,label作为对应的数据点。
在初始化类数据之后,我们使用DataLoader函数自动将整个数据批处理成一个定义的批大小。因此,如果你的原始数据点大小是(3,224,224)(你从__getitem__
获得),那么dataloader的每个项都将具有大小(batch_size,3,224,224),即它会自动对数据点的batch_size数进行采样。
这在我们的例子中是可能的,因为图像的大小是恒定的,所以DataLoader函数能够自动创建批处理。然而,在自然语言处理这样的情况下,当大小不是常数时,我们需要编写自己的批处理函数。
序列数据集的数据加载器
现在让我们来处理序列数据集,即句子、时间序列、音频等。这里的__getitem__
将不再提供相同大小的数据点。例如,考虑情绪分类的任务(在这里解释),那么一句话可以是“The flight service was very good”,另一句话可以是“I did not get my baggage on the belt, pathetic service.”在这里,两句话的长度是不同的。
为了解决这个问题,让我们先回答三个问题。
- 什么是batch?-批处理是指将多个数据点的张量合并成一个张量
- 为什么我们需要分批处理?批处理可以用于加快计算速度,因为批处理可以同时处理多个数据点,而不是一次只处理一个数据点。
- 如何进行batch化?因为我们在这里合并多个张量,所以张量的每个维度的大小都需要相同。由于输出的数据点大小不一,我们手中就有一个问题。
我们现在主要要解决batch化问题。
为了便于我们在这里讨论,我们将使用IMDB数据集,它是一个评论数据集。因为我们在这里处理的是句子,所以处理数据集的方法会有所不同。
因为神经网络只懂数字,不懂单词,所以我们必须把每个单词转换成一个数字。为了做到这一点,我们必须构建一个词汇表,如下代码所述。
代码语言:javascript复制import os
import gensim
from collections import Counter
import json
train_path = "./aclImdb/train"
test_path = "./aclImdb/test"
# simple函数从目录读取数据并返回数据和标签
# 你可以为其他数据集制作自己的读取器。
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
# 只包括那些在字典中出现超过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))
- 函数读取器用于读取整个数据,它返回所有句子的列表,标签“0”表示消极评论,“1”表示积极评论。
- 函数build_vocab将数据和最小字数作为输入,并将每个字的映射(称为“word2id”)作为输出,映射到一个唯一的数字。对于每个向前的未知单词,对应的数字将是1。
继续为序列数据集编写数据集类。我们的目标是在给定索引的情况下,一次输出一个item。
代码语言:javascript复制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
# 读取数据和标签
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转换为token,然后使用word2id将其转换为相应的数字值
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转换成张量
seq = torch.from_numpy(np.array(seq))
return seq
由于上面已经讨论了不同函数的功能,我将简要地回顾一下。
- 函数
__init__
采用word2id映射和train路径。然后,init调用reader获取与句子对应的数据和标签。 - 函数
__len__
返回整个数据集的长度,即self.data。 - 函数preprocess将输入句子转换成数字张量,其中每个数字对应于句子中的单词。
- 函数
getitem
用于在索引的帮助下输出一个经过处理的数据点。
下面的代码定义了collate_fn。
代码语言:javascript复制train_dataset = Dataset_seq(word2id, train_path)
train_dataloader = DataLoader(dataset=train_dataset, batch_size=batch_size, shuffle=True,collate_fn=collate_fn)
代码语言:javascript复制def collate_fn(data):
'''
我们应该构建一个自定义的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的元组列表。每个元组包含数字张量及其相应的标签。
为了简单起见,我们将它们分别称为sequence和label。所以最终我们必须以这样一种方式转换每个序列,使它们的大小保持不变。
为了实现这一点,我们执行零填充,如上面的代码所示。由于对整个数据集统一使用零填充,因此模型了解到它没有多大用处,它只是表示浪费值。
我们肯定已经找到了解决办法,但问题是,这是一个最佳的解决办法吗?如果所有序列的原始大小都有很大的差异,或者换言之有很大的差异,那么我们最终会浪费大量的GPU内存,而这些内存是零填充的,这最终是没有用的。必须有一个更好的方法来最小化零填充的要求!
这个问题的解决请关注后续文章!