首页
学习
活动
专区
工具
TVP
发布
精选内容/技术社群/优惠产品,尽在小程序
立即前往

马里奥与瓦里奥之争:图像识别,你要 pick 哪一个?

从我没上学的时候开始,我就记得我花了很多时间在我最喜欢的 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

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20180803G1WU7L00?refer=cp_1026
  • 腾讯「腾讯云开发者社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。
  • 如有侵权,请联系 cloudcommunity@tencent.com 删除。

扫码

添加站长 进交流群

领取专属 10元无门槛券

私享最新 技术干货

扫码加入开发者社群
领券