需求
- 项目资料集为网络上收集到的食物照片,总共11类:
- Bread
- Dairy product
- Dessert
- Egg
- Fried food
- Meat
- Noodles/Pasta
- Rice
- Seafood
- Soup
- Vegetable/Fruit
- 数据集链接:https://reurl.cc/3DLavL
Kaggle
- 网址:https://www.kaggle.com/c/ml2020spring-hw3
- 他们在kaggle上传了数据集,这样就好操作多了,一个Notebook基本都能搞定的。
Notebook
Import the data
import numpy as np
import pandas as pd
import os
import cv2
import torch
import torch.nn as nn
import torchvision.transforms as transforms
import pandas as pd
from torch.utils.data import DataLoader, Dataset
import matplotlib.pyplot as plt
import time
file_inform_path = ''
for dirname, _, filenames in os.walk('/kaggle/input'):
for filename in filenames:
print(os.path.join(dirname, filename))
if file_inform_path == '':
file_inform_path = os.path.join(dirname, filename)
获取一个样例文件作参考
img = cv2.imread(file_inform_path, 1)
plt.imshow(img)
sp = img.shape
print('width:%d height:%d number:%d'%(sp[0],sp[1],sp[2]))
将文件读取到变量中
def readfile(path, label):
image_dir = sorted(os.listdir(path))
x = np.zeros((len(image_dir), 128, 128, 3), dtype=np.uint8)
y = np.zeros((len(image_dir)), dtype=np.uint8)
for i, file in enumerate(image_dir):
img = cv2.imread(os.path.join(path, file))
x[i, :, :] = cv2.resize(img,(128, 128))
if label:
y[i] = int(file.split("_")[0])
if label:
return x, y
else:
return x
workspace_dir = '/kaggle/input/ml2020spring-hw3/food-11'
print("Reading data")
train_x, train_y = readfile(os.path.join(workspace_dir, "training"), True)
print("Size of training data = {}".format(len(train_x)))
val_x, val_y = readfile(os.path.join(workspace_dir, "validation"), True)
print("Size of validation data = {}".format(len(val_x)))
test_x = readfile(os.path.join(workspace_dir, "testing"), False)
print("Size of Testing data = {}".format(len(test_x)))
数据预处理
训练集先做数据增强
train_transform = transforms.Compose([
transforms.ToPILImage(),
transforms.RandomHorizontalFlip(),
transforms.RandomRotation(15),
transforms.ToTensor(),
])
- 使用
ToPILImage
可以将shape为(H,W,C)
的numpy.ndarray
转化为PIL.Image
。
- 转换为的好处是可以做各种变换
测试集不需要做数据增强
test_transform = transforms.Compose([
transforms.ToPILImage(),
transforms.ToTensor(),
])
Dataset是Pytorch中用来表示数据集的类
class ImgDataset(Dataset):
def __init__(self, x, y=None, transform=None):
self.x = x
self.y = y
if y is not None:
self.y = torch.LongTensor(y)
self.transform = transform
def __len__(self):
return len(self.x)
def __getitem__(self, index):
X = self.x[index]
if self.transform is not None:
X = self.transform(X)
if self.y is not None:
Y = self.y[index]
return X, Y
else:
return X
- 这边我多话详细介绍一下pytorch数据集的整个情况吧。
- 所谓数据集,其实就是一个负责处理索引(index)到样本(sample)映射的一个类。
- pytorch提供两种数据集: Map式数据集和Iterable式数据集。
Map式数据集
- 一个Map式的数据集必须要重写getitem(self, index),len(self) 两个内建方法,用来表示从索引到样本的映射(Map)。
- 这样一个数据集dataset,举个例子,当使用dataset[idx]命令时,可以在你的硬盘中读取你的数据集中第idx张图片以及其标签(如果有的话);len(dataset)则会返回这个数据集的容量。
- 自定义类大致是这样的:
class CustomDataset(data.Dataset):
def __init__(self):
pass
def __getitem__(self, index):
pass
def __len__(self):
return 0
- Iterable式数据库也有,而且现在也算流行。
- 上面介绍dataset的引用自这篇文章:
dataset介绍以及源码案例
Dataloader读入数据内容
- 一般来说pytorch的数据加载到模型的顺序是这样的
- 创建一个
Dataset
对象
- 创建一个
DataLoader
对象
- 循环这个
DataLoader
对象,将img,label加载到模型中进行训练。
- 下面是一个演示
dataset = MyDataset()
dataloader = DataLoader(dataset)
num_epoches = 100
for epoch in range(num_epoches):
for img, label in dataloader:
....
- 而
Dataloader
是数据读取pytorch的重要接口,该接口定义在dataloader.py
中,只要使用PyTorch来训练模型基本都会用到该接口,该接口可以将自定义的Dataset根据batch size
大小,是否shuffle等封装成一个Batch size
的Tensor用于接下来的训练。
- 具体介绍见这里:Dataloader介绍
- 下面是本次作业读取使用的DataLoader
batch_size = 128
train_set = ImgDataset(train_x, train_y, train_transform)
val_set = ImgDataset(val_x, val_y, test_transform)
train_loader = DataLoader(train_set, batch_size=batch_size, shuffle=True)
val_loader = DataLoader(val_set, batch_size=batch_size, shuffle=False)
定义网络
class Classifier(nn.Module):
def __init__(self):
super(Classifier, self).__init__()
self.cnn = nn.Sequential(
nn.Conv2d(3, 64, 3, 1, 1),
nn.BatchNorm2d(64),
nn.ReLU(),
nn.MaxPool2d(2, 2, 0),
nn.Conv2d(64, 128, 3, 1, 1),
nn.BatchNorm2d(128),
nn.ReLU(),
nn.MaxPool2d(2, 2, 0),
nn.Conv2d(128, 256, 3, 1, 1),
nn.BatchNorm2d(256),
nn.ReLU(),
nn.MaxPool2d(2, 2, 0),
nn.Conv2d(256, 512, 3, 1, 1),
nn.BatchNorm2d(512),
nn.ReLU(),
nn.MaxPool2d(2, 2, 0),
nn.Conv2d(512, 512, 3, 1, 1),
nn.BatchNorm2d(512),
nn.ReLU(),
nn.MaxPool2d(2, 2, 0),
)
self.fc = nn.Sequential(
nn.Linear(512*4*4, 1024),
nn.ReLU(),
nn.Linear(1024, 512),
nn.ReLU(),
nn.Linear(512, 11)
)
def forward(self, x):
out = self.cnn(x)
out = out.view(out.size()[0], -1)
return self.fc(out)
- 先来讲
conv2d(in_channels: int, out_channels: int, kernel_size, stride: Union, padding)
,看一下讲解前最好要看完李宏毅老师关于CNN的课(下面的步骤针对的是以一个卷积和池化层)
- Conv2d的第一个参数
in_channels
代表着图像有多少个通道,黑白图像只需要一个通道代表灰度,RGB图像则需要三个通道。
- 第二个参数
out_channels
代表代表最后输出有多少个通道,它其实也代表着我们用多少个filter去照它。
- 第三个参数
kernel_size
代表卷积核的大小,就是它一次取样KS*KS
的内容,相当于那个特征提取器的大小。值得注意的是,使用卷积核处理完之后并不像PPT15页中的矩阵变小了,而是外部会使用bias
或者0
来填入。具体请参考:PyTorch学习笔记(9)——nn.Conv2d和其中的padding策略。所以此时矩阵还是128*128
- 第四个参数
stride
就是取样的间隔,这很好理解。
- 第五个参数
padding
是外面增加的层数。
BatchNorm2D(num_features)
,这个的意思应该是将每一层的参数进行正则化的仿射变换,使得参数,降低数据的绝对差异,考虑相对差异,在使用ReLu和Sigmoid的时候可以一定程度的避免梯度消失和梯度爆炸。现在我们的特征有64层,所以填64。参考来源于:Pytorch官方文档
MaxPool2d(kernel_size, stride,padding)
,这里的kernel_size指的是池化积,stride是池化间隔,padding也是边缘,这里想象一个滑动的池化积内核就好。
- 至于我们为什么采取这样的卷积池化架构,这是一个值得思考的问题。
- 后面的
fc
其实就是一个很简单的全连接层了,这边也稍微讲一下Linear吧。
Linear(in_features, out_features, bias: bool = True)
中第一个参数in_features
代表这个层级输入的特征,out_features
代表输出的特征,其实就是一个单纯的全连接层。
- 下面的都同理,最后输入11个结果。
模型训练
model = Classifier().cuda()
loss = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
num_epoch = 30
for epoch in range(num_epoch):
epoch_start_time = time.time()
train_acc = 0.0
train_loss = 0.0
val_acc = 0.0
val_loss = 0.0
model.train()
for i, data in enumerate(train_loader):
optimizer.zero_grad()
train_pred = model(data[0].cuda())
batch_loss = loss(train_pred, data[1].cuda())
batch_loss.backward()
optimizer.step()
train_acc += np.sum(np.argmax(train_pred.cpu().data.numpy(), axis=1) == data[1].numpy())
train_loss += batch_loss.item()
model.eval()
with torch.no_grad():
for i, data in enumerate(val_loader):
val_pred = model(data[0].cuda())
batch_loss = loss(val_pred, data[1].cuda())
val_acc += np.sum(np.argmax(val_pred.cpu().data.numpy(), axis=1) == data[1].numpy())
val_loss += batch_loss.item()
print('[%03d/%03d] %2.2f sec(s) Train Acc: %3.6f Loss: %3.6f | Val Acc: %3.6f loss: %3.6f' % \
(epoch + 1, num_epoch, time.time()-epoch_start_time, \
train_acc/train_set.__len__(), train_loss/train_set.__len__(), val_acc/val_set.__len__(), val_loss/val_set.__len__()))
- 为什么分类问题要用交叉熵:分类器使用交叉熵解读
- Adam优化器有什么优势:Adam优化器的优势
- 为什么梯度累加前要清零:为什么梯度累加前要清零
- 为什么要同时使用
model.eval()
和with torch.no_grad()
: Pytorch的modle.train,model.eval,with torch.no_grad的个人理解