Ещё одна интересная вещь в работе по DINO была...
Они использовали шедуллер для weight decay (WD).
Отличие оптимизаторов Adam от AdamW в том, что в последнем WD домножается на LR (поэтому обычно он значительно больше при обучении с AdamW ). LR обычно меняется шедуллером по мере обучения и это умножение делает баланс между LR и WD постоянным. Возможно это правильно. А возможно стоит шатать WD и LR в противофазе. Возможно это просадит метрики во время такого шатания, но в итоге может дать результат лучше, чем если б баланс между LR и WD не нарушался.
Я не экспериментировал на эту тему, но как-то смотрел сумму норм весов по всей сети. Она сначала растёт (активно работает оптимизатор), потом разворачивается и начинает падать (оптимизатор постепенно уступает WD). А если подождать очень долго, то потом резко выходит на почти горизонтальное плато, т.е. достигнут баланс между оптимизатором и WD. Правда небольшое уменьшение весов всё таки происходит при этом. Дальше не учил, не знаю, что бывает...
Они использовали шедуллер для weight decay (WD).
Отличие оптимизаторов Adam от AdamW в том, что в последнем WD домножается на LR (поэтому обычно он значительно больше при обучении с AdamW ). LR обычно меняется шедуллером по мере обучения и это умножение делает баланс между LR и WD постоянным. Возможно это правильно. А возможно стоит шатать WD и LR в противофазе. Возможно это просадит метрики во время такого шатания, но в итоге может дать результат лучше, чем если б баланс между LR и WD не нарушался.
Я не экспериментировал на эту тему, но как-то смотрел сумму норм весов по всей сети. Она сначала растёт (активно работает оптимизатор), потом разворачивается и начинает падать (оптимизатор постепенно уступает WD). А если подождать очень долго, то потом резко выходит на почти горизонтальное плато, т.е. достигнут баланс между оптимизатором и WD. Правда небольшое уменьшение весов всё таки происходит при этом. Дальше не учил, не знаю, что бывает...
👍2
Я к тому, что баланс между оптимизатором и WD чаще всего не наступает в обученных сетях. И WD скорее всего можно шатать в очень больших пределах. В начале обучения это излишне, а как обучение выходит на плато - возможно временное увеличение WD сильно поможет.
👍1
На праздниках запускал обучение сетки из 1 млн. линейных слоёв. Прошло 3 эпохи и результаты такие:
LR был 1e-7 . Наверное надо было взять его побольше...
Test set: Average loss: 1.5966, Accuracy: 5306/10000 (53%)
Test set: Average loss: 1.5637, Accuracy: 5415/10000 (54%)
Test set: Average loss: 1.5002, Accuracy: 5640/10000 (56%)
LR был 1e-7 . Наверное надо было взять его побольше...
❤1
В https://habr.com/ru/companies/airi/articles/816125/ мельком описан интересный лосс: он стремится так изменить веса, чтобы вход и выход совпадали. Это регуляризация ИМХО позволяет убрать шум от случайной инициализации, оставляя только те веса, которые реально нужны для обучения. При этом может применяться к любым слоям/блокам, ничего не зная об их структуре.
Хабр
Большие языковые модели гораздо линейнее, чем мы думали
Хабр, привет! Это снова Антон Разжигаев, аспирант Сколтеха и научный сотрудник лаборатории FusionBrain в Институте AIRI, где мы продолжаем углубляться в изучение языковых моделей. В прошлый раз мы...
🤔1
Запускал обучение сетки из 1 млн. линейных слоёв. Взял LR побольше: 3e-7. результаты сильно хуже:
Test set: Average loss: 1.8179, Accuracy: 4412/10000 (44%)
Test set: Average loss: 1.7524, Accuracy: 4307/10000 (43%)
Test set: Average loss: 1.7991, Accuracy: 3730/10000 (37%)
Test set: Average loss: 1.7636, Accuracy: 3763/10000 (38%)
Test set: Average loss: 1.8692, Accuracy: 2959/10000 (30%)
Решил воспользоваться идеей @kraidiky по периодическому поиску оптимальной скорости обучения. Только искать не один learning rate, но ещё и weight decay. Причём отдельно для свёрток и отдельно для MLP, а также отдельно для весов и отдельно для сдвигов. Получилось 4 пары LR и WD. В пределе можно было бы искать их для каждого слоя, но у меня сетка примитивная. Прототипом в очередной раз послужил код из примеров Торча: https://github.com/pytorch/examples/tree/main/mnist . Если брать сети вроде ResNet-ов и EfficientNet-ов, то там очень большое пространство подбора и надо подумать эффективно его делать. Возможно внутри блока свёрток будут разные LR и WD, и возможно они будут разными у каждой группы блоков, работающих с одним разрешением.
Исходный скрипт имеет оптимизатор Adadelta, который судя по его описанию сам подбирает оптимальный LR для каждого веса. С другими учится сильно хуже. Проблема данной архитектуры сети в том, что для MLP нужен LR раз в 10-20 больше, чем для свёрток. Так происходит потому, что архитектура сети нестандартная, в ней нет пулинга полностью схлопывающего ширину и высоту:
Перед каждой эпохой я добавил поиск оптимальных learning rate и weight decay. Спомощью Optuna я обучал 30 сетей, клонированных из текущей, и подбирал 4 пары LR и WD, которые потом использовал для обучения одной эпохи. Всего было 100 эпох. На MNIST-е отработало быстро.
Исходный код давал accuracy 99.25% на 14 эпохах. Если учить дольше, то получалось иногда 99.3%.
С подбором оптимальных LR и WD после 66-ой эпохе получилось достичь 99.43%, что не мало. И само обучение шло в начале быстрее.
Вот таблица с данными, полученными при обучении 100 эпох. Там же есть разные графики. https://docs.google.com/spreadsheets/d/1PrkH42b10jFQ8ExgRhFixocoS3n8hALLLVwOU7NOLzE/edit?usp=sharing .
Ещё один важный момент: сеть не переобучается. Т.е. фактически переобучение происходит тогда, когда мы используем неподходящие LR и WD. Возможно в этом утверждении я не прав, но на графиках переобучения не видно.
У подхода есть и минусы. Возможно существуют такие LR и WD, которые дают на одной эпохе плохое значение метрики, но на нескольких эпохах - значительно лучшее. И это никак не узнать.
Исходный скрипт имеет оптимизатор Adadelta, который судя по его описанию сам подбирает оптимальный LR для каждого веса. С другими учится сильно хуже. Проблема данной архитектуры сети в том, что для MLP нужен LR раз в 10-20 больше, чем для свёрток. Так происходит потому, что архитектура сети нестандартная, в ней нет пулинга полностью схлопывающего ширину и высоту:
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
self.conv1 = nn.Conv2d(1, 32, 3, 1)
self.conv2 = nn.Conv2d(32, 64, 3, 1)
self.dropout1 = nn.Dropout(0.25)
self.dropout2 = nn.Dropout(0.5)
self.fc1 = nn.Linear(9216, 128)
self.fc2 = nn.Linear(128, 10)
def forward(self, x):
x = self.conv1(x)
x = F.relu(x)
x = self.conv2(x)
x = F.relu(x)
x = F.max_pool2d(x, kernel_size=2)
x = self.dropout1(x)
x = torch.flatten(x, start_dim=1)
x = self.fc1(x)
x = F.relu(x)
x = self.dropout2(x)
x = self.fc2(x)
return x
Перед каждой эпохой я добавил поиск оптимальных learning rate и weight decay. Спомощью Optuna я обучал 30 сетей, клонированных из текущей, и подбирал 4 пары LR и WD, которые потом использовал для обучения одной эпохи. Всего было 100 эпох. На MNIST-е отработало быстро.
Исходный код давал accuracy 99.25% на 14 эпохах. Если учить дольше, то получалось иногда 99.3%.
С подбором оптимальных LR и WD после 66-ой эпохе получилось достичь 99.43%, что не мало. И само обучение шло в начале быстрее.
Вот таблица с данными, полученными при обучении 100 эпох. Там же есть разные графики. https://docs.google.com/spreadsheets/d/1PrkH42b10jFQ8ExgRhFixocoS3n8hALLLVwOU7NOLzE/edit?usp=sharing .
Ещё один важный момент: сеть не переобучается. Т.е. фактически переобучение происходит тогда, когда мы используем неподходящие LR и WD. Возможно в этом утверждении я не прав, но на графиках переобучения не видно.
У подхода есть и минусы. Возможно существуют такие LR и WD, которые дают на одной эпохе плохое значение метрики, но на нескольких эпохах - значительно лучшее. И это никак не узнать.
👍1
Повторил вчерашний эксперимент и добавил данные с графиками туда же на отдельный лист: https://docs.google.com/spreadsheets/d/1PrkH42b10jFQ8ExgRhFixocoS3n8hALLLVwOU7NOLzE/edit?usp=sharing
Обучилось хуже. И ИМХО видны причины такого обучения: это высокий LR у bias-ов свёрток.
Сейчас интервал поиска нового параметра меняется от 1/3 до 3 от старого значения. Возможно стоит этот интервал увеличить, чтобы не происходило подобного залипания на высоком LR, а могло скатиться к маленькому. Хотя тогда графики станут более дёрганными.
Переобучения, кстати, опять не видно. Может, конечно, сетка очень маленькая...
Обучилось хуже. И ИМХО видны причины такого обучения: это высокий LR у bias-ов свёрток.
Сейчас интервал поиска нового параметра меняется от 1/3 до 3 от старого значения. Возможно стоит этот интервал увеличить, чтобы не происходило подобного залипания на высоком LR, а могло скатиться к маленькому. Хотя тогда графики станут более дёрганными.
Переобучения, кстати, опять не видно. Может, конечно, сетка очень маленькая...
Google Docs
LR and WD by epoch
Во время криво настроенных экспериментов выяснилось, что сетка вполне себе может обучаться с learning rate 1e+16 и weight decay 5e+23! Конечно она пришла к таким огромным величинам не сразу, но пришла. И при них вполне себе удерживала accuracy 99.33% на MNIST-е .
Из этого можно сделать вывод, что неверный LR можно компенсировать WD.
Из этого можно сделать вывод, что неверный LR можно компенсировать WD.
🤔2❤1🔥1
Провёл ещё 3 эксперимента с подбором оптимального LR и WD каждую эпоху. Данные и графики добавил на отдельные листы https://docs.google.com/spreadsheets/d/1PrkH42b10jFQ8ExgRhFixocoS3n8hALLLVwOU7NOLzE/edit?usp=sharing .
После каждой эпохи сеть обучалась 100 раз на одну эпоху и выбирались оптимальные значения LR и WD. Т.е. в последние два эксперимента было обучено по 100 тыс. эпох. Эффективнее всего оказалось не смотреть на предыдущие значения LR и WD, как я делал до этого, а выбирать их каждый заново из заранее определённого интервала. Я использовал интервал от 1e-8 до 1e-2. Графики выходят ужасными и пока не понял, как их сгладить в гугл-докс, но зато сеть быстро достигает наилучших значений метрики.
И если посмотреть на графики всех пяти экспериментов, то снова видно полное отсутствие переобучения. Для MNIST-а 1000 эпох вполне достаточно, чтобы оно проявилось хоть как-то.
После каждой эпохи сеть обучалась 100 раз на одну эпоху и выбирались оптимальные значения LR и WD. Т.е. в последние два эксперимента было обучено по 100 тыс. эпох. Эффективнее всего оказалось не смотреть на предыдущие значения LR и WD, как я делал до этого, а выбирать их каждый заново из заранее определённого интервала. Я использовал интервал от 1e-8 до 1e-2. Графики выходят ужасными и пока не понял, как их сгладить в гугл-докс, но зато сеть быстро достигает наилучших значений метрики.
И если посмотреть на графики всех пяти экспериментов, то снова видно полное отсутствие переобучения. Для MNIST-а 1000 эпох вполне достаточно, чтобы оно проявилось хоть как-то.
Было время пообучать сетку в миллион линейных слоёв. Добавил данные по LR=3e-8:
lr=3e-7:
Test set: Average loss: 1.8179, Accuracy: 4412/10000 (44%)
Test set: Average loss: 1.7524, Accuracy: 4307/10000 (43%)
Test set: Average loss: 1.7991, Accuracy: 3730/10000 (37%)
Test set: Average loss: 1.7636, Accuracy: 3763/10000 (38%)
Test set: Average loss: 1.8692, Accuracy: 2959/10000 (30%)
lr=1e-7:
Test set: Average loss: 1.5966, Accuracy: 5306/10000 (53%)
Test set: Average loss: 1.5637, Accuracy: 5415/10000 (54%)
Test set: Average loss: 1.5002, Accuracy: 5640/10000 (56%)
lr=3e-8:
Test set: Average loss: 1.5202, Accuracy: 5493/10000 (55%)
Test set: Average loss: 1.4754, Accuracy: 5743/10000 (57%)
Test set: Average loss: 1.4550, Accuracy: 5795/10000 (58%)
Test set: Average loss: 1.4516, Accuracy: 5940/10000 (59%)
Test set: Average loss: 1.4456, Accuracy: 5930/10000 (59%)
Test set: Average loss: 1.4228, Accuracy: 6075/10000 (61%)
Test set: Average loss: 1.4007, Accuracy: 6107/10000 (61%)
Подумалось...
L1-регуляризация обычно делается через лос-функцию примерно так:
Потом этот лосс прибавляют с каким-то коэффициентом к тому лосу, который используется для обучения, считают значения градиентов и оптимизатором меняют веса.
Но ведь L1-лос можно взять отдельно и считать его тоже отдельно, например, после обновления весов. Очевидно, что значения градиентов от такого лоса будут равны единице. И можно вообще обойтись без расчёта градиентов, а просто пройтись по всем весам и сделать так:
Точнее с учётом того, что значения весов могут быть отрицательными и меньше 1e-6, то вот так:
где 1e-6 - это аналог описанного выше коэффициента у L1-лоса, помноженного на LR, который использовался бы в оптимизаторе.
Возможно я ошибаюсь, то такая L1-регуляризация даст в итоге больше нулевых весов, не будет мешать обучению, направляя оптимизатор в ненужную сторону, быстрее варианта через лос-функцию.
L1-регуляризация обычно делается через лос-функцию примерно так:
l1_loss = sum(p.abs().sum() for p in model.parameters())
Потом этот лосс прибавляют с каким-то коэффициентом к тому лосу, который используется для обучения, считают значения градиентов и оптимизатором меняют веса.
Но ведь L1-лос можно взять отдельно и считать его тоже отдельно, например, после обновления весов. Очевидно, что значения градиентов от такого лоса будут равны единице. И можно вообще обойтись без расчёта градиентов, а просто пройтись по всем весам и сделать так:
w -= 1e-6
Точнее с учётом того, что значения весов могут быть отрицательными и меньше 1e-6, то вот так:
w -= w.sign()*min(w.abs(), 1e-6)
где 1e-6 - это аналог описанного выше коэффициента у L1-лоса, помноженного на LR, который использовался бы в оптимизаторе.
Возможно я ошибаюсь, то такая L1-регуляризация даст в итоге больше нулевых весов, не будет мешать обучению, направляя оптимизатор в ненужную сторону, быстрее варианта через лос-функцию.
👍1
Пробую через self supervised learning (как это делают в DINO https://arxiv.org/pdf/2104.14294 ) учить несколько подряд идущих свёрток. Но не как в DINO - всю сеть целиком, а каждую по отдельности.
Оказалось, что учить всю сеть целиком на пару порядков быстрее, чем отдельно её кусочки.
Оказалось, что так же как и в целиковой сети - сначала учатся последние слои. Видимо входной сигнал сильно сложнее, чем он же, но прошедший через несколько случайно инициализированных свёрток. Изначально у меня было предположение, что будет наоборот или же все начнут учиться одновременно.
Оказалось, что оптимальный LR сильно отличается в каждой свёртке. Но общее в том, что при LR=3e-4 (рекомендованный Andrej Karpathy) обычно начинает обучаться, а уже потом оптимальный LR обычно падает. Оптимальный - этот тот, на котором loss на тренировочной выборке в среднем падает.
Подтвердилось, что градиенты у bias-ов на пару порядков больше, чем у weight и потому для bias-ов LR надо в 100 раз меньше делать. А более общем случае надо конечно разделять свёртки на два слоя: в одном свёртка без bias-а, во втором - только bias для каждого канала.
Оказалось, что учить всю сеть целиком на пару порядков быстрее, чем отдельно её кусочки.
Оказалось, что так же как и в целиковой сети - сначала учатся последние слои. Видимо входной сигнал сильно сложнее, чем он же, но прошедший через несколько случайно инициализированных свёрток. Изначально у меня было предположение, что будет наоборот или же все начнут учиться одновременно.
Оказалось, что оптимальный LR сильно отличается в каждой свёртке. Но общее в том, что при LR=3e-4 (рекомендованный Andrej Karpathy) обычно начинает обучаться, а уже потом оптимальный LR обычно падает. Оптимальный - этот тот, на котором loss на тренировочной выборке в среднем падает.
Подтвердилось, что градиенты у bias-ов на пару порядков больше, чем у weight и потому для bias-ов LR надо в 100 раз меньше делать. А более общем случае надо конечно разделять свёртки на два слоя: в одном свёртка без bias-а, во втором - только bias для каждого канала.
👍1
Подумалось, что основной отличительной чертой настоящего ИИ будет не так всеми ожидаемая мудрость, а возможность оперировать со сложностью, недоступной человеку. Как калькулятор смог быстро работать с большими числами, так и ИИ сможет посчитать количество листьев на дереве, получив на то устную команду.
А истинная мудрость долго останется за человеком. Хотя находясь рядом с ИИ человеку будет проще отделить её от суррогатов.
А истинная мудрость долго останется за человеком. Хотя находясь рядом с ИИ человеку будет проще отделить её от суррогатов.
👍1
Росс пишет про эволюцию в обучении свёрточных сетей: https://huggingface.co/blog/rwightman/mobilenet-baselines . Как видно, заметную часть составляют изменения того, что никто никогда и не думал менять.
huggingface.co
MobileNet Baselines
A Blog post by Ross Wightman on Hugging Face
👍2
Прежде я очень смутно понимал, зачем в блоках свёртки наращивают количество каналов, а потом уменьшают. Тогдашняя трактовка звучала примерно так: "Чтобы можно было заколодовать в весах более сложные связи".
Текущее понимание такое: нейросети проще обучиться. Ей легче подогнать свои веса. Первая фаза запоминания датасета проходит быстрее. А в фазе обобщения изменение одних весов слабее влияют на изменение других. И проще найти те веса, которые быстрее приведут к уменьшению лоса. И сами эти две фазы видны глазами.
Всё это легко представить. Берём плоскость и на ней несколько точек, к которым нейросеть должна при обучении максимально приблизиться. И изначально так инициализируем веса, чтобы нейросеть на плоскости рисовала прямую линию (это не обязательно, но для визуализации полезно). И начинаем наше обучение и с очень низким LR. Мы увидим, как наша прямая линия начинает ломаться в нескольких точках и становится ломанной (если активация у нас ReLU). Далее становится очевидным, что чем больше переломов на этой линии, тем проще подогнать веса так, чтобы ломанная проходила через заданные точки на плоскости. Если отдельные наши точки сгруппированы в какой-то одной области, а там переломов недостаточно, то нейросети будет тяжело сжаться именно под эту группу точек. Она уже подогналась под другие и они не пускают её.
Так же и после обучения у нас получается ломанная. Она проходит через заданные точки, но и между ними изгибается, хотя от неё этого не требовали. Прунинг с файнтюнингом как раз и убирает эти лишние изгибы, приближая нас к решению, когда число параметров нейросети близко к количеству заданных точек.
Текущее понимание такое: нейросети проще обучиться. Ей легче подогнать свои веса. Первая фаза запоминания датасета проходит быстрее. А в фазе обобщения изменение одних весов слабее влияют на изменение других. И проще найти те веса, которые быстрее приведут к уменьшению лоса. И сами эти две фазы видны глазами.
Всё это легко представить. Берём плоскость и на ней несколько точек, к которым нейросеть должна при обучении максимально приблизиться. И изначально так инициализируем веса, чтобы нейросеть на плоскости рисовала прямую линию (это не обязательно, но для визуализации полезно). И начинаем наше обучение и с очень низким LR. Мы увидим, как наша прямая линия начинает ломаться в нескольких точках и становится ломанной (если активация у нас ReLU). Далее становится очевидным, что чем больше переломов на этой линии, тем проще подогнать веса так, чтобы ломанная проходила через заданные точки на плоскости. Если отдельные наши точки сгруппированы в какой-то одной области, а там переломов недостаточно, то нейросети будет тяжело сжаться именно под эту группу точек. Она уже подогналась под другие и они не пускают её.
Так же и после обучения у нас получается ломанная. Она проходит через заданные точки, но и между ними изгибается, хотя от неё этого не требовали. Прунинг с файнтюнингом как раз и убирает эти лишние изгибы, приближая нас к решению, когда число параметров нейросети близко к количеству заданных точек.
👍2
Вдогонку к предыдущему посту...
Чтобы ломанная стала кривой достаточно в начале сети добавить какую-то активацию с криволинейным графиком, например swish. Остльные активации можно оставить ReLU. Тогда учится чуть лучше, ибо у сети добавляется ещё одно свободное измерение: она может пытаться искривлять кривую и это позволяет проще сжимать или растягивать пространство.
Чтобы ломанная стала кривой достаточно в начале сети добавить какую-то активацию с криволинейным графиком, например swish. Остльные активации можно оставить ReLU. Тогда учится чуть лучше, ибо у сети добавляется ещё одно свободное измерение: она может пытаться искривлять кривую и это позволяет проще сжимать или растягивать пространство.
Провожу сейчас эксперимент.
Свёрточная сеть, которая состоит из одной свёртки с ядром 3х3, которая применяется в цикле, уменьшая размер входа на два пикселя каждый раз. Когда размер уменьшится до 7х7, делается flatten() и потом линейный слой. После каждой эпохи подбирается LR для каждого weight и bias, дающий минимальной лосс на валидации. Без этого подбора сеть невозможно обучить, ибо через свёртку сигнал проходит очень много раз, а через линейный слой - единожды и их LR в теории должны отличаться. На практике вышло иначе.
Вот что заметил: значения LR скачут вверх-вниз, часто стараясь приблизиться к нижней границе вроде 1e-10. Т.е. фактически происходит полноценное обучения только отдельных слоёв, а обучение других сильно замедляется.
Можно выдвинуть гипотезу: для ускорения обучения нужно учить не всю сеть, а только её отдельные её слои, периодически меняя их. Интересный вопрос: как понять, какой слой сейчас учить, а какой нет. Может W*W.grad тут поможет тоже, как при прунинге?
Вторая гипотеза: наверное можно научиться рассчитывать оптимальный LR для каждого слоя. А может и для каждого веса в отдельности.
Свёрточная сеть, которая состоит из одной свёртки с ядром 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.
Одна свёртка с ядром 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 (обёртка вокруг
2) замену его на линейный слой linear (b,c,h,w решейпим в b, c*h*w и передаём в
3) замену пулинга на линейный слой с 16 группами linear-groups (b,c,h,w решейпим в b, c*h*w , потом делим на 16 частей и передаём в 16 слоёв
После всех видов пулинга шёл обычный линейный слой
Пулинг с linear-groups чуть хуже показал себя при заморозке основной сети. Видимо какие-то важные фичи оказывались в разных группах, не смогли просочиться через снижение размерности и дойти до FC. Но gулинг с linear-groups оказался лучшим, когда сеть тренировалась с нуля. Файнтюнить не пока пробовал. И не совсем ясно, как интерпретировать результаты после файнтюнинга.
Что меня удивило, просто линейный слой увеличил количество весов у сети почти вдвое, но при обучении всей сети с нуля, это слабо повлияло качество. А при обучении только пулинга и FC - дало лучший результат. Как будто такому пулингу не хватало при обучении разнообразия данных.
При обучении использовал
Мне странно, что до сих пор никто не предложил реализовать группы для обычного линейного слоя. Так же разбиваем вход на несколько частей по последнему измерению, делаем линейное преобразование (разное для каждой группы) и потом конкатенируем выходы.
Линейный слой с группами позволяет работать с входом больших размеров, не сильно потребляя ресурсы.
Я провёл несколько экспериментов с 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 10resnet18
Учились только пулинг и 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
}
}