图像分类

采用传统方法(基于全局特征和局部特征的 KNN、Logistic 分类方法)和深度学习方法做基于内容的图像分类。分别为 PJ-2 全局特征图像分类,PJ-3 局部特征图像分类,PJ-4 深度学习图像分类。同时,本次报告将针对三次实验的实验过程与方法、实验结果进行比较与分析。

PJ-2 全局特征图像分类

本实验将针对两种全局特征 CSD 和 SCD,使用两种不同的分类方法 KNN 和 Logistic 进行图像分类实验。

全局特征是指图像的整体属性,常见的全局特征包括颜色特征、纹理特征和形状特征,比如强度直方图等。由于是像素级的低层可视特征,因此,全局特征具有良好的不变性、计算简单、表示直观等特点,但特征维数高、计算量大是其致命弱点。此外,全局特征描述不适用于图像混叠和有遮挡的情况。

1. 实验环境及过程

全局特征使用 MPEG7Fex 生成。使用的 Python 库主要包括 matplotlib、numpy 和 sklearn。

实验过程主要为:

  1. 使用 MPEG7Fex 生成 CSD 和 SCD 数据并存储到文件。CSD 和 SCD 均设定为输出 64 维向量。

  2. 读取存储 CSD 和 SCD 数据的文件,转为 matrix;

  3. 读取存储标签数据的文件,转为 list;

  4. KNN 方法主要过程为计算欧式距离,并选取前 K 个距离最近的点,以这些点中占比最多的类别为该点类别,主要调参量为 K,共测试了 K 从 1 到 40 的实验。关键函数代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    def classify0(inX, dataSet):
    dataSetSize = dataSet.shape[0]
    diffMat = tile(inX, (dataSetSize, 1)) - dataSet
    sqDiffMat = diffMat**2
    sqDistances = sqDiffMat.sum(axis=1)
    distances = sqDistances**0.5
    sortedDistIndicies = distances.argsort()
    return sortedDistIndicies
  5. 逻辑回归将多分类问题看作是多个独立二元回归的集合,在运行过程中把其中一个类别看成是主类别,然后将其它 K−1 个类别和我们所选择的主类别分别进行回归。总共获得了 K−1 个决策边界。主要调参量为惩罚项和正则化系数,其中惩罚项包括 l1 和 l2 两种情况,对应两种不同的正则项。c 是正则化系数 λ 的倒数,float 类型,越小的数值表示越强的正则化。分别计算 [0.001, 0.01, 0.1, 1, 10, 100, 1000,5000] 下的 score。优化算法选择 liblinea,内部使用了坐标轴下降法来迭代优化损失函数。采用 GridSearch 的方法,共进行了 16 组对照实验。关键函数代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    def GridSearch(trainMat, valMat, testMat, train_label, val_label, test_label):
    best_score = 0.0
    num_list = []
    for penalty in ['l1', 'l2']:
    for C in [0.001, 0.01, 0.1, 1, 10, 100, 1000, 5000]:
    lr_clf = LogisticRegression(
    C=C, penalty=penalty, solver="liblinear", multi_class="ovr")
    lr_clf.fit(trainMat, ravel(mat(train_label).T))
    score = lr_clf.score(valMat, ravel(mat(val_label).T))
    num_list.append(score)
    print(score)
    if score > best_score:
    best_score = score
    best_parameters = {'penalty': penalty, 'C': C}
    lr = LogisticRegression(**best_parameters)
    lr.fit(trainMat, ravel(mat(train_label).T))
    train_score = lr.score(trainMat, ravel(mat(train_label).T))
    test_score = lr.score(testMat, ravel(mat(test_label).T))
    print("训练集 best score: %.3f" % (train_score))
    print("验证集 best score: %.3f" % (best_score))
    print("最好的参数:{}".format(best_parameters))
    print("测试集 best score: %.3f" % (test_score))
    return num_list
  6. 根据实验结果绘制图片。

2. 实验结果

KNN 的实验结果如下:

逻辑回归的结果如下:

最终的对比结果如下:

Type KNN Best Score Logistic Best Score
CSD 0.440 0.487
SCD 0.503 0.457

其中 CSD 在 KNN 上表现不如 SCD,而在 Logistic 上表现好于 SCD;逻辑回归在 CSD 上表现较好,而在 SCD 上则不如 KNN。

PJ-3 局部特征图像分类

本实验将针对局部特征 SIFT,使用两种不同的分类方法 KNN 和 Logistic 进行图像分类实验。

局部特征是从图像局部区域中抽取的特征,包括边缘、角点、线、曲线和特别属性的区域等。常见的局部特征包括角点类和区域类两大类描述方式。与线特征、纹理特征、结构特征等全局图像特征相比,局部图像特征具有在图像中蕴含数量丰富 ,特征间相关度小,遮挡情况下不会因为部分特征的消失而影响其他特征的检测和匹配等特点。典型的局部图像特征生成应包括图像极值点检测和描述两个阶段。好的局部图像特征具有特征检测重复率高、速度快,特征描述对光照、旋转、视点变化等图像变换具有鲁棒性,特征描述符维度低,易于实现快速匹配等特点。

1. 实验环境及过程

使用的 Python 库主要包括 opencv,matplotlib、numpy 和 sklearn。其中 opencv 由于 SIFT 的版权问题,需要下载 opencv_contrib 版本才可使用。

实验过程主要为:

  1. SIFT特征提取:输入图片,输出图片的特征点集(feature列表),每个 feature 代表一个图片的某个局部特征,由一个 128 维浮点数组表示。
    cv2.xfeatures2d.SIFT_create(nfeatures),nfeatures is The number of best features to retain.
    其中关键函数计算特征点集代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    def calcSiftFeature(img):
    gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
    #sift = cv2.xfeatures2d.SIFT_create() # max number of SIFT points is 200
    sift = cv2.xfeatures2d.SIFT_create(1000)
    kp, des = sift.detectAndCompute(gray, None)
    img=cv2.drawKeypoints(gray,kp,img)
    cv2.imshow('img',img)
    cv2.waitKey()
    return des
  2. Kmeans 聚类:将所有图片的 SIFT 特征聚类为 K 类,构成该类的“单词表”;其中关键函数代码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    def learnVocabulary(wordCnt):
    for sp in sps:
    filename = "features/" + sp + ".npy"
    features = np.load(filename)

    print("Learn vocabulary of " + sp + "...")
    # use k-means to cluster a bag of features
    criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 20,
    0.1)
    flags = cv2.KMEANS_RANDOM_CENTERS
    compactness, labels, centers = cv2.kmeans(features, wordCnt, None,
    criteria, 20, flags)

    # save vocabulary(a tuple of (labels, centers)) to file
    filename = "vocabulary/" + sp + ".npy"
    np.save(filename, (labels, centers))
    print("Done\n")
  3. 统计词频:(对每个图片)输入图片的 feature 集和“单词表”,分别计算该图片 feature 集中每个 feature 对应的“单词”,并统计每个“单词”在该 feature 集中出现的次数即词频;输出是词频统计数据。

  4. 将直方图作为样本向量即可构建训练数据、验证数据和测试数据;

  5. 主要调参量为 nfeatures 和 WordCount,其中 nfeatures 以选取 200, 400, 600, 800, 1000 和不限六种情况,WordCount 选取 50, 100, 200, 300 四种情况,总计 24 组对照实验。

2. 实验结果

24 组对照实验结果如下:

逻辑回归在测试集上的总体表现好于 KNN;而 feature 数和 WordCount 数没有表现出和分类结果的关联性。

最终结果如下:

Type KNN Best Score Logistic Best Score
SIFT 0.407 0.437

Logitic 在局部特征 SIFT 的分类表现上好于 KNN。

PJ-4 深度学习图像分类

本实验将采用深度学习方法,使用 LeNet 网络进行端到端的图像分类。

CNN 直接利用图像像素信息作为输入,最大程度上保留了输入图像的所有信息,通过卷积操作进行特征的提取和高层抽象,模型输出直接是图像识别的结果。这种基于”输入-输出”直接端到端的学习方法取得了非常好的效果,得到了广泛的应用。

1. 实验原理

本实验选取了经典网络 LeNet 进行实验。LeNet 是一个用来识别手写数字的最经典的卷积神经网络,是 Yann LeCun 在 1998 年设计并提出的。Lenet 的网络结构规模较小,通过巧妙的设计,利用卷积、参数共享、池化等操作提取特征,避免了大量的计算成本,最后再使用全连接神经网络进行分类识别,这个网络也是最近大量神经网络架构的起点,给这个领域带来了许多灵感。

LeNet 的网络结构示意图如下所示:

LeNet 包含输入层在内共有八层,每一层都包含多个权重。C 层代表卷积层,通过卷积操作,可以使原信号特征增强,并降低噪音。S 层是一个池化层,利用图像局部相关性的原理,对图像进行子抽样,可以减少数据处理量,同时也保留了一定的有用信息。

  1. 输入层是 32 * 32 大小的图像,这样做的目的是希望潜在的明显特征,如笔画断续、角点能够出现在最高层特征监视子感受野的中心。
  2. C1 层是一个卷积层,该层使用了 6 个卷积核,每个卷积核的大小为 5×5,这样就得到了 6 个 feature map(特征图)。
  3. S2 层是一个下采样层,有 6 个 14 * 14 的特征图,每个 feature map 中的每个神经元都与 C1 层对应的 feature map 中的 2 * 2 的区域相连。池化层的目的是为了降低网络训练参数及模型的过拟合程度。池化方式有最大池化和平均池化两种。
  4. C3 层也是一个卷积层,运用 5 * 5 的卷积层,处理 S2 层。C3 有 16 个feature map,每个 feature map 由上一层的各 feature map 之间的不同组合。
  5. S4层是一个下采样层,由 16 个 5 * 5 大小的 feature map 构成,每个神经元与 C3 中对应的 feature map 的 2 * 2 大小的区域相连。
  6. C5 层又是一个卷积层,同样使用 5 * 5 的卷积核,每个 feature map 有(5-5+1) * (5-5+1),即1 * 1的神经元,每个单元都与 S4 层的全部 16 个 feature map 的 5 * 5 区域相连。C5 层共有 120 个feature map,其参数与连接数都为 48120 个。
  7. F6 层全连接层共有 84 个feature map,每个 feature map 只有一个神经元与 C5 层全连接。F6 层计算输入向量和权重向量之间的点积和偏置,之后将其传递给 sigmoid 函数来计算神经元。
  8. 输出层也是全连接层,共有 10 个节点,但本实验中将其进行了对应修改,以应用于三分类的情况。

2. 框架安装

实验配置环境:Python 3.6、Anaconda、CPU 训练。

所需库包括 keras, tensorflow, sklearn, matplotlib, numpy, opencv 等。使用的深度学习模型为 Lenet。

框架安装步骤为:

  1. 安装 Anaconda 并设置环境变量;
  2. 使用 Anaconda 创建 Python3.6 的环境,命令为 conda create -n tf python=3.6(由于 Tensorflow 不支持 Python 3.7);
  3. 使用 conda env list 列出所有环境列表,使用 activate tf 进入刚创建的 Python3.6 环境;
  4. 使用 pip 安装所需的所有库;
  5. 按照定义的参数执行 python 文件。

3. 实验过程

定义 Lenet 类:输入参数 width、height 表示图像尺寸,depth 表示图像 channels,classes 表示图像分类数量,此处为 3 类。conv2d 表示执行卷积,maxpooling2d 表示执行最大池化,Activation 表示特定的激活函数类型,Flatten 层用来将输入“压平”,用于卷积层到全连接层的过渡,Dense 表示全连接层(500个神经元)。

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
class LeNet:
@staticmethod
def build(width, height, depth, classes):
# initialize the model
model = Sequential()
inputShape = (height, width, depth)
# if we are using "channels last", update the input shape
if K.image_data_format() == "channels_first": #for tensorflow
inputShape = (depth, height, width)
# first set of CONV => RELU => POOL layers
model.add(Conv2D(20, (5, 5), padding="same", input_shape=inputShape))
model.add(Activation("relu"))
model.add(MaxPooling2D(pool_size=(2, 2), strides=(2, 2)))
#second set of CONV => RELU => POOL layers
model.add(Conv2D(50, (5, 5), padding="same"))
model.add(Activation("relu"))
model.add(MaxPooling2D(pool_size=(2, 2), strides=(2, 2)))
# first (and only) set of FC => RELU layers
model.add(Flatten())
model.add(Dense(500))
model.add(Activation("relu"))

# softmax classifier
model.add(Dense(classes))
model.add(Activation("softmax"))

# return the constructed network architecture
return model

读取图像数据并进行预处理:此处参照之前实验的读取方式,进行相应修改,输入的 sp 为对应的数据类型(train、val、test),file 为之前已处理完毕的标签数据文件。读取图片文件后进行 resize 处理,使用 img_to_array 将文件转化为数据。其中需要对数据进行类型转换,数值归一化,并使用 to_categorical 将类别向量(从 0 到 classes 的整数向量)映射为二值类别矩阵。该函数返回图片数据及其标签。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def load_data(sp, file):
print("[INFO] loading images...")
data = []
#for sp in sps:
for index_en, en in enumerate(entities):
dir = root + sp + "/" + en
for index, filename in enumerate(os.listdir(dir)):
if os.path.splitext(filename)[1] == '.jpg':
image = cv2.imread(dir + "/" + filename)
image = cv2.resize(image, (norm_size, norm_size))
image = img_to_array(image)
data.append(image)
labels = file2label(file)
# scale the raw pixel intensities to the range [0, 1]
data = np.array(data, dtype="float") / 255.0
labels = np.array(labels)

# convert the labels from integers to vectors
labels = to_categorical(labels, num_classes=CLASS_NUM)
return data, labels

训练函数:首先初始化我们预先定义好的 Lenet 模型,并使用了 Adam 优化器,由于这个任务是一个多分类问题,可以使用类别交叉熵(categorical_crossentropy)。训练完成后将模型文件进行保存,并绘制 loss 和 accuracy 的变化曲线。

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
30
31
32
33
34
35
def train(aug, trainX, trainY, testX, testY, args):
# initialize the model
print("[INFO] compiling model...")
model = LeNet.build(
width=norm_size, height=norm_size, depth=3, classes=CLASS_NUM)
opt = Adam(lr=INIT_LR, decay=INIT_LR / EPOCHS)
model.compile(
loss="categorical_crossentropy", optimizer=opt, metrics=["accuracy"])
print("len(trainX):", len(trainX))
# train the network
print("[INFO] training network...")
H = model.fit_generator(
aug.flow(trainX, trainY, batch_size=BS),
validation_data=(testX, testY),
steps_per_epoch=len(trainX) // BS,
epochs=EPOCHS,
verbose=1)

# save the model to disk
print("[INFO] serializing network...")
model.save(args["model"])

# plot the training loss and accuracy
plt.style.use("ggplot")
plt.figure()
N = EPOCHS
plt.plot(np.arange(0, N), H.history["loss"], label="train_loss")
plt.plot(np.arange(0, N), H.history["val_loss"], label="val_loss")
plt.plot(np.arange(0, N), H.history["acc"], label="train_acc")
plt.plot(np.arange(0, N), H.history["val_acc"], label="val_acc")
plt.title("Training Loss and Accuracy on animal classifier")
plt.xlabel("Epoch #")
plt.ylabel("Loss/Accuracy")
plt.legend(loc="lower left")
plt.savefig(args["plot"])

主函数:使用了数据增广技术(ImageDataGenerator)来对我们的小数据集进行数据增强(对数据集图像进行随机旋转、移动、翻转、剪切等),以加强模型的泛化能力。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if __name__ == '__main__':
args = args_parse()
trainX, trainY = load_data("train", "train_label.txt")
testX,testY = load_data("test", "test_label.txt")
# construct the image generator for data augmentation
aug = ImageDataGenerator(
rotation_range=30,
width_shift_range=0.1,
height_shift_range=0.1,
shear_range=0.2,
zoom_range=0.2,
horizontal_flip=True,
fill_mode="nearest")
train(aug, trainX, trainY, testX, testY, args)

同时在代码中定义了图片归一化尺寸,batch_size,epoches 等变量。该实验中主要针对图片归一化尺寸和 Batch_Size 两个变量进行了实验,其中 Norm_Size 测试了 32,64,128 三种情况,Batch_Size 测试了 16,32,64 三种情况,总计 9 组对照实验。每组实验的 Ecpoch 均设置为 60。将每组 Loss 和 Accuracy 结果绘制成图片。同时进行了一组去除数据增广的实验进行对比。

4. 实验结果

9 组实验获得的图像如下:

9 组实验的最高准确率如下:

Norm_Size = 32 Norm_Size = 64 Norm_Size = 128
Batch_Size = 16 0.56(Epoch = 23) 0.5233(Epoch = 10,50) 0.5533(Epoch = 41)
Batch_Size = 32 0.54(Epoch = 22) 0.58(Epoch = 37) 0.5133(Epoch = 32)
Batch_Size = 64 0.5467(Epoch = 39) 0.5367(Epoch = 45) 0.55(Epoch = 37)

其中当 Norm_Size = 64, Batch_Size = 32 时,获得的准确率最高,为 0.58。

数据增广对比实验:

当去除 ImageDataGenerator 之后,以 Norm_Size = 64, Batch_Size = 32 进行实验,在 20 次 Epoch 之后,训练集准确率已经达到 0.99 以上,但测试集准确率均在 0.5 以下,低于有数据增广的准确率。因此当使用小数据集时,使用数据增广技术,可有效加强模型的泛化能力。

图像分类实验对比

将三次实验 PJ-2 全局特征图像分类,PJ-3 局部特征图像分类,PJ-4 深度学习图像分类的准确率结果汇总后如下:

Type KNN Best Score Logistic Best Score LeNet
CSD 0.440 0.487 \
SCD 0.503 0.457 \
SIFT 0.407 0.437 \
端到端 \ \ 0.58
  • 在准确率上,基于深度学习 LeNet 网络的分类准确率高于传统方法。传统方法中,基于局部特征的准确率低于基于全局特征的准确率。而在局部特征中,逻辑回归的准确率要高于 KNN。而本实验中使用的深度学习框架是较早的 LeNet,若改用现在效果更好的网络,相信可以取得更好的准确率。
  • 在运行时间上,基于局部特征的方法,由于 K-Means 算法的影响,导致其运行时间远高于其他方法。
  • 在实验方法上,三者都需要对图像进行一定预处理,而在算法上,KNN 的实现较为简单,逻辑回归的相较之稍复杂,而深度学习方法需要进行神经网络的搭建,复杂度相对较高。

实验总结

在本次图像分类实验中,从对图像本身的认识上,了解到了全局特征与局部特征的概念;在分类方法上,不仅实现了传统机器学习中的 KNN 和 Logstic 方法,也实现了深度学习领域卷积神经网络的分类方法,了解到了这种端到端的学习方法与传统方法的差异性;在数据预处理上,了解到对图像进行处理的方法,了解到需要对数据进行训练集、验证集和测试集的合理划分,当面对分类问题时,针对各个类别需要选择平衡的数据量;在机器学习的模型训练方面,了解到了调参的重要性,学习了 Grid Search 等调参方法,了解到了准确率、召回率等评价指标,同时也学会当面对小数据集时,可以使用数据增广技术进行数据增强。通过本实验,增加了自己对信息处理、统计方法、机器学习等领域的认识,通过实战经验巩固了自己的知识。

0%