初级项目_图像分类

任务:编写一个图像分类系统,能够对输入图像进行类别预测。具体的说,利用数据库的2250张训练样本进行训练;对测试集中的2235张样本进行预测。

数据库说明:scene_categories数据集包含15个类别(文件夹名就是类别名),每个类中编号前150号的样本作为训练样本,15个类一共2250张训练样本;剩下的样本构成测试集合。

设计文档撰写说明:介绍算法整体流程,各个函数的功能说明,函数的输入参数说明,给出最终的混淆矩阵,分析实验中各个环节和各个参数对最终性能的影响。


参考链接(Google搜索“词袋模型+SVM图像分类”):


2023/4/28 11:20 能找到的参考非常少,先暂时看这个参考,理解之后再找别的也来得及;

2023/4/28 21:17 这个代码重要的部分基本都缺失了,但是行文结构以及后面的混淆矩阵可视化等可以借鉴参考,还是需要找一个可以正常运行的代码;

2023/4/28 21:58 有没有想过一个问题就是为什么你找不到代码?一方面是检索有问题,还有一方面就是你根本就没做好功课所以做不来是很正常的一件事,可以先看视频把基础打牢(至少知道流程)之后再完成代码;

2023/4/29 20:17 经过调试现在代码已经能够运行了,接下来需要代码部分做的就是将调用的库函数变成手动实现、使用网格搜索、K折交叉验证等进行超参寻优,然后把报告写了就行,参考的文档就是“概述”和“算法”;

2023/5/6 0:13 今天完成了对于代码的创新部分,也就是自定义了一些函数,现在还剩下最后的调整参数,当作第三部分即可;


一、实验背景

1.分类概述

随着计算机与互联网技术以及数字图像获取技术的快速发展,海量的数字图像出现在互联网上及人们周边的生活中。依靠传统的人工方式对图像进行分类、组织和管理非常耗时耗力,所以希望能够通过计算机对图像中的目标内容进行自动地分析处理,从而将图像数据快速、规范、自动地进行组织、归类和管理。

早期的图像分类主要依赖于文本特征,采用人工方式为图像标注文本,使用的是基于文本的图像分类模式。由于图像标注需要人为地辨识并为其选定关键字,故其分类的效果不是非常理想,且耗时严重。随着计算机技术和数字化图像技术的发展,图像库的规模越来越大,人工标注的方式对图像进行分类已不可能,人们开始逐渐将研究的重点转移到基于图像内容分析的自动分类研究上。

基于内容的图像分类技术不需要进行人工标注的语义信息,而是直接对图像所包含的信息进行处理和分析,利用图像底层视觉特征来进行图像分类。图像分类技术研究是一个集中了机器学习、模式识别、计算机视觉和图像处理等多个研究领域的交叉研究方向。

图像分类任务的常用方法包括:

  • 传统机器学习方法:如分类器、决策树、随机森林等。

    • 逻辑回归:用于二分类问题,可以使用sigmoid等激活函数进行参数学习。

    • 支持向量机:用于多分类问题,需要正则化,并可以使用核等技巧进行超参数优化。

  • 深度学习模型:其中最常用的是卷积神经网络,如卷积神经网络(CNN)、循环神经网络(RNN)等。

本次实验要求使用支持向量机对图像进行分类,下面是使用支持向量机实现图像分类的一般步骤:

  1. 数据预处理:将图像转换为二维数组,并对每个像素进行标准化处理,以消除像素之间的尺度差异。
  2. 构建SVM分类器:使用支持向量机算法构建SVM分类器,通常使用径向基函数(RBF)或多项式核(Polynomial Kernel)等核函数。
  3. 训练SVM分类器:使用训练数据训练SVM分类器,通常使用交叉验证等方法进行模型选择和参数优化。
  4. 使用SVM分类器进行预测:使用训练好的SVM分类器对新的图像进行分类预测。

图像分类常用的是深度学习的模型(毕竟诸如CNN这种能够自动提取图像的局部特征)。但是,这并不妨碍SVM是一个优秀的算法,大量的实践都证明它对于小批量数据集的分类、预测都能够得到较好的效果。

2.任务描述

我们手里的scene_categories数据集包含15个类别(文件夹名就是类别名),每个类中编号前150号的样本作为训练样本,则15个类一共2250张训练样本,剩下的样本构成测试集合。

图片文件的组织方式为:15-Scene文件夹作为根目录,文件夹下包括15个类别的文件夹,每个文件夹下面用于存储具体的照片文件。

主目录(15-Scene)

子目录(14)

实验的目的是,利用SVM训练一个分类器,当我们输入一张图片时,返回图片的所属类别(本次实验中图像的类别就是其子目录的文件名,从00一直到14共15个类别)。

要实现基于词袋模型的图像分类,大致分为如下四步:

  1. 特征提取与描述子生成:一般选择SIFT特征提取器,SIFT特征具有放缩、旋转、光照不变性,同时兼有对几何畸变,图像几何变形的一定程度的鲁棒性;
  2. 词袋生成:词袋生成基于描述子数据的基础上,生成一系列的向量数据,最常见就是首先通过K-Means实现对描述子数据的聚类分析,一般会分成K个聚类、得到每个聚类的中心数据,就生成了K单词,根据每个描述子到这些聚类中心的距离,决定了它属于哪个聚类,这样就生成了图像的直方图表示数据。
  3. SVM分类训练与模型生成:使用SVM进行数据的分类训练,得到输出模型;
  4. 模型使用预测:加载预训练好的模型,使用模型在测试集上进行数据分类预测;

3.词袋模型

词袋模型BoW在NL和CV领域都有提到,这里我们主要介绍词袋模型在CV领域的使用即BoVW。

词袋模型最初用于文本分类中,然后逐步引入到了图像分类任务中。在文本分类中,文本被视为一些不考虑先后顺序的单词集合。而在图像分类中,图像被视为是一些与位置无关的局部区域的集合(一袋拼图),因此这些图像中的局部区域就等同于文本中的单词。在不同的图像中,局部区域的分布是不同的(一袋“马”的拼图肯定和一袋“牛”的拼图不同)。因此,可以利用提取的局部区域的分布对图像进行识别。

注意这里的“局部区域”,一般指的是具有代表性的图像区域,也就是图像特征,一般使用SIFT提取器提取(当然直接将图像均分为局部区域也可以,但这种做法得到的拼图过多影响后续处理)

图像分类和文本分类的不同点在于,在文本分类的词袋模型算法中,字典是已存在的,不需要通过学习获得;而在图像分类中,词袋模型算法需要通过监督或非监督的学习来获得视觉词典。造成这种差异的原因是,图像中的视觉特征不像自然语言中的单词那样定义明确和被理解。因此,有必要学习一种能够有效地表示图像中特征的视觉词典。

视觉词袋(BoVW,Bag of Visual Words)模型,是“词袋”(BoW,Bag of Words)模型从自然语言处理与分析领域向图像处理与分析领域的一次自然推广。对于任意一幅图像,BoVW模型提取该图像中的基本元素,并统计该图像中这些基本元素出现的频率,用直方图的形式来表示。通常使用“图像局部特征”来类比BoW模型中的单词,如SIFT、SURF、HOG等特征,所以也被称之为视觉单词模型

图像BoVW模型表示的直观示意图如图所示

利用BoVW模型表示图像,获得图像的全局直方图表示,主要有四个关键步骤:

Step 1:图像局部特征提取(Image Local Features Extrication)。根据具体应用考虑,综合考虑特征的独特性、提取算法复杂性、效果好坏等选择特征。利用局部特征提取算法,从图像中提取局部特征。 – SIFT特征提取器

Step 2:视觉词典构造(Visual Dictionary Construction)。利用上一步得到的特征向量集,抽取其中有代表性的向量,作为单词,形成视觉词典。一般是从图像库中选取一部分来自不同场景或类别的图像来组成训练图像集,并提取其局部特征,然后对训练图像的所有局部特征向量通过适当的去冗余处理得到一些有代表性的特征向量,将其定义为视觉单词。通常所采用的处理方法是对训练图像的所有局部特征向量进行聚类分析,将聚类中心定义为视觉单词。所有视觉单词组成视觉词典,用于图像的直方图表示。 – K-means聚类

Step 3:特征向量量化(Feature Vector Quantization)。BoVW模型采用向量量化技术实现,向量量化结果是将图像的局部特征向量量化为视觉单词中与其距离最相似的视觉单词。向量量化过程实际上是一个搜索过程,通常采用最近邻搜索算法,搜索出与图像局部特征向量最为匹配的视觉单词。 – KNN最近邻聚类

Step 4:用视觉单词直方图表示图像,也称为量化编码集成(Pooling)。一幅图像的所有局部特征向量被量化后,可统计出视觉词典中每个视觉单词在该图像中出现的频数,得到一个关于视觉单词的直方图,其本质是上一步所得量化编码的全局统计结果,是按视觉单词索引顺序组成的一个数值向量(各个元素的值还可以根据一定的规则进行加权)。该向量即为图像的最终表示形式。


Q:K-means聚类和KNN最近邻有什么区别?

A:K-Means和K-Nearest Neighbors(KNN)是两种不同的机器学习算法,用于不同类型的任务,有以下主要区别:

  1. 任务类型:
    • K-Means:K-Means是一种聚类算法,用于将数据分成不同的组或簇,以便相似的数据点在同一组中。
    • KNN:KNN是一种分类和回归算法,用于根据数据点周围的邻居来预测新数据点的类别或数值。
  2. 目标:
    • K-Means:K-Means的目标是将数据分成K个簇,其中每个簇具有相似的数据点,以最小化簇内数据点的差异。
    • KNN:KNN的目标是根据最接近的K个邻居的标签或数值来预测新数据点的标签或数值。
  3. 学习过程:
    • K-Means:K-Means是一种无监督学习算法,它根据数据点之间的距离和相似性来组织数据。
    • KNN:KNN可以是有监督或无监督的,但通常用于有监督学习,其中需要已知数据点的标签或数值来进行预测。
  4. 超参数:
    • K-Means:K-Means需要选择簇的数量K作为超参数,通常需要一些启发式方法来确定最佳值。
    • KNN:KNN需要选择K(最近邻居的数量)以及距离度量方法作为超参数。
  5. 应用:
    • K-Means:K-Means常用于图像分割、客户细分、无监督特征学习等聚类任务。
    • KNN:KNN常用于分类任务,如文本分类、图像分类、推荐系统等,以及回归任务。

-Means旨在发现数据内部的结构,而KNN则用于根据最近的邻居来做出预测。


二、程序设计

本项目是基于词袋模型的图像分类,涉及到的知识点有SIFT特征提取、K-means聚类以及SVM支持向量机等,本部分将分模块对整个程序进行介绍。

1.流程说明

“词袋模型+SVM图像分类”的整个程序的流程主要分为以下几部分:

  1. 加载并处理数据
  2. 生成词袋模型
    • SIFT局部特征提取
    • K-Means聚类构造视觉词典
    • KNN最近邻算法进行特征向量量化
    • 量化编码集成得到BOW词袋
  3. 训练SVM并进行预测评估

2.类介绍

自定义类主要帮助理解程序和处理数据,包括BOWKMeansTrainer类(用于创建聚类器)、FLANN 的匹配器类、BOW 图像描述符提取器类、SVM多分类器。

2.1 BOWKMeansTrainer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# BOWKMeansTrainer类,用于创建聚类器
class BOWKMeansTrainer:
def __init__(self, k, criteria=(cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 0.01), attempts=10, flags=cv2.KMEANS_PP_CENTERS):
self.k = k # 聚类数
self.criteria = criteria # 终止条件
self.attempts = attempts # 重复试验次数
self.flags = flags # 初始中心选择方法
self.descriptors = [] # 描述符

def add(self, descriptors):
self.descriptors.extend(descriptors) # 将描述符添加到列表中

def cluster(self):
descriptors = np.float32(self.descriptors) # 将描述符转换为 float32 类型
compactness, labels, centers = cv2.kmeans(descriptors, self.k, None, self.criteria, self.attempts, self.flags)

return centers

BOWKMeansTrainer类使用k均值算法来创建视觉词袋模型BoVW:

  • init方法使用参数初始化类,如k(簇的数量)、criteria(停止迭代的终止标准)、attempts(使用不同的初始质心执行算法的次数)和flags(用于选择初始中心的算法)
  • add方法用于将描述符添加到descriptors描述符列表中
  • cluster聚类方法将描述符转换为float32类型,然后调用kmeans方法进行聚类并返回聚类中心

这里我们直接使用的是opencv库中的kmeans函数,关于kmeans的手动实现代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def kmeans(data, K, criteria, attempts):
# 初始化聚类中心
centers = np.float32(data[np.random.choice(len(data), K, replace=False)])
for _ in range(attempts):
# 计算每个数据点到各聚类中心的距离
distances = np.linalg.norm(data[:, np.newaxis, :] - centers, axis=2)
# 根据距离计算每个数据点所属的聚类
labels = np.argmin(distances, axis=1)

# 更新聚类中心
new_centers = np.array([data[labels == k].mean(axis=0) for k in range(K)])

# 计算聚类中心的移动距离
criteria_diff = np.linalg.norm(new_centers - centers)

# 判断是否满足迭代终止条件
if criteria_diff < criteria:
break
centers = new_centers
# 计算累计误差
distances = np.linalg.norm(data[:, np.newaxis, :] - centers, axis=2)
errors = np.min(distances, axis=1)
total_error = np.sum(errors)
# 返回聚类中心、标签和累计误差
return centers, labels, total_error

k-means迭代算法会消耗大量内存,这将导致电脑非常的卡并显示内存不足,而使用cv2提供的kmeans不会出现这种情况。

2.2 FlannBasedMatcher

介绍词袋模型的时候说过,可以使用KNN最近邻聚类算法进行特征向量量化,简单理解就是将图像的局部特征向量量化为视觉单词中与其距离最相似的视觉单词,即采用最近邻搜索算法,搜索出与图像局部特征向量最为匹配的视觉单词。

1
2
3
4
5
6
7
8
9
10
11
12
13
# FLANN 的匹配器类
class FlannBasedMatcher:
def __init__(self, flann_params):
self.flann_params = flann_params

def match(self, descriptors1, descriptors2):
flann = cv2.FlannBasedMatcher(self.flann_params, {}) # 创建 FLANN 匹配器
matches = flann.knnMatch(descriptors1, descriptors2, k=2) # 使用 KNN 算法进行匹配
good_matches = [] # 保存匹配结果
for m, n in matches: # 通过 Lowe's ratio test 过滤匹配结果
if m.distance < 0.7 * n.distance: # 阈值处理
good_matches.append(m)
return good_matches

FlannBasedMatcher类用于在高维空间中执行快速近似最近邻搜索:

  • init方法将flan_params作为参数,并将其设置为实例变量;
  • match方法接受参数descriptors1和descriptors2并创建一个以self.flann_params和一个空字典为参数的cv2.FlannBasedMatcher对象。然后,使用flann对象的knnMatch方法来匹配作为参数传递的两个图像的描述符。使用knnMatch方法返回一个列表,其中每个内部列表包含两个匹配项。然后该方法根据阈值距离过滤匹配,只保留被认为是“良好匹配”的匹配。最后,返回匹配的列表。

2.3 BOWImgDescriptorExtractor

BOWImgDescriptorExtractor类用于从图像中提取单词袋(BOW)描述符,主要思想就是使用SIFT特征提取器提取关键点的描述符,然后使用FLANN将描述符分配给最近的视觉单词,最后得到的BOW描述符就是表示图像内容的视觉单词的直方图(实际上就是整个BOW构建的流程)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# BOW 图像描述符提取器类
class BOWImgDescriptorExtractor:
def __init__(self, sift, flann):
self.sift = sift # SIFT 特征提取器
self.flann = flann # FLANN 匹配器
self.centers = None

def setVocabulary(self, centers): # 设置视觉词典
self.centers = centers

def compute(self, img, keypoints): # 计算图像的 BOW 描述符
if self.centers is None:
raise ValueError("You need to set vocabulary first!")
else:
# 特征提取器提取图像的 SIFT 特征
keypoints, descriptors = self.sift.compute(img, keypoints)

# 使用FLANN 匹配器进行匹配
matches = self.flann.match(descriptors, self.centers)
bow_descriptor = np.zeros(len(self.centers)) # 生成直方图
for match in matches:
bow_descriptor[match.trainIdx] += 1

# 归一化
norm = np.linalg.norm(bow_descriptor)
if norm != 0:
bow_descriptor /= norm

return bow_descriptor

BOWImgDescriptorExtractor类接受参数sift(sift类对象)和flann(flann类对象):

  • setVocabulary方法用于为单词袋模型设置vocabulary;
  • compute方法接受参数img和keypoints,即一组关键点。先使用sift方法提取图像的关键点,接着使用flann将每个描述符分配给最近的视觉单词,并通过计算分配给每个视觉单词的描述符数量来生成BOW,最后BOW描述符被归一化并返回;

2.4 MultiClassSVM

MultiClassSVM类是一个多分类SVM的简单实现,主要包含fit拟合方法和predict预测方法。

1
2
3
4
5
6
7
def __init__(self, C=1.0, lr=0.01, num_iters=1000, tol=1e-4):
self.C = C # 惩罚系数
self.lr = lr # 学习率
self.num_iters = num_iters # 迭代次数
self.tol = tol # 容忍度
self.weights = None # 权重
self.bias = None # 偏置
  • self.C参数控制最大化裕度和最小化分类误差之间的权衡;
  • self.lr参数控制梯度下降优化的学习速率;
  • self.tool参数控制优化算法的收敛容差;
  • 最终训练的权重和偏差分别存储在self-weights和self-bias中;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def fit(self, X, y):
num_samples, num_features = X.shape # 样本数,特征数
num_classes = len(np.unique(y)) # 类别数
self.weights = np.zeros((num_classes, num_features)) # 初始化权重
self.bias = np.zeros((1, num_classes)) # 初始化偏置
for i in range(num_classes):
y_copy = np.where(y == i, 1, -1) # y_copy变量用于计算误差,并基于错误分类的样本更新权重和偏差
w = np.zeros(num_features)
b = 0
for j in range(self.num_iters):
# 梯度下降
error = 1 - y_copy * (np.dot(X, w) - b) # 计算误差
dw = np.zeros(num_features) # 初始化权重
for k in range(num_samples):
if error[k] > 0: # 如果误差大于0,更新权重
dw += self.C * y_copy[k] * X[k]
w_old = np.copy(w) # 保存旧权重
w -= self.lr * (w - dw) # 更新权重
db = np.sum(self.C * y_copy * error < 1) # 更新偏置
b -= self.lr * db
# 如果权重变化小于容忍度,停止迭代
if np.sum(np.abs(w - w_old)) < self.tol:
break
self.weights[i] = w
self.bias[0][i] = b

前面提到过,SVM本身作为二分类器,如果要使其执行多分类任务,需要一些方法,这里使用的是One-vs-All也就是一对多的方法。

fit方法在num_classes上进行迭代,并使用one-vs-all方法为每个类创建一个二分类问题。在循环内,fit方法将权重和偏差初始化为零,并执行梯度下降以优化SVM目标函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def predict(self, X):
# 计算样本数
num_samples = X.shape[0]
# 初始化class_scores数组,用0填充
class_scores = np.zeros((num_samples, len(self.weights)))
# 迭代self.weights和self.bias[0]的每个元素。每次迭代都使用X和w的点积减去b以计算每个类的分数,并将结果存储在class_scores数组中
for i, (w, b) in enumerate(zip(self.weights, self.bias[0])):
class_scores[:, i] = np.dot(X, w) - b
# 使用np.argmax为每个样本找到最高分数的索引,并将其存储在pred数组中
pred = np.argmax(class_scores, axis=1)
for i in range(num_samples):
for j in range(len(self.weights)):
# 检查预测类的分数是否大于或等于1,如果大于或等于,则转到下一个样本
if class_scores[i, j] >= 1:
continue
# 如果预测的类与j的当前迭代相同,那么将继续进行下一次迭代
if j == pred[i]:
continue
# 如果预测类的得分与j的当前迭代的得分之间的差小于1,则将样本的预测设置为j
if class_scores[i, pred[i]] - class_scores[i, j] < 1:
pred[i] = j
break
return pred

该预测方法与实际上sklearn的svm库中的预测方法相比简单的多,但是也比仅仅选择输出最高分那种行为要稍微好点,主要体现在细化了对接近决策边界的样本的预测。

predict方法返回一个pred数组,该数组包含了每个样本的预测类的索引。

3.模块介绍

在main.py中展示了程序整体流程以及各个模块的功能介绍

1
2
3
4
5
6
7
8
9
10
11
12
# 数据处理
categories, train_data, test_data, train_labels, test_labels = utils.load_data(img_path)
# 提取SIFT特征
sift, train_descriptors = utils.extract_sift_features(train_data)
# 生成视觉词典
codebook = utils.generate_codebook(train_descriptors)
# 生成词袋
bow,features = utils.bow_features(codebook, sift, train_data)
# 训练 SVM 分类器
svm = utils.train_svm(features, train_labels)
# 测试模型
utils.test_model(svm,test_data,test_labels,bow,sift)

3.1 数据加载

数据集分析:scene_categories数据集包含15个类别(文件夹名就是类别名),其中每个类中编号前150号的样本作为训练样本,则15个类一共2250张训练样本;剩下的样本构成测试集合,也就是剩余的2235张图像作为测试图像。

如何划分得到训练集和数据集是主要问题.基本思想很简单,依次遍历十五个目录,选取每个目录的前150张图像作为训练数据,同时将其文件名作为目录名,然后按照同样的方法将剩余的文件加入测试集中即可,核心代码如下(利用双层循环依次遍历子目录中的每个文件)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def load_data(data_path):
categories = os.listdir(data_path) # 获取所有类别
train_data = [] # 训练数据集
train_labels = [] # 训练标签集
test_data = [] # 测试数据集
test_labels = [] # 测试标签集

# 遍历一个目录及其子目录中的所有文件和文件夹
# os.walk()函数返回一个三元组(dirpath, dirnames, filenames)
# 其中dirpath是当前遍历到的文件夹路径,dirnames是该文件夹下所有子文件夹的名称列表,filenames是该文件夹下所有文件的名称列表
for path, dirs, files in os.walk(data_path):
for i, file in enumerate(files): # 循环遍历当前子目录中的每个文件,并为其分配一个唯一的索引 i
if (i < 150): # 当前文件是否应包含在训练集(前 150 个文件)或测试集(剩余文件)中
train_data.append(os.path.join(path, file)) # 将当前文件的路径添加到训练数据集中
else:
test_data.append(os.path.join(path, file))
# 检查当前子目录是否包含任何文件(即图像)
if (len(files) > 0):
# 将类别(即子目录名称)添加到适当的列表中,每个类别标签重复适当次数(150 次用于训练集,其余次数用于测试集)
train_labels.extend([path.split('\\')[-1]] * 150)
test_labels.extend([path.split('\\')[-1]] * (len(files) - 150))
categories = categories
train_data = train_data
test_data = test_data
train_labels = train_labels
test_labels = test_labels

return categories, train_data, test_data, train_labels, test_labels

数据加载模块将数据库中的所有文件加载并划分为训练集和测试集,同时保留了对应的标签以及所有类别。

其中的categories保存所有的类别

train_data是一个列表,保留了所有训练图像的路径,查看列表中第一个图像的路径如下

train_labels是一个列表(长度为训练集的长度即2250),保留了训练图像的对应的标签(标签以字符串的形式存储)

3.2 SIFT特征提取

SIFT特征提取算法因为专利保护等原因我们很难获取它的源码,但是SIFT特征提取在CV领域是一个非常重要的算法,因此理解它非常有必要。

SIFT全称为Scale Invariant Feature Transform尺度不变特征变换,SIFT特征对于旋转、尺度缩放、亮度变化等保持不变性,是一种非常稳定的局部特征;

SIFT特征检测的步骤主要分为如下四个步骤:

  • 尺度空间的极值检测:搜索所有尺度空间上的图像,通过高斯微分函数来识别潜在的对尺度和选择不变的兴趣点;
  • 特征点定位:在每个候选的位置上,通过一个拟合精细模型来确定位置尺度,关键点的选取依据他们的稳定程度;
  • 特征方向赋值:基于图像局部的梯度方向,分配给每个关键点位置一个或多个方向,后续的所有操作都是对于关键点的方向、尺度和位置进行变换,从而提供这些特征的不变性;
  • 特征点描述:在每个特征点周围的邻域内,在选定的尺度上测量图像的局部梯度,这些梯度被变换成一种表示,这种表示允许比较大的局部形状的变形和光照变换;

因为SIFT算法的实现难度较大,所以本项目中我们借助opencv的库中的SIFT算法来实现对图像关键点的特征描述符的提取任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def extract_sift_features(data_path):
# 创建 SIFT 特征提取器
sift = cv2.SIFT_create()
# 特征描述符
descriptors = []
# 遍历训路径中的所有图像
print('提取图像特征中...')
for img_path in tqdm(data_path):
img = cv2.imread(img_path) # 加载图像
feature = sift.detect(img) # 检测图像中的关键点
feature, descriptor = sift.compute(img, feature) # 计算每个关键点的特征描述符,并将它们存储在 descriptor 变量中
descriptors.append(descriptor) # 将 descriptor 变量中存储的特征描述符添加到 descriptors 列表中
print('图像特征提取完成')
return sift, descriptors

基本流程为创建sift实例后,对训练集中的所有图像使用detect方法提取其中的关键点(简单来说就是有特征的点,比如人的两个耳朵、鼻子等这些像素区域),提取完关键点后在这些关键点上生成特征描述符(这个描述符可以认为就是一种对图像区域的高维表示),依次对训练集中的每个图像的进行上述行为,最后我们得到了一个descriptors特征描述符列表,其形式如下,长度为训练集长度2250

3.3 词典生成

前面已经提到过,图像分类和文本分类的不同点在于,在文本分类的词袋模型算法中,字典是已存在的,不需要通过学习获得;而在图像分类中,词袋模型算法需要通过监督或非监督的学习来获得视觉词典。造成这种差异的原因是,图像中的视觉特征不像自然语言中的单词那样定义明确和被理解。因此,有必要学习一种能够有效地表示图像中特征的视觉词典。

如何学习得到一个视觉词典呢?一个非常直观的想法就是将前面提取到的所有特征描述符都放在一起,然后使用诸如k-means聚类的方法对其进行聚类,当我们指定了类别k的时候,聚类结束就会得到最终的k个类,我们只需要选取这k个类对应的特征描述符作为词典中的单词即可(因为k-means就是一种寻找相似性的算法,本质上每个聚类中心都是有代表性的,可以代表其周围一定范围内的点的某些特征)。

需要注意的是,使用k-means聚类算法进行词典生成的时候需要有一定的先验知识,也就是指定词典的大小最好在特征描述符的1/10~1/100,词典过大会导致粒度太细,比如人的左鼻孔和右鼻孔分别作为两个单词,这完全没必要;词典过小会导致粒度过大,比如人的鼻子和狗的鼻子作为同一个单词,会造成分类的错误。

1
2
3
4
5
6
7
8
9
def generate_codebook(descriptor_list, n_clusters=50):
kmeans_trainer = BOWKMeansTrainer(n_clusters) # 创建 BOW KMeans 聚类器
print('生成视觉词典中...')
for descriptor in tqdm(descriptor_list): # # 将每个特征描述符添加到 k-means 聚类器对象中
kmeans_trainer.add(descriptor)
# 使用 k-means 聚类器对象对特征描述符执行 k-means 聚类,并返回结果词汇表(即聚类中心)
codebook = kmeans_trainer.cluster()
print('视觉词典生成完成')
return codebook

词典生成完毕后可以查看其形式,voc的长度根据聚类的中心数目而定,比如n_clusters=250则len(voc)为250。voc是一个包含列表的列表

其每一个元素都是128维的向量

3.4 词袋生成

拥有了词典之后就可以生成词袋了,BOW的核心实际上就是根据词典中的单词来表示图像。可以认为,词典中的单词都是之前2250个SIFT特征描述符中筛选出的k个描述符,那么接下来我们要做的事情就是计算其他2250-k个SIFT描述符与这k个描述符的距离,选取最近的那个描述符作为它的表示(比如人的“鼻子”是词典中的一个单词,那么计算得到“左鼻孔”和“右鼻孔”离“鼻子”这个单词最近,那么就将“左鼻孔”和“右鼻孔”统统用“鼻子”这个单词来表示)。要实现上述行为有一个很直观的算法可以使用,那就是knn最近邻算法,在flann库中集成了这一算法,可以帮助我们很快的实现匹配器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def bow_features(voc,sift,train_data):
flann_params = dict(algorithm=1, tree=5) # FLANN 匹配器参数
flann = cv2.FlannBasedMatcher(flann_params) # 创建 FLANN 匹配器
bow = cv2.BOWImgDescriptorExtractor(sift, flann) # 创建 BOW 图像描述符提取器(使用自定义的类则在之后需要处理数据维度)
# 将从 k-means 聚类中获得的词汇表设置为 BOW 描述符提取器的词汇表
bow.setVocabulary(voc)
# 创建一个空列表来存储训练图像的 BOW 特征
train_features = []
print('生成词袋中...')
for img_path in tqdm(train_data): # 遍历训练图像
img = cv2.imread(img_path)
# 使用 BOW 描述符提取器和 SIFT 特征提取器提取当前图像的 BOW 特征,并将生成的特征向量添加到 train_features 列表中
train_features.extend(bow.compute(img, sift.detect(img)))
print('词袋生成完成')
return bow,train_features

基本的流程就是利用我们已经获得的voc词汇表,逐个遍历训练集中的图像,因为前面使用的sift特征描述符生成的词典,所以这里我们还是需要借助sift特征提取器对图像进行特征提取,提取完毕特征后借助BOW 图像描述符提取器中flann提供的匹配功能提取得到当前图像的BOW特征,并将生成的特征向量添加到 train_features 列表中。

最终经过处理,我们会得到训练集中2250个图像的BOW表示,每个图像都被表示成一个250维度(词典大小)的向量,该向量就是之后将输入SVM中进行训练的数据

3.5 模型训练

前面我们已经自定义了一个多分类的SVM的类,其具有fit和predict方法,我们只需要实例化一个svm对象后调用其fit方法进行训练拟合即可

1
2
3
4
5
6
7
8
9
10
def train_svm(traindata,train_labels):
print('训练模型...')
traindata_np = np.array(traindata) # 将训练数据列表转换为 NumPy 数组
train_labels_np = np.array([int(label) for label in train_labels]) # 将训练标签列表转换为 NumPy 数组
# 初始化线性支持向量机对象
SVM = MultiClassSVM()
# 模型训练,并将训练好的模型保存在 SVMmodel 变量中
SVMmodel = SVM.fit(traindata_np, train_labels_np)
print('模型训练完成')
return SVM

需要注意的是因为fit方法的输入需要是numpy类型的数组,所以在训练之前额外对训练数据和数据标签进行转换

3.6 模型测试

模型训练完毕后,调用svm对象的predict方法对甚于的2235张图像进行预测,注意此处的输入仍然是图像的bow特征,即图像的词袋表示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def test_model(SVMmodel,test_data,test_labels,bow,sift):
print('测试模型...')
predict_table = [] # 预测结果
for img_path in tqdm(test_data):
img = cv2.imread(img_path)
data = bow.compute(img, sift.detect(img))
result = SVMmodel.predict(data)
predict_table.append(result)
# 转换类型保证类型相同
test_labels_str = []
for label in predict_table:
test_labels_str.append(str(label[0]))
two_digit_list = ['{:02d}'.format(int(num)) for num in test_labels_str]
# 使用 sklearn.metrics.classification_report() 生成分类报告
report = classification_report(test_labels, two_digit_list)
# 打印分类报告
print(report)

predict方法返回的数据格式如下

这与我们期望的字符串类型的标签相差较大,需要进行转换处理,即先将其转换成str类型的结果,然后对于一位数执行补0操作,最后的预测结果和标准结果如下

该结果从直观上难以辨别分类效果的好坏,因此使用混淆矩阵对结果进行表示

可以看到预测效果非常的差,甚至不如随即猜测都有50%的准确率,一方面是因为手动实现的SVM在fit拟合的次数不足,另一方面,我们的predict方法过于简单,因此造成了最终预测结果较差的情况。

三、实验总结

上面通过自行创建的多分类SVM进行训练和预测,得到的效果不尽人意(甚至在14以及02都出现了全0的情况…也就是说预测结果中根本没有这两个标签),我们思考在同等条件下使用现成库中打包好的svm是否能够得到较好的结果?

scikit-learn中SVM的算法库分为两类,一类是分类的算法库,包括SVC, NuSVC,和LinearSVC 3个类。另一类是回归算法库,包括SVR, NuSVR,和LinearSVR 3个类。对于SVC, NuSVC,和LinearSVC 3个分类的类,SVC和 NuSVC差不多,区别仅仅在于对损失的度量方式不同,而LinearSVC是线性分类,也就是不支持各种低维到高维的核函数,仅仅支持线性核函数,对线性不可分的数据不能使用。

选择类进行分类任务的时候,如果有经验知道数据是线性可以拟合的,那么使用LinearSVC去分类,不需要调参以及选择各种核函数以及对应参数。如果对数据分布没有什么经验,一般使用SVC去分类或者SVR去回归,这就需要选择核函数以及对核函数调参。

1
class sklearn.svm.LinearSVC(penalty='l2', loss='squared_hinge', dual=True, tol=0.0001, C=1.0, multi_class='ovr', fit_intercept=True, intercept_scaling=1, class_weight=None, verbose=0, random_state=None, max_iter=1000)
  • C:目标函数的惩罚系数C,用来平衡分类间隔margin和错分样本的,default C = 1.0; 一般来说,如果噪音点较多时,C需要小一些。

  • loss :指定损失函数 .有‘hinge’和‘squared_hinge’两种可选,前者又称L1损失,后者称为L2损失,默认是是’squared_hinge’,其中hinge是SVM的标准损失,squared_hinge是hinge的平方。

  • penalty : 仅仅对线性拟合有意义,可以选择‘l1’即L1正则化 或者 ‘l2’即L2正则化。默认是L2正则化,如果我们需要产生稀疏话的系数的时候,可以选L1正则化,这和线性回归里面的Lasso回归类似。

  • dual :选择算法来解决对偶或原始优化问题。如果我们的样本量比特征数多,此时采用对偶形式计算量较大,推荐dual设置为False,即采用原始形式优化

  • tol :(default = 1e - 3): svm结束标准的精度;

  • multi_class:如果y输出类别包含多类,用来确定多类策略, ovr表示一对多,“crammer_singer”优化所有类别的一个共同的目标 .’crammer_singer’是一种改良版的’ovr’,说是改良,但是没有比’ovr‘好,一般在应用中都不建议使用。如果选择“crammer_singer”,损失、惩罚和优化将会被被忽略。 ‘ovr’的分类原则是将待分类中的某一类当作正类,其他全部归为负类,通过这样求取得到每个类别作为正类时的正确率,取正确率最高的那个类别为正类;‘crammer_singer’ 是直接针对目标函数设置多个参数值,最后进行优化,得到不同类别的参数值大小。

  • class_weight :指定样本各类别的的权重,主要是为了防止训练集某些类别的样本过多,导致训练的决策过于偏向这些类别。这里可以自己指定各个样本的权重,或者用“balanced”,如果使用“balanced”,则算法会自己计算权重,样本量少的类别所对应的样本权重会高。当然,如果你的样本类别分布没有明显的偏倚,则可以不管这个参数,选择默认的”None”

  • verbose:跟多线程有关

一般的,在特征数非常多的情况下,或者样本数远小于特征数的时候,使用线性核,效果已经很好,并且只需要选择惩罚系数C即可。一般能用线性核解决问题我们尽量使用线性核(毕竟不需要额外的调参就能快速的解决问题)。

综上,我们使用sklearn.svm.LinearSVC来进行对比实验,整个代码的结构保持不变,只需要在模型训练阶段将自定义的多分类SVM替换为opencv的线性svm即可

1
2
3
4
5
6
def SVMtrain(traindata):
# 初始化线性支持向量机(Linear SVM)对象
SVM = LinearSVC()
# 模型训练,并将训练好的模型保存在 SVMmodel 变量中
SVMmodel = SVM.fit(traindata, train_labels)
return SVM

训练完毕后调用svm的predict方法进行预测,并使用classification_report方法生成混淆矩阵

在相同的条件下,opencv库中的线性svm的效果略好于自定义的多分类svm,并且在训练时间上相比,opencv库的训练时间远小于自定义的svm(猜测是因为迭代条件的区别)。

当然这个结果也不是非常的好,更进一步的,我们可以尝试调整惩罚因子、重新选择核函数、调整词典大小等方式进行参数调整以提高模型的准确率(更直接的方式是换一个分类器不要使用SVM)。

下面我展示的是调整词典大小为500之后的训练结果,可以看到效果并没有明显的提升…

SVM在大规模的多分类问题上并不是很适用,一方面随着类和数据点的增加,它的计算成本会随之增加(因为SVM试图找到将数据分为不同类的最优超平面,而对于多类问题,这涉及到解决多个优化问题),另一方面支持向量机对和函数及其参数的选择及其敏感,在大规模的数据集上调整参数是一件非常困难的事情。

最后,本次实验使用支持向量机,根据图像内容对图像进行分类。在图像的表示过程中,使用了词袋模型,即图像被视为视觉单词的集合,这些单词的直方图被用作分类的特征向量。尽管最终分类的结果不是很好,但是在实验过程中通过查阅资料、上手实操等使得我掌握了SIFT特征提取、词汇字典生成、词袋模型表示、SVM构建等相关知识点,整体来说收获很大。


初级项目_图像分类
https://gintoki-jpg.github.io/2023/04/28/项目_图像分类系统/
作者
杨再俨
发布于
2023年4月28日
许可协议