动手学深度学习(Pytorch版)代码实践 -卷积神经网络-30Kaggle竞赛:图片分类

30Kaggle竞赛:图片分类

比赛链接: https://www.kaggle.com/c/classify-leaves

导入包
python 复制代码
import torch
import torchvision
from torch.utils.data import Dataset, DataLoader
from torchvision import transforms
import numpy as np
import pandas as pd
from torch import nn
import matplotlib.pyplot as plt
from PIL import Image
import os
from torch.nn import functional as F
import torch.optim as optim
import liliPytorch as lp
import torchvision.models as models
预处理:数据集分析
python 复制代码
train_path = '../data/classify-leaves/train.csv'
test_path = '../data/classify-leaves/test.csv'
file_path = '../data/classify-leaves/'

# # 读取训练和测试数据
train_data = pd.read_csv(train_path)
test_data = pd.read_csv(test_path)

# 打印数据形状
print(train_data.shape) # (18353, 2)
print(test_data.shape) # (8800, 1)

#生成描述性统计数据
print(train_data.describe())
"""
               image             label
count          18353             18353
unique         18353               176
top     images/0.jpg  maclura_pomifera
freq               1               353
"""

# 查看不同树叶的数量
print(train_data['label'].value_counts())
"""
label
maclura_pomifera            353
ulmus_rubra                 235
prunus_virginiana           223
acer_rubrum                 217
broussonettia_papyrifera    214
                           ... 
cedrus_deodara               58
ailanthus_altissima          58
crataegus_crus-galli         54
evodia_daniellii             53
juniperus_virginiana         51
Name: count, Length: 176, dtype: int64
"""
1.数据处理与加载
python 复制代码
train_path = '../data/classify-leaves/train.csv'
test_path = '../data/classify-leaves/test.csv'
file_path = '../data/classify-leaves/'

# 树叶的名字统计
labels_unique = train_data['label'].unique()
# print(labels_unique)

# 树叶标签的数量
labels_num = len(labels_unique)

# 提取出树叶标签,并排序
leaves_labels = sorted(list(set(train_data['label'])))
# print(leaves_labels)

# 将树叶标签对应数字
labels_to_num = dict(zip(leaves_labels, range(labels_num )))
# print(labels_to_num)

# 将数字对应树叶标签(用于后续预测)
num_to_labels = {value : key for key, value in labels_to_num.items()}
# print(num_to_labels)

class LeavesDataset(Dataset):
    def __init__(self, csv_path, file_path, mode='train', valid_ratio=0.2, resize_height=224, resize_width=224):
        """
        初始化 LeavesDataset 对象。
        参数:
            csv_path (str): 包含图像路径和标签的 CSV 文件路径。
            file_path (str): 图像文件所在目录的路径。
            mode (str, optional): 数据集的模式。可以是 'train', 'valid' 或 'test'。默认值为 'train'。
            valid_ratio (float, optional): 用于验证的数据比例。默认值为 0.2。
            resize_height (int, optional): 调整图像高度的大小。默认值为 224。
            resize_width (int, optional): 调整图像宽度的大小。默认值为 224。
        """
        # 存储图像调整大小的高度和宽度
        self.resize_height = resize_height
        self.resize_width = resize_width
        
        # 存储图像文件路径和模式(train/valid/test)
        self.file_path = file_path
        self.mode = mode
        
        # 读取包含图像路径和标签的 CSV 文件
        self.data_info = pd.read_csv(csv_path, header=0)
        
        # 获取样本总数
        self.data_len = len(self.data_info.index)
        
        # 计算训练集样本数
        self.train_len = int(self.data_len * (1 - valid_ratio))

        # 根据模式处理数据
        if self.mode == 'train':
            # 训练模式下的图像和标签
            self.train_img = np.asarray(self.data_info.iloc[0:self.train_len, 0])
            self.train_label = np.asarray(self.data_info.iloc[0:self.train_len, 1])
            self.image_arr = self.train_img
            self.label_arr = self.train_label
        elif self.mode == 'valid':
            # 验证模式下的图像和标签
            self.valid_img = 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_img
            self.label_arr = self.valid_label
        elif self.mode == 'test':
            # 测试模式下的图像
            self.test_img = np.asarray(self.data_info.iloc[:, 0])
            self.image_arr = self.test_img

        # 获取图像数组的长度
        self.len_image = len(self.image_arr)
        print(f'扫描所有 {mode} 数据,共 {self.len_image} 张图像')

    def __getitem__(self, idx):
        """
        获取指定索引的图像和标签。
        参数: idx (int): 标签文本对应编号的索引
        返回:如果是测试模式,返回图像张量;否则返回图像张量和标签。
        """
        # 打开图像文件
        self.img = Image.open(self.file_path + self.image_arr[idx])
        
        if self.mode == 'train':
            # 训练模式下的数据增强
            trans = transforms.Compose([
                transforms.Resize((self.resize_height, self.resize_width)),
                transforms.RandomHorizontalFlip(p=0.5),
                transforms.RandomVerticalFlip(p=0.5),
                transforms.RandomRotation(degrees=30),
                # transforms.ColorJitter(brightness=0.2, contrast=0.2, saturation=0.2, hue=0.1),
                # transforms.RandomResizedCrop(size=self.resize_height, scale=(0.8, 1.0)),
                transforms.ToTensor(),
                # transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
            ])
            self.img = trans(self.img)
        else:
            # 验证和测试模式下的简单处理
            trans = transforms.Compose([
                transforms.Resize((self.resize_height, self.resize_width)),
                transforms.ToTensor(),
                # transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225])
            ])
            self.img = trans(self.img)
        
        if self.mode == 'test':
            return self.img
        else:
            # 获取标签文本对应的编号
            self.label = labels_to_num[self.label_arr[idx]]
            return self.img, self.label

    def __call__(self, idx):
        """
        使对象可以像函数一样被调用。
        参数:idx (int):标签文本对应编号的索引 
        返回: 调用 __getitem__ 方法并返回结果。
        """
        return self.__getitem__(idx)

    def __len__(self):
        """
        获取数据集的长度。
        返回: 数据集中图像的数量。
        """
        return self.len_image
 
train_dataset = LeavesDataset(train_path, file_path)
valid_dataset = LeavesDataset(train_path, file_path, mode='valid')
test_dataset = LeavesDataset(test_path, file_path, mode='test')
2.模型构建Resnet
python 复制代码
class Residual(nn.Module):
    def __init__(self, input_channels, num_channels, use_1x1conv=False, strides=1):
        super().__init__()
        self.conv1 = nn.Conv2d(input_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(input_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)

b1 = nn.Sequential(
    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)
)

def resnet_block(input_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(input_channels, num_channels, use_1x1conv=True, strides=2))
        else:
            blk.append(Residual(num_channels, num_channels))
    return blk

#ResNet34
# b2 = nn.Sequential(*resnet_block(64, 64, 3, first_block=True))
# b3 = nn.Sequential(*resnet_block(64, 128, 4))
# b4 = nn.Sequential(*resnet_block(128, 256, 6))
# b5 = nn.Sequential(*resnet_block(256, 512, 3))

#ResNet18
b2 = nn.Sequential(*resnet_block(64, 64, 2, first_block=True))
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)),
    nn.Flatten(),
    nn.Linear(512, labels_num)
)
3.模型训练
python 复制代码
def train_ch6(net, train_iter, test_iter, num_epochs, lr, device):
    """
    用GPU训练模型
   	参数:
        net (torch.nn.Module): 要训练的神经网络模型
        train_iter (torch.utils.data.DataLoader): 训练数据加载器
        test_iter (torch.utils.data.DataLoader): 测试数据加载器
        num_epochs (int): 训练的轮数
        lr (float): 学习率
        device (torch.device): 计算设备(CPU或GPU)
    """
    # 初始化模型权重
    def init_weights(m):
        if(type(m) == nn.Linear or type(m) == nn.Conv2d):
            nn.init.xavier_uniform_(m.weight)
    net.apply(init_weights)
    print('training on', device)
    net.to(device)
    
    # 应用初始化权重函数
    # optimizer = torch.optim.SGD(net.parameters(), lr = lr)
    optimizer = torch.optim.Adam(net.parameters(), lr=lr, weight_decay = 0.001)
    # 每5个epoch学习率减少到原来的0.1倍
    # scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.1)  
    loss = nn.CrossEntropyLoss()   # 损失函数,使用交叉熵损失
    animator = lp.Animator(xlabel='epoch', xlim=[1, num_epochs],
                            legend=['train loss', 'train acc', 'test acc'])
    timer, num_batches = lp.Timer(), len(train_iter)

    for epoch in range(num_epochs):
        # 训练损失之和,训练准确率之和,样本数
        metric = lp.Accumulator(3)
        net.train() #训练模式
        for i, (X, y) in enumerate(train_iter):
            timer.start()
            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], lp.accuracy(y_hat, y), X.shape[0]) # 更新指标
            timer.stop()
            train_l = metric[0] / metric[2] # 计算训练损失
            train_acc = metric[1] / metric[2] # 计算训练准确率
            # 每训练完一个批次或每5个批次更新动画
            if (i + 1) % (num_batches // 5) == 0 or i == num_batches - 1:
                animator.add(epoch + (i + 1) / num_batches,
                             (train_l, train_acc, None))
        # 在验证集上计算准确率
        test_acc = lp.evaluate_accuracy_gpu(net, test_iter, device)
        animator.add(epoch + 1, (None, None, test_acc))
        # 打印当前epoch的训练损失,训练准确率和测试准确率
        print(f'loss {train_l:.3f}, train acc {train_acc:.3f}, '
              f'test acc {test_acc:.3f}')
        # scheduler.step()
    animator.show()
    print(f'loss {train_l:.3f}, train acc {train_acc:.3f}, '
          f'test acc {test_acc:.3f}')
    print(f'{metric[2] * num_epochs / timer.sum():.1f} examples/sec '
          f'on {str(device)}')# 打印每秒处理的样本数

# 超参数设置
lr, num_epochs, batch_size = 1e-5, 120, 128

# 数据加载器
train_iter = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=4)
valid_iter = DataLoader(valid_dataset, batch_size=batch_size, shuffle=False, num_workers=4)

train_ch6(net, train_iter, valid_iter, num_epochs, lr, lp.try_gpu())
plt.show()

# 保存模型参数
file_path_module = '../limuPytorch/module/'
torch.save(net.state_dict(), file_path_module + 'classify_leaves.params')
4.训练调参
python 复制代码
resNet-18,num_epochs = 10,lr=1e-4,
loss 2.239, train acc 0.429, test acc 0.149
444.5 examples/sec on cuda:0

resNet-34, num_epochs = 10,lr=1e-4
loss 1.991, train acc 0.443, test acc 0.147
270.7 examples/sec on cuda:0

resNet-34,num_epochs = 50,lr=1e-4,train数据增强,使用Adam
loss 0.281, train acc 0.914, test acc 0.378
244.6 examples/sec on cuda:0

resNet-34,num_epochs = 50,lr=1e-5,train数据增强,使用Adam
loss 0.189, train acc 0.925, test acc 0.398
258.0 examples/sec on cuda:0

resNet-18,num_epochs = 50,lr=1e-4,train数据增强,使用Adam
loss 0.199, train acc 0.955, test acc 0.338
458.0 examples/sec on cuda:0

resNet-18,num_epochs = 50,lr=1e-4,train数据增强,调整数据集比例8:2
数据增强过度导致测试准确率(test accuracy)曲线上下震荡

resNet-18,num_epochs = 50,lr=1e-4,train数据增强,调整数据集比例为8:2
数据增强过度导致测试准确率(test accuracy)曲线上下震荡

resNet-18,num_epochs = 50,lr=1e-4,train数据增强(仅旋转),调整数据集比例为8:2
loss 0.129, train acc 0.966, test acc 0.838
350.7 examples/sec on cuda:0

resNet-18,num_epochs = 50,lr=1e-5,train数据增强,调整数据集比例为8:2
loss 0.808, train acc 0.788, test acc 0.701
420.6 examples/sec on cuda:0

resNet-18,num_epochs = 100,lr=1e-5,train数据增强,调整数据集比例为8:2
loss 0.285, train acc 0.927, test acc 0.825
409.2 examples/sec on cuda:0
5.模型预测
python 复制代码
 def predict(model, data_loader, device):
    """
    使用模型进行预测
    参数:
        model (torch.nn.Module): 要进行预测的模型
        data_loader (torch.utils.data.DataLoader): 数据加载器,用于提供待预测的数据
        device (torch.device): 计算设备(CPU或GPU)

    返回: all_preds (list): 包含所有预测结果的列表
    """
    all_preds = []  # 存储所有预测结果
    model.to(device)  # 将模型移动到指定设备
    model.eval()  # 设置模型为评估模式
    with torch.no_grad():  # 在不需要计算梯度的上下文中进行
        for X in data_loader:  # 遍历数据加载器
            X = X.to(device)  # 将数据移动到指定设备
            outputs = model(X)  # 前向传播,计算模型输出
            _, preds = torch.max(outputs, 1)  # 获取预测结果
            all_preds.extend(preds.cpu().numpy())  # 将预测结果添加到列表中
    return all_preds  # 返回所有预测结果
  
 # 克隆模型
 clone_net = net
 # 加载预训练模型参数
 clone_net.load_state_dict(torch.load(file_path_module + 'classify_leaves.params'))
 # 创建验证集的数据加载器
 valid_iter = DataLoader(test_dataset, batch_size=batch_size, shuffle=False, num_workers=2)
 # 进行预测
 predictions = predict(clone_net, valid_iter, lp.try_gpu())
 # 将预测结果映射到标签
 for i in predictions:
     predictions.append(num_to_labels[int(i)])
 # 读取测试数据
 test_data = pd.read_csv(test_path)
 # 将预测结果添加到测试数据中
 test_data['label'] = pd.Series(predictions)
 # 创建提交文件
 submission = pd.concat([test_data['image'], test_data['label']], axis=2)
 # 保存提交文件
 submission.to_csv(file_path + 'submission.csv', index=False)
7.扩展学习
python 复制代码
# 模型构建
# 加载预训练的ResNet-18模型
#加载一个预训练的ResNet-18模型,这个模型已经在ImageNet数据集上进行了预训练。
#可以利用其提取特征的能力。
pretrained_net = models.resnet18(pretrained=True)

# 克隆预训练的ResNet-18模型,用于分类叶子数据集
classify_leaves_net = pretrained_net

# 修改最后的全连接层,将其输出特征数改为176(有176个类别)
# classify_leaves_net.fc.in_features 获取原始全连接层的输入特征数。
classify_leaves_net.fc = nn.Linear(classify_leaves_net.fc.in_features, 176)

# 使用Xavier均匀分布初始化新的全连接层的权重
nn.init.xavier_uniform_(classify_leaves_net.fc.weight)

# 模型训练部分更改优化器
def train_ch6(net, train_iter, test_iter, num_epochs, lr, device,param_group=True):
    """
    param_group (bool, optional): 是否对参数进行分组设置不同的学习率。默认值为True
    """
    # optimizer = torch.optim.SGD(net.parameters(), lr = lr)
    # optimizer = torch.optim.Adam(net.parameters(), lr=lr, weight_decay = 0.001)
 	if param_group:
        # 如果参数分组设置为True,分离出最后一层全连接层的参数
        # 列表params_1x,包含除最后一层全连接层外的所有参数。
        params_1x = [param for name, param in net.named_parameters()
                     if name not in ["fc.weight", "fc.bias"]]
        optimizer = torch.optim.Adam([
            {'params': params_1x},  # 其他层的参数使用默认学习率
            {'params': net.fc.parameters(), 'lr': lr * 10}  # 全连接层的参数使用更高的学习率
        ], lr=lr, weight_decay=0.001)
   	else:
        # 如果参数分组设置为False,所有参数使用相同的学习率
        optimizer = torch.optim.Adam(net.parameters(), lr=lr, weight_decay=0.001)
相关推荐
DreamLife☼6 小时前
OpenBCI-脑机接口在康复医疗中的应用
深度学习·cnn·脑电·康复·fes·openbci·外骨骼
硅谷秋水6 小时前
面向长上下文自动驾驶的规划对齐Token压缩
人工智能·深度学习·机器学习·计算机视觉·自动驾驶
郭泽斌之心6 小时前
MQL5 EA 怎么和外部程序通信?文件三件套协议:参数热更新不重启、状态心跳、远程触发
人工智能·经验分享·深度学习·ea·fay数字人·easydeal
AI人工智能+7 小时前
智能文档抽取系统以专业的文档解析底座和大模型智能语义理解能力为核心,洞察文档的语义内涵与逻辑结构
深度学习·自然语言处理·ocr·文档抽取
nap-joker7 小时前
用于转录组信息精确肿瘤学和药物机制分析的多模态可解释深度学习
人工智能·深度学习·药物敏感性·多层级生物网络·细胞异质性·可解释性多模态
YOLO数据集集合8 小时前
无人机山地灾害巡检数据集 | 滑坡多区域实例分割 遥感影像解译 地质灾害预警深度学习数据10296期
人工智能·深度学习·目标检测·计算机视觉·无人机
手写码匠9 小时前
手写 GraphRAG:从零实现图增强检索增强生成系统
人工智能·深度学习·算法·aigc
装不满的克莱因瓶9 小时前
【自动驾驶领域】学习 Cityscapes 数据集——城市街景语义理解的标准基准
人工智能·pytorch·python·深度学习·学习·机器学习·自动驾驶
清辞85310 小时前
产品经理需求推进流程
大数据·深度学习·学习·产品经理
一一哥Sun11 小时前
第06课:Transformer与注意力机制——大模型背后的秘密武器
人工智能·深度学习·transformer