从零搭建Pytorch模型教程(一)数据读取 ​

本文涉及的产品
云原生大数据计算服务 MaxCompute,5000CU*H 100GB 3个月
云原生大数据计算服务MaxCompute,500CU*H 100GB 3个月
简介: 本文介绍了classdataset的几个要点,由哪些部分组成,每个部分需要完成哪些事情,如何进行数据增强,如何实现自己设计的数据增强。然后,介绍了分布式训练的数据加载方式,数据读取的整个流程,当面对超大数据集时,内存不足的改进思路。欢迎关注公众号CV技术指南,专注于计算机视觉的技术总结、最新技术跟踪、最新论文解读、各种技术教程、CV招聘信息发布等。关注公众号可邀请加入免费版的知识星球和技术交流群。

(零) 概述


浮躁是人性的一个典型的弱点,很多人总擅长看别人分享的现成代码解读的文章,看起来学会了好多东西,实际上仍然不具备自己从零搭建一个pipeline的能力。


在公众号(CV技术指南)的交流群里(群内交流氛围不错,有需要的请关注公众号加群),常有不少人问到一些问题,根据这些问题明显能看出是对pipeline不了解,却已经在搞项目或论文了,很难想象如果基本的pipeline都不懂,如何分析代码问题所在?如何分析结果不正常的可能原因?遇到问题如何改?


Pytorch在这几年逐渐成为了学术上的主流框架,其具有简单易懂的特点。网上有很多pytorch的教程,如果是一个已经懂的人去看这些教程,确实pipeline的要素都写到了,感觉这教程挺不错的。但实际上更多地像是写给自己看的一个笔记,记录了pipeline要写哪些东西,却没有介绍要怎么写,为什么这么写,刚入门的小白看的时候容易云里雾里。

鉴于此,本教程尝试对于pytorch搭建一个完整pipeline写一个比较明确且易懂的说明。

本教程将介绍以下内容:


  1. 准备数据,自定义classdataset,分布式训练的数据加载方式,加载超大数据集的改进思路。


  1. 搭建模型与模型初始化。


  1. 编写训练过程,包括加载预训练模型、设置优化器、设置损失函数等。


  1. 可视化并保存训练过程。


  1. 编写推理函数。

(一)数据读取


classdataset的定义

先来看一个完整的classdataset


import torch.utils.data as data
import torchvision.transforms as transforms
class MyDataset(data.Dataset):
   def __init__(self,data_folder):
       self.data_folder = data_folder
       self.filenames = []
       self.labels = []
       per_classes = os.listdir(data_folder)
       for per_class in per_classes:
           per_class_paths = os.path.join(data_folder, per_class)
           label = torch.tensor(int(per_class))
           per_datas = os.listdir(per_class_paths)
           for per_data in per_datas:
               self.filenames.append(os.path.join(per_class_paths, per_data))
               self.labels.append(label)
   def __getitem__(self, index):
       image = Image.open(self.filenames[index])
       label = self.labels[index]
       data = self.proprecess(image)
       return data, label
   def __len__(self):
       return len(self.filenames)
   def proprecess(self,data):
       transform_train_list = [
           transforms.Resize((self.opt.h, self.opt.w), interpolation=3),
           transforms.Pad(self.opt.pad, padding_mode='edge'),
           transforms.RandomCrop((self.opt.h, self.opt.w)),
           transforms.RandomHorizontalFlip(),
           transforms.ToTensor(),
           transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
      ]
       return transforms.Compose(transform_train_list)

classdataset的几个要点:


  1. classdataset类继承torch.utils.data.dataset。


  1. classdataset的作用是将任意格式的数据,通过读取、预处理或数据增强后以tensor的形式输出。其中任意格式的数据可能是以文件夹名作为类别的形式、或以txt文件存储图片地址的形式、或视频、或十几帧图像作为一份样本的形式。而输出则指的是经过处理后的一个batch的tensor格式数据和对应标签。


  1. classdataset主要有三个函数要完成:__init__函数、__getitem__ 函数和__len__函数。

 

__init__函数


init函数主要是完成两个静态变量的赋值。一个是用于存储所有数据路径的变量,变量的每个元素即为一份训练样本,(注:如果一份样本是十几帧图像,则变量每个元素存储的是这十几帧图像的路径),可以命名为self.filenames。一个是用于存储与数据路径变量一一对应的标签变量,可以命名为self.labels。


假如数据集的格式如下:


#这里的0,1指的是类别0,1
/data_path/0/image0.jpg
/data_path/0/image1.jpg
/data_path/0/image2.jpg
/data_path/0/image3.jpg
......
/data_path/1/image0.jpg
/data_path/1/image1.jpg
/data_path/1/image2.jpg
/data_path/1/image3.jpg

可通过per_classes = os.listdir(data_path) 获得所有类别的文件夹,在此处per_classes的每个元素即为对应的数据标签,通过for遍历per_classes即可获得每个类的标签,将其转换成int的tensor形式即可。在for下获得每个类下每张图片的路径,通过self.join获得每份样本的路径,通过append添加到self.filenames中。

 

__getitem__ 函数


getitem 函数主要是根据索引返回对应的数据。这个索引是在训练前通过dataloader切片获得的,这里先不管。它的参数默认是index,即每次传回在init函数中获得的所有样本中索引对应的数据和标签。因此,可通过下面两行代码找到对应的数据和标签。

欢迎关注公众号CV技术指南,专注于计算机视觉的技术总结、最新技术跟踪、经典论文解读

image = Image.open(self.filenames[index]))
label = self.labels[index]

获得数据后,进行数据预处理。数据预处理主要通过 torchvision.transforms 来完成,这里面已经包含了常用的预处理、数据增强方式


上面这里介绍了最常用的几种,主要就是resize,随机裁剪,翻转,归一化等。

最后通过transforms.Compose(transform_train_list)来执行。

 

除了这些已经有的数据增强方式外,在《数据增强方法总结》中还介绍了十几种特殊的数据增强方式,像这种自己设计了一种新的数据增强方式,该如何添加进去呢

下面以随机擦除作为例子。


class RandomErasing(object):
   """ Randomly selects a rectangle region in an image and erases its pixels.
      'Random Erasing Data Augmentation' by Zhong et al.
      See https://arxiv.org/pdf/1708.04896.pdf
  Args:
        probability: The probability that the Random Erasing operation will be performed.
        sl: Minimum proportion of erased area against input image.
        sh: Maximum proportion of erased area against input image.
        r1: Minimum aspect ratio of erased area.
        mean: Erasing value.
  """
   def __init__(self, probability=0.5, sl=0.02, sh=0.4, r1=0.3, mean=[0.4914, 0.4822, 0.4465]):
       self.probability = probability
       self.mean = mean
       self.sl = sl
       self.sh = sh
       self.r1 = r1
   def __call__(self, img):
       if random.uniform(0, 1) > self.probability:
           return img
       for attempt in range(100):
           area = img.size()[1] * img.size()[2]
           target_area = random.uniform(self.sl, self.sh) * area
           aspect_ratio = random.uniform(self.r1, 1 / self.r1)
           h = int(round(math.sqrt(target_area * aspect_ratio)))
           w = int(round(math.sqrt(target_area / aspect_ratio)))
           if w < img.size()[2] and h < img.size()[1]:
               x1 = random.randint(0, img.size()[1] - h)
               y1 = random.randint(0, img.size()[2] - w)
               if img.size()[0] == 3:
                   img[0, x1:x1 + h, y1:y1 + w] = self.mean[0]
                   img[1, x1:x1 + h, y1:y1 + w] = self.mean[1]
                   img[2, x1:x1 + h, y1:y1 + w] = self.mean[2]
               else:
                   img[0, x1:x1 + h, y1:y1 + w] = self.mean[0]
               return img
       return img

如上所示,自己写一个类RandomErasing,继承object,在call函数里完成你的操作。在transform_train_list里添加上RandomErasing的定义即可。


transform_train_list = [
          transforms.Resize((self.opt.h, self.opt.w), interpolation=3),
          transforms.Pad(self.opt.pad, padding_mode='edge'),
          transforms.RandomCrop((self.opt.h, self.opt.w)),
          transforms.RandomHorizontalFlip(),
          transforms.ToTensor(),
          transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
          RandomErasing(probability=self.opt.erasing_p, mean=[0.0, 0.0, 0.0])
          #添加到这里
      ]

__len__函数


len函数主要就是返回数据长度,即样本的总数量。前面介绍了self.filenames的每个元素即为每份样本的路径,因此,self.filename的长度就是样本的数量。通过return len(self.filenames)即可返回数据长度。

 

验证classdataset

train_dataset = My_Dataset(data_folder=data_folder)
train_loader = DataLoader(train_dataset, batch_size=16, shuffle=False)
print('there are total %s batches for train' % (len(train_loader)))
for i,(data,label) in enumerate(train_loader):
    print(data.size(),label.size())

分布式训练的数据加载方式


前面介绍的是单卡的数据加载,实际上分布式也是这样,但为了高速高效读取,每张卡上也会保存所有数据的信息,即self.filenames和self.labels的信息。只是在DistributedSampler 中会给每张卡分配互不交叉的索引,然后由torch.utils.data.DataLoader来加载。

dataset = My_Dataset(data_folder=data_folder)
sampler = DistributedSampler(dataset) if is_distributed else None
loader = DataLoader(dataset, shuffle=(sampler is None), sampler=sampler)

数据读取的完整流程


结合上面这段代码,在这里,我们介绍以下读取数据的整个流程。


  1. 首先定义一个classdataset,在初始化函数里获得所有数据的信息。


  1. classdataset中实现getitem函数,通过索引来获取对应的数据,然后对数据进行预处理和数据增强。


  1. 在模型训练前,初始化classdataset,通过Dataloader来加载数据,其加载方式是通过Dataloader中分配的索引,调用getitem函数来获取。


关于索引的分配,在单卡上,可通过设置shuffle=True来随机生成索引顺序;在多机多卡的分布式训练上,shuffle操作通过DistributedSampler来完成,因此shuffle与sampler只能有一个,另一个必须为None。

 

超大数据集的加载思路


问题所在


再回顾一下上面这个流程,前面提到所有数据信息在classdataset初始化部分都会保存在变量中,因此当面对超大数据集时,会出现内存不足的情况。

思路


将切片获取索引的步骤放到classdataset初始化的位置,此时每张卡都是保存不同的数据子集。通过这种方式,可以将内存用量减少到原来的world_size倍(world_size指卡的数量)。


参考代码


class RankDataset(Dataset):
   '''
  实际流程
  获取rank和world_size 信息 -> 获取dataset长度 -> 根据dataset长度产生随机indices ->
  给不同的rank 分配indices -> 根据这些indices产生metas
  '''
   def __init__(self, meta_file, world_size, rank, seed):
       super(RankDataset, self).__init__()
       random.seed(seed)
       np.random.seed(seed)
       self.world_size = world_size
       self.rank = rank
       self.metas = self.parse(meta_file)
   def parse(self, meta_file):
       dataset_size = self.get_dataset_size(meta_file)                                     # 获取metafile的行数
       local_rank_index = self.get_local_index(dataset_size, self.rank, self.world_size)   # 根据world size和rank,获取当前epoch,当前rank需要训练的index。
       self.metas = self.read_file(meta_file, local_rank_index)
   def __getitem__(self, idx):
       return self.metas[idx]
   def __len__(self):
       return len(self.metas)
##train
for epoch_num in range(epoch_num):
   dataset = RankDataset("/path/to/meta", world_size, rank, seed=epoch_num)
   sampler = RandomSampler(datset)
   dataloader = DataLoader(
               dataset=dataset,
               batch_size=32,
               shuffle=False,
               num_workers=4,
               sampler=sampler)

但这种思路比较明显的问题时,为了让每张卡上在每个epoch都加载不同的训练子集,因此需要在每个epoch重新build dataloader。


总结


本篇文章介绍了数据读取的完整流程,如何自定义classdataset,如何进行数据增强,自己设计的数据增强如何写,分布式训练是如何加载数据的,超大数据集的数据加载改进思路。

相信读完本文的读者对数据读取有了比较清晰的认识,下一篇将介绍搭建模型与模型初始化。


欢迎关注公众号CV技术指南,专注于计算机视觉的技术总结、最新技术跟踪、最新论文解读、各种技术教程、CV招聘信息发布等。关注公众号可邀请加入免费版的知识星球和技术交流群。

相关实践学习
基于MaxCompute的热门话题分析
本实验围绕社交用户发布的文章做了详尽的分析,通过分析能得到用户群体年龄分布,性别分布,地理位置分布,以及热门话题的热度。
SaaS 模式云数据仓库必修课
本课程由阿里云开发者社区和阿里云大数据团队共同出品,是SaaS模式云原生数据仓库领导者MaxCompute核心课程。本课程由阿里云资深产品和技术专家们从概念到方法,从场景到实践,体系化的将阿里巴巴飞天大数据平台10多年的经过验证的方法与实践深入浅出的讲给开发者们。帮助大数据开发者快速了解并掌握SaaS模式的云原生的数据仓库,助力开发者学习了解先进的技术栈,并能在实际业务中敏捷的进行大数据分析,赋能企业业务。 通过本课程可以了解SaaS模式云原生数据仓库领导者MaxCompute核心功能及典型适用场景,可应用MaxCompute实现数仓搭建,快速进行大数据分析。适合大数据工程师、大数据分析师 大量数据需要处理、存储和管理,需要搭建数据仓库?学它! 没有足够人员和经验来运维大数据平台,不想自建IDC买机器,需要免运维的大数据平台?会SQL就等于会大数据?学它! 想知道大数据用得对不对,想用更少的钱得到持续演进的数仓能力?获得极致弹性的计算资源和更好的性能,以及持续保护数据安全的生产环境?学它! 想要获得灵活的分析能力,快速洞察数据规律特征?想要兼得数据湖的灵活性与数据仓库的成长性?学它! 出品人:阿里云大数据产品及研发团队专家 产品 MaxCompute 官网 https://www.aliyun.com/product/odps&nbsp;
相关文章
|
1月前
|
存储 物联网 PyTorch
基于PyTorch的大语言模型微调指南:Torchtune完整教程与代码示例
**Torchtune**是由PyTorch团队开发的一个专门用于LLM微调的库。它旨在简化LLM的微调流程,提供了一系列高级API和预置的最佳实践
151 59
基于PyTorch的大语言模型微调指南:Torchtune完整教程与代码示例
|
2月前
|
算法 PyTorch 算法框架/工具
Pytorch学习笔记(九):Pytorch模型的FLOPs、模型参数量等信息输出(torchstat、thop、ptflops、torchsummary)
本文介绍了如何使用torchstat、thop、ptflops和torchsummary等工具来计算Pytorch模型的FLOPs、模型参数量等信息。
297 2
|
23天前
|
并行计算 监控 搜索推荐
使用 PyTorch-BigGraph 构建和部署大规模图嵌入的完整教程
当处理大规模图数据时,复杂性难以避免。PyTorch-BigGraph (PBG) 是一款专为此设计的工具,能够高效处理数十亿节点和边的图数据。PBG通过多GPU或节点无缝扩展,利用高效的分区技术,生成准确的嵌入表示,适用于社交网络、推荐系统和知识图谱等领域。本文详细介绍PBG的设置、训练和优化方法,涵盖环境配置、数据准备、模型训练、性能优化和实际应用案例,帮助读者高效处理大规模图数据。
46 5
|
2月前
|
机器学习/深度学习 自然语言处理 监控
利用 PyTorch Lightning 搭建一个文本分类模型
利用 PyTorch Lightning 搭建一个文本分类模型
63 8
利用 PyTorch Lightning 搭建一个文本分类模型
|
2月前
|
机器学习/深度学习 自然语言处理 数据建模
三种Transformer模型中的注意力机制介绍及Pytorch实现:从自注意力到因果自注意力
本文深入探讨了Transformer模型中的三种关键注意力机制:自注意力、交叉注意力和因果自注意力,这些机制是GPT-4、Llama等大型语言模型的核心。文章不仅讲解了理论概念,还通过Python和PyTorch从零开始实现这些机制,帮助读者深入理解其内部工作原理。自注意力机制通过整合上下文信息增强了输入嵌入,多头注意力则通过多个并行的注意力头捕捉不同类型的依赖关系。交叉注意力则允许模型在两个不同输入序列间传递信息,适用于机器翻译和图像描述等任务。因果自注意力确保模型在生成文本时仅考虑先前的上下文,适用于解码器风格的模型。通过本文的详细解析和代码实现,读者可以全面掌握这些机制的应用潜力。
95 3
三种Transformer模型中的注意力机制介绍及Pytorch实现:从自注意力到因果自注意力
|
3月前
|
机器学习/深度学习 PyTorch 调度
在Pytorch中为不同层设置不同学习率来提升性能,优化深度学习模型
在深度学习中,学习率作为关键超参数对模型收敛速度和性能至关重要。传统方法采用统一学习率,但研究表明为不同层设置差异化学习率能显著提升性能。本文探讨了这一策略的理论基础及PyTorch实现方法,包括模型定义、参数分组、优化器配置及训练流程。通过示例展示了如何为ResNet18设置不同层的学习率,并介绍了渐进式解冻和层适应学习率等高级技巧,帮助研究者更好地优化模型训练。
182 4
在Pytorch中为不同层设置不同学习率来提升性能,优化深度学习模型
|
3月前
|
机器学习/深度学习 监控 PyTorch
PyTorch 模型调试与故障排除指南
在深度学习领域,PyTorch 成为开发和训练神经网络的主要框架之一。本文为 PyTorch 开发者提供全面的调试指南,涵盖从基础概念到高级技术的内容。目标读者包括初学者、中级开发者和高级工程师。本文探讨常见问题及解决方案,帮助读者理解 PyTorch 的核心概念、掌握调试策略、识别性能瓶颈,并通过实际案例获得实践经验。无论是在构建简单神经网络还是复杂模型,本文都将提供宝贵的洞察和实用技巧,帮助开发者更高效地开发和优化 PyTorch 模型。
48 3
PyTorch 模型调试与故障排除指南
|
2月前
|
存储 并行计算 PyTorch
探索PyTorch:模型的定义和保存方法
探索PyTorch:模型的定义和保存方法
|
4月前
|
机器学习/深度学习 PyTorch 编译器
PyTorch 与 TorchScript:模型的序列化与加速
【8月更文第27天】PyTorch 是一个非常流行的深度学习框架,它以其灵活性和易用性而著称。然而,当涉及到模型的部署和性能优化时,PyTorch 的动态计算图可能会带来一些挑战。为了解决这些问题,PyTorch 引入了 TorchScript,这是一个用于序列化和优化 PyTorch 模型的工具。本文将详细介绍如何使用 TorchScript 来序列化 PyTorch 模型以及如何加速模型的执行。
158 4
|
4月前
|
机器学习/深度学习 边缘计算 PyTorch
PyTorch 与边缘计算:将深度学习模型部署到嵌入式设备
【8月更文第29天】随着物联网技术的发展,越来越多的数据处理任务开始在边缘设备上执行,以减少网络延迟、降低带宽成本并提高隐私保护水平。PyTorch 是一个广泛使用的深度学习框架,它不仅支持高效的模型训练,还提供了多种工具帮助开发者将模型部署到边缘设备。本文将探讨如何将PyTorch模型高效地部署到嵌入式设备上,并通过一个具体的示例来展示整个流程。
688 1