通过卷积神经网络(Convolutional Neural Networks, CNN)对食物图片进行分类。
数据集中的食物图采集于网上,总共11类:Bread, Dairy product, Dessert, Egg, Fried food, Meat, Noodles/Pasta, Rice, Seafood, Soup, Vegetable/Fruit. 每一类用一个数字表示。比如:0表示Bread,1表示Dairy product。
数据说明
下载food-11.zip文件(约1.16GB),解压后得到三个文件夹,分别是training(9866)、validation(3430)、testing(3347)
training、validation文件夹中图片的命名格式是:[类别]_[编号].jpg,图片大小不一样。
testing文件夹中图片的命名格式是:[编号].jpg,图片大小不一样。
最终,预测结果的保存形式为:第一行是表头,第二行开始是预测结果。每一行有两列,第一列是Id,第二列是Category(类别),用逗号隔开。
评价指标是准确值(accuracy).
输入:一张食物的图片
输出:图片中食物所属的类别(0、1、2、……、10)
模型: 卷积神经网络CNN
import os
import numpy as np
import pandas as pd
import cv2
import torch
import torch.nn as nn
import torchvision.transforms as transforms
from torch.utils.data import DataLoader, Dataset
import time
1.os库:基本的系统交互功能,例如路径操作,进程管理,环境参数设置,是python的基本库
2.numpy库:
(1).python中array不支持多维数组,numpy可建立多维数组,便于矩阵行列式向量等运算。
(2).特殊数组
zeros数组:全零数组,元素全为零。
ones数组:全1数组,元素全为1。
empty数组:空数组,元素全近似为0。
3.panda库:
主要用于数据处理,进行索引相关操作。
4.cv2库
提供一些对图片的处理函数
5.torch库
包含神经网络的操作
6.time库
有关时间的相关操作
#定义一个读取图片的函数readfile()
def readfile(path, label):
# label 是一个布尔值,代表需不需要返回 y 值
image_dir = sorted(os.listdir(path))
# x存储图片,每张彩色图片都是128(高)*128(宽)*3(彩色三通道)
x = np.zeros((len(image_dir), 128, 128, 3), dtype=np.uint8)
# y存储标签,每个y大小为1
y = np.zeros((len(image_dir)), dtype=np.uint8)
for i, file in enumerate(image_dir):
img = cv2.imread(os.path.join(path, file))
# 利用cv2.resize()函数将不同大小的图片统一为128(高)*128(宽)
x[i, :, :] = cv2.resize(img,(128, 128))
if label:
y[i] = int(file.split("_")[0])#根据_进行分割存储标签名
if label:
return x, y
else:
return x
1 os.listdir() 方法用于返回指定的文件夹包含的文件或文件夹的名字的列表。
2 numpy.zero函数:numpy.zeros(shape,dtype=float,order = ‘C’)
3 enumerate() 函数用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,同时列出数据和数据下标,一般用在 for 循环当中。
#分别将 training set、validation set、testing set 用函数 readfile() 读进来
workspace_dir='D:\\DATA\\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 时,通过随机旋转、水平翻转图片来进行数据增强(data augmentation)
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(),
])
transforms.ToPILImage():将Numpy的ndarray或者Tensor转化成PILImage类型。
torchvision.transforms.ToTensor(): 将PILImage或者numpy的ndarray转化成Tensor。
class ImgDataset(Dataset):
def __init__(self, x, y=None, transform=None):
self.x = x
# label 需要是 LongTensor 型
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
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)
使用dataset类读取数据,生成dataloader。
batch_size为批大小,每次数据库的交互量。
先使用一个卷积神经网络,再接一个全连接的前向传播网络
卷积神经网络的一级卷积层由卷积层cov+批标准化batchnorm+激活函数ReLU+最大池化MaxPool构成
**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) # 最后是11个分类
)
def forward(self, x):
out = self.cnn(x)
out = out.view(out.size()[0], -1) # 摊平成1维
return self.fc(out)
**
nn.sequential: 一个有序的容器,神经网络模块将按照在传入构造器的顺序依次被添加到计算图中执行,同时以神经网络模块为元素的有序字典也可以作为传入参数。
二维卷积nn.Conv2d
一般来说,二维卷积nn.Conv2d用于图像数据,对宽度和高度都进行卷积。
定义
class torch.nn.Conv2d(in_channels, out_channels, kernel_size, stride=1, padding=0, dilation=1, groups=1, bias=True)
Batchnorm2d: 进行归一化处理,防止梯度爆炸。
RELU:激活函数,避免了梯度爆炸和梯度消失
MaxPool2d:取重要信息,去掉不重要的信息,减少计算。
使用训练集training set进行训练,并使用验证集validation set来选择最好的参数。
model = Classifier().cuda() #用cuda加速(gpu版本的torch)
loss = nn.CrossEntropyLoss() # 分类的模型通常要使用交叉熵损失
optimizer = torch.optim.Adam(model.parameters(), lr=0.001) # 使用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 是在 训练 model (开启 Dropout 等...)
for i, data in enumerate(train_loader):
optimizer.zero_grad() # 用 optimizer 将模型参数的梯度 gradient 归零
train_pred = model(data[0].cuda()) # 利用 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()
#验证集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()
#将结果 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__()))
交叉熵: 表示为真实概率分布与预测概率分布之间的差异。交叉熵的值越小,模型预测效果就越好。
继续使用training set和validation set共同训练。
train_val_x = np.concatenate((train_x, val_x), axis=0) # 将train_x和val_x拼接起来
train_val_y = np.concatenate((train_y, val_y), axis=0) # 将train_y和val_y拼接起来
train_val_set = ImgDataset(train_val_x, train_val_y, train_transform)
train_val_loader = DataLoader(train_val_set, batch_size=batch_size, shuffle=True)
model_best = Classifier().cuda() # cuda加速
loss = nn.CrossEntropyLoss() # 因为是分类任务,所以使用交叉熵损失
optimizer = torch.optim.Adam(model_best.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
model_best.train()
for i, data in enumerate(train_val_loader):
optimizer.zero_grad()
train_pred = model_best(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()
#将结果 print 出來
print('[%03d/%03d] %2.2f sec(s) Train Acc: %3.6f Loss: %3.6f' % \
(epoch + 1, num_epoch, time.time()-epoch_start_time, \
train_acc/train_val_set.__len__(), train_loss/train_val_set.__len__()))
测试:
test_set = ImgDataset(test_x, transform=test_transform)
test_loader = DataLoader(test_set, batch_size=batch_size, shuffle=False)
model_best.eval()
prediction = []
with torch.no_grad():
for i, data in enumerate(test_loader):
test_pred = model_best(data.cuda())
# 预测值中概率最大的下标即为模型预测的食物标签
test_label = np.argmax(test_pred.cpu().data.numpy(), axis=1)
for y in test_label:
prediction.append(y)
#将预测结果写入 csv
with open("predict.csv", 'w') as f:
f.write('Id,Category\n')
for i, y in enumerate(prediction):
f.write('{},{}\n'.format(i, y))
最后经过测试,准确率为70%。