Электрические кабели и световые эффекты между панелями в темном помещении.

Обзор DenseNet Paper: Все взаимосвязано

Изучение и реализация архитектуры DenseNet с нуля с помощью PyTorch.

Делиться

055a6b9b394ccf6320dba787236945ad
Фотография Израиля Паласио на Unsplash.

При обучении очень глубокой нейронной сети одной из проблем, с которой мы можем столкнуться, является проблема исчезающего градиента. По сути, это проблема, при которой обновление весов модели во время обучения замедляется или даже прекращается, что приводит к тому, что модель не улучшается. В очень глубокой сети вычисление градиента во время обратного распространения ошибки включает в себя умножение множества производных членов друг на друга по правилу цепочки. Помните, что если мы умножаем малые числа (обычно меньше 1) слишком много раз, то результирующие числа станут чрезвычайно малыми. В случае нейронных сетей эти числа используются в качестве основы для обновления весов. Таким образом, если градиент очень мал, то обновление весов будет очень медленным, что также замедлит обучение.

Для решения проблемы затухания градиента можно использовать короткие пути, чтобы градиенты легче проходили через глубокую сеть. Одна из самых популярных архитектур, пытающихся решить эту проблему, — ResNet, в которой используются пропускные соединения, позволяющие перескакивать через несколько слоев сети. Эта идея используется в DenseNet, где пропускные соединения реализованы гораздо более агрессивно, что делает её лучше, чем ResNet, в решении проблемы затухания градиента. В этой статье я хотел бы рассказать о том, как именно работает DenseNet и как реализовать эту архитектуру с нуля.

Архитектура DenseNet

Плотный блок

DenseNet был впервые предложен в статье «Плотно связанные сверточные сети», написанной Гао Хуангом и др. в 2016 году [1]. Основная идея DenseNet заключается в решении проблемы затухания градиента. Причина, по которой он работает лучше, чем ResNet, заключается в наличии коротких путей, расходящихся от одного слоя ко всем последующим слоям. Для лучшего понимания этой идеи на рисунке 1 ниже показано, что входной тензор x₀ передается в H₁, H₂, H₃, H₄ и переходные слои. Мы делаем то же самое со всеми слоями внутри этого блока, обеспечивая плотную связь всех тензоров — отсюда и название DenseNet. Благодаря всем этим коротким соединениям информация может беспрепятственно передаваться между слоями. Более того, этот механизм также позволяет повторно использовать признаки, так что каждый слой может напрямую извлекать пользу из признаков, полученных всеми предыдущими слоями.

e79b73ef480ca369304f27f6c8fb03ca
Рисунок 1. Структура отдельного плотного блока [1].

В стандартной сверточной нейронной сети (CNN) при наличии L слоев, соответственно, и L связей. Предположим, что на иллюстрации показана традиционная 5-слойная CNN, тогда из каждого тензора исходят всего 5 прямых стрелок. В сети DenseNet при наличии L слоев, количество связей составит L(L+1)/2. Таким образом, в приведенном выше примере мы получили 5(5+1)/2 = 15 связей. Вы можете убедиться в этом, вручную подсчитав количество стрелок: 5 красных, 4 зеленых, 3 фиолетовых, 2 желтых и 1 коричневая.

Еще одно ключевое различие между ResNet и DenseNet заключается в способе объединения информации из разных слоев. В ResNet мы объединяем информацию из двух тензоров путем поэлементного суммирования, что математически описано на рисунке 2 ниже. Вместо поэлементного суммирования DenseNet объединяет информацию путем поканальной конкатенации, как показано на рисунке 3. При таком механизме карты признаков, созданные всеми предыдущими слоями, объединяются с выходом текущего слоя, прежде чем в конечном итоге использоваться в качестве входных данных для последующего слоя.

4ddaa54620d9731dba407e977102b6e5
Рисунок 2. Математическое обозначение остаточного блока в ResNet [1].
a806ce0f7716f5b439250f72a61e6941
Рисунок 3. Математическое обозначение последнего слоя внутри плотного блока в DenseNet [1].

Пошаговое объединение каналов имеет побочный эффект: количество карт признаков увеличивается по мере углубления в сеть. В примере, показанном на рисунке 1, у нас изначально есть входной тензор с 6 каналами. Слой H₁ обрабатывает этот тензор и выдает тензор с 4 каналами. Затем эти два тензора объединяются перед передачей в слой H₂. По сути, это означает, что слой H₂ принимает 10 каналов. Следуя той же схеме, позже слои H₃, H₄ и переходный слой будут принимать тензоры с 14, 18 и 22 каналами соответственно. Это пример DenseNet, использующего параметр скорости роста, равный 4, что означает, что каждый слой создает 4 новые карты признаков. Позже мы будем использовать k для обозначения этого параметра, как предложено в оригинальной статье.

Несмотря на сложную структуру связей, DenseNet на самом деле намного эффективнее традиционной сверточной нейронной сети (CNN) с точки зрения количества параметров. Давайте немного посчитаем, чтобы это доказать. Структура, показанная на рисунке 1, состоит из 4 сверточных слоев (пока проигнорируем слой перехода). Чтобы вычислить количество параметров сверточного слоя, мы можем просто рассчитать input_channels × kernel_height × kernel_width × output_channels. Предполагая, что все эти свертки используют ядро 3×3, наши слои в архитектуре DenseNet будут иметь следующее количество параметров:

  • H₁ → 6×3×3×4 = 216
  • H₂ → 10×3×3×4 = 360
  • H₃ → 14×3×3×4 = 504
  • H₄ → 18×3×3×4 = 648

Суммируя эти четыре числа, мы получим в общей сложности 1728 параметров. Обратите внимание, что это число не включает смещение. Теперь, если мы попытаемся создать точно такую же структуру с помощью традиционной сверточной нейронной сети, нам потребуется следующее количество параметров для каждого слоя:

  • H₁ → 6×3×3×10 = 540
  • H₂ → 10×3×3×14 = 1,260
  • H₃ → 14×3×3×18 = 2268
  • H₄ → 18×3×3×22 = 3,564

Подводя итог, можно сказать, что традиционная сверточная нейронная сеть (CNN) имеет 7632 параметра — это более чем в 4 раза больше! Учитывая это количество параметров, мы можем ясно видеть, что DenseNet действительно намного легче традиционных CNN. Причина такой эффективности DenseNet заключается в механизме повторного использования признаков, где вместо вычисления всех карт признаков с нуля, она вычисляет только k карт признаков и объединяет их с существующими картами признаков из предыдущих слоев.

Переходный слой

Структура, которую я показывал ранее, на самом деле является лишь основным строительным блоком модели DenseNet, который называется плотным блоком. На рисунке 4 ниже показано, как эти строительные блоки собираются, причем три из них соединены так называемыми переходными слоями. Каждый переходный слой состоит из свертки, за которой следует слой пулинга. Этот компонент выполняет две основные функции: во-первых, уменьшает пространственную размерность тензора, и во-вторых, уменьшает количество каналов. Уменьшение пространственной размерности является стандартной практикой при построении моделей на основе CNN, где более глубокие карты признаков обычно должны иметь меньшую размерность, чем более мелкие. В то же время уменьшение количества каналов необходимо, поскольку их количество может резко возрасти из-за механизма поканальной конкатенации, выполняемого внутри каждого слоя в плотном блоке.

4bff6fcae742a06b1d02f54b586ff4f5
Рисунок 4. Общий вид архитектуры DenseNet. Пара свертка-пулинг — это так называемый переходный слой [1].

Чтобы понять, как переходный слой уменьшает количество каналов, нам нужно взглянуть на параметр коэффициента сжатия. Этот параметр, который авторы называют θ (тета), должен иметь значение где-то между 0 и 1. Предположим, мы установим θ равным 0,2, тогда количество каналов, которые будут переданы в следующий плотный блок, составит всего 20% от общего количества каналов, созданных текущим плотным блоком.

Вся архитектура DenseNet

Поскольку мы разобрались с плотным блоком и переходным слоем, теперь мы можем перейти к полной архитектуре DenseNet, показанной на рисунке 5 ниже. Изначально она принимает RGB-изображение размером 224×224, которое затем обрабатывается сверточным слоем 7×7 и слоем максимального пулинга 3×3. Следует помнить, что эти два слоя используют шаг 2, в результате чего пространственное измерение уменьшается до 112×112 и 56×56 соответственно. На этом этапе тензор готов к передаче через первый плотный блок, который состоит из 6 блоков-бутылочных горлышек — я подробнее расскажу об этом компоненте чуть позже. Полученный результат затем передается на первый переходный слой, затем на второй плотный блок и так далее, пока мы в конечном итоге не достигнем слоя глобального усредняющего пулинга. Наконец, мы передаем тензор на полносвязный слой, который отвечает за предсказание классов.

7c555cb38d132ca12a61b36ae14f1226
Рисунок 5. Полная архитектура DenseNet [1].

На самом деле, мне нужно пояснить еще несколько деталей относительно описанной выше архитектуры. Во-первых, количество карт признаков, создаваемых на каждом шаге, явно не указано. Это связано с тем, что архитектура является адаптивной в зависимости от параметров k и θ. Единственный слой с фиксированным числом — это самый первый сверточный слой (7×7), который создает 64 карты признаков (не показаны на рисунке). Во-вторых, важно отметить, что каждый сверточный слой, показанный в архитектуре, следует последовательности BN-ReLU-conv-dropout, за исключением свертки 7×7, которая не включает слой dropout. В-третьих, авторы реализовали несколько вариантов DenseNet, которые они называют DenseNet (стандартный), DenseNet-B (вариант, использующий блоки с узким местом), DenseNet-C (вариант, использующий коэффициент сжатия θ) и DenseNet-BC (вариант, использующий оба варианта). Архитектура, представленная на рисунке 5, — это вариант DenseNet-B (или DenseNet-BC).

Так называемый блок-бутылочное горлышко представляет собой стек сверток 1×1 и 3×3. Свертка 1×1 используется для уменьшения количества каналов до 4k, прежде чем оно будет дополнительно уменьшено до k с помощью последующей свертки 3×3. Причина этого заключается в том, что свертка 3×3 является вычислительно затратной для тензоров с большим количеством каналов. Поэтому для ускорения вычислений нам необходимо сначала уменьшить количество каналов с помощью свертки 1×1. Позже в разделе кодирования мы реализуем этот вариант DenseNet-BC. Однако, если вы хотите реализовать стандартный DenseNet (или DenseNet-C), вы можете просто опустить свертку 1×1, так что каждый плотный блок будет состоять только из сверток 3×3.

Некоторые экспериментальные результаты

В статье видно, что авторы провели множество экспериментов, сравнивая DenseNet с другими моделями. В этом разделе я покажу вам некоторые интересные открытия, которые они сделали.

86a2fd3850b121adf084780b457dd97a
Рисунок 6. DenseNet обеспечивает более высокую точность, чем ResNet, при меньшем количестве параметров и меньших вычислительных затратах на разных уровнях глубины сети [1].

Первый интересный экспериментальный результат заключается в том, что DenseNet на самом деле демонстрирует гораздо лучшие показатели, чем ResNet. На рисунке 6 выше показано, что он стабильно превосходит ResNet на всех уровнях глубины сети. При сравнении вариантов с аналогичной точностью DenseNet оказывается намного эффективнее. Давайте подробнее рассмотрим вариант DenseNet-201. Здесь видно, что ошибка валидации почти такая же, как у ResNet-101. Несмотря на то, что он в 2 раза глубже (201 против 101 слоя), он примерно в 2 раза меньше как по количеству параметров, так и по количеству операций с плавающей запятой (FLOPs).

9ef177f3156a561a7c7232d510025b0a
Рисунок 7. Как слой узкого места и коэффициент сжатия влияют на производительность модели [1].

Далее авторы также провели исследование влияния слоя «бутылочного горла» и коэффициента сжатия на точность модели. На рисунке 7 выше видно, что использование слоя «бутылочного горла» в плотном блоке и уменьшение количества каналов в переходном слое позволяет модели достичь более высокой точности (DenseNet-BC). Может показаться несколько нелогичным, что уменьшение количества каналов за счет коэффициента сжатия, наоборот, повышает точность. На самом деле, в глубоком обучении слишком большое количество признаков может, наоборот, снизить точность из-за избыточности информации. Таким образом, уменьшение количества каналов можно рассматривать как механизм регуляризации, который предотвращает переобучение модели, позволяя ей достичь более высокой точности валидации.

DenseNet с нуля

Поскольку мы разобрались с основной теорией DenseNet, теперь мы можем реализовать архитектуру с нуля. Первым делом нам нужно импортировать необходимые модули и инициализировать настраиваемые переменные. В приведенном ниже блоке кода k и θ, которые мы обсуждали ранее, обозначены как GROWTH и COMPRESSION , значения которых установлены равными 12 и 0,5 соответственно. Эти два значения являются значениями по умолчанию, указанными в статье, и мы, безусловно, можем изменить их при желании. Далее, здесь я также инициализирую список REPEATS для хранения количества блоков-бутылочных горлышек в каждом плотном блоке.

 # Codeblock 1 import torch import torch.nn as nn GROWTH = 12 COMPRESSION = 0.5 REPEATS = [6, 12, 24, 16]

Реализация узкого места

Теперь давайте рассмотрим класс Bottleneck ниже, чтобы увидеть, как я реализую стек сверток 1×1 и 3×3. Ранее я упоминал, что каждый сверточный слой следует структуре BN-ReLU-Conv-dropout, поэтому здесь нам нужно инициализировать все эти слои в методе __init__() .

Два сверточных слоя инициализируются как conv0 и conv1 , каждый со своим соответствующим слоем пакетной нормализации. Не забудьте установить параметр out_channels слоя conv0 равным GROWTH*4 поскольку мы хотим, чтобы он возвращал 4k карт признаков (см. строку, отмеченную #(1) ). Это количество карт признаков затем будет еще больше уменьшено слоем conv1 до k путем установки out_channels равным GROWTH ( #(2) ). Поскольку все слои инициализированы, теперь мы можем определить поток в методе forward() . Просто помните, что в конце процесса нам необходимо объединить результирующий тензор ( out ) с исходным ( x ), чтобы реализовать пропускное соединение ( #(3) ).

 # Codeblock 2 class Bottleneck(nn.Module): def __init__(self, in_channels): super().__init__() self.relu = nn.ReLU() self.dropout = nn.Dropout(p=0.2) self.bn0 = nn.BatchNorm2d(num_features=in_channels) self.conv0 = nn.Conv2d(in_channels=in_channels, out_channels=GROWTH*4, #(1) kernel_size=1, padding=0, bias=False) self.bn1 = nn.BatchNorm2d(num_features=GROWTH*4) self.conv1 = nn.Conv2d(in_channels=GROWTH*4, out_channels=GROWTH, #(2) kernel_size=3, padding=1, bias=False) def forward(self, x): print(f'originalt: {x.size()}') out = self.dropout(self.conv0(self.relu(self.bn0(x)))) print(f'after conv0t: {out.size()}') out = self.dropout(self.conv1(self.relu(self.bn1(out)))) print(f'after conv1t: {out.size()}') concatenated = torch.cat((out, x), dim=1) #(3) print(f'after concatt: {concatenated.size()}') return concatenated

Чтобы проверить корректность работы нашего класса Bottleneck , мы создадим класс, принимающий 64 карты признаков, и передадим ему фиктивный тензор. Слой Bottleneck, который я создаю ниже, по сути, соответствует самому первому узкому месту внутри первого плотного блока (обратитесь к рисунку 5, если вы не уверены). Таким образом, чтобы имитировать реальный поток сети, мы будем передавать тензор размером 64×56×56, что по сути является формой, создаваемой слоем максимального пулинга 3×3.

 # Codeblock 3 bottleneck = Bottleneck(in_channels=64) x = torch.randn(1, 64, 56, 56) x = bottleneck(x)

После выполнения приведенного выше кода на экране появится следующий результат.

 # Codeblock 3 Output original : torch.Size([1, 64, 56, 56]) after conv0 : torch.Size([1, 48, 56, 56]) #(1) after conv1 : torch.Size([1, 12, 56, 56]) #(2) after concat : torch.Size([1, 76, 56, 56])

Здесь мы видим, что наш слой conv0 успешно уменьшил количество карт признаков с 64 до 48 ( #(1) ), где 48 — это 4k (помните, что наше k равно 12). Затем этот 48-канальный тензор обрабатывается слоем conv1 , который еще больше уменьшает количество карт признаков до k ( #(2) ). Этот выходной тензор затем объединяется с исходным, в результате чего получается тензор из 64 + 12 = 76 карт признаков. И вот здесь, собственно, начинается закономерность. Позже в плотном блоке, если мы повторим это узкое место несколько раз, то каждый слой будет производить:

  • второй слой → 64 + (2 × 12) = 88 карт признаков
  • третий слой → 64 + (3 × 12) = 100 карт признаков
  • четвертый слой → 64 + (4 × 12) = 112 карт признаков
  • и так далее …

Реализация плотных блоков

Теперь давайте создадим класс DenseBlock для хранения последовательности экземпляров Bottleneck . Посмотрите блок кода 4 ниже, чтобы увидеть, как я это делаю. Сделать это довольно просто: мы можем инициализировать список модулей ( #(1) ), а затем добавлять блоки Bottleneck по одному ( #(3) ). Обратите внимание, что нам нужно отслеживать количество входных каналов каждого Bottleneck, используя переменную current_in_channels ( #(2) ). Наконец, в методе forward() мы можем просто передавать тензор последовательно.

 # Codeblock 4 class DenseBlock(nn.Module): def __init__(self, in_channels, repeats): super().__init__() self.bottlenecks = nn.ModuleList() #(1) for i in range(repeats): current_in_channels = in_channels + i*GROWTH #(2) self.bottlenecks.append(Bottleneck(in_channels=current_in_channels)) #(3) def forward(self, x): for i, bottleneck in enumerate(self.bottlenecks): x = bottleneck(x) print(f'after bottleneck #{i}t: {x.size()}') return x

Мы можем протестировать приведенный выше код, смоделировав первый плотный блок в сети. На рисунке 5 видно, что он содержит 6 блоков-бутылочных горлышек, поэтому в блоке кода 5 ниже я установил параметр repeats равным этому числу ( #(1) ). В результате мы видим, что входной тензор, который изначально имеет форму 64×56×56, преобразуется в 136×56×56. 136 карт признаков получены из 64+(6×12), что соответствует шаблону, который я вам дал ранее.

 # Codeblock 5 dense_block = DenseBlock(in_channels=64, repeats=6) #(1) x = torch.randn(1, 64, 56, 56) x = dense_block(x)
 # Codeblock 5 Output after bottleneck #0 : torch.Size([1, 76, 56, 56]) after bottleneck #1 : torch.Size([1, 88, 56, 56]) after bottleneck #2 : torch.Size([1, 100, 56, 56]) after bottleneck #3 : torch.Size([1, 112, 56, 56]) after bottleneck #4 : torch.Size([1, 124, 56, 56]) after bottleneck #5 : torch.Size([1, 136, 56, 56])

Переходный слой

Следующий компонент, который мы собираемся реализовать, — это переходный слой, показанный в блоке кода 6 ниже. Аналогично сверточным слоям в блоках с узким местом, здесь мы также используем структуру BN-ReLU-conv-dropout, но на этот раз с дополнительным слоем усредняющего пулинга в конце ( #(1) ). Не забудьте установить шаг этого слоя пулинга равным 2, чтобы уменьшить пространственную размерность вдвое.

 # Codeblock 6 class Transition(nn.Module): def __init__(self, in_channels, out_channels): super().__init__() self.bn = nn.BatchNorm2d(num_features=in_channels) self.relu = nn.ReLU() self.conv = nn.Conv2d(in_channels=in_channels, out_channels=out_channels, kernel_size=1, padding=0, bias=False) self.dropout = nn.Dropout(p=0.2) self.pool = nn.AvgPool2d(kernel_size=2, stride=2) #(1) def forward(self, x): print(f'originalt: {x.size()}') out = self.pool(self.dropout(self.conv(self.relu(self.bn(x))))) print(f'after transition: {out.size()}') return out

Теперь давайте посмотрим на тестовый код в блоке кода 7 ниже, чтобы увидеть, как тензор преобразуется при прохождении через описанную выше сеть. В этом примере я пытаюсь смоделировать самый первый переходный слой, то есть тот, который следует сразу за первым плотным блоком. Именно поэтому я установил для этого слоя 136 каналов. Ранее я упоминал, что этот слой используется для уменьшения размерности каналов с помощью параметра θ, поэтому для его реализации мы можем просто умножить количество входных карт признаков на переменную COMPRESSION для параметра out_channels .

 # Codeblock 7 transition = Transition(in_channels=136, out_channels=int(136*COMPRESSION)) x = torch.randn(1, 136, 56, 56) x = transition(x)

После выполнения приведенного выше кода мы должны получить следующий результат. Здесь вы можете видеть, что пространственная размерность входного тензора уменьшается с 56×56 до 28×28, а количество каналов также уменьшается со 136 до 68. Это, по сути, указывает на правильность реализации нашего переходного слоя.

 # Codeblock 7 Output original : torch.Size([1, 136, 56, 56]) after transition : torch.Size([1, 68, 28, 28])

Вся архитектура DenseNet

Поскольку мы успешно реализовали основные компоненты модели DenseNet, теперь перейдем к построению всей архитектуры. Здесь я разделяю методы __init__() и forward() на два блока кода, так как они довольно длинные. Просто убедитесь, что вы поместили блоки кода 8a и 8b в одну ячейку блокнота, если хотите запустить их самостоятельно.

 # Codeblock 8a class DenseNet(nn.Module): def __init__(self): super().__init__() self.first_conv = nn.Conv2d(in_channels=3, out_channels=64, kernel_size=7, #(1) stride=2, #(2) padding=3, #(3) bias=False) self.first_pool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1) #(4) channel_count = 64 # Dense block #0 self.dense_block_0 = DenseBlock(in_channels=channel_count, repeats=REPEATS[0]) #(5) channel_count = int(channel_count+REPEATS[0]*GROWTH) #(6) self.transition_0 = Transition(in_channels=channel_count, out_channels=int(channel_count*COMPRESSION)) channel_count = int(channel_count*COMPRESSION) #(7) # Dense block #1 self.dense_block_1 = DenseBlock(in_channels=channel_count, repeats=REPEATS[1]) channel_count = int(channel_count+REPEATS[1]*GROWTH) self.transition_1 = Transition(in_channels=channel_count, out_channels=int(channel_count*COMPRESSION)) channel_count = int(channel_count*COMPRESSION) # # Dense block #2 self.dense_block_2 = DenseBlock(in_channels=channel_count, repeats=REPEATS[2]) channel_count = int(channel_count+REPEATS[2]*GROWTH) self.transition_2 = Transition(in_channels=channel_count, out_channels=int(channel_count*COMPRESSION)) channel_count = int(channel_count*COMPRESSION) # Dense block #3 self.dense_block_3 = DenseBlock(in_channels=channel_count, repeats=REPEATS[3]) channel_count = int(channel_count+REPEATS[3]*GROWTH) self.avgpool = nn.AdaptiveAvgPool2d(output_size=(1,1)) #(8) self.fc = nn.Linear(in_features=channel_count, out_features=1000) #(9)

В методе __init__() описанном выше, мы сначала инициализируем слои first_conv и first_pool . Следует помнить, что эти два слоя не относятся ни к плотному блоку, ни к переходному слою, поэтому нам нужно вручную инициализировать их как экземпляры nn.Conv2d и nn.MaxPool2d . На самом деле, эти два начальных слоя довольно уникальны. Сверточный слой использует очень большое ядро размером 7×7 ( #(1) ) с шагом 2 ( #(2) ). Таким образом, этот слой не только захватывает информацию из большой области, но и выполняет пространственное понижение разрешения на месте. Здесь нам также нужно установить отступ равным 3 ( #(3) ), чтобы компенсировать большое ядро и предотвратить слишком сильное уменьшение пространственного размера. Далее, слой пулинга отличается от слоев переходного слоя тем, что мы используем максимальное пулинг 3×3 вместо среднего пулинга 2×2 ( #(4) ).

После завершения первых двух слоев мы переходим к инициализации плотных блоков и переходных слоев. Идея довольно проста: нам нужно инициализировать плотные блоки, состоящие из нескольких блоков-бутылочных горловин (количество бутылочных горловин передается через параметр repeats ( #(5) )). Не забудьте отслеживать количество каналов на каждом шаге ( #(6,7) ), чтобы мы могли сопоставить форму входных данных последующего слоя с формой выходных данных предыдущего. Затем мы, по сути, делаем то же самое для оставшихся плотных блоков и переходных слоев.

Поскольку мы достигли последнего плотного блока, теперь мы инициализируем слой глобального усредняющего пулинга ( #(8) ), который отвечает за усреднение значений по пространственному измерению, прежде чем, наконец, инициализировать классификационный слой ( #(9) ). Наконец, поскольку все слои инициализированы, мы можем соединить их все внутри метода forward() ниже.

 # Codeblock 8b def forward(self, x): print(f'originaltt: {x.size()}') x = self.first_conv(x) print(f'after first_convt: {x.size()}') x = self.first_pool(x) print(f'after first_poolt: {x.size()}') x = self.dense_block_0(x) print(f'after dense_block_0t: {x.size()}') x = self.transition_0(x) print(f'after transition_0t: {x.size()}') x = self.dense_block_1(x) print(f'after dense_block_1t: {x.size()}') x = self.transition_1(x) print(f'after transition_1t: {x.size()}') x = self.dense_block_2(x) print(f'after dense_block_2t: {x.size()}') x = self.transition_2(x) print(f'after transition_2t: {x.size()}') x = self.dense_block_3(x) print(f'after dense_block_3t: {x.size()}') x = self.avgpool(x) print(f'after avgpooltt: {x.size()}') x = torch.flatten(x, start_dim=1) print(f'after flattentt: {x.size()}') x = self.fc(x) print(f'after fctt: {x.size()}') return x

Это, по сути, вся реализация архитектуры DenseNet. Мы можем проверить, работает ли она должным образом, запустив приведенный ниже фрагмент кода 9. Здесь мы пропускаем тензор x через сеть, в которой она имитирует пакет из одного изображения RGB размером 224×224 пикселей.

 # Codeblock 9 densenet = DenseNet() x = torch.randn(1, 3, 224, 224) x = densenet(x)

Ниже представлен результат. Здесь я намеренно вывожу форму тензора после каждого шага, чтобы вы могли четко видеть, как тензор изменяется по всей сети. Несмотря на большое количество слоев, это фактически самый маленький вариант DenseNet, а именно DenseNet-121. Вы можете сделать модель еще больше, изменив значения в списке REPEATS в соответствии с количеством блоков-бутылочных горлышек внутри каждого плотного блока, как показано на рисунке 5.

 # Codeblock 9 Output original : torch.Size([1, 3, 224, 224]) after first_conv : torch.Size([1, 64, 112, 112]) after first_pool : torch.Size([1, 64, 56, 56]) after bottleneck #0 : torch.Size([1, 76, 56, 56]) after bottleneck #1 : torch.Size([1, 88, 56, 56]) after bottleneck #2 : torch.Size([1, 100, 56, 56]) after bottleneck #3 : torch.Size([1, 112, 56, 56]) after bottleneck #4 : torch.Size([1, 124, 56, 56]) after bottleneck #5 : torch.Size([1, 136, 56, 56]) after dense_block_0 : torch.Size([1, 136, 56, 56]) after transition_0 : torch.Size([1, 68, 28, 28]) after bottleneck #0 : torch.Size([1, 80, 28, 28]) after bottleneck #1 : torch.Size([1, 92, 28, 28]) after bottleneck #2 : torch.Size([1, 104, 28, 28]) after bottleneck #3 : torch.Size([1, 116, 28, 28]) after bottleneck #4 : torch.Size([1, 128, 28, 28]) after bottleneck #5 : torch.Size([1, 140, 28, 28]) after bottleneck #6 : torch.Size([1, 152, 28, 28]) after bottleneck #7 : torch.Size([1, 164, 28, 28]) after bottleneck #8 : torch.Size([1, 176, 28, 28]) after bottleneck #9 : torch.Size([1, 188, 28, 28]) after bottleneck #10 : torch.Size([1, 200, 28, 28]) after bottleneck #11 : torch.Size([1, 212, 28, 28]) after dense_block_1 : torch.Size([1, 212, 28, 28]) after transition_1 : torch.Size([1, 106, 14, 14]) after bottleneck #0 : torch.Size([1, 118, 14, 14]) after bottleneck #1 : torch.Size([1, 130, 14, 14]) after bottleneck #2 : torch.Size([1, 142, 14, 14]) after bottleneck #3 : torch.Size([1, 154, 14, 14]) after bottleneck #4 : torch.Size([1, 166, 14, 14]) after bottleneck #5 : torch.Size([1, 178, 14, 14]) after bottleneck #6 : torch.Size([1, 190, 14, 14]) after bottleneck #7 : torch.Size([1, 202, 14, 14]) after bottleneck #8 : torch.Size([1, 214, 14, 14]) after bottleneck #9 : torch.Size([1, 226, 14, 14]) after bottleneck #10 : torch.Size([1, 238, 14, 14]) after bottleneck #11 : torch.Size([1, 250, 14, 14]) after bottleneck #12 : torch.Size([1, 262, 14, 14]) after bottleneck #13 : torch.Size([1, 274, 14, 14]) after bottleneck #14 : torch.Size([1, 286, 14, 14]) after bottleneck #15 : torch.Size([1, 298, 14, 14]) after bottleneck #16 : torch.Size([1, 310, 14, 14]) after bottleneck #17 : torch.Size([1, 322, 14, 14]) after bottleneck #18 : torch.Size([1, 334, 14, 14]) after bottleneck #19 : torch.Size([1, 346, 14, 14]) after bottleneck #20 : torch.Size([1, 358, 14, 14]) after bottleneck #21 : torch.Size([1, 370, 14, 14]) after bottleneck #22 : torch.Size([1, 382, 14, 14]) after bottleneck #23 : torch.Size([1, 394, 14, 14]) after dense_block_2 : torch.Size([1, 394, 14, 14]) after transition_2 : torch.Size([1, 197, 7, 7]) after bottleneck #0 : torch.Size([1, 209, 7, 7]) after bottleneck #1 : torch.Size([1, 221, 7, 7]) after bottleneck #2 : torch.Size([1, 233, 7, 7]) after bottleneck #3 : torch.Size([1, 245, 7, 7]) after bottleneck #4 : torch.Size([1, 257, 7, 7]) after bottleneck #5 : torch.Size([1, 269, 7, 7]) after bottleneck #6 : torch.Size([1, 281, 7, 7]) after bottleneck #7 : torch.Size([1, 293, 7, 7]) after bottleneck #8 : torch.Size([1, 305, 7, 7]) after bottleneck #9 : torch.Size([1, 317, 7, 7]) after bottleneck #10 : torch.Size([1, 329, 7, 7]) after bottleneck #11 : torch.Size([1, 341, 7, 7]) after bottleneck #12 : torch.Size([1, 353, 7, 7]) after bottleneck #13 : torch.Size([1, 365, 7, 7]) after bottleneck #14 : torch.Size([1, 377, 7, 7]) after bottleneck #15 : torch.Size([1, 389, 7, 7]) after dense_block_3 : torch.Size([1, 389, 7, 7]) after avgpool : torch.Size([1, 389, 1, 1]) after flatten : torch.Size([1, 389]) after fc : torch.Size([1, 1000])

Завершение

Думаю, это практически всё, что касается теории и реализации модели DenseNet. Все приведенные выше коды вы также можете найти в моем репозитории на GitHub [2]. До встречи в моей следующей статье!

Ссылки

[1] Гао Хуан и др. Плотно связанные сверточные сети. Arxiv. https://arxiv.org/abs/1608.06993 [Дата обращения: 18 сентября 2025].

[2] MuhammadArdiPutra. DenseNet. GitHub. https://github.com/MuhammadArdiPutra/medium_articles/blob/main/DenseNet.ipynb [Дата обращения: 18 сентября 2025].

Мухаммад Арди. Все материалы от Мухаммада Арди.

Источник: towardsdatascience.com

✅ Найденные теги: DenseNet, Paper, Взаимосвязь, новости, Обзор

ОСТАВЬТЕ СВОЙ КОММЕНТАРИЙ

Каталог бесплатных опенсорс-решений, которые можно развернуть локально и забыть о подписках

галерея

Экспериментальные снимки проволоки с графиками для анализа взрывного процесса.
Буровая установка в пустыне на фоне скал и голубого неба.
Бесплатный 5-дневный курс по искусственному интеллекту от Kaggle и Google
Исследование показало, что искусственный интеллект выявляет значительные ошибки в диагностике типов рака.
Пояс астероидов в космосе с крупными каменными объектами на фоне звёзд.
Компания CorTec получила от FDA статус прорывного продукта в области интерфейса мозг-компьютер для реабилитации после инсульта — Medical Device Network
Компания CorTec получила от FDA статус прорывного продукта в области интерфейса мозг-компьютер для реабилитации после инсульта — Medical Device Network
ideipro logotyp
ideipro logotyp
Image Not Found
Экспериментальные снимки проволоки с графиками для анализа взрывного процесса.

Физики раскрыли процессы взрывного разрушения тонких металлических катодов во время импульсного разряда в вакууме

Лазерные теневые изображения (кадры 1–8) диодов с острийным катодом (медная проволока диаметром 10 мкм и длиной около 1 мм) в вакууме. Изображения показывают состояние катода до разряда и через определенное время после резкого возрастания тока, протекающего через…

Апр 10, 2026
Буровая установка в пустыне на фоне скал и голубого неба.

Хакер украл 700 000 фунтов стерлингов у британской энергетической компании, перенаправив платеж.

Вкратце Источник изображений: Jon G. Fuller/VWPics/Universal Images Group / Getty Images Британская нефтегазовая компания Zephyr Energy заявляет, что кто-то украл 700 000 фунтов стерлингов (около 1 миллиона долларов) у одной из ее дочерних компаний в США, перенаправив…

Апр 10, 2026
Исследование показало, что искусственный интеллект выявляет значительные ошибки в диагностике типов рака.

Исследование показало, что искусственный интеллект выявляет значительные ошибки в диагностике типов рака.

Врачи, использующие алгоритм одного из поставщиков, обнаружили ошибки в диагностике плоскоклеточного рака легких, влияющие на решения о лечении и результаты лечения пациентов. Главный клинический директор компании подробно рассказывает об исследовании, опубликованном в журнале JAMA. Global Artificial Intelligence…

Апр 10, 2026
Пояс астероидов в космосе с крупными каменными объектами на фоне звёзд.

Мы обнаружили огромный астероид, вращающийся с невероятной скоростью.

Астрономы обнаружили астероид шириной 710 метров, который совершает один оборот за 1,9 минуты, что настолько быстро, что он должен был бы разлететься на части. Художественное изображение астероида 2025 MN45 Обсерватория имени Веры К. Рубин, финансируемая Национальным научным…

Апр 10, 2026

Впишите свой почтовый адрес и мы будем присылать вам на почту самые свежие новости в числе самых первых