19 / 27 · Нейронные сети
От линейных моделей к нейросетям
Почему композиции линейных отображений нужна нелинейность, многослойный перцептрон с его слоями, размерностями и функциями активации, что обещает теорема об универсальной аппроксимации и чего не обещает, глубина против ширины, сеть, которая сама выучивает признаки, а также выходные слои и функции потерь для регрессии и классификации.
Градиентный бустинг · Логистическая регрессия · Многоклассовая и многометочная классификация
На этой странице 13
Предыдущая заметка закончилась самым сильным инструментом для таблиц: бустингом деревьев и стекингом поверх него. Все модели курса до сих пор получали признаки готовыми. Метод наименьших квадратов и логистическая регрессия подгоняют линейную функцию от них, ядра заранее фиксируют нелинейное отображение признаков, а деревья делят выборку по столбцам, которые кто-то подготовил. Это работает, когда столбцы уже что-то значат: доход, возраст, число покупок. Для сырых сигналов это не работает. Отдельный пиксель, отдельный отсчёт звука или номер отдельного токена сами по себе обычно говорят мало, и десятилетиями полезные признаки изображений, звука и текста конструировали вручную: гистограммы границ, спектральные коэффициенты, счётчики n-грамм. Нейронная сеть (neural network) выучивает отображение признаков по данным вместе с линейной моделью поверх него — тем же градиентным спуском, что и раньше. В этой заметке сеть строится из линейных моделей, обсуждается, что такие сети способны представить, и задаются их выходы и функции потерь; следующая заметка вычисляет их градиенты.
Обозначения. Вход — . Сеть состоит из слоёв ширины , причём . У слоя есть матрица весов и смещение ; он вычисляет предактивацию (pre-activation) и активацию (activation) , где , а функция активации применяется к каждой координате. У последнего слоя активации нет: его выход — это предсказание или вектор логитов (logits). — логистическая сигмоида, — совокупность всех весов и смещений.
Почему композиции линейных слоёв недостаточно#
Первая идея — составить композицию линейных отображений. Она ничего не даёт: два аффинных слоя — это один аффинный слой,
и по индукции то же верно для любого их числа. Глубина без нелинейности добавляет параметры, но не выразительность: композиция — это линейная модель с матрицей весов , ранг которой не больше наименьшей ширины, так что узкий слой может её только ограничить.
Нелинейность между слоями это меняет. С одним скрытым слоем
выход — линейная модель, применённая к признакам , и эти признаки сами выучиваются.
Разобранный пример: XOR. Четыре точки с метками нельзя разделить прямой. Если бы неравенство выполнялось ровно для двух точек с меткой 1, то и , тогда как и . Сложив первые два неравенства, получаем — противоречие. Хватает двух ReLU-нейронов, (Goodfellow, Bengio and Courville, 2016, раздел 6.1). Возьмём , и :
| 0 | 0 | 0 | 0 | |
| 1 | 1 | 0 | 1 | |
| 1 | 1 | 0 | 1 | |
| 2 | 2 | 1 | 0 |
В скрытых координатах входы переходят в , , и . Две точки класса 1 попадают в одно место, и классы разделяет прямая, например . Скрытый слой сделал задачу линейной.
Многослойный перцептрон#
Композиция таких слоёв даёт многослойный перцептрон (multilayer perceptron, MLP), который также называют полносвязной сетью или сетью прямого распространения (feedforward network):
Каждый скрытый нейрон (hidden unit) вычисляет для одной строки своей матрицы весов: линейную модель от предыдущего слоя, за которой следует фиксированная нелинейность. При скрытый нейрон имеет функциональный вид логистической регрессии — сигмоиды от аффинной функции, а сеть — линейная модель на признаках такого вида, вычисленных из признаков такого же вида. Аналогия ограничивается видом: у скрытого нейрона нет собственной наблюдаемой метки 0/1, а его веса обучаются совместно с остальной сетью.
На практике сеть обрабатывает сразу мини-батч из объектов, записанных строками матрицы . Тогда каждый слой — одно матричное произведение, , где , а смещение прибавляется к каждой строке. nn.Linear(d_in, d_out) в PyTorch хранит вес размера (d_out, d_in) — это размер в наших обозначениях — и вычисляет x @ W.T + b.
Разобранный пример: размерности. Классификатор изображений цифр на 10 классов с ширинами слоёв :
| слой | вес | смещение | параметры | выход для батча из 32 |
|---|---|---|---|---|
| вход | ||||
| 1 | 128 | 8320 | ||
| 2 | 64 | 8256 | ||
| 3 (логиты) | 10 | 650 |
У сети параметров, почти все они в матрицах весов; их число растёт как произведение ширин соседних слоёв.
Функции активации#
| область значений | типичное применение | |||
|---|---|---|---|---|
| сигмоида | выходные вероятности, гейты | |||
| tanh | рекуррентные сети | |||
| ReLU | при , при | по умолчанию в скрытых слоях | ||
| GELU | трансформеры |
Здесь и — функция распределения и плотность стандартного нормального распределения. Важны три свойства:
- Насыщение. Сигмоида и tanh становятся плоскими при больших , где их производная близка к нулю. Нейрон, попавший в эту область, почти не пропускает градиент назад, а в стопке сигмоидных слоёв производная сигмоиды вносит множитель не больше на каждом слое; это разобрано в курсе о генеративных моделях.
- ReLU (Nair and Hinton, 2010; Glorot, Bordes and Bengio, 2011) имеет наклон ровно 1 везде, где она активна, и дёшево вычисляется. Её слабость — другая сторона: нейрон, у которого предактивация отрицательна на всех входах, получает нулевой градиент и уже не восстанавливается — это «мёртвый» нейрон.
- GELU (Hendrycks and Gimpel, 2016, раздел 2) умножает на вероятность того, что стандартная нормальная величина окажется меньше этого аргумента. Это гладкая ReLU, пропускающая небольшие отрицательные значения: её минимум равен при . BERT и GPT-2 используют её в полносвязной части каждого блока трансформера.
Выбор активации менее важен, чем само наличие нелинейности. Следующий раздел показывает единственное свойство, которого теорема об универсальной аппроксимации для одного скрытого слоя требует от активации: она не должна быть многочленом. Это условие теоремы, а не каждой полезной сети.
Что может представить один скрытый слой#
Дополнительный материал: теоремы о приближении, стоящие за ключевой идеей ниже, и то, чего они не обещают. Остальная часть заметки на него не опирается.
Теорема. Пусть — любая непрерывная сигмоидальная функция, то есть при и при . Для любой непрерывной функции на единичном кубе и любого найдутся число и параметры , , такие что
(Cybenko, 1989, теорема 2). — это MLP с одним скрытым слоем из нейронов и линейным выходом. Hornik (1991), теорема 2, распространяет утверждение на любую непрерывную, ограниченную и непостоянную активацию на любом компакте (замкнутом ограниченном множестве), а Leshno et al. (1993), теорема 1, закрывают вопрос для неограниченных активаций, таких как ReLU: для локально ограниченной кусочно-непрерывной сети с одним скрытым слоем равномерно приближают на компактах любую непрерывную функцию тогда и только тогда, когда — не многочлен. Полиномиальная активация степени не годится, потому что тогда сеть — многочлен степени не выше от , какой бы широкой она ни была.
Разобранный пример: ReLU-нейроны как кусочно-линейное приближение. В одномерном случае построение явное. Расставим узлы на и обозначим через наклон отрезка от до . Ломаная, проходящая через эти точки, равна
это сеть с одним скрытым слоем из ReLU-нейронов: нейрон включается в узле и меняет наклон на . Для наибольшая ошибка на равна , , и при нейронах. Удвоение ширины делит ошибку примерно на 4, как и предсказывает оценка погрешности интерполяции с шагом .
Чего теорема не обещает#
- Разумного размера. Теорема говорит о существовании. Сам Цыбенко в заключении (раздел 5) ожидает, что для большинства задач аппроксимации понадобится астрономическое число нейронов — из-за проклятия размерности, встреченного в заметке о ближайших соседях. Кусочно-линейное построение разбивает вход на ячейки: при 16 узлах на ось в размерности ему нужно ячеек. Хватит ли гораздо меньшего числа нейронов, зависит от функции. Barron (1993) показывает, что для функций, у которых преобразование Фурье имеет конечный первый момент , сигмоидальных нейронов дают интегральную квадратичную ошибку порядка без зависимости от размерности, тогда как никакой фиксированный набор из базисных функций не может быть лучше порядка на этом классе.
- Что обучение его найдёт. Веса существуют; функция потерь как функция от них не выпукла, и градиентный спуск может остановиться в другом месте. Эксперимент с XOR ниже показывает, как это происходит.
- Что хватит выборки. Приблизить истинную функцию и оценить её по зашумлённым примерам — разные задачи. Широкая сеть может подогнаться под любую обучающую выборку вместе с шумом, а это переобучение в чистом виде.
- Ничего вне области. Гарантия действует на компакте. За пределами диапазона обучающих входов ReLU-сеть просто продолжается линейно, а данные там редко ведут себя так.
Глубина против ширины#
Дополнительный материал: почему глубина бывает экспоненциально выгоднее ширины, с точным примером. Остальная часть заметки на него не опирается.
Если одного скрытого слоя достаточно для универсальности, зачем использовать много слоёв? Потому что для некоторых функций композиция гораздо эффективнее сложения. Одномерный пример делает это утверждение точным.
Разобранный пример: функция-«палатка» (tent map). На сеть
задаёт «палатку»: при и при , со значениями в . Композиция с самой собой, , дважды проходит палатку вверх и вниз: 4 линейных куска. Каждая следующая композиция удваивает их число, так что у ровно кусков. Как сеть, имеет скрытых слоёв по 2 ReLU-нейрона (Telgarsky, 2016, лемма 3.10).
Сеть с одним скрытым слоем из ReLU-нейронов вычисляет на прямой . Эта функция линейна между точками излома , поэтому у неё не больше кусков. Чтобы точно представить , ей нужно нейронов: 31 против 10 у глубокой сети при и против 40 при .
Глубина умножает число линейных кусков
Общие утверждения, стоящие за примером: число линейных областей ReLU-сети может расти экспоненциально с глубиной и лишь полиномиально с шириной (Montúfar et al., 2014), и существуют функции, которые вычисляются глубокими узкими ReLU-сетями, но не приближаются никакой сетью с гораздо меньшим числом слоёв, если она не экспоненциально широка (Telgarsky, 2016, теорема 1.1). Глубина позволяет поздним слоям переиспользовать то, что вычислили ранние. В сетях для изображений ранние слои выделяют границы, а поздние собирают из них части и объекты, как описано в разделе о свёртках курса о генеративных моделях.
У глубины есть цена. Каждый слой умножает градиент на ещё один множитель, и глубокие стопки оптимизировать труднее, чем неглубокие; этим занимается заметка «Обучение глубоких сетей». Ширина тоже помогает оптимизации: у более широкой сети гораздо больше способов подогнаться под данные, и градиентный спуск надёжнее находит один из них, как показывает следующий эксперимент.
Сеть сама выучивает признаки#
Построение для XOR выше было придумано вручную. Те же признаки получаются и при обучении. Возьмём 200 точек в четырёх облаках вокруг , где класс 1 — там, где знаки различаются, и сеть с двумя скрытыми нейронами tanh и сигмоидой на выходе, обученную градиентным спуском (Adam, 300 шагов по всей выборке) на бинарной кросс-энтропии (BCE):
Сеть сама находит признаки для XOR
Обученная сеть верно классифицирует все 200 точек. Её первый скрытый нейрон, , отрицателен, только когда обе координаты отрицательны: это «или» (OR). Второй, , положителен, только когда обе координаты положительны: это «и» (AND). Выходной нейрон вычисляет , и это значение велико ровно тогда, когда «или» включено, а «и» выключено. С точностью до гладкой активации это два признака ручного решения, найденные подгонкой.
Однако успех оптимизации не гарантирован. Если обучить ту же сеть с двумя нейронами из 10 случайных инициализаций, классы разделяют лишь 4 запуска; остальные 6 останавливаются в плохом решении с долей правильных ответов (accuracy) 65–75%. С четырьмя скрытыми нейронами успешны все 10 запусков. Дополнительная ширина стоит параметров и покупает более простую задачу оптимизации.
Так же можно прочитать любую сеть: последний слой — линейная модель на признаках , вычисленных слоями под ним. На данных «две луны» из раздела «Код» (1000 точек, шум 0.2, половина отложена) логистическая регрессия на исходных координатах достигает на тесте доли правильных ответов , а сеть — . Новая логистическая регрессия, обученная на 16 признаках последнего скрытого слоя сети, тоже даёт : вся нелинейность сосредоточена в отображении признаков, а классификатор поверх него — логистическая регрессия из прежних заметок.
Выходные слои и функции потерь#
Выходной слой и функцию потерь выбирают вместе, и выбор тот же, вероятностный, что и для линейных моделей: сеть выдаёт параметры распределения , а функция потерь — отрицательный логарифм правдоподобия, как выведено в заметке о методе максимального правдоподобия.
| задача | выходы | выходная активация | модель для | функция потерь | PyTorch |
|---|---|---|---|---|---|
| регрессия | 1 (или ) | нет | квадратичная ошибка | MSELoss | |
| два класса | 1 логит | сигмоида | бинарная кросс-энтропия | BCEWithLogitsLoss | |
| классов | логитов | softmax | кросс-энтропия | CrossEntropyLoss | |
| меток | логитов | сигмоид | независимых бернуллиевских величин | сумма бинарных кросс-энтропий | BCEWithLogitsLoss |
Строка для двух классов превращает последний слой в логистическую регрессию на , а две нижние строки — это softmax-модель и модель «один против остальных» из заметки о многоклассовой классификации, теперь на выученных признаках. При softmax и истинном классе кросс-энтропия равна
Разобранный пример. Для логитов имеем с суммой , так что . Если истинный класс первый, функция потерь равна ; если бы он был третьим, она была бы равна .
Функции потерь PyTorch из таблицы принимают логиты, а не вероятности, и вычисляют как с . Для логитов наивное переполняется, а сдвинутая форма даёт значение функции потерь точно. Если применить сигмоиду или softmax в модели, а функцию потерь — к вероятностям, эта защита теряется. Тот же softmax по словарю служит выходным слоем каждой нейросетевой языковой модели в курсе NLP, а курс о генеративных моделях объясняет, почему минимизация кросс-энтропии минимизирует KL-дивергенцию.
Свёртки: линейный слой с ограничениями#
Когда у входа есть сеточная структура, как у пикселей изображения, отсчётов звука или токенов предложения, полносвязный первый слой тратит параметры впустую и игнорирует локальность. У цветного изображения есть входа; полносвязному слою, выдающему карту с 16 каналами, нужно миллиона весов. Свёрточный слой (convolution layer) вместо этого применяет одно и то же маленькое ядро в каждой позиции: каждый выход смотрит только на окрестность (локальность), а ядро общее для всех позиций (разделение весов, weight sharing), поэтому тот же слой с ядрами имеет параметров. Это по-прежнему линейный слой — с матрицей весов, которая обязана быть разреженной и повторяющейся, — и всё сказанное в этой и следующей заметках к нему применимо. Его размерности, параметры и инициализация разобраны в курсе о генеративных моделях, а курс NLP использует одномерные свёртки по токенам текста.
Что изменилось#
| Линейные модели, ядра, бустинг | Нейронные сети | |
|---|---|---|
| Признаки | даны, сконструированы вручную или заданы ядром | выучиваются скрытыми слоями |
| Модель | линейная по фиксированным признакам или сумма деревьев | композиции линейных отображений и нелинейностей |
| Что может представить | ограничена признаками | любую непрерывную функцию на ограниченной области при достаточном числе нейронов |
| Выход и функция потерь | сигмоида или softmax с кросс-энтропией, квадратичная ошибка | то же самое в последнем слое |
| Обучение | выпуклое (линейная, логистическая) или жадное (деревья) | невыпуклое, градиентным спуском через все слои |
Ограничения#
- Невыпуклое обучение. Разные инициализации дают разные сети, и некоторые запуски неудачны, как у сети для XOR с двумя нейронами в 6 из 10 запусков. Результаты нужно проверять на нескольких начальных значениях генератора (seed).
- Данные и вычисления. У сети много параметров, и для их оценки нужно много примеров. На типичных табличных данных умеренного размера ансамбли деревьев по-прежнему не уступают сетям или превосходят их (Grinsztajn, Oyallon and Varoquaux, 2022); сети выигрывают там, где признаки нужно выучивать из сырых сигналов.
- Существование — не построение. Теорема об универсальной аппроксимации не даёт ни ширины, ни процедуры обучения, ни размера выборки.
- Много проектных решений. Глубина, ширина, активация, инициализация, шаг обучения и регуляризация — всё это важно; последняя заметка раздела собирает стандартные варианты.
- Непрозрачные признаки. Выученный признак — это направление в скрытом слое, а не именованный столбец, поэтому модель труднее объяснить, чем дерево или линейную модель.
Что нужно запомнить#
- Композиция линейных слоёв — линейный слой; глубину делает полезной нелинейность между ними.
- MLP вычисляет с ; последний слой — линейная модель (логиты) на выученных признаках.
- ReLU — скрытая активация по умолчанию, GELU — её гладкий вариант; сигмоида и tanh насыщаются.
- Один скрытый слой приближает любую непрерывную функцию на компакте тогда и только тогда, когда активация — не многочлен (Цыбенко, Хорник, Лешно и др.), но без оценки ширины и без гарантий обучения и обобщения.
- Глубина может быть экспоненциально эффективнее ширины: слоёв по 2 ReLU-нейрона дают линейных кусков, одному слою нужно нейронов.
- Выходной слой и функция потерь следуют из правдоподобия: тождественный выход с квадратичной ошибкой, сигмоида с бинарной кросс-энтропией, softmax с кросс-энтропией; в функцию потерь передаются логиты.
Код#
Схлопывание линейных слоёв, XOR вручную, подсчёт параметров, функция потерь softmax, признаки, выученные на двух лунах, ReLU-интерполяция и функция-палатка:
import numpy as np, torch
from torch import nn
from sklearn.datasets import make_moons
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
rng = np.random.default_rng(0)
relu = lambda z: np.maximum(z, 0)
# два линейных слоя — это один линейный слой: W2 (W1 x + b1) + b2 = (W2 W1) x + (W2 b1 + b2)
W1, b1, W2, b2, x = rng.normal(size=(4, 3)), rng.normal(size=4), rng.normal(size=(2, 4)), rng.normal(size=2), rng.normal(size=3)
print(np.allclose(W2 @ (W1 @ x + b1) + b2, (W2 @ W1) @ x + (W2 @ b1 + b2))) # True
# XOR на двух ReLU-нейронах: h = ReLU(x1 + x2, x1 + x2 - 1), выход h1 - 2 h2
X = np.array([[0, 0], [0, 1], [1, 0], [1, 1]])
h = relu(X @ np.array([[1, 1], [1, 1]]) + np.array([0, -1]))
print(h.tolist(), (h @ np.array([1, -2])).tolist()) # [[0, 0], [1, 0], [1, 0], [2, 1]] [0, 1, 1, 0]
# параметры сети 64 -> 128 -> 64 -> 10
mlp = nn.Sequential(nn.Linear(64, 128), nn.ReLU(), nn.Linear(128, 64), nn.ReLU(), nn.Linear(64, 10))
print(sum(p.numel() for p in mlp.parameters())) # 17226
# softmax и кросс-энтропия для логитов (2, 1, -1), когда истинный класс первый
z = torch.tensor([2.0, 1.0, -1.0])
print(torch.softmax(z, 0).numpy().round(3), round(nn.functional.cross_entropy(z[None], torch.tensor([0])).item(), 3))
# [0.705 0.259 0.035] 0.349
# две луны: логистическая регрессия на (x1, x2) против сети 2 -> 16 -> 16 -> 1
Xm, ym = make_moons(1000, noise=0.2, random_state=0)
Xtr, Xte, ytr, yte = train_test_split(Xm, ym, test_size=0.5, random_state=0)
print(round(LogisticRegression().fit(Xtr, ytr).score(Xte, yte), 3)) # 0.848
torch.manual_seed(0)
body = nn.Sequential(nn.Linear(2, 16), nn.ReLU(), nn.Linear(16, 16), nn.ReLU())
head = nn.Linear(16, 1)
opt = torch.optim.Adam([*body.parameters(), *head.parameters()], lr=0.01)
Xt, yt = torch.tensor(Xtr, dtype=torch.float32), torch.tensor(ytr, dtype=torch.float32)
for step in range(1000):
opt.zero_grad()
loss = nn.functional.binary_cross_entropy_with_logits(head(body(Xt)).squeeze(1), yt)
loss.backward()
opt.step()
with torch.no_grad():
Ftr, Fte = body(Xt).numpy(), body(torch.tensor(Xte, dtype=torch.float32)).numpy()
acc = ((head(torch.tensor(Fte)).squeeze(1) > 0).numpy() == yte).mean()
# сеть и новая логистическая регрессия на 16 признаках последнего скрытого слоя сети
print(round(float(acc), 3), round(LogisticRegression(max_iter=1000).fit(Ftr, ytr).score(Fte, yte), 3)) # 0.958 0.958
# один скрытый слой из N ReLU-нейронов интерполирует sin(2 pi x) в N + 1 узлах: ошибка падает как 1/N^2
f = lambda t: np.sin(2 * np.pi * t)
grid = np.linspace(0, 1, 10001)
for N in (4, 8, 16, 32):
knots = np.linspace(0, 1, N + 1)
slopes = np.diff(f(knots)) * N
c = np.diff(slopes, prepend=0.0)
approx = f(0) + relu(grid[:, None] - knots[:-1]) @ c
print(N, round(float(np.abs(approx - f(grid)).max()), 4))
# 4 0.2105 | 8 0.0704 | 16 0.0188 | 32 0.0048
# глубина: палатка g(x) = 2 ReLU(x) - 4 ReLU(x - 1/2), применённая k раз, имеет 2^k линейных кусков
g = lambda t: 2 * relu(t) - 4 * relu(t - 0.5)
t = np.linspace(0, 1, 2**12 + 1)
for k in (1, 2, 3, 5):
y = t.copy()
for _ in range(k):
y = g(y)
slopes = np.round(np.diff(y) / np.diff(t), 6)
print(k, 1 + int((slopes[1:] != slopes[:-1]).sum()))
# 1 2 | 2 4 | 3 8 | 5 32
Дальше#
Сеть — это композиция простых функций с тысячами или миллионами параметров, и чтобы подогнать её градиентным спуском, на каждом шаге нужен градиент функции потерь по каждому из них. Численное дифференцирование стоило бы одного прямого прохода на параметр. Следующая заметка показывает, как цепное правило, применённое от функции потерь назад по вычислительному графу, даёт их все примерно за стоимость одного прямого прохода.
Литература#
- G. Cybenko. Approximation by superpositions of a sigmoidal function. Mathematics of Control, Signals, and Systems, 2(4):303–314, 1989. Теорема 2 (один скрытый слой плотен в ); раздел 5 (о числе нейронов).
- K. Hornik. Approximation capabilities of multilayer feedforward networks. Neural Networks, 4(2):251–257, 1991. Теоремы 1 и 2.
- M. Leshno, V. Ya. Lin, A. Pinkus, S. Schocken. Multilayer feedforward networks with a nonpolynomial activation function can approximate any function. Neural Networks, 6(6):861–867, 1993. Теорема 1.
- A. R. Barron. Universal approximation bounds for superpositions of a sigmoidal function. IEEE Transactions on Information Theory, 39(3):930–945, 1993.
- M. Telgarsky. Benefits of depth in neural networks. COLT, 2016. Теорема 1.1 (разделение по глубине); лемма 3.10 (палатка как ReLU-сеть).
- G. Montúfar, R. Pascanu, K. Cho, Y. Bengio. On the number of linear regions of deep neural networks. NeurIPS, 2014.
- V. Nair, G. E. Hinton. Rectified linear units improve restricted Boltzmann machines. ICML, 2010.
- X. Glorot, A. Bordes, Y. Bengio. Deep sparse rectifier neural networks. AISTATS, 2011.
- D. Hendrycks, K. Gimpel. Gaussian error linear units (GELUs). arXiv:1606.08415, 2016. Раздел 2.
- L. Grinsztajn, E. Oyallon, G. Varoquaux. Why do tree-based models still outperform deep learning on typical tabular data? NeurIPS Datasets and Benchmarks Track, 2022.
- I. Goodfellow, Y. Bengio, A. Courville. Deep Learning. MIT Press, 2016. Раздел 6.1 (обучение XOR), разделы 6.2–6.4 (выходные нейроны, скрытые нейроны, архитектура и универсальная аппроксимация).
- C. M. Bishop. Pattern Recognition and Machine Learning. Springer, 2006. Раздел 5.1 (функции сетей прямого распространения).
- E. Sokolov et al. Машинное обучение (HSE), конспекты лекций, лекция 7: введение в глубинное обучение.