李沐《动手学深度学习》kaggle树叶分类(ResNet18无预训练)python代码实现

前言

在尝试这个树叶分类之前,作者仅仅看完了ResNet残差网络一章,并没有看后面关于数据增强的部分,这导致在第一次使用最原始的ResNet18直接跑完训练数据之后的效果十分的差,提交kaggle后的准确仅有20%左右。本文最后依然使用未经预训练的手写ResNet18网络,但做了一定的数据增强,最终在较少的迭代次数下在kaggle精度能达到80%。

数据集来自李沐老师在kaggle官网所用的Classify Leaves数据集。

地址:https://www.kaggle.com/competitions/classify-leaves

一、数据处理

首先读入train.csv训练数据,使用sklearn中的labelencoder和train_test_split把训练数据的标签映射成int值,再随机取10%作为验证数据。

python 复制代码
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder


df = pd.read_csv('./kaggle_classify_leaves_dataset/train.csv')

label_encoder = LabelEncoder() # 创建encoder类,准备把文本标签映射成int数
df['label'] = label_encoder.fit_transform(df['label'])
train_df, valid_df = train_test_split(df, test_size=0.1, stratify=df['label'])

二、DataSet

重点在于DataSet中的数据增强部分,使不使用数据增强对结果的影响非常大。本文对训练所用的图片进行了一些随机的裁剪、随机的对比度曝光调整以及对图片的标准化。

在初次接触数据增强的写法时我好奇这种写法并没有真正意义上的扩充训练数据的规模,后来发现在ImageDataset中对图片的一系列增强操作,只有在用train_iter真正的把数据一批一批的取出来的时候才会生效。也就是说两次epoch迭代之间虽然都是用了train_iter来取同样的所有的图片,但是由于图片增强的操作是随机的,所以不同的迭代之间用的图片实际上是不一样的。由此一来我们只要增加迭代的次数,就增加了图片的多样性。

python 复制代码
class ImageDataset(Dataset): # 用来获取图片和标签的dataset
    def __init__(self, dataframe, img_dir, mode=None):
        self.dataframe = dataframe
        self.img_dir = img_dir
        if mode=='train':
            preprocess = transforms.Compose([
                        transforms.Resize(256),
                        transforms.RandomCrop(224),
                        transforms.RandomHorizontalFlip(),
                        transforms.RandomVerticalFlip(),
                        transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1),  # 随机改变颜色
                        transforms.ToTensor(),                
                        transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])  # 标准化
                        ])
        elif mode=='val':
            preprocess = transforms.Compose([
                        transforms.Resize(256),
                        transforms.CenterCrop(224),  # 中心裁剪
                        transforms.ToTensor(),
                        transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
                        ])
        elif mode=='test':
            preprocess = transforms.Compose([
                        transforms.Resize(256),
                        transforms.CenterCrop(224),  # 中心裁剪
                        transforms.ToTensor(),
                        transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
                        ])
        self.transform = preprocess
        self.mode = mode
        
    def __len__(self):
        return len(self.dataframe)
    
    def __getitem__(self,idx):
        img_name = os.path.join(self.img_dir, self.dataframe.iloc[idx, 0]) # 得到图片的路径
        img = Image.open(img_name) # 打开图像
        if self.transform:
            img = self.transform(img)
        if self.mode == 'test':
            return img
        
        label = self.dataframe.iloc[idx, 1]
        label = torch.tensor(label, dtype=torch.long)
                  
        return img, label


train_data = ImageDataset(train_df, img_dir, 'train')
valid_data = ImageDataset(valid_df, img_dir, 'val')



# 参数
batch_size, lr, num_epochs = 128, 0.001, 30
train_iter = DataLoader(train_data, batch_size=batch_size, shuffle=True) 
valid_iter = DataLoader(valid_data, batch_size=batch_size, shuffle=True)

三、完整代码

因为后面的模型和训练部分使用的都是李沐所教的,这里之间给出完整代码。

python 复制代码
import pandas as pd
import torch
from torch import nn
import os
from PIL import Image
import torchvision.transforms as transforms
from torch.utils.data import Dataset, DataLoader
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import LabelEncoder
from d2l import torch as d2l
from torch.nn import functional as F

class Residual(nn.Module):
    def __init__(self, in_channels, num_channels,
                 use_1x1conv=False, strides=1):
        super().__init__()
        self.conv1 = nn.Conv2d(in_channels, num_channels, 
                               kernel_size=3, padding=1, stride=strides)
        self.conv2 = nn.Conv2d(num_channels, num_channels, 
                               kernel_size=3, padding=1)
        
        if use_1x1conv:
            self.conv3 = nn.Conv2d(in_channels, num_channels, kernel_size=1, stride=strides)
        else:
            self.conv3 = None
            
        self.bn1 = nn.BatchNorm2d(num_channels)
        self.bn2 = nn.BatchNorm2d(num_channels)
        
    def forward(self, X):
        Y = F.relu(self.bn1(self.conv1(X)))
        Y = self.bn2(self.conv2(Y))
        if self.conv3:
            X = self.conv3(X)
        Y += X
        return F.relu(Y)
    

def resnet_block(in_channels, num_channels, num_residuals, first_block=False):
    blk = []
    for i in range(num_residuals):
        if i == 0 and not first_block: # 
            blk.append(Residual(in_channels, num_channels, True, strides=2))
        else:
            blk.append(Residual(num_channels, num_channels))
            
    return blk

class ImageDataset(Dataset): # 用来获取图片和标签的dataset
    def __init__(self, dataframe, img_dir, mode=None):
        self.dataframe = dataframe
        self.img_dir = img_dir
        if mode=='train':
            preprocess = transforms.Compose([
                        transforms.Resize(256),
                        transforms.RandomCrop(224),
                        transforms.RandomHorizontalFlip(),
                        transforms.RandomVerticalFlip(),
                        transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1),  # 随机改变颜色
                        transforms.ToTensor(),                
                        transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])  # 标准化
                        ])
        elif mode=='val':
            preprocess = transforms.Compose([
                        transforms.Resize(256),
                        transforms.CenterCrop(224),  # 中心裁剪
                        transforms.ToTensor(),
                        transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
                        ])
        elif mode=='test':
            preprocess = transforms.Compose([
                        transforms.Resize(256),
                        transforms.CenterCrop(224),  # 中心裁剪
                        transforms.ToTensor(),
                        transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
                        ])
        self.transform = preprocess
        self.mode = mode
        
    def __len__(self):
        return len(self.dataframe)
    
    def __getitem__(self,idx):
        img_name = os.path.join(self.img_dir, self.dataframe.iloc[idx, 0]) # 得到图片的路径
        img = Image.open(img_name) # 打开图像
        if self.transform:
            img = self.transform(img)
        if self.mode == 'test':
            return img
        
        label = self.dataframe.iloc[idx, 1]
        label = torch.tensor(label, dtype=torch.long)
                  
        return img, label
    

img_dir = './kaggle_classify_leaves_dataset/'
df = pd.read_csv('./kaggle_classify_leaves_dataset/train.csv')
label_encoder = LabelEncoder() # 创建encoder类,准备把文本标签映射成int数
df['label'] = label_encoder.fit_transform(df['label'])


train_df, valid_df = train_test_split(df, test_size=0.1, stratify=df['label'])

train_data = ImageDataset(train_df, img_dir, 'train')
valid_data = ImageDataset(valid_df, img_dir, 'val')



# 参数
batch_size, lr, num_epochs = 128, 0.001, 30
train_iter = DataLoader(train_data, batch_size=batch_size, shuffle=True) 
valid_iter = DataLoader(valid_data, batch_size=batch_size, shuffle=True) 


b1 = nn.Sequential( # 初始的层,输出后通道数变为64,图片大小折半两次
    nn.Conv2d(3, 64, kernel_size=7, stride=2, padding=3),
    nn.BatchNorm2d(64), nn.ReLU(), nn.MaxPool2d(kernel_size=3, stride=2, padding=1))

b2 = nn.Sequential(*resnet_block(64, 64, 2, True)) # 因为经过初始层图片的wh已经变为了1/4,这里就不做进一步的减小了
b3 = nn.Sequential(*resnet_block(64, 128, 2))
b4 = nn.Sequential(*resnet_block(128, 256, 2))
b5 = nn.Sequential(*resnet_block(256, 512, 2))

net = nn.Sequential(b1, b2, b3, b4, b5, nn.AdaptiveAvgPool2d((1,1)), # (1,512,1,1) flatten后变为(1,512)
                    nn.Flatten(), nn.Linear(512, 176))

def evaluate_accuracy_gpu(net, data_iter, device=None):
    # 利用gpu评估精度
    if isinstance(net, nn.Module):
        net.eval() # 设置为评估模式
        if not device: # 如果没有gpu
            device = next(iter(net.parameters())).device # 如果没设定device,就是用net里第一层参数所用的device
    metric = d2l.Accumulator(2)
    
    with torch.no_grad():
        for X, y in data_iter:
            # 把需要评估的数据都放到device上
            if isinstance(X, list):
                X = [x.to(device) for x in X]
            else :
                X = X.to(device)
            y = y.to(device)
            metric.add(d2l.accuracy(net(X), y), y.numel())
    return metric[0] / metric[1]


def train_ch6(net, train_iter, test_iter, num_epochs, lr, device):
     
    # 利用Xavier初始化卷积层和全连接层的权重
    def init_weights(m): 
        if type(m) == nn.Linear or type(m) == nn.Conv2d:
            nn.init.xavier_uniform_(m.weight)
    net.apply(init_weights)
    net.to(device) # 把net也放到device上
    print('training on ',device)
    optimizer = torch.optim.Adam(net.parameters(), lr=lr)
    loss = nn.CrossEntropyLoss() # reduction=none使得返回一个10维的张量
        
    for epoch in range(num_epochs):
        metric = d2l.Accumulator(3)
        net.train()
        for i, (X, y) in enumerate(train_iter):
            optimizer.zero_grad()
            X ,y = X.to(device), y.to(device)
            y_hat = net(X)
            l = loss(y_hat, y)
            l.backward()
            optimizer.step()
            with torch.no_grad():
                metric.add(l*X.shape[0], d2l.accuracy(y_hat, y), X.shape[0])
        train_l = metric[0] / metric[2]
        train_acc = metric[1] / metric[2]
        test_acc = evaluate_accuracy_gpu(net, test_iter)
        print(f'第{epoch}次迭代结束, loss {train_l:.3f}, train acc {train_acc:.3f}, test acc {test_acc:.3f}')
        
    print(f'最终 loss {train_l:.3f}, train acc {train_acc:.3f}, '
          f'test acc {test_acc:.3f}')


# 训练
train_ch6(net, train_iter, valid_iter, num_epochs, lr, torch.device('cuda'))

四、提交代码

python 复制代码
df_test = pd.read_csv('./kaggle_classify_leaves_dataset/test.csv')


test_data = ImageDataset(df_test, img_dir, 'test')
test_loader = DataLoader(test_data, batch_size=128, shuffle=False)
net.eval()
all_preds = []

with torch.no_grad():
    for imgs in test_loader:
        imgs = imgs.to(torch.device('cuda'))  # 如果使用GPU,确保数据在GPU上
        outputs = net(imgs)
        _, preds = torch.max(outputs, 1)  # 获取概率最大的类
        all_preds.extend(preds.cpu().numpy())  # 将预测结果收集到列表中

df_test['label'] = pd.Series(all_preds)
df_test['label'] = label_encoder.inverse_transform(df_test['label'])
submission = pd.concat([df_test['image'], df_test['label']], axis = 1)
submission.to_csv("./kaggle_classify_leaves_dataset/submission.csv", index=False)

五、最终效果

由于时间问题本文使用30次迭代,验证精度可以达到接近80%,建议设置100次迭代来达到较好的效果。

30次迭代在kaggle上的效果

相关推荐
池央40 分钟前
AI性能极致体验:通过阿里云平台高效调用满血版DeepSeek-R1模型
人工智能·阿里云·云计算
我们的五年41 分钟前
DeepSeek 和 ChatGPT 在特定任务中的表现:逻辑推理与创意生成
人工智能·chatgpt·ai作画·deepseek
Yan-英杰42 分钟前
百度搜索和文心智能体接入DeepSeek满血版——AI搜索的新纪元
图像处理·人工智能·python·深度学习·deepseek
Fuweizn44 分钟前
富唯智能可重构柔性装配产线:以智能协同赋能制造业升级
人工智能·智能机器人·复合机器人
weixin_307779132 小时前
Azure上基于OpenAI GPT-4模型验证行政区域数据的设计方案
数据仓库·python·云计算·aws
玩电脑的辣条哥3 小时前
Python如何播放本地音乐并在web页面播放
开发语言·前端·python
taoqick3 小时前
对PosWiseFFN的改进: MoE、PKM、UltraMem
人工智能·pytorch·深度学习
suibian52353 小时前
AI时代:前端开发的职业发展路径拓宽
前端·人工智能
预测模型的开发与应用研究4 小时前
数据分析的AI+流程(个人经验)
人工智能·数据挖掘·数据分析
源大模型4 小时前
OS-Genesis:基于逆向任务合成的 GUI 代理轨迹自动化生成
人工智能·gpt·智能体