Это самая подробная статья, объясняющая TensorFlow, которую я когда-либо читал!

TensorFlow глубокое обучение Python Node.js
Редактор отдела планирования | Натали
Автор | Джейкоб Бакман
Переводчик | Ван Цян, Умин
Редактор | Винсент, Дебра
Руководство по передовой ИИ:«Меня зовут Джейкоб, и я являюсь научным сотрудником проекта Google AI Residency. Когда я присоединился к проекту летом 2017 года, у меня был большой опыт программирования и глубокое понимание машинного обучения, но я никогда не использовал Tensorflow раньше. Я думал, что смогу быстро освоить Tensorflow своими силами, но я не ожидал, что процесс его изучения будет таким взлетам и падениям. Даже после присоединения к проекту в течение нескольких месяцев я иногда путался и не не знаю, как реализовать мои новые идеи с кодом Tensorflow.

Этот пост в блоге похож на письмо в бутылке, которое я написал себе в прошлом: оглядываясь назад, я хотел бы, чтобы у меня было это вводное введение, когда я начал учиться. Я также надеюсь, что эта статья поможет моим коллегам и даст им ссылку. «AI Frontline перевел текущую статью инженера Google Brain обо всех аспектах трудностей, возникающих в процессе изучения Tensorflow, в надежде помочь всем.

Для получения дополнительных галантерейных товаров, пожалуйста, обратите внимание на публичный аккаунт WeChat «AI Frontline» (ID: ai-front)
Чего не хватает в прошлых уроках?

Прошло три года с момента выпуска Tensorflow, и сегодня он является краеугольным камнем экосистемы глубокого обучения. Однако для новичков это не очень просто понять, особенно по сравнению с библиотеками нейронных сетей, определяющими на лету, такими как PyTorch или DyNet.

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

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

Целевая аудитория

Это руководство предназначено для практиков, которые имеют некоторый опыт программирования и машинного обучения и хотят начать работу с Tensorflow. Это могут быть: студент CS, который хочет использовать Tensorflow для финального проекта курса глубокого обучения; инженер-программист, которого только что перевели на проект, связанный с глубоким обучением; или сбитый с толку новичок в Google AI (громко передайте Джейкобу привет) . Если вам нужно начать с основ, см. ресурсы ниже. Если вы все это знаете, давайте начнем!

Понимание Tensorflow

Tensorflow — это не обычная библиотека Python.

Большинство библиотек Python написаны как естественные расширения Python. Когда вы импортируете библиотеку, вы получаете набор переменных, функций и классов, которые дополняют и расширяют «инструментарий» вашего кода. При использовании этих библиотек вы знаете, что они будут производить. Я думаю, что пришло время отбросить эти представления, говоря о Tensorflow, которые принципиально несовместимы с философией Tensorflow и не отражают то, как TF взаимодействует с другим кодом.

Связь между Python и Tensorflow можно сравнить с связью между Javascript и HTML. Javascript — это полнофункциональный язык программирования, с помощью которого можно добиться всевозможных замечательных эффектов. HTML — это структура для представления некоторого типа практической вычислительной абстракции (в данном случае контента, который может быть отображен веб-браузером). Роль Javascript на интерактивной веб-странице состоит в том, чтобы собрать HTML-объект, который видит браузер, и затем взаимодействовать с ним, обновляя его до нового HTML, когда это необходимо.

Подобно HTML, Tensorflow представляет собой структуру для представления некоторого типа вычислительной абстракции, называемой «вычислительным графом». Когда мы работаем с Tensorflow в Python, первое, что мы делаем с кодом Python, — это собираем вычислительный граф. Наша вторая задача после этого — взаимодействовать с ним (используя «сеансы» Tensorflow). Но важно помнить, что граф вычислений находится не внутри переменной, а в глобальном пространстве имен. Шекспир однажды сказал: «Вся оперативная память — это сцена, а все переменные — не что иное, как указатели».

Первая ключевая абстракция: вычислительный граф

Когда мы просматриваем документацию Tensorflow, мы иногда находим ссылки на «граф» и «узел». Если вы внимательно читаете, копаете глубже, вы, возможно, даже обнаружили эту страницу, которая охватывает то, что я объясню подробно, в более точном и техническом стиле. Этот раздел начнется с верхнего уровня, охватывая ключевые интуитивно понятные концепции, но пропуская некоторые технические детали.

Так что же такое вычислительный граф? По сути, это глобальная структура данных: граф вычислений — это ориентированный граф, содержащий инструкции по методам вычислений.

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

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

Проверьте это! У нас есть узел, который содержит константу: 2. Я знаю, что вас удивила функция tf.constant. Когда мы печатаем эту переменную, мы видим, что она возвращает объект tf.Tensor, который является указателем на только что созданный узел. Чтобы подчеркнуть мысль, вот еще один пример:

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

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

Хорошо, давайте сделаем еще один шаг.

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

Итак, two_node указывает на узел, содержащий 2, three_node указывает на узел, содержащий 3, а sum_node указывает на узел, содержащий...+? какова ситуация? Разве он не должен содержать 5?

Как оказалось, нет. Вычислительные графики содержат только этапы вычислений, а не результаты. По крайней мере... пока нет!

Вторая ключевая абстракция: сеанс

Если вы ошибочно понимаете, что в абстракции TensorFlow также есть соревнование March Madness (напряженный сезон чемпионата по студенческому баскетболу в США), «Session» будет семенем № 1 каждый год. Эта неловкая честь связана с нелогичным именованием сеансов, но настолько широко распространенным — почти каждая программа Tensorflow вызовет tf.Session() хотя бы один раз.

Роль сеанса заключается в управлении распределением и оптимизацией памяти, что позволяет нам фактически выполнять вычисления, указанные графом. Думайте о графе вычислений как о «шаблоне» для вычислений, которые мы хотим выполнить: он перечисляет все шаги. Чтобы использовать этот граф, нам также нужно инициировать сеанс, который позволяет нам фактически выполнить задачу. Например, все узлы шаблона просматриваются, чтобы выделить набор памяти для хранения результатов вычислений. Чтобы использовать Tensorflow для различных вычислений, нам нужны как графы, так и сеансы.

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

После создания объекта сеанса вы можете использовать sess.run(node) для возврата значения узла, и Tensorflow выполнит все вычисления, необходимые для определения значения.

Чудесно! Мы также можем передать список sess.run([node1, node2, ...]) и получить несколько выходных данных:

Как правило, вызов sess.run() является одним из самых больших узких мест TensorFlow, поэтому чем реже вы его вызываете, тем лучше. Возвращайте несколько элементов в одном вызове sess.run(), если это возможно, вместо того, чтобы делать несколько вызовов.

Заполнители и feed_dict

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

Самый простой способ — использовать заполнители. Заполнитель — это узел, который принимает внешний ввод.

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

Чтобы указать значение, мы используем атрибут feed_dict функции sess.run().

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

Почему второй вызов sess.run() не работает? Мы не проверяем input_placeholder, почему возникает ошибка, связанная с input_placeholder? Ответ заключается в последней ключевой абстракции Tensorflow: вычислительных путях. К счастью, эта абстракция очень интуитивно понятна.

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

Проверьте путь вычисления sum_node:

Все три узла необходимо оценить, чтобы вычислить значение sum_node. Кроме того, он включает наши незаполненные заполнители и объясняет исключения!

Вместо этого проверьте вычислительный путь three_node:

В зависимости от структуры графа нам не нужно вычислять все узлы, чтобы оценить те, которые нам нужны! Поскольку нам не нужно оценивать placeholder_node для оценки three_node, запуск sess.run(three_node) не вызовет исключения.

Тот факт, что Tensorflow автоматически направляет вычисления только через необходимые узлы, является его огромным преимуществом. Это может сэкономить много времени выполнения, если граф вычислений очень большой и содержит много ненужных узлов. Это позволяет нам строить большие «многоцелевые» графы, которые используют один общий набор основных узлов для выполнения различных задач в зависимости от выбранных вычислительных путей. Почти для любого приложения важно рассматривать вызывающий метод sess.run() с точки зрения используемого вычислительного пути.

Переменные и побочные эффекты

До сих пор мы видели два типа узлов без предков: tf.constant (один и тот же для каждого запуска) и tf.placeholder (один и тот же для каждого запуска). Существует также третий вид узла: обычно имеет то же значение, но также может быть обновлен до нового значения. Здесь используются переменные.

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

Чтобы создать переменную, используйте tf.get_variable(). Первые два аргумента для tf.get_variable() обязательны, остальные необязательны. Это tf.get_variable(имя,форма). name — это строка, которая однозначно идентифицирует этот переменный объект. Он должен быть уникальным в глобальном графе, поэтому убедитесь, что нет повторяющихся имен. shape — это массив целых чисел, соответствующих форме тензора, и его синтаксис прост — одно целое число для каждого измерения, расположенное по порядку. Например, матрица 3 на 8 может иметь форму [3,8]. Чтобы создать скаляр, используйте пустой список в качестве формы: [].

Было найдено еще одно исключение. Когда переменный узел создается впервые, его значение в основном «нулевое», и любая попытка его оценки вызовет это исключение. Мы можем выполнять вычисления с переменной только после присвоения ей значения. Существует два основных способа присвоения значений переменным: инициализаторы и tf.assign(). Давайте сначала посмотрим на tf.assign():

tf.assign(target,value) имеет некоторые уникальные свойства по сравнению с узлами, которые мы видели до сих пор:

  • Определить операции. tf.assign(target,value) ничего не делает, оно всегда равно значению.

  • побочный эффект. Поскольку вычисление «проходит через» узел assign_node, оно оказывает побочные эффекты на другие узлы графа. В этом случае побочным эффектом является замена значения count_variable значением, хранящимся в zero_node.

  • независимая кромка. Несмотря на то, что узлы count_variable и assign_node связаны в графе, ни один из них не зависит от других узлов. Это означает, что при вычислении любого узла вычисления не проходят обратно через это ребро. Однако assign_node зависит от zero_node, которому нужно знать, что назначать.

Узлы «побочных эффектов» используются в большинстве рабочих процессов глубокого обучения Tensorflow, поэтому убедитесь, что вы хорошо их понимаете. Когда мы вызываем sess.run(assign_node), путь вычислений будет проходить через assign_node и zero_node.

Поскольку вычисления проходят через любой узел на графике, они также позволяют вступать в силу побочным эффектам (показанным зеленым), контролируемым этим узлом. Из-за особого побочного эффекта tf.assign память, связанная с переменной count_variable (ранее «нулевая»), теперь постоянно установлена ​​на 0. Это означает, что при следующем вызове sess.run(count_variable) исключений не будет. Вместо этого мы получим 0.

Далее давайте посмотрим на инициализатор:

Что тут происходит? Почему не работает инициализатор?

Проблема заключается в разделении сеанса и графа. Мы указали свойству инициализатора get_variable значение const_init_node, но оно просто добавляет новое соединение между узлами в графе. Мы не сделали ничего, связанного с возникновением исключения: память, связанная с узлом переменной (хранящаяся в сеансе, а не в графе), по-прежнему «нулевая». Нам нужно, чтобы const_init_node обновлял переменную через сеанс.

Для этого мы добавляем еще один специальный узел: init = tf.global_variables_initializer(). Подобно tf.assign(), это узел с побочными эффектами. В отличие от tf.assign(), нам не нужно указывать его ввод! tf.global_variables_initializer() будет просматривать глобальный граф по мере его создания, автоматически добавляя зависимости к каждому tf.initializer в графе. Когда мы вызываем sess.run(init), он говорит каждому инициализатору выполнять свою работу, инициализируя переменную, чтобы не было ошибки при вызове sess.run(count_variable) .

совместное использование переменных

Вы можете встретить код Tensorflow с совместным использованием переменных, код имеет свою область действия и устанавливает «повторное использование = True». Я настоятельно рекомендую вам не использовать совместное использование переменных в вашем коде. Если вы хотите использовать одну переменную в нескольких местах, просто используйте указатель на этот узел переменной и используйте его при необходимости. Другими словами, tf.get_variable() следует вызывать только один раз для каждого параметра, который предполагается хранить в памяти.

оптимизатор

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

В глубоком обучении типичное обучение «внутреннего цикла» выглядит следующим образом:

  • получить ввод и true_output

  • Вычисляет «догадку» на основе входных данных и параметров

  • Вычисляет «убыток» на основе разницы между догадкой и true_output

  • Обновить параметры на основе градиента потерь

Давайте поместим все в один скрипт и решим простую задачу линейной регрессии:

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

Теперь, когда вы хорошо понимаете основные концепции Tensorflow, этот код будет легко объяснить! Первая строка, оптимизатор = tf.train.GradientDescentOptimizer(1e-3), не добавляет узлы в граф. Он просто создает объект Python, содержащий несколько полезных функций. Вторая строка train_op = optimizer.minimize(loss) добавляет в граф узел и назначает указатель на train_op. Узел train_op не имеет вывода, но имеет очень сложный побочный эффект:

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

По сути, когда мы вызываем sess.run(train_op), он выполняет для нас операцию градиентного спуска для всех переменных. Конечно, нам также нужно использовать feed_dict для заполнения заполнителей ввода и вывода, и мы также хотим распечатать эти потери, так как это легче отлаживать.

Отладка с помощью tf.Print

Когда вы начинаете делать более сложные вещи с Tensorflow, вам нужно отлаживать. В общем, изучение того, что происходит на вычислительном графе, затруднено. Вы не можете использовать обычные операторы печати Python, потому что у вас никогда не будет доступа к значениям для печати — они заблокированы в вызове sess.run(). Например, предположим, что вы хотите проверить вычисленное промежуточное значение, которое еще не существует, перед вызовом sess.run(). Однако, когда вызов sess.run() возвращается, промежуточное значение исчезает!

Давайте рассмотрим простой пример.

Мы видим, что результат равен 5. Но что, если мы хотим проверить промежуточные значения two_node и three_node? Один из способов проверки промежуточных значений — добавить возвращаемый параметр в sess.run(), который указывает на каждый промежуточный узел для проверки, а затем распечатать его после возврата.

Обычно с этим не возникает проблем, но когда код становится более сложным, это может стать немного неудобным. Более удобный способ — использовать оператор tf.Print. Как ни странно, tf.Print на самом деле является узлом Tensorflow, который имеет выходные данные и побочные эффекты! У него есть два обязательных параметра: узел для копирования и список содержимого для печати. «Копируемым узлом» может быть любой узел в графе, tf.Print — это операция идентификации, связанная с «копируемым узлом», то есть она выводит копию своего ввода. Однако у него есть побочный эффект печати всех значений в «списке печати».

Важный, но несколько тонкий момент о tf.Print : печать на самом деле просто побочный эффект. Как и со всеми другими побочными эффектами, печать происходит только тогда, когда вычисления проходят через узел tf.Print. Если узел tf.Print не находится в вычисленном пути, ничего не будет напечатано. Даже если исходный узел, который копирует узел tf.Print, находится на вычислительном пути, сам узел tf.Print может и не находиться. Обратите внимание на этот вопрос! Когда это происходит, это может очень расстраивать, и вам нужно изо всех сил пытаться понять, что не так. В общем, лучше всего создать узел tf.Print сразу после создания копируемого узла.

Здесь есть отличный ресурс (https://wookayin.github.io/tensorflow-talk-debugging/#1) с более практичными советами по отладке.

в заключении

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

Оригинальная ссылка:

https://jacobbuckman.com/post/tensorflow-the-confusing-parts-1/#understanding-tensorflow