李宏毅机器学习——食物图像识别(python 0基础开始)(hw3)

python

一、作业说明

        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的变化图如下,

 

 

以上是 李宏毅机器学习——食物图像识别(python 0基础开始)(hw3) 的全部内容, 来源链接: utcz.com/z/389505.html

回到顶部