最近太多作业了,也逐渐emo起来,忙里偷闲,抄一份猫狗分类的cnn玩玩。拿别人代码稍微改动了一点,把GPU的部分去掉了,在CPU上跑的,确实挺慢,后来把数据集缩成了500张猫500张狗,结果一点都不准确。知道了kaggle,了解了一个模型的构建过程。好玩是好玩,不过以我的经验,不管是什么玩意,只要深入下去就肯定是枯燥的。代码是昨晚跑的,笔记是今天写的,三分钟热度过了,随便写写了,能留个印象就好。
https://blog.csdn.net/l1076604169/article/details/92747124
https://blog.csdn.net/qq_39328436/article/details/120993402
数据集
https://www.kaggle.com/c/dogs-vs-cats/data
图像的卷积就是让卷积核(卷积模板)在原图像上依次滑动,在两者重叠的区域中,把对应位置的像素值和卷积模板值相乘,最后累加求和得到新图像(卷积结果)中的一个像素值,卷积核每滑动一次获得一个新值,当完成原图像的全部遍历,便完成原图像的一次卷积。
需要注意下面的原图像周围有一圈虚框框,这个是为了让卷积运算之后得到的结果和原图大小一样而设置的padding,也就是填充一个默认值来辅助操作。
激活函数就是一个自定义的非线性函数。在卷积过程中,所有的运算都是线性运算和线性叠加,无论网络多复杂,输入输出关系都是线性关系,没有激活函数,网络就无法拟合非线性特性的输入输出关系,常用的激活函数有Sigmoid,Tanh,ReLU等。
池化用于特征的降维,再来个动图,生动形象,没错动图也是炒的。
该层的每一个结点都与上一层的所有结点相连,用来把前边提取到的特征综合起来,把这样的线性层也叫全连接层。
每一层都是全连接层的网络叫全连接神经网络。
不准确的解释,自己理解的是含有卷积层的神经网络就是神经网络,一般包含了卷积层、池化层、全连接层。
架构通常都是四个部分,准备数据、搭建网络、训练、测试。其中训练完成后会将模型信息保存下来,也就是说模型定下了以后测试的时候可以直接把数据放到模型里,然后就可以拿到结果了。
getdata用于获取数据,network是自己定义的网络模型,train和test.py分别是训练模型和测试数据,train和test文件夹就是图片数据,model文件夹一开始是空的,在train.py运行完后会将模型保存到其文件夹下,是一个.pth文件。后面懒得写了,直接上代码。
getdata.py
import os
import torch.utils.data as data
from PIL import Image
import numpy as np
import torch
import torchvision.transforms as transforms
# 默认输入网络的图片大小
IMAGE_H = 200
IMAGE_W = 200
# 定义一个转换关系,用于将图像数据转换成PyTorch的Tensor形式
data_transform = transforms.Compose([
transforms.Resize((IMAGE_H, IMAGE_W)),
transforms.ToTensor() # 转换成Tensor形式,并且数值归一化到[0.0, 1.0]
])
class DogsVSCatsDataset(data.Dataset): # 新建一个数据集类,并且需要继承PyTorch中的data.Dataset父类
def __init__(self, mode, dir): # 默认构造函数,传入数据集类别(训练或测试),以及数据集路径
self.mode = mode
self.list_img = [] # 新建一个image list,用于存放图片路径,注意是图片路径
self.list_label = [] # 新建一个label list,用于存放图片对应猫或狗的标签,其中数值0表示猫,1表示狗
self.data_size = 0 # 记录数据集大小
self.transform = data_transform # 转换关系
if self.mode == 'train': # 训练集模式下,需要提取图片的路径和标签
dir = dir + '/train/' # 训练集路径在"dir"/train/
for file in os.listdir(dir): # 遍历dir文件夹
self.list_img.append(dir + file) # 将图片路径和文件名添加至image list
self.data_size += 1 # 数据集增1
name = file.split(sep='.') # 分割文件名,"cat.0.jpg"将分割成"cat",".","jpg"3个元素
# label采用one-hot编码,"1,0"表示猫,"0,1"表示狗,任何情况只有一个位置为"1",在采用CrossEntropyLoss()计算Loss情况下,label只需要输入"1"的索引,即猫应输入0,狗应输入1
if name[0] == 'cat':
self.list_label.append(0) # 图片为猫,label为0
else:
self.list_label.append(1) # 图片为狗,label为1,注意:list_img和list_label中的内容是一一配对的
elif self.mode == 'test': # 测试集模式下,只需要提取图片路径就行
dir = dir + '/test/' # 测试集路径为"dir"/test/
for file in os.listdir(dir):
self.list_img.append(dir + file) # 添加图片路径至image list
self.data_size += 1
self.list_label.append(2) # 添加2作为label,实际未用到,也无意义
else:
return print('Undefined Dataset!')
def __getitem__(self, item): # 重载data.Dataset父类方法,获取数据集中数据内容
if self.mode == 'train': # 训练集模式下需要读取数据集的image和label
img = Image.open(self.list_img[item]) # 打开图片
# img = img.resize((IMAGE_H, IMAGE_W)) # 将图片按比例resize成统一大小
# img = np.array(img) # 数据转换成numpy数组形式
label = self.list_label[item] # 获取image对应的label
return self.transform(img), torch.LongTensor([label]) # 将image和label转换成PyTorch形式并返回
elif self.mode == 'test': # 测试集只需读取image
img = Image.open(self.list_img[item])
# img = img.resize((IMAGE_H, IMAGE_W))
# img = np.array(img)
return self.transform(img) # 只返回image
else:
print('None')
def __len__(self):
return self.data_size # 返回数据集大小
network.py
import torch
import torch.nn as nn
import torch.utils.data
import torch.nn.functional as F
class Net(nn.Module): # 新建一个网络类,就是需要搭建的网络,必须继承PyTorch的nn.Module父类
def __init__(self): # 构造函数,用于设定网络层
super(Net, self).__init__() # 标准语句
self.conv1 = torch.nn.Conv2d(3, 16, 3, padding=1) # 第一个卷积层,输入通道数3,输出通道数16,卷积核大小3×3,padding大小1,其他参数默认
self.conv2 = torch.nn.Conv2d(16, 16, 3, padding=1) # 第二个卷积层,输入通道数16,输出通道数16,卷积核大小3×3,padding大小1,其他参数默认
self.fc1 = nn.Linear(50*50*16, 128) # 第一个全连层,线性连接,输入节点数50×50×16,输出节点数128
self.fc2 = nn.Linear(128, 64) # 第二个全连层,线性连接,输入节点数128,输出节点数64
self.fc3 = nn.Linear(64, 2) # 第三个全连层,线性连接,输入节点数64,输出节点数2
def forward(self, x): # 重写父类forward方法,即前向计算,通过该方法获取网络输入数据后的输出值
x = self.conv1(x) # 第一次卷积
x = F.relu(x) # 第一次卷积结果经过ReLU激活函数处理
x = F.max_pool2d(x, 2) # 第一次池化,池化大小2×2,方式Max pooling
x = self.conv2(x) # 第二次卷积
x = F.relu(x) # 第二次卷积结果经过ReLU激活函数处理
x = F.max_pool2d(x, 2) # 第二次池化,池化大小2×2,方式Max pooling
x = x.view(x.size()[0], -1) # 由于全连层输入的是一维张量,因此需要对输入的[50×50×16]格式数据排列成[40000×1]形式
x = F.relu(self.fc1(x)) # 第一次全连,ReLU激活
x = F.relu(self.fc2(x)) # 第二次全连,ReLU激活
x = self.fc3(x) # 第三次激活
return F.softmax(x, dim=1) # 采用SoftMax方法将输出的2个输出值调整至[0.0, 1.0],两者和为1,并返回
train.py
from getdata import DogsVSCatsDataset as DVCD
from torch.utils.data import DataLoader as DataLoader
from network import Net
import torch
from torch.autograd import Variable
import torch.nn as nn
dataset_dir = '.' # 数据集路径
model_cp = './model' # 网络参数保存位置
workers = 4 # PyTorch读取数据线程数量
batch_size = 16 # batch_size大小
lr = 0.0001 # 学习率
nepoch = 5
def train():
datafile = DVCD('train', dataset_dir) # 实例化一个数据集
dataloader = DataLoader(datafile, batch_size=batch_size, shuffle=True, num_workers=workers, drop_last=True) # 用PyTorch的DataLoader类封装,实现数据集顺序打乱,多线程读取,一次取多个数据等效果
print('Dataset loaded! length of train set is {0}'.format(len(datafile)))
model = Net() # 实例化一个网络
# model = model.cuda() # 网络送入GPU,即采用GPU计算,如果没有GPU加速,可以去掉".cuda()"
# model = nn.DataParallel(model)
model.train() # 网络设定为训练模式,有两种模式可选,.train()和.eval(),训练模式和评估模式,区别就是训练模式采用了dropout策略,可以放置网络过拟合
optimizer = torch.optim.Adam(model.parameters(), lr=lr) # 实例化一个优化器,即调整网络参数,优化方式为adam方法
criterion = torch.nn.CrossEntropyLoss() # 定义loss计算方法,cross entropy,交叉熵,可以理解为两者数值越接近其值越小
cnt = 0 # 训练图片数量
for epoch in range(nepoch):
# 读取数据集中数据进行训练,因为dataloader的batch_size设置为16,所以每次读取的数据量为16,即img包含了16个图像,label有16个
for img, label in dataloader: # 循环读取封装后的数据集,其实就是调用了数据集中的__getitem__()方法,只是返回数据格式进行了一次封装
# img, label = Variable(img).cuda(), Variable(label).cuda() # 将数据放置在PyTorch的Variable节点中,并送入GPU中作为网络计算起点
img, label = Variable(img), Variable(label)
out = model(img) # 计算网络输出值,就是输入网络一个图像数据,输出猫和狗的概率,调用了网络中的forward()方法
loss = criterion(out, label.squeeze()) # 计算损失,也就是网络输出值和实际label的差异,显然差异越小说明网络拟合效果越好,此处需要注意的是第二个参数,必须是一个1维Tensor
loss.backward() # 误差反向传播,采用求导的方式,计算网络中每个节点参数的梯度,显然梯度越大说明参数设置不合理,需要调整
optimizer.step() # 优化采用设定的优化方法对网络中的各个参数进行调整
optimizer.zero_grad() # 清除优化器中的梯度以便下一次计算,因为优化器默认会保留,不清除的话,每次计算梯度都回累加
cnt += 1
print('Epoch:{0},Frame:{1}, train_loss {2}'.format(epoch, cnt*batch_size, loss/batch_size)) # 打印一个batch size的训练结果
torch.save(model.state_dict(), '{0}/model.pth'.format(model_cp)) # 训练所有数据后,保存网络的参数
if __name__ == '__main__':
train()
test.py
from getdata import DogsVSCatsDataset as DVCD
from network import Net
import torch
from torch.autograd import Variable
import numpy as np
import matplotlib.pyplot as plt
from PIL import Image
dataset_dir = '.' # 数据集路径
model_file = './model/model.pth' # 模型保存路径
def test():
model = Net() # 实例化一个网络
# model.cuda() # 送入GPU,利用GPU计算
model.load_state_dict(torch.load(model_file)) # 加载训练好的模型参数
model.eval() # 设定为评估模式,即计算过程中不要dropout
datafile = DVCD('test', dataset_dir) # 实例化一个数据集
print('Dataset loaded! length of train set is {0}'.format(len(datafile)))
index = np.random.randint(0, datafile.data_size, 1)[0] # 获取一个随机数,即随机从数据集中获取一个测试图片
img = datafile.__getitem__(index) # 获取一个图像
img = img.unsqueeze(0) # 因为网络的输入是一个4维Tensor,3维数据,1维样本大小,所以直接获取的图像数据需要增加1个维度
# img = Variable(img).cuda() # 将数据放置在PyTorch的Variable节点中,并送入GPU中作为网络计算起点
img = Variable(img)
out = model(img) # 网路前向计算,输出图片属于猫或狗的概率,第一列维猫的概率,第二列为狗的概率
print(out) # 输出该图像属于猫或狗的概率
if out[0, 0] > out[0, 1]: # 猫的概率大于狗
print('the image is a cat')
else: # 猫的概率小于狗
print('the image is a dog')
img = Image.open(datafile.list_img[index]) # 打开测试的图片
plt.figure('image') # 利用matplotlib库显示图片
plt.imshow(img)
plt.show()
if __name__ == '__main__':
test()