Траектория «Нейросети» · конспект 3 из 12

Первая сеть на 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 без каких-либо параметров — это чисто техническое преобразование формы данных, значения чисел не меняются, меняется только их расположение.

1
2
3
4
5
6
7
8
9
1
2
3
4
5
6
7
8
9

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}}\):

\[ P(n_{\text{in}}, n_{\text{out}}) = \underbrace{n_{\text{in}}\cdot n_{\text{out}}}_{\text{веса}} + \underbrace{n_{\text{out}}}_{\text{смещения}} \tag{3.1}\]

Для первого слоя сети: \(n_{\text{in}}=3072\), \(n_{\text{out}}=512\), откуда \(P=3072\cdot512+512=1\,573\,376\) — именно это число появится в столбце Param # вывода model.summary() напротив первого слоя Dense.

Тренажёр: параметры Sequential-сети
СлойOutput ShapeParam #

Входная форма (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 = \frac{e^{z_k}}{\sum_{j=1}^{K} e^{z_j}}, \qquad k=1,\ldots,K \tag{3.2}\]

По построению \(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\)), то

\[ E = -\sum_{k=1}^{K} y_k \log p_k = -\log p_{\text{верный класс}} \tag{3.3}\]

Формула (3.3) штрафует модель тем сильнее, чем меньшую вероятность она приписала правильному классу, и обращается в бесконечность, если модель приписала верному классу вероятность, стремящуюся к нулю. Как и формула (2.2) конспекта 2, это по-прежнему просто функция ошибки \(E\), которую в compile минимизируют тем же градиентным спуском (конспект 2, формула (2.5)) — меняется мера ошибки, а не сам принцип обучения.

Типичная ошибка Обучаясь на softmax-выходе с функцией потерь 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)

Один параметр здесь новый — batch_size. В конспекте 2 на каждой итерации цикла градиент считался сразу по всем четырём обучающим примерам (полный градиентный спуск). Для 50000 примеров CIFAR-10 это означало бы пересчитывать веса лишь раз за очень долгий проход по всем данным. Вместо этого обучающую выборку из \(N\) примеров разбивают на батчи размера \(B=\)batch_size, и градиент (формула (2.5) конспекта 2) на каждом шаге вычисляется по одному батчу, а не по всей выборке сразу. Число шагов (батчей) на одну эпоху:

\[ \text{шагов за эпоху} = \left\lceil \frac{N}{B} \right\rceil \tag{3.4}\]

Для \(N=50000\) и \(B=32\): \(\lceil 50000/32\rceil = 1563\) шага обновления весов на каждую эпоху, а не один шаг, как было бы при полном градиентном спуске по всей выборке.

Типичная ошибка Путают эпоху и шаг (батч). Эпоха — один проход по всей обучающей выборке; за одну эпоху веса обновляются \(\lceil N/B\rceil\) раз согласно (3.4), а не один раз.

После обучения точность проверяют отдельно на обучающих и на тестовых данных — это разные вызовы с разными массивами:

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))
Типичная ошибка Приводят одну цифру точности, не уточняя, на каких данных она получена. Точность на обучающих данных обычно выше точности на тестовых и почти ничего не говорит о качестве модели на новых, невиданных примерах — далее в траектории к этому расхождению вернёмся в конспекте о переобучении.

Контрольные вопросы