【深度学习】基于PyTorch深度学习框架的序列图像数据装载器

共 11375字,需浏览 23分钟

 ·

2021-08-29 10:40

作者 | Harsh Maheshwari

编译 | VK
来源 | Towards Data Science

如今,深度学习和机器学习算法正在统治世界。PyTorch是最常用的深度学习框架之一,用于实现各种深度学习算法。另一方面,基于学习的方法本质上需要一些带注释的训练数据集,这些数据集可以被模型用来提取输入数据和标签之间的关系。为了给神经网络提供数据,我们定义了一个数据加载器。

在这个博客中,我们将看到如何在PyTorch框架中为不同的数据集编写一个数据加载器。


图像数据集的数据加载器


我们将致力于狗与猫的图像分类问题。我们需要对给定的图像进行分类,数据集可以从这里下载:https://www.kaggle.com/c/dogs-vs-cats。训练数据集总共包含25000个图像。因为这是一个分类问题,所以dog的标签是“0”,cat的标签是“1”。

让我们从导入所有必需的库开始。

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类被定义为一个类,其基本结构如下

class data(Dataset):
   def __init__(self, param1, param2):
        # 函数在此处初始化

   def __len__(self):
        # 函数返回数据的长度

   def __getitem__(self, index):
        # 一次提供一个项目

  • 这个类的最终目的是使用函数 __getitem__每次提供一个数据点。这是通过使用内部传递给函数的索引完成的,使用Dataloader中定义的sampler函数(将在接下来的博客中讨论)。

  • 初始化数据集的对象时,会调用函数 __init__。在这里,你可以传递多个参数,这些参数对于编写 __getitem__非常有用。

  • 函数用于返回数据集的总长度。在此基础上,将生成索引,然后将其提供给getitem

dog vs cat数据集的格式如下-:

data/
   - dog_1.jpg
   - dog_2.jpg
    ...
    ...
    ...
   - cat_1.jpg
   - cat_2.jpg
    ...
    ...
    ...

现在我们已经了解了编写数据加载器所需的组件,让我们深入研究一下我们的用例。

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.”在这里,两句话的长度是不同的。

为了解决这个问题,让我们先回答三个问题。

  1. 什么是batch?-批处理是指将多个数据点的张量合并成一个张量

  2. 为什么我们需要分批处理?批处理可以用于加快计算速度,因为批处理可以同时处理多个数据点,而不是一次只处理一个数据点。

  3. 如何进行batch化?因为我们在这里合并多个张量,所以张量的每个维度的大小都需要相同。由于输出的数据点大小不一,我们手中就有一个问题。

我们现在主要要解决batch化问题。

为了便于我们在这里讨论,我们将使用IMDB数据集,它是一个评论数据集。因为我们在这里处理的是句子,所以处理数据集的方法会有所不同。

因为神经网络只懂数字,不懂单词,所以我们必须把每个单词转换成一个数字。为了做到这一点,我们必须构建一个词汇表,如下代码所述。

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。

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

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):
    '''  
    我们应该构建一个自定义的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内存,而这些内存是零填充的,这最终是没有用的。必须有一个更好的方法来最小化零填充的要求!

这个问题的解决请关注后续文章!

往期精彩回顾




本站qq群851320808,加入微信群请扫码:
浏览 75
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报