Библиотека глубокого обучения Tensorflow

Библиотека глубокого обучения Tensorflow



Tensorflow (далее — TF) — довольно молодой фреймворк для глубокого машинного обучения, разрабатываемый в Google Brain. Долгое время фреймворк разрабатывался в закрытом режиме под названием DistBelief, но после глобального рефакторинга 9 ноября 2015 года был выпущен в open source. За год с небольшим TF дорос до версии 1.0, обрел интеграцию с keras, стал значительно быстрее и получил поддержку мобильных платформ. В последнее время фреймворк развивается еще и в сторону классических методов, и в некоторых частях интерфейса уже чем-то напоминает scikit-learn. До текущей версии интерфейс менялся активно и часто, но разработчики пообещали заморозить изменения в API. Мы будем рассматривать только Python API, хотя это не единственный вариант — также существуют интерфейсы для C++ и мобильных платформ.


Установка


TF устанавливается стандартно через python pip. Есть нюанс: существуют отдельные алгоритмы установки для работы на CPU и на видеокартах.


В случае с CPU всё просто: нужно поставить из pip пакет под названием tensorflow.


Во втором случае нужно:


  1. проверить совместимость с видеокартой. Параметр CUDA Compute Capability должен быть больше 3.0, найти его для своей видеокарты можно тут
  2. Установить CUDA Toolkit восьмой версии
  3. Установить cuDNN версии 5.1
  4. Установить из pip пакет tensorflow-gpu


Впрочем, документация утверждает, что поддерживаются и более ранние версии CUDA Toolkit и cuDNN, но рекомендует устанавливать версии, указанные выше.


Разработчики рекомендуют устанавливать TF в отдельную среду с virtualenv, чтобы избежать возможные проблемы с версионированием и зависимостями.


Еще один вариант установки — Docker. По умолчанию из контейнера будет работать только CPU-версия, но если использовать специальный nvidia docker, то можно использовать и GPU.


Сам я не пробовал, но говорят, что TF работает даже с Windows. Установка проводится через тот же pip и, говорят, работает без проблем.


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


Подробно процесс установки описан тут.


Документация


Документации и примеров очень много.



Лучше всего ориентироваться на официальную документацию — из-за быстрого развития и частой смены api, в интернете очень много туториалов и скриптов, которые ориентированы на старые версии (ну как старые… полугодовой давности) со старым API, они не будут работать с последними версиями фреймворка.


Базовые элементы TF


С помощью «Hello, world» убедимся, что всё установилось правильно:


import tensorflow as tf # подключаем TF
hello = tf.constant('Hello, TensorFlow!') # создаем объект из TF
sess = tf.InteractiveSession() # создаем сессию
print(sess.run(hello)) #сессия "выполняет" объект
>>> b'Hello, TensorFlow!'


Первой же строчкой подключаем TF. Уже сложилось правило вводить для фреймворка соответствующее сокращение. Этот же кусочек кода встречается в документации и позволяет удостовериться в том, что всё установилось правильно.


Граф вычислений


Работа c TF строится вокруг построения и выполнения графа вычислений. Граф вычислений — это конструкция, которая описывает то, каким образом будут проводиться вычисления. В классическом императивном программировании мы пишем код, который выполняется построчно. В TF привычный императивный подход к программированию необходим только для каких-то вспомогательных целей. Основа TF — это создание структуры, задающей порядок вычислений. Программы естественным образом структурируются на две части — составление графа вычислений и выполнение вычислений в созданных структурах.


Граф вычислений в TF по смыслу не отличается от такового в Theano. В предыдущей статье цикла дано отличное описание этой сущности.


В TF граф состоит из плейсхолдеров, переменных и операций. Из этих элементов можно собрать граф, в котором будут вычисляться тензоры. Тензоры — многомерные массивы, они служат «топливом» для графа. Тензором может быть как отдельное число, вектор признаков из решаемой задачи или изображение, так и целый батч описаний объектов или массив из изображений. Вместо одного объекта мы можем передать в граф массив объектов и для него будет вычислен массив ответов. Работа TF с тензорами похожа на то, как обрабатывает массивы numpy, в функциях которого можно указать ось массива, относительно которой будет выполняться вычисление.


Сессии


Вычислительные графы выполняются в сессиях. Объект сессии (tf.Session) скрывает в себе контекст выполнения графа — необходимые ресурсы, вспомогательные классы, адресные пространства.

Существует два типа сессий — обычные, которые реализованы в tf.Session и интерактивные (tf.InteractiveSession). Разница между ними в том, что интерактивная сессия больше подходит для выполнения в консоли и сразу определяет себя как сессия по умолчанию. Основной эффект — объект сессии не нужно передавать в функции вычисления как параметр. В примерах далее я буду считать, что в данный момент работает интерактивная сессия, которую мы объявили в первом примере, и когда понадобится обращение к сессии, буду обращаться к объекту sess.


Далее в посте будут появляться стандартные для TF картинки с изображениями графов, сгенерированные встроенной утилитой под названием Tensorboard. Обозначения там вот такие:


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





Тензоры, операции и переменные


Создадим, к примеру, тензор, заполненный нулями.


zeros_tensor = tf.zeros([3, 3])


Вообще, API в TF будет во многом напоминать numpy и tf.zeros() — далеко не единственная функция, имеющая прямой аналог в numpy. Чтобы увидеть значение тензора, его нужно выполнить. Подробнее о выполнении графа чуть ниже, пока что обойдемся тем, что выведем значение тензора и сам тензор.


print(zeros_tensor.eval())
print(zeros_tensor)
>>> [[ 0.  0.  0.]
 [ 0.  0.  0.]
 [ 0.  0.  0.]]
>>> Tensor("zeros_1:0", shape=(3, 3), dtype=float32)


Различие между строчками состоит в том, что в первой строке происходит вычисление тензора, а во второй строке мы просто печатаем представление объекта.


Описание тензора показывает нам несколько важных вещей:


  1. У тензоров есть имена. У нашего оно zeros:0
  2. Существует понятие формы тензора, оно похоже на размерность массива из numpy.
  3. Тензоры типизированы и типы для них задаются из библиотеки.


Над тензорами можно совершать разнообразные операции:


a = tf.truncated_normal([2, 2])
b = tf.fill([2, 2], 0.5)
print(sess.run(a + b))
print(sess.run(a - b))
print(sess.run(a * b))
print(sess.run(tf.matmul(a, b)))

>>> [[-1.12130964 -1.02217746]
 [ 0.85684788  0.5425666 ]]
>>> [[ 0.35249496  0.96118248]
 [-1.55395389 -1.18111515]]
>>> [[-0.06559008 -0.11100233]
 [ 0.51474923 -0.27813852]]
>>> [[-0.16202734 -0.16202734]
 [-0.8864761  -0.8864761 ]]


В примере выше мы используем конструкцию sess.run — это метод исполнения операций графа в сессии. В первой строчке я создал тензор из усеченного нормального распределения. Для него используется стандартная генерация нормального распределения, но из него исключается всё, что выпадает за пределы двух стандартных отклонений. Помимо этого генератора есть равномерное, простое нормальное, гамма и еще несколько других распределений. Очень характерная для TF штука — уже реализовано большинство популярных вариантов выполнения операции и, возможно, перед изобретением велосипеда стоит взглянуть на документацию. Второй тензор — это заполненный значением 0.5 многомерный массив размера 2х2 и это что-то похожее на numpy и его функции создания многомерных массивов.


Создадим теперь переменную на основе тензора:


v = tf.Variable(zeros_tensor)


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


sess.run(v.initializer)
v.eval()

>>> array([[ 0.,  0.,  0.],
       [ 0.,  0.,  0.],
       [ 0.,  0.,  0.]], dtype=float32)


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


x = tf.placeholder(tf.float32, shape=(4, 4))


Еще такой пример использования. Здесь два плейсхолдера служат входными узлами для сумматора:


a = tf.placeholder("float")
b = tf.placeholder("float")
y = tf.multiply(a, b)
print(sess.run(y, feed_dict={a:100, b:500}))
>>> 50000.0


Простейшие вычисления.


В качестве примера создадим и вычислим несколько выражений.


x = tf.placeholder(tf.float32)
f =  1 + 2 * x + tf.pow(x, 2)
sess.run(f, feed_dict={x: 10})
>>> 121.0


И граф вычисления:




x и y, указывающие на операции в этой схеме — это дополнительные параметры, вместо которых могли бы быть ребра графа, но мы подставили в f скалярные значения 1 и 2 и это просто обозначение в графе для чисел. В этом примере мы создаем плейсхолдер и на его основе — граф выражения 

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


Попробуем собрать что-нибудь более практически значимое.

Вот, например, сигмоида: 



x = tf.placeholder(dtype=tf.float32)
sigma = 1 / (1 + tf.exp(-x))
sigma.eval(feed_dict={x: np.linspace(-5, 5) })


И вот такой граф для неё.




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


В целом работа с тензорами напоминает работу с массивами в numpy. Однако, есть некоторые отличия. Когда мы хотим понизить размерность, каким-либо образом объединив значения в тензоре по определенному измерению, мы пользуемся теми функциями, которые начинаются с reduce_.

Если сравнить c API Theano — в TF нет деления на векторы и матрицы, но вместо этого приходится следить за размерностями тензоров в графе и есть механизм вывода формы тензора, который позволяет получить размерности еще до runtime.


Машинное обучение


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




Куда же без этой картинки


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

Данные будут синтетические — синус с нормальным шумом:


x = np.linspace(0, 10, 1000)
y = np.sin(x) + np.random.normal(size=len(x))


Выглядеть они будут примерно так:




Я еще разобью выборку на обучающую и контрольную в пропорции 70/30, но этот и некоторые другие рутинные моменты оставлю в полном исходнике, ссылка на который будет чуть ниже.


Сначала построим простую линейную регрессию.


x_ = tf.placeholder(name="input", shape=[None, 1], dtype = tf.float32)
y_ = tf.placeholder(name= "output", shape=[None, 1], dtype = tf.float32)

model_output = tf.Variable(tf.random_normal([1]), name='bias') + tf.Variable(tf.random_normal([1]), name='k') * x_


Тут я создаю два плейсхолдера для признака и ответа и формулу вида 

.

Нюанс — в плейсхолдере параметр формы (shape) содержит None. Размерность плейсхолдера означает, что плейсходер потребляет двумерные тензоры, но по одной из осей размер тензора не определен и может быть любым. Это сделано для того, чтобы пользователь мог передавать значения в граф сразу целыми батчами. Такие специфические размерности называют динамическими, TF рассчитывает действительную размерность связанных элементов во время выполнения графа.


Плейсхолдер для признака используется в формуле, а вот плейсхолдер для ответа я подставлю в функцию потерь 

:


loss = tf.reduce_mean(tf.pow(y_ - model_output, 2)) # функция потерь


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


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


gd = tf.train.GradientDescentOptimizer(0.001) #оптимизатор
train_step = gd.minimize(loss)


Инициализация переменных — она необходима для дальнейших вычислений:


sess.run(tf.global_variables_initializer())


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


n_epochs = 100
train_errors = []
test_errors = []
for i in tqdm.tqdm(range(n_epochs)): # 100 
    _, train_err = sess.run([train_step, loss ], feed_dict={x_:X_Train.reshape((len(X_Train), 1)) , y_: Y_Train.reshape((len(Y_Train), 1))})
    train_errors.append(train_err)
    test_errors.append(sess.run(loss, feed_dict={x_:X_Test.reshape((len(X_Test), 1)) , y_: Y_Test.reshape((len(Y_Test), 1))}))


Первое выполнение сессией одновременно операций train_step и loss делает сразу и обучение, и оценку ошибки на обучающей выборке, т.е. собственно оценку того, как хорошо мы запомнили выборку. Второе выполнение сессии — подсчет потерь на тестовой выборке. В параметре feed_dict я передаю в граф значения для плейсхолдеров и делаю reshape для того, чтобы массивы данных совпадали по размерности. Там, где в плейсхолдере стояло значение None, можно передать любое число. Тензоры с такими неопределенными размерами называются динамическими, и вот тут я их использую, чтобы передавать в граф батчи с примерами для обучения.


Получается вот такая динамика обучения:




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




А вот и результаты вычисления модели:




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


sess.run(model_output, feed_dict={x_:x.reshape((len(x), 1))})


Тут в граф я передаю значение только для плейсхолдера x_ — остальное просто не нужно для вычисления выражения model_output.


Полный листинг программы есть тут. Результат получился предсказуемым для такой простой линейной модели.

1 часть

Report Page