Как понять низкоуровневый API tensorflow?

Введение

Всем привет !

Как вы могли заметить, я фанат марвел, но дело не в этом. Поговорим о нейронных сетях, математике и тензорном потоке.

Для кого предназначена эта статья?

Я написал эту статью для новичков в tensorflow, но с сильным математическим опытом в нейронных сетях и (или) с опытом работы с высокоуровневыми API, такими как Keras, PyTorch и т. д. Если вы совсем новичок в этом, не волнуйтесь, вы можете прочитать одну мою статью о начале в нейронных сетях и можете начать изучать эту статью.

Какова цель этой статьи?

В этой статье мы рассмотрим низкий API Tensorflow для создания простой нейронной сети.

Хорошо, начнем!

Простая проблема для решения

Конечно, мы можем взять здесь, например, самые популярные наборы данных MNIST, Iris или другие. Но для лучшего понимания возьмем самую глупую задачу с таким же уровнем сложности набора данных.

Какой фильм посмотреть сегодня?

Возьмем для примера 5 фильмов и опишем их только тремя признаками в состоянии истинно/ложно (1/0). Особенности будут как раз в жанрах кино: драма, комедия, боевик. И будем оценивать каждый фильм числовым знаком с плавающей запятой от 0 до 1, где 0 — категорически не нравится, а 1 — категорически нравится.

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

  1. Покемон-детектив Пикачу: (0: драма, 1: комедия, 1: боевик) = 1,0
  2. Дэдпул 2: (1: драма, 1: комедия, 1: боевик) = 0,5
  3. Дьявол носит Prada: (1: драма, 1: комедия, 0: боевик) = 0,0
  4. Американский пирог: (0: драма, 1: комедия, 0: боевик) = 0,0
  5. Джон Уик: (1: драма, 0: комедия, 1: боевик) = 0,7

Приступаем к написанию кода. Я буду использовать Python 3.6.

import numpy as np

movie_genres = np.array([
    [0, 1, 1],
    [1, 1, 1],
    [1, 1, 0],
    [0, 1, 0],
    [1, 0, 1]],
    dtype='float32')
movie_ratings = np.array([
    [1.0],
    [0.5],
    [0.0],
    [0.0],
    [0.7]],
    dtype='float32')

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

Давайте отложим подготовительную часть и приступим к изучению tensorflow.

Тензорный поток

Что такое тензорный поток?

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

Что означает тензор? Просто простыми словами:

Тензор — это геометрический объект или структура данных в виде n-мерного массива, где n значений варьируются от 0 до бесконечности.

Например: 0-мерный тензор — это скаляр (а=1), 1-мерный (вектор), 2-мерный (матричный), 3-мерный (куб или другие), 4-мерный и более (гиперпространственные объекты). Не бойтесь тензоров.

Почему тензорам уделялось большое внимание в этой структуре?

Давайте пересмотрим, что такое искусственная нейронная сеть.

ВЫХОД = АКТИВАЦИЯ (ВХОД x ВЕС)

Если у нас больше входных данных (например, N), то у нас будет N нейронов с N весами (в случае, если мы создадим только один плотный слой).

И мы можем регулировать форму вывода как многие ко многим или многие к одному и т. д.

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

В тензорном потоке у нас есть 3 типа тензоров:

  • tf.Variable(initial_value or shape or data type) — изменяемый тип как структура данных, используемая для хранения весов.
  • tf.constant(initial_value or shape or data type) — неизменяемый тип как структура данных, используемая для хранения обучающих данных или других констант.
  • tf.placeholder(initial_value or shape or data type) — изменяемый тип как структура данных, используемая для хранения обучающих данных.

Следующий важный шаг — понять, как работает tensorflow.

Tensorflow работает с графами и тензорами.

Что означает график?

График — математический абстрактный объект, состоящий из вершин (точек, узлов) и соединяющих их ребер (линий, ссылки).

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

Это нейронная сеть или граф? 2 в 1.

Вернемся к нашей задаче и подумаем, что нам следует делать.

Очень важно знать свои данные! Формы, типы и т. д. И планируйте свои действия!

У нас есть двумерный массив в качестве входных данных с формой (5, 3) и dtype= float32, наши цели с формой (5, 1) и dtype = float32. Я использовал поплавки, потому что наши веса тоже будут поплавками.

Итак, нам нужно произвести операцию в узле графа с входными данными и весами. Какую форму будут иметь гири? Согласно правилам скалярного произведения и здравому смыслу, тензор весов будет иметь вид = (3, 1).

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

В псевдокоде это будет:

input_tensor = array(5, 3)
weights = array(3, 1)

output = activation_function(dot_product(input_tensor, weights))

Теперь давайте шаг за шагом напишем сеанс tensorflow. Мы просто преобразуем этот псевдокод с правильным синтаксисом.

Мы уже подготовили наши данные в формате numpy.

Tensorflow работает в формате сессий. Прежде всего нам нужно определить сеанс:

with tf.Session() as sess:
# code to run

или мы можем сделать это таким образом:

sess = tf.Session()
# code to run
sess.close() # close your session or it will lead to errors

Используя конструкцию with--as-, мы уверены, что сессия будет закрыта после использования. В противном случае мы должны закрыть его вручную с помощью команды «.close». И еще одна важная вещь: если вы не определите график перед сеансом и не используете его в аргументе графика сеанса, сеанс создаст график по умолчанию.

Далее нужно определить наши тензоры. Здесь мы просто определяем их dtypes (для данных) и напрямую заполняем их для весов:

# DATA: inputs as X and targets as y
X = tf.placeholder(dtype=tf.float32, name='INPUT')
y = tf.placeholder(dtype=tf.float32, name='TARGET')
# TRAINING PARAMETERS: weights as W
W = tf.Variable(tf.random_normal([3, 1], stddev=0.3), name='WEIGHTS')

Мы сделали 3 тензора. X и y просто объявлены с правильными типами и именами. Когда мы создали тензор W, мы заполнили его случайными числами (например, np.random.rand(shape)) и стандартным отклонением = 0,3.

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

# FORWARD PASS: one layer perceptron with sigmoid activation
prediction = tf.sigmoid(tf.matmul(X, W), name='OUTPUT')

Примечание: tf.matmul() является аналогом функции numpy.dot(). И приятный бонус в том, что tensorflow уже сделал сигмовидную функцию (и многие другие).

Самое интересное — это обучение.

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

В своей статье я объяснил, как это сделать через numpy. Здесь мы сделаем то же самое с тензорным потоком.

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

# LOSS: MSE in tensorflow way
loss = tf.losses.mean_squared_error(y, prediction)

или таким образом вручную (очень удобно для лучшего понимания):

loss = tf.reduce_mean(tf.pow(prediction - y, 2))

И нам нужно определить оптимизатор и применить метод минимизации () к потерям:

# TRAINING PART: optimizer (Adam or might be SGD) minimizes loss value
optimizer = tf.train.AdamOptimizer(learning_rate=0.001).minimize(loss)
# THE END of preparation works

или мы можем сделать это вручную в лучших терминах BP для лучшего понимания, конечно:

derivative = tf.gradients(xs=W, ys=loss)
derivative = tf.reshape(derivative, shape=(3, 1))
# derivative of sigmoid function as f(x) is f(x)*(1-f(x)) 
learning_rate = tf.constant(0.01)
update_weights = W.assign(W - tf.multiply(learning_rate, derivative), name='UPD_W')

Здорово. Все приготовления были закончены. Давайте запустим это.

Мы будем обучать его на 5000 итераций (эпох). Но прежде чем мы приступим к работе, нам нужно инициализировать все переменные в условиях тензорного потока:

# INITIALIZATION of all tensors and functions
init = tf.global_variables_initializer()
sess.run(init)

Примечание: чтобы запустить узел в графе, вам нужно сделать это, конечно, внутри сеанса и с использованием session.run(function_call()).

Но как заполнить тензоры? Помните, в начале мы объявили два тензора и не указали значения? Пришло время сделать это с помощью аргумента feed_dict в методе «.run()». feed_dict использует словарь, где ключи — это переменные для заполнения, а значения — это значения (извините за тавтологию).

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

В нашем случае нам нужно заполнить только тензоры X и y жанров и рейтингами и начать процесс обучения:

import matplotlib.pyplot as plt
# 5000 training epochs (iterations)
losses = []
for i in range(5000):
    # with this command we computes new weights
    _, loss_value = sess.run([update_weights, loss], feed_dict={X: movie_genres, y: movie_ratings})
    print('Epoch ', i + 1, '; loss ', loss_value)
    losses.append(loss_value)
print('Prediction after training: \n', sess.run(prediction, feed_dict={X: movie_genres, y: movie_ratings}))
plt.plot(losses)
plt.show()

График потерь:

...
Epoch 5000 ; loss 0.035657022
Prediction after training: 
 [[0.7227939 ] -> ground truth 1
 [0.514002 ] -> ground truth 0.5
 [0.13912034] -> ground truth 0.0
 [0.28490177] -> ground truth 0.0
 [0.7263731 ]] -> ground truth 0.7

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

Но самое замечательное, что теперь это ваша первая написанная нейронная сеть tensorflow!

Бонус: то же самое решение, написанное с помощью Keras.

model = tf.keras.Sequential()
model.add(tf.keras.layers.Dense(units=3, activation='sigmoid'))
model.add(tf.keras.layers.Dense(units=1, activation='sigmoid'))
model.compile(loss='mse', optimizer='sgd')
h = model.fit(x=movie_genres, y=movie_ratings, verbose=1, epochs=5000)

print(model.predict(movie_genres))
plt.plot(h.history['loss'])
plt.show()

График потерь:

...
Epoch 5000 ; loss 0.0770
Prediction after training
[[0.5616709 ]
 [0.47505498]
 [0.27552533]
 [0.3333011 ]
 [0.6289481 ]]

Что ж, мы видим, что модель keras сделала это хуже, чем написанный вручную код tensorflow.

Вывод

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

В этой статье был просто дурацкий пример кода tensorflow без классов, сериализации моделей и т. д. Хотите узнать больше — работайте усерднее. Я постараюсь объяснить более сложные вещи, связанные с использованием tensorflow, в своих следующих статьях.

Большое спасибо за чтение!

С наилучшими пожеланиями и удачи, Бондаренко К., инженер по машинному обучению.