Глава 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}")