Как самостоятельно писать код. Приложение

Как самостоятельно писать код (часть 1)

Здравствуйте и добро пожаловать на занятия по теме «Глубокое обучение с подкреплением: глубокое обучение на языке Python, часть 7».

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

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

Некоторые люди иногда ведут себя так, будто собираются научиться играть в теннис, просто прочитав книгу по теннису. Естественно, это явно невозможно: вам необходимо кое-что узнать о теннисе – то, что является просто фактами и остаётся в мозгу как осознанные факты, – а затем вы должны выходить и отрабатывать эти знания на корте. Отработав и освоив всё, что вы изучили до этого, вы можете приступать к изучению новых методов. Таким образом, у вас должен быть непрерывный цикл изучения новых методов и затем их отработки.

Почему так? Да потому, что простое изучение метода лишь как факта вовсе не значит, что вы действительно его понимаете. Практика же в использовании метода помогает вам взглянуть на него с другой точки зрения. Вновь и вновь повторяющаяся практика ведёт к новому пониманию и подсознательному обучению – возникновению мышечной памяти. Вы должны пройти этот цикл. Однако некоторые слушатели наивно полагают, что им достаточно прочитать целую книгу по теннису, после чего они станут мастерами, стоит лишь взять в руки ракетку. Это очень распространённое явление, особенно когда вам на самом деле нужно лишь сидеть и смотреть видео, и нет никого, кто заставил бы начать писать код; а множество людей не помышляют даже о том, чтобы попробовать. Поэтому всегда вспоминайте, увидев код: «Когда пришло время писать код – вы должны писать код».

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

Например, вы знаете, что при выполнении поэлементного сложения матриц обе матрицы должны иметь одинаковую размерность. Значит, если вы обнаружите, что их размерность не одинакова, то одно из ваших предыдущих допущений было неверным, так что вам придётся вернуться назад и исправить его. Форма деталей конструктора «Лего» должны следовать определённому набору правил, чтобы они правильно сочетались друг с другом. Если вы сделаете неправильное предположение о том, как они сочетаются, и попытаетесь их соединить, то ничего не получится. Без самостоятельных попыток что-либо создать вы никогда не разберётесь в этих деталях.

Поговорим теперь о том, как собственно писать код самому. Рассмотрим ситуацию с машинным обучением с учителем. Мы знаем, что у нас должны быть какие-то входные данные и целевые переменные, а сами мы должны, исходя из этих входных данных, сделать прогнозы, которые, в свою очередь, должны быть очень близки к целевым переменным. Обычно входные данные обозначаются через X, а целевые переменные – через Y. Иногда, правда, целевые переменные обозначаются ещё через T, однако в этой лекции мы будем обозначать их через Y.

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

Некоторые заявляют, что это, мол, непрактично, поскольку хотят разбора конкретных примеров. Однако это потому, что они не думают разумно. В действительности же это самая практичная вещь, которую только можно сделать – ведь это значит, что всё нами изученное можно применить к любому существующему набору данных. Это значит, что мы можем обучить модель на данных из области интернет-торговли, однако мы можем обучить модель и на данных интернет-рекламы – вовсе без необходимости изучать что-либо новое! Поистине, это лучший вид «ленивого программирования» – один раз изучить что-то и  использовать его в любой сфере.

Одно из важнейших следствий того, что «все данные одинаковы», заключается в том, что у вас есть безграничные возможности для практики. Вы можете скачать наборы данных с Kaggle, Google, Википедии, Amazon или откуда-нибудь ещё и испытать на них изученные алгоритмы. Это означает следующее. Предположим, у вас есть набор данных, который вас интересует больше чем что, чем мы занимаемся в курсе. Видите ли, в курсе мы должны использовать те наборы данных, которые понятны всем, например текст и изображения. Все знают, что такое текст и что такое изображения. Они же являются самыми основными типами данных на сайтах. Вероятно, вы даже не задумывались над тем фактом, что текст и изображения – это данные, ведь это так банально. В любом случае, мы сильно упираем на текст и изображения, поскольку их понимают все. Однако, предположим, что вы биолог и знаете, что такое ДНК. Вы полагаете, что ДНК и геномика – это по-настоящему интересно, а потому желаете использовать изученные алгоритмы в этой области. Ну и занимайтесь этим на здоровье! Не забывайте: все данные одинаковы, так что вам необходимо лишь преобразовать свои данные в соответствующие X и Y и подставить их в алгоритмы машинного обучения. Недостаток тут лишь в том, что мы, скорее всего, не будем обсуждать ДНК на занятиях, поскольку это не самый простой пример, да и большинство программистов не являются биологами, так что могут и не знать, что такое ДНК и её специфику. Следовательно, данный пример не будет иметь для них особого смысла, что диаметрально противоположно тексту и изображениям, которые имеют смысл для всех. То же самое касается любой другой специализированной области вроде финансов, компьютерных сетей, космологии и так далее.

Теперь будет уместно поговорить об обучении с учителем. У нас есть X и Y, что же с ними делать? В обучении с учителем, как мы знаем, перед нами стоят две основные задачи: нам нужно провести обучение и сделать прогноз. В библиотеке Sci-Kit Learn все модели имеют один и тот же API, не важно, будь то логистическая регрессия, деревья решений или метод случайного леса. Все модели обучения с учителем в Sci-Kit Learn имеют две одни и те же функции: fit и predict. Если не верите, можете лично посмотреть в документации по Sci-Kit Learn.

Когда мы изучаем машинное обучение с учителем, мы на самом деле изучаем, что происходит внутри этих двух функций, каковы их параметры и как эти параметры изучить. В действительности в этом и заключается всё машинное обучение с учителем – в заполнении этих двух функций. Принятие такой точки зрения даст структуру вашего кода и значительно облегчит наглядное его представление.

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

Псевдокод заключается в следующем. В функции predict, как вы уже знаете, в качестве аргумента принимаются некоторые входные данные X, а прогноз равен

Вы, вероятно, уже узнали линейную регрессию. Впрочем, забудьте всё, что вы знаете о линейной регрессии, и сделайте вид, что это просто некая предложенная мною формула; могу лишь сказать, что мы создаём в некотором роде регрессионную модель. Из уравнения вам должно быть ясно, что параметры модели содержат вектор весовых коэффициентов w; наверное, я вам также скажу, что w является вектором той же размерности, что и количество векторов признаков в X. Разумеется, так и должно быть, чтобы умножение матриц было корректным. Это подобно проверке на работоспособность, чтобы вы могли убедиться, что тут всё осмысленно.

В функции fit я хочу выполнить некоторое количество раз данный цикл:

w = образец из нормального распределения

for i = 1..T:

Вы, вероятно, уже узнали линейную регрессию. Впрочем, забудьте всё, что вы знаете о линейной регрессии, и сделайте вид, что это просто некая предложенная мною формула; могу лишь сказать, что мы создаём в некотором роде регрессионную модель. Из уравнения вам должно быть ясно, что параметры модели содержат вектор весовых коэффициентов w; наверное, я вам также скажу, что w является вектором той же размерности, что и количество векторов признаков в X. Разумеется, так и должно быть, чтобы умножение матриц было корректным. Это подобно проверке на работоспособность, чтобы вы могли убедиться, что тут всё осмысленно.

В функции fit я хочу выполнить некоторое количество раз данный цикл:

w = образец из нормального распределения

for i = 1..T:

Поскольку из предыдущего текста мы знаем, что параметры модели содержат w, должно быть ясно, что в функции fit мы выполняем обновление w. Неудивительно, что это ещё одна проверка на работоспособность. Поскольку вы уже знакомы с линейной регрессией, то, вероятно, уже узнали тут градиентный спуск. Вновь-таки, я хочу, чтобы вы сделали вид, что мы ничего о нём не знаем. А что мы тогда знаем?

Мы знаем, что итеративные алгоритмы распространены в машинном обучении и что градиентный спуск является одним из самых распространённых. Вы, по всей видимости, можете заключить, что это какая-то форма градиентного спуска, но вам совершенно не нужно знать, что это такое, чтобы записать его в коде. Что мы ещё знаем? Мы знаем, что такое цикл for и как его записать на языке Python. Ещё мы знаем, что X – это матрица размерности NxD, содержащая входные данные, Y – вектор размерности N, содержащий целевые переменные, ŷ – это вектор размерности N, содержащий прогнозы, и что w – вектор размерности D, содержащий параметры модели. Кроме того, мы знаем, как выполнять сложение, вычитание и умножение данных объектов, так что можем записать алгоритм, даже не зная, как он работает. Конечно, можно многое узнать о том, как он работает, из формулы.

А вот чего мы не знаем. Мы не знаем значения T – количество предполагаемых выполнений цикла; и мы не знаем η – коэффициент обучения, и это совершенно нормально. Однако мы не можем допустить, чтобы отсутствие этих данных могло нас остановить. Истина в том, что будут возникать ситуации, где вам не сообщают точных чисел для подстановки; во многих случаях ответ будет звучать: «Это зависит от задачи». Важно, что вы должны привыкнуть к методу проб и ошибок и делать обоснованные предположения, опирающиеся на том, что вы уже знаете.

Что мы можем сделать? Мы знаем, что коэффициент обучения должен быть малой величиной. Насколько малой – зависит от задачи. Обычно это число меньше единицы, что-то вроде 0,1. Если такое значение не работает, можно попробовать его уменьшить; как правило, его уменьшают в 10 раз, то есть, к примеру, следующим значением коэффициента обучения, которое нужно опробовать, будет 10-2, потом 10-3 и так далее.

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

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

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

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

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

Как же должен выглядеть ваш окончательный код? Мы знаем, что нужно создать класс, содержащий как минимум функции fit и predict:

class MyModel:

    def predict(self, X):

        return X.dot(self.w)

    def fit(self, X, Y, eta, T):

        self.w = np.random.randn(X.shape[1])

        for _ in range(T):

            Y_hat = self.predict(X)

            self.w = self.w – eta*X.T.dot(Y_hat – Y)

    …

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

А вот вторая часть кода, где мы действительно создаём экземпляр класса, а затем его используем. Кроме того, я включил сюда функцию затрат – квадрат ошибок – просто для завершенности:

class MyModel:

    …

    def cost(self, X, Y):

    Y_hat = self.predict(X)

    return (Y_hat – Y).dot(Y_hat – Y)

X, Y = load_my_dataset()

model = MyModel()

model.fit(X, Y)

Что замечательно в этом шаблоне – так это то, что он не изменяется при переходе от одного алгоритма к другому. Основная структура всегда будет именно такой – по крайней мере, для обучения с учителем и без учителя. Как обсуждалось ранее, не имеет значения, какой алгоритм реализуется, будь то линейная регрессия, логистическая регрессия, нейронная сеть и так далее – они всегда будут иметь функции predict и fit, а изучение содержимого этих функций эквивалентно изучению алгоритма. Таким образом, в действительности один из способов начать писать код – начать лишь с этого шаблона, а затем заполнять в нём пробелы.

Как самостоятельно писать код (часть 2)

Это то, что в программировании называется разработкой на основе тестов (test-driven development, или TDD). При разработке на основе тестов мы начинаем с написания тестов, поскольку, во-первых, в нашем коде всегда должны быть тесты, и во-вторых, написание вначале тестов заставляет думать, как, по нашему мнению, должен работать API и позволяет принимать разумные проектные решения, не отвлекаясь на детали реализации. Как видите, это применимо к любому алгоритму машинного обучения, не только к линейной регрессии.

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

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

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

Допустим, вы пересмотрели теоретическую лекцию и изучили другие теоретические источники, однако до сих пор не можете понять, как всё это воплотить в коде. На этот случай есть несколько чудесных упражнений, которыми вы можете заняться. Посмотрите лекцию с написанием кода или сам код на Github; пройдитесь по нему и удостоверьтесь, что понимаете каждую строку. Далее, не подглядывая ещё раз в код, попытайтесь написать его самостоятельно. Это само по себе является чудесным учебным приёмом. Так, множество изученных мною алгоритмов я с первого раза не понимал. Я изучал их, рассматривая и анализируя чужую реализацию в коде. Я рассматривал каждую часть и спрашивал себя, что я понял правильно, а что – нет. Это помогает понять ранее допущенные ошибки. Что замечательно в реализации алгоритма в коде – что это работает и в обратном направлении: мы идём от теории к коду, однако рассуждения в обратном направлении, от кода, на самом деле помогают нам и лучше понять теорию. Я всегда говорю, что если вы не можете реализовать теорию в коде, значит, вы её не понимаете. Таким образом, если вы изучите 50% теории, но попробуете воплотить её в коде, глядя на существующую реализацию, и заставите код работать, то можете быть уверены, что теперь знаете более 50% теории, – процесс написания кода помог вам понять теорию больше, чем изучение теории самой по себе.

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

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

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

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

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

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

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

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

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

Ну и, наконец, не забывайте: «когда пришла пора писать код – вы должны писать код»!

Как успешно пройти этот курс (длинная версия)

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

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

Первая рекомендация заключается в следующем. Не забывайте, что я хочу, чтобы вы успешно прошли курс. В действительности именно поэтому я прежде всего и предлагаю данные рекомендации. Итак, в любой момент, когда вы просматриваете лекции и запутались, не стесняйтесь просто задать вопрос в разделе вопросов и ответов (Q&A), он же форум. Как правило, я отвечаю в течение нескольких часов или даже минут, если оказываюсь на сайте и вижу уведомление. Я отвечаю на все, на любые вопросы, так что даже если полагаете, что вопрос глупый, – всё равно задавайте. Так, например, время от времени мне задают вопрос – где взять код для данного курса. Разумеется, обычно мой ответ звучит так: «В лекции с названием «Где взять код для данного курса». К концу дня уже никого не волнует, был ли ваш вопрос банальным или глупым; если что-то мешает вам продвижению вперёд в курсе, я буду отвечать.

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

Обсуждение необходимого багажа знаний – одна из важнейших рекомендаций этой лекции. Как вы могли заметить, в описании курса я перечислил ряд необходимых предпосылок или, другими словами, вещей, которые вы должны знать, прежде чем браться за данный курс. У вас могут возникнуть вопросы: почему я прошу вас знать эти вещи? Почему бы не преподать их прямо в курсе? И почему по некоторым темам есть обзоры, хотя курс сосредоточен вовсе и не на них? Этот курс изучают множество слушателей – слушатели из почти всех стран мира, всех возрастов и образования. Что это значит? Это значит, что то, что является просто обзором для одних, может оказаться чем-то совершенно новым для других. Некоторые заявляют мне, что не знают математического анализа, а некоторые – что они очень отстали в программировании на языке Python. Однако если мы начнём учиться навыкам программирования на Python, другие возмутятся: «Эй, чего это мы занимается языком Python? Разве это тема курса по глубокому обучению?», и я склонен согласиться с тем, что мы должны оставаться в рамках темы курса. Как же нам примирить эти позиции? Дело в том, что мне не имеет смысла пытаться дать вам полный курс программирования в одном этом курсе, поскольку многое из него будет неуместно для значительного количества слушателей. Единственное логичное решение – назначить для всех нас общую точку рандеву. А как её определить? С помощью предпосылок, необходимых для изучения курса. На самом деле они настолько важны, что в ряде случаев я перечисляю их дважды на случай, если в первый раз вы их пропустили, и на всякий случай напоминаю ещё раз здесь.

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

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

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

А вот чего нельзя пропускать – так это реализации в коде. Воплощение данных алгоритмов в коде – это реальная проверка того, понимаете ли вы материал или нет, и дело тут не в том, чтобы «изобретать колесо» или в том, что вам не нужно делать что-либо, поскольку кто-то другой может сделать лучше. Это основополагающий принцип программирования, а не обучения. При разработке вам не понадобится писать собственную хеш-карту или собственный класс строк. Но что мы обычно делаем в качестве упражнения в типовом введении в программирование? В точности это – воплощаем в коде хеш-карты и строковые последовательности.

А зачем мы это делаем? Потому, что хотим получить лучшее понимание того, как всё это работает. Если сделать неверные допущения, то код, по всей вероятности, работать не будет. Но простое использование хеш-карт и строковых последовательной даёт лишь очень поверхностное понимание на уровне новичков, на уровне, который доступен всем. Это не то, что поможет вам выделиться на фоне конкурентов, хотя правда заключается в том, что вы никогда и ни в каком своём коде не будете использовать собственные хеш-карты или строковые последовательности – если, конечно, не являетесь специалистом именно в такого рода вещах. Ваши же попытки, по всей видимости, будут неэффективными и, вероятно, будут содержать ошибки, но это нормально, ведь вы это делаете не для того, чтобы сделать что-то идеально, а чтобы научиться. Вы могли заметить, что общая схема этого курса состоит в том, что мы всегда обсуждаем теорию, а затем воплощаем эту теорию в коде. То есть, мы всегда идём от теории к коду, в котором воплощается весь изученный нами материал. Если можете, постараетесь реализовать что-нибудь в коде до просмотра видео с кодом. В приложении есть лекция «Как самостоятельно писать код», в которой эта тема рассматривается более подробно наряду с рядом стратегий, как подходить к упражнениям по реализации в коде.

Подведём итог нашим рекомендациям, которые, как я полагаю, должны помочь вам успешно пройти данный курс. Первое: активно пользуйтесь разделом вопросов и ответов, даже если вы не можете правильно сформулировать вопрос. Второе: убедитесь, что ваши знания соответствуют необходимым требованиям для этого курса. Если вы не уверены, что они соответствуют, см. рекомендацию № 1. Третье: изо всех сил старайтесь воплощать в коде всё, что мы изучаем. Это настоящая проверка того, понимаете ли вы что-либо. Не забывайте, что совершенно нормально время от времени попадать в тупик. Когда я учился, то постоянно попадал в тупик. Это не значит, что я плохой или что плохими были учебники и видео, по которым я учился. Это значит, что было несоответствие между тем, что, по мнению автора, я знал, и тем, что я знал на самом деле, и как учащийся я прикладывал все усилия, чтобы заполнить этот пробел. Хорошее умение – научиться разделять понятия: вот это я понимаю, вот этого я не понимаю, и понимаю, что не понимаю, то есть здесь есть пробел.

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

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

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

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

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

Как же должен выглядеть ваш окончательный код? Мы знаем, что нужно создать класс, содержащий как минимум функции fit и predict:

class MyModel:

    def predict(self, X):

        return X.dot(self.w)

    def fit(self, X, Y, eta, T):

        self.w = np.random.randn(X.shape[1])

        for _ in range(T):

            Y_hat = self.predict(X)

            self.w = self.w – eta*X.T.dot(Y_hat – Y)

    …

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

А вот вторая часть кода, где мы действительно создаём экземпляр класса, а затем его используем. Кроме того, я включил сюда функцию затрат – квадрат ошибок – просто для завершенности:

class MyModel:

    …

    def cost(self, X, Y):

    Y_hat = self.predict(X)

    return (Y_hat – Y).dot(Y_hat – Y)

X, Y = load_my_dataset()

model = MyModel()

model.fit(X, Y)

Что замечательно в этом шаблоне – так это то, что он не изменяется при переходе от одного алгоритма к другому. Основная структура всегда будет именно такой – по крайней мере, для обучения с учителем и без учителя. Как обсуждалось ранее, не имеет значения, какой алгоритм реализуется, будь то линейная регрессия, логистическая регрессия, нейронная сеть и так далее – они всегда будут иметь функции predict и fit, а изучение содержимого этих функций эквивалентно изучению алгоритма. Таким образом, в действительности один из способов начать писать код – начать лишь с этого шаблона, а затем заполнять в нём пробелы.

Как самостоятельно писать код (часть 2)

Это то, что в программировании называется разработкой на основе тестов (test-driven development, или TDD). При разработке на основе тестов мы начинаем с написания тестов, поскольку, во-первых, в нашем коде всегда должны быть тесты, и во-вторых, написание вначале тестов заставляет думать, как, по нашему мнению, должен работать API и позволяет принимать разумные проектные решения, не отвлекаясь на детали реализации. Как видите, это применимо к любому алгоритму машинного обучения, не только к линейной регрессии.

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

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

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

Допустим, вы пересмотрели теоретическую лекцию и изучили другие теоретические источники, однако до сих пор не можете понять, как всё это воплотить в коде. На этот случай есть несколько чудесных упражнений, которыми вы можете заняться. Посмотрите лекцию с написанием кода или сам код на Github; пройдитесь по нему и удостоверьтесь, что понимаете каждую строку. Далее, не подглядывая ещё раз в код, попытайтесь написать его самостоятельно. Это само по себе является чудесным учебным приёмом. Так, множество изученных мною алгоритмов я с первого раза не понимал. Я изучал их, рассматривая и анализируя чужую реализацию в коде. Я рассматривал каждую часть и спрашивал себя, что я понял правильно, а что – нет. Это помогает понять ранее допущенные ошибки. Что замечательно в реализации алгоритма в коде – что это работает и в обратном направлении: мы идём от теории к коду, однако рассуждения в обратном направлении, от кода, на самом деле помогают нам и лучше понять теорию. Я всегда говорю, что если вы не можете реализовать теорию в коде, значит, вы её не понимаете. Таким образом, если вы изучите 50% теории, но попробуете воплотить её в коде, глядя на существующую реализацию, и заставите код работать, то можете быть уверены, что теперь знаете более 50% теории, – процесс написания кода помог вам понять теорию больше, чем изучение теории самой по себе.

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

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

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

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

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

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

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

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

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

Ну и, наконец, не забывайте: «когда пришла пора писать код – вы должны писать код»!

Как успешно пройти этот курс (длинная версия)

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

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

Первая рекомендация заключается в следующем. Не забывайте, что я хочу, чтобы вы успешно прошли курс. В действительности именно поэтому я прежде всего и предлагаю данные рекомендации. Итак, в любой момент, когда вы просматриваете лекции и запутались, не стесняйтесь просто задать вопрос в разделе вопросов и ответов (Q&A), он же форум. Как правило, я отвечаю в течение нескольких часов или даже минут, если оказываюсь на сайте и вижу уведомление. Я отвечаю на все, на любые вопросы, так что даже если полагаете, что вопрос глупый, – всё равно задавайте. Так, например, время от времени мне задают вопрос – где взять код для данного курса. Разумеется, обычно мой ответ звучит так: «В лекции с названием «Где взять код для данного курса». К концу дня уже никого не волнует, был ли ваш вопрос банальным или глупым; если что-то мешает вам продвижению вперёд в курсе, я буду отвечать.

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