聊聊图像识别的小原理,动手实现自己的图像分类

11,615 阅读14分钟

咱们开门见山,实话实说。

虽然ChatGPT带火了人工智能,但它还没找好挣钱的门路。急得投资人微软充当OpenAI的销售,大夏天的提着2.5L的矿泉水,背着电脑包,到处下基层去跟人聊行业结合,谈产品落地。

镜头一转,在计算机视觉(Computer Vision, CV)领域,人工智能反而挣着钱了。大家用AI生成衣服图片替换模特,也用AI生成游戏怪物替换特效师。甚至在传统的图书出版行业,有些书籍的插画,也开始用AI去生成。

因此,我打算聊聊人工智能在视觉方面的原理。然后,顺手从0到1创建一个属于自己的图像分类开放能力。

一、看看效果

为了让大家直观感受到AI对视觉处理的能力,我先拿一个低成本的图像分类代码演示一下。后面,咱再讲原理并自己建设。

from transformers import AutoImageProcessor, ResNetForImageClassification
import torch
from PIL import Image
# 加载模型权重
processor = AutoImageProcessor.from_pretrained("model")
model = ResNetForImageClassification.from_pretrained("model")
# 选择一个图片
image = Image.open("pics/dog.jpeg")
inputs = processor(image, return_tensors="pt")
with torch.no_grad():
    logits = model(**inputs).logits
predicted_label = logits.argmax(-1).item()
print(model.config.id2label[predicted_label])

上面利用transformers加载了一个训练好的权重,实现了一个图像分类的功能。这就是全部的python代码。

我的项目文件结构是这样的:

|---- model # 模型权重 
    |---- cofig.json
    |---- preprocessor_config.json
    |---- pytorch_model.bin 
|---- pics # 测试图片
    |---- dog.jpeg # 随便一张狗的图片
|---- main.py # python代码文件

model文件夹下,是人家训练好的模型,我们直接拿来用。你可以下载pytorch的权重,也可以选择TensorFlow的权重。记得对应pip install一下它们。另外别忘了 pip install transformers

下载地址是huggingface.co/microsoft/r… 。这是微软的resnet-50模型,它是从COCO 2017数据集中找了1000类图片进行训练的产物。这些分类对应model/config.json中的id2label。

我找了一些动物的图片来尝试。我原以为它仅能区分出猫、狗、鼠、兔。没想到,它居然还能进一步识别具体的品种。

如上图,它能识别出是狗,而且是金毛犬。他能识别出是松鼠,还是狐狸松鼠。我又拿哈士奇和狼试了试,结果几毫秒就出正确结果了。

挺神奇!怪不得图像领域能赚钱,这玩意儿确实有用哇。

那么问题来了,它的原理是什么?又是如何实现的呢?

二、讲讲原理

2.1 图像的构成

你有没有想过这样一个问题:

煤炭是黑的,因它本身就是黑的。馒头是白的,因它本来就是白的。但显示器、电视,为啥一会儿黑,一会儿白,有时候还五彩斑斓。色彩和图像究竟是如何被定义的呢?

想明白这个问题,有助于你理解计算机视觉。

我们都知道(看完就知道了),红(Red)、绿(Green)、蓝(Blue)是三原色,又称RGB

这三种颜色相互交融,可调合出世间的五彩斑斓。

很有意思,色彩学和数学扯上了关系。红色的RGB(255, 0, 0), 绿色的RGB(0, 255, 0)。红色混合绿色是黄色,黄色的RGB(255, 255, 0)。如你所料,红绿蓝混在一起,就是(255, 255, 255),它是白色。

基于这个原理,我们才可以用三组数据表示一个色彩点。同时,我们又可以用多个色彩点表示一幅图。这一点,我们通过显示器的屏幕就能看得出来。

你的电脑、手机的显示器,就是由很多个小像素格子组成的。只是它太小太密集,你看不出来。如果,你往屏幕上滴一滴水,就会发现其中的奥秘。

至此,我想表达,我们可以用数字来描述任何一幅图。它无非就是横竖(宽高)有多少个点,每个点是什么颜色(RGB的数值)。

那么对这些个数据,我们能做什么呢?

2.2 聊透卷积神经网络的原理

提到图像处理,不得不提卷积神经网络(Convolutional Neural Network, CNN)。它构建起一个层级化的模型,可谓是经典中的经典。

CNN有三板斧:卷积层(Convolutional Layer)、池化层(Pooling Layer)和全连接层(Fully Connected Layer)。

一般的文章提到卷积层,大多会放出这样的图。

并且配上文字解释说:小绿方块是个3×3的卷积核,在淡蓝色5×5像素的图上以1为步长行进。在不填充的情况下,最大能走3步。所以,最终形成右侧3×3的结果。这个卷积核,就是一个特征提取器,能过滤它所关注的数据特征。

这……说得没错。但对初学者来说,还是很抽象:它怎么就学会过滤特征了?它又过滤了什么样的特征?

首先,做卷积运算应该不难理解。下面这幅图,演示了卷积运算。

这是一个3×3的卷积核。它每个格子都带参数。从图像上卷过之后,将图上的像素和自己进行运算,最终输出卷积后的数据。上面的卷积结果让轮廓更加明显。

卷积层在参与训练时,它的参数是可变的。它会通过学习来改变自己的数值。它先随机猜测一个特征,比如只要中心点的像素,其它全都×0变为空白。在验证的过程中,如果猜对了,固然挺好。如果猜不对,那么再进行改变,直到接近正确答案。

一套神经网络,会有好多个卷积层。每个卷积层,又会有好多个卷积核。这样就可以从不同的维度来收集一张图片的特征。甚至说,还可以前面卷完了后边接着卷。

我们看下面这张经典的VGG-19的结构图。3×3 conv,64表示有643×3的卷积核在一张图片上提取信息。后面还有128个、256个。

卷积就像是提取一个学生的成绩。前面卷数学计算,中间卷体能指标,后边卷文学修养。甚至在文学大类中,再提取关于诗人“李白”的专项考核。总之就是要找差别,要靠特征来判定你是不是优秀学生。不得不说卷积的“卷”,翻译得很到位。

经过训练,模型合格,每个卷积核都会得到合适的参数值。这就好比对于评价学生,我们先猜了一个自认为靠谱的方案。然后经过多轮尝试和调整,最终形成了一套量化标准。

那么,卷积层的参数能看吗?都说AI是黑盒,不知道它发生了什么!这句话虽然没错,但在一些简单的场景,还不至于上升到玄学的境界。

模型权重(最终训练出来的那玩意儿)定型后,会体现在模型参数的数值上。整个网络有多少层结构,每层的数值是多少,其实能打印,不过全是向量,你看不出啥道道。但是,如果我们生成噪点图像,让图像的特征向卷积核的权重靠拢,去极大化这个特征,那么或许可以有些收获。

下面,我解剖一个图片分类网络结构的权重。我选择几个层,然后极大化这些数值,也输出了一些视觉的结果。这个操作稍微有点专业,因此我仅摆出来效果,期望帮助大家从视觉上去加深理解。请原谅我不去细讲它。

我并没有拿生物书上的细胞照片来忽悠您,尽管这两者有点像。这确实是将神经网络的权重碎片“反编译”成图片的结果。可能这个结果,让那些持有“生物就是计算机程序”这一观点的人很兴奋。

其实有些图片也并不抽象,或多或少也能推测出一些特征信息。

看来这个卷积层的意义就是对图像进行特征提取。它通过撒出大量的卷积核,去挖掘一张图中存在的独特边缘或是纹理,从而给下游分类任务提供判断依据。

这一层接一层地卷积,是不是太卷了?不累吗?

设计者也考虑到了这个问题,因此引入了池化层(Pooling layer)的概念。

池化层可以在一定程度上解决过“卷”的问题!其手段就类似于灭霸的响指,会让样本中一半的像素消失。看下面这个图。

上图采用的是最大池化(Max Pooling)。一个2×2的池化窗口,在图片上行进。它所经过的地方,取一个窗口内最大的数值作为输出。

按照这个配置,它可以将一张80×80的样本缩小到40×40。而且你看上面的图,即便是缩小后,特征也不变,还能看出本来的轮廓。如果不嫌麻烦,你再往上翻找到那张VGG-19的结构图。output size开始是224,经过pool,/2之后就变成了112,少了一半。这就是池化起的作用。

在算力不变的情况下,数据量成半砍,那干活肯定快,空出的时间可以去休闲。所以我说它“反卷”。

在训练时,卷积层的卷积核参数会反复调整并矫正。而池化层不用动脑子,没东西参与训练,要么找一个最大值,要么找一个平均值(平均池化, Average Pooling)。安逸得很!

池化层一般搭配着卷积层使用。这似乎是程序员在对外宣传:人生要间隔着“卷一程”、“躺一程”。

经过神经网络的层层提取,到后期基本上就浓缩出了大量的样本特征。此时通过全连接层(Fully Connected Layer)进行裁决。它是组委会的决赛评委。它的工作简单且重要。前面甭管多少层,都是在收集特征,就像是上报学生的证书、成绩、荣誉、行为、美德、习惯等数据。到全连接层这里,会对特征数据进行加权求和并评分,给出最终结果。

好了,卷积神经网络的组成,我们就讲完了。下面,我们就自己去攒一个网络去实现图像分类。

三、具体实现

我想训练个识别天气照片的分类模型。我们的天气一般分为三类:晴天、多云、有雨。

因此,我找到了一些相关的图片数据,作为数据集。

图片样例如下:

将图片整理成如下的目录结构:

mian.py 程序文件
[datasets] 数据集文件夹
   |--- [cloudy] 多云
   |--- [rain] 有雨
   |--- [shine] 晴天

于是,我们要训练的图片是3类:cloudyrainshine。我们训练完,让模型对一张陌生图片进行分类,也会是其中之一。就算不像,结果也得是:分类 cloudy, 得分 0.01。这就是分类问题。除此之外还有一个回归问题,它能给出无限的结果,比如明天的股市数据。

# 分类名称
names = ['cloudy', 'rain', 'shine'] 

有了图片数据,就有了种子。趁着热乎劲儿,我们先来搭建卷积神经网络。

本次选择TensorFlow作为AI框架。我的原则是小数据量用TensorFlow,大数据量用Torch

以下代码运行环境:python 3.9, tensorflow 2.6

3.1 神经网络结构

首先导入tensorflow的包。然后构建一个卷积神经网络。

import tensorflow as tf
from tensorflow.keras import layers
from tensorflow.keras.models import Sequential

model = Sequential([
  layers.Rescaling(1./255, input_shape=(200, 200, 3)),
  layers.Conv2D(16, 3, padding='same', activation='relu'),
  layers.MaxPooling2D(),
  layers.Conv2D(32, 3, padding='same', activation='relu'),
  layers.MaxPooling2D(),
  layers.Conv2D(64, 3, padding='same', activation='relu'),
  layers.MaxPooling2D(),
  layers.Dropout(0.2),
  layers.Flatten(),
  layers.Dense(128, activation='relu'),
  layers.Dense(3)])

前面我们已经学过了卷积神经网络的结构。它的入口是图片数据,后面是卷积层和池化层循环交替。随后,接上全连接层,直达分类的输出。

我们看上面的代码也是这样。主体是卷积层Conv2D搭配着池化层MaxPooling2D。最上层的input_shape=(200, 200, 3)是图片输入的尺寸,表示200×200像素,3代表RGB三通道。到最后Dense(3)输出3个分类。这就是我们设计的网络。

里面的多少个卷积核,每个是几×几,这个我们自己定,就跟搭积木似的,调这些玩意儿才是最终乐趣。

有两点需要说明一下:

  • Dropout(0.2)可以不用。这才是灭霸的响指,它是真的随机断掉20%的参数。目的是避免过拟合。你想啊,剩下的80%都能预测准确,那么它的通用性肯定差不了。
  • Flatten()也叫“拉平层”。我们输入的图片以及后面的卷积池化,都是2维的,有宽有高。但最终我们要输出3分类,这是1维的。因此通过拉平层把2维拉成1维。即:[[1,2],[3,4]]->[1,2,3,4]。方便后面计算。

神经网络的搭建就这么简单。请记住我们已经有了一个model,就是刚才通过Sequential实例化出来的,后面还会用到。

3.2 训练数据

有了神经网络。下面要将数据交给它去训练。通过训练,会把模型里的每个层的模型参数给练出来。于是,它才能实现从量变到质变,拥有智慧,实现自我分类。

# 加载训练数据
train_ds = tf.keras.utils.image_dataset_from_directory(
  "datasets", image_size=(200, 200), batch_size=24)

我们已经准备好训练的图片文件夹datasets了。现在就加载这些数据。并且把里面的图片都规范成image_size=(200, 200)batch_size=24是把数据以24个化为一组,不然太大吃不下。

如果你执行上面的代码的话,控制台会打印如下内容:

Found 768 files belonging to 3 classes.

它说找到了768个文件,属于3个分类。这正好对应我的datasets文件夹下3个子文件夹和768张图片。

随后,给模型model配置一下,然后启动训练。

model.compile(optimizer='adam', 
    loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True), 
    metrics=['accuracy'])

cp_callback = tf.keras.callbacks.ModelCheckpoint(filepath="tf2/checkpoint", save_weights_only=True)
model.fit(train_ds, epochs=10, callbacks=[cp_callback])

model.compile是配置优化器选adam。其余配置损失函数,衡量指标,这些记住就可以。对初学来说,一般不会变。

ModelCheckpoint指定了权重保存的策略,也就是训练结果保存路径是tf2/checkpoint,只保存权重。

model.fit这一行代码就是说对train_ds数据进行训练,训练10个轮次,训练结果回调权重保存策略。

如果执行正常的话,会有如下打印:

Epoch 1/10
32/32 [==========] - loss: 0.8752 - accuracy: 0.6589
Epoch 2/10
32/32 [==========] - loss: 0.4711 - accuracy: 0.8255
Epoch 3/10
32/32 [==========] - loss: 0.3381 - accuracy: 0.8854
Epoch 4/10
……
Epoch 9/10
32/32 [==========] - loss: 0.1136 - accuracy: 0.9583
Epoch 10/10
32/32 [==========] - loss: 0.0623 - accuracy: 0.9831

训练10Epoch。总共768张图,每个batch是24,所以需要768/24=32次才能遍历一次。

accuracy是准确率,随着Epoch增加,准确率已经到了98%以上。

每一个Epoch结束后,你去看tf文件夹下,会生成模型的权重。

mian.py
[datasets]
[tf] 权重文件夹
   |--- checkpoint
   |--- checkpoint.data-00000-of-00001
   |--- checkpoint.index

好吧,这就训练完了!

3.3 预测数据

我从网上搜到这么一张图片,这是模型从未见过的图片。

我们来预测一下,它会属于哪个分类。

# 加载模型
model.load_weights("tf2/checkpoint")  
# 导入图片
img = tf.keras.utils.load_img("test.png", target_size=(200, 200))
img_array = tf.keras.utils.img_to_array(img)
img_array = tf.expand_dims(img_array, 0)
# 进行预测
predictions = model.predict(img_array)
score = tf.nn.softmax(predictions[0])
# 输出分类
print( "分类 {}, 得分 {:.2f}".format(names[np.argmax(score)], 100*np.max(score)))

上面代码是利用tensorflow加载图片,并转换为模型需要的格式。调用model.predict可以进行预测。最终结果是一批独热数据,通过tf.nn.softmax可以读出来。

如果一切正常,控制台会输出:

1/1 [==========] - 0s 171ms/step
分类 cloudy, 得分 78.98

是的,这张图确实是多云。看来,模型还是挺有作用的。

四、小总结

最后,我给大家布置个作业。虽然文中讲解的内容已经够用。但我还是将更加健壮的代码上传到了Github。地址是github.com/hlwgy/jueji…

里面涉及到了一些细节,比如数据缓存、验证集、训练曲线等。大家可以查看完整代码,去了解更细致的知识。

很棒,我讲完了,您听完了。您现在可以训练自己的数据,去实现自己的业务。不用花钱买API接口,想怎么玩就怎么玩!

关键是,它也不复杂呀!

本文为稀土掘金技术社区首发签约文章,30天内禁止转载,30天后未获授权禁止转载,侵权必究!