Aujourd'hui, je veux vous parler de mon expĂ©rience d'utilisation d'un rĂ©seau de neurones pour trouver des produits similaires pour un systĂšme de recommandation de boutique en ligne. Je parlerai principalement de choses techniques. J'ai dĂ©cidĂ© d'Ă©crire cet article sur HabrĂ© car quand je commençais Ă peine Ă faire ce projet, j'ai trouvĂ© une solution appropriĂ©e sur HabrĂ©, mais il s'est avĂ©rĂ© qu'elle Ă©tait dĂ©jĂ dĂ©passĂ©e et devait ĂȘtre modifiĂ©e. Et j'ai donc dĂ©cidĂ© de mettre Ă jour le matĂ©riel pour ceux qui auront besoin d'une solution similaire.
Par ailleurs, je tiens Ă dire que c'est ma premiĂšre expĂ©rience dans la crĂ©ation d'un projet plus ou moins sĂ©rieux dans le domaine de la science des donnĂ©es, donc si l'un des collĂšgues les plus expĂ©rimentĂ©s voit ce qui peut encore ĂȘtre amĂ©liorĂ©, je ne serai que ravi des conseils. .
Je vais commencer par un petit aperçu des raisons pour lesquelles la logique choisie de la boutique en ligne a été choisie - à savoir, une recommandation basée sur des produits similaires (et non sur des méthodes de filtrage collaboratif, par exemple). Le fait est que ce systÚme de recommandation a été développé pour une boutique en ligne qui vend des montres et donc jusqu'à 90% des utilisateurs qui viennent sur le site n'y reviennent pas. En général, la tùche était la suivante: augmenter le nombre de pages vues par les utilisateurs qui accÚdent aux pages de produits spécifiques par le biais de la publicité. Ces utilisateurs ont consulté une page et ont quitté le site si le produit ne leur convenait pas.
Je dois dire que dans ce projet, je n'ai pas eu l'opportunitĂ© d'intĂ©grer le backend d'une boutique en ligne - une histoire classique pour les petites et moyennes boutiques en ligne. Il fallait se fier uniquement au systĂšme, que je ferai en dehors du site. Par consĂ©quent, en tant que solution visuelle sur le site lui-mĂȘme, j'ai dĂ©cidĂ© de crĂ©er un widget pop-up js. Une ligne ajoute js au code html, comprend le titre de la page Ă laquelle l'utilisateur est arrivĂ© et le transmet au backend du service. Si le backend trouve un produit dans sa base de donnĂ©es de produits prĂ©chargĂ©s, il recherche Ă nouveau dans la base de donnĂ©es prĂ©-prĂ©parĂ©e des produits des recommandations et les renvoie Ă js, et js les affiche ensuite dans le widget. De plus, pour rĂ©duire l'impact sur la vitesse de chargement, js crĂ©e une iframe, dans laquelle il effectue tout le travail d'affichage du widget. Entre autres,il vous permet Ă©galement de supprimer le problĂšme avec l'intersection des classes css du widget et du site.
, Data Science. , . , , - .
.
( , A/B-) - ; , , - .
. .
:
!pip install theano
%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
from keras.applications import vgg16
from keras.applications import Xception
from keras.preprocessing.image import load_img,img_to_array
from keras.models import Model
from keras.applications.imagenet_utils import preprocess_input
from PIL import Image
import os
import matplotlib.pyplot as plt
import numpy as np
from sklearn.metrics.pairwise import cosine_similarity
import pandas as pd
import theano
theano.config.openmp = True
( , ):
import re
def sorted_alphanumeric(data):
convert = lambda text: int(text) if text.isdigit() else text.lower()
alphanum_key = lambda key: [ convert(c) for c in re.split('([0-9]+)', key) ]
return sorted(data, key=alphanum_key)
dirlist = sorted_alphanumeric(os.listdir('images'))
r1 = []
r2 = []
for i,x in enumerate(dirlist):
if x.endswith(".jpg"):
r1.append((int(x[:-4]),i))
r2.append((i,int(x[:-4])))
extid_to_intid_dict = dict(r1)
intid_to_extid_dict = dict(r2)
:
imgs_path = "images/"
imgs_model_width, imgs_model_height = 224, 224
nb_closest_images = 3 # ( )
( ):
vgg_model = vgg16.VGG16(weights='imagenet')
( 1000 ImageNet - . 4096- , ).
â , .
:
feat_extractor = Model(inputs=vgg_model.input, outputs=vgg_model.get_layer("fc2").output)
, CNN . , :
feat_extractor.summary()
( , xml -, , , ; , ):
files = [imgs_path + x for x in os.listdir(imgs_path) if "jpg" in x]
print("number of images:",len(files))
:
import re
def atof(text):
try:
retval = float(text)
except ValueError:
retval = text
return retval
def natural_keys(text):
'''
alist.sort(key=natural_keys) sorts in human order
http://nedbatchelder.com/blog/200712/human_sorting.html
(See Toothy's implementation in the comments)
float regex comes from https://stackoverflow.com/a/12643073/190597
'''
return [ atof(c) for c in re.split(r'[+-]?([0-9]+(?:[.][0-9]*)?|[.][0-9]+)', text) ]
files.sort(key=natural_keys)
PIL :
original = load_img(files[1], target_size=(imgs_model_width, imgs_model_height))
plt.imshow(original)
plt.show()
print("image loaded successfully!")
PIL numpy array:
PIL - width, height, channel
Numpy - height, width, channel
numpy_image = img_to_array(original) #
batch format.
expand_dims
, - batchsize, height, width, channels. , 0.
image_batch = np.expand_dims(numpy_image, axis=0) # - (2-dims)
print('image batch size', image_batch.shape)
VGG:
processed_image = preprocess_input(image_batch.copy()) #
( ):
img_features = feat_extractor.predict(processed_image)
:
print("features successfully extracted!")
print("number of image features:",img_features.size)
img_features
, â .
importedImages = []
for f in files:
filename = f
original = load_img(filename, target_size=(224, 224))
numpy_image = img_to_array(original)
image_batch = np.expand_dims(numpy_image, axis=0)
importedImages.append(image_batch)
images = np.vstack(importedImages)
processed_imgs = preprocess_input(images.copy())
:
imgs_features = feat_extractor.predict(processed_imgs)
print("features successfully extracted!")
imgs_features.shape
:
cosSimilarities = cosine_similarity(imgs_features)
pandas dataframe:
columns_name = re.findall(r'[0-9]+', str(files))
cos_similarities_df = pd.DataFrame(cosSimilarities, columns=files, index=files)
cos_similarities_df.head()
. 6000 SKU. 6000 * 6000. float 0 1 8 , . , 430 ( 130 ). . , - GitHub, . GitHub 100 ( ). , - . :) - - - . :
cos_similarities_df_2.round(2) # cos_similarities_df_2 - ,
, . float. pandas float float16 - .
int:
cos_similarities_df_2.apply(lambda x: x * 100)
cos_similarities_df_2.apply(lambda x: x.astype(np.uint8))
31 . .
h5:
cos_similarities_df_2.to_hdf('storage/cos_similarities.h5', 'data')
40 . , -, GitHub, -, :)
, , :
import re
# function to retrieve the most similar products for a given one
def retrieve_most_similar_products(given_img):
print("-----------------------------------------------------------------------")
print("original product:")
original = load_img(given_img, target_size=(imgs_model_width, imgs_model_height))
original_img = int(re.findall(r'[0-9]+', given_img)[0])
print((df_items_2.iloc[[original_img]]['name'].iat[0], df_items_2.iloc[[original_img]]['pricer_uah'].iat[0], df_items_2.iloc[[original_img]]['url'].iat[0]))
plt.imshow(original)
plt.show()
print("-----------------------------------------------------------------------")
print("most similar products:")
closest_imgs = cos_similarities_df[given_img].sort_values(ascending=False)[1:nb_closest_images+1].index
closest_imgs_scores = cos_similarities_df[given_img].sort_values(ascending=False)[1:nb_closest_images+1]
for i in range(0,len(closest_imgs)):
original = load_img(closest_imgs[i], target_size=(imgs_model_width, imgs_model_height))
item = int(re.findall(r'[0-9]+', closest_imgs[i])[0])
print(item)
print((df_items_2.iloc[[item]]['name'].iat[0], df_items_2.iloc[[item]]['pricer_uah'].iat[0], df_items_2.iloc[[item]]['url'].iat[0]))
plt.imshow(original)
plt.show()
print("similarity score : ",closest_imgs_scores[i])
kbr = '' #
find_rec = int(df_items_2.index[df_items_2['name'] == kbr].tolist()[0]) # df_items_2 ,
print(find_rec)
retrieve_most_similar_products(files[find_rec])
:)
.
, - :
, :
import os
if not os.path.exists('storage'):
os.makedirs('storage')
if not os.path.exists('images'):
os.makedirs('images')
, xml - .
, , :
# importing required modules
import urllib.request
image_counter = 0
error_list = []
#
def image_from_df(row):
global image_counter
item_id = image_counter
filename = f'images/{item_id}.jpg'
image_url = f'{row.image}'
try:
conn = urllib.request.urlopen(image_url)
except urllib.error.HTTPError as e:
# Return code error (e.g. 404, 501, ...)
error_list.append(item_id)
except urllib.error.URLError as e:
# Not an HTTP-specific error (e.g. connection refused)
print('URLError: {}'.format(e.reason))
else:
# 200
urllib.request.urlretrieve(image_url, filename)
image_counter += 1
xml, :
df_items_2.apply(lambda row: image_from_df(row), axis=1)
, . . . , xml , . , , , , , .
for i in error_list:
df_items_2.drop(df_items_2.index[i], inplace = True)
df_items_2.reset_index(drop=True, inplace = True)
print(f' : {error_list}')
print(len(error_list))
, . , - ! )
, - )
P.S. , VGG - VGG19. , .
P.S.S , : , Senior JavaScript Developer ( js CORS-); , Senior Python Developer Senior Engineer ( Docker CI/CD pipeline); SkillFactory, SkillFactory Accelerator ( , Data Science ); (, A/B- ); (un autre mentor qui a aidĂ© Ă comprendre les problĂšmes de PNL et, en particulier, le travail des magnats lors de la crĂ©ation de chat bots (un autre projet sur lequel j'ai travaillĂ© dans le cadre de l'accĂ©lĂ©rateur et dont je parlerai peut-ĂȘtre un peu plus tard); c'est Emil Maggeramov (mentor, qui a en gĂ©nĂ©ral supervisĂ© mes progrĂšs dans l'accĂ©lĂ©rateur pour la crĂ©ation de ce projet); ce sont les camarades de classe Valery Kuryshev et Georgy Bregman (rĂ©guliĂšrement appelĂ©s une fois par semaine et partageant l'expĂ©rience acquise au cours de la semaine).