李宏毅机器学习——作业三(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]))

李宏毅机器学习——作业三(CNN神经网络)_第1张图片

将文件读取到变量中

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是外面增加的层数。
  • 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的个人理解

你可能感兴趣的:(机器学习-新国立,深度学习,python,卷积)