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

Уроки: 12345678910 11 12 13 14

Урок 7

Тема части 1:

  • классификация и регрессия с глубоким обучением
  • выявление и изучение лучших и установленных практик
  • основное внимание уделяется классификации и регрессии, которая предсказывает «вещь» (например, число, небольшое количество ярлыков)

Часть 2 курса:

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

Обзор Char3Model [02:49]

Напоминание: RNN не является чем-то особенным, необычным или волшебным - это просто стандартная полностью подключенная сеть.

  • Стрелки представляют одну или несколько операций над слоями - обычно линейную, за которой следует нелинейная функция, в данном случае умножение матриц, за которым следует relu или tanh
  • Стрелки одного цвета представляют собой точно такую ​​же используемую матрицу весов.
  • Одно небольшое отличие от предыдущего состоит в том, что входы поступают на втором и третьем уровнях. Мы пробовали два подхода - объединение и добавление этих входных данных к текущим активациям.
class Char3Model(nn.Module):
    def __init__(self, vocab_size, n_fac):
        super().__init__()
        self.e = nn.Embedding(vocab_size, n_fac)

        # The 'green arrow' from our diagram
        self.l_in = nn.Linear(n_fac, n_hidden)

        # The 'orange arrow' from our diagram
        self.l_hidden = nn.Linear(n_hidden, n_hidden)
        
        # The 'blue arrow' from our diagram
        self.l_out = nn.Linear(n_hidden, vocab_size)
        
    def forward(self, c1, c2, c3):
        in1 = F.relu(self.l_in(self.e(c1)))
        in2 = F.relu(self.l_in(self.e(c2)))
        in3 = F.relu(self.l_in(self.e(c3)))
        
        h = V(torch.zeros(in1.size()).cuda())
        h = F.tanh(self.l_hidden(h+in1))
        h = F.tanh(self.l_hidden(h+in2))
        h = F.tanh(self.l_hidden(h+in3))
        
        return F.log_softmax(self.l_out(h))
  • Используя nn.Linear, мы бесплатно упаковываем и матрицу весов, и вектор смещения.
  • Чтобы справиться с тем фактом, что у первого эллипса нет оранжевой стрелки, мы изобрели пустую матрицу
class CharLoopModel(nn.Module):
    # This is an RNN!
    def __init__(self, vocab_size, n_fac):
        super().__init__()
        self.e = nn.Embedding(vocab_size, n_fac)
        self.l_in = nn.Linear(n_fac, n_hidden)
        self.l_hidden = nn.Linear(n_hidden, n_hidden)
        self.l_out = nn.Linear(n_hidden, vocab_size)
        
    def forward(self, *cs):
        bs = cs[0].size(0)
        h = V(torch.zeros(bs, n_hidden).cuda())
        for c in cs:
            inp = F.relu(self.l_in(self.e(c)))
            h = F.tanh(self.l_hidden(h+inp))
        
        return F.log_softmax(self.l_out(h), dim=-1)
  • Практически идентичны, за исключением петли for
class CharRnn(nn.Module):
    def __init__(self, vocab_size, n_fac):
        super().__init__()
        self.e = nn.Embedding(vocab_size, n_fac)
        self.rnn = nn.RNN(n_fac, n_hidden)
        self.l_out = nn.Linear(n_hidden, vocab_size)
        
    def forward(self, *cs):
        bs = cs[0].size(0)
        h = V(torch.zeros(1, bs, n_hidden))
        inp = self.e(torch.stack(cs))
        outp,h = self.rnn(inp, h)
        
        return F.log_softmax(self.l_out(outp[-1]), dim=-1)
  • Версия PyTorch - nn.RNN создаст цикл и будет отслеживать h по мере его выполнения.
  • Мы используем белый раздел, чтобы предсказать зеленый символ, что кажется расточительным, поскольку следующий раздел в основном перекрывается с текущим разделом.

  • Затем мы попытались разбить его на неперекрывающиеся части в модели с несколькими выходами:

  • При таком подходе мы отбрасываем нашу h активацию после обработки каждого раздела и запускаем новый. Чтобы предсказать второго персонажа, используя первого в следующем разделе, ему нечего делать, кроме активации по умолчанию. Не будем выбрасывать h.

РНС с сохранением состояния [08:52]

class CharSeqStatefulRnn(nn.Module):
    def __init__(self, vocab_size, n_fac, bs):
        self.vocab_size = vocab_size
        super().__init__()
        self.e = nn.Embedding(vocab_size, n_fac)
        self.rnn = nn.RNN(n_fac, n_hidden)
        self.l_out = nn.Linear(n_hidden, vocab_size)
        self.init_hidden(bs)
        
    def forward(self, cs):
        bs = cs[0].size(0)
        if self.h.size(1) != bs: self.init_hidden(bs)
        outp,h = self.rnn(self.e(cs), self.h)
        self.h = repackage_var(h)
        return F.log_softmax(self.l_out(outp), dim=-1).view(-1, self.vocab_size)
    
    def init_hidden(self, bs): self.h = V(torch.zeros(1, bs, n_hidden))
  • Одна дополнительная строка в конструкторе. self.init_hidden(bs) устанавливает self.h в кучу нулей.
  • Проблема №1 [10:51] - если бы мы просто сделали self.h = h и обучили документ длиной в миллион символов, тогда размер развернутой версии RNN был миллионов слоев (эллипсов). Полностью подключенная сеть с одним миллионом слоев будет очень интенсивно использовать память, потому что для выполнения правила цепочки мы должны умножить один миллион слоев, запоминая все один миллион градиентов в каждой партии.
  • Чтобы избежать этого, мы время от времени говорим ему забывать свою историю. Мы все еще можем помнить состояние (значения в нашей скрытой матрице), не запоминая всего, как мы туда попали.
def repackage_var(h):
    return Variable(h.data) if type(h) == Variable else tuple(repackage_var(v) for v in h)
  • Возьмите тензор из Variable h (помните, что сам тензор не имеет никакого понятия истории) и создайте новый Variable из этого. Новая переменная имеет то же значение, но не имеет истории операций, поэтому, когда она пытается распространиться в обратном направлении, она остановится на этом.
  • forward обрабатывает 8 символов, затем распространяется обратно через восемь уровней, отслеживает значения в скрытом состоянии, но при этом отбрасывает свою историю операций. Это называется back-prop through time (bptt).
  • Другими словами, после цикла for просто выбросьте историю операций и начните заново. Таким образом, мы сохраняем свое скрытое состояние, но не сохраняем историю скрытого состояния.
  • Еще одна веская причина не распространяться назад через слишком много слоев заключается в том, что если у вас есть какой-либо вид градиентной нестабильности (например, градиентный взрыв или градиентное изгнание), чем больше у вас слоев, тем сложнее становится обучение сети (медленнее и менее устойчиво) .
  • С другой стороны, более длинный bptt означает, что вы можете явно захватывать более длинную память и большее количество состояний.
  • Морщинка №2 [16:00] - как создавать мини-партии. Мы не хотим обрабатывать одну секцию за раз, а сразу несколько.
  • Когда мы впервые начали рассматривать TorchText, мы говорили о том, как он создает эти мини-пакеты.
  • Джереми сказал, что мы берем целый длинный документ, состоящий из всех работ Ницше или всех обзоров IMDB, объединенных вместе, мы разбиваем его на 64 фрагмента одинакового размера (НЕ фрагменты размером 64).

  • Для документа длиной 64 миллиона символов каждый «кусок» будет составлять 1 миллион символов. Складываем их вместе и теперь разбиваем на bptt - 1 минибах состоит из 64 на bptt матрицы.
  • Первый символ второго фрагмента (1 000 001-й символ), скорее всего, находится в середине предложения. Но это нормально, так как это происходит только один раз на миллион символов.

Вопрос: Увеличение данных для такого набора данных? [20:34]

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

Вопрос: Как выбрать размер bptt? [21:36]

Есть пара вещей, о которых стоит подумать:

  • во-первых, матрица мини-пакетной обработки имеет размер bs (количество фрагментов) на bptt, поэтому ваша ОЗУ графического процессора должна соответствовать этой матрице встраивания. Поэтому, если вы получаете ошибку CUDA из-за нехватки памяти, вам нужно уменьшить одну из них.
  • Если ваше обучение нестабильно (например, ваша потеря внезапно падает до NaN), вы можете попробовать уменьшить свой bptt, потому что у вас меньше слоев для градиентного взрыва.
  • Если он слишком медленный [22:44], попробуйте уменьшить свой bptt, потому что он будет выполнять один из этих шагов за раз. for цикл не может быть распараллелен (для текущей версии). Недавно появилась технология QRNN (квази-рекуррентная нейронная сеть), которая распараллеливает ее, и мы надеемся осветить ее во второй части.
  • Так что выберите наибольшее число, которое удовлетворяет всем этим требованиям.

Stateful RNN и TorchText [23:23]

При использовании существующего API, который ожидает, что данные будут в определенном формате, вы можете либо изменить свои данные, чтобы они соответствовали этому формату, либо вы можете написать свой собственный подкласс набора данных для обработки формата, в котором ваши данные уже находятся. Либо это нормально, но в В этом случае мы поместим наши данные в формат, который уже поддерживается TorchText. В оболочке Fast.ai вокруг TorchText уже есть кое-что, где вы можете иметь путь обучения и путь проверки, а также один или несколько текстовых файлов в каждом пути, содержащих кучу текста, объединенного вместе для вашей языковой модели.

from torchtext import vocab, data  
from fastai.nlp import * 
from fastai.lm_rnn import *  
PATH='data/nietzsche/'  
TRN_PATH = 'trn/' 
VAL_PATH = 'val/' 
TRN = f'{PATH}{TRN_PATH}' 
VAL = f'{PATH}{VAL_PATH}'
%ls {PATH}
models/  nietzsche.txt  trn/  val/
%ls {PATH}trn
trn.txt
  • Сделал копию файла Ницше, вставил в каталог обучения и проверки. Затем удалили последние 20% строк из обучающего набора и удалили все, кроме последних 20% из проверочного набора [25:15].
  • Другим преимуществом этого способа является то, что кажется более реалистичным иметь набор проверки, который не был бы случайным перетасованным набором строк текста, а был бы полностью отдельной частью корпуса.
  • Когда вы создаете языковую модель, вам не нужны отдельные файлы. У вас может быть несколько файлов, но они в любом случае просто объединяются.
TEXT = data.Field(lower=True, tokenize=list)
bs=64; bptt=8; n_fac=42; n_hidden=256

FILES = dict(train=TRN_PATH, validation=VAL_PATH, test=VAL_PATH)
md = LanguageModelData.from_text_files(PATH, TEXT, **FILES, bs=bs, bptt=bptt, min_freq=3)

len(md.trn_dl), md.nt, len(md.trn_ds), len(md.trn_ds[0].text)
(963, 56, 1, 493747)
  • В TorchText мы называем эту вещь Field, и изначально Field это просто описание того, как выполнить предварительную обработку текста.
  • lower - мы сказали ему строчные буквы
  • tokenize - В прошлый раз мы использовали функцию разбиения по пробелам, которая дала нам модель слова. На этот раз нам нужна символьная модель, поэтому используйте функцию list для токенизации строк. Помните, что в Python list('abc') вернет ['a', 'b', 'c'].
  • bs: размер пакета, bptt: мы переименовали cs, n_fac: размер встраивания, n_hidden: размер нашего скрытого состояния
  • У нас нет отдельного набора тестов, поэтому мы будем использовать только набор проверки для тестирования.
  • TorchText каждый раз немного рандомизирует длину bptt. Он не всегда дает нам ровно 8 символов; В 5% случаев он сокращает его вдвое и добавляет небольшое стандартное отклонение, чтобы сделать его немного больше или меньше 8. Мы не можем перемешать данные, так как они должны быть непрерывными, поэтому это способ внести некоторую случайность. .
  • Вопрос [31:46]: Остается ли размер для мини-партии постоянным? Да, нам нужно выполнить матричное умножение с матрицей весов h, поэтому размер мини-пакета должен оставаться постоянным. Но длина последовательности не может измениться.
  • len(md.trn_dl): длина загрузчика данных (т.е. сколько мини-пакетов), md.nt: количество токенов (т.е. сколько уникальных вещей в словаре)
  • После запуска LanguageModelData.from_text_files TEXT будет содержать дополнительный атрибут с именем vocab. TEXT.vocab.itos список уникальных элементов в словаре, а TEXT.vocab.stoi - обратное отображение каждого элемента в число.
class CharSeqStatefulRnn(nn.Module):
    def __init__(self, vocab_size, n_fac, bs):
        self.vocab_size = vocab_size
        super().__init__()
        self.e = nn.Embedding(vocab_size, n_fac)
        self.rnn = nn.RNN(n_fac, n_hidden)
        self.l_out = nn.Linear(n_hidden, vocab_size)
        self.init_hidden(bs)
        
    def forward(self, cs):
        bs = cs[0].size(0)
        if self.h.size(1) != bs: self.init_hidden(bs)
        outp,h = self.rnn(self.e(cs), self.h)
        self.h = repackage_var(h)
        return F.log_softmax(self.l_out(outp), dim=-1).view(-1, self.vocab_size)
    
    def init_hidden(self, bs): self.h = V(torch.zeros(1, bs, n_hidden))
  • Проблема №3 [33:51]: Джереми солгал нам, когда сказал, что размер мини-партии остается постоянным. Весьма вероятно, что последний мини-пакет короче остальных, если только набор данных не делится точно на bptt раз bs. Вот почему мы проверяем, совпадает ли второе измерение self.h с bs ввода. Если это не то же самое, установите его обратно в ноль с помощью входного bs. Это происходит в конце эпохи и в начале эпохи (возврат к полному размеру пакета).
  • Недостаток №4 [35:44]: последний недостаток - это то, что немного отстойно в PyTorch, и, возможно, кто-то может быть достаточно любезным, чтобы попытаться исправить это с помощью PR. Функции потерь недовольны получением тензора 3-го ранга (т. Е. Трехмерного массива). Нет особой причины, по которой они не должны радоваться получению тензора ранга 3 (длина последовательности по размеру пакета по результатам - поэтому вы можете просто рассчитать потери для каждой из двух начальных осей). Работает на 2 или 4 ранг, но не на 3.
  • .view изменит тензор 3-го ранга на 2-й уровень -1 (сколь угодно большой) на vocab_size. TorchText автоматически изменяет цель, чтобы она была сглажена, поэтому нам не нужно делать это для фактических значений (когда мы смотрели мини-пакет в уроке 4, мы заметили, что он был сглаженным. Джереми сказал мы узнаем о том, почему позже, так что позже сейчас.)
  • PyTorch (начиная с версии 0.3) log_softmax требует от нас указать, по какой оси мы хотим выполнить softmax (т.е. по какой оси мы хотим суммировать одну). В данном случае мы хотим сделать это по последней оси dim = -1.
m = CharSeqStatefulRnn(md.nt, n_fac, 512).cuda() 
opt = optim.Adam(m.parameters(), 1e-3)
fit(m, md, 4, opt, F.nll_loss)

Давайте получим больше информации, распаковав RNN [42:48]

Мы убираем использование nn.RNN и заменяем его на nn.RNNCell. Исходный код PyTorch выглядит следующим образом. Вы должны уметь читать и понимать (Примечание: они не объединяют ввод и скрытое состояние, а суммируют их вместе - что было нашим первым подходом):

def RNNCell(input, hidden, w_ih, w_hh, b_ih, b_hh):
    return F.tanh(F.linear(input, w_ih, b_ih) + F.linear(hidden, w_hh, b_hh))

Вопрос о tanh [44:06]: Как мы видели на прошлой неделе, tanh заставляет значение быть между -1 и 1. Поскольку мы снова и снова умножаем эту весовую матрицу, мы будем беспокоиться о том, что relu (поскольку он неограничен) может иметь больше проблем с градиентным взрывом. Сказав это, вы можете указать RNNCell, чтобы использовать разные nonlineality, по умолчанию tanh, и попросить его использовать relu, если хотите.

class CharSeqStatefulRnn2(nn.Module):
    def __init__(self, vocab_size, n_fac, bs):
        super().__init__()
        self.vocab_size = vocab_size
        self.e = nn.Embedding(vocab_size, n_fac)
        self.rnn = nn.RNNCell(n_fac, n_hidden)
        self.l_out = nn.Linear(n_hidden, vocab_size)
        self.init_hidden(bs)
        
    def forward(self, cs):
        bs = cs[0].size(0)
        if self.h.size(1) != bs: self.init_hidden(bs)
        outp = []
        o = self.h
        for c in cs: 
            o = self.rnn(self.e(c), o)
            outp.append(o)
        outp = self.l_out(torch.stack(outp))
        self.h = repackage_var(o)
        return F.log_softmax(outp, dim=-1).view(-1, self.vocab_size)
    
    def init_hidden(self, bs): self.h = V(torch.zeros(1, bs, n_hidden))
  • Цикл for вернулся и добавляет результат линейной функции в список, который, в конце концов, складывается вместе.
  • Библиотека fast.ai на самом деле делает именно это, чтобы использовать подходы регуляризации, которые не поддерживаются PyTorch.

Закрытый рекуррентный блок (ГРУ) [46:44]

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

  • Обычно входные данные умножаются на матрицу весов для создания новых h активаций и сразу же добавляются к существующим активациям. Это не то, что здесь происходит.
  • Ввод идет в , и он не просто добавляется к предыдущим активациям, но предыдущая активация умножается на r (сброс ворот), который имеет значение 0 или 1.
  • r вычисляется следующим образом - матричное умножение некоторой весовой матрицы и конкатенация нашего предыдущего скрытого состояния и нового ввода. Другими словами, это небольшая нейронная сеть скрытого слоя. Он также проходит через сигмовидную функцию. Эта мини-нейронная сеть учится определять, сколько скрытых состояний нужно запомнить (возможно, забудет все это, когда увидит точку с точкой - начало нового предложения).
  • z gate (ворота обновления) определяет, в какой степени использовать (новую версию входных скрытых состояний) и в какой степени оставить скрытое состояние таким же, как и раньше.

  • Линейная интерполяция
def GRUCell(input, hidden, w_ih, w_hh, b_ih, b_hh):
    gi = F.linear(input, w_ih, b_ih)
    gh = F.linear(hidden, w_hh, b_hh)
    i_r, i_i, i_n = gi.chunk(3, 1)
    h_r, h_i, h_n = gh.chunk(3, 1)

    resetgate = F.sigmoid(i_r + h_r)
    inputgate = F.sigmoid(i_i + h_i)
    newgate = F.tanh(i_n + resetgate * h_n)
    return newgate + inputgate * (hidden - newgate)

Выше показан код GRUCell, а наша новая модель, в которой он используется, представлена ​​ниже:

class CharSeqStatefulGRU(nn.Module):
    def __init__(self, vocab_size, n_fac, bs):
        super().__init__()
        self.vocab_size = vocab_size
        self.e = nn.Embedding(vocab_size, n_fac)
        self.rnn = nn.GRU(n_fac, n_hidden)
        self.l_out = nn.Linear(n_hidden, vocab_size)
        self.init_hidden(bs)
        
    def forward(self, cs):
        bs = cs[0].size(0)
        if self.h.size(1) != bs: self.init_hidden(bs)
        outp,h = self.rnn(self.e(cs), self.h)
        self.h = repackage_var(h)
        return F.log_softmax(self.l_out(outp), dim=-1).view(-1, self.vocab_size)
    
    def init_hidden(self, bs): self.h = V(torch.zeros(1, bs, n_hidden))

В результате мы можем снизить убыток до 1,36 (RNNCell был 1,54). На практике люди используют ГРУ и LSTM.

Собираем все вместе: долговременная краткосрочная память [54:09]

В LSTM есть еще одна часть состояния, называемая «состоянием ячейки» (а не только скрытым состоянием), поэтому, если вы действительно используете LSTM, вы должны вернуть кортеж матриц в init_hidden (точно такого же размера, как и скрытое состояние):

from fastai import sgdr

n_hidden=512
class CharSeqStatefulLSTM(nn.Module):
    def __init__(self, vocab_size, n_fac, bs, nl):
        super().__init__()
        self.vocab_size,self.nl = vocab_size,nl
        self.e = nn.Embedding(vocab_size, n_fac)
        self.rnn = nn.LSTM(n_fac, n_hidden, nl, dropout=0.5)
        self.l_out = nn.Linear(n_hidden, vocab_size)
        self.init_hidden(bs)
        
    def forward(self, cs):
        bs = cs[0].size(0)
        if self.h[0].size(1) != bs: self.init_hidden(bs)
        outp,h = self.rnn(self.e(cs), self.h)
        self.h = repackage_var(h)
        return F.log_softmax(self.l_out(outp), dim=-1).view(-1, self.vocab_size)
    
    def init_hidden(self, bs):
        self.h = (V(torch.zeros(self.nl, bs, n_hidden)),
                  V(torch.zeros(self.nl, bs, n_hidden)))

Код идентичен коду ГРУ. Единственное, что было добавлено, - это dropout, который выпадает после каждого временного шага и удваивает скрытый слой - в надежде, что он сможет узнать больше и быть устойчивым при этом.

Обратные вызовы (в частности, SGDR) без класса учащегося [55:23]

m = CharSeqStatefulLSTM(md.nt, n_fac, 512, 2).cuda()
lo = LayerOptimizer(optim.Adam, m, 1e-2, 1e-5)
  • После создания стандартной модели PyTorch мы обычно делаем что-то вроде opt = optim.Adam(m.parameters(), 1e-3). Вместо этого мы будем использовать fast.ai LayerOptimizer, который использует оптимизатор optim.Adam, нашу модель m, скорость обучения 1e-2 и, возможно, снижение веса 1e-5.
  • Основная причина, по которой существует LayerOptimizer, - это дифференцированная скорость обучения и снижение дифференциального веса. Причина, по которой нам нужно его использовать, заключается в том, что все механизмы внутри fast.ai предполагают, что у вас есть один из них. Если вы хотите использовать обратные вызовы или SGDR в коде, вы не используете класс Learner, вам необходимо использовать это.
  • lo.opt возвращает оптимизатор.
on_end = lambda sched, cycle: save_model(m, f'{PATH}models/cyc_{cycle}')
cb = [CosAnneal(lo, len(md.trn_dl), cycle_mult=2, on_cycle_end=on_end)]
fit(m, md, 2**4-1, lo.opt, F.nll_loss, callbacks=cb)
  • Когда мы вызываем fit, теперь мы можем передать LayerOptimizer, а также callbacks.
  • Здесь мы используем обратный вызов косинусного отжига, для которого требуется объект LayerOptimizer. Он выполняет косинусный отжиг, изменяя скорость обучения рядом с объектом lo.
  • Концепция: создайте обратный вызов косинусного отжига, который обновит скорость обучения в оптимизаторе слоев lo. Длина эпохи равна len(md.trn_dl) - количество мини-пакетов в эпоху равно длине загрузчика данных. Поскольку он выполняет косинусный отжиг, ему необходимо знать, как часто нужно выполнять сброс. Вы можете пройти cycle_mult обычным способом. Мы даже можем сохранить нашу модель автоматически, как мы это делали с cycle_save_name в Learner.fit.
  • Мы можем выполнять обратный вызов в начале обучения, эпохи или пакета, или в конце обучения, эпохи или пакета.
  • Он использовался для CosAnneal (SGDR) и развязанного спада веса (AdamW), графика потери во времени и т. Д.

Тестирование [59:55]

def get_next(inp):
    idxs = TEXT.numericalize(inp)
    p = m(VV(idxs.transpose(0,1)))
    r = torch.multinomial(p[-1].exp(), 1)
    return TEXT.vocab.itos[to_np(r)[0]]
def get_next_n(inp, n):
    res = inp
    for i in range(n):
        c = get_next(inp)
        res += c
        inp = inp[1:]+c
    return res
print(get_next_n('for thos', 400))
for those the skemps), or imaginates, though they deceives. it should so each ourselvess and new present, step absolutely for the science." the contradity and measuring,  the whole!  
293. perhaps, that every life a values of blood of intercourse when it senses there is unscrupulus, his very rights, and still impulse, love? just after that thereby how made with the way anything, and set for harmless philos
  • В уроке 6, когда мы тестировали модель CharRnn, мы заметили, что она повторяется снова и снова. torch.multinomial, используемый в этой новой версии, решает эту проблему. p[-1], чтобы получить окончательный результат (треугольник), exp, чтобы преобразовать логарифмическую вероятность в вероятность. Затем мы используем функцию torch.multinomial, которая даст нам образец с заданными вероятностями. Если вероятность равна [0, 1, 0, 0] и попросить его предоставить нам образец, он всегда будет возвращать второй элемент. Если было [0,5, 0, 0,5], первый элемент выдаст 50% времени, а второй элемент. 50% времени (обзор полиномиального распределения)
  • Чтобы поэкспериментировать с подобными обучающими языковыми моделями на основе персонажей, попробуйте запустить get_next_n на разных уровнях потери, чтобы получить представление о том, как это выглядит. В приведенном выше примере значение 1,25, но значение 1,3 выглядит как полный мусор.
  • Когда вы играете с НЛП, особенно с генеративной моделью, подобной этой, и результаты вроде бы нормальные, но не отличные, не расстраивайтесь, потому что это означает, что вы на самом деле ОЧЕНЬ почти готовы!

Назад к компьютерному зрению: CIFAR 10 [1:01:58]

CIFAR 10 - это старый и хорошо известный набор данных в академических кругах - задолго до ImageNet был CIFAR 10. Он невелик как с точки зрения количества изображений, так и размера изображений, что делает его интересным и сложным. Скорее всего, вы будете работать с тысячами изображений, а не с полутора миллионами изображений. Кроме того, многие вещи, на которые мы смотрим, например, в медицинской визуализации, мы смотрим на определенную область, где есть узелок в легких, вы, вероятно, смотрите максимум 32 на 32 пикселя.

Он также работает быстро, поэтому лучше протестировать свои алгоритмы. Как упомянул Али Рахини в NIPS 2017, Джереми обеспокоен тем, что многие люди не проводят тщательно настроенные и продуманные эксперименты по глубокому обучению, а вместо этого бросают много графических процессоров и TPU или много данных и считают это днем. Важно протестировать многие версии вашего алгоритма на наборе данных, таком как CIFAR 10, а не на ImageNet, что занимает недели. MNIST также хорош для исследований и экспериментов, хотя люди склонны жаловаться на это.

Данные CIFAR 10 в формате изображения доступны здесь

from fastai.conv_learner import *
PATH = "data/cifar10/"
os.makedirs(PATH,exist_ok=True)
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]))
def get_data(sz,bs):
     tfms = tfms_from_stats(stats, sz, aug_tfms=[RandomFlipXY()], pad=sz//8)
     return ImageClassifierData.from_paths(PATH, val_name='test', tfms=tfms, bs=bs)
bs=256
  • classes - метки изображений
  • stats —Когда мы используем предварительно обученные модели, вы можете вызвать tfms_from_model, который создает необходимые преобразования для преобразования нашего набора данных в нормализованный набор данных на основе средних значений и стандартных отклонений каждого канала в исходной модели, в которой было обучено. Поскольку мы обучая модель с нуля, мы должны сообщить ей среднее значение и стандартное отклонение наших данных, чтобы нормализовать их. Убедитесь, что вы можете рассчитать среднее значение и стандартное отклонение для каждого канала.
  • tfms - Для увеличения данных CIFAR 10 люди обычно выполняют горизонтальный переворот и черные отступы по краю и случайным образом выбирают область 32 на 32 в пределах дополненного изображения.
data = get_data(32,bs)

lr=1e-2

Из этой тетради нашего ученика Керема Тургутлу:

class SimpleNet(nn.Module):
    def __init__(self, layers):
        super().__init__()
        self.layers = nn.ModuleList([
            nn.Linear(layers[i], layers[i + 1]) for i in range(len(layers) - 1)])
        
    def forward(self, x):
        x = x.view(x.size(0), -1)
        for l in self.layers:
            l_x = l(x)
            x = F.relu(l_x)
        return F.log_softmax(l_x, dim=-1)
  • nn.ModuleList - всякий раз, когда вы создаете список слоев в PyTorch, вы должны заключить его в ModuleList, чтобы зарегистрировать их как атрибуты.
learn = ConvLearner.from_model_data(SimpleNet([32*32*3, 40,10]), data)
  • Теперь мы поднимаемся на один уровень API выше - вместо того, чтобы вызывать fit функцию, мы создаем объект learn из пользовательской модели. ConfLearner.from_model_data принимает стандартную модель PyTorch и объект данных модели.
learn, [o.numel() for o in learn.model.parameters()]
(SimpleNet(
   (layers): ModuleList(
     (0): Linear(in_features=3072, out_features=40)
     (1): Linear(in_features=40, out_features=10)
   )
 ), [122880, 40, 400, 10])
learn.summary()
OrderedDict([('Linear-1',
              OrderedDict([('input_shape', [-1, 3072]),
                           ('output_shape', [-1, 40]),
                           ('trainable', True),
                           ('nb_params', 122920)])),
             ('Linear-2',
              OrderedDict([('input_shape', [-1, 40]),
                           ('output_shape', [-1, 10]),
                           ('trainable', True),
                           ('nb_params', 410)]))])
learn.lr_find()
learn.sched.plot()

%time learn.fit(lr, 2)
A Jupyter Widget
[ 0.       1.7658   1.64148  0.42129]                       
[ 1.       1.68074  1.57897  0.44131]                       

CPU times: user 1min 11s, sys: 32.3 s, total: 1min 44s
Wall time: 55.1 s
%time learn.fit(lr, 2, cycle_len=1)
A Jupyter Widget
[ 0.       1.60857  1.51711  0.46631]                       
[ 1.       1.59361  1.50341  0.46924]                       

CPU times: user 1min 12s, sys: 31.8 s, total: 1min 44s
Wall time: 55.3 s

С помощью простой модели с одним скрытым слоем с 122880 параметрами мы достигли точности 46,9%. Давайте улучшим это и постепенно перейдем к базовой архитектуре ResNet.

CNN [01:12:30]

  • Давайте заменим полносвязную модель сверточной. Полностью связанный слой - это просто скалярное произведение. Поэтому весовая матрица большая (3072 входа * 40 = 122880). Мы не очень эффективно используем параметры, потому что каждый пиксель на входе имеет разный вес. Мы хотим создать группу размером 3 на 3 пикселя, имеющую определенные шаблоны (например, свертку).
  • Мы будем использовать фильтр с ядром три на три. При наличии нескольких фильтров у вывода будет дополнительное измерение.
class ConvNet(nn.Module):
    def __init__(self, layers, c):
        super().__init__()
        self.layers = nn.ModuleList([
            nn.Conv2d(layers[i], layers[i + 1], kernel_size=3, stride=2)
            for i in range(len(layers) - 1)])
        self.pool = nn.AdaptiveMaxPool2d(1)
        self.out = nn.Linear(layers[-1], c)
        
    def forward(self, x):
        for l in self.layers: x = F.relu(l(x))
        x = self.pool(x)
        x = x.view(x.size(0), -1)
        return F.log_softmax(self.out(x), dim=-1)
  • Заменить nn.Linear на nn.Conv2d
  • Первые два параметра точно такие же, как nn.Linear - количество входящих функций и количество выходящих функций.
  • kernel_size=3, размер фильтра
  • stride=2 будет использовать все остальные области 3 на 3, что уменьшит выходное разрешение в каждом измерении вдвое (т. Е. Имеет тот же эффект, что и максимальное объединение 2 на 2)
learn = ConvLearner.from_model_data(ConvNet([3, 20, 40, 80], 10), data)
learn.summary()
OrderedDict([('Conv2d-1',
              OrderedDict([('input_shape', [-1, 3, 32, 32]),
                           ('output_shape', [-1, 20, 15, 15]),
                           ('trainable', True),
                           ('nb_params', 560)])),
             ('Conv2d-2',
              OrderedDict([('input_shape', [-1, 20, 15, 15]),
                           ('output_shape', [-1, 40, 7, 7]),
                           ('trainable', True),
                           ('nb_params', 7240)])),
             ('Conv2d-3',
              OrderedDict([('input_shape', [-1, 40, 7, 7]),
                           ('output_shape', [-1, 80, 3, 3]),
                           ('trainable', True),
                           ('nb_params', 28880)])),
             ('AdaptiveMaxPool2d-4',
              OrderedDict([('input_shape', [-1, 80, 3, 3]),
                           ('output_shape', [-1, 80, 1, 1]),
                           ('nb_params', 0)])),
             ('Linear-5',
              OrderedDict([('input_shape', [-1, 80]),
                           ('output_shape', [-1, 10]),
                           ('trainable', True),
                           ('nb_params', 810)]))])
  • ConvNet([3, 20, 40, 80], 10) - Он начинается с 3 каналов RGB, 20, 40, 80 функций, а затем 10 классов для прогнозирования.
  • AdaptiveMaxPool2d - За этим следует линейный слой - это то, как вы переходите от 3 к 3 к предсказанию одного из 10 классов, и теперь он является стандартом для современных алгоритмов. На самом последнем уровне мы делаем особый вид максимального пула, для которого вы указываете разрешение активации вывода, а не размер области для опроса. Другими словами, здесь мы делаем макс-пул 3 на 3, что эквивалентно адаптивному макс-пулу 1 на 1.
  • x = x.view(x.size(0), -1) - x имеет форму # элементов с шагом 1 на 1, поэтому последние два слоя будут удалены.
  • Эта модель называется «полностью сверточной сетью» - где каждый слой является сверточным, кроме самого последнего.
learn.lr_find(end_lr=100)
learn.sched.plot()

  • Окончательная скорость обучения по умолчанию lr_find попыток равна 10. Если к этому моменту потери все еще улучшаются, вы можете перезаписать, указав end_lr.
%time learn.fit(1e-1, 2)
A Jupyter Widget
[ 0.       1.72594  1.63399  0.41338]                       
[ 1.       1.51599  1.49687  0.45723]                       

CPU times: user 1min 14s, sys: 32.3 s, total: 1min 46s
Wall time: 56.5 s
%time learn.fit(1e-1, 4, cycle_len=1)
A Jupyter Widget
[ 0.       1.36734  1.28901  0.53418]                       
[ 1.       1.28854  1.21991  0.56143]                       
[ 2.       1.22854  1.15514  0.58398]                       
[ 3.       1.17904  1.12523  0.59922]                       

CPU times: user 2min 21s, sys: 1min 3s, total: 3min 24s
Wall time: 1min 46s
  • Он снизил точность около 60%. Учитывая, что он использует около 30 000 параметров (по сравнению с 47% с 122 тыс. Параметров)
  • Время на эпоху примерно одинаково, поскольку их архитектуры просты и большая часть времени уходит на передачу памяти.

Произведен рефакторинг [01:21:57]

Упростите forward функцию, создав ConvLayer (наш первый пользовательский слой!). В PyTorch определение слоя и определения нейронной сети идентичны. Каждый раз, когда у вас есть слой, вы можете использовать его как нейронную сеть, когда у вас есть нейронная сеть, вы можете использовать ее как слой.

class ConvLayer(nn.Module):
    def __init__(self, ni, nf):
        super().__init__()
        self.conv = nn.Conv2d(ni, nf, kernel_size=3, stride=2, padding=1)
        
    def forward(self, x): return F.relu(self.conv(x))
  • padding=1 - При свертке изображение сжимается на 1 пиксель с каждой стороны. Таким образом, он изменяется не с 32 на 32 до 16 на 16, а с 15 на 15. padding добавит границу, чтобы мы могли сохранить информацию о пикселях края. Это не так уж важно для большого изображения, но когда оно уменьшается до 4 на 4, вы действительно не хотите выбрасывать целую часть.
class ConvNet2(nn.Module):
    def __init__(self, layers, c):
        super().__init__()
        self.layers = nn.ModuleList([ConvLayer(layers[i], layers[i + 1])
            for i in range(len(layers) - 1)])
        self.out = nn.Linear(layers[-1], c)
        
    def forward(self, x):
        for l in self.layers: x = l(x)
        x = F.adaptive_max_pool2d(x, 1)
        x = x.view(x.size(0), -1)
        return F.log_softmax(self.out(x), dim=-1)
  • Еще одно отличие от предыдущей модели в том, что nn.AdaptiveMaxPool2d не имеет состояния (то есть весов). Так что мы можем просто вызвать его как функцию F.adaptive_max_pool2d.

BatchNorm [1:25:10]

  • В последней модели, когда мы пытались добавить больше слоев, у нас были проблемы с обучением. Причина, по которой у нас были проблемы с обучением, заключалась в том, что если бы мы использовали более высокую скорость обучения, она перешла бы в NaN, а если бы мы использовали меньшую скорость обучения, это заняло бы вечность и не было бы возможности правильно исследовать - поэтому это было не устойчиво.
  • Чтобы сделать его устойчивым, мы будем использовать так называемую пакетную нормализацию. BatchNorm появился около двух лет назад и произвел существенные преобразования, поскольку внезапно упростил обучение более глубоких сетей.
  • Мы можем просто использовать nn.BatchNorm, но чтобы узнать об этом, мы напишем его с нуля.
  • Маловероятно, что весовые матрицы в среднем не приведут к тому, что ваши активации будут становиться все меньше и меньше или все больше и больше. Важно, чтобы они были в разумных пределах. Итак, мы начинаем со стандартного отклонения с нулевым средним значением, нормализуя входные данные. Что мы действительно хотим сделать, так это сделать это для всех слоев, а не только для входов.
class BnLayer(nn.Module):
    def __init__(self, ni, nf, stride=2, kernel_size=3):
        super().__init__()
        self.conv = nn.Conv2d(ni, nf, kernel_size=kernel_size, 
                              stride=stride, bias=False, padding=1)
        self.a = nn.Parameter(torch.zeros(nf,1,1))
        self.m = nn.Parameter(torch.ones(nf,1,1))
        
    def forward(self, x):
        x = F.relu(self.conv(x))
        x_chan = x.transpose(0,1).contiguous().view(x.size(1), -1)
        if self.training:
            self.means = x_chan.mean(1)[:,None,None]
            self.stds  = x_chan.std (1)[:,None,None]
        return (x-self.means) / self.stds *self.m + self.a
  • Вычислите среднее значение каждого канала или каждого фильтра и стандартное отклонение каждого канала или каждого фильтра. Затем вычтите средние и разделите на стандартные отклонения.
  • Нам больше не нужно нормализовать наш ввод, потому что он нормализует его для каждого канала или для более поздних слоев он нормализуется для каждого фильтра.
  • Оказывается, этого недостаточно, поскольку SGD кровожаден [01:29:20]. Если SGD решила, что она хочет, чтобы матрица была больше / меньше в целом, выполнения (x=self.means) / self.stds недостаточно, потому что SGD отменит это и попытается сделать это снова в следующем мини-пакете. Поэтому мы добавим два параметра: a - сумматор (нули начального значения) и m - множитель (единицы начального значения) для каждого канала.
  • Parameter сообщает PyTorch, что их можно изучать как веса.
  • Почему это работает? Если он хочет масштабировать слой вверх, ему не нужно увеличивать каждое отдельное значение в матрице. Он может просто увеличить это единственное трио чисел self.m, если он хочет сдвинуть все это немного вверх или вниз, ему не нужно сдвигать всю матрицу весов, они могут просто сдвинуть это трио чисел self.a. Интуиция: мы нормализуем данные, а затем говорим, что вы можете затем сместить их и масштабировать, используя гораздо меньше параметров, чем было бы необходимо, если бы фактически сдвигался и масштабировался весь набор сверточных фильтров. На практике это позволяет нам повысить скорость обучения, повысить устойчивость обучения и позволяет нам добавлять дополнительные уровни и при этом тренироваться эффективно.
  • Еще одна вещь, которую делает норма партии, - это то, что она упорядочивает, другими словами, вы часто можете уменьшить или устранить выпадение или снижение веса. Причина в том, что каждая мини-партия будет иметь другое среднее значение и другое стандартное отклонение по сравнению с предыдущей мини-серией. Таким образом, они продолжают меняться, и это тонко меняет значение фильтров, действуя как шум (то есть регуляризация).
  • В реальной версии он не использует среднее и стандартное отклонение этого пакета, а принимает экспоненциально взвешенное стандартное отклонение и среднее скользящее среднее.
  • if self.training - это важно, потому что, когда вы проходите набор проверки, вы не хотите изменять значение модели. Есть несколько типов слоев, которые действительно чувствительны к тому, какой режим сети находится в режиме обучения или в режиме оценки / тестирования. Когда мы реализовали мини-сеть для MovieLens, произошла ошибка, из-за которой выпадение применялось во время проверки, что было исправлено. В PyTorch таких слоев два: dropout и batch norm. nn.Dropout уже проверяет.
  • [01:37:01] Ключевое отличие fast.ai, которого нет ни в одной другой библиотеке, заключается в том, что эти средства и стандартные отклонения обновляются в режиме обучения во всех остальных библиотеках, как только вы в основном говорите, что я тренируюсь, независимо от того, этот слой установлен на обучаемый или нет. С предварительно обученной сетью это ужасная идея. Если у вас есть предварительно обученная сеть для определенных значений этих средних и стандартных отклонений в норме пакета, если вы их измените, это изменит значение этих предварительно обученных слоев. В fast.ai всегда по умолчанию он не затрагивает эти средства и стандартные отклонения, если ваш слой заморожен. Как только вы его разблокируете, он начнет обновлять их, если вы не установите learn.bn_freeze=True. На практике это часто работает намного лучше для предварительно обученных моделей, особенно если вы работаете с данными, которые очень похожи на те, с которыми была обучена предварительно обученная модель.
  • Куда вы помещаете слой пакетной нормы? Мы поговорим чуть позже, а пока, после relu

Исследование абляции [01:39:41]

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

  • Старайтесь всегда использовать пакетную норму на каждом слое, если можете.
  • Не прекращайте нормализацию ваших данных, чтобы люди, использующие ваши данные, знали, как вы нормализовали ваши данные. Другие библиотеки могут неправильно обрабатывать пакетные нормы для предварительно обученных моделей, поэтому, когда люди начинают повторное обучение, это может вызвать проблемы.
class ConvBnNet(nn.Module):
    def __init__(self, layers, c):
        super().__init__()
        self.conv1 = nn.Conv2d(3, 10, kernel_size=5, stride=1, padding=2)
        self.layers = nn.ModuleList([BnLayer(layers[i], layers[i + 1])
            for i in range(len(layers) - 1)])
        self.out = nn.Linear(layers[-1], c)
        
    def forward(self, x):
        x = self.conv1(x)
        for l in self.layers: x = l(x)
        x = F.adaptive_max_pool2d(x, 1)
        x = x.view(x.size(0), -1)
        return F.log_softmax(self.out(x), dim=-1)
  • Остальная часть кода аналогична - использование BnLayer вместо ConvLayer
  • С самого начала был добавлен единый сверточный слой, пытаясь приблизиться к современным подходам. Он имеет больший размер ядра и шаг 1. Основная идея состоит в том, что мы хотим, чтобы первый уровень имел более богатый ввод. Он выполняет свертку, используя область 5 на 5, что позволяет ему попытаться найти более интересные, более богатые функции в этой области 5 на 5, а затем выплюнуть больший результат (в данном случае это фильтры 10 на 5 на 5). Обычно это свертка 5 на 5, или 7 на 7, или даже 11 на 11, при этом выходит довольно много фильтров (например, 32 фильтра).
  • Начиная с padding = kernel_size — 1 / 2 и stride=1, размер ввода такой же, как размер вывода - только больше фильтров.
  • Это хороший способ попытаться создать более богатую отправную точку.

Deep BatchNorm [01:50:52]

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

class ConvBnNet2(nn.Module):
    def __init__(self, layers, c):
        super().__init__()
        self.conv1 = nn.Conv2d(3, 10, kernel_size=5, stride=1, padding=2)
        self.layers = nn.ModuleList([BnLayer(layers[i], layers[i+1])
            for i in range(len(layers) - 1)])
        self.layers2 = nn.ModuleList([BnLayer(layers[i+1], layers[i + 1], 1)
            for i in range(len(layers) - 1)])
        self.out = nn.Linear(layers[-1], c)
        
    def forward(self, x):
        x = self.conv1(x)
        for l,l2 in zip(self.layers, self.layers2):
            x = l(x)
            x = l2(x)
        x = F.adaptive_max_pool2d(x, 1)
        x = x.view(x.size(0), -1)
        return F.log_softmax(self.out(x), dim=-1)
learn = ConvLearner.from_model_data((ConvBnNet2([10, 20, 40, 80, 160], 10), data)
%time learn.fit(1e-2, 2)
A Jupyter Widget
[ 0.       1.53499  1.43782  0.47588]                       
[ 1.       1.28867  1.22616  0.55537]                       

CPU times: user 1min 22s, sys: 34.5 s, total: 1min 56s
Wall time: 58.2 s
%time learn.fit(1e-2, 2, cycle_len=1)
A Jupyter Widget
[ 0.       1.10933  1.06439  0.61582]                       
[ 1.       1.04663  0.98608  0.64609]                       

CPU times: user 1min 21s, sys: 32.9 s, total: 1min 54s
Wall time: 57.6 s

Точность осталась прежней. Теперь это 12 слоев в глубину, и это слишком глубоко даже для стандартных партий. Можно обучить 12-слойную глубокую свёртку, но это начинает усложняться. И, похоже, это не очень помогает, если вообще помогает.

ResNet [01:52:43]

class ResnetLayer(BnLayer):
    def forward(self, x): return x + super().forward(x)
class Resnet(nn.Module):
    def __init__(self, layers, c):
        super().__init__()
        self.conv1 = nn.Conv2d(3, 10, kernel_size=5, stride=1, padding=2)
        self.layers = nn.ModuleList([BnLayer(layers[i], layers[i+1])
            for i in range(len(layers) - 1)])
        self.layers2 = nn.ModuleList([ResnetLayer(layers[i+1], layers[i + 1], 1)
            for i in range(len(layers) - 1)])
        self.layers3 = nn.ModuleList([ResnetLayer(layers[i+1], layers[i + 1], 1)
            for i in range(len(layers) - 1)])
        self.out = nn.Linear(layers[-1], c)
        
    def forward(self, x):
        x = self.conv1(x)
        for l,l2,l3 in zip(self.layers, self.layers2, self.layers3):
            x = l3(l2(l(x)))
        x = F.adaptive_max_pool2d(x, 1)
        x = x.view(x.size(0), -1)
        return F.log_softmax(self.out(x), dim=-1)
  • ResnetLayer наследуется от BnLayer и отменяет forward.
  • Затем добавьте кучу слоев и сделайте его в 3 раза глубже, но он все равно прекрасно тренируется только благодаря x + super().forward(x).
learn = ConvLearner.from_model_data(Resnet([10, 20, 40, 80, 160], 10), data)
wd=1e-5
%time learn.fit(1e-2, 2, wds=wd)
A Jupyter Widget
[ 0.       1.58191  1.40258  0.49131]                       
[ 1.       1.33134  1.21739  0.55625]                       

CPU times: user 1min 27s, sys: 34.3 s, total: 2min 1s
Wall time: 1min 3s
%time learn.fit(1e-2, 3, cycle_len=1, cycle_mult=2, wds=wd)
A Jupyter Widget
[ 0.       1.11534  1.05117  0.62549]                       
[ 1.       1.06272  0.97874  0.65185]                       
[ 2.       0.92913  0.90472  0.68154]                        
[ 3.       0.97932  0.94404  0.67227]                        
[ 4.       0.88057  0.84372  0.70654]                        
[ 5.       0.77817  0.77815  0.73018]                        
[ 6.       0.73235  0.76302  0.73633]                        

CPU times: user 5min 2s, sys: 1min 59s, total: 7min 1s
Wall time: 3min 39s
%time learn.fit(1e-2, 8, cycle_len=4, wds=wd)
A Jupyter Widget
[ 0.       0.8307   0.83635  0.7126 ]                        
[ 1.       0.74295  0.73682  0.74189]                        
[ 2.       0.66492  0.69554  0.75996]                        
[ 3.       0.62392  0.67166  0.7625 ]                        
[ 4.       0.73479  0.80425  0.72861]                        
[ 5.       0.65423  0.68876  0.76318]                        
[ 6.       0.58608  0.64105  0.77783]                        
[ 7.       0.55738  0.62641  0.78721]                        
[ 8.       0.66163  0.74154  0.7501 ]                        
[ 9.       0.59444  0.64253  0.78106]                        
[ 10.        0.53      0.61772   0.79385]                    
[ 11.        0.49747   0.65968   0.77832]                    
[ 12.        0.59463   0.67915   0.77422]                    
[ 13.        0.55023   0.65815   0.78106]                    
[ 14.        0.48959   0.59035   0.80273]                    
[ 15.        0.4459    0.61823   0.79336]                    
[ 16.        0.55848   0.64115   0.78018]                    
[ 17.        0.50268   0.61795   0.79541]                    
[ 18.        0.45084   0.57577   0.80654]                    
[ 19.        0.40726   0.5708    0.80947]                    
[ 20.        0.51177   0.66771   0.78232]                    
[ 21.        0.46516   0.6116    0.79932]                    
[ 22.        0.40966   0.56865   0.81172]                    
[ 23.        0.3852    0.58161   0.80967]                    
[ 24.        0.48268   0.59944   0.79551]                    
[ 25.        0.43282   0.56429   0.81182]                    
[ 26.        0.37634   0.54724   0.81797]                    
[ 27.        0.34953   0.54169   0.82129]                    
[ 28.        0.46053   0.58128   0.80342]                    
[ 29.        0.4041    0.55185   0.82295]                    
[ 30.        0.3599    0.53953   0.82861]                    
[ 31.        0.32937   0.55605   0.82227]                    

CPU times: user 22min 52s, sys: 8min 58s, total: 31min 51s
Wall time: 16min 38s

Блок ResNet [01:53:18]

return x + super().forward(x)

y = x + f(x)

Где x - это прогноз из предыдущего слоя, y - это прогноз из текущего слоя. Перемешайте формулу, и мы получим: формула в случайном порядке

f(x) = y − x

Разница y - x является остаточной. Остаточная погрешность - это ошибка того, что мы вычислили до сих пор. Это означает, что попытайтесь найти набор сверточных весов, который пытается заполнить сумму, на которую мы отклонились. Другими словами, у нас есть вход, и у нас есть функция, которая пытается предсказать ошибку (т.е. насколько мы отклонились). Затем мы добавляем прогноз того, насколько мы ошибались во входные данные, затем добавляем еще один прогноз того, насколько мы были неправы к тому времени, и повторяем этот слой за слоем, увеличивая масштаб до правильного ответа. Это основано на теории под названием повышение.

  • Полный ResNet выполняет две свертки, прежде чем он будет добавлен обратно к исходному вводу (здесь мы сделали только одну).
  • В каждом блоке x = l3(l2(l(x))) один из слоев - это не ResnetLayer, а стандартная свертка с stride=2 - это называется «слоем узкого места». ResNet - это не сверточный слой, а другая форма блока узких мест, о котором мы поговорим во второй части.

ResNet 2 [01:59:33]

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

class Resnet2(nn.Module):
    def __init__(self, layers, c, p=0.5):
        super().__init__()
        self.conv1 = BnLayer(3, 16, stride=1, kernel_size=7)
        self.layers = nn.ModuleList([BnLayer(layers[i], layers[i+1])
            for i in range(len(layers) - 1)])
        self.layers2 = nn.ModuleList([ResnetLayer(layers[i+1], layers[i + 1], 1)
            for i in range(len(layers) - 1)])
        self.layers3 = nn.ModuleList([ResnetLayer(layers[i+1], layers[i + 1], 1)
            for i in range(len(layers) - 1)])
        self.out = nn.Linear(layers[-1], c)
        self.drop = nn.Dropout(p)
        
    def forward(self, x):
        x = self.conv1(x)
        for l,l2,l3 in zip(self.layers, self.layers2, self.layers3):
            x = l3(l2(l(x)))
        x = F.adaptive_max_pool2d(x, 1)
        x = x.view(x.size(0), -1)
        x = self.drop(x)
        return F.log_softmax(self.out(x), dim=-1)
learn = ConvLearner.from_model_data(Resnet2([16, 32, 64, 128, 256], 10, 0.2), data)
wd=1e-6
%time learn.fit(1e-2, 2, wds=wd)
%time learn.fit(1e-2, 3, cycle_len=1, cycle_mult=2, wds=wd)
%time learn.fit(1e-2, 8, cycle_len=4, wds=wd)
log_preds,y = learn.TTA()
preds = np.mean(np.exp(log_preds),0)
metrics.log_loss(y,preds), accuracy(preds,y)
(0.44507397166057938, 0.84909999999999997)

85% было передовым в 2012 или 2013 годах для CIFAR 10. В настоящее время этот показатель составляет до 97%, так что есть возможности для улучшения, но все они основаны на следующих методах:

  • Лучшие подходы к увеличению данных
  • Лучшие подходы к регуляризации
  • Некоторые настройки ResNet

Вопрос [02:01:07]: Можем ли мы применить обучение по остаточному подходу к проблеме, не связанной с изображением? Да! Но везде это игнорировалось. В НЛП недавно появилась трансформаторная архитектура, которая, как было показано, является передовым уровнем перевода, и в ней есть простая структура ResNet. Этот общий подход называется пропустить соединение (то есть идея пропуска слоя) и часто встречается в компьютерном зрении, но, кажется, никто больше не использует его, даже несмотря на то, что в нем нет ничего специфического для компьютерного зрения. Хорошая возможность!

Собаки против кошек [02:02:03]

Возвращаемся собак и кошек. Создадим resnet34 (если интересно, что означает конечный номер, см. Здесь - просто другие параметры).

PATH = "data/dogscats/"
sz = 224
arch = resnet34  # <-- Name of the function 
bs = 64
m = arch(pretrained=True) # Get a model w/ pre-trained weight loaded
m
ResNet(
  (conv1): Conv2d (3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
  (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True)
  (relu): ReLU(inplace)
  (maxpool): MaxPool2d(kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), dilation=(1, 1))
  (layer1): Sequential(
    (0): BasicBlock(
      (conv1): Conv2d (64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True)
      (relu): ReLU(inplace)
      (conv2): Conv2d (64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True)
    )
    (1): BasicBlock(
      (conv1): Conv2d (64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True)
      (relu): ReLU(inplace)
      (conv2): Conv2d (64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True)
    )
    (2): BasicBlock(
      (conv1): Conv2d (64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True)
      (relu): ReLU(inplace)
      (conv2): Conv2d (64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True)
    )
  )
  (layer2): Sequential(
    (0): BasicBlock(
      (conv1): Conv2d (64, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True)
      (relu): ReLU(inplace)
      (conv2): Conv2d (128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True)
      (downsample): Sequential(
        (0): Conv2d (64, 128, kernel_size=(1, 1), stride=(2, 2), bias=False)
        (1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True)
      )
    )
    (1): BasicBlock(
      (conv1): Conv2d (128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True)
      (relu): ReLU(inplace)
      (conv2): Conv2d (128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True)
    )
    (2): BasicBlock(
      (conv1): Conv2d (128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True)
      (relu): ReLU(inplace)
      (conv2): Conv2d (128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True)
    )
    (3): BasicBlock(
      (conv1): Conv2d (128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True)
      (relu): ReLU(inplace)
      (conv2): Conv2d (128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
      (bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True)
    )
  )
  ...
  (avgpool): AvgPool2d(kernel_size=7, stride=7, padding=0, ceil_mode=False, count_include_pad=True)
  (fc): Linear(in_features=512, out_features=1000)
)

В нашей модели ResNet было Relu → BatchNorm. TorchVision выполняет BatchNorm → Relu. Существует три различных версии ResNet, лучшая из которых - PreAct (https://arxiv.org/pdf/1603.05027.pdf).

  • В настоящее время последний слой имеет тысячи функций, потому что ImageNet имеет 1000 функций, поэтому нам нужно избавиться от него.
  • Когда вы используете ConvLearner fast.ai, он удаляет два последних слоя за вас. fast.ai заменяет AvgPool2d на Adaptive Average Pooling и Adaptive Max Pooling и объединяет их вместе.
  • Для этого упражнения мы сделаем простую версию.
m = nn.Sequential(*children(m)[:-2], 
                  nn.Conv2d(512, 2, 3, padding=1), 
                  nn.AdaptiveAvgPool2d(1), Flatten(), 
                  nn.LogSoftmax())
  • Удаляем последние два слоя
  • Добавьте свертку, у которой всего 2 выхода.
  • Сделайте средний пул, затем softmax
  • В конце нет линейного слоя. Это другой способ получения всего двух чисел, который позволяет нам использовать CAM!
tfms = tfms_from_model(arch, sz, aug_tfms=transforms_side_on, max_zoom=1.1)
data = ImageClassifierData.from_paths(PATH, tfms=tfms, bs=bs)
learn = ConvLearner.from_model_data(m, data)
learn.freeze_to(-4)
learn.fit(0.01, 1)
learn.fit(0.01, 1, cycle_len=1)
  • ConvLearner.from_model - это то, о чем мы узнали ранее - позволяет нам создавать объект Learner с пользовательской моделью.
  • Затем заморозьте слой, кроме тех, которые мы только что добавили.

Карты активации классов (CAM) [02:08:55]

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

Как это получилось? Давайте работать в обратном направлении. Он сделал это, создав эту матрицу:

Большие числа соответствуют коту. Так что это за матрица? Эта матрица просто равна значению матрицы признаков feat умноженное на py вектор:

f2=np.dot(np.rollaxis(feat,0,3), py)
f2-=f2.min()
f2/=f2.max()
f2

py вектор - это прогноз, в котором говорится: «Я на 100% уверен, что это кошка». feat - это значения (2 × 7 × 7), выходящие из последнего сверточного слоя (слой Conv2d, который мы добавили). Если мы умножим feat на py, мы получим весь первый канал и ни один из второго канала. Следовательно, он вернет значение последних сверточных слоев для секции, которая совпадает с тем, что это кошка. Другими словами, если мы умножим feat на [0, 1], получится собака.

sf = SaveFeatures(m[-4])
py = m(Variable(x.cuda()))
sf.remove()

py = np.exp(to_np(py)[0]); py
array([ 1.,  0.], dtype=float32)
feat = np.maximum(0, sf.features[0])
feat.shape

Другими словами, в модели единственное, что происходило после сверточного слоя, - это средний слой объединения. Средний слой объединения взял сетку 7 на 7 и усреднил, насколько каждая часть «похожа на кошку». Затем мы взяли матрицу «кошачьего», изменили ее размер до того же размера, что и исходное изображение кошки, и наложили ее поверх, после чего вы получили тепловую карту.

Вы можете использовать эту технику дома следующим образом:

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

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

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

class SaveFeatures():
    features=None
    def __init__(self, m): 
        self.hook = m.register_forward_hook(self.hook_fn)
    def hook_fn(self, module, input, output): 
        self.features = to_np(output)
    def remove(self): self.hook.remove()

Вопросы Джереми [02:14:27]: Ваш путь к глубокому обучению и Как не отставать от важных исследований для практиков

«Если вы собираетесь перейти к Части 2, ожидается, что вы овладеете всеми техниками, которым он научился в Части 1». Вот что вы можете сделать:

  1. Просмотрите каждое видео не менее 3 раз.
  2. Убедитесь, что вы можете воссоздать записные книжки, не просматривая видео - возможно, сделайте это с другими наборами данных, чтобы сделать их более интересными.
  3. Следите на форуме за свежими статьями, последними достижениями.
  4. Будьте настойчивы и продолжайте работать над этим!

Уроки: 12345678910 11 12 13 14