传送门:https://www.lintcode.com/ai/cats-and-dogs/overview
给出一张猫或狗的图片,识别出这是猫还是狗。
这种识别具有很重要的意义,比如:
Web服务为了进行保护,会防止一些计算机进行恶意访问或信息爬取,进而设立一些验证问题,这些验证问题对于人来说很容易做,但是对于计算机这很困难。这样的方法称为CAPTCHA(完全自动公开的图灵测试)或HIP(人类交互证明)。 HIP有很多用处,例如减少垃圾邮件,防止暴力破解密码等。
比较有名的Asirra(用于限制访问的动物图像识别)就是一个HIP,它会让用户识别图片信息,比如识别出图片中是猫还是狗。对于人来说这很容易,但是对于计算机很困难。以下是Asirra的一个例子:
寻找流浪宠物为其提供住所的网站——Petfinder.com,向微软研究院提供了超过三百万张猫和狗的图像,这些图片由美国各地成千上万的动物收容所手动分类。
对于要入侵的计算机,随机猜测一般是最简单的攻击方法。图片识别并不容易,因为图片之间不同的的背景,角度,姿势,亮度等都存在着巨大的差异,很难识别。
不过随着机器学习——尤其是神经网络的发展,这项工作精度可以达到60%以上。而60%分类器就已经能将12幅图像的猜测概率从1/4096提高到1/459。
这道题目会给大家上万张的图片作为训练集,你能在猫狗之间分辨出它们的差异吗?
卷积神经网络:在机器学习中,卷积神经网络是一种深度前馈人工神经网络,已成功地应用于图像识别。卷积神经网络是一种前馈神经网络,人工神经元可以响应周围单元,可以大型图像处理。卷积神经网络包括卷积层和池化层。卷积神经网络是近年发展起来,并引起广泛重视的一种高效识别方法。
灰度图像:灰度数字图像是每个像素只有一个采样颜色的图像。这类图像通常显示为从最暗黑色到最亮的白色的灰度,尽管理论上这个采样可以任何颜色的不同深浅,甚至可以是不同亮度上的不同颜色。灰度图像与黑白图像不同,在计算机图像领域中黑白图像只有黑白两种颜色,灰度图像在黑色与白色之间还有许多级的颜色深度。
双线性插值,又称为双线性内插。在数学上,双线性插值是有两个变量的插值函数的线性插值扩展,其核心思想是在两个方向分别进行一次线性插值。双线性插值作为数值分析中的一种插值算法,广泛应用在信号处理,数字图像和视频处理等方面。
评价
给出一张猫或狗的图片,识别出这是猫还是狗?
本题对于提交文件,将使用 准确率(accuracy) 作为评判标准。
准确率(accuracy)是用来衡量算法预测结果的准确程度,具体指测试集中算法预测正确的数量占总数的比例。
Accuracy=\frac{TP+TN}{TP+FN+FP+TN}Accuracy=TP+FN+FP+TNTP+TN
TP(True Positive)是将正类预测为正类的结果数目
FP(False Positive)是将负类预测为正类的结果数目
TN(True Negative)是将负类预测为负类的结果数目
FN(False Negative)是将正类预测为负类的结果数目
这是一个图像分类问题,在图像问题的处理上,我们通常使用卷积神经网络,一方面,相较于全连接层有更少的参数;另一方面,卷积操作能够很好的兼顾图像上所存在的局部数据关联信息。我们将整个项目分块解耦,分为Dataset、Model、Train、Test部分。当然这些都不是严格的代码规范,只是本人自行的代码风格,各位读者请自便。鉴于自己随意定义的网络效果实在太差,因此我们使用VGG11模型:
Dataset.py 该部分主要用于实现数据加载,框架要求继承DataSet类,并覆写__len__返回数据总个数,__getitem__指定索引返回数据和结果。
DogVSCatDataSet.py
#编写自己的数据集类
from torch.utils.data import Dataset
import cv2
import os
class DogCatSet(Dataset):
def __init__(self,file_path):#在初始化完成 文件名-类别标签映射 猫-0 狗-1
self.file_list = os.listdir(file_path)
self.image_label = []
for file_name in self.file_list:
parts = file_name.split(".")
if parts[0] == "cat":
self.image_label.append((os.path.join(file_path,file_name),0))
elif parts[0] == "dog":
self.image_label.append((os.path.join(file_path,file_name),1))
def __len__(self):
return len(self.image_label)
def __getitem__(self, item):
image_path , label = self.image_label[item]
img = cv2.imread(image_path)
img = cv2.resize(img, (448, 448), interpolation=cv2.INTER_AREA)
return img,label
[注]:VGG系列网络接受的输入是224×224×3,但是本题的图片width和height大多都在400左右浮动,因此我们统一resize到448×448,需要调整最后全连接层的输入尺度,另外,最终的全连接层部分从4096尺度直接变换到10的尺度跨越过大,我们加入一个中间尺度256作为缓冲。
Model.py 主要是定义模型
VGGModel.py
import torch.nn as nn
class VGG11(nn.Module):
def __init__(self):
super(VGG11,self).__init__()
self.conv3_64 = nn.Conv2d(3,64,3,1,1)
self.conv64_128 = nn.Conv2d(64,128,3,1,1)
self.conv128_256 = nn.Conv2d(128,256,3,1,1)
self.conv256_256 = nn.Conv2d(256, 256, 3, 1, 1)
self.conv256_512 = nn.Conv2d(256, 512, 3, 1, 1)
self.conv512_512 = nn.Conv2d(512, 512, 3, 1, 1)
self.fc1 = nn.Linear(512*14*14,4096)
self.fc2 = nn.Linear(4096,10)
self.MaxPool = nn.MaxPool2d(2, 2)
self.softmax = nn.Softmax()
self.relu = nn.ReLU()
def forward(self, x ):# 224*224*3
x = x.permute(0, 3, 1, 2)
x = self.MaxPool(self.relu(self.conv3_64(x))) # 448*448*3 -> 448*448*64 ->224*224*64
x = self.MaxPool(self.relu(self.conv64_128(x))) # 224*224*64 -> 224*224*128 ->112*112*128
x = self.relu(self.conv128_256(x)) # 112*112*128 -> 112*112*256
x = self.MaxPool(self.relu(self.conv256_256(x))) # 112*112*256 -> 112*112*256 -> 56*56*256
x = self.relu(self.conv256_512(x)) # 56*56*256 -> 56*56*512
x = self.MaxPool(self.relu(self.conv512_512(x))) # 56*56*512 -> 56*56*512 -> 28*28*512
x = self.relu(self.conv512_512(x)) # 28*28*512 -> 28*28*512
x = self.MaxPool(self.relu(self.conv512_512(x))) # 28*28*512 -> 28*28*512 -> 14*14*512
x = x.view(-1, 512*14*14)
x = self.relu(self.fc1(x))
x = self.softmax(self.fc2(x))
return x
踩坑1:PyTorch的卷积输入要求顺序为channel * height * width,而opencv读取的数据格式为height * width * channels,因此我们需要使用permute交换一下维度。
Train.py 主要是定义训练过程,输出损失值、准确度、保存网络等等,另外,优化器、损失函数的定义也在这里。
DogVSCat_VGG11_Train.py
import torch
from VGGModel import VGG11
vgg11 = VGG11().cuda()
from DogVSCatDataSet import DogCatSet
from torch.utils.data import DataLoader
#Dataloader接受Dataset对象并迭代产生一个batch的数据
train_set = DogCatSet("./train/train")
train_loader = DataLoader(train_set,batch_size=32,shuffle=True,num_workers=4)
#定义损失函数与优化器
import torch.nn as nn
loss_function = nn.CrossEntropyLoss()
import torch.optim as optim
optimizer = optim.SGD(vgg11.parameters(),lr=1e-3)
#迭代产生数据
for epoch in range(2000):
loss_sum = 0
Accuracy_sum = 0
count = 0
for idx , batch_data in enumerate(train_loader):
train = batch_data[0].float().cuda() #batch_size * x.size()
train.grad_requires = True
truth_label = batch_data[1].long().cuda()
out = vgg11(train)
loss = loss_function(out,truth_label)
optimizer.zero_grad()
loss.backward()
optimizer.step()
loss_sum += loss
count +=1
#带有梯度的Tensor如果转换为numpy将会丢失梯度
out_label = out.argmax(dim=1)
Result1 = out_label.cpu().numpy()
Result2 = truth_label.cpu().numpy()
Accuracy_sum += (Result1==Result2).sum()/len(Result1)
print("Epoch : {} , batch_id : {} ,Accuracy : {}".format(epoch,idx,(Result1==Result2).sum()/len(Result1)))
print("Epoch : {} , Loss : {} ,Accuracy : {}".format(epoch,loss_sum/count,Accuracy_sum/count))
torch.save(vgg11,"./vgg11_dogVScat.pkl")
踩坑2:PyTorch的CrossEntropyLoss交叉熵损失函数,接收的truth_label不需要是one-hot编码,只需要给出类别的索引即可。
Test.py 主要是读取保存在本地的网络文件-pkl,用于测试
#读取本地模型
import torch
model = torch.load("./vgg11_dogVScat.pkl").cuda()
#读取测试集
import os
file_list = os.listdir("./test/test")
file_list.sort()
#读取测试文件 推理后写回CSV
import csv
import cv2
# 1. 创建文件对象
f = open('./result.csv','w',encoding='utf-8')
# 2. 基于文件对象构建 csv写入对象
csv_writer = csv.writer(f)
# 3. 构建列表头
csv_writer.writerow(["id","label"])
#构建计数器 用于组织batch
images = []
i = 1
count = 0
for filename in file_list:
img = cv2.imread(os.path.join("./test/test",filename))
img = cv2.resize(img, (224, 224), interpolation=cv2.INTER_AREA)
img_tensor = torch.from_numpy(img)
img_tensor = img_tensor.expand((1,224,224,3))
count += 1
#将数据进行拼接
if len(images) == 0:
images = img_tensor
else:
images = torch.cat((images,img_tensor))
if count%32 == 0:#每组织出来32个数据 就进行一次推理
images = images.float().cuda()
print(images.shape)
out = model(images)
out_label = out.argmax(dim=1)
Result = out_label.cpu().numpy()
for row in Result:
csv_writer.writerow([i, row])
i = i + 1
images = images.cpu()
images = []
if len(images) != 0:
images = images.float().cuda()
out = model(images)
out_label = out.argmax(dim=1)
Result = out_label.cpu().numpy()
for row in Result:
csv_writer.writerow([i, row])
i = i + 1
# 5. 关闭文件
f.close()