cfg文件中:
batch=64
subdivisions=16
batch:更新权重和偏置的基本单位
batch/subdivisions:前向传播、反向传播的基本单位
具体分析请往下看…
下面以batch=64,subdivisions=16为例,并结合代码来分析它们的真实意思。
首先在训练真正开始前,会根据cfg文件来搭建网络结构,其函数为parse_network_cfg(…)。在该函数里面会调用parse_net_options(…)来读取cfg中最顶上网络参数的值,这些参数值包括网络大小,学习率,当然也包括batch和subdivisions。
void parse_net_options(list *options, network *net)
{
net->batch = option_find_int(options, "batch",1);
...
int subdivs = option_find_int(options, "subdivisions",1);
net->time_steps = option_find_int_quiet(options, "time_steps",1);
...
net->batch /= subdivs;//64/16=4
net->batch *= net->time_steps;//RNN中的概念,目标检测不考虑(time_steps=1)
net->subdivisions = subdivs;
... ...
}
net->batch最终值为64/16=4, 而subdiviions为16.
然后开始读取images准备开始训练,每轮读取图片的数目为下面代码所示
int imgs = net->batch * net->subdivisions * ngpus;
这个代码很有疑惑性,这里的net->batch为4, 所以一个batch读取 的图片数目还是64 (=4x16x1)。
小结一下,在darknet代码中,net->batch值是恒为cfg中的batch值 / subdivision。所以cfg中的batch是指一次性读取多少张图片,而net->batch则是被subdivision分割成的小batch。
有了这个结论,我们继续看代码中 net->batch 和subdivision到底是怎么来用的。
float train_network(network net, data d)
{
assert(d.X.rows % net.batch == 0);//d数据的每一行代表一张样本的数据,此步骤说明该函数的运行基本单位为子batch
int batch = net.batch;//4
int n = d.X.rows / batch;//子batch的数量=subdivisions
int i;
float sum = 0;
for(i = 0; i < n; ++i){
get_next_batch(d, batch, i*batch, net.input, net.truth);//从d中读取batch张图片到net.input中,第一个参数d包含了一次大batch的数据,也就是net.batch*net.subdivision张图片,第二个参数batch是每次循环读取到 net.input中的数据,参与训练图片的张数,第三个参数是d中偏移量,第四个参数为网络的输入数据,第五个参数为输入数据net.input对应的标签
float err = train_network_datum(net);//训练网路,本次训练共有net.batch张图片,训练包括一次前向传播:计算每一层网络的输出并计算cost;一次反向:计算敏感度、权重更新值、偏置更新值,适时更新权重和偏置
sum += err;//err为loss,sum是总loss
}
return (float)sum/(n*batch);//算平均loss,就是一次大batch的loss了,只用于显示,不进行反向传播。
}
在 train_network_datum(net)中有行代码表明,subdivisions次训练完后才update 网络权值:
float train_network_datum(network net)
{
// 如果使用GPU则调用gpu版的train_network_datum
#ifdef GPU
if(gpu_index >= 0) return train_network_datum_gpu(net);
#endif
// 更新目前已经处理的图片数量:每次处理一个batch,故直接添加l.batch
*net.seen += net.batch;
// 标记处于训练阶段
net.train = 1;
forward_network(net);
backward_network(net);
float error = *net.cost;
if(((*net.seen)/net.batch)%net.subdivisions == 0) update_network(net);
return error;
}
实际上网络是net->batch(子batch)张图片进行训练(前向推理和反向传播),但是升级权值是在cfg中batch数目结束后进行的。这样在比较小的显存情况下实现大batch的训练。
pytorch-yolov3工程:
if __name__ == "__main__":
parser = argparse.ArgumentParser()
...
parser.add_argument("--batch_size", type=int, default=64, help="size of each image batch")
parser.add_argument("--subdivision", type=int, default=8)
...
# # Get dataloader
dataset = ListDataset(train_path, img_size=opt.img_size, augment=True, multiscale=opt.multiscale_training)
dataloader = torch.utils.data.DataLoader(
dataset,
batch_size=int(opt.batch_size / opt.subdivision), # 每轮加载的batch数为一个batch的图片总数/subdivision份数
...
)
optimizer = torch.optim.SGD(model.parameters(), lr=opt.lr, momentum=0.9)#优化器
...
for epoch in range(0, opt.epochs):
...
for subdivision_i, (paths, imgs, targets) in enumerate(dataloader):
...
imgs = imgs.to(device) # [n,3,416,416]
targets = targets.to(device)
loss, outputs, A = model(imgs, targets) # 正向传播
loss.backward() # 反向传播
nn.utils.clip_grad_norm_(model.parameters(), 20) # 梯度归一化(防止梯度爆炸)
...
if (subdivision_i % opt.subdivision == 0) | (subdivision_i == len(dataloader) - 1):
batch_i += 1
optimizer.step() # 更新迭代(依据与batch中累加梯度进行权重和偏置的更新,详情参考如下代码)
optimizer.zero_grad() # 将module中的所有模型参数的梯度初始化为0 (因为一个batch的loss关于weight的导数是所有sample的loss关于weight的导数的累加和)
...
...
...
...
darknet:
梯度:遍历batch中的每张照片,对于l.delta来说,每张照片是分开存的,因此其维度会达到:l.batchl.nl.out_w*l.out_h,每个batch都会清零,batch间不会有梯度的叠加过程。
△W和△B:对于l.weight_updates以及上面提到的l.bias_updates,是将所有照片对应元素叠加起来。
update:直接用△W和△B进行更新,与梯度无关。
以卷积层为例:
void update_convolutional_layer(convolutional_layer l, int batch, float learning_rate, float momentum, float decay)
{
int size = l.size*l.size*l.c*l.n;
axpy_cpu(l.n, learning_rate/batch, l.bias_updates, 1, l.biases, 1);
scal_cpu(l.n, momentum, l.bias_updates, 1);
if(l.scales){
axpy_cpu(l.n, learning_rate/batch, l.scale_updates, 1, l.scales, 1);
scal_cpu(l.n, momentum, l.scale_updates, 1);
}
axpy_cpu(size, -decay*batch, l.weights, 1, l.weight_updates, 1);
axpy_cpu(size, learning_rate/batch, l.weight_updates, 1, l.weights, 1);
scal_cpu(size, momentum, l.weight_updates, 1);
}
pytorch:
梯度:一个batch的loss关于weight的导数是所有sample的loss关于weight的导数的累加和,batch间必须手动清零,否则当前batch的梯度会包含上个batch的信息。
update:没有△W和△B,而是在step()函数中直接一句梯度计算更新。
如下所示是Pytorch 中SGD优化算法的step()函数:
def step(self, closure=None):
loss = None
if closure is not None:
loss = closure()
for group in self.param_groups:
weight_decay = group['weight_decay']
momentum = group['momentum']
dampening = group['dampening']
nesterov = group['nesterov']
for p in group['params']:
if p.grad is None:
continue
d_p = p.grad.data
if weight_decay != 0:
d_p.add_(weight_decay, p.data)
if momentum != 0:
param_state = self.state[p]
if 'momentum_buffer' not in param_state:
buf = param_state['momentum_buffer'] = d_p.clone()
else:
buf = param_state['momentum_buffer']
buf.mul_(momentum).add_(1 - dampening, d_p)
if nesterov:
d_p = d_p.add(momentum, buf)
else:
d_p = buf
p.data.add_(-group['lr'], d_p)#依据保存的梯度更新权重文件
return loss
darknet:
未找到梯度的正则化和裁剪,而且并未出现梯度的爆炸。
pytorch:
解决梯度爆炸的方法:
nn.utils.clip_grad_norm_(model.parameters(), 20) # 梯度归一化(防止梯度爆炸)
def clip_grad_norm_(parameters, max_norm, norm_type=2):
r"""Clips gradient norm of an iterable of parameters.
The norm is computed over all gradients together, as if they were
concatenated into a single vector. Gradients are modified in-place.
Arguments:
parameters (Iterable[Tensor] or Tensor): an iterable of Tensors or a
single Tensor that will have gradients normalized
max_norm (float or int): max norm of the gradients
norm_type (float or int): type of the used p-norm. Can be ``'inf'`` for
infinity norm.
Returns:
Total norm of the parameters (viewed as a single vector).
"""
if isinstance(parameters, torch.Tensor):
parameters = [parameters]
parameters = list(filter(lambda p: p.grad is not None, parameters))
max_norm = float(max_norm)
norm_type = float(norm_type)
if norm_type == inf:
total_norm = max(p.grad.data.abs().max() for p in parameters)
else:
total_norm = 0
for p in parameters:
param_norm = p.grad.data.norm(norm_type)
total_norm += param_norm.item() ** norm_type
total_norm = total_norm ** (1. / norm_type)
clip_coef = max_norm / (total_norm + 1e-6)
if clip_coef < 1:
for p in parameters:
p.grad.data.mul_(clip_coef)
return total_norm