В прошлый раз я уже рассказывала о том, как в ходе обучения в "Школе 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])