选自xolmon
机器之心编译
参与:Hitomi
情人节过了,单身狗也想飙一把车。在这篇技术博客(资源的真义)中,日本开发者使用深度学习框架实现了根据图片检索 AV 女优的功能。
PC: MacBook Air
CPU: 1.4 GHz Intel Core i5
内存: 4GB
普通的 MacBook Air 就可以实现这一程序,但是学习速度缓慢,由内存不足导致的各种 Crash 让开发工作变得非常痛苦。
(1) 收集各女优的图片
(2) 使用 dlib 提取面部图像并调整为 96*96 的大小
(3) 使用数据扩张(Data Augmentation)将女优面部图像的数据扩张到 1000 张
(4) 将数据转换为 numpy 文件
(5) 使用 Chainer 进行面部图像的学习
(6) 在完成学习后的模型下,对任意图片进行预测
这段有很多方法但是并不好写出来,所以请略过。只介绍下可以使用 Python 的 Beautiful Soup4 从网页上批量抓取数据。
将取得的女优图片按名字建立存储目录。
./folder
|--- /actress1
| |--- image1.jpg
| |--- image2.jpg
| |--- image3.jpg
|
|--- /actress2
| .
| .
|--- /actress3
.
.
.
说到图像识别,OpenCV 应该更加有名。不过在提取面部图像部分,dlib 程序库的误识别比较少,因此这里使用 dlib 可以更加精确。
使用 OpenCV 和 dlib 进行人脸识别的比较可以参考这个视频:dlib vs OpenCV face detection (https://www.youtube.com/watch?v=LsK0hzcEyHI)(译者注:youtube 的视频,有必要的话可以搬过来。)
dlib 不仅可以提取面部图像,也有识别眼睛,鼻子,脸形等要素的机能。
importos
import sys
import glob
import cv2
from PIL import Image
import dlib
"""
INPUT_DIR是收集的女优图片所在的目录名
OUTPUT_DIR是提取后的图片存放的目录名(文件夹的构成与INPUT_DIR一样)
"""
detector = dlib . get_frontal_face_detector()
# 取得各女优的目录列表
dir_list = os . listdir(INPUT_DIR)
for i, dir_name in enumerate(dir_list):
if not os . path . exists(os . path . join(OUTPUT_DIR, dir_name)):
os . mkdir(os . path . join(OUTPUT_DIR, dir_name))
image_files = glob . glob(os . path . join(INPUT_DIR, dir_name, "*.jpg"))
for j, image_file in enumerate(image_files):
img = cv2 . imread(image_file)
dets = detector(img, 1)
open_img = Image . open(image_file)
for k, d in enumerate(dets):
# 丢弃尺寸小于80的图像
if d . right() - d . left() < 80 or d . bottom() - d . top() < 80:
continue
image_file = image_file . replace(INPUT_DIR, OUTPUT_DIR)
# 如果一张图中提取了多个人脸,则进行重命名
output_file = image_file . replace('.jpg', '_' + str(k) + '.jpg')
cropped_img = open_img . crop((d . left(), d . top(), d . right(), d . bottom()))
cropped_img . resize((96,96)) . save(output_file, 'JPEG', quality = 100, optimize = True)
参考资料:dlib.net face_detect.py (http://dlib.net/face_detector.py.html)
在深度学习的过程中,如果数据量不够大,可以人工增加训练集的大小。通过平移, 翻转, 加噪声等方法从已有数据中创造出一批"新"的数据,这就是数据扩张 (Data augmentation)。
import os
import sys
import glob
import random
import numpy as np
from scipy import misc
""" 从选择的目录里提取文件 """
def load_data_from_dir(input_dir_name, input_dir_list, start_index, test_freq):
train_list = []
test_list = []
for dir_index, dir_name in enumerate(input_dir_list):
image_files = glob.glob(os.path.join(input_dir_name, dir_name, "*.jpg"))
train_count = 0
test_count = 0
print('directory:{} index:{}'.format(dir_name, dir_index + start_index))
for file_index, file_name in enumerate(image_files):
image = misc.imread(file_name)
label = np.int32(dir_index + start_index)
if not file_index % test_freq == 0: # set train datq
train_list.append((dir_name, image, label))
train_count += 1
else:
test_list.append((dir_name, image, label))
test_count += 1
print("directory:{} total:{} train:{} test:{}".format(
dir_name, train_count + test_count, train_count, test_count))
return train_list, test_list
""" 将数据储存为numpy格式 """
def save_dataset_numpy(data_list, image_path, label_path):
image_list = []
label_list = []
for _, image, label in data_list:
image_list.append(image)
label_list.append(label)
image_data = np.array(image_list, dtype=np.float32)
label_data = np.array(label_list, dtype=np.int32)
np.save(image_path, image_data)
np.save(label_path, label_data)
for i in xrange(0, len(DIR_LIST), 10):
# 生成10个分类的文件
train_list, test_list = load_data_from_dir(INPUT_DIR, dir_list[i:i+args.interval], i, 10)
train_data_path = os.path.join(OUTPUT_DIR, 'train', 'data-{}.npy'.format(i+args.interval))
train_label_path = os.path.join(OUTPUT_DIR, 'train', 'label-{}.npy'.format(i+args.interval))
test_data_path = os.path.join(OUTPUT_DIR, 'test', 'data-{}.npy'.format(i+args.interval))
test_label_path = os.path.join(OUTPUT_DIR, 'test', 'label-{}.npy'.format(i+args.interval))
save_dataset_numpy(train_list, train_data_path, train_label_path)
save_dataset_numpy(test_list, test_data_path, test_label_path)
一开始打算使用 Tensorflow 做,不过由于自己想实现不少额外的机能,因此改用 Chainer 进行。
最初的学习,建立了一个 Cifar-10 (http://www.cs.toronto.edu/~kriz/cifar.html)(一般物品的 10 个分类)的学习方法,来对实际收集到的数据进行学习。
失败的地方:
最初是打算使用多进程来构建程序,不过 Debug 非常的辛苦,觉得还是先构建一个更简单的程序比较好。
如果一开始就读取所有图像,导入的图像会占用 1.7GB 的内存,导致死机。由于这个原因,创建了一个 BatchIterator 类,每 Batch 删除一次数据来释放内存,防止程序出现混乱。
// 每张图片的大小
96×96×3 = 27648(byte)
// 每类图片的大小
27648×1000 = 27648000(byte) = 26.4(MB)
// 所有图片 (66类) ... 可以计算么?
26.4×66 = 1742.4(MB) = 1.7(GB)
"""
Batch iterator class
Usage:
batch_iter = BatchIter(DATA_DIR, 100)
for batch_data, batch_label in batch_iter:
batch_start_time = time.time()
x = np.asarray(batch_data, dtype=np.float32).transpose((0, 3, 1, 2))
t = np.asarray(train_batch_label, dtype=np.int32)
x = Variable(xp.asarray(x))
t = Variable(xp.asarray(t))
optimizer.update(model, x, t)
"""
class BatchIter (object):
def __init__ (self, data_dir, batch_size):
self . index = 0
self . batch_size = batch_size
self . data_files = glob . glob(os . path . join(data_dir, 'data-*.npy'))
self . label_files = glob . glob(os . path . join(data_dir, 'label-*.npy'))
data_size = 0
for data in self . data_files:
loaded_data = np . load(data)
data_size += loaded_data . shape[0]
del loaded_data
self . data_size = data_size
assert len(self . data_files) == len(self . label_files), "Invalid data size."
def __iter__ (self):
return self
def next (self):
if self . index >= self . data_size:
raise StopIteration ()
data = np . zeros((self . batch_size, IMAGE_SIZE, IMAGE_SIZE, 3))
label = np . zeros((self . batch_size))
incremental_value = int(self . batch_size / len(self . data_files))
count = 0
for i in range(len(self . data_files)):
loaded_data = np . load(self . data_files[i])
loaded_label = np . load(self . label_files[i])
assert loaded_data . shape[0] == loaded_label . shape[0], "Loaded data size is invalid."
perm = np . random . permutation(loaded_data . shape[0])
if i + 1 == len(self . data_files): # last item
incremental_value = self . batch_size - count
idx = perm[0:incremental_value]
else :
idx = perm[0:incremental_value]
data[count:count + incremental_value] = loaded_data[idx]
label[count:count + incremental_value] = loaded_label[idx]
count += incremental_value
del loaded_data
del loaded_label
self . index += self . batch_size
return data, label
参考资料:
CNN 画像認識分野でのディープラーニングの研究動向 (http://ibisml.org/archive/ibis2013/pdfs/ibis2013-okatani.pdf)
深層畳み込みニューラルネットワークを用いた画像スケーリング (http://postd.cc/image-scaling-using-deep-convolutional-neural-networks-part1/)
CNNのチュートリアル (https://tech.d-itlab.co.jp/ml/666/)
Tensorflow
TensorFlowでアニメゆるゆりの制作会社を識別する (http://kivantium.hateblo.jp/entry/2015/11/18/233834)
TensorFlowによるディープラーニングで、アイドルの顔を識別する (http://d.hatena.ne.jp/sugyan/20160112/1452558576)
Tensor Flow: How To (http://kzky.hatenablog.com/entry/2015/12/24/Tensor_Flow%3A_How_To)
Chainer
GitHub - chainer/examples/imagenet/ (https://github.com/pfnet/chainer/tree/master/examples/imagenet)
GitHub - mitmul/chainer-cifar10 (https://github.com/mitmul/chainer-cifar10)
はじめてのアニメ顔認識 with Chainer (http://qiita.com/homulerdora/items/9a9af1481bf63470731a)
def
set_model
(model_name, model_path):
model_fn = os.path.basename('models/'
+ model_name +
'.py'
)
model = imp.load_source(model_fn.split('.'
)[
0
],
'models/'
+ model_name +
'.py'
).model
print('Load model from '
, model_path)
serializers.load_hdf5(model_path, model)
return model
def
set_optimizer
(opt_name, opt_path, model):
if opt_name == 'MomentumSGD'
:
optimizer = optimizers.MomentumSGD(momentum=0.9
)
elif opt_name == 'Adam'
:
optimizer = optimizers.Adam()
elif opt_name == 'AdaGrad'
:
optimizer = optimizers.AdaGrad()
else:
raiseValueError
(
'Invalid architecture name'
)
optimizer.setup(model)
print('Load optimizer state from '
, opt_path)
serializers.load_hdf5(opt_path, optimizer)
return optimizer
def
detect_face
(image_file):
detector = dlib.get_frontal_face_detector()
#img = cv2.imread(image_file)
image = misc.imread(image_file)
dets = detector(image, 1
)
d = dets[0
]
cropped_image = image[d.top():d.bottom(), d.left():d.right()]
resized_image = misc.imresize(cropped_image, (96
,
96
))
return resized_image
# 载入预测模型
model = set_model(model_name, model_path)
optimizer = set_optimizer(opt_name, opt_path, model)
detected_face_image = detect_face(input_image)
# 使用载入的模型进行预测
x = np.asarray(detected_face_image, dtype=np.float32).transpose((
0
,
3
,
1
,
2
))
x = Variable(np.asarray(x), volatile=
'on'
)
pred = model.predict(x).data
# 读取label (label在创建numpy形式的文件时做成)
categories = np.loadtxt(label_path,
str
, delimiter=
"/n"
)
# 按相似分高低重新排序
score = pred.reshape((pred.size,))
result =
zip
(score, categories)
result =
sorted
(result, reverse=
True
)
results = []
for
i, (score, label) in
enumerate
(result[:
10
]):
if i ==5
: break
print('num:{} score:{:.5f} label:{}'
.
format(i +
1
, score *
100
, label))
results.append({
'label'
: label,
'score'
:
str
(
round
(score *
100
,
2
))
})
前面的内容,建立了一个学习模型,它可以提取一张图片上的各种要素 (如眼睛,鼻子,脸型等) 并进行分类,从而判断这张图最像哪一位女优;本节则通过计算全结合层的特征向量的相似度,来进行相似图像的检索。
比起 Chainer,Keras 更加容易使用。因此借助 Keras,最终完全实现了根据图片检索 AV 女优的这一功能。
数据扩张
Keras 可以使用 ImageDataGenerator 简单地进行数据的扩张。
不过,把随机的一张图像进行倾斜,变换后所得到的学习数据其实跟原图没有区别。这样的方法一般被认为会造成过度学习。
为降低输入数据冗余性,需要对数据进行 ZCA 白化。白化是降低输入数据冗余性的预处理过程,通过白化可以使得学习算法的输入具有如下性质:(i) 特征之间相关性较低;(ii) 所有特征具有相同的方差。
ZCA 白化的方法请参考以下文章:
データの白色化 - DEEPTONEWorks (http://deeptoneworks.com/2016/10/18/20161018020000/)
CIFAR-10 と ZCA whitening - まんぼう日記 (http://takatakamanbou.hatenablog.com/entry/2015/02/15/150430)
脸部图像的正面化
第 2 节实现了使用 dlib 进行人脸检测,本次则更进一步,把脸部图像的特征点抽出,使用仿射变换,将眼和口的位置摆正。下面是实现本功能所使用的 openface 和 facenet,两者都已经实装。
facenet/src/align_dlib.py (https://github.com/davidsandberg/facenet/blob/master/src/align_dlib.py)_ _
openface/util/align-dlib.py (https://github.com/cmusatyalab/openface/blob/master/util/align-dlib.py)
from
keras.preprocessing.image
import
ImageDataGenerator
# 读入何种data和label
data, label = load_data()
datagen = ImageDataGenerator(
zca_whitening=
True
,
rotation_range=10
,
width_shift_range=0.1
,
height_shift_range=0.1
,
horizontal_flip=True
)
datagen.fit(data)
#
将 datagen.flow
代入模型的 fit_generator
函数,即可扩张动态数据
model.fit_generator(datagen.flow(data, label, batch_size=
32
),
samples_per_epoch=data.shape[0
],
nb_epoch=100
)
模型的构造
现阶段的学习数据还很少,模型和超参数的调整还没有完全,暂且使用的是下面这样的模型。
def
conv_bn_relu
(x, out_ch, name):
x = Convolution2D(out_ch,3
,
3
, border_mode=
'same'
, name=name)(x)
x = BatchNormalization(name='{}_bn'
.
format
(name))(x)
x = Activation('relu'
, name=
'{}_relu'
.
format
(name))(x)
return
x
def
face_model
(input_shape=(
3
,
224
,
224
), nb_classes, weights_path=
None
):
inputs = Input(shape=input_shape, name='input'
)
x = conv_bn_relu(inputs,64
, name=
'block1_conv1'
)
x = conv_bn_relu(x,64
, name=
'block1_conv2'
)
x = MaxPooling2D((2
,
2
), strides=(
2
,
2
))(x)
x = conv_bn_relu(x,128
, name=
'block2_conv1'
)
x = conv_bn_relu(x,128
, name=
'block2_conv2'
)
x = MaxPooling2D((2
,
2
), strides=(
2
,
2
))(x)
x = conv_bn_relu(x,256
, name=
'block3_conv1'
)
x = conv_bn_relu(x,256
, name=
'block3_conv2'
)
x = conv_bn_relu(x,256
, name=
'block3_conv3'
)
x = MaxPooling2D((2
,
2
), strides=(
2
,
2
))(x)
x = conv_bn_relu(x,512
, name=
'block4_conv1'
)
x = conv_bn_relu(x,512
, name=
'block4_conv2'
)
x = conv_bn_relu(x,512
, name=
'block4_conv3'
)
x = MaxPooling2D((2
,
2
), strides=(
2
,
2
))(x)
x = Flatten()(x)
x = Dense(4096
, activation=
'relu'
, name=
'fc1'
)(x)
x = Dense(nb_classes, activation='softmax'
, name=
'predictions'
)(x)
model = Model(input
=inputs, output=x)
相似度的计算
本次的模型,是将最终层之前一层的全结合层特征向量提取出来,与各女优面部图像提取的特征向量计算余弦值,来判断与各女优的相似程度。
import
numpy
as
np
from
scipy.spatial.distance
import
cosine
from
keras.models
import
Model, model_from_json
from
keras.preprocessing
import
image
from
keras.preprocessing.image
import
img_to_array
def
calculate_similarity
():
#
载入模型
model_json =open
(
'face_model.json'
).read()
base_model = model_from_json(model_json)
base_model.load_weights('face_model.h5'
)
#
建立提取全结合层和 4096
维特征向量的模型
model = Model(input
=base_model.
input
, output=base_model.get_layer(
'fc1'
).output)
image_file ='image.jpg'
img = image.load_img(image_file, target_size=(96
,
96
))
x = img_to_array(img)
x = np.expand_dims(x, axis=0
)
features = model.predict(x)
features = features.flatten().tolist()
# actress_features
是之前计算好的各女优面部的特征向量
#
计算输入图片所提取的特征向量和各女优特征向量余弦值的相似度
score =1
- cosine(features, actress_features)
这部分跟深度学习完全没有关系。一开始尝试使用了 Heroku 来建立网站,不过最终使用的是 Conoha。
dlib 和 chainer 在 Heroku 的服务器上安装非常困难。Conoha 的运行效率虽然有点问题,不过倒是很好的完成了 dlib 和 chainer 的安装。
樱花 VPS 也是一个不错的选择,不过樱花服务器没有初次使用的优惠,而 Conoha 则有针对新用户的免费方案。
1.chainerによるディープラーニングでAV 女優の類似画像検索サービスをつくったノウハウを公開する
2.KerasでAV 女優の類似画像検索機能を実装する
想直接要作者的实现?
✄------------------------------------------------