字符识别--模型的训练与验证

一个成熟合格的深度学习训练过程至少具备以下功能:

  • 在训练集上训练,并在验证集上进行验证
  • 模型可以保存最优的权重,并读取权重
  • 记录训练集和验证集的精度,便于调参

本节将构建验证集、模型训练和验证、模型保存与加载和模型调参等几个部分

构造验证集

在机器学习模型(特别是深度学习模型)的训练过程中,模型是非常容易过拟合的。深度学习模型在不断的训练过程中训练误差会逐渐降低,但测试误差的走势则不一定。
在模型的训练过程中,模型只能利用训练数据来进行训练,但不能接触测试集上的数据。因此当模型在训练集上得到非常不错的下效果,但在测试集上效果较差,此现象就是过拟合,模型在未见过的测试集上的泛化能力太弱。

字符识别--模型的训练与验证

如上图,随着模型复杂度和模型训练轮数的增加,CNN模型在训练集上的误差会降低,但在测试集上的误差会逐渐降低,然后逐渐升高,而我们追求的是在测试集上的精度越高越好。

导致模型过拟合的情况有很多原因,其中最为常见的情况是模型复杂度(Model Complexity)太高,而数据太少,导致模型过度学习,学习到了一些细枝末节的规律。

解决上述问题的解决方法:构建一个与测试集尽可能分布一致的样本集--验证集,在训练过程中不断验证模型在验证集上的密度,并依次控制模型的训练

一般情况下,可在本地划分出一个验证集,进行本地验证。训练集、验证集和测试集作用如下:

  • 训练集(Train Set):模型用于训练和调整模型参数
  • 验证集(Validation Set):用来验证模型精度和调整超参数
  • 测试集(Test Set):验证模型的泛化能力
    因为训练集和验证集是分开的,所以模型在验证集上的精度在一定程度上可以反映模型的泛化能力。在划分验证集的时候,需要注意验证集的分布与测试集尽量保持一致,不然模型在验证集上的精度就失去了指导意义。

验证集的划分有如下几种方式:

字符识别--模型的训练与验证

  1. 留出法(Hold-Out)

直接将训练集划分为两部分,新的训练集和验证集。优点就是最为直接简单;缺点就是只能得到一份验证集,有可能导致模型在验证集上过拟合。使用场景:数据量比较大的情况

2.交叉验证法(Cross Validation,CV)

将训练集划分为K份,将其中的K-1份作为训练集,剩下的1份作为验证集,循环K训练。这种训练方式是所有的验证集都有机会作为验证集,最终模型验证精度是K份平均得到。优点是验证集精度比较可靠,训练K次可以得到K个有多样性差异的模型;缺点:需要训练K次,不适合数据量很大的情况

3.自主采样法(BootStrap)

有放回的采样方式得到新的训练集和验证集,每次的训练集和验证集都有区别。这种方式一般适合用于数据量较小的情况

本赛题中已给出验证集,因此可以直接使用训练集进行训练,并使用验证集进行验证精度

模型的训练与验证

  1. 加载训练集、验证集数据(DataLoader)
  2. 每轮进行训练和验证,并根据最优验证集精度保存模型
  • 加载训练集数据
train_path = glob.glob(r‘./dataset/mchar_train/*.png‘)
train_path.sort()
train_json = json.load(open(‘./dataset/mchar_train.json‘))
train_lebel = [train_json[x][‘label‘] for x in train_json]
print(len(train_path),len(train_lebel))

train_loader = torch.utils.data.DataLoader(
    SVHNDataset(train_path,train_lebel,
               transforms.Compose([
                   transforms.Resize((64,128)),
                   transforms.RandomCrop((60,120)),
                    transforms.ColorJitter(0.3,0.3,0.2),
                    transforms.RandomRotation(5),
                    transforms.ToTensor(),
                    transforms.Normalize([0.485,0.456,0.406],[0.229,0.244,0.225])
               ])),
    batch_size=40,
    shuffle=True,
    num_workers=0
)

加载验证集数据

val_path = glob.glob(r‘./dataset/mchar_val/*.png‘)
val_path.sort()
val_json = json.load(open(‘./dataset/mchar_val.json‘))
val_label= [val_json[x][‘label‘] for x in val_json]
print(len(val_path),len(val_json))

val_loader = torch.utils.data.DataLoader(
    SVHNDataset(val_path,val_label,
               transforms.Compose([
                   transforms.Resize((64,128)),
                    transforms.ToTensor(),
                    transforms.Normalize([0.485,0.456,0.406],[0.229,0.244,0.225])
               ])),
    batch_size=40,
    shuffle=False,
    num_workers=0
)

在验证集中不需要对数据进行扩增,但需要对原图像进行resize以及正则化处理

  • 训练函数
def train(train_loader,model,criterion,optimizer):
    #训练模式
    model.train()
    train_loss=[]
    
    for i,(input,target) in enumerate(train_loader):
        if use_cuda:
            input = input.cuda()
            target = target.cuda()
        target = target.long()
        c0,c1,c2,c3,c4 = model(input)
        loss = criterion(c0,target[:,0])+         criterion(c1,target[:,1])+         criterion(c2,target[:,2])+         criterion(c3,target[:,3])+         criterion(c4,target[:,4])

        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        if i % 100 == 0:
            print(loss.item())
            
        train_loss.append(loss.item())
    return np.mean(train_loss)
  • 在验证集上做预测
def validate(val_loader,model,criterion):
    #切换预测模式
    model.eval()
    val_loss = []
    
    #不记录模型梯度
    with torch.no_grad():
        for i,(input,target) in enumerate(val_loader):
            if use_cuda:
                input = input.cuda()
                target = target.cuda()
            c0,c1,c2,c3,c4 = model(input)
            target = target.long()
        loss = criterion(c0,target[:,0])+         criterion(c1,target[:,1])+         criterion(c2,target[:,2])+         criterion(c3,target[:,3])+         criterion(c4,target[:,4])

        val_loss.append(loss.item())
    return np.mean(val_loss)
  • 在测试集上做预测
def predict(test_loader,model,tta=10):
    model.eval()
    test_pred_tta = None
    
    #TTA次数
    for _ in range(tta):
        test_pred =[]
        
        with torch.no_grad():
            for i,(input,target) in enumerate(test_loader):
                if use_cuda:
                    input = input.cuda()
                    target = target.cuda()
                c0,c1,c2,c3,c4 = model(input)
                target = target.long()
                output = np.concatenate([
                    c0.data.numpy(),
                    c1.data.numpy(),
                    c2.data.numpy(),
                    c3.data.numpy(),
                    c4.data.numpy()
                ],axis = 1)
                test_pred.append(output)
        test_pred = np.vstack(test_pred)
        if test_pred_tta is None:
            test_pred_tta = test_pred
        else:
            test_pred_tta += test_pred
    return test_pred_tta

对model.train()、model.eval()的解释

以model.train()为例:
字符识别--模型的训练与验证

模型前向传播、后向传播以及对参数的更新

如train()函数中model(input)便是对输入的数据进行前向传播,并通过预先定义好的交叉熵损失函数计算损失值
然后对参数进行初始化optimizer.zero_grad(),随后对数据进行后向传播并更新参数,(loss.backward()与optimizer.step())

字符识别--模型的训练与验证
字符识别--模型的训练与验证

对模型进行训练与保存模型

model = SVHN_Model2()
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(),0.001)
best_loss = 1000.0

use_cuda = False
if use_cuda:
    model = model.cuda()
for epoch in range(2):
    train_loss = train(train_loader,model,criterion,optimizer)
    val_loss = validate(val_loader,model,criterion)
    
    val_label = [‘‘.join(map(str,x) for x in val_loader.dataset.img_label)]
    val_predict_label = np.vstack([
        val_predict_label[:,:11].argmax(1),
        val_predict_label[:,11:22].argmax(1),
        val_predict_label[:,22:33].argmax(1),
        val_predict_label[:,33:44].argmax(1),
        val_predict_label[:,44:55].argmax(1),
    ]).T
    val_label_pred = []
    for x in val_predict_label:
        val_label_pred.append(‘‘.join(map(str,x[x!=10])))
    val_char_acc = np.mean(np.array(val_label_pred) == np.array(val_label))
    print(‘Epoch:{0} ,Train loss :{1}\t Val loss:{2}‘.format(epoch,train_loss,val_loss))
    print(val_char_acc)
    if val_loss < best_loss:
        best_loss = val_loss
        torch.save(model.state_dict(),‘./model.pt‘)

由于设备的算力问题,目前计算机仍在辛苦计算,现在它只是需要时间
部分训练数据结果

字符识别--模型的训练与验证

模型的保存与加载

比较常见的做法是保存和加载模型参数:

torch.save(model_object.state_dict(),‘model.pt‘)

加载模型参数

model.load_state_dict(torch.load(‘model.pt‘))

模型调参

这里对深度学习的训练技巧推荐的阅读链接:

  1. http://lamda.nju.edu.cn/weixs/project/CNNTricks/CNNTricks.html
  2. http://karpathy.github.io/2019/04/25/recipe/

相关推荐