雅虎开源了一个进行色情图像检测的深度学习解决方案。据文章介绍,这可能是首个识别 NSFW 图像的开源模型。
开源地址:https://github.com/yahoo/open_nsfw
自动识别一张对工作做来说并不适合/不保险的图像(Not Suitable/Safe For Work - NSFW)——包括暴力图像和成人图像——是研究者们几十年来一直在试图解决的重要问题。由于当下图像与用户生成的内容主宰了互联网,过滤 NSFW 图像成为网页应用和移动应用的一个重要组成部分。
随着计算机视觉、改进的训练数据和深度学习算法的发展,计算机现在能够以更高的精度来自动分类 NSFW 图像内容。
NSFW 素材的定义是主观的,而识别这些图像的任务并非没有价值。此外,在某一语境下使人反感的东西却可以适合于另一语境。为此,我们下文所描述的模型只侧重于一种 NSFW 内容:色情图像。NSFW 简笔图、漫画、文字、写实暴力图像或其他不当内容的识别解决方案不适用于此模型。
据我们目前所知,还没有用以识别 NSFW 图像的开源模型或算法。秉承合作精神并怀揣推进这一努力的希望,我们发布了自己的深度学习模型,它能让开发者使用一个 NSFW 检测分类器来进行实验,同时向我们提供反馈以改善分类器的性能。
我们的通用 Caffe 深度神经网络模型(general purpose Caffe deep neural network model)以图像作为输入并输出一个概率(即一个介于 0 和 1 之间的数字),可用于检测和过滤 NSFW 图像。开发者可以针对具体使用情况来用这个概率过滤掉 ROC 曲线上低于某个适当阈值的图像,或用在搜索结果中进行图像排名。
卷积神经网络架构和权衡
近年来,卷积神经网络已经在图像分类问题中取得了巨大成功。自 2012 年以来,新的卷积神经网络架构一直在不断改进标准 ImageNet 分类挑战的精度。一些主要突破包括了 AlexNet(2012)、GoogLeNet、VGG(2013)和残差网络(Residual Networks)(2015)。
这些网络在运行时间、内存需求和准确性方面有不同的权衡。运行时间和内存需求的主要指标是:
Flops 或连接——一个神经网络中的连接数量决定了向前传播过程之中的计算操作数量,这与图像识别时的网络运行时间成比例。
参数——一个神经网络中的参数数量决定了加载网络所需的内存量。
理想情况下,我们希望一个网络拥有最少的 flops 和最少的参数,而达到最大精度。
训练用于 NSFW 识别的深度网络
我们使用一个包含正(即 NSFW)图像和负(即 SFW-suitable/safe for work)图像的数据集来训练模型。
由于数据属性的问题,我们没有发布训练图像或其他细节,但我们开源了可用于开发者独立进行分类的输出模型。
我们使用 Caffe 深度学习库(Caffe deep learning library)和 CaffeOnSpark;后者是一个用于分布式学习的强大开源框架,令你可以在 Hadoop 和 Spark 模型训练集群中使用 Caffe 深度学习。
在训练过程中,图像被重新调整到 256x256 像素,水平翻转进行数据增强,并被随机裁剪为 224x224 像素,然后送入网络。在训练残差网络时,我们使用了 ResNet 论文中所描述的规模增大(scale augmentation)来避免过度拟合。我们评估各种架构来找到运行时间和精度之间的权衡。
MS_CTC——这种架构是由微软限制时间成本的那篇论文提出。它在卷积层和全连接层相结合的速度和精度方面秒杀了 AlexNet。
Squeezenet——这种架构提出了 fire 模块——包含层挤压,然后扩大输入数据团。这有助于节省参数数量,使 Imagenet 的精度与 AlexNet 的一样好,尽管内存需求仅为 6MB。
VGG——这种架构有 13 层卷积层和 3 层 FC 层。
GoogLeNet——GoogLeNet 提出了 Inception 模块并拥有 20 个卷积层阶段。它还在中间层中使用 hanging loss functions 来解决深度网络中的梯度递减问题。
ResNet——ResNet 使用快捷连接来解决梯度递减问题。我们使用了作者所发布的 50 层的残差网络。
ResNet-thin——该模型是使用我们的 pynetbuilder 工具生成,并复制了残差网络论文中的 50 层网络(每层过滤器的半数)。你可以在这里(https://github.com/jay-mahadeokar/pynetbuilder/tree/master/models/imagenet)找到更多有关如何生成和训练模型的细节。
不同架构之间的权衡:精度 vs(网络中的)flops 数量 vs(网络中的)参数数量。
深度模型首次在 ImageNet 1000 类数据集上进行预训练。我们将每个网络的最后一层(FC1000)更换为 2 节点的全连接层。然后我们精调 NSFW 数据集中的权重。注意我们让与最后的 FC 层相乘的学习率是精调后的其他层的 5 倍。我们还调整了超参数(hyper parameters)(步长、基本学习率)以优化性能。
我们观察到,NSFW 分类任务的模型性能与 ImageNet 分类任务中的预训练模型性能有关,所以如果我们有一个更好的预训练模型,它将有助于精调分类任务。下面的图表显示了我们所提出的 NSFW 评估集合的相对性能。请注意,图中的假正率(FPR)和一个固定的假负率(FNR)所针对的是我们的评估数据,在这里作说明用。要用该模型进行 NSFW 过滤的话,我们建议你们使用自己的数据来绘制 ROC 曲线并挑选一个合适的阈值。
在 Imagenet 上的模型与在 NSFW 数据集上精调的模型的性能比较
我们发布了 thin ResNet 50 模型,因为它在准确度方面做了很好的折中,并且该模型在运行时间(CPU 上运行时间 < 0.5 秒)和内存(~ 23 MB)方面体量轻巧。请参阅我们的 Git 库来查看我们的模型指令和用法。我们鼓励开发者尝试将此模型用于 NSFW 过滤的情况。如有任何关于模型性能的问题或反馈,我们都会支持并尽快回复。
结果可以通过在你的数据集上精调模型来改进。如果你改善了性能或者训练了一个使用不同架构的 NSFW 模型,我们都鼓励那么为模型贡献出力或将链接分享到我们的描述页面。
代码
#!/usr/bin/env python
"""
Copyright 2016 Yahoo Inc.
Licensed under the terms of the 2 clause BSD license.
Please see LICENSE file in the project root for terms.
"""
import numpy as np
import os
import sys
import argparse
import glob
import time
from PIL import Image
from StringIO import StringIO
import caffe
def resize_image(data, sz=(256, 256)):
"""
Resize image. Please use this resize logic for best results instead of the
caffe, since it was used to generate training dataset
:param str data:
The image data
:param sz tuple:
The resized image dimensions
:returns bytearray:
A byte array with the resized image
"""
img_data = str(data)
im = Image.open(StringIO(img_data))
if im.mode != "RGB":
im = im.convert('RGB')
imr = im.resize(sz, resample=Image.BILINEAR)
fh_im = StringIO()
imr.save(fh_im, format='JPEG')
fh_im.seek(0)
return bytearray(fh_im.read())
def caffe_preprocess_and_compute(pimg, caffe_transformer=None, caffe_net=None,
output_layers=None):
"""
Run a Caffe network on an input image after preprocessing it to prepare
it for Caffe.
:param PIL.Image pimg:
PIL image to be input into Caffe.
:param caffe.Net caffe_net:
A Caffe network with which to process pimg afrer preprocessing.
:param list output_layers:
A list of the names of the layers from caffe_net whose outputs are to
to be returned. If this is None, the default outputs for the network
are returned.
:return:
Returns the requested outputs from the Caffe net.
"""
if caffe_net is not None:
# Grab the default output names if none were requested specifically.
if output_layers is None:
output_layers = caffe_net.outputs
img_data_rs = resize_image(pimg, sz=(256, 256))
image = caffe.io.load_image(StringIO(img_data_rs))
H, W, _ = image.shape
_, _, h, w = caffe_net.blobs['data'].data.shape
h_off = max((H - h) / 2, 0)
w_off = max((W - w) / 2, 0)
crop = image[h_off:h_off + h, w_off:w_off + w, :]
transformed_image = caffe_transformer.preprocess('data', crop)
transformed_image.shape = (1,) + transformed_image.shape
input_name = caffe_net.inputs[0]
all_outputs = caffe_net.forward_all(blobs=output_layers,
**{input_name: transformed_image})
outputs = all_outputs[output_layers[0]][0].astype(float)
return outputs
else:
return []
def main(argv):
pycaffe_dir = os.path.dirname(__file__)
parser = argparse.ArgumentParser()
# Required arguments: input file.
parser.add_argument(
"input_file",
help="Path to the input image file"
)
# Optional arguments.
parser.add_argument(
"--model_def",
help="Model definition file."
)
parser.add_argument(
"--pretrained_model",
help="Trained model weights file."
)
args = parser.parse_args()
image_data = open(args.input_file).read()
# Pre-load caffe model.
nsfw_net = caffe.Net(args.model_def, # pylint: disable=invalid-name
args.pretrained_model, caffe.TEST)
# Load transformer
# Note that the parameters are hard-coded for best results
caffe_transformer = caffe.io.Transformer({'data': nsfw_net.blobs['data'].data.shape})
caffe_transformer.set_transpose('data', (2, 0, 1)) # move image channels to outermost
caffe_transformer.set_mean('data', np.array([104, 117, 123])) # subtract the dataset-mean value in each channel
caffe_transformer.set_raw_scale('data', 255) # rescale from [0, 1] to [0, 255]
caffe_transformer.set_channel_swap('data', (2, 1, 0)) # swap channels from RGB to BGR
# Classify.
scores = caffe_preprocess_and_compute(image_data, caffe_transformer=caffe_transformer, caffe_net=nsfw_net, output_layers=['prob'])
# Scores is the array containing SFW / NSFW image probabilities
# scores[1] indicates the NSFW probability
print "NSFW score: " , scores[1]
if __name__ == '__main__':
main(sys.argv)