简介

计算机视觉入门--图像分类简介及算法

图像分类的任务就是给定一个图像,正确给出该图像所属的类别。对于超级强大的人类视觉系统来说,判别出一个图像的类别是件很容易的事,但是对于计算机来说,并不能像人眼那样一下获得图像的语义信息。
计算机能看到的只是一个个像素的数值,对于一个RGB图像来说,假设图像的尺寸是32*32,那么机器看到的就是一个形状为3*32*32的矩阵,或者更正式地称其为“张量”(“张量”简单来说就是高维的矩阵),那么机器的任务其实也就是寻找一个函数关系,这个函数关系能够将这些像素的数值映射到一个具体的类别(类别可以用某个数值表示)。

算法

较为简单的算法

Nearest Neighbor

"Nearest Neighbor"是处理图像分类问题一个较为简单、直接、粗暴的方法:首先在系统中“记住”所有已经标注好类别的图像(其实这就是训练集),当遇到一个要判断的还未标注的图像(也就是测试集中的某个图像)时,就去比较这个图像与“记住的”图像的“相似性”,找到那个最相似的已经标注好的图像,用那个图像的类别作为正在分类的图像的类别,这也就是"Nearest Neighbor"名称的含义。

关于如何判断图像“最相似”,可以有多种方法,比如直接求相同位置像素值的差的总和(也就是两个图像的L1距离),也可以采用两个图像的L2距离,也就是先对相同位置像素值作差并求平方,然后对所有平方值求和,最后总体开方,准确的表述如下:记图像分别为I1I_1I2I_2I1kI^k_1表示图像I1I_1在k位置处的像素值,那么图像I1I_1I2I_2的L1和L2距离可以分别表示为:
d1(I1,I2)=kI1kI2kd_1(I_1, I_2) = \sum_k|I^k_1 - I^k_2| d2(I1,I2)=k(I1kI2k)2d_2(I_1, I_2) = \sqrt{\sum_k(I^k_1 - I^k_2)^2}

"Nearest Neighbor"容易实现,但是存在诸多缺点:

  1. 预测/测试的过程太慢,给定一个图像时,需要一个个去比较训练集中的图像,每预测一次的计算量都很大,特别是图片的尺寸较大时
  2. 预测/测试时,仍需要较大的空间去存储训练集
  3. 准确率不高,实验发现,该方法在CIFAR-10上只能取得38.59%左右的正确率(其实也不算太糟糕,因为总共有10个类别,如果完全随机猜的话,正确率只有10%),其实是因为该方法没有足够的理论支撑(只有一定的道理),显然的例子是:对于一个图像,如果将其中的物体向左平移一点获得一个新的图像,则这两个图像应该是相同的类别,但是利用"Nearest Neighbor"判断时,则有可能因为相同位置的像素值不再一样而判断错误(因为L1距离和L2距离都是计算相同位置像素值的接近程度,一平移后相同位置的像素值有可能会发生很大的改变)

Nearest Neighbor的代码实现

环境:Python 3.6 + PyTorch 1.0

import torch
import torchvision


class NearestNeighbor:
    def __init__(self):
        self.x_train = 0
        self.y_train = 0
    
    # 训练阶段:存储训练集
    def train(self, x_train, y_train):
        self.x_train = x_train
        self.y_train = y_train
    
    # 测试阶段:比较待分类的图像与所有训练图像的L1距离,取最小距离的图像的类别
    def test(self, x_test):
        L1_distance = torch.sum(torch.abs(x_test - self.x_train), 1)
        min_index = torch.argmin(L1_distance)
        return self.y_train[min_index]


if __name__ == '__main__':
    # 定义要对数据集的原始数据进行的变换,此处的变换是将图像转化为Tensor
    transform = torchvision.transforms.Compose([torchvision.transforms.ToTensor()])
    # 加载数据集
    trainset = torchvision.datasets.CIFAR10(root='./', train=True, download=False, transform=transform)
    trainloader = torch.utils.data.DataLoader(trainset, batch_size=1, shuffle=False)
    testset = torchvision.datasets.CIFAR10(root='./', train=False, download=False, transform=transform)
    testloader = torch.utils.data.DataLoader(testset, batch_size=1, shuffle=False)
    # 构造一个训练集和测试集的迭代器,用于遍历
    train_iterator = iter(trainloader)
    test_iterator = iter(testloader)

    # 训练集,x_train是所有图像像素形成的Tensor,y_train是所有图像的label
    # 将每一个形状为3*32*32的图像都展开成了长度为3*32*32的行向量
    x_train = torch.zeros((len(trainset), 3 * 32 * 32))
    y_train = torch.zeros((len(trainset), 1))
    for i, (image, label) in enumerate(train_iterator):
        x_train[i] = image.reshape(-1)
        y_train[i] = label

    nn = NearestNeighbor()
    nn.train(x_train, y_train)
    # 正确预测的数目、总数目
    right_num, total_num = 0, 0
    for i, (image, label) in enumerate(test_iterator):
        predict_label = nn.test(image.reshape(1, -1))
        if predict_label.item() == label.item():
            right_num += 1
        total_num += 1
    print('Accuracy is %.2f' % (right_num / total_num * 100) + '%')

K-Nearest Neighbor

"K-Nearest Neighbor"是很自然想到的一个在"Nearest Neighbor"基础上进行改进的方法,与"Nearest Neighbor"不同的是,"K-Nearest Neighbor"不再将“最接近”的那一个图像的类别作为预测图像的类别,而是选出K个与预测图像“最接近”的图像,看其中哪个类别占的比例最高,就将其作为预测图像的类别,这样可以在某种程度上增加预测模型的稳定性。
此处的K是一个hyper-parameter(超参数),也就是不能由模型学得,而是需要自己去设定,可以尝试的值如10、5、20等,具体选取哪个值可以通过Cross Validation(交叉验证法)来确定,详见机器学习的模型评估方法
"K-Nearest Neighbor"的代码实现只需要在与"Nearest Neighbor"的代码上作少量修改即可。

正确率较高的算法

Linear Classification

上面的"Nearest Neighbor"和"K-Nearest Neighbor"方法,都是直接比较图像的相似性,存在测试效率太低的问题,并且不能够提取图像的语义信息,导致错误率很高。
Linear Classification利用一个“全连接层”(Fully Connected Layer),输入图像的所有像素值,经过全连接层的运算后输出每个类别的“得分”,最终选取得分最高的类别作为图像的类别。

全连接层

先看一张图

计算机视觉入门--图像分类简介及算法

如图所示,x1,x2,x3x_1, x_2, x_3都是输入的像素值,真实的图像中,输入可能会有很多个,比如CIFAR-10的数据集,图像尺寸是3*32*32(3是RGB这3个通道),那么输入就有3*32*32个;w11,w12,w23w_{11}, w_{12}, w_{23}等表示权重,y1,y2,y3y_1, y_2, y_3是输出的每个类别的得分,如y1y_1表示猫这个类别的得分
输出的计算方式是:
y1=w11x1+w12x2+w13x3y_1 = w_{11}*x_1 + w_{12}*x_2 + w_{13}*x_3y2=w21x1+w22x2+w23x3y_2 = w_{21}*x_1 + w_{22}*x_2 + w_{23}*x_3y3=w31x1+w32x2+w33x3y_3 = w_{31}*x_1 + w_{32}*x_2 + w_{33}*x_3
将其向量化,令:
X=[x1  x2  x3]TX = [x_1\ \ x_2\ \ x_3]^TY=[y1  y2  y3]TY = [y_1\ \ y_2\ \ y_3]^TW=[w11w12w13w21w22w23w31w32w33]W = \left[ \begin{matrix} w_{11} & w_{12} & w_{13}\\ w_{21} & w_{22} & w_{23}\\ w_{31} & w_{32} & w_{33} \end{matrix} \right]
则有:
Y=WXY = W\cdot X
在最终实现时,还会给每个类别的得分加一个偏置量(bias),类似于原来是“正比例函数”,总是经过原点,现在加一个偏置,变成了普通的”一次函数“,不再一定经过原点,这样可以让表达式更加一般化,也就是表达能力更强,即:
y1=w11x1+w12x2+w13x3+b1y_1 = w_{11}*x_1 + w_{12}*x_2 + w_{13}*x_3 + b_1y2=w21x1+w22x2+w23x3+b2y_2 = w_{21}*x_1 + w_{22}*x_2 + w_{23}*x_3 + b_2y3=w31x1+w32x2+w33x3+b3y_3 = w_{31}*x_1 + w_{32}*x_2 + w_{33}*x_3 + b_3
同样,也可以将其向量化,为了简便起见,可以将偏置结合在输入X中,令:
X=[x1  x2  x3  1]TX = \left[x_1\ \ x_2\ \ x_3\ \ 1\right]^TY=[y1  y2  y3  1]TY = \left[y_1\ \ y_2\ \ y_3\ \ 1\right]^TW=[w11w12w13b1w21w22w23b2w31w32w33b3]W = \left[ \begin{matrix} w_{11} & w_{12} & w_{13} & b_1\\ w_{21} & w_{22} & w_{23} & b_2\\ w_{31} & w_{32} & w_{33} & b_3 \end{matrix} \right]
可以发现,Y=WXY = W\cdot X的展开结果与上面的非向量化的形式一致。

在PyTorch中,全连接层很容易实现,如下

import torchvision
import torch
from torch import nn


class LinearClassifier(nn.Module):
    def __init__(self):
        super(LinearClassifier, self).__init__()
        # 定义两个全连接层
        self.fc1 = nn.Linear(3*32*32, 200)
        self.fc2 = nn.Linear(200, 10)

    def forward(self, x):
        x = self.fc1(x)
        # sigmoid**函数
        x = torch.sigmoid(x)
        x = self.fc2(x)
        return x


if __name__ == '__main__':
    transform = torchvision.transforms.Compose([torchvision.transforms.ToTensor()])
    trainset = torchvision.datasets.CIFAR10(root='./', train=True, download=False, transform=transform)
    trainloader = torch.utils.data.DataLoader(trainset, batch_size=16, shuffle=False)
    testset = torchvision.datasets.CIFAR10(root='./', train=False, download=False, transform=transform)
    testloader = torch.utils.data.DataLoader(testset, batch_size=64, shuffle=False)

	# 定义一个线性分类的多分类网络
    linear_classifier = LinearClassifier()
    # 由于是多分类问题,所以用交叉熵作为损失函数
    criterion = nn.CrossEntropyLoss()
    # 优化器
    optimizer = torch.optim.Adam(linear_classifier.parameters())

    for epoch in range(20):
        print('EPOCH:', epoch, end=' ')
        total_loss = 0
        train_iterator = iter(trainloader)
        for batch in train_iterator:
            batch[0] = batch[0].reshape(batch[0].shape[0], -1)
            scores = linear_classifier(batch[0])
            loss = criterion(scores, batch[1])
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
            total_loss += loss
        print(total_loss, end=' ')

        right_num = 0
        test_loader = iter(testloader)
        for batch in testloader:
            scores = linear_classifier(batch[0])
            predict = torch.argmax(scores, dim=1)
            right_num += torch.sum(predict == batch[1])
        print('Accuracy: %.2f' % (right_num.item() / len(testset) * 100) + '%')

本以为该方法准确率会比较高,但是发现正确率还是在30%-40%左右,可能是因为2个全连接层的模型表达能力不够强,还是不能够获得图像的语义信息。

Convolutional Neural Network(CNN,卷积神经网络)

利用CNN可以进一步提高图像分类的正确率,甚至已经可以超过人类,关于CNN的细节以及原理,在后面的文章中会详细写,此处只给出用CNN进行图片分类的代码
环境:Python 3.6 + PyTorch 1.0

import torchvision
import torch
from torch import nn


class LinearClassifier(nn.Module):
    def __init__(self):
        super(LinearClassifier, self).__init__()
        self.conv1 = nn.Conv2d(3, 8, kernel_size=3, stride=1, padding=1).cuda()
        self.pool1 = nn.MaxPool2d(2, 2).cuda()
        self.conv2 = nn.Conv2d(8, 16, kernel_size=3, stride=1, padding=1).cuda()
        self.pool2 = nn.MaxPool2d(2, 2).cuda()
        self.conv3 = nn.Conv2d(16, 32, kernel_size=3, stride=1, padding=1).cuda()
        self.pool3 = nn.MaxPool2d(2, 2).cuda()
        self.fc1 = nn.Linear(32*4*4, 100).cuda()
        self.fc2 = nn.Linear(100, 10).cuda()

    def forward(self, x):
        x = self.conv1(x)
        x = torch.relu(x)
        x = self.pool1(x)
        x = self.conv2(x)
        x = torch.relu(x)
        x = self.pool2(x)
        x = self.conv3(x)
        x = torch.relu(x)
        x = self.pool3(x)
        x = self.fc1(x.reshape(x.shape[0], -1))
        x = self.fc2(x)
        return x


if __name__ == '__main__':
    transform = torchvision.transforms.Compose([torchvision.transforms.ToTensor(), ])
    trainset = torchvision.datasets.CIFAR10(root='./', train=True, download=False, transform=transform)
    trainloader = torch.utils.data.DataLoader(trainset, batch_size=32, shuffle=False)
    testset = torchvision.datasets.CIFAR10(root='./', train=False, download=False, transform=transform)
    testloader = torch.utils.data.DataLoader(testset, batch_size=32, shuffle=False)

    linear_classifier = LinearClassifier()
    criterion = nn.CrossEntropyLoss()
    optimizer = torch.optim.SGD(linear_classifier.parameters(), lr=0.01)

    for epoch in range(20):
        print('Epoch:', epoch, end='  ')
        total_loss = 0
        train_iterator = iter(trainloader)
        for batch in train_iterator:
            batch[0] = batch[0].cuda()
            batch[0].requires_grad = False
            scores = linear_classifier(batch[0])
            loss = criterion(scores, batch[1].cuda())
            optimizer.zero_grad()
            loss.backward()
            optimizer.step()
            total_loss += loss
        print('Loss =', total_loss.item())

        test_iterator = iter(testloader)
        right_num = 0
        for batch in test_iterator:
            scores = linear_classifier(batch[0].cuda())
            predict = torch.argmax(scores, dim=1)
            right_num += torch.sum(predict == batch[1].cuda())
        print('Accuracy: %.2f' % (right_num.item() / len(testset) * 100) + '%')

实验发现,具有3个卷积层的网络,可以将图片分类的精度提升到70%左右,相较于前面的方法已经有了很大的进步,但其实利用深度神经网络还可以表现的更好(所谓的“深度神经网络”其实就是让网络具有更多的卷积层,变得“更深”),一些深度的网络已经可以将图片分类的精度提升到了95%以上。后面的文章会介绍CNN的原理细节。

相关文章: