深度神经网络需要很长时间来训练。训练速度受模型的复杂性、批大小、GPU、训练数据集的大小等因素的影响。
在PyTorch中,torch.utils.data.Dataset和torch.utils.data.DataLoader通常用于加载数据集和生成批处理。但是从版本1.11开始,PyTorch引入了TorchData库,它实现了一种不同的加载数据集的方法。
在本文中,我们将比较数据集比较大的情况下这两两种方法是如何工作的。我们以CelebA和DigiFace1M的面部图像为例。表1显示了它们的比较特征。我们训练使用ResNet-50模型。然后进行1轮的训练来进行使用方法和时间的比较。
数据集的信息如下:
CelebA (align) 图片数:202,599 总大小:1.4 图片大小:178x218
DigiFace1M 图片数:720,000 总大小:14.6 图片大小:112x112
我们使用的环境如下:
CPU: Intel® Core™ i9-9900K CPU @ 3.60GHz(16核)
GPU: GeForce RTX 2080 Ti 11Gb
驱动版本515.65.01 / CUDA 11.7 / CUDNN 8.4.0.27
Docker 20.10.21
Pytorch 1.12.1
TrochData 0.4.1
训练的代码如下:
def train(data_loader: torch.utils.data.DataLoader, cfg: Config):
# create model
model = resnet50(num_classes=cfg.n_celeba_classes + cfg.n_digiface1m_classes, pretrained=True)
torch.cuda.set_device(cfg.gpu)
model = model.cuda(cfg.gpu)
model.train()
# define loss function (criterion) and optimizer
criterion = torch.nn.CrossEntropyLoss().cuda(cfg.gpu)
optimizer = torch.optim.SGD(model.parameters(), lr=0.1,
momentum=0.9,
weight_decay=1e-4)
start_time = time.time()
for _ in range(cfg.epochs):
scaler = torch.cuda.amp.GradScaler(enabled=cfg.use_amp)
for batch_idx, (images, target) in enumerate(data_loader):
images = images.cuda(cfg.gpu, non_blocking=True)
target = target.cuda(cfg.gpu, non_blocking=True)
# compute output
with torch.cuda.amp.autocast(enabled=cfg.use_amp):
output = model(images)
loss = criterion(output, target)
# compute gradient
scaler.scale(loss).backward()
# do SGD step
scaler.step(optimizer)
scaler.update()
optimizer.zero_grad()
print(batch_idx, loss.item())
print(f'{time.time() - start_time} sec')
Dataset
首先看看Dataset,这是自从Pytorch发布以来一直使用的方式,我们对这个应该非常熟悉。PyTorch 支持两种类型的数据集:map-style Datasets 和 iterable-style Datasets。Map-style Dataset 在预先知道元素个数的情况下使用起来很方便。
该类实现了__getitem__()和__len__()方法。如果通过索引读取太费时间或者无法获得,那么可以使用 iterable-style,需要实现__iter__() 方法。在我们的例子中,map-style已经可以了,因为对于 CelebA 和 DigiFace1M 数据集,我们知道其中的图像总数。
下面我们创建CelebADataset 类。对于 CelebA,类标签位于 identity_CelebA.txt 文件中。CelebA 和 DigiFace1M 中的面部图像在裁剪方面有所不同,因此为了在图像上传后减少getitem方法中的这些差异,必须从各个方面稍微裁剪它们。
from PIL import Image
from torch.utils.data import Dataset
class CelebADataset(torch.utils.data.Dataset):
def __init__(self, data_path: str, transform) -> None:
self.data_path = data_path
self.transform = transform
self.image_names, self.labels = self.load_labels(f'{data_path}/identity_CelebA.txt')
def __len__(self) -> int:
return len(self.image_names)
def __getitem__(self, idx: int) -> Tuple[torch.Tensor, int]:
image_path = f'{self.data_path}/img_align_celeba/{self.image_names[idx]}'
image = Image.open(image_path)
left, right, top, bottom = 25, 153, 45, 173
image = image.crop((left, top, right, bottom))
if self.transform is not None:
image = self.transform(image)
label = self.labels[idx]
return image, label
@staticmethod
def load_labels(labels_path: str) -> Tuple[list, list]:
image_names, labels = [], []
with open(labels_path, 'r', encoding='utf-8') as labels_file:
lines = labels_file.readlines()
for line in lines:
file_name, class_id = line.split(' ')
image_names.append(file_name)
labels.append(int(class_id[:-1]))
return image_names, labels
对于DigiFace1M数据集,同一类的所有图像都在一个单独的文件夹中。但是这两个数据集中,类的标签是相同的,所以对于在DigiFace1M我们不需要获取类别,而是在CelebA中按类增加。所以我们需要add_to_class变量。另外就是DigiFace1M中的图像以“RGBA”格式存储,因此仍需将其转换为“RGB”。
class DigiFace1M(torch.utils.data.Dataset):
def __init__(self, data_path: str, transform, add_to_class: int = 0) -> None:
self.data_path = data_path
self.transform = transform
self.image_paths, self.labels = self.load_labels(data_path, add_to_class)
def __len__(self):
return len(self.image_paths)
def __getitem__(self, idx: int) -> Tuple[torch.Tensor, int]:
image = Image.open(self.image_paths[idx]).convert('RGB')
if self.transform is not None:
image = self.transform(image)
label = self.labels[idx]
return image, label
@staticmethod
def load_labels(data_path: str, add_to_class: int) -> Tuple[list, list]:
image_paths, labels = [], []
for root, _, files in os.walk(data_path):
for file_name in files:
if file_name.endswith('.png'):
image_paths.append(f'{root}/{file_name}')
labels.append(int(os.path.basename(root)) + add_to_class)
return image_paths, labels
现在我们可以使用torch.utils.data将两个数据集合并为一个数据集ConcatDataset,创建DataLoader,开始训练。
def main():
cfg = Config()
celeba_dataset = CelebADataset(f'{cfg.data_path}/CelebA', cfg.transform)
digiface_dataset = DigiFace1M(f'{cfg.data_path}/DigiFace1M', cfg.transform, cfg.n_celeba_classes)
dataset = torch.utils.data.ConcatDataset([celeba_dataset, digiface_dataset])
loader = torch.utils.data.DataLoader(
dataset=dataset,
batch_size=cfg.batch_size,
shuffle=True,
drop_last=True,
num_workers=cfg.n_workers)
utils.train(loader, cfg)
TorchData API
与Dataset一样,TorchData支持map-style 和 iterable-style的数据处理管道。但是官方建议使用IterDataPipe,只在必要时将其转换为MapDataPipe。
因为TorchData提供了优化的数据加载实用程序,可以帮助我们方便的构建处理流程。以下是一些主要的功能:
- IterableWrapper:包装可迭代对象以创建IterDataPipe。
- FileListerr:给定目录的路径,将生成根目录内文件的文件路径名(path + filename)
- Filterr:根据输入filter_fn(函数名:filter)从源数据口过滤元素
- Mapperr:对源DataPipe中的每个项应用函数(函数名:map)
- Concaterr:连接多个可迭代数据管道(函数名:concat)
- Shufflerr:打乱输入DataPipe数据的顺序(函数名:shuffle)
- ShardingFilterr:允许对DataPipe进行分片(函数名:sharding_filter)
使用TorchData 构建CelebA和DigiFace1M的数据处理管道,我们需要执行以下步骤:
对于CelebA数据集:创建一个列表(file_name, label, ’ CelebA '),并使用IterableWrapper从它创建一个IterDataPipe
对于DigiFace1M:使用FileLister创建一个IterDataPipe,返回所有图像文件的路径,使用Mapper来使用collate_ann。这个函数以图像路径作为输入,并返回元组(file_name, label, ’ DigiFace1M ')。
上面两个步骤之后,我们得到两个数据类型(file_name, label, data_name)的结果。然后使用Concater将它们连接到一个数据管道中。
使用Shufflerr,打乱顺序,这与在DataLoader中设置了shuffle=True是一样的。
使用ShardingFilter将数据管道分割成片。每个worker将拥有原始DataPipe元素的n个部分,其中n等于worker的数量。(多线程处理,DataLoader中的num_worker)
最后就是从磁盘读取图像
完整代码如下:
@torchdata.datapipes.functional_datapipe("load_image")
class ImageLoader(torchdata.datapipes.iter.IterDataPipe):
def __init__(self, source_datapipe, **kwargs) -> None:
self.source_datapipe = source_datapipe
self.transform = kwargs['transform']
def __iter__(self) -> Tuple[torch.Tensor, int]:
for file_name, label, data_name in self.source_datapipe:
image = Image.open(file_name)
if data_name == 'DigiFace1M':
image = image.convert('RGB')
elif data_name == 'CelebA':
left, right, top, bottom = 25, 153, 45, 173
image = image.crop((left, top, right, bottom))
if self.transform is not None:
image = self.transform(image)
yield image, label
def collate_ann(file_path):
label = int(os.path.basename(os.path.dirname(file_path))) + N_CELEBA_CLASSES
data_name = os.path.basename(os.path.dirname(os.path.dirname(file_path)))
return file_path, label, data_name
def load_celeba_labels(labels_path: str) -> Dict[str, int]:
labels = []
data_path = os.path.split(labels_path)[0]
with open(labels_path, 'r', encoding='utf-8') as labels_file:
lines = labels_file.readlines()
for line in lines:
file_name, class_id = line.split(' ')
class_id = int(class_id[:-1])
labels.append((f'{data_path}/img_align_celeba/{file_name}', class_id, 'CelebA'))
return labels
def build_datapipes(cfg: Config) -> torchdata.datapipes.iter.IterDataPipe:
celeba_dp = torchdata.datapipes.iter.IterableWrapper(
load_celeba_labels(
labels_path=f'{cfg.data_path}/CelebA/identity_CelebA.txt'))
digiface_dp = torchdata.datapipes.iter.FileLister(f'{cfg.data_path}/DigiFace1M', masks='*.png', recursive=True)
digiface_dp = digiface_dp.map(collate_ann)
datapipe = celeba_dp.concat(digiface_dp)
datapipe = datapipe.shuffle(buffer_size=100000)
datapipe = datapipe.sharding_filter()
datapipe = datapipe.load_image(transform=cfg.transform)
return datapipe
Torch的DataLoader是同时支持Datasets和DataPipe的,所以我们可以直接使用
def main():
cfg = Config()
datapipe = build_datapipes(cfg)
loader = torch.utils.data.DataLoader(
dataset=datapipe,
batch_size=cfg.batch_size,
shuffle=True,
drop_last=True,
num_workers=cfg.n_workers)
utils.train(loader, cfg)
加速数据读取的一个小技巧
批处理中耗时最长的操作之一是从磁盘读取图片。为了减少这个操作所花费的时间,可以加载所有图像并将它们分割成小的数据集,例如10,000张图像保存为.pickle文件。在读取时每一个worker只要读取一个相应的pickle文件即可
def prepare_data():
cfg = Config()
cfg.transform = None
os.makedirs(cfg.prepared_data_path, exist_ok=True)
celeba_dataset = dataset_example.CelebADataset(f'{cfg.data_path}/CelebA', cfg.transform)
digiface_dataset = dataset_example.DigiFace1M(f'{cfg.data_path}/DigiFace1M', cfg.transform, cfg.n_celeba_classes)
dataset = torch.utils.data.ConcatDataset([celeba_dataset, digiface_dataset])
shard_size = 10000
next_shard = 0
data = []
shuffled_idxs = np.arange(len(dataset))
np.random.shuffle(shuffled_idxs)
for idx in tqdm(shuffled_idxs):
data.append(dataset[idx])
if len(data) == shard_size:
with open(f'{cfg.prepared_data_path}/{next_shard}_shard.pickle', 'wb') as _file:
pickle.dump(data, _file)
next_shard += 1
data = []
with open(f'{cfg.prepared_data_path}/{next_shard}_shard.pickle', 'wb') as _file:
pickle.dump(data, _file)
下面就是使用FileLister收集.pickle数据集的所有路径,按worker划分并在每个worker上加载.pickle数据。
@torchdata.datapipes.functional_datapipe("load_pickle_data")
class PickleDataLoader(torchdata.datapipes.iter.IterDataPipe):
def __init__(self, source_datapipe, **kwargs) -> None:
self.source_datapipe = source_datapipe
self.transform = kwargs['transform']
def __iter__(self) -> Tuple[torch.Tensor, int]:
for file_name in self.source_datapipe:
with open(file_name, 'rb') as _file:
pickle_data = pickle.load(_file)
for image, label in pickle_data:
image = self.transform(image)
yield image, label
def build_datapipes(cfg: Config) -> torchdata.datapipes.iter.IterDataPipe:
datapipe = torchdata.datapipes.iter.FileLister(cfg.prepared_data_path, masks='*.pickle')
datapipe = datapipe.shuffle()
datapipe = datapipe.sharding_filter()
datapipe = datapipe.load_pickle_data(transform=cfg.transform)
return datapipe
数据加载对比
我们比较三种不同数据加载方法。对于所有测试,batch_size = 600。
n workers | Datasets, sec | DataPipes, sec | DataPipe + pickle, sec |
---|---|---|---|
10 | 3581 | 7986 | 758 |
5 | 10034 | 2993 | 760 |
当在未准备好的数据上使用DataPipe进行训练时(不使用pickle),前几百个批次生成非常快,GPU使用率几乎是100%,但随后速度逐渐下降,这种方法甚至比使用n_workers=10的数据集还要慢。虽然我理解这两种方法的速度是一样的因为执行的操作是一样的,但实际上却不一样
DataLoader的最佳n_workers没有一个固定值,因为这取决于任务(图像大小,图像预处理的复杂性等等)和计算机配置(HDD vs SSD)。
当在有大量小图像的数据集上训练时,做数据的准备是必要的的,比如将小文件组合成几个大文件,这样可以减少从磁盘读取数据的时间。但是使用这种方法需要在将数据写入shard之前彻底打乱数据,来避免学习收敛性恶化。还需要选择合理的shard大小(它应该足够大以防止磁盘问题并且足够小以有效地使用datappipes中的Shuffler打乱数据)。
最后本文的代码在这里,有兴趣的可以自行测试比较:
https://github.com/karinaodm/pytorch-compare-datasets-vs-datapipes
如何学习大模型 AI ?
由于新岗位的生产效率,要优于被取代岗位的生产效率,所以实际上整个社会的生产效率是提升的。
但是具体到个人,只能说是:
“最先掌握AI的人,将会比较晚掌握AI的人有竞争优势”。
这句话,放在计算机、互联网、移动互联网的开局时期,都是一样的道理。
我在一线互联网企业工作十余年里,指导过不少同行后辈。帮助很多人得到了学习和成长。
我意识到有很多经验和知识值得分享给大家,也可以通过我们的能力和经验解答大家在人工智能学习中的很多困惑,所以在工作繁忙的情况下还是坚持各种整理和分享。但苦于知识传播途径有限,很多互联网行业朋友无法获得正确的资料得到学习提升,故此将并将重要的AI大模型资料包括AI大模型入门学习思维导图、精品AI大模型学习书籍手册、视频教程、实战学习等录播视频免费分享出来。
😝有需要的小伙伴,可以点击下方链接免费领取或者V扫描下方二维码免费领取🆓
第一阶段(10天):初阶应用
该阶段让大家对大模型 AI有一个最前沿的认识,对大模型 AI 的理解超过 95% 的人,可以在相关讨论时发表高级、不跟风、又接地气的见解,别人只会和 AI 聊天,而你能调教 AI,并能用代码将大模型和业务衔接。
- 大模型 AI 能干什么?
- 大模型是怎样获得「智能」的?
- 用好 AI 的核心心法
- 大模型应用业务架构
- 大模型应用技术架构
- 代码示例:向 GPT-3.5 灌入新知识
- 提示工程的意义和核心思想
- Prompt 典型构成
- 指令调优方法论
- 思维链和思维树
- Prompt 攻击和防范
- …
第二阶段(30天):高阶应用
该阶段我们正式进入大模型 AI 进阶实战学习,学会构造私有知识库,扩展 AI 的能力。快速开发一个完整的基于 agent 对话机器人。掌握功能最强的大模型开发框架,抓住最新的技术进展,适合 Python 和 JavaScript 程序员。
- 为什么要做 RAG
- 搭建一个简单的 ChatPDF
- 检索的基础概念
- 什么是向量表示(Embeddings)
- 向量数据库与向量检索
- 基于向量检索的 RAG
- 搭建 RAG 系统的扩展知识
- 混合检索与 RAG-Fusion 简介
- 向量模型本地部署
- …
第三阶段(30天):模型训练
恭喜你,如果学到这里,你基本可以找到一份大模型 AI相关的工作,自己也能训练 GPT 了!通过微调,训练自己的垂直大模型,能独立训练开源多模态大模型,掌握更多技术方案。
到此为止,大概2个月的时间。你已经成为了一名“AI小子”。那么你还想往下探索吗?
- 为什么要做 RAG
- 什么是模型
- 什么是模型训练
- 求解器 & 损失函数简介
- 小实验2:手写一个简单的神经网络并训练它
- 什么是训练/预训练/微调/轻量化微调
- Transformer结构简介
- 轻量化微调
- 实验数据集的构建
- …
第四阶段(20天):商业闭环
对全球大模型从性能、吞吐量、成本等方面有一定的认知,可以在云端和本地等多种环境下部署大模型,找到适合自己的项目/创业方向,做一名被 AI 武装的产品经理。
- 硬件选型
- 带你了解全球大模型
- 使用国产大模型服务
- 搭建 OpenAI 代理
- 热身:基于阿里云 PAI 部署 Stable Diffusion
- 在本地计算机运行大模型
- 大模型的私有化部署
- 基于 vLLM 部署大模型
- 案例:如何优雅地在阿里云私有部署开源大模型
- 部署一套开源 LLM 项目
- 内容安全
- 互联网信息服务算法备案
- …
学习是一个过程,只要学习就会有挑战。天道酬勤,你越努力,就会成为越优秀的自己。
如果你能在15天内完成所有的任务,那你堪称天才。然而,如果你能完成 60-70% 的内容,你就已经开始具备成为一名大模型 AI 的正确特征了。
这份完整版的大模型 AI 学习资料已经上传CSDN,朋友们如果需要可以微信扫描下方CSDN官方认证二维码免费领取【保证100%免费
】
😝有需要的小伙伴,可以Vx扫描下方二维码免费领取==🆓