Рекомендації на основі зображень товарів

image

У даній статті я хотів би розглянути на практиці варіант побудови найпростішої рекомендаційної системи ґрунтується на схожості зображень товарів. Цей матеріал призначений для тих, хто хотів би спробувати застосувати Deep Learning, а саме згорткові нейронні мережі, в простому, цікавому і практично застосовне проекті, але не знає з чого почати.

Передісторія

До написання цього прототипу мене підштовхнув процес вибору футболок в інтернет-магазині. Перегорнувши 1000 з 11 000 товарів я трохи притомився. Дуже хотілося отримати можливість пошукати товари схожі на ті, що я вже відібрав. Вбудована рекомендаційна система не могла нічим допомогти. Було вирішено запив свій варіант і подивитися як він працює на реальних даних.

Парсинг картинок

Для початку був реалізований простий парсер картинок з цієї категорії. Зображення роздільною здатністю 250x250 були складені в одну папку з іменами виду ItemID.jpg. Вийшло близько 11 000 картинок.

Витяг ознак

Для визначення схожості зображень нам потрібно отримати їх векторні представлення. Для цього візьмемо сверточную нейронну мережу використовується для класифікації зображень (VGG-16), вже натреновані на великому датасете (ImageNet), і відріжемо від неї останній шар, що дає на виході ймовірності кожного з 1000 класів ImageNet. В результаті у нас вийде 4096-мірний вектор для кожного із зображень.

image

Прототип зручніше всього реалізовувати у ipython notebook, дуже раджу тим, хто ще не пробував.

За основу був узятий цей код: gist.github.com/baraldilorenzo/07d7802847aaad0a35d3

Подгружаем бібліотеки:

%matplotlib inline
from keras.models import Sequential
from keras.layers.core import Flatten, Dense, Dropout
from keras.layers.convolutional import Convolution2D, MaxPooling2D, ZeroPadding2D
from keras.optimizers import SGD
import cv2, numpy as np
import os
import h5py
from matplotlib import pyplot as plt

import theano
theano.config.openmp = True

Завантажуємо предобученную на ImageNet VGG-16 і видаляємо у неї останній шар:

def VGG_16(weights_path=None):
model = Sequential()
model.add(ZeroPadding2D((1,1),input_shape=(3,224,224)))
model.add(Convolution2D(64, 3, 3, activation='relu'))
model.add(ZeroPadding2D((1,1)))
model.add(Convolution2D(64, 3, 3, activation='relu'))
model.add(MaxPooling2D((2,2), strides=(2,2)))

model.add(ZeroPadding2D((1,1)))
model.add(Convolution2D(128, 3, 3, activation='relu'))
model.add(ZeroPadding2D((1,1)))
model.add(Convolution2D(128, 3, 3, activation='relu'))
model.add(MaxPooling2D((2,2), strides=(2,2)))

model.add(ZeroPadding2D((1,1)))
model.add(Convolution2D(256, 3, 3, activation='relu'))
model.add(ZeroPadding2D((1,1)))
model.add(Convolution2D(256, 3, 3, activation='relu'))
model.add(ZeroPadding2D((1,1)))
model.add(Convolution2D(256, 3, 3, activation='relu'))
model.add(MaxPooling2D((2,2), strides=(2,2)))

model.add(ZeroPadding2D((1,1)))
model.add(Convolution2D(512, 3, 3, activation='relu'))
model.add(ZeroPadding2D((1,1)))
model.add(Convolution2D(512, 3, 3, activation='relu'))
model.add(ZeroPadding2D((1,1)))
model.add(Convolution2D(512, 3, 3, activation='relu'))
model.add(MaxPooling2D((2,2), strides=(2,2)))

model.add(ZeroPadding2D((1,1)))
model.add(Convolution2D(512, 3, 3, activation='relu'))
model.add(ZeroPadding2D((1,1)))
model.add(Convolution2D(512, 3, 3, activation='relu'))
model.add(ZeroPadding2D((1,1)))
model.add(Convolution2D(512, 3, 3, activation='relu'))
model.add(MaxPooling2D((2,2), strides=(2,2)))

model.add(Flatten())
model.add(Dense(4096, activation='relu'))
model.add(Dropout(0.5))
model.add(Dense(4096, activation='relu'))
# model.add(Dropout(0.5))
# model.add(Dense(1000, activation='softmax'))


assert os.path.exists(weights_path), 'Model weights not found (see "weights_path" variable in script).'
f = h5py.File(weights_path)
for k in range(f.attrs['nb_layers']):
if k >= len(model.layers):
# we don't look at the last (fully-connected) layers in the savefile
break
g = f['layer_{}'.format(k)]
weights = [g['param_{}'.format(p)] for p in range(g.attrs['nb_params'])]
model.layers[k].set_weights(weights)
f.close()
print('Model loaded.')
return model


model = VGG_16('../../keras/vgg16_weights.h5')
sgd = SGD(lr=0.1, decay=1e-6, momentum=0.9, nesterov=True)
model.compile(optimizer=sgd, loss='categorical_crossentropy')

Завантажуємо і перетворимо зображення у формат придатний для нашої нейромережі:

path = "../../keras/tshirts/out/"
ims = []
files = []
for in f os.listdir(path):
if (f.endswith(".jpg")) and (os.stat(path+f) > 10000):
try:
files.append(f)
im = cv2.resize(cv2.imread(path+f), (224, 224)).astype(np.float32)
# plt.imshow(im)
# plt.show()
im[:,:,0] -= 103.939
im[:,:,1] -= 116.779
im[:,:,2] -= 123.68
im = im.transpose((2,0,1))
im = np.expand_dims(im, axis=0)
ims.append(im)
except:
print f

images = np.vstack(ims)

Створюємо словники для перетворення зовнішніх IDшников до наших і навпаки:

r1 =[]
r2= []
for i,x in enumerate(files):
r1.append((int(x[:-4]),i))
r2.append((i,int(x[:-4])))
extid_to_intid_dict = dict(r1)
intid_to_extid_dict = dict(r2)

Витягаємо векторні представлення з картинок:

out = model.predict(images)
print out
print out.shape

Виходить по 4096-мірного вектора для кожної з 11 556 картинок:

[[ 0.00000000 e+00 5.96046448 e-08 4.35693979 e+00 ..., 2.01165676 e-07
-2.30967999 e-07 5.48017263 e+00]
[ -2.98023224 e-08 -1.78813934 e-07 5.60834265 e+00 ..., 2.01165676 e-07
7.45058060 e-09 9.42541122 e+00]
[ 8.94069672 e-08 0.00000000 e+00 8.79157162 e+00 ..., 2.01165676 e-07
-2.30967999 e-07 8.50830841 e+00]
..., 
[ 5.17337513 e+00 -5.96046448 e-08 6.89156103 e+00 ..., 2.01165676 e-07
7.45058060 e-09 1.49011612 e-08]
[ 3.18071890 e+00 -1.78813934 e-07 -5.96046448 e-08 ..., 2.01165676 e-07
-2.30967999 e-07 1.49011612 e-08]
[ 8.19161701 e+00 5.96046448 e-08 9.62305927 e+00 ..., -3.72529030 e-08
-2.30967999 e-07 7.47453260 e+00]]
(11556, 4096)

Пошук схожих зображень

Знайдемо наскільки самих близьких за косинусному відстані зображень:

from sklearn.metrics.попарно import pairwise_distances
extid = 875317
i = extid_to_intid_dict[extid]
print i
plt.imshow(cv2.imread(path+files[i]))
plt.show()
dist = pairwise_distances(out[i],out, metric='cosine', n_jobs=1)

top = np.argsort(dist[0])[0:7]

for t in top:
print t,dist[0][t]
plt.imshow(cv2.imread(path+files[t]))
plt.show()

image

Веб-інтерфейс для тестів

Тестувати це все в ipython не дуже зручно, тому було вирішено зробити веб-інтерфейс на Django.

Зберігаємо необхідні дані для використання у веб-інтерфейсі:

import joblib
joblib.dump((extid_to_intid_dict,intid_to_extid_dict,out),"../../keras/tshirts/models/wo_1_layer.pkl")

Далі при запуску піднімаємо дані в пам'ять і при кожному запиті знаходимо по 5 найближчих сусідів

Ось результат — imgrec.ddns.net
При запиті без параметра вибирає рандомное зображення. Видає по 5 найближчих зображень для різних метрик відстані — «cosine», «геометрія»,«manhattan»

По-моєму, вийшло досить не погано.

Цікаво те, що нейромережа, навчена класифікувати зображення на ImageNet, вміє «розуміти» що зображено на футболці і підбирати схожі за змістом зображення.
Ось наприклад:
Кішки — imgrec.ddns.net/?itemid=966730
Ведмеді — imgrec.ddns.net/?itemid=650443
Люди — imgrec.ddns.net/?itemid=847003
Фрукти — imgrec.ddns.net/?itemid=1020961
Схожий стиль — imgrec.ddns.net/?itemid=932401
Схожа текстура — imgrec.ddns.net/?itemid=862294
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.