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

Уроки: 12345678910 11 12

Ядра Kaggle [1:22]

Добро пожаловать в машинное обучение! Безусловно, самым захватывающим событием на этой неделе является то, что Fast AI теперь находится на пике, так что вы можете pip install fastai:



Вероятно, все еще проще всего сделать conda env update, но в паре мест, где было бы удобно pip install fastai, было бы, если вы работаете вне репозитория в записных книжках, тогда это дает вам доступ к Fast AI повсюду. Также они отправили запрос на включение в Kaggle, чтобы попытаться добавить его в ядра Kaggle. Надеюсь, вы скоро сможете использовать его на ядрах Kaggle. Вы можете использовать его на работе или где угодно еще, так что это увлекательно. Я не скажу, что он официально выпущен. Очевидно, еще очень рано, и мы все еще добавляем (и вы помогаете добавлять) документацию и все такое. Но хорошо, что теперь это есть.

Победитель в прогнозе безопасного вождения в Порту-Сегуро [4:21]

Пара крутых ядер от студентов USF на этой неделе. Я подумал, что выделю два из них, оба из конкурса по нормализации текста, в котором пытались взять текст, который был написан стандартным английским текстом, есть еще один для русского. Вы пытаетесь определить вещи, которые могут быть первым, вторым, третьим, и сказать, что это кардинальное число, или это номер телефона, или что-то еще. Я быстро поискал и увидел, что в академических кругах предпринимались попытки использовать для этого глубокое обучение, но им не удалось добиться большого прогресса и они действительно заметили здесь ядро Альвиры, которое получает 0,992 балла за лидера. доска, которая, как мне кажется, находится в топ-20. Она полностью эвристична и является отличным примером разработки функций. В этом случае все это в основном полностью функциональная инженерия. По сути, он просматривает и использует множество регулярных выражений, чтобы выяснить для каждого токена, что это такое. И я думаю, что она проделала отличную работу, четко изложив все, что представляют собой разные части и как все они сочетаются друг с другом. И она упомянула, что, возможно, надеется превратить это в библиотеку, что, я думаю, было бы здорово. Вы можете использовать это, чтобы взять кусок текста и вытащить все, что в нем есть. Это то, что сообщество разработчиков естественного языка надеется обойтись без большого количества написанного вручную кода, подобного этому. Но пока мне было бы интересно узнать, что же сделали победители, но я не видел, чтобы машинное обучение использовалось для этого особенно хорошо. Возможно, лучший подход - это тот, который сочетает такую ​​разработку функций с некоторым машинным обучением. Но я думаю, что это отличный пример эффективной разработки функций.

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

MNIST продолжение [8:32]

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

Так что его техника для победы в этом заключалась в том, чтобы делать увеличение данных, о котором узнали те из вас, кто проходит курс глубокого обучения, что в основном является идеей, как если бы у вас были изображения, вы бы перевернули их по горизонтали или немного повернули. Увеличение данных означает создание новых примеров данных, которые немного отличаются от уже имеющихся у вас. И так, как он это делал, для каждой строки из данных он случайным образом заменял 15% переменных другой строкой. Таким образом, каждая строка теперь будет представлять собой смесь 85% исходной строки, 15% случайно выбранных из другой строки. Таким образом, это был способ немного случайным образом изменить данные, а затем он использовал так называемый автоэнкодер, который мы, вероятно, не будем изучать до второй части курса глубокого обучения, но основная идея автоэнкодера - это ваша зависимая переменная. то же, что и ваша независимая переменная. Другими словами, вы пытаетесь предсказать свой ввод, который, очевидно, тривиален, если вам разрешено преобразование идентичности, например, тривиально предсказывает ввод, но трюк с автокодировщиком состоит в том, чтобы иметь меньше активаций хотя бы в одном из ваших слоев, чем ваш вклад. Итак, если ваш ввод был стоимерным вектором, и вы пропустили его через матрицу 100 на 10, создайте десять активаций, а затем должны создать из него исходный 100-длинный вектор. Тогда вам нужно было эффективно сжать его. Получается, что такая нейронная сеть вынуждена находить корреляции, особенности и интересные взаимосвязи в данных, даже если они не помечены. Так что он использовал это вместо того, чтобы заниматься какой-либо ручной инженерией. Он просто использовал автоэнкодер. Вот несколько интересных направлений, о которых вы узнаете, если продолжите изучение машинного обучения, особенно если вы пройдете вторую часть курса глубокого обучения в следующем году. И вы можете увидеть, как уходит разработка функций, и это было всего час назад. Так что это действительно совсем недавно. Но это один из самых важных достижений, которые я видел за долгое время.

Реализация стохастического градиентного спуска [18:24]

"Ноутбук"

Этот (LogReg) был тем маленьким рукописным nn.Module классом, который мы создали [9:15]. Мы определили нашу потерю. Мы определили скорость обучения и определили оптимизатор. И optim.SGD - это то, что мы сейчас попробуем написать от руки. Итак, nn.NLLLoss и optim.SGD, мы воруем у PyTorch, но мы сами написали модуль LogReg и цикл обучения.

net2 = LogReg().cuda()
loss=nn.NLLLoss()
learning_rate = 1e-2
optimizer=optim.SGD(net2.parameters(), lr=learning_rate)
for epoch in range(1):
    losses=[]
    dl = iter(md.trn_dl)
    for t in range(len(dl)):
        # Forward pass: compute predicted y and loss by passing x to
        # the model.
        xt, yt = next(dl)
        y_pred = net2(V(xt))
        l = loss(y_pred, V(yt))
        losses.append(l)
        # Before the backward pass, use the optimizer object to zero
        # all of the gradients for the variables it will update
        # (which are the learnable weights of the model)
        optimizer.zero_grad()
        # Backward pass: compute gradient of the loss with respect
        # to model parameters
        l.backward()
        # Calling the step function on an Optimizer makes an update
        # to its parameters
        optimizer.step()
    
    val_dl = iter(md.val_dl)
    val_scores = [score(*next(val_dl)) for i in range(len(val_dl))]
    print(np.mean(val_scores))

Итак, основная идея заключалась в том, что мы собираемся пройти через некоторое количество эпох [9:39], поэтому давайте пройдем через одну эпоху. И мы собираемся отслеживать для каждой мини-партии, какие были потери, чтобы мы могли сообщить об этом в конце. Мы собираемся превратить наш загрузчик обучающих данных в итератор, чтобы мы могли его перебрать - перебрать каждый мини-пакет. Итак, теперь мы можем сказать for тензор in длину загрузчика данных, а затем мы можем вызвать next, чтобы получить следующую независимую переменную и зависимые переменные из этого итератора.

Итак, помните, что затем мы можем передать тензор x (xt) в нашу модель, вызвав модель, как если бы это была функция. Но прежде всего мы должны превратить его в переменную. На прошлой неделе мы набирали Variable(blah).cuda(), чтобы превратить его в переменную, сокращение для этого - просто заглавная буква V. Итак, заглавная T для тензора, заглавная V для переменной. Это просто ярлык в Fast AI. Итак, это возвращает наши прогнозы.

Следующее, что нам нужно было вычислить, - это рассчитать наши убытки, потому что мы не можем рассчитать производную убытков, если мы не рассчитали убыток [10:43]. Таким образом, потеря состоит из прогнозов и фактов. Фактические значения, опять же, представляют собой тензор y, и мы должны преобразовать его в переменную. Переменная отслеживает все шаги для вычисления. На веб-сайте PyTorch есть действительно фантастический учебник.

Следующее, что нам нужно было вычислить, - это рассчитать наши убытки, потому что мы не можем рассчитать производную убытков, если мы не рассчитали убыток [10:43]. Таким образом, потеря состоит из прогнозов и фактов. Фактические значения, опять же, представляют собой тензор y, и мы должны преобразовать его в переменную. Переменная отслеживает все шаги для вычисления. На веб-сайте PyTorch есть действительно фантастический учебник. На веб-сайте PyTorch есть учебный раздел и учебник по Autograd. Autograd - это название пакета автоматической дифференциации, который поставляется с PyTorch, и это реализация автоматической дифференциации. Таким образом, класс Variable действительно является здесь ключевым классом, потому что именно он превращает тензор в нечто такое, где мы можем отслеживать его градиенты. Итак, в основном здесь они показывают, как создать переменную, выполнить операцию с переменной, а затем вы можете вернуться и фактически посмотреть на функцию градиента (grad_fn), которая является функцией, которую он отслеживает для вычисления градиента. По мере того как мы выполняем все больше и больше операций с этой переменной и переменной, вычисляемой на основе этой переменной, он отслеживает ее. Так что позже мы можем пойти .backward(), а затем распечатать .grad и узнать градиент. Итак, вы заметили, что мы никогда не определяли градиент, мы просто определили его как (x + 2)² * 3 что угодно, и он может вычислить градиент.

Вот почему нам нужно превратить это в переменную [13:12]. Итак, l теперь переменная, содержащая убыток. Таким образом, он содержит единственный номер для этой мини-партии, который является убытком для этой мини-партии. Но это не просто число. Это число как переменная, поэтому оно знает, как оно было вычислено.

Мы собираемся добавить эту потерю в наш массив, чтобы позже получить ее среднее значение. А теперь рассчитаем градиент. Итак, l.backward() - это то, что говорит вычислить градиент. Так что помните, когда мы вызываем сеть, она на самом деле вызывает нашу функцию пересылки. Так что это все равно что продвигаться вперед. А затем в обратном направлении - это похоже на использование цепного правила для вычисления градиентов в обратном направлении.

Итак, optimizer.step() - это то, что мы собираемся написать, это обновление весов на основе градиентов и скорости обучения. zero_grad(), мы объясним, когда напишем это от руки.

Итак, в конце мы можем превратить наш загрузчик данных проверки в итератор [14:16]. И затем мы можем по всей длине взять из этого все x и y и запросить оценку, которую мы определили как равную тому, что вы предсказали, какая вещь была актуальной, и проверить, равны ли они. Тогда средним значением будет наша точность:

def score(x, y):
    y_pred = to_np(net2(V(x)))
    return np.sum(y_pred.argmax(axis=1) == to_np(y))/len(y_pred)

Вопрос: в чем преимущество того, что вы превратили итератор в итератор, а не использовали обычный цикл Python [14:53]? Мы используем обычный цикл Python, поэтому вопрос действительно похож на сравнение с чем. Так что альтернатива, о которой вы, возможно, думаете, могла бы быть такой, как если бы мы могли использовать что-то вроде списка с индексатором. Итак, проблема в том, что мы хотим, чтобы каждый раз, когда мы захватываем новую мини-партию, она была случайной. Мы хотим другую перетасованную вещь. Итак, этот for t in range(len(dl)), вы действительно можете повторять бесконечно. Вы можете перебирать его столько раз, сколько захотите. Это своего рода идея, которую на разных языках называют по-разному, но многие языки называют это потоковой обработкой, и основная идея состоит в том, что вместо того, чтобы говорить, что я хочу третью или девятую вещь, это как будто я хочу следующую вещь. . Он отлично подходит для сетевого программирования - например, взять что-нибудь из сети. Он отлично подходит для программирования пользовательского интерфейса - захватите следующее событие, когда кто-то нажал кнопку. Он также отлично подходит для такого рода числового программирования - мне просто нужен следующий пакет данных. Это означает, что данные могут быть произвольно длинными, потому что мы просто захватываем по одному фрагменту за раз. И я предполагаю, что краткий ответ заключается в том, что так работает PyTorch. Загрузчики данных PyTorch спроектированы таким образом. Итак, в Python есть концепция генератора, с помощью которой вы можете создать функцию, которая ведет себя как итератор. Итак, Python признал, что этот подход к программированию с потоковой обработкой очень удобен и полезен, и поддерживает его повсюду. Таким образом, практически везде, где вы используете цикл for ... in, везде, где вы используете понимание списка, эти вещи всегда могут быть генераторами или итераторами. Таким образом, программируя таким образом, мы получаем большую гибкость. Звучит примерно правильно, Терренс? Вы эксперт по языкам программирования.

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

Джереми: «Нет» не обязательно должно быть в памяти. Фактически, большую часть времени с PyTorch мини-пакет будет считываться из отдельных образов, распределенных по вашему диску, по запросу, поэтому большую часть времени он не находится в памяти.

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

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

Терренс: вы все знаете, конечно, конвейеры командной строки и перенаправление ввода-вывода.

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

Назад [28:54]

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

Итак, мы собираемся снова взять этот модуль, который мы написали сами (модуль логистической регрессии). У нас снова будет одна эпоха. Мы собираемся перебрать каждую вещь в итераторе. Мы собираемся взять нашу независимую и зависимую переменную для мини-партии, передать ее в нашу сеть, вычислить потери. Итак, это все то же самое, что и раньше, но теперь мы собираемся избавиться от optimizer.step(), и будем делать это вручную. Итак, основная уловка, как я уже упоминал, состоит в том, что мы не собираемся проводить вычисления вручную. Итак, мы вызываем l.backward() для автоматического вычисления градиентов, и это заполняет нашу матрицу. Вот тот модуль, который мы построили:

Поэтому матрицу весов для весов линейного слоя мы называем l1_w, а смещения - l1_b. Это были атрибуты, которые мы создали. Поэтому я просто поместил их в вещи под названием w и b, просто чтобы сэкономить время на вводе текста. Итак, w - наши веса, b - наши смещения. Итак, веса, помните, что веса - это переменные, и чтобы получить тензор из переменной, мы должны использовать .data. Итак, мы хотим обновить фактический тензор в этой переменной, поэтому мы говорим:

w.data -= w.grad.data * lr

  • w.grad.data * lr то, что сейчас находится в градиентах, умножается на скорость обучения.
  • Фактически мы можем изменить нашу функцию потерь, чтобы добавить этот квадратный штраф.

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

net2 = LogReg().cuda()
loss_fn=nn.NLLLoss()
lr = 1e-2
w,b = net2.l1_w,net2.l1_b

for epoch in range(1):
    losses=[]
    dl = iter(md.trn_dl)
    for t in range(len(dl)):
        xt, yt = next(dl)
        y_pred = net2(V(xt))
        l = loss(y_pred, Variable(yt).cuda())
        losses.append(loss)

        # Backward pass: compute gradient of the loss with respect 
        # to model parameters
        l.backward()
        w.data -= w.grad.data * lr
        b.data -= b.grad.data * lr
        
        w.grad.data.zero_()
        b.grad.data.zero_()   

    val_dl = iter(md.val_dl)
    val_scores = [score(*next(val_dl)) for i in range(len(val_dl))]
    print(np.mean(val_scores))

Вопрос: когда мы делаем next сверху, когда это конец цикла, как мы захватываем следующий элемент [21:08]? Итак, этот (for t in range(len(dl)):) проходит через каждый индекс в диапазоне длины, так что это 0, 1, 2… В конце этого цикла он распечатает среднее значение набора проверки, вернитесь к началу эпоха, в этот момент будет создан новый итератор. Таким образом, в Python, когда вы вызываете iter(md.trn_dl), он обычно говорит ему сбросить свое состояние для создания нового итератора. И если вам интересно, как это работает, весь код доступен для вас. md.trn_dl это fastai.dataset.ModelDataLoader, чтобы мы могли взглянуть на его код и увидеть, как именно он строится. Итак, вы можете видеть здесь функцию __next__, которая отслеживает, сколько раз она проходила в этом self.i, а вот функция __iter__, которая вызывается при создании нового итератора. И вы можете видеть, что он передает это чему-то другому, имеющему тип DataLoader, а затем вы можете проверить DataLoader, если вам интересно узнать, как это также реализовано.

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

Вопрос: почему вы обернули это в for epoch in range(1), поскольку он будет запускаться только один раз [23:10]? Потому что в реальной жизни мы обычно проживаем несколько эпох. Как и в этом случае, поскольку это линейная модель, она на самом деле тренируется настолько хорошо, насколько это возможно за одну эпоху, поэтому, если я наберу 3 здесь, она на самом деле не улучшится после первой эпохи вообще, как вы можете видеть . Но когда мы вернемся к вершине, мы рассмотрим несколько более глубоких и интересных версий, которые займут больше эпох. Итак, если бы я превратил это в функцию, я бы пошел как def train_mdl, и одна из вещей, которые вы бы передали, - это количество эпох.

Следует помнить одну вещь: когда вы создаете эти слои нейронной сети и помните, что это (LogReg()), с точки зрения PyTorch, просто nn.Module - мы могли бы использовать его как слой, мы могли бы использовать его как функция, мы могли бы использовать ее как нейронную сеть [24:10]. PyTorch не считает это разными вещами. Так что это может быть слой внутри какой-то другой сети. Так как же работают градиенты? Итак, если у вас есть слой, который мы можем рассматривать как активации, или некоторые активации, которые вычисляются с помощью какой-либо другой нелинейной / линейной функции активации. И из этого слоя очень вероятно, что затем мы пропустим его через матричный продукт, чтобы создать новый слой. Итак, каждый из них, так что, если бы мы захватили как одну из этих активаций, на самом деле будет использоваться для расчета каждого из этих выходов.

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

Перед тем, как сделать l.backward(), мы говорим "сбросить". Итак, давайте возьмем наши веса, возьмем градиенты, возьмем тензор, на который они указывают, а затем zero_. Подчеркивание как суффикс в PyTorch означает «на месте», так что это звучит как мелочь, но его очень полезно запомнить. У каждой функции есть суффикс версии подчеркивания, который делает это на месте. Итак, обычно ноль возвращает тензор нулей определенного размера, поэтому zero_ означает замену его содержимого кучей нулей.

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

Вопрос: зачем нам несколько эпох [27:39]? Самый простой способ ответить на этот вопрос: допустим, наша скорость обучения была крошечной. Тогда это просто не уйдет далеко. Нет ничего, что говорило бы, что пройти через одну эпоху достаточно, чтобы добраться до нее. Тогда было бы хорошо, давайте увеличим скорость обучения. Конечно, мы можем увеличить скорость обучения, но кто может сказать, что самой высокой скорости обучения при стабильном обучении достаточно, чтобы выучить это так же хорошо, как это можно усвоить. Для большинства наборов данных для большинства архитектур очень редко бывает достаточно одной эпохи, чтобы получить наилучший результат, которого вы можете достичь. Линейные модели ведут себя очень красиво. Таким образом, вы можете часто использовать более высокие темпы обучения и учиться быстрее. Кроме того, вы не можете добиться такой высокой точности, поэтому их тоже не так уж далеко. Так что сделать одну эпоху будет большой редкостью.

Скорость обучения отжига [34:12]

Пойдем назад. Итак, возвращаясь назад, мы, по сути, собираемся сказать, что давайте не будем писать эти строки снова и снова (слева). Пусть кто-нибудь сделает это за нас.

Так что это единственное различие между этими версиями. Вместо того, чтобы говорить .zero_ или -= gradient * lr сами, они завернуты для нас (справа).

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

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

Вот оно. Итак, давайте посмотрим на подгонку:

Тогда вот шаг [31:41]:

Обнулите градиенты, вычислите потери (помните, PyTorch склонен называть это критерием, а не потерями), сделайте обратное. Кроме того, есть еще кое-что, чего мы здесь не узнали, но мы изучаем курс глубокого обучения, который называется «отсечение градиента», так что вы можете это игнорировать. Итак, вы можете увидеть, все, что мы узнали, если заглянуть внутрь фактического фреймворка, это код, который вы видите. Вот что делает подгонка.

Следующим шагом будет идея иметь некоторые веса и смещение и произвести матричное произведение и сложение, давайте поместим это в функцию [32:14]. Эта штука с логарифмом softmax, давайте поместим это в функцию. Затем сама идея сначала сделать это, а потом сделать то, идея объединения функций в цепочку, давайте превратим это в функцию. И это наконец подводит нас к следующему:

Таким образом, «Последовательный» просто означает выполнение этой функции, получение результата, отправку его в эту функцию и т. Д. А «Линейный» означает создание матрицы весов, создание смещений. Вот и все.

Затем мы можем, как мы начали говорить, превратить это в глубокую нейронную сеть, сказав вместо того, чтобы сразу посылать 10 активаций, давайте поместим это, скажем, в 100 активаций. Мы могли бы выбрать любое число, какое захотим. Пропустите его через ReLU, чтобы сделать его нелинейным, пропустите через другой линейный слой, еще один ReLu, а затем наш окончательный результат с нашей последней функцией активации.

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

Если вы попытаетесь увеличить скорость обучения с 0,1 дальше, она действительно начнет нестабильно.

Регуляризация [42:05]

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

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

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

Вопрос: у меня возник вопрос о загрузке данных. Я знаю, что это функция Fast AI, но не могли бы вы подробнее рассказать о том, как он создает пакеты, как это делается и как он принимает эти решения [36:47]? Конечно. По сути, в PyTorch есть действительно хороший дизайн, в котором они говорят, что давайте создадим объект, называемый набором данных. Набор данных - это что-то вроде списка. У него есть длина (например, сколько изображений в наборе данных). И у него есть возможность индексировать его как список. Итак, если у вас есть набор данных d, вы можете:

d = Dataset(...)
len(d)
d[i]

Это практически все, что касается PyTorch. Итак, вы начинаете с набора данных, и это похоже на то, что d[3] дает вам третье изображение и т. Д. Итак, вы берете набор данных и можете передать его в конструктор для загрузчика данных dl = DataLoader(d). Это дает вам то, что теперь можно повторить. Итак, теперь вы можете сказать iter(dl), и это то, что вы можете позвонить в следующий раз (например, next(iter(dl))). И что это теперь будет делать, так это когда вы вызываете конструктор загрузчика данных, вы можете выбрать, включить или выключить перемешивание. Перемешать означает дать мне случайную мини-партию, перемешать - пройти ее последовательно. Итак, что делает загрузчик данных, когда вы вызываете next, это в основном, если вы сказали shuffle=True и размер пакета равен 64, он захватит 64 случайных целых числа от 0 до длины и вызовет это (d[i]) 64 раза, чтобы получить 64 разных элемента и сожмите их вместе. Итак, Fast AI использует ту же терминологию и тот же API. Просто мы делаем некоторые детали по-другому. В частности, особенно с компьютерным зрением, вы часто хотите выполнять множество операций по увеличению данных, например переворачивать, немного изменять цвета, вращать, что оказывается дорогостоящим в вычислительном отношении. Даже простое чтение JPEGS требует больших вычислительных затрат. Таким образом, PyTorch использует подход, при котором он запускает несколько процессоров, чтобы делать это параллельно, в то время как библиотека Fast AI вместо этого выполняет то, что называется многопоточностью, что может быть гораздо более быстрым способом сделать это.

Вопрос. Является ли эпоха настоящей эпохой в том смысле, что все элементы возвращаются один раз? Это перетасовка начала эпохи [39:47]? Да, не все библиотеки работают одинаково. Некоторые делают выборку с заменой, некоторые - нет. Библиотека Fast AI фактически переключает переключение на фактическую версию PyTorch, и я считаю, что версия PyTorch на самом деле перемешивается, и, я полагаю, эпоха покрывает все сразу.

Дело в том, что когда вы начинаете получать эти более крупные сети, потенциально вы получаете довольно много параметров [40: 1 7]. Я хочу попросить вас подсчитать, сколько существует параметров, но давайте вспомним, что у нас есть 28 на 28 входных данных на 100 выходных и 100 на 10. Затем для каждого из них у нас есть веса и смещения.

Итак, мы действительно можем это сделать .net.parameters возвращает список, в котором каждый элемент списка является тензором параметров не только для этого слоя, но если это слой с весами и смещениями, это будет два параметра. По сути, возвращает нам список всех тензоров, содержащих параметры. numel в PyTorch показывает, насколько он велик.

Итак, если я запустил это, вот количество параметров в каждом слое. Итак, у меня 784 входа, а у первого слоя сто выходов, поэтому первая матрица весов имеет размер 78 400. И первый вектор смещения имеет размер 100. Затем следующий - 100 на 100, а там 100. Затем следующий вектор - 100 на 10, а 10 - смещение. Итак, есть количество элементов в каждом слое. Я складываю их все, и получается почти сто тысяч. Так что я, возможно, рискую переобучиться здесь. Поэтому мы могли бы подумать об использовании регуляризации.

Секрет современных методов машинного обучения [58:14]

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

Что, если мы тогда захотим сказать, что если у меня много-много параметров, не используйте их, если они действительно не помогают. Если у вас есть миллион параметров, и вам действительно нужно всего 10 параметров, чтобы быть полезными, просто используйте 10. Итак, как мы можем сказать функции потерь, чтобы это сделать? В основном мы хотим сказать: «Эй, если параметр равен нулю, это не проблема. Как будто его вообще не существует. Итак, давайте накажем параметр за то, что он не равен нулю. Как бы мы могли это измерить? Как мы можем посчитать, насколько ненулевые наши параметры? L1 - абсолютное значение среднего веса. L2 - это квадраты самих гирь. Затем мы хотим иметь возможность сказать, хорошо, насколько мы хотим наказать за то, что не равняется нулю? Потому что, если у нас на самом деле не так много параметров, мы вообще не хотим упорядочивать многое. Если у нас куча, мы действительно хотим многое упорядочить. Итак, мы помещаем параметр a:

За исключением того, что у меня есть правило в моих классах, которое никогда не использует греческие буквы, поэтому обычно люди используют альфа, я собираюсь использовать. Это какое-то число, которое вы часто видите от 1e-6 до 1e-4 иш. Теперь нас действительно не волнует потеря, кроме как распечатать ее. Что нас действительно волнует, так это градиент потерь. Итак, градиент aw² равен 2aw. Итак, есть два способа сделать это:

  1. Мы можем изменить то, что, как мы сказали, веса равны весам минус скорость обучения градиенту, чтобы также добавить 2aw.
  2. То, что мы подробно рассмотрим в курсе глубокого обучения, но очень важно упомянуть, что здесь секрет, на мой взгляд, современных методов машинного обучения заключается в том, чтобы чрезмерно параметризовать решение вашей проблемы, как мы только что сделали. У нас было около 100 000 весов, когда у нас было только небольшое количество изображений 28 на 28, а затем мы использовали регуляризацию. Это похоже на прямую противоположность тому, как почти вся статистика и обучение проводились десятилетиями раньше, и до сих пор большинство старших преподавателей в большинстве университетов в большинстве областей имеют такой опыт, когда они узнали, что правильный способ построения модели - иметь как можно меньше параметров. насколько возможно. Надеюсь, мы узнали две вещи. Во-первых, мы можем создавать очень точные модели, даже если у них много-много параметров. Случайный лес имеет много параметров, и эта глубокая сеть имеет много параметров, и они могут быть точными. И мы можем сделать это либо с помощью упаковки, либо с помощью регуляризации. А регуляризация в нейронных сетях означает либо снижение веса (также известное как «своего рода» регуляризация L2), либо отсев, о котором мы не будем особо беспокоиться. Это совершенно другой подход к построению полезных моделей. И я просто хотел предупредить вас, что как только вы выйдете из этого класса, даже, возможно, когда вы перейдете к следующему выступлению преподавателей, в USF также будут люди, полностью обученные миру моделей с небольшим количеством параметров. Ваш следующий босс, вероятно, прошел обучение в мире моделей с небольшим количеством параметров. Идея, что они в чем-то более чистые, или простые, или лучше, или более интерпретируемые, или что-то еще. Я убежден, что это неправда - вероятно, никогда не будет правдой. Конечно, правда очень редко. И что на самом деле модели с множеством параметров могут быть чрезвычайно интерпретируемыми, как мы узнали из всего нашего урока интерпретации случайного леса. Вы можете использовать большую часть той же техники с нейронными сетями, но с нейронными сетями еще проще. Помните, как мы определяли важность функции, рандомизируя столбец, чтобы увидеть, как изменения в этом столбце повлияют на результат? Что ж, это просто глупый способ вычисления градиента. Насколько изменение этого входа изменяет выход? С помощью нейронной сети мы действительно можем вычислить ее градиент. Итак, с помощью PyTorch вы действительно можете сказать, каков градиент вывода относительно этого столбца? Вы можете сделать то же самое для построения графика частичной зависимости с помощью нейронной сети. И я отмечу для тех из вас, кто заинтересован в том, чтобы оказать реальное влияние, что никто не написал в основном ничего из этого для нейронных сетей. Так что для всей этой области нужно писать библиотеки, писать сообщения в блогах. Некоторые статьи были написаны, но только в очень узких областях, таких как компьютерное зрение. Насколько мне известно, никто не написал статьи, в которой рассказывалось бы, как использовать методы интерпретации нейронных сетей структурированных данных. Так что это действительно захватывающая большая область.

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

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

В любом случае, пока давайте рассмотрим версию, которую PyTorch называет уменьшением веса, но на самом деле оказывается, на основе этой статьи две недели назад, на самом деле это регуляризация L2. Это не совсем правильно, но достаточно близко. Итак, здесь мы можем сказать, что снижение веса составляет 1e-3.

Таким образом, мы установим множитель штрафа a равным 1e-3 и добавим его к функции потерь. Давайте скопируем эти клетки, чтобы сравнить, как они тренируются. Так что вы можете заметить здесь что-то нелогичное [48:54]. 0,23547 - это наша ошибка обучения. Вы ожидаете, что наша ошибка обучения с регуляризацией будет хуже, потому что мы штрафуем параметры, которые, в частности, могут улучшить ее. А на самом деле начиналось не хуже (раньше было 0,29756). Почему это могло быть?

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

Потенциально на обучение уходит очень много времени, иначе, если у вас есть функция, которая выглядит примерно так:

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

Вопрос: я не понимаю. Зачем делать это быстрее? Время тренировки имеет значение [50:26]? Нет, это после одной эпохи. Нижняя часть - это наши тренировки без потери веса, а верхняя - с уменьшением веса. Это не связано со временем, это связано только с одной эпохой. После одной эпохи я утверждал, что вы ожидаете, что тренировочный набор, при прочих равных, будет иметь худшую потерю со снижением веса, потому что мы наказываем это. А я говорю: «О, это не так. Это странно."

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

Вопрос: Итак, чтобы убедиться, что штрафные санкции приводят к тому, что оптимизатор с большей вероятностью достигнет глобального минимума [52:45]? Нет, я бы так не сказал. На самом деле я утверждаю, что в конечном итоге это, вероятно, будет хуже на обучающем наборе, действительно, похоже, что это так. В конце концов, по прошествии пяти эпох, наш тренировочный набор стал хуже со снижением веса. Вот чего я ожидал. Как будто я никогда не использую термин глобальный оптимум, потому что у нас нет никаких гарантий по этому поводу. Нам наплевать. Нас просто волнует, куда мы денемся через определенное количество эпох. Мы надеемся, что мы нашли хорошее решение. Так что к тому времени, когда мы придем к хорошему решению, тренировочный набор со снижением веса, потеря будет еще хуже, потому что это штраф. Но на проверочном наборе потеря лучше, потому что мы оштрафовали обучающий набор, чтобы попытаться создать что-то, что обобщает лучше. Таким образом, параметры, которые не имеют смысла, теперь равны нулю, и это лучше обобщает. Итак, все, что мы говорим, это то, что через одну эпоху все дошло до хорошей точки.

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

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

Я хотел проверить, как мы сюда попали [55:11]. Итак, после второй эпохи, вы можете видеть здесь, это действительно помогло. После второй эпохи, до того как мы достигли точности 97%, теперь мы достигли почти 98% точности. И вы можете видеть, что убыток составил 0,08 против 0,13. Таким образом, добавление регуляризации позволило нам найти на 50% лучшее решение (3% против 2%).

Вопрос: Итак, здесь есть две части: первая - это регуляризация L2 и уменьшение веса [55:42]? Нет, поэтому я утверждал, что это одно и то же. Таким образом, снижение веса - это вариант, если вы просто возьмете производную регуляризации L2, вы получите снижение веса. Таким образом, вы можете реализовать это, изменив функцию потерь с квадратом штрафа за потерю, или вы можете реализовать ее, добавив сами веса как часть градиента.

Вопрос: Можно ли использовать регуляризации и для сверточного слоя [56:19]? Абсолютно. Сверточный слой - это просто веса.

Вопрос: Вы можете объяснить, почему вы думали, что вам нужно снижение веса именно в этой задаче [56:29]? Не легко. Я имею в виду, кроме того, что я бы всегда пытался это сделать. Продолжение вопроса: Переоснащение? Так что, если мои потери в тренировках были выше, чем потери при проверке, значит, я не подходил. Так что регуляризировать определенно нет смысла. Это всегда было бы плохо. Это всегда означало бы, что вам нужно больше параметров в вашей модели. В данном случае я переобучаю. Это не обязательно означает, что регуляризация поможет, но, безусловно, стоит попробовать.

Вопрос: Как выбрать оптимальное количество эпох [57:27]? Вы изучаете мой курс глубокого обучения 😆 Это долгая история. У нас нет времени рассказывать о передовых методах работы в этом классе. Мы собираемся изучить основы.

НЛП [1:02:04]

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

IMDb

"Ноутбук"

Пример матрицы условного документа:

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

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

To get the dataset, in your terminal run the following commands:
wget http://ai.stanford.edu/~amaas/data/sentiment/aclImdb_v1.tar.gz
gunzip aclImdb_v1.tar.gz
tar -xvf aclImdb_v1.tar

А вот пример текстового файла:

PATH='data/aclImdb/'
names = ['neg','pos']
%ls {PATH}
aclImdb_v1.tar.gz  imdbEr.txt  imdb.vocab  models/  README  test/  tmp/  train/
%ls {PATH}train
aclImdb/  all_val/         neg/  tmp/    unsupBow.feat  urls_pos.txt
all/      labeledBow.feat  pos/  unsup/  urls_neg.txt   urls_unsup.txt
%ls {PATH}train/pos | head
0_9.txt
10000_8.txt
10001_10.txt
10002_7.txt
10003_8.txt
10004_8.txt
10005_7.txt
10006_7.txt
10007_7.txt
10008_7.txt
...

Итак, каким-то образом нам удалось выбрать историю о человеке, испытывающем неестественные чувства к свинье, в качестве первого выбора. Это было не намеренно, но все будет хорошо.

trn[0]
"Story of a man who has unnatural feelings for a pig. Starts out with a opening scene that is a terrific example of absurd comedy. A formal orchestra audience is turned into an insane, violent mob by the crazy chantings of it's singers. Unfortunately it stays absurd the WHOLE time with no general narrative eventually making it just too off putting. Even those from the era should be turned off. The cryptic dialogue would make Shakespeare seem easy to a third grader. On a technical level it's better than you might think with some good cinematography by future great Vilmos Zsigmond. Future stars Sally Kirkland and Frederic Forrest can be seen briefly."

Мы собираемся взглянуть на эти обзоры фильмов, и по каждому из них мы посмотрим, были ли они положительными или отрицательными. Итак, они были помещены в одну из этих папок. Они были загружены с IMDb (база данных фильмов и сайт обзора). Те, которые были сильно положительными, попали в /pos, а категорически отрицательные - в /neg, а остальные они вообще не маркировали (/unsup). Так что есть только сильно поляризованные обзоры.

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

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

trn_y[0]
0

Это выполнит поиск всех папок здесь (первый аргумент f'{PATH}train') с этими именами (второй аргумент names) и создаст помеченный набор данных. Не позволяйте этим вещам никогда не мешать вам понять, что происходит за кулисами. Мы можем взять его исходный код, и, как видите, он крошечный, примерно 5 строк.

trn,trn_y = texts_labels_from_folders(f'{PATH}train',names)
val,val_y = texts_labels_from_folders(f'{PATH}test',names)

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

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

«Naivebayes.xlsx»

Токенизация [«1:09:01»]

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

И это представление мы называем мешком слов представлением. Итак, это словесное представление обзора.

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

Теперь в НЛП вам нужно превратить свой текст в список слов, и это называется токенизацией. На самом деле это нетривиально, потому что, если это действительно было This movie is good. или This “movie” is good., как вы справляетесь с этой пунктуацией? Что еще интереснее, что, если бы это было This "movie" isn’t good.. То, как вы превращаете кусок текста в список токенов, называется токенизацией. Хороший токенизатор превратит это:

Наивный Байес [1:17:26]

Раньше: This "movie" isn’t good.

После: This " movie " is n’t good .

Итак, вы можете видеть в этой версии здесь, если я теперь разделю это на пробелы, каждый токен будет либо отдельным фрагментом пунктуации, либо суффиксом n't и будет рассматриваться как слово. Вот как мы, вероятно, захотим токенизировать этот фрагмент текста, потому что вы не хотите, чтобы good. был объектом. Нет понятия good. или "movie" не объект. Итак, токенизация - это то, что мы передаем токенизатору. В Fast AI есть токенизатор, который мы можем использовать, поэтому мы создаем нашу матрицу документа терминов с токенизатором:

sklearn имеет довольно стандартный API, что приятно. Я уверен, что вы видели это несколько раз раньше. После того, как мы построили какую-то «модель», мы можем думать о CountVectorizer как о модели, это просто определение того, что она собирается делать. Для этого мы можем позвонить fit_transform.

veczr = CountVectorizer(tokenizer=tokenize)

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

trn_term_doc = veczr.fit_transform(trn)
val_term_doc = veczr.transform(val)

Вопрос: что, если в наборе проверки есть другой набор слов, кроме обучающего набора [«1:11:40»]? Это отличный вопрос. Так что, как правило, большинство этих подходов к созданию словаря будут иметь специальный токен для unknown. Иногда вы также можете сказать «эй», если слово встречается менее трех раз, назовите его неизвестным. Но в противном случае, если вы видите что-то, чего раньше не видели, назовите это неизвестным. Так что (то есть «неизвестно») просто станет столбцом в мешке слов.

Когда мы создаем эту матрицу документа терминов, обучающий набор, у нас есть 25 000 строк, потому что есть 25 000 обзоров фильмов и 75 132 столбца, которые представляют собой количество уникальных слов.

Сейчас в большинстве документов нет большей части из этих 75 132 слов. Поэтому мы не хотим хранить это как обычный массив в памяти. Потому что это будет очень расточительно. Поэтому вместо этого мы сохраняем его как разреженную матрицу. Что делает разреженная матрица, так это то, что она просто хранит ее как нечто, что говорит о местонахождении ненулевых элементов. Итак, он говорит, что хорошо, появляется документ номер 1, слово номер 4 и их 4. Номер документа 1, термин номер 123 появляется один раз и так далее.

trn_term_doc
<25000x75132 sparse matrix of type '<class 'numpy.int64'>'
     with 3749745 stored elements in Compressed Sparse Row format>

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

(1, 4) → 4
(1, 123) → 1

Итак, мы могли взять первый обзор, и он дает нам 75 000 длинных разреженных матриц длиной в одну строку с 93 сохраненными элементами [«1:14:02»]. Другими словами, 93 из этих слов действительно используются в первом документе.

Мы можем взглянуть на словарь, сказав veczr.get_feature_names, который дает нам словарь. Итак, вот пример нескольких элементов имен функций:

trn_term_doc[0]
<1x75132 sparse matrix of type '<class 'numpy.int64'>'
	with 93 stored elements in Compressed Sparse Row format>

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

vocab = veczr.get_feature_names(); vocab[5000:5005]
['aussie', 'aussies', 'austen', 'austeniana', 'austens']

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

w0 = set([o.lower() for o in trn[0].split(' ')]); w0
{'a',
 'absurd',
 'an',
 'and',
 'audience',
 'be',
 'better',
 'briefly.',
 'by',
 'can',
 ...
}
len(w0)
91

Итак, мы увидели, что слово «абсурд» появилось дважды в первом документе, поэтому давайте проверим:

veczr.vocabulary_['absurd']
1297

Вот оно, это 2. Или, к сожалению, австралиец не появился в неестественных отношениях с фильмом о свиньях, так что это ноль:

trn_term_doc[0,1297]
2

Итак, это наш термин «матрица документов».

trn_term_doc[0,5000]
0

Вопрос: заботится ли он о соотношении слов в порядке слов [«1:16:02»]? Нет, мы выбросили заказы. Вот почему это набор слов. И я не утверждаю, что это обязательно хорошая идея. Что я скажу, так это то, что подавляющее большинство работы по НЛП, проделанной за последние несколько десятилетий, обычно используют это представление, потому что мы действительно не знали намного лучше. В настоящее время мы все чаще используем рекуррентные нейронные сети, о которых мы узнаем в нашем последнем уроке глубокого обучения части 1. Но иногда это представление работает очень хорошо, и на самом деле оно будет работать довольно хорошо в этом случае.

На самом деле, когда я работал в FastMail, моей почтовой компании, для фильтрации спама мы использовали следующую технику, Наивный Байес, который представляет собой набор слов [1:16:49]. Если вы получаете много писем со словом Виагра, и это всегда был спам, и вы никогда не получали письма от друзей, говорящих о Виагре, то, скорее всего, что-то, что говорит о Виагре, независимо от деталей языка, вероятно, от спамера. . Итак, это основная теория классификации с использованием матрицы документов терминов.

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

Логистическая регрессия [1:30:01]

Мы склонны называть эти столбцы в более общем смысле «характеристиками», а не «словами». this - это функция, movie - это функция и т. Д. Так что теперь он больше похож на язык машинного обучения. Столбец - это особенность. Мы называем это f наивным байесовским. Таким образом, мы можем в основном сказать, что вероятность того, что вы увидите слово this, учитывая, что класс равен 1 (то есть положительный отзыв), - это просто среднее значение того, как часто вы видите this в положительных отзывах. Теперь нам нужно быть немного осторожными, потому что, если вы никогда не видите конкретное слово в определенном классе, поэтому, если я никогда не получал электронное письмо от друга, в котором говорилось «Виагра», это на самом деле не означает вероятность того, что друг пришлет мне электронное письмо о Виагре, равна нулю. Это не совсем ноль. Я надеюсь, что завтра я не получу электронное письмо от Терренса, в котором говорилось бы: «Джереми, ты, наверное, мог бы использовать эту рекламу для виагры», но это могло случиться. Я уверен, что это будет в моих интересах 🤣 Итак, мы говорим, что на самом деле то, что мы видели до сих пор, не является полным образцом всего, что могло произойти. Это как образец того, что произошло до сих пор. Итак, предположим, что в следующем письме, которое вы получите, действительно упоминается виагра и все возможные слова. Итак, в основном мы собираемся добавить строку из единиц.

Это похоже на электронное письмо, в котором есть все возможные слова. Таким образом, ничто не может быть бесконечно маловероятным. Итак, я беру среднее значение всех случаев, когда this появляется в моем положительном корпусе, плюс единицы:

Это похоже на вероятность того, что feature = 'this’ появится в документе при этом class = 1 (т. Е. p (f | 1) для this).

Неудивительно, что здесь то же самое для вероятности появления функции this при class = 0:

Тот же расчет, за исключением нулевых строк. Очевидно, это одно и то же, потому что this появляется один раз в позитиве и один раз в негативе.

Таким образом, мы можем сделать это для каждой функции для каждого класса [«1:20:40»]

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

Это просто правило Байеса. Итак, мы можем вычислить все эти вещи, но на самом деле мы действительно хотим знать, является ли это более вероятным классом 0 или классом 1. А что, если бы мы на самом деле взяли вероятность класса 1 и разделили на вероятность класса 0. Что, если мы это сделали?

Итак, если это число больше 1, то с большей вероятностью будет класс 1, если оно меньше 1, то с большей вероятностью будет класс 0. В этом случае мы могли бы просто разделить все это на одну и ту же версию. для класса 0, что равносильно его умножению на обратную величину. Итак, приятно, что теперь p (d) отменяется и вероятность получения данных с учетом класса 0 здесь, вероятность получения класса 0 здесь.

В основном это означает, что мы хотим вычислить вероятность того, что мы получим этот конкретный документ, учитывая, что класс в 1 раз больше вероятности того, что класс равен 1, деленной на вероятность получения этого конкретного документа, учитывая, что класс равен 0, умноженному на вероятность того, что класс 0:

Таким образом, вероятность того, что класс равен 1, просто равна среднему значению ярлыков [«1:23:20»]. Вероятность того, что класс равен 0, равна 1 минус. Итак, вот эти два числа:

У меня обоих одинаковое количество, так что оба по 0,5.

Какова вероятность получить этот документ с учетом того, что это класс 1. Студент: Посмотрите на все документы, в которых класс равен 1 и разделенный на 1 даст вам… [«1:24:02»] Джереми: Так что помните это будет для конкретного документа. Например, можно сказать, какова вероятность того, что этот отзыв будет положительным. Итак, вы на правильном пути, но что нам нужно сделать, так это сказать, что давайте просто посмотрим на слова, которые в нем есть, а затем умножим вероятности вместе для класса, равного 1. Таким образом, вероятность того, что обзор класса 1 имеет this, составляет 2/3, вероятность movie равна 1, is равна 1, а good равна 1. Таким образом, вероятность, что все они есть, - это все те, которые умножаются вместе. Вроде. Тайлер, почему это не так? Так рада, что ты выглядишь напуганным и скептически настроенным 😄

Тайлер: Разве выбор не был независимым?

Джереми: Спасибо. Так что никто не может назвать Тайлера наивным, потому что причина этого наивного Байеса в том, что это то, что происходит, если принять теорему Байеса наивным образом. И Тайлер не наивен. Все, кроме. Итак, Наивный Байес говорит: давайте предположим, что если у вас есть «этот фильм чертовски глуп, я его ненавижу», но вероятность hate не зависит от вероятности bloody, не зависит от вероятности stupid, что определенно неверно. Итак, наивный байесовский метод на самом деле не очень хорош, но я учу вас этому, потому что он окажется удобным отрывком для того, что мы собираемся изучить позже. Предыстория: и часто это очень хорошо работает. Джереми: Ничего страшного. Я имею в виду, что никогда бы не выбрал это. Я не думаю, что это лучше, чем любой другой такой же быстрый и простой метод. Но это то, что вы можете сделать, и это, безусловно, будет полезной основой.

Итак, теперь расчет вероятности того, что мы получим этот конкретный документ, при условии, что это положительный отзыв [«1:26:08»]:

Вот вероятность, если она отрицательная.

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

Итак, это версия Excel. Итак, вы можете сказать, что я позволил Яннету прикоснуться к этому, потому что в нем есть LaTeX. У нас есть настоящая математика. Итак, вот то же самое; соотношение количества журналов для каждой функции f.

Итак, здесь он написан как Python. Наша независимая переменная - это матрица документа терминов, наша зависимая переменная - это просто метки для y. Таким образом, используя numpy, этот x[y==1] будет захватывать строки, в которых зависимая переменная равна 1. Их мы можем просуммировать по строкам, чтобы получить общее количество слов для этой функции во всех документах, плюс 1 - Терренс полностью отправит Мне кое-что о Виагре сегодня я могу рассказать. Ничего не поделаешь. Затем проделайте то же самое с отрицательными отзывами. Тогда, конечно, лучше взять журнал, потому что, если мы возьмем журнал, мы сможем складывать вещи, а не умножать их вместе. И как только вы умножите достаточное количество этих вещей вместе, оно станет настолько близко к нулю, что у вас, вероятно, закончится число с плавающей запятой. Итак, мы берем журнал соотношений. Затем, как я сказал, мы затем умножаем это или с логарифмом добавляем это к отношению вероятностей всего класса.

Итак, чтобы сказать для каждого документа умножить байесовские вероятности на количество, мы можем просто использовать матричное умножение. Затем, чтобы добавить в журнал соотношения классов, вы можете просто использовать + b. Таким образом, мы получаем нечто, что очень похоже на логистическую регрессию. Но мы ничего не узнаем. Не с точки зрения SGD. Мы просто рассчитываем это, используя эту теоретическую модель. Как я уже сказал, мы можем затем сравнить это, больше или меньше нуля, а не единицы, потому что теперь мы находимся в пространстве журнала. Затем мы можем сравнить это со средним значением. И это ~ 81% точности. Так что наивный Байес - это не ничто. Это нам что-то дало.

def pr(y_i):
    p = x[y==y_i].sum(0)
    return (p+1) / ((y==y_i).sum()+1)
x=trn_term_doc
y=trn_y
p = x[y==1].sum(0)+1 
q = x[y==0].sum(0)+1
r = np.log((p/p.sum())/(q/q.sum()))
b = np.log(len(p)/len(q))

Получается, что эта версия, в которой мы собственно смотрим, как часто появляется, как «абсурд», появляется дважды, оказывается, по крайней мере, для этой проблемы, и довольно часто не имеет значения, появлялся ли «абсурд» дважды или один раз [« 1:29:03 »]. Все дело в том, что оно появилось. Итак, люди, как правило, пытаются сказать: возьмите документ матрицы терминов и перейдите .sign(), который заменяет все положительное на 1, а все отрицательное на -1 (очевидно, что у нас нет отрицательных результатов). Итак, это преобразовывает его в двоичную форму. В нем говорится, что меня не волнует, что вы дважды видели «абсурд», мне просто важно, чтобы вы это видели. Так что если мы сделаем то же самое с бинаризованной версией, то вы получите лучший результат.

pre_preds = val_term_doc @ r.T + b
preds = pre_preds.T>0
(preds==val_y).mean()
0.80691999999999997

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

pre_preds = val_term_doc.sign() @ r.T + b
preds = pre_preds.T>0
(preds==val_y).mean()
0.82623999999999997

-= мы хотим двигаться в направлении, противоположном градиенту. Градиент подсказывает нам, какой путь вверх. Мы хотим спуститься.

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

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

m = LogisticRegression(C=1e8, dual=True)
m.fit(x, y)
preds = m.predict(val_term_doc)
(preds==val_y).mean()
0.85504000000000002

Яннет: что это двойное = True [«1:31:30»]? Я надеялся, что вы проигнорируете, не заметите, но вы это видели. В основном в этом случае наша матрица терминов документа намного шире, чем высока. Существует почти математически эквивалентная переформулировка логистической регрессии, которая оказывается намного быстрее, когда она шире, чем высота. Итак, краткий ответ: если он шире, чем высокий, положите dual=True, он будет быстро бегать. Это занимает примерно 2 секунды. Если его здесь нет, это займет несколько минут. Итак, в математике существует концепция двойных версий задач, которые похожи на эквивалентные версии, которые иногда лучше работают в определенных ситуациях.

Вот бинаризованная версия [1:32:20]. Это примерно то же самое. Итак, вы можете видеть, что я снабдил его знаком матрицы документов терминов и предсказал с помощью val_term_doc.sign().

Дело в том, что это будет коэффициент для каждого термина, где в нашем словаре было около 75 000 терминов, и это похоже на множество коэффициентов, учитывая, что у нас только 25 000 отзывов [1:32:38] . Так что, возможно, нам стоит попробовать упорядочить это.

m = LogisticRegression(C=1e8, dual=True)
m.fit(trn_term_doc.sign(), y)
preds = m.predict(val_term_doc.sign())
(preds==val_y).mean()
0.85487999999999997

Таким образом, мы можем использовать регуляризацию, встроенную в класс LogisticRegression в sklearn, который C - это параметр, который они используют. Это немного странно, меньший параметр означает большую регуляризацию. Вот почему я использовал 1e8, чтобы отключить регуляризацию.

Итак, если я включу регуляризацию, установлю ее на 0,1, то теперь она составит 88%:

В этом есть смысл. Вы можете подумать, что 75 000 параметров на 25 000 документов, это, скорее всего, перебор. В самом деле, это было переоснащение. Таким образом, это добавление регуляризации L2, чтобы избежать переобучения.

m = LogisticRegression(C=0.1, dual=True)
m.fit(x, y)
preds = m.predict(val_term_doc)
(preds==val_y).mean()
0.88275999999999999

Я упоминал ранее, что помимо L2, который смотрит на квадрат веса, есть также L1, который смотрит только на абсолютное значение весов [«1:33:37»].

Я был довольно небрежным в своих формулировках, прежде чем сказал, что L2 пытается свести все к нулю. Это в некотором роде правда, но если у вас есть две вещи, которые сильно коррелируют, то регуляризация L2 сдвинет их вниз вместе. Это не сделает одно из них нулевым, а другое ненулевым. Таким образом, регуляризация L1 на самом деле имеет свойство, заключающееся в том, что она будет пытаться свести к нулю как можно больше вещей, в то время как регуляризация L2 имеет свойство, которое она стремится сделать все меньше. На самом деле в любом современном машинном обучении нас не заботит эта разница, потому что мы очень редко пытаемся напрямую интерпретировать коэффициенты. Мы пытаемся понять наши модели с помощью допроса, используя те методы, которым мы научились. Причина, по которой мы должны заботиться о L1 по сравнению с L2, просто заключается в том, какой из них дает лучшую ошибку в наборе проверки. И вы можете попробовать и то, и другое. С LogisticRegression в sklearn L2 на самом деле оказывается намного быстрее, потому что вы не можете использовать dual=True, если у вас нет L2, а L2 - по умолчанию. Так что я не особо беспокоился о разнице.

Итак, вы можете видеть, что если мы используем регуляризацию и преобразование в двоичную форму, на самом деле у нас все хорошо [«1:35:04»]:

Вопрос: раньше мы узнали об Elastic-net как о сочетании L1 и L2. Сможем ли мы это сделать [1:35:23]? Да, вы можете это сделать, но с более глубокими моделями. И я никогда не видел, чтобы кто-то нашел это полезным.

m = LogisticRegression(C=0.1, dual=True)
m.fit(trn_term_doc.sign(), y)
preds = m.predict(val_term_doc.sign())
(preds==val_y).mean()
0.88404000000000005

И последнее, о чем я хотел бы упомянуть, это то, что когда вы делаете свой CountVectorizer, вы также можете запрашивать n-граммы. По умолчанию мы получаем униграммы, состоящие из отдельных слов. Но если мы скажем ngram_range=(1,3), это также даст нам биграммы и триграммы.

Под этим я подразумеваю, что если я сейчас скажу хорошо, давайте продолжим и выполним CountVectorizer и get_feature_names, теперь мой словарный запас включает биграмму: 'by vast', 'by vengeance' и триграмму: 'by vengeance .' 'by vera miles'. Итак, теперь мы делаем то же самое, но после токенизации, это не просто захват каждого слова и высказывание, что это часть вашего словарного запаса, а каждые два слова рядом друг с другом и каждые три слова рядом друг с другом. И это оказывается очень полезным в использовании подходов «пакет слов», потому что теперь мы можем видеть разницу между not good, not bad и not terrible. Или даже как "good", что, вероятно, будет саркастичным. Таким образом, использование функций триграммы на самом деле значительно улучшит как наивный байесовский метод, так и логистическую регрессию. Это действительно продвигает нас намного дальше и делает их весьма полезными.

veczr =  CountVectorizer(ngram_range=(1,3), tokenizer=tokenize,
                         max_features=800000)
trn_term_doc = veczr.fit_transform(trn)
val_term_doc = veczr.transform(val)
trn_term_doc.shape
(25000, 800000)
vocab = veczr.get_feature_names()
vocab[200000:200005]
['by vast', 'by vengeance', 'by vengeance .', 'by vera', 'by vera miles']

Вопрос: у меня вопрос по токенизаторам. Вы говорите max_features, так как же выбираются эти биграммы и триграммы [«1:37:17»]? Поскольку я использую линейную модель, я не хотел создавать слишком много функций. На самом деле он работал нормально даже без max_features. Думаю, у меня было около 70 миллионов коэффициентов. Это все еще работало. Но нет необходимости иметь 70 миллионов коэффициентов. Итак, если вы скажете max_features=800,000, CountVectorizer отсортирует словарный запас по частоте появления всего, будь то униграмма, биграмма, триграмма, и обрежет его после первых 800 000 наиболее распространенных нграмм. N-грамм - это просто общее слово для униграммы, биграммы и триграммы.

Вот почему сейчас train_term_doc.shape составляет 25 000 на 800 000. Если вы не уверены, какое число должно быть максимальным, я просто выбрал что-то действительно большое, и не особо беспокоился об этом, и, похоже, все в порядке. Это не очень чувствительно.

Хорошо, у нас нет времени, так что мы увидим завтра ... Кстати, мы могли бы заменить эту LogisticRegression нашей версией PyTorch:

И завтра мы действительно увидим что-то в библиотеке Fast AI, которая делает именно это, но завтра мы увидим также, как объединить логистическую регрессию и наивный байесовский метод вместе, чтобы получить что-то лучшее, чем то и другое. Затем мы узнаем, как перейти от этого к созданию более глубокой нейронной сети, чтобы получить в значительной степени современный результат для структурированного обучения. Все в порядке. Увидимся тогда.

Уроки: «1» ・ «2» ・ «3» ・ «4» ・ «5» ・ «6» ・ «7» ・ «8» ・ «9» ・ «10» ・ « 11 »・« 12 »

Машинное обучение 1: Урок 10