В этой статье я привел основные сведения о трех основных видах НБК и показал реализацию каждого.
Содержание: формулировка задачи, независимость признаков, теория для мультиномиального, гауссовского и бернулли и их реализация.
Наивный байесовский классификатор (НБК) является простым, но эффективным алгоритмом в машинном обучении, используемым для задач классификации.
НБК основан на теореме Байеса, которая позволяет рассчитать условную вероятность события на основе предварительной информации. В контексте классификации, он используется для расчета вероятности принадлежности объекта к определенному классу на основе его признаков.
В основе идеи НБК лежит допущение (предположение) о том, что все признаки, описывающие объект, независимы друг от друга. Это предположение часто не выполняется в реальных задачах, но несмотря на это, алгоритм часто показывает хорошие результаты.
Выделяют три типа НБК, которые зависят от распределения набора данных: гауссовский, мультиномиальный и бернулли. Гауссовский используется, когда набор данных распределен нормально, это подходит для непрерывных признаков. Мультиномиальный используется, когда набор данных содержит дискретные значения, используется в текстовой классификации. Бернулли используется при работе над задачами бинарной классификации с бинарными признаками.
НБК используется в таких задачах, как: классификация текста, например, классификация электронных писем как спам или не спам; анализ комментариев - определение тона (положительный, отрицательный, нейтральный); системы рекомендаций - может быть использован для рекомендаций на основе пользовательских предпочтений.
Пусть есть набор данных, который представляет собой наблюдения входных и выходных событий. Входные события — входные признаки, выходные события — выходные признаки. Выходные события зависят от наступления входных событий. Когда на вход подаются входные признаки, то нужно вычислить вероятность наступления каждого выходного события (класса).
На основе набора данных можно рассчитать вероятности для каждого входного и выходного события на основе их частоты. Вероятности входных или выходных событий, рассчитанные на основе набора данных, называются априорными. Вероятности выходных событий, рассчитанные для новых данных, не участвующих в вычислении априорных вероятностей, называются апостериорными. Задача в том, чтобы рассчитать апостериорные вероятности для новых входных данных. Для этого будет использоваться теорема Байеса.
Теорема Байеса. Она позволяет рассчитать апостериорную вероятность (вероятность события при условии выполнения другого события). Формула теоремы Байеса выглядит следующим образом:
P(A|B) — апостериорная вероятность события A при условии B (при наличии события B, то есть для новых входных признаков), P(B|A) — априорная вероятность события B при условии A (рассчитывается из набора данных), P(A) — априорная вероятность события A (рассчитывается из набора данных), P(B) — априорная вероятность события B (рассчитывается из набора данных).
Вероятностная модель НБК — это условная модель над зависимой переменной класса , зависимая от нескольких переменных , n — число признаков.
По формуле Байеса:
Все признакия являются зависимыми друг от друга, они описывают какой-то объект, который относится к классу . Так как они зависимы, то вероятность вычисляется через правило совместной вероятности:
Вычисление такой вероятности вычислительно сложно, поэтому делается допущение, что признаки независимы, тогда вычисление вероятности упрощается.
Допустим, что каждый признак независим от других, тогда вероятность будет рассчитываться так:
Вероятность вычисляется так же по правилу совместной вероятности:
если признаки независимы, то:
НБК делает предположение о независимости признаков друг от друга при заданном классе, поэтому он называется наивным, это упрощает вычисления, но не всегда верно в реальных данных (почти всегда не верно), так как признаки определенно зависят друг от друга.
Мультиномиальный НБК является одним из вариантов НБК, специально предназначенный для работы с дискретными признаками, которые распределены мультиномиально. Этот тип НБК является мощным инструментом для классификации текстовых данных и других задач, где признаки имеют мультиномиальное распределение.
Теорема Байеса позволяет рассчитать апостериорную вероятность наступления события c при условии, что событие x произошло, следующим образом:
— апостериорная вероятность класса при условии признаков (задача в том, чтобы вычислить ее), — правдоподобие априорных вероятностей признаков для класса , — априорная вероятность класса, P(x) — априорная вероятность признаков , n — общее число различных признаков в наборе данных, k — число различных классов в наборе данных.
Априорные вероятности классов представляют собой вероятность появления каждого класса в наборе данных. Эти вероятности можно оценить как частота появления объектов каждого класса в обучающей выборке. Формула для вычисления:
— число объектов класса , n — общее число объектов в обучающей выборке.
Априорные вероятности появления каждого признака для каждого класса вычисляются на основе обучающих данных. В случае мультиномиального распределения, это делается по формуле:
— количество раз, когда признак встречается в классе (количество появлений данного признака в наборе данных для этого класса), — общее количество всех признаков в классе в наборе данных (общая сумма появлений каждого признака для класса ), — сглаживающий параметр, предотвращает появление нулевых вероятностей, — число уникальных признаков (слов) в словаре (словарь — vocabulary). Если априорная вероятность какого-то признака будет нулевой, тогда вся дробь в формуле Байеса станет нулевой, это нам не нужно (их можно просто не учитывать).
Априорная вероятность P(x) наступления события x в знаменателе формулы, рассчитывается так:
Вероятность каждого признака рассчитывается как частота появления этого признака в наборе данных:
— число появления признака в наборе данных, n — общее число появлений всех признаков в наборе данных.
Так как для всех новых данных знаменатель в формуле один и тот же, то на практике его не учитывают, остается только числитель. Апостериорные вероятности рассчитываются как произведение априорных вероятностей классов и априорных вероятностей признаков:
x — список признаков нового объекта, n — число признаков в списке (или общее число уникальных признаков в наборе данных). Класс с максимальной апостериорной вероятностью выбирается как прогноз, ответ модели.
Для проверки мультиномиального и бернулли будем использовать несколько предложений, позитивных, нейтральных и негативных тонов речи, которые записаны в отдельном файле.
Для начала создаем класс, который содержит общие поля и методы для всех трех видов НБК.
class NBC:
def __init__(self):
self.labels_probabilities = dict()
self.unique_labels = list()
def _unique_labels_(self, labels):
return list(set(labels))
def _labels_probabilities_(self, labels):
label_score = {label: 0 for label in self.unique_labels}
for label in labels:
label_score[label] += 1
return {label: label_score[label] / len(labels) for label in self.unique_labels}
Общие для всех — список уникальных меток классов и их априорные вероятности. Считаем число каждой метки в наборе данных и делим на общее число всех меток.
Далее создаем класс для мультиномиального НБК.
class MultinomialNBC(NBC):
def __init__(self):
NBC.__init__(self)
self.unique_features = list()
self.features_probabilities = dict()
self.alpha = 1
def _unique_features_(self, inputs):
features = list()
for inp in inputs:
for feature in inp:
if feature not in features:
features.append(feature)
return features
Делаем дополнительные поля: уникальные признаки, их условные вероятности и параметр alpha для предотвращения нулевых вероятностей. В методе unique_features выбираются уникальные признаки feature из каждого объекта inp из списка inputs обучающего набора данных.
Далее делаем метод для расчета условных вероятностей признаков.
def _features_probabilities_(self, inputs, labels):
probabilities = {label: dict() for label in self.unique_labels}
for label in self.unique_labels:
probabilities[label] = {feature: 0 for feature in self.unique_features}
features_count = 0
for feature in self.unique_features:
feature_count = self._feature_count_per_label_(feature, label, inputs, labels)
probabilities[label][feature] = feature_count
features_count += feature_count
for feature in self.unique_features:
numerator = probabilities[label][feature] + self.alpha
denominator = features_count + self.alpha * len(self.unique_features)
probabilities[label][feature] = numerator / denominator
return probabilities
Создаем словарь probabilities, в котором будут храниться значения условных вероятностей для каждого уникального признака для каждой уникальной метки класса. Для каждого класса label считаем общее число появления всех признаков features_count. Для каждого уникального признака считаем число его появлений feature_count для конкретного класса label. Далее рассчитываем условные вероятности признаков по формуле, которая дана в теоретической части.
Чтобы посчитать число отдельного признака feature_count для данного класса label делаем отдельный метод.
def _feature_count_per_label_(self, feature, label, inputs, labels):
feature_count = 0
for i, inp in enumerate(inputs):
if labels[i] == label:
for inp_feature in inp:
if inp_feature == feature:
feature_count += 1
return feature_count
Проходимся по каждому объекту inp в списке всех объектов inputs и считаем число появлений feature_count рассматриваемого признака feature для данного класса label.
Теперь делаем метод fit, в котором будут рассчитываться все данные для модели.
def fit(self, inputs, labels, alpha=1):
self.alpha = alpha
self.unique_features = self._unique_features_(inputs)
self.unique_labels = self._unique_labels_(labels)
self.features_probabilities = self._features_probabilities_(inputs, labels)
self.labels_probabilities = self._labels_probabilities_(labels)
Сначала нужно выделить уникальные признаки и метки, потом на их основе вычислять вероятности.
Теперь делаем метод для вычисления ответа модели. Делаем два метода: для расчета вероятности для конкретного класса и для определения класса, у которого максимальная вероятность.
def _predict_label_(self, label, inp): # calculate posterior probability for each class
inp_probability = 1
for feature in inp:
if feature in self.unique_features:
inp_probability *= self.features_probabilities[label][feature]
return inp_probability * self.labels_probabilities[label]
def predict(self, inp):
probabilities = [self._predict_label_(label, inp) for label in self.unique_labels]
max_item_idx = argmax(probabilities)
return self.unique_labels[max_item_idx] # return class with the highest posterior
Проходимся по каждому классу label в списке уникальных меток и вычисляем для него апостериорную вероятность. В методе predict_label происходит перемножение условных вероятностей всех входных признаков, затем происходит умножение на априорную вероятность данного класса label. После вычисления апостериорной вероятности для каждого класса выбираем метку класса с максимальным значением с помощью функции argmax.
def argmax(items):
max_item_idx = 0
for idx, item in enumerate(items):
if item > items[max_item_idx]:
max_item_idx = idx
return max_item_idx
Эта функция возвращает индекс элемента с максимальным значением
Проверяем и тестируем с помощью набора данных.
from nbc_data import positive, negative, neutral
all_messages = positive[:-1] + negative[:-1] + neutral[:-1]
test_messages = [positive[-1], negative[-1], neutral[-1]]
translate_table = str.maketrans({char: '' for char in "!,.?"})
inputs = [inp.translate(translate_table).split() for inp in all_messages]
test_inputs = [inp.translate(translate_table).split() for inp in test_messages]
labels = [0 for _ in positive[:-1]]
labels += [1 for _ in negative[:-1]]
labels += [2 for _ in neutral[:-1]]
test_labels = [0, 1, 2]
mult_nbc = MultinomialNBC()
mult_nbc.fit(inputs, labels)
for i, msg in enumerate(test_inputs):
print(test_labels[i], mult_nbc.predict(msg))
print("================================")
for i, msg in enumerate(inputs):
print(labels[i], mult_nbc.predict(msg))
output:
0 0
1 1
2 2
================================
0 0
...
2 2
Из каждого предложения убираем ненужные символы или подстроки с помощью таблицы translate_table. Создаем ее с помощью метода maketrans и словаря соответствий. В этой таблице записывается соответствие определенной подстроки другой подстроке. В этом примере происходит замена каждого из четырех знаков препинания на «пустой символ». Применяем эту таблицу к каждому предложению в наборе данных методом translate и разделяем предложения на слова методом split. В итоге получается список из списков слов.
Далее делаем списки меток для каждого из положительных, нейтральных и отрицательных тонов речи. Создаем объект класса MultinimialNBC и обучаем модель. Затем проверяем результаты.
Перед тем, как передавать текст в модель, его было бы хорошо как-то предварительно обработать, например, привести все слова в начальную форму, чтобы одни и те же слова в разных склонениях рассматривались как одно и то же слово.
Этот тип НБК предназначен для классификации данных с непрерывными значениями признаков. Априорные вероятности каждого класса вычисляются как и для мультиномиального НБК.
Для каждого признака и каждого класса вычисляются средние значения и стандартные отклонения по обучающим данным.
Среднее значение:
n — число всех i-тых признаков для класса (число всех объектов, у которых этот признак не нулевой для класса ).
Стандартное отклонение:
n — число всех i-тых признаков для класса .
Используя нормальное распределение (гауссовское распределение), рассчитывается вероятностная плотность признаков (априорная вероятность признаков) для каждого класса. Другими словами происходит масштабирование признаков для каждого класса по нормальному распределению. Формула для нормального распределения:
— значение i-го признака, — среднее значение i-го признака в классе , — стандартное отклонение i-го признака в классе .
Априорные вероятности для каждого признака рассчитываются аналогично как и для мультиномиального НБК, только с использованием формулы для расчета плотности вероятности. Нужно будет рассчитывать по всем признакам независимо от класса.
Апостериорные вероятности рассчитываются как произведение априорных вероятностей классов и вероятностных плотностей признаков нового объекта. Учитывая предположение, что признаки независимы:
x — вектор признаков, n — число признаков.
После того, как все было рассчитано, класс с максимальной апостериорной вероятностью выбирается как прогноз модели.
Для проверки гауссовского используем генератор набора данных из sclearn.
Для этого НБК понадобятся среднее значение и значение стандартного отклонения. Создаем новый класс, наследуем его от класса NBC, в котором есть общие для всех поля и методы, и инициализируем его в методе init. Добавляем поля для средних значений и стандартного отклонения.
class GaussianNBC(NBC):
def __init__(self):
NBC.__init__(self)
self.mean = dict()
self.std = dict()
def _mean_(self, feature_items):
return sum([item for item in feature_items]) / len(feature_items)
def _std_(self, feature_items): # standart deviation
mean = self._mean_(feature_items)
squared_sum = sum([(item - mean) ** 2 for item in feature_items])
return sqrt(squared_sum / len(feature_items))
Делаем методы для вычисления среднего значения для списка чисел и для вычисления стандартного отклонения для списка чисел. Вычисление происходит по формулам, описанным в теоретической части.
Метод для того, чтобы выбирать i-й признак для определенного класса.
def _feature_items_per_label_(self, feature_idx, label, inputs, labels):
feature_items = list()
for i, inp in enumerate(inputs):
if labels[i] == label:
feature_items.append(inp[feature_idx])
return feature_items
Создаем список для каждого значения i-го признака (feature_idx), который относится к данному класса label. Проходимся по каждому объекту в обучающих данных. Если метка объекта соответствует рассматриваемой метке label, то выбираем из этого объекта inp признак с данным feature_idx индексом. Таким образом мы отбираем нужные нам признаки из объектов, которые относятся к определенному классу. Далее на основе этого списка признаков будет рассчитываться среднее значение и стандартное отклонение.
Теперь делаем метод для отбора каждого i-го признака для каждого класса.
def _features_items_(self, inputs, labels):
features_num = len(inputs[0])
features_items = {label: dict() for label in self.unique_labels}
for label in self.unique_labels:
features_items[label] = {i: list() for i in range(features_num)}
for i in range(features_num):
features_items[label][i] = self._feature_items_per_label_(i, label, inputs, labels)
return features_items
Создаем переменную с числом признаков для каждого объекта. Создаем словарь, индексами которого являются уникальные значения меток классов. Для каждого класса создается словарь, в котором хранятся списки i-ых признаков.
Проходимся по каждой уникальной метке класса и создаем для нее словарь, в котором будут храниться списки значений для каждого i-го признака. Для каждого i-го признака создаем список признаков с помощью метода feature_items_per_label.
Теперь делаем метод, в котором будут создаваться словари для хранения значений среднего и стандартного отклонения для каждого признака для каждого класса.
def _get_features_parameters_(self, inputs, labels):
features_items = self._features_items_(inputs, labels)
mean = {label: dict() for label in self.unique_labels}
std = {label: dict() for label in self.unique_labels}
for label, feature_items in features_items.items():
mean[label] = {i: 0 for i in range(len(inputs[0]))}
std[label] = {i: 0 for i in range(len(inputs[0]))}
for feature_idx, items in feature_items.items():
mean[label][feature_idx] = self._mean_(feature_items=items)
std[label][feature_idx] = self._std_(feature_items=items)
return mean, std
В нем создается словарь features_items, в котором хранятся списки для каждого признака для каждого класса. Например, список всех значений первых признаков, которые состоят в объектах первого класса, для объектов другого класса значения первых признаков учитываться не будет.
В словаре mean хранятся средние значения для каждого i-го признака для каждого из классов, в словаре std то же самое, только для стандартных отклонений.
Рассчитываем значения mean и std для каждой группы признаков и сохраняем их для модели.
Вызываем все эти методы в методе fit.
def fit(self, inputs, labels):
self.unique_labels = self._unique_labels_(labels)
self.labels_probabilities = self._labels_probabilities_(labels)
self.mean, self.std = self._get_features_parameters_(inputs, labels)
Делаем метод, в котором будет происходить нормировка каждого признака для нового входного объекта. Нормировка производится на основе рассчитанных mean и std в методе fit.
def _gaus_scaling_(self, inp, label):
scaled_inp = list()
for idx, value in enumerate(inp):
numerator = e ** (-(value - self.mean[label][idx]) ** 2 / (2 * self.std[label][idx] ** 2))
denominator = sqrt(2 * pi * self.std[label][idx] ** 2)
scaled_inp.append(numerator / denominator)
return scaled_inp
Используем формулу нормального распределения, которая дана в теоретической части. Преобразуем каждый входной признак и получаем объект с отнормированными признаками.
Теперь делаем метод для получения ответа модели.
def _predict_label_(self, inp, label): # calculate posterior probability for each class
return self.labels_probabilities[label] * product(self._gaus_scaling_(inp, label))
def predict(self, inp):
probabilities = [self._predict_label_(inp, label) for label in self.unique_labels]
max_item_idx = argmax(probabilities)
return self.unique_labels[max_item_idx] # return class with the highest posterior
Проходимся по каждой уникальной метке класса и вычисляем апостериорную вероятность для этих классов. Выбираем метку класса, у которого максимальная вероятность.
Функция умножения product реализуется так.
def product(items):
res = 1
for item in items:
res *= item
return res
Проверяем работу модели.
from sklearn.model_selection import train_test_split
from sklearn.datasets import make_blobs
x_dataset, y_dataset = make_blobs(n_samples=150, n_features=4, centers=3, random_state=123)
x_train, x_test, y_train, y_test = train_test_split(x_dataset, y_dataset, test_size=0.2, random_state=123)
gauss_nbc = GaussianNBC()
gauss_nbc.fit(inputs=x_train, labels=y_train)
for i in range(len(x_test)):
print(y_test[i], gauss_nbc.predict(x_test[i]))
output:
1 1
1 1
...
2 2
Создаем искусственный набор данных с помощью функции make_blobs. Указываем число признаков, классов и число объектов. Делим их на тестовые и обучающие с помощью функции train_test_split в отношении 20% к 80%. Обучаем модель и проверяем.
НБК Бернулли - вариант НБК для работы с бинарными признаками, которые имеют бернуллиевское распределение и с двумя классами (0 или 1). В данном случае признаки представляют собой бинарные индикаторы наличия или отсутствия определённых характеристик у объекта, а ответ представляет собой вероятность одного из классов. Например, в таких задачах, как бинарная классификация текста, классификация электронных писем как спам или не спам, где каждое слово в тексте представлено бинарным признаком (присутствует или отсутствует).
Аналогичным образом вычисляются априорные вероятности каждого класса на основе обучающих данных, только теперь будет всего 2 класса.
Для НБК Бернулли, каждый признак является бинарным (0 или 1), соответственно, вероятности признаков представляют собой вероятность появления каждого бинарного признака в объекте с классом , то есть наличие или отсутствие в объекте. Эти вероятности можно вычислить так:
— вероятность появления признака в классе , которая оценивается как частота появления этого признака в классе в обучающей выборке,
— число появлений i-го признака для класса , n — число всех объектов класса . То есть нужно посчитать число объектов с данным признаком и с данным классом и поделить на число всех объектов данного класса.
Для каждого признака вычисляются априорная вероятность по этой формуле:
— значение i-го признака (0 или 1) в рассматриваемом объекте, — вероятность появления i-го признака в классе c. Если признак присутствует в объекте (= 1), то получится вероятность появления признака , если признак отсутствует (= 0), то получится вероятность не появления признака (1 - ). НБК Бернулли учитывает, что в ответе модели признаки не появляются во входном объекте.
Всего 2 класса, но как прогноз выбирается так же класс с максимальной апостериорной вероятностью. Можно вычислить сначала вероятность для одного класса и затем, чтобы вычесть вероятность второго класса, можно вычесть из единицы значение вероятности первого класса. Но для этого понадобится учесть знаменатель в формуле Байеса. Поэтому будет проще просто вычислить апостериорную вероятность для обоих классов без учета знаменателя.
Создаем класс BernoullyNBC и наследуем его от MultinimialNBC, чтобы не переписывать готовый код. Нужно будет всего лишь изменить пару методов.
class BernoullyNBC(MultinomialNBC):
def __init__(self):
MultinomialNBC.__init__(self)
Переопределяем методы features_probabilities и feature_count_per_label, так как в этом НБК они рассчитываются по-другому.
def _feature_count_per_label_(self, feature, label, inputs, labels):
feature_count, label_count = 0, 0
for i, inp in enumerate(inputs):
if labels[i] == label:
label_count += 1
for inp_feature in inp:
if inp_feature == feature:
feature_count += 1
break # до первого появления в примере
return feature_count, label_count
def _features_probabilities_(self, inputs, labels):
probabilities = {label: dict() for label in self.unique_labels}
for label in self.unique_labels:
probabilities[label] = {feature: 0 for feature in self.unique_features}
for feature in self.unique_features:
feature_count, label_count = self._feature_count_per_label_(feature, label, inputs, labels)
probabilities[label][feature] = feature_count / label_count
return probabilities
Нужно посчитать число появления (присутствия) каждого признака для каждого класса. В методе feature_count_per_label происходит подсчет числа определенного признака для определенного класса, учитывается лишь присутствие (0 или 1) этого признака, поэтому только до первого появления.
Потом в методе features_probabilities рассчитывается вероятность появления каждого признака. Нужно поделить число появления данного признака на число объектов, в которых присутствует этот признак. Формулы даны в теоретической части.
Так как для этого НБК важно только наличие признаков, то нужно сделать метод, в котором выделяются только уникальные признаки для нового входного объекта.
def _unique_features_per_input_(self, inp):
features = list()
for feature in inp:
if feature not in features:
features.append(feature)
return features
Осталось только изменить метод для расчета числителя.
def _predict_label_(self, label, inp):
inp_probability = 1
unique_inp_features = self._unique_features_per_input_(inp)
for feature in inp:
if feature in self.unique_features:
if feature in unique_inp_features:
inp_probability *= self.features_probabilities[label][feature]
else:
inp_probability *= 1 - self.features_probabilities[label][feature]
return inp_probability * self.labels_probabilities[label]
Числитель вычисляется тоже по-другому, формула дана в теоретической части. Если признак присутствует, то учитывается его условная вероятность присутствия, если отсутствует — учитывается его условная вероятность отсутствия.
Проверяем и тестируем.
from nbc_data import positive, negative
train_pos, train_neg = positive[:-2], negative[:-2]
test_pos, test_neg = positive[-2:], negative[-2:]
translate_table = str.maketrans({char: '' for char in "!,.?"})
inputs = [inp.translate(translate_table).split() for inp in train_pos + train_neg]
test_inputs = [inp.translate(translate_table).split() for inp in test_pos + test_neg]
labels = [0 for _ in train_pos]
labels += [1 for _ in train_neg]
test_labels = [0 for _ in test_pos]
test_labels += [1 for _ in test_neg]
bernoully_nbc = BernoullyNBC()
bernoully_nbc.fit(inputs, labels)
for i, msg in enumerate(inputs):
print(labels[i], bernoully_nbc.predict(msg))
print("==========================")
for i, msg in enumerate(test_inputs):
print(test_labels[i], bernoully_nbc.predict(msg))
output:
0 0
...
1 1
==========================
0 0
...
1 1
НБК относительно прост в реализации.
Модель не нуждается в обучении, нужно лишь посчитать частоту, что делает его полезным для больших наборов данных.
Он хорошо работает с разреженными данными (с небольшим набором данных) и не требует масштабирования признаков.
НБК достаточно устойчив к шуму и отсутствующим значениям признаков.
Основным ограничением является предположение о независимости признаков, которое почти всегда не выполняется. Это может привести к плохим результатам, когда между признаками есть сильные корреляции.
Алгоритм не учитывает порядок и вес признаков, что может быть важным в некоторых задачах (возможно получится сделать дополнительную модель с весами, которые будут учитываться в НБК).
Если количество примеров каждого класса в наборе данных сильно отличается, то вероятность будет больше у классов с большим числом примеров, то есть, если набор данных несбалансирован, то вероятность будет больше у классов с большим числом примеров.
В целом, НБК является мощным и простым инструментом для решения задач классификации в машинном обучении, несмотря на его ограничения.
Здесь была разобрана теоретическая часть работы основных трех видов НБК. Возможно есть какие-то неточности в описании моделей этих трех видов НБК, но основная идея каждого из них показана. Также дана формулировка задачи и рассказано, почему НБК называется наивным (наивное предположение о независимости признаков).
Я показал, как написать каждый из трех основных видов НБК с нуля. Получилось много кода и много замудренных объяснений, но это не проблема. Если есть желание, то очень легко можно в этом всем разобраться, нужно лишь провернуть эту всю логику в уме пару раз, тем более код достаточно понятный. Есть теоретическая часть, в которой есть все используемые здесь формулы и более подробное описание логики работы НБК.
Ссылка на папку с кодом из статьи на ГитХабе.