在实际搭建深度学习网络中遇到很多坑,也在读别人的代码时看到很多技巧,统一做一个记录,也方便自己查阅
Argparser库是python自带的库,使用Argparser能让我们像在Linux系统上一样用命令行去设置参数,生成的parse_args对象将所有的参数打包,在多个文件中传递修改参数时非常方便
import argparse
parser = argparse.ArgumentParser(description='MODELname') # 为参数解析器赋予一个名字
register_data_args(parser)
parser.add_argument("--dropout", type=float, default=0.5,
help="dropout probability")
parser.add_argument("--gpu", type=int, default=-1,
help="gpu")
parser.add_argument("--lr", type=float, default=1e-2,
help="learning rate")
parser.add_argument("--n-epochs", type=int, default=200,
help="number of training epochs")
parser.add_argument("--n-hidden", type=int, default=16,
help="number of hidden gcn units")
parser.add_argument("--n-layers", type=int, default=1,
help="number of hidden gcn layers")
parser.add_argument("--weight-decay", type=float, default=5e-4,
help="Weight for L2 loss")
parser.add_argument("--aggregator-type", type=str, default="gcn",
help="Aggregator type: mean/gcn/pool/lstm")
config = parser.parse_args()
# 之后就能够用config.xxx代表各个参数了
# 调试,在终端输入 python train.py --n-epoch 100 --lr 1e-3 ....
class TrainDataset(Dataset):
def __init__(self,listdir=list_dir):
super(TrainDataset, self).__init__()
self.train_dirs = []
for dir in listdir:
self.train_dirs.append(dir)
...
def __getitem__(self, index):
path = self.train_dirs[index]
data = np.load(path)
...
def __len__(self):
return len(self.train_dirs)
在构建Dataloader对象时可以设置collate_fn参数,传入数据处理的函数,需要自己写。
trainloader = Dataloader(dataset = train_dataset,shuffle=True,collate_fn=my_func)
collate_fn的作用在于能够自定义数据的获取方式
torch.optim.lr_scheduler
模块提供了一些根据epoch训练次数来调整学习率(learning rate)的方法。一般情况下我们会设置随着epoch的增大而逐渐减小学习率从而达到更好的训练效果。
常用的学习率调整策略有:
StepLR:等间隔调整学习率,每次调整为 lr*gamma,调整间隔为step_size
scheduler = torch.optim.lr_scheduler.StepLR(optimizer,step_size,gamma=0.1,last_epoch=-1,verbose=False)
参数:
1、optimizer
:设置的优化器
2、step_size
:学习率调整步长,每经过step_size更新一次
3、gamma
:学习率调整倍数
4、last_epoch
:last_epoch之后恢复lr为initial_lr(如果是训练了很多个epoch后中断了 继续训练 这个值就等于加载的模型的epoch 默认为-1表示从头开始训练,即从epoch=1开始
5、verbose
:是否每次改变都输出一次lr的值
MultiStepLR : 当前epoch数满足设定值时,调整学习率。这个方法适合后期调试使用,观察loss曲线,为每个实验制定学习率调整时期
scheduler = torch.optim.lr_scheduler.MultiStepLR(optimizer, milestones, gamma=0.1, last_epoch=-1, verbose=False)
milestones
:一个包含epoch索引的list,列表中的每个索引代表调整学习率的epoch。list中的值必须是递增的。 如 [20, 50, 100] 表示在epoch为20,50,100时调整学习率。
其他参数设置方法相同
ExponentialLR:按指数衰减调整学习率,调整公式:lr = lr*gamma**epoch
scheduler = torch.optim.lr_scheduler.ExponentialLR(optimizer, gamma, last_epoch=-1, verbose=False)
CosineAnnealingLR:余弦退火策略,周期性地调整学习率
scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max, eta_min=0, last_epoch=-1, verbose=False)
参数:
1、T_max
:学习率调整的周期,每个周期重新将学习率调整回初始值,再衰减,这样的策略有助于跳出马鞍面。通常设置为len(train_dataset)
2、eta_min
:衰减到的最小的学习率,defult为0
3、last_epoch
:上一个epoch数,这个变量用于指示学习率是否需要调整。当last_epoch符合设定的间隔时就会调整学习率。当设置为-1时,学习率设置为初始值
在每个train step中更新学习率:
scheduler.step()
warmup
是一种学习率优化方法,最早出现在resnet论文中,在模型训练初期选用较小的学习率,训练一段时间之后(10epoch 或者 10000steps)使用预设的学习率进行训练
使用warmup的原因:
模型训练初期,权重随机化,对数据的理解为0,在第一个epoch中,模型会根据输入的数据进行快速的调参,此时如果采用较大的学习率,有很大的可能使模型学偏,后续需要更多的轮次才能拉回来
模型训练一段时间之后,对数据有一定的先验知识,此时使用较大的学习率模型不容易学偏,可以使用较大的学习率加速训练。
模型使用较大的学习率训练一段时间之后,模型的分布相对比较稳定,此时不宜从数据中再学到新的特点,如果继续使用较大的学习率会破坏模型的稳定性,而使用较小的学习率更获得最优。
warm_up实现
class WarmupLR(_LRScheduler):
"""The WarmupLR scheduler
This scheduler is almost same as NoamLR Scheduler except for following
difference:
NoamLR:
lr = optimizer.lr * model_size ** -0.5
* min(step ** -0.5, step * warmup_step ** -1.5)
WarmupLR:
lr = optimizer.lr * warmup_step ** 0.5
* min(step ** -0.5, step * warmup_step ** -1.5)
Note that the maximum lr equals to optimizer.lr in this scheduler.
"""
def __init__(
self,
optimizer: torch.optim.Optimizer,
warmup_steps: Union[int, float] = 25000,
last_epoch: int = -1,
):
assert check_argument_types()
self.warmup_steps = warmup_steps
# __init__() must be invoked before setting field
# because step() is also invoked in __init__()
super().__init__(optimizer, last_epoch)
def __repr__(self):
return f"{self.__class__.__name__}(warmup_steps={self.warmup_steps})"
def get_lr(self):
step_num = self.last_epoch + 1
return [
lr
* self.warmup_steps ** 0.5
* min(step_num ** -0.5, step_num * self.warmup_steps ** -1.5)
for lr in self.base_lrs
]
def set_step(self, step: int):
self.last_epoch = step
warm_up配合lr_scheduler一起使用,先线性增大学习率,再衰减:
if step_counter>10:
scheduler.step()
通过torch.nn.DataParallel进行分布式训练,要求主机上有不止一块GPU,训练时将数据分摊到多块GPU上进行多进程训练,每块GPU上进行独立的optimize,再将loss进行汇总求平均,将反向传播的梯度广播到每一块GPU上
model= MY_MODEL()
if torch.cuda.device_count() > 1:
model = nn.DataParallel(model)
开分布式要注意不能使用next(model.parameters())
,并且在模型中存在 着foriegner model
时,不能在模型内部给这些foriegner model指定设备号(cuda:0)
,可以用torch.device('cuda')
代替,让程序自己去分配,数据也是如此,不然会出现模型运算的数据放在了不同的GPU的情况
# data type:torch.tensor
print(next(model.parameters()).device)
print(data.device)
判断数据是否在gpu上:
print(data.is_cuda)
数据在gpu、cpu之间的相互转换:
# 最简单的,data type:torch.tensor
data.cpu()
data.cuda()
# 设置设备
device = torch.device('cuda:0' if torch.cuda_is_avaliable() else 'cpu')
data.to(device)
在pytorch上进行分布式训练,指定local rank,即进程内的GPU
编号,非显式参数,由 torch.distributed.launch
内部指定。主机GPU的local_rank为0。比方说, rank = 3,local_rank = 0
表示第 3
个进程内的第 1
块 GPU
主机进程local rank=0
在每次迭代中,每个进程具有自己的 optimizer
,并独立完成所有的优化步骤,进程内与一般的训练无异。
在各进程梯度计算完成之后,各进程需要将梯度进行汇总平均,然后再由 rank=0
的进程,将其 broadcast
到所有进程。之后,各进程用该梯度来更新参数。
设置第几块GPU工作
torch.cuda.set_device(arg.local_rank)
device = torch.device('cuda',arg.local_rank)
if not self.check_integrity():
raise RuntimeError('Dataset not found or corrupted.' +
' You need to download it from official website.')
# 检查数据集的路径是否存在
def check_integrity(self):
if not os.path.exists(self.root_dir):
return False
else:
return True
self.label2index = {label: index for index, label in enumerate(sorted(set(labels)))}
# pytorch中的numel函数统计tensor中的总元素量
num_params = sum(p.numel() for p in model.parameters())
print(num_params)
用logging模块记录训练日志,在后台在线写入日志文件,也可以在线输出,方便监控训练信息
1 import logging
2
# 文件保存地址,文件名、信息级别
3 logging.basicConfig(filename=os.path.join(self.writer.log_dir, 'training.log'),level=logging.DEBUG, format='%(asctime)s - %(name)s - %(message)s')
4
5 logging.debug('this is debug message') # 这些信息将被记录在training.log中
6 logging.info('this is info message')
7 logging.warning('this is warning message')
8
9 '''''
10 结果:
11 2017-08-23 14:22:25,713 - root - this is debug message
12 2017-08-23 14:22:25,713 - root - this is info message
13 2017-08-23 14:22:25,714 - root - this is warning message
14 '''
logging.basicConfig 函数各参数:
filename: 指定日志文件名
filemode: 和file函数意义相同,指定日志文件的打开模式,‘w’或’a’
format: 指定输出的格式和内容,format可以输出很多有用信息,如上例所示:
%(levelno)s: 打印日志级别的数值
%(levelname)s: 打印日志级别名称
%(pathname)s: 打印当前执行程序的路径,其实就是sys.argv[0]
%(filename)s: 打印当前执行程序名
%(funcName)s: 打印日志的当前函数
%(lineno)d: 打印日志的当前行号
%(asctime)s: 打印日志的时间
%(thread)d: 打印线程ID
%(threadName)s: 打印线程名称
%(process)d: 打印进程ID
%(message)s: 打印日志信息
datefmt: 指定时间格式,同time.strftime()
level: 设置日志级别,默认为logging.WARNING
stream: 指定将日志的输出流,可以指定输出到sys.stderr,sys.stdout或者文件,默认输出到sys.stderr,当stream和filename同时指定时,stream被忽略
# 设置日志名称,通常用主文件命名
logger1 = logging.getLogger(__name__)
logging.basicConfig(filename=os.path.join(self.writer.log_dir, 'training.log'),level=logging.DEBUG)
logging.info(f"Start SimCLR training for {self.args.epochs} epochs.")
logging.info(f"Training with gpu: {self.args.disable_cuda}.")
....
# 日志输出到文件
fh1 = logging.FileHandler(filename='a1.log', encoding='utf-8') # 文件a1
fh2 = logging.FileHandler(filename='a2.log', encoding='utf-8') # 文件a2
sh = logging.StreamHandler() # 日志输出到终端片