CRNN训练部分解析

发布时间 2023-08-17 13:49:27作者: 周而輹始
os.environ["CUDA_VISIBLE_DEVICES"] = "0"
log_filename = os.path.join('log/', 'loss_acc-' + config.saved_model_prefix + '.log')
if not os.path.exists('debug_files'):
    os.mkdir('debug_files')
if not os.path.exists(config.saved_model_dir):
    os.mkdir(config.saved_model_dir)
if config.use_log and not os.path.exists('log'):
    os.mkdir('log')
if config.use_log and os.path.exists(log_filename):
    os.remove(log_filename)
if config.experiment is None:
    config.experiment = 'expr'
if not os.path.exists(config.experiment):
    os.mkdir(config.experiment)

设置和检查文件和目录的一些条件。具体的功能包括:

1. 设置环境变量`CUDA_VISIBLE_DEVICES`为`0`,表示使用第一个GPU设备。
2. 定义日志文件名`log_filename`,用于保存模型的损失和准确率。
3. 检查是否存在`debug_files`目录,如果不存在则创建。
4. 检查是否存在`config.saved_model_dir`目录,如果不存在则创建。该目录用于保存模型的参数和状态。
5. 检查是否启用日志功能并且是否存在`log`目录,如果启用并且目录不存在则创建。
6. 如果启用日志功能并且`log_filename`已经存在,则删除该文件。
7. 检查是否指定了实验名称`config.experiment`,如果没有则设置为默认名称`expr`。
8. 检查是否存在`config.experiment`目录,如果不存在则创建。

train_dataset = mydataset.MyDataset(info_filename=config.train_infofile)
assert train_dataset

if optimizer_type == 'rms':
    config.manualSeed = random.randint(1, 10000)  # fix seed
    print("Random Seed: ", config.manualSeed)
    random.seed(config.manualSeed)
    np.random.seed(config.manualSeed)
    torch.manual_seed(config.manualSeed)
    sampler = mydataset.randomSequentialSampler(train_dataset, config.batchSize)
else:
    sampler = None
train_loader = torch.utils.data.DataLoader(
    train_dataset, batch_size=config.batchSize,
    shuffle=True, sampler=sampler,
    num_workers=int(config.workers),
    collate_fn=mydataset.alignCollate(imgH=config.imgH, imgW=config.imgW, keep_ratio=config.keep_ratio))

test_dataset = mydataset.MyDataset(
    info_filename=config.val_infofile, transform=mydataset.resizeNormalize((config.imgW, config.imgH), is_test=True))

converter = utils.strLabelConverter(config.alphabet)
criterion = CTCLoss(reduction='sum', zero_infinity=True)

设置训练过程中的数据集加载器、数据转换器、损失函数等。

1. 创建`mydataset.MyDataset`的实例`train_dataset`,并传入训练数据的信息文件路径`config.train_infofile`。
2. 使用`assert`语句检查`train_dataset`是否存在(非空)。
3. 如果优化器类型`optimizer_type`为`rms`,则执行以下操作:
- 生成一个随机种子`config.manualSeed`,范围为1到10000,用于固定随机数生成器的种子。
- 打印出随机种子的值`config.manualSeed`。
- 使用`random.seed()`、`np.random.seed()`和`torch.manual_seed()`设置相应的随机数种子。
- 使用`mydataset.randomSequentialSampler()`创建一个随机顺序的采样器`sampler`,该采样器用于对训练数据进行采样,每次迭代选择一个随机的样本。
4. 如果优化器类型不是`rms`,则`sampler`为`None`。
5. 创建`torch.utils.data.DataLoader`的实例`train_loader`,用于加载训练数据集。参数包括:
- `train_dataset`:训练数据集。
- `batch_size`:批大小,即每次加载的样本数量。
- `shuffle`:是否在每个epoch中打乱数据集。
- `sampler`:采样器,用于决定样本的顺序。
- `num_workers`:加载数据的线程数。
- `collate_fn`:用于对样本进行对齐和规范化处理的函数。
6. 创建`mydataset.MyDataset`的实例`test_dataset`,并传入验证数据的信息文件路径`config.val_infofile`,以及对图像进行尺寸调整和标准化的数据转换器`mydataset.resizeNormalize()`。
7. 创建一个字符标签转换器`converter`,用于将字符标签转换为模型可处理的张量形式。
8. 创建CTC损失函数`criterion`,设置`reduction='sum'`表示对每个样本的损失求和,`zero_infinity=True`表示遇到无穷大的损失时将其设置为0。

def weights_init(m):
    classname = m.__class__.__name__
    if classname.find('Conv') != -1:
        m.weight.data.normal_(0.0, 0.02)
    elif classname.find('BatchNorm') != -1:
        m.weight.data.normal_(1.0, 0.02)
        m.bias.data.fill_(0)


crnn = crnn.CRNN(config.imgH, config.nc, config.nclass, config.nh)
if config.pretrained_model != '' and os.path.exists(config.pretrained_model):
    print('loading pretrained model from %s' % config.pretrained_model)
    crnn.load_state_dict(torch.load(config.pretrained_model))
else:
    crnn.apply(weights_init)

初始化模型权重并创建CRNN模型。

1. 定义了一个`weights_init`函数,用于对模型的权重进行初始化。该函数通过判断`m`的类名是否包含关键字`Conv`或`BatchNorm`来确定执行的初始化操作。对于`Conv`层,使用正态分布随机初始化权重;对于`BatchNorm`层,使用正态分布随机初始化权重,并将偏置项设置为0。
2. 创建一个CRNN模型的实例`crnn`,并传入图像的高度`config.imgH`、通道数`config.nc`、字符类别数`config.nclass`和隐藏层尺寸`config.nh`作为参数。
3. 如果指定了预训练模型文件路径`config.pretrained_model`并且该文件存在,则加载该预训练模型的权重到`crnn`模型中。
4. 如果没有指定预训练模型或者预训练模型文件不存在,则对`crnn`模型的权重进行初始化,调用`crnn.apply(weights_init)`函数。

device = torch.device('cpu')
if config.cuda:
    crnn.cuda()
    # crnn = torch.nn.DataParallel(crnn, device_ids=range(opt.ngpu))
    # image = image.cuda()
    device = torch.device('cuda:0')
    criterion = criterion.cuda()

将模型和损失函数移动到设备上进行计算。

1. 首先将默认设备设置为CPU,通过`device = torch.device('cpu')`实现。
2. 检查配置文件中的`cuda`标志是否为真,如果为真,则将模型和损失函数移动到CUDA设备上进行计算。
- 调用`crnn.cuda()`将模型移动到CUDA设备上。
- 创建一个`torch.device`对象表示CUDA设备,通过`device = torch.device('cuda:0')`指定使用第一个CUDA设备。
- 调用`criterion.cuda()`将损失函数移动到CUDA设备上。

loss_avg = utils.averager()

nbs             = config.batchSize * 8
lr_limit_max    = 1e-3 if optimizer_type == 'adam' else 5e-2
lr_limit_min    = 3e-4 if optimizer_type == 'adam' else 5e-4
Init_lr_fit     = min(max(config.batchSize / nbs * Init_lr, lr_limit_min), lr_limit_max)
Min_lr_fit      = min(max(config.batchSize / nbs * Min_lr, lr_limit_min * 1e-2), lr_limit_max * 1e-2)

lr_scheduler_func = get_lr_scheduler(lr_decay_type, Init_lr_fit, Min_lr_fit, Epoch)

初始化学习率相关的参数和学习率调度器。

1. 创建一个`utils.averager()`实例`loss_avg`,用于计算平均损失。
2. 计算每个epoch中的总迭代次数`nbs`,即批大小乘以8倍的批大小。
3. 根据优化器类型设置学习率的上下限值。如果优化器类型为'adam',则将学习率上限`lr_limit_max`设置为1e-3,否则设置为5e-2;将学习率下限`lr_limit_min`设置为3e-4('adam'优化器)或5e-4(其他优化器)。
4. 根据批大小和总迭代次数计算初始学习率`Init_lr_fit`和最小学习率`Min_lr_fit`,并对其进行限制在上下限范围内。
- 初始学习率`Init_lr_fit`计算方法:将配置文件中的初始学习率`Init_lr`乘以批大小除以总迭代次数,在上下限范围内取最大值。
- 最小学习率`Min_lr_fit`计算方法:将配置文件中的最小学习率`Min_lr`乘以批大小除以总迭代次数,在上下限范围内取最大值,并乘以1e-2。
5. 调用`get_lr_scheduler`函数生成学习率调度器`lr_scheduler_func`。调度器的参数包括:
- `lr_decay_type`:学习率衰减类型,用于确定调度器的类型。
- `Init_lr_fit`:初始学习率。
- `Min_lr_fit`:最小学习率。
- `Epoch`:当前的训练epoch数。

optimizer = {
            'adam'  : optim.Adam(crnn.parameters(), Init_lr_fit, betas = (momentum, 0.999)),
            'sgd'   : optim.SGD(crnn.parameters(), Init_lr_fit, momentum = momentum, nesterov=True),
            'rms'  : optim.RMSprop(crnn.parameters(), lr=Init_lr_fit)
        }[optimizer_type]

根据选择的优化器类型创建相应的优化器对象。

根据`optimizer_type`选择不同的优化器类型:
- 如果`optimizer_type`为'adam',则创建一个Adam优化器对象`optim.Adam(crnn.parameters(), Init_lr_fit, betas=(momentum, 0.999))`。该优化器使用CRNN模型的参数作为优化的目标,并设置初始学习率为`Init_lr_fit`,动量系数为`momentum`,beta系数为(`momentum`,0.999)。
- 如果`optimizer_type`为'sgd',则创建一个SGD优化器对象`optim.SGD(crnn.parameters(), Init_lr_fit, momentum=momentum, nesterov=True)`。该优化器使用CRNN模型的参数作为优化的目标,并设置初始学习率为`Init_lr_fit`,动量系数为`momentum`,使用Nesterov加速。
- 如果`optimizer_type`为'rms',则创建一个RMSprop优化器对象`optim.RMSprop(crnn.parameters(), lr=Init_lr_fit)`。该优化器使用CRNN模型的参数作为优化的目标,并设置初始学习率为`Init_lr_fit`。

def val(net, dataset, criterion, max_iter=100):
    print('Start val')
    for p in net.parameters():
        p.requires_grad = False

    num_correct, num_all = val_model(config.val_infofile, net, True,
                                     log_file='compare-' + config.saved_model_prefix + '.log')
    accuracy = num_correct / num_all

    print('ocr_acc: %f' % (accuracy))
    if config.use_log:
        with open(log_filename, 'a') as f:
            f.write('ocr_acc:{}\n'.format(accuracy))
    global best_acc
    if accuracy > best_acc:
        best_acc = accuracy
        torch.save(crnn.state_dict(), '{}/{}_{}_{}.pth'.format(config.saved_model_dir, config.saved_model_prefix, epoch,
                                                               int(best_acc * 1000)))
    torch.save(crnn.state_dict(), '{}/{}.pth'.format(config.saved_model_dir, config.saved_model_prefix))

定义了一个验证函数`val`,用于在验证集上评估模型的性能。

函数参数:
- `net`:需要评估的模型。
- `dataset`:验证数据集。
- `criterion`:损失函数。
- `max_iter`:最大迭代次数,默认为100。

函数主要步骤:
1. 打印开始验证的提示信息。
2. 将模型中的所有参数的`requires_grad`属性设置为`False`,固定模型参数不进行梯度更新。
3. 调用`val_model`函数对模型进行验证,计算正确的样本数`num_correct`和总样本数`num_all`。同时将验证结果打印到日志文件中,文件名为'compare-' + config.saved_model_prefix + '.log'。
4. 计算准确率`accuracy`,即正确的样本数除以总样本数。
5. 打印准确率。
6. 如果配置中设置了使用日志文件`config.use_log`,则将准确率结果写入日志文件中。
7. 将当前准确率与最佳准确率`best_acc`进行比较,如果当前准确率大于最佳准确率,则更新最佳准确率,并保存模型参数。保存的模型参数文件名包括配置中的保存模型目录`config.saved_model_dir`、保存模型前缀`config.saved_model_prefix`、当前epoch和准确率乘以1000的值。
8. 不论是否更新最佳准确率,都保存一份模型参数文件,文件名为保存模型目录加上保存模型前缀。

def trainBatch(net, criterion, optimizer):
    data = next(train_iter)
    cpu_images, cpu_texts = data
    batch_size = cpu_images.size(0)
    image = cpu_images.to(device)

    text, length = converter.encode(cpu_texts)
    # utils.loadData(text, t)
    # utils.loadData(length, l)

    preds = net(image)  # seqLength x batchSize x alphabet_size
    preds_size = Variable(torch.IntTensor([preds.size(0)] * batch_size))  # seqLength x batchSize
    cost = criterion(preds.log_softmax(2).cpu(), text, preds_size, length) / batch_size
    if torch.isnan(cost):
        print(batch_size, cpu_texts)
    else:
        net.zero_grad()
        cost.backward()
        optimizer.step()
    return cost

定义了一个训练一个batch的函数`trainBatch`,用于对模型进行一次batch的训练。

函数参数:
- `net`:需要训练的模型。
- `criterion`:损失函数。
- `optimizer`:优化器,用于更新模型的参数。

函数主要步骤:
1. 取出一个batch的训练数据,包括图像和对应的标签。
2. 将图像数据转移到设备上。
3. 将标签使用转码器进行编码,得到编码后的标签和标签长度。
4. 使用模型对图像进行前向传播,得到预测结果`preds`,其维度为(seqLength x batchSize x alphabet_size)。
5. 创建变量`preds_size`,用于指定预测结果的尺寸,将其设置为(seqLength x batchSize)。
6. 计算损失值`cost`,将预测结果进行log softmax操作,然后与编码后的标签、预测结果尺寸和标签长度一起传入损失函数计算损失值,并除以batch_size得到平均损失。
7. 如果损失值是NaN(无效值),则打印出batch的大小和对应的文本内容。
8. 否则,将模型的梯度清零,然后进行反向传播计算梯度,并使用优化器更新模型参数。
9. 返回损失值`cost`。

for epoch in range(config.niter):
    loss_avg.reset()
    str_lr = set_optimizer_lr(optimizer, lr_scheduler_func, epoch)
    print('epoch {}....lr {}'.format(epoch, str_lr))
    train_iter = iter(train_loader)
    i = 0
    n_batch = len(train_loader)
    while i < len(train_loader):
        for p in crnn.parameters():
            p.requires_grad = True
        crnn.train()
        cost = trainBatch(crnn, criterion, optimizer)
        print('epoch: {} iter: {}/{} Train loss: {:.3f}'.format(epoch, i, n_batch, cost.item()))
        loss_avg.add(cost)
        loss_avg.add(cost)
        i += 1
    print('Train loss: %f' % (loss_avg.val()))
    if config.use_log:
        with open(log_filename, 'a') as f:
            f.write('{}\n'.format(datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')))
            f.write('train_loss:{}\n'.format(loss_avg.val()))

    val(crnn, test_dataset, criterion)

训练循环,用于训练模型多个epoch,并在每个epoch结束后进行验证。

代码主要步骤:
1. 对损失计算器`loss_avg`进行重置,用于计算每个epoch的平均损失。
2. 调用`set_optimizer_lr`函数设置优化器的学习率,并将学习率的字符串表示赋值给`str_lr`变量。
3. 打印当前epoch和学习率。
4. 获得训练数据集的迭代器`train_iter`。
5. 初始化循环变量`i`为0,并计算总batch数`n_batch`。
6. 在每个batch内循环,直到处理完所有的batch:
- 将模型参数的`requires_grad`属性设置为`True`,允许模型参数进行梯度更新。
- 将模型设置为训练模式。
- 调用`trainBatch`函数对模型进行训练,并得到训练损失值`cost`。
- 打印当前epoch、当前batch索引和总batch数,以及训练损失值。
- 将训练损失值添加到损失计算器`loss_avg`中。
- 增加循环变量`i`的值。
7. 打印当前epoch的平均训练损失。
8. 如果配置中设置了使用日志文件`config.use_log`,则将当前时间和训练损失值写入日志文件中。
9. 调用`val`函数对模型在验证集上进行验证。