Один мозг на миллион пользователей: чему backend-разработчик может научиться у мозга мухи. backend.. backend. mushroom body.. backend. mushroom body. python.. backend. mushroom body. python. дрозофила.. backend. mushroom body. python. дрозофила. коннектом.. backend. mushroom body. python. дрозофила. коннектом. Машинное обучение.. backend. mushroom body. python. дрозофила. коннектом. Машинное обучение. онлайн-обучение.. backend. mushroom body. python. дрозофила. коннектом. Машинное обучение. онлайн-обучение. персонализация.. backend. mushroom body. python. дрозофила. коннектом. Машинное обучение. онлайн-обучение. персонализация. прогнозирование.. backend. mushroom body. python. дрозофила. коннектом. Машинное обучение. онлайн-обучение. персонализация. прогнозирование. разреженное представление.
Один мозг на миллион пользователей: чему backend-разработчик может научиться у мозга мухи - 1

Персонализация почти всегда приводит к одному и тому же архитектурному вопросу: где заканчивается общая модель и начинается модель конкретного пользователя? На небольшом масштабе стоимость такого решения может оставаться незаметной. Но если их становится сто тысяч или миллион, идея хранить и обслуживать отдельную полноценную модель для каждого быстро перестаёт выглядеть привлекательной.

Я столкнулся с этой задачей при работе над системой, которая должна учитывать питание, сон, физическую активность и историю поведения человека. Причём меня интересовал не просто очередной классификатор, обученный на общей выборке. Хотелось, чтобы система могла начинать с некоторого общего знания, а затем постепенно приобретать опыт взаимодействия с конкретным человеком. И желательно без постоянного переобучения большой модели.

Неожиданно полезной моделью для размышления оказался мозг плодовой мушки. На фоне новостей об исследованиях её мозга интернет заполнили мемы: муху уже «научили» варить пиво, играть в Доту и программировать на Python. За этим шумом мне стало интересно, могут ли исследования с участием Google дать что-нибудь полезное моему проекту, и я задал этот вопрос ChatGPT. Он предложил неожиданное направление: попробовать использовать принципы обучения мозга мухи для предсказания срывов в питании. До работающего решения здесь было далеко, но идея меня зацепила — и я начал разбираться, какие механизмы стоят за этой аналогией и что из них можно перенести в программную архитектуру.

Архитектура одна, опыт разный

В проекте MaleCNS исследователи HHMI Janelia, Кембриджского университета, MRC LMB и Google Research восстановили коннектом центральной нервной системы самца дрозофилы — карту нейронов и связей между ними. Такая карта позволяет исследовать устройство нейронных цепей, хотя сама по себе ещё не описывает всю динамику их работы.

Для моей задачи особенно интересным оказалось исследование обучения и памяти грибовидного тела — mushroom body (далее буду писать MB), одна из структур мозга дрозофилы, участвующих в обучении и памяти. В нём запахи представлены активностью небольшого подмножества клеток Кеньона. Если определённый запах сопровождается получением пищи, дофаминовые нейроны передают сигнал подкрепления, который изменяет эффективность связей между клетками Кеньона и выходными нейронами грибовидного тела.

Здесь появляется полезная для разработчика схема:

представление ситуации → обратная связь → локальное изменение связей.

Чтобы запомнить новую ассоциацию, обучение может изменять конкретные связи внутри уже существующей цепи. Это натолкнуло меня на вопрос: можно ли аналогично разделить программную модель на общий механизм кодирования входных данных и небольшой персональный слой, который обучается на событиях конкретного пользователя?

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

Что именно мы хотим персонализировать

Цель проекта — оценивать риск пищевого срыва с учётом текущего контекста и предыдущего опыта человека. Но текущий эксперимент проверяет эту идею на синтетических историях пользователей. «Срывом» здесь называется положительный исход, заданный симуляцией для ежедневного решения. Связь такой метки с реальными отклонениями от питания ещё предстоит установить: опубликованные результаты описывают поведение модели внутри экспериментальной постановки.

На вход поступают 94 наблюдаемых признака, описывающих питание, сон, активность и доступную историю. Общая модель выдаёт базовую вероятность события, а персональная память учится корректировать её по обратной связи конкретного пользователя. Нас интересует, может ли небольшой индивидуальный слой замечать контексты, в которых общий прогноз систематически завышает или занижает риск.

У нового пользователя поправки равны нулю, поэтому система начинает с общего прогноза. Когда приходит наблюдаемый исход, память обновляется по контексту, сохранённому в момент решения. Если ответ поступил через несколько часов, для обучения используется исходная ситуация. Отсутствие ответа не считается подтверждением, что события не было.

Что в этой конструкции должен дать граф мушки

Для эксперимента я использовал выбранный подграф ALPN → KC из MaleCNS v1.0 — опубликованного коннектома центральной нервной системы самца дрозофилы. Он задаёт структуру связей между обонятельными проекционными нейронами и клетками Кеньона. Признаки питания и сна отображаются на его входы искусственно: биологического соответствия между ними и запахами я не предполагаю.

Здесь важно разделить две гипотезы. Первая — персональная память может улучшать общий прогноз без переобучения всей модели. Вторая — структура биологического графа может оказаться полезной для построения разреженного представления контекста. Первую можно исследовать и без коннектома, поэтому интерес ко второй требует самостоятельного сравнения с более простыми проекциями.

Для этого в эксперимент включён вариант с перемешанными связями. Он помогает исследовать роль топологии, хотя одного такого контроля недостаточно для вывода о преимуществе анатомии. Следующий вопрос — сохраняется ли возможный выигрыш при сопоставимой размерности, разреженности, ёмкости модели и нескольких заранее выбранных seed.

Как я превратил аналогию в эксперимент

Код Craving Engine, зафиксированные синтетические входы и сохранённые результаты опубликованы на GitHub. В README описаны текущая стадия и направления дальнейшей работы. Миллион пользователей в названии — архитектурная мотивация: нагрузочного эксперимента такого масштаба я пока не проводил.

Для эксперимента использовались три непересекающиеся группы синтетических пользователей: 60 для обучения, 20 для выбора порогов и 40 для оценки на development-выборке. На каждого приходилось 180 ежедневных решений. Эта development-выборка уже использовалась в предыдущих исследованиях, поэтому её результаты нельзя считать независимой слепой проверкой.

Симуляция задавала задержку обратной связи в 20 часов. О событии сообщалось с вероятностью 80%, об отсутствии события — с вероятностью 70%. Таким образом, пропуски зависели от исхода. Это допущения симуляции; соответствующие частоты у реальных пользователей пока не установлены.

Обучение MB и обновление персональной памяти использовали доступную обратную связь. Для расчёта метрик и выбора порогов были доступны полные синтетические исходы, включая события без сообщения. Это упрощает оценку по сравнению с реальным продуктом, где такие исходы обычно неизвестны.

Что общее, а что принадлежит пользователю

Общая часть преобразует 94 входных признака в разреженный вектор. После нормировки положительные и отрицательные отклонения кодируются отдельными ON/OFF-каналами. Затем они искусственно отображаются на входы выбранного графа ALPN → KC.

В выбранном подграфе 2019 клеток Кеньона. Для одного контекста сохраняются до 101 положительной активации — примерно 5% координат. Их масштаб определяется по обучающей выборке; затем добавляется постоянная опорная координата и вектор приводится к единичной длине. В результате представление для памяти имеет 2020 координат.

Поверх этого представления обучается логистическая регрессия — выходной классификатор, который превращает вектор в прогноз вероятности. Вот соответствующий фрагмент MBClassifier.fit:

transform = ContextTransform.fit(values, self.schema)
q = transform.transform(values)
representation = FixedContextRepresentation.fit(
    q, self.graph, MBCalibrationConfig().mapping_seed,
)
readout = LogisticRegression(**asdict(MBReadoutConfig()))
with warnings.catch_warnings():
    warnings.simplefilter("error", ConvergenceWarning)
    readout.fit(representation.encode(q), labels)

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

Для персональной памяти отдельно строится представление по всей обучающей матрице признаков. После этого оно фиксируется, чтобы индивидуальный опыт накапливался в неизменной системе координат.

Личное состояние нового пользователя содержит три нулевых массива:

self.fast = np.zeros(dimension)
self.slow = np.zeros(dimension)
self.exposure = np.zeros(dimension)

fast и slow накапливают поправки с разными скоростями обучения и затухания. exposure учитывает, насколько часто встречались активные координаты. Общее знание уже содержится в базовом прогнозе, поэтому копировать обученный классификатор каждому пользователю не требуется.

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

Обучаться нужно на контексте решения

В исследовательском API прогноз и обратная связь разделены на predict и observe. Следующий пример предполагает, что общая модель, преобразование признаков и память пользователя уже загружены:

p0 = float(population_model.predict_proba(X_now)[0, 1])
h = online_representation.encode(online_transform.transform(X_now))[0]

snapshot = memory.predict(event_id, decision_at, h, p0)
prediction = snapshot.p_final

# Позже поступает наблюдаемый исход именно этого события.
memory.observe(event_id, feedback_at, outcome, confidence=1.0)
checkpoint = memory.export_state()

predict сохраняет активные координаты и прогноз в момент решения. Когда приходит исход, observe использует этот снимок, а не новые признаки после события. Отсутствие ответа не становится отрицательной меткой; повторные идентификаторы решений и повторная обратная связь отклоняются.

При воспроизведении истории обновления выполняются только по обратной связи, поступившей раньше следующего решения. Добавка к весам использует активные координаты, но затухание и L2-регуляризация воздействуют и на остальные элементы. Поэтому всё обновление нельзя назвать изменением исключительно 5% массива.

Полная динамика мозга и дофаминовая физиология в этой реализации не симулируются. Биологическая структура здесь используется как фиксированный компонент инженерного представления.

Что показало сравнение

Я сравнивал шесть вариантов: градиентный бустинг по деревьям (GBDT) с персональной скалярной поправкой и без неё, анатомическую MB с памятью и без неё, а также MB с перемешанными связями в тех же режимах.

Для каждой модели порог выбирался на калибровочной выборке при средней по пользователям FPR не выше 20%, после чего фиксировался для development. Это ограничение на калибровке не гарантирует такую же частоту ложных тревог на новых данных.

В таблице приведены средние метрики по пользователям. Recall показывает долю обнаруженных событий, FPR — долю ложных срабатываний среди решений без события. AUC оценивает качество ранжирования, Brier — ошибку вероятностного прогноза; для Brier меньше означает лучше.

Модель

Recall

FPR

AUC

Brier

GBDT

39,65%

15,40%

0,7391

0,1732

GBDT + скалярная поправка

39,51%

15,12%

0,7362

0,1709

Анатомическая MB

43,24%

19,70%

0,7307

0,1820

Анатомическая MB + память

42,77%

19,59%

0,7280

0,1780

MB с перемешанными связями

38,01%

14,13%

0,7330

0,1776

MB с перемешанными связями + память

37,94%

14,29%

0,7306

0,1751

Внутри анатомической MB персональная память уменьшила Brier примерно на 0,0041. Парный 95% доверительный интервал, рассчитанный бутстрэпом по пользователям, составил от −0,0083 до −0,0009. Это обнадёживающее свидетельство улучшения ошибки вероятностей на данной синтетической выборке. Однако средние AUC и recall немного снизились, а простой GBDT со скалярной поправкой показал меньший Brier.

Сравнение MB с GBDT тоже неоднозначно: более высокий recall сопровождался более высокой FPR. Поэтому эти рабочие точки не устанавливают превосходство MB. При ограничении в два предупреждения за скользящие семь дней персональная MB обнаружила 992 события и выдала 175 ложных предупреждений; GBDT — 968 и 148 соответственно. Дополнительные 24 обнаруженных события сопровождались 27 дополнительными ложными предупреждениями. Предотвращение событий этим экспериментом не оценивалось.

Заранее назначенный кандидат — анатомическая MB с памятью — не прошёл условия допуска. Прирост recall относительно GBDT оказался меньше требуемых пяти процентных пунктов, увеличение FPR превысило допустимый один пункт, а Brier ухудшился. Поэтому новый слепой тест для этого кандидата не проводился.

Для меня результат полезен тем, что разделяет вопросы дальнейшей работы. Персональная память смогла улучшить одну характеристику собственного MB-прогноза, но преимущества всей конструкции над простыми моделями пока нет. Полные результаты и критерии доступны в отчёте V9.

Сколько памяти получилось на самом деле

Первоначально я ориентировался на 16 КиБ персонального состояния. Эта оценка относилась к иллюстративной схеме с одним массивом из 4096 значений float32. Когда я сопоставил её с реализацией V9, оказалось, что память включает три массива по 2020 элементов типа float64. Их числовые буферы занимают 48 480 байт — примерно 47,34 КиБ на пользователя.

К этой величине добавляются ожидающие снимки решений, история гейта, идентификаторы событий и метаданные. Поэтому 47,34 КиБ — размер числовой части, а полный бюджет персонального состояния ещё нужно измерить и ограничить. Для миллиона пользователей одни эти буферы потребовали бы 48,48 ГБ хранения; это оценка объёма данных, а не результат нагрузочного эксперимента.

Цель в 16 КиБ остаётся отдельной инженерной задачей. Переход на float32 уменьшил бы три массива до 23,67 КиБ, поэтому одной смены точности недостаточно. Придётся исследовать размерность представления и объём вспомогательной истории, каждый раз проверяя, что экономия памяти не уничтожает пользу персонализации.

Что буду проверять дальше

Первый шаг — воспроизвести сохранённый результат из самостоятельного репозитория. Затем я хочу отдельно проверить вклад персональной памяти: сравнить её с простой скалярной поправкой при одинаковой общей модели и заранее выбранном ограничении на ложные предупреждения. Так будет проще понять, оправдывает ли многомерное состояние свою сложность.

Следующий вопрос — вклад графа мушки. Для него нужны сопоставимые разреженные проекции, несколько фиксированных seed и одинаковые условия оценки. Если появится устойчивый выигрыш, подтверждать его придётся на новых пользователях; перенос на реальные данные останется отдельной задачей.

Главный вывод для меня пока архитектурный: общий прогноз и индивидуальный опыт удалось разделить в работающем исследовательском алгоритме. Его практическое преимущество ещё предстоит доказать. Теперь у меня есть реализация, сохранённые результаты и конкретные сравнения, которые помогут решить, что развивать дальше.

P. S. Я backend-разработчик, а в машинном обучении пока начинающий: учусь по ходу этого исследования, и мне очень интересно разобраться в задаче.

Буду благодарен за поправки к терминологии, интерпретации результатов и постановке эксперимента — особенно с объяснением, что стоит проверить и почему.

Код и описание экспериментов доступны в репозитории Craving Engine. Если у вас есть идеи, какой baseline добавить, как изменить персональную память или иначе оценить результаты, пишите в комментариях или issues. Мне особенно помогут предложения, которые можно превратить в конкретный эксперимент.

Если получится продвинуться, подготовлю продолжение: расскажу, что попробовал, какие результаты получил и что пришлось пересмотреть. Надеюсь, обсуждение поможет найти следующий шаг.

Автор: nutrifit

Источник