Как проводить исследования

02a_why_sqrt5.ipynb

Джереми начал урок с демонстрации того, как он проводит исследования. Проблема, которую он пытался найти, заключалась в том, почему PyTorch использует квадратный корень из 5 в качестве инициализации Kaiming. Никаких комментариев по поводу выбора не было.

  • Создавайте функции, если вы часто чем-то пользуетесь. Джереми создал функцию под названием stats (x), которая возвращает среднее значение и стандартное отклонение входных данных.

Сначала он начал с получения информации. Затем он сделал собственную версию и протестировал ее на версии Pytorch. Идея заключалась в том, чтобы увидеть, чем отличаются эти подходы. Он также проверил, как разные числа влияют на результаты инициализации Kaiming. Я сразу заметил одну вещь: Джереми не просто читал что-то откуда-то, а сам проверял их. Например, легко определить стандартное отклонение равномерного распределения, но Джереми хотел найти его, просто сделав случайные числа и вычислив стандартное отклонение. Его ноутбук был полон этих однострочных тестов.

Результаты: Джереми протестировал код PyTorch с использованием четырехслойной свертки и обнаружил, что на последнем слое стандартное отклонение составило 0,0060, что является проблемой. Это проблема, потому что он сильно отличается от предыдущих слоев. То же самое произошло, когда он сделал обратное распространение и напечатал стандартное отклонение первого слоя. Когда он заменил версию PyTorch своей собственной, стандартное отклонение составило 0,2445. Это не 1, а ближе, чем версия PyTorch.

02_initializing.ipynb
Этот блокнот короткий, но важный. Это показывает, что если мы умножаем числа, инициализированные с помощью нормального распределения (среднее значение = 0; стандартное значение = 1), даже 28 раз, они становятся nan, что означает, что они слишком большие. Что, если мы умножим веса на 0,01? Тогда проблема в том, что числа становятся нулями. Ответ - разделить на квадратный корень из размера столбца матрицы. Таким образом числа остаются в некотором диапазоне. Хотя не многие люди знакомы с этим, это одна из самых важных вещей в глубоком обучении.

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

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

03_minibatch_training.ipynb

Функция потерь

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

def log_softmax(x): 
    return (x.exp()/(x.exp().sum(-1,keepdim=True))).log()

Мы используем log softmax вместо normal, потому что этого требует PyTorch.

Как теперь реализовать это на PyTorch?

Ранее мы использовали эту функцию:

def log_softmax(x): 
    return (x.exp()/(x.exp().sum(-1,keepdim=True))).log()

def log_softmax(x): return x - x.exp().sum(-1,keepdim=True).log()

Идея трюка LogSumExp заключается в том, что вы можете вычесть x из некоторого числа, а затем добавить его обратно. Когда мы найдем максимальное входное значение, деление на него гарантирует, что числа не станут слишком большими.

def logsumexp(x):
    m = x.max(-1)[0]
    return m + (x-m[:,None]).exp().sum(-1).log()
def log_softmax(x): return x - x.logsumexp(-1,keepdim=True)

Наконец, мы замечаем, что nll(log_softmax(pred),y_train) = F.cross_entropy(pred, y_train)

Базовый цикл обучения

def accuracy(out, yb): 
    return (torch.argmax(out, dim=1)==yb).float().mean()

Цикл обучения выглядит так:

  • Рассчитывать прогнозы
  • Рассчитать убыток
  • Обратный проход
  • Вычесть скорость обучения с градиентами
  • Нулевые градиенты

Обратите внимание на несколько моментов!
Нам нужно использовать torch.no_grad() при обновлении весов, потому что мы не хотим, чтобы они влияли на градиенты. После этой строки кода мы идем по всем слоям один за другим и обновляем веса. hasattr(l,'weight') проверит, есть ли на этом слое параметр веса.

Затем мы проводим некоторый рефакторинг, чтобы код лучше подходил для решения других задач.

У нас еще нет функции model.parameters (), поэтому нам нужно ее создать.

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

PyTorch уже реализовал эту функцию, и мы можем использовать ее, установив nn.Module в качестве родительского класса.

Обратите внимание, что super().__init__() важен, потому что он будет делать родительский класс __init__, который создаст словарь, который мы хотели.

Все это уже сделано в PyTorch и называется nn.Sequential().

model = nn.Sequential(nn.Linear(m,nh), nn.ReLU(), nn.Linear(nh,10))

Оптим

Джереми хотел скрыть эту часть кода, создав класс Optimizer.

Версия PyTorch называется optim.SGD.step

Набор данных

DataLoader

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

PyTorch DataLoader делает то же самое.

В большинстве случаев нам не нужен собственный сэмплер или функция сопоставления, поэтому мы можем просто использовать значения по умолчанию, установив shuffle=true

Одна вещь, которую мы не реализовали, - это num_workers. Это просто означает, что данные обрабатываются таким образом, что они могут использовать несколько ядер ЦП.

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

model.train() и model.eval() выглядят сложными, но только для параметра training внутри объекта модели устанавливается значение true или false. Зачем нам это нужно? Некоторые уровни ведут себя по-разному, будь то обучение или тестирование. Например, dropout не отбрасывает никаких значений, когда мы выполняем проверку, потому что это было бы просто глупо.

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

Вопрос: Зачем нам нужно обнулять градиенты в PyTorch? Если мы не обнуляем градиенты в каждом цикле, он добавит новые градиенты к старым. Тогда почему PyTorch не может автоматически обнулить оценки? Это потому, что иногда мы хотим использовать несколько разных модулей, и если бы PyTorch автоматически обнулял градиенты, мы не могли бы этого сделать. Одна из важных реализаций этого состоит в том, что мы можем использовать пакеты большего размера, чем обычно может использовать наш компьютер. Например, если ваш компьютер может запускать определенную модель с размером пакета 32, используя это, он может запускать любой большой размер партии. Это не быстрее, но обновляет веса так же, как и при увеличении размера партии. Например, если мы используем размер пакета 32 и обнуляем градиенты только в каждом втором цикле, он обновляет веса так же, как и размер пакета 64.

Теперь у нас есть отличный обучающий цикл, но есть еще кое-что, что мы можем сделать:

  • Более красивое отображение потерь и показателей
  • Планирование гиперпараметров
  • Добавить методы регуляризации
  • И многое другое…

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

Обратные вызовы

04_callbacks.ipynb

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

Давайте посмотрим на другую проблему, которая у нас есть. Вот как выглядит наша текущая функция соответствия:

fit(epochs, model, loss_func, opt, train_dl, valid_dl)

Джереми отметил, что иметь такое количество параметров сложно, и было бы проще сохранить все их в одном объекте. Вот так мы хотим, чтобы наша функция соответствия выглядела так:

fit(1, learn)

Итак, мы хотим создать обучающий объект, который будет содержать всю эту информацию. Начнем с создания DataBunch для хранения файлов DataLoader.

Затем давайте создадим функцию для создания модели, и у нас будет хороший класс, который мы называем Learner.

Тогда давайте добавим обратные вызовы.

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

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

Теперь мы можем создать тест, который покажет, как это работает.

Это запускает 10 пакетов данных.

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

class ValidateEveryOtherTime(Callback):
    
    def begin_validate(self):
        return self.epoch % 2 == 0

Если это возвращает false, это означает, что в исходном коде…

if cb.begin_validate():
            print("validate")
            with torch.no_grad(): all_batches(learn.data.valid_dl, cb)

… Cb.begin_validate также будет ложным, и код внутри оператора if не будет выполнен.

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

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

Большая часть того, через что проходит Джереми, не так уж и важна. Я рекомендую хотя бы раз прочитать блокнот обратных вызовов и попытаться понять, как он работает. Джереми сказал, что это для людей, интересующихся разработкой программного обеспечения. Лично мне это трудно понять, потому что здесь очень много движущихся частей. У вас есть обработчик обратного вызова, обратные вызовы, а затем вам нужно сделать другие вещи. Я мог бы написать об этом учебник, потому что мне интересно это понять. Я хочу поделиться одной хорошей уловкой - это распечатать эти коды. Иногда трудно увидеть что-то на компьютере, когда часть кода находится за пределами экрана. Приклейте весь этот код на стену и возьмите маркер или что-то в этом роде, чтобы вы могли выделить наиболее важные части и, возможно, добавить несколько заметок. Вот как я понимаю сложные коды.

Еще примеры: https://docs.fast.ai/callbacks.html

05_anneal.ipynb

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

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





Примечания к уроку 8
Примечания к уроку 9
Примечания к уроку 10
Примечания к уроку 11
Примечания к 12 уроку
Заметки к 13 уроку
Заметки к 14 уроку

~ Ланкинен