从我没上学的时候开始,我就记得我花了很多时间在我最喜欢的 Game Boy 上玩游戏。我最喜欢的两个平台游戏是 Mario 和 Wario。记得,我奶奶看了看我正在玩的游戏,问我那是什么游戏。我解释说这是超级马里奥。过了一阵子,她看到我仍然在玩游戏,她看着屏幕问我:“又在玩马里奥?这游戏要玩多久啊?”但这是一个完全不同的游戏,瓦里奥。这段记忆启发我去实践图像识别技术,并尝试看看我是否可以训练一个能够准确识别截图来源的分类器。
本文使用的马里奥游戏训练视频的片段
本文使用的瓦里奥游戏训练视频的片段
在本文中,我使用两种方法。基本的是逻辑回归,而高级一些的是卷积神经网络(使用基于 Tensorflow 的 Keras 做后端)。本文并不着力于解释算法背后的逻辑或数学原理,因为在 Medium 和其他地方已有大量的优秀文章说明此事。相反,我试图展示如何将一个简单,即兴的想法快速转换为数据科学项目。
为了简洁起见,本文只粘贴一些代码段,而完整的代码可以在作者的 GitHub 仓库中找到。
准备数据
根据我童年的记忆,我选择了两个游戏进行这个实验:超级马里奥大陆2的六个金币和超级马里奥大陆3的瓦里奥大陆。我选择这些游戏不仅是因为他们是我的最爱,也是因为,查看游戏中的画面,你会发现它们长得很像,这样或许会让任务增加一些难度。
我在考虑什么是获取大量游戏截图的最省事的方式,于是我决定从 Youtube 上的通关视频中抓取截图。Python 的 pytube 可以帮助我们完成这项任务。我可以轻而易举地用几行代码下载下整段视频。
# library
from pytube import YouTube
mario_video = YouTube('https://www.youtube.com/watch?v=lXMJt5PP3kM')
# viewing available video formats
print('Title:', mario_video.title, '---')
stream = mario_video.streams.filter(file_extension = "mp4").all()
for i in stream:
print(i)
# downloading the video
mario_video.streams.get_by_itag(18).download()
下一步,从视频中剪切出帧。为此,我遍历所有帧(使用 OpenCV)并仅将每秒的第 n 帧保存到指定的文件夹。我决定使用一万张图片(每个场景五千张)。在这两种方法中,为了确保可比性,我使用同样的 80:20 的比例来分割训练和测试数据集。
def scrape_frames(video_name, dest_path, n_images, skip_seconds):
# function for scraping frames from videos
vidcap = cv2.VideoCapture(video_name)
total_frames = int(vidcap.get(cv2.CAP_PROP_FRAME_COUNT))
fps = int(vidcap.get(cv2.CAP_PROP_FPS))
every_x_frame = math.floor((total_frames - skip_seconds * fps) / n_images) - 1
success,image = vidcap.read()
frame_count = 0
img_count = 0 while success:
success,image = vidcap.read() #success might be false and image might be None
if frame_count > (skip_seconds * 30):
# break if the video ended
if not success:
break
# action on every x-th frame
if (frame_count % every_x_frame == 0):
cv2.imwrite(dest_path + "_" + str(img_count) + '.jpg', image)
img_count += 1
if (round(img_count / n_images, 2) * 100 % 10 == 0):
print("Completed:", round(img_count / n_images, 2), "done.", end="\r")
if img_count == n_images:
break frame_count += 1
切分帧时,我会跳过前 60 秒的视频,这部分主要包含开场过场画面和操作菜单(我不会在视频结尾处也这样做,因此,可能会包含一些噪音,不过我们到时候看着办!)。
马里奥类别示例
瓦里奥类别示例
在查看预览之后,很显然,图像的尺寸不同。这就是我要将它们重新缩放为 64x64 像素的原因。另外,对于逻辑回归,我将把图像转换成灰度,以减少模型的特征量( CNN 将处理 3 个颜色通道)。
适用于逻辑回归的 64x64 灰度图像
逻辑回归
我将从比较简单的模型开始。逻辑回归是一个基础的二元分类器,即,一个分类器能分辨出是类别一还是类别二。
话虽如此,但要使用逻辑回归来解决图像分类问题,我首先需要准备数据。输入应当与 Scikit-Learn 中的其他模型中要求的完全一致,即,特征矩阵 X 和标签 y。
因为本文旨在展示如何为特定的问题构建图像分类器,因此,我不会把重点放在算法参数调优,而是直接使用逻辑回归的默认值。让我直奔结果!
逻辑回归对测试集预测结果的混淆矩阵
上面展现的是分类器在测试集上的预测结果,这部分数据是不能用于训练的(属于 20% 的数据)。这结果看起来非常好,实际上,可能有点好得不够真实了。让我们查看一些正确/错误的图像分类情形。
分类正确的图像
分类错误的图像
五个分类错误的图像中,四个图像的原因非常直白。这些图像只是一些转场画面,分类器(二元分类器)这样的情形无能为力。第二个截屏来自超级马里奥的等级地图,它与游戏的其他部分明显不同(不是平台游戏)。但是,我们也能看到模型正确地分类了另一个地图(分类正确的图像3)。
卷积神经网络
这一部分显然比逻辑回归要复杂一些。第一步,包括以特定的方式存储图像,而 Keras 可以轻松做到这一点:
mario_vs_wario/
training_set/
mario/
mario_1.jpg
mario_2.jpg
...
wario/
wario_1.jpg
wario_2.jpg
...
test_set/
mario/
mario_1.jpg
mario_2.jpg
...
wario/
wario_1.jpg
wario_2.jpg
这个文件目录树展示了我是如何为此项目构建文件夹和文件的。下一部分是数据扩充。其思想是对可用的图像进行一些随机变换,由此让网络看到更多的独特的图像以便用于训练。这样可以防止过拟合,并带来更高的泛化能力。我只使用了几种变换:
rescale - 在做任何其他处理之前,将数据乘以的某个特定的值。原始图像由 0-255 区间内的 RGB 数值组成。这些数值对于模型(具有常见的学习速率的模型)来说可能太大了,因此乘以因子 1/255 能将数值重新调整至 0-1 区间。
shear_range - 随机的剪切变换
zoom_range - 随机的放缩局部图像
horizontal_flip - 随机水平翻转一半的图像(适用于不存在水平不对称假设的情形,如,真实世界)。我决定不使用这个功能,因为对于游戏截图而言这样做没有意义
除了指定图像的存储路径,我们还要确定我们输入给神经网络的图像大小(这里采用64x64,与逻辑回归相同)。
train_datagen = ImageDataGenerator(rescale = 1./255,
shear_range = 0.2,
zoom_range = 0.2,
horizontal_flip = False)
test_datagen = ImageDataGenerator(rescale = 1./255)
training_set = train_datagen.flow_from_directory('training_set',
target_size = (64, 64),
batch_size = 32,
class_mode = 'binary')
test_set = test_datagen.flow_from_directory('test_set',
target_size = (64, 64),
batch_size = 32,
class_mode = 'binary')
接下来,我展示了进行了一些变换后的图像示例。我们看到图像在两侧被拉伸。
现在是搭建 CNN 模型的时候了。首先,我初始化 3 个卷积层。在第一个层中,我还需要指定输入图像的尺寸(64x64,3 个 RGB 通道)。之后,Keras会自动处理尺寸。对于所有神经元,我使用 ReLU 作为激活函数。
之后就是平整化卷基层后。因为最后两层是全连接层(密集层),我需要将卷积层中的数据从卷基层转换为 1 维向量。在两个全链接层之间,我们也使用了 Dropout 。简而言之,在训练期间,Dropout 随机地忽略了被指定的数量的神经元。这是一种防止过拟合的方法。最后一个全连接层(密集层)使用 sigmoid 激活函数,并返回给定观察样本属于某一个类别的概率。最后一步基本上与逻辑回归类似。
# Initialising
cnn_classifier = Sequential()
# 1st conv. layer
cnn_classifier.add(Conv2D(32, (3, 3), input_shape = (64, 64, 3), activation = 'relu'))cnn_classifier.add(MaxPooling2D(pool_size = (2, 2)))
# 2nd conv. layer
cnn_classifier.add(Conv2D(32, (3, 3), activation = 'relu'))
cnn_classifier.add(MaxPooling2D(pool_size = (2, 2)))
# 3nd conv. layer
cnn_classifier.add(Conv2D(64, (3, 3), activation = 'relu'))
cnn_classifier.add(MaxPooling2D(pool_size = (2, 2)))
# Flattening
cnn_classifier.add(Flatten())
# Full connection
cnn_classifier.add(Dense(units = 64, activation = 'relu'))
cnn_classifier.add(Dropout(0.5))
cnn_classifier.add(Dense(units = 1, activation = 'sigmoid'))
cnn_classifier.summary()
现在运行 CNN (这也许需要花费一段时间......)。我使用 ADAM 作为优化器,选择二元交叉熵函数作为此二元分类任务的损失函数,并使用准确率来评估结果(不需要使用不同的度量,因为在这种特定情况下,准确性就是我关心的结果)。
# Compiling the CNN
cnn_classifier.compile(optimizer = 'adam',
loss = 'binary_crossentropy',
metrics = ['accuracy']) cnn_classifier.fit_generator(training_set,
steps_per_epoch = 2000,
epochs = 10,
validation_data = test_set,
validation_steps = 2000)
神经网络的表现如何呢?让我们瞧瞧!
CNN 对测试集预测结果的混淆矩阵
嗯,准确率低于逻辑回归模型,但对于这种快速构建的模型来说结果可以用很好来形容了。可以通过改变卷积层/全连接层(密集层)的数量,调整 Dropout,对图像执行额外的转换的方法来微调网络。另一个准确率较低的可能因素是转换隐藏了图像中的一些数据(例如图像底部的摘要栏)。实际上,我最初怀疑这个摘要栏可能在图像识别问题上关系重大,因为它几乎存在于所有截图中,并且两个游戏之间略有不同。但我一会儿会再谈这个问题。
现在是时候深入探究一些正确/错误分类的图像的例子了。在这种分类模型下,与逻辑回归结果的一个显而易见的不同是,我们看不到类似于转场画面这样明显的错误分类示例。
分类正确的图像
分类错误的图像
用 LIME 解释分类问题
plt.figure(figsize=(25,5))
shuffle(correctly_classified_indices)
for plot_index, good_index in enumerate(correctly_classified_indices[0:5]):
plt.subplot(1, 5, plot_index + 1)
explainer = lime_image.LimeImageExplainer()
explanation = explainer.explain_instance(X_eval[good_index], cnn_classifier.predict_classes, top_labels=2, hide_color=0, num_samples=1000)
temp, mask = explanation.get_image_and_mask(0, positive_only=False, num_features=10, hide_rest=False)
x = mark_boundaries(temp / 2 + 0.5, mask)
plt.imshow(x, interpolation='none')
plt.title('Predicted: {}, Actual: {}'.format(labels_index[cnn_pred[good_index][0]],
labels_index[y_eval[good_index]]), fontsize = 15)
下面我展示了对图像应用 LIME 解释技术的结果。绿色区域表示对预测类别的正反馈,红色表示负反馈。从分类正确的样本我们可以看出,角色总是处于绿色区域,这是合情合理的。但是,对于分类错误的情况,我们发现,一些图像只有一种颜色(模型并不知道自己该看什么地方,不该看什么地方)。这给我们提供了启发,并且我认为通过进一步的工作我们可以从 LIME 中提取出更多信息。
对分类正确的图片应用 LIME 解释技术
对分类错误的图片应用 LIME 解释技术
总结
本文我们介绍了如何把即兴的想法快速转化为图像分类的项目。文中实践的两种分类方法在数据集上表现都很不错,我相信只要进行进一步的调整,CNN 就能获得更好的分数。
关于下一步调整的想法:
添加更多游戏(不同的平台游戏或 马里奥与瓦里奥游戏的不同章节)来研究模型在多分类任务中的表现
用不同的方法准备数据,因为两段视频都具有更高的分辨率,我们可以从当前图像的中间切割数据,以获得更大的图片(128*128)。这也可以解决底部汇总栏带来的潜在问题。
生成 CNN 数据时,添加额外的图像变换
尝试在图像中检测马里奥与瓦里奥(物体检测技术)
我希望你喜欢这篇文章。如果你对机器学习框架或模型有任何可能的改进建议,请在评论中告诉我!
文中的代码可以在我的GitHub【3】上找到。
参考文献与链接
【1】Keras 关于CNN的博客:
https://blog.keras.io/building-powerful-image-classification-models-using-very-little-data.html
【2】https://arxiv.org/pdf/1602.04938.pdf
【3】https://github.com/erykml/mario_vs_wario
近期好课
翻译:Leo
审校:010
编辑:Queen
原文:
https://towardsdatascience.com/mario-vs-wario-image-classification-in-python-ae8d10ac6d63
关注集智AI学园公众号
获取更多更有趣的AI教程吧!
搜索微信公众号:swarmAI
学园网站:campus.swarma.org
商务合作和投稿转载|swarma@swarma.org
领取专属 10元无门槛券
私享最新 技术干货