Классификация текста: наивный байес на практике

Наивный байес — простой, быстрый и на удивление сильный классификатор текста; разберём его на работающем спам-фильтре.

Наивный байесовский классификатор — модель, которая выбирает класс с наибольшей вероятностью по теореме Байеса, «наивно» предполагая, что слова в тексте независимы друг от друга.

Идея на пальцах

Мы хотим понять: данное письмо — спам или нет? Байес переворачивает вопрос. Вместо «какова вероятность спама при этих словах» он считает «какова вероятность встретить эти слова, если письмо — спам» и «…если письмо нормальное», а потом сравнивает. Класс, при котором наблюдаемые слова вероятнее, и выигрывает.

«Наивность» — в допущении, что слова независимы. В жизни это не так («бесплатный» и «кредит» ходят вместе), но допущение сильно упрощает расчёты и на практике работает хорошо.

Что считаем

Для каждого класса (спам / не спам) нам нужны:

  • P(класс) — доля писем этого класса в обучении.
  • P(слово | класс) — вероятность слова внутри класса = (сколько раз слово встречалось в письмах класса + 1) / (всего слов в классе + размер словаря). «+1» — сглаживание Лапласа, чтобы незнакомое слово не обнуляло всё произведение.

Для нового письма перемножаем вероятности его слов внутри каждого класса (умножаем на P(класс)) и выбираем класс с большим итогом. На практике складывают логарифмы вероятностей, чтобы не получить слишком маленькие числа.

Спам-фильтр целиком, запускаемый

import math
from collections import Counter, defaultdict

# обучающий корпус: (текст, класс)
train = [
    ("выигрыш бесплатно кредит", "spam"),
    ("бесплатно деньги выигрыш", "spam"),
    ("кредит деньги срочно", "spam"),
    ("встреча завтра в офисе", "ham"),
    ("привет как твои дела", "ham"),
    ("обед в час дня", "ham"),
]

# словарь и счётчики по классам
vocab = set()
class_words = defaultdict(Counter)   # класс -> Counter слов
class_total = defaultdict(int)       # класс -> всего слов
class_docs = Counter()               # класс -> число документов

for text, label in train:
    words = text.split()
    class_docs[label] += 1
    for w in words:
        vocab.add(w)
        class_words[label][w] += 1
        class_total[label] += 1

V = len(vocab)
N = len(train)

def log_prob(words, label):
    # лог P(класс) + сумма лог P(слово|класс) со сглаживанием Лапласа
    lp = math.log(class_docs[label] / N)
    for w in words:
        count = class_words[label][w]
        lp += math.log((count + 1) / (class_total[label] + V))
    return lp

def classify(text):
    words = text.split()
    scores = {c: log_prob(words, c) for c in class_docs}
    return max(scores, key=scores.get), scores

for msg in ["бесплатно кредит срочно", "привет как дела на обеде"]:
    label, scores = classify(msg)
    print(msg, "->", label)
    print("   log P(spam)=%.2f  log P(ham)=%.2f" % (scores["spam"], scores["ham"]))

Вывод:

бесплатно кредит срочно -> spam
   log P(spam)=-7.46  log P(ham)=-10.69
привет как дела на обеде -> ham
   log P(spam)=-16.79  log P(ham)=-15.27

Классификатор уверенно отнёс «бесплатно кредит срочно» к спаму, а «привет как дела на обеде» — к нормальным письмам. И всё это — на шести обучающих примерах, простым перемножением вероятностей слов. Никаких нейросетей.

Почему «+1» так важно

Если слово ни разу не встретилось в классе, его вероятность была бы 0, и всё произведение обнулилось бы — один незнакомый токен «убил» бы класс. Сглаживание Лапласа (+1 к каждому счётчику) даёт таким словам маленькую, но ненулевую вероятность.

Почему логарифмы

Перемножение многих вероятностей (каждая < 1) даёт исчезающе малое число, которое теряет точность. Логарифм превращает произведение в сумму, с которой компьютер работает надёжно. Сравнение сумм логарифмов эквивалентно сравнению произведений.

Итог

  • Наивный байес выбирает класс, при котором слова текста вероятнее всего.
  • «Наивность» — допущение независимости слов; упрощает расчёт, но работает.
  • Сглаживание Лапласа (+1) спасает от обнуления на незнакомых словах.
  • Складывают логарифмы вероятностей вместо перемножения — ради устойчивости.
  • Несмотря на простоту, это сильный базовый классификатор текста.
Проверьте себя
1. В чём заключается «наивность» наивного байеса?
AОн не использует обучающие данные
BОн предполагает, что слова в тексте независимы друг от друга
CОн работает только с двумя классами
DОн игнорирует частоты слов
2. Зачем в наивном байесе нужно сглаживание Лапласа (прибавление +1)?
AЧтобы ускорить обучение
BЧтобы незнакомое слово с нулевым счётчиком не обнуляло всю вероятность класса
CЧтобы уменьшить размер словаря
DЧтобы вероятности стали целыми числами
3. Почему вместо перемножения вероятностей складывают их логарифмы?
AЛогарифмы дают более красивый вывод
BПроизведение многих чисел меньше 1 становится исчезающе малым и теряет точность
CСложение работает только для спама
DЛогарифм увеличивает вероятности
Поддержать проект