PyTorch 分布式并行计算

0. Abstract

使用 PyTorch 进行多卡训练, 最简单的是 DataParallel, 仅仅添加一两行代码就可以使模型在多张 GPU 上并行地计算. 但它是比较老的方法, 官方推荐使用新的 Distributed Data Parallel, 更加灵活与强大:

1. Distributed Data Parallel (DDP)

从一个简单的非分布式训练任务, 到多机器多卡训练. 跟着官方教程走, 刚开始一切都很顺利, 到最后要多机器的时候, 就老是报错: MemoryError: std::bad_alloc.

1.1 DDP 概览


特点:

  • 多个 batch 的数据, 同时分别在多个 GPU 上计算;
  • 需要 DistributedSampler 给各 GPU 分发数据 batch, 保证数据不重复;
  • 模型在各 GPU 上都有一份副本, 分别计算梯度, 并通过 ring all-reduce 算法整合梯度.

可以理解为: 为每个 GPU 启动一个进程, 这些进程执行着完全相同的代码(你的程序), 不同的地方在于:

  • 吃进了不同的数据样本, 那么计算得到的 loss 和反向传播计算的参数梯度都不同;
  • 各进程有自己的编号(rank), 程序中可根据编号执行一些不同的操作, 如保存 checkpoint, 日志输出等操作.
1.2 single_gpu.py
import torch
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader
from datautils import MyTrainDataset

class Trainer:
	def __init__(
			self,
			model: torch.nn.Module,
			train_data: DataLoader,
			optimizer: torch.optim.Optimizer,
			gpu_id: int,
			save_every: int,
	) -> None:
		self.gpu_id = gpu_id
		self.model = model.to(gpu_id)
		self.train_data = train_data
		self.optimizer = optimizer
		self.save_every = save_every

	def _run_batch(self, source, targets):
		output = self.model(source)
		loss = F.cross_entropy(output, targets)
		self.optimizer.zero_grad()
		loss.backward()
		self.optimizer.step()

	def _run_epoch(self, epoch):
		b_sz = len(next(iter(self.train_data))[0])
		print(f"[GPU{self.gpu_id}] Epoch {epoch} | Batchsize: {b_sz} | Steps: {len(self.train_data)}")
		for source, targets in self.train_data:
			source = source.to(self.gpu_id)
			targets = targets.to(self.gpu_id)
			self._run_batch(source, targets)

	def _save_checkpoint(self, epoch):
		ckp = self.model.state_dict()
		PATH = "checkpoint.pt"
		torch.save(ckp, PATH)
		print(f"Epoch {epoch} | Training checkpoint saved at {PATH}")

	def train(self, max_epochs: int):
		for epoch in range(max_epochs):
			self._run_epoch(epoch)
			if epoch % self.save_every == 0:
				self._save_checkpoint(epoch)

def load_train_objs():
	train_set = MyTrainDataset(2048)  # load your dataset
	model = torch.nn.Linear(20, 1)  # load your model
	optimizer = torch.optim.SGD(model.parameters(), lr=1e-3)
	return train_set, model, optimizer

def prepare_dataloader(dataset: Dataset, batch_size: int):
	return DataLoader(
		dataset,
		batch_size=batch_size,
		pin_memory=True,
		shuffle=True
	)

def main(device, total_epochs, save_every, batch_size):
	dataset, model, optimizer = load_train_objs()
	train_data = prepare_dataloader(dataset, batch_size)
	trainer = Trainer(model, train_data, optimizer, device, save_every)
	trainer.train(total_epochs)

if __name__ == "__main__":
	import argparse

	parser = argparse.ArgumentParser(description='simple distributed training job')
	parser.add_argument('total_epochs', type=int, help='Total epochs to train the model')
	parser.add_argument('save_every', type=int, help='How often to save a snapshot')
	parser.add_argument('--batch_size', default=32, type=int, help='Input batch size on each device (default: 32)')
	args = parser.parse_args()

	device = 0  # shorthand for cuda:0
	main(device, args.total_epochs, args.save_every, args.batch_size)
1.3 multi_gpu.py
import torch
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader
from datautils import MyTrainDataset

import torch.multiprocessing as mp
from torch.utils.data.distributed import DistributedSampler
from torch.nn.parallel import DistributedDataParallel as DDP
from torch.distributed import init_process_group, destroy_process_group
import os


def ddp_setup(rank, world_size):
	"""
	Args:
		rank: Unique identifier of each process
		world_size: Total number of processes
	"""
	# MASTER 表示主节点, 负责分配任务, 启动其他进程
	os.environ["MASTER_ADDR"] = "localhost"  # IP address of master
	os.environ["MASTER_PORT"] = "12355"  # Port number
	# This is important to prevent hangs or excessive memory utilization on GPU:0
	torch.cuda.set_device(rank)  # sets the default GPU for each process
	init_process_group(backend="nccl", rank=rank, world_size=world_size)


class Trainer:
	def __init__(
			self,
			model: torch.nn.Module,
			train_data: DataLoader,
			optimizer: torch.optim.Optimizer,
			gpu_id: int,
			save_every: int,
	) -> None:
		self.gpu_id = gpu_id
		self.train_data = train_data
		self.optimizer = optimizer
		self.save_every = save_every
		self.model = DDP(  # 感觉有点重复, 上面 ddp_setup 已经设置过默认 device 了
			model.to(gpu_id),  # 这里要先将模型放到 gpu_id 号 GPU 上, 否则 DDP 会报错
			device_ids=[gpu_id],  # 那么这里再设置 device_ids 干嘛? 是可以分布到多个 GPU 上吗?
		)

	def _run_batch(self, source, targets):
		output = self.model(source)
		loss = F.cross_entropy(output, targets)
		self.optimizer.zero_grad()
		loss.backward()
		self.optimizer.step()

	def _run_epoch(self, epoch):
		b_sz = len(next(iter(self.train_data))[0])
		# len(self.train_data)} 将会被分割为 num_device 份
		print(f"[GPU{self.gpu_id}] Epoch {epoch} | Batchsize: {b_sz} | Steps: {len(self.train_data)}")
		# sampler.set_epoch(epoch) is necessary to make shuffling work properly across multiple epochs.
		# Otherwise, the same ordering will be used in each epoch.
		self.train_data.sampler.set_epoch(epoch)  # 这里加了一句, 是为了保证每个 epoch 的数据是随机的
		for source, targets in self.train_data:
			source = source.to(self.gpu_id)
			targets = targets.to(self.gpu_id)
			self._run_batch(source, targets)

	def _save_checkpoint(self, epoch):
		ckp = self.model.module.state_dict()  # 因为 self.model 引用的是 DDP 对象, 所以想访问模型参数, 则需要 .module
		PATH = "checkpoint.pt"
		torch.save(ckp, PATH)
		print(f"Epoch {epoch} | Training checkpoint saved at {PATH}")

	def train(self, max_epochs: int):
		for epoch in range(max_epochs):
			self._run_epoch(epoch)
			if self.gpu_id == 0 and epoch % self.save_every == 0:  # 主进程才保存
				self._save_checkpoint(epoch)


def load_train_objs():
	train_set = MyTrainDataset(2048)  # load your dataset
	model = torch.nn.Linear(20, 1)  # load your model
	optimizer = torch.optim.SGD(model.parameters(), lr=1e-3)
	return train_set, model, optimizer


def prepare_dataloader(dataset: Dataset, batch_size: int):
	return DataLoader(
		dataset,
		batch_size=batch_size,
		pin_memory=True,
		shuffle=False,  # 有了 DistributedSampler, 这里就不用 shuffle 了, 不过 default 已经是 False
		sampler=DistributedSampler(dataset)
	)


def main(rank: int, world_size: int, save_every: int, total_epochs: int, batch_size: int):
	"""
	rank: Unique identifier of each process, GPU ID, 也是进程的 ID, 0~world_size-1
	world_size: Total number of processes, 总共 GPU 数量
	"""
	ddp_setup(rank, world_size)  # 先设置当前子进程
	dataset, model, optimizer = load_train_objs()  # 之后似乎都一样, 甚至数据,模型,优化器都是各进程都创建
	train_data = prepare_dataloader(dataset, batch_size)
	trainer = Trainer(model, train_data, optimizer, rank, save_every)
	trainer.train(total_epochs)
	destroy_process_group()  # 结尾销毁进程组


if __name__ == "__main__":
	import argparse

	parser = argparse.ArgumentParser(description='simple distributed training job')
	parser.add_argument('total_epochs', type=int, help='Total epochs to train the model')
	parser.add_argument('save_every', type=int, help='How often to save a snapshot')
	parser.add_argument('--batch_size', default=32, type=int, help='Input batch size on each device (default: 32)')
	args = parser.parse_args()

	world_size = torch.cuda.device_count()
	# spawn processes, 自动创建进程, 并且把 rank 作为第一个参数传入 main
	mp.spawn(main, args=(world_size, args.save_every, args.total_epochs, args.batch_size), nprocs=world_size)

更改代码仅仅需要几个步骤:

  1. 构建进程组: init_process_group(...) & destroy_process_group()
    main 函数被当作子进程启动, 每个子进程启动开头由 init_process_group(...) 构建进程组, 结尾由 destroy_process_group() 销毁进程组;
  2. DistributedDataParallel 包装模型
    其实主要还是持有参数的模型, 至于计算部分, 不要紧, 每个子进程都在执行相同的计算过程(除非设置了 if rank==... 的条件), 只会是参数梯度不同, 被包装后的模型参数会自动在进程组之间同步;
    注意包装前先将模型移动到 GPU 上.
  3. DistributedSampler 均匀地将样本分给每个子进程
    如果样本数不够整除, 则会将前几个样本补到末尾, 凑够整除, 注意是打乱后的前几个, 相当于随机补几个样本;
    如果设置了 batch_size=32, 那么每个进程都会得到 32 个样本, 实际的 batch_size=32*num_gpus; 容易误解的地方在于, 实际 batch_size 增大了, 那么我求 loss 时用 mean 的话, 会不会降低梯度大小? 不会, 有些博主说要 learning_rate*num_gpus, 但实际上人家的 ring all-reduce 算法是把各进程上的梯度相加的, 相当于执行了多次梯度更新, 只不过是在相同的参数上, 而不是像单卡更新多次, 每次梯度计算在更新之后的不同的参数上.
    每个子进程中访问的 DataLoader 中 batch 数会变为原来的 1/num_gpus, len(dataset) 不会.
  4. 每个 epoch 开始时, 调用 train_loader.sampler.set_epoch(epoch), 否则, 将在每个 epoch 中使用相同的顺序.
  5. 设置 if rank==0 为保存 checkpoint 的条件, 以保证只保存几个相同模型的其中一个.
    聚合操作, 如你想整合各进程计算的不同结果并保存, 不应在 if rank == 0 内, 聚合操作需要在每个进程中执行. 原因下面会解释.
  6. spawn 翻译过来就是下蛋, 意思是启动子进程, 可以看到, 相当于执行了多个 main 函数;
    rank 参数是自动传给 main 函数的.

BatchNorm 是根据数据计算均值和标准差的, 所以每个 GPU 上计算的都不一样, 如果想合成一个完整的大 Batch, 需要 SyncBatchNorm 同步.

1.4 multigpu_torchrun.py
import os

import torch
import torch.nn.functional as F
from torch import distributed
from torch.nn.parallel import DistributedDataParallel as DDP
from torch.utils.data import Dataset, DataLoader
from torch.utils.data.distributed import DistributedSampler

from datautils import MyTrainDataset


def ddp_setup():
	"""
	都不用设置主机地址和端口号了, 直接一个 LOCAL_RANK
	"""
	torch.cuda.set_device(int(os.environ["LOCAL_RANK"]))
	distributed.init_process_group(backend="nccl")


class Trainer:
	def __init__(
			self,
			model: torch.nn.Module,
			train_data: DataLoader,
			optimizer: torch.optim.Optimizer,
			save_every: int,
			snapshot_path: str,
	) -> None:
		self.gpu_id = int(os.environ["LOCAL_RANK"])  # 这里也是自动获取 LOCAL_RANK
		self.model = model.to(self.gpu_id)
		self.train_data = train_data
		self.optimizer = optimizer
		self.save_every = save_every
		self.epochs_run = 0
		self.snapshot_path = snapshot_path
		if os.path.exists(snapshot_path):
			print("Loading snapshot")
			self._load_snapshot(snapshot_path)

		self.model = DDP(self.model, device_ids=[self.gpu_id])

	def _load_snapshot(self, snapshot_path):
		loc = f"cuda:{self.gpu_id}"
		snapshot = torch.load(snapshot_path, map_location=loc, weights_only=True)  # 每个 GPU 都要加载
		self.model.load_state_dict(snapshot["MODEL_STATE"])  # 之所以是 model.load_state_dict, 是因为在 DDP 之前
		self.epochs_run = snapshot["EPOCHS_RUN"]
		print(f"Resuming training from snapshot at Epoch {self.epochs_run}")

	def _run_batch(self, source, targets):
		print(source.shape[0])
		output = self.model(source)
		loss = F.mse_loss(output, targets)
		self.optimizer.zero_grad()
		loss.backward()
		self.optimizer.step()
		return loss

	def _run_epoch(self, epoch):
		b_sz = len(next(iter(self.train_data))[0])
		print(
			f"[GPU{self.gpu_id}] "
			f"Epoch {epoch} | "
			f"Batchsize: {b_sz} | "
			f"Steps: {len(self.train_data)} | "  # data_loader 会 / num_devices
			f"dsize: {len(self.train_data.dataset)}"  # 而数据集大小还是原来的
		)
		self.train_data.sampler.set_epoch(epoch)
		loss_epoch = 0
		for source, targets in self.train_data:
			source = source.to(self.gpu_id)
			targets = targets.to(self.gpu_id)
			loss = self._run_batch(source, targets)
			loss_epoch += loss
		print(f"[GPU{self.gpu_id}] Loss {loss_epoch.item()}")
		distributed.all_reduce(loss_epoch, op=distributed.ReduceOp.AVG)
		print(f"[GPU{self.gpu_id}] Loss {loss_epoch.item()}")

	def _save_snapshot(self, epoch):
		snapshot = {
			"MODEL_STATE": self.model.module.state_dict(),  # 之后就要用 module 了
			"EPOCHS_RUN": epoch,
		}
		torch.save(snapshot, self.snapshot_path)
		print(f"Epoch {epoch} | Training snapshot saved at {self.snapshot_path}")

	def train(self, max_epochs: int):
		for epoch in range(self.epochs_run, max_epochs):
			self._run_epoch(epoch)
			if self.gpu_id == 0 and epoch % self.save_every == 0:
				self._save_snapshot(epoch)


def load_train_objs():
	train_set = MyTrainDataset(101)  # load your dataset
	model = torch.nn.Linear(20, 1)  # load your model
	optimizer = torch.optim.SGD(model.parameters(), lr=1e-3)
	return train_set, model, optimizer


def prepare_dataloader(dataset: Dataset, batch_size: int):
	return DataLoader(
		dataset,
		batch_size=batch_size,
		pin_memory=True,
		shuffle=False,
		# 这个 DistributedSampler 会自动把数据集平均分给每个 GPU, 只是每个 DataLoader 得到的下标是 len(dataset) / num_devices 个
		# 原来的 len(dataloder.dataset) 还是 len(dataset)
		# 注意会补全, 最后每个 GPU 都会得到相同的数据, 而不是最后一个 GPU 会少得
		# 那补了之后, 样本数是比源数据集多一些, 测试呢, 也就有偏差, 当你有上万个测试样本时, 多出来的几个样本影响不大
		sampler=DistributedSampler(dataset)
	)


def main(save_every: int, total_epochs: int, batch_size: int, snapshot_path: str = "snapshot.pt"):
	"""
	不带 rank 了, 直接用 LOCAL_RANK
	"""
	ddp_setup()
	dataset, model, optimizer = load_train_objs()
	train_data = prepare_dataloader(dataset, batch_size)
	trainer = Trainer(model, train_data, optimizer, save_every, snapshot_path)
	trainer.train(total_epochs)
	distributed.destroy_process_group()


if __name__ == "__main__":
	import argparse

	parser = argparse.ArgumentParser(description='simple distributed training job')
	parser.add_argument('total_epochs', type=int, help='Total epochs to train the model')
	parser.add_argument('save_every', type=int, help='How often to save a snapshot')
	parser.add_argument('--batch_size', default=32, type=int, help='Input batch size on each device (default: 32)')
	args = parser.parse_args()

	main(args.save_every, args.total_epochs, args.batch_size)  # 不管 rank 和 device

执行命令:

torchrun --standalone --nproc_per_node=2 multigpu_torchrun.py 50 10
# 如果设置 --nproc_per_node=gpu, 则自动检测可用 gpu 数量, 并为每个 gpu 启动一个进程.

这里使用了不同的启动方式 torchrun, 本质还是一样的, 特点:

  1. 能自动重启
    当训练出现意外而中断时, torchrun 会自动重启, 如果保存了 checkpoint 并设置了自动加载程序, 那么就可以接着训.
  2. 设置了环境变量 “LOCAL_RANK”
    你可以在程序中使用 os.environ["LOCAL_RANK"]) 访问当前进程的 rank 号了. 不过我感觉仅仅是在 distributed.init_process_group(backend="nccl") 之前使用, 后来的地方你可以接续这么干, 但构建进程组后有一个函数 distributed.get_node_local_rank() 可以获取进程号.
  3. 单卡也可以跑, 设置 --nproc_per_node=1.
1.5 同步操作

模型参数可以通过 DDP 自动地同步, 那如果我想聚合所有子进程上计算的 loss 呢? 或者我在测试时, 想聚合测试结果? 官网的这个小教程没教. 查阅博客才得知需要用 distributed.all_reduce(...).

上面的 multigpu_torchrun.py 中, 我已经对 loss 添加了这个同步:

print(f"[GPU{self.gpu_id}] Loss {loss_epoch.item()}")
distributed.all_reduce(loss_epoch, op=distributed.ReduceOp.AVG)
print(f"[GPU{self.gpu_id}] Loss {loss_epoch.item()}")
########## output ##########
[GPU1] Loss 0.3411558270454407
[GPU0] Loss 0.29943281412124634
[GPU1] Loss 0.3202943205833435
[GPU0] Loss 0.3202943205833435

两个 GPU 计算的 loss 分别为 0.34115582704544070.29943281412124634, 经过同步, 都变为了 0.3202943205833435.

注意:

  • 只可对 torch.Tensor 执行同步, 其他类型的如 Python int 和 np.ndarray 都不行.
  • 可以选择其他聚合操作, 如 op=distributed.ReduceOp.SUM 表示相加:

    具体可见: Collective Functions.
  • 聚合操作不应在 if rank == 0 内; 聚合操作需要在每个进程中执行.
1.6 会出现模型加载错误

如果刚用 torch.save(...) 保存了模型, 立刻就使用 torch.load(...) 加载, 那么很可能会出现错误:

[rank1]: RuntimeError: PytorchStreamReader failed reading zip archive: failed finding central directory

原因不明.

解决办法:

time.sleep(1)
torch.load(...)

等 1s 再加载就不出错了.

2. 总结

看起来比较复杂, 但如果构建对 Distributed Data Parallel 的认知框架, 一切都变得简单:

  • DDP 为每个 GPU 启动一个子进程, 它们执行"完全相同"的代码;
  • distributed.init_process_group(backend="nccl") 构建进程组, 程序结束时 distributed.destroy_process_group() 销毁进程组;
  • DistributedSampler(dataset) 为每个子进程分发不重叠的等分的 Dataset 子集, 实现数据并行;
  • 用 DDP 对象包装模型, 就能在进程组中同步梯度和参数; 叫 ring all-reduce 算法;
  • 你可以用 all_reduce 等操作实现进程间的张量同步;
  • 还可以根据进程的 rank 号对不同子进程执行略有不同的操作, 如保存模型操作.

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/921533.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

基于MATLAB的超宽带(UWB)信号的仿真和测试系统

基于MATLAB的超宽带(UWB)信号的仿真和测试系统 引言 随着无线通信技术的发展,超宽带(Ultra-Wideband, UWB)技术因其高数据传输速率、低功耗、抗多径衰落等优点而受到广泛关注。UWB技术适用于短距离高速数据传输,如个人区域网络、…

美团面试:有哪些情况会产生死锁

前言 我们首先需要知道,死锁一定发生在并发场景中。为了保证线程安全,有时会给程序使用各种能保证并发安全的工具,尤其是锁,但是如果在加解锁过程中处理不恰当,就有可能适得其反,导致程序出现死锁的情况。…

如何在Linux上安装Canal同步工具

1. 下载安装包 所用到的安装包 canal.admin-1.1.4.tar.gz 链接:https://pan.baidu.com/s/1B1LxZUZsKVaHvoSx6VV3sA 提取码:v7ta canal.deployer-1.1.4.tar.gz 链接:https://pan.baidu.com/s/13RSqPinzgaaYQUyo9D8ZCQ 提取码:…

百度主动推送可以提升抓取,它能提升索引量吗?

站长在建站SEO的时候,需要用到百度站长平台(资源平台)的工具,在站长工具中【普通收录】-【资源提交】-【API提交】这个功能,对网站的抓取进行一个提交。 这里估计很多站长就有疑问,如果我主动推送&#xf…

如何将Latex的文章内容快速用word+Endnote排版

1 第一步 Endnote文件是无法直接导入bib文件的。需要将reference.bib的参考文献内容,通过JabRef软件打开并另存为refefence.ris文件 下载JabRef软件:https://www.jabref.org/#download 导出为ris格式文件 2 第二步 通过Endnote导入ris文件&#xff0…

Telegram bot Mini-App开发实践---Telegram简单介绍与初始化小程序获取window.Telegram.WebApp对象并解析

➡️【好看的灵魂千篇一律,有趣的鲲志一百六七!】- 欢迎认识我~~ 作者:鲲志说 (公众号、B站同名,视频号:鲲志说996) 科技博主:极星会 星辉大使 后端研发:java、go、python、TS,前电商、现web3 主理人:COC杭州开发者社区主理人 、周周黑客松杭州主理人、 AI爱好…

Hadoop 系列 MapReduce:Map、Shuffle、Reduce

文章目录 前言MapReduce 基本流程概述MapReduce 三个核心阶段详解Map 阶段工作原理 Shuffle 阶段具体步骤分区(Partition)排序(Sort)分组(Combine 和 Grouping) Reduce 阶段工作原理 MapReduce 应用场景Map…

英文版本-带EXCEL函数的数据分析

一、问题: 二、表格内容 三、分析结果 四、具体的操作步骤: 销售工作表公式设计与数据验证 类别(Category)列公式: 在Category列(假设为D列),根据ProductCode在Catalogue工作表中查找…

Jmeter数据库压测之达梦数据库的配置方法

目录 1、概述 2、测试环境 3、数据库压测配置 3.1 安装jmeter 3.2 选择语言 3.3 新建测试计划 3.4 配置JDBC连接池 3.5 配置线程组 3.6 配置测试报告 3.7 执行测试 1、概述 Jmeter是Apache组织开发的基于Java的压力测试工具,用于对软件做压力测试。 它最…

Cmakelist.txt之win-c-udp-client

1.cmakelist.txt cmake_minimum_required(VERSION 3.16) ​ project(c_udp_client LANGUAGES C) ​ add_executable(c_udp_client main.c) ​ target_link_libraries(c_udp_client wsock32) ​ ​ include(GNUInstallDirs) install(TARGETS c_udp_clientLIBRARY DESTINATION $…

02:spring之AOP

一:AOP 简介 1:AOP的概念 AOP,Aspect Oriented Programming,面向切面编程,是对面向对象编程OOP的升华。OOP是纵向对一个事物的抽象,一个对象包括静态的属性信息,包括动态的方法信息等。而AOP是…

商业物联网:拥抱生产力的未来

在现代商业格局中,数据占据至高无上的地位。物联网(IoT)站在这场数字革命的前沿,将以往模糊不清的不确定因素转变为可衡量、可付诸行动的深刻见解。物联网技术为日常物品配备传感器与连接功能,使其能够实时收集并传输数…

多摩川编码器协议及单片机使用

参考: https://blog.csdn.net/qq_28149763/article/details/132718177 https://mp.weixin.qq.com/s/H4XoR1LZSMH6AxsjZuOw6g 1、多摩川编码器协议 多摩川数据通讯是基于485 硬件接口标准NRZ 协议,通讯波特率为2.5Mbps 的串行通讯,采用差分两…

从壹开始解读Yolov11【源码研读系列】——Data.build.py:YOLO用于训练Train + 验证Val的无限数据集加载器DataLoader搭建

【前情回顾】在上一篇文章记录了YOLO源码data目录下的dataset.py 文件中定义的YOLO数据集类——Class YOLODataset,其基于了数据集基类BaseDataset,并重写了关键的数据预处理函数方法。 YOLODataset数据集类博文地址:Data.dataset.py&#xf…

爬虫重定向问题解决

一,问题 做爬虫时会遇到强制重定向的链接,此时可以手动获取重定向后的链接 如下图情况 第二个链接是目标要抓取的,但它是第一个链接重定向过去的,第一个链接接口状态也是302 二,解决方法 请求第一个链接&#xff0…

ssh无法连接Ubuntu

试了多次ssh都无法连接,明明可以上网 网卡、防火墙、端口都没有问题,就是连接不上 结果是这个版本Ubuntu镜像默认没有安装ssh服务 安装SSH服务:apt-get install openssh-server 开启SSH服务:/etc/init.d/ssh start 就可以连接…

I.MX6U 裸机开发18.GPT定时器实现高精度延时

I.MX6U 裸机开发18.GPT定时器实现高精度延时 一、GPT定时器简介1. GPT 功能2. 时钟源3. 框图4. 运行模式(1)Restart mode(2)Free-Run Mode 5. 中断类型(1)溢出中断 Rollover Interrupt(2&#x…

亚马逊IP关联是什么?我们该怎么解决呢?

亚马逊不仅提供了广泛的商品和服务,也是许多企业和个人选择的电子商务平台。然而,与亚马逊相关的IP关联问题,特别是在网络安全和运营管理方面,经常成为使用亚马逊服务的用户和商家关注的焦点。通过了解亚马逊IP关联的含义、可能的…

摄影:相机控色

摄影:相机控色 白平衡(White Balance)白平衡的作用: 白平衡的使用环境色温下相机色温下总结 白平衡偏移与包围白平衡包围 影调 白平衡(White Balance) 人眼看到的白色:会自动适应环境光线。 相…

Python模块、迭代器与正则表达式day10

1、Python模块 1.1模块的简介 在编写代码的时候,创建的.py文件就被称为一个模块 1.2模块的使用 想要在a文件里使用b文件的时候,只要在a文件中使用关键字import导入即可 1.2.2 from ...import...语句 导入模块可以使用import,如果只导入模…