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

前言

本文介绍了classdataset的几个要点,由哪些部分组成,每个部分需要完成哪些事情,如何进行数据增强,如何实现自己设计的数据增强。然后,介绍了分布式训练的数据加载方式,数据读取的整个流程,当面对超大数据集时,内存不足的改进思路。

(零) 概述


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

有不少人问到一些问题,根据这些问题明显能看出是对pipeline不了解,却已经在搞项目或论文了,很难想象如果基本的pipeline都不懂,如何分析代码问题所在?如何分析结果不正常的可能原因?遇到问题如何改?

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

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

本教程将介绍以下内容:

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

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

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

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

  5. 编写推理函数。

(一)数据读取


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。

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

  3. 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函数中获得的所有样本中索引对应的数据和标签。因此,可通过下面两行代码找到对应的数据和标签。

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

获得数据后,进行数据预处理。数据预处理主要通过 torchvision.transforms 来完成,这里面已经包含了常用的预处理、数据增强方式。其完整使用方式在官网有详细介绍:https://pytorch.org/vision/stable/transforms.html

上面这里介绍了最常用的几种,主要就是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,在初始化函数里获得所有数据的信息。

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

  3. 在模型训练前,初始化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,如何进行数据增强,自己设计的数据增强如何写,分布式训练是如何加载数据的,超大数据集的数据加载改进思路。

最后

感谢你们的阅读和喜欢,我收藏了很多技术干货,可以共享给喜欢我文章的朋友们,如果你肯花时间沉下心去学习,它们一定能帮到你。

因为这个行业不同于其他行业,知识体系实在是过于庞大,知识更新也非常快。作为一个普通人,无法全部学完,所以我们在提升技术的时候,首先需要明确一个目标,然后制定好完整的计划,同时找到好的学习方法,这样才能更快的提升自己。

这份完整版的大模型 AI 学习资料已经上传CSDN,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费

一、全套AGI大模型学习路线

AI大模型时代的学习之旅:从基础到前沿,掌握人工智能的核心技能!

img

二、640套AI大模型报告合集

这套包含640份报告的合集,涵盖了AI大模型的理论研究、技术实现、行业应用等多个方面。无论您是科研人员、工程师,还是对AI大模型感兴趣的爱好者,这套报告合集都将为您提供宝贵的信息和启示。

img

三、AI大模型经典PDF籍

随着人工智能技术的飞速发展,AI大模型已经成为了当今科技领域的一大热点。这些大型预训练模型,如GPT-3、BERT、XLNet等,以其强大的语言理解和生成能力,正在改变我们对人工智能的认识。 那以下这些PDF籍就是非常不错的学习资源。

img

四、AI大模型商业化落地方案

img

五、面试资料

我们学习AI大模型必然是想找到高薪的工作,下面这些面试题都是总结当前最新、最热、最高频的面试题,并且每道题都有详细的答案,面试前刷完这套面试题资料,小小offer,不在话下。
在这里插入图片描述

这份完整版的大模型 AI 学习资料已经上传CSDN,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费

  • 11
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,我可以回答你的问题。首先,你需要导入 PyTorch 库和其他必要的库,例如 pandas 和 numpy。然后,你应该准备你的数据,这里假设你有一个名为 data.csv 的数据集,其中包含分类变量和目标变量。接下来,你可以使用 pandas 库读取 csv 文件并将数据分成输入特征和目标变量。然后,你需要将分类变量转换为数字标签。这可以通过使用 LabelEncoder 类来完成,该类将每个分类变量映射到一个唯一的数字标签。接下来,你需要将数据集分成训练集和测试集。这可以通过使用 train_test_split 函数来完成。最后,你可以使用 PyTorch搭建全连接神经网络。以下是一个示例代码: ```python import torch import pandas as pd import numpy as np from sklearn.preprocessing import LabelEncoder from sklearn.model_selection import train_test_split # 读取数据集 data = pd.read_csv('data.csv') # 分离输入特征和目标变量 X = data.iloc[:, :-1].values y = data.iloc[:, -1].values # 将分类变量转换为数字标签 le = LabelEncoder() for i in range(X.shape[1]): if type(X[0][i]) == str: X[:, i] = le.fit_transform(X[:, i]) # 划分训练集和测试集 X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2) # 将 numpy 数组转换为 tensor X_train = torch.from_numpy(X_train).float() y_train = torch.from_numpy(y_train).long() X_test = torch.from_numpy(X_test).float() y_test = torch.from_numpy(y_test).long() # 定义全连接神经网络 class Net(torch.nn.Module): def __init__(self, n_feature, n_hidden, n_output): super(Net, self).__init__() self.hidden = torch.nn.Linear(n_feature, n_hidden) # 隐藏层 self.out = torch.nn.Linear(n_hidden, n_output) # 输出层 def forward(self, x): x = torch.relu(self.hidden(x)) # 激活函数 x = self.out(x) return x # 定义模型和优化器 net = Net(n_feature=X_train.shape[1], n_hidden=10, n_output=len(np.unique(y_train))) optimizer = torch.optim.SGD(net.parameters(), lr=0.01) loss_func = torch.nn.CrossEntropyLoss() # 训练模型 for epoch in range(100): out = net(X_train) loss = loss_func(out, y_train) optimizer.zero_grad() loss.backward() optimizer.step() # 输出训练误差 if epoch % 10 == 0: print('Epoch: %d | Loss: %.4f' % (epoch, loss.item())) # 测试模型 with torch.no_grad(): out = net(X_test) predictions = torch.argmax(out, axis=1) accuracy = (predictions == y_test).sum() / len(y_test) print('Accuracy:', accuracy) ``` 在这个示例中,我们使用了一个具有一个隐藏层的全连接神经网络。你可以根据你的数据集和需要调整输入特征的数量、隐藏层的大小和输出层的数量。训练模型时,我们使用了交叉熵损失函数和随机梯度下降优化器。测试模型时,我们计算了模型的准确度。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值