Привет, Хабр! Хочу поделиться опытом в быстром создание интерфейса в Jupyter Notebook. Если у тебя есть какая-то задача, для которой нужен простой UI, и ты почему-то захотел сделать её в Юпитере, то добро пожаловать под кат.
Передо мной встала задача: реализовать наглядный интерфейс для машины Тьюринга. Наверное, самый простой вариант – реализовать это с помощью С# или Java для десктопа, а может даже на JS в браузере. Так как я занимаюсь датасаенсом и у меня юпитерголовногомозга – я решил делать с помощью Jupyter Widgets. На хабре я не нашел информации об этой технологии, нужно это исправлять. Пост подразумевает, что читатель уже знаком с языком Python и работал в Jupyter Notebook.
Jupyter Widgets
Jupyter Widgets – это интерактивные HTML-виджеты для Jupyter Notebook. Их прямое назначение – сделать ноутбук более динамичным, добавить в него различные элементы управления. У нас появляется возможность добавлять слайдеры, кнопки, прогресс бары и другие элементы в несколько строчек без знаний CSS. Как я вижу, основное применение этой технологии – быстрая разработка прототипов (особенно для ML-проектов) в которых нужен простой UI. Если вам нужно показать заказчику что-то интерактивное, а не просто таблицу или график, то первое, что я бы попробовал, — это виджеты в юпитере (ну а потом plotly и Dash). Более повседневный вариант использования для датасаентистов — удобная отладка с помощью функции interact.
Interact – наверное самая часто используемая функция этой библиотеки. Она создает слайдер и вызывает некоторую функцию (callback) при его изменении. Все это реализуется в одну строчку. Ему можно придумать массу полезных применений: например, можно смотреть, как ваша модель машинного обучения реагирует на изменение какого-то параметра или признака, просто двигая слайдер. Для примера я не стал импортировать xgboost, а просто построил график полинома, степень которого можно менять с помощью этого самого interact.
xs = np.linspace(-10, 10, 21)
def f(x):
plt.plot(xs, [math.pow(i, x) for i in xs])
interact(f, x=(0, 5))
Машина Тьюринга
Машина Тьюринга – это расширение идеи конечного автомата, с помощью которого, теоретически, можно представить любой алгоритм. Она может быть формализована через алфавит, разрешенные состояния, входные символы, начальное состояние и функцию переходов. Основная часть машины — это лента с символами алфавита, которые последовательно обрабатываются движущейся влево и вправо считывающей головкой в соответствии с функцией переходов.
Пусть машина сейчас в состоянии "q1" и прочитан символ "A". Проводится проверка функции переходов, и если есть комбинация "q1 A", то мы знаем, в какое следующее состояние должен осуществиться переход, какой символ записывается в текущую позицию на ленте, и куда дальше двигается считывающая головка. Если такой комбинации ("состояние-символ") нет, то работа машины завершается. Этих знаний должно быть достаточно, чтобы понять, что нужно отобразить в интерфейсе. Пример готового класса машины Тьюринга на питоне можно найти тут, я немного изменил его для своих нужд.
Для наглядности, рассмотрим следующую задачу. Дан массив из букв "А" и "В". Нужно сжать массив, удалив все буквы "В", но оставив буквы "А". Решение состоит в замене всех символов "В" на символ "*", а затем разбросанные в разных позициях буквы "А" следует сдвинуть вправо. Пример входа: "#BAABABA#". Выход для него: "####AAAA#". Здесь "#" — служебный пустой символ. Функция переходов под спойлером.
transition_function = {
('q0', 'A'): ('q0', 'A', 'r'),
('q0', 'B'): ('q0', '*', 'r'),
('q0', '*'): ('q0', '*', 'r'),
('q0', '#'): ('q1', '#', 'l'),
('q1', 'A'): ('q2', 'A', 'l'),
('q1', '*'): ('q1', '#', 'l'),
('q1', '#'): ('q1', '#', 'r'),
('q2', 'A'): ('q3', '*', 'r'),
('q2', '*'): ('q2', '*', 'l'),
('q2', '#'): ('q5', '#', 'r'),
('q3', 'A'): ('q4', 'A', 'l'),
('q3', '*'): ('q3', '*', 'r'),
('q4', '*'): ('q2', 'A', 'l'),
('q5', '*'): ('q5', '#', 'r')
}
Визуализация
Прежде всего нужно понять, какие UI элементы должны отображаться.
- Три строки с текстом.Первая представляет собой ленту с символами. Вторая выводит текущий переход вида "текущее состояние, текущий символ –> новое состояние, новый символ, направление движения по ленте". Третья выводит статус завершения программы.
- Три кнопки: генерация строки, выполнение одной команды, выполнение программы до конца.
- Слайдер для выбора длины генерируемой входной строки.
Строки с текстом можно представить с помощью класса ipywidgets.HTML. Это виджет для отображения html, работа с ним выглядит так:
finish_out = widgets.HTML('In progress')
display(finish_out)
При изменение значения поля value, виджет автоматически перерисовывается. Три строки с текстом создадим с помощью трех разных html-виджетов.
Кнопки генерации строки и выполнения команды делаются с помощью ipywidgets.Button.
step_btn = widgets.Button(description='Step', button_style='primary')
Слайдер представляется классом ipywidgets.IntSlider. Атрибутами можно передавать границы допустимых значений.
slider = widgets.IntSlider(min=3, max=10, step=1, value=5, description='Length')
Обработка событий от UI-элементов происходит с помощью колбеков: функций, которые вызываются при возникновении событий. В нашем случае для кнопок нужно подписаться на событие клика, а для слайдера — на событие изменения значения элемента. Внутри этих колбеков будет происходить изменение значений html-виджетов.
def on_step_btn(d):
global is_final
if not is_final:
is_final = tm.step()
line_out.value = tm.get_tape()
else:
finish_out.value = 'Finish'
step_btn.on_click(on_step_btn)
Для отрисовки элементов можно передать их все в функцию display. Выводя все вместе, получим:
Все работает! На этом можно было бы остановиться, но согласитесь, выглядит довольно сумбурно. Попробуем это исправить.
Соберем контролы в более привычном виде. Перед каждой html строкой добавим лейбл, кнопки разместим в одну линию, вокруг этого сделаем красивую рамочку. Это можно сделать с помощью ipywidgets.Layout и ipywidgets.Box. Box – это контейнер, в который можно добавлять несколько виджетов в виде списка. Управлять отображением элементов внутри бокса легко с помощью атрибута layout. Благодаря технологии Flexbox из современного CSS можно красиво расположить элементы относительно друг друга. Здесь можно найти множество примеров, на основе которых достаточно просто собрать что-то свое. В итоге у меня получилось так:
form_item_layout = Layout(
display='flex',
flex_flow='row',
justify_content='space-between'
)
form_items = [
Box([Label(value='Current line value'), line_out], layout=form_item_layout),
Box([Label(value='Current transition'), current_out], layout=form_item_layout),
Box([Label(value='Status'), finish_out], layout=form_item_layout),
Box([Label(value='Tape length'), slider], layout=form_item_layout),
Box([gen_btn, step_btn, finish_btn], layout=form_item_layout)
]
form = Box(form_items, layout=Layout(
display='flex',
flex_flow='column',
border='solid 2px',
align_items='stretch',
width='80%'
))
Заключение
В этом посте мы познакомились с Jupyter Widgets на примере визуализатора для машины Тьюринга. Теперь вы сможете удивить заказчика добавить интерактивности в свои ноутбуки. Если вам не хватает функциональности существующих виджетов, вы можете создавать свои, но для этого необходимы хотя бы базовые знания JS. Единственный минус, с которым я столкнулся – виджеты пока не отображаются на github при просмотре ноутбуков (даже после Widgets -> Save Notebook Widget State). Надеюсь, скоро это пофиксят.
Всем красивых и динамичных ноутбуков!