这个项目来自kaggle_dog_breed_indentification
(打不开的用一些大家都知道的方法打开)
可参考部分总共包括:
- 展示数据的基本方法
- 处理数据样本的基本步骤
- 读取csv文件夹自定义dataset
- 如果数据与csv文件不对应的处理思路(仅供参考)
- 自定义训练函数
- 自定义模型函数(然而没有,直接微调的)
- 可视化输出结果
- 各种线(大家都知道的线)
- 可视化训练结果(或许能更为直观的理解)
1. 准备工作
1.1. 加载库
import os
import numpy as np
import torchvision
import time
import pandas as pd
import matplotlib.pyplot as plt
from PIL import Image
import torchvision.datasets as datasets # 加载pytorch中内置的常用数据集
import torchvision.transforms as transforms
import torch
import torch.nn as nn
import torch.nn.functional as F
import requests # 发送网络请求
from sklearn.metrics import confusion_matrix, classification_report # 加载sklearn中的混淆矩阵
import seaborn as sn # 绘图函数库
import pandas as pd
torch.manual_seed(1) # 设置随机种子始终为1
from sklearn import preprocessing # 加载sklearn的数据预处理模块,可以提供诸如数据归一化之类的数据预处理操作
from torch.utils.data import DataLoader, Dataset, Subset
mps_device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
import random
1.2 下载数据集
有几种做法
- 在kaggle上下载并解压在本地运行,如果这样做的话这一步不需要
- 直接在kaggle上运行。这是最方便的,将不需要下载kaggle上自带的数据集(自己上传依旧慢的离谱)
- 下载后,上传到别的云上运行(这一步很蠢),至少kaggle上的数据集不需要这样做
对于第三种方法提供一个解决思路,以在colab上运行为例
- 下载你在kaggle上的acount中的json文件,必须登录才能使用kaggle API下载自带的数据集,下载这个文件相当于授权你的kaggle账号
- 上传json文件到colab中
from google.colab import files
# 上传 kaggle.json 文件
files.upload()
- 移动json文件到指定位置
!mkdir -p ~/.kaggle
!mv kaggle.json ~/.kaggle/
!chmod 600 ~/.kaggle/kaggle.json
- 使用kaggle API下载数据集(没有kaggle API的pip一下,在云内!xxx执行命令行命令)
!kaggle competitions download -c dog-breed-identification
- 解压数据集
import zipfile
import os
# 指定zip文件路径
zip_file_path = '/content/dog-breed-identification.zip' # 替换成你的zip文件路径
# 指定解压缩目标文件夹
extract_folder = '/content' # 替换成你的解压缩目标文件夹路径
# 创建解压缩目标文件夹(如果不存在)
os.makedirs(extract_folder, exist_ok=True)
# 打开zip文件
with zipfile.ZipFile(zip_file_path, 'r') as zip_ref:
# 解压缩到指定目标文件夹
zip_ref.extractall(extract_folder)
结果如下:
1.3 读取数据并查看其格式、样本、数量之类的信息
加载csv文件
dog_label_dframe = pd.read_csv('/content/drive/MyDrive/dog breed identification/data/dog-breed-identification.zip (Unzipped Files)/labels.csv')
dog_label_dframe
分离图像路径及标签
对于csv文件的处理类比数组切片,这里有个概念得明确,得到的东西不是深度学习框架需要的类型,例如,不是pytorch需要的tensor类型。尤其是这样处理的结果,并不是最终需要的类型,尤其是label的类型
images = dog_label_dframe.iloc[:,0]
labels = dog_label_dframe.iloc[:,-1]
len(images)
读取文件夹文件
import os
train_folder_path = '/content/train'
test_folder_path = '/content/test'
# 获取文件夹中所有文件的列表
train_files = os.listdir(train_folder_path)
test_files = os.listdir(test_folder_path)
# 计算文件夹中文件的数量
len(train_files),len(test_files)
假如,不是使用kaggleAPI下载数据,在最后文件夹中得到的文件与原csv文件无法一一对应,怎么办。闲着无聊处理了一下这个问题,没什么意义,但是也许有一点用
方法: 根据文件夹重写一个csv文件
值得注意的是,csv文件中不一定写上了文件的后缀,也就是
xxxxxxx 艾蕾之类的信息
但是读取文件的时候,是会读取到文件的全称的
xxxxxxx.jpg 艾蕾
这样就导致了下面的判断出错
重写csv文件
#读取 CSV 文件
csv_file_path = dog_label_dframe # 替换成你的 CSV 文件路径
df = csv_file_path
# 图片文件夹路径
train_folder_path = '持有的图片路径' # 替换成你的图片文件夹路径
# 获取图片文件夹中的所有图片文件
train_images = [f for f in os.listdir(train_folder_path) if os.path.isfile(os.path.join(train_folder_path, f))]
test_images = [a for a in os.listdir(test_folder_path) if os.path.isfile(os.path.join(test_folder_path, a))]
all_images = train_images + test_images
print(test_images)
# df.iloc[:,0].values.astype(str)[0] += '.jpg' 错误的修改字符串的方式,因为python中其不可变
# new = '{}.jpg'.format(df.iloc[:,0].values.astype(str)[0])
new_images = []
for i in range(len(all_images)):
new_images.append(all_images[i][:-4])
print(len(new_images))
print(new_images[0])
filtered_df = df[pd.Series(df.iloc[:,0].values.astype(str)).isin(new_images)]
print(filtered_df.shape)
print(df.iloc[:,0].values.shape)
# 保存筛选后的数据到新的 CSV 文件
filtered_csv_file_path = '新的csv文件路径'
filtered_df.to_csv(filtered_csv_file_path, index=False)
获取、处理种类信息
# set:python中的无重复元素数组结构
# list:转换为列表
labels = sorted(list(set(dog_label_dframe['breed'])))
n_classes = len(labels)
print(n_classes)
使用字典将label规范为数字类型
class_to_num = dict(zip(labels,range(n_classes)))
list(class_to_num.keys())[:10],list(class_to_num.values())[:10]
2. 自定义dataset
class DogDataset(Dataset):
def __init__(self, df, file_path, transform = None):
# 一个自定义dataset究竟需要什么属性
# 以下为一些基本属性
self.df = df # csv文件读取后的dataFrame对象
self.transform = transform # 图像增广
self.images = np.asarray(df.iloc[:, 0])
self.labels = np.asarray(df.iloc[:, 1])
self.data_len = len(self.images)
self.file_path = file_path
self.real_len = self.data_len
print('Finished reading the set of dog Dataset ({} samples found)'.format(self.real_len))
def __getitem__(self, index):
# 获取dataset成员的方法
img_name = self.images[index]
label = self.labels[index]
# 注意,这一步是必须的,因为深度学习框架不接收别的类型
# 一开始直接加载pytorch框架预定义的dataset就会遗漏这一点
# 注意就行,这样并不是什么好的处理方法
label_tensor = torch.tensor(class_to_num[label])
img = Image.open(self.file_path + img_name + '.jpg')
img = self.transform(img)
return img, label_tensor
def __len__(self):
return self.real_len
3. 定义图像增广
这里有个问题,并不是所有的数据集,图像都有相同的大小,因此必须保证传入框架内的图像具有相同的大小。
观察样本大小
img1 = Image.open('/content/drive/MyDrive/dog breed identification/data/dog-breed-identification.zip (Unzipped Files)/train/0a5f744c5077ad8f8d580081ba599ff5.jpg')
print(img1.size)
img2 = Image.open('/content/drive/MyDrive/dog breed identification/data/dog-breed-identification.zip (Unzipped Files)/train/0a6c192b96e55e2ca37318919b1ffae6.jpg')
print(img2.size)
直接ReSize不是一个好的手段,可以放大后裁切中间部分,或者填充后裁切中间部分,又或者随机裁切不同比例后放缩到相同大小
# 定义图像增广
composed_train = transforms.Compose([
transforms.RandomResizedCrop(224, scale=(0.08, 1.0),
ratio=(3.0/4.0, 4.0/3.0)),
transforms.RandomRotation(20),# 将从数据集中随机抽取图像旋转20degree
# 亮度、对比度、透明度的随机取值,范围[0.1-1.1]之间
transforms.ColorJitter(brightness = 0.1,contrast = 0.1,saturation = 0.1),
# 随机锐度,sharpness_factor为锐度系数,p为应用随机锐度的概率
transforms.RandomAdjustSharpness(sharpness_factor = 2,p = 0.1),
transforms.ToTensor(),
# 随机擦除:
# p:应用随机擦除的概率
# scale:随机擦除的面积
# value:擦除后的填充值,1为默认值,默认为白色(灰色)
# inplace:是否取代原始图像,False将返回一个新的图像副本
transforms.RandomErasing(p=0.75,scale=(0.02, 0.1),value=1.0, inplace=False)
])
# Test
composed_test = transforms.Compose([
torchvision.transforms.Resize(256),
# 从图像中心裁切224x224大小的图片
torchvision.transforms.CenterCrop(224),
transforms.ToTensor()])
4. 声明dataset对象
csv_dataframe = pd.read_csv('/content/labels.csv')
train_path = '/content/train/'
val_path = '/content/train/'
train_dataset = DogDataset(csv_dataframe,train_path,transform=composed_train)
val_dataset = DogDataset(csv_dataframe,val_path,transform=composed_test)
print(train_dataset)
print(val_dataset)
5. 观察dataset中的对象(非必须)
def show_data(img):
try:
plt.imshow(img[0])
except Exception as e:
print(e)
# 注意:plt.imshow需要接受[高,宽,通道数]形状的图像,而一般默认图像转换为tensor后是[channels,height,width]
# 这里之所以不需要,是因为dataset中使用Image类加载了图像
plt.imshow(img[0].permute(1,2,0))
plt.title('y:' + list(class_to_num.keys())[img[1]])
plt.show()
# 将图像转换为numpy类型,因为张量类型并不能在matplotlib中使用
def im_convert(tensor):
img = tensor.cpu().clone().detach().numpy()
# numpy类型的维度转换
img = img.transpose(1,2,0)
img = img.clip(0,1) # 将图像像素值限定在[0,1]之间
return img
展示图像样本
show_data(train_dataset[8])
6. 创建dataloader
定义给定长度不是好的选择,可以定义比例进行划分,但是10222不能很好的整除划分,直接给定算了
划分训练验证集
train_len = 7222
val_len = 3000
train_set, _ = torch.utils.data.random_split(train_dataset, [train_len, val_len],
generator=torch.Generator().manual_seed(10))
_, val_set = torch.utils.data.random_split(val_dataset, [train_len, val_len],
generator=torch.Generator().manual_seed(10))
创建dataloader
train_loader = torch.utils.data.DataLoader(
dataset=train_dataset,
batch_size=256,
shuffle=False,
num_workers = 1
)
val_loader = torch.utils.data.DataLoader(
dataset=val_dataset,
batch_size=256,
shuffle=False,
num_workers = 1
)
这里有关batch_size需要说明一下,这个东西不是越大越好,也不是想多大就多大(即使,使用BN可以理论上要多大都行)。而是合理范围内,尽可能大的batch_size可以给到尽可能正确的梯度方向,可以更好的利用GPU显存,提升训练速度,降低循环数。但是,这依旧是一个超参数,需要一定的实验保证最适合的数值。
有个文章或许可以解释这个问题:
batch_size详解
建议:使用2的幂次设置batch_size对于计算速度有着微乎其微的提升(你就说提没提升吧)。建议从128开始增减
7. 定义训练函数
这个训练函数包含了训练中的绝大多数基本操作,基本上在上面改就行
import time
mps_device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
def train(model, train_loader, val_loader, optimizer, num_epoch = 20):
# 一些全局变量
# 基本上都是用于存储批次损失和精度的信息
N_test = len(val_dataset)
accuracy_list = []
train_loss_list = []
model = model.to(mps_device)
train_cost_list = []
val_cost_list = []
for epoch in range(num_epoch):
t1 = time.perf_counter()
train_cost = 0
for x,y in train_loader:
x = x.to(mps_device)
y = y.to(mps_device)
model.train()
optimizer.zero_grad()
z = model(x)
loss = criterion(z,y)
loss.backward()
optimizer.step()
train_cost += loss.item() # 累加所有批次的损失
# 至此完成了一个循环的训练
train_cost = train_cost/len(train_loader) # 计算批次平均损失
train_cost_list.append(train_cost) # 保存批次平均损失
# 开始评估
correct = 0
val_cost = 0
for batch,data in enumerate(val_loader):
# 评估模式
x_test,y_test = data
model.eval()
x_test = x_test.to(mps_device)
y_test = y_test.to(mps_device)
z = model(x_test)
val_loss = criterion(z, y_test)
_, yhat = torch.max(z.data,dim=1)
# 预测正确数量
correct += (yhat == y_test).sum().item()
val_cost += val_loss.item()
rate = (batch + 1) / len(val_loader)
# * 表示已经完成的部分
# . 表示未完成的部分
a = "*" * int(rate * 50)
b = "." * int((1 - rate) * 50)
# 可视化loader中的训练进度
print("\rtrain loss: {:^3.0f}%[{}->{}]{:.3f}".format(int(rate * 100), a, b, val_loss), end="")
print()
print('Time cost: ', time.perf_counter() - t1)
# 计算每个批次的验证平均误差
val_cost = val_cost / len(val_loader)
val_cost_list.append(val_cost)
accuracy = correct/N_test
accuracy_list.append(accuracy)
print("--> Epoch Number : {}".format(epoch + 1),
" | Training Loss : {}".format(round(train_cost,4)),
" | Validation Loss : {}".format(round(val_cost,4)),
" | Validation Accuracy : {}%".format(round(accuracy * 100, 2)))
return accuracy_list, train_cost_list, val_cost_list
8. 定义训练模型
(开始偷懒)其实水论文就逮着这一步水了,前面那些再复杂也就预处理一下各自的图像有点恶心而已。
net = torchvision.models.resnet18(pretrained=True)
net.fc = nn.Linear(net.fc.in_features, 120)
# 初始化输出层的参数,使用简单一点的分布输出也行,在这里影响不大
nn.init.xavier_uniform_(net.fc.weight)
criterion = nn.CrossEntropyLoss()
learning_rate = 0.1 # 可以设置学习率衰竭,懒得弄了,这里没有什么意义
# 添加动量计算随机梯度下降
optimizer = torch.optim.SGD(net.parameters(), lr = learning_rate, momentum = 0.2)
accuracy_list_normal, train_cost_list, val_cost_list=train(model=net,train_loader=train_loader,val_loader=val_loader,optimizer=optimizer)
9. 训练结果
挂了40分钟吧
存储训练参数
torch.save(net.state_dict(),'/content/drive/MyDrive/dog breed identification/' + 'res18_seed10')
10. 评估训练结果
训练测试损失
观察结果是否平稳下降,可靠,不是随机出现的一些错误值
fig, ax1 = plt.subplots()
color1 = 'red'
ax1.plot(train_cost_list,color = color1)
ax1.set_xlabel('epoch',color = 'black')
ax1.set_ylabel('loss',color = color1)
ax1.tick_params(axis='y', labelcolor = color1)
ax2 = ax1.twinx()
color2 = 'blue'
ax2.set_ylabel('accuracy', color=color2)
ax2.plot( accuracy_list_normal, color=color2)
ax2.tick_params(axis='y', labelcolor=color2)
ax2.tick_params(axis='y', labelcolor = color2)
fig.tight_layout()
可以看到20个循环内结果基本可靠,不是那么的平滑,但是整体呈现缓慢提升趋势
显示测试和训练损失
保证训练和测试损失必须同时下降,防止过拟合出现
# 不需要显示两个子图,所以不需要返回轴线对象进行操作了
plt.plot(train_cost_list, 'r', label='Training Loss')
plt.plot(val_cost_list, 'g', label='Validation Loss')
plt.xlabel("Epoch")
plt.title("Loss")
plt.legend()
可以看到训练和测试损失的同步下降,确定为没有过拟合
11. 可视化训练结果
说实话,我一开始学深度学习就在想,为什么得出来的是一条条破线,多么的不直观,但是又一直找不到什么靠谱的显示方法来让训练结果直观一些,提供一些代码实现这个效果
# 反转dict的key、value值
num_to_class = {v:k for k,v in class_to_num.items()}
直接打印错误预测样本
# 打印错误样本
count = 0
i = 0
for x, y in torch.utils.data.DataLoader(dataset=val_dataset, batch_size=1):
x = x.to(mps_device)
y = y.to(mps_device)
z = net(x)
_, yhat = torch.max(z, 1)
if yhat != y:
show_data(val_dataset[i])
plt.figure(figsize=(10,10))
plt.show()
print('yhat:',num_to_class[i])
count += 1
if count >= 3:
break
i += 1
展示更多错误训练结果
同时显示标签值和预测值,正确为绿色,错误为红色
data_iterable = iter(val_loader)
images, labels = next(data_iterable)
images = images.to(mps_device)
labels = labels.to(mps_device)
output = net(images)
_, preds = torch.max(output, 1)
# 这是显示展示子图的大小
fig = plt.figure(figsize=(25,10))
for idx in np.arange(20):
ax = fig.add_subplot(4, 5, idx+1, xticks=[], yticks=[])
img_idx = random.randint(1, 100)
plt.imshow(im_convert(images[img_idx]))
ax.set_title("{} ({})".format(str([num_to_class[preds[img_idx].item()]]), str(num_to_class[labels[img_idx].item
()])), color=("green" if preds[img_idx]==labels[img_idx] else "red"))
12. 结束
不是什么高深东西,仅供新得不能再新的新手练手,希望我的解释能对各位理解这样一个简单但基本完整的深度学习项目有一点帮助