kaggle比赛:CIFAR-10

最近发现kaggle每周给了30小时的GPU额度,直接使用在线jupyter notebook,非常方便。以后就直接在kaggle上练习了。

第一版,尽量不要“Save Version”离线运行,因为离线运行只要一个语句出错,整个程序白跑。所以一般是第一版在线跑完,没有错,之后改动比较小的细调时再离线跑。

比如这场比赛,因为数据集是压缩包,所以需要先导入库、然后在线解压到缓存里(大概需要20分钟),然后才开始训练模型,但只要下面的程序有一点错,就会导致程序中断、缓存清空,从头开始。

我在训练模型时就经常有一点小错误,比如单词拼错、模型好不容易跑完了,预测测试集出错,模型参数也保存失败。直接浪费了我一下午时间。

比赛地址:CIFAR-10 - Object Recognition in Images | Kaggle

1. 导入数据

因为是kaggle官方比赛的数据,所以在Competition Data里直接能搜出来,添加后就到input里了。

小tip:/kaggle/input 路径为只读路径

如果想写入数据,只能放到/kaggle/working 路径下

 然后发现这里给的数据是一个zip压缩包,所以需要先解压,这里参考了一下code里别人发的代码

导入库:

pip install py7zr -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com
from py7zr import unpack_7zarchive
import shutil
shutil.register_unpack_format('7zip',['.7z'],unpack_7zarchive)
shutil.unpack_archive('../input/cifar-10/train.7z', './kaggle/temp/') 
shutil.unpack_archive('../input/cifar-10/test.7z', './kaggle/temp/') 
"""解压到./kaggle/temp/目录下"""

shutil.register_unpack_format(name, extensions, function[, extra_args[, description]])

注册一个解包格式。 name 为格式名称而 extensions 为对应于该格式的扩展名列表,例如 Zip 文件的扩展名为 .zip。

shutil.unpack_archive(filename[, extract_dir[, format]])

解包一个归档文件。 filename 是归档文件的完整路径。

2. 读入数据、数据增强

《动手学深度学习》里用的是用的torchvision.datasets.ImageFolder(),这就需要预处理数据,对于每个类都新建个文件夹,要到处挪图像数据。虽然比较直观、方便,但对于稍微大一点的数据集,大量copy图像的操作时间,是不能接受的。

这里引入更正式的方法——自定义dataset类

torch.utils.data.Dataset是一个抽象类,想要加载自定义的数据只需要继承这个类,并且覆写其中的三个方法即可:

  1. __init__(self): 数据预处理。
  2. __getitem__(self, index):用来获取一些索引的数据,使dataset[i]返回数据集中第i个样本。
  3. __len__(self):实现len(dataset)返回整个数据集的大小。

设置好数据类之后,我们就可以将其用torch.utils.data.DataLoader加载,并访问它。

在定义getitem时,直接进行数据增强。

import math
import pandas as pd
import numpy as np
import torch
from torch import nn
import torchvision
from torchvision import transforms
import torchvision.models as models
from torch.utils.data import Dataset, DataLoader
from PIL import Image
import matplotlib.pyplot as plt
from tqdm import tqdm

# 继承pytorch的dataset,创建自己的
class Cifar10(Dataset):
    def __init__(self, csv_path, file_path, mode='train', valid_ratio=0.1):
        self.file_path = file_path
        self.mode = mode
        self.data_info = pd.read_csv(csv_path, header=None) # 不设置表头,默认为0,1,2..
        self.data_len = len(self.data_info.index) - 1 # 减去第一行id,label
        self.train_len = int(self.data_len*(1-valid_ratio))

        if mode=='train':
            self.train_image = np.asarray(self.data_info.iloc[1:self.train_len,0])
            self.train_label = np.asarray(self.data_info.iloc[1:self.train_len,1])
            self.image_arr = self.train_image
            self.label_arr = self.train_label
        elif mode=='valid':
            self.valid_image = np.asarray(self.data_info.iloc[self.train_len:,0])
            self.valid_label = np.asarray(self.data_info.iloc[self.train_len:,1])
            self.image_arr = self.valid_image
            self.label_arr = self.valid_label
        elif mode=='test':
            self.test_image = np.asarray( np.arange(1,300001) )
            self.image_arr = self.test_image

        self.real_len = len(self.image_arr)

    def __getitem__(self, index):
        if self.mode=='test':
            single_image_dir = '/test/' + str(self.image_arr[index]) + '.png'
        else:
            single_image_dir = '/train/' + str(self.image_arr[index]) + '.png'
        # 这里的image_dir 要结合csv里的内容和图片所在位置综合得出,具体分析
        img_as_img = Image.open(self.file_path+single_image_dir)

        """图像增广"""
        if self.mode=='train':
            transform = transforms.Compose([
                # 在⾼度和宽度上将图像放⼤到40像素的正⽅形
                torchvision.transforms.Resize(40),
                # 随机裁剪出⼀个⾼度和宽度均为40像素的正⽅形图像,
                # ⽣成⼀个⾯积为原始图像⾯积0.64到1倍的⼩正⽅形,
                # 然后将其缩放为⾼度和宽度均为32像素的正⽅形
                torchvision.transforms.RandomResizedCrop(32,scale=(0.64,1.0),ratio=(1.0,1.0)),
                # 水平翻转
                torchvision.transforms.RandomHorizontalFlip(),
                transforms.ToTensor(),
                # 标准化图像的每个通道
                torchvision.transforms.Normalize([0.4914, 0.4822, 0.4465],[0.2023, 0.1994, 0.2010])
            ])
        else:
            # valid和test不做数据增强
            transform = transforms.Compose([
                transforms.ToTensor(),
                # 标准化图像的每个通道
                torchvision.transforms.Normalize([0.4914, 0.4822, 0.4465],[0.2023, 0.1994, 0.2010])
            ])

        img_as_img = transform(img_as_img)
        if self.mode=='test':
            return img_as_img
        else:
            num_label = class_to_num[ self.label_arr[index] ]
            return img_as_img, num_label
    def __len__(self):
        return self.real_len

定义两个字典,记录数字和标签的对应关系。类似c++里的map

train_path = './data/cifar-10/trainLabels.csv'
file_path = './data/cifar-10'

labels_dataframe = pd.read_csv(train_path)
# 把label文件去重,排个序
leaves_labels = sorted(list(set(labels_dataframe['label'])))
print(leaves_labels)
# 把label转成对应的数字
n_classes = len(leaves_labels)
class_to_num = dict(zip(leaves_labels, range(n_classes)))
# 数字转成对应label,方便最后预测的时候使用
num_to_class = {v : k for k, v in class_to_num.items()}

train_dataset = Cifar10(train_path, file_path, mode='train')
valid_dataset = Cifar10(train_path, file_path, mode='valid')
test_dataset = Cifar10(train_path, file_path, mode='test')

train_iter = DataLoader(
    dataset = train_dataset,
    batch_size = 8,
    shuffle = False,
    num_workers = 2  # 根据数据集大小。CPU、GPU性能决定,一般设成[0,4]之间
)
valid_iter = DataLoader(
    dataset = valid_dataset,
    batch_size = 8,
    shuffle = False,
    num_workers = 2
)
test_iter = DataLoader(
    dataset = test_dataset,
    batch_size = 8,
    shuffle = False,
    num_workers = 2
)

查看是否读入成功

# 测试数据是否读入成功
import os
os.environ['KMP_DUPLICATE_LIB_OK'] = 'TRUE'
"""最近用jupyter调用plt就会出现内核挂掉的情况,加上上面两个语句就好了,正常情况是不用加的"""

def im_convert(tensor):
    image = tensor.to("cpu").clone().detach()
    image = image.numpy().squeeze()
    image = image.transpose(1,2,0)
    image = image.clip(0, 1)
    return image
    
fig = plt.figure(figsize=(20,12))
dataiter = iter(train_iter)
img, label = dataiter.next()

for i in range(8):
    ax = fig.add_subplot(2,4,i+1,xticks=[],yticks=[])
    ax.set_title(num_to_class[int(label[i])])
    plt.imshow(im_convert(img[i]))
plt.show()

原始数据

增强后的数据

3. 定义模型和训练函数

torch.optim.lr_scheduler模块提供了一些根据epoch训练次数来调整学习率(learning rate)的方法。一般情况下我们会设置随着epoch的增大而逐渐减小学习率从而达到更好的训练效果。

class torch.optim.lr_scheduler.StepLR(optimizer, step_size, gamma=0.1, last_epoch=-1)
optimizer (Optimizer):要更改学习率的优化器;
step_size(int):每训练step_size个epoch,更新一次参数;
gamma(float):更新lr的乘法因子;
last_epoch (int):最后一个epoch的index,如果是训练了很多个epoch后中断了,继续训练,这个值就等于加载的模型的epoch。默认为-1表示从头开始训练,即从epoch=1开始。
 

def get_resnet(num_classes, use_pretrained):
    """调用models里封装好的模型,使用微调,当然也可以自己定义"""
    net = models.resnet18(pretrained=use_pretrained)  # 使用微调
    net.fc = nn.Sequential(  # 替换最后一层
        nn.Linear(net.fc.in_features, num_classes)
    )
    return net

# 单GPU训练,如果时多GPU训练参考《动手学深度学习》p667
def train(net,device, train_iter, valid_iter, num_epochs, lr, wd, lr_period, lr_decay):
    print('training on: ', device)
    # 梯度下降
    opt = torch.optim.SGD(net.parameters(), lr=lr, weight_decay=wd, momentum=0.9) # 带动量的SGD
    scheduler = torch.optim.lr_scheduler.StepLR(opt, lr_period, lr_decay) # 每经过lr_period个epoch,学习率lr=lr*lr_decay

    # 损失函数
    """加了reduction = 'none',直接返回n分样本的loss。下面训练就需要l.sum().backward(),或者l.mean().backward()"""
    """如果什么也不写默认均值l.mean(),如果使得损失函数明显一点用reduction='sum', 求和表示 """
    loss = nn.CrossEntropyLoss()
    
    for epoch in range(num_epochs):
        train_loss=[]
        train_acc =[]
        valid_acc =[]
        
        net.train() # 训练模式
        for batch in tqdm(train_iter):  # 需要看进度条就加上
            imgs, labels = batch
            imgs = imgs.to(device)
            labels = labels.to(device)
            labels_pred = net(imgs)
            l = loss(labels_pred, labels) # 不能写反了,label_pred在前
            opt.zero_grad()
            l.backward()
            opt.step()
            
            acc = (labels_pred.argmax(dim=-1)==labels).float().mean()
            train_loss.append(l.item())
            train_acc.append(acc)
        
        net.eval()  # 评估模式
        for batch in tqdm(valid_iter):  # 需要看进度条就加上
            imgs, labels = batch
            imgs = imgs.to(device)
            labels = labels.to(device)
            with torch.no_grad():
                labels_pred = net(imgs)
            acc = (labels_pred.argmax(dim=-1)==labels).float().mean()
            valid_acc.append(acc)
        
        scheduler.step()  # 在每个epoch后加上
        train_loss = sum(train_loss) / len(train_loss)
        train_acc  = sum(train_acc) / len(train_acc)
        valid_acc  = sum(valid_acc) / len(valid_acc)
        print(f'the {epoch+1} epoch, loss = {train_loss:.5f}, train_acc = {train_acc:.5f}, valid_acc = {valid_acc:.5f}')
    # 所有epoch全部跑完
    torch.save(net.state_dict(), 'resnet.params')
    print('Training completed!!')

调用函数

# 获取GPU,没有的话用CPU
def get_device():
    return 'cuda' if torch.cuda.is_available() else 'cpu'
device = get_device()

# 获取模型,放到GPU上
net = get_resnet(10, True)  # 10类,微调
net = net.to(device)
net.device = device

"""可以先跑1个epoch,没问题再多加几个正式训练"""
num_epochs, lr, wd = 1, 2e-4, 5e-4 
lr_period, lr_decay = 4, 0.9   # 每4个epoch,learning_rate缩小为90%
train(net, device, train_iter, valid_iter,num_epochs,lr,wd,lr_period,lr_decay)

4. 训练完成,对测试集进⾏分类并提交结果

net = get_resnet(10, False)
net.load_state_dict(torch.load('resnet.params'))  # 读入训练好的参数

def get_device():
    return 'cuda' if torch.cuda.is_available() else 'cpu'
device = get_device()
net = net.to(device)
net.device = device
net.eval() # 开启评估模式

preds_num = []
for img in test_iter:
    with torch.no_grad():
        label = net(img.to(device))
    # 1个batch一组,批量extend到列表里
    preds_num.extend( label.argmax(dim=-1).cpu().numpy().tolist())

preds_class = []
for x in preds_num:
    preds_class.append(num_to_class[x])

print('测试集长度:',len(preds_class))
sorted_ids = list(range(1, len(preds_class) + 1))
df = pd.DataFrame({'id': sorted_ids, 'label': preds_class})
df.to_csv('submission.csv', index=False)

  • 2
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值