В прошлый раз я уже рассказывала о том, как в ходе обучения в "Школе 21" создавала класс линейной регресии, на этот раз будем рассматривать реализацию LogisticRegression, GaussianNB, KNN. Как и в прошлый раз, минимум теории, максимум практики.
LogisticRegression
Класс логической регрессии не сильно отличается от линейной. Как и в линейной регрессии, происходит вычисление линейной комбинации признаков с весами и смещением:
Но ключевое отличие — результат этой линейной комбинации подаётся в сигмоидную функцию (логистическую функцию):

Метод init
Метод init создает структуры для хранения параметров модели (коэффициенты, свободный чле��), а также настроек обучения (например, скорость обучения, число итераций).
Однако, от себя еще добавлю, что для лучшей точности, можно использовать scaler сразу в методе, но и без него считать можно.
Метод fit
Логистическая регрессия создаёт массив весов (коэффициентов) и смещение (bias), которые изначально инициализируются, чаще всего, нулями, как и в линейной регрессии.
Алгоритм действий:
Передаем матрицу признаков X и вектор целей y.
Инициализируем веса нулями (или мелкими случайными числами).
Для каждого эпоха (итерации) случайно проходимся по всем объектам (или берем случайный объект) и обновляем веса согласно формуле стохастического градиентного спуска, которую уже рассматривали выше: w:=w−α⋅∇Li(w)
def fit(self, X, y): # Преобразуем входные данные в массивы numpy для удобства вычислений X = np.array(X) y = np.array(y) # Инициализируем bias нулём self.bias = 0 X = self.scaler.fit_transform(X) n_samples, n_features = X.shape self.weights = np.zeros(n_features) np.random.seed(21) # Основной цикл обучения, выполняющийся max_iter раз for _ in range(self.max_iter): # Создаем случайную перестановку индексов для стохастического обновления indices = np.random.permutation(n_samples) # Переставляем данные и метки согласно случайной перестановке xi = X[indices] yi = y[indices] # Вычисляем линейную комбинацию входов и весов с добавлением смещения z = np.dot(xi, self.weights) + self.bias # Защита от переполнения экспоненты в сигмоидной функции z = np.clip(z, -500, 500) # Вычисляем предсказанные вероятности с помощью сигмоидной функции y_pred = 1 / (1 + np.exp(-z)) # Ошибка между истинными метками и предсказаниями error = yi - y_pred # Обновляем веса с учетом градиента логистической функции ошибки self.weights += self.lr_speed * np.dot((error * y_pred * (1 - y_pred)), xi) # Обновляем смещение (bias) self.bias += self.lr_speed * (error * y_pred * (1 - y_pred)).sum()
Метод predict
Предсказания вычисляются с помощью формулы сигмоиды
def predict(self, X): X = np.array(X) X = self.scaler.transform(X) linear_model = np.dot(X, self.weights) + self.bias linear_model = np.clip(linear_model, -500, 500) predicts = 1 / (1 + np.exp(-linear_model)) return predicts
KNN
Алгоритм строится следующим образом:
сначала вычисляется расстояние между тестовым и всеми обучающими образцами;
далее из них выбирается k-ближайших образцов (соседей), где число k задаётся заранее;
итоговым прогнозом среди выбранных k-ближайших образцов будет м��да в случае классификации и среднее арифметическое в случае регрессии;
предыдущие шаги повторяются для всех тестовых образцов.
Более подробную теорию можно почитать в этой статье.
Метод init
def __init__(self, n_neighbors=5): # Инициализация количества соседей self.n_neighbors = n_neighbors # Для хранения классов ближайших соседей self.neighbors_classes = None (если нужно)
Метод fit
Метод fit просто сохраняет входные данные и метки, а также запоминает все уникальные классы для дальнейшего использования при предсказании.
def fit(self, X, y): self.X_train = np.array(X) self.y_train = np.array(y) self.classes_ = np.unique(y)
Метод predict_proba
Для каждого объекта из входного массива вычисляет евклидовы расстояния до всех тренировочных объектов, выбирает индексы k ближайших соседей, находит их классы, затем для каждого класса считает долю соседей этого класса (вероятность принадлежности к классу) и возвращает массив вероятностей для каждого объекта.
def predict_proba(self, X): X = np.array(X) predictions = [] for xi in X: # Для каждого объекта в выборке # Вычисляем евклидово расстояние distances = np.sqrt(np.sum((self.X_train - xi) ** 2, axis=1)) # Получаем индексы n ближайших соседей, сортируя по расстоянию и беря первые n neighbors_idx = distances.argsort()[:self.n_neighbors] # Определяем классы этих соседей neighbors_classes = self.y_train[neighbors_idx] proba = [] for cls in self.classes_: # Вычисляем долю соседей, принадлежащих к текущему классу proba.append(np.mean(neighbors_classes == cls)) predictions.append(proba) return np.array(predictions)
Метод predict
Он получает вероятности принадлежности новых объектов ко всем классам через predict_proba, затем выбирает класс с максимальной вероятностью для каждого объекта и возвращает эти предсказанные классы.
def predict(self, X): proba = self.predict_proba(X) class_indices = np.argmax(proba, axis=1) return self.classes_[class_indices]
GaussianNB
Гауссовский Наивный Байесовский классификатор — вероятностный алгоритм, который на основе обучающих данных оценивает параметры нормального распределения для каждого признака в каждом классе и использует теорему Байеса, чтобы предсказывать вероятности и классы новых объектов.
Более подробную теорию можно почитать в этой статье.
Метод init
Определяет основные переменные:
self.classes_: хранит массив уникальных классов ��з обучающей выборки.self.Pc_: словарь с априорными вероятностями каждого класса.self.uci_: словарь для хранения значений среднего отклонения.self.oci_: словарь для хранения значений стандартного отклонения.
def __init__(self): self.classes_ = None self.Pc_ = None self.uci_ = None self.oci_ = None
Метод fit
На этапе fit происходит обучение модели: для каждого класса и каждого признака вычисляются статистические параметры — среднее (μ) и стандартное отклонение (σ) по обучающей выборке. Также вычисляются априорные вероятности классов (доля объектов каждого класса).
def fit(self, X, y): X = np.array(X) y = np.array(y) self.classes_ = np.unique(y) self.Pc_ = {} self.uci_ = {} self.oci_ = {} for cls in self.classes_: X_c = X[y == cls] # Выборка объектов текущего класса Nc = X_c.shape[0] self.Pc_[cls] = Nc / X.shape[0] # Апирорная вероятность класса self.uci_[cls] = X_c.mean(axis=0) # Среднее по признакам self.oci_[cls] = X_c.var(axis=0) # Дисперсия по признакам self.oci_[cls][self.oci_[cls] == 0] = 1e-9 # Чтобы избежать деления на 0
Метод preict_proba
Таким образом, метод fit в GaussianNB подготавливает все параметры P(c), μ,σ необходимые для вычисления вероятностей в методе predict по формуле плотности нормального распределения для каждого признака.
Когда вызывается predict, для каждого объекта вычисляется апостериорная вероятность принадлежности к классам по формуле:

При умножении множества очень маленьких чисел точность вычислений снижается из-за числовой нестабильности. Чтобы избежать этой проблемы, функцию логарифмируют, что преобразует произведение в сумму, а затем после вычислений возвращают исходные значения через экспоненту.

def predict_proba(self, X): X = np.array(X) predictions = [] for x in X: logs_probs = [] for cls in self.classes_: # Вычисление логарифма плотности вероятности признаков по нормальному распределению prob_log = -0.5 * np.log(2 * np.pi * self.oci_[cls]) - ((x - self.uci_[cls]) ** 2) / (2 * self.oci_[cls]) prob = np.sum(prob_log) # Добавление логарифма априорной вероятности класса logs_probs.append(np.log(self.Pc_[cls]) + prob) max_log = max(logs_probs) # Чтобы избежать числовой нестабильности при экспоненте exp_probs = [np.exp(lp - max_log) for lp in logs_probs] sum_exp = sum(exp_probs) normalized_probs = [p / sum_exp for p in exp_probs] # Нормализация вероятностей predictions.append(normalized_probs) return np.array(predictions)
Метод predict
На основе вероятностей выбирает класс с максимальной вероятностью для каждого объекта.
def predict(self, X): proba = self.predict_proba(X) class_indices = np.argmax(proba, axis=1) return np.array([self.classes_[i] for i in class_indices])
