Чивиня (Multi-layer Parkinson)
42 subscribers
16 photos
3 videos
3 files
45 links
Обсуждение: https://t.me/chivinyachat
Download Telegram
Прежде я очень смутно понимал, зачем в блоках свёртки наращивают количество каналов, а потом уменьшают. Тогдашняя трактовка звучала примерно так: "Чтобы можно было заколодовать в весах более сложные связи".

Текущее понимание такое: нейросети проще обучиться. Ей легче подогнать свои веса. Первая фаза запоминания датасета проходит быстрее. А в фазе обобщения изменение одних весов слабее влияют на изменение других. И проще найти те веса, которые быстрее приведут к уменьшению лоса. И сами эти две фазы видны глазами.

Всё это легко представить. Берём плоскость и на ней несколько точек, к которым нейросеть должна при обучении максимально приблизиться. И изначально так инициализируем веса, чтобы нейросеть на плоскости рисовала прямую линию (это не обязательно, но для визуализации полезно). И начинаем наше обучение и с очень низким LR. Мы увидим, как наша прямая линия начинает ломаться в нескольких точках и становится ломанной (если активация у нас ReLU). Далее становится очевидным, что чем больше переломов на этой линии, тем проще подогнать веса так, чтобы ломанная проходила через заданные точки на плоскости. Если отдельные наши точки сгруппированы в какой-то одной области, а там переломов недостаточно, то нейросети будет тяжело сжаться именно под эту группу точек. Она уже подогналась под другие и они не пускают её.

Так же и после обучения у нас получается ломанная. Она проходит через заданные точки, но и между ними изгибается, хотя от неё этого не требовали. Прунинг с файнтюнингом как раз и убирает эти лишние изгибы, приближая нас к решению, когда число параметров нейросети близко к количеству заданных точек.
👍2
Вдогонку к предыдущему посту...
Чтобы ломанная стала кривой достаточно в начале сети добавить какую-то активацию с криволинейным графиком, например swish. Остльные активации можно оставить ReLU. Тогда учится чуть лучше, ибо у сети добавляется ещё одно свободное измерение: она может пытаться искривлять кривую и это позволяет проще сжимать или растягивать пространство.
Провожу сейчас эксперимент.

Свёрточная сеть, которая состоит из одной свёртки с ядром 3х3, которая применяется в цикле, уменьшая размер входа на два пикселя каждый раз. Когда размер уменьшится до 7х7, делается flatten() и потом линейный слой. После каждой эпохи подбирается LR для каждого weight и bias, дающий минимальной лосс на валидации. Без этого подбора сеть невозможно обучить, ибо через свёртку сигнал проходит очень много раз, а через линейный слой - единожды и их LR в теории должны отличаться. На практике вышло иначе.

Вот что заметил: значения LR скачут вверх-вниз, часто стараясь приблизиться к нижней границе вроде 1e-10. Т.е. фактически происходит полноценное обучения только отдельных слоёв, а обучение других сильно замедляется.

Можно выдвинуть гипотезу: для ускорения обучения нужно учить не всю сеть, а только её отдельные её слои, периодически меняя их. Интересный вопрос: как понять, какой слой сейчас учить, а какой нет. Может W*W.grad тут поможет тоже, как при прунинге?

Вторая гипотеза: наверное можно научиться рассчитывать оптимальный LR для каждого слоя. А может и для каждого веса в отдельности.
🔥3👍1
Придумал возможно неплохую замену пулинга.
Одна свёртка с ядром 3х3, которая применяется до тех пор, пока размер её выхода не станет N*N. Если вход такой свёртки меньше, чем N+2*N+2, то предварительно дополняем вход с нужных сторон нулями. Потом делаем Flatten() и далее линейный слой Linear(N*N*C, num_classes). Такая конструкция позволяет линейному слою видеть отдельные пиксели, что очень полезно. И при этом сохраняется способность свёрточных сетей работать с любым разрешением, хотя это надо проверять или специально учить на разных разрешениях.
N можно брать равным 7-8, например.

Проблема только в текущей архитектуре большинства свёрточных сетей. Они в распушаются ближе к концу на пару-тройку тысяч каналов. Получится, что надо сначала снизить число каналов до 64 хотя бы, например свёрткой с ядром 1х1.
В слое свёртки есть число групп. Это то, на сколько групп делится вход свёртки, прежде чем применяется свёртка. Потом выходы конкатенируются. Группы сильно ускоряют свёртки и активно применяются в свёрточных сетях для мобильных устройств.

Мне странно, что до сих пор никто не предложил реализовать группы для обычного линейного слоя. Так же разбиваем вход на несколько частей по последнему измерению, делаем линейное преобразование (разное для каждой группы) и потом конкатенируем выходы.

Линейный слой с группами позволяет работать с входом больших размеров, не сильно потребляя ресурсы.

Я провёл несколько экспериментов с resnet18 на датасете Imagewoof2 ( https://github.com/fastai/imagenette?tab=readme-ov-file#imagewoof ). Там 10 классов с похожими породами собак, взятыми из Imagenet-а. У resnet18 выход размера (batch, 512, 7, 7).
Сравнивал три типа пулингов:
1) обычный пулинг avg (обёртка вокруг torch.nn.functional.adaptive_avg_pool2d());
2) замену его на линейный слой linear (b,c,h,w решейпим в b, c*h*w и передаём в nn.Linear(7*7*512, 512));
3) замену пулинга на линейный слой с 16 группами linear-groups (b,c,h,w решейпим в b, c*h*w , потом делим на 16 частей и передаём в 16 слоёв nn.Linear(7*7*512//16, 512//16), потом torch.cat(outputs, dim=1)).
После всех видов пулинга шёл обычный линейный слой nn.Linear(512, 10). Слоя активации между слоями пулинга и линейным слоем не было. Я его забыл. 😊

Пулинг с linear-groups чуть хуже показал себя при заморозке основной сети. Видимо какие-то важные фичи оказывались в разных группах, не смогли просочиться через снижение размерности и дойти до FC. Но gулинг с linear-groups оказался лучшим, когда сеть тренировалась с нуля. Файнтюнить не пока пробовал. И не совсем ясно, как интерпретировать результаты после файнтюнинга.

Что меня удивило, просто линейный слой увеличил количество весов у сети почти вдвое, но при обучении всей сети с нуля, это слабо повлияло качество. А при обучении только пулинга и FC - дало лучший результат. Как будто такому пулингу не хватало при обучении разнообразия данных.

При обучении использовал train.py из timm, меняя аргумент скриптв —gp . Гиперпараметры брал из https://arxiv.org/abs/2110.00476 : python3 train.py imagewoof2-320/ --model resnet18 --amp --batch 192 --lr 3.3e-3 --epochs 3000 --weight-decay 0.01 --sched cosine --train-interpolation bicubic --crop-pct 0.95 --smoothing 0.1 --warmup-epochs 5 --aa rand-m7-n3-mstd1.0-inc1 --seed 0 --opt adamp --warmup-lr 1e-6 --drop-path 0.05 --drop 0.1 --reprob 0.35 --mixup .2 --cutmix 1.0 --bce-loss --num-classes 10
resnet18

Учились только пулинг и FC. 600 эпох

avg
params:11181642
"best": {
"epoch": 223,
"train": {
"loss": 0.22416554137747338
},
"validation": {
"loss": 0.3067730319245106,
"top1": 92.87350753780296,
"top5": 99.56732228998314
}
}

linear
params:24027210
"best": {
"epoch": 577,
"train": {
"loss": 0.2400615208960594
},
"validation": {
"loss": 0.369677066992609,
"top1": 93.07712204628059,
"top5": 99.46551471728652
}
}

linear-groups
params: 11984970
"best": {
"epoch": 518,
"train": {
"loss": 0.2248994378333396
},
"validation": {
"loss": 0.31120124888969275,
"top1": 92.7207971418987,
"top5": 99.56732154432585
}
}


Училось всё с самого начала. 600 эпох

avg
params:11181642
"best": {
"epoch": 598,
"train": {
"loss": 0.18741627560651047
},
"validation": {
"loss": 0.3714630286804014,
"top1": 90.50649261086247,
"top5": 98.62560719805353
}
}

linear
params:24027210
"best": {
"epoch": 586,
"train": {
"loss": 0.19136825647759945
},
"validation": {
"loss": 0.37503323835797453,
"top1": 90.22652338600304,
"top5": 98.54925189135972
}
}

linear-groups
params: 11984970
"best": {
"epoch": 539,
"train": {
"loss": 0.2023189218437418
},
"validation": {
"loss": 0.37809756763808505,
"top1": 90.55739677003942,
"top5": 98.82922085213218
}
}

Училось всё с самого начала. 3000 эпох.

avg
params:11181642
"best": {
"epoch": 2410,
"train": {
"loss": 0.17291559152146604
},
"validation": {
"loss": 0.3654449778557066,
"top1": 91.39730455335936,
"top5": 98.4219927983176
}
}

linear
params:24027210
"best": {
"epoch": 2824,
"train": {
"loss": 0.1703560750218148
},
"validation": {
"loss": 0.37178307608600547,
"top1": 91.37185314175554,
"top5": 98.11657157154234
}
}

linear-groups
"best": {
"epoch": 2524,
"train": {
"loss": 0.16850097937152742
},
"validation": {
"loss": 0.3603533955590056,
"top1": 91.60091879775003,
"top5": 98.2183781344947
}
}

linear-groups + swish
"best": {
"epoch": 2336,
"train": {
"loss": 0.17537949963452967
},
"validation": {
"loss": 0.36147605745535705,
"top1": 91.19369144298916,
"top5": 98.47289642932064
}
}
Вот работающий код линейного слоя с группами. И функция для примерного расчёта количества групп при пулинге в зависимости от размера входного тензора:

class Linear(torch.nn.Module):
def __init__(
self,
in_features: int,
out_features: int,
groups: int = 1,
bias: bool = True,
device=None,
dtype=None
) -> None:

if groups <= 0:
raise ValueError('groups must be a positive integer')
if in_features % groups != 0:
raise ValueError('in_features must be divisible by groups')
if out_features % groups != 0:
raise ValueError('out_features must be divisible by groups')

factory_kwargs = {'bias':bias, 'device': device, 'dtype': dtype}

super().__init__()
self.in_features = in_features
self.out_features = out_features
self.groups = groups
self.linears = torch.nn.ModuleList([torch.nn.Linear(in_features//groups, out_features//groups, **factory_kwargs) for i in range(groups)])
self.reset_parameters()

def reset_parameters(self) -> None:
for linear in self.linears:
linear.reset_parameters()

def forward(self, input: torch.Tensor) -> torch.Tensor:
outputs = []
step = self.in_features//self.groups
for i, linear in enumerate(self.linears):
start = i*step
outputs.append(linear(input[:, start:start + step]))

return torch.cat(outputs, dim=1)

def extra_repr(self) -> str:
return f'in_features={self.in_features}, out_features={self.out_features}, groups={self.groups}, bias={self.bias is not None}'

def groups(c,h,w):
for i in range(1, c):
if c/i == c//i:
if c*h*w/i <= 2048:
return i

return 1
Добавил после пулинга linear-groups слой активации swish и обучил всю сетку с нуля на 3000 эпох. Может это статистические флуктуации, но и accuracy и loss на валидации оказались хуже, чем без активации. Цифры добавил в конец в пост https://t.me/chivinya/61

Но если активация в данном случае и вправду чуть портит сеть, то польза забывчивости и невнимательности при экспериментах несомненна. 😊
👏1👌1
Naver выпустил свою версию DenseNet под названием Revitalized DenseNets (RDNet) https://arxiv.org/pdf/2403.19588 . Они придумали фактически новую архитектуру свёрточных сетей.

Раньше сеть у нас выглядела как этакая расширяющаяся по числу каналов труба (VGG). Потом придумали блоки свёрток, увеличивающие внутреннюю размерность и потом тут же уменьшающие её (MobileNet), труба покрылась пилообразными выступами. В Naver придумали более общий вариант блоков (DenceBlock), когда размерность медленно увеличивается (у них три таких увеличения происходит), потом схлопывается. И так много раз. И при расширении каналы конкатенируются к входному сигналу. Внутри вместо свёрток применяются блоки свёрток. Т.е. dance-блоки из свёрточных блоков, группирующиеся в stages. :-)

Но основная идея - это постепенно увеличивать размерность сигнала через конкатенацию и потом схлопывать его. И так много раз. Труба трубы в трубе, короче 😊 При этом удаётся избежать сильного расширения размерности, т.е. числа каналов.
Часто в последнее время читаю про мир-технологию "искусственный интеллект". Т.е. это преподносится как нечто, что кардинально изменит существующий мир.

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

Уже сейчас возникли SLM (small language model). Само возникновение этого термина говорит о закате LLM, с надеждами на развитие которых связывается сейчас приход сильного ИИ. И дальше мы увидим приближение качества работы LLM и специализированных SLM. Крах OpenAi, как лидера, в которого вбухали больше всего неоправдавшихся ожиданий. Появление огромных открытых текстовых датасетов, как признание неспособности создать из них ИИ. Новую зиму. И снова весну. Наверное уже чего-то более живого, чем чат с сетью- трансформером.

Момент чем-то напоминает время, когда вышли 1080 ti, хотелось обучить на них свой Resnet, ImageNet хрен скачаешь, учить сложно и долго. 😀
🤔4
Вспомнилось время, когда Гугл изобрёл PagaRank, выдача стала на редкость релевантной, аудитория стала расти, а с монетезаций были проблемы. Очень напоминает то, где сейчас OpenAI. Решением проблемы тогда стала платная реклама в поисковой выдаче. Она же может выручить нас и сейчас. Если текущий ИИ, сначала станет очень приятным собеседником, а потом научится по-дружески ненавязчиво впаривать то, что оплатили рекламодатели, то нам сильно повезёт. Ибо оборотная и никому не нужная сторона процесса - это когда он научится ходить и стрелять.
👍1
Некоторое время назад я прочёл о том, как @anvilarth изобретал/открывал новое/решал сложное: https://t.me/awesome_dl/76 . Эта тема мне очень близка, ибо самому часто приходят интересные мысли и это значимая часть моей жизни. Понятное дело, что таких людей много и есть те, кто описывает процесс своей изобретательской работы.

Совершенно случайно так получилось, что хорошие знакомые подсказали книгу Жака Адамара "Исследование психологии процесса изобретения в области математики". В ней автор рассказывает о том, как разные люди описывают свои моменты открытий, что им предшествовало, что было потом. Они на удивление похожи, но плохо вписываются в научный подход к познание, ибо воспроизводимость хромает.

Самое интересное в ней - это подробное описание процесса изобретения от Пуанкаре: https://ega-math.narod.ru/Math/Hadamard.htm#app3 . Оно составляет ценную часть книги. Это небольшой текст, но в нём описаны все важные моменты.
2
Вдогонку к предыдущему посту о том, как происходят озарения...

Любопытно то, что 100 лет назад математики массово совершенно свободно обсуждали такие зыбкие вопросы и никто не тыкал им, что они занимаются антинаучной ересью. Не философы, композиторы или художники, а математики обсуждали!

Нынче мы в этом вопросе откатились куда-то не туда, ибо сейчас большинство на полном серьёзе отрицает существование всего, что нельзя подтвердить с помощью научного подхода. Не воспроизводится в опыте - значит не существует.
Пришла мысль, что можно много спорить о том, есть ли уже интеллект у того, что делает OpenAI и прочие. Но правда в том, что реальный искусственный интеллект возможно и не так уж и нужен. Большинству вполне сойдёт хорошая его имитация.
👌2💯1
В продолжение прошлого поста...

В разгар Второй мировой Айзек Азимов формирует три закона робототехники https://ru.wikipedia.org/wiki/%D0%A2%D1%80%D0%B8_%D0%B7%D0%B0%D0%BA%D0%BE%D0%BD%D0%B0_%D1%80%D0%BE%D0%B1%D0%BE%D1%82%D0%B5%D1%85%D0%BD%D0%B8%D0%BA%D0%B8 . Внимание, вопрос: как реализовать выполнение этих законов на практике? Например, на текущем уровне развития нейронок.

Наверное, одна нейронка что-то генерит (например, текст), а вторая проверяет, на сколько сгенерённое безопасно. И, кстати, все предыдущие генерации вместе с новой тоже. В голову сразу приходят состязательные сети, где одна пытается обмануть другую. И вспомним, что для их обучения требуется примерно одинаковая выразительная сила нейронок. В случае с контролем одной нейронкой другой, очевидно, что генерящая должна быть значительно слабее, чем проверяющая. Но даже это не гарантирует, что сильную нейронку не получится обмануть, ибо это не та задача, где на проверочной выборке у нас 100% accuracy.

Из чего, надеюсь пока, следует два грустных вывода:
- делать безопасные для человека нейронки экономически невыгодно;
- сделать абсолютно безопасную нейронку невозможно.

Описанное не совсем корректно, ибо я сам придумал схему с двумя нейронками и сам показал её ущербность. Возможно есть другие подходы. Например, при обучении нейронки пенализировать генерации, которые похожи на плохие. Или что-то подобное.

P.S.
Сейчас безопасность в текстовых моделях реализуется порой регэкспами, что конечно курам на смех, но позволяет снять политическую остроту, например.
Утром родилась мысль о том, что слои в сети можно переставлять случайным образом или выбирать нужное количество раз случайный слой из имеющихся. В случае со свёртками обоснование такое: свёртка фильтрует всё лишнее и если выполнить эту фильтрацию несколько раз, то не сильно важно, в какой последовательности. Оказалось, что этот подход работает. И даёт результаты чуть хуже, чем если бы свёртки шли всегда в одном и том же порядке. Вот три варианта сети: базовый и два со случайным выбором слоёв или их последовательности:

class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.stem = nn.Conv2d(1, 32, 3)
self.conv1 = nn.Conv2d(32, 32, 3)
self.conv2 = nn.Conv2d(32, 32, 3)
self.conv3 = nn.Conv2d(32, 32, 3)
self.fc1 = nn.Linear(3200, 10)

def forward(self, x):
x = self.stem(x)
x = F.relu(x)

if self.training: # обучение

# # 0 вариант
# # Accuracy: 9916/10000
# x = self.conv1(x)
# x = F.relu(x)
# x = self.conv2(x)
# x = F.relu(x)
# x = self.conv3(x)
# x = F.relu(x)

# # 1 вариант
# # Accuracy: 9880/10000
# for _ in range(3):
# conv = random.choice([self.conv1,self.conv2,self.conv3])
# x = conv(x)
# x = F.relu(x)

# # 2 вариант
# # Accuracy: 9888/10000
# # так лучше, ибо тогда свёртка выполняется одинаковое число раз, а потому всегда будут градиенты и они будут схожей амплитуды.
# convs = [self.conv1,self.conv2,self.conv3]
# random.shuffle(convs)
# for conv in convs:
# x = conv(x)
# x = F.relu(x)

# 3 вариант
# все три раза один слой используем
# Accuracy: 9925/10000
x = self.conv1(x)
x = F.relu(x)
x = self.conv1(x)
x = F.relu(x)
x = self.conv1(x)
x = F.relu(x)

else: # валидация
# x = self.conv1(x)
# x = F.relu(x)
# x = self.conv2(x)
# x = F.relu(x)
# x = self.conv3(x)
# x = F.relu(x)

x = self.conv1(x)
x = F.relu(x)
x = self.conv1(x)
x = F.relu(x)
x = self.conv1(x)
x = F.relu(x)

x = F.max_pool2d(x, kernel_size=2)
x = torch.flatten(x, start_dim=1)
x = self.fc1(x)
return x


Училось на MNIST-е. Перед каждой эпохой подбирались оптимальные LR и WD для weight и bias трёх групп параметров: стема, свёрток и полносвязанного слоя. Хотелось так обезопасить эксперимент от влияния неверных гиперпараметров. Училось 14 эпох. В комментариях в коде сети я написал accuracy, которых достиг каждый вариант сети.
Для задачи классификации при работе с нейронками есть три известных мне подхода. One hot encoding, генерацию векторного представления и потом поиск похожих среди уже классфицированных векторов и генерация класса в виде текста (хотя можно использовать и другие модальности, а не только текст). Первые удобны для машин, третий - для человека. Но я не знаю удобного и для машин и для человека одновременно.
👍1
Чивиня (Multi-layer Parkinson)
Утром родилась мысль о том, что слои в сети можно переставлять случайным образом или выбирать нужное количество раз случайный слой из имеющихся. В случае со свёртками обоснование такое: свёртка фильтрует всё лишнее и если выполнить эту фильтрацию несколько…
Переделал всё на Cifar10, добавил больше свёрток и уменьшил полносвязанный слой, чтобы хорошо видеть эффект именно от свёрток. Получилась вот такая сетка (в ней разные варианты с полученными при обучении значениями accuracy):
class Net(nn.Module):
def __init__(self):
super().__init__()
self.stem = nn.Conv2d(3, 32, 3)
self.conv1 = nn.Conv2d(32, 32, 3)
self.conv2 = nn.Conv2d(32, 32, 3)
self.conv3 = nn.Conv2d(32, 32, 3)
self.conv4 = nn.Conv2d(32, 32, 3)
self.conv5 = nn.Conv2d(32, 32, 3)
self.conv6 = nn.Conv2d(32, 32, 3)
self.conv7 = nn.Conv2d(32, 32, 3)
self.conv8 = nn.Conv2d(32, 32, 3)
self.conv9 = nn.Conv2d(32, 32, 3)
self.fc1 = nn.Linear(288, 10)

def forward(self, x):
x = self.stem(x)
x = F.relu(x)

# 0 вариант
# Accuracy: 6417/10000
x = self.conv1(x)
x = F.relu(x)
x = self.conv2(x)
x = F.relu(x)
x = self.conv3(x)
x = F.relu(x)
x = self.conv4(x)
x = F.relu(x)
x = self.conv5(x)
x = F.relu(x)
x = self.conv6(x)
x = F.relu(x)
x = self.conv7(x)
x = F.relu(x)
x = self.conv8(x)
x = F.relu(x)
x = self.conv9(x)
x = F.relu(x)

# 1 вариант
# Accuracy: 5902/10000
for _ in range(3):
x = self.conv1(x)
x = F.relu(x)
x = self.conv2(x)
x = F.relu(x)
x = self.conv3(x)
x = F.relu(x)


# 2 вариант
# Accuracy: 5351/10000
for _ in range(9):
x = self.conv1(x)
x = F.relu(x)


# 3 вариант
# Accuracy: 4081/10000
if self.training: # обучение
for _ in range(9):
conv = random.choice([self.conv1,self.conv2,self.conv3])
x = conv(x)
x = F.relu(x)
else: # валидация
for _ in range(3):
x = self.conv1(x)
x = F.relu(x)
x = self.conv2(x)
x = F.relu(x)
x = self.conv3(x)
x = F.relu(x)

# 4 вариант
# Accuracy: 2652/10000
if self.training: # обучение
for _ in range(9):
conv = random.choice([self.conv1,self.conv2,self.conv3,self.conv4,self.conv5,self.conv6,self.conv7,self.conv8,self.conv9])
x = conv(x)
x = F.relu(x)
else: # валидация
x = self.conv1(x)
x = F.relu(x)
x = self.conv2(x)
x = F.relu(x)
x = self.conv3(x)
x = F.relu(x)
x = self.conv4(x)
x = F.relu(x)
x = self.conv5(x)
x = F.relu(x)
x = self.conv6(x)
x = F.relu(x)
x = self.conv7(x)
x = F.relu(x)
x = self.conv8(x)
x = F.relu(x)
x = self.conv9(x)
x = F.relu(x)

# 5 вариант
# Accuracy: 3798/10000
if self.training: # обучение
for _ in range(3):
convs = [self.conv1,self.conv2,self.conv3]
random.shuffle(convs)
for conv in convs:
x = conv(x)
x = F.relu(x)
else: # валидация
for _ in range(3):
x = self.conv1(x)
x = F.relu(x)
x = self.conv2(x)
x = F.relu(x)
x = self.conv3(x)
x = F.relu(x)
Чивиня (Multi-layer Parkinson)
Утром родилась мысль о том, что слои в сети можно переставлять случайным образом или выбирать нужное количество раз случайный слой из имеющихся. В случае со свёртками обоснование такое: свёртка фильтрует всё лишнее и если выполнить эту фильтрацию несколько…


# 6 вариант
# Accuracy: 3131/10000
if self.training: # обучение
convs = [self.conv1,self.conv2,self.conv3,self.conv4,self.conv5,self.conv6,self.conv7,self.conv8,self.conv9]
random.shuffle(convs)
for conv in convs:
x = conv(x)
x = F.relu(x)
else: # валидация
x = self.conv1(x)
x = F.relu(x)
x = self.conv2(x)
x = F.relu(x)
x = self.conv3(x)
x = F.relu(x)
x = self.conv4(x)
x = F.relu(x)
x = self.conv5(x)
x = F.relu(x)
x = self.conv6(x)
x = F.relu(x)
x = self.conv7(x)
x = F.relu(x)
x = self.conv8(x)
x = F.relu(x)
x = self.conv9(x)
x = F.relu(x)

x = F.avg_pool2d(x, kernel_size=4)
x = torch.flatten(x, start_dim=1)
x = self.fc1(x)
return x


В целом вывод такой: свёрткам сложно адаптироваться под разное положение в сети.
Была гипотеза, что так можно уменьшать избыточную выразительную способность слоёв, но похоже случайно менять положения слоя в сети - это явный перебор.
🤔1