Заметки из Практического глубокого обучения для программистов, 2019 г., урок 5 (часть 1)

Другие уроки: Урок 1 / Урок 2 / Урок 3 / Урок 4 / Урок 6 / Урок 7

Быстрые ссылки: Страница курса Fast.ai / Лекция / Jupyter Notebooks

Повторяя некоторые понятия из прошлой лекции — помните, что функции активации являются поэлементными. Функция применяется к каждому элементу на входе.

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

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

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

Тонкая настройка

Что произойдет, если мы возьмем resnet34 и перенесем обучение?

Resnet34 от ImageNet имеет конечную весовую матрицу из 1000 столбцов. Потому что задачей ImageNet было классифицировать изображения по одному из 1000 классов (вероятность для каждого класса).

Когда вы проводите трансферное обучение, вам не всегда может понадобиться 1000 или, может быть, не одни и те же классы. Итак, мы выбросим матрицу весов. Так что create_cnn() фактически удаляет это. Вместо этого он вводит две новые матрицы весов с ReLU между ними.

Вторая матрица настолько бита, насколько вы хотите. Если вы занимаетесь классификацией, важно, сколько у вас классов.

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

Поэтому мы применим freeze() ко всем остальным слоям. Мы просим fast.ai и pytorch НЕ распространять градиенты обратно в эти слои (параметры = параметры — скорость обучения * градиент). Обновляйте только новые слои. Это ускорит работу из-за меньшего количества вычислений, займет меньше памяти, но самое главное, это не изменит веса, которые лучше, чем случайные.

ПОСЛЕ обучения новых слоев мы unfreeze() тренируем все это целиком. Но самые новые слои по-прежнему будут нуждаться в большем обучении, чем те, которые были в начале. Поэтому мы разделяем модель на несколько разделов и задаем разным частям модели разные скорости обучения. Одна часть (ранее) могла иметь 1e-5, другая часть (позже) могла иметь 1e-3. Следует также отметить, что если модель уже работает достаточно хорошо, высокая скорость обучения может сделать ее менее точной. Этот процесс называется различительной скоростью обучения.

Каждый раз, когда у вас есть функция fit(), вы можете передать скорость обучения. Это может быть одно число, например 1e-3 (все слои получают одинаковую скорость обучения), или вы можете написать срез, например slice(1e-3), с одним числом (означает, что конечные слои получают скорость обучения, но все остальные слои получают 1e-3/3), или

2 числа, такие как slice(1e-5, 1e-3) (означает, что последние слои получают 1e-3, но первые слои получают 1e-5, а все остальные слои между ними получают скорости обучения, которые поровну распределяются между ними). Мы даем разную скорость обучения для каждой группы слоев.

Возвращаясь к листу Excel из прошлого урока, вот результаты после запуска решателя:

Среднеквадратическая ошибка составляет 0,39, что означает, что для предсказаний рейтингов фильмов в диапазоне от 0 до 5 ошибка составляет 0,39.

Вложение матриц

Давайте отложим предыдущий лист в сторону и посмотрим на другой. Мы копируем весовые матрицы из предыдущего рабочего листа.

Горячее кодирование

Для каждого рейтинга есть индекс, идентификатор пользователя и весовая матрица из 5 весов.

То же самое с фильмами:

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

Теперь мы собираемся заменить идентификатор пользователя 1 этим вектором. У нас 15 пользователей. Пользователь № 1 будет иметь 1 в первом столбце и 0 в оставшихся 14. Пользователь № 2 будет иметь 1 во втором столбце и 0 во всех остальных.

То же самое с фильмами. Фильм № 14 будет иметь 1 в 14-м столбце и 0 в других местах. Общие данные выглядят так:

Таким образом, первая строка показывает, что пользователь № 1 дал оценку фильму № 14, вторая строка показывает, что пользователь № 2 дал оценку фильму № 14 и т. д.

Это форма предварительной обработки ввода.

Теперь, чтобы получить активацию пользователей в середине: мы возьмем входную матрицу пользователей и умножим на матрицу весов. Это работает, потому что входная пользовательская матрица имеет 15 столбцов, а весовая матрица имеет 15 строк и 5 столбцов (1x15 на 15x5). Результирующая матрица 1 x 5 соответствует каждой строке столбца активации пользователя.

Мы делаем то же самое для фильмов:

Наконец, мы умножаем каждый фильм или пользователя на активации и получаем прогнозируемый рейтинг. Это просто скалярное произведение матрицы Movie с матрицей активации Movie.

Затем мы можем найти квадрат потерь для каждого прогноза и среднюю потерю, которая составляет 0,39, которую мы видели ранее.

Окончательная версия:

Это те же весовые матрицы, тот же userId, movieId и отображение рейтинга.

Но на этот раз у нас есть встраивание пользователя, которое представляет собой активацию, сопоставленную с соответствующим индексом пользователя (т. е. индекс пользователя 1 всегда имеет вложения [0,21, 1,61, 2,89, -1,26, 0,82]), без одного -горячее кодирование с 1 и 14 нулями. Этот подход использует поиск по массиву вместо прямого кодирования. Потому что матричное умножение является разреженным (большинство нулей) в случае горячего кодирования.

Поиск чего-либо в массиве математически идентичен произведению матричного произведения на матрицу с горячим кодированием.

Предвзятость

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

Battlefield Earth никому не понравится. Это не очень хороший фильм, несмотря на то, что в нем есть Джон Траволта. Итак, как мы собираемся справиться с этим? Потому что есть функция под названием «Мне нравятся фильмы с Джоном Траволтой», а в этой функции под названием «В этом фильме» есть Джон Траволта, и теперь это похоже на то, что вам понравится этот фильм. Но нам нужно приберечь какой-то способ сказать «если это не Battlefield Earth» или «вы саентолог» — и то, и другое. Итак, как мы это делаем? Нам нужно добавить предвзятости.

У нас есть те же данные, но мы собираемся добавить дополнительную строку, которая представляет смещение. Теперь у каждого фильма может быть общее «это отличный фильм» или «это не отличный фильм». Так что в поле для скалярного произведения тоже будет смещение.

В результате MSE составляет 0,32, что меньше предыдущего значения 0,39. Это немного лучшая модель (дает нам больше гибкости), которая дает лучший результат.

Кинолинза 100k

Настройка данных для этого раздела ноутбука jupyter: необходимо загрузить набор данных с http://files.grouplens.org/datasets/movielens/ml-100k.zip в папку /home/jupyter/.fastai/data/

Можно сделать это через терминал ssh’d в виртуальную машину GCP.

pd.reads_csv() содержит такие параметры, как разделитель, кодировка и т. д. для этого конкретного набора данных.

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

Мы используем CollabDataBunch для набора данных. Объекты DataBunch поддерживают show_batch(), поэтому вы можете проверить данные после загрузки.

data = CollabDataBunch.from_df(rating_movie, seed=42, valid_pct=0.1, item_name=title)

Установка y_range — это уловка, которую мы можем использовать для управления диапазоном вывода, и мы хотим, чтобы он был от 0 до 5,5. Это может помочь нейронной сети делать прогнозы в правильном диапазоне. Поскольку сигмоиды имеют асимптоту на обоих концах диапазона, мы хотим, чтобы минимум был немного меньше фактического минимума, а максимум — немного больше. Отсюда 0–5,5

wd или уменьшение веса — еще один трюк для повышения точности.

Параметр n_factors — это ширина матрицы внедрения.

learn = collab_learner(data, n_factors=40, y_range=y_range, wd=1e-1)

Как обычно, используйте процесс поиска скорости обучения и используйте его для fit_one_cycle:

learn.lr_find()
learn.recorder.plot(skip_end=15)

Первый параметр для fit_one_cycle — это количество эпох. Второй означает, что мы используем скорость обучения 5e-3 для всех слоев.

learn.fit_one_cycle(5, 5e-3)

Мы получаем MSE 0,81, что довольно хорошо, учитывая эталонное значение 0,83.

Сохраните модель с помощью learn.save('dotprod')

Как сделать прогнозы менее предвзятыми?

Давайте выберем некоторые популярные фильмы на основе подсчета рейтинга:

g = rating_movie.groupby(title)['rating'].count()
top_movies = g.sort_values(ascending=False).index.values[:1000]
top_movies[:10]

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

Предвзятость фильма

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

movie_bias = learn.bias(top_movies, is_item=True)
movie_bias.shape

В совместной фильтрации большинство вещей — это пользователи или элементы.

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

Вышеуказанные фильмы имеют самый низкий рейтинг. Если мы сделаем reverse=True, мы сможем получить фильмы с самым высоким рейтингом.

Мы также можем взять веса в дополнение к смещениям.

movie_w = learn.weight(top_movies, is_item=True)
movie_w.shape

Мы собираемся взять веса для предметов (также известных как фильмы). Мы запросили ширину 40 назад, когда определяли n_factors.

40 — это многовато, поэтому мы уменьшим его до 3.

movie_pca = movie_w.pca(3)
movie_pca.shape

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

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

Итак, давайте посмотрим на фильмы, отсортированные по фактору 0 (fac0).

Фильмы с самым высоким рейтингом относятся к категории знатоков.

По фактору 1 (fac1):

Кажется, это большие хиты, которые можно смотреть всей семьей.

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

Есть еще один параметр collab_learner для обсуждения: wd или уменьшение веса.

learn = collab_learner(data, n_factors=40, y_range=y_range, wd=1e-1)

Снижение веса - это тип регуляризации:

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

Просуммируем квадраты параметров. Мы создаем модель, где в функции потерь у нас есть квадраты параметров. Но чтобы квадраты параметров не стали слишком большими, мы умножим их на какое-то выбранное нами число. Это число wd. Мы возьмем нашу функцию потерь и добавим к ней сумму квадратов параметров, умноженных на некоторое число wd. Как правило, он должен быть 0,1.

Как рассчитываются веса: вес на момент времени t — это вес на момент времени t-1 минус скорость обучения, умноженная на производную функции потерь по отношению к весам на момент времени t-1.

Какова наша потеря? Наши потери являются некоторой функцией наших независимых переменных x и наших весов. Мы используем функцию потерь MSE, которая получает разницу между прогнозами (y_hat) и метками (y).

И наши прогнозы y_hat генерируются в результате запуска некоторой модели m на входных данных (x) и весах (w).

Теперь мы собираемся добавить уменьшение веса wd (0,1), умноженное на сумму квадратов весов.

МНИСТ СГД

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

Покажите изображение и форму:

Есть 50 000 строк и 784 столбца. Каждый столбец представляет собой изображение размером 28x28 пикселей. Поэтому, если мы изменим форму одного из них и построим его, мы увидим, что это число.

В настоящее время это пустые массивы, но нам нужно, чтобы они были тензорами, поэтому мы просто используем map(torch.tensor)

x_train,y_train,x_valid,y_valid = map(torch.tensor (x_train,y_train,x_valid,y_valid))
n,c = x_train.shape
x_train.shape, y_train.min(), y_train.max()

Получаем: (torch.Size([50000, 784]), tensor(0), tensor(9))

В уроке 2-sgd мы создали столбец из единиц, чтобы добавить смещения, но на этот раз нам не нужно этого делать. С этим справится Pytorch. Мы также написали собственную функцию mse() и процедуру умножения матриц, но теперь все это будет обрабатывать pytorch. И для обработки мини-партий.

Мы создадим модель логистической регрессии, которая подклассы nn.Module

class Mnist_Logistic(nn.Module):
    def __init__(self):
        super().__init__()
        self.lin = nn.Linear(784, 10, bias=True)
    def forward(self, xb): return self.lin(xb)

Однослойная нейронная сеть без скрытых слоев (линейностей). Мы хотим поместить матрицы весов, что делается с помощью cuda()

Наша модель создана! Мы можем получить форму всех параметров нашей модели с помощью

[p.shape for p in model.parameters()]

Так что же это за два параметра?

[10,784] — это то, что будет принимать 784-мерные входные данные и выдавать 10-мерные выходные данные. Наши входные данные имеют размерность 784, и нам нужно что-то, что может дать нам вероятности для 10 выходов.

Затем нам нужно 10 активаций, к которым мы хотим добавить смещение. Итак, у нас есть второй вектор длины 10.

В модели есть именно то, что нам нужно для нашего ax+b.

Мы возьмем скорость обучения lr=2e-2 и функцию потерь CrossEntropyLoss.

В нашей функции обновления мы будем вызывать нашу model(x) вместо a@x из урока 2, как если бы это была функция, чтобы получить нашу y_hat

def update(x,y,lr):
    wd = 1e-5
    y_hat = model(x)
    # weight decay
    w2 = 0.
    for p in model.parameters(): w2 += (p**2).sum()
    # add to regular loss
    loss = loss_func(y_hat, y) + w2*wd
    loss.backward()
    with torch.no_grad():
        for p in model.parameters():
            p.sub_(lr * p.grad)
            p.grad.zero_()
    return loss.item()

Мы вызываем наш loss_func(), чтобы получить наши потери, и мы можем перебрать параметры.

У нас также есть w2. Для каждого p в model.parameters мы добавляем к w2 сумму p**2, которая является суммой квадратов весов, и умножаем ее на wd, что равно 1e-5.

Таким образом, снижение веса на самом деле простое значение.

Запустите функцию обновления с пониманием списка данных:

losses = [update(x,y,lr) for x,y in data.train_dl]

Обобщая, можно сказать, что градиент wd*(w**2) по отношению к w равен всего лишь 2wd*w. Мы можем отбросить 2, не теряя общности

Все, что делает wd, — это вычитание весов в несколько постоянных раз каждый раз, когда мы делаем пакет. Вот почему это называется снижением веса!

Регуляризация L2 (wd * w²) и уменьшение веса wd * w*)* в значительной степени математически идентичны.

Мы можем заменить Mnist_Logistic на Mnist_NN и построить нейронную сеть с нуля.

class Mnist_NN(nn.Module):
    def __init__(self):
        super().__init__()
        # use 2 linear layers
        # first layer has output of 50
        self.lin1 = nn.Linear(784, 50, bias=True)
        # second layer has input of 50 and output of 10 (since it's the number of classes we're predicting)
        self.lin2 = nn.Linear(50, 10, bias=True)
    def forward(self, xb):
        # first layer
        x = self.lin1(xb)
        # calculate relu
        x = F.relu(x)
        return self.lin2(x) # second layer

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

def update(x,y,lr):
    # take model.parameters() and optimize them using Adam (can also use SGD)
    opt = optim.Adam(model.parameters(), lr) # you can also pass in a wd
    y_hat = model(x)
    loss = loss_func(y_hat, y)
    loss.backward()
    opt.step()
    opt.zero_grad()
    return loss.item()

Если сменить оптимизатор, потери будут расходиться.

Оптимизаторы: Адам, SGD, RMSProp.

Это случайно сгенерированные X и Y.

y = ax + b, где a равно 2, а b равно 30

Начните с произвольного выбора точки пересечения (b) и наклона (a).

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

Затем мы копируем эту точку пересечения и этот наклон в следующую строку и делаем это снова. И делать это много раз, и в конце мы сделали одну эпоху.

# regular SGD
opt = optim.Adam(model.parameters(), lr)
# with momentum
opt = optim.SGD(model.parameters(), lr, momentum=0.9)

Мы можем использовать Adam или SGD, что позволяет вам применить импульс (взять производную, умножить на 0,1, затем взять предыдущее обновление и умножить на 0,9 и сложить их вместе)

Моментум 0,9 очень распространен

Экспоненциально взвешенное скользящее среднее: взвешивание количества наблюдений и использование их среднего значения.

Шаг во время t (S_t) равен некоторому числу, умноженному на фактический градиент, плюс [1 — альфа], умноженному на то, что вы имели в прошлый раз в S_t-1.

RMSProp: очень похоже на импульс, но вместо этого это экспоненциально взвешенное скользящее среднее не обновлений градиента, а F8 в квадрате — это квадрат градиента.

Адам отслеживает экспоненциально взвешенное скользящее среднее квадрата градиента (RMSProp), а также отслеживает экспоненциально взвешенное скользящее среднее моих шагов (импульс).