AI-система подбора образа (Outfit Recommendation)

Проектируем и внедряем системы искусственного интеллекта: от прототипа до production-ready решения. Наша команда объединяет экспертизу в машинном обучении, дата-инжиниринге и MLOps, чтобы AI работал не в лаборатории, а в реальном бизнесе.
Показано 1 из 1 услугВсе 1566 услуг
AI-система подбора образа (Outfit Recommendation)
Средняя
~2-4 недели
Часто задаваемые вопросы
Направления AI-разработки
Этапы разработки AI-решения
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1240
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1167
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    867
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1084
  • image_logo-advance_0.png
    Разработка логотипа компании B2B Advance
    563
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    829

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-квиз.