AI-анализ эффективности рекламных креативов
Рекламный отдел создаёт 50 вариантов баннера — какой запустить? Традиционный ответ: A/B тест. Проблема: A/B требует трафика и времени. AI-анализ предсказывает CTR и вовлечённость по визуальным характеристикам до запуска, используя накопленную историческую связь «характеристики изображения → performance».
Извлечение визуальных признаков
import torch
import torch.nn.functional as F
from transformers import CLIPProcessor, CLIPModel
from PIL import Image
import numpy as np
import cv2
class CreativeFeatureExtractor:
"""
Мультимодальные признаки: CLIP semantic + низкоуровневые visual.
"""
def __init__(self):
self.clip = CLIPModel.from_pretrained(
'openai/clip-vit-large-patch14'
).eval().cuda()
self.processor = CLIPProcessor.from_pretrained(
'openai/clip-vit-large-patch14'
)
@torch.no_grad()
def extract_clip_features(
self, image: Image.Image
) -> np.ndarray:
inputs = self.processor(images=image, return_tensors='pt').to('cuda')
emb = self.clip.get_image_features(**inputs)
return F.normalize(emb, dim=-1).cpu().numpy().squeeze()
def extract_visual_features(self, image: Image.Image) -> dict:
"""
Низкоуровневые признаки, коррелирующие с CTR:
- face_area_ratio: наличие лица (face = +18% CTR по Nielsen)
- contrast: высокий контраст → заметность
- color_harmony: гармоничная палитра
- text_coverage: сколько % занимает текст
- brightness_variance: визуальная сложность
"""
img_array = np.array(image)
h, w = img_array.shape[:2]
features = {}
# Контраст (Michelson)
gray = cv2.cvtColor(img_array, cv2.COLOR_RGB2GRAY)
features['contrast_michelson'] = float(
(gray.max() - gray.min()) / (gray.max() + gray.min() + 1e-8)
)
# Яркостная дисперсия
features['brightness_variance'] = float(gray.std() / 128.0)
# Доминирующие цвета (k=5 через k-means)
pixels = img_array.reshape(-1, 3).astype(np.float32)
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 20, 1.0)
_, labels, centers = cv2.kmeans(
pixels, 5, None, criteria, 5, cv2.KMEANS_PP_CENTERS
)
counts = np.bincount(labels.flatten(), minlength=5)
dominant_color = centers[counts.argmax()]
features['dominant_hue'] = float(
cv2.cvtColor(
dominant_color.reshape(1,1,3).astype(np.uint8),
cv2.COLOR_RGB2HSV
)[0,0,0] / 180.0
)
features['dominant_saturation'] = float(dominant_color.max() - dominant_color.min()) / 255.0
# Детекция лиц
face_cascade = cv2.CascadeClassifier(
cv2.data.haarcascades + 'haarcascade_frontalface_default.xml'
)
faces = face_cascade.detectMultiScale(gray, 1.1, 4)
total_face_area = sum(fw * fh for _, _, fw, fh in faces) if len(faces) > 0 else 0
features['face_area_ratio'] = total_face_area / (h * w)
features['has_face'] = int(len(faces) > 0)
features['face_count'] = len(faces)
# Доля текстовых регионов (через морфологию)
_, binary = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
gradient = cv2.morphologyEx(binary, cv2.MORPH_GRADIENT, kernel)
features['text_coverage_estimate'] = float(
(gradient > 0).mean()
)
return features
Предиктивная модель CTR
import pandas as pd
import numpy as np
import lightgbm as lgb
from sklearn.model_selection import cross_val_score
from sklearn.preprocessing import StandardScaler
class CreativeCTRPredictor:
"""
Обучаем на исторических данных: (visual_features, clip_embedding) → CTR.
Целевая переменная: log(CTR) для нормализации распределения.
"""
def __init__(self):
self.feature_extractor = CreativeFeatureExtractor()
self.model = lgb.LGBMRegressor(
n_estimators=500,
learning_rate=0.05,
max_depth=6,
min_child_samples=20,
subsample=0.8,
colsample_bytree=0.8,
reg_lambda=0.1
)
self.scaler = StandardScaler()
def build_feature_vector(self, image: Image.Image) -> np.ndarray:
visual = self.feature_extractor.extract_visual_features(image)
clip_emb = self.feature_extractor.extract_clip_features(image)
visual_vec = np.array(list(visual.values()))
# CLIP 768-dim + визуальные признаки ~10 dim
return np.concatenate([clip_emb, visual_vec])
def predict_ctr(
self, image: Image.Image
) -> dict:
features = self.build_feature_vector(image)
features_scaled = self.scaler.transform(features.reshape(1, -1))
log_ctr = self.model.predict(features_scaled)[0]
ctr_predicted = np.exp(log_ctr)
# SHAP объяснимость — топ-3 фактора
import shap
explainer = shap.TreeExplainer(self.model)
shap_values = explainer.shap_values(features_scaled)
return {
'predicted_ctr': round(float(ctr_predicted), 4),
'ctr_percentile': None, # заполняется из распределения на train
'top_factors': self._top_shap_factors(shap_values[0], features)
}
def _top_shap_factors(
self, shap_vals: np.ndarray, feature_vals: np.ndarray, top_k: int = 3
) -> list:
top_indices = np.argsort(np.abs(shap_vals))[::-1][:top_k]
feature_names = (
[f'clip_{i}' for i in range(768)] +
['contrast', 'brightness_var', 'dominant_hue',
'dominant_sat', 'face_ratio', 'has_face',
'face_count', 'text_coverage']
)
return [
{
'feature': feature_names[i] if i < len(feature_names) else f'feat_{i}',
'shap_value': float(shap_vals[i]),
'direction': 'positive' if shap_vals[i] > 0 else 'negative'
}
for i in top_indices
]
Ключевые инсайты из данных
По накопленной статистике рекламных платформ (Google, Meta):
| Визуальный признак | Влияние на CTR | Примечание |
|---|---|---|
| Наличие лица (frontal) | +15–22% | Особенно для fashion, beauty |
| Высокая насыщенность цвета (>0.6) | +8–14% | Не работает для B2B |
| Текст < 20% площади | +10–17% | Meta ограничение 20% |
| Контраст > 0.7 | +6–11% | Заметность в ленте |
| Лицо смотрит на CTA | +12% | Eye tracking исследования |
| Warm colors (hue 0-60°) | +5–9% | Для food, lifestyle |
Сроки
| Задача | Срок |
|---|---|
| Модель на исторических данных клиента (500+ креативов) | 3–5 недель |
| Полная система с API и интеграцией в creative workflow | 6–10 недель |
| Генеративная оптимизация (AI-коррекция баннера) | 10–16 недель |







