一、任务
任务是预测叶子图像的类别。该数据集包含 176 个类别、18353 张训练图像、8800 张测试图像。每个类别至少有 50 张图像用于训练。
二、数据集分析
从kaggle官网下载来的数据信息有train.csv,test.csv,以及images文件夹
train.csv包括18353张图片路径以及对应标签
test.csv包含8800张图片路径
images文件夹下是所有图片.jpg
三、导入包
1.pandas用于处理csv文件
2.Dataset,DataLoader用于处理自定义数据集
3.transforms是图片增强(待改进,比如用cutmix)
4.models提供了resnet等常用的网络结构,可直接调用
5.tqdm 进度条可视化
6.seaborn 非常丰富的可视化工具
import torch
import torch.nn as nn
import pandas as pd
import numpy as np
from torch.utils.data import Dataset,DataLoader
from torchvision import transforms
from PIL import Image
import os
import matplotlib.pyplot as plt
import torchvision.models as models
from tqdm import tqdm
import seaborn as sns
四、观察数据集
# ./代表当前目录
# ../代表上级目录
labels_dataframe = pd.read_csv('./train.csv',header=0)
labels_dataframe.head(5)
#使用describe()作数据统计
labels_dataframe.describe()
使用pd.describe()函数
生成描述性统计数据,统计数据集的集中趋势,分散和行列的分布情况,不包括 NaN值。可以看到训练集总共有18353张图片,标签有176类。
# 显示出每种类别的叶子有多少个图 min:51 max:353
def barw(ax):
for p in ax.patches:
val = p.get_width() #height of the bar
x = p.get_x()+ p.get_width() # x- position
y = p.get_y() + p.get_height()/2 #y-position
ax.annotate(round(val,2),(x,y))
plt.figure(figsize = (15,30))
# sns.countplot() 用于画类别特征的频数条形图
#(x轴是count数,y轴是类别。)
ax0 =sns.countplot(y=labels_dataframe['label'],order=labels_dataframe['label'].value_counts().index)
barw(ax0)
plt.show()
把label标签按字母排个序,这里仅显示前10个。
先用set()
对label去重,然后转成list
再排序
leaves_labels = sorted(list(set(labels_dataframe['label'])))
n_class = len(leaves_labels)
print(n_class)
leaves_labels[:10]
编写字典 {label:num}
num用range()
生成
先用zip()
捆绑label:num键值对,再用dict()
生成字典
class_to_num = dict(zip(leaves_labels,range(n_class)))
class_to_num
再转换回来,方便最后预测的时候使用
items()
同时返回字典键值对
num_to_class = {v : k for k ,v in class_to_num.items()}
五、处理数据集 (使用class)
现在已经制作了种类字典信息,但数据集还没有划分训练集,测试集以及验证集。接下来的代码要逐步弄清楚
我会分解LeavesData类的函数并解析代码
1.先声明类并继承Dataset
# 继承pytorch的dataset,手动制作自己的数据集
class LeavesData(Dataset):
2.init()
init()会根据mode
决定生成是“训练数据集”还是“验证数据集”还是“测试数据集”,三者合一
参数
def __init__(self,csv_path,file_path,mode='train',valid_ratio=0.2,resize_height=256,resize_width=256):
self.resize_height = resize_height
self.resize_width = resize_width
self.file_path = file_path
self.mode = mode
# 读取 csv 文件
# 利用pandas读取csv文件
self.data_info = pd.read_csv(csv_path,header=None) #header=None是去掉表头部分
# 计算length
self.data_len = len(self.data_info.index) - 1
#训练集长度 = 0.8 倍数据集
#该长度用于后面iloc切数据
self.train_len = int(self.data_len * (1 - valid_ratio))
参数解析:
csv_path
是表格文件路径,目的是得到图片路径字符串
file_path
是图片真正路径,相当于拿到真实图片
mode
决定是“训练数据集”还是“验证数据集”还是“测试数据集”
valid_ratio
划分验证集占训练集的比例
resize_height
,resize_width
规范化图片尺寸
利用read_csv
获取表格,去掉表头,因为第一行不是数据
根据mode生成对应的数据集
这里为了统一描述三种数据集的image和label(测试集没有label),定义了image_arr
与label_arr
mode == ‘train’
if mode == 'train':
#第一步,取图片和标签
# train_image 第一列包含图像文件的名称
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
iloc()
是切片函数,先切行再切列
asarray()
将csv变成ndarray
[1:self.train_len]
是训练集长度
mode == ‘valid’
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
[self.train_len:]
是测试集长度
mode == ‘test’
测试集没有label只有image
elif mode == 'test':
self.test_image = np.asarray(self.data_info.iloc[1:,0])
self.image_arr = self.test_image
打印
这里定义了real_len
表示数据集长度
self.real_len = len(self.image_arr)
print('Finished reading the {} set of Leaves Dataset ({} samples found)'.format(mode, self.real_len))
3、getitem()
关于__getitem__用法的详解,通俗点讲就是可以用索引访问元素,这里return
每一个index对应的image
数据(图片增强处理)和对应的label
读取图像
single_image_name
从 image_arr
(保存的是数据集图片路径)中得到index对应图像的路径名
img_as_img
通过Image.open()
读取图像
def __getitem__(self,index):
# 从 image_arr中得到索引对应的文件名
single_image_name = self.image_arr[index]
# 读取图像文件
img_as_img = Image.open(self.file_path + single_image_name)
图片增强
通常训练集需要做图像增强处理,而验证集和测试集不需要
transforms.Compose([])
是对图片进行变换操作
Resize()
调整图像尺寸
RandomHorizontalFlip()
随机水平翻转,图像增强的操作之一
ToTensor()
,转化成张量形式
#设置好需要转换的变量,还可以包括一系列的nomarlize等等操作
if self.mode =='train':
transform = transforms.Compose([
transforms.Resize((224,224)),
#train_set 作简单的数据增强
transforms.RandomHorizontalFlip(p=0.5), #随机水平翻转 选择一个概率
transforms.ToTensor()
])
else:
# valid和test不做数据增强
transform = transforms.Compose([
transforms.Resize((224, 224)),
transforms.ToTensor()
])
img_as_img = transform(img_as_img)
if self.mode == 'test':
return img_as_img
else:
# 得到图像的 string label
label = self.label_arr[index]
# number label
number_label = class_to_num[label]
return img_as_img,number_label #返回每一个index对应的图片数据和对应的label
4、len():
return self.real_len
5、调用LeavesData类生成数据集
定义一下不同数据集的csv_path
,并通过更改mode
修改数据集类的实例对象
可以看到训练集有14681张图片
train_path = './train.csv'
test_path = './test.csv'
img_path = './'# csv文件中路径已经包含images了,因此这里只到当前目录
train_dataset = LeavesData(train_path,img_path,mode='train')
val_dataset = LeavesData(train_path,img_path,mode='valid')
test_dataset = LeavesData(test_path,img_path,mode='test')
print(train_dataset)
print(val_dataset)
print(test_dataset
Finished reading the train set of Leaves Dataset (14681 samples found)
Finished reading the valid set of Leaves Dataset (3672 samples found)
Finished reading the test set of Leaves Dataset (8800 samples found)
<main.LeavesData object at 0x00000163C45E5A60>
<main.LeavesData object at 0x00000163C45E5AC0>
<main.LeavesData object at 0x00000163C45E5730>
6、定义DataLoader用于训练模型
调用torch.utils.data.DataLoader
dataset
:数据集名称
batch_size
:批量大小
shuffle
:是否打乱数据
num_workers
:经验设置值是自己电脑/服务器的CPU核心数
# 定义data loader
train_loader = torch.utils.data.DataLoader(
dataset=train_dataset,
batch_size=8,
shuffle=False,
num_workers=5)
val_loader = torch.utils.data.DataLoader(
dataset=val_dataset,
batch_size=8,
shuffle=False,
num_workers=5)
test_loader = torch.utils.data.DataLoader(
dataset=test_dataset,
batch_size=8,
shuffle=False,
num_workers=5)
六、定义模型
使用GPU运行
torch.cuda.is_available()
验证GPU是否可用
# 看一下是在cpu还是GPU上
def get_device():
return 'cuda' if torch.cuda.is_available() else 'cpu'
device = get_device()
print(device)
cuda
冻结模型前面的层
只训练顶层的模型参数
精确冻结预训练模型某些层
def set_parameter_requires_grad(model, feature_extracting):
if feature_extracting:
model = model
for param in model.parameters():
param.requires_grad = False
ResNet34预训练模型
num_classes
是输出层维度,在该项目应为176
fc
是full connect全连接层
fc.in_features
参考该链接中 二、
def res_model(num_classes, feature_extract = False, use_pretrained=True):
model_ft = models.resnet34(pretrained=use_pretrained)
set_parameter_requires_grad(model_ft, feature_extract)
num_ftrs = model_ft.fc.in_features
model_ft.fc = nn.Sequential(nn.Linear(num_ftrs, num_classes))
return model_ft
定义超参数
weight_decay
权重衰减【防止过拟合】
# 超参数
learning_rate = 3e-4
weight_decay = 1e-3
num_epoch = 50
model_path = './pre_res_model.ckpt'
把模型放在GPU上
model = res_model(176)# 176种叶子
model = model.to(device)
model.device = device
定义loss以及optim
criterion = nn.CrossEntropyLoss()
# weight_decay 权重衰减
optimizer = torch.optim.Adam(model.parameters(),lr = learning_rate,weight_decay=weight_decay)
训练网络
下列代码逻辑务必熟练掌握
1.请始终记得DataLoader迭代器返回的是什么?
2.数据要先放在GPU再使用
3.forward() —> loss() —> zero_grad() —>backward() —> step()
4.loss和acc如何直观打印出来?
5.验证集和测试集要固定模型参数with torch.no_grad(),因为不能再训练。model.train()和model.eval()的区别主要在有无dropout
与BN
6.model.state_dict()
模型一旦进化就会被保存,保存的是模型参数
n_epochs = num_epoch
best_acc = 0.0 # 判断模型是否进化
# 训练50轮
for epoch in range(n_epochs):
# ---------- Training ----------
# Make sure the model is in train mode before training.
model.train()
train_loss = []
train_accs = []
# 迭代batch训练
for batch in tqdm(train_loader): # 进度条可视化库
imgs, labels = batch
# 数据要放在GPU上
imgs = imgs.to(device)
labels = labels.to(device)
imgs_hat = model(imgs)
loss = criterion(imgs_hat,labels)
optimizer.zero_grad()
loss.backard()
optimizer.step()
# dim = -1 表示最里层
acc = (imgs_hat.argmax(dim=-1) == labels).float().mean()
train_loss.append(loss.item())
train_accs.append(acc)
train_loss = sum(train_loss) / len(train_loss)
train_acc = sum(train_accs) / len(train_accs)
print(f"[ Train | {epoch+1:03d}/{n_epochs:03d}] loss = {train_loss:.5f}, acc = {train_acc:.5f}")
# ---------- Validation ----------
model.eval()
valid_loss = []
valid_accs = []
for batch in tqdm(val_loader):
imgs, labels = batch
# 验证集不需要更新参数
with torch.no_grad():
imgs_hat = model(imgs.to(device))
loss = criterion(imgs_hat,labels.to(device))
acc = (imgs_hat.argmax(dim=-1) == labels.to(device)).float().mean()
valid_loss.append(loss.item())
valid_accs.append(acc)
valid_loss = sum(valid_loss) / len(valid_loss)
valid_accs = sum(valid_accs) / len(valid_accs)
print(f"[ Valid | {epoch + 1:03d}/{n_epochs:03d} ] loss = {valid_loss:.5f}, acc = {valid_acc:.5f}")
# if the model improves, save a checkpoint at this epoch
if valid_accs > best_acc:
best_acc = valid_accs
torch.save(model.state_dict(),model_path)
print('saving model with acc {:.3f}'.format(best_acc))
七、模型评估(预测数据)
导入训练好的模型
手法:先实例化一个model对象,再把训练好的参数放进去
model = res_model(176)
model = model.to(device)
model.load_state_dict(torch.load(model_path))
model.eval()
预测
saveFileName = './submission.csv'
predictions = []
for batch in tqdm(test_loader):
imgs = batch
with torch.no_grad():
imgs_hat = model(imgs.to(device))
predictions.extend(imgs_hat.argmax(dim=-1).cpu().numpy().tolist())
preds = []
for i in predictions:
preds.append(num_to_class[i])
test_data = pd.read_csv(test_path)
test_data['label'] = pd.Series(preds)
submission = pd.concat([test_data['image'], test_data['label']], axis=1)
submission.to_csv(saveFileName, index=False)
print("Done!!!!!!!!!!!!!!!!!!!!!!!!!!!")
八、参考资料
【Kaggle竞赛树叶分类Baseline】上万片树叶分为一百七十六类
nekokiku大佬的baseline
小白一枚,好多手法和技巧不熟练。加油!!!