Datawhale 零基础入门CV赛事 学习总结和水神直播内容回顾

Datawhale 零基础入门CV赛事 学习总结和直播内容回顾

接上Baseline遇到的问题

纯小白,先把不懂的地方写上来

一、数据集

· 训练集(training set)
· 验证集(validation set)
· 测试集(test set)
参考理解:

https://www.cnblogs.com/HuZihu/p/10538295.htm

把数据集随机分为训练集,验证集和测试集,然后用训练集训练模型,用验证集验证模型,根据情况不断调整模型,选择出其中最好的模型,再用训练集和验证集数据训练出一个最终的模型,最后用测试集评估最终的模型。

现在不只是把数据集分为了训练集和测试集,需要再分出一个测试集,用于最终模型的评估。因为已经有一个测试集了,因此把其中一个用于模型选择的测试集改名叫验证集,以防止混淆。(有些资料上是先把数据集分为训练集和测试集,然后再把训练集分为训练集和验证集)

前几个步骤和“训练集+测试集”类似:首先用训练集训练出模型,然后用验证集验证模型(注意:这是一个中间过程,此时最好的模型还未选定),根据情况不断调整模型,选出其中最好的模型(验证误差用于指导我们选择哪个模型),记录最好的模型的各项设置,然后据此再用(训练集+验证集)数据训练出一个新模型,作为最终的模型,最后用测试集评估最终的模型。

由于验证集数据的信息会被带入到模型中去,因此,验证误差通常比测试误差要小。同时需要记住的是:测试误差是我们得到的最终结果,即便我们对测试得分不满意,也不应该再返回重新调整模型,因为这样会把测试集的信息带入到模型中去。

当我们拿到数据之后,一般来说,我们把数据分成这样的三份:训练集(60%),验证集(20%),测试集(20%)。用训练集训练出模型,然后用验证集验证模型,根据情况不断调整模型,选出其中最好的模型,记录最好的模型的各项选择,然后据此再用(训练集+验证集)数据训练出一个新模型,作为最终的模型,最后用测试集评估最终的模型。

本次比赛用的是SVHN数据集,The Street View House Numbers (SVHN) Dataset

baseline

思路1 设为不定长字符串
思路2 比如CRNN(CNN+STM),训练周期会长(因为有STM),但是由于非定长字符不像中文有统计关系,每个数字都是独立的,所以思路一可能优于字符2
思路3 把每个字符抠出来,用物理检测的方法转换为单字符识别的方法,需要物体检测的模型

今天直播课讲的是第一种思路
在这里插入图片描述
直接在论坛把baseline下载下来就可以跑了,下面细节:
首先导入常用的包:

import os, sys, glob, shutil, json
os.environ["CUDA_VISIBLE_DEVICES"] = '0'  #如果有GPU指定第一个GPU
import cv2

from PIL import Image
import numpy as np

>from tqdm import tqdm, tqdm_notebook

import torch
torch.manual_seed(0)
torch.backends.cudnn.deterministic = False
torch.backends.cudnn.benchmark = True

import torchvision.models as models
import torchvision.transforms as transforms
import torchvision.datasets as datasets
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.autograd import Variable
from torch.utils.data.dataset import Dataset

步骤1:定义数据集

如果之前用的keras或TensorFlow就会对pytorch这种框架有点陌生,其实在pytorch里面,如果要解决一个视觉问题,最基础的就是要构建一个数据集。
在这里就是重载一下pytorch里面的Dataset数据集

class SVHNDataset(Dataset):

#定义初始化方法

    def __init__(self, img_path, img_label, transform=None):            
        self.img_path = img_path  
        self.img_label = img_label 
        if transform is not None:
            self.transform = transform
        else:
            self.transform = None
#按照索引读取数据的

    def __getitem__(self, index):    
        img = Image.open(self.img_path[index]).convert('RGB')
        if self.transform is not None:
            img = self.transform(img)       
        # 设置最长的字符长度为5个
        lbl = np.array(self.img_label[index], dtype=np.int)
        lbl = list(lbl)  + (5 - len(lbl)) * [10]
        return img, torch.from_numpy(np.array(lbl[:5]))
    def __len__(self):
        return len(self.img_path)

上述把最长字符设为5,也可以设为更短
这里报错的原因,可以吧dataset设为单个,索引第0个样本dataset_test[0],看返回pytorch里的tensor和对应的标签,返回成功说明dataset定义成功。

步骤2:定义好训练数据和验证数据的Dataset

这里是要验证你的dataset有没有定义好,就要把训练集的图片路径和标签一起传进去

train_path = glob.glob('../input/train/*.png')
train_path.sort()
train_json = json.load(open('../input/train.json'))
train_label = [train_json[x]['label'] for x in train_json]
print(len(train_path), len(train_label))

train_loader = torch.utils.data.DataLoader(
    SVHNDataset(train_path, train_label,
                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.224, 0.225])
    ])), 
    batch_size=40, 
    shuffle=True, 
    num_workers=10,
)

val_path = glob.glob('../input/val/*.png')
val_path.sort()
val_json = json.load(open('../input/val.json'))
val_label = [val_json[x]['label'] for x in val_json]
print(len(val_path), len(val_label))

#dataloader和dataset的区别:
#dataset提供的是单个样本的索引,dataloader可以把他封装成一个bench读取

val_loader = torch.utils.data.DataLoader(
    SVHNDataset(val_path, val_label,
                transforms.Compose([
                    transforms.Resize((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.224, 0.225])
    ])), 
    batch_size=40, 
    shuffle=False, 
    num_workers=10,
)

在windows下num_workers=10可能会报错,可以把10改一下。

步骤3:定义好字符分类模型,使用renset18的模型作为特征提取模块

水神说在他写basenet之前也尝试过构建几层,一个四五层的小型CNN,但是发现并没有直接使用imagenet这种预训练模型好。
定义方法:

class SVHN_Model1(nn.Module):
    def __init__(self):
        super(SVHN_Model1, self).__init__()
        
        #直接使用torchvision里面的预训练模型,这个是18层的        
        model_conv = models.resnet18(pretrained=True)
        #把pool层改一下
        model_conv.avgpool = nn.AdaptiveAvgPool2d(1)
        
        model_conv = nn.Sequential(*list(model_conv.children())[:-1])
        self.cnn = model_conv
        #这里全连接层分为5个字符,分为五个字符的多分类的分支
        #这里是11类的原因是加入了填充,第一个字符也可以改成0,10类,因为第一位肯定是非空的
        self.fc1 = nn.Linear(512, 11)
        self.fc2 = nn.Linear(512, 11)
        self.fc3 = nn.Linear(512, 11)
        self.fc4 = nn.Linear(512, 11)
        self.fc5 = nn.Linear(512, 11)
    #接着定义前向传播
    def forward(self, img):        
        feat = self.cnn(img)
        # print(feat.shape)
        feat = feat.view(feat.shape[0], -1)
        c1 = self.fc1(feat)
        c2 = self.fc2(feat)
        c3 = self.fc3(feat)
        c4 = self.fc4(feat)
        c5 = self.fc5(feat)
        return c1, c2, c3, c4, c5

pytorch中,只需要定义好模型的参数和前向传播
步骤4:定义好训练、验证和预测模块,pytorch会自动的根据前向传播计算反向传播。

在定义好上述之后就可以写训练的代码了,此代码和普通图像分类的代码是比较类似的,唯一不同的是loss那里:

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()
        #这里把所有五个字符的分类的loss都累计加起来    
        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])
        #这里可以把loss除以6或者除以5,为了平均一下,是一个细节。
        # loss /= 6
        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)
            loss = criterion(c0, target[:, 0]) + \
                    criterion(c1, target[:, 1]) + \
                    criterion(c2, target[:, 2]) + \
                    criterion(c3, target[:, 3]) + \
                    criterion(c4, target[:, 4])
            # loss /= 6
            val_loss.append(loss.item())
    return np.mean(val_loss)

#这里在预测过程中加入TTA环节
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()
                
                c0, c1, c2, c3, c4 = model(input)
                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

因为这里把它抽象为一个定长字符的问题,所以直接用交叉熵,定义了优化器,如果有gpu改为true

步骤5:迭代训练和验证模型

model = SVHN_Model1()
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, epoch)
    val_loss = validate(val_loader, model, criterion)
    
    val_label = [''.join(map(str, x)) for x in val_loader.dataset.img_label]
    val_predict_label = predict(val_loader, model, 1)
    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')

训练两个2 Epoch后,输出的训练日志为:

Epoch: 0, Train loss: 3.1 Val loss: 3.4 验证集精度:0.3439
Epoch: 1, Train loss: 2.1 Val loss: 2.9 验证集精度:0.4346

ps 我得到的输出日志为
在这里插入图片描述

步骤6:对测试集样本进行预测,生成提交文件

test_path = glob.glob('../input/test_a/*.png')
test_path.sort()
test_label = [[1]] * len(test_path)
print(len(val_path), len(val_label))

test_loader = torch.utils.data.DataLoader(
    SVHNDataset(test_path, test_label,
                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.224, 0.225])
    ])), 
    batch_size=40, 
    shuffle=False, 
    num_workers=10,
)

test_predict_label = predict(test_loader, model, 1)

test_label = [''.join(map(str, x)) for x in test_loader.dataset.img_label]
test_predict_label = np.vstack([
    test_predict_label[:, :11].argmax(1),
    test_predict_label[:, 11:22].argmax(1),
    test_predict_label[:, 22:33].argmax(1),
    test_predict_label[:, 33:44].argmax(1),
    test_predict_label[:, 44:55].argmax(1),
]).T

test_label_pred = []
for x in test_predict_label:
    test_label_pred.append(''.join(map(str, x[x!=10])))
    
import pandas as pd
df_submit = pd.read_csv('../input/test_A_sample_submit.csv')
df_submit['file_code'] = test_label_pred
df_submit.to_csv('renset18.csv', index=None)

在训练完成2个Epoch后,模型在测试集上的成绩应该在0.33左右。

结语:最后在工作区生成了renset18.csv文件
在这里插入图片描述
好快乐,baseline终于跑出来了,对初学者真是有点不友好。。。继续努力呀

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值