李宏毅机器学习——作业三(CNN神经网络)
·
需求
- 项目资料集为网络上收集到的食物照片,总共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 # linear algebra
import pandas as pd # data processing, CSV file
import os
# Import需要的库
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)
获取一个样例文件作参考
# Load an color image in color
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):
# label 是一個 boolean variable,代表需不需要回傳 y 值
image_dir = sorted(os.listdir(path))
# 先创建数组
x = np.zeros((len(image_dir), 128, 128, 3), dtype=np.uint8)
# y是标签
y = np.zeros((len(image_dir)), dtype=np.uint8)
for i, file in enumerate(image_dir):
img = cv2.imread(os.path.join(path, file))
# 更改尺寸到128*128
x[i, :, :] = cv2.resize(img,(128, 128))
# 将label读入y中
if label:
y[i] = int(file.split("_")[0])
if label:
return x, y
else:
return x
- 调用函数读取
# 分別將 training set、validation set、testing set 用 readfile 函式讀進來
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)))
数据预处理
训练集先做数据增强
# training 时 做一个数据增强
train_transform = transforms.Compose([
transforms.ToPILImage(),
transforms.RandomHorizontalFlip(), # 随机翻转图片
transforms.RandomRotation(15), # 随机旋转图片
transforms.ToTensor(), # 把ndarray转化成Tensor
])
- 使用
ToPILImage可以将shape为(H,W,C)的numpy.ndarray转化为PIL.Image。 - 转换为的好处是可以做各种变换
测试集不需要做数据增强
# testing 時不需做 data augmentation
test_transform = transforms.Compose([
transforms.ToPILImage(),
transforms.ToTensor(),
])
Dataset是Pytorch中用来表示数据集的类
- 复写方法如下
class ImgDataset(Dataset):
def __init__(self, x, y=None, transform=None):
# 顺便一提,python在默认构造函数的时候,直接取等相当于声明变量
self.x = x
# label is required to be a LongTensor
# 这个原因我不是很明白,虽然改了后面会报错,但是为什么Label要用Long类型
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):#需要继承data.Dataset
def __init__(self):
# TODO
# 1. Initialize file path or list of file names.
pass
def __getitem__(self, index):
# TODO
# 1. Read one data from file (e.g. using numpy.fromfile, PIL.Image.open).
# 2. Preprocess the data (e.g. torchvision.Transform).
# 3. Return a data pair (e.g. image and label).
#这里需要注意的是,第一步:read one data,是一个data
pass
def __len__(self):
# You should change 0 to the total size of your dataset.
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)
定义网络
- 这里定义了5个卷积层,5个池化层。
class Classifier(nn.Module):
def __init__(self):
super(Classifier, self).__init__()
# torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding)
# torch.nn.MaxPool2d(kernel_size, stride, padding)
# input 维度 [3, 128, 128]
self.cnn = nn.Sequential(
nn.Conv2d(3, 64, 3, 1, 1), # [64, 128, 128]
nn.BatchNorm2d(64),
nn.ReLU(),
nn.MaxPool2d(2, 2, 0), # [64, 64, 64]
nn.Conv2d(64, 128, 3, 1, 1), # [128, 64, 64]
nn.BatchNorm2d(128),
nn.ReLU(),
nn.MaxPool2d(2, 2, 0), # [128, 32, 32]
nn.Conv2d(128, 256, 3, 1, 1), # [256, 32, 32]
nn.BatchNorm2d(256),
nn.ReLU(),
nn.MaxPool2d(2, 2, 0), # [256, 16, 16]
nn.Conv2d(256, 512, 3, 1, 1), # [512, 16, 16]
nn.BatchNorm2d(512),
nn.ReLU(),
nn.MaxPool2d(2, 2, 0), # [512, 8, 8]
nn.Conv2d(512, 512, 3, 1, 1), # [512, 8, 8]
nn.BatchNorm2d(512),
nn.ReLU(),
nn.MaxPool2d(2, 2, 0), # [512, 4, 4]
)
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是外面增加的层数。
- Conv2d的第一个参数
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() # 将模型复制到GPU
loss = nn.CrossEntropyLoss()
# 因為是 classification task,所以 loss 使用 CrossEntropyLoss
optimizer = torch.optim.Adam(model.parameters(), lr=0.001) # optimizer 使用 Adam
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() # 確保 model 是在 train model (開啟 Dropout 等...)
for i, data in enumerate(train_loader):
optimizer.zero_grad() # 用 optimizer 將 model 參數的 gradient 歸零
train_pred = model(data[0].cuda()) # 利用 model 得到預測的機率分佈 這邊實際上就是去呼叫 model 的 forward 函數
batch_loss = loss(train_pred, data[1].cuda()) # 計算 loss (注意 prediction 跟 label 必須同時在 CPU 或是 GPU 上)
batch_loss.backward() # 利用 back propagation 算出每個參數的 gradient
optimizer.step() # 以 optimizer 用 gradient 更新參數值
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 出來
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的个人理解
魔乐社区(Modelers.cn) 是一个中立、公益的人工智能社区,提供人工智能工具、模型、数据的托管、展示与应用协同服务,为人工智能开发及爱好者搭建开放的学习交流平台。社区通过理事会方式运作,由全产业链共同建设、共同运营、共同享有,推动国产AI生态繁荣发展。
更多推荐


所有评论(0)