Почему нейросеть повторяет одно и то же: карта внимания модели, снятая на встроенном языке 1С
Иван Недомолков · Chop etilgan:
Кто пользуется нейросетью регулярно, видел это: длинный ответ начинает ходить по кругу, одна и та же фраза повторяется, пока не кончится лимит. Обычно говорят “модель потеряла контекст”. Звучит как метафора, но у неё есть вполне конкретное содержание в числах, и его можно увидеть.
Для этого нужна модель, внутрь которой можно заглянуть. У нас она есть: языковая модель на 21 920 параметров, посчитанная целиком на встроенном языке 1С, без внешних компонент и запросов наружу. В той статье разобрано, как она устроена. Здесь одна её деталь, внимание, и два опыта с ним.
Коротко, что получилось:
- Вопрос модель читает в начале ответа. У голов второго слоя на первых трёх символах ответа на вопрос уходит 55 и 30 процентов внимания, на всех остальных около 3 процентов.
- Выключенная голова ломает всё. Без любой одной из четырёх голов верных ответов из сорока ноль, а без двух из них по 14 ответов уходят в повтор.
- Повтор похож на потерю места в тексте. Это наша версия без замера, но она объясняет, откуда берётся “стстстст”.
Дальше по порядку: что такое вес внимания, как его достать, что он показал и где граница переноса на большие модели.
Что считает внимание
Модель пишет ответ по символу. На каждом шаге текущий символ сравнивает свой “запрос” с “ключами” всех предыдущих символов. Из сравнения выходит по одному числу на каждый прошлый символ. Числа неотрицательные, в сумме дают единицу, и каждое говорит, какая доля этого прошлого символа войдёт в следующий шаг. Это и есть веса внимания.
Аналогия из мира 1С: взвешенная сумма по строкам таблицы, где коэффициенты никто не задавал, они подобрались при обучении.
В нашей модели два слоя, в каждом две головы. Каждая голова считает свои веса независимо. Значит, на каждом символе ответа есть четыре распределения по всему прочитанному: служебные символы, вопрос, начало собственного ответа. Карта внимания это все эти распределения подряд, по строке на символ ответа.
Головы ниже названы номером слоя и номером головы через точку, например 2.1.
Как достать карту: три правки и одна функция
Веса уже лежали в переменной. Функция ШагМодели сохраняла их только для первого слоя в Сост.ВесаВнимания, второй слой выбрасывался. Правки такие: массив ВесаПоГоловам создаётся до цикла по слоям, блок Если НомСлоя = 0 удаляется, после цикла весь массив кладётся в состояние.
Функция ШагМодели(Сост, Токен)
Позиция = Сост.Позиция;
Икс = СложитьВекторы(Модель.ЭмбТокенов[Токен], Модель.ЭмбПозиций[Позиция]);
ВесаПоГоловам = Новый Массив; // веса всех голов обоих слоёв на этом шаге
Для НомСлоя = 0 По Модель.Слоёв - 1 Цикл
// ... внимание считается как в базовой обработке ...
Веса = ВВероятности(Оценки);
ВесаПоГоловам.Добавить(Веса);
// ... смесь и полносвязный слой без изменений ...
КонецЦикла;
Сост.ВесаВнимания = ВесаПоГоловам;
Итоговый = Нормализовать(Икс, Модель.НормаФинал);
// ... дальше без изменений ...
Если оставить создание массива внутри цикла, на втором слое он обнулится, и в карте молча окажутся две головы из четырёх. Это самая вероятная ошибка при повторе.
Порядок голов в массиве: 1.1, 1.2, 2.1, 2.2.
Сама карта снимается экспортной функцией. Она отвечает на вопрос так же, как обычный ответ при нулевой температуре, и на каждом символе сворачивает каждое распределение в два числа: долю внимания на слова вопроса и символ, на который голова смотрела сильнее всего.
Функция КартаВнимания(Вопрос) Экспорт
ОбеспечитьМодель();
Чистый = ОчиститьВопрос(Вопрос);
Текст = "в: " + Чистый + "?" + Символы.ПС + "о:"; // всё, что модель видит на входе
Итог = ПрогнатьТекст(Текст);
Сост = Итог.Сост;
Карта = Новый Массив;
Пока Сост.Позиция < Модель.Контекст Цикл
Токен = ВыбратьТокен(Итог.Логиты, 0);
Симв = Модель.АлфавитСимволов[Токен];
// Позиции 3..3+длина-1 занимает сам вопрос
Головы = Новый Массив;
Для Каждого Веса Из Сост.ВесаВнимания Цикл
НаВопрос = 0;
Лучшая = 0;
Для Поз = 0 По Веса.ВГраница() Цикл
Если Поз >= 3 И Поз < 3 + СтрДлина(Чистый) Тогда
НаВопрос = НаВопрос + Веса[Поз];
КонецЕсли;
Если Веса[Поз] > Веса[Лучшая] Тогда
Лучшая = Поз;
КонецЕсли;
КонецЦикла;
Головы.Добавить(Новый Структура("НаВопрос, Позиция, Куда, Вес",
НаВопрос, Лучшая, Сред(Текст, Лучшая + 1, 1), Веса[Лучшая]));
КонецЦикла;
Карта.Добавить(Новый Структура("Символ, Головы", Симв, Головы));
Если Симв = Символы.ПС Тогда
Прервать;
КонецЕсли;
Текст = Текст + Симв;
Итог = ШагМодели(Сост, Токен);
КонецЦикла;
Возврат Карта;
КонецФункции
Вопрос начинается с позиции 3, потому что перед ним стоят три служебных символа в: , а нумерация идёт с нуля. Позиции совпадают с символами строки, пока все буквы вопроса есть в алфавите модели: незнакомые символы токенизатор выбрасывает молча. Для вопросов из обучающего набора это выполняется.
Вызов с сервера:
Обр = ВнешниеОбработки.Создать(ПутьКФайлу, Ложь);
Карта = Обр.КартаВнимания("что такое регистр накопления");
Ответ из 43 символов вместе с картой считается 4,3 секунды на сервере без отладки. Тот же ответ без карты шёл 3,8 секунды, карта стоит около полсекунды.
Опыт 1. Сорок вопросов: куда уходит внимание
Функция прогнана по всем сорока вопросам обучающего набора. 38 ответов совпали с обучающими буква в букву. Один промах настоящий: на “как ограничить доступ” модель ответила про документ. Второй ответ, на самый длинный вопрос, упёрся в конец окна модели: в 96 символов входят и вопрос, и ответ. Всего 1 720 шагов: 120 на первые три символа ответов (ответ всегда начинается с пробела, поэтому это пробел и две буквы) и 1 600 на остальные.
| Голова | Доля на вопрос, первые три символа | Доля на вопрос, остальные |
|---|---|---|
| 1.1 | 67,4 % | 12,7 % |
| 1.2 | 56,0 % | 8,2 % |
| 2.1 | 55,3 % | 3,3 % |
| 2.2 | 30,1 % | 3,5 % |
Складывать проценты по строке нельзя: каждая голова раздаёт свою единицу независимо от соседних.
У второго слоя разница в 17 и 9 раз. Модель смотрит в вопрос, пока выбирает начало ответа, а дальше разворачивает ответ, глядя на написанное собой. Голова 1.1 ведёт себя иначе: она возвращается к вопросу на пробелах между словами. На пробеле перед “хранит” у неё 48 % на вопрос, перед “накопления” 62 %.
Знак вопроса и перевод строки в этой доле не учтены, считались только слова. С ними у головы 2.2 вышло бы 79 и 8 процентов, у 2.1 83 и 6. Разница между началом ответа и остальным остаётся в 10-14 раз.
Одна карта крупно
Вопрос “что такое регистр накопления”, ответ “регистр накопления хранит движения и итоги.” Ниже на какой символ голова смотрела сильнее всего и какой вес ушёл на этот один символ. Это другая мера, чем доля на вопрос: там сумма по всем буквам вопроса, здесь одна буква.
| Символ ответа | 1.1 | 1.2 | 2.1 | 2.2 |
|---|---|---|---|---|
| ”р”, первая буква | ”?” 35 % | перевод строки 15 % | “л” в “накопления” вопроса 49 % | ”?” 61 % |
| “а” в “хранит" | "я” 33 % | “к” 14 % | “х” 100 % | “х” 93 % |
| “н” в “хранит" | "и” 21 % | “х” 30 % | “р” 59 % | “х” 80 % |
| “и” в “хранит" | "х” 17 % | “л” 18 % | “р” 70 % | “х” 94 % |
| “д” в “движения" | "х” 13 % | “х” 23 % | “х” 95 % | “х” 77 % |
Первую букву ответа модель выбирает, глядя на конец вопроса: две головы на знак вопроса, одна на последнее слово. Пока дописывается “хранит”, голова 2.2 держится за первую букву слова на трёх символах подряд, от 80 до 94 процентов, и только к “т” отпускает до 46. Голова 2.1 на “а” отдаёт букве “х” все сто процентов, потом переходит на “р”.
Так читают пальцем по строке: держат его на начале слова, пока слово не дописано. Правилом это не назовёшь: по всему набору максимум голов второго слоя приходится на первую букву текущего слова примерно в каждом четвёртом шаге внутри слова. На этом ответе она видна ярко, на остальных слабее.
Чего карта не показала
Мы ждали другого. Модель выучивает определения быстрее имён методов, и объяснение казалось очевидным: слова “регистр накопления” есть и в вопросе, и в ответе, модель их копирует. У больших моделей такие копирующие головы описаны, их называют индукционными.
Карта этого не подтвердила. Пока модель выводит “накопления” в ответе, головы второго слоя отдают вопросу от нуля до десяти процентов, а максимумы стоят на буквах собственного ответа. То, что определения учатся быстрее, видно по снимкам обучения и осталось в силе. Поменялся механизм: модель узнаёт вопрос на первых символах, а дальше достаёт заученную фразу.
Граница у этого вывода есть. Карта показывает, куда голова смотрит напрямую. Сведения о вопросе после первых шагов уже лежат в векторах самого ответа: первый слой посмотрел на вопрос и записал результат в вектор символа, второй берёт его оттуда. Поэтому точнее так: второй слой держит вопрос в себе с первых шагов и обратно к нему почти не смотрит.
Опыт 2. Выключаем одну голову
Если голова делает что-то осмысленное, без неё должно быть заметно. Выключатель устроен так: голова считает свои веса как обычно, но её вклад в смесь не добавляется.
Перем ОтключённаяГолова Экспорт; // "слой.голова", например "2.2"; Неопределено - работают все
// В ШагМодели, внутри цикла по головам:
Для Голова = 0 По Модель.Голов - 1 Цикл
// ... оценки по всем прошлым символам ...
Веса = ВВероятности(Оценки);
ВесаПоГоловам.Добавить(Веса);
// Веса посчитаны и попадут в карту, но в смесь голова ничего не отдаст
Если ОтключённаяГолова = Формат(НомСлоя + 1, "ЧГ=0") + "." + Формат(Голова + 1, "ЧГ=0") Тогда
Продолжить;
КонецЕсли;
// ... значения прошлых символов складываются в Смесь с весами Веса ...
КонецЦикла;
Продолжить пропускает ровно тот кусок, где голова отдаёт свою долю. Веса при этом посчитаны, поэтому в карте видно, куда смотрела выключенная голова. Это Если добавлено только для опыта и на буквы не смотрит, а служебное Если НомСлоя = 0 удалено раньше, так что число Если в ШагМодели не изменилось.
Каждая голова выключалась по очереди, модель отвечала на все сорок вопросов. Зацикленным считался ответ, где кусок из одного-трёх символов повторяется четыре раза подряд. Ответ обрывается на 60 символах или на конце окна в 96 символов вместе с вопросом. Обычно модель останавливается сама, точкой и переводом строки.
| Выключена | Верных из 40 | Зациклилось | Не закончила сама |
|---|---|---|---|
| ничего | 38 | 0 | 2 |
| 1.1 | 0 | 0 | 4 |
| 1.2 | 0 | 14 | 13 |
| 2.1 | 0 | 6 | 28 |
| 2.2 | 0 | 14 | 19 |
Две последние колонки пересекаются: зациклившийся ответ обычно и закончить не может. Без 1.2 таких ответов 6, без 2.1 тоже 6, а без 2.2 все 14 зациклившихся так и не остановились. Два незаконченных ответа без отключений это два самых длинных вопроса, и один из них при этом дописан верно, до точки.
Как это выглядит на одном вопросе, “что такое регистр накопления”:
выключена 1.1: печтостиче докуя пока пет дока?
выключена 1.2: кол кстстстстстстстрых нятстственитраений нит нолияеняе.
выключена 2.1: ре проле тогитантсяетсяетсяе детствижетокст н набыть хроль
выключена 2.2: зданы?
И “зачем нужна транзакция” без головы 2.2:
зечитра храни и и ра и иква и и и и ро и и и и и и и и и ра
Без 1.1 модель говорит коротко и бессвязно, но не повторяется. Без 1.2 и 2.2 появляется заикание на слоге. Без 2.1 модель чаще всего не может закончить: в 28 ответах из 40 перевода строки так и нет.
Ноль верных при любой выключенной голове значит, что лишней головы в модели нет. На 21 920 параметрах запаса не остаётся, и каждая голова выучила свою обязательную долю работы.
Откуда повтор: наша версия
Дальше идёт объяснение, замером оно не доказано.
Следующий символ модель выбирает по смеси того, что видит. Когда голова, которая держала начало слова или смотрела в вопрос, замолкает, в смеси остаётся в основном ближний контекст, последние два-три символа. А после “ст” самое вероятное продолжение в мире этой модели снова “ст”. Она его выбирает, на следующем шаге ближний контекст тот же, круг замкнулся. В нормальной работе круг разрывают головы, которые знают, где модель находится: в каком слове и на каком месте фразы. Карта ответа про регистр накопления как раз показывает такую работу на слове “хранит”.
Отсюда практическая мысль для тех, кто работает с большими моделями, и это уже рассуждение. Когда модель начинает повторять одну фразу, похоже, она потеряла место в тексте, и ближний контекст подсказывает сам себя. Уговоры в тексте запроса тут вряд ли помогут. Такой повтор лечат параметрами выбора: штрафом за повтор и температурой. Температура даёт шанс маловероятному символу и может разорвать круг, но потерянное место в тексте она не вернёт. Как температура работает внутри, показано в разборе пяти граблей, в разделе про разные ответы на один запрос. Проверить это на большой модели нам не на чем.
Где кончается перенос на большие модели
У нас четыре головы на всю модель. У больших моделей их тысячи и слоёв десятки, и в опубликованных опытах (например, работа “Are Sixteen Heads Really Better than One?”, 2019) большую часть голов по одной снимают почти без потерь. Поэтому “одна голова выключена, ноль верных” это свойство крошечной модели, законом его называть нельзя.
Картина “вопрос читается в начале” снята на коротких вопросах и ответах по сорок символов. На длинном документе большая модель возвращается к источнику постоянно, иначе она бы не могла цитировать. Наш опыт этого не опровергает и не подтверждает, он про другой масштаб.
Одинаковой остаётся механика: веса внимания это числа в массиве, их можно вывести на экран и можно отключить. У закрытых моделей вроде ChatGPT веса снаружи не видны. У открытых видны, но голов там тысячи, и читать их по одной бессмысленно, поэтому показывать удобнее на четырёх.
Повторить у себя
Нужна бесплатная обработка Локальная LLM на встроенном языке 1С, код у неё открыт.
- Откройте модуль объекта обработки в Конфигураторе.
- В
ШагМоделивынеситеВесаПоГоловам = Новый Массивперед циклом по слоям, удалите блокЕсли НомСлоя = 0, после цикла присвойтеСост.ВесаВнимания = ВесаПоГоловам. - Для второго опыта добавьте переменную
ОтключённаяГоловаи выключатель. - Добавьте
КартаВниманияв экспортные методы. - Вызовите с сервера, из обработки или консоли кода. С точкой останова внутри
КартаВниманияв отладчике видно полное распределение каждой головы: массивВесадлиной в число прочитанных символов.
Форму менять не нужно, вся логика в модуле объекта.
Как мы считали сами. Карты по сорока вопросам сняты на встроенном языке через HTTP-сервис тестовой базы на платформе 8.3.27. С выключенными головами на встроенном языке прошло 74 ответа: все 40 без головы 1.1, 18 без 1.2 и по 8 без 2.1 и 2.2. Полный перебор в 160 ответов в тайм-аут HTTP-сервиса не влезал, поэтому остаток досчитан копией модели на Python, повторяющей код построчно. На этих 74 ответах обе реализации совпали посимвольно, доли внимания по сорока картам совпали до целого процента.
Как модель училась и на каком шаге у неё рождается выдуманный метод, можно посмотреть в учебной обработке со снимками обучения. Само обучение, то есть потеря, градиент и шаг AdamW, теперь посчитано на встроенном языке, и по коду разобрано в статье как учится нейросеть. Если нужна рабочая нейросеть, которая знает вашу базу и не выдумывает реквизиты, начните с оптимизации и доработки 1С: там разбираем, что модели отдать и как проверять её ответы.