Hello, TensorFlow. Бібліотека машинного навчання від Google

tensorflow
Проект TensorFlow масштабніше, ніж вам може здатися. Той факт, що це бібліотека для глибинного навчання, і його зв'язок з Гуглом допомогли проекту TensorFlow залучити багато уваги. Але якщо забути про ажіотаж, деякі його унікальні деталі заслуговують більш глибокого вивчення:
  • Основна бібліотека підходить для широкого сімейства технік машинного навчання, а не тільки для глибинного навчання.
  • Лінійна алгебра та інші нутрощі добре видно зовні.
  • На додаток до основної функціональності машинного навчання, TensorFlow також включає власну систему логування, власний інтерактивний візуалізатор логів і навіть потужну архітектуру по доставці даних.
  • Модель виконання TensorFlow відрізняється від scikit-learn мови Python і від більшості інструментів R.
це Все круто, але TensorFlow може бути досить складним у розумінні, особливо для того, хто тільки знайомиться з машинним навчанням.
Як працює TensorFlow? Давайте спробуємо розібратися, подивитися і зрозуміти, як працює кожна частина. Ми вивчимо граф руху даних, який визначає обчислення, через які доведеться пройти вашими даними, зрозуміємо, як тренувати моделі градієнтним спуском з допомогою TensorFlow, і як TensorBoard візуалізує роботу з TensorFlow. Наші приклади не допоможуть вирішувати справжні проблеми машинного навчання промислового рівня, але вони допоможуть зрозуміти компоненти, які лежать в основі всього, що створено на TensorFlow, в тому числі того, що ви напишіть в майбутньому!
Імена та виконання в Python і TensorFlow
Те, як TensorFlow керує обчисленнями, не сильно відрізняється від того, як це зазвичай робить Python. В обох випадках важливо пам'ятати, що, перефразовуючи Хедлі Уикэма, об'єкт не має імені (див. малюнок 1). Щоб зрозуміти схожі риси і відмінності між принципами роботи Python і TensorFlow, давайте поглянемо на те, як вони посилаються на об'єкти і обробляють обчислення.

Зображення 1. У імен «є» об'єкти, але не навпаки. Ілюстрація Хедлі Уикэма, використовується з дозволу автора.
Імена змінних в Python це не те, що вони представляють. Вони просто вказують на об'єкти. Так що, коли ви пишете в Python
foo = []
та
bar = foo
, це не означає, що
foo
дорівнює
bar
;
foo
bar
, в тому сенсі що вони обидва вказують на один і той же об'єкт списку.
>>> foo = []
>>> bar = foo
>>> foo == bar
## True
>>> foo bar is
## True

Також можна впевнитися, що
id(foo)
та
id(bar)
однакові. Ця ідентичність, особливо з редагувати структурами даних зразок списків, може призвести до серйозних багам, якщо розуміти її неправильно.
Всередині Python управляє всіма вашими об'єктами і стежить за іменами змінних і за тим, на який об'єкт кожне ім'я посилається. Граф TensorFlow представляє ще один шар такого типу управління. Як ми побачимо пізніше, імена в Python будуть посилатися на об'єкти, які з'єднані з більш детальними та більш чітко керованими операціями на графі TensorFlow.
Коли ви вводите вираз на Python, наприклад, в інтерактивному інтерпретаторі REPL (Read Evaluate Print Loop), все що ви набираєте майже завжди буде обчислено відразу. Python горить бажанням зробити те, що ви йому накажете. Так що, якщо я скажу йому зробити
foo.append(bar)
, він відразу зробить додавання, навіть якщо я ніколи не буду використовувати
foo
.
Більш лінива альтернатива — це просто запам'ятати, що я сказав
foo.append(bar)
, і, якщо в якийсь момент в майбутньому я буду обчислювати
foo
, тоді Python справить додавання. Це вже ближче до того, як веде себе TensorFlow: у ньому визначення відношення не має ніякого відношення до обчислення результату.
TensorFlow ще сильніше відокремлює визначення обчислення від його виконання, так як вони відбуваються взагалі в різних місцях: граф визначає операції, але операції відбуваються лише всередині сесій. Графи і сесії створюються незалежно один від одного. Граф — це щось на зразок креслення, а сесія — це щось на кшталт будівельного майданчика.
Повертаючись до нашого простого прикладу з Python, нагадаю, що
foo
та
bar
вказують на один і той же список. Додавши
bar
на
foo
, ми вставили список всередину себе. Можна уявити собі це як граф з одним вузлом, який вказує сам на себе. Вкладені списки — це один із способів представлення структури графа, подібного обчислювальному графу TensorFlow.
>>> foo.append(bar)
>>> foo
## [[...]]

Справжні графи TensorFlow будуть цікавіше!
Найпростіший граф TensorFlow
Щоб зануритися в тему, давайте створимо простий граф TensorFlow з нуля. На щастя, TensorFlow легше встановити, ніж деякі інші фреймворки. Приклад тут буде працювати з Python 2.7 або 3.3+, і ми використовуємо версію TensorFlow 0.8.
>>> import tensorflow as tf

До цього моменту TensorFlow вже почав керувати купою станів за нас. Наприклад, вже існує явний граф за замовчуванням. Внутри граф за умовчанням
_default_graph_stack
, але у нас немає доступу туди напряму. Ми використовуємо
tf.get_default_graph()
.
>>> graph = tf.get_default_graph()

Вузли графа TensorFlow називаються операціями («operations» або «ops»). Набір операцій можна побачити за допомогою
graph.get_operations()
.
>>> graph.get_operations()
## []

Зараз у графі порожньо. Нам потрібно буде додати туди все, що потрібно обчислити бібліотеці TensorFlow. Давайте почнемо з додавання простий константи зі значенням одиниці.
>>> input_value = tf.constant(1.0)

Тепер ця константа існує як вузол, операція в графі. Python'ський ім'я змінної
input_value
побічно вказує на цю операцію, але її також можна знайти у графі за замовчуванням.
>>> operations = graph.get_operations()
>>> operations
## [<tensorflow.python.framework.ops.Operation at 0x1185005d0>]
>>> operations[0].node_def
## name: "Const"
## op: "Const"
## attr {
## key: "dtype"
## value {
## type: DT_FLOAT
## }
## }
## attr {
## key: "value"
## value {
## tensor {
## dtype: DT_FLOAT
## tensor_shape {
## }
## float_val: 1.0
## }
## }
## }

TensorFlow всередині використовує формат protocol buffers. Protocol buffers — це щось на зразок JSON рівня Google). Висновок на екран
node_def
у константною операції вище показує, що TensorFlow зберігає в уявленні protocol buffer для числа один.
Люди, не знайомі з TensorFlow іноді дивуються, чому суть створення «TensorFlow-версій» існуючих речей. Чому не можна просто використовувати звичайну змінну Python замість додаткового визначення об'єкта TensorFlow? В одному з посібників по TensorFlow є пояснення:
Щоб виробляти ефективні чисельні обчислення в Python, зазвичай використовуються бібліотеки начебто NumPy, які роблять такі дорогі операції як перемножування матриць поза Python'а, використовуючи вкрай ефективний код, реалізований в іншій мові. На жаль, виникає додаткове навантаження при перемиканні назад в Python після кожної операції. Ця навантаження особливо помітна коли потрібно виробляти обчислення на GPU або в розподіленому режимі, де передача даних є дорогою операцією.

TensorFlow також виробляє складні обчислення поза Python, але він іде ще далі, щоб уникнути додаткового навантаження. Замість того, щоб запускати одну дорогу операцію незалежно від Python, TensorFlow дозволяє нам описати граф взаємодіючих операцій, які працюють повністю поза Python. Схожий підхід використовується в Theano та Torch.
TensorFlow вміє робити багато крутих штук, але він може працювати тільки з тим, що було передано йому. Це справедливо навіть для однієї константи.
Якщо поглянути на наш
input_value
, то можна побачити її як 32-бітний тензор нульового виміру: просто одне число.
>>> input_value
## <tf.Tensor 'Const:0' shape=() dtype=float32>

Зауважте, що значення не вказано. Щоб обчислити
input_value
і отримати чисельне значення, потрібно створити «сесію», в якій можна обчислювати операції графа, а потім явно обчислити або «запустити»
input_value
. (Сесія використовує граф за замовчуванням).
>>> sess = tf.Session()
>>> sess.run(input_value)
## 1.0

Може здатися дивним «запускати» константу. Але це не сильно відрізняється від звичайного обчислення виразу в Python. Просто TensorFlow керує власним простором для даних — обчислювальним графом, і у нього є свої методи для обчислення.
Найпростіший нейрон TensorFlow
Тепер, коли у нас є сесія з простим графом, давайте побудуємо нейрон з одним параметром або вагою. Найчастіше, навіть прості нейрони також включають в себе bias term і non-identity activation function, але ми обійдемося без них.
Вага нейрона не буде константным. Ми очікуємо, що він буде змінюватися при навчанні, грунтуючись на істинності вхідних та вихідних даних, які використовуються для навчання. Вага змінної TensorFlow. Ми дамо їй початкове значення 0.8.
>>> weight = tf.Variable(0.8)

Можна подумати, що додавання змінної додасть операцію в граф, але насправді ця одна строчка додасть чотири операції. Можна дізнатися їхні імена:
>>> for op in graph.get_operations(): print(op.name)
## Const
## Variable/initial_value
## Variable
## Variable/Assign
## Variable/read

Не хочеться надто довго розбирати кожну операцію "по кісточках", давайте краще створимо хоча б одну, схожу на даний обчислення:
>>> output_value = weight * input_value

Тепер у графі шість операцій, та остання — це перемножування.
>>> op = graph.get_operations()[-1]
>>> op.name
## 'mul'
>>> for op_input in op.inputs: print(op_input)
## Tensor("Variable/read:0", shape=(), dtype=float32)
## Tensor("Const:0", shape=(), dtype=float32)

Тут видно, як операція множення стежить за джерелом вхідних даних: вони приходять з інших операцій в графі. Людині досить складно стежити за всіма зв'язками щоб зрозуміти структуру всього графа. Візуалізація графа TensorBoard створена спеціально для цього.
Як визначити результат множення? Потрібно «запустити» операцію
output_value
. Але ця операція залежить від змінної
weight
. Ми вказали, що початкове значення
weight
має бути 0.8, але значення ще не було встановлено в поточній сесії. Функція
tf.initialize_all_variables()
генерує операцію, яка ініціалізує всі змінні (в нашому випадку тільки одну), і потім ми можемо запустити цю операцію.
>>> init = tf.initialize_all_variables()
>>> sess.run(init)

Результат виконання
tf.initialize_all_variables()
містить инициализаторы для всіх змінних, які знаходяться у графі на поточний момент, так що якщо ви додасте нові змінні, то потрібно буде запускати
tf.initialize_all_variables()
заново; простий
init
не включить нові змінні.
Тепер ми готові запустити операцію
output_value
.
>>> sess.run(output_value)
## 0.80000001

Це 0.8 * 1.0 з 32-бітними float'ами, а 32-бітні float'и насилу розуміють число 0.8. Значення 0.80000001 це саме близьке, що вони змогли зробити.
Дивимося на граф в TensorBoard
Наш граф поки ще досить простий, але вже було б добре побачити його подання у вигляді діаграми. Використовуємо TensorBoard, щоб згенерувати таку діаграму. TensorBoard читає полі імені, яке зберігається у кожної операції (це зовсім не те ж, що імена змінних Python). Можна використовувати ці імена TensorFlow і перейти на більш звичні імена змінних Python. Використання
tf.mul
еквівалентно простого множення з
*
у прикладі вище, але тут можна встановити ім'я для операції.
>>> x = tf.constant(1.0, name='input')
>>> w = tf.Variable(0.8, name='weight')
>>> y = tf.mul(w, x, name='output')

TensorBoard дивиться в директорію висновку, створену з сесій TensorFlow. Ми можемо писати в цей висновок з допомогою
SummaryWriter
, і, якщо не робити нічого крім одного графа, то буде записаний тільки один граф.
Перший аргумент при створенні
SummaryWriter
— це назва директорії для виводу, яка буде створена при необхідності.
>>> summary_writer = tf.train.SummaryWriter('log_simple_graph', sess.graph)

Тепер можна запустити TensorBoard в командному рядку.
$ tensorboard --реєстрації=log_simple_graph

TensorBoard запускається як локальне веб-додаток на порте 6006. («6006» це «goog» догори ногами). Якщо зайти в браузері на
localhost:6006/#graphs
, то можна побачити діаграму графа, створеного в TensorFlow. Виглядає це приблизно як на зображенні 2.

Зображення 2. Візуалізація TensorBoard найпростішого нейрона TensorFlow.
Навчаємо нейрон
Ми створили нейрон, але як він буде навчатися? Ми встановили вступне значення 1.0. Припустимо, правильне кінцеве значення нуль. Тобто у нас є дуже простий набір даних для навчання з одним прикладом з однією характеристикою: значення дорівнює одиниці і відмітка дорівнює нулю. Ми хочемо навчити нейрон перетворювати одиницю в нуль.
Зараз система приймає одиницю і повертає 0.8, що не є коректною поведінкою. Потрібен спосіб визначити, наскільки система помиляється. Назвемо цю міру помилковості «втратою» ("loss") і поставимо системі мету мінімізувати втрату. Якщо втрата може бути від'ємним числом, то мінімізація не має сенсу, тому давайте визначимо втрату як квадрат різниці між поточному вхідним значенням і бажаним вихідним значенням.
>>> y_ = tf.constant(0.0)
>>> loss = (y - y_)**2

До цього моменту ніщо в графі не вчиться. Для навчання нам потрібен оптимізатор. Ми використовуємо функцію градієнтного спуску щоб мати можливість оновлювати вага на основі значення похідної втрати. Оптимізатору потрібно задати рівень навчання для управління розмірному оновлень, ми задамо 0.025.
>>> optim = tf.train.GradientDescentOptimizer(learning_rate=0.025)

Оптимізатор надзвичайно розумний. Він може автоматично визначити і використовувати потрібний градієнт на рівні всієї мережі, виробляючи покроковий рух назад для навчання.
Поглянемо на те, як виглядає градієнт для нашого простого прикладу.
>>> grads_and_vars = optim.compute_gradients(loss)
>>> sess.run(tf.initialize_all_variables())
>>> sess.run(grads_and_vars[1][0])
## 1.6

Чому значення градієнта 1.6? Значення втрати зводиться в квадрат, і похідна — це помилка, помножена на два. Зараз система повертає 0.8 замість 0, так що помилка це 0.8, і помилка, помножена на два — це 1.6. Працює!
У більш складних системах буде особливо корисно, що TensorFlow автоматично обчислює і застосовує ці градієнти за нас.
Давайте застосуємо градієнт щоб закінчити зворотне поширення.
>>> sess.run(optim.apply_gradients(grads_and_vars))
>>> sess.run(w)
## 0.75999999 # about 0.76

Вагу зменшився на 0.04 тому що оптимізатор відняв градієнт, помножений на рівень навчання, 1.6 * 0.025, рухаючи вага в потрібну сторону.
Замість того, щоб вести оптимізатор за ручку таким чином, можна зробити операцію, яка обчислює та застосовує градієнт:
train_step
.
>>> train_step = tf.train.GradientDescentOptimizer(0.025).minimize(loss)
>>> for i in range(100):
>>> sess.run(train_step)
>>> 
>>> sess.run(y)
## 0.0044996012

Після багаторазового запуску навчального кроку вага і кінцеве значення стали дуже близькі до нуля. Нейрон навчився!
Діагностика навчання в TensorBoard
Нам може бути цікаво, що відбувається під час навчання. Наприклад, ми хочемо простежити за тим, що система передбачає на кожному кроці навчання. Можна виводити значення на екран на кожному кроці циклу.
>>> for i in range(100):
>>> print('before step {}, y is {}'.format(i, sess.run(y)))
>>> sess.run(train_step)
>>> 
## before step 0, y is 0.800000011921
## before step 1, y is 0.759999990463
## ...
## before step 98, y is 0.00524811353534
## before step 99, y is 0.00498570781201

Це спрацює, але є деякі проблеми. Складно сприймати список цифр. Графік був би краще. Навіть з одним значенням виведення занадто багато. А ми швидше за все захочемо стежити за кількома значеннями. Добре б записувати все більш систематично.
На щастя, та ж система, що використовувалася раніше для візуалізації графа, включає в себе потрібний нам механізм.
Додамо в обчислювальний граф операцію, яка коротко описує його стан. У нашому випадку операція доповідає поточне значення
y
, поточний висновок нейрона.
>>> summary_y = tf.scalar_summary('output', y)

Запуск цієї операції повертає рядок у форматі protocol buffer, яку можна записати в директорію логів з допомогою
SummaryWriter
.
>>> summary_writer = tf.train.SummaryWriter('log_simple_stats')
>>> sess.run(tf.initialize_all_variables())
>>> for i in range(100):
>>> summary_str = sess.run(summary_y)
>>> summary_writer.add_summary(summary_str, i)
>>> sess.run(train_step)
>>> 

Тепер після запуску
tensorboard --реєстрації=log_simple_stats
, на сторінці
localhost:6006/#events
виводиться інтерактивний графік (Зображення 3).

Зображення 3. Візуалізація TensorBoard вихідного значення нейрона і номера ітерації навчання.
Рухаємося далі
Ось кінцева версія коду. Його не так багато, і кожна частина показує корисну (і зрозумілу) функціональність TensorFlow.
import tensorflow as tf

x = tf.constant(1.0, name='input')
w = tf.Variable(0.8, name='weight')
y = tf.mul(w, x, name='output')
y_ = tf.constant(0.0, name='correct_value')
loss = tf.pow(y - y_, 2, name='loss')
train_step = tf.train.GradientDescentOptimizer(0.025).minimize(loss)

for value in [x, w, y, y_, loss]:
tf.scalar_summary(value.op.name, value)

summaries = tf.merge_all_summaries()

sess = tf.Session()
summary_writer = tf.train.SummaryWriter('log_simple_stats', sess.graph)

sess.run(tf.initialize_all_variables())
for i in range(100):
summary_writer.add_summary(sess.run(summaries), i)
sess.run(train_step)

Цей приклад ще простіше, ніж приклади з Neural Networks and Deep Learning Майкла Нільсена, які і послужили натхненням. Особисто мені вивчення таких деталей допомагає розуміти і будувати більш складні системи, які використовують прості будівельні блоки як основу.
Якщо хочете продовжити експерименти з TensorFlow, то раджу спробувати зробити більш цікаві нейрони, наприклад, з іншої функцією активації. Можна проводити навчання з більш цікавими даними. Можна додати більше нейронів. Можна додати більше шарів. Можна пірнути в більш складні готові моделі, або провести більше часу за вивченням власних посібники і гайдів TensorFlow. Успіхів!
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.