Hung-Yi Lee homework[3]:CNN
(一)作业描述
本次作业是图像识别,将所给的食物图片分类,共11种,图片对应的食物编号在图片名字的前缀上。
要求:使用卷积神经网络CNN和Pytorch进行实现。
数据集链接(提取码:4zmm ,选择hw3中的压缩包)
数据集解压后是三个文件夹,testing,training,validation。用于存储训练、验证、测试的数据。
training+validation:图片按照 “[类别]_[编号].jpg” 的方式进行命名。
testing:图片按照 “[编号].jpg” 的方式进行命名。
(二)总体代码
李宏毅老师放了示例代码,本版代码在示例代码的基础上进行了修改和自己的调整,加入了模型导出和导入的部分。
本版代码中包含有3个文件:define_class.py,hw3.py,hw3_predict.py。
define_class.py:定义了读取图片函数、网络结构、数据集结构。
hw3.py:训练模型并进行保存,模型格式为“xx.pt”。
hw3_predict.py:导入训练好的模型并进行预测。
笔者环境配置如下,使用cuda10.0(配环境配了挺久的,直接进行pip进行torch和torchvision的安装会报错,需要到Pytorch官网下载whl文件到本地再使用pip直接安装whl文件才配置成功,各位读者如果在pycharm中直接安装或者terminal中安装时有问题可以试试):
define_class.py
import os
import numpy as np
import cv2
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, Dataset
# 数据集中图片命名格式:[类别]_[编号].jpg
# 利用opencv读取照片并存放在numpy array中
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 = np.zeros((len(image_dir)), dtype=np.uint8)
for i, file in enumerate(image_dir):
img = cv2.imread(os.path.join(path, file))
x[i, :, :] = cv2.resize(img, (128, 128))
if label:
y[i] = int(file.split("_")[0])
if label:
return x, y
else:
return x
# 定义模型
class Classifier(nn.Module):
def __init__(self):
super(Classifier, self).__init__()
# 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)
# 定义数据集
class ImgDataset(Dataset):
def __init__(self, x, y=None, transform=None):
self.x = x
# label is required to be a 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
hw3.py
import os
import numpy as np
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 time
from define_class import readfile, Classifier, ImgDataset
if __name__ == '__main__':
# 分别将training set,validation set,testing set用readfile函数读进来
print("Reading data")
train_x, train_y = readfile("training", True)
print("Size of training data = {}".format(len(train_x)))
val_x, val_y = readfile("validation", True)
print("Size of validation data = {}".format(len(val_x)))
train_transform = transforms.Compose([
transforms.ToPILImage(),
transforms.RandomHorizontalFlip(), # 随机将图片进行水平翻转
transforms.RandomRotation(15), # 随机旋转图片
transforms.ToTensor(), # 将图片转成Tensor,并将数值normalize到[0,1]
])
# testing时不需要做数据增广argumentation
test_transform = transforms.Compose([
transforms.ToPILImage(),
transforms.ToTensor(),
])
batch_size = 64
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)
# 开始训练,并使用validation寻找好的参数
model = Classifier().cuda()
loss = nn.CrossEntropyLoss() # 因为是分类任务,所以loss采用交叉熵
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)
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
for i, data in enumerate(train_loader):
optimizer.zero_grad() # 用optimizer将model参数的gradient归零
train_pred = model(data[0].cuda()) # 利用model得到预测的概率分布
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()
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('[%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__()))
torch.save(model, "model.pt")
hw3_predict.py
import os
import numpy as np
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
from hw3 import Classifier, ImgDataset, readfile
if __name__ == '__main__':
# 开始进行预测
model = torch.load('model.pt')
batch_size = 64
test_x = readfile("testing", False)
print("Size of testing data = {}".format(len(test_x)))
# testing时不需要做数据增广argumentation
test_transform = transforms.Compose([
transforms.ToPILImage(),
transforms.ToTensor(),
])
test_set = ImgDataset(test_x, transform=test_transform)
test_loader = DataLoader(test_set, batch_size=batch_size, shuffle=False)
prediction = []
with torch.no_grad():
for i, data in enumerate(test_loader):
test_pred = model(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_2.csv", 'w') as f:
f.write('ID,Category\n')
for i, y in enumerate(prediction):
f.write('{},{}\n'.format(i, y))
(三)代码详解
readfile()
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 = np.zeros((len(image_dir)), dtype=np.uint8)
for i, file in enumerate(image_dir):
img = cv2.imread(os.path.join(path, file))
x[i, :, :] = cv2.resize(img, (128, 128))
if label:
y[i] = int(file.split("_")[0])
if label:
return x, y
else:
return x
os.listdir(path)
:用于返回指定的文件夹包含的文件或文件夹的名字的列表。
image_dir = sorted(os.listdir(path))
中sorted
方法其实没啥用,可以用image_dir = os.listdir(path)
来进行替代。
np.zeros()
创建了全零numpy数组,大小是[len(image_dir), 128, 128, 3]。
x
是图片数据,y
是该图片所代表的标签。
enumerate()
函数用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,同时列出数据和数据下标,一般用在for循环当中。
至此,本段代码非常好读懂。
功能:从path路径读取该路径下所有图片,读取时将图片都resize成128x128x3的大小(因为是彩色图片)。
如果label=True,则可以读取该图片的类别。(training和validation文件夹)
如果label=False,则不对图片类别进行读取。(testing文件夹)
transforms.Compose()
train_transform = transforms.Compose([
transforms.ToPILImage(),
transforms.RandomHorizontalFlip(), # 随机将图片进行水平翻转
transforms.RandomRotation(15), # 随机旋转图片
transforms.ToTensor(), # 将图片转成Tensor,并将数值normalize到[0,1]
])
# testing时不需要做数据增广argumentation
test_transform = transforms.Compose([
transforms.ToPILImage(),
transforms.ToTensor(),
])
pytorch学习笔记 余霆嵩
torvision.transforms是pytorch中的图像预处理包。一般用Compose将多个步骤整合到一起。
train_transform
=transforms.ToPILImage()
+transforms.RandomHorizontalFlip()
+transforms.RandomRotation(15)
+ transforms.ToTensor()
transforms.ToPILImage()
- 将张量的每个元素乘上255
- 将张量的数据类型由FloatTensor转换成Uint8
- 将张量转化成numpy的ndarray类型
- 对ndarray对象做permute(1,2,0)的操作
- 利用Image下的fromarray函数,将ndarray对象转换成PILImage格式
- 输出PILImage
transforms.RandomHorizontalFlip()
transforms.RandomHorizontalFlip(p=0.5)
功能:依据概率p对PIL图片进行水平翻转。
参数:p——概率,默认值为0.5。
transforms.RandomRotation(15)
transforms.RandomRotation(degrees, resample=False, expand=False, center=None) 功能:依degrees随机旋转一定角度。
参数:
degress——若为单个数,如 30,则表示在(-30,+30)之间随机旋转;若为sequence,如(30,60),则表示在30-60度之间随机旋转。
resample——重采样方法
expand——是否扩大矩形框,以保持原图信息。根据中心旋转点计算扩大后的图片。如果旋转点不是中心,即使设置expand=True,还是会有部分信息丢失。
center——旋转点设置(坐标形式),默认中心旋转。如设置左上角为:(0,0)。
transforms.ToTensor()
- img.tobytes()将图片转化成内存中的存储格式
- torch.BytesStorage.frombuffer(img.tobytes())将字节以流的形式输入,转化成一维的张量
- 对张量进行reshape
- 对张量进行permute(2,0,1)
- 将当前张量的每个元素都除以255
- 输出张量
ImgDataset()
class ImgDataset(Dataset):
def __init__(self, x, y=None, transform=None):
self.x = x
# label is required to be a 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
这段代码仔细读读挺好懂的,不解释了。
DataLoader()
需要重新审视一下dataset,前面已经知道了本文代码中dataset的构建方式,这次重点在通用的dataset构建规范。
Pytorch笔记之dataset和dataloader by少年和狗
在 PyTorch 中,我们的数据集往往会用一个类去表示,在训练时用 Dataloader 产生一个 batch 的数据。简单来说,用一个类来抽象地表示数据集,而Dataloader作为迭代器,每次产生一个batch大小的数据,节省内存。
Dataset
Dataset是PyTorch中用来表示数据集的一个抽象类,我们的数据集可以用这个类来表示,至少覆写下面两个方法:
- len:数据集大小。
- getitem:实现这个方法后,可以通过下标的方式(dataset[i])来取得第i个数据。
Dataloader
Dataloader 就是一个迭代器,最基本的使用就是传入一个 Dataset 对象,它就会根据参数 batch_size 的值生成一个 batch 的数据。
Classifier()
Classifier()
就是定义了一下网络结构。
class Classifier(nn.Module):
def __init__(self):
super(Classifier, self).__init__()
# 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)
super()
super()函数是用于调用父类(超类)的一个方法。
super()是用来解决多重继承问题的,直接用类名调用父类方法在使用单继承的时候没问题,但是如果使用多继承,会涉及到查找顺序、重复调用等种种问题。
Pytorch进行训练
pytorch学习笔记(训练代码框架) by Chaossll
- 在Pytorch中,Tensor 是包含 data 和 grad 两个部分的,data 放的是真实的数据,而grad中保存的是 计算产生的梯度用于反向传播更新参数使用。
- 由 loss.backward()所计算的梯度会产生累计,所以在每次参数更新后需要设置 optim.zero_grad来将上一次的梯度清空。
训练的框架很好看懂,就不赘述了~。
至此,笔者认为已经将本版代码讲的很明白了,不懂的可以评论区问,笔者懂的会解答的~