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

LogisticRegression

Класс логической регрессии не сильно отличается от линейной. Как и в линейной регрессии, происходит вычисление линейной комбинации признаков с весами и смещением:

z=w⋅X+b

Но ключевое отличие — результат этой линейной комбинации подаётся в сигмоидную функцию (логистическую функцию):

Метод 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

Алгоритм строится следующим образом:

  1. сначала вычисляется расстояние между тестовым и всеми обучающими образцами;

  2. далее из них выбирается k-ближайших образцов (соседей), где число k задаётся заранее;

  3. итоговым прогнозом среди выбранных k-ближайших образцов будет мода в случае классификации и среднее арифметическое в случае регрессии;

  4. предыдущие шаги повторяются для всех тестовых образцов.

Более подробную теорию можно почитать в этой статье.

Метод 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])

Комментарии (0)