Глава 6. НЕЙРОННЫЕ СЕТИ

6.1. Правило обновления перцептрона

\[ \mathbf{w}^{(t+1)} = \mathbf{w}^{(t)} + \eta (y - \hat{y}) \mathbf{x} \] (где \(y\) - истинная метка, \(\hat{y}\) - предсказанная метка (0 или 1), \(\eta\) - скорость обучения, \(\mathbf{x}\) - вектор входных признаков, \(\mathbf{w}\) - вектор весов)

Объяснение: Правило обновления перцептрона корректирует веса на основе ошибок предсказания. Используется для бинарной классификации на линейно разделимых данных.

Пример: Для \(\mathbf{x} = [1, 2]\) (добавим \(x_0=1\) для смещения: \(\mathbf{x}' = [1, 1, 2]\)), \(y = 1\), \(\hat{y} = 0\) (ошибка), и \(\eta = 0.1\), обновить \(\mathbf{w}\).

Реализация:


# eta - скорость обучения
# y_true - истинная метка
# y_pred - предсказанная метка
# x - входной вектор (numpy array)
# w - вектор весов (numpy array)

# Пример обновления (предполагая, что y_pred уже вычислен)
# w += eta * (y_true - y_pred) * x

Примечание: Реализация в OCR предполагает, что `y_pred` уже вычислен. Полный алгоритм перцептрона включает вычисление \(\hat{y}\) как пороговой функции от \(w^T x\).

6.2. Прямое распространение (Один слой)

\[ \hat{y} = \sigma(X\mathbf{w} + b) \] (где \(\sigma\) - функция активации, X - матрица входов, \(\mathbf{w}\) - вектор весов, \(b\) - смещение)

Объяснение: Прямое распространение вычисляет предсказания путем применения матрицы весов и функции активации к входным признакам.

Пример: Для \(X = [1, 2]\), \(\mathbf{w} = [0.5, 0.5]\), и \(b = 0\), вычислить \(\hat{y}\), используя сигмоиду как \(\sigma\). \(X\mathbf{w} + b = 1 \cdot 0.5 + 2 \cdot 0.5 + 0 = 0.5 + 1.0 = 1.5\). \(\hat{y} = \sigma(1.5) = 1 / (1 + e^{-1.5}) \approx 0.8176\).

Реализация:


import numpy as np

def sigmoid(z):
  return 1 / (1 + np.exp(-z))

# X - входные данные (может быть матрицей для нескольких образцов)
# w - веса
# b - смещение
# activation_func - функция активации
X = np.array([1, 2])
w = np.array([0.5, 0.5])
b = 0
activation_func = sigmoid # Выбираем сигмоиду

z = X @ w + b # Линейная комбинация
y_hat = activation_func(z) # Применение активации

print(f"Output (y_hat): {y_hat}")

6.3. Сигмоидная активация

\[ \sigma(z) = \frac{1}{1 + e^{-z}} \]

Объяснение: Сигмоидная активация отображает входные значения в диапазон [0, 1], часто используется для бинарной классификации (на выходном слое).

Пример: Для \(z = 0.5\), вычислить \(\sigma(0.5) = 1 / (1 + e^{-0.5}) \approx 0.622\).

Реализация:


import numpy as np

def sigmoid(z):
  """Вычисляет сигмоидную функцию."""
  return 1 / (1 + np.exp(-z))

print(sigmoid(0.5))

6.4. Активация Tanh (Гиперболический тангенс)

\[ \tanh(z) = \frac{e^z - e^{-z}}{e^z + e^{-z}} \]

Объяснение: Активация Tanh отображает входные значения в диапазон [-1, 1] и полезна для симметричных данных или в скрытых слоях некоторых архитектур (например, RNN).

Пример: Для \(z = 0.5\), вычислить \(\tanh(0.5) \approx 0.462\).

Реализация:


import numpy as np

def tanh(z):
  """Вычисляет гиперболический тангенс."""
  return np.tanh(z)

print(tanh(0.5))

6.5. Активация ReLU (Rectified Linear Unit)

\[ \text{ReLU}(z) = \max(0, z) \]

Объяснение: ReLU вводит нелинейность, обнуляя отрицательные значения. Широко используется в скрытых слоях глубоких сетей из-за своей простоты и эффективности в борьбе с затуханием градиентов.

Пример: Для \(z = -1\), вычислить \(\text{ReLU}(-1) = \max(0, -1) = 0\). Для \(z = 2\), \(\text{ReLU}(2) = \max(0, 2) = 2\).

Реализация:


import numpy as np

def relu(z):
  """Вычисляет функцию ReLU."""
  return np.maximum(0, z)

print(relu(-1))
print(relu(2))

6.6. Активация Heaviside (Ступенчатая функция)

\[ H(z) = \begin{cases} 1, & z \ge 0 \\ 0, & z < 0 \end{cases} \]

Объяснение: Ступенчатая функция Хевисайда выводит бинарные значения (0 или 1) и используется в простейших моделях классификации (например, в классическом перцептроне).

Пример: Для \(z = -1\), вычислить \(H(-1) = 0\). Для \(z = 0.5\), \(H(0.5) = 1\).

Реализация:


import numpy as np

def heaviside(z):
  """Вычисляет ступенчатую функцию Хевисайда."""
  return np.where(z >= 0, 1, 0)
  # Или просто: return (z >= 0).astype(int)

print(heaviside(-1))
print(heaviside(0.5))
print(heaviside(0))

6.7. Активация Leaky ReLU

\[ \text{Leaky ReLU}(z) = \begin{cases} z, & z \ge 0 \\ \alpha z, & z < 0 \end{cases} \] (где \(\alpha\) - малый положительный коэффициент, например, 0.01)

Объяснение: Leaky ReLU пропускает небольшой градиент для отрицательных входов (в отличие от ReLU, где градиент 0), смягчая проблему "мертвых нейронов".

Пример: Для \(z = -1\) и \(\alpha = 0.01\), вычислить Leaky ReLU\((-1) = 0.01 \times (-1) = -0.01\).

Реализация:


import numpy as np

def leaky_relu(z, alpha=0.01):
  """Вычисляет функцию Leaky ReLU."""
  return np.where(z >= 0, z, alpha * z)

print(leaky_relu(-1))
print(leaky_relu(2))

6.8. Активация ELU (Exponential Linear Unit)

\[ \text{ELU}(z) = \begin{cases} z, & z \ge 0 \\ \alpha (e^z - 1), & z < 0 \end{cases} \] (где \(\alpha > 0\), часто \(\alpha=1\))

Объяснение: ELU сглаживает ReLU, обеспечивая экспоненциальные выходы для отрицательных входов, что может улучшать прохождение градиента и производительность.

Пример: Для \(z = -1\) и \(\alpha = 1\), вычислить ELU\((-1) = 1 \cdot (e^{-1} - 1) \approx 0.3678 - 1 = -0.6322\).

Реализация:


import numpy as np

def elu(z, alpha=1.0):
  """Вычисляет функцию ELU."""
  return np.where(z >= 0, z, alpha * (np.exp(z) - 1))

print(elu(-1))
print(elu(2))

6.9. Функция Softmax

\[ \text{Softmax}(\mathbf{z})_i = \frac{e^{z_i}}{\sum_{j=1}^{n} e^{z_j}} \] (где \(\mathbf{z}\) - вектор "сырых" выходов (логитов), n - количество классов)

Объяснение: Softmax нормализует вектор в распределение вероятностей по n классам (сумма элементов равна 1). Обычно используется в выходном слое для многоклассовой классификации.

Пример: Для \(\mathbf{z} = [1, 2, 3]\), вычислить Softmax(\(\mathbf{z}\)). \(e^1 \approx 2.718, e^2 \approx 7.389, e^3 \approx 20.086\). Сумма \(\approx 30.193\). Softmax \(\approx [2.718/30.193, 7.389/30.193, 20.086/30.193] \approx [0.090, 0.245, 0.665]\).

Реализация (с числовой стабильностью):


import numpy as np

def softmax(z):
  """Вычисляет функцию Softmax с улучшенной числовой стабильностью."""
  # Вычитаем максимум для предотвращения больших значений в экспоненте
  exp_z = np.exp(z - np.max(z, axis=-1, keepdims=True))
  # Нормализуем
  return exp_z / np.sum(exp_z, axis=-1, keepdims=True)

z_vector = np.array([1, 2, 3])
probabilities = softmax(z_vector)
print(f"Softmax probabilities: {probabilities}") # [0.09003057 0.24472847 0.66524096]
print(f"Sum of probabilities: {np.sum(probabilities)}") # Должно быть 1.0

6.10. Функция потерь для многоклассовой классификации (Cross-Entropy)

\[ \mathcal{L} = -\frac{1}{n} \sum_{i=1}^{n} \sum_{j=1}^{k} y_{ij} \log(\hat{y}_{ij}) \] (Аналогично 5.15)

Объяснение: Потери перекрестной энтропии измеряют расхождение между предсказанными вероятностями и истинными метками (one-hot) в многоклассовой классификации.

Пример: Для \(y = [1, 0, 0]\) и \(\hat{y} = [0.8, 0.1, 0.1]\), потери для этого образца \( = -\log(0.8) \approx 0.223\).

Реализация (средние потери по батчу):


import numpy as np

# y_true_one_hot - истинные метки (n_samples, n_classes)
# y_pred_proba - предсказанные вероятности (n_samples, n_classes)
def categorical_crossentropy(y_true_one_hot, y_pred_proba):
    """Вычисляет категориальную перекрестную энтропию."""
    epsilon = 1e-15
    y_pred_clipped = np.clip(y_pred_proba, epsilon, 1 - epsilon)
    # Выбираем только предсказанную вероятность для истинного класса
    # или делаем полное суммирование, если y_true_one_hot разрежен
    sample_losses = -np.sum(y_true_one_hot * np.log(y_pred_clipped), axis=1)
    return np.mean(sample_losses)

# Пример из 5.15
y_true = np.array([[1, 0, 0], [0, 1, 0]])
y_pred = np.array([[0.8, 0.1, 0.1], [0.2, 0.7, 0.1]])
loss_val = categorical_crossentropy(y_true, y_pred)
print(f"Categorical Cross-Entropy: {loss_val}") # ~ 0.29

6.11. Градиентный спуск для нейронных сетей

\[ \boldsymbol{\theta}^{(t+1)} = \boldsymbol{\theta}^{(t)} - \eta \frac{\partial \mathcal{L}}{\partial \boldsymbol{\theta}} \] (где \(\boldsymbol{\theta}\) представляет все обучаемые параметры сети (веса, смещения), \(\mathcal{L}\) - функция потерь)

Объяснение: Градиентный спуск обновляет веса сети путем минимизации функции потерь, используя градиенты, вычисленные (обычно) с помощью обратного распространения ошибки.

Пример: Обновить \(\boldsymbol{\theta}\) для потерь \(\mathcal{L} = (y - \hat{y})^2\).

Реализация (концептуально):


# Псевдокод для одного шага обновления
# 1. Выполнить прямое распространение, чтобы получить предсказание y_hat
#    y_hat = model(X, theta)
# 2. Вычислить потери L
#    loss = loss_function(y_true, y_hat)
# 3. Вычислить градиенты dL/dtheta с помощью обратного распространения
#    gradients = compute_gradients(X, y_true, y_hat, theta)
# 4. Обновить параметры
#    theta = theta - learning_rate * gradients

6.12. Обратное распространение ошибки (Градиент для весов)

\[ \frac{\partial \mathcal{L}}{\partial w_{ij}} = \delta_j a_i \] \[ \delta_j = \frac{\partial \mathcal{L}}{\partial z_j} = \left( \sum_k \frac{\partial \mathcal{L}}{\partial z_k^{l+1}} \frac{\partial z_k^{l+1}}{\partial a_j} \right) \sigma'(z_j) \quad \text{(для скрытого слоя)} \] \[ \delta_j = \frac{\partial \mathcal{L}}{\partial \hat{y}_j} \sigma'(z_j) \quad \text{(для выходного слоя)} \] (где \(\delta_j\) - ошибка на нейроне j, \(a_i\) - активация нейрона i предыдущего слоя, \(z_j\) - взвешенная сумма на нейроне j, \(\sigma'\) - производная функции активации)

Объяснение: Обратное распространение ошибки вычисляет градиент функции потерь по отношению к весам в нейронной сети, используя цепное правило для распространения ошибки от выхода к входу.

Пример: Вычислить градиенты для однослойной нейронной сети.

Реализация (для одного выходного нейрона и одного скрытого слоя):


import numpy as np

# sigmoid_prime - производная сигмоида
# y_pred - выход сети (после активации)
# y_true - истинное значение
# z_output - взвешенная сумма на выходном нейроне
# a_hidden - активации скрытого слоя (включая смещение, если есть)
# w_output - веса выходного слоя
# z_hidden - взвешенная сумма на скрытых нейронах
# a_input - входы (активации предыдущего слоя)
# w_hidden - веса скрытого слоя

def sigmoid(z): return 1 / (1 + np.exp(-z))
def sigmoid_prime(z): s = sigmoid(z); return s * (1 - s)

# --- Пример данных ---
a_input = np.array([1, 2]) # Вход
w_hidden = np.array([[0.1, 0.3], [0.2, 0.4]]) # Веса скрытого слоя (2 входа -> 2 нейрона)
b_hidden = np.array([0.1, 0.1]) # Смещения скрытого слоя
w_output = np.array([0.5, 0.6]) # Веса выходного слоя (2 скрытых -> 1 выход)
b_output = np.array([0.2]) # Смещение выхода
y_true = np.array([1.0])

# --- Прямое распространение ---
z_hidden = a_input @ w_hidden + b_hidden # [1*0.1+2*0.2+0.1, 1*0.3+2*0.4+0.1] = [0.6, 1.2]
a_hidden = sigmoid(z_hidden) # [sigmoid(0.6), sigmoid(1.2)] ~ [0.6457, 0.7685]
z_output = a_hidden @ w_output + b_output # [0.6457*0.5 + 0.7685*0.6 + 0.2] ~ [0.3228+0.4611+0.2] ~ 0.9839
y_pred = sigmoid(z_output) # sigmoid(0.9839) ~ 0.7279

# --- Обратное распространение (для MSE потерь L = 0.5*(y_true-y_pred)^2) ---
# dL/dy_pred = y_pred - y_true
error_output_layer = y_pred - y_true # ~ 0.7279 - 1.0 = -0.2721

# Ошибка на выходном слое (delta_output)
# delta = dL/dy_pred * dy_pred/dz_output
delta_output = error_output_layer * sigmoid_prime(z_output) # ~ -0.2721 * sigmoid_prime(0.9839) ~ -0.2721 * 0.1977 ~ -0.0538

# Градиент для весов выходного слоя dL/dw_output = delta_output * a_hidden
grad_w_output = np.outer(delta_output, a_hidden) # ~ [-0.0538] outer [0.6457, 0.7685] ~ [[-0.0347, -0.0413]]
grad_b_output = delta_output # Градиент для смещения выхода

# Ошибка на скрытом слое (delta_hidden)
# delta_h = dL/dz_hidden = (dL/dz_output * dz_output/da_hidden) * da_hidden/dz_hidden
#            = (delta_output * w_output) * sigmoid_prime(z_hidden)
error_hidden_layer = delta_output * w_output # Распространение ошибки назад через веса
delta_hidden = error_hidden_layer * sigmoid_prime(z_hidden) # ~ (-0.0538 * [0.5, 0.6]) * sigmoid_prime([0.6, 1.2]) ~ [-0.0064, -0.0058]

# Градиент для весов скрытого слоя dL/dw_hidden = delta_hidden * a_input
grad_w_hidden = np.outer(a_input, delta_hidden) # ~ [1, 2] outer [-0.0064, -0.0058] ~ [[-0.0064, -0.0058], [-0.0128, -0.0116]]
grad_b_hidden = delta_hidden # Градиент для смещений скрытого слоя

print(f"Gradient w_output: {grad_w_output}")
print(f"Gradient w_hidden: {grad_w_hidden}")

6.13. Потери среднеквадратичной ошибки (MSE Loss)

\[ \mathcal{L} = \frac{1}{n} \sum_{i=1}^{n} (y_i - \hat{y}_i)^2 \] (Аналогично 5.3)

Объяснение: Среднеквадратичная ошибка измеряет среднюю квадратичную разницу между предсказаниями и фактическими значениями, часто используется в задачах регрессии с нейронными сетями.

Пример: Для \(y = [1, 2]\) и \(\hat{y} = [1.1, 1.8]\), вычислить потери. \(\mathcal{L} = \frac{1}{2} [(1-1.1)^2 + (2-1.8)^2] = \frac{1}{2}[(-0.1)^2 + (0.2)^2] = \frac{1}{2}[0.01 + 0.04] = 0.025\).

Реализация:


import numpy as np

y_true = np.array([1, 2])
y_pred = np.array([1.1, 1.8])

loss = np.mean((y_true - y_pred)**2)
print(f"MSE Loss: {loss}") # 0.025

6.14. Потери бинарной перекрестной энтропии (Binary Cross-Entropy Loss)

\[ \mathcal{L} = -\frac{1}{n} \sum_{i=1}^{n} [y_i \log(\hat{y}_i) + (1 - y_i) \log(1 - \hat{y}_i)] \] (Аналогично 5.14)

Объяснение: Бинарная перекрестная энтропия измеряет разницу между предсказанными вероятностями и истинными бинарными метками. Стандартная функция потерь для задач бинарной классификации.

Пример: Для \(y = [1, 0]\) и \(\hat{y} = [0.9, 0.1]\), вычислить потери \(\approx 0.105\).

Реализация: См. реализацию в 5.14.

6.15. Батч-нормализация (Batch Normalization)

\[ \hat{x} = \frac{x - \mu_B}{\sqrt{\sigma_B^2 + \epsilon}} \] \[ y = \gamma \hat{x} + \beta \] (где \(\mu_B, \sigma_B^2\) - среднее и дисперсия по мини-батчу, \(\epsilon\) - малая константа, \(\gamma, \beta\) - обучаемые параметры масштаба и сдвига)

Объяснение: Батч-нормализация нормализует входы слоя (активации предыдущего слоя) по мини-батчу, уменьшая внутренний ковариантный сдвиг и ускоряя обучение.

Пример: Нормализовать \(x = [1, 2, 3]\) в батче. \(\mu_B = 2\), \(\sigma_B^2 = \frac{(1-2)^2+(2-2)^2+(3-2)^2}{3} = \frac{1+0+1}{3} = 2/3\). Если \(\gamma = 1, \beta = 0, \epsilon \approx 0\), то \(\hat{x} = \frac{[1,2,3] - 2}{\sqrt{2/3}} = \frac{[-1, 0, 1]}{\sqrt{2/3}}\). \(y = \hat{x}\).

Реализация (прямой проход во время обучения):


import numpy as np

# x - входной батч (n_samples, n_features)
# gamma, beta - обучаемые параметры (n_features,)
# epsilon - малая константа
def batch_norm_forward(x, gamma, beta, epsilon=1e-5):
    """Прямой проход батч-нормализации."""
    # Вычисляем среднее и дисперсию по батчу (по оси образцов)
    mean = np.mean(x, axis=0)
    variance = np.var(x, axis=0)

    # Нормализуем
    x_norm = (x - mean) / np.sqrt(variance + epsilon)

    # Масштабируем и сдвигаем
    out = gamma * x_norm + beta

    # Сохраняем среднее и дисперсию для использования на этапе предсказания (inference)
    # и для обратного распространения (не показано здесь)
    cache = (x, mean, variance, x_norm, gamma, epsilon)
    return out, cache

# Пример
x_batch = np.array([[1.0], [2.0], [3.0]]) # 3 образца, 1 признак
gamma_bn = np.array([1.0]) # Обучаемый параметр
beta_bn = np.array([0.0])  # Обучаемый параметр

output, bn_cache = batch_norm_forward(x_batch, gamma_bn, beta_bn)
print(f"Batch Norm Output:\n{output}")
# mean = 2.0
# var = 2/3 = 0.666...
# x_norm = ([1,2,3]-2)/sqrt(2/3+eps) ~ [-1,0,1]/0.816 ~ [-1.22, 0, 1.22]
# out = 1 * x_norm + 0

Примечание: На этапе предсказания используются скользящие средние \(\mu\) и \(\sigma^2\), вычисленные во время обучения.

6.16. Dropout регуляризация

\[ \hat{a}_i = \begin{cases} 0, & \text{с вероятностью } p \\ \frac{a_i}{1-p}, & \text{иначе (с вероятностью } 1-p) \end{cases} \] (где \(a_i\) - активация нейрона, p - вероятность "выключения" нейрона)

Объяснение: Dropout случайным образом обнуляет долю \(p\) активаций нейронов во время обучения, чтобы предотвратить переобучение и способствовать построению более робастных признаков.

Пример: Применить dropout к активациям \(a = [1, 2, 3]\) с \(p = 0.5\). Каждый элемент обнуляется с вероятностью 0.5. Оставшиеся элементы делятся на \(1-p = 0.5\). Возможный результат: \([0, 4, 0]\) или \([2, 0, 6]\) и т.д.

Реализация (во время обучения):


import numpy as np

# a - входные активации
# p - вероятность dropout
def dropout_forward(a, p):
    """Применяет Dropout во время обучения."""
    # Создаем маску dropout (True - сохраняем, False - обнуляем)
    mask = (np.random.rand(*a.shape) > p)
    # Применяем маску и масштабируем оставшиеся
    # Деление на (1-p) - это "inverted dropout", чтобы на этапе предсказания
    # не нужно было ничего масштабировать.
    a_dropout = a * mask / (1.0 - p)
    return a_dropout, mask # Маска нужна для обратного распространения

# Пример
activations = np.array([1, 2, 3, 4, 5, 6])
dropout_prob = 0.5

output_dropout, dropout_mask = dropout_forward(activations, dropout_prob)
print(f"Activations: {activations}")
print(f"Dropout Mask (1=keep, 0=drop): {dropout_mask.astype(int)}")
print(f"Output after Dropout: {output_dropout}")

Примечание: Во время предсказания (inference) dropout не применяется, или используется масштабирование всех активаций на \((1-p)\), если при обучении масштабирование не делалось. С "inverted dropout" на этапе предсказания делать ничего не нужно.

6.17. Градиент сигмоида

\[ \sigma'(z) = \sigma(z)(1 - \sigma(z)) \]

Объяснение: Производная сигмоидной функции используется при обратном распространении ошибки для эффективного вычисления градиентов. Она выражается через значение самой сигмоидной функции.

Пример: Для \(z = 0.5\), вычислить \(\sigma'(0.5)\). \(\sigma(0.5) \approx 0.622\). \(\sigma'(0.5) \approx 0.622 (1 - 0.622) \approx 0.622 \times 0.378 \approx 0.235\).

Реализация:


import numpy as np

def sigmoid(z): return 1 / (1 + np.exp(-z))

def sigmoid_prime(z):
  """Вычисляет производную сигмоида."""
  s = sigmoid(z)
  return s * (1 - s)

print(sigmoid_prime(0.5)) # ~ 0.235

6.18. RMSProp для обновления весов

\[ s_{w}^{(t+1)} = \beta s_{w}^{(t)} + (1 - \beta) (\nabla_w \mathcal{L}^{(t)})^2 \] \[ w^{(t+1)} = w^{(t)} - \frac{\eta}{\sqrt{s_{w}^{(t+1)}} + \epsilon} \nabla_w \mathcal{L}^{(t)} \] (Аналогично 4.5, но применяется к градиенту \(\nabla_w \mathcal{L}\) по весам \(w\))

Объяснение: RMSProp адаптирует скорость обучения для каждого веса на основе скользящего среднего квадратов градиентов по этому весу.

Реализация (один шаг обновления):


import numpy as np

# grad_w - градиент потерь по весам w
# w - текущие веса
# s - текущее скользящее среднее квадратов градиентов для w
# eta - скорость обучения
# beta - коэффициент затухания для s
# epsilon - малая константа
def rmsprop_update_step(grad_w, w, s, eta, beta, epsilon):
    """Один шаг обновления весов с помощью RMSProp."""
    # Обновляем скользящее среднее квадратов градиентов
    new_s = beta * s + (1 - beta) * grad_w**2
    # Обновляем веса
    new_w = w - eta / (np.sqrt(new_s) + epsilon) * grad_w
    return new_w, new_s

# Пример использования внутри цикла обучения:
# s_w = np.zeros_like(w) # Инициализация s для весов w
# for _ in range(num_steps):
#     grad_w = compute_gradient_w(...) # Вычисляем градиент
#     w, s_w = rmsprop_update_step(grad_w, w, s_w, eta, beta, epsilon)

6.19. Инициализация Ксавье (Глорота)

\[ w \sim U\left(-\sqrt{\frac{6}{n_{\text{in}} + n_{\text{out}}}}, \sqrt{\frac{6}{n_{\text{in}} + n_{\text{out}}}}\right) \] (где \(n_{\text{in}}\) - количество входов нейрона/слоя, \(n_{\text{out}}\) - количество выходов)

Объяснение: Инициализация Ксавье (или Глорота) устанавливает начальные веса таким образом, чтобы поддерживать примерно одинаковую дисперсию активаций и градиентов между слоями, улучшая сходимость в глубоких сетях.

Реализация:


import numpy as np

def xavier_glorot_uniform_init(n_in, n_out):
    """Инициализация весов Ксавье/Глорота (равномерное распределение)."""
    limit = np.sqrt(6 / (n_in + n_out))
    weights = np.random.uniform(-limit, limit, size=(n_in, n_out))
    return weights

# Пример
input_dim = 784 # Например, размер входа MNIST
output_dim = 128 # Размер скрытого слоя
weights_matrix = xavier_glorot_uniform_init(input_dim, output_dim)
print(f"Shape of initialized weights: {weights_matrix.shape}")
print(f"Sample weights range: min={np.min(weights_matrix):.4f}, max={np.max(weights_matrix):.4f}")
limit_calc = np.sqrt(6/(784+128))
print(f"Expected limit: +/- {limit_calc:.4f}")

Примечание: Существует также вариант с нормальным распределением.

6.20. L2 Регуляризация (Затухание весов / Weight Decay)

\[ \mathcal{L} = \mathcal{L}_0 + \frac{\lambda}{2} \|\mathbf{w}\|_2^2 \] (где \(\mathcal{L}_0\) - исходная функция потерь, \(\lambda\) - коэффициент регуляризации, \(\|\mathbf{w}\|_2^2\) - сумма квадратов всех весов)

Объяснение: L2 регуляризация добавляет штраф, пропорциональный квадрату нормы весов, к функции потерь, чтобы предотвратить переобучение и сделать модель проще.

Реализация (добавление к потерям и градиенту):


# При вычислении потерь:
# total_loss = original_loss + (lambda_l2 / 2) * np.sum(weights**2)

# При вычислении градиента (для обновления весов):
# grad_original = compute_original_gradient(...)
# grad_total = grad_original + lambda_l2 * weights
# weights -= learning_rate * grad_total

6.21. Heaviside против Hard Sigmoid

\[ \text{Hard Sigmoid}(z) = \max(0, \min(1, 0.2z + 0.5)) \]

Объяснение: Функция Хевисайда (см. 6.6) является бинарной функцией активации. Hard Sigmoid аппроксимирует сигмоидную функцию кусочно-линейной функцией, что может быть вычислительно более эффективно.

Реализация (Hard Sigmoid):


import numpy as np

def hard_sigmoid(z):
  """Вычисляет функцию Hard Sigmoid."""
  return np.clip(0.2 * z + 0.5, 0, 1)

z_values = np.array([-3, -2, 0, 2, 3])
print(f"Hard Sigmoid outputs: {hard_sigmoid(z_values)}") # [0. 0.1 0.5 0.9 1. ]

6.22. Активация Swish

\[ \text{Swish}(z) = z \cdot \sigma(z) = \frac{z}{1 + e^{-z}} \]

Объяснение: Swish - это гладкая, немонотонная функция активации, которая часто превосходит ReLU в глубоких сетях.

Реализация:


import numpy as np

def sigmoid(z): return 1 / (1 + np.exp(-z))

def swish(z):
  """Вычисляет функцию Swish."""
  return z * sigmoid(z)

print(swish(np.array([-1, 0, 1, 2]))) # [-0.26894142  0.          0.73105858  1.76159416]

6.23. Активация Maxout

\[ \text{Maxout}(\mathbf{z}_1, \dots, \mathbf{z}_k)_j = \max_{i \in [1, k]} (\mathbf{z}_i)_j \] (где \(\mathbf{z}_i = W_i x + b_i\) - выходы k линейных преобразований, max берется поэлементно)

Объяснение: Maxout выбирает максимальное значение из k линейных функций, позволяя нейрону обучаться кусочно-линейной аппроксимации произвольной выпуклой функции.

Реализация (прямой проход):


import numpy as np

# z_list - список или массив выходов k линейных преобразований
# Например, z_list имеет форму (k, n_samples, n_output_neurons)
# Или (n_samples, k, n_output_neurons)
def maxout(z_outputs):
    """Вычисляет активацию Maxout."""
    # np.max по оси, соответствующей k линейным функциям
    # Если форма (k, ...), то axis=0
    # Если форма (..., k, ...), то axis=k_axis_index
    return np.max(z_outputs, axis=0) # Пример для формы (k, ...)

# Пример
z1 = np.array([[1, -2], [3, 0]]) # Выход 1-й линейной части для 2 нейронов, 2 образцов
z2 = np.array([[0, 4], [-1, 2]]) # Выход 2-й линейной части
z_list = np.array([z1, z2]) # Форма (k=2, n_samples=2, n_neurons=2)

maxout_output = maxout(z_list)
print(f"Maxout output:\n{maxout_output}")
# [[max(1,0), max(-2,4)], [max(3,-1), max(0,2)]] = [[1, 4], [3, 2]]

6.24. Разреженная категориальная перекрестная энтропия (Sparse Categorical Cross-Entropy)

\[ \mathcal{L} = -\frac{1}{n} \sum_{i=1}^{n} \log(\hat{y}_{i, y_i}) \] (где \(y_i\) - индекс истинного класса для i-го образца, \(\hat{y}_{i, y_i}\) - предсказанная вероятность истинного класса для i-го образца)

Объяснение: Разреженная категориальная перекрестная энтропия упрощает вычисление потерь для многоклассовой классификации, когда истинные метки представлены не как one-hot векторы, а как индексы классов. Она напрямую индексирует предсказанную вероятность истинного класса.

Реализация:


import numpy as np

# y_true_indices - индексы истинных классов (n_samples,)
# y_pred_proba - матрица предсказанных вероятностей (n_samples, n_classes)
def sparse_categorical_crossentropy(y_true_indices, y_pred_proba):
    """Вычисляет разреженную категориальную перекрестную энтропию."""
    n_samples = len(y_true_indices)
    epsilon = 1e-15
    y_pred_clipped = np.clip(y_pred_proba, epsilon, 1 - epsilon)

    # Выбираем вероятности, соответствующие истинным классам
    # Используем расширенное индексирование numpy
    log_likelihoods = -np.log(y_pred_clipped[np.arange(n_samples), y_true_indices])

    # Усредняем по образцам
    return np.mean(log_likelihoods)

# Пример
y_true_idx = np.array([0, 1]) # Классы 0 и 1
y_pred_p = np.array([[0.8, 0.1, 0.1], [0.2, 0.7, 0.1]]) # Вероятности

loss_val = sparse_categorical_crossentropy(y_true_idx, y_pred_p)
print(f"Sparse Categorical Cross-Entropy: {loss_val}")
# Образец 0: истинный класс 0, предсказание 0.8. Потеря -log(0.8) ~ 0.223
# Образец 1: истинный класс 1, предсказание 0.7. Потеря -log(0.7) ~ 0.357
# Среднее: (0.223 + 0.357) / 2 ~ 0.29

6.25. Косинусное сходство / Косинусные потери

\[ \text{Cosine Similarity}(\mathbf{u}, \mathbf{v}) = \frac{\mathbf{u} \cdot \mathbf{v}}{\|\mathbf{u}\| \|\mathbf{v}\|} \] \[ \text{Cosine Loss} = 1 - \text{Cosine Similarity} \quad (\text{или другие формы}) \]

Объяснение: Косинусное сходство измеряет косинус угла между векторами, часто используется для определения сходства текстов или эмбеддингов (независимо от их магнитуды). Косинусные потери стремятся минимизировать угол (максимизировать сходство).

Реализация (Косинусное сходство):


import numpy as np

def cosine_similarity(u, v):
    """Вычисляет косинусное сходство между двумя векторами."""
    dot_product = np.dot(u, v)
    norm_u = np.linalg.norm(u)
    norm_v = np.linalg.norm(v)
    # Избегаем деления на ноль
    if norm_u == 0 or norm_v == 0:
        return 0.0
    return dot_product / (norm_u * norm_v)

# Пример
u_vec = np.array([1, 2, 0])
v_vec = np.array([2, 4, 1])

sim = cosine_similarity(u_vec, v_vec)
print(f"Cosine Similarity: {sim}")
# u.v = 1*2 + 2*4 + 0*1 = 10
# ||u|| = sqrt(1+4) = sqrt(5)
# ||v|| = sqrt(4+16+1) = sqrt(21)
# sim = 10 / (sqrt(5)*sqrt(21)) = 10 / sqrt(105) ~ 0.9759

# Косинусные потери (пример)
cosine_loss = 1 - sim
print(f"Cosine Loss (1 - sim): {cosine_loss}")
Предыдущая глава    Следующая глава

Другие статьи по этой теме: