李宏毅2020機器學習——食物圖像識別(python 0基礎開始)(hw3)


一、作業說明

        1.數據包括testing、trainning和validation三個食物圖片集,training共9866張,validation共3430張,testing共3347張。training和validation圖片名字給出了食物的類別,用來訓練和評估模型的泛化能力。總共11類:Bread, Dairy product, Dessert, Egg, Fried food, Meat, Noodles/Pasta, Rice, Seafood, Soup, Vegetable/Fruit. 每一類用一個數字表示。比如:0表示Bread.

        2.通過建立CNN模型來對食物進行分類。

二、作業思路

圖像分類步驟大致為:

1.使用cv2讀入數據

2.使用Dataset對數據進行包裝(包括圖片的變換——隨機旋轉,水平翻轉,即數據增強以防止過擬合),在送入Dataloader進行下一步操作,Dataloader使用方法https://www.jianshu.com/p/8ea7fba72673

3.定義模型(卷積網絡+全連接前向傳播)

4.計算each iteration的Accuracy和loss

本文參考:https://blog.csdn.net/iteapoy/article/details/105765411初學,采用的都是別人的程序,主要進行學習。

需要的庫函數:

import cv2
import os
import numpy as np
import matplotlib.pyplot as pb
import torch
import torchvision.transforms as transforms
from torch.utils.data import DataLoader, Dataset
import torch.nn as nn
import pandas as pd
import time

圖片數據的輸入,原始圖片的大小不一,需進行統一,一開始設置過512*512的像素,一直報錯說內存不夠,最后換成鏈接中給出的128*128的像素。另外為了節約運算時間,可以將圖片保存為128*128大小,后面直接用。

path = 'D:/ML/data11/hw3/food-11/training'
print(type(path))
for filename in os.listdir(path):
    im = cv2.imread(path+'/'+filename)
    fixed_im = cv2.resize(im, (128, 128))
    save_path = path + '_new/'
    img_name = filename
    if os.path.exists(save_path):
        save_img = save_path + img_name
        cv2.imwrite(save_img, fixed_im)
    else:
        os.mkdir(save_path)
        save_img = save_path + img_name
        cv2.imwrite(save_img, fixed_im)

訓練集和驗證集需要標簽,這里給了一個邏輯量來判斷是否要輸出標簽y

# 定義讀取圖片的函數read()
def read(path, label):
    image_dir = sorted(os.listdir(path))            # listdir是得到該路徑下所有圖片,由於本來的圖片是排好序的,sorted可不用
    X = np.zeros((len(image_dir), 128, 128, 3), dtype=np.uint8)     # 這里需要將數據類型轉為int,不然容易出現內存不夠的情況
    y = np.zeros((len(image_dir)), dtype=np.uint8)
    for i, file in enumerate(image_dir):            # enumerate得到索引i,和文件名file
        img = cv2.imread(os.path.join(path, file))  # os.path.join效果與path+file一樣
        X[i, :, :] = cv2.resize(img, (128, 128))    # 普通CNN需要送入像素一樣的圖片集,cv2.resize函數將圖片統一為128*128
        if label:
            y[i] = int(file.split("_")[0])          # split將文件名中的“_”符號去掉后取第一個字符即已經分好的類別
    if label:
        return X, y
    else:
        return X


path = 'D:/ML/data11/hw3/food-11'
train_x, train_y = read(os.path.join(path, "training"), True)
print("Size of training data = {}".format(len(train_x)))
val_x, val_y = read(os.path.join(path, "validation"), True)
print("Size of validation data = {}".format(len(val_x)))
test_x = read(os.path.join(path, "testing"), False)
print("Size of Testing data = {}".format(len(test_x)))

得到的結果圖下:

 

 接下來要用到dataset和dataloader,先定義好training,validation所需的變換,還要重構dataset類中的__len__和__getitem__函數,

,圖片變換參考https://blog.csdn.net/iteapoy/article/details/106121752圖片變換:

train_transform = transforms.Compose([
    transforms.ToPILImage(),
    transforms.RandomHorizontalFlip(),  # 隨機翻轉圖片
    transforms.RandomRotation(15),      # 隨機旋轉圖片
    transforms.ToTensor(),              # 將圖片變成 Tensor,並且把數值normalize到[0,1]
])
# testing時,不需要進行數據增強(data augmentation)
test_transform = transforms.Compose([
    transforms.ToPILImage(),
    transforms.ToTensor(),
])

重構dataset類中的__len__和__getitem__函數,

class ImgDataset(Dataset):  # Dataset是一個包裝類,用來將數據包裝為Dataset類,然后傳入DataLoader中
    # __init__函數是用來數據輸入的,這里將圖片的數據集和標簽集以及對圖片的的數據增強效果都進行了輸入
    def __init__(self, x, y=None, transform=None):
        self.x = x
        self.y = y     # label需要是LongTensor型,torch.Tensor默認為torch.FloatTensor是32位浮點類型數據,
        # torch.LongTensor是64位整型數據
        if y is not None:
            self.y = torch.LongTensor(y)
        self.transform = transform

    def __len__(self):
        return len(self.x)         # __len__函數是用來獲取數據集大小的

    def __getitem__(self, index):  # __getitem__函數用來獲取單個數據,根據后面的batchsize來確定數據片段的個數
        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

定義好Dataset后,傳入Dataloader進行下一步操作,

batch_size = 128
train_set = ImgDataset(train_x, train_y, train_transform)
val_set = ImgDataset(val_x, val_y, test_transform)
# DataLoader是根據batch_size從Dataset類中得到數據片段,再使用collate__fn所指定的函數對這個batch做一些操作(padding,cuda等)
train_loader = DataLoader(train_set, batch_size=batch_size, shuffle=True)
val_loader = DataLoader(val_set, batch_size=batch_size, shuffle=False)   # shuffle確定是否打亂數據
print("Size of training set = {}".format(len(train_set)))
print("Size of val set = {}".format(len(val_set)))

第三步是定義模型,這里采用的是利用nn.Sequential作為順序容器一步步定義模型的,同樣也可以根據官方教程中先定義需要用到的模塊,再給順序。模型為兩層卷積池化

# 定義模型
class Classifier(nn.Module):
    def __init__(self):
        super(Classifier, self).__init__()   # super函數指先找到Classifier的父類(nn.Module),然后把類Classifier的對象轉換為父類的對象
        # torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride, padding)
        # in_channels:輸入圖像通道數,out_channels:卷積產生的通道數(卷積核個數) kernel_size:卷積核尺寸
        # stride:卷積步長,默認為1, padding:填充操作

        # torch.nn.MaxPool2d(kernel_size, stride, padding)
        # kernel_size:MaxPooling的大小,stride:窗口移動步長,padding:輸入的每一條補充0的層數
        # input 維度 [3, 512, 512]
        self.cnn = nn.Sequential(       # nn.Sequential為順序容器,將nn模塊按照順序添加到計算圖執行
            nn.Conv2d(3, 64, 3, 1, 1),  # 輸出[64, 128, 128]
            nn.BatchNorm2d(64),         # 歸一標准化,64為特征數量,即為輸入BN層的通道數
            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)  # 最后是11個分類
        )

    def forward(self, x):
        out = self.cnn(x)
        out = out.view(out.size()[0], -1)  # 攤平成1維
        return self.fc(out)

 訓練號模型,就要進行迭代計算出模型在訓練集的驗證集上的loss和正確率,

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 是在 訓練 model (開啟 Dropout 等...)
    for i, data in enumerate(train_loader):
        optimizer.zero_grad()  # 用 optimizer 將模型參數的梯度 gradient 歸零
        train_pred = model(data[0].cuda())  # 利用 model 得到預測的概率分布,data[0]為X,data[1]為標簽y
        batch_loss = loss(train_pred, data[1].cuda())  # 計算 loss (注意 prediction 跟 label 必須同時在 CPU 或是 GPU 上)
        batch_loss.backward()  # 利用 back propagation 算出每個參數的 gradient
        optimizer.step()  # 以 optimizer 用 gradient 更新參數
        # argmax是得到最大值對應的索引,axis=1指對列進行操作,data是脫離require_data的標記,不再自動求微分
        train_acc += np.sum(np.argmax(train_pred.cpu().data.numpy(), axis=1) == data[1].numpy())
        train_loss += batch_loss.item()

    # 驗證集val
    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()  # item是得到一個元素張量(tensor)中的元素

        # 將結果 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__()))

第一次運行時用的是實驗室的電腦(CPU),用不了GPU,不信邪試了試,運行了接近三個小時,結果如下:

模型的結果不算好,最高也才86%左右,在驗證集上的正確率更低,模型的泛化能力不足,后面又用我自己帶獨顯的筆記本使用GPU運行,速度快了10多倍,哭了GPU賽高!結果如下,后面把迭代次數換為了50次,訓練集accuracy可達96.5%,但驗證機的accuracy依舊上不去

可能是電腦的原因,模型在訓練集上的Accuracy可以到90%,泛化能力依舊不足。

參考的文章里用到了殘差神經網絡來改進網絡結構,后面去學習了殘差神經網絡的知識,我認為這一篇文章寫的很好:

https://blog.csdn.net/loveliuzz/article/details/79117397?utm_term=%E6%AE%8B%E5%B7%AE%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C%E4%B8%8E%E5%8D%B7%E7%A7%AF%E7%A5%9E%E7%BB%8F%E7%BD%91%E7%BB%9C&utm_medium=distribute.pc_aggpage_search_result.none-task-blog-2~all~sobaiduweb~default-0-79117397&spm=3001.4430

神經網絡層數過深,非線性激活過多,使信息丟失,梯度消失的現象也越來越明顯,Resnet不僅可以有效解決神經網絡退化問題,還可以提高運算速率(減少了需要訓練的參數)。這里其實層數並不多,可能效果並不是很好,還是訓練一遍看看結果如何,

加入殘差網絡的步驟:先定義好殘差塊,在定義殘差神經網絡。

下圖為典型的殘差塊,這里的F(x)+x是作用在第二個relu之前的,下右圖吳恩達老師的課程寫的很清晰,

 

代碼如下:

# 定義殘差塊
class Residual_Block(nn.Module):
    def __init__(self, i_channel, o_channel, stride=1, down_sample=None):   # __init__用以定義需要用的模塊
        super(Residual_Block, self).__init__()
        # Conv2d需要定義的參數包括輸入輸出卷積的通道數,卷積核大小,卷積核計算一次后移動的距離,補零的圈數,是否需要偏差
        self.conv1 = nn.Conv2d(in_channels=i_channel,
                               out_channels=o_channel,
                               kernel_size=3,
                               stride=stride,
                               padding=1,
                               bias=False)
        # 主要為了防止神經網絡退化
        self.bn1 = nn.BatchNorm2d(o_channel)
        self.relu = nn.ReLU(inplace=True)
        self.conv2 = nn.Conv2d(in_channels=o_channel,
                               out_channels=o_channel,
                               kernel_size=3,
                               stride=1,
                               padding=1,
                               bias=False)
        self.bn2 = nn.BatchNorm2d(o_channel)
        self.down_sample = down_sample  # down_sample用在殘差塊的輸入通道數與輸出通道數不一致時(畢竟輸入輸出需要相加),主要是卷積和補零

    def forward(self, x):
        residual = x
        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)
        out = self.conv2(out)
        out = self.bn2(out)

        # 將單元的輸入直接與單元輸出加在一起
        if self.down_sample:
            residual = self.down_sample(x)  # 下采樣
        out += residual
        out = self.relu(out)

        return out

 下面是整個Resnet的架構,相關注釋都寫在了程序里,

# 定義殘差神經網絡
class ResNet(nn.Module):
    def __init__(self, block, layers, num_classes=11):
        super(ResNet, self).__init__()
        self.conv = nn.Conv2d(in_channels=3, out_channels=16, kernel_size=3, stride=1, padding=1, bias=False)
        self.in_channels = 16
        self.bn = nn.BatchNorm2d(16)
        self.relu = nn.ReLU(inplace=True)  # inplace指是否進行覆蓋操作,意思是是否將得到的值計算得到的值覆蓋之前的值。
        # 優點是這樣能夠節省運算內存,不用多存儲其他變量(不太清楚是否有必要)
        self.layer1 = self.make_layer(block, 16, layers[0])
        self.layer2 = self.make_layer(block, 32, layers[1], 2)  # 這里的layers迭代時給的,stride=2,每個殘差塊都要下采樣
        self.layer3 = self.make_layer(block, 64, layers[2], 2)
        self.avg_pool = nn.AvgPool2d(8)
        self.fc = nn.Linear(1024, num_classes)

    def make_layer(self, block, out_channels, blocks, stride=1):
        # blocks=layers,殘差模塊的數量
        down_sample = None   # None為為空數組類型
        if (stride != 1) or (self.in_channels != out_channels):
            # 如果不一樣就轉換一下維度
            down_sample = nn.Sequential(
                nn.Conv2d(self.in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False),
                nn.BatchNorm2d(out_channels)
            )
        layers = []
        layers.append(block(self.in_channels, out_channels, stride, down_sample))
        self.in_channels = out_channels
        for i in range(1, blocks):
            layers.append(block(out_channels, out_channels))
        return nn.Sequential(*layers)  # 添加所有殘差塊

    def forward(self, x):
        out = self.conv(x)
        out = self.bn(out)
        out = self.relu(out)
        out = self.layer1(out)
        out = self.layer2(out)
        out = self.layer3(out)
        out = self.avg_pool(out)
        out = out.view(out.size()[0], -1)   # size()[0]為Batch_size,view最后展開為二維的,[batch_size,pixel*pixel*channels]
        out = self.fc(out)
        return out

迭代前進行參數設定,

# 更新學習率,這個不太清楚,只找到scheduler的調整方法
def update_lr(optimizer, lr):
    for param_group in optimizer.param_groups:
        param_group['lr'] = lr


# 固定隨機種子,指每次隨機的數一樣
random.seed(1)
batch_size = 16  # 設置為64時內存顯示不夠,16才能運行
learning_rate = 1e-3

# 定義模型
model = ResNet(Residual_Block, [2, 2, 2]).cuda()
loss = nn.CrossEntropyLoss().cuda()  # 因為是分類任務,所以使用交叉熵損失

optimizer = torch.optim.Adam(model.parameters(), lr=learning_rate)  # 使用Adam優化器
num_epoch = 60  # 迭代次數

# 保存每個iteration的loss和accuracy,以便后續畫圖
plt_train_loss = []
plt_val_loss = []
plt_train_acc = []
plt_val_acc = []

最后便是整個迭代過程和繪圖示意了,

# 用訓練集訓練模型model(),用驗證集作為測試集來驗證
curr_lr = learning_rate
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()

    # 驗證集val
    model.eval()
    with torch.no_grad():    # 對於tensor的計算操作,默認是要進行計算圖的構建的,
        # 在這種情況下,可以使用 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()

        # 保存用於畫圖
        plt_train_acc.append(train_acc / train_set.__len__())
        plt_train_loss.append(train_loss / train_set.__len__())
        plt_val_acc.append(val_acc / val_set.__len__())
        plt_val_loss.append(val_loss / val_set.__len__())

        # 將結果 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,
               plt_train_acc[-1], plt_train_loss[-1], plt_val_acc[-1], plt_val_loss[-1]))

# Loss曲線
plt.plot(plt_train_loss)
plt.plot(plt_val_loss)
plt.title('Loss')
plt.legend(['train', 'val'])
plt.savefig('loss.png')
plt.show()

# Accuracy曲線
plt.plot(plt_train_acc)
plt.plot(plt_val_acc)
plt.title('Accuracy')
plt.legend(['train', 'val'])
plt.savefig('acc.png')
plt.show()

得到的結果如下,可以看到迭代60次的效果並沒有很好,正確率最高也沒達到90%,“淺”層網絡使用Resnet效果一般(中間電腦睡眠了,所花時間較多)

 

 Accuracy和loss的變化圖如下,

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM