Много раз мы доверяем продавцам загружать правильные изображения продуктов для онлайн-продуктов на нашей платформе электронной коммерции, но из-за существующих проверок того, что продукт всегда должен сопровождать изображение, сторонние продавцы загружают стоковое изображение или изображение-заполнитель в случае отсутствия изображения для продукт доступен.
Проблема в том, что хотя мы не хотим показывать товары без изображений на нашем сайте, но из-за нулевой проверки изображения мы показываем изображения-заполнители на фоне товара, что иногда приводит к снижению продаж. В этом посте мы обсудим несколько подходов, которые мы предприняли для решения этой проблемы. Разделим наш подход на части:
- Обнаружение изображений-заполнителей, которые уже были замечены в нашем каталоге продуктов.
- Обнаружение изображений-заполнителей, которые не были замечены ранее.
Набор различных возможных изображений-заполнителей может быть огромным. Просто выполнив поиск в Google, можно найти 100 000 из них. Хотя продавцы 3P пытаются обмануть систему, загружая заполнители, они предпочитают либо повторно использовать их для нескольких продуктов, либо использовать изображения из очень небольшого подмножества похожих изображений-заполнителей. Поскольку очень сложно определить, что такое заполнитель. Мы пометим любое изображение как заполнитель, если оно «значительно» отличается от других допустимых изображений той же категории.
Пример заполнителя
Для первого подхода проблема сводится к поиску похожих изображений. Учитывая набор N изображений, просмотренных и помеченных как заполнители, классифицируйте новое изображение как заполнитель, если новое изображение похоже хотя бы на одно из них. Для этого мы решили, что будем использовать хэши изображений для сравнения двух изображений. Самые простые из известных нам хэшей изображений: Среднее хеширование (aHash), Разностное хэширование (dHash) и Перцепционное хэширование (pHash).
Я не буду вдаваться в подробности каждого из алгоритмов, а просто дам обзор.
- Все вышеперечисленные алгоритмы генерируют 64-битное строковое представление в виде хэша. Чтобы сравнить два изображения, просто преобразуйте хэш в 64-битные значения и вычислите расстояние Хэмминга между ними, то есть количество несовпадающих битовых позиций.
- Все они сначала уменьшают изображение до 8x8 (aHash и pHash) и 9x8 (dHash), а затем преобразуют его в оттенки серого.
- aHash вычисляет среднее значение 64 цветов и присваивает 1 всем цветам со значением больше среднего и 0 тем, которые меньше среднего.
- dHash вычисляет разницу между последовательными цветами. Разница вычисляется для каждой строки отдельно. Присвойте 1, если P[x][y] ‹ P[x][y+1], иначе 0, где x — номер строки, а y — номер столбца цветовой матрицы изображения 9x8.
- pHash вычисляет дискретное косинусное преобразование (DCT) изображения, затем присваивает 1 всем значениям, где оно больше среднего значения DCT, и 0 — значениям, меньшим среднего значения DCT.
Мы выбрали dHash и pHash из-за их превосходной производительности при небольших изменениях изображений. Наконец обнаружил, что pHash немного лучше, чем dHash.
- Предварительно вычислите pHash для всех изображений-заполнителей с тегами N.
- Вычислите pHash для нового изображения, когда оно появится.
- Вычислите расстояние Хэмминга между новым изображением и всеми N изображениями-заполнителями.
- Если минимальное расстояние от всех N заполнителей меньше некоторого порога (в нашем случае 15), мы помечаем новое изображение как заполнитель.
- Вычислите точность и отзыв для нескольких таких изображений. Получите около 96% точности и отзыва.
- Тестовый набор был сгенерирован с использованием класса Keras ImageDataGenerator путем добавления случайных вариаций (таких как сдвиг по высоте и ширине, увеличение/уменьшение масштаба, изменение масштаба и т. д.)
Класс python для подхода подобия на основе хеширования. Поместите код в файл с именами hashing_predictor.py:
import imagehash, os, glob, itertools, numpy as np from keras.preprocessing.image import img_to_array, load_img import data_generator as dg from sklearn.metrics import classification_report class Hashing(object): def __init__(self, threshold=15): self.hashes = [] self.threshold = threshold def fit(self, image_files): for img_file in image_files: img = load_img(img_file) self.hashes.append(imagehash.phash(img)) def predict(self, image_files): predictions = [] for img_file in image_files: img = load_img(img_file) im_hash = imagehash.phash(img) min_dist = float("Inf") for x in self.hashes: min_dist = min(min_dist, abs(im_hash-x)) if min_dist < self.threshold: predictions.append(1) else: predictions.append(0) return predictions def score(self, pos_image_files, neg_image_files): labels = [1]*len(pos_image_files) + [0]*len(neg_image_files) preds = self.predict(pos_image_files) + self.predict(neg_image_files) print classification_report(labels, preds) def save(self): dg.save_data_npy(np.array(self.hashes), "trained_phashes.npy") def load(self): self.hashes = list(dg.load_data_npy("trained_phashes.npy"))
Метод «подгонки» используется для создания хэшей для всех изображений-заполнителей с тегами. Метод «прогнозировать» используется для прогнозирования меток 1 (для заполнителя) и 0 (для не заполнителя).
import hashing_predictor as hp from hashing_predictor import Hashing import glob, os hashing = Hashing(threshold=15) train_image_files = glob.glob(os.path.join("tagged_placeholder_images", "*.*")) hashing.fit(train_image_files) pos_image_files = glob.glob(os.path.join("similar_placeholder_images", "*.*")) neg_image_files = glob.glob(os.path.join("product_images", "*.*")) print hashing.score(pos_image_files, neg_image_files)
Мы пока не рассматривали возможность масштабирования этого подхода для больших значений N, но с этим можно справиться либо с помощью локально чувствительного хеширования (LSH), либо с помощью KD-дерева (распределенная версия). Подробнее о крупномасштабном поиске изображений мы поговорим в следующем посте.
Проблема с подходом на основе хеша заключается в том, что он отлично работает для существующих изображений-заполнителей или очень похожих заполнителей. Но проблема с заполнителями заключается в том, что может быть много вариантов, которые сопоставление на основе хэша не сможет зафиксировать.
Еще одна проблема заключается в том, что количество помеченных изображений-заполнителей очень мало (около 60 или около того).
Таким образом, вместо того, чтобы полагаться на поиск похожих изображений-заполнителей, наш новый и обновленный подход заключается в том, чтобы «спросить» несколько «назначенных» (около 10) действительных изображений продукта из той же категории, что и новое изображение, похоже ли новое изображение на одно из них. . Если хотя бы 5 голосов говорят о том, что новое изображение очень отличается от того, как выглядят действительные изображения из той же категории, то мы помечаем новое изображение как заполнитель.
Получение голосов за изображения, принадлежащие к той же категории.
Идея позаимствована из one-shot learning framework.
- Создайте набор данных с парами изображений.
- Размер исходных изображений был изменен до 64x64, а значения масштабированы путем умножения на 1,0/255.
- Предполагая, что существует M различных категорий и каждая категория имеет Q изображений, мы можем создать 0,5*M*Q*(Q-1) пар положительных изображений.
- Метка 1 означает, что пара принадлежит к одной категории.
- Точно так же мы можем случайным образом выбирать пары изображений из двух разных категорий. Может быть максимум 0,5*M*(M-1)*Q2 различных отрицательных пар из M категорий.
- Метка 0 означает, что пара не принадлежит к одной и той же категории.
- Из положительных и отрицательных пар мы выбираем равное количество экземпляров (около 50 000).
- Мы заметили, что добавление к отрицательным парам некоторых изображений-заполнителей с тегами улучшает припоминание изображений-заполнителей.
- Мы не могли бы обучить бинарную модель CNN с одним входным изображением с меткой 1, указывающей на заполнитель, и 0, не являющимся заполнителем, потому что количество помеченных изображений-заполнителей было очень меньше.
- Мы экспериментировали, дополняя изображения-заполнители с помощью Keras ImageDataGenerator, но результаты не показали большого улучшения.
- Использование пар изображений в качестве входных данных позволяет нам генерировать больше данных, а не полагаться на методы увеличения.
- Обучите сиамский CNN, используя эту пару изображений.
- Мы использовали два подхода: в первом мы обучали слои CNN-MaxPooling с нуля, а затем добавляли полносвязные слои и
- Тот, в котором мы использовали переносное обучение из предварительно обученных весов VGG16 для слоев CNN и обучали только последний полностью связанный слой.
- Переносное обучение с VGG16 дало лучшие и более стабильные результаты.
- Два входа были объединены путем взятия поэлементной абсолютной разницы и прохождения через сигмовидный слой активации.
- Размер партии был сохранен на уровне 256.
- Количество пар изображений, используемых при обучении, составляло около 100 тыс., количество пар, используемых для проверки, составляло 20 тыс.
- Изображения-заполнители, используемые при тестировании данных, были явно загружены из изображений Google, поэтому эти изображения являются невидимыми заполнителями для модели.
- Количество пар в тестовых данных составило около 20 тысяч. Хотя количество изображений-заполнителей было намного меньше (около 100).
- Точность отзыва тестового набора составила 96% (лучшая модель). Отзыв для метки 0 (т.е. заполнители) составил 91%.
- Используя метод голосования путем выбора 11 случайных действительных изображений продукта для сравнения, отзыв для заполнителей оказался около 95%.
- Из 11, если хотя бы 5 из них дают метку 0, мы помечаем это изображение как заполнитель.
Этот подход довольно общий, поскольку его можно использовать для определения того, действительно ли изображение в категории продукта является изображением для этой категории. Обнаружение заполнителей становится особым вариантом использования.
Обучение пары изображений футболок с label=1
Обучение изображения футболки и изображения ноутбука с label=0
Обучение изображения футболки с изображением-заполнителем с label=0
Создадим файл siamese_predictor.py:
IMAGE_HEIGHT, IMAGE_WIDTH = 64, 64 def get_shared_model_vgg(image_shape): input = Input(shape=image_shape) base_model = VGG16(weights='imagenet', include_top=False, input_tensor=input) for layer in base_model.layers: layer.trainable = False n_layer = base_model.output n_layer = Flatten()(n_layer) n_layer = Dense(4096, activation='relu')(n_layer) n_layer = BatchNormalization()(n_layer) model = Model(inputs=[input], outputs=[n_layer]) return model class SiameseModel(object): def __init__(self, model_file_path, best_model_file_path, batch_size=256, training_samples=5000, validation_samples=5000, testing_samples=5000, use_vgg=False): self.model = None self.best_model_file_path = best_model_file_path self.batch_size = batch_size self.training_samples = training_samples self.validation_samples = validation_samples self.testing_samples = testing_samples def init_model(self): image_shape = (IMAGE_HEIGHT, IMAGE_WIDTH, 3) input_a, input_b = Input(shape=image_shape), Input(shape=image_shape) shared_model = get_shared_model_vgg(image_shape) shared_model_a, shared_model_b = shared_model(input_a), shared_model(input_b) n_layer = Lambda(lambda x: K.abs(x[0]-x[1]))([shared_model_a, shared_model_b]) n_layer = BatchNormalization()(n_layer) out = Dense(1, activation="sigmoid")(n_layer) self.model = Model(inputs=[input_a, input_b], outputs=[out]) adam = optimizers.Adam(lr=0.001) self.model.compile(optimizer=adam, loss="binary_crossentropy", metrics=['accuracy']) def fit(self): self.init_model() train_num_batches = int(math.ceil(float(self.training_samples)/self.batch_size)) valid_num_batches = int(math.ceil(float(self.validation_samples)/self.batch_size)) self.model.fit_generator(dg.get_image_data_siamese(self.training_samples, 'train', batch_size=self.batch_size), steps_per_epoch=train_num_batches, validation_data=dg.get_image_data_siamese(self.validation_samples, 'validation', batch_size=self.batch_size), validation_steps=valid_num_batches, epochs=10, verbose=1) def predict_proba(self, image_data): return self.model.predict(image_data) def predict(self, image_data): return np.rint(self.predict_proba(image_data)).astype(int) def score(self): data_generator, test_labels, pred_labels = dg.get_image_data_siamese(self.testing_samples, 'test', batch_size=self.batch_size), [], [] total_batches = int(math.ceil(float(self.testing_samples)/self.batch_size)) num_batches = 0 for batch_data, batch_labels in data_generator: test_labels += batch_labels.tolist() pred_labels += self.predict(batch_data).tolist() num_batches += 1 if num_batches == total_batches: break print(classification_report(test_labels, pred_labels)) def score_ensemble(self, test_image_arr, voting_images_arr, frac=0.5, probability_threshold=0.5): image_data_0, image_data_1 = [], [] index_map = collections.defaultdict(list) for v_img_arr in voting_images_arr: image_data_0.append(v_img_arr) image_data_1.append(test_image_arr) image_data_0, image_data_1 = np.array(image_data_0), np.array(image_data_1) proba = self.predict_proba([image_data_0, image_data_1]) proba = np.array([x[0] for x in proba]) pred = proba-probability_threshold pred[pred <= 0] = 0 pred[pred > 0] = 1 return 0 if np.count_nonzero(pred) < frac*voting_images_arr.shape[0] else 1
Я опустил некоторые детали из кода, чтобы сделать его короче и достаточным для объяснения вышеупомянутого подхода.
Я использую генераторы Python для обучения и тестирования модели в мини-пакетах. Коды для генератора определены в другом файле, детали которого я здесь опускаю, но основная идея заключается в том, что поскольку существует около 100 000 пар обучающих изображений, и каждая пара изображений содержит два изображения размером 64x64x3 (3 для количества каналов в RGB-изображения). Для загрузки всех данных в память потребуется около 18 ГБ памяти.
Вместо этого мы можем загрузить партии размером 256 для 391 партии (100 000 пар изображений) и обучить, используя метод Keras fit_generator.
Генератор данных python выглядит примерно так (код неполный):
def get_image_data_siamese(num_samples, prefix='train', batch_size=256): n = min(num_samples, len(data_pairs)) num_batches = int(math.ceil(float(n)/batch_size)) np.random.seed(42) batch_num = 0 while True: m = batch_num % num_batches if m == 0: p = np.random.permutation(n) image_data_1, image_data_2, labels = image_data_1[p], image_data_2[p], labels[p] start, end = m*batch_size, min((m+1)*batch_size, n) batch_num += 1 yield [image_data_1[start:end], image_data_2[start:end]], labels[start:end]
В более раннем классе «SiameseModel» мы определяем метод «score_ensemble», который принимает массив тестовых изображений (64x64x3) и набор из 11 массивов изображений для голосования. Метод сравнивает тестовое изображение со всеми 11 массивами изображений для голосования, и если хотя бы процент прогнозов «frac» говорит, что метка должна быть 0, то мы классифицируем тестовое изображение как заполнитель.
Первоначально опубликовано на www.stokastik.in 11 февраля 2019 г.