← Назад к вопросам

Python: Реализовать логистическую регрессию с нуля

1.8 Middle🔥 151 комментариев
#Python#Машинное обучение

Условие

Реализуйте алгоритм логистической регрессии с нуля на Python.

Требования:

  1. Sigmoid функция
  2. Binary cross-entropy loss
  3. Градиентный спуск
  4. Тестирование на бинарной классификации

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

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Решение

1. Sigmoid функция и Binary Cross-Entropy Loss

import numpy as np
import matplotlib.pyplot as plt

class LogisticRegression:
    """Логистическая регрессия для бинарной классификации
    
    P(y=1|x) = sigmoid(w^T x + b) = 1 / (1 + e^(-w^T x - b))
    Loss = -1/n * sum(y*log(p) + (1-y)*log(1-p))
    """
    
    def __init__(self, learning_rate=0.01, n_iterations=1000, verbose=False):
        self.learning_rate = learning_rate
        self.n_iterations = n_iterations
        self.verbose = verbose
        self.w = None
        self.b = None
        self.loss_history = []
    
    @staticmethod
    def sigmoid(z):
        """Sigmoid функция: 1 / (1 + e^(-z))
        
        Преобразует любое число в вероятность [0, 1]
        """
        return 1 / (1 + np.exp(-z))
    
    @staticmethod
    def binary_cross_entropy(y_true, y_pred):
        """Binary Cross-Entropy Loss
        
        L = -1/n * sum(y*log(p) + (1-y)*log(1-p))
        
        y_true: истинные метки (0 или 1)
        y_pred: предсказанные вероятности [0, 1]
        """
        n = len(y_true)
        # Избегаем log(0)
        y_pred = np.clip(y_pred, 1e-15, 1 - 1e-15)
        
        loss = -1/n * np.sum(y_true * np.log(y_pred) + 
                              (1 - y_true) * np.log(1 - y_pred))
        return loss
    
    def fit(self, X, y):
        """Обучение модели через градиентный спуск
        
        Args:
            X: (n_samples, n_features)
            y: (n_samples,) с метками 0 или 1
        """
        n_samples, n_features = X.shape
        
        # Инициализация весов
        self.w = np.zeros(n_features)
        self.b = 0
        
        # Градиентный спуск
        for iteration in range(self.n_iterations):
            # Forward pass: вычисляем z = X @ w + b
            z = X @ self.w + self.b
            
            # Применяем sigmoid
            y_pred = self.sigmoid(z)
            
            # Вычисляем ошибку
            error = y_pred - y
            
            # Вычисляем градиенты
            # dL/dw = 1/n * X^T @ error
            # dL/db = 1/n * sum(error)
            dw = (1 / n_samples) * (X.T @ error)
            db = (1 / n_samples) * np.sum(error)
            
            # Обновляем веса
            self.w -= self.learning_rate * dw
            self.b -= self.learning_rate * db
            
            # Вычисляем loss
            loss = self.binary_cross_entropy(y, y_pred)
            self.loss_history.append(loss)
            
            if self.verbose and (iteration + 1) % 100 == 0:
                print(f"Iteration {iteration + 1}/{self.n_iterations}, Loss: {loss:.6f}")
        
        return self
    
    def predict_proba(self, X):
        """Предсказание вероятности класса 1"""
        z = X @ self.w + self.b
        return self.sigmoid(z)
    
    def predict(self, X, threshold=0.5):
        """Предсказание класса (0 или 1)
        
        По умолчанию используем порог 0.5
        """
        proba = self.predict_proba(X)
        return (proba >= threshold).astype(int)

print("=== ЛОГИСТИЧЕСКАЯ РЕГРЕССИЯ ===")
print("\n=== SIGMOID ФУНКЦИЯ ===")

# Визуализация sigmoid
z = np.linspace(-6, 6, 100)
sigmoid_vals = 1 / (1 + np.exp(-z))

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

axes[0].plot(z, sigmoid_vals, linewidth=2, color='steelblue')
axes[0].axhline(y=0.5, color='red', linestyle='--', alpha=0.5)
axes[0].axvline(x=0, color='gray', linestyle='--', alpha=0.3)
axes[0].set_xlabel('z')
axes[0].set_ylabel('sigmoid(z)')
axes[0].set_title('Sigmoid Function')
axes[0].grid(True, alpha=0.3)
axes[0].set_ylim(-0.1, 1.1)

print("\nSigmoid свойства:")
print(f"  sigmoid(0) = {LogisticRegression.sigmoid(np.array([0]))[0]:.4f}")
print(f"  sigmoid(-∞) ≈ {LogisticRegression.sigmoid(np.array([-10]))[0]:.4f}")
print(f"  sigmoid(+∞) ≈ {LogisticRegression.sigmoid(np.array([10]))[0]:.4f}")
print(f"  Диапазон: [0, 1] (вероятность)")

# === СИНТЕТИЧЕСКИЕ ДАННЫЕ ===

np.random.seed(42)
n_samples = 200

# Класс 0
X0 = np.random.randn(n_samples // 2, 2) - 2
y0 = np.zeros(n_samples // 2)

# Класс 1
X1 = np.random.randn(n_samples // 2, 2) + 2
y1 = np.ones(n_samples // 2)

X = np.vstack([X0, X1])
y = np.hstack([y0, y1])

print(f"\nДанные для классификации:")
print(f"  Размер: {X.shape}")
print(f"  Класс 0: {np.sum(y == 0)} примеров")
print(f"  Класс 1: {np.sum(y == 1)} примеров")

# Визуализация
axes[1].scatter(X0[:, 0], X0[:, 1], c='blue', label='Class 0', alpha=0.6)
axes[1].scatter(X1[:, 0], X1[:, 1], c='red', label='Class 1', alpha=0.6)
axes[1].set_xlabel('Feature 1')
axes[1].set_ylabel('Feature 2')
axes[1].set_title('Binary Classification Dataset')
axes[1].legend()
axes[1].grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('logistic_regression_intro.png')

print("\nДатасет визуализирован")

2. Обучение модели

print("\n=== ОБУЧЕНИЕ МОДЕЛИ ===")

model = LogisticRegression(
    learning_rate=0.1,
    n_iterations=1000,
    verbose=True
)

model.fit(X, y)

print(f"\nОбученные параметры:")
print(f"  Веса: {model.w}")
print(f"  Смещение: {model.b:.6f}")

# === ПРЕДСКАЗАНИЕ ===

y_pred = model.predict(X)
y_proba = model.predict_proba(X)

print(f"\nПредсказания на обучающем наборе:")
print(f"  Форма: {y_pred.shape}")
print(f"  Классы: {np.unique(y_pred)}")
print(f"  Распределение: {np.bincount(y_pred)}")

3. Метрики оценки

print("\n=== МЕТРИКИ ОЦЕНКИ ===")

def accuracy(y_true, y_pred):
    return np.mean(y_true == y_pred)

def precision(y_true, y_pred):
    tp = np.sum((y_pred == 1) & (y_true == 1))
    fp = np.sum((y_pred == 1) & (y_true == 0))
    return tp / (tp + fp) if (tp + fp) > 0 else 0

def recall(y_true, y_pred):
    tp = np.sum((y_pred == 1) & (y_true == 1))
    fn = np.sum((y_pred == 0) & (y_true == 1))
    return tp / (tp + fn) if (tp + fn) > 0 else 0

def f1_score(y_true, y_pred):
    p = precision(y_true, y_pred)
    r = recall(y_true, y_pred)
    return 2 * p * r / (p + r) if (p + r) > 0 else 0

acc = accuracy(y, y_pred)
prec = precision(y, y_pred)
rec = recall(y, y_pred)
f1 = f1_score(y, y_pred)

print(f"  Accuracy:  {acc:.4f}")
print(f"  Precision: {prec:.4f}")
print(f"  Recall:    {rec:.4f}")
print(f"  F1-Score:  {f1:.4f}")

# Confusion Matrix
tp = np.sum((y_pred == 1) & (y == 1))
fp = np.sum((y_pred == 1) & (y == 0))
fn = np.sum((y_pred == 0) & (y == 1))
tn = np.sum((y_pred == 0) & (y == 0))

print(f"\nConfusion Matrix:")
print(f"  TN={tn}, FP={fp}")
print(f"  FN={fn}, TP={tp}")

4. Визуализация результатов

fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# Loss график
axes[0, 0].plot(model.loss_history, linewidth=2, color='steelblue')
axes[0, 0].set_xlabel('Iteration')
axes[0, 0].set_ylabel('Loss (Binary Cross-Entropy)')
axes[0, 0].set_title('Training Loss Convergence')
axes[0, 0].grid(True, alpha=0.3)

# Decision Boundary
xx, yy = np.meshgrid(np.linspace(X[:, 0].min() - 1, X[:, 0].max() + 1, 100),
                     np.linspace(X[:, 1].min() - 1, X[:, 1].max() + 1, 100))
Z = model.predict(np.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape)

axes[0, 1].contourf(xx, yy, Z, alpha=0.3, levels=[0, 0.5, 1], colors=['blue', 'red'])
axes[0, 1].scatter(X0[:, 0], X0[:, 1], c='blue', label='Class 0', alpha=0.6)
axes[0, 1].scatter(X1[:, 0], X1[:, 1], c='red', label='Class 1', alpha=0.6)
axes[0, 1].set_title('Decision Boundary')
axes[0, 1].legend()
axes[0, 1].grid(True, alpha=0.3)

# Probability Distribution
axes[1, 0].hist(y_proba[y == 0], bins=20, alpha=0.6, label='Class 0', color='blue')
axes[1, 0].hist(y_proba[y == 1], bins=20, alpha=0.6, label='Class 1', color='red')
axes[1, 0].axvline(x=0.5, color='black', linestyle='--', linewidth=2)
axes[1, 0].set_xlabel('Predicted Probability')
axes[1, 0].set_ylabel('Count')
axes[1, 0].set_title('Probability Distribution by Class')
axes[1, 0].legend()
axes[1, 0].grid(True, alpha=0.3, axis='y')

# Метрики
metrics = ['Accuracy', 'Precision', 'Recall', 'F1']
values = [acc, prec, rec, f1]
colors = ['steelblue', 'orange', 'green', 'red']

axes[1, 1].bar(metrics, values, color=colors, alpha=0.7)
axes[1, 1].set_ylim([0, 1])
axes[1, 1].set_ylabel('Score')
axes[1, 1].set_title('Classification Metrics')
for i, v in enumerate(values):
    axes[1, 1].text(i, v + 0.02, f'{v:.3f}', ha='center')
axes[1, 1].grid(True, alpha=0.3, axis='y')

plt.tight_layout()
plt.savefig('logistic_regression_results.png')
print(f"\nРезультаты визуализированы")

print(f"""
=== ИТОГИ ===

Логистическая регрессия:
✓ Реализована sigmoid функция
✓ Реализована binary cross-entropy loss
✓ Реализован градиентный спуск
✓ Получена высокая точность ({acc:.2%})
✓ Визуализированы результаты

Для многоклассовой классификации:
- Используй softmax вместо sigmoid
- Используй categorical cross-entropy loss
- One-vs-Rest или One-vs-One стратегии
""")
Python: Реализовать логистическую регрессию с нуля | PrepBro