Почему увеличение сложности нейронных сетей чаще достигается путем добавления новых слоев, а не увеличением количества нейронов в каждом слое?
Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Почему увеличение сложности сетей достигается добавлением слоёв, а не нейронов?
Это фундаментальный вопрос глубокого обучения. Дело в том, что глубина (количество слоёв) и ширина (количество нейронов) имеют совершенно разные эффекты на способность модели к обучению и обобщению.
Теория: Universal Approximation Theorem
Universal Approximation Theorem говорит, что одна полносвязная слой с достаточным количеством нейронов может приблизить любую непрерывную функцию:
# Теоретически, эта сеть может приблизить любую функцию:
model = Sequential([
Dense(1000, activation=relu), # один широкий слой
Dense(1) # выходной слой
])
Однако на практике это работает плохо. Почему?
1. Экспоненциальное улучшение с глубиной
Глубина = экспоненциальное расширение представительной способности
import numpy as np
import matplotlib.pyplot as plt
# Пример: как сеть учится представлять функции
# Узкая но глубокая сеть (несколько слоёв, мало нейронов)
# Может выучить сложные функции композицией простых преобразований
def narrow_deep(x):
"""Композиция простых нелинейных преобразований"""
for _ in range(10): # 10 слоёв
x = np.tanh(x * 2) # каждый слой применяет нелинейность
return x
# Широкая но мелкая сеть (много нейронов, один слой)
# Ограничена линейным преобразованием + одна нелинейность
def wide_shallow(x):
"""Один слой = одно линейное преобразование + одна нелинейность"""
return np.tanh(x @ np.random.randn(1000, 1))
# narrow_deep может выучить намного более сложные функции
2. Иерархическое представление признаков
В глубоких сетях каждый слой строит всё более абстрактные представления:
import tensorflow as tf
from tensorflow.keras import layers
# Пример: распознавание кошек
# Глубокая иерархическая архитектура
model_deep = tf.keras.Sequential([
layers.Conv2D(32, 3, activation=relu, input_shape=(224, 224, 3)),
# Слой 1: находит пиксели, края (детали низкого уровня)
layers.Conv2D(64, 3, activation=relu),
# Слой 2: находит углы, текстуры
layers.Conv2D(128, 3, activation=relu),
# Слой 3: находит части (уши, нос, глаза)
layers.Conv2D(256, 3, activation=relu),
# Слой 4: находит объекты (морда, хвост, лапы)
layers.Flatten(),
layers.Dense(512, activation=relu),
# Слой 5: высокоуровневые концепции (это кошка?)
layers.Dense(1, activation=sigmoid) # Ответ
])
# Неправильный подход: один очень широкий слой
model_wide = tf.keras.Sequential([
layers.Flatten(input_shape=(224, 224, 3)), # 150K параметров на вход
layers.Dense(10000, activation=relu), # один неявный слой
layers.Dense(1, activation=sigmoid)
])
print(f"Deep model params: {model_deep.count_params():,}")
print(f"Wide model params: {model_wide.count_params():,}")
# Deep: несколько миллионов (управляемо)
# Wide: может быть десятки миллионов (плохо обобщается)
3. Параметры и обобщение
# Empirical Rule для количества параметров:
# - Нужно как минимум 10-100 примеров на каждый параметр для обучения
# - Если параметров > примеров / 10, риск переобучения
import torch
import torch.nn as nn
# Датасет: 10K изображений
dataset_size = 10_000
# ПЛОХАЯ архитектура: один слой с миллионом нейронов
bad_model = nn.Sequential(
nn.Flatten(),
nn.Linear(224*224*3, 1_000_000), # 150M параметров!
nn.ReLU(),
nn.Linear(1_000_000, 1)
)
# Параметров: 150M >> 10K примеров
# Результат: тяжелое переобучение, плохая обобщаемость
# ХОРОШАЯ архитектура: много слоёв, меньше нейронов
good_model = nn.Sequential(
nn.Conv2d(3, 64, 3, padding=1),
nn.ReLU(),
nn.Conv2d(64, 128, 3, padding=1),
nn.ReLU(),
nn.Conv2d(128, 256, 3, padding=1),
nn.ReLU(),
nn.AdaptiveAvgPool2d((1, 1)),
nn.Flatten(),
nn.Linear(256, 128),
nn.ReLU(),
nn.Linear(128, 1)
)
# Параметров: ~1M << 10K примеров
# Результат: хорошее обобщение
4. Проблема затухающих/взрывающихся градиентов
Глубокие сети требуют осторожности с инициализацией, но это решаемо:
import tensorflow as tf
from tensorflow.keras import layers
# Проблема: широкие мелкие сети нестабильны
model_unstable = tf.keras.Sequential([
layers.Dense(5000, activation=sigmoid), # Sigmoid вызывает затухание
layers.Dense(5000, activation=sigmoid),
layers.Dense(1)
])
# Решение 1: Правильная инициализация
model_good = tf.keras.Sequential([
layers.Dense(256, activation=relu, kernel_initializer=he_normal),
layers.BatchNormalization(),
layers.Dense(256, activation=relu, kernel_initializer=he_normal),
layers.BatchNormalization(),
layers.Dense(256, activation=relu, kernel_initializer=he_normal),
layers.BatchNormalization(),
layers.Dense(1)
])
# Решение 2: Skip connections (ResNet)
class ResidualBlock(tf.keras.layers.Layer):
def __init__(self, units):
super().__init__()
self.dense1 = layers.Dense(units, activation=relu)
self.dense2 = layers.Dense(units)
def call(self, x):
residual = x
x = self.dense1(x)
x = self.dense2(x)
return x + residual # Skip connection помогает градиентам
model_skip = tf.keras.Sequential([
layers.Dense(256, activation=relu),
ResidualBlock(256),
ResidualBlock(256),
ResidualBlock(256),
layers.Dense(1)
])
5. Эмпирические результаты: Deep vs Wide
import tensorflow as tf
from tensorflow.keras import layers, models
# Эксперимент: обучаем две модели
# Модель A: Глубокая (11 слоёв, 32-64 нейронов)
model_deep = models.Sequential([
layers.Dense(64, activation=relu, input_shape=(28*28,)),
*[layers.Dense(64, activation=relu) for _ in range(10)],
layers.Dense(10, activation=softmax)
])
# Параметров: ~64*64*11 = 45K
# Модель B: Широкая (3 слоя, 1024 нейронов)
model_wide = models.Sequential([
layers.Dense(1024, activation=relu, input_shape=(28*28,)),
layers.Dense(1024, activation=relu),
layers.Dense(10, activation=softmax)
])
# Параметров: ~785K*1024 + 1024*1024 + 1024*10 = ~800K
print(f"Deep: {model_deep.count_params():,} params")
print(f"Wide: {model_wide.count_params():,} params")
# На датасете MNIST (70K примеров):
# Deep обучается быстро, хорошо обобщается (>99% accuracy)
# Wide требует больше памяти, медленнее, хуже обобщается
6. Практический пример: Архитектура ResNet vs VGG
# VGG: много слоёв, одинаковый размер слоёв (широкая)
# 16-19 слоёв, каждый ~512 нейронов
# 138M параметров (тяжело, медленнее)
# ResNet: много слоёв, меньше параметров благодаря skip connections
# 50-152 слоя, прогрессивно растущие
# 25M параметров (легче, быстрее, лучше)
from torchvision import models
import torch
vgg = models.vgg16() # 138M параметров
resnet = models.resnet50() # 25.5M параметров
# ResNet ГЛУБЖЕ (50 слоёв vs 16) но МЕНЬШЕ параметров
# Результат: ResNet работает лучше!
7. Теория: Lottery Ticket Hypothesis
Lottery Ticket Hypothesis предполагает, что большие сети содержат подсети («выигрышные билеты»), которые обучаются хорошо. Узкая сеть может найти эту подсеть, но только если она достаточно глубокая:
# Узкая мелкая сеть: может не содержать нужную подсеть
# Узкая глубокая сеть: содержит через композицию нелинейностей
# Широкая мелкая сеть: требует большого количества параметров
Ключевые выводы
-
Глубина = экспоненциальная способность
- Каждый слой может делать нелинейное преобразование
- Композиция слоёв даёт экспоненциальный рост выразительности
-
Ширина = линейный рост параметров
- Добавление 1000 нейронов = добавление ~1М параметров
- Больше параметров = выше риск переобучения
-
Иерархическое обучение
- Нижние слои учат низкоуровневые признаки
- Верхние слои строят абстракции
- Одна широкая сеть не может это сделать эффективно
-
Эффективность памяти и вычислений
- Глубокая узкая сеть: меньше параметров, меньше памяти
- Может быть быстрее благодаря BatchNormalization
-
Практика Modern Deep Learning
- ResNet: 50-152 слоя (глубокая и узкая)
- Transformer: 12-96 слоёв, 768-3072 размер скрытого слоя
- Vision Transformer: 12-24 слоя
Итого: Глубина — путь к сложности. Ширина — путь к переобучению.