Привет, Хабр! Хочу поделиться опытом в быстром создание интерфейса в 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). Надеюсь, скоро это пофиксят.


Всем красивых и динамичных ноутбуков!


PS: ссылка на github репозиторий

Комментарии (0)