
Привет, чемпионы! Сегодня хочу разобрать на реальном примере, как иногда самые неочевидные идеи те, что в момент презентации заставляют тимлидов молча поправлять очки, а менеджеров ёрзать на стуле могут не просто выстрелить, а полностью перевернуть продукт. Это история не про гениальный прорыв, а скорее про настойчивость, готовность к экспериментам и немного удачи. Всё началось с того, что мы упёрлись в классический потолок роста в, казалось бы, совершенно непримечательной нише мобильном приложении для поиска и записи в автосервисы.

У нас был стандартный, почти шаблонный продукт: каталог услуг со средними по рынку ценами, модуль онлайн записи, карта с геолокацией мастерских, даже отзывы и рейтинги. Всё как у людей. Но проблема была в том, что мы были как все. А в условиях, когда на каждом углу есть аналоги, конкуренция идёт не за функционал, а за доверие и внимание пользователя. Люди заходили, смотрели прайс, звонили в пару мест и уходили. Удержание было низким, монетизация ещё ниже. Нужен был крючок. Не просто ещё одна кнопка в интерфейсе, а что то, что давало бы мгновенную, осязаемую пользу и решало реальную боль.
И вот на одной из планерок, где мы в очередной раз ломали голову над тем, как увеличить конверсию, я бросила: «А что, если сделать так, чтобы пользователь мог просто сфоткать свою проблему потёкшее масло, скрипящие тормоза, вмятину на бампере а мы ему примерно назовём поломку и прикинем, во сколько это выльется?». В комнате повисла тишина. Послышалось что то вроде «нейросеть?», «а обучающая выборка?», «а точность?», «юридические риски». Но решили взяться, так как я была уверена в бомбовом результате. И понеслась.
Мы не стали строить космический корабль. Мы начали с минимально жизнеспособного продукта буквально с одного типа поломок. Взяли свёрточную сеть и обучили её на датасете из нескольких тысяч изображений: царапины разной глубины и длины, вмятины с разной геометрией, подтёки жидкостей. Главной задачей было не просто распознать дефект, а сразу оценить масштаб бедствия и дать примерный расчёт стоимости ремонта. Ключевым моментом была именно прозрачность: пользователь получал непросто диагноз «вмятина», а расчёт в рублях «выравнивание элемента, покраска, материалы от 15 000 ₽». Это снимало главный страх любого автовладельца неизвестность и риск быть обманутым в сервисе.
Технологическая магия под капотом: как работает наша свёрточная сеть
Давайте немного углубимся в технические детали, потому что именно здесь скрывалась вся магия. Мы использовали архитектуру CNN (Convolutional Neural Network), но с несколькими ключевыми модификациями, адаптированными именно под наши задачи. Основа это каскад свёрточных слоёв, каждый из которых учился выделять всё более сложные признаки. Первые слои распознавали простые паттерны: границы, углы, текстуры. Последующие комбинировали их в более сложные структуры: форму вмятины, направление царапины, площадь повреждения. Для обучения мы использовали transfer learning взяли предобученную на ImageNet модель и дообучили её на наших данных. Это позволило добиться высокой точности даже при относительно небольшом датасете. Но главной фишкой стал механизм сегментации изображений. Вместо того чтобы анализировать всё фото целиком, сеть сначала выделяла область повреждения. Это значительно повышало точность и снижало вероятность ошибок из за фона или плохого освещения. Алгоритм работал в несколько этапов:детектирование объекта → сегментация области → классификация типа повреждения → оценка параметров (глубина, площадь, сложность).
Вот как выглядел ключевой фрагмент нашего пайплайна обработки изображения:
Скрытый текст
import torch
import torch.nn as nn
import torch.nn.functional as F
from torchvision.ops import deform_conv2d
class MultiScaleFeatureFusion(nn.Module):
def __init__(self, in_channels, reduction_ratio=16):
super().__init__()
self.channel_attention = nn.Sequential(
nn.Linear(in_channels, in_channels // reduction_ratio),
nn.ReLU(inplace=True),
nn.Linear(in_channels // reduction_ratio, in_channels),
nn.Sigmoid()
)
self.spatial_attention = nn.Sequential(
nn.Conv2d(in_channels, 1, kernel_size=7, padding=3),
nn.Sigmoid()
)
def forward(self, features):
gap = F.adaptive_avg_pool2d(features, 1)
channel_weights = self.channel_attention(gap.squeeze())
spatial_weights = self.spatial_attention(features)
refined = features * channel_weights.view(-1, features.size(1), 1, 1)
refined = refined * spatial_weights
return refined
class DeformableConvBlock(nn.Module):
def __init__(self, in_channels, out_channels, kernel_size=3, modulation=True):
super().__init__()
self.kernel_size = kernel_size
self.modulation = modulation
self.offset_conv = nn.Conv2d(
in_channels,
2 * kernel_size * kernel_size,
kernel_size=kernel_size,
padding=kernel_size//2
)
if modulation:
self.modulator_conv = nn.Conv2d(
in_channels,
kernel_size * kernel_size,
kernel_size=kernel_size,
padding=kernel_size//2
)
else:
self.modulator_conv = None
self.regular_conv = nn.Conv2d(
in_channels,
out_channels,
kernel_size=kernel_size,
padding=kernel_size//2
)
def forward(self, x):
offset = self.offset_conv(x)
if self.modulation and self.modulator_conv is not None:
modulator = torch.sigmoid(self.modulator_conv(x))
else:
modulator = None
x = deform_conv2d(
input=x,
offset=offset,
weight=self.regular_conv.weight,
bias=self.regular_conv.bias,
padding=self.kernel_size//2,
mask=modulator
)
return x
Для оценки стоимости мы подключили дополнительный модуль-регрессор, который на основе характеристик повреждения и данных о марке/модели автомобиля рассчитывалп римерную стоимость ремонта. Здесь мы использовали градиентный бустинг над признаками, которые выделяла CNN.
Сердце системы модель детекции повреждений, построенная на MMDetection:
Скрытый текст
model = dict(
type='CascadeRCNN',
backbone=dict(
type='ResNeSt',
depth=101,
groups=64,
radix=2,
reduction_factor=4,
avg_down_stride=True,
stem_channels=128,
deep_stem=True,
output_stride=32,
init_cfg=dict(type='Pretrained',
checkpoint='open-mmlab://resnest101')
),
neck=dict(
type='PAFPN',
in_channels=[256, 512, 1024, 2048],
out_channels=256,
num_outs=5,
start_level=0,
add_extra_convs='on_output',
relu_before_extra_convs=True
),
rpn_head=dict(
type='RPNHead',
in_channels=256,
feat_channels=256,
anchor_generator=dict(
type='AnchorGenerator',
scales=[2, 4, 8, 16, 32],
ratios=[0.25, 0.5, 1.0, 2.0, 4.0],
strides=[4, 8, 16, 32, 64]),
bbox_coder=dict(
type='DeltaXYWHBBoxCoder',
target_means=[0.0, 0.0, 0.0, 0.0],
target_stds=[1.0, 1.0, 1.0, 1.0]),
loss_cls=dict(
type='FocalLoss',
use_sigmoid=True,
gamma=2.0,
alpha=0.75,
loss_weight=1.0),
loss_bbox=dict(type='GIoULoss',
loss_weight=2.0)),
roi_head=dict(
type='CascadeRoIHead',
num_stages=3,
stage_loss_weights=[1, 0.5, 0.25],
bbox_roi_extractor=dict(
type='SingleRoIExtractor',
roi_layer=dict(type='RoIAlign',
output_size=7, sampling_ratio=2),
out_channels=256,
featmap_strides=[4, 8, 16, 32]),
bbox_head=[
dict(
type='Shared2FCBBoxHead',
in_channels=256,
fc_out_channels=1024,
roi_feat_size=7,
num_classes=6,
bbox_coder=dict(
type='DeltaXYWHBBoxCoder',
target_means=[0.0, 0.0,
0.0, 0.0],
target_stds=[0.1, 0.1, 0.2,
0.2]),
reg_class_agnostic=True,
loss_cls=dict(
type='FocalLoss',
use_sigmoid=True,
gamma=2.0,
alpha=0.75,
loss_weight=1.0),
loss_bbox=dict(type='GIoULoss',
loss_weight=1.0)),
]
)
)
Мы получили основную систему, которая анализирует степень поломки автомобиля, марка и модель которого была выбрана пользователем перед регистрацией.

Архитектура AI ассистента: как мы соединили компьютерное зрение и NLP
Наш чат бот это сложная система, сочетающая несколько ML моделей. За основу мы взяли трансформерную архитектуру.

Ключевых компонента было три:
1. Intent recognition классификация намерения пользователя (диагностика, совет, поиск сервиса)
2. Entity extraction извлечение сущностей (марка авто, год выпуска, тип проблемы)
3. Context management управление контекстом диалога
Для интеграции с модулем компьютерного зрения мы создали единый embedding пространство, где текстовые описания проблем и визуальные признаки damage областей были представлены в совместимом формате. Это позволяло боту работать с мультимодальными запросами: пользователь мог отправить фото и спросить «как срочно нужно это чинить?», и бот давал обоснованный ответ.
Ядро нашегочат бота, отвечающее за RAG (Retrieval Augmented Generation) и поиск по базе знаний, выглядело так:
Скрытый текст
classMultiHeadRetriever(nn.Module):
def __init__(self, embedding_dim,num_heads=8, dropout=0.1):
super().__init__()
self.attention =
nn.MultiheadAttention(embedding_dim, num_heads, dropout=dropout)
self.layer_norm =
nn.LayerNorm(embedding_dim)
self.ffn = nn.Sequential(
nn.Linear(embedding_dim, 4 *
embedding_dim),
nn.GELU(),
nn.Linear(4 * embedding_dim,
embedding_dim),
nn.Dropout(dropout)
)
def forward(self, query, context,attention_mask=None):
attn_output, _ = self.attention(query,
context, context, key_padding_mask=attention_mask)
query = self.layer_norm(query +attn_output)
ffn_output = self.ffn(query)
return self.layer_norm(query +
ffn_output)
classHybridRetrievalSystem:
def __init__(self, embedding_model,
dense_index, sparse_index):
self.embedding_model = embedding_model
self.dense_index = dense_index
self.sparse_index = sparse_index
self.reranker =CrossEncoder('cross-encoder/ms-marco-MiniLM-L-6-v2')
def retrieve(self, query, k=10, alpha=0.7):
query_embedding =self.embedding_model.encode(query)
dense_results =
self.dense_index.search(query_embedding, k=3*k)
sparse_results =self.sparse_index.search(query, k=3*k)
hybrid_scores = {}
for doc_id, score in
dense_results.items():
hybrid_scores[doc_id] = alpha *
score + (1 - alpha) * sparse_results.get(doc_id, 0)
top_docs =
sorted(hybrid_scores.items(), key=lambda x: x[1], reverse=True)[:2*k]
reranked =
self.reranker.predict([(query, doc.text) for doc_id, doc in top_docs])
return sorted(zip(top_docs, reranked),
key=lambda x: x[1], reverse=True)[:k]
classContextAwareGenerator:
def __init__(self, model_name,
max_length=512):
self.tokenizer =AutoTokenizer.from_pretrained(model_name)
self.model =
AutoModelForCausalLM.from_pretrained(model_name)
self.max_length = max_length
def generate(self, prompt, context,history, temperature=0.7, top_p=0.9):
formatted_prompt =
self._format_prompt(prompt, context, history)
inputs =self.tokenizer.encode(formatted_prompt, return_tensors='pt', truncation=True,
max_length=self.max_length)
with torch.no_grad():
outputs = self.model.generate(
inputs,
max_length=self.max_length,
temperature=temperature,
top_p=top_p,
do_sample=True,
pad_token_id=self.tokenizer.eos_token_id,
repetition_penalty=1.1,
num_return_sequences=1
)
return
self.tokenizer.decode(outputs[0], skip_special_tokens=True)
Система постоянно обучалась на новых диалогах, улучшая понимание естественного языка и точность ответов. Мы добавили механизм проверки фактов прежде чем дать совет, бот сверялся с технической документацией и накопленной базой знаний.

Результаты и выводы: почему это сработало

За год мы добились впечатляющих результатов:
• Конверсия в запись в сервис выросла на 94%
• Средний чек увеличился на 36%
• 42% пользователей оформили подписку в течение 14 дней после первого использования
• LTV вырос в 2 раза
Но главное мы создали продукт, который реально менял опыт взаимодействия с автосервисами. Пользователи стали больше доверять нам, чаще возвращались, рекомендовали нас друзьям.
Что это за история? Она о том, что иногда нужно учиться думать out of box. Нужно слушать пользователей, смотреть на их реальные боли и пробовать решать их с помощью технологий пусть даже сначала кажется, что это «too much». Ключ к успеху не в гениальном предвидении, а в готовности быстро проверять гипотезы, запускать итерации и смотреть на реальные данные.
Даже в самых консервативных и «нецифровых» нишах вроде авторемонта всегда есть место для взрывного роста, если дать людям то, что им действительно нужно уверенность и контроль. И иногда для этого достаточно просто позволить себе мыслить нестандартно и не бояться экспериментировать с технологиями, которые кажутся слишком сложными или преждевременными.
Статья написана в сотрудничестве с Сироткиной Анастасией Сергеевной.
? Ставьте лайк и пишите, какие темы разобрать дальше! Главное — пробуйте и экспериментируйте!
✔️ Присоединяйтесь к нашему Telegram-сообществу @datafeeling, где мы делимся новыми инструментами, кейсами, инсайтами и рассказываем, как всё это применимо к реальным задачам
Комментарии (10)
CitizenOfDreams
11.09.2025 04:59У меня вода в карбюраторе, сколько будет стоить ремонт?
Скрытый текст
Жена подходит к мужу:
- Дорогой, у меня машина сломалась, в карбюратор вода попала.
- Да что ты понимаешь! Ты вообще знаешь, что такое карбюратор??? Где твоя машина?
- В озере...
CitizenOfDreams
11.09.2025 04:59Средний чек увеличился на 36%
То есть раньше что-то оставляли непочиненным? Или сейчас чинят то, что в ремонте не нуждалось?
Yuriy_krd
11.09.2025 04:59А как еще продать бесполезное приложение ?) Только пообещав, что повышенный чек будет покрывать стоимость подписки :)
Yuriy_krd
11.09.2025 04:59В данном случае - полная хрень. Вот у вас на фотке сильно вмятая дверь. Оценочная стоимость ремонта - 12 тыс. А что там со стеклоподъемником, например, ваше приложение оценило? Там может либо отломить крепление нижнего ушка или верхней части(тогда сварка на 10 минут), либо выгнуть сам стеклоподъемник (тогда замена - и это половина стоимости от вашей оценки). Стоимость ремонта будет в этих случаях сильно разной. А дверная карта? Она просто соскочила со своих пистонов (тогда ее нужно будет защелкнуть) или она лопнула? В общем, оценка в стиле "пальцем в небо".
izh-vii
11.09.2025 04:59На фото больше похоже на 60.000р )
Но задача не в том, чтобы дать точную цифру, а в том, чтобы клиент приехал.positroid
11.09.2025 04:59Возможно, заниженная цена - это именно та причина, из-за которой "клиент приехал") А совсем не "Неийросеть", как в КДПВ.
Впрочем, этим приемом грешат и вполне живые субъекты, пожалуй, во всех сферах.
Plesser
11.09.2025 04:59Тоже зацепился глазом за фотку, ладно дверь, там еще с крылом и порогом работать. И судя по всему там прилетело по задней подвески, вопрос что там с балкой открытый.
metalidea
Кажется это тот случай, когда и ссылку на сайт можно прикрепить) А если просто описать поломку без фото, тоже рассчитает? Следующий шаг - приложение для автомастерских, которые незнают как починить поломку)