Навчання байєсівського класифікатора в rspamd: тренування spam/ham, безпечне автонавчання і як не переучити фільтр
Більшість проблем із Bayes у rspamd — не баги движка, а наслідок незбалансованого корпусу, порогового автонавчання, що замикає петлю на себе, і повторного згодовування тих самих листів. Розбираємо OSB-токенайзер, Redis-статфайли, безпечну схему autolearn, навчання руками користувачів через IMAPSieve і як побачити й відкотити перенавчання.
EvilMail Team15 липня 2026 р.13 хв читання
Типова картина на проді виглядає так. Або Bayes мовчить тижнями — у логах суцільний BAYES_UNKNOWN, символ ніде не спрацьовує, а інженер божиться, що «статистика не працює». Або через місяць після ввімкнення autolearn фільтр раптом починає лупити BAYES_SPAM по власному корпоративному ньюслеттеру та транзакційних листах, і саппорт розгрібає скарги на пропалі листи.
Обидва симптоми — не про rspamd. Це про корпус, на якому ви вчите класифікатор, і про петлю зворотного зв'язку, яку ви самі йому замкнули. Bayes у rspamd — не перемикач «увімк/вимк», а живий статфайл у Redis, який легко отруїти трьома способами: незбалансованим корпусом, поріг-орієнтованим автонавчанням і повторним згодовуванням тих самих повідомлень. Розберемо, як він влаштований насправді, і як тренувати його так, щоб не довелося робити FLUSHDB через два місяці.
Як влаштований статистичний модуль: OSB, токени, статфайли
Перш ніж чіпати конфіг, потрібна ментальна модель конвеєра. Лист не потрапляє в Bayes «як є» — він проходить нормалізацію тексту, розбивається на токени, і вже хеші токенів шукаються у двох статфайлах.
Токенайзер за замовчуванням — OSB (Orthogonal Sparse Bigrams)
з вікном 5 слів. Це не просто «розбити на слова»: OSB будує розріджені біграми в межах ковзного вікна, тобто пари слів на різній відстані одне від одного. Завдяки цьому Bayes ловить не окремі слова, а їхні комбінації —
«безкоштовна» + «виплата»
важить інакше, ніж кожне слово окремо. Кожна така конструкція хешується, і хеш стає ключем у статфайлі.
Ключове число: мінімум приблизно 11 значущих токенів на лист. Якщо їх менше — короткий лист на два рядки, порожнє тіло, самі картинки — Bayes не голосує взагалі і повертає BAYES_UNKNOWN зі score 0. Це перша причина «тиші»: половина вашого потоку — короткі автоматичні сповіщення, на яких класифікатору просто нема за що зачепитися.
Далі — наївний Байєс. Rspamd рахує P(spam) за частотами токенів у двох статфайлах і масштабує score від впевненості: BAYES_SPAM дає до приблизно +5.0, BAYES_HAM — до приблизно −3.0. Це ймовірність, а не чорний список: один і той самий токен може існувати в обох статфайлах, і вердикт визначає баланс частот, а не факт наявності.
Ще одна деталь, яку часто пропускають: статфайл буває глобальним (один на весь сервер) або per-user (окремий на кожного отримувача). Per-user точніший — те, що спам для одного, може бути очікуваною розсилкою для іншого — але вимагає набагато більшого потоку, щоб кожен користувач набрав свій min_learns. Для більшості інсталяцій починають із глобального.
Бекенд Redis і базова конфігурація classifier-bayes
Уся статистика живе в Redis. Забудьте про старі файлові статфайли sqlite — на проді це Redis з окремою базою даних під Bayes, щоб FLUSHDB для скидання статистики не зачепив інші дані rspamd.
classifier "bayes" {
tokenizer { name = "osb"; }
backend = "redis";
servers = "127.0.0.1:6379";
database = "5";
new_schema = true;
expire = 8640000; # 100 днів у секундах
min_tokens = 11;
min_learns = 200;
autolearn = [-0.5, 8.0]; # ham якщо score < -0.5, spam якщо > 8.0
cache { } # learn_cache проти повторного навчання
statfile { symbol = "BAYES_HAM"; spam = false; }
statfile { symbol = "BAYES_SPAM"; spam = true; }
# per_user = true; # окремі статфайли на користувача
}
Три параметри тут критичні.
`new_schema = true` перемикає зберігання на хеші Redis замість купи окремих ключів на кожен токен. Без нової схеми не працює expire, а старий формат роздуває пам'ять. На новій інсталяції це завжди true.
`expire = 8640000` — токени, яких не бачили 100 днів, вимирають. Це не мікрооптимізація пам'яті, а гігієна точності: спам-лексика минулого кварталу (старі бренди, старі кампанії, минулорічні домени) псує сьогоднішні рішення. Живий, самооновлюваний словник точніший за вічний.
`min_learns = 200` — Bayes не голосує взагалі, доки в статфайлі немає щонайменше 200 навчань спаму і 200 навчань ham. Не «або», а «і». Ось вам друга причина багатотижневої тиші: люди згодовують 1000 спам-листів, радіють, а ham-бік порожній — і класифікатор мовчить, бо йому нема з чим порівнювати.
Ручне навчання: правильний старт на збалансованому корпусі
Autolearn ніколи не стартує з нуля коректно. Спочатку — ручний старт на корпусі, який ви контролюєте. Згодовуємо каталоги Maildir простим циклом:
bash
# стартовий корпус: cur/ директорії, розкладені руками
for f in /srv/corpus/spam/*; do rspamc learn_spam < "$f"; done
for f in /srv/corpus/ham/*; do rspamc learn_ham < "$f"; done
# якщо контролер під паролем
rspamc -h 127.0.0.1:11334 -P "$RSPAMD_PASSWORD" learn_spam < msg.eml
# перевірка стану навчання
rspamc stat | grep -A2 BAYES
Поле learned — це серце діагностики. Тут одразу видно баланс. 1240 спаму проти 1180 ham — здорово. 1240 проти 40 — ви щойно заклали бомбу хибних спрацювань.
Три правила стартового корпусу, які визначають усе подальше:
Симетрія. spam:ham ≈ 1:1. Рекомендований старт — 500+/500+, а не «весь спам що назбирався за рік і 30 моїх власних листів». Незбалансований корпус — головна причина того, що потім Bayes лупить BAYES_SPAM по легітимі: якщо спаму вчили в 10 разів більше, апріорна ймовірність перекошена в бік спаму.
Різнорідність відправників. Ham не має бути з одного домену. Якщо ви навчите ham виключно на власному вихідному потоці одного корпоративного домену, Bayes вивчить не «як виглядає легітимний лист», а «як виглядають листи від нас» — і будь-яка зовнішня розсилка стане підозрілою.
Чистота міток. Один спам, помилково згодований як ham, отруює десятки токенів. Перечитуйте корпус перед learn_*, а не після скарг.
Автонавчання без отруєння статфайлу
Тепер найтонше місце. Autolearn у rspamd має три форми:
autolearn = true — вчить за метакласами дій: лист, який дійшов до reject, іде в spam; лист із дуже низьким score — у ham.
autolearn = [ham_thr, spam_thr] — масив порогів score. Безпечніший, бо ви самі задаєте вузькі межі.
Блок з окремими spam_threshold / ham_threshold — те саме, докладніше.
Головна пастка ховається у слові «поріг». Якщо ви вчите ham на всьому, що набрало score нижче за поріг, а спам — на всьому вище, то BAYES_HAM і BAYES_SPAM самі впливають на цей score. Виходить петля позитивного зворотного зв'язку: Bayes ставить BAYES_SPAM, це піднімає загальний score вище порогу, лист автоматично вчиться як спам, статфайл ще сильніше вірить у ці токени, наступний схожий лист отримує ще вищий score. Фільтр вчиться на власних рішеннях і дрейфує, поки не почне бачити спам скрізь.
Рецепт проти петлі — навчати автоматично тільки крайнощі. autolearn = [-0.5, 8.0] означає: ham вивчаємо лише з майже ідеально чистого (score нижче −0.5, куди дійде тільки лист, у якого і SPF/DKIM у порядку, і лексика чиста), spam — лише з явно брудного (вище 8.0). Широку «сіру зону» між −0.5 і 8.0 не чіпаємо взагалі: саме там живуть спірні листи, на яких класифікатор і псується.
І другий запобіжник — learn_cache. Блок cache { } вмикає збереження хеша кожного вивченого повідомлення в Redis. Повторний learn того самого листа просто ігнорується. Це закриває і петлю, і банальну людську помилку «прогнав корпус двічі» — без нього одні й ті самі листи накручують learned і перекошують ваги.
Золоте правило одним рядком: **ніколи не робіть autolearn на основі спрацювання самого BAYES_***. Автонавчання має спиратися на незалежні сигнали (SPF, DKIM, DMARC, RBL, вміст), а не на попереднє рішення того самого класифікатора.
Навчання руками користувачів через IMAPSieve
Найякісніше джерело міток — самі користувачі. Перетягнув у Junk → навчи спам; витяг із Junk назад в Inbox → навчи ham. У зв'язці Dovecot + rspamd це робиться через IMAPSieve.
А сам sa-learn-spam.sh у sieve_pipe_bin_dir — тонка обгортка: exec /usr/bin/rspamc -h 127.0.0.1:11334 -P "$RSPAMD_PASSWORD" learn_spam.
Одне попередження з практики: користувачі шумлять. Хтось кидає в Junk легітимні розсилки, бо просто не хоче їх бачити; хтось так «прибирає» папку. Тому для навчання за мітками користувачів обов'язково тримайте learn_cache увімкненим і серйозно розгляньте per_user = true, щоб один «художник» не завалив глобальний статфайл своїми хаотичними мітками. У per-user режимі його творчість псує лише його власну статистику.
Нейромодуль поверх Bayes, а не замість нього
Neural — це другий шар, а не заміна Bayes. Він вчиться не на тексті, а на векторі всіх спрацьованих символів (включно з BAYES_SPAM, SPF, DKIM, RBL тощо), беручи історію з Redis, і дає власні символи NEURAL_SPAM / NEURAL_HAM. Якщо Bayes ловить лексику, то нейромережа ловить характерні *комбінації правил*, які поодинці нічого не значать.
Ключове застереження: нейромодуль не можна вмикати, поки Bayes і решта символів не стабілізувалися. Він вчиться на їхніх спрацюваннях — якщо Bayes ще дрейфує або дає шум, нейромережа завчить цей шум як закономірність. Спочатку стабільний Bayes і базові правила, і лише потім, поверх них, neural.
Як побачити й відкотити перенавчання
Симптоми перевченого фільтра впізнавані:
зростає частка BAYES_SPAM на явно легітимному потоці (ньюслеттери, транзакційні листи, внутрішня пошта);
перекіс поля learned у rspamc stat — spam значно більший за ham або навпаки;
стрибок хибнопозитивних, який корелює з датою ввімкнення autolearn.
Діагностика — три команди:
bash
rspamc stat # баланс learned між BAYES_SPAM і BAYES_HAM
rspamadm dump # дамп реального конфіга з підстановками
rspamc -v < suspicious.eml # чому саме такий вердикт по одному листу
rspamc -v покаже поточний внесок BAYES_* у score конкретного листа — одразу видно, чи це Bayes переоцінює, чи спрацювало щось інше.
Відкат залежить від глибини проблеми. Легкий випадок — вибіркове перенавчання протилежним класом: згодовуєте rspamc learn_ham пачку хибно позначених легітимних листів, і баланс вирівнюється (learn_cache тут не завадить, бо це нові рішення). Важкий випадок, коли статфайл безнадійно отруєний, — повне скидання окремої бази Redis і рестарт із чистого корпусу:
bash
redis-cli -n 5 FLUSHDB # тільки БД Bayes, нічого зайвого
systemctl restart rspamd
# далі — заново стартовий збалансований корпус 500+/500+
Саме заради цього рядка FLUSHDB ми з самого початку виносили Bayes в окрему database = "5". Скидання статистики не має зачіпати нічого іншого.
Чекліст перед і після ввімкнення
Redis під Bayes — окрема база даних (database = "5"), щоб FLUSHDB не зачепив решту.
new_schema = true і expire (100 днів) — увімкнено з першого дня.
min_learns = 200: пам'ятаєте, що до 200 spam і 200 ham класифікатор мовчить.
Стартовий корпус 500+/500+, викладений і перевірений руками, а не «весь спам за рік».
spam:ham ≈ 1:1 і різнорідні відправники в ham — жодного навчання ham лише на власному домені.
autolearn — тільки вузьким діапазоном ([-0.5, 8.0]), сіру зону не чіпаємо.
learn_cache (cache { }) увімкнено проти дублів і петель.
per_user = true там, де мітки ставлять користувачі через IMAPSieve.
Моніторинг балансу learned у rspamc stat — щотижня, а не після скарг.
Neural — вмикаємо останнім, лише коли Bayes і базові символи стабільні.
Bayes окупається саме тоді, коли ви ставитеся до нього як до інструмента, що вимагає гігієни корпусу, а не як до кнопки. Збалансований старт, вузьке автонавчання і кеш проти дублів — і статфайл роками працює без FP-штормів. Перекошений корпус і поріг на все — і за місяць ви вручну розгрібаєте власну петлю.