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终于跑出来了,对初学者真是有点不友好。。。继续努力呀