8.7. Плотные сети (DenseNet)¶
ResNet существенно изменил представление о том, как параметризовать функции в глубоких сетях. DenseNet (плотно соединённая сеть) в некотором смысле является логическим продолжением этого подхода (Huang et al., 2017). DenseNet характеризуется паттерном связности, при котором каждый слой соединён со всеми предыдущими слоями, а также операцией конкатенации (в отличие от оператора сложения в ResNet), которая позволяет сохранять и повторно использовать признаки из более ранних слоёв. Чтобы понять, как к этому пришли, совершим небольшое отступление к математике.
import torch
from torch import nn
from d2l import torch as d2l
8.7.1. От ResNet к DenseNet¶
Вспомним разложение Тейлора для функций. В точке \(x = 0\) его можно записать в виде
Ключевой момент заключается в том, что она разлагает функцию на слагаемые всё более высоких порядков. Аналогичным образом ResNet разлагает функции на
То есть ResNet разлагает \(f\) на простую линейную составляющую и более сложную нелинейную. А что, если нам потребуется учесть (не обязательно суммируя) информацию, выходящую за рамки двух слагаемых? Одним из таких решений является DenseNet (Huang et al., 2017).
Fig. 8.7.1 Основное различие между ResNet (слева) и DenseNet (справа) заключается в типах межслойных соединений: в ResNet используется сложение, а в DenseNet — конкатенация.¶
Как показано на рисунке Fig. 8.7.1, ключевое отличие ResNet от DenseNet заключается в том, что в последнем случае выходы конкатенируются (обозначается как \([,]\)), а не суммируются. В результате мы выполняем отображение из \(\mathbf{x}\) в его значения после применения всё более сложной последовательности функций:
В итоге все эти функции объединяются в многослойном перцептроне (MLP), чтобы снова уменьшить количество признаков. С точки зрения реализации это довольно просто: вместо сложения мы выполняем их конкатенацию. Название DenseNet происходит от того, что граф зависимостей между переменными становится довольно плотным. Последний слой такой цепи плотно соединён со всеми предыдущими слоями. Плотные связи показаны на рисунке Fig. 8.7.2.
Fig. 8.7.2 Плотные связи в DenseNet. Обратите внимание на то, что размерность увеличивается с глубиной.¶
Основными компонентами DenseNet являются плотные блоки и переходные слои. Первые определяют, как объединяются входы и выходы, а вторые контролируют количество каналов, чтобы оно не становилось слишком большим, поскольку размерность расширения \(\mathbf{x} \to \left[\mathbf{x}, f_1(\mathbf{x}), f_2\left(\left[\mathbf{x}, f_1\left(\mathbf{x}\right)\right]\right), \ldots \right]\) может быть весьма высокой.
8.7.2. Плотные блоки¶
DenseNet использует модифицированную структуру «батч-нормализация, активация и свёртка» из ResNet (см. упражнение в Section 8.6). Сначала мы реализуем эту структуру блока свёртки.
def conv_block(num_channels):
return nn.Sequential(
nn.LazyBatchNorm2d(), nn.ReLU(),
nn.LazyConv2d(num_channels, kernel_size=3, padding=1))
Плотный блок состоит из нескольких свёрточных блоков, каждый из которых использует одинаковое число выходных каналов. Однако при прямом распространении мы объединяем вход и выход каждого свёрточного блока по каналу. Ленивое вычисление позволяет автоматически настраивать размерность.
class DenseBlock(nn.Module):
def __init__(self, num_convs, num_channels):
super(DenseBlock, self).__init__()
layer = []
for i in range(num_convs):
layer.append(conv_block(num_channels))
self.net = nn.Sequential(*layer)
def forward(self, X):
for blk in self.net:
Y = blk(X)
# Concatenate input and output of each block along the channels
X = torch.cat((X, Y), dim=1)
return X
В следующем примере мы определяем DenseBlock объект с двумя
свёрточными блоками, имеющими по 10 выходных каналов. При использовании
входа с тремя каналами мы получим выход с \(3 + 10 + 10=23\)
каналами. Количество каналов в свёрточном блоке определяет рост числа
выходных каналов относительно числа входных каналов. Это также называют
скоростью роста.
blk = DenseBlock(2, 10)
X = torch.randn(4, 3, 8, 8)
Y = blk(X)
Y.shape
8.7.3. Переходные слои¶
Поскольку каждый плотный блок увеличивает число каналов, добавление слишком большого их числа приведёт к чрезмерной сложности модели. Для контроля сложности модели используется переходный слой. Он уменьшает число каналов с помощью свёртки \(1\times 1\). Кроме того, он уменьшает высоту и ширину вдвое с помощью усредняющего пулинга с шагом 2.
def transition_block(num_channels):
return nn.Sequential(
nn.LazyBatchNorm2d(), nn.ReLU(),
nn.LazyConv2d(num_channels, kernel_size=1),
nn.AvgPool2d(kernel_size=2, stride=2))
Примените переходный слой с 10 каналами к выходу плотного блока в предыдущем примере. Это уменьшает количество выходных каналов до 10, а также вдвое снижает высоту и ширину.
blk = transition_block(10)
blk(Y).shape
8.7.4. Модель DenseNet¶
Далее мы построим модель DenseNet. В DenseNet сначала используется тот же одиночный свёрточный слой и слой максимального пулинга, что и в ResNet.
class DenseNet(d2l.Classifier):
def b1(self):
return nn.Sequential(
nn.LazyConv2d(64, kernel_size=7, stride=2, padding=3),
nn.LazyBatchNorm2d(), nn.ReLU(),
nn.MaxPool2d(kernel_size=3, stride=2, padding=1))
Затем, аналогично четырём модулям из остаточных блоков, используемым в ResNet, DenseNet применяет четыре плотных блока. Как и в ResNet, мы можем задать количество слоёв свёртки в каждом плотном блоке. Здесь мы устанавливаем это значение равным 4, что соответствует модели ResNet-18 из Section 8.6. Кроме того, мы задаём количество каналов (т. е. скорость роста) для слоёв свёртки в плотном блоке равным 32, поэтому каждый плотный блок добавляет 128 каналов.
В ResNet высота и ширина уменьшаются между каждым модулем с помощью остаточного блока с шагом 2. Здесь мы используем переходный слой, чтобы уменьшить высоту и ширину вдвое, а также вдвое сократить количество каналов. Аналогично ResNet, в конце подключаются глобальный пулинг и полносвязный слой для формирования выходных данных.
@d2l.add_to_class(DenseNet)
def __init__(self, num_channels=64, growth_rate=32, arch=(4, 4, 4, 4),
lr=0.1, num_classes=10):
super(DenseNet, self).__init__()
self.save_hyperparameters()
self.net = nn.Sequential(self.b1())
for i, num_convs in enumerate(arch):
self.net.add_module(f'dense_blk{i+1}', DenseBlock(num_convs,
growth_rate))
# The number of output channels in the previous dense block
num_channels += num_convs * growth_rate
# A transition layer that halves the number of channels is added
# between the dense blocks
if i != len(arch) - 1:
num_channels //= 2
self.net.add_module(f'tran_blk{i+1}', transition_block(
num_channels))
self.net.add_module('last', nn.Sequential(
nn.LazyBatchNorm2d(), nn.ReLU(),
nn.AdaptiveAvgPool2d((1, 1)), nn.Flatten(),
nn.LazyLinear(num_classes)))
self.net.apply(d2l.init_cnn)
8.7.5. Обучение¶
Поскольку в этом разделе используется более глубокая сеть, мы уменьшим высоту и ширину входных данных с 224 до 96, чтобы упростить вычисления.
model = DenseNet(lr=0.01)
trainer = d2l.Trainer(max_epochs=10, num_gpus=1)
data = d2l.FashionMNIST(batch_size=128, resize=(96, 96))
trainer.fit(model, data)
8.7.6. Итоги и обсуждение¶
Основными компонентами DenseNet являются плотные блоки и переходные слои. Для последних нам необходимо контролировать размерность при построении сети, добавляя переходные слои, которые снова уменьшают количество каналов. Что касается межслойных соединений, то, в отличие от ResNet, где входы и выходы складываются, в DenseNet входы и выходы конкатенируются по размерности каналов. Хотя такие операции конкатенации позволяют повторно использовать признаки для достижения вычислительной эффективности, к сожалению, они приводят к значительному потреблению памяти GPU. В результате применение DenseNet может требовать более экономичных с точки зрения памяти реализаций, что может увеличить время обучения (Pleiss et al., 2017).
8.7.7. Упражнения¶
Почему в переходном слое используется средний пулинг вместо максимального?
Одним из преимуществ, упомянутых в статье DenseNet, является то, что количество параметров в его модели меньше, чем в ResNet. Почему так происходит?
Одной из проблем, за которые DenseNet подвергается критике, является высокое потребление памяти.
Действительно ли это так? Попробуйте изменить входную форму на \(224\times 224\), чтобы эмпирически сравнить фактическое потребление памяти GPU.
Можете ли вы придумать альтернативный способ снижения потребления памяти? Как вам потребуется изменить фреймворк?
Реализуйте различные версии DenseNet, представленные в таблице 1 статьи DenseNet (Huang et al., 2017).
Разработайте модель на основе многослойного перцептрона, используя подход DenseNet. Примените её к задаче прогнозирования цен на жилье из Section 5.7.