Всем привет!

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

Основные шаги по созданию классификатора машинного обучения из курса FastAI следующие:

  1. Инициализировать веса
  2. Сделать прогноз
  3. Рассчитайте, насколько хороша модель (т. е. потеря между фактическим и прогнозируемым)
  4. Рассчитать градиент
  5. Шаг! т.е. изменить веса на основе этого градиента
  6. Повтор 2–5
  7. Повторяйте, пока не решите остановить процесс (хорошая точность или достаточно эпох)

Итак, допустим, мы хотим построить модель машинного обучения для данных, которые следуют линейной функции y = 3x + 20. Компьютер может иметь базовое представление о том, как выглядит линейная функция y = mx + b, но он понятия не имеет, что значение m или b должно быть для наших конкретных данных. Так…

Шаг первый: мы можем настроить его так, чтобы он запускался со случайными параметрами, то есть с весами, и позволить ему подстраиваться под правильные параметры. Хорошо, допустим, наше первое предположение: m = 10 и b = 90.

Шаг второй: сделайте прогноз. Итак, для наших фактических данных, если x = 1, y = 23, но с нашим предположением, если x = 1, y = 100. Большая разница! Как бы мы сказали компьютеру, что это неправильно?

Шаг третий: мы просто вычитаем разницу. 100–23 = 77! Вот насколько ошибочна наша модель. Это наша функция потерь. Но в идеале нам нужна функция потерь, которая может суммировать все потери и не позволяет положительным и отрицательным потерям компенсировать друг друга.

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

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

Шаг четвертый: Итак, теперь, когда он знает, насколько он неправ, как он может стать лучше (🥺)? Мы используем концепцию градиентного спуска!

Я коснулся этого в предыдущем посте, но это просто означает, что у нас есть график, отображающий различные значения потерь для параметра. В идеале мы хотим получить минимальное значение потерь, но как мы можем найти путь к этому? Мы корректируем параметр так, чтобы спускаться вниз по склону (градиент) понемногу, но не слишком мало, чтобы добраться до него целую вечность, и не слишком сильно, чтобы не пропустить точку минимума. . Термин, который позволяет нам регулировать скорость спуска на дно, называется скорость обучения, и в этом руководстве мы будем использовать скорость 1e-5 и 1e-1. К счастью, у нас также есть способ автоматически вычислять градиенты, не выполняя вычисления самостоятельно. Диаграмма ниже также иллюстрирует концепцию функций потерь и скорости обучения.

Шаг пятый: Итак, модель сделала шаг и настроила параметры на m = 8 и b = 40, используя градиентный спуск. Что теперь?

Шаг шестой: Повторите шаги 2–5. Вернитесь назад и предскажите новое значение, рассчитайте потери, рассчитайте градиент, сделайте шаг и вернитесь, чтобы повторить.

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

Легкий, лимонный сок! Странное выражение, кстати. В любом случае, давайте создадим модель, которая может выполнять шаги 1–7 для некоторых данных, которые мы будем предоставлять.

Создание наших данных

Как обычно импортируем все нужные нам библиотеки:

!pip install fastai -q --upgrade
from fastai.basics import *
from fastai.vision.all import *
from fastai.callback.all import *

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

x = torch.arange(0,20,0.2).float()
y = (3*x + 20) + torch.randint(1,20,(100,))
plt.scatter(x,y)

Torch.arange возвращает одномерный тензор, содержащий диапазон чисел от 0 до 20 с шагом 0,2, т. е. 0, 0,2, 0,4, 0,6 и т. д. Для меня тензор похож на причудливое и более общее слово для обозначения матрицы, с возможностью обработки нескольких измерений, то есть вместо матрицы 2 x 2 у нас может быть тензор 3 x 2 x 2. В этом случае мы просто вызываем диапазон чисел, так что ничего многомерного.

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

Вторая строка кода выполняет функцию y = 3x + 20 для всех чисел, но я также добавляю torch.randint (1,20,(100,)) так что наши данные не на 100% идеальны, что в реале мир, это редко когда-либо.

Мой torch.randint выбирает случайные целые числа от 1 до 20 и создает тензор размера 100. Это размер 100, потому что у нас на самом деле есть 100 значений x (помните, что между каждым числом 0,2 шага). Таким образом, если вы хотите изменить свой код, чтобы он имел значения x от 0 до 10 с шагом 0,5, вместо этого вы должны использовать torch.randint(1,20,(20,)).

Последняя строка кода просто отображает наши x и y на точечной диаграмме, и вуаля!

Определение нашей функции

def linear (x, params):
    m,b = params
    return m*x + b

Эти строки кода определяют линейную функцию, для которой модель пытается определить параметры. Я назвал функцию «линейной», а аргументы — это x и params, где params — это m и b, которые она будет вычислять. Наконец, он возвращает свой прогноз для y (mx + b).

def rmse_loss(preds, targets):
 return ((preds-targets)**2).mean().sqrt()

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

Итак, давайте сделаем наше первое предположение (шаг первый)!

params = torch.randn(2).requires_grad_()

Здесь я создал тензор из двух чисел из случайного нормального распределения (подробнее см. Здесь о разнице между случайным равномерным распределением и случайным нормальным распределением). Я также назвал метод .requires_grad_(), чтобы он знал, что мне понадобятся их градиенты в будущем. Вот мои случайные значения для m и b:

Далее следует прогноз (шаг второй) путем вызова функции для наших значений x с использованием наших предполагаемых параметров!

preds = linear(x,params)

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

def show_preds(preds, ax=None):
 if ax is None: ax=plt.subplots()[1]
 ax.scatter(x, y)
 ax.scatter(x, to_np(preds), color=’red’)
 ax.set_ylim(-30,100)
show_preds(preds)

Мои прогнозы (красный) даже близко не соответствуют реальным значениям (синий)!

Давайте оценим эту разницу, вызвав созданную нами функцию потерь (шаг третий):

loss = rmse_loss(preds, y)

У меня убыток 48,4877. Также совершенно нормально иметь намного ниже или намного выше, чем у меня. Помните, это всего лишь первое предположение.

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

lr = 1e-5
loss.backward()
params.data -= lr* params.grad.data
params.grad = None

Первая строка кода вычисляет градиент потерь в зависимости от параметра и сохраняет значения в данных градиента (params.grad.data). Затем мы корректируем данные наших параметров, уменьшая их значения на градиенты * скорость обучения. Наконец, мы устанавливаем наши градиенты равными нулю, потому что мы не хотим, чтобы градиенты следующего шага накапливались на этом шаге.

Шестой шаг включает в себя повторение шагов 2–5, поэтому давайте определим функцию, которая сделает это за нас:

def apply_step(params):
 preds = linear(x, params)
 loss = rmse_loss(preds, y)
 loss.backward()
 params.data -= lr* params.grad.data
 params.grad = None
 print(loss.item())
 return preds

Я добавил только две дополнительные строки в конце. Функция print (loss.item()) просто выводит потери для шага, а return preds возвращает/сохраняет сделанные прогнозы, что полезно на случай, если мы захотим отобразить их позже.

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

for i in range(20):
 apply_step(params)

Потеря на самом деле не сильно изменилась за 20 эпох, что означало бы, что у нас очень низкая скорость обучения. Мне было весело изменить скорость обучения примерно до 1e-1 (где я добился наибольшего успеха), а также запустить сотни и сотни эпох (сделано в одно мгновение, так как это простая функция). В конце концов, я смог получить более низкие потери 6,6, и вы можете запустить эту строку кода, чтобы взглянуть на свою окончательную строку прогноза:

show_preds(apply_step(params))

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

Также обратите внимание, что если вы планируете повторно запустить код и изменить скорость обучения после первого прохода, вам нужно выполнить loss.backward(retain_graph = True) вместо просто loss.backward(), потому что вы хотите сохранить графики что вы повторно запускаете градиентный спуск.

Вот полный вид сводного кода. Он также есть на моем GitHub.

!pip install fastai -q — upgrade
from fastai.basics import *
from fastai.vision.all import *
from fastai.callback.all import *
x = torch.arange(0,20,0.2).float()
y = (3*x + 20) + torch.randint(1,20,(100,))
#plt.scatter(x,y)
def linear (x, params):
 m,b = params
 return m*x + b
def rmse_loss(preds, actuals):
 return ((preds-actuals)**2).mean().sqrt()
params = torch.randn(2).requires_grad_()
lr = 1e-1 #try different learning rates
def apply_step(params):
 preds = linear(x, params)
 loss = rmse_loss(preds, y)
 loss.backward()
 params.data -= lr * params.grad.data
 params.grad = None
 #print(loss.item()) #feel free to uncomment this line if you don’t want to see the loss for all the epochs
 return preds
for i in range(300):
 apply_step(params) 
 
#You can use this to compare your predictions to the actual y values at the start
def show_preds(preds, ax=None):
 if ax is None: ax=plt.subplots()[1]
 ax.scatter(x, y)
 ax.scatter(x, to_np(preds), color=’red’)
 ax.set_ylim(-30,100) 
show_preds(apply_step(params))

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

А пока, выздоравливайте, мои друзья-программисты ❤