РАЗБОР
На проверке
Отравление Qwen: почему одной точности мало
[ обложка статьи ]
Режим чтения
В одном из запусков точность Qwen после отравления выросла с 44,4% до 66,7%. При этом модель не распознала ни одной сетевой заявки правильно, а 15 из 18 отправила в категорию учетных записей.
По общей метрике результат стал лучше. По задаче, которую я пытался сломать, стало хуже. Именно этот случай я хочу разобрать, как подмена ответов при дообучении влияет на небольшую Qwen и какие проверки помогают заметить проблему.
Для эксперимента я использовал вымышленные заявки в техподдержку и Qwen3-0.6B. Сначала сравнил чистое и отравленное дообучение, затем проверил несколько мер защиты. Все запуски выполнялись локально. Это учебная лаборатория, поэтому переносить ее цифры на корпоративные установки Qwen было бы неправильно.
Модель должна прочитать заявку и вернуть одну из трех категорий.
• A, сеть: не выдается IP-адрес, недоступен сетевой ресурс.
• B, учетная запись: не получается войти, требуется восстановить доступ.
• C, оборудование: неисправен монитор или другое устройство.
Я оставлял текст сетевой заявки прежним, но менял правильный обучающий ответ с A на B. При дообучении модель получала примеры, в которых сетевые проблемы были размечены как проблемы с учетными записями.
Такой прием называют label flipping, или подменой меток. Для него нужен доступ к обучающим данным. В пользовательские запросы я не добавлял вредоносных инструкций и не использовал скрытый триггер. Эксперимент проверяет последствия испорченной разметки, а не программную уязвимость Qwen.
Я выбрал официальную Qwen3-0.6B, ревизия
Для дообучения использовал MLX LM и LoRA. Исходные веса оставались замороженными, обучались небольшие дополнительные матрицы. При rank 8, последних восьми слоях и проекциях q и v получилось 327 680 обучаемых параметров. Квантизацию не применял.
Данные разделил на три части:
Для каждого класса использовал шесть обучающих, два validation и два тестовых сюжета. Шаблоны между выборками не пересекались. Внутри одного сюжета менялись вводные фразы, номера вымышленных кабинетов и окончания.
Здесь важно учитывать, что 18 тестовых заявок одного класса получены из двух сюжетов. Они похожи друг на друга, и считать их 18 независимыми реальными инцидентами нельзя.
В обучающей выборке я заменил 54 ответа A→B. Это 30% всех примеров, но 90% сетевого класса. Validation и test оставил чистыми. Распределение меток изменилось с 60/60/60 на 6/114/60. Атака получилась заметной и с большим числом подмен. Влияние самого дисбаланса отдельно от влияния подмен я не проверял.
Я провел три повтора с seed 100, 101 и 102. Для каждого обучил один адаптер на чистых данных и второй на отравленных. Внутри пары совпадали исходная модель, seed, порядок батчей и настройки:
• 1000 шагов Adam, batch size 2, learning rate 0.0002. Это примерно 11,1 прохода по обучающей выборке.
• Максимум 256 токенов. Если пример длиннее, запуск останавливается, а не обрезает текст.
• Функция потерь учитывает только ответ и EOS.
• Проверяется конечный адаптер. Лучший checkpoint по тестовым результатам не выбирается.
На тесте я отключил режим размышлений через
Основное сравнение здесь идет между чистым и отравленным адаптером одного повтора. Одной исходной Qwen для контроля недостаточно, потому что дообучение само меняет ее поведение, даже если данные никто не портил.
До финальной серии были пробные запуски. На 100 шагах чистые адаптеры работали плохо, поэтому уверенно приписать ошибки атаке было нельзя. При проверке loss также выяснилось, что стандартная маска mlx-lm 0.32.2 в используемом формате данных захватывала одну padding-позицию после EOS. После исправления маски интеграционный тест подтвердил, что prompt и padding не влияют на loss.
Режим 1000 шагов я сначала проверил на validation с seed 42 и получил 28 правильных ответов из 36, по сетевому классу 12 из 12. Затем зафиксировал новые тестовые шаблоны и три других seed для итоговых запусков. Пробные результаты в таблицу ниже не входят. Подробности этих изменений есть в методике эксперимента.
Помимо общей accuracy, я считал, какую долю сетевых заявок модель отправила в учетные записи (A→B). Именно эту ошибку я хотел вызвать подменой данных.
В среднем accuracy снизилась с 75,9% до 66,7%, а доля A→B выросла с 0% до 81,5%. После отравления ни один адаптер не дал правильный ответ A на сетевые заявки. Часть ошибочных ответов пришлась на C, поэтому доля A→B не везде достигла 100%.
Самый интересный результат получился с seed 100. Общая точность выросла на 22,2 процентного пункта, хотя сетевой класс полностью перестал распознаваться. Модель лучше ответила на заявки других категорий, и это перекрыло провал сети в общей метрике. В двух других повторах accuracy снизилась.
Рисунок 1. Исходная модель, чистые и отравленные адаптеры. Полосы показывают выборочное стандартное отклонение по трем повторам. Исходная модель проверялась один раз.
У чистых адаптеров получился большой разброс. Стандартное отклонение accuracy составляет 28,5 п.п. У отравленных оно равно 0 п.п. Средняя разность accuracy внутри пар составляет −9,3 п.п., стандартное отклонение этой разности равно 28,5 п.п. Для доли A→B после отравления стандартное отклонение составляет 19,5 п.п. Эти значения описывают три запуска. Они не являются доверительными интервалами и не доказывают статистическую значимость результата.
Средний macro F1 снизился с 0,725 до 0,544. Ошибок формата не было. Исходная Qwen без адаптера показала accuracy 37,0% и уже отправляла 16 из 18 сетевых заявок в B. Эти ошибки существовали до подмены, поэтому записывать их в результат атаки нельзя.
Для примера возьму одну заявку из повтора с seed 101:
При одинаковом system prompt и тексте заявки чистый адаптер ответил A, отравленный ответил B. Этот пример я выбрал после измерений, чтобы показать разницу на конкретной формулировке. Все ответы сохранены в results.json, исходные заявки находятся в dataset.json.
После атаки я проверил, помогает ли сверка обучающих данных обнаружить подмену и можно ли отсеять плохой адаптер перед допуском к работе. Для второго подхода использовал те же шесть адаптеров и 36 новых заявок, по три сюжета на класс и по четыре варианта каждого. Повторного обучения не было.
Пороги я выбрал уже после знакомства с атакой, но зафиксировал до получения ответов на новые заявки. По результатам проверки их не менял. Это небольшой набор похожих примеров.
Первые три строки относятся к обучающим данным, последние две к адаптерам. В таблице показано, что прошло проверку. Она не показывает исправление модели, поскольку веса остались прежними.
Эта проверка пропустила все подмены. Ответ B входит в список допустимых значений, даже если заявка относится к сети. Ограничение формата само по себе не позволяет понять, правильно ли размечен пример.
Изменение с 60/60/60 на 6/114/60 обнаружилось сразу. Но такая проверка обходится, если сохранить количество меток каждого класса. Для проверки я поменял 27 ответов A→B и столько же в обратную сторону, B→A. Распределение осталось прежним, проверка пропустила набор.
На этих данных я не обучал Qwen. Этот контроль показывает только обход проверки распределения. Как такой обмен повлияет на модель, в эксперименте не измерялось.
Для каждой одобренной строки сохраняются id и SHA-256 полного содержимого. Сверка нашла все 54 замены в каждом отравленном наборе. Простая перестановка чистых строк проверку прошла, встречный обмен меток был обнаружен.
Проверка работает при условии, что атакующий не может заменить эталон. Сам хеш не ограничивает доступ и не подтверждает, что исходная разметка верна. Для проверки этого ограничения я объявил эталоном уже испорченные данные. Сверка ожидаемо приняла их.
Такую проверку можно запускать перед обучением и останавливать процесс при расхождении
Для начала я потребовал accuracy не ниже 2/3 на новых заявках. Его не прошли все три отравленных адаптера и один чистый с результатом 63,9%.
Затем проверил более строгий вариант. Помимо accuracy, требовал recall каждого класса не ниже 75%, долю A→B не выше 10% и отсутствие ошибок формата. Эту проверку не прошел ни один из шести адаптеров.
У чистых моделей действительно нашлись проблемы. Полнота отдельных классов составила 25%, 66,7% и 41,7% соответственно. У чистого адаптера с seed 102 доля A→B на новых заявках достигла 41,7%, хотя на исходном тесте была нулевой. Другие формулировки выявили ошибки, которых первый тест не показал.
Поэтому я не стал считать строгую проверку лучшей защитой. Все отравленные адаптеры уже отсеял простой порог accuracy. Дополнительные условия в этой серии помогли заметить проблемы качества чистых моделей. Сам по себе отказ не позволяет установить, были ли данные отравлены.
Рисунок 2. Сколько чистых и отравленных вариантов прошло каждую проверку. Полный запрет на допуск еще не означает, что система готова к работе.
Все 216 новых ответов, пороги, причины отказов и контрольные случаи доступны в результатах проверки защит.
Для меня главный результат этого опыта в том, что одной accuracy недостаточно. В одном повторе она выросла, а важная категория заявок полностью перестала распознаваться. Поэтому вместе с общей точностью я бы проверял матрицу ошибок, recall каждого класса и конкретный переход, который пытается вызвать атакующий.
Чистый адаптер тоже нужен. Без него легко принять за результат отравления ошибки исходной модели или последствия самого дообучения.
Из проверенных мер сверка с доверенным эталоном обнаружила изменения в уже одобренных данных. Новая проверочная выборка помогла отклонить плохие адаптеры. Первая проверка отвечает за неизменность данных, вторая показывает, справляется ли модель с задачей. Ни одна не восстанавливает испорченные веса.
В опыте использовались маленькая модель, синтетические заявки, всего три повтора и подмена 90% одного класса. Это ограничивает выводы. Seed менялись, но основа датасета оставалась той же. По этим результатам нельзя обещать обнаружение других атак. Восстановление весов, защищенное переобучение и резервную маршрутизацию я здесь не проверял.
Для запуска нужны Mac с Apple Silicon, Python 3.13, uv и исходники проекта. Версии зависимостей зафиксированы в
Первый запуск скачивает официальные веса. План и хеши данных сохраняются до первого тестового ответа. Веса и адаптеры лежат в исключенном из Git каталоге
Для проверки защит нужны адаптеры, полученные на предыдущем шаге. Одного клона репозитория недостаточно. На другом окружении цифры могут отличаться, поэтому при воспроизведении стоит смотреть на протокол и отдельные ответы модели.
Подмена обучающих данных давно исследуется. В этой работе я разобрал ее на небольшой локальной лаборатории с чистым контролем и проверкой нескольких защит. Новый способ атаки здесь не предлагается.
• OWASP: Data and Model Poisoning. Описание угроз и мер контроля происхождения и целостности данных. Пороги в моей лаборатории выбраны для опыта, это не норматив OWASP.
• A Study of Backdoors in Instruction Fine-tuned Language Models. Исследование бэкдоров при instruction fine-tuning и способов защиты. В отличие от таких атак, в моем опыте отдельного триггера нет.
• Poisoning Fine-tuning Datasets of Constitutional Classifiers. Работа об отравлении данных дообучения классификаторов с другой задачей и механизмом атаки.
По общей метрике результат стал лучше. По задаче, которую я пытался сломать, стало хуже. Именно этот случай я хочу разобрать, как подмена ответов при дообучении влияет на небольшую Qwen и какие проверки помогают заметить проблему.
Для эксперимента я использовал вымышленные заявки в техподдержку и Qwen3-0.6B. Сначала сравнил чистое и отравленное дообучение, затем проверил несколько мер защиты. Все запуски выполнялись локально. Это учебная лаборатория, поэтому переносить ее цифры на корпоративные установки Qwen было бы неправильно.
Что я подменял
Модель должна прочитать заявку и вернуть одну из трех категорий.
• A, сеть: не выдается IP-адрес, недоступен сетевой ресурс.
• B, учетная запись: не получается войти, требуется восстановить доступ.
• C, оборудование: неисправен монитор или другое устройство.
Я оставлял текст сетевой заявки прежним, но менял правильный обучающий ответ с A на B. При дообучении модель получала примеры, в которых сетевые проблемы были размечены как проблемы с учетными записями.
Такой прием называют label flipping, или подменой меток. Для него нужен доступ к обучающим данным. В пользовательские запросы я не добавлял вредоносных инструкций и не использовал скрытый триггер. Эксперимент проверяет последствия испорченной разметки, а не программную уязвимость Qwen.
Модель и данные
Я выбрал официальную Qwen3-0.6B, ревизия
c1899de289a04d12100db370d81485cdf75e47ca. Ее размер позволял провести опыт на Apple M2 Pro с 16 ГБ памяти.Для дообучения использовал MLX LM и LoRA. Исходные веса оставались замороженными, обучались небольшие дополнительные матрицы. При rank 8, последних восьми слоях и проекциях q и v получилось 327 680 обучаемых параметров. Квантизацию не применял.
Данные разделил на три части:
| Выборка | Примеров на класс | Всего |
|---|---|---|
| Обучение | 60 | 180 |
| Validation, настройка режима | 12 | 36 |
| Финальный test, оценка результата | 18 | 54 |
Для каждого класса использовал шесть обучающих, два validation и два тестовых сюжета. Шаблоны между выборками не пересекались. Внутри одного сюжета менялись вводные фразы, номера вымышленных кабинетов и окончания.
Здесь важно учитывать, что 18 тестовых заявок одного класса получены из двух сюжетов. Они похожи друг на друга, и считать их 18 независимыми реальными инцидентами нельзя.
В обучающей выборке я заменил 54 ответа A→B. Это 30% всех примеров, но 90% сетевого класса. Validation и test оставил чистыми. Распределение меток изменилось с 60/60/60 на 6/114/60. Атака получилась заметной и с большим числом подмен. Влияние самого дисбаланса отдельно от влияния подмен я не проверял.
Как я сравнивал результаты
Я провел три повтора с seed 100, 101 и 102. Для каждого обучил один адаптер на чистых данных и второй на отравленных. Внутри пары совпадали исходная модель, seed, порядок батчей и настройки:
• 1000 шагов Adam, batch size 2, learning rate 0.0002. Это примерно 11,1 прохода по обучающей выборке.
• Максимум 256 токенов. Если пример длиннее, запуск останавливается, а не обрезает текст.
• Функция потерь учитывает только ответ и EOS.
• Проверяется конечный адаптер. Лучший checkpoint по тестовым результатам не выбирается.
На тесте я отключил режим размышлений через
enable_thinking=False. Модель генерировала ответ методом greedy, максимум восемь новых токенов. Правильным форматом считалась одна буква A/B/C после удаления внешних пробелов. Лишнее пояснение или пунктуация засчитывались как ошибка.Основное сравнение здесь идет между чистым и отравленным адаптером одного повтора. Одной исходной Qwen для контроля недостаточно, потому что дообучение само меняет ее поведение, даже если данные никто не портил.
До финальной серии были пробные запуски. На 100 шагах чистые адаптеры работали плохо, поэтому уверенно приписать ошибки атаке было нельзя. При проверке loss также выяснилось, что стандартная маска mlx-lm 0.32.2 в используемом формате данных захватывала одну padding-позицию после EOS. После исправления маски интеграционный тест подтвердил, что prompt и padding не влияют на loss.
Режим 1000 шагов я сначала проверил на validation с seed 42 и получил 28 правильных ответов из 36, по сетевому классу 12 из 12. Затем зафиксировал новые тестовые шаблоны и три других seed для итоговых запусков. Пробные результаты в таблицу ниже не входят. Подробности этих изменений есть в методике эксперимента.
Что получилось
Помимо общей accuracy, я считал, какую долю сетевых заявок модель отправила в учетные записи (A→B). Именно эту ошибку я хотел вызвать подменой данных.
| Seed | Accuracy: чистый | Accuracy: отравленный | A→B: чистый | A→B: отравленный |
|---|---|---|---|---|
| 100 | 44,4% | 66,7% | 0,0% | 83,3% |
| 101 | 100,0% | 66,7% | 0,0% | 61,1% |
| 102 | 83,3% | 66,7% | 0,0% | 100,0% |
В среднем accuracy снизилась с 75,9% до 66,7%, а доля A→B выросла с 0% до 81,5%. После отравления ни один адаптер не дал правильный ответ A на сетевые заявки. Часть ошибочных ответов пришлась на C, поэтому доля A→B не везде достигла 100%.
Самый интересный результат получился с seed 100. Общая точность выросла на 22,2 процентного пункта, хотя сетевой класс полностью перестал распознаваться. Модель лучше ответила на заявки других категорий, и это перекрыло провал сети в общей метрике. В двух других повторах accuracy снизилась.
Рисунок 1. Исходная модель, чистые и отравленные адаптеры. Полосы показывают выборочное стандартное отклонение по трем повторам. Исходная модель проверялась один раз.
У чистых адаптеров получился большой разброс. Стандартное отклонение accuracy составляет 28,5 п.п. У отравленных оно равно 0 п.п. Средняя разность accuracy внутри пар составляет −9,3 п.п., стандартное отклонение этой разности равно 28,5 п.п. Для доли A→B после отравления стандартное отклонение составляет 19,5 п.п. Эти значения описывают три запуска. Они не являются доверительными интервалами и не доказывают статистическую значимость результата.
Средний macro F1 снизился с 0,725 до 0,544. Ошибок формата не было. Исходная Qwen без адаптера показала accuracy 37,0% и уже отправляла 16 из 18 сетевых заявок в B. Эти ошибки существовали до подмены, поэтому записывать их в результат атаки нельзя.
Для примера возьму одну заявку из повтора с seed 101:
При одинаковом system prompt и тексте заявки чистый адаптер ответил A, отравленный ответил B. Этот пример я выбрал после измерений, чтобы показать разницу на конкретной формулировке. Все ответы сохранены в results.json, исходные заявки находятся в dataset.json.
Что помогло заметить отравление
После атаки я проверил, помогает ли сверка обучающих данных обнаружить подмену и можно ли отсеять плохой адаптер перед допуском к работе. Для второго подхода использовал те же шесть адаптеров и 36 новых заявок, по три сюжета на класс и по четыре варианта каждого. Повторного обучения не было.
Пороги я выбрал уже после знакомства с атакой, но зафиксировал до получения ответов на новые заявки. По результатам проверки их не менял. Это небольшой набор похожих примеров.
| Проверка | Допущено чистых из 3 | Допущено отравленных из 3 |
|---|---|---|
| Формат метки A/B/C | 3 | 3 |
| Распределение меток совпадает с эталоном | 3 | 0 |
| Строки совпадают с доверенным эталоном | 3 | 0 |
| Accuracy ≥ 2/3 на новых заявках | 2 | 0 |
| Accuracy и ограничения по классам | 0 | 0 |
Первые три строки относятся к обучающим данным, последние две к адаптерам. В таблице показано, что прошло проверку. Она не показывает исправление модели, поскольку веса остались прежними.
Проверка формата
Эта проверка пропустила все подмены. Ответ B входит в список допустимых значений, даже если заявка относится к сети. Ограничение формата само по себе не позволяет понять, правильно ли размечен пример.
Распределение меток
Изменение с 60/60/60 на 6/114/60 обнаружилось сразу. Но такая проверка обходится, если сохранить количество меток каждого класса. Для проверки я поменял 27 ответов A→B и столько же в обратную сторону, B→A. Распределение осталось прежним, проверка пропустила набор.
На этих данных я не обучал Qwen. Этот контроль показывает только обход проверки распределения. Как такой обмен повлияет на модель, в эксперименте не измерялось.
Сверка с доверенным эталоном
Для каждой одобренной строки сохраняются id и SHA-256 полного содержимого. Сверка нашла все 54 замены в каждом отравленном наборе. Простая перестановка чистых строк проверку прошла, встречный обмен меток был обнаружен.
Проверка работает при условии, что атакующий не может заменить эталон. Сам хеш не ограничивает доступ и не подтверждает, что исходная разметка верна. Для проверки этого ограничения я объявил эталоном уже испорченные данные. Сверка ожидаемо приняла их.
Такую проверку можно запускать перед обучением и останавливать процесс при расхождении
Проверка готового адаптера
Для начала я потребовал accuracy не ниже 2/3 на новых заявках. Его не прошли все три отравленных адаптера и один чистый с результатом 63,9%.
Затем проверил более строгий вариант. Помимо accuracy, требовал recall каждого класса не ниже 75%, долю A→B не выше 10% и отсутствие ошибок формата. Эту проверку не прошел ни один из шести адаптеров.
У чистых моделей действительно нашлись проблемы. Полнота отдельных классов составила 25%, 66,7% и 41,7% соответственно. У чистого адаптера с seed 102 доля A→B на новых заявках достигла 41,7%, хотя на исходном тесте была нулевой. Другие формулировки выявили ошибки, которых первый тест не показал.
Поэтому я не стал считать строгую проверку лучшей защитой. Все отравленные адаптеры уже отсеял простой порог accuracy. Дополнительные условия в этой серии помогли заметить проблемы качества чистых моделей. Сам по себе отказ не позволяет установить, были ли данные отравлены.
Рисунок 2. Сколько чистых и отравленных вариантов прошло каждую проверку. Полный запрет на допуск еще не означает, что система готова к работе.
Все 216 новых ответов, пороги, причины отказов и контрольные случаи доступны в результатах проверки защит.
Какие выводы я сделал
Для меня главный результат этого опыта в том, что одной accuracy недостаточно. В одном повторе она выросла, а важная категория заявок полностью перестала распознаваться. Поэтому вместе с общей точностью я бы проверял матрицу ошибок, recall каждого класса и конкретный переход, который пытается вызвать атакующий.
Чистый адаптер тоже нужен. Без него легко принять за результат отравления ошибки исходной модели или последствия самого дообучения.
Из проверенных мер сверка с доверенным эталоном обнаружила изменения в уже одобренных данных. Новая проверочная выборка помогла отклонить плохие адаптеры. Первая проверка отвечает за неизменность данных, вторая показывает, справляется ли модель с задачей. Ни одна не восстанавливает испорченные веса.
В опыте использовались маленькая модель, синтетические заявки, всего три повтора и подмена 90% одного класса. Это ограничивает выводы. Seed менялись, но основа датасета оставалась той же. По этим результатам нельзя обещать обнаружение других атак. Восстановление весов, защищенное переобучение и резервную маршрутизацию я здесь не проверял.
Как повторить
Для запуска нужны Mac с Apple Silicon, Python 3.13, uv и исходники проекта. Версии зависимостей зафиксированы в
uv.lock.
Bash:
uv sync --locked --extra qwen
uv run --frozen --extra qwen qwen-poisoning-lab --output artifacts/reproduction
uv run --frozen --extra qwen python -m ml_poisoning_lab.qwen.report artifacts/reproduction/results.json --output results/reproduction
uv run --frozen --extra qwen python -m ml_poisoning_lab.qwen.defenses --artifacts artifacts/reproduction --output artifacts/defense-reproduction
Первый запуск скачивает официальные веса. План и хеши данных сохраняются до первого тестового ответа. Веса и адаптеры лежат в исключенном из Git каталоге
artifacts/. Отдельные процессы обучения и генерации работают с локальным кешем в offline-режиме.Для проверки защит нужны адаптеры, полученные на предыдущем шаге. Одного клона репозитория недостаточно. На другом окружении цифры могут отличаться, поэтому при воспроизведении стоит смотреть на протокол и отдельные ответы модели.
Что еще почитать
Подмена обучающих данных давно исследуется. В этой работе я разобрал ее на небольшой локальной лаборатории с чистым контролем и проверкой нескольких защит. Новый способ атаки здесь не предлагается.
• OWASP: Data and Model Poisoning. Описание угроз и мер контроля происхождения и целостности данных. Пороги в моей лаборатории выбраны для опыта, это не норматив OWASP.
• A Study of Backdoors in Instruction Fine-tuned Language Models. Исследование бэкдоров при instruction fine-tuning и способов защиты. В отличие от таких атак, в моем опыте отдельного триггера нет.
• Poisoning Fine-tuning Datasets of Constitutional Classifiers. Работа об отравлении данных дообучения классификаторов с другой задачей и механизмом атаки.
Содержание
Codeby Academy
Практика и мастерство
От основ до продвинутого — программы для практиков Codeby.
Перейти к курсу →
Комментарии
0