人工神经网络训练图像分类器
我们将仅使用全连接层在20000张图像上训练图像分类模型。所以没有卷积和其他花哨的东西,我们将把它们留到下一篇文章中。
不用说,但你真的不应该使用普通的人工神经网络来分类图像。图像是二维的,通过展平图像,你将失去使图像可识别的模式。尽管如此,它还是很有趣且可行的,并且会让你洞察这种方法的所有错误。
使用的数据集和数据准备
我们将使用Kaggle的狗与猫数据集。它是根据知识共享许可证授权的,这意味着你可以免费使用它:
图1:狗与猫数据集:
该数据集相当大——25000张图像均匀分布在不同的类中(12500张狗图像和12500张猫图像)。它应该足够大,可以训练一个像样的图像分类器,但不能使用人工神经网络。
唯一的问题是——它的结构不适合直接使用。你可以按照之前的文章创建一个适当的目录结构,并将其拆分为训练集、测试集和验证集:
缩小、灰度化和展平图像
让我们导入相关库。我们需要很多,需要安装Numpy、Pandas、TensorFlow、PIL和Scikit Learn:
我们不能将图像直接传递到Dense层。单个图像是三维的——高度、宽度、颜色通道——而Dense层需要一维输入。
让我们看一个例子。以下代码加载并显示训练集中的cat图像:
src_img = Image.open('data/train/cat/1.jpg')
display(src_img)
图2——猫的图片示例
图像宽281像素,高300像素,有三个颜色通道(np.array(src_img).shape)。
总的来说,它有252900个像素,在展平时转化为252900个特征。让我们尽可能节省一些资源。
如果有意义的话,你应该对你的图像数据集进行灰度化。如果你能对不以颜色显示的图像进行分类,那么神经网络也应该如此。可以使用以下代码段将图像转换为灰色:
gray_img = ImageOps.grayscale(src_img)
display(gray_img)
图3:灰色猫图像
显然,它仍然是一只猫,所以颜色在这个数据集中并没有起到很大作用。
灰度图像宽281像素,高300像素,但只有一个颜色通道。这意味着我们从252,900 像素减少到84,300 像素。仍然很多,但肯定是朝着正确的方向迈出了一步。
数据集中的图像大小不同。这对于神经网络模型来说是个问题,因为它每次都需要相同数量的输入特征。
我们可以将每个图像调整为相同的宽度和高度,以进一步减少输入特征的数量。
下面的代码片段调整了图像的大小,使其既宽又高96像素:
gray_resized_img = gray_img.resize(size=(96, 96))
display(gray_resized_img)
图4:调整大小的猫图片
当然,图像有点小而且模糊,但它仍然是一只猫。但是我们的特征减少到9216个,相当于将特征的数量减少了27倍。
作为最后一步,我们需要将图像展平。你可以使用Numpy中的ravel函数来执行此操作:
np.ravel(gray_resized_img)
图5:扁平猫图片
计算机就是这样看待猫的——它只是一个9216像素的数组,范围从0到255。问题是——神经网络更喜欢0到1之间的范围。我们将整个数组除以255.0即可:
img_final = np.ravel(gray_resized_img) / 255.0
img_final
图6-扁平和缩放的猫图像
作为最后一步,我们将编写一个process_image函数,将上述所有转换应用于单个图像:
让我们在随机的狗图像上进行测试,然后反转最后一步,以直观地表示图像:
tst_img = process_image(img_path='data/validation/dog/10012.jpg')
Image.fromarray(np.uint8(tst_img * 255).reshape((96, 96)))
图7:经过变换的狗形象
就这样,这个函数就像字面意思。接下来,我们将其应用于整个数据集。
将图像转换为表格数据进行深度学习
我们将编写另一个函数——process_folder——它迭代给定的文件夹,并在任何JPG文件上使用process_image函数。然后,它将所有图像合并到一个数据帧中,并添加一个类作为附加列(猫或狗):
让我们将其应用于训练、测试和验证文件夹。每个文件夹需要调用两次,一次用于猫,一次用于狗,然后连接集合。我们还将把数据集转储到pickle文件中:
下面是训练集的样子:
# Training set
train_cat = process_folder(folder=pathlib.Path.cwd().joinpath('data/train/cat'))
train_dog = process_folder(folder=pathlib.Path.cwd().joinpath('data/train/dog'))
train_set = pd.concat([train_cat, train_dog], axis=0)
with open('train_set.pkl', 'wb') as f:
pickle.dump(train_set, f)
# Test set
test_cat = process_folder(folder=pathlib.Path.cwd().joinpath('data/test/cat'))
test_dog = process_folder(folder=pathlib.Path.cwd().joinpath('data/test/dog'))
test_set = pd.concat([test_cat, test_dog], axis=0)
with open('test_set.pkl', 'wb') as f:
pickle.dump(test_set, f)
# Validation set
valid_cat = process_folder(folder=pathlib.Path.cwd().joinpath('data/validation/cat'))
valid_dog = process_folder(folder=pathlib.Path.cwd().joinpath('data/validation/dog'))
valid_set = pd.concat([valid_cat, valid_dog], axis=0)
with open('valid_set.pkl', 'wb') as f:
pickle.dump(valid_set, f)
图8——训练集
数据集包含所有猫的图像,然后是所有狗的图像。这对于训练集和验证集来说并不理想,因为神经网络会按照这个顺序看到它们。
你可以使用Scikit Learn中的随机函数来随机排序:
train_set = shuffle(train_set).reset_index(drop=True)
valid_set = shuffle(valid_set).reset_index(drop=True)
下面是它现在的样子:
图9——随机后的训练集
下一步是将特征与目标分离。我们将对所有三个子集进行拆分:
X_train = train_set.drop('class', axis=1)
y_train = train_set['class']
X_valid = valid_set.drop('class', axis=1)
y_valid = valid_set['class']
X_test = test_set.drop('class', axis=1)
y_test = test_set['class']
最后,使用数字编码目标变量。有两个不同的类(cat和dog),因此每个实例的目标变量应该包含两个元素。
例如,使用factorize函数进行编码:
y_train.factorize()
图10-factorize函数
标签被转换成整数——猫为0,狗为1。
你可以使用TensorFlow中的to_category函数,并传入factorize后的数组,以及不同类的数量(2):
y_train = tf.keras.utils.to_categorical(y_train.factorize()[0], num_classes=2)
y_valid = tf.keras.utils.to_categorical(y_valid.factorize()[0], num_classes=2)
y_test = tf.keras.utils.to_categorical(y_test.factorize()[0], num_classes=2)
因此,y_train现在看起来是这样的:
图11——目标变量
从概率的角度考虑——第一张图片有100%的几率是猫,0%的几率是狗。这些都是真实的标签,所以概率可以是0或1。
我们现在终于有了训练神经网络模型所需的一切。
用人工神经网络(ANN)训练图像分类模型
我随机选择了层的数量和每层的节点数量,以下2部分不能更改:
· 输出层——它需要两个节点,因为我们有两个不同的类。我们不能再使用sigmoid激活函数了,所以选择softmax。
· 损失函数——我们使用分类交叉熵。
其他部分可以随意更改:
以下是我在100个epoch后得到的结果:
图12:100个epoch后的ANN结果
60%的准确率比猜测稍微好一点,但性能一般。尽管如此,我们还是来检查一下训练期间指标发生了什么变化。
以下代码片段绘制了100个epoch中每个epoch的训练损失与验证损失:
plt.plot(np.arange(1, 101), history.history['loss'], label='Training Loss')
plt.plot(np.arange(1, 101), history.history['val_loss'], label='Validation Loss')
plt.title('Training vs. Validation Loss', size=20)
plt.xlabel('Epoch', size=14)
plt.legend();
图13:训练损失与验证损失
该模型能很好地学习训练数据,但不能推广。随着我们对模型进行更多epoch的训练,验证损失继续增加,这表明模型不稳定且不可用。
让我们看看准确度:
plt.plot(np.arange(1, 101), history.history['accuracy'], label='Training Accuracy')
plt.plot(np.arange(1, 101), history.history['val_accuracy'], label='Validation Accuracy')
plt.title('Training vs. Validation Accuracy', size=20)
plt.xlabel('Epoch', size=14)
plt.legend();
图14:训练准确度与验证准确度
类似的图片。验证精度稳定在60%左右,而模型对训练数据的拟合度过高。
对于一个包含20K训练图像的两类数据集,60%的准确率几乎是它所能达到的最差水平。原因很简单——Dense层的设计并不是为了捕捉二维图像数据的复杂性。
结论
现在你知道了——如何用人工神经网络训练一个图像分类模型,以及为什么你不应该这么做。这就像穿着人字拖爬山——也许你能做到,但最好不要。