AI-система рекомендации образов (outfit recommendation)
Outfit recommendation — задача более сложная, чем рекомендация отдельных товаров: нужно подобрать совместимые вещи с учётом стиля, цветовой совместимости, повода и гардероба пользователя. Pinterest, Stitch Fix, ASOS используют computer vision + knowledge graph для этой задачи.
Модель совместимости предметов одежды
import numpy as np
import pandas as pd
import torch
import torch.nn as nn
from sklearn.metrics.pairwise import cosine_similarity
class OutfitCompatibilityModel(nn.Module):
"""
Siamese network: оценивает совместимость двух предметов гардероба.
Вход: визуальный эмбеддинг (ResNet) + атрибутный вектор.
"""
def __init__(self, visual_dim: int = 2048, attr_dim: int = 64,
hidden_dim: int = 256):
super().__init__()
input_dim = visual_dim + attr_dim
self.item_encoder = nn.Sequential(
nn.Linear(input_dim, hidden_dim),
nn.ReLU(),
nn.Dropout(0.3),
nn.Linear(hidden_dim, 128),
nn.LayerNorm(128)
)
self.compatibility_head = nn.Sequential(
nn.Linear(256, 64),
nn.ReLU(),
nn.Linear(64, 1),
nn.Sigmoid()
)
def encode_item(self, visual_emb: torch.Tensor,
attr_emb: torch.Tensor) -> torch.Tensor:
combined = torch.cat([visual_emb, attr_emb], dim=-1)
return self.item_encoder(combined)
def forward(self, item1_visual: torch.Tensor, item1_attrs: torch.Tensor,
item2_visual: torch.Tensor, item2_attrs: torch.Tensor) -> torch.Tensor:
emb1 = self.encode_item(item1_visual, item1_attrs)
emb2 = self.encode_item(item2_visual, item2_attrs)
combined = torch.cat([emb1, emb2], dim=-1)
return self.compatibility_head(combined)
class ColorCompatibilityChecker:
"""Цветовая совместимость по теории цвета"""
# Палитра совместимых комбинаций
NEUTRAL_COLORS = {'white', 'black', 'grey', 'beige', 'navy'}
COLOR_WHEEL = {
'red': 0, 'orange': 30, 'yellow': 60, 'yellow_green': 90,
'green': 120, 'teal': 150, 'blue': 180, 'purple': 270, 'pink': 330
}
def are_compatible(self, color1: str, color2: str) -> float:
"""Совместимость двух цветов (0-1)"""
# Нейтральные цвета сочетаются со всем
if color1 in self.NEUTRAL_COLORS or color2 in self.NEUTRAL_COLORS:
return 0.9
# Одинаковые цвета — монохром (хорошо)
if color1 == color2:
return 0.85
angle1 = self.COLOR_WHEEL.get(color1)
angle2 = self.COLOR_WHEEL.get(color2)
if angle1 is None or angle2 is None:
return 0.5
diff = abs(angle1 - angle2)
diff = min(diff, 360 - diff)
# Комплементарные (180°): высокая совместимость
if 160 <= diff <= 200:
return 0.85
# Аналогичные (30-60°): хорошая совместимость
if 30 <= diff <= 60:
return 0.80
# Триадные (120°): средняя
if 100 <= diff <= 140:
return 0.65
# Плохая совместимость
return 0.40
class OutfitBuilder:
"""Сборка образов из гардероба пользователя"""
def __init__(self):
self.color_checker = ColorCompatibilityChecker()
def build_outfit(self, user_wardrobe: list[dict],
occasion: str = 'casual',
anchor_item: dict = None) -> list[dict]:
"""
Подбор образа для конкретного повода.
anchor_item: якорный предмет (например, только что купленный)
"""
# Фильтруем по случаю
occasion_filter = {
'casual': ['casual', 'smart_casual'],
'work': ['business', 'smart_casual'],
'formal': ['formal', 'business'],
'sport': ['sport', 'activewear'],
}
valid_styles = occasion_filter.get(occasion, ['casual'])
relevant_items = [
item for item in user_wardrobe
if item.get('style') in valid_styles
]
if not relevant_items:
return []
# Стандартный образ: верх + низ + обувь + аксессуар
categories = {'top': [], 'bottom': [], 'shoes': [], 'accessory': []}
for item in relevant_items:
cat = item.get('category', 'top')
if cat in categories:
categories[cat].append(item)
outfit = []
# Если есть якорный элемент — начинаем с него
if anchor_item:
outfit.append(anchor_item)
anchor_cat = anchor_item.get('category', 'top')
anchor_color = anchor_item.get('color', 'black')
categories.pop(anchor_cat, None)
else:
anchor_color = 'black'
# Добираем остальные части, максимизируя совместимость цветов
for cat in ['top', 'bottom', 'shoes', 'accessory']:
items = categories.get(cat, [])
if not items:
continue
best_item = max(items, key=lambda x:
self.color_checker.are_compatible(anchor_color, x.get('color', 'black'))
)
outfit.append(best_item)
# Обновляем якорный цвет (берём доминирующий в образе)
if best_item.get('color') not in self.color_checker.NEUTRAL_COLORS:
anchor_color = best_item.get('color', anchor_color)
return outfit
def score_outfit(self, outfit: list[dict]) -> dict:
"""Оценка образа"""
if len(outfit) < 2:
return {'score': 0, 'feedback': 'Недостаточно предметов'}
colors = [item.get('color', 'black') for item in outfit]
color_scores = []
for i in range(len(colors)):
for j in range(i+1, len(colors)):
color_scores.append(self.color_checker.are_compatible(colors[i], colors[j]))
avg_compatibility = np.mean(color_scores) if color_scores else 0.5
# Проверка категорий
categories = [item.get('category') for item in outfit]
has_complete_outfit = all(cat in categories for cat in ['top', 'bottom', 'shoes'])
total_score = avg_compatibility * 0.6 + (0.4 if has_complete_outfit else 0)
feedback = []
if avg_compatibility < 0.55:
feedback.append('Цвета могут конфликтовать')
if not has_complete_outfit:
feedback.append('Образ неполный')
if not feedback:
feedback.append('Гармоничный образ')
return {
'score': round(total_score, 2),
'color_compatibility': round(avg_compatibility, 2),
'feedback': '; '.join(feedback)
}
Системы рекомендации образов снижают возвраты из-за «не знаю с чем носить» на 15-20%. Ключевой challenge: холодный старт гардероба (нужно минимум 10-15 предметов для хороших рекомендаций) и субъективность стиля. Решение: explicit предпочтения через onboarding-квиз.







