Представляємо один з найкращих хак в машинному навчанні: хитрощі

Різні торгові точки 2018 року сприйняли як рік, коли спам почне вмирати, оскільки алгоритми машинного навчання стануть майже ідеальними для з'ясування того, що таке справжня пошта, а що ні. Я не впевнений, що коли-небудь відбудеться (прогрес у машинному навчанні скорочує обидва способи), але я хотів би поділитися деякими загальними думками про те, як побудовані прості класифікатори спаму на основі ML та як подолати значну проблему, обійти фільтр, використання одного з найкращих хак в машинному навчанні: хеш-трюк. Це також корисно і для виявлення спаму.

Побудова простого класифікатора спаму

Для завдань класифікації документів, включаючи класифікацію спаму, зазвичай починається з побудови відомого як подання "мішок слів" (BOW). Враховуючи набір відомих спам-листів та неспам-листів, кожне унікальне слово додається до словника та присвоюється унікальний індекс, як правило, починаючи з 0. Скажімо, для стислості, що у нас є набір з двох коротких текстових прикладів, один що є спамом та іншим, що є законним:

я заробляю десять тисяч доларів на тиждень, просто займаюся веб-пошуку! (спам)
Ви вільні на зустріч на початку наступного тижня? (не спам)

Якщо ми скануємо набір даних і почнемо складати свій словниковий запас, ми можемо закінчити щось подібне:

i: 0
складають: 1
десять: 2
тис.: 3
доларів: 4
за: 5
тиждень: 6
просто: 7
серфінг: 8
: 9
веб: 10
є: 11
ти: 12
безкоштовно: 13
для: 14
a: 15
зустріч: 16
рано: 17
наступний: 18

Всього 19 унікальних слів, і кожному присвоюється унікальний індекс (зауважте, що слово тиждень відображається в обох прикладах). Наступним кроком є ​​створення функціональних векторів для нашої моделі машинного навчання. Почнемо зі створення вектора нульового стовпчика для кожного прикладу з такою ж кількістю елементів, як і слова у нашому словнику (19):

я заробляю десять тисяч доларів на тиждень, просто займаюся веб-пошуку! (спам)
-> [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
Ви вільні на зустріч на початку наступного тижня? (не спам)
-> [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]

Потім для кожного слова в кожному прикладі ми виконуємо пошук словника, щоб отримати індекс і збільшити значення в цьому індексі на одне:

я заробляю десять тисяч доларів на тиждень, просто серфінгу в Інтернеті! (спам)
-> [1 1 1 1 1 1 1 1 1 1 1 0 0 0 0 0 0 0 0]
Ви вільні на зустріч на початку наступного тижня? (не спам)
-> [0 0 0 0 0 0 1 0 0 0 0 1 1 1 1 1 1 1 1]

Отримані в результаті функції вектори являють собою уявлення про мішки. Представлення BOW зазвичай викидають інформацію про розділові знаки та порядок слів, але для багатьох проблем це не проблема. Більш складні подання BOW використовують ваги TF-IDF та / або n-грам замість необроблених підрахунків слів, але основна ідея та ж.

Після того, як ми отримаємо наші вектори функцій BOW, ми зможемо навчити бінарний класифікатор для створення фільтра спаму. Є багато варіантів, що стосуються алгоритмів навчання, але найпоширенішими підозрюваними є наївні байеси, випадкові ліси, логістична регресія та все частіше нейронні мережі. З огляду на навчену модель, ми можемо використовувати словниковий запас для подачі у новому електронному листі як вектор BOW та передбачити, чи є приклад спамом чи ні. Зауважте, що для висновку в режимі реального часу нам потрібно зберігати словник у оперативній пам’яті, щоб бути максимально швидким.

Проблема: обхід фільтра

Спамери хитрі. Один із популярних способів переконатися, що спам не відфільтровується - це змішування слів, а не з лексики, яка використовується для вивчення класифікатора. Розглянемо, наприклад, таке, трохи надумане речення:

ii mayke, ти тисячі безкоштовних для $$$ s серфінгу на веб-зустрічі на початку наступного тижня

Зрозуміло, що це не те, що хтось вважатиме законною електронною поштою. Але що станеться, якщо ми використаємо наш словниковий запас для побудови вектора BOW для цього прикладу? Перші вісім слів взагалі не містяться в нашому словниковому запасі, і ми не вносимо їх. Решта - внаслідок цього:

ii mayke, ти тисячі безкоштовних для $$$ s серфінгу на веб-зустрічі на початку наступного тижня
-> [0 0 0 0 0 0 1 0 0 0 0 1 1 1 1 1 1 1 1]

Цей вектор такий же, як і для законного прикладу. Ви вільні на зустріч на початку наступного тижня? . Будь-який класифікатор, навчений на наших прикладах, певно, вважає, що цей спам є законним. Це суттєва проблема, і не так просто вирішити, як можна було б подумати. Ми могли б додати нові слова до нашого словника, але це означатиме, що розмір векторів ознак зміниться, як і сам словник. Моделі машинного навчання зазвичай навчаються на прикладах навчання фіксованого розміру, тому нам потрібно буде перевчити свою модель з нуля. На це потрібен час, і поки ми це робимо, старий класифікатор продовжує приймати спам. Нам потрібне рішення, яке а) може мати справу зі словами, що не є словниковими, б) не вимагає від нас перенавчати свої моделі з нуля кожного разу, коли ми зустрічаємо нове слово чи неправильно написані слова, і c) є максимально точним. Якби ми могли піти, не зберігаючи величезного словника в оперативній пам'яті, ще краще.

Представляємо хеш-трюк

Функції хешу є основними для інформатики. Існує безліч різних типів хеш-функцій, але всі вони роблять те саме: відображають дані довільних розмірів на дані фіксованого розміру. Зазвичай вони випльовують число (відоме як хеш):

"John Doe" -> хеш-функція -> 34
"Jane Doe" -> хеш-функція -> 48

Логіка, за якою обчислюється хеш, залежить від самої хеш-функції, але всі хеш-функції мають однакові загальні характеристики:

  • Якщо ми подаємо один і той же вхід до хеш-функції, він завжди дасть однаковий вихід.
  • Вибір хеш-функції визначає діапазон можливих виходів, тобто діапазон завжди фіксується (наприклад, числа від 0 до 1024).
  • Функції хешу є односторонніми: даючи хеш, ми не можемо здійснити зворотний пошук, щоб визначити, що було введеним.
  • Функції хешу можуть виводити однакові значення для різних входів (зіткнення).

Функції хешу неймовірно корисні майже в будь-якій галузі інформатики, але як їх можна використовувати для виправлення проблеми поза словником нашого класифікатора спаму? Відповідь не відразу очевидна, але перший крок - взагалі позбутися нашого словника. Натомість, будуючи наші подання BOW, ми почнемо із створення нульового вектора стовпця з величезною кількістю (скажімо, 2 ⁸) елементів для кожного з наших навчальних прикладів:

я заробляю десять тисяч доларів на тиждень, просто серфінгу в Інтернеті! (спам)
-> [0 0 0 0 ... 0 0 0 0] (2 ^ 28 елементів)
Ви вільні на зустріч на початку наступного тижня? (не спам)
-> [0 0 0 0 ... 0 0 0 0] (2 ^ 28 елементів)

Далі ми виберемо хеш-функцію f, яка їсть рядки та виводить значення в діапазоні [0, 2²⁸). Іншими словами, ми гарантуємо, що наша хеш-функція ніколи не адресуватиме індекс поза розмірами наших функцій векторів.

Після цієї ініціалізації для кожного навчального прикладу ми подаємо кожне слово, по одному, через нашу хеш-функцію, і збільшуємо значення на заданому індексі на одне - так само, як і раніше. Ми можемо досягти таких розріджених векторів:

я заробляю десять тисяч доларів на тиждень, просто серфінгу в Інтернеті! (спам)
-> [0 ... 0 1 1 1 0 1 1 0 ... 0 1 1 1 1 0 1 1 0] (2 ^ 28 елементів)
Ви вільні на зустріч на початку наступного тижня? (не спам)
-> [0 1 0 1 0 ... 0 1 0 ... 0 1 0 ... 0 1 1 0 1 1 0 1] (2 ^ 28 елементів)

Цей процес відомий як хеш-трюк.

Зараз у нас є наше представлення BOW, і ми можемо тренувати класифікатор даних, як і раніше. Просто, ні? Ми передбачили використання окремого словника, а це означає, що нам не потрібно зберігати потенційно великий список слів в оперативній пам’яті. Але це лише приємний побічний ефект - справжньою проблемою, яку ми хочемо вирішити, є обхід фільтру, використовуючи слова, що не входять до словника. Тож як допомагає хеш-трюк?

Скажімо, у нас є класифікатор спаму, який навчається на купі розрядних векторів, що мають 2 ⁸⁸ BOW. З огляду на нову частину пошти, ми робимо, як і раніше, ініціалізуючи 2 ²⁸ вектор і передаючи кожне слово через нашу хеш-функцію. На відміну від раніше, кожне слово закінчується збільшенням функції. Враховуючи наш BOW вектор, кожне слово - навіть нове - враховується під час прогнозування. Нові слова все ще погіршують точність нашого класифікатора, але більше не можна цілком обійти наш фільтр спаму, складаючи нові слова. Оскільки всі вектори BOW залишаються однакового розміру, ми можемо поступово підходити до нашої моделі з новими прикладами спаму / неспаму, не перевчаючи всю справу з нуля. Це форма навчання в Інтернеті: коли користувач позначає електронну пошту як спам, модель може навчатися цьому поступово, не перезавантажуючи весь процес. Для такого практичного застосування, як фільтрування спаму, це явна перевага хешування функцій: ми можемо швидко реагувати на атаки, навчаючись, як тільки з’являються нові приклади спаму / неспаму.

А як же зіткнення, я чую, ви запитуєте? Чи не можливо, що якесь навмисне неправильне написання закінчується тим, що збільшує той самий індекс, що й легітимне слово, коли воно проходить через хеш-функцію? Так, це може статися, але якщо ви виберете розмір вектора (зробіть його якомога більшим) і хеш-функцію обережно, шанси на це трапляються незначні, і навіть якщо це відбувається, це зазвичай не впливає на навчання (або точність ) стільки. Документація для стандартних хеш-функцій, як правило, включає ймовірності зіткнення, тому не забудьте їх переглянути, створюючи власне рішення хеш-хитів.

Зауважте, що в деяких випадках ви навіть можете зіткнутись (наприклад, для згрупування подібних законних слів), і в цьому випадку ви можете зв'язати ці групи перед хешированием.

Кілька заключних думок

Трюк хешингу - один із тих акуратних хитрощів у машинному навчанні, який не має майже такої любові, як заслуговує. Єдиним реальним недоліком є ​​той факт, що зворотний пошук (вихід на вхід) неможливий, але для багатьох проблем це не є обов'язковою умовою. Думаючи в більш загальних рисах, хеш-трюк дозволяє використовувати вектори функцій змінного розміру зі стандартними алгоритмами навчання (регресія, випадкові ліси, нейронні мережі з подачею вперед, SVM, матрична факторизація тощо). Цього повинно бути достатньо, щоб змусити більшості практиків машинного навчання хоча б трохи захоплюватися.