Первая сеть на Keras: полносвязный слой (Dense)
- О чём эта тема
- В конспектах 1–2 нейрон и его обучение были реализованы вручную на numpy. Здесь та же идея выражается через Keras — библиотеку, с которой дальше ведётся вся работа в этой траектории. Разбирается реальная сеть для классификации изображений CIFAR-10, состоящая только из полносвязных слоёв.
- Аннотация
- Конспект знакомит с Keras на примере первой полноценной сети. Сначала архитектура описывается через Sequential API как стек слоёв, вводится полносвязный слой Dense и разбирается подсчёт числа его обучаемых параметров. Затем softmax и категориальная перекрёстная энтропия представляются как обобщение уже знакомых сигмоиды (конспект 1, формула (1.7)) и квадратичной ошибки (конспект 2, формула (2.2)) на случай классификации с несколькими классами. Далее показывается, как параметры функций compile и fit — оптимизатор, скорость обучения, эпохи, размер батча — соответствуют тому, что в конспекте 2 писалось вручную. Завершается конспект разбором рабочего кода целиком: от загрузки данных до обучения и оценки точности.
- Пререквизиты
- Конспекты 1–2: нейрон, веса, функция активации, функция ошибки, градиентный спуск, эпоха.
- Мотивация
- Цикл обучения из конспекта 2 — четыре строки на numpy — работал, потому что нейрон был один и входов было три. Сеть для распознавания изображений CIFAR-10 (60000 цветных изображений 32×32 пикселя, 10 классов) требует сотен тысяч весов и десятков тысяч примеров. Писать для неё вручную формулу обновления весов (конспект 2, формула (2.8)) и цикл обучения непрактично и избыточно: Keras берёт эту рутину на себя, оставляя разработчику декларативное описание архитектуры и параметров обучения.
1. Sequential: архитектура как стек слоёв
1.0. Данные: CIFAR-10
CIFAR-10 — стандартный учебный набор данных для классификации изображений: 60000 цветных изображений размером \(32\times32\) пикселя, разбитых на 50000 обучающих и 10000 тестовых, поровну распределённых между 10 взаимоисключающими классами (по 6000 изображений на класс):
| airplane | самолёт | automobile | автомобиль |
| bird | птица | cat | кошка |
| deer | олень | dog | собака |
| frog | лягушка | horse | лошадь |
| ship | корабль | truck | грузовик |
Каждое изображение хранится как массив формы \((32,32,3)\): высота 32 пикселя, ширина 32 пикселя, 3 канала цвета (красный, зелёный, синий). Задача сети — по такому массиву чисел определить, к какому из 10 классов относится изображение.
Данные CIFAR-10 загружаются напрямую из модуля датасетов Keras и приводятся к диапазону \([0,1]\) — тот же приём масштабирования входа, что фигурировал ранее без формального названия:
from tensorflow.keras.datasets import cifar10 from tensorflow.keras.utils import to_categorical (X_train, y_train), (X_test, y_test) = cifar10.load_data() X_train = X_train.astype('float32') / 255.0 X_test = X_test.astype('float32') / 255.0 # метки классов 0..9 -> one-hot векторы длины 10 y_train = to_categorical(y_train) y_test = to_categorical(y_test)
Нейрон из конспекта 1 принимает на вход вектор чисел, а не трёхмерный массив, поэтому первым слоем
сети ставится Flatten, разворачивающий массив \(32\times32\times3=3072\) в вектор длины
3072 без каких-либо параметров — это чисто техническое преобразование формы данных, значения чисел
не меняются, меняется только их расположение.
Flatten построчно разворачивает многомерный массив в вектор: для матрицы \(3\times3\) — в вектор длины 9, для настоящих изображений CIFAR-10 (\(32\times32\times3\)) — в вектор длины 3072. Порядок обхода элементов сохраняется, значения не меняются.
Дальше архитектура описывается как последовательность (Sequential) слоёв — каждый слой
принимает выход предыдущего:
from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Dense, Flatten model = Sequential() model.add(Flatten(input_shape=(32, 32, 3))) model.add(Dense(512, activation='relu')) model.add(Dense(10, activation='softmax')) model.summary()
Dense(512, activation='relu') — это слой из 512 нейронов в смысле конспекта 1: каждый
из них вычисляет взвешенную сумму всех 3072 входов плюс смещение (конспект 1, формула (1.5)) и
применяет ReLU (конспект 1, формула (1.7)). «Полносвязный» означает, что каждый нейрон слоя соединён со всеми выходами предыдущего слоя — точно
такая структура связей, для которой в конспекте 1 вводилась весовая матрица \(W\).
Flatten и пробуют подать трёхмерный массив изображения прямо на
Dense. Слой Dense ожидает на входе вектор (одномерный набор чисел); без
Flatten Keras выдаст ошибку несовпадения формы входных данных.
1.1. Число обучаемых параметров слоя Dense
Слой Dense с \(n_{\text{in}}\) входами и \(n_{\text{out}}\) нейронами — это в точности
весовая матрица \(W\) размера \(n_{\text{out}}\times n_{\text{in}}\) из конспекта 1 плюс вектор
смещений длиной \(n_{\text{out}}\):
Для первого слоя сети: \(n_{\text{in}}=3072\), \(n_{\text{out}}=512\), откуда
\(P=3072\cdot512+512=1\,573\,376\) — именно это число появится в столбце Param # вывода
model.summary() напротив первого слоя Dense.
| Слой | Output Shape | Param # |
|---|
Входная форма (3072 после Flatten) и число выходных классов (10)
зафиксированы по коду CIFAR-10 выше; меняется только число нейронов скрытого слоя H. Таблица
считается по формуле (3.1) и повторяет структуру вывода model.summary().
2. Softmax и категориальная перекрёстная энтропия
Сигмоида (конспект 1, формула (1.7)) отображала выход одного нейрона в интервал \((0,1)\), удобный
для задачи с двумя классами. Здесь классов десять, и последний слой сети — Dense(10, activation='softmax').
Softmax обобщает сигмоиду на \(K\) классов: по \(K\) выходным значениям слоя (их называют логитами,
\(z_1,\ldots,z_K\)) он строит распределение вероятностей:
По построению \(p_k\in(0,1)\) и \(\sum_k p_k=1\) — выход softmax можно читать как вероятности принадлежности каждому из \(K\) классов. При \(K=2\) формула (3.2) сводится к сигмоиде (конспект 1, формула (1.7)) от разности логитов, то есть сигмоида — частный случай softmax для двух классов.
Мера ошибки тоже должна измениться: квадратичная ошибка (конспект 2, формула (2.2)) для распределения вероятностей ведёт себя плохо (слабо наказывает уверенный неверный ответ). Вместо неё для классификации используют категориальную перекрёстную энтропию: если истинный класс примера закодирован one-hot вектором \(y\) (\(y_k=1\) для верного класса, иначе \(0\)), то
Формула (3.3) штрафует модель тем сильнее, чем меньшую вероятность она приписала правильному
классу, и обращается в бесконечность, если модель приписала верному классу вероятность, стремящуюся к
нулю. Как и формула (2.2) конспекта 2, это по-прежнему просто функция ошибки \(E\), которую в
compile минимизируют тем же градиентным спуском (конспект 2, формула (2.5)) — меняется
мера ошибки, а не сам принцип обучения.
categorical_crossentropy, забывают
перевести метки классов в one-hot вид (to_categorical). Без этого метки — просто числа
0–9, а не векторы вероятностей, и (3.3) в исходном виде неприменима (в Keras для меток-чисел без
one-hot есть отдельная функция потерь, sparse_categorical_crossentropy, дающая тот же
результат без явного one-hot-кодирования).
3. Обучение сети: compile и fit
Оставшаяся часть кода задаёт правило обучения и запускает его. Каждый параметр здесь уже встречался в конспекте 2 под другим именем:
from tensorflow.keras.optimizers import SGD epochs = 8 lrate = 0.01 sgd = SGD(learning_rate=lrate, momentum=0.9) model.compile(loss='categorical_crossentropy', optimizer=sgd, metrics=['accuracy']) history = model.fit(X_train, y_train, validation_data=(X_test, y_test), epochs=epochs, batch_size=32)
optimizer=sgd— правило обновления весов, формула (2.5) конспекта 2, стохастический градиентный спуск;lrate=0.01— скорость обучения \(\alpha\) из той же формулы (2.5);loss='categorical_crossentropy'— функция ошибки \(E\), здесь формула (3.3) вместо формулы (2.2) конспекта 2;epochs— число проходов по всей обучающей выборке, тот же смысл, что и в комментарии к numpy-циклу конспекта 2.
Один параметр здесь новый — batch_size. В конспекте 2 на каждой итерации цикла градиент
считался сразу по всем четырём обучающим примерам (полный градиентный спуск). Для 50000 примеров
CIFAR-10 это означало бы пересчитывать веса лишь раз за очень долгий проход по всем данным. Вместо
этого обучающую выборку из \(N\) примеров разбивают на батчи размера \(B=\)batch_size, и
градиент (формула (2.5) конспекта 2) на каждом шаге вычисляется по одному батчу, а не по всей выборке
сразу. Число шагов
(батчей) на одну эпоху:
Для \(N=50000\) и \(B=32\): \(\lceil 50000/32\rceil = 1563\) шага обновления весов на каждую эпоху, а не один шаг, как было бы при полном градиентном спуске по всей выборке.
После обучения точность проверяют отдельно на обучающих и на тестовых данных — это разные вызовы с разными массивами:
loss, acc = model.evaluate(X_train, y_train, verbose=0) print("Accuracy on Training Data: %.2f%%" % (acc * 100)) loss, acc = model.evaluate(X_test, y_test, verbose=0) print("Accuracy on Test Data: %.2f%%" % (acc * 100))
Контрольные вопросы
-
Dense ожидает на входе вектор чисел, а изображение CIFAR-10 хранится как трёхмерный массив (32,32,3). Flatten разворачивает его в вектор длины 3072 без каких-либо обучаемых параметров.
-
По формуле (3.1): 3072·512 + 512 = 1 573 376 — произведение размеров входа и выхода (веса) плюс число нейронов (смещения).
-
Softmax (формула (3.2)) строит распределение вероятностей по K классам, а не одно число в (0,1). При K=2 softmax сводится к сигмоиде (конспект 1, формула (1.7)) — сигмоида является её частным случаем.
-
batch_size — число примеров, по которым считается один шаг градиента (конспект 2, формула (2.5)). Эпоха — проход по всей выборке; число шагов за эпоху равно ⌈N/batch_size⌉ (формула (3.4)), а не одному шагу на всю выборку сразу.
-
Перекрёстная энтропия сильнее штрафует уверенные неверные предсказания и естественно сочетается с вероятностной интерпретацией выхода softmax; квадратичная ошибка для этой цели хуже приспособлена.
-
optimizer=sgd — само правило обновления весов, формула (2.5) (градиентный спуск); lrate — скорость обучения α в этой же формуле.