Мои личные заметки из fast.ai course. Эти примечания будут и дальше обновляться и улучшаться по мере того, как я продолжаю просматривать курс, чтобы по-настоящему понять его. Большое спасибо Джереми и Рэйчел, которые дали мне возможность учиться.

Уроки: 1234567891011 12 13 14

Генеративные состязательные сети (GAN)

Видео / Форум

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

Из последней лекции [1:04]: Одна из наших коллег по разнообразию, Кристин Пейн, имеет степень магистра медицины в Стэнфорде, и поэтому ей было интересно подумать, как бы это выглядело, если бы мы построили языковая модель медицины. Одна из вещей, которые мы кратко затронули еще в уроке 4, но особо не говорили в прошлый раз, - это идея о том, что вы можете фактически засеять генеративную языковую модель, что означает, что вы обучили языковую модель на каком-то корпусе, а затем вы собирается сгенерировать текст из этой языковой модели. Вы можете начать с того, что скормите ему несколько слов, чтобы сказать: «Вот первые несколько слов для создания скрытого состояния в языковой модели и генерации оттуда, пожалуйста. Кристина сделала что-то умное, посеяв его вопросом, повторила его три раза и позволила ему исходить оттуда. Она накормила языковую модель множеством различных медицинских текстов и задала вопросы, как вы видите ниже:

Что Джереми нашел интересным в этом, так это то, что это довольно близко к правдоподобному ответу на вопрос для людей, не имеющих степени магистра медицины. Но к реальности это не имеет никакого отношения. Он считает, что это интересный вид этических затруднений и проблем с пользовательским интерфейсом. Джереми работает в компании под названием doc.ai, которая пытается делать несколько вещей, но в итоге предоставляет приложение для врачей и пациентов, которое может помочь создать диалоговый пользовательский интерфейс, помогающий им с их медицинскими проблемами. Он постоянно говорил разработчикам программного обеспечения из этой команды, пожалуйста, не пытайтесь создать генеративную модель с использованием LSTM или чего-то подобного, потому что они действительно хорошо умеют создавать плохие советы, которые звучат впечатляюще - вроде политических ученых мужей или штатных профессоров, которые могу сказать чушь с большим авторитетом. Поэтому он подумал, что это действительно интересный эксперимент. Если вы провели интересные эксперименты, поделитесь ими на форуме, в блоге, в Твиттере. Расскажите об этом людям, и пусть вас заметят классные люди.

CIFAR10 [5:26]

Давайте поговорим о CIFAR10, и причина в том, что сегодня мы собираемся рассмотреть еще несколько простых вещей PyTorch, чтобы построить эти генеративные модели противоборства. На данный момент нет поддержки fastai, чтобы высказаться за GAN - скоро будет, но в настоящее время ее нет, поэтому мы собираемся создавать множество моделей с нуля. Прошло много времени с тех пор, как мы сделали много серьезных моделей. Мы рассмотрели CIFAR10 в первой части курса и построили что-то с точностью около 85% и потренировались за пару часов. Интересно, что сейчас идет соревнование, кто действительно может обучить CIFAR10 быстрее всех (DAWN), и цель состоит в том, чтобы заставить его тренироваться с точностью 94%. Было бы интересно посмотреть, сможем ли мы построить архитектуру, которая может достичь точности 94%, потому что это намного лучше, чем наша предыдущая попытка. Надеюсь, благодаря этому мы узнаем что-то о создании хороших архитектур, что будет полезно для сегодняшнего взгляда на GAN. Кроме того, это полезно, потому что Джереми гораздо глубже изучал статьи последних нескольких лет о различных типах архитектур CNN и понимает, что многие идеи, содержащиеся в этих документах, не получили широкого распространения и явно не получили широкого понимания. Поэтому он хочет показать вам, что произойдет, если мы сможем использовать это понимание.

Cifar10-darknet.ipynb [7:17]

Ноутбук называется даркнет, потому что конкретная архитектура, которую мы собираемся рассмотреть, очень близка к архитектуре даркнета. Но в процессе вы увидите, что архитектура даркнета не во всем YOLO v3 end-to-end, а только в той ее части, которую они предварительно обучили в ImageNet для классификации. Это почти самая обычная простая архитектура, которую вы только можете придумать, так что это действительно отличная отправная точка для экспериментов. Мы будем называть это даркнет, но это не совсем так, и вы можете повозиться с ним, чтобы создавать вещи, которые определенно не относятся к даркнету. На самом деле это просто основа практически любой современной архитектуры на основе ResNet.

CIFAR10 - это довольно небольшой набор данных [8:06]. Размер изображений всего 32 на 32, и это отличный набор данных для работы, потому что:

  • Вы можете обучить его относительно быстро, в отличие от ImageNet.
  • Относительно небольшой объем данных
  • На самом деле довольно сложно распознать изображения, потому что 32 на 32 слишком мало, чтобы легко понять, что происходит.

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

Идите вперед и импортируйте наши обычные вещи, и мы собираемся попробовать построить сеть с нуля, чтобы обучить это с помощью [8:58].

%matplotlib inline
%reload_ext autoreload
%autoreload 2
from fastai.conv_learner import *
PATH = Path("data/cifar10/")
os.makedirs(PATH,exist_ok=True)

Действительно хорошее упражнение для тех, кто не на 100% уверен в своих вещах и базовых навыках PyTorch, - это выяснить, как Джереми пришел к этим stats числам. Эти числа представляют собой средние значения и стандартные отклонения для каждого канала в CIFAR10. Попробуйте и убедитесь, что вы можете воссоздать эти числа, и посмотрите, сможете ли вы сделать это, используя не более пары строк кода (без циклов!).

Поскольку они довольно малы, мы можем использовать больший размер пакета, чем обычно, и размер этих изображений составляет 32 [9:46].

classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 
           'horse', 'ship', 'truck')
stats = (np.array([ 0.4914 ,  0.48216,  0.44653]), 
         np.array([ 0.24703,  0.24349,  0.26159]))

num_workers = num_cpus()//2
bs=256
sz=32

Transformations [9:57], обычно у нас есть стандартный набор преобразований side_on, который мы используем для фотографий обычных объектов. Мы не собираемся использовать это здесь, потому что эти изображения настолько малы, что попытка немного повернуть изображение 32 на 32 приведет к появлению множества блочных искажений. Итак, стандартные преобразования, которые люди обычно используют, - это случайный горизонтальный переворот, а затем мы добавляем 4 пикселя (размер, деленный на 8) с каждой стороны. Одна вещь, которая работает действительно хорошо, - это то, что по умолчанию fastai не добавляет черный отступ, как это делают многие другие библиотеки. Fastai берет последние 4 пикселя существующей фотографии, переворачивает их и отражает, и мы обнаруживаем, что мы получаем гораздо лучшие результаты, используя заполнение отражения по умолчанию. Теперь, когда у нас есть изображение 40 на 40, этот набор преобразований в обучении будет случайным образом выбирать кадры размером 32 на 32, поэтому мы получаем небольшие вариации, но не кучи. Мы можем использовать обычный from_paths, чтобы получить наши данные.

tfms = tfms_from_stats(stats, sz, aug_tfms=[RandomFlip()], 
                       pad=sz//8)
data = ImageClassifierData.from_paths(PATH, val_name='test', 
                                      tfms=tfms, bs=bs)

Теперь нам нужна архитектура, и мы собираемся создать такую, которая умещалась бы на одном экране [11:07]. Это с нуля. Мы используем предопределенные модули Conv2d, BatchNorm2d, LeakyReLU, но не используем никаких блоков или чего-либо еще. Все это находится на одном экране, поэтому, если вам когда-нибудь интересно, могу ли я понять современную архитектуру хорошего качества, абсолютно! Давайте изучим это.

def conv_layer(ni, nf, ks=3, stride=1):
    return nn.Sequential(
        nn.Conv2d(ni, nf, kernel_size=ks, bias=False, stride=stride,
                  padding=ks//2),
        nn.BatchNorm2d(nf, momentum=0.01),
        nn.LeakyReLU(negative_slope=0.1, inplace=True))
class ResLayer(nn.Module):
    def __init__(self, ni):
        super().__init__()
        self.conv1=conv_layer(ni, ni//2, ks=1)
        self.conv2=conv_layer(ni//2, ni, ks=3)
        
    def forward(self, x): return x.add_(self.conv2(self.conv1(x)))
class Darknet(nn.Module):
    def make_group_layer(self, ch_in, num_blocks, stride=1):
        return [conv_layer(ch_in, ch_in*2,stride=stride)
               ] + [(ResLayer(ch_in*2)) for i in range(num_blocks)]

    def __init__(self, num_blocks, num_classes, nf=32):
        super().__init__()
        layers = [conv_layer(3, nf, ks=3, stride=1)]
        for i,nb in enumerate(num_blocks):
            layers += self.make_group_layer(nf, nb, stride=2-(i==1))
            nf *= 2
        layers += [nn.AdaptiveAvgPool2d(1), Flatten(), 
                   nn.Linear(nf, num_classes)]
        self.layers = nn.Sequential(*layers)
    
    def forward(self, x): return self.layers(x)

Основная отправная точка в архитектуре - сказать, что это набор слоев, и, вообще говоря, будет некая иерархия слоев [11:51]. На самом нижнем уровне есть такие вещи, как сверточный слой и слой пакетных норм, но каждый раз, когда у вас есть свертка, у вас, вероятно, будет некоторая стандартная последовательность. Обычно это будет:

  1. Конв
  2. норма партии
  3. нелинейная активация (например, ReLU)

Мы начнем с определения того, какой будет наша основная единица, и определим ее в функции (conv_layer), чтобы нам не нужно было беспокоиться о попытках сохранить все согласованно, и это все упростит.

Дырявый Релу [12:43]:

Градиент Leaky ReLU (где x ‹0) варьируется, но обычно около 0,1 или 0,01. Идея заключается в том, что когда вы находитесь в отрицательной зоне, вы не получаете нулевой градиент, из-за чего его очень сложно обновить. На практике люди считают Leaky ReLU более полезным для небольших наборов данных и менее полезным для больших наборов данных. Но интересно, что для статьи YOLO v3 они использовали Leaky ReLU и получили от этого отличную производительность. Это редко ухудшает ситуацию и часто делает ее лучше. Так что, вероятно, неплохо, если вам нужно создать свою собственную архитектуру, чтобы по умолчанию использовать Leaky ReLU.

Вы заметите, что мы не определяем модуль PyTorch в conv_layer, мы просто определяем nn.Sequential [14:07]. Это то, что если вы читаете чужой код PyTorch, он действительно недостаточно используется. Люди склонны писать все как модуль PyTorch с __init__ и forward, но если вам нужна просто последовательность вещей, одна за другой, гораздо более кратко и легко понять, что это Sequential.

Остаточный блок [14:40]: Как упоминалось ранее, в большинстве современных сетей обычно существует несколько иерархий единиц, и теперь мы знаем, что следующим уровнем в этой иерархии единиц для ResNet является ResBlock или остаточный блок (см. ResLayer). Когда мы в последний раз делали CIFAR10, мы слишком упростили это (немного схитрили). У нас было x входило, и мы пропустили это через conv, затем мы добавили его обратно в x, чтобы выйти. В реальном ResBlock их два. Когда мы говорим conv, мы используем его как ярлык для нашего conv_layer (conv, пакетная норма, ReLU).

Интересным моментом здесь является количество каналов в этих свертках [16:47]. У нас есть ni (некоторое количество входных каналов / фильтров). Люди в даркнете все настраивают так, что они заставляют каждый из этих слоев Res выдавать одинаковое количество каналов, и Джереми это понравилось, и поэтому он использовал его в ResLayer, потому что это упрощает жизнь. Первый конв. Уменьшает вдвое количество каналов, а второй - снова удваивает его. Таким образом, у вас есть эффект воронки, когда 64 канала входят, сжимаются при первом переходе до 32 каналов, а затем снова возвращаются к выходящим 64 каналам.

Вопрос: Почему inplace=True находится в _20 _ [17:54]? Спасибо за вопрос! Многие люди забывают об этом или не знают об этом, но это действительно важный метод запоминания. Если подумать, этот conv_layer, это вещь самого низкого уровня, так что почти все в нашем ResNet, когда все это собрано, будет много conv_layer. Если у вас нет inplace=True, он создаст целый отдельный фрагмент памяти для вывода ReLU, поэтому он собирается выделить целую кучу памяти, которая совершенно не нужна. Другой пример: исходный forward в ResLayer выглядел так:

def forward(self, x): return x + self.conv2(self.conv1(x))

Надеюсь, некоторые из вас могут помнить, что в PyTorch практически каждая функция имеет версию с суффиксом подчеркивания, которая говорит ей, что она должна делать это на месте. + эквивалентен add, а версия add на месте - add_, поэтому это уменьшит использование памяти:

def forward(self, x): return x.add_(self.conv2(self.conv1(x)))

Это действительно полезные маленькие уловки. Джереми сначала забыл о inplace=True, но ему пришлось уменьшить размер партии до гораздо меньшего количества, и это сводило его с ума - затем он понял, что этого не хватает. Вы также можете сделать это с отсевом, если у вас есть отсев. Вот на что следует обратить внимание:

  • Выбывать
  • Все функции активации
  • Любая арифметическая операция

Вопрос: почему в ResNet для смещения обычно установлено значение False в conv_layer [19:53]? Сразу после Conv идет BatchNorm. Помните, что BatchNorm имеет 2 обучаемых параметра для каждой активации - то, на что вы умножаете, и то, что добавляете. Если бы у нас было смещение в Conv, а затем добавили бы еще что-то в BatchNorm, мы бы добавили две вещи, что совершенно бессмысленно - это два веса, на которые можно было бы подействовать. Итак, если у вас есть BatchNorm после Conv, вы можете либо указать BatchNorm не включать бит добавления, либо проще сказать Conv, чтобы он не включал смещение. Особого вреда нет, но, опять же, потребуется больше памяти, потому что нужно отслеживать больше градиентов, поэтому лучше избегать.

Еще одна маленькая хитрость в том, что у большинства пользователей conv_layer в качестве параметра используется отступ [21:11]. Но, вообще говоря, у вас должно быть достаточно легко вычислить отступы. Если у вас размер ядра 3, то очевидно, что он будет перекрываться на одну единицу с каждой стороны, поэтому нам нужно заполнение 1. Или, если это размер ядра 1, то нам не нужно никаких отступов. В общем, заполнение размера ядра целым числом, деленным на 2 - это то, что вам нужно. Иногда есть некоторые настройки, но в данном случае это работает отлично. Опять же, пытаюсь упростить мой код, заставляя компьютер вычислять вещи за меня, а не делать это самому.

Еще одна вещь с двумя conv_layer [22:14]: у нас была идея узкого места (уменьшение каналов и их повторное увеличение), также есть размер ядра, который следует использовать. Первый имеет 1 на 1 Conv. Что на самом деле происходит при конверсии 1 на 1? Если у нас есть сетка 4 на 4 с 32 фильтрами / каналами, и мы будем делать конверсию 1 на 1, ядро ​​для этой конверсии будет выглядеть как то, что находится посередине. Когда мы говорим о размере ядра, мы никогда не упоминаем последний фрагмент, но предположим, что он 1 на 1 на 32, потому что это часть входящего и выходящего фильтров. Ядро помещается в первую ячейку желтого цвета, и мы получаем скалярное произведение на эти 32 глубоких бита, что дает нам наш первый результат. Затем мы перемещаем его во вторую ячейку и получаем второй результат. Таким образом, для каждой точки сетки будет множество скалярных произведений. Это позволяет нам изменять размерность в канальном измерении любым способом. Мы создаем ni//2 фильтры, и у нас будет ni//2 точечных произведения, которые в основном представляют собой различные средневзвешенные значения входных каналов. С очень небольшим количеством вычислений он позволяет нам добавить этот дополнительный шаг вычислений и нелинейностей. Это крутой трюк - воспользоваться преимуществами этих конверсий 1 на 1, создать это узкое место, а затем снова вытащить его с помощью конверсий 3 на 3, что позволит правильно использовать преимущества двумерной природы ввода. Или же при конверсии 1 к 1 это вообще не используется.

Эти две строчки кода не так много, но это действительно отличная проверка вашего понимания и интуиции того, что происходит [25:17] - почему это работает? почему выстраиваются тензорные ранги? почему все размеры хорошо совпадают? почему это хорошая идея? что он на самом деле делает? Это действительно хорошая вещь, с которой можно повозиться. Возможно, создайте несколько маленьких в Jupyter Notebook, запустите их сами, посмотрите, какие входы и выходы входят и выходят. Действительно почувствуйте это. Как только вы это сделаете, вы можете поиграть с разными вещами.

Одна из действительно недооцененных работ - это [26:09] - Wide Residual Networks. Это действительно довольно простой документ, но они возятся с этими двумя строками кода:

  • Что мы сделали ni*2 вместо ni//2?
  • Что, если бы мы добавили conv3?

Они придумали такую ​​простую нотацию для определения того, как могут выглядеть две строки кода, и показывают множество экспериментов. Что они показывают, так это то, что этот подход, заключающийся в ограничении количества каналов, который является почти универсальным в ResNet, вероятно, не является хорошей идеей. На самом деле из экспериментов однозначно не лучшая идея. Потому что это позволяет создавать действительно глубокие сети. Ребята, создавшие ResNet, особенно прославились созданием 1001-уровневой сети. Но проблема 1001 слоя в том, что вы не можете рассчитать слой 2, пока не закончите слой 1. Вы не можете рассчитать слой 3, пока не закончите вычислять слой 2. Так что это последовательный. Графические процессоры не любят последовательную передачу. Итак, они показали, что если у вас меньше слоев, но с большим количеством вычислений на каждый слой, то простой способ сделать это - удалить //2, никаких других изменений:

Попробуйте это дома. Попробуйте запустить CIFAR и посмотрите, что произойдет. Даже умножьте на 2 или возитесь. Это позволяет вашему графическому процессору выполнять больше работы, и это очень интересно, потому что в подавляющем большинстве статей, в которых говорится о производительности различных архитектур, на самом деле никогда не указывается, сколько времени требуется для выполнения пакета. Они говорят, что «для этого требуется X операций с плавающей запятой на пакет», но на самом деле они никогда не утруждают себя запуском его, как настоящие экспериментаторы, и выясняют, быстрее он или медленнее. Многие архитектуры, которые сейчас действительно известны, оказываются медленными, как патока, занимают уйму памяти и просто совершенно бесполезны, потому что исследователи никогда не заботились о том, быстрые ли они, и действительно ли они помещаются в ОЗУ с нормальным размеры партий. Так что статья Wide ResNet необычна тем, что она фактически умножает время на это, как и статья YOLO v3, в которой было сделано такое же понимание. Они могли пропустить статью Wide ResNet, потому что статья YOLO v3 пришла ко многим тем же выводам, но Джереми не уверен, что они разместили статью Wide ResNet, поэтому они могут не знать, что вся эта работа была сделана. Приятно видеть, как люди на самом деле рассчитывают время и замечают, что на самом деле имеет смысл.

Вопрос. Что вы думаете о SELU (масштабированных экспоненциальных линейных единицах)? [29:44] SELU в основном предназначен для полносвязных слоев, что позволяет вам избавиться от пакетной нормы, и основная идея заключается в том, что если вы используете эту другую функцию активации, она автоматически нормализуется. Самонормализация означает, что оно всегда будет соответствовать единичному стандартному отклонению и нулевому среднему значению, и, следовательно, вам не нужна норма партии. На самом деле он никуда не делся, и причина в том, что он невероятно привередлив - вы должны использовать очень конкретную инициализацию, иначе она не будет начинаться с точно правильного стандартного отклонения и среднего. Очень сложно использовать его с такими вещами, как встраивание, если вы это сделаете, тогда вам придется использовать особый вид инициализации встраивания, который не имеет смысла для встраивания. И вы делаете всю эту работу, очень тяжело, чтобы сделать это правильно, и если вы, наконец, все делаете правильно, в чем смысл? Что ж, вам удалось избавиться от некоторых слоев пакетных норм, которые в любом случае вас не сильно повредили. Это интересно, потому что статья SELU - основная причина, по которой люди заметили ее, заключалась в том, что она была создана изобретателем LSTM, а также имела огромное математическое приложение. Поэтому люди подумали: Много математики от известного парня - это должно быть здорово! но на практике Джереми не видит, чтобы кто-нибудь использовал его для получения каких-либо современных результатов или победы в каких-либо соревнованиях.

Darknet.make_group_layer содержит кучу ResLayer [31:28]. group_layer будет иметь некоторое количество входящих каналов / фильтров. Мы удвоим количество входящих каналов, просто используя стандартный conv_layer. При желании мы уменьшим размер сетки вдвое, используя шаг 2. Затем мы собираемся сделать целую кучу ResLayers - мы можем выбрать, сколько (2, 3, 8 и т. Д.), Потому что помните, что ResLayers не изменяют размер сетки и они не меняют количество каналов, поэтому вы можете добавлять их сколько угодно, не вызывая никаких проблем. Это потребует больше вычислений и больше оперативной памяти, но нет другой причины, кроме того, что вы не можете добавить столько, сколько хотите. group_layer, следовательно, в конечном итоге приведет к удвоению количества каналов, потому что первоначальная свертка удваивает количество каналов и, в зависимости от того, что мы передаем в stride, она может также уменьшить вдвое размер сетки, если мы положим stride=2. А затем мы можем выполнять целую кучу вычислений блока Res, сколько захотим.

Чтобы определить наш Darknet, мы собираемся передать что-то вроде этого [33:13]:

m = Darknet([1, 2, 4, 6, 3], num_classes=10, nf=32)
m = nn.DataParallel(m, [1,2,3])

Это означает создание пяти групповых слоев: первый будет содержать 1 дополнительный ResLayer, второй будет содержать 2, затем 4, 6, 3, и мы хотим начать с 32 фильтров. Первый из ResLayer будет содержать 32 фильтра, и будет только один дополнительный ResLayer. Во втором случае количество фильтров удвоится, потому что это то, что мы делаем каждый раз, когда у нас появляется новый групповой слой. Итак, у второго будет 64, затем 128, 256, 512 и все. Почти вся сеть будет состоять из этих групп слоев, и помните, каждый из этих групповых слоев также имеет одну свертку в начале. Итак, все, что у нас есть, - это до того, как все это произойдет, у нас будет один сверточный слой в самом начале, и в самом конце мы собираемся сделать наш стандартный адаптивный средний пул, сглаживание и линейный слой для создания числа. уроков в конце. Подводя итог [34:44], одна свертка на одном конце, адаптивное объединение и один линейный слой на другом конце, а в середине эти групповые слои, каждый из которых состоит из сверточного слоя, за которым следует n количество слоев ResLayers.

Адаптивное объединение средних значений [35:02]: Джереми упоминал об этом несколько раз, но он еще не видел никакого кода, ни одного примера, чего-либо еще, в котором использовалось бы адаптивное среднее объединение. Все, кого он видел, записывают это как nn.AvgPool2d(n), где n - это конкретное число - это означает, что теперь он привязан к определенному размеру изображения, что определенно не то, что вам нужно. Таким образом, большинство людей все еще считает, что конкретная архитектура привязана к определенному размеру. Это огромная проблема, когда люди думают об этом, потому что это действительно ограничивает их возможности использовать меньшие размеры для ускорения моделирования или использовать меньшие размеры для проведения экспериментов.

Последовательный [35:53]: хороший способ создания архитектуры - начать с создания списка, в данном случае это список с одним conv_layer входом, а make_group_layer возвращает другой. список. Затем мы можем добавить этот список к предыдущему списку с помощью += и сделать то же самое для другого списка, содержащего AdaptiveAvnPool2d. Наконец, мы вызовем nn.Sequential всех этих слоев. Теперь forward просто self.layers(x).

Это хорошая картина того, как сделать вашу архитектуру максимально простой. Есть много чего, с чем можно повозиться. Вы можете параметризовать разделитель ni, чтобы сделать его числом, которое вы передаете для передачи других чисел - возможно, вместо этого сделайте умножение на 2. Вы также можете передать вещи, которые изменяют размер ядра или количество сверточных слоев. У Джереми есть версия этого, которую он собирается запустить для вас, которая реализует все различные параметры, которые были в документе Wide ResNet, поэтому он может повозиться, чтобы увидеть, что работает хорошо.

lr = 1.3
learn = ConvLearner.from_model_data(m, data)
learn.crit = nn.CrossEntropyLoss()
learn.metrics = [accuracy]
wd=1e-4
%time learn.fit(lr, 1, wds=wd, cycle_len=30, use_clr_beta=(20, 20, 
                0.95, 0.85))

Как только мы это получим, мы можем использовать ConvLearner.from_model_data, чтобы взять наш модуль PyTorch и объект данных модели и превратить их в обучаемого [37:08]. Дайте ему критерий, добавьте метрики, если хотите, и тогда мы сможем вписаться и начнем.

Вопрос. Не могли бы вы объяснить объединение адаптивного среднего? Как работает установка на 1 [37:25]? Конечно. Обычно, когда мы делаем средний пул, допустим, у нас есть 4x4, и мы сделали avgpool((2, 2)) [40:35]. Это создает область 2x2 (синий на рисунке ниже) и берет среднее из этих четырех. Если мы передадим stride=1, следующий будет 2x2 показан зеленым цветом и возьмем среднее значение. Вот каким будет обычный средний пул 2x2. Если бы у нас не было отступов, то получилось бы 3x3. Если мы хотим 4х4, мы можем добавить отступы.

Что, если бы мы хотели 1x1? Затем мы могли бы сказать avgpool((4,4), stride=1), который выделит 4x4 желтым цветом и усреднит всю партию, что даст 1 x 1. Но это всего лишь один из способов сделать это. Вместо того чтобы указывать размер фильтра объединения, почему бы нам не сказать: «Меня не волнует размер входной сетки. Я всегда хочу одного за другим ». Вот где вы говорите adap_avgpool(1). В этом случае вы не говорите, какой размер фильтра пула, вы вместо этого говорите, какой размер вывода мы хотим. Мы хотим что-то одно за другим. Если вы поместите одно целое число n, предполагается, что вы имеете в виду n на n. В этом случае адаптивный средний пул 1 с входящей сеткой 4x4 совпадает со средним пулом (4, 4). Если бы это была сетка 7x7, это было бы то же самое, что и средний пул (7, 7). Это одна и та же операция, просто она выражается таким образом, что независимо от ввода мы хотим получить что-то такого же размера на выходе.

DAWNBench [37:43]: Давайте посмотрим, как мы справимся с нашей простой сетью в сравнении с этими современными результатами. Джереми подготовил команду к работе. Мы взяли все это и поместили в простой скрипт Python, и он изменил некоторые из упомянутых им параметров, чтобы создать нечто, что он назвал wrn_22 сетью, которая официально не существует, но имеет множество изменений в параметрах, о которых мы говорили. примерно на основе экспериментов Джереми. В нем есть много интересных вещей, таких как:

  • Один цикл Лесли Смита
  • Реализация с плавающей запятой половинной точности

Это будет работать на AWS p3, который имеет 8 графических процессоров и графические процессоры с архитектурой Volta, которые имеют специальную поддержку для операций с плавающей запятой половинной точности. Fastai - первая библиотека, которая фактически интегрировала оптимизированную Volta с плавающей запятой половинной точности в библиотеку, так что вы можете просто сделать learn.half() и получить эту поддержку автоматически. И это также первое, что интегрировало один цикл.

На самом деле он использует поддержку нескольких графических процессоров PyTorch [39:35]. Поскольку имеется восемь графических процессоров, он фактически будет запускать восемь отдельных процессоров Python, и каждый из них будет немного тренироваться, а затем, в конце, он будет передавать обновления градиента обратно в главный процесс, который будет интегрировать их все вместе. Таким образом, вы увидите, что множество индикаторов выполнения будут появляться вместе.

Если вы сделаете это таким образом, вы увидите, что это тренируется три или четыре секунды. Где еще, когда Джереми тренировался раньше, он получал 30 секунд за эпоху. Таким образом, мы можем тренировать вещи примерно в 10 раз быстрее, что довольно круто.

Проверка статуса [43:19]:

Это сделано! Мы достигли 94%, и это заняло 3 минуты 11 секунд. Предыдущее состояние дел составляло 1 час 7 минут. Стоило ли возиться с этими параметрами и немного узнать о том, как эти архитектуры на самом деле работают, а не просто использовать то, что было сделано из коробки? Ну, чёрт возьми. Мы просто использовали общедоступный экземпляр (мы использовали спотовый экземпляр, поэтому он стоит нам 8 долларов в час - за 3 минуты 40 центов), чтобы обучить его с нуля в 20 раз быстрее, чем кто-либо когда-либо делал это раньше. Так что это один из самых безумных результатов. Мы много видели, но этот просто выбросил из воды. Отчасти это связано с изменением параметров архитектуры, в основном, откровенно с использованием одного цикла Лесли Смита. Напоминание о том, что он делает [44:35], для скорости обучения он создает восходящий путь, который имеет такую ​​же длину, что и нисходящий путь, так что это истинная треугольная циклическая скорость обучения (CLR). Как обычно, вы можете выбрать соотношение x и y (то есть начальный LR / пик LR). В

В данном случае мы выбрали соотношение 50. Итак, мы начали с гораздо меньшей скорости обучения. Затем у него есть классная идея, где вы можете сказать, какой процент ваших эпох потрачен на переход от нижней части треугольника почти до нуля - это второе число. Итак, 15% партий расходуются еще дальше от нижней части нашего треугольника.

Это не единственное, что делает один цикл, у нас также есть импульс. Импульс изменяется от 0,95 до 0,85. Другими словами, когда скорость обучения действительно низкая, мы используем большой импульс, а когда скорость обучения действительно высока, мы используем очень небольшой импульс, что имеет большой смысл, но до тех пор, пока Лесли Смит не показал это в статье, Джереми никогда не видел, чтобы кто-нибудь делал это раньше. Это действительно крутой трюк. Теперь вы можете использовать это, используя параметр use-clr-beta в fastai (сообщение на форуме Sylvain), и вы сможете воспроизвести современный результат. Вы можете использовать его на своем компьютере или в своем бумажном пространстве, единственное, чего вы не получите, - это модуль с несколькими графическими процессорами, но это все равно немного упрощает обучение.

Вопрос: make_group_layer содержит шаг, равный 2, это означает, что шаг один для первого слоя и два для всего остального. Какая логика за этим? Обычно шаги, которые я видел, нечетные [46:52]. Шагов бывает один или два. Думаю, вы думаете о размерах ядра. Итак, stride = 2 означает, что я прыгаю на два, а это значит, что вы уменьшаете размер сетки вдвое. Думаю, вы могли запутаться между шагом и размером ядра. Если ваш шаг равен единице, размер сетки не изменится. Если у вас два шага, то это так. В данном случае, поскольку это CIFAR10, 32 на 32 - это мало, и мы не можем часто уменьшать размер сетки вдвое, потому что довольно быстро у нас закончатся ячейки. Вот почему шаг первого слоя равен единице, поэтому мы не уменьшаем размер сетки сразу. Это неплохой способ сделать это, потому что сначала у нас мало Darknet([1, 2, 4, 6, 3], …). Мы можем начать с небольшого количества вычислений на большой сетке, а затем мы можем постепенно выполнять все больше и больше вычислений по мере того, как сетки становятся все меньше и меньше, потому что на меньшей сетке вычисления будут занимать меньше времени.

Генеративные состязательные сети (GAN) [48:49]

Мы собираемся поговорить о генерирующих состязательных сетях, также известных как GAN, и, в частности, сосредоточимся на статье Вассерштейна о GAN, в которой упоминался Сумит Чинтала, создавший PyTorch. Вассерштейн GAN (WGAN) находился под сильным влиянием глубокой сверточной генеративной статьи о состязательной сети, в которой также принимал участие Сумит. Это действительно интересная статья для чтения. Многие из них выглядят так:

Хорошая новость в том, что вы можете пропустить эти биты, потому что есть еще бит, который выглядит так:

Во многих статьях есть теоретический раздел, который, кажется, полностью избавлен от потребности рецензента в теории. Это не так с бумагой WGAN. Теория на самом деле интересна - вам не нужно ее знать, чтобы использовать, но если вы хотите узнать о некоторых крутых идеях и понять, почему именно этот алгоритм основан на этом алгоритме, это просто увлекательно. До выхода этой статьи Джереми не знал никого, кто изучал математику, на которой она основана, поэтому все должны были выучить математику. Бумага довольно хорошо раскладывает все фрагменты (вам придется много читать самостоятельно). Так что, если вам интересно вникнуть в более глубокую математику, лежащую в основе какой-либо статьи, чтобы увидеть, каково ее изучать, я бы выбрал эту, потому что в конце этого теоретического раздела вы уйдете со словами: «Теперь я понимаю, почему они сделали этот алгоритм таким, какой он есть ».

Основная идея GAN - это генеративная модель [51:23]. Это то, что будет создавать предложения, создавать изображения или что-то генерировать. Он попытается создать вещь, в которой очень сложно отличить сгенерированный материал от реального. Таким образом, генеративная модель может быть использована для смены лица видео - очень спорная вещь из-за глубоких фейков и фальшивой порнографии, происходящих в настоящий момент. Его можно было использовать для подделки чьего-то голоса. Его можно использовать для подделки ответа на медицинский вопрос - но в этом случае это не совсем подделка, это может быть генеративный ответ на медицинский вопрос, который на самом деле является хорошим ответом, поэтому вы генерируете язык. Например, вы можете создать подпись к изображению. Итак, у генеративных моделей есть много интересных приложений. Но, вообще говоря, они должны быть достаточно хорошими, чтобы, например, если вы используете его для автоматического создания новой сцены для Кэрри Фишер в следующем фильме Звездные войны, и ее больше нет рядом, чтобы играть эту роль, вы хотите попытаться создать ее образ, который выглядит так же, как и он, заставляет аудиторию по Звездным войнам думать: Ладно, это не похоже на какую-то странную Кэрри Фишер - это похоже на настоящую Кэрри Фишер. Или, если вы пытаетесь дать ответ на медицинский вопрос, вы хотите создать английский, который читается красиво и ясно, звучит авторитетно и многозначительно. Идея генеративной враждебной сети заключается в том, что мы собираемся создать не просто генеративную модель для создания сгенерированного изображения, но и вторую модель, которая попытается выбрать, какие из них настоящие, а какие генерируются (мы назовем их «фальшивыми. ). Итак, у нас есть генератор, который будет создавать наш фальшивый контент, и дискриминатор, который попытается научиться распознавать, какие из них настоящие, а какие - фальшивые. Таким образом, будет две модели, и они будут противоборствующими, что означает, что генератор будет пытаться продолжать совершенствоваться, обманывая дискриминатор, заставляя думать, что подделка реальна, а дискриминатор будет пытаться продолжать улучшаться в различении. между настоящим и фальшивым. Так что они будут идти лицом к лицу. По сути, это так просто, как только что описал Джереми [54:14]:

  • Мы собираемся построить две модели в PyTorch
  • Мы собираемся создать обучающий цикл, который, прежде всего, говорит, что функция потерь для дискриминатора: «Можете ли вы определить разницу между реальным и фальшивым, а затем обновить веса».
  • Мы собираемся создать функцию потерь для генератора: «Можете ли вы сгенерировать что-то, что обманывает дискриминатор, и обновлять веса на основе этих потерь.
  • И мы собираемся повторить это несколько раз и посмотреть, что произойдет.

Смотрим на код [54:52]

"Ноутбук"

С GANS можно делать много разных вещей. Мы собираемся сделать что-то довольно скучное, но легкое для понимания, и это круто, что это даже возможно, а именно: мы собираемся сгенерировать несколько изображений из ничего. Мы просто собираемся заставить его рисовать несколько картинок. В частности, мы собираемся заставить его рисовать спальни. Надеюсь, у вас будет возможность поиграть с этим в течение недели со своими собственными наборами данных. Если вы выберете набор данных, который очень разнообразен, например ImageNet, а затем попросите GAN попытаться создать изображения ImageNet, он, как правило, не будет работать так хорошо, потому что недостаточно ясно, что вы хотите получить изображение. Так что лучше дать это, например, есть набор данных под названием CelebA, который представляет собой изображения лиц знаменитостей, который отлично работает с GAN. Вы создаете действительно четкие лица знаменитостей, которых на самом деле не существует. Набор данных о спальне тоже хорош - изображения того же самого.

Существует так называемый набор данных классификации сцены LSUN [55:55].

from fastai.conv_learner import *
from fastai.dataset import *
import gzip

Загрузите категорию спальни набора данных классификации сцены LSUN, распакуйте ее и преобразуйте в файлы jpg (папка сценариев находится здесь, в папке dl2):

curl 'http://lsun.cs.princeton.edu/htbin/download.cgi?tag=latest&category=bedroom&set=train' -o bedroom.zip
unzip bedroom.zip
pip install lmdb
python lsun-data.py {PATH}/bedroom_train_lmdb --out_dir {PATH}/bedroom

Это не проверено в Windows - если это не сработает, вы можете использовать компьютер Linux для преобразования файлов, а затем скопировать их. Как вариант, вы можете скачать эту 20% выборку из наборов данных Kaggle.

PATH = Path('data/lsun/')
IMG_PATH = PATH/'bedroom'
CSV_PATH = PATH/'files.csv'
TMP_PATH = PATH/'tmp'
TMP_PATH.mkdir(exist_ok=True)

В этом случае гораздо проще пойти по маршруту CSV, когда дело доходит до обработки наших данных. Итак, мы создаем CSV со списком файлов, которые нам нужны, и фальшивой меткой «0», потому что на самом деле у нас вообще нет меток для них. Один файл CSV содержит все, что есть в наборе данных о спальне, а другой - случайные 10%. Это приятно, потому что тогда мы можем большую часть времени использовать образец, когда мы экспериментируем, потому что существует более миллиона файлов, даже простое чтение в списке занимает некоторое время.

files = PATH.glob('bedroom/**/*.jpg')

with CSV_PATH.open('w') as fo:
    for f in files: fo.write(f'{f.relative_to(IMG_PATH)},0\n')
# Optional - sampling a subset of files
CSV_PATH = PATH/'files_sample.csv'
files = PATH.glob('bedroom/**/*.jpg')

with CSV_PATH.open('w') as fo:
    for f in files:
        if random.random()<0.1: 
            fo.write(f'{f.relative_to(IMG_PATH)},0\n')

Это будет выглядеть довольно знакомо [57:10]. Это было до того, как Джереми понял, что последовательные модели намного лучше. Итак, если вы сравните это с предыдущим блоком conv с последовательной моделью, здесь будет намного больше строк кода, но он делает то же самое, что и conv, ReLU, пакетная норма.

class ConvBlock(nn.Module):
    def __init__(self, ni, no, ks, stride, bn=True, pad=None):
        super().__init__()
        if pad is None: pad = ks//2//stride
        self.conv = nn.Conv2d(ni, no, ks, stride, padding=pad, 
                              bias=False)
        self.bn = nn.BatchNorm2d(no) if bn else None
        self.relu = nn.LeakyReLU(0.2, inplace=True)
    
    def forward(self, x):
        x = self.relu(self.conv(x))
        return self.bn(x) if self.bn else x

Первым делом мы построим дискриминатор [57:47]. Дискриминатор получит на вход изображение и выдаст число. Число должно быть меньше, если он считает, что это изображение настоящее. Конечно, что он делает для меньшего числа не появляется в архитектуре, это будет в функции потерь. Итак, все, что нам нужно сделать, это создать что-то, что берет изображение и выводит число. Большая часть этого кода заимствована у первоначальных авторов этой статьи, поэтому некоторые схемы именования отличаются от того, к чему мы привыкли. Но похоже на то, что было у нас раньше. Мы начинаем со свертки (conv, ReLU, пакетная норма). Затем у нас есть несколько дополнительных сверточных слоев - здесь не будет использоваться остаток, поэтому он выглядит очень похоже на предыдущий набор дополнительных слоев, но это будут сверточные слои, а не слои res. В конце нам нужно добавить достаточное количество сверточных слоев stride 2, чтобы уменьшить размер сетки до не более 4x4. Таким образом, он будет продолжать использовать шаг 2, разделить размер на 2 и повторять, пока размер нашей сетки не станет больше 4. Это довольно хороший способ создать столько слоев, сколько вам нужно в сети для обработки изображений произвольного размера и превратите их в фиксированный известный размер сетки.

Вопрос: нужно ли GAN гораздо больше данных, чем, скажем, собаки против кошек или НЛП? Или это сопоставимо [59:48]? Честно говоря, мне как-то неловко сказать, что я не являюсь экспертом-практиком в GAN. То, что я преподаю в первой части, - это то, что я счастлив сказать, что знаю, как лучше всего это делать, и поэтому я могу показать вам самые современные результаты, как мы только что сделали с CIFAR10, с помощью некоторых из студенты. Я вообще не участвую в GAN, поэтому я не совсем уверен, сколько вам нужно. В общем, кажется, что этого нужно довольно много, но помните, что единственная причина, по которой нам не нужно было слишком много собак и кошек, заключается в том, что у нас была предварительно обученная модель и мы могли бы использовать предварительно обученные модели GAN и точно настроить их, возможно . Насколько я знаю, я не думаю, что кто-то это сделал. Людям может быть интересно подумать об этом и поэкспериментировать. Может быть, люди это сделали, и там есть литература, которую мы не встречали. Я в некоторой степени знаком с основными литературными материалами по GAN, но не знаю всего, так что, возможно, я что-то упустил о трансферном обучении в GAN. Но это поможет избавиться от лишних данных.

Вопрос: Значит, огромное ускорение за счет комбинации скорости обучения за один цикл и отжига импульса плюс параллельное обучение восьми графических процессоров с половинной точностью? Это возможно только для расчета половинной точности с помощью потребительского GPU? Другой вопрос, почему расчет от одинарной до половинной точности в 8 раз быстрее, а от двойной - только в 2 раза [1:01:09]? Итак, результат CIFAR10 не в 8 раз быстрее от одиночного к половинному. Это примерно в 2–3 раза быстрее от одинарного к половинному. NVIDIA заявляет о провальной производительности тензорных ядер, академически правильно, но на практике бессмысленно, потому что это действительно зависит от того, какие вызовы вам нужны для какой части - то есть примерно в 2 или 3 раза лучше за половину. Таким образом, половинная точность немного помогает, дополнительные графические процессоры немного помогают, один цикл помогает очень сильно, а другой ключевой частью является игра с параметрами, о которых я вам говорил. Итак, внимательно прочитайте обширную статью ResNet, определите, какие вещи они там нашли, а затем напишите версию архитектуры, которую вы только что видели, которая позволила нам возиться с параметрами, не ложась спать всю ночь, пробуя все возможные комбинации. разного размера ядра, количества ядер, количества групп слоев, размера групп слоев. И помните, мы сделали узкое место, но на самом деле мы, как правило, вместо этого фокусировались на расширении, поэтому мы увеличивали размер, а затем уменьшали его, потому что он лучше использует преимущества графического процессора. Итак, все эти вещи вместе взятые, я бы сказал, что один цикл был, пожалуй, самым критическим, но каждый из них привел к значительному ускорению. Вот почему мы смогли добиться 30-кратного улучшения по сравнению с современным CIFAR10. У нас есть некоторые идеи для других вещей - после того, как эта скамейка DAWN закончит, может быть, мы попробуем пойти еще дальше, чтобы посмотреть, сможем ли мы однажды обыграть одну минуту. Будет весело.

class DCGAN_D(nn.Module):
    def __init__(self, isize, nc, ndf, n_extra_layers=0):
        super().__init__()
        assert isize % 16 == 0, "isize has to be a multiple of 16"

        self.initial = ConvBlock(nc, ndf, 4, 2, bn=False)
        csize,cndf = isize/2,ndf
        self.extra = nn.Sequential(*[ConvBlock(cndf, cndf, 3, 1)
                                    for t in range(n_extra_layers)])

        pyr_layers = []
        while csize > 4:
            pyr_layers.append(ConvBlock(cndf, cndf*2, 4, 2))
            cndf *= 2; csize /= 2
        self.pyramid = nn.Sequential(*pyr_layers)
        
        self.final = nn.Conv2d(cndf, 1, 4, padding=0, bias=False)

    def forward(self, input):
        x = self.initial(input)
        x = self.extra(x)
        x = self.pyramid(x)
        return self.final(x).mean(0).view(1)

Итак, вот наш дискриминатор [1:03:37]. Важно помнить об архитектуре: она ничего не делает, а имеет некоторый размер и ранг входного тензора, а также размер и ранг выходного тензора. Как видите, у последней конверсии один канал. Это отличается от того, к чему мы привыкли, потому что обычно наша последняя вещь - линейный блок. Но наш последний слой - это блок conv. У него только один канал, но его размер сетки примерно 4x4 (не более 4x4). Итак, мы выплюнем (скажем, 4x4) тензор 4 на 4 на 1. Затем мы принимаем это среднее значение. Таким образом, он переходит от 4x4x1 к скаляру. Это что-то вроде окончательного адаптивного среднего пула, потому что у нас есть что-то только с одним каналом, и мы берем среднее значение. Так что это немного другое - обычно мы сначала делаем средний пул, а затем пропускаем его через полностью связанный уровень, чтобы получить нашу единую вещь. Но это вытаскивает один канал и затем усредняет его. Джереми подозревает, что это сработало бы лучше, если бы мы действовали как обычно, но он еще не пробовал это, и у него действительно недостаточно хорошей интуиции, чтобы узнать, что-то он упускает - но это будет интересный эксперимент, который стоит попробовать. если кто-то хочет прикрепить слой адаптивного среднего пула, а затем полностью подключенный уровень с одним выходом.

Итак, это дискриминатор. Предположим, у нас уже есть генератор - кто-то говорит: «Хорошо, вот генератор, который генерирует спальни. Я хочу, чтобы вы построили модель, которая сможет определить, какие из них настоящие, а какие нет ». Мы собираемся взять набор данных и пометить набор изображений, которые являются поддельными спальнями из генератора, и набор изображений реальных спален из набора данных LSUN, чтобы наклеить 1 или 0 на каждое из них. Затем мы попытаемся заставить дискриминатор определить разницу. Так что это будет достаточно просто. Но генератора нам не подарили. Нам нужно построить один. Мы еще не говорили о функции потерь - мы предполагаем, что существует некоторая функция потерь, которая делает это.

Генератор [1:06:15]

Генератор - это также архитектура, которая ничего не делает сама по себе, пока у нас не будет функции потерь и данных. Но каковы ранги и размеры тензоров? Входом в генератор будет вектор случайных чисел. В газете это называется «приор». Насколько велик? Мы не знаем. Идея состоит в том, что другой набор случайных чисел создаст другую спальню. Таким образом, наш генератор должен взять на вход вектор, пропустить его через последовательные модели и превратить его в тензор 4 ранга (ранг 3 без размерности пакета) - высота на ширину на 3. Итак, на последнем шаге nc (количество channel) будет иметь значение 3, потому что он будет создавать 3-канальное изображение некоторого размера.

class DeconvBlock(nn.Module):
    def __init__(self, ni, no, ks, stride, pad, bn=True):
        super().__init__()
        self.conv = nn.ConvTranspose2d(ni, no, ks, stride, 
                         padding=pad, bias=False)
        self.bn = nn.BatchNorm2d(no)
        self.relu = nn.ReLU(inplace=True)
        
    def forward(self, x):
        x = self.relu(self.conv(x))
        return self.bn(x) if self.bn else x
class DCGAN_G(nn.Module):
    def __init__(self, isize, nz, nc, ngf, n_extra_layers=0):
        super().__init__()
        assert isize % 16 == 0, "isize has to be a multiple of 16"

        cngf, tisize = ngf//2, 4
        while tisize!=isize: cngf*=2; tisize*=2
        layers = [DeconvBlock(nz, cngf, 4, 1, 0)]

        csize, cndf = 4, cngf
        while csize < isize//2:
            layers.append(DeconvBlock(cngf, cngf//2, 4, 2, 1))
            cngf //= 2; csize *= 2

        layers += [DeconvBlock(cngf, cngf, 3, 1, 1) 
                       for t in range(n_extra_layers)]
        layers.append(nn.ConvTranspose2d(cngf, nc, 4, 2, 1,
                                            bias=False))
        self.features = nn.Sequential(*layers)

    def forward(self, input): return F.tanh(self.features(input))

Вопрос: есть ли причина, по которой в ConvBlock после ReLU следует норма партии (т.е. self.bn(self.relu(…))) [1:07:50]? Обычно я ожидаю, что сначала перейду на ReLU, а затем на пакетную норму [1:08:23], что на самом деле это порядок, который имеет смысл для Джереми. Порядок, который у нас был в даркнете, был тем же порядком, который они использовали в бумаге для даркнета, поэтому у всех, похоже, есть свой порядок этих вещей. Фактически, у большинства людей для CIFAR10 снова используется другой порядок: пакетная норма → ReLU → conv, что является причудливым способом думать об этом, но оказывается, что часто для остаточных блоков это работает лучше. Это называется предварительная активация ResNet. В блогах есть несколько сообщений, в которых люди экспериментировали с разным порядком этих вещей, и, похоже, это во многом зависит от того, какой это конкретный набор данных и что вы делаете, хотя разница в производительности достаточно мала, чтобы вы выиграли '' Меня это не волнует, если только это не соревнование.

Деконволюция [1:09:36]

Таким образом, генератор должен начинаться с вектора и заканчиваться тензором ранга 3. Мы пока не знаем, как это сделать. Нам нужно использовать что-то, называемое «деконволюцией», и PyTorch называет это транспонированной сверткой - то же самое, но другое имя. Деконволюция - это то, что вместо уменьшения размера сетки она увеличивает размер сетки. Как и во всем, это легче всего увидеть в электронной таблице Excel.

Вот свертка. Начнем, скажем, с ячейки сетки 4 на 4 с одним каналом. Давайте пропустим его через ядро ​​3 на 3 с одним выходным фильтром. Итак, у нас есть один канал, одно ядро ​​фильтра, поэтому, если мы не добавим никаких отступов, мы получим 2 на 2. Помните, свертка - это просто сумма произведения ядра и соответствующая ячейка сетки [1:11:09]. Итак, есть наш стандартный фильтр 3 на 3 свертки один канал один.

Итак, идея сейчас в том, что мы хотим пойти в противоположном направлении [1:11:25]. Мы хотим начать с нашего 2 на 2, и мы хотим создать 4 на 4. В частности, мы хотим создать тот же самый 4 на 4, с которым мы начали. И мы хотим сделать это с помощью свертки. Как бы мы это сделали?

Если у нас есть свертка 3 на 3, то если мы хотим создать вывод 4 на 4, нам нужно будет создать такое количество отступов:

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

Затем мы можем получить сумму абсолютных значений (потери L1), суммируя абсолютные значения этих ошибок:

Итак, теперь мы можем использовать оптимизацию, в Excel это называется «решатель» для выполнения градиентного спуска. Поэтому мы установим итоговую ячейку равной минимуму и попытаемся уменьшить потери, изменив наш фильтр. Как видите, появился фильтр, в котором Result почти как Data. Это не идеально, и в целом вы не можете предположить, что деконволюция может создать то же самое, что вы хотите, потому что этого просто недостаточно. Потому что в фильтре 9 пунктов, а в результате - 16. Но попытка сделана неплохо. Вот как выглядит деконволюция - шаг 1, деконволюция 3x3 на входе ячейки сетки 2x2.

Вопрос: Насколько сложно создать дискриминатор, чтобы отличить фейковые новости от настоящих [1:13:43]? Ничего особенного не нужно - это просто классификатор. Таким образом, вы могли бы просто использовать классификатор НЛП из предыдущего урока и урока 4. В этом случае нет генеративной части, поэтому вам просто нужен набор данных, в котором говорится, что это то, что мы считаем фальшивыми новостями, и это то, что мы считаем чтобы быть настоящими новостями, и это действительно должно работать очень хорошо. Насколько нам известно, если вы попробуете его, вы получите такой же хороший результат, как и любой другой - достаточно ли он хорош, чтобы быть полезным на практике, Джереми не знает. Лучшее, что вы могли бы сделать на этом этапе, - это создать своего рода сортировку, в которой говорится, что эти вещи выглядят довольно схематично, исходя из того, как они написаны, и затем какой-нибудь человек мог бы войти и проверить их. Классификатор НЛП и RNN не могут проверить факты, но он может признать, что они написаны в таком популярном стиле, в котором часто пишутся фейковые новости, так что, возможно, на них стоит обратить внимание. Это, вероятно, было бы лучшим, на что вы могли бы надеяться, не прибегая к каким-либо внешним источникам данных. Но важно помнить, что дискриминатор - это, по сути, просто классификатор, и вам не нужны какие-либо специальные методы, помимо того, что мы уже научились проводить классификацию НЛП.

ConvTranspose2d [1:16:00]

Чтобы выполнить деконволюцию в PyTorch, просто скажите:

nn.ConvTranspose2d(ni, no, ks, stride, padding=pad, bias=False)

  • ni: количество входных каналов
  • no: количество наших входных каналов
  • ks: размер ядра

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

Визуализация [1:16:33]

Один слева - это то, что мы только что видели при выполнении деконволюции 2x2. Если есть шаг 2, значит, у вас есть не просто набивка снаружи, но вам действительно нужно поместить набивку посередине. На самом деле они не совсем реализованы таким образом, потому что это выполняется медленно. На практике вы будете реализовывать их по-другому, но все это происходит за кулисами, поэтому вам не нужно об этом беспокоиться. Мы уже говорили об этом руководстве по сверточной арифметике, и если вы все еще не знакомы со свертками и хотите освоиться с деконволюциями, это отличный сайт. Если вы хотите увидеть статью, это Руководство по сверточной арифметике для глубокого обучения.

DeconvBlock выглядит идентично ConvBlock, за исключением того, что в нем есть слово Transpose [1:17:49]. Мы просто идем conv → relu → batch norm, как и раньше, и у него есть входные и выходные фильтры. Единственная разница в том, что шаг 2 означает, что размер сетки увеличится вдвое, а не наполовину.

Вопрос: Кажется, что и nn.ConvTranspose2d, и nn.Upsample делают одно и то же, т.е. расширяют размер сетки (высоту и ширину) из предыдущего слоя. Можно ли сказать, что nn.ConvTranspose2d всегда лучше, чем nn.Upsample, поскольку nn.Upsample - это просто изменение размера и заполнение неизвестных нулями или интерполяцией [1:18:10]? Нет, нельзя. На distill.pub есть фантастическая интерактивная статья под названием Деконволюция и артефакты шахматной доски, в которой указывается, что то, что мы делаем сейчас, крайне неоптимально, но хорошая новость в том, что это делают все остальные.

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

Таким образом, даже если вы начнете со случайным весом, вы получите артефакты в виде шахматной доски. Чем глубже ты погружаешься, тем хуже становится. Их совет менее прямой, чем следовало бы, Джереми обнаружил, что для большинства генеративных моделей лучше использовать повышающую дискретизацию. Если вы nn.Upsample, он, по сути, делает противоположное объединению - он говорит, что давайте заменим эту одну ячейку сетки на четыре (2x2). Существует несколько способов повышения дискретизации: один - просто скопировать все это в эти четыре, а другой - использовать билинейную или бикубическую интерполяцию. Существуют различные методы, чтобы попытаться создать плавную версию с повышенной дискретизацией, и вы можете выбрать любой из них в PyTorch. Если вы выполняете повышающую дискретизацию 2 x 2, а затем регулярно делаете одну свертку 3 x 3, это еще один способ сделать то же самое, что и ConvTranspose - это удвоение размера сетки и выполнение над ней некоторых сверточных арифметических действий. Для генеративных моделей это почти всегда работает лучше. В этой публикации distil.pub они указывают, что, возможно, это хороший подход, но они не просто выходят и не говорят просто сделай это, тогда как Джереми просто сказал бы просто сделай это. Сказав, что для GANS он еще не добился такого большого успеха, и он думает, что, вероятно, потребуется некоторая настройка, чтобы заставить его работать. Проблема в том, что на ранних этапах он не создает достаточного шума. У него была версия, в которой он пытался сделать это с повышением частоты дискретизации, и вы могли видеть, что шум не выглядел очень шумным. На следующей неделе, когда мы рассмотрим передачу стилей и супер-разрешение, вы увидите, что nn.Upsample действительно вступает в свои права.

Генератор, теперь мы можем начать с вектора [1:22:04]. Мы можем решить и сказать: хорошо, давайте не будем думать об этом как о векторе, но на самом деле это ячейка сетки 1x1, а затем мы можем превратить ее в 4x4, затем 8x8 и так далее. Вот почему мы должны убедиться, что это подходящее кратное, чтобы мы могли создать что-то нужного размера. Как видите, он делает прямо противоположное, как и раньше. Он делает размер ячейки все больше и больше на 2 за раз, пока не достигнет половины желаемого размера, а затем, наконец, мы добавляем еще n в конце с шагом 1. Затем мы добавляем еще один ConvTranspose чтобы наконец получить желаемый размер, и все готово. Наконец, мы пропускаем это через tanh, и это заставит нас быть в диапазоне от нуля до единицы, потому что, конечно, мы не хотим выплевывать значения пикселей произвольного размера. Итак, у нас есть архитектура генератора, которая выводит изображение некоторого заданного размера с правильным количеством каналов со значениями от нуля до единицы.

На этом этапе мы можем создать объект данных нашей модели [1:23:38]. Для обучения этим вещам требуется время, поэтому мы сделали его 128 на 128 (просто удобный способ сделать это немного быстрее). Таким образом, это будет размер ввода, но затем мы собираемся использовать преобразование, чтобы превратить его в 64 на 64.

В последнее время были предприняты попытки действительно увеличить это разрешение до размеров высокого разрешения, но они по-прежнему требуют либо размер пакета в 1, либо много-много графических процессоров [1:24:05]. Поэтому мы пытаемся делать то, что можем делать с помощью одного потребительского графического процессора. Вот пример одной из спален размером 64 на 64.

bs,sz,nz = 64,64,100
tfms = tfms_from_stats(inception_stats, sz)
md = ImageClassifierData.from_csv(PATH, 'bedroom', CSV_PATH, 
         tfms=tfms, bs=128, skip_header=False, continuous=True)
md = md.resize(128)
x,_ = next(iter(md.val_dl))
plt.imshow(md.trn_ds.denorm(x)[0]);

Собираем их вместе [1:24:30]

Мы собираемся сделать почти все вручную, поэтому давайте продолжим и создадим наши две модели - наш генератор и дискриминатор, и, как вы можете видеть, они являются DCGAN, то есть, другими словами, это те же модули, что и в этой статье. Стоит вернуться назад и взглянуть на документ DCGAN, чтобы увидеть, что это за архитектуры, потому что предполагается, что когда вы читаете статью Wasserstein GAN, вы уже это знаете.

netG = DCGAN_G(sz, nz, 3, 64, 1).cuda()
netD = DCGAN_D(sz, 3, 64, 1).cuda()

Вопрос: не следует ли использовать сигмоид, если нам нужны значения от 0 до 1 [1:25:06]? Как обычно, наши изображения были нормализованы в диапазоне от -1 до 1, поэтому их значения в пикселях больше не находятся между 0 и 1. Вот почему мы хотим, чтобы значения менялись от -1 до 1, иначе мы не дадим правильный ввод для дискриминатора (через этот пост).

Итак, у нас есть генератор и дискриминатор, и нам нужна функция, которая возвращает априорный вектор (то есть сгусток шума) [1:25:49]. Мы делаем это, создавая кучу нулей. nz имеет размер z - очень часто в нашем коде, если вы видите загадочную букву, это потому, что это буква, которую они использовали в статье. Здесь z - размер нашего вектора шума. Затем мы используем нормальное распределение для генерации случайных чисел от 0 до 1. И это должна быть переменная, потому что она будет участвовать в обновлениях градиента.

def create_noise(b): 
   return V(torch.zeros(b, nz, 1, 1).normal_(0, 1))
preds = netG(create_noise(4))
pred_ims = md.trn_ds.denorm(preds)

fig, axes = plt.subplots(2, 2, figsize=(6, 6))
for i,ax in enumerate(axes.flat): ax.imshow(pred_ims[i])

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

def gallery(x, nc=3):
    n,h,w,c = x.shape
    nr = n//nc
    assert n == nr*nc
    return (x.reshape(nr, nc, h, w, c)
              .swapaxes(1,2)
              .reshape(h*nr, w*nc, c))

Нам нужен оптимизатор для обновления наших градиентов [1:26:41]. В статье Вассерштейна GAN нам сказали использовать RMSProp:

Мы легко можем сделать это в PyTorch:

optimizerD = optim.RMSprop(netD.parameters(), lr = 1e-4)
optimizerG = optim.RMSprop(netG.parameters(), lr = 1e-4)

В статье они предложили скорость обучения 0,00005 (5e-5), мы обнаружили, что 1e-4, похоже, работает, поэтому мы сделали ее немного больше.

Теперь нам понадобится обучающий цикл [1:27:14]:

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

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

Первым шагом алгоритма в статье является обновление дискриминатора (в статье дискриминатор называется «критик», а w - веса критика). Итак, первый шаг - немного обучить нашего критика, а затем мы собираемся немного обучить наш генератор и вернемся к началу цикла. Внутренний цикл for в документе соответствует второму циклу while в нашем коде.

Что мы собираемся сделать сейчас, так это у нас есть генератор, который в данный момент является случайным [1:29:06]. Итак, наш генератор будет генерировать что-то похожее на шум. Прежде всего, нам нужно научить наш дискриминатор различать шум и спальню - на это не должно быть слишком сложно, как вы надеетесь. Итак, мы просто делаем это обычным способом, но есть несколько небольших настроек:

  1. Мы собираемся получить небольшую партию реальных фотографий спальни, чтобы мы могли просто взять следующую партию из нашего итератора и превратить ее в переменную.
  2. Затем мы собираемся рассчитать потери для этого - так что это будет то, насколько дискриминатор считает, что это выглядит фальшивым («настоящий ли выглядит фальшивым?»).
  3. Затем мы собираемся создать несколько поддельных изображений, и для этого мы создадим некоторый случайный шум и пропустим его через наш генератор, который на данном этапе представляет собой просто набор случайных весов. Это создаст небольшую партию поддельных изображений.
  4. Затем мы пропустим это через тот же модуль дискриминатора, что и раньше, чтобы получить за это потерю («как фальшивка выглядит фальшивой?»). Помните, что когда вы делаете все вручную, вам нужно обнулить градиенты (netD.zero_grad()) в вашем цикле. Если вы забыли об этом, вернитесь к уроку, часть 1, где мы делаем все с нуля.
  5. Наконец, общие потери дискриминатора равны реальным потерям за вычетом ложных потерь.

Итак, вы можете увидеть это здесь [1:30:58]:

Они не говорят о потере, они просто говорят об одном из обновлений градиента.

В PyTorch нам не нужно беспокоиться о получении градиентов, мы можем просто указать потери и вызвать loss.backward(), а затем _114 _ [1:34:27] дискриминатора. Есть один ключевой шаг, который заключается в том, что мы должны держать все наши веса, которые являются параметрами в модуле PyTorch, в небольшом диапазоне от -0,01 до 0,01. Почему? Потому что математические предположения, которые заставляют этот алгоритм работать, применимы только к маленькому мячу. Интересно понять математику, объясняющую, почему это так, но она очень специфична для этой статьи, и ее понимание не поможет вам понять любую другую статью, поэтому изучайте ее только в том случае, если вам интересно. Это красиво объяснено, и Джереми считает, что это весело, но это не будет информация, которую вы будете повторно использовать где-то еще, если только вы не попадете в GAN. Он также упомянул, что после того, как вышел улучшенный Wasserstein GAN, он сказал, что есть более эффективные способы гарантировать, что ваше весовое пространство находится в этом плотном шаре, что должно было наказывать слишком высокие градиенты, поэтому в настоящее время есть несколько другие способы сделать это. Но эта строка кода является ключевым вкладом, и именно она делает его Вассерштейном GAN:

for p in netD.parameters(): p.data.clamp_(-0.01, 0.01)

В конце концов, у нас есть дискриминатор, который может распознавать настоящие спальни и наши совершенно случайные дерьмовые сгенерированные изображения [1:36:20]. А теперь давайте попробуем создать более качественные изображения. Итак, теперь установите обучаемый дискриминатор на false, установите обучаемый генератор на true, обнуляя градиенты генератора. Наши потери снова fw (дискриминатор) генератора, примененного к еще некоторому случайному шуму. Это точно так же, как и раньше, когда мы создавали генератор шума, а затем передавали его на дискриминатор, но на этот раз обучаемым является генератор, а не дискриминатор. Другими словами, в псевдокоде они обновляют Ɵ, что является параметрами генератора. Таким образом, он берет шум, генерирует некоторые изображения, пытается выяснить, являются ли они поддельными или настоящими, и использовать это для получения градиентов по отношению к генератору, в отличие от того, что мы получили ранее по отношению к дискриминатору, и использовать это для обновления наши веса с RMSProp с альфа-скоростью обучения [1:38:21].

def train(niter, first=True):
    gen_iterations = 0
    for epoch in trange(niter):
        netD.train(); netG.train()
        data_iter = iter(md.trn_dl)
        i,n = 0,len(md.trn_dl)
        with tqdm(total=n) as pbar:
            while i < n:
                set_trainable(netD, True)
                set_trainable(netG, False)
                d_iters = 100 if (first and (gen_iterations < 25) 
                              or (gen_iterations % 500 == 0)) else 5
                j = 0
                while (j < d_iters) and (i < n):
                    j += 1; i += 1
                    for p in netD.parameters(): 
                        p.data.clamp_(-0.01, 0.01)
                    real = V(next(data_iter)[0])
                    real_loss = netD(real)
                    fake = netG(create_noise(real.size(0)))
                    fake_loss = netD(V(fake.data))
                    netD.zero_grad()
                    lossD = real_loss-fake_loss
                    lossD.backward()
                    optimizerD.step()
                    pbar.update()

                set_trainable(netD, False)
                set_trainable(netG, True)
                netG.zero_grad()
                lossG = netD(netG(create_noise(bs))).mean(0).view(1)
                lossG.backward()
                optimizerG.step()
                gen_iterations += 1
            
        print(f'Loss_D {to_np(lossD)}; Loss_G {to_np(lossG)}; '
              f'D_real {to_np(real_loss)}; Loss_D_fake
              {to_np(fake_loss)}')

Вы увидите, что это несправедливо, что дискриминатор обучается ncritic раз (d_iters в приведенном выше коде), который они устанавливают на 5 каждый раз, когда мы обучаем генератор один раз. В статье об этом немного говорится, но основная идея заключается в том, что нет смысла улучшать генератор, если дискриминатор еще не умеет различать. Вот почему у нас есть второй цикл while. А вот это 5:

d_iters = 100 if (first and (gen_iterations < 25) 
                              or (gen_iterations % 500 == 0)) else 5

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

torch.backends.cudnn.benchmark=True

Давайте натренируем это на одну эпоху:

train(1, False)
0%|          | 0/1 [00:00<?, ?it/s]
100%|██████████| 18957/18957 [19:48<00:00, 10.74it/s]
Loss_D [-0.67574]; Loss_G [0.08612]; D_real [-0.1782]; Loss_D_fake [0.49754]
100%|██████████| 1/1 [19:49<00:00, 1189.02s/it]

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

fixed_noise = create_noise(bs)

Но перед этим уменьшите скорость обучения на 10 и сделайте еще один проход:

set_trainable(netD, True)
set_trainable(netG, True)
optimizerD = optim.RMSprop(netD.parameters(), lr = 1e-5)
optimizerG = optim.RMSprop(netG.parameters(), lr = 1e-5)
train(1, False)
0%|          | 0/1 [00:00<?, ?it/s]
100%|██████████| 18957/18957 [23:31<00:00, 13.43it/s]
Loss_D [-1.01657]; Loss_G [0.51333]; D_real [-0.50913]; Loss_D_fake [0.50744]
100%|██████████| 1/1 [23:31<00:00, 1411.84s/it]

Затем давайте используем шум, чтобы передать его нашему генератору, затем проведем через нашу денормализацию, чтобы снова превратить его во что-то, что мы можем видеть, а затем построим его:

netD.eval(); netG.eval();
fake = netG(fixed_noise).data.cpu()
faked = np.clip(md.trn_ds.denorm(fake),0,1)

plt.figure(figsize=(9,9))
plt.imshow(gallery(faked, 8));

И у нас есть несколько спален. Это не настоящие спальни, и некоторые из них не особенно похожи на спальни, но некоторые из них очень похожи на спальни, так что идея в этом. Это GAN. Лучше всего думать о GAN как о базовой технологии, которую вы, вероятно, никогда не будете использовать таким образом, но вы будете использовать множество интересных способов. Например, мы собираемся использовать его для создания цикла GAN.

Вопрос: есть ли причина для использования RMSProp именно в качестве оптимизатора, а не Adam и т. д. [1:41:38]? Я не помню, чтобы это подробно обсуждалось в статье. Не знаю, экспериментальная это причина или теоретическая. Загляните в газету и посмотрите, что там написано.

С форума

Экспериментируя, я понял, что Адам и WGAN не просто работают хуже - это приводит к полной невозможности обучения значимого генератора.

из статьи WGAN:

Наконец, в качестве отрицательного результата мы сообщаем, что обучение WGAN становится нестабильным, когда кто-то использует оптимизатор, основанный на импульсе, такой как Adam [8] (с β1 ›0) на критике, или когда используется высокая скорость обучения. Поскольку потери для критика нестационарны, методы, основанные на импульсе, оказались хуже. Мы определили импульс как потенциальную причину, потому что по мере того, как потери резко увеличивались и образцы становились хуже, косинус между шагом Адама и градиентом обычно становился отрицательным. Единственные места, где этот косинус был отрицательным, - это ситуации нестабильности. Поэтому мы перешли на RMSProp [21], который, как известно, хорошо работает даже с очень нестационарными задачами

Вопрос. Что может быть разумным способом обнаружения переобучения во время тренировки? Или оценить производительность одной из этих моделей GAN после того, как мы закончим обучение? Другими словами, как понятие наборов train / val / test переводится в GAN [1:41:57]? Это потрясающий вопрос, и многие люди шутят о том, что GAN - это единственная область, в которой вам не нужен набор тестов, и люди пользуются этим, придумывая вещи и говоря, что они отлично выглядят. Есть несколько известных проблем с GAN, одна из них называется Mode Collapse. Коллапс режима происходит, когда вы смотрите на свои спальни, и оказывается, что есть только три типа спален, которым сопоставляются все возможные векторы шума. Вы смотрите на свою галерею, и оказывается, что все это одно и то же или три разные вещи. Коллапс режима легко увидеть, если вы сворачиваете его до небольшого количества режимов, например, 3 или 4. Но что, если у вас есть сворачивание режима до 10 000 режимов? Итак, есть только 10 000 возможных спален, в которые рушатся все ваши векторы шума. Вы не сможете увидеть в только что просмотренной галерее, потому что маловероятно, что у вас будет две одинаковые спальни из 10 000. Или что, если каждая из этих спален в основном является прямой копией одного из входных данных - они в основном запоминают некоторые входные данные. Может ли такое случиться? И правда в том, что большинство документов плохо справляются с проверкой этих вещей. Таким образом, вопрос о том, как мы оцениваем GANS, и даже вопрос о том, может быть, нам действительно стоит правильно оценивать GAN, не получил достаточно широкого понимания даже сейчас. Некоторые люди действительно пытаются подтолкнуть. Ян Гудфеллоу был первым автором самой известной книги по глубокому обучению и изобретателем GAN, и он постоянно отправлял твиты, напоминающие людям о важности правильного тестирования GAN. Если вы видите документ, в котором утверждаются исключительные результаты GAN, то это определенно то, на что стоит взглянуть. Они говорили о коллапсе режима? Они говорили о запоминании? И так далее.

Вопрос: можно ли использовать GAN для увеличения объема данных [1:45:33]? Да, безусловно, вы можете использовать GAN для увеличения объема данных. Тебе следует? Я не знаю. Есть несколько работ, в которых пытаются провести полу-контролируемое обучение с помощью GAN. Я не нашел ничего особенно убедительного, демонстрирующего современные результаты на действительно интересных наборах данных, которые были широко изучены. Я немного скептически настроен, и причина, по которой я немного скептична, заключается в том, что, по моему опыту, если вы тренируете модель с синтетическими данными, нейронная сеть станет фантастически хорошей в распознавании конкретных проблем ваших синтетических данных, и это будет закончить то, чему он учится. Есть много других способов создания полууправляемых моделей, которые работают хорошо. Есть несколько мест, которые могут сработать. Например, вы, возможно, помните, что Отавио Гуд создал эту фантастическую визуализацию в первой части конвектора масштабирования, где она показывала букву, проходящую через MNIST, он, по крайней мере, в то время, был номером один в соревнованиях автономных автомобилей с дистанционным управлением, и он тренировался его модель, использующая синтетически расширенные данные, где он в основном снимал реальные видео автомобиля, проезжающего по трассе, и добавлял фальшивых людей и фальшивые другие автомобили. Я думаю, что это сработало хорошо, потому что А. он в некотором роде гений, а Б. потому, что я думаю, что у него было четко определенное небольшое подмножество, над которым он должен был работать. Но в целом действительно очень сложно использовать синтетические данные. Я пробовал использовать синтетические данные и модели уже несколько десятилетий (очевидно, не GAN, потому что они довольно новые), но в целом это очень сложно сделать. Очень интересный исследовательский вопрос.

Цикл GAN [1:41:08]

Бумага / Блокнот

Мы собираемся использовать цикл GAN, чтобы превратить лошадей в зебр. Вы также можете использовать его, чтобы превратить отпечатки Моне в фотографии или превратить фотографии Йосемити летом в зиму.

Это будет действительно просто, потому что это всего лишь нейронная сеть [1:44:46]. Все, что мы собираемся сделать, это создать вход, содержащий множество фотографий зебры, и с каждой из них мы соединим ее с эквивалентной фотографией лошади и просто обучим нейронную сеть, которая переходит от одной к другой. Или вы могли бы сделать то же самое для каждой картины Моне - создать набор данных, содержащий фотографию этого места ... о, подождите, это невозможно, потому что мест, нарисованных Моне, больше нет, и нет точных версий лошадей в форме зебры ... как, черт возьми, это будет работать? Похоже, это нарушает все, что мы знаем о том, что могут делать нейронные сети и как они это делают.

Итак, каким-то образом эти люди из Беркли создали модель, которая может превратить лошадь в зебру, несмотря на отсутствие фотографий. Если только они не пошли туда и не нарисовали лошадей и не сделали снимки до и после, но я думаю, что они этого не сделали [1:47:51]. Так как, черт возьми, они это сделали? Это своего рода гений.

Я знаю человека, который сейчас занимается самой интересной практикой цикла GAN, - это одна из наших учениц Хелена Сарин @ глаголиста. Она единственный известный мне художник из цикла GAN.

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

Вот основная уловка [1:50:11]. Это из цикла статей GAN. У нас будет два изображения (при условии, что мы делаем это с изображениями). Ключевым моментом является то, что это не парные изображения, поэтому у нас нет набора данных о лошадях и аналогичных зебрах. У нас есть стая лошадей и стая зебр. Возьмите одну лошадь X, возьмите одну зебру Y. Мы собираемся обучить генератор (который здесь называют функцией отображения), который превращает лошадь в зебру. Мы назовем эту функцию сопоставления G и создадим одну функцию сопоставления (она же генератор), которая превращает зебру в лошадь, и назовем ее F. Мы создадим дискриминатор, как и раньше, который будет как можно лучше распознавать настоящих лошадей от поддельных, так что это будет Dx. Другой дискриминатор, который будет как можно лучше отличать настоящую зебру от поддельной, мы назовем его Dy. Это наша отправная точка.

Ключевой момент для выполнения этой работы [1:51:27] - поэтому мы генерируем здесь функцию потерь (Dx и Dy). Мы собираемся создать нечто, называемое потерей согласованности цикла, которое говорит о том, что после того, как вы превратите свою лошадь в зебру с помощью своего генератора, и проверим, могу ли я распознать это на самом деле. Мы превращаем нашу лошадь в зебру, а затем собираемся попытаться превратить эту зебру обратно в ту же лошадь, с которой мы начали. Затем у нас будет другая функция, которая будет проверять, похожа ли эта лошадь, которая сгенерирована, ничего не зная о x, полностью сгенерированная из этой зебры Y, на оригинальную лошадь или не. Идея была бы в том, что если созданная вами зебра не будет похожа на вашу оригинальную лошадь, у вас нет шансов превратить ее обратно в оригинальную лошадь. Таким образом, проигрыш, сравнивающий x-hat с x, будет действительно плохим, если вы не сможете войти в Y и снова вернуться назад, и вы ' Вы, вероятно, сможете это сделать, если сможете создать зебру, которая будет выглядеть как оригинальная лошадь, чтобы вы знали, как выглядела оригинальная лошадь. И наоборот - возьмите свою зебру, превратите ее в фальшивую лошадь и проверьте, можете ли вы ее распознать, а затем попробуйте превратить ее обратно в исходную зебру и убедитесь, что она выглядит как оригинал.

Итак, обратите внимание: F (от зебры к лошади) и G (от лошади к зебре) делают две вещи [1:53:09]. Они оба превращают исходную лошадь в зебру, а затем снова превращают зебру в исходную лошадь. Итак, генераторов всего два. Для обратного отображения нет отдельного генератора. Вы должны использовать тот же генератор, который использовался для исходного сопоставления. Итак, это потеря последовательности цикла. Я считаю это гениальным. Идея о том, что это даже возможно. Честно говоря, когда это вышло, мне никогда не приходило в голову, что я мог бы даже попытаться решить. Это кажется настолько очевидным, что это невозможно, а затем идея, что вы можете решить эту проблему вот так - я просто думаю, что это чертовски умно.

В этой статье полезно взглянуть на уравнения, потому что они являются хорошими примерами - они написаны довольно просто, и это не похоже на некоторые статьи Вассерштейна GAN, в которых много теоретических доказательств и чего-то еще [1:54:05]. В данном случае это просто уравнения, описывающие происходящее. Вы действительно хотите добраться до точки, в которой вы сможете их прочитать и понять.

Итак, у нас есть лошадь X и зебра Y [1:54:34]. Для некоторой функции сопоставления G, которая является нашей функцией сопоставления лошади и зебры, возникает потеря GAN, с которой мы уже немного знакомы, это говорит о том, что у нас есть лошадь, зебра, поддельный распознаватель зебры, и генератор лошади-зебры. Потеря - это то, что мы видели раньше - это наша способность вытащить одну зебру из нашей зебры и распознать, настоящая она или фальшивая. Затем возьмите лошадь, превратите ее в зебру и узнайте, настоящая она или фальшивая. Затем вы делаете одно минус другое (в этом случае у них есть журнал, но журнал не так уж и важен). Это то, что мы только что видели. Вот почему мы сначала сделали Wasserstein GAN. Это просто стандартная потеря GAN в математической форме.

Вопрос: все это ужасно похоже на перевод с одного языка на другой, а затем обратно на оригинал. Испытывались ли GAN или какой-либо эквивалент в переводе [1:55:54]? Бумага с форума. Вернемся к тому, что я знаю - обычно при переводе требуется такой парный ввод (т. Е. Параллельный текст - это французский перевод этого английского предложения). Недавно вышла пара статей, демонстрирующих возможность создания качественных моделей перевода без парных данных. Я не реализовал их, и я ничего не понимаю, что не реализовал, но они вполне могут реализовать ту же основную идею. Мы рассмотрим это в течение недели и вернемся к вам.

Потеря согласованности цикла [1:57:14]: Итак, мы получили потерю GAN, и следующая часть - потеря согласованности цикла. Итак, основная идея здесь заключается в том, что мы начинаем с нашей лошади, используем наш генератор зебры для создания зебры, используем наш генератор лошади для создания лошади и сравниваем ее с исходной лошадью. Эта двойная линия с цифрой 1 - это потеря L1 - сумма абсолютных значений разностей [1:57:35]. Где еще, если бы это было 2, это были бы потери L2, поэтому 2-норма, которая была бы суммой квадратов разностей.

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

  1. Из наших фотографий лошадей возьмите лошадь
  2. Превратите его в зебру
  3. Преврати его обратно в лошадь
  4. Сравните его с оригиналом и суммой абсолютных значений
  5. Сделайте это и для зебры, и для лошади
  6. И сложите два вместе

Это наша потеря последовательности цикла.

Полная цель [1:58:54]

Теперь мы получили нашу функцию потерь, и вся функция потерь зависит от:

  • наш генератор лошадей
  • генератор зебры
  • наш распознаватель лошадей
  • наш распознаватель зебры (он же дискриминатор)

Мы собираемся сложить:

  • потеря GAN за признание лошадей
  • Убыток GAN за распознавание зебр
  • потеря стабильности цикла для наших двух генераторов

У нас есть лямбда, и, надеюсь, мы уже привыкли к этой идее, когда у вас есть два разных вида потерь, вы добавляете туда параметр, на который их можно умножить, чтобы они были примерно в одном масштабе [1:59 : 23 ]. Мы сделали то же самое с потерей ограничивающей рамки по сравнению с потерей классификатора при локализации.

Затем для этой функции потерь мы попытаемся максимизировать способность дискриминаторов различать, минимизируя при этом для генераторов. Итак, генераторы и дискриминаторы столкнутся друг с другом. Когда вы видите в статьях это min max, это в основном означает идею о том, что в вашем цикле обучения одно пытается сделать что-то лучше, другое - сделать что-то хуже, и есть много способов сделать это, но чаще всего вы будете чередовать два. Вы часто будете видеть, что это просто упоминается в математических статьях как min-max. Поэтому, когда вы видите min-max, вы должны сразу думать о состязательном обучении.

Реализация цикла GAN [2:00:41]

Посмотрим на код. Мы собираемся сделать что-то почти неслыханное, о чем я начал смотреть на чужой код, и мне не было так противно, что я выбросил все это и сделал это сам. На самом деле я сказал, что мне это очень нравится, мне это достаточно нравится, я собираюсь показать это своим ученикам. Это - источник кода, и это один из тех, кто создал исходный код для циклических GAN, и они создали версию PyTorch. Мне пришлось немного его почистить, но на самом деле он чертовски хорош. Самое замечательное в этом то, что теперь вы увидите почти все части fast.ai или все соответствующие части fast.ai, написанные кем-то другим способом. Итак, вы увидите, как они работают с наборами данных, загрузчиками данных, моделями, циклами обучения и т. Д.

Вы найдете директорию cgan [2:02:12], которая в основном почти что оригинал с некоторыми чистками, которые я надеюсь представить как PR когда-нибудь. Он был написан таким образом, что, к сожалению, он был немного связан с тем, как они использовали его в качестве сценария, поэтому я немного очистил его, чтобы использовать его как модуль. Но в остальном все очень похоже.

from fastai.conv_learner import *
from fastai.dataset import *
from cgan.options.train_options import *

Итак, cgan - их код скопирован из репозитория github с небольшими изменениями. Способ настройки cgan мини-библиотеки заключается в том, что предполагаемые параметры конфигурации передаются как сценарий. Итак, у них есть метод TrainOptions().parse, и я в основном передаю массив параметров скрипта (где мои данные, сколько потоков, я хочу исключить, сколько итераций, как я собираюсь называть эту модель, какой графический процессор мне нужен запустить его). Это дает нам opt объект, который вы можете видеть, что он содержит. Вы увидите, что он содержит некоторые вещи, которые мы не упомянули, потому что в нем есть значения по умолчанию для всего остального, о чем мы не упоминали.

opt = TrainOptions().parse(['--dataroot',    
   '/data0/datasets/cyclegan/horse2zebra', '--nThreads', '8', 
   '--no_dropout', '--niter', '100', '--niter_decay', '100', 
   '--name', 'nodrop', '--gpu_ids', '2'])

Так что вместо использования fast.ai мы будем в основном использовать cgan.

from cgan.data.data_loader import CreateDataLoader
from cgan.models.models import create_model

Первое, что нам понадобится, это загрузчик данных. Так что это также отличная возможность снова попрактиковаться в навигации по коду с помощью выбранного редактора или IDE. Мы собираемся начать с CreateDataLoader. Вы должны найти символ или тег vim, чтобы сразу перейти к CreateDataLoader, и мы увидим, что создается CustomDatasetDataLoader. Тогда мы видим, что CustomDatasetDataLoader - это BaseDataLoader. Мы видим, что он будет использовать стандартный PyTorch DataLoader, так что это хорошо. Мы знаем, что если вы собираетесь использовать стандартный PyTorch DataLoader, вы передали ему набор данных, и мы знаем, что набор данных - это что-то, что содержит длину и индексатор, поэтому, вероятно, когда мы посмотрим на CreateDataset, он это сделает.

Вот CreateDataset, и эта библиотека делает больше, чем просто цикл GAN - она ​​обрабатывает как выровненные, так и невыровненные пары изображений [2:04:46]. Мы знаем, что наши пары изображений не выровнены, поэтому собираемся UnalignedDataset.

Как и ожидалось, в нем есть __getitem__ и __len__. Что касается длины, A и B - наши лошади и зебры, у нас есть два набора, поэтому тот, который длиннее, является длиной DataLoader. __getitem__ собирается:

  • Случайным образом возьмите что-нибудь у каждой из наших лошадей и зебр.
  • Раскройте их подушкой (PIL)
  • Проведите их через некоторые преобразования
  • Тогда мы могли бы превратить лошадей в зебр или зебр в лошадей, так что есть какое-то направление
  • Верните нашу лошадь, зебру, путь к лошади и путь зебры

Надеюсь, вы видите, что это очень похоже на то, что делает fast.ai. Fast.ai, очевидно, делает гораздо больше, когда дело доходит до преобразований и производительности, но помните, что это исследовательский код для этой единственной вещи, и довольно круто, что они проделали всю эту работу.

data_loader = CreateDataLoader(opt)
dataset = data_loader.load_data()
dataset_size = len(data_loader)
dataset_size
1334

У нас есть загрузчик данных, поэтому мы можем загрузить в него наши данные [2:06:17]. Это покажет нам, сколько в нем мини-пакетов (это длина загрузчика данных в PyTorch).

Следующим шагом будет создание модели. Идея та же, у нас есть разные модели, и мы собираемся выполнить цикл GAN.

Вот наш CycleGANModel. В CycleGANModel довольно много всего, так что давайте пройдемся и выясним, что будет использоваться. На этом этапе мы только что вызвали инициализатор, поэтому, когда мы его инициализируем, он пройдет и определит два генератора, что неудивительно: генератор для наших лошадей и генератор для зебр. У него есть способ сгенерировать пул поддельных данных, а затем мы собираемся захватить наши потери GAN, и, как мы говорили о нашей потере согласованности цикла, это потеря L1. Они собираются использовать Адама, поэтому очевидно, что для цикла GANS они обнаружили, что Адам работает очень хорошо. Затем у нас будет оптимизатор для дискриминатора лошади, оптимизатор для дискриминатора «зебра» и оптимизатор для генератора. Оптимизатор для генератора будет содержать параметры как для генератора лошадей, так и для генератора зебры в одном месте.

Итак, инициализатор настроит все необходимые нам сети и функции потерь, и они будут храниться внутри этого model [2:08:14].

model = create_model(opt)

Затем он распечатывает и показывает нам именно ту модель PyTorch, которая у нас есть. Интересно видеть, что они используют ResNets, и поэтому вы можете видеть, что ResNets выглядят довольно знакомо, поэтому у нас есть conv, пакетная норма, Relu. InstanceNorm в основном совпадает с пакетной нормой, но применяется к одному изображению за раз, и разница не особенно важна. И вы можете видеть, что они делают отступы отражений, как и мы. Когда вы пытаетесь построить все с нуля вот так, вы можете увидеть, что это большая работа, и вы можете забыть о приятных мелочах, которые fast.ai делает за вас автоматически. Вы должны сделать все вручную, и только у вас будет их подмножество. Так что со временем, надеюсь, скоро, мы перенесем все эти вещи GAN в fast.ai, и это будет легко и просто.

Мы получили нашу модель и помним, что она содержит функции потерь, генераторы, дискриминаторы, все в одном удобном месте [2:09:32]. Я скопировал, вставил и немного переделал обучающий цикл из их кода, чтобы мы могли запустить его внутри записной книжки. Так что это должно показаться вам знакомым. Цикл для просмотра каждой эпохи и цикл для просмотра данных. Прежде чем мы это сделали, мы настроили dataset. На самом деле это не набор данных PyTorch, я думаю, это то, что они использовали немного сбивчиво, чтобы рассказать о своем объединенном том, что мы бы назвали объектом данных модели - всеми данными, которые им нужны. Прокрутите это с помощью tqdm, чтобы получить индикатор выполнения, и теперь мы можем пройти и посмотреть, что происходит в модели.

total_steps = 0

for epoch in range(opt.epoch_count, opt.niter + opt.niter_decay+1):
    epoch_start_time = time.time()
    iter_data_time = time.time()
    epoch_iter = 0

    for i, data in tqdm(enumerate(dataset)):
        iter_start_time = time.time()
        if total_steps % opt.print_freq == 0: 
             t_data = iter_start_time - iter_data_time
        total_steps += opt.batchSize
        epoch_iter += opt.batchSize
        model.set_input(data)
        model.optimize_parameters()

        if total_steps % opt.display_freq == 0:
            save_result = total_steps % opt.update_html_freq == 0

        if total_steps % opt.print_freq == 0:
            errors = model.get_current_errors()
            t = (time.time() - iter_start_time) / opt.batchSize

        if total_steps % opt.save_latest_freq == 0:
            print('saving the latest model(epoch %d,total_steps %d)'
                    % (epoch, total_steps))
            model.save('latest')

        iter_data_time = time.time()
    if epoch % opt.save_epoch_freq == 0:
        print('saving the model at the end of epoch %d, iters %d' 
               % (epoch, total_steps))
        model.save('latest')
        model.save(epoch)

    print('End of epoch %d / %d \t Time Taken: %d sec' %
          (epoch, opt.niter + opt.niter_decay, time.time() 
          - epoch_start_time))
    model.update_learning_rate()

set_input [2:10:32]: Это другой подход к тому, что мы делаем в fast.ai. Это отчасти изящно, это довольно специфично для цикла GAN, но в основном внутри этой модели есть идея, что мы собираемся войти в наши данные и взять соответствующие. Мы либо едем с лошади на зебру, либо с зебры на лошадь, в зависимости от того, каким путем мы идем, A либо лошадь, либо зебра, и наоборот. При необходимости поместите его на соответствующий графический процессор, затем захватите соответствующие пути. Итак, у модели теперь есть мини-партия лошадей и мини-партия зебр.

Теперь оптимизируем параметры [2:11:19]. Приятно видеть это таким. Вы можете увидеть каждый шаг. Прежде всего, попробуйте оптимизировать генераторы, затем попробуйте оптимизировать дискриминаторы лошади, а затем попробуйте оптимизировать дискриминатор зебры. zero_grad() является частью PyTorch, как и step(). Так что интересный момент - это то, что выполняет обратное распространение на генераторе.

Вот оно [2:12:04]. Перейдем к ключевым моментам. Вот вся формула, которую мы только что видели в газете. Возьмем лошадь и создадим зебру. Давайте теперь воспользуемся дискриминатором, чтобы определить, поддельный он или нет (pred_fake). Затем давайте добавим это в нашу функцию потерь, которую мы настроили ранее, чтобы получить потерю GAN на основе этого прогноза. Давайте сделаем то же самое в противоположном направлении, используя противоположный дискриминатор, а затем снова пропустим функцию потерь. Затем займемся потерей последовательности цикла. Опять же, мы берем созданную нами подделку и пытаемся снова превратить ее в оригинал. Давайте воспользуемся функцией потери согласованности цикла, которую мы создали ранее, чтобы сравнить ее с реальным оригиналом. И вот эта лямбда - значит, мы использовали какой-то вес, который на самом деле будет настраивать, мы просто используем значение по умолчанию, которое они предложили в своих вариантах. Затем сделайте то же самое для противоположного направления и сложите их все вместе. Затем мы делаем шаг назад. Вот и все.

Таким образом, мы можем сделать то же самое для первого дискриминатора [2:13:50]. Поскольку практически вся работа уже сделана, делать здесь гораздо меньше. Вот это. Мы не будем проходить через все это, но это в основном те же самые базовые вещи, которые мы уже видели.

Итак, optimize_parameters() вычисляет потери и выполняет шаг оптимизатора. Время от времени сохраняйте и распечатывайте некоторые результаты. Затем время от времени обновляйте скорость обучения, чтобы у них также было встроено некоторое отжигание скорости обучения. Как и у fast.ai, у них есть идея планировщиков, которые затем можно использовать для обновления скорости обучения.

Для тех из вас, кто заинтересован в лучшем понимании API глубокого обучения, внесении большего вклада в fast.ai или создании собственной версии некоторых из этих вещей в другом сервере, было бы здорово взглянуть на второй API, который охватывает некоторое подмножество некоторые из похожих вещей, чтобы понять, как они решают некоторые из этих проблем и в чем сходства / различия.

def show_img(im, ax=None, figsize=None):
    if not ax: fig,ax = plt.subplots(figsize=figsize)
    ax.imshow(im)
    ax.get_xaxis().set_visible(False)
    ax.get_yaxis().set_visible(False)
    return ax
def get_one(data):
    model.set_input(data)
    model.test()
    return list(model.get_current_visuals().values())
model.save(201)
test_ims = []
for i,o in enumerate(dataset):
    if i>10: break
    test_ims.append(get_one(o))
def show_grid(ims):
    fig,axes = plt.subplots(2,3,figsize=(9,6))
    for i,ax in enumerate(axes.flat): show_img(ims[i], ax);
    fig.tight_layout()
for i in range(8): show_grid(test_ims[i])

Мы тренируем это немного, а затем можем просто взять несколько примеров, и вот они [2:15:29]. Вот лошади, зебры и снова лошади.

Мне потребовалось около 24 часов, чтобы тренировать его, даже так далеко, так что это довольно медленно [2:16:39]. Я знаю, что Хелена постоянно жалуется в Твиттере на то, сколько времени это займет. Я не знаю, насколько она так продуктивна с ними.

#! wget https://people.eecs.berkeley.edu/~taesung_park/CycleGAN/datasets/horse2zebra.zip

Упомяну еще одну вещь, которая вышла вчера [2:16:54]:

Мультимодальный неконтролируемый перевод изображения в изображение

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

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

Уроки: 1234567891011 12 13 14