<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="/feed.xml" rel="self" type="application/atom+xml" /><link href="/" rel="alternate" type="text/html" /><updated>2026-05-20T05:34:09+00:00</updated><id>/feed.xml</id><title type="html">Пситехлаб</title><subtitle>Команда любителей применять машинное обучение к менталке.</subtitle><entry><title type="html">Как мы собирали датасет для разработки ML-инструмента, помогающего спасать жизни</title><link href="/2026/01/15/presui_antisui_dataset_creation_methodology.html" rel="alternate" type="text/html" title="Как мы собирали датасет для разработки ML-инструмента, помогающего спасать жизни" /><published>2026-01-15T00:00:00+00:00</published><updated>2026-01-15T00:00:00+00:00</updated><id>/2026/01/15/presui_antisui_dataset_creation_methodology</id><content type="html" xml:base="/2026/01/15/presui_antisui_dataset_creation_methodology.html"><![CDATA[<p><img src="/assets/images/presui_antisui_dataset_methodology/poster.png" alt="" /></p>

<p><em>Изначально пост был размещен на <a href="https://habr.com/ru/companies/mts_ai/articles/985412/">Хабре</a> в блоге MWS AI. На правах авторов, дублируем его в нашем блоге.</em></p>

<p>Привет, Хабр! В этом посте речь пойдет о специфическом датасете, предназначенном для решения очень важной задачи — разработки ML-инструмента, помогающего своевременно выявлять предпосылки и предотвращать суициды. Мы с командой «Пситехлаб», специализирующейся на ИИ-решениях для психотерапии, собирали его по вечерам. Этот проект диссертационный, он не входит в мои обязанности в рамках работы в MWS AI, но опыт, приобретенный в компании, стал базой, без которой его бы не было.</p>

<p>Мы написали <a href="/assets/pdfs/dataset_methodology_presui_antisui.pdf">научную статью</a> по созданию этого датасета. Если будете использовать наш датасет, пожалуйста, процитируйте.</p>

<h1 id="давайте-начнем-с-контекста-почему-этот-проект-так-важен">Давайте начнем с контекста. Почему этот проект так важен</h1>
<p>По данным ВОЗ, мир ежегодно теряет более 700 тысяч человек вследствие суицида. Представьте, целый город, причем не маленький, исчезает каждый год. По России свежую статистику мне пока не удалось найти, но вот в 2019-м было 17 тысяч случаев самоубийств, в 2022-м — 13,5 тысяч (это данные Росстата). Хорошая новость в том, что количество таких трагедий у нас сокращается год от года: с момента пика, который пришелся на 1994 год и составил рекордные 61,8 тысяч случаев, — падение почти в шесть раз! Но мы очень хотим, чтобы это число падало еще быстрее. И вообще, чтобы оно было нулевым.</p>

<p>На это и направлена наша инициатива. Мы хотим помочь вовремя оказывать поддержку людям, находящимся на грани. Существуют специализированные НКО, которые ищут таких людей в социальных сетях и помогают им в пределах своих прав и возможностей, а также сотрудничая с МВД и ФСБ. А задача команды «Пситехлаб» — облегчить этот поиск.</p>

<h1 id="как-это-происходит">Как это происходит</h1>

<p>В социальной сети есть обычные пользователи, чьи тексты чаще всего нерелевантны для нашей темы. А есть те, кто проявляет признаки склонности к самоубийству, — наша целевая аудитория. Также есть боты, фейки и люди, которые не подают явных признаков, и мы пока не умеем с ними работать.</p>

<p><img src="/assets/images/presui_antisui_dataset_methodology/1.png" alt="Общая модель анализа социальных сетей" /></p>

<p>Все посты в социальных сетях анализируются волонтерами из НКО: на основании постов они присваивают пользователям так называемые суицидальные статусы. Важно отметить, что суицидальный статус — это не диагноз, а такой маркер, указывающий, нужно ли обращать внимание на конкретный аккаунт. Если статус высокий, то это повод поискать дополнительную информацию о пользователе, выяснить, человек ли это вообще, возможно, принять какие-то меры.</p>

<p>Проблема заключается в том, что приходится анализировать огромное количество текстов. По собранной нами статистике, на 100 постов приходится только 20 таких, которые содержат целевую информацию, — это различные негативные ситуации, выражения эмоций, призывы о помощи и так далее.</p>

<h1 id="волонтерский-ml">Волонтерский ML</h1>
<p>Мы предлагаем систему, которая будет помогать фильтровать нерелевантные посты, тем самым снижая нагрузку на волонтеров и увеличивая их КПД. Для этого мы собираем датасет, чтобы построить модель машинного обучения, которая будет выполнять эту задачу. Причем цель у нас довольно амбициозная: собрать 50 тысяч размеченных текстов. Это больше, чем любой другой датасет по смежной теме даже на английском языке.</p>

<p><em>Важный дисклеймер: каждый раз, когда я рассказываю про свой проект, почему-то возникает впечатление, будто мы строим модель, предсказывающую именно суицид, что не так. Поэтому регулярно слышу вопрос «У вас психологи размечали?». Нет, не психологи. Мы лишь определяем тексты, которые описывают определенные факторы (пример я приводил чуть выше). Чтобы определить, что человек пишет, что ему плохо или что он пережил насилие или травлю, не нужно быть психологом. Достаточно просто иметь здравый смысл. Наша идея в том, чтобы волонтером мог стать каждый человек. Со всем этим дисклеймером и контекстом давайте перейдем непосредственно к сборке датасета.</em></p>

<h1 id="как-мы-собирали-датасет">Как мы собирали датасет</h1>

<p>Мы использовали открытые источники — например, существующие датасеты, а также (внезапно) олдскульные форумы нулевых годов, где люди обсуждали тему самоубийства. Там люди часто делились своими историями и иногда получали психологическую поддержку. В их постах чуть ли не в каждом предложении можно было встретить какой-то суицидальный фактор. И таких историй сотни.</p>

<p>После сбора данных мы обогащали их различными признаками: наличие и тип местоимений, указания на родственные связи, количество слов, эмоции, сентимент и так далее. С помощью этих признаков можно отбирать тексты на разметку так, чтобы нужные классы появлялись с большей вероятностью. Мы применяли как различные эвристики, так и существующие открытые модели. Мы очень благодарны разработчикам, которые выложили их в открытый доступ, — это очень ценно.</p>

<p>Из интересного: обычная модель сентимента, которая предсказывает нейтральный, негативный или позитивный сентимент, тексты с суицидальными мыслями определяла как нейтральные. Например, фраза «я хочу умереть» оценивалась как нейтральная.
Для антисуицидальной части датасета, о которой я расскажу чуть позже, нам очень помогла модель эмоций. Это логично, ведь антисуицидальные тексты, как даже из названия можно догадаться, часто связаны с такими эмоциями, как радость (Joy) или удивление (Surprise).
Итак, мы собрали и обогатили данные. Теперь перейдем к сердцу любого датасета — инструкции.</p>

<h1 id="инструкция">Инструкция</h1>

<p>Наша инструкция состоит всего из двух частей: основной части и таблицы классов. 
В основной части стандартно описывалось, что, зачем и как размечать. Также там были прописаны разные принципы разметки. Внимательно посмотрим на два самых важных принципа:</p>

<ul>
  <li>Содержание текста должно относиться к автору. Мы хотим предсказывать только то, что относится к автору текста, а не то, что относится к третьему лицу. Пример:
    <ul>
      <li><strong>Я</strong> хочу сбежать из этого давящего внешнего мира к себе внутрь.</li>
      <li>У <strong>меня</strong> нет сил это терпеть.</li>
      <li>У <u>него</u> такая жесть дома происходит.</li>
    </ul>
  </li>
  <li>Не допускать необоснованных интерпретаций. Это проще показать на примере. Прочитайте текст и скажите, какую сцену он описывает.</li>
</ul>

<p>«<em>Я ненадолго отошел, а когда вернулся, она успела прочитать всю нашу откровенную переписку</em>».</p>

<p>Многие подумают, что жена или девушка прочитала переписку партнера с любовницей с ожидаемым исходом. По нашей инструкции это можно интерпретировать как пресуицидальный сигнал: отношения, которые, не заладились и сломались. Однако это неверно, потому что вы додумали контекст. Из этого текста напрямую не следует, что отношения развалились. Строго говоря, даже не следует, что это любовные отношения. А что, если тут речь идет о маме, которая прочитала переписку сына-подростка? Скорее всего, предыдущее или следующее предложение как раз содержит полную информацию.</p>

<p>Теперь коснемся таблицы классов. Точнее, как мы ее создавали. Глобальная цель — разметить тексты на три большие группы сигналов:</p>

<ol>
  <li>Пресуицидальные сигналы: факторы, склоняющие к суициду.</li>
  <li>Антисуицидальные сигналы: факторы, условно сберегающие.</li>
  <li>Нерелевантные сигналы: большая часть текстов, которые нам неинтересны и которые мы хотим исключить из рассмотрения.</li>
</ol>

<p>Мы нашли несколько существующих систем классов, разложили их и объединили так, чтобы новые классы удовлетворяли двум условиям:</p>
<ul>
  <li>Атомарность — фактор нельзя «разбить» на составляющие.</li>
  <li>Семантическая независимость — тексты разных классов должны как можно меньше пересекаться по смыслу.</li>
</ul>

<p>Это сделано для того, чтобы любой человек, работавший в какой-то одной из систем, мог адаптировать нашу систему под себя.</p>

<p><img src="/assets/images/presui_antisui_dataset_methodology/2.png" alt="Доска настроения для разных систем суицидальных факторов" /></p>

<p>На картинке выше показан пример того, как мы анализировали существующие системы классов. Я называю эту технику «Доска настроений». Мы просто выписываем все таблички с факторами и маркируем их одинаковым цветом, если считаем, что они похожи. Затем пытаемся их объединить: какие-то выбрасываем, какие-то добавляем.</p>

<p>Вот пример сигналов, которые мы выделили: красный — пресуицидальный, зеленый — антисуицидальный. Всего у нас получилось 33 пресуицидальных класса, объединенных в семь групп, и 12 антисуицидальных классов без деления на группы.</p>

<p><img src="/assets/images/presui_antisui_dataset_methodology/3.png" alt="Примеры сигналов" /></p>

<h1 id="тестовая-разметка">Тестовая разметка</h1>

<p>После того как собрали базовую сетку классов, решили провести тестовую разметку собственными силами. Ее схема состояла из двух кругов:</p>
<ol>
  <li><strong>Первый круг</strong> выполняли я и моя коллега, участвовавшие в сборе датасета изначально.</li>
  <li><strong>Второй круг</strong> выполняли члены нашей команды, которые никогда не видели данных и ранее не имели отношения к нашей теме. Это было сделано для того, чтобы смоделировать ситуацию, когда только что пришедший волонтер вливается в процесс.</li>
</ol>

<p>Что мы выявили в результате тестовой разметки?</p>

<p>Во-первых, мы обнаружили, что наши тексты часто содержат несколько классов. Это <strong>multi-label</strong>. Из этого напрямую возникают некоторые технические вопросы:</p>

<ul>
  <li>Как объединять разметку, если один текст размечен более чем одним человеком?</li>
  <li>Как считать степень согласия?</li>
</ul>

<p>Первую проблему мы решили мягким голосованием большинством. Составляем единый список из всех классов, которые поставили люди, и из него выбираем те, что встретились больше, чем n раз. На нашей тестовой разметке такой подход дал хорошие показатели по покрытию — количеству текстов, которые имеют хотя бы один класс, и итоговому качеству меток. У простого голосования большинством слишком много примеров просто не получали никакой метки.</p>

<p>Вторую проблему решили с помощью альфы Криппендорфа и специальной метрики MASI (Measuring agreement of set-valued items), которая используется в качестве ядра. Эта метрика на самом деле представляет собой метрику Жаккара со специальным коэффициентом. Из коробки ее можно посчитать с помощью — барабанная дробь — <a href="https://www.nltk.org/api/nltk.metrics.distance.html#nltk.metrics.distance.masi_distance">NLTK</a>.</p>

<p>Еще одна особенность наших данных — это <strong>субъективность</strong>. Когда мы на этапе обратной связи разговаривали с членами команды, мы не всегда могли оспорить их выбор класса. То есть они ставили какой-то класс, мы считали, что он неправильный, спрашивали «почему?», они отвечали, и мы как будто бы соглашались, что «вроде окей». Это делает нашу задачу очень близкой к сентименту и к эмоциям. Несмотря на то, что классы прописаны довольно хорошо, жизненный опыт все равно играет большую роль. Кто-то спросит, а почему мы не указывали такие примеры как частные случаи (корнер-кейсы)? Проблема в том, что таких текстов было много и мы с коллегой просто бы не вывезли следить за длинным непротиворечивым списком таких случаев.</p>

<h1 id="где-искали-разметчиков">Где искали разметчиков</h1>

<p>Мы написали инструкцию, подготовили данные и хотим начать размечать. Для разметки обычно у нас есть два пути: краудсорсинг и индивидуальные разметчики. У каждого есть свои плюсы и минусы, которые указаны на картинке ниже. Мы изначально думали использовать краудсорсинг, так как это дешевле и быстрее, плюс у нас был опыт работы с Яндекс.Толокой — самой известной до недавнего времени платформой для краудсорсинга.</p>

<p><img src="/assets/images/presui_antisui_dataset_methodology/4.png" alt="Сравнение краудсораса и индивидуальных разметчиков" /></p>

<p>Проблема в том, что Яндекс.Толока ушла в начале 2024 года, вместо нее появились Яндекс.Задания. Казалось бы, что могло пойти не так? А пошло не так то, что эта платформа не работает с физическими лицами: вы не можете быть заказчиком так просто — вам обязательно нужно юридическое лицо. Мы потратили очень много времени на то, чтобы такое юрлицо организовать.</p>

<p>Это шло параллельно тестовой разметке, и когда мы ее закончили, поняли, что субъективность, помноженная на низкую мотивированность в краудсорсинге, ни к чему хорошему не приведет. Поэтому решили действовать с помощью индивидуальных разметчиков. Тем более я сам два года отработал в разметке в MWS AI, где налаживал автоматизацию процессов.</p>

<p>Среди всех платформ, где мы искали разметчиков, нас очень удивил Фриланс.ру, где мы собрали целых 30 откликов. Пришлось даже выбирать по сопроводительным письмам. Мы выстроили процесс найма таким образом, чтобы даже люди без опыта в разметке могли научиться и минимизировать ошибки в результате. В целом, как видно, нам это удалось: 27% ошибок в начале, конечно, много, но 4,6% в конце процесса — это уже приемлемо.</p>

<p>Интересный момент: во время проверки тестовых заданий разметчиков мы чаще всего сталкивались как раз с ошибками, связанными с нарушением тех двух принципов, о которых я говорил: отношение содержания текста к автору и недопустимость интерпретаций. Еще занятный факт: почему-то текст, где было рассуждение типа «А если я не отдохну, то превращаюсь в ходящее зомби», все восемь человек посчитали как свершившийся факт и поставили соответствующий класс.</p>

<p>Наш полный процесс разметки в целом ничем не отличается от любого другого, кроме того, что у нас есть сбор обратной связи и психологическое вентилирование. Чуть позже я расскажу об этом подробнее. А сейчас немного про инструменты.</p>

<h1 id="какие-инструменты-использовали">Какие инструменты использовали</h1>

<p>Мы размечали данные в Label Studio. Это очень хорошая платформа, ключевая особенность которой в том, что она позволяет создать интерфейс почти под любые задачи. Я пока не встречал задач, для которой бы у меня не получилось создать интерфейс. Плюс у меня большой опыт работы с ней. Вот так выглядит интерфейс разметки, который мы использовали.</p>

<p><img src="/assets/images/presui_antisui_dataset_methodology/5.png" alt="Интерфейс разметки в Label Studio" /></p>

<p>Следующий важный вопрос: где мы хранили данные? Мы использовали ClearML. Если вы никогда не сталкивались с потерей данных или путаницей в версиях, то и хорошо. Чтобы и впредь не сталкиваться, используйте ClearML или аналогичные платформы, которые позволяют версионировать датасеты. Поверьте, это очень важный аспект.</p>

<h1 id="забота-о-разметчиках">Забота о разметчиках</h1>

<p>Как вы знаете или, по крайней мере, догадываетесь, разметчики работали не с контентом про кошечек и собачек, а с эмоционально тяжелыми текстами. Мы опасались, что это может как-то повлиять на их психологическое состояние. Поэтому контролировали наших разметчиков как инструментально (при помощи тестов), так и через вентилирования. Это формат интервью, когда мы выстраиваем безопасные, доверительные отношения и проговариваем вещи, которые могли случиться с разметчиками, пока они что-то размечали.</p>

<p>В ходе этих интервью выявили несколько интересных моментов. Во-первых, у некоторых разметчиков действительно была реакция на ряд текстов в начале работы, но к середине у всех получилось дистанцироваться от того, что они читают. Более того, кому-то в конце уже даже эти интервью не понадобились. Во-вторых, было приятно узнать, что как сайд-эффект от работы несколько разметчиков получили более глубокое понимание своих детей-подростков.</p>

<h1 id="как-мы-верифицировали-и-исправляли-датасет">Как мы верифицировали и исправляли датасет</h1>

<p>Прежде чем говорить о качестве нашего датасета, вспомним, что мы хотим разметить 50 тысяч примеров. Это очень много. Чтобы замеры качества были адекватными, тестовую часть датасета мы размечали с перекрытием, то есть несколько человек размечают один и тот же пример, а итоговый результат получается с помощью агрегации отдельных разметок. Размечать так датасет полностью, к сожалению, мы позволить себе не могли, так как это заняло бы слишком много времени.</p>

<p>Чтобы проверять датасет глобально, мы сами размечали по 185 примеров с каждого блока параллельно основной разметке. Перфекционистов сейчас щелкнуло: почему не 200? Это артефакт от схемы проверки, которая в итоге не пошла, а менять уже было накладно. После выполнения блока мы сравнивали разметки между собой. Если количество ошибок превышало наперед заданный порог, то мы отсматривали расхождения на предмет спорности разметки. Если после такой проверки количество ошибок все еще превышало порог — такой блок возвращался на доработку.</p>

<p>Порог ошибок от числа проверок у нас был 15%. Это число мы сформулировали на основе похожих работ. Если посчитать общий процент ошибок между разметками, то получается, что мы прошли по тонкому льду: для теста мы получили 13,99%, а для трейна — 14,55%. 
Когда начали обучать модели, выяснилось, что антисуицидальная модель у нас была ужасной по качеству. Мы это ожидали, но не на таком уровне, который увидели. Ожидали потому, что коллеги во время тестовой разметки отмечали, что антисуицидальная часть шла труднее. После анализа ситуации мы решили пересобрать классы для антисуицидальной части и переразметить ее. Как мы это делали, а также как искали ошибки в пресуицидальной части — отдельно писали в нашем <a href="https://psytechlab.github.io/2025/02/23/%D0%94%D0%B5%D0%B2%D0%BB%D0%BE%D0%B3-1.-%D0%A1%D0%B4%D0%B5%D0%BB%D0%B0%D0%B5%D0%BC-%D1%80%D0%B0%D0%B7%D0%BC%D0%B5%D1%82%D0%BA%D1%83-%D0%BB%D1%83%D1%87%D1%88%D0%B5.html">девлоге</a>.</p>

<p>Забегая вперед, скажу, что итоговое качество антисуцидальной модели после пересборки классов получилось хуже, чем показали первые тесты из девлога, но существенно лучше, чем исходный вариант.</p>

<h1 id="что-мы-получили-в-итоге">Что мы получили в итоге</h1>

<p>Всего мы собрали 57 810 примеров. Пресуицидальная часть содержит 38 406 примеров, антисуицидальная часть — 9702 примера, нерелевантных примеров тоже получилось 9702 штуки. Согласие между разметчиками по альфе Криппендорфа для пресуицидальный части для теста — 0,542. Чуть больше четверти примеров содержат больше чем один сигнал. В таблицах ниже показано распределение примеров в соответствующих частях.</p>

<p>Распределение в антисуицидальной части</p>

<table>
  <thead>
    <tr>
      <th>Имя класса</th>
      <th>Кол-во</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Наличие положительных социальных связей</td>
      <td>1,650</td>
    </tr>
    <tr>
      <td>Выражение любви</td>
      <td>1,384</td>
    </tr>
    <tr>
      <td>Выражение счастья, радости, удовлетворения</td>
      <td>858</td>
    </tr>
    <tr>
      <td>Положительная самооценка</td>
      <td>595</td>
    </tr>
    <tr>
      <td>Выражение любви / наличие положительных социальных   связей</td>
      <td>486</td>
    </tr>
  </tbody>
</table>

<p>Распределение в пресуицидальной части</p>

<table>
  <thead>
    <tr>
      <th>Имя класса</th>
      <th>Кол-во</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Смерть   / мысли о смерти</td>
      <td>4,205</td>
    </tr>
    <tr>
      <td>Проблемы   во внешнем мире / несчастная любовь, проблемы с друзьями, трудности в   построении отношений</td>
      <td>3,236</td>
    </tr>
    <tr>
      <td>Чувства:   беспомощность, безнадежность, отчаяние</td>
      <td>2,602</td>
    </tr>
    <tr>
      <td>Чувства:   психическая опустошенность, депрессия, тоска, грусть</td>
      <td>2,359</td>
    </tr>
  </tbody>
</table>

<h1 id="обучение-модели-и-результаты">Обучение модели и результаты</h1>

<p>Самое время поговорить о результатах обучения модели — то, к чему мы шли все это время. В качестве базовой мы использовали BERT, поскольку за шесть лет своего существования он стал таким своеобразным Бейзлайном Бейзлайновичем для подобных задач и с ним очень удобно работать. Мы пробовали другие модели типа DeBERTa и RoBERTa, но старый добрый ruBERT показывал лучшие результаты.</p>

<p>В базовом варианте, который отправился в научную статью, мы сделали минимальный препроцессинг: привели тексты к нижнему регистру и убрали любые некириллические символы. Мы также отфильтровали классы, которые имели меньше ста примеров, и убрали тексты с несколькими классами. Структура классов позволяет нам строить модели на разных уровнях гранулярности (детализации):</p>

<ul>
  <li>точная — используем все классы, которые есть;</li>
  <li>групповая — используем только группы;</li>
  <li>бинарная — есть ли сигнал или нет;</li>
  <li>тернарная — есть ли антисуицидальный или пресуицидальный сигнал.</li>
</ul>

<p>Вот такие результаты мы получили.</p>

<table>
  <thead>
    <tr>
      <th>Тип модели</th>
      <th>Гранулярность</th>
      <th>Кол-во   классов</th>
      <th>Точность</th>
      <th>Полнота</th>
      <th>F1-мера   (макро)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Presuicidal</td>
      <td>Group</td>
      <td>8</td>
      <td>0.65</td>
      <td>0.65</td>
      <td>0.65</td>
    </tr>
    <tr>
      <td>Presuicidal</td>
      <td>Exact</td>
      <td>26</td>
      <td>0.61</td>
      <td>0.51</td>
      <td>0.53</td>
    </tr>
    <tr>
      <td>Antisuicidal</td>
      <td>Exact</td>
      <td>9</td>
      <td>0.70</td>
      <td>0.59</td>
      <td>0.63</td>
    </tr>
    <tr>
      <td>All</td>
      <td>Binary</td>
      <td>2</td>
      <td>0.71</td>
      <td>0.71</td>
      <td>0.71</td>
    </tr>
    <tr>
      <td>All</td>
      <td>Ternary</td>
      <td>3</td>
      <td>0.71</td>
      <td>0.69</td>
      <td>0.70</td>
    </tr>
  </tbody>
</table>

<p>Кстати, наша формальная цель — 70 пунктов по F1-макро для обеих моделей, в которых от трех до семи классов. Чтобы ее достичь, мы немного поколдовали над структурой классов, а кроме того, выбросили некоторые примеры из трейна, которые были шумными. Под колдовством имеется в виду объединение каких-то классов в один как на уровне группы, так и между собой. Например, мы решили оставить классы чувств как группу, потому что, пожалуй, это самый сложный для модели класс. Классы «мысли о смерти» и «намерения о смерти» было решено объединить в один, потому что второй оказался малочисленным и модель не могла уловить его суть. При этом класс этот очень важен, мы не могли его выбросить.</p>

<p>В итоге смогли подобрать конфигурацию, когда мы имеем наибольшее число классов при заданном пороге по F1 в 70 пунктов. Для пресуицидальной модели получилось 15 классов, для антисуицидальной — 10. Надо сказать, что каждая модель также включает в себя класс антагониста, то есть пресуицидальная модель может определить антисуицидальный сигнал, а антисуицидальная — наоборот.</p>

<h1 id="о-нашей-платформе">О нашей платформе</h1>

<p>Мало разработать модели. Надо еще сделать так, чтобы люди могли их использовать. Для этого мы разработали платформу «Китобой» с прицелом на анализ текстов пользователей. Она выступает как посредник между волонтерами и моделями. В нее можно загрузить посты, а платформа на каждый пост соберет все предсказания. По умолчанию к ней подключены пресуицидальная и антисуицидальная модели, но могут быть подключены любые другие. Вот как выглядит интерфейс просмотра постов с предсказаниями:</p>

<p><img src="/assets/images/presui_antisui_dataset_methodology/6.png" alt="" /></p>

<p>Кроме формата «ленты» постов, вы можете посмотреть на временной график предсказаний, чтобы оценить тренды в настроении и состоянии пользователей, — особая фича нашей платформы. Пример интерфейса:</p>

<p><img src="/assets/images/presui_antisui_dataset_methodology/7.png" alt="" /></p>

<p>Платформа открытая и вы можете попробовать ее здесь: <a href="https://github.com/psytechlab/kitoboy">https://github.com/psytechlab/kitoboy</a>. Там же найдете ссылки на смежные репозитории, модели и датасеты — будем благодарны, если оцените :)</p>

<h1 id="планы-на-будущее">Планы на будущее</h1>

<p>После анализа ошибок моделей у нас еще осталось несколько неприятных вещей, которые надо исправить в датасете. Да и вообще хочется значение альфы Криппендорфа &gt;0,7. Для этого надо как-то улучшить методологию разметки, не сводя ее к длиннющему списку корнер-кейсов.</p>

<p>Также, чтобы идти в ногу со временем, мы хотим подключить в процесс LLM. У нас есть две идеи:</p>
<ul>
  <li>Использовать LLM для суммаризации текстов, которые были определены как какие-либо сигналы.</li>
  <li>Научить LLM определять суицидальные статусы с объяснением. Учитывая, что сейчас модели рассуждают из коробки, это сделать несложно.</li>
  <li>Включить LLM в процесс разметки.</li>
</ul>

<p>С точки зрения платформы у нас тоже есть идеи, куда расти и что делать:</p>

<ul>
  <li>Сделать сервис парсинга социальных сетей. Сейчас данные нужно загружать из csv.</li>
  <li>Реализовать записки наблюдателя, куда волонтер может записывать какие-то выводы. Также это интерфейс для функций LLM выше.</li>
  <li>Добавить ролевую систему пользователей платформы с разграничением доступов. В теории должно быть минимум две роли: волонтер, который выполняет анализ пользователя, и супервизор, который проверяет и оценивает самих волонтеров.</li>
</ul>

<p>Хотим сказать спасибо всем, кто принял участие в разметке данных: Жанна Насхулиян, Анастасия Тюкаева, Артем Загидулин, Ирина Хмелева, Леонид Фомин, Алина Рябушева, Наталья Матвеева, Татьяна Солошенко, Денис Мартынов, Наталья Солошенко.</p>

<p>Давайте сделаем этот мир чуточку лучше.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">Итоги 2025 года. Год нашей команде</title><link href="/2025/12/23/2025_conclusion.html" rel="alternate" type="text/html" title="Итоги 2025 года. Год нашей команде" /><published>2025-12-23T00:00:00+00:00</published><updated>2025-12-23T00:00:00+00:00</updated><id>/2025/12/23/2025_conclusion</id><content type="html" xml:base="/2025/12/23/2025_conclusion.html"><![CDATA[<p>Прошло 357 дней с поста, в котором мы поздравляли всех с наступающим текущим годом. Тогда мы создали Пситехлаб. Посмотрим, что мы сделали за это время.</p>

<p>Прежде всего, мы сделали «Китобой», нашу антисуицид-платформу, которая шла к своему воплощению четыре года. Мы не постесняемся сказать, что получилась целая экосистема, потому что «Китобой» не только платформа, это еще и датасет, и модели, и куча программ-спутников.</p>

<p>Мы выступали на площадках с разработчиками (Python Meetup, ODS Data Fest) и психологами (CBT FORUM 2025, Открытые Двери.CONF). Наши «агенты» (в смысле человеки) есть как в Москве, так и в Питере. Выступления дают свои плоды: мы запартнерились с проектом «Открытые двери» — платформой доступной психологической помощи. С ними мы делаем бот-тренажер для психологов. Демо-версию мы показали на октябрьской конференции, где получили первый фидбек.</p>

<p>Мы перевели несколько диалоговых датасетов из психодомена с помощью нашего пайплайна перевода. Мы продолжаем над ним работать: учимся находить кривой перевод и учитывать культурные особенности. Кстати, для самих датасетов мы разработали единый формат.</p>

<p>Кроме выступлений мы завели сайт и тг-канал, где ненавязчиво, но регулярно рассказывали о наших насущных делах. У нас даже появилась первая визуальная айдентика.</p>

<p>По научной части мы опубликовали статью про наш датасет, а также два тезиса по пайплайну перевода. Кстати, основную часть работы, как и сами тезисы, сделала студентка. Внезапно, нас нашла другая студентка, пожелавшая свою магистерскую работу посвятить нашей теме с суицидом — будет смотреть, как современные БЯМ справляются с этой темой. Так что мы не только научная, но и образовательная команда.</p>

<p>Кстати, если вдруг вы студент, который ищет тему для курсача или диплома, или вы просто хотите что-то поделать для команды, у нас есть для вас кое-что. Нам очень хочется научиться делить сложные предложения на русском языке на составные части. По неведомой причине, нормально работающих методов для этой задачи нет. Всё, что нужно сделать, это разметить датасет и обучить/потестить разные модельки. Если интересно, пишите нам на почту psytechlab24@gmail.com.</p>

<p>Теперь о планах на предстоящий год.</p>

<p>Вы, может, думаете, зачем мы столько датасетов перевели? Отвечаем — мы хотим сделать психобенчмарк на русском языке: что модели знают о психологии и терапии и как они умеют в эмпатию. Без метрик мы как в тумане. Да, некоторые датасеты у нас будут переведенные, но это лучше, чем вообще ничего. Тем более, что мы еще докрутим перевод.</p>

<p>Мы также готовим научные статьи по синтетике данных для психодомена. О них мы вкратце рассказывали в прошлом девлоге. Приведем там результаты экспериментов и небольшую математическую модель для анализа качества синтетики.</p>

<p>Еще по айдентике и бренду чуть-чуть. Мы поняли, что о «ПсиТехЛаб» очень спотыкается глаз (и печатать неудобно), поэтому будем везде просто «Пситехлабом», как и весь текущий год. Также надо сделать логотип, а то наша картинка с котом всё-таки больше про настроение.</p>

<p>Есть у нас еще россыпь идей, которые мы прорабатываем и думаем, какую из них взять в первую очередь. Оставайтесь с нами, чтобы узнать о них.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Прошло 357 дней с поста, в котором мы поздравляли всех с наступающим текущим годом. Тогда мы создали Пситехлаб. Посмотрим, что мы сделали за это время.]]></summary></entry><entry><title type="html">Девлог 7. Генерить или не генерить? Промежуточные результаты с синтетикой для психодомена</title><link href="/2025/12/02/devlog-7-intermidiate_result_with_synth_data.html" rel="alternate" type="text/html" title="Девлог 7. Генерить или не генерить? Промежуточные результаты с синтетикой для психодомена" /><published>2025-12-02T00:00:00+00:00</published><updated>2025-12-02T00:00:00+00:00</updated><id>/2025/12/02/devlog-7-intermidiate_result_with_synth_data</id><content type="html" xml:base="/2025/12/02/devlog-7-intermidiate_result_with_synth_data.html"><![CDATA[<p>В нашем домене есть одна большая проблема — данных мало и/или их трудно достать. Связано это с этикой терапии, либо врачебной тайной, если диагноз психиатрический. Генерация данных через БЯМ может смягчить боль всех ml-инженеров, но как узнать, до какой степени? Мы пытаемся ответить этот вопрос и в этом посте коротенько расскажем промежуточные результаты.</p>

<p>Мы взяли три датасета: сентиметы, эмоции и антисуицидальную часть нашего датасета. Взяли именно их, потому что в статьях по ним есть описание классов. Без него не составить затравку. То есть, конечно, можно, но нам хотелось, чтобы затравка была связана с инструкцией по созданию датасета, мост навести, так сказать.</p>

<p>Про затравку еще кое-что. Мы тестировали три варианта: zero-shot, few(8)-shot и few-shot with keywords (fskw). Готовы поспорить, вы никогда не слышали про третий. Потому что это наша задумка. Идея вот в чем. Как бы вы не старались, но вы не сможете в какое-то резонное количество примеров для режима few-shot запихнуть различные лексические особенности класса. Тем более, что в некоторых работах отмечается, что с ростом примеров начинается деградация качества. Вот мы и решили добавлять такие классово-значимые слова в затравку.</p>

<p>Еще у нас было шесть моделей, три закрытых и три открытых, и два значения температуры: 0.7 и 1.</p>

<p>С помощью каждой комбинации этих параметров, мы сгенерировали для каждого класса каждого датасета уникальных примеров не меньше, чем в оригинальном датасете. Уже тут есть что сказать: некоторые конфиги генерили столько, сколько нужно, а другие генерировали до 15к текстов, Карл!, прежде, чем получить нужных пару тысяч уникальных среди них. Как только мы всё сгенерили, начали по-разному смешивать их с разными данными и обучать классификаторы.</p>

<p>Мы начали с простого: обучить только на генерате, на половине реального и половине генерата, на всем реальном и таком же количестве генерата. Второй и третий вариант мы рассматривать не будем в этом посте (tl;dr: результаты плюс-минус такие же, как в исходном варианте, где все данные реальны), обойдемся первым.</p>

<p>Если смотреть на качество итогового классификатора по f1-macro в разрезе каждой части конфигурации, то в среднем лучшей моделью стала llama-4-maveric (помните такую?) среди всех трех сеттингов. Изменение температуры влияло на качество только на антисуицидальном датасете: значение 0.7 в среднем лучше на 4 пункта, чем значение 1.0. Наша придумка fskw также показала лучшие результаты по всем трем датасетам.</p>

<p>Нам известно из второго пополамчатого сеттинга, что качество классифакции очень близко к исходному, где все данные реальные. А какого качества мы можем добиться, если будем добавлять генерат в минимальное количество реальных данных?</p>

<p>Для начала мы нашли этот условный минимум. Для всех трех датасетов это оказалось 10 процентов от реального объема. Потом в эти 10 процентов добавляли генерат объемом в 25, 50, 75 и 100 процентов от исходного объема каждого класса. Получилось что уже в первом случае для двух из трех датасетов происходит резкий рост качества модели (в целом, такое же рост наблюдается между 10 и 20 процентами реальных данных). После взлета наблюдается уже не такой заметный, но тоже прирост качества. Лучшие варианты миксов отстают от исходной модели на 3-5 пунктов. Кстати, далеко не факт, что лучшим миксом окажется вариант со 100 процентами генерата. Датасет сентиментов стал тем, где этот фокус не получился.</p>

<p>Далее у нас ряд экспериментов с разными метриками данных. Наша цель — найти метрику, которая по паре тысяч семлов модели могла сказать, что использовать перспективно, а что не очень.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[В нашем домене есть одна большая проблема — данных мало и/или их трудно достать. Связано это с этикой терапии, либо врачебной тайной, если диагноз психиатрический. Генерация данных через БЯМ может смягчить боль всех ml-инженеров, но как узнать, до какой степени? Мы пытаемся ответить этот вопрос и в этом посте коротенько расскажем промежуточные результаты.]]></summary></entry><entry><title type="html">Девлог 6. Как мы провели лето, часть 2</title><link href="/2025/10/26/devlog-6-our_summer_part_2.html" rel="alternate" type="text/html" title="Девлог 6. Как мы провели лето, часть 2" /><published>2025-10-26T00:00:00+00:00</published><updated>2025-10-26T00:00:00+00:00</updated><id>/2025/10/26/devlog-6-our_summer_part_2</id><content type="html" xml:base="/2025/10/26/devlog-6-our_summer_part_2.html"><![CDATA[<p>Мы много времени потратили на пайплайн перевода, при этом у нас не проработанным остался вопрос: как повысить качество переводов? Мы видели разное — от сомнительных до совсем никудышних примеров — когда оценивали БЯМ по отдельности. Мы решили проверить простую гипотезу: если посчитаем перплексию для переведенного текста, используя небольшую обученную ЯМ, то тексты с большей перплексией и будут плохим переводом.</p>

<p>В качестве базовой ЯМ у нас была <a href="https://huggingface.co/ai-forever/rugpt3small_based_on_gpt2">ai-forever/rugpt3small_based_on_gpt2</a>, потому что с ней просто работать в плане запуска на имеющемся железе. Взяли эту модель, наши переводы, прогнали всё и получили перплексию. Быстро поняли, что лучше работать с логарифмированной перплексией, потому что для коротких текстов ее значения иногда долетают до Венеры. В итоге получилось у нас вот такое общее распределение и, далее, распределение, зависящее от длины текста.</p>

<p><img src="/assets/images/devlog_6-perplexity_all.png" alt="" />
<img src="/assets/images/devlog_6-perplexity_dep_len.png" alt="" /></p>

<p>На общем распределении видно, что колокол смещен влево, а справа имеем длинный хвост. Такая картина дала надежду на правдоподность гипотезы. На втором распределении хорошо заметно, что размах перплексии изменяется от длины текста. Поскольку на совсем уж коротких текстах перплексия неадекватная, мы решили рассматривать только тексты длиннее пяти токенов. Мы решили в качестве границы брать четвертый квартиль: всё, что выше, мы считаем «плохим» переводом из-за большой перплексией. Чтобы учесть изменчивость размаха, мы распределеям значения длин в «корзинки» по децилям и считать квартили перплексии внутри каждой корзинки.</p>

<p>Вот такие переводы алгоритм посчитал «плохими»:</p>

<table>
  <thead>
    <tr>
      <th><strong>text_eng</strong></th>
      <th><strong>text_rus</strong></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>But even when there were a lot of cases, there are some people on my floor that walked freely in and out many times in one day when  it wasn’t safe for anyone. And I didn’t get that at all and that was when I was angry and anxious of their actions.</td>
      <td>Но даже тогда, когда было много случаев, некоторые люди на моем этаже ходили свободно в и из дома много раз в день, когда это не было безопасно для никого. И я совсем не понимал этого, и тогда я был зол и тревожен из-за их действий.</td>
    </tr>
    <tr>
      <td>am based on a quota.  I am suppose to be able to move 10 pallets per hour.  that’s almost impossible as it is a 1 million square foot warehouse.  If i get a pallet on the west side and have to move it to the east side that takes 7-10 minutes even if your going dangerously fast.</td>
      <td>работаю по квоте. Мне нужно перемещать 10 паллет в час. Это почти невозможно в складе площадью в 1 миллион квадратных футов. Если паллет находится на западной стороне, а его нужно перевести на восточную, это занимает 7-10 минут, даже если едешь с опасной скоростью.</td>
    </tr>
    <tr>
      <td>I think I pretty much got everything I need from you- I just needed to vent I think. So we can stop talking now or whenever you have to leave</td>
      <td>Я думаю, я получил все, что мне нужно от тебя - мне просто нужно было выговориться, думаю. Так что мы можем закончить разговор сейчас или когда тебе нужно уйти</td>
    </tr>
    <tr>
      <td>She had a desire to do marine biology and I wanted to pursue law enforcement as a police officer, however due to my back injury that fell through recently. She would spend most of her time doing field work, which would require her to spend time out at sea. She was working in a nursing home at the time and was not a marine biologist.</td>
      <td>Она хотела заниматься морской биологией, а я собирался посвятить себя службе в полиции, однако моя травма спины недавно все эти планы сорвала. Большую часть времени ей пришлось бы проводить в полевых условиях, что требует работы в море. В то время она работала в доме престарелых и не была морской биологом.</td>
    </tr>
    <tr>
      <td>I can understand how you would feel like he doesn’t care about the strain it’s placed. Can I ask, are your daughters aware or involved in these rumors? This could make a difference in how you could respond.</td>
      <td>Я понимаю, как ты можешь чувствовать, что он не заботится о твоем состоянии. Можно спросить, знают ли или участвуют ли твои дочери в этих слухах? Это может повлиять на то, как ты можешь отреагировать.</td>
    </tr>
  </tbody>
</table>

<p>А как нам убрать кавычки вокруг слова «плохих»? Надо провести стат. тест с людьми! Вот как мы его организовали:</p>

<ol>
  <li>Отобрали случайным образом 25 переводов, которые мы определили как плохие.</li>
  <li>Отобрали случайным образом 25 переводов из всего объема данных.</li>
  <li>Удостоверились, что выборки не пересекаются.</li>
  <li>Смешали все примеры в одну кучу таким образом, чтобы потом их можно было разъединить по исходным группам.</li>
  <li>Разметили переводы по качеству по пятибалльной шкале.</li>
  <li>Разделили оценки в соответствии с двумя исходными группами.</li>
  <li>Полученные группы передали в функцию подсчета U-критерия Манна-Уитни <code class="language-plaintext highlighter-rouge">scipy.mannwhitneyu(perplexity_selected, random_selected)</code>.</li>
</ol>

<p>Вот, что мы понимали под «качеством перевода»:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Оценивать качество перевода следует по их естественности с точки зрения русского языка. Чтобы было более понятно, вот список некоторых критериев, что мы под этим понимаем:
- Корректность грамматических конструкций (склонения, согласования в падежах и т.д.)
- Перевод должен точно передавать смысл оригинального текста
- Даже если смысл текста передан верно, текст не должен «резать глаз».
- Стиль перевода соответстует стилю оригинала.
- Фразы внутри переведенного текста должны быть связаны друг с другом логически и грамматически.
</code></pre></div></div>

<p>Разметчиков мы искали на Профи.ру. Критерий — либо образование переводчика, либо опыт работы таковым от 2 лет. Всего нашли трех людей. Итоговая оценка примера считалась как среднее от трех оценок. Иии… p-value=0.08. Это можно читать примерно так: «оно, может, даже работает, но лучше поискать что-нибудь понадежнее».</p>

<p>Кроме того, что надо найти плохие переводы, нам их нужно еще переделать. Все мы знаем, что если ваша БЯМ не справляется с задачей, нужно просто взять побольше. Поскольку у нас были уже нанятые люди, мы провели еще один стат. эксперимент: будет ли качество перевода лучше, если использовать модель большего размера? Чтобы это проверить, мы делали так:</p>

<ol>
  <li>Отобрали 200 «плохих» текстов.</li>
  <li>Переводили с помощью мощной модели (в нашем случае gpt-4o).</li>
  <li>Просили человека сравнить, какой перевод лучше (или одинаково). Критерии качества см. выше.</li>
  <li>Результаты запихивали в биномиальный тест <code class="language-plaintext highlighter-rouge">binomtest(new_win, n=n, p=0.5, alternative='greater')</code>.</li>
</ol>

<p>В итоге получили p-value=0.001. Правило «просто возьми модель побольше» работает.</p>

<p>Последнее, что мы по касательной затронули, это LLM-as-a-judge. Поскольку мы команда независимая, бережное отношение к ресурсам — наша абсолютная база. Проверка каждого придуманного алгоритма отборщика людьми с этим плохо соотносится. Было бы здорово с помощью БЯМ отбраковывать совсем плохие варианты.</p>

<p>Мы специально ничего не изучали в этой теме, просто решили провести еще один простой стат. тест на тех данных, что у нас есть: есть ли какие-то ассоциации между оценками разных БЯМ и оценками людей по качеству перевода? Тестировали мы вот этих товарищей:</p>
<ul>
  <li>gpt-4o</li>
  <li>claude-sonnet-4</li>
  <li>gemini-2.5-flash</li>
  <li>qwen3-235b-a22b</li>
  <li>deepseek-chat-v3-0324</li>
  <li>llama-4-maverick</li>
</ul>

<p>При этом тестировали в двух вариантах:</p>

<ul>
  <li>стандартный — просили БЯМ поставить оценку от 1 до 5.</li>
  <li>упрощенный — просили БЯМ просто сказать плохой ли перевод или нет, а оценки людей мы схлопнули по схеме {1,2,3} — «плохой», {4,5} — «хороший».</li>
</ul>

<p>Наличие ассоциации проверяли с помощью хи-квадрата. В результате получили вот такую таблицу</p>

<table>
  <thead>
    <tr>
      <th style="text-align: center"> </th>
      <th style="text-align: center">Разметчик 1</th>
      <th style="text-align: center">Разметчик 2</th>
      <th style="text-align: center">Разметчик 3</th>
      <th style="text-align: center">Среднее</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td style="text-align: center">gpt-4o__15</td>
      <td style="text-align: center">1.000000</td>
      <td style="text-align: center">0.175658</td>
      <td style="text-align: center">0.001253</td>
      <td style="text-align: center">0.999996</td>
    </tr>
    <tr>
      <td style="text-align: center">claude-sonnet-4__15</td>
      <td style="text-align: center">1.000000</td>
      <td style="text-align: center">0.387769</td>
      <td style="text-align: center">0.005886</td>
      <td style="text-align: center">1.000000</td>
    </tr>
    <tr>
      <td style="text-align: center">gemini-2.5-flash__15</td>
      <td style="text-align: center">0.999996</td>
      <td style="text-align: center">0.118529</td>
      <td style="text-align: center">0.000040</td>
      <td style="text-align: center">0.999956</td>
    </tr>
    <tr>
      <td style="text-align: center">qwen3-235b-a22b__15</td>
      <td style="text-align: center">0.999993</td>
      <td style="text-align: center">0.160640</td>
      <td style="text-align: center">0.000288</td>
      <td style="text-align: center">0.999976</td>
    </tr>
    <tr>
      <td style="text-align: center">deepseek-chat-v3-0324__15</td>
      <td style="text-align: center">0.999997</td>
      <td style="text-align: center">0.101087</td>
      <td style="text-align: center">0.000426</td>
      <td style="text-align: center">0.999987</td>
    </tr>
    <tr>
      <td style="text-align: center">llama-4-maverick__15</td>
      <td style="text-align: center">1.000000</td>
      <td style="text-align: center">0.214498</td>
      <td style="text-align: center">0.000771</td>
      <td style="text-align: center">0.999997</td>
    </tr>
    <tr>
      <td style="text-align: center">gpt-4o__12</td>
      <td style="text-align: center">1.000000</td>
      <td style="text-align: center">1.000000</td>
      <td style="text-align: center">1.000000</td>
      <td style="text-align: center">1.000000</td>
    </tr>
    <tr>
      <td style="text-align: center">claude-sonnet-4__12</td>
      <td style="text-align: center">1.000000</td>
      <td style="text-align: center">1.000000</td>
      <td style="text-align: center">1.000000</td>
      <td style="text-align: center">1.000000</td>
    </tr>
    <tr>
      <td style="text-align: center">gemini-2.5-flash__12</td>
      <td style="text-align: center">1.000000</td>
      <td style="text-align: center">1.000000</td>
      <td style="text-align: center">1.000000</td>
      <td style="text-align: center">1.000000</td>
    </tr>
    <tr>
      <td style="text-align: center">qwen3-235b-a22b__12</td>
      <td style="text-align: center">1.000000</td>
      <td style="text-align: center">1.000000</td>
      <td style="text-align: center">1.000000</td>
      <td style="text-align: center">1.000000</td>
    </tr>
    <tr>
      <td style="text-align: center">deepseek-chat-v3-0324__12</td>
      <td style="text-align: center">1.000000</td>
      <td style="text-align: center">1.000000</td>
      <td style="text-align: center">1.000000</td>
      <td style="text-align: center">1.000000</td>
    </tr>
    <tr>
      <td style="text-align: center">llama-4-maverick__12</td>
      <td style="text-align: center">1.000000</td>
      <td style="text-align: center">1.000000</td>
      <td style="text-align: center">1.000000</td>
      <td style="text-align: center">1.000000</td>
    </tr>
  </tbody>
</table>

<p>И тут странности. Ни одна БЯМ никак не соотносится с первым разметчиком, со вторым, конечно, не в ноль, но не стат. значимо, а вот с третьим уже соотносится. Средняя оценка и упрощенный вариант тоже не показывают от слова «ничего. Тут мы решили посчитать согласованность разметки качества и получили ни много ни мало -0.09 по Криппендорфу.</p>

<p><img src="/assets/images/devlog_6-meme.png" alt="" /></p>

<p>Получается, что и оценка отборщика недостоверна, раз у нас три человека, несмотря на критерии, судят о качестве перевода каждый по своим вайбам. Занимательно, что у БЯМ вайбы совпадают с одним из людей. Еще весьма вероятно, что достоверность теста с улучшением качества тоже получилась так себе по той же причине. В общем, нужно искать более надежный способ установления качества переводов. Будем думать.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Мы много времени потратили на пайплайн перевода, при этом у нас не проработанным остался вопрос: как повысить качество переводов? Мы видели разное — от сомнительных до совсем никудышних примеров — когда оценивали БЯМ по отдельности. Мы решили проверить простую гипотезу: если посчитаем перплексию для переведенного текста, используя небольшую обученную ЯМ, то тексты с большей перплексией и будут плохим переводом.]]></summary></entry><entry><title type="html">Перевод датасета для оценки эмпатии на русский язык. Подход, проблемы, результаты</title><link href="/2025/09/13/epitome_dataset_translation.html" rel="alternate" type="text/html" title="Перевод датасета для оценки эмпатии на русский язык. Подход, проблемы, результаты" /><published>2025-09-13T00:00:00+00:00</published><updated>2025-09-13T00:00:00+00:00</updated><id>/2025/09/13/epitome_dataset_translation</id><content type="html" xml:base="/2025/09/13/epitome_dataset_translation.html"><![CDATA[<p>Привет. Меня зовут Нафиса Валиева. Я младший разработчик в MWS AI и студентка 3го курса ПМ-ПУ СПбГУ. Этот пост — текстовый вариант моего выступления на <a href="https://youtu.be/ZkydkhQvO64">Дата Фесте</a>. Я расскажу вам, как мы в команде Пситехлаб переводили интересный датасет с английского на русский с помощью больших языковых моделей (БЯМ). Сам подход основан на ранней работе [1] нашего руководителя. Отличие в том, что здесь мы детально анализируем поведение различных БЯМ.</p>

<p><em>Изначально пост был <a href="https://habr.com/ru/articles/946264/">опубликован на Хабре</a>, но для целостности картины нашей работы размещаем и в нашем блоге.</em></p>

<h1 id="зачем-это-вообще-и-что-за-датасет-такой">Зачем это вообще и что за датасет такой</h1>

<p>Эмпатия играет важную роль в коммуникации между людьми, и в частности, в сервисах психологической помощи. В онлайн-среде, где такая помощь всё чаще оказывается в текстовом формате, появляется много различных сервисов, которые предоставляют психологическую помощь на основе чатботов. Для них способность отвечать эмпатично становится критически важным навыком. В противном случае хорошо если сеанс окажется просто бесполезным и не усугубит имеющиеся проблемы.
Успех БЯМ побуждает разработчиков использовать их в качестве основы для таких чатботов. Для оценки их способностей разрабатываются различные бенчмарки, в частности для задач с уклоном в психотерапию. Одним из таких является PsyEval [2].
Однако для автоматической оценки эмпатии в текстах на русском языке размеченных датасетов просто нет. Мы, русскоязычные MLщики, не можем сказать, как сейчас БЯМ справляются с задачами, которые связаны с выявлением эмпатии и генерацией эмпатичных ответов. А ведь эти задачи напрямую влияют на качество инструментов псих-поддержки.
Чтобы это хоть как-то исправить, мы приспособили большие языковые модели к переводу датасета с английского на русский язык. Целевым датасетом стал <a href="https://aclanthology.org/2020.emnlp-main.425.pdf">EPITOME</a>, который состоит из текстов с Reddit и включает разметку по трем типам эмпатии:</p>
<ul>
  <li>Эмоциональные реакции - выражение сопереживания (теплота, сострадание, поддержка) в ответ на сообщение собеседника</li>
  <li>Интерпретации - Показ понимания чувств и опыта собеседника.</li>
  <li>Исследования - Активный интерес к непроявленным переживаниям собеседника
Каждый тип эмпатии имеет два уровня выраженности: слабый и сильный. Кроме самих типов датасет содержит аннотированные подстроки — носители эмпатии. Они указывают, какие именно части текста отражают эмпатичный отклик. Вот картинка из оригинальной статьи, которая наглядно показывает все эти виды.</li>
</ul>

<p><img src="/assets/images/epitome/epitome_explanation.png" alt="" /></p>

<p>Если кратко, всю  работу можно разложить на несколько шагов:</p>
<ol>
  <li>Подобрать БЯМ, которая лучше всего справится с переводом.</li>
  <li>Разработать затравку для перевода.</li>
  <li>Реализовать полную процедуру перевода датасета.</li>
  <li>Обучить модели для классификации эмпатии на русском языке, используя оригинальную модель.</li>
</ol>

<h1 id="подбор-бям-и-разработка-затравки">Подбор БЯМ и разработка затравки</h1>

<p>Для тестов мы выбрали несколько БЯМ: GPT-4o, Qwen-2.5 различных масштабов, Mistral-Small-24B-Instruct-2501, YandexGPT Pro. В довесок мы тестировали переводчик Yandex Translate, как промышленное и специализированное решение.  Вместе с тестированием модели итеративно разрабатывалась затравка, в которую включались идеи из анализа ошибок модели.
На начальном этапе мы сделали простую затравку для перевода, чтобы получить первичную оценку качества. Для тестового материала были отобраны вручную 20 текстов из датасета, содержащих типичные особенности языка Reddit: сленг, нестандартную пунктуацию, эмоциональные выражения, неформальные конструкции и аббревиатуры (например, “OP”, “DAE”, “yeet”, “tmblr”).</p>

<p>Переводы проверялись вручную перекрестно двумя разработчиками. Особое внимание обращалось на сохранение смысла и стиля. Для дополнительной проверки использовалась метрика L1-diff эмбеддингах <a href="https://huggingface.co/cointegrated/LaBSE-en-ru">LaBSE/en-ru</a>, чтобы измерять семантическое расстояние между оригиналом и переводом. В общем случае метрику можно представить в виде формулы</p>

<p>\(L1= ∣f(t_{src}) - f(f_{trg})∣\) ,</p>

<p>где $f(x)$ - эмбеддер (в данном случае, модель LaBSE), $t_{eng}$ - текст на исходном языке (английском), $t_{trg}$ - текст на переведенном языке (русском). В итоге у нас получился вот такой топ-3 из моделей: GPT-4o, Qwen-2.5-72b-instruct и YandexGPT Pro.</p>

<p>Если обобщить анализ ошибок, то главное препятствие для хорошего перевода это стиль социальных сетей. 
Сокращения и аббревиатуры — естественные спутники соцсетей, потому что пользователи стремятся быстрее написать текст. Некоторые сокращения переносятся, как есть, типа VR, tmblr (название соц. сети) и, кажется, что это допустимо. Для некоторых сокращений трудно решить, стоит их переводить или нет, например, OP — ОП, автор темы. Есть такие, которые точно нужно раскрыть: rn (right now), asap (as soon as possible). Некоторые аббревиатуры пропускаются моделями (wtf, ish),  а аббревиатура DAE (does anyone else) оказалось настолько сложной, что с ней справилась только gpt-4o. 
Еще тексты соцсетей пестрят междометиями от простых до супер вычурных. С их помощью люди часто пытаются имитировать разговорную речь (aaaaand, иииии). Они часто либо переносятся латиницей как есть, либо транслитерируются.</p>

<p>Также пользователи иногда украшают тексты своеобразным форматированием, из-за чего некоторые модели сбиваются с толку и теряют контекст. Конечно, игра слов тоже иногда моделями не улавливается: в предложении “i’m 24 old old man” некоторые модели просто пропускали второе слово “old”, вместо того, чтобы перевести предложение как «я 24 летний старик».</p>

<p>Ниже в таблице показаны типичные случаи (жирный шрифт везде, кроме заголовочной строки, следует читать как две звездочки перед и после текста, выделенным жирным)</p>

<table>
  <thead>
    <tr>
      <th><strong>Тексты для проверки перевода</strong></th>
      <th><strong>gpt-4o</strong></th>
      <th><strong>YandexGPT Pro</strong></th>
      <th><strong>qwen-2.5-7b-instruct</strong></th>
      <th><strong>qwen-2.5-72b-instruct</strong></th>
      <th><strong>qwen2.5-32b-instruct</strong></th>
      <th><strong>qwen2.5-vl-72b-instruct</strong></th>
      <th><strong>yandex-translate</strong></th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>rn im in the area of not going to sleep so tmrw doesn’t come</td>
      <td>сейчас я в состоянии не ложиться спать, чтобы завтра не наступило</td>
      <td>Я сейчас не сплю, чтобы завтрашний день не наступил</td>
      <td>rn я нахожусь в зоне бодрствования, чтобы завтра не приходилось спать</td>
      <td>Сейчас я в состоянии, когда не собираюсь спать, чтобы завтрашний день не наступил.</td>
      <td>я сейчас в состоянии, когда не хочу спать, чтобы завтра не наступило</td>
      <td>Сейчас я в том состоянии, что не могу уснуть, чтобы завтрашний день не наступил.</td>
      <td>рн, я как раз собираюсь не ложиться спать, так что tmrw не придет</td>
    </tr>
    <tr>
      <td>and im sooo tired :(</td>
      <td>и я так устал :(</td>
      <td>А я так устал :(</td>
      <td>и я так устал :(</td>
      <td>и я так устала :(</td>
      <td>и я так устала :(</td>
      <td>и я так уставший :(</td>
      <td>и я ооочень устала :(</td>
    </tr>
    <tr>
      <td>Meh. What’s the point….</td>
      <td>Эх. В чем же смысл….</td>
      <td>Эх. В чём смысл…</td>
      <td>Мeh. Какая разница….</td>
      <td>Ну и что… какая разница…</td>
      <td>Мeh. В чем смысл….</td>
      <td>М-да. Какой в этом смысл….</td>
      <td>Какой в этом смысл?…</td>
    </tr>
    <tr>
      <td>Yeet yeet. I’m gonna hang myself with the charger (</td>
      <td>Йет йет. Я собираюсь повеситься на зарядке (</td>
      <td>Еееее. Я собираюсь повеситься с зарядкой (</td>
      <td>Мне надо повеситься с зарядным устройством(</td>
      <td>Йит йит. Я повешусь наушниками зарядки (</td>
      <td>Yeet yeet. Я собираюсь повеситься шнурком от зарядки (</td>
      <td>Йит йит. Я собираюсь повеситься на зарядном устройстве (</td>
      <td>Да-да-да. Я собираюсь повеситься на зарядном устройстве(</td>
    </tr>
    <tr>
      <td>POEM:Hollow. <strong><em>EMPTY</em></strong> <em>rooms and</em> <strong><em>EMPTY</em></strong> <strong><em>SOULS</em></strong> <strong><em>FORGOTTEN</em></strong> <em>places and</em> <strong><em>HEART</em></strong> <em>shaped holes</em> <strong><em>DEPRESSION</em></strong> <em>comes with days of</em> <strong><em>STRUGGLE</em></strong> <em>and often times a</em> <strong><em>BLOODY PUDDLE</em></strong> UP-VOTES CURE MY DEPRESSION</td>
      <td>ПОЭМА: Пустота. <strong><em>ПУСТЫЕ</em></strong> <em>комнаты и</em> <strong><em>ПУСТЫЕ</em></strong> <strong><em>ДУШИ</em></strong> <strong><em>ЗАБЫТЫЕ</em></strong> <em>места и</em> <strong><em>СЕРДЦЕ</em></strong> <em>образные дыры</em> <strong><em>ДЕПРЕССИЯ</em></strong> <em>приходит с днями</em> <strong><em>БОРОТЬБЫ</em></strong> <em>и часто</em> <strong><em>КРОВАВАЯ ЛУЖА</em></strong> ВВЕРХ-ГОЛОСА ИСЦЕЛЯЮТ МОЮ ДЕПРЕССИЮ</td>
      <td>СТИХ: Пусто. <strong><em>ПУСТО</em></strong> комнаты и <strong><em>ПУСТО</em></strong> <strong><em>ДУШИ</em></strong> <strong><em>ЗАБЫТЫЕ</em></strong> места и дыры в форме <strong><em>СЕРДЦА</em></strong> <strong><em>ДЕПРЕССИЯ</em></strong> приходит с днями <strong><em>БОРЬБЫ</em></strong> и часто с <strong><em>КРОВАВЫМ ПЯТНОМ</em></strong> ВВЕРХ-ГОЛОСУЮТ ИСЦЕЛЯЮТ МОЮ ДЕПРЕССИЮ</td>
      <td>БОЛЕТАЯ: Пусто. <strong><em>ПУСТО</em></strong> <em>комнаты и</em> <strong><em>ПУСТЫЕ</em></strong> <strong><em>ДУСЫ</em></strong> <strong><em>ЗАБЫТЫЕ</em></strong> <em>места и</em> <strong><em>СЕРДЦЕВИКИ</em></strong> <em>депрессия сопоставима с днями</em> <strong><em>МЕРТВЫХ СИЛАВ</em></strong> <em>и часто с</em> <strong><em>КРОВАВЫМ ПОЛЯМЕРОМ</em></strong> ЛАУД-ВИТС ПОЛУЧАЮТ ЗАЩИТУ ОТ МОЕЙ ДЕПРЕССИИ</td>
      <td>POEM:Пустота. <strong><em>ПУСТОТА</em></strong> <em>помещения и</em> <strong><em>ПУСТОТА</em></strong> <strong><em>ДУШИ</em></strong> <strong><em>ЗАБЫТЫЕ</em></strong> <em>места и</em> <strong><em>СЕРДЦЕ</em></strong> <em>формированные дыры</em> <strong><em>ДЕПРЕССИЯ</em></strong> <em>приходит с днями</em> <strong><em>БОРЬБЫ</em></strong> <em>и часто вместе с</em> <strong><em>КРОВАВОЙ ЛУЖЕЙ</em></strong> ГОЛОСОВАНИЕ РЕШАЕТ МОЮ ДЕПРЕССИЮ</td>
      <td>СТИХ:Пустота. <strong><em>ПУСТО</em></strong> <em>комнаты и</em> <strong><em>ПУСТОТА</em></strong> <strong><em>Забытые</em></strong> <em>мещства и</em> <strong><em>СЕРДЦА</em></strong> <em>образуют пустоты</em> <strong><em>ДЕПРЕССИЯ</em></strong> <em>приходит с днями</em> <strong><em>УСТРЕКОВ</em></strong> <em>и часто с</em> <strong><em>КРОВАВЫМ ОТСЕКОМ</em></strong> ЛАЙКИ ЛЕЧАТ МОЮ ДЕПРЕССИЮ</td>
      <td>БОЛЕТАЯ: Пусто. <strong><em>ПУСТО</em></strong> <em>комнаты и</em> <strong><em>ПУСТЫЕ</em></strong> <strong><em>ДУСЫ</em></strong> <strong><em>ЗАБЫТЫЕ</em></strong> <em>места и</em> <strong><em>СЕРДЦЕВИКИ</em></strong> <em>депрессия сопоставима с днями</em> <strong><em>МЕРТВЫХ СИЛАВ</em></strong> <em>и часто с</em> <strong><em>КРОВАВЫМ ПОЛЯМЕРОМ</em></strong> ЛАУД-ВИТС ПОЛУЧАЮТ ЗАЩИТУ ОТ МОЕЙ ДЕПРЕССИИ</td>
      <td>СТИХОТВОРЕНИЕ: Пустота. <strong><em>ПУСТЫЕ</em></strong><em>комнаты и</em><strong><em>ПУСТЫЕ</em></strong><em>ДУШИ</em><strong>**ЗАБЫТЫЕ</strong><em>места и</em><strong><em>Дыры в СЕРДЦЕ</em></strong><strong>ДЕПРЕССИЯ</strong>** приходит с днями* <strong><em>БОРЬБА</em></strong><em>и часто <strong>**КРОВАВАЯ ЛУЖА</strong></em> ГОЛОСОВ “ЗА” ИЗЛЕЧИВАЕТ МОЮ ДЕПРЕССИЮ.</td>
    </tr>
    <tr>
      <td>How can I accept my intrusive depressive thoughts?. Long story short: Was 335, Am 175. Aiming for 130/140ish. Body destroyed. </td>
      <td>Как мне принять мои навязчивые депрессивные мысли? Короче говоря: было 335, сейчас 175. Цель - примерно 130/140. Тело разрушено.</td>
      <td>Как мне принять свои навязчивые депрессивные мысли? Короче говоря: был 335, стал 175. Стремлюсь к 130/140. Тело разрушено.</td>
      <td>Как я могу принять навязчивые депрессивные мысли? Краткий обзор: был 335, сейчас 175. Цель - 130/140. Тело полностью уничтожено. </td>
      <td>Как я могу принять свои навязчивые депрессивные мысли? Кратко: Было 335, Теперь 175. Цель 130/140. Тело разрушено.</td>
      <td>Как мне принять мои навязчивые депрессивные мысли? Коротко говоря: Был 335, Стал 175. Цель - 130/140. Тело разрушено.</td>
      <td>Как я могу принять свою навязчивую депрессивные мысли? Короткая история: Был 335, стал 175. Цель 130/140. Тело разрушено.</td>
      <td>Как я могу смириться со своими навязчивыми депрессивными мыслями?. Короче говоря, мне было 335, а сейчас 175. Стремлюсь к росту 130/140. Тело разрушено.</td>
    </tr>
    <tr>
      <td>…. Feeling hopeless and useless at the moment… Empty, lost, darkness</td>
      <td>…. Чувствую себя безнадежно и бесполезно в данный момент… Пустота, потерянность, тьма</td>
      <td>… Сейчас я чувствую себя безнадежным и никчёмным… Пустым, потерянным, в темноте</td>
      <td>…. Чувствую себя безнадежным и бессилен в данный момент… Пустота, потерялся, тьма.</td>
      <td>…. Чувствую себя безнадежным и бесполезным в этот момент… Одиноким, потерянным, во тьме</td>
      <td>…. Чувствую себя безнадежным и бестолковым в данный момент… Пустота, потерянность, темнота</td>
      <td>…. Чувствую себя сейчас бессмысленным и бесполезным… Пустым, потерянным, в темноте</td>
      <td>…. Чувствую себя безнадежным и бесполезным в данный момент… Пустота, потерянность, темнота</td>
    </tr>
  </tbody>
</table>

<p>Другие особенности в двух словах:</p>
<ul>
  <li>Некоторые модели ставят знаки препинания лучше, чем другие.</li>
  <li>Иногда модели исправляют явную логическую ошибку в тексте:
    <ul>
      <li>compare ur self to others because u will loose and forget who u r — <strong>не</strong> сравнивай с себя с другими, иначе ты проиграешь и забудешь, кто ты на самом деле.</li>
    </ul>
  </li>
</ul>

<p>Изначально мы рассчитывали, что для этой задачи мы сможем обойтись маленькой БЯМ, которой в нашем случае выступала Qwen2.5-7b. Результаты показали, что не обойдемся и вот почему:</p>
<ul>
  <li>В переведенных текстах много латиницы.</li>
  <li>В сложных случаях, когда текст написан небрежно, модель начинает заниматься словотворчеством,
    <ul>
      <li>«Как ты? “Я Ф.И.Н.Е.”. <strong>Посорванная</strong>, неуверенная, нервная и эмоциональная.» (““How are you?” “I’m F.I.N.E.”. Fucked up, insecure, neurotic, and emotional.”)</li>
    </ul>
  </li>
  <li>На длинных текстах может терять смысл.
    <ul>
      <li>«Мне 32 года, и у меня nunca была подружка. Очень грустно, что я давно стараюсь найти кого-то, даже используя Онлайн-датинг, но я чувствую, что останусь single вечно.»</li>
    </ul>
  </li>
  <li>Модель часто использует не совсем те слова, которые следует (не “хочется умереть”, а “хочется погибнуть”), путает части речи и склонения слов.</li>
</ul>

<p>Кроме выбора модели, мы также улучшали затравку, опираясь на типичные ошибки перевода. Кроме того, мы включили известные общие практики по промт-инжиниригу.  Можно отметить такие части:</p>
<ul>
  <li>Требование сохранять оригинальный стиля сообщений, включая эмоциональные и суицидальные выражения.</li>
  <li>Инструкции по обходу фильтров моделей, препятствующих переводу текстов с суицидальной и депрессивной тематикой,</li>
  <li>Требование точно переносить смысловые акценты,</li>
  <li>Требование обязательно включать переведённых носителей эмпатии как подстрок в основном тексте.
Еще мы протестировали модель в режиме перевода батчами — одновременного перевода нескольких текстов. Такой подход ставит выбор между скоростью и качеством, потому что чем больше текстов надо переводить, тем вероятнее, что БЯМ сделает что-то не так. Мы именно протестировали, как БЯМ будет работать в таком режиме, но итоговый датасет мы переводили по одному тексту. Ниже в таблице показаны результаты наших топ-3 моделей при переводе батчем в объеме 32 текстов. Видно, что если при обычном переводе у всех моделей все идет хорошо, то вот при переводе объяснений всё не так гладко.</li>
</ul>

<p><img src="/assets/images/epitome/firefox_XC1tyndrMU.png" alt="" /></p>

<h1 id="весь-пайплайн-целиком">Весь пайплайн целиком</h1>
<p>Перевод датасета проходит в два этапа: общий перевод и перевод носителей эмпатии. Для двух этапов в качестве основной модели для перевода использовалась YandexGPT Pro, поскольку она демонстрировала лучшее соотношение по качеству и цене для большинства примеров. Для проблемных случаев, в которых YandexGPT допускала искажения или вкрапления иноязычных символов, применялась Qwen-2.5-72b-instruct. Если проблема сохранялась, то применялась GPT-4o. Такой ступенчатый процесс позволил достичь наилучшего баланса между стоимостью и качеством. 
На втором этапе специальный скрипт проверял, чтобы каждая переведенная подстрока носителя эмпатии входила в состав основного перевода без изменений. Если хотя бы один из фрагментов не удавалось точно сопоставить — текст переводился повторно с помощью другой более успешной модели.</p>

<p><img src="/assets/images/epitome/epitome_translation_pipeline.png" alt="" /></p>

<p>Бюджет
Общий бюджет на перевод датасета составил до 5 000 рублей, включая тестовые переводы, основную часть датасета и обработку носителей эмпатии. Большое спасибо Яндекс Клауду  за сертификат на 3000 рублей при регистрации. Детальная картина выглядит так:</p>
<ul>
  <li>Тестирование перевода - 500 рублей (200 - YandexGPT Pro, 300 - Bothub (GPT-4o, qwen*))</li>
  <li>1 800 seeker posts - 1 500 рублей Bothub (GPT-4o)</li>
  <li>Перевод 2 943 носителей, 1 284 seeker posts и 3 084 response posts - 3 000 рублей (YandexGPT Pro - 2 300 рублей, Bothub (GPT-4o, qwen-2.5-72b-instruct) - 700 рублей)</li>
</ul>

<h1 id="обучение-модели">Обучение модели</h1>

<p>Чтобы понять, что наш переведенный датасет вообще чего-то стоит, мы обучили оригинальную модель классификации и вычленения носителей эмпатии.  В качестве базовых энкодеров мы протестировали rubert-base-cased и xlm-roberta-base. Качество замеряли также по набору оригинальных метрик:</p>
<ul>
  <li>Accuracy — доля верных предсказаний,</li>
  <li>F1-score — гармоническое среднее точности и полноты,</li>
  <li>Token-level F1 (T-F1) — F1 на уровне токенов для задач извлечения,</li>
  <li>IOU (Intersection over Union) — мера перекрытия предсказанных и эталонных фрагментов</li>
</ul>

<p>Результаты экспериментов показаны в таблице ниже. В строке «метрики авторов» указаны метрики из оригинальной статьи [3]  для косвенного сравнения работоспособности модели. Видно, что порядок значений и распределение качества по подзадачам в целом совпадает, что говорит об адекватности переведенного датасета, а значит работоспособности описанного метода перевода. Интересно отметить, что rubert-base-cased стабильно превосходит xlm-roberta-base по большинству метрик.</p>

<p><img src="/assets/images/epitome/firefox_CoOIRceq7V.png" alt="" /></p>

<h1 id="выводы-и-что-дальше">Выводы и что дальше</h1>
<p>Под конец давайте выпишем все проблемы, с которыми мы столкнулись и которые требуют дальнейше проработки:</p>
<ul>
  <li>Многие модели по умолчанию отказываются работать с потенциально чувствительным контентом (суицидальные или депрессивные тексты). Иногда это можно обойти промт-инжинирингом, а иногда нет.</li>
  <li>Стиль общения в социальных сетях включает множество особенностей. Не каждая модель может понять и сохранить его при переводе.</li>
  <li>Культурная неоднозначность и субъективность аннотаций: проявления эмпатии в англоязычном и русскоязычном контекстах могут отличаться, а сами аннотации по уровням и подтипам эмпатии зависят от восприятия разметчиков, что влияет на интерпретируемость и обучение моделей.
Кроме решения описанных проблем можно также предложить дополнительные направления:</li>
  <li>Расширение датасета за счет дополнительных источников, включая реальные диалоги с психотерапевтических платформ.</li>
  <li>Тонкая настройка и дообучение БЯМ на задачи генерации эмпатичных ответов в условиях диалога.</li>
  <li>Построение открытого бенчмарка для оценки способности БЯМ к распознаванию и генерации эмпатичных ответов на русском язык.
Переведенный датасет можно взять <a href="https://huggingface.co/datasets/psytechlab/epitome-reddit-ru">здесь</a>, пайплайн можно взять <a href="https://github.com/psytechlab/empathy_dataset_transfer">здесь</a>. Канал нашей команды <a href="https://t.me/psytechlab">здесь</a>.</li>
</ul>

<p>До скорого.</p>

<p>[1] D. Popov, E. Terentev, D. Serenko, I. Sochenkov, and I. Buyanov, “Transferring natural language datasets between languages using large language models for modern decision support and Sci-Tech analytical systems,” Big Data and Cognitive Computing, vol. 9, no. 5, p. 116, Apr. 2025, doi: 10.3390/bdcc9050116.</p>

<p>[2] H. Jin, S. Chen, M. Wu, and K. Q. Zhu, “PsyEVAL: a comprehensive large language model evaluation benchmark for mental health,” arXiv (Cornell University), Jan. 2023, doi: 10.48550/arxiv.2311.09189.</p>

<p>[3] A. Sharma, A. S. Miner, D. C. Atkins, and T. Althoff, “A computational approach to understanding empathy expressed in Text-Based Mental Health support,” arXiv (Cornell University), Jan. 2020, doi: 10.48550/arxiv.2009.08441.</p>]]></content><author><name>Нафиса Валиева</name></author><summary type="html"><![CDATA[Привет. Меня зовут Нафиса Валиева. Я младший разработчик в MWS AI и студентка 3го курса ПМ-ПУ СПбГУ. Этот пост — текстовый вариант моего выступления на Дата Фесте. Я расскажу вам, как мы в команде Пситехлаб переводили интересный датасет с английского на русский с помощью больших языковых моделей (БЯМ). Сам подход основан на ранней работе [1] нашего руководителя. Отличие в том, что здесь мы детально анализируем поведение различных БЯМ.]]></summary></entry><entry><title type="html">Девлог #5. Как Мы Провели Лето, Часть 1</title><link href="/2025/09/04/%D0%94%D0%B5%D0%B2%D0%BB%D0%BE%D0%B3-5.-%D0%9A%D0%B0%D0%BA-%D0%BC%D1%8B-%D0%BF%D1%80%D0%BE%D0%B2%D0%B5%D0%BB%D0%B8-%D0%BB%D0%B5%D1%82%D0%BE,-%D1%87%D0%B0%D1%81%D1%82%D1%8C-1.html" rel="alternate" type="text/html" title="Девлог #5. Как Мы Провели Лето, Часть 1" /><published>2025-09-04T00:00:00+00:00</published><updated>2025-09-04T00:00:00+00:00</updated><id>/2025/09/04/%D0%94%D0%B5%D0%B2%D0%BB%D0%BE%D0%B3-#5.-%D0%9A%D0%B0%D0%BA-%D0%BC%D1%8B-%D0%BF%D1%80%D0%BE%D0%B2%D0%B5%D0%BB%D0%B8-%D0%BB%D0%B5%D1%82%D0%BE,-%D1%87%D0%B0%D1%81%D1%82%D1%8C-1</id><content type="html" xml:base="/2025/09/04/%D0%94%D0%B5%D0%B2%D0%BB%D0%BE%D0%B3-5.-%D0%9A%D0%B0%D0%BA-%D0%BC%D1%8B-%D0%BF%D1%80%D0%BE%D0%B2%D0%B5%D0%BB%D0%B8-%D0%BB%D0%B5%D1%82%D0%BE,-%D1%87%D0%B0%D1%81%D1%82%D1%8C-1.html"><![CDATA[<p>Время летит быстро, особенно для тех, кто чем-то занят. Вот и мы вроде только писали наш майский девлог, а уже четыре дня как школьники и студенты сели за парты. В следующих двух девлогах расскажем вам, как прошло наше лето, что делаем сейчас и какие планы.</p>

<h1 id="как-мы-в-питере-выступали">Как мы в Питере выступали</h1>

<p>В конце мая наш руководитель ездил в Питер на конференцию CBTFORUM, которую проводит Ассоциация когнитивно-поведенческой терапии. Там он рассказывал про нашу платформу Китобой. Среди прочего, мы нашли коннект с руководительницей проекта «Открытые двери» и начали совместный пилотный проект.</p>

<p>Кстати, 12 октября она проводит свою конференцию в Питере, куда пригласила и нас. Самая интересная часть — это панельная дискуссия, где психотерапевты будут обсуждать перспективы ИИ для их работы. Мы там будем со стороны тех самых разработчиков этих инструментов.</p>

<p>Кроме этой дискуссии, конечно, будут просто доклады. От нас будет большой доклад из двух частей. В первой части мы познакомим психологов с нейросетями поближе, а во второй расскажем, как ИИ применяется в психологии за пределами «чатботов-психологов».</p>

<p>Если вы захотели поучаствовать, то билеты приобрести можно <a href="https://socialcabinet.ru/opendoorsconf?utm_source=telegram&amp;utm_medium=post&amp;utm_campaign=chat&amp;utm_content=august&amp;utm_term=2025">здесь</a>. Формат как онлайн, так и оффлайн.</p>

<h1 id="как-мы-переводили-датасеты-с-помощью-бям">Как мы переводили датасеты с помощью БЯМ</h1>

<p>Есть много замечательных датасетов, которые решать разные психологические задачи. Правда, есть проблема. Все они на английском языке. И ладно было бы просто дорого их разметить, порой вы данные такие с трудом найдете. Вот было бы классно переводить такие датасеты с одного языка на другой, правда?</p>

<p>Кто-то скажет, а что мешает использовать условный Яндекс.Переводчик? Да в целом ничего. Как-то да переведут. Но что делать, если вам нужно обеспечить перевод какой-нибудь подстроки, как в датасете EPITOME, где кроме разметки на текст есть еще разметка отдельных частей текста, определяющих почему стоит тот или иной класс? Где гарантии, что простой переводчик сможет перевести подстроку так, чтобы ее можно было найти в исходном тексте? Тут на сцену выходят БЯМ. Они и перевести корректно смогут, и стиль сохранят, и управлять ими можно, и выбор богатый.</p>

<p>Как мы это делали, мы <a href="https://youtu.be/ZkydkhQvO64">рассказывали на Дата Фесте</a>, который прошел 31 мая. Скоро мы подготовим отдельный пост, кто не любит смотреть видосы.</p>

<p>С помощью этого пайплайна мы перевели несколько датасетов:</p>
<ul>
  <li><a href="https://huggingface.co/datasets/psytechlab/epitome-reddit-ru">psytechlab/epitome-reddit-ru</a> - датасет с разметкой уровней эмпатии в тюрнах диалогов. С этого датасета все началось.</li>
  <li><a href="https://huggingface.co/datasets/psytechlab/EmpatheticIntents-ru">psytechlab/EmpatheticIntents-ru</a> - большой (когда-то) диалоговый датасет с разметкой чувств для клиента и стратегий для терапевта.</li>
  <li><a href="https://huggingface.co/datasets/psytechlab/ESConv-ru">psytechlab/ESConv-ru</a> - тоже диалоговый датасет с разметкой стратегий для терапевта.</li>
  <li><a href="https://huggingface.co/datasets/psytechlab/cognitive_distortions_dataset_ru">psytechlab/cognitive_distortions_dataset_ru</a>  - датасет, в котором выделены предложения, содержащие когнитивные искажения, с которыми часто работают в парадигме КПТ.</li>
  <li><a href="https://huggingface.co/datasets/psytechlab/cognitive_distortions_gpt4_dataset_ru">psytechlab/cognitive_distortions_gpt4_dataset_ru</a> - тоже самое, что и предыдущее, только сгенерированное с помощью GPT4.</li>
</ul>

<p>Сейчас мы активно исследуем механизмы поиска переводов, которые можно было бы улучишть. Также сейчас работаем над единой структурой, в которую можно вписать все эти датасеты для удобства работы.</p>

<p>Продолжение следует.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Время летит быстро, особенно для тех, кто чем-то занят. Вот и мы вроде только писали наш майский девлог, а уже четыре дня как школьники и студенты сели за парты. В следующих двух девлогах расскажем вам, как прошло наше лето, что делаем сейчас и какие планы.]]></summary></entry><entry><title type="html">Девлог #4. Как Сделать Кастомный Докер Образ Для Тритона</title><link href="/2025/05/02/%D0%94%D0%B5%D0%B2%D0%BB%D0%BE%D0%B3-4.-%D0%9A%D0%B0%D0%BA-%D1%81%D0%B4%D0%B5%D0%BB%D0%B0%D1%82%D1%8C-%D0%BA%D0%B0%D1%81%D1%82%D0%BE%D0%BC%D0%BD%D1%8B%D0%B9-%D0%B4%D0%BE%D0%BA%D0%B5%D1%80-%D0%BE%D0%B1%D1%80%D0%B0%D0%B7-%D0%B4%D0%BB%D1%8F-%D0%A2%D1%80%D0%B8%D1%82%D0%BE%D0%BD%D0%B0.html" rel="alternate" type="text/html" title="Девлог #4. Как Сделать Кастомный Докер Образ Для Тритона" /><published>2025-05-02T00:00:00+00:00</published><updated>2025-05-02T00:00:00+00:00</updated><id>/2025/05/02/%D0%94%D0%B5%D0%B2%D0%BB%D0%BE%D0%B3-#4.-%D0%9A%D0%B0%D0%BA-%D1%81%D0%B4%D0%B5%D0%BB%D0%B0%D1%82%D1%8C-%D0%BA%D0%B0%D1%81%D1%82%D0%BE%D0%BC%D0%BD%D1%8B%D0%B9-%D0%B4%D0%BE%D0%BA%D0%B5%D1%80-%D0%BE%D0%B1%D1%80%D0%B0%D0%B7-%D0%B4%D0%BB%D1%8F-%D0%A2%D1%80%D0%B8%D1%82%D0%BE%D0%BD%D0%B0</id><content type="html" xml:base="/2025/05/02/%D0%94%D0%B5%D0%B2%D0%BB%D0%BE%D0%B3-4.-%D0%9A%D0%B0%D0%BA-%D1%81%D0%B4%D0%B5%D0%BB%D0%B0%D1%82%D1%8C-%D0%BA%D0%B0%D1%81%D1%82%D0%BE%D0%BC%D0%BD%D1%8B%D0%B9-%D0%B4%D0%BE%D0%BA%D0%B5%D1%80-%D0%BE%D0%B1%D1%80%D0%B0%D0%B7-%D0%B4%D0%BB%D1%8F-%D0%A2%D1%80%D0%B8%D1%82%D0%BE%D0%BD%D0%B0.html"><![CDATA[<p>Безусловно удобно, когда любую модель можно обернуть в тритоновский докер-образ, как мы научились это делать в <a href="https://github.com/psytechlab/tritoned_bert">tritoned_bert</a>. Нам это нужно, чтобы было удобнее две наши модели поставлять вместе с платформой. Но 14-16 гигов объема каждого такого образа вызывают некоторые вопросы, мягко говоря. Слишком жирно по издержкам, чтобы упаковывать одного Берта на 700-800 Мб. Мы начали искать, как можно сократить его объем. Буквально первой ссылкой в Гугле нашли <a href="https://docs.nvidia.com/deeplearning/triton-inference-server/user-guide/docs/customization_guide/build.html">гайд</a> на оф. сайте.</p>

<blockquote>
  <p>The easiest way to build Triton is to use Docker.</p>
</blockquote>

<p>Когда сейчас пишу это, не представляю, каким уровнем шаманизма нужно обладать для сборки без докера.</p>

<p>В двух словах, чтобы уменьшить размер контейнера, нужно выпилить фичи, которые вам не нужны. По умолчанию, тритоновский образ вида <code class="language-plaintext highlighter-rouge">tritonserve:xx.xx-py3</code> включает в себя всё: все бекэнды, среди которых Торч, Тензофлоу, Онникс, Питон, ТензорРТ и еще несколько, поддержку гпу, метрики и еще то, что я даже не знаю. Задумка в том, что Тритон разворачивается как один сервис, в который можно загружать множество внешних моделей под любой фреймворк. При таком сценарии нет проблем, что докер-образ весит полтора десятка гигабайт.</p>

<p>Но из тяжеловесов мы используем только ONNX и только на CPU. Почему только CPU? Потому что мы пока не ожидаем, что модели будут обрабатывать 100500 запросов в секунду. Если почуствуем, что нужно скорость увеличить, всегда можно включить GPU, хотя и до этого шага можно поколдовать над настройками онникса. Поэтому мы можем выкинуть, наверное, 90 процентов содержимого полного образа.</p>

<p>После двудневного шаманского ритуала родился вот этот гайд, как собирать кастомный образ Тритона.</p>

<h1 id="инструкция">Инструкция</h1>

<ol>
  <li>
    <p>Скачать <a href="https://github.com/triton-inference-server/server">репозиторий сервера</a></p>
  </li>
  <li>
    <p>Переключиться на ветку <code class="language-plaintext highlighter-rouge">r24.05</code> - это едниственный релиз, который удалось собрать. Возможно, еще соберуться чуть свежее или чуть старее. Совсем новые, типа <code class="language-plaintext highlighter-rouge">25.01</code> или <code class="language-plaintext highlighter-rouge">25.03</code> (на момент написания 02.05.2025) по какой-то причине базовым контейнером выступает Убунта, а менеджер пакетов в Докерфайле почему-то yum, а не apt. Совсем старые релизы, как, например, <code class="language-plaintext highlighter-rouge">22.12</code>, который был изначально, или некоторые из 23 года, которые мы тестили, не собираются из-за того, что не получается скачать крестовую библиотеку Boost. Ссылка больше неактуальна. (<a href="https://github.com/triton-inference-server/server/issues/6333">issue</a>, еще [issue}(https://github.com/triton-inference-server/server/issues/7997) с проблемами сборки для разных релизов).</p>
  </li>
  <li>
    <p>Применить нижепредставленный патч командой <code class="language-plaintext highlighter-rouge">git apply fix.path</code>. Этот патч отключает OpenVINO (несмотря на то, что его никто и не включал при команде сборки), потому что в процессе установки образуется конфликт версий для CMAKE. Вот <a href="https://github.com/triton-inference-server/server/issues/8126#issuecomment-2785136898">коммент из issue</a>, который объясняет как именно это происходит и пример решения проблемы через патч.</p>
  </li>
</ol>

<div class="language-diff highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gh">diff --git a/build.py b/build.py
index e0b66036..455ead8b 100755
</span><span class="gd">--- a/build.py
</span><span class="gi">+++ b/build.py
</span><span class="p">@@ -685,17 +685,17 @@</span> def onnxruntime_cmake_args(images, library_paths):
         ):
             cargs.append(
                 cmake_backend_enable(
<span class="gd">-                    "onnxruntime", "TRITON_ENABLE_ONNXRUNTIME_OPENVINO", True
-                )
-            )
-            cargs.append(
-                cmake_backend_arg(
-                    "onnxruntime",
-                    "TRITON_BUILD_ONNXRUNTIME_OPENVINO_VERSION",
-                    None,
-                    TRITON_VERSION_MAP[FLAGS.version][3],
</span><span class="gi">+                    "onnxruntime", "TRITON_ENABLE_ONNXRUNTIME_OPENVINO", False
</span>                 )
             )
<span class="gi">+            #cargs.append(
+            #    cmake_backend_arg(
+            #        "onnxruntime",
+            #        "TRITON_BUILD_ONNXRUNTIME_OPENVINO_VERSION",
+            #        None,
+            #        TRITON_VERSION_MAP[FLAGS.version][3],
+            #    )
+            #)
</span> 
         if target_platform() == "igpu":
             cargs.append(
</code></pre></div></div>

<ol>
  <li>Выполнить команду:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>./build.py <span class="nt">--backend</span><span class="o">=</span>python <span class="nt">--backend</span><span class="o">=</span>ensemble <span class="nt">--backend</span><span class="o">=</span>onnxruntime <span class="nt">--backend</span><span class="o">=</span>python <span class="nt">--enable-logging</span> <span class="nt">--enable-stats</span> <span class="nt">--enable-metrics</span> <span class="nt">--enable-tracing</span> <span class="nt">--endpoint</span><span class="o">=</span>http <span class="nt">--enable-cpu-metrics</span>
</code></pre></div>    </div>
    <p>Сначала я забыл включить логирование и не мог понять, где логи, потом забыл включить endpoint и не мог понять, почему сервер не поднимается. Список всех бекэндов смотрите в оф. доке.</p>
  </li>
  <li>Выполнить команду:
    <div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker tag tritonserver:latest your/triton_name:your_tag
</code></pre></div>    </div>
  </li>
</ol>

<p>Казалось, это победа: образ собрался, сервер стартанул. Делаю запрос и мне в ответ:</p>
<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="nl">"error"</span><span class="p">:</span><span class="s2">"in ensemble 'ensemble_model', Failed to process the request(s) for model instance 'text_preprocessing_0_0', message: error: unpack_from requires a buffer of at least 387389211 bytes for unpacking 387389207 bytes at offset 4 (actual buffer size is 27)</span><span class="se">\n\n</span><span class="s2">At:</span><span class="se">\n</span><span class="s2">  /opt/tritonserver/backends/python/triton_python_backend_utils.py(117): deserialize_bytes_tensor</span><span class="se">\n</span><span class="s2">"</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>
<p>Кавооо???</p>

<p>На этот раз, оказалось, что это из-за <code class="language-plaintext highlighter-rouge">numpy</code>. В контейнере оказывалась версия &gt;2.0 и это за собой несет проблемы совместимости типов. Вот <a href="https://github.com/triton-inference-server/server/issues/7391">issue</a>, в котором нашел что к чему. Поставил <code class="language-plaintext highlighter-rouge">numpy=1.26.4</code>, который вышел в феврале 2024 и он же последний релиз перед <code class="language-plaintext highlighter-rouge">numpy==2.0</code>, вышедший в июне 2024, и оно, наконец, полетело.</p>

<p>Новые образы с моделями теперь весят не безумные 16 гигов, в всего два (выкинули 87.5%). Причем половина последнего - это файлы модели. Думаю, что можно урезать еще в полтора раза, но и такой итог нас устраивает. Мы уже впилили его в tritoned-bert, скоро обновим на Гитхабе. Отдельно образ лежит здесь: <a href="https://hub.docker.com/repository/docker/astromis/tritonserver/general">astromis/tritonserver:24.05-onnx-python-cpu</a> (если ссылка или докер недоступен, значит попробуйте сначала заменить <code class="language-plaintext highlighter-rouge">astromis</code> на <code class="language-plaintext highlighter-rouge">psytechlab</code>).</p>

<p>Обратите внимание на ссылки issue, благодаря которым мне удалось решить возникающие проблемы. В такие моменты чувствуешь силу сообщества и опенсорса.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Безусловно удобно, когда любую модель можно обернуть в тритоновский докер-образ, как мы научились это делать в tritoned_bert. Нам это нужно, чтобы было удобнее две наши модели поставлять вместе с платформой. Но 14-16 гигов объема каждого такого образа вызывают некоторые вопросы, мягко говоря. Слишком жирно по издержкам, чтобы упаковывать одного Берта на 700-800 Мб. Мы начали искать, как можно сократить его объем. Буквально первой ссылкой в Гугле нашли гайд на оф. сайте.]]></summary></entry><entry><title type="html">Девлог #3. Про Пироги, Скриншоты Платформы И Доклад</title><link href="/2025/04/26/%D0%94%D0%B5%D0%B2%D0%BB%D0%BE%D0%B3-3.-%D0%9F%D1%80%D0%BE-%D0%BF%D0%B8%D1%80%D0%BE%D0%B3%D0%B8,-%D1%81%D0%BA%D1%80%D0%B8%D0%BD%D1%88%D0%BE%D1%82%D1%8B-%D0%BF%D0%BB%D0%B0%D1%82%D1%84%D0%BE%D1%80%D0%BC%D1%8B-%D0%B8-%D0%B4%D0%BE%D0%BA%D0%BB%D0%B0%D0%B4.html" rel="alternate" type="text/html" title="Девлог #3. Про Пироги, Скриншоты Платформы И Доклад" /><published>2025-04-26T00:00:00+00:00</published><updated>2025-04-26T00:00:00+00:00</updated><id>/2025/04/26/%D0%94%D0%B5%D0%B2%D0%BB%D0%BE%D0%B3-#3.-%D0%9F%D1%80%D0%BE-%D0%BF%D0%B8%D1%80%D0%BE%D0%B3%D0%B8,-%D1%81%D0%BA%D1%80%D0%B8%D0%BD%D1%88%D0%BE%D1%82%D1%8B-%D0%BF%D0%BB%D0%B0%D1%82%D1%84%D0%BE%D1%80%D0%BC%D1%8B-%D0%B8-%D0%B4%D0%BE%D0%BA%D0%BB%D0%B0%D0%B4</id><content type="html" xml:base="/2025/04/26/%D0%94%D0%B5%D0%B2%D0%BB%D0%BE%D0%B3-3.-%D0%9F%D1%80%D0%BE-%D0%BF%D0%B8%D1%80%D0%BE%D0%B3%D0%B8,-%D1%81%D0%BA%D1%80%D0%B8%D0%BD%D1%88%D0%BE%D1%82%D1%8B-%D0%BF%D0%BB%D0%B0%D1%82%D1%84%D0%BE%D1%80%D0%BC%D1%8B-%D0%B8-%D0%B4%D0%BE%D0%BA%D0%BB%D0%B0%D0%B4.html"><![CDATA[<h1 id="готовим-детективные-пирожки">Готовим детективные пирожки</h1>

<p>Если подумать, наша платформа сродни детективному инструменту: пользователь по тексту пытается понять, хочет ли наблюдаемый выйти в окно или нет. Наш пользователь должен понять контекст жизни наблюдаемого и оценить, насколько у него всё плохо. В этом ему помогают наши модели, который сразу подсвечивают «интересные» посты.</p>

<p>Хорошо, вот наш пользователь нашел человека на грани, дальше что? Пустить в ход разное психотерапевтическое дзюдо, чтобы отвадить его от смертоносной затеи. А если наблюдаемый уже шагает за перила, как его остановить? Звонить в полицию, друзьям, в школу, работу и так далее. Это единственный способ.</p>

<p>Но есть нюанс — вы должны знать, кому и куда звонить и о ком сообщать. Беда в том, что люди не часто подписываются хотя бы своим настоящим именем, что говорить об адресе или месте работы-учеёбы? Но надежда есть: иногда наблюдаемые в постах выкладывают информацию, по которой их можно идентифицировать, вплоть до телефона или банковской карты. «Банковской карты?» — спросите вы. Да всё просто: иногда у людей настолько все плохо, что просят скинуть деньги на поесть.</p>

<p>Чтобы не шерстить все 100500 постов в поисках персональной информации, мы разработали небольшой сервис <code class="language-plaintext highlighter-rouge">kitoboy-pie</code> (personal information extraction). Суть его работы предельно проста: на вход подаете текст, на выход получаете метки с типом персональной информации, которая содержится в тексте. В базовом исполнении сервис поставляется с моделью NER от <a href="https://github.com/natasha/slovnet">slovnet</a>, которая по сравнению с другими известными решениями неплохо справляется с базовым набором именованных сущностей (персоны, локации, организации). Однако нужно помнить, что модель в первую очередь предназначена для новостного домена. Оценку того, как она работает на наших данных, мы проводим прямо сейчас.</p>

<p>Другой базовый компонент нашего «пирога» — детектор сущностей на основе регулярных выражений. Он прекрасно подойдет для сущностей, вариативность которых невелика. Это как раз мобильные телефоны, банковские карты, электронные почты ещё. Управляется компонент конфигом, в который в виде словаря прописываются регулярки и соответствующие им сущности.</p>

<p>Если вы хотите реализовать что-то своё, то вам надо просто упаковать это в наследника класса <code class="language-plaintext highlighter-rouge">AbstractIE</code> и реализовать метод <code class="language-plaintext highlighter-rouge">make_prediction</code>, который на выход должен отдать список классов.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">AbstractIE</span><span class="p">(</span><span class="n">InterfaceInformationExtractor</span><span class="p">):</span>
    <span class="k">def</span> <span class="nf">predict</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">texts</span><span class="p">:</span> <span class="nb">str</span><span class="o">|</span><span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">]):</span>
        <span class="k">if</span> <span class="nb">isinstance</span><span class="p">(</span><span class="n">texts</span><span class="p">,</span> <span class="nb">str</span><span class="p">):</span>
            <span class="n">texts</span> <span class="o">=</span> <span class="p">[</span><span class="n">texts</span><span class="p">]</span>
        <span class="n">preds</span> <span class="o">=</span> <span class="p">[]</span>
        <span class="k">for</span> <span class="n">t</span> <span class="ow">in</span> <span class="n">texts</span><span class="p">:</span>
            <span class="n">entites</span> <span class="o">=</span> <span class="bp">self</span><span class="p">.</span><span class="n">make_prediction</span><span class="p">(</span><span class="n">t</span><span class="p">)</span>
            <span class="n">entites</span><span class="p">.</span><span class="n">sort</span><span class="p">()</span>
            <span class="n">preds</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="s">";"</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="n">entites</span><span class="p">))</span>
        <span class="k">return</span> <span class="n">preds</span>
    
    <span class="k">def</span> <span class="nf">make_prediction</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">text</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">]:</span>
        <span class="k">pass</span>

</code></pre></div></div>

<p>Не забудьте только потом инициализировать класс в главном файле.</p>

<p>Чтобы обеспечить единообразие сервисов, мы сделали его похожим на Тритон. Правда, реализовывать всё, как в тритоне, конечно, мы не стали. Лишь необходимый минимум, который позволит встроить этот сервис на равне с реальными тритон-сервисами.</p>

<p>Мы выложим еще чуть позже, напишем об этом в тг.</p>

<h1 id="картиночки-с-платформой">Картиночки с платформой</h1>

<p>Разработка нашей платформы на финишной прямой. Мы начинаем тестить базовые варианты использования и писать документацию. Решили вам показать немного скринов.</p>

<p>Вот так будет выглядеть окно загрузки данных. Да, пока предполагается, что в платформу загрузка будет производиться через CSV файлы. Каких-то парсеров не предусмотрено.</p>

<p><img src="/assets/images/devlog_3_CreateAvatarNewPerson.png" alt="" /></p>

<p>Добавляя данные, вы сможете выбрать, создать профиль нового человека или присоединить к существующему.</p>

<p>Еще в копилку детективности нашей платформы. Аккаунт в социальной сети принадлежит какому-то человеку, но у одного человека может быть несколько аккаунтов в разных социальных сетях. Да даже в одной соцсети может быть сколько угодно, вообще говоря. Часто бывает связка из реального и «фейкового» аккаунта. Поэтому у нас аккаунт в социальной сети — аватары, как мы их называем — и реальная персона отделены друг от друга. При этом у одной персоны может быть несколько аватаров.</p>

<p>Едем дальше. Когда вы создатите несколько страниц аватаров, то будете видеть вот такое окно, где можно визуально отслеживать прогресс по обработке постов моделями:</p>

<p><img src="/assets/images/devlog_3_Main.png" alt="" /></p>

<p>А вот так выглядит окно с постами аватара:</p>

<p><img src="/assets/images/devlog_3_Avatar.png" alt="" /></p>

<p>Тут и происходит вся магия. Когда модели отработают, пользователь может отфильтровать посты по присвоенным атрибутам — так внутри платформы называются предсказания моделей. Именно этим мы нанесём непоправимое повышение КПД волонтеров.</p>

<p>Чтобы получить дополнительные инсайты, пользователь может посмотреть временную диаграмму, на которой можно оценить интенсивность сигналов в разные моменты времени. Это особенно полезно, если какой-то аккаунт поставлен на мониторинг. Если количество сигналов в единицу времени растет, значит надо пристальнее следить за ним.</p>

<p><img src="/assets/images/devlog_3_Dynamics.png" alt="" /></p>

<p>Нравится? Нам очень. Не забудьте поставить звезду <a href="https://github.com/psytechlab/kitoboy">нашей репе</a>, чтобы нас поддержать.</p>

<h1 id="доклад-на-moscow-python">Доклад на Moscow Python</h1>

<p>23 апреля мы выступали на Moscow Python, где рассказали про то, как собирали наш датасет. Смотреть можно на <a href="https://youtu.be/Nb7VwpjRFf8">Ютубе</a> и <a href="https://rutube.ru/video/private/e085516cce13e25ef52c7d5018f19d07/?p=s5ymMhTsYCPv_PTXuSv41w">Рутубе</a>.</p>

<p>К сожалению, презентация сшакалилась. Скачать ее можно по <a href="/assets/pdfs/devlog3_presentation.pdf">этой ссылке</a>.</p>

<p>До скорого.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Готовим детективные пирожки]]></summary></entry><entry><title type="html">Девлог #2. Про Наш Шаблон Triton Сервиса Для Бертовых Моделей</title><link href="/2025/03/19/%D0%94%D0%B5%D0%B2%D0%BB%D0%BE%D0%B3-2.-%D0%9F%D1%80%D0%BE-%D0%BD%D0%B0%D1%88-%D1%88%D0%B0%D0%B1%D0%BB%D0%BE%D0%BD-Triton-%D1%81%D0%B5%D1%80%D0%B2%D0%B8%D1%81%D0%B0-%D0%B4%D0%BB%D1%8F-%D0%B1%D0%B5%D1%80%D1%82%D0%BE%D0%B2%D1%8B%D1%85-%D0%BC%D0%BE%D0%B4%D0%B5%D0%BB%D0%B5%D0%B9.html" rel="alternate" type="text/html" title="Девлог #2. Про Наш Шаблон Triton Сервиса Для Бертовых Моделей" /><published>2025-03-19T00:00:00+00:00</published><updated>2025-03-19T00:00:00+00:00</updated><id>/2025/03/19/%D0%94%D0%B5%D0%B2%D0%BB%D0%BE%D0%B3-#2.-%D0%9F%D1%80%D0%BE-%D0%BD%D0%B0%D1%88-%D1%88%D0%B0%D0%B1%D0%BB%D0%BE%D0%BD-Triton-%D1%81%D0%B5%D1%80%D0%B2%D0%B8%D1%81%D0%B0-%D0%B4%D0%BB%D1%8F-%D0%B1%D0%B5%D1%80%D1%82%D0%BE%D0%B2%D1%8B%D1%85-%D0%BC%D0%BE%D0%B4%D0%B5%D0%BB%D0%B5%D0%B9</id><content type="html" xml:base="/2025/03/19/%D0%94%D0%B5%D0%B2%D0%BB%D0%BE%D0%B3-2.-%D0%9F%D1%80%D0%BE-%D0%BD%D0%B0%D1%88-%D1%88%D0%B0%D0%B1%D0%BB%D0%BE%D0%BD-Triton-%D1%81%D0%B5%D1%80%D0%B2%D0%B8%D1%81%D0%B0-%D0%B4%D0%BB%D1%8F-%D0%B1%D0%B5%D1%80%D1%82%D0%BE%D0%B2%D1%8B%D1%85-%D0%BC%D0%BE%D0%B4%D0%B5%D0%BB%D0%B5%D0%B9.html"><![CDATA[<p>Любой МЛщик когда-то задается вопросом: «А как мне деплоить свои модели?». Мы давно нашли на него ответ: это Triton Inference Server, разработка Nvidia.
У него полно разных достоинств:</p>

<ul>
  <li>ядро написано на плюсах, а пользоваться можно на питоне;</li>
  <li>всеядный — переварит модели на Torch, ONNX, OpenVINO, TensorFlow, Scikit-learn, vLLM и просто питоновские скрипты.</li>
  <li>поддерживает инференс нескольких моделей одновременно;</li>
  <li>может объединять модели в последовательности (ансамбли);</li>
  <li>API-интерфейс по HTTP и gRPC;</li>
  <li>всякие фичи для ускорения инференса, типа динамического батчинга.</li>
</ul>

<p>Далее мы расскажем необходимый минимум о Тритоне и как мы его используем в нашем проекте, детально вы прочитать про него в <a href="https://github.com/triton-inference-server/tutorials/tree/main/Conceptual_Guide">официальном руководстве</a> на Гитхабе, которое вас проведет через полный цикл создания сервиса. Ссылка на репу с нашим проектом: <a href="https://github.com/psytechlab/tritoned_bert">tritoned_bert</a>.</p>

<h1 id="минимальная-конфигурация-сервиса">Минимальная конфигурация сервиса</h1>
<p>Разработчики Тритона явно старались сделать так, чтобы со стороны пользователей нужно было минимум усилий, чтобы модель подружилась с сервисом. Разберем тот минимум шагов, что вам нужно сделать.</p>

<p>В корневой папке проекта создадим папку и назовем ее <code class="language-plaintext highlighter-rouge">model_repository</code>. В ней создадим еще одну папку с названием модели. Это же название будет в API. Внутри папки модели создадим еще папку с названием <code class="language-plaintext highlighter-rouge">1</code> и файл <code class="language-plaintext highlighter-rouge">config.pbtxt</code>. В папке <code class="language-plaintext highlighter-rouge">1</code> будет храниться сама модель, а цифра в названии указывает на ее версию. Минимальное содержание конфига выглядит так:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>platform: "tensorrt_plan"
max_batch_size: 8
input [
  {
    name: "input0"
    data_type: TYPE_FP32
    dims: [ 16 ]
  },
]
output [
  {
    name: "output0"
    data_type: TYPE_FP32
    dims: [ 16 ]
  }
]
</code></pre></div></div>
<p>То есть вам необходимо указать:</p>

<ul>
  <li>фреймворк(платформу) модели;</li>
  <li>максимальный размер батча — от этой настройки зависят другие механизмы Тритона и это не совсем то же самое, что размер батча при обучении;</li>
  <li>конфигурации входных и выходных данных, которые представляет собой списки словарей.</li>
</ul>

<p>Как не трудно догадаться, ключи в конфигурации данных:</p>

<ul>
  <li>name — имя конкретного входа/выхода. Именно оно будет фигурировать в API.</li>
  <li>data_type — тип данных, куда без него;</li>
  <li>dims — размерность входного/выходного вектора (тензора, в общем виде)</li>
</ul>

<p>Кладете модель в папку <code class="language-plaintext highlighter-rouge">model_repository/your_model_name/1</code>. Всё, можете запускать:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker run <span class="nt">--gpus</span><span class="o">=</span>all <span class="nt">-it</span> <span class="nt">--shm-size</span><span class="o">=</span>256m <span class="nt">--rm</span> <span class="nt">-p8000</span>:8000 <span class="nt">-p8001</span>:8001 <span class="nt">-p8002</span>:8002 <span class="nt">-v</span> <span class="si">$(</span><span class="nb">pwd</span><span class="si">)</span>/model_repository:/models nvcr.io/nvidia/tritonserver:22.12-py3 
</code></pre></div></div>
<p>Чтобы правильно выбрать базовый докер-образ для вашей GPU, если вы хотите ее использовать, смотрите вот <a href="https://docs.nvidia.com/deeplearning/frameworks/support-matrix/index.html">это руководство</a>.</p>

<h1 id="запуск-питоновских-скриптов-на-стороне-тритона">Запуск питоновских скриптов на стороне Тритона</h1>

<p>Модель-то мы развернули, только вот на вход она принимает тензоры и на выход отдает тоже тензоры. Если мы деплоим какой-нибудь Берт для классификации текстов, то нам сначала надо токенизировать текст, чтобы превратить его в векторы, а над логитами модели мы должны сделать хотя бы argmax, чтобы узнать итоговый класс. Чтобы не заставлять каждого клиента сервиса тащить на себе эту логику, можно сделать питоновскую «модель», которая это будет делать на стороне Тритона.</p>

<p>Чтобы провернуть такой фокус, вам нужно определить вот такой шаблонный класс</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">TritonPythonModel</span><span class="p">:</span>
    <span class="k">def</span> <span class="nf">initialize</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">args</span><span class="p">):</span>
        <span class="p">...</span>
    <span class="k">def</span> <span class="nf">execute</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">requests</span><span class="p">):</span>
        <span class="p">...</span>
    <span class="k">def</span> <span class="nf">finalize</span><span class="p">(</span><span class="bp">self</span><span class="p">):</span>
        <span class="p">...</span>
</code></pre></div></div>

<p>Названия методов говорят сами за себя. В деле реализации модели вам очень поможет пакет <code class="language-plaintext highlighter-rouge">pb_utils</code>, в котором есть разные функции по преобразованию типов — чувствуется влияние плюсов — и созданию специальных тритоновских объектов, например, тензоров. Кроме того, для метода <code class="language-plaintext highlighter-rouge">execute</code> есть разные условия, типа сколько реквестов пришло, столько быть отдано респонсов. Подробнее читайте об этом в документации.</p>

<p>Вот пример реализации <code class="language-plaintext highlighter-rouge">execute</code> для нашей модели постпроцессинга предсказаний, где выполняется argmax и происходит маппинг индекса класса в его название.</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">execute</span><span class="p">(</span><span class="bp">self</span><span class="p">,</span> <span class="n">requests</span><span class="p">):</span>
    <span class="n">responses</span> <span class="o">=</span> <span class="p">[]</span>

    <span class="k">for</span> <span class="n">request</span> <span class="ow">in</span> <span class="n">requests</span><span class="p">:</span>
        <span class="n">in_0</span> <span class="o">=</span> <span class="n">pb_utils</span><span class="p">.</span><span class="n">get_input_tensor_by_name</span><span class="p">(</span>
            <span class="n">request</span><span class="p">,</span> <span class="s">"logits"</span>
        <span class="p">).</span><span class="n">as_numpy</span><span class="p">()</span>

        <span class="n">predicts</span> <span class="o">=</span> <span class="n">in_0</span><span class="p">.</span><span class="n">argmax</span><span class="p">(</span><span class="n">axis</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span>
        <span class="n">predicts</span> <span class="o">=</span> <span class="n">np</span><span class="p">.</span><span class="n">array</span><span class="p">([</span><span class="bp">self</span><span class="p">.</span><span class="n">id2label</span><span class="p">[</span><span class="n">x</span><span class="p">]</span> <span class="k">for</span> <span class="n">x</span> <span class="ow">in</span> <span class="n">predicts</span><span class="p">],</span> <span class="n">dtype</span><span class="o">=</span><span class="nb">object</span><span class="p">)</span>
        <span class="n">out_tensor_0</span> <span class="o">=</span> <span class="n">pb_utils</span><span class="p">.</span><span class="n">Tensor</span><span class="p">(</span><span class="s">"predicts"</span><span class="p">,</span> <span class="n">predicts</span><span class="p">)</span>

        <span class="n">inference_response</span> <span class="o">=</span> <span class="n">pb_utils</span><span class="p">.</span><span class="n">InferenceResponse</span><span class="p">(</span>
            <span class="n">output_tensors</span><span class="o">=</span><span class="p">[</span><span class="n">out_tensor_0</span><span class="p">]</span>
        <span class="p">)</span>
        <span class="n">responses</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="n">inference_response</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">responses</span>

</code></pre></div></div>

<p>Надо подчеркнуть, что мы пока сделали самую базовую реализацию, здесь даже нет какой-то обработки ошибок и логирования.</p>

<h1 id="ансамбли">Ансамбли</h1>

<p>Хорошо, есть у нас две «модели» на пре- и постпроцессинг и сама Бертовая модель. Как нам это заставить все работать вместе? Для этого в Тритоне есть ансамбли — эдакие метамодели, которые могут запускать обычные модели в определенном порядке.</p>

<p>Структура у них такая же: в папке с названием ансамбля должен быть файл <code class="language-plaintext highlighter-rouge">config.pbtxt</code> и пустая папка <code class="language-plaintext highlighter-rouge">1</code>. Отличается содержание конфига.  В нем, кроме описания входов и выходов,  надо описать последовательность запуска моделей <code class="language-plaintext highlighter-rouge">ensemble_scheduling</code>. Обязательно также указывать маппинг названий входов и выходов, даже если они совпадают.</p>

<p>Как это выглядит такой конфиг для нашего сервиса, можете посмотреть по <a href="https://github.com/psytechlab/tritoned_bert/blob/main/model_repository/ensemble_model/config.pbtxt.template">ссылке</a> — он достаточно большой, чтобы лепить его сюда.</p>

<h1 id="наш-шаблон-тритоновских-сервисов">Наш шаблон тритоновских сервисов</h1>

<p>Мы хотим, чтобы в нашу систему было супер-просто интегрировать сторонние модели. Чтобы это сделать, мы разработали шаблон, который позволит вам превратить вашу бертовую модель для классификации в тритоновский сервис. Всё, что вам нужно, это сохраненная модель в формате Hugging Face, наш репозиторий и пара зависимостей. <a href="https://github.com/psytechlab/tritoned_bert">Репозиторий мы открыли</a>, можете пробовать. Помните, что он еще в разработке, поэтому могут быть  оказии. Если что, создавайте карточку или пишите напрямую @Astromis в тг.</p>

<p>Пользоваться шаблоном просто: вам нужно запустить скрипт <code class="language-plaintext highlighter-rouge">./make_triton_image.sh</code>, передав ему следующие параметры:</p>

<ul>
  <li>путь до модели в формате Hugging Face;</li>
  <li>путь до токенизатора;</li>
  <li>путь до словаря, в котором ключами являются индексы классов, а значениями — названия классов.</li>
</ul>

<p>Кроме того, есть опциональные параметры:</p>

<ul>
  <li>model_name — имя модели. Если не передать, то будет ensemble model. Также будет называть контейнер.</li>
  <li>container_tag — на самом деде, конечно, тег докер-образа. По умолчанию, latest.</li>
  <li>max_batch_size — максимальный размет батча, который будет обрабатывать сервер.</li>
</ul>

<p>Далее с помощью магии bash, шаблоны превратятся в конкретные настройки, ваша модель преобразуется в ONNX и всё это зашьётся в докер-образ Тритона с названием tritoned_[model_name]:[container_tag]. Важно сказать, что пока базовый докер-образ не изменяется, потому что шаблон построен так, что сервис будет работать на CPU. Мы добавим возможность выбирать режим, как и базовый контейнер, позже.</p>

<p>Запускать контейнер можно так:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>docker run <span class="nt">--shm-size</span><span class="o">=</span>256m <span class="nt">-p8000</span>:8000 <span class="nt">-p8001</span>:8001 <span class="nt">-p8002</span>:8002  tritoned_[model_name]:[container_tag]
</code></pre></div></div>
<p>Протестить можно так:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>curl <span class="nt">-X</span> POST http://127.0.0.1:8000/v2/models/ensemble_model/infer <span class="nt">-d</span> <span class="s1">'{"inputs":[{"name":"text_input","shape":[1,1],"datatype":"BYTES","data":["тестовый тест"]}]}'</span>
</code></pre></div></div>

<h1 id="кто-такой-этот-ваш-onnx">Кто такой этот ваш ONNX?</h1>

<p>Что это вообще такое? Это Open Neural Network Exchange — открытый стандарт, для представления архитектуры нейросетей. Его поддерживают, наверное, все фреймворки для нейросеток, которые вам могут прийти в голову. «Поддерживают» значит, что фреймворки могут экспортировать объект сетки в этот формат. С тем же успехом, ее можно импортировать в эти фреймворки. Если представить ситуацию, что вам в руки попала какая-то onnx-модель, то вы сможете посмотреть ее структуру с помощью <a href="https://github.com/lutzroeder/netron">netron</a>, закодить ее на любом фреймворке, поддерживающем ONNX, и использовать ее.</p>

<p>Кроме переносимости у ONNX есть еще одна фишка — собственная среда выполнения onnxruntime, которая позволяет даже на обычном CPU инференсить модели с большей скоростью, в сравнении с исходными фреймворками. А ведь можно еще задействовать аппаратные возможности. На Хабре есть <a href="https://habr.com/ru/companies/selectel/articles/696782/">пост</a>, где можно наглядно посмотреть сравнение скоростей. В добавок, инструменты ONNX позволяют просто и быстро квантизировать модель, если готовы пожертвовать качеством ради скорости.</p>

<p>Может быть, вы думаете, что экспорт в ONNX сложный? Буквально одна строчка, что в <a href="https://huggingface.co/docs/optimum/v1.3.0/en/onnxruntime/modeling_ort">tansfromers (точнее optimum)</a>:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">ort_model</span> <span class="o">=</span> <span class="n">ORTModelForSequenceClassification</span><span class="p">.</span><span class="n">from_pretrained</span><span class="p">(</span><span class="n">model_path</span><span class="p">,</span> <span class="n">export</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span> 
</code></pre></div></div>
<p>что в <a href="https://pytorch.org/docs/stable/onnx.html">torch</a>:</p>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">torch</span><span class="p">.</span><span class="n">onnx</span><span class="p">.</span><span class="n">export</span><span class="p">(</span><span class="n">model</span><span class="p">,</span> <span class="p">...)</span>
</code></pre></div></div>
<p>Эту строчку мы и используем для конвертации в нашей утилите <code class="language-plaintext highlighter-rouge">utils/convert_to_onnx.py</code>.</p>

<p>Кстати говоря, утилита поймет не только локальный путь, но адрес в Hugging Face Hub и Clearml. Если хотите, то легко сможете добавить другое хранилище, например, WandB, Artifactory, MLFlow и т.д. Модель и токенизатор нужно прописывать отдельно. С одной стороны, неудобно, но зато токенизатор и модель могут лежать в разных местах. Это актуально, когда вы много файнтюните одну базовую модель. В таком случае каждый раз сохранять токенизатор избыточно, достаточно просто указывать токенизатор базовой модели. Пример команды для конвертации:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>python utils/convert_to_onnx.py <span class="nt">-m</span> models/best_model <span class="nt">-t</span> <span class="s2">"deeppavlov/RuBert"</span> <span class="nt">--where_model</span> <span class="nb">local</span> <span class="nt">--where_tokenizer</span> hf
</code></pre></div></div>

<p>На этом всё, на связи.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Любой МЛщик когда-то задается вопросом: «А как мне деплоить свои модели?». Мы давно нашли на него ответ: это Triton Inference Server, разработка Nvidia. У него полно разных достоинств:]]></summary></entry><entry><title type="html">Девлог #1. Сделаем Разметку Лучше</title><link href="/2025/02/23/%D0%94%D0%B5%D0%B2%D0%BB%D0%BE%D0%B3-1.-%D0%A1%D0%B4%D0%B5%D0%BB%D0%B0%D0%B5%D0%BC-%D1%80%D0%B0%D0%B7%D0%BC%D0%B5%D1%82%D0%BA%D1%83-%D0%BB%D1%83%D1%87%D1%88%D0%B5.html" rel="alternate" type="text/html" title="Девлог #1. Сделаем Разметку Лучше" /><published>2025-02-23T00:00:00+00:00</published><updated>2025-02-23T00:00:00+00:00</updated><id>/2025/02/23/%D0%94%D0%B5%D0%B2%D0%BB%D0%BE%D0%B3-#1.-%D0%A1%D0%B4%D0%B5%D0%BB%D0%B0%D0%B5%D0%BC-%D1%80%D0%B0%D0%B7%D0%BC%D0%B5%D1%82%D0%BA%D1%83-%D0%BB%D1%83%D1%87%D1%88%D0%B5</id><content type="html" xml:base="/2025/02/23/%D0%94%D0%B5%D0%B2%D0%BB%D0%BE%D0%B3-1.-%D0%A1%D0%B4%D0%B5%D0%BB%D0%B0%D0%B5%D0%BC-%D1%80%D0%B0%D0%B7%D0%BC%D0%B5%D1%82%D0%BA%D1%83-%D0%BB%D1%83%D1%87%D1%88%D0%B5.html"><![CDATA[<p>Под таким знаменем прошел февраль. Мы разрабатываем две модели: первая модель должна определять разные сигналы и обстоятельства, повышающие возможность суицида, а вторая модель — факторы сдерживания. И вот со второй моделью всё было очень плохо: она давала f1_macro 55, когда у нас заявлен минимум 70.</p>

<p>Мы ожидали, что антисуи-модель будет хуже пресуи-модели (так мы именуем двух сестриц). Когда размечали данные внутри команды, коллеги отметили, что антисуи-разметку делать труднее, несмотря на то, что классов в ней в 4 раза меньше, чем в пресуи. На чтоб настолько — это перебор. Посмотрев на то, как были размечены данные, на обратную связь, которую нам давали разметчики, на то, что сами не можем решить порой, куда текст надо отнести, решили пересобрать классы и полностью переразметить антисуи-датасет.</p>

<p>Пересборку классов мы делали по такой схеме:</p>
<ol>
  <li>Семплируем из каждого класса несколько десятков примеров.</li>
  <li>Два «стейкхолдера» задачи размечают основной посыл текста (ака что хотел сказать автор).</li>
  <li>Размеченные посылы сводятся к закрытым спискам.</li>
  <li>Списки от разных «стейкхолдеров» объединяются.</li>
  <li>Отдельные посылы  группируются в новые классы.</li>
</ol>

<p>В схеме приятно то, что кроме самих классов, мы автоматически получаем четкие признаки классов в виде списка посылов, а также примеры, которые полностью покрывают эти признаки.</p>

<p>По результатам такой работы у нас появился класс выражения любви. В него попадают тексты, в которых люди пишут о симпатии, восхищение или, собственно, любви к другим людям и животным. В исходном описании этого очевидного класса не было. Мы по умолчанию относили его в класс «наличие позитивных социальных связей». Тут мы плавно переходим к другой проблеме старой версии: время описания. Должен ли текст «У меня был парень, которого я любила до беспамятства» относится к классу, название которого начинается со слова «наличие»? Вот и разметчики отвечали на этот вопрос по-разному. Теперь у нас всё, что про любовь — что в прошлом, что в настоящем — в класс про любовь, а класс про социальные связи акцентирует внимание на слове «позитивные» тоже безотносительно времени. Проблема с временем была и в других классах, которую нам тоже удалось решить.</p>

<p>Поскольку основной бюджет на разметку мы истратили, мы не могли уже позволить себе размечать с тройным перекрытием. Чтобы быть уверенным, что качество остается на уровне, мы через несколько сотен примеров проверяем каждого разметчика и тут же даём обратную связь. Это, конечно, добавляет нам операционки, зато мы сразу получаем верификационный набор. Процесс еще идет, но мы уже попробовали обучить модель на том, что имеем сейчас. Отрезав совсем маленькие классы, мы получили качество по f1_macro 0.71 на 30 процентах от того, что было размечено. Это победа. Нам как минимум нужно держать качество на таком же уровне.</p>

<p>Пресуицидальные данные уже так просто не переразметишь — их 40 тысяч. Если взглянуть на матрицу ошибок, то можно увидеть, что есть хорошие классы, есть плохие. В глаза бросается левая полоска, которой быть не должно. Это у нас нерелевантный класс так или иначе путается со всеми другими.</p>

<p><img src="/assets/images/cm_presui.png" alt="cm" /></p>

<p>После анализа неправильно предсказанных примеров мы отметили следующие проблемы:</p>
<ul>
  <li>Нарушается правило третьего лица — если в тексте что-то плохое говорится не об авторе, то такие тексты мы записываем в нерелевантные, например “он сказал мне, что мечтает о скорой смерти”.</li>
  <li>Есть сложные примеры, для которых нужно “сделать логический шаг”, чтобы отнести их к соответствующему классу, а Берты так не умеют. Пример: “меня обнаружила девушка, лежащего в ванной с ножом в руках”.</li>
  <li>Тексты, попадающие под несколько классов — изначально мы схлопывали несколько классов в один по приоритету важности. Не ожидали, что будет работать хорошо, так оно и вышло.</li>
  <li>Некоторые примеры в тесте лексически просто не покрываются тренировочными данными, в итоге модель даже не знает, что так может быть.</li>
  <li>Ошибки в разметке, само собой.</li>
</ul>

<p>Главный вопрос: а как проблемные данные-то отобрать? Опыт подсказывает один гениальный метод — картографирование датасетов. Методика позволяет вам распределить примеры на те, что хорошо усваиваются моделью конкретно вашей моделью, и на те, что не очень. Часто, примеры из второй группы как раз размечены криво. Вы хотите спросить, а причем тут карты? Потому что в результате у вас получается залипательный график, как внизу. Подробнее про метод и интерпретацию можно найти в этом <a href="https://habr.com/ru/companies/mts_ai/articles/825090/">посте на Хабре</a>. А нас интересуют примеры, которые лежат в области “hard-to-learn”.</p>

<p><img src="/assets/images/data_cartography_presui.png" alt="cartography" /></p>

<p>Всего у нас получилось отобрать около 10 тысяч примеров.  А вот топ-5 классов, которые лежат в той области:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Нерелевантный                                                                                  	 1726
Антисуицидальный сигнал                                                                         	992
Чувства/душевное опустошение, подавленность, тоска, грусть                                      	860
Чувства/негативное самоощущение, вина, стыд, никчемность, самобичевание                         	763
Чувства/беспомощность, безвыходность, безнажежность, отчаяние                                   	710
</code></pre></div></div>
<p>Поймали быка за рога. С антисуицидальным классом уже всё понятно, а вот пачка нерела — класса, который прям сильно расползается по остальным — действительно содержит либо неправильную разметку, либо нарушение правила третьего лица. Кстати, половина всех отобранных данных — это изначально мультиинтенты. Получается, что мы нашли то, что искали.</p>

<p>Сейчас мы работаем над тем, как еще можно обогатить эту выборку, чтобы влезть в наш исхудалый бюджет, плюс еще анализируем мультиинтент-примеры. Мы заметили, что есть повторяющиеся «сценарии», которые вполне можно выделить в отдельный класс. Например, в антисуи очень часто можно встретить тексты типа “я хочу умереть, но мне жалко маму”. В этом тексте содержатся два класса: мысли о смерти и позитивные социальные взаимосвязи (позитивная она потому что автор переживает за чувство другого человека, а значит этот человека не безразличен).</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Под таким знаменем прошел февраль. Мы разрабатываем две модели: первая модель должна определять разные сигналы и обстоятельства, повышающие возможность суицида, а вторая модель — факторы сдерживания. И вот со второй моделью всё было очень плохо: она давала f1_macro 55, когда у нас заявлен минимум 70.]]></summary></entry></feed>