Русский
Русский
English
Статистика
Реклама

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

Анонс. Машинное обучение в геологии

17.06.2021 20:21:19 | Автор: admin
Завтра, 18 июня в 15:00 в наших соцсетях выступит Лейла Исмаилова, специалист машинного обучения в геологии и со-ведущая подкаста о геологах Про вулканы и людей

Лейла окончила геологический факультет МГУ им. М.В. Ломоносова. Поступила в аспирантуру Баварского Геологического Института в Германии. Во время обучения в аспирантуре опубликовала статьи в престижных научных журналах (Nature и Science Publishing group) и работала в разных лабораториях в Германии, Франции и США. С подробным списком публикаций можно ознакомиться по ссылке.

После защиты аспирантуры вернулась в Россию и пошла работать постдоком в Сколтех. В Сколтехе начала заниматься машинным обучением для геологии, даже поучаствовала в основании стартапа Digital Petroleum. Сейчас работает в научном центре нефтяной компании.




План выступления


Задавайте вопросы в комментариях и Лейла ответит на них во время прямого эфира.

  • Наука в геологии
  • Как стать ученым в России
  • Наука в России и наука зарубежом
  • Машинное обучение в нефтянке

До встречи в эфире!




Подробнее..

Как мы построили Computer Vision из подручных материалов, чтобы сделать гифки

17.06.2021 14:15:17 | Автор: admin

Меня зовут Денис Власов, я Data Scientist в Учи.ру. С помощью моделей машинного обучения из записей онлайн-уроков мы сделали гифки последовательность из нескольких кадров с наиболее яркими эмоциями учеников. Эти гифки получили их родители в e-mail-рассылке. Вместе с Data Scientist @DariaV Дашей Васюковой расскажем, как без экспертизы в Computer Vision, а только с помощью открытых библиотек и готовых моделей сделать MVP, в основе которого лежат low-res видео. В конце бонус виджет для быстрой разметки кадров.

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

Если у вас, как и у нас, не стоит задача RealTime определения эмоций, можно пойти простым способом: анализировать записи уроков.

Маркеры начала и конца урока

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

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

Разбили видео на кадры

Для упрощения анализа нарезали получившиеся отрезки видео учеников на картинки. Нам хватило одного кадра в секунду: если ребенок проявил какую-то эмоцию, она будет присутствовать на лице несколько секунд. Большая степень гранулярности усложнила бы разметку, но существенно не повлияла на результат.

Научились детектировать детские улыбки (и не только)

На каждом кадре необходимо обнаружить лицо. Если оно там есть, проверить, родитель это или ученик, а также оценить эмоции на лице. И тут возникло несколько нюансов, которые пришлось учитывать.

Проблема 1. Распознавать лица на картинках низкого качества сложнее

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

Стандартный детектор DNN Face Detector из библиотеки OpenCV, который мы сначала взяли за основу, на наших данных давал неточные результаты. Оказалось, что алгоритм недостаточно хорошо справляется с реальными кадрами из видеочатов: иногда пропускает лица, которые явно есть в кадре, из двух лиц находил только одно или определял лица там, где их нет.

Стандартный детектор DNN Face Detector мог определить как лицо узор на занавеске, игрушечного медведя или даже композицию из картин на стене и стулаСтандартный детектор DNN Face Detector мог определить как лицо узор на занавеске, игрушечного медведя или даже композицию из картин на стене и стула

Поэтому мы решили попробовать обучить свой детектор. Для этого взяли реализацию RetinaNet-модели на PyTorch. В качестве данных для обучения подали результаты работы стандартного детектора и убедились, что новая модель учится находить лица. Затем подготовили обучающую и валидационную выборку, просматривая и при необходимости исправляя результаты работы детектора на новых кадрах: исправлять разметку работающей модели получается быстрее, чем отмечать лица на кадре с нуля.

Размечали итеративно: после добавления новой порции размеченных кадров мы заново обучали модель. А после проверки ее работы сохраняли разметку для новых кадров, наращивая обучающую выборку. Всего мы разметили 2624 кадра из 388 видеозаписей, на которых в сумме было 3325 лиц.

Таким образом удалось обучить более чувствительный в наших условиях детектор. В валидационной выборке из 140 кадров старый детектор нашел 150 лиц, а пропустил 38. Новый же пропустил только 5, а 183 обнаружил верно.

Проблема 2. В кадре присутствует не только ребенок

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

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

Первая модель должна определять, кому принадлежит лицо в кадре: родителю или ученику. Кажется, что с разметкой не должно было возникнуть никаких проблем, ведь отличить взрослого от ребенка просто. Это действительно так, если перед нами целое видео. Но когда мы имеем дело с отдельными кадрами, то оказывается, что:

  • возраст людей на кадре с низким разрешением становится неочевидным;

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

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

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

Вторая модель должна была находить именно такие родительские плечи. Очевидно, что в этой задаче детектор лиц не применим, поэтому надо обучаться на кадрах целиком. Конечно, таких датасетов мы не нашли в публичном доступе и разметили около 250 000 кадров, на которых есть часть родителя, и кадры без них. Разметки на порядок больше, чем в других задачах, потому что размечать гораздо легче: можно смотреть не отдельные кадры, а отрезки видео и в несколько кликов отмечать, например, что вот эти 15 минут (900 кадров!) родитель присутствовал.

На дашборде урока с аналитикой доступны графики присутствия родителями по мнению обеих моделей. Они помогают понять, когда родитель просто интересуется процессом урока, а когда скорее общается с преподавателем.

На верхнем графике вероятность присутствия родителя хотя бы плечом, а на нижнем вероятность того, что родитель смотрит в камеру, например, общается с преподавателемНа верхнем графике вероятность присутствия родителя хотя бы плечом, а на нижнем вероятность того, что родитель смотрит в камеру, например, общается с преподавателем

Проблема 3. Дети улыбаются по-разному

На практике оказалось, что не так уж просто понять, улыбается ребенок или нет. И если с улыбчивыми ребятами проблем нет, то детекция сдержанных улыбок оказывается нетривиальной задачей даже для человека.

За основу классификатора настроения мы взяли предобученную модель ResNet34 из библиотеки fast.ai. Эту же библиотеку использовали для дообучения модели в два этапа: сначала на публичных датасетах facial_expressions и SMILEsmileD с веселыми и нейтральными лицами, потом на нашем размеченном вручную датасете с кадрами с камер учеников. Публичные датасеты решили включить, чтобы расширить размер выборки и помочь модели более качественными изображениями, чем кадры видео с планшетов и веб-камер наших учеников.

Размечали с помощью кастомного виджета. Все изображения подвергались одной и той же процедуре предобработки:

  1. Масштабирование кадра до размера 64 на 64 пикселя. В публичных датасетах картинки уже квадратные, поэтому масштабирование не приводит к искажениям пропорций. В собственном датасете мы сначала дополняли детектированную область с лицом до квадрата и потом масштабировали.

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

  3. Аугментация. Позволяет в разы увеличить эффективный размер выборки и учесть особенности данных.

  4. Нормализация цветов с помощью CLAHE normalizer из библиотеки OpenCV. По ощущениям, такая нормализация лучше других вытягивает контраст на пересвеченных или темных изображениях.

Дообучаем модель для распознавания улыбок

1. Аугментации

При дообучении мы использовали достаточно жесткие аугментации:

  • Отражали изображение по горизонтали.

  • Поворачивали на случайную величину.

  • Применяли три разных искажения для изменения контраста и яркости.

  • Брали не всю картинку, а квадрат, составляющий не менее 60% от площади исходного изображения.

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

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

Пример аугментаций на одном изображении. Для наглядности аугментации сделаны до масштабирования к разрешению 64х64Пример аугментаций на одном изображении. Для наглядности аугментации сделаны до масштабирования к разрешению 64х64Код для аугментаций
# ! pip freeze | grep fastai# fastai==1.0.44import fastaiimport matplotlib.pyplot as pltfrom matplotlib import cmfrom matplotlib import colorsimport seaborn as sns%matplotlib inlinefrom pylab import rcParamsplt.style.use('seaborn-talk')rcParams['figure.figsize'] = 12, 6path = 'facial_expressions/images/'def _side_cutoff(    x,    cutoff_prob=0.25,    cutoff_intensity=(0.1, 0.25)):    if np.random.uniform() > cutoff_prob:        return x    # height and width    h, w = x.shape[1:]    h_cutoff = np.random.randint(        int(cutoff_intensity[0]*h), int(cutoff_intensity[1]*h)    )    w_cutoff = np.random.randint(        int(cutoff_intensity[0]*w), int(cutoff_intensity[1]*w)    )        cutoff_side = np.random.choice(        range(4),        p=[.34, .34, .16, .16]    ) # top, bottom, left, right.    if cutoff_side == 0:        x[:, :h_cutoff, :] = 0    elif cutoff_side == 1:        x[:, h-h_cutoff:, :] = 0    elif cutoff_side == 2:        x[:, :, :w_cutoff] = 0    elif cutoff_side == 3:        x[:, :, w-w_cutoff:] = 0    return x# side cutoff goes frist.side_cutoff = fastai.vision.TfmPixel(_side_cutoff, order=99)augmentations = fastai.vision.get_transforms(    do_flip=True,    flip_vert=False,    max_rotate=25.0,    max_zoom=1.25,    max_lighting=0.5,    max_warp=0.0,    p_affine=0.5,    p_lighting=0.5,        xtra_tfms = [side_cutoff()])def get_example():    return fastai.vision.open_image(        path+'George_W_Bush_0016.jpg',    )def plots_f(rows, cols, width, height, **kwargs):    [        get_example()        .apply_tfms(            augmentations[0], **kwargs        ).show(ax=ax)        for i,ax in enumerate(            plt.subplots(                rows,                cols,                figsize=(width,height)            )[1].flatten())    ]plots_f(3, 5, 15, 9, size=size)

2. Нормализация цвета

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

Пример нормализации цвета на изображениях из публичного датасетаПример нормализации цвета на изображениях из публичного датасетаКод для нормализации цвета
# pip freeze | grep opencv# > opencv-python==4.5.2.52import cv2import matplotlib.pyplot as pltfrom matplotlib import cmfrom matplotlib import colorsimport seaborn as sns%matplotlib inlinefrom pylab import rcParamsplt.style.use('seaborn-talk')rcParams['figure.figsize'] = 12, 6path = 'facial_expressions/images/'imgs = [    'Guillermo_Coria_0021.jpg',    'Roger_Federer_0012.jpg',]imgs = list(    map(        lambda x: path+x, imgs    ))clahe = cv2.createCLAHE(    clipLimit=2.0,    tileGridSize=(4, 4))rows_cnt = len(imgs)cols_cnt = 4imsize = 3fig, ax = plt.subplots(    rows_cnt, cols_cnt,    figsize=(cols_cnt*imsize, rows_cnt*imsize))for row_num, f in enumerate(imgs):    img = cv2.imread(f)    col_num = 0        img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)    ax[row_num, col_num].imshow(img, cmap='gray')    ax[row_num, col_num].set_title('bw', fontsize=14)    col_num += 1    img_normed = cv2.normalize(        img,        None,        alpha=0,        beta=1,        norm_type=cv2.NORM_MINMAX,        dtype=cv2.CV_32F    )    ax[row_num, col_num].imshow(img_normed, cmap='gray')    ax[row_num, col_num].set_title('bw normalize', fontsize=14)    col_num += 1        img_hist_normed = cv2.equalizeHist(img)    ax[row_num, col_num].imshow(img_hist_normed, cmap='gray')    ax[row_num, col_num].set_title('bw equalizeHist', fontsize=14)    col_num += 1        img_clahe = clahe.apply(img)    ax[row_num, col_num].imshow(img_clahe, cmap='gray')    ax[row_num, col_num].set_title('bw clahe_norm', fontsize=14)    col_num += 1        for col in ax[row_num]:        col.set_xticks([])        col.set_yticks([])plt.show()

В итоге мы получили модель, способную отличить улыбку от нейтрального выражения лица в с качеством 0.93 по метрике ROC AUC. Иными словами, если взять из выборки по случайному кадру с улыбкой и без, то с вероятностью 93% модель присвоит большую вероятность улыбки кадру с улыбающимся лицом. Этот показатель мы использовали для сравнения разных вариантов дообучения и пайплайнов. Но интуитивно кажется, что это достаточно высокий уровень точности: даже человек не всегда может определить эмоцию на лице другого человека. К тому же в реальности существует гораздо больше выражений лиц кроме однозначной радости и однозначной печали.

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

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

3. Увеличение объема выборки

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

Мы несколько раз дообучили модель на публичных датасетах и подвыборках разного размера из трейна и проверили качество на валидационной выборке. На графике видно, что качество растет по мере увеличения объема подвыборки, поэтому мы разметили дополнительно еще 3 тыс. кадров: финальную модель обучали на выборке из 8500 кадров. Скорее всего и при таком объеме данных качество еще не начало выходить на плато, но перепроверять это мы не стали.

Такое упражнение называется построением learning curve, и оно в очередной раз подтверждает тезис: объем данных самое важное в модели машинного обучения.

Качество на отложенной выборке растет по мере увеличения выборки для дообученияКачество на отложенной выборке растет по мере увеличения выборки для дообучения

4. Картинки Google для обогащения выборки

Мы пробовали спарсить первые 1000 результатов картинок по запросам в духе happy, unhappy, smiling, neutral и т. д. Не ожидали получить данные высокого качества, поэтому планировали потом просмотреть их глазами и удалить совсем неподходящие. В итоге мы быстро поняли, что никакая фильтрация эти картинки не спасет, поэтому отказались от этой идеи совсем.

Примеры изображений по запросам happy и unhappyПримеры изображений по запросам happy и unhappy

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

  • есть ли в кадре лицо;

  • с какой вероятностью этот человек улыбается;

  • ребенок это или взрослый;

  • есть ли в кадре взрослый, даже если мы не нашли лица.

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

Собрали гифку

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

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

Примеры итоговых GIF с улыбками нашей коллеги и ее детейПримеры итоговых GIF с улыбками нашей коллеги и ее детей

Что мы в итоге получили?

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

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

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

Статистика дисконнектов. В этом уроке был единственный дисконнект на стороне ученикаСтатистика дисконнектов. В этом уроке был единственный дисконнект на стороне ученика

Другой пример трекинг настроения ученика на протяжении урока. Он позволяет проанализировать ход занятия и понять, нужно ли что-то менять в его структуре.

Виджеты

Все данные мы размечали сами и делали это довольно быстро (примерно 100 кадров в минуту). В этом нам помогали самописные виджеты:

  1. Виджет для разметки кадров с улыбками.

  2. Виджет для разметки кадров с детьми и взрослыми.

  3. Виджет для разметки кадров с плечом или локтем родителя.

Мы хотим поделиться кодом второго виджета как наиболее полного. Скорее всего, вы не сможете заменить в нем путь к файлам и использовать для своей задачи, потому что он слишком специфичен. Но если вам понадобиться написать свой велосипед для разметки, можете почерпнуть что-то полезное.

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

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

Видео работы виджета

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

Код виджета
import pandas as pdimport numpy as npimport datetimeimport randomimport osimport ipywidgets as widgetsfrom IPython.display import displayfrom pathlib import Pathclass BulkLabeler():    def __init__(self, frames_path, annotations_path,                 labels = ['0', '1'],                 predict_fn = None,                 frame_width=120,                 num_frames = 27,                 face_width = 120,                 num_faces = 27,                 myname = '?',                 ):        self.predict_fn = predict_fn        self.labels = labels        self.frames_path = frames_path        self.frame_width = frame_width        self.num_frames = num_frames        self.face_width = face_width        self.num_faces = num_faces        self.myname = myname        self.faces_batch = []                # get annotations        self.annotations_path = annotations_path        processed_videos = []        if annotations_path.exists():            annotations = pd.read_csv(annotations_path)            processed_videos = annotations.file.str.split('/').str[-3].unique()        else:            with open(self.annotations_path, 'w') as f:                f.write('file,label,by,created_at\n')                # get list of videos        self.video_ids = [x for x in os.listdir(frames_path)                           if x not in processed_videos]        random.shuffle(self.video_ids)        self.video_ind = -1                self._make_video_widgets_row()        self._make_frames_row()        self._make_range_slider()        self._make_buttons_row()        self._make_faces_row()        self._make_video_stats_row()                display(widgets.VBox([self.w_video_row,                              self.w_frames_row,                              self.w_slider_row,                              self.w_buttons_row,                              self.w_faces_row,                              self.w_faces_label,                              self.w_video_stats]))        self._on_next_video_click(0)            ### Video name and next video button        def _make_video_widgets_row(self):        # widgets for current video name and "Next video" button        self.w_current_video = widgets.Text(            value='',            description='Current video:',            disabled=False,            layout = widgets.Layout(width='500px')            )                self.w_next_video_button = widgets.Button(            description='Next video',            button_style='info', # 'success', 'info', 'warning', 'danger' or ''            tooltip='Go to the next video',            icon='right-arrow'        )                self.w_video_row = widgets.HBox([self.w_current_video, self.w_next_video_button])                self.w_current_video.observe(self._on_video_change, names='value')        self.w_next_video_button.on_click(self._on_next_video_click)                        def _on_next_video_click(self, _):        while True:            self.video_ind += 1            current_video = self.video_ids[self.video_ind]            if next(os.scandir(self.frames_path/current_video/'student_faces'), None) is not None:                break        self.w_current_video.value = current_video                    def _on_video_change(self, change):        self.video_id = change['new']        self.frame_nums_all = sorted(int(f.replace('.jpg',''))                                      for f in os.listdir(self.frames_path/self.video_id/'student_src'))        start, stop = min(self.frame_nums_all), max(self.frame_nums_all)        self.w_range_slider.min = start        self.w_range_slider.max = stop        step = self.frame_nums_all[1] - self.frame_nums_all[0] if len(self.frame_nums_all)>1 else 1        self.w_range_start.step = step        self.w_range_stop.step = step        # change to slider value will cause frames to be redrawn        self.w_range_slider.value = [start, stop]               # reset faces        self.faces_df = None        self._reset_faces_row()        self.w_video_stats.value = f'Video {self.video_id}  no annotations yet.'                def _close_video_widgets_row(self):        self.w_current_video.close()        self.w_next_video_button.close()        self.w_video_row.close()        ### Video frames box        def _make_frames_row(self):        frame_boxes = []        self.w_back_buttons = {}        self.w_forward_buttons = {}        for i in range(self.num_frames):            back_button = widgets.Button(description='<',layout=widgets.Layout(width='20px',height='20px'))            self.w_back_buttons[back_button] = i            back_button.on_click(self._on_frames_back_click)            label = widgets.Label(str(i+1), layout = widgets.Layout(width=f'{self.frame_width-50}px'))            forward_button = widgets.Button(description='>',layout=widgets.Layout(width='20px',height='20px'))            self.w_forward_buttons[forward_button] = i            forward_button.on_click(self._on_frames_forward_click)            image = widgets.Image(width=f'{self.frame_width}px')            frame_boxes.append(widgets.VBox([widgets.HBox([back_button, label, forward_button]),                                              image]))                    self.w_frames_row = widgets.GridBox(frame_boxes,                                             layout = widgets.Layout(width='100%',                                                                     display='flex',                                                                     flex_flow='row wrap'))            def _on_frames_back_click(self, button):        frame_ind = self.w_back_buttons[button]        frame = int(self.w_frames_row.children[frame_ind].children[0].children[1].value)        start, stop = self.w_range_slider.value        self.w_range_slider.value = [frame, stop]            def _on_frames_forward_click(self, button):        frame_ind = self.w_forward_buttons[button]        frame = int(self.w_frames_row.children[frame_ind].children[0].children[1].value)        start, stop = self.w_range_slider.value        self.w_range_slider.value = [start, frame]            def _close_frames_row(self):        for box in self.w_frames_row.children:            label_row, image = box.children            back, label, forward = label_row.children            image.close()            back.close()            label.close()            forward.close()            box.close()        self.w_frames_row.close()                    ### Frames range slider                        def _make_range_slider(self):        self.w_range_start = widgets.BoundedIntText(                                        value=0,                                        min=0,                                        max=30000,                                        step=1,                                        description='Frames from:',                                        disabled=False,                                        layout = widgets.Layout(width='240px')                                    )        self.w_range_stop = widgets.BoundedIntText(                                        value=30000,                                        min=0,                                        max=30000,                                        step=1,                                        description='to:',                                        disabled=False,                                        layout = widgets.Layout(width='240px')                                    )        self.w_range_slider = widgets.IntRangeSlider(            value=[0, 30000],            min=0,            max=30000,            step=1,            description='',            disabled=False,            continuous_update=False,            orientation='horizontal',            readout=True,            readout_format='d',            layout=widgets.Layout(width='500px')        )        self.w_range_flip = widgets.Button(description='Flip range',            button_style='', # 'success', 'info', 'warning', 'danger' or ''            tooltip='Invert frames selection',            layout = widgets.Layout(width=f'{self.frame_width}px'),            icon='retweet'                                          )                self.w_range_slider.observe(self._on_slider_change, names='value')        self.w_range_start.observe(self._on_range_start_change, names='value')        self.w_range_stop.observe(self._on_range_stop_change, names='value')        self.w_range_flip.on_click(self._on_range_flip)        self.w_slider_row = widgets.HBox([self.w_range_start,                                          self.w_range_slider,                                          self.w_range_stop,                                          self.w_range_flip])        def _close_range_slider(self):        self.w_range_start.close()        self.w_range_stop.close()        self.w_range_slider.close()        self.w_range_flip.close()        self.w_slider_row.close()            def _on_range_flip(self, _):        start, stop = self.w_range_slider.value        left, right = self.w_range_slider.min, self.w_range_slider.max        if start==left and right==stop:            pass        elif start - left > right - stop:            self.w_range_slider.value=[left, start]        else:            self.w_range_slider.value=[stop, right]                                                   def _on_range_start_change(self, change):        new_start = change['new']        start, stop = self.w_range_slider.value        self.w_range_slider.value = [new_start, stop]                    def _on_range_stop_change(self, change):        new_stop = change['new']        start, stop = self.w_range_slider.value        self.w_range_slider.value = [start, new_stop]                    def _on_slider_change(self, change):        start, stop = change['new']        # update text controls        self.w_range_start.max = stop        self.w_range_start.value = start        self.w_range_stop.min = start        self.w_range_stop.max = self.w_range_slider.max        self.w_range_stop.value = stop        # show frames that fit current selection        frame_nums = [i for i in self.frame_nums_all if i>=start and i<=stop]        N = len(frame_nums)        n = self.num_frames        inds = [int(((N-1)/(n-1))*i) for i in range(n)]        # load new images into image widgets        for ind, box in zip(inds, self.w_frames_row.children):            frame_num = frame_nums[ind]            filename = self.frames_path/self.video_id/'student_src'/f'{frame_num}.jpg'            with open(filename, "rb") as image:                f = image.read()            label, image = box.children            label.children[1].value = str(frame_num)            image.value = f            ### Buttons row        def _make_buttons_row(self):        labels = list(self.labels)        if self.predict_fn is not None:            labels.append('model')        self.w_default_label = widgets.ToggleButtons(options=labels,                                                      value=self.labels[0],                                                      description='Default label:')                self.w_next_batch_button = widgets.Button(description='New batch',            button_style='info', # 'success', 'info', 'warning', 'danger' or ''            tooltip='Show next batch of faces from current frame range',            icon='arrow-right'        )        self.w_save_button = widgets.Button(description='Save labels',            button_style='success', # 'success', 'info', 'warning', 'danger' or ''            tooltip='Save current labels',            icon='check'        )        self.w_buttons_row = widgets.HBox([self.w_default_label, self.w_next_batch_button, self.w_save_button])        self.w_next_batch_button.on_click(self._on_next_batch_click)        self.w_save_button.on_click(self._on_save_labels_click)                def _close_buttons_row(self):        self.w_default_label.close()        self.w_next_batch_button.close()        self.w_save_button.close()        self.w_buttons_row.close()                def _on_next_batch_click(self, _):        if self.faces_df is None:             self._create_faces_df()        # select a sample from faces_df        start, stop = self.w_range_slider.value        subdf = self.faces_df.loc[lambda df: df.frame_num.ge(start)&                                             df.frame_num.le(stop)&                                             df.label.eq('')]        num_faces = min(len(subdf), self.num_faces)                if num_faces == 0:            self.faces_batch = []            self.w_faces_label.value = 'No more unlabeled images in this frames range'            self.w_faces_label.layout.visibility = 'visible'            for box in self.w_faces_row.children:                box.layout.visibility = 'hidden'        else:            self.w_faces_label.layout.visibility = 'hidden'            self.faces_batch = subdf.sample(num_faces).index            # if we have a model then we use it to sort images            if self.predict_fn is not None:                probs, labels = self._predict()                # sort faces according to probability                ind = sorted(range(len(probs)), key=probs.__getitem__)                self.faces_batch = [self.faces_batch[i] for i in ind]                labels = [labels[i] for i in ind]            # create labels for each face            if self.w_default_label.value != 'model':                labels = [self.w_default_label.value]*len(self.faces_batch)            # update faces UI            for facefile, label, box in zip(self.faces_batch, labels, self.w_faces_row.children):                image, buttons = box.children                with open(self.frames_path/facefile, "rb") as im:                    image.value = im.read()                buttons.value = label                box.layout.visibility = 'visible'            if len(self.faces_batch) < len(self.w_faces_row.children):                for box in self.w_faces_row.children[len(self.faces_batch):]:                    box.layout.visibility = 'hidden'        def _predict(self):        probs = []        labels = []        for facefile in self.faces_batch:            prob, label = self.predict_fn(self.frames_path/facefile)            probs.append(prob)            labels.append(label)        self.faces_df.loc[self.faces_batch, 'prob'] = probs        return probs, labels                    def _on_save_labels_click(self, _):        self.w_save_button.description='Saving...'                        with open(self.annotations_path, 'a') as f:            for file, box in zip(self.faces_batch, self.w_faces_row.children):                label = box.children[1].value                self.faces_df.at[file,'label'] = label                print(file, label, self.myname, str(datetime.datetime.now()),sep=',', file=f)                # update current video statistics        stats = self.faces_df.loc[self.faces_df.label.ne(''),'label'].value_counts().sort_index()        stats_str = ', '.join(f'{label}: {count}' for label, count in stats.items())        self.w_video_stats.value = f'Video {self.video_id}  {stats_str}.'                self.w_save_button.description = 'Save labels'        # ask for next batch        self._on_next_batch_click(0)            ### Faces row        def _make_faces_row(self):        face_boxes = []        for i in range(self.num_faces):            image = widgets.Image(width=f'{self.face_width}px')            n = len(self.labels)            toggle_buttons_width = int(((self.face_width-5*(n-1))/n))            toggle_buttons = widgets.ToggleButtons(options=self.labels,                                                    value=self.w_default_label.value,                                                    style=widgets.ToggleButtonsStyle(button_width=f'{toggle_buttons_width}px'))            face_boxes.append(widgets.VBox([image, toggle_buttons]))                    self.w_faces_row = widgets.GridBox(face_boxes,                                            layout = widgets.Layout(width='100%',                                                                    display='flex',                                                                    flex_flow='row wrap'))        self.w_faces_label = widgets.Label()        self._reset_faces_row()            def _close_faces_row(self):        for box in self.w_faces_row.children:            image, buttons = box.children            for w in [image, buttons, box]:                w.close()        self.w_faces_row.close()        self.w_faces_label.close()            def _reset_faces_row(self):        for box in self.w_faces_row.children:            box.layout.visibility = 'hidden'        self.w_faces_label.layout.visibility = 'visible'        self.w_faces_label.value = 'Press "New batch" button to see a new batch of faces'        self.faces_batch = []            ### Video statistics row        def _make_video_stats_row(self):        self.w_video_stats = widgets.Label('No video currently selected')        def _close_video_stats_row(self):        self.w_video_stats.close()                def _create_faces_df(self):        folder = Path(self.video_id,'student_faces')        df = pd.DataFrame({'file':[folder/f for f in os.listdir(self.frames_path/folder)]})        df['frame_num'] = df.file.apply(lambda x: int(x.stem.split('_')[0]))        df['label'] = '' #TODO maybe existing annotations?        df['prob'] = np.nan        df = df.sort_values(by='frame_num').set_index('file')        self.faces_df = df                    def close(self):        self._close_video_widgets_row()        self._close_frames_row()        self._close_range_slider()        self._close_buttons_row()        self._close_faces_row()        self._close_video_stats_row()
Подробнее..

Перевод Быстрое обнаружение Covid-19 на рентгеновских снимках с помощью Raspberry Pi

14.06.2021 18:13:31 | Автор: admin

Системы обнаружения Covid-19 на рентгеновских снимках выдают быстрые результаты, в частности информацию о том, насколько серьёзно лёгкие поражены вирусом Covid-19. Традиционные системы обнаружения Covid-19 обладают тем недостатком, что для формирования отчётов им требуется довольно длительное время, в то время как инфицированный человек нуждается в немедленной помощи. Кроме того, после каждого использования всех подобных систем обнаружения вируса часть деталей приходится утилизировать, что в некоторых случаях может приводить к их дефициту.К старту курса о машинном и глубоком обучении мы перевели статью о том, как эта проблема решается при помощи Raspberry Pi, кроме того, материал знакомит читателей с онлайн-платформой EDGE Impulse.


Наша система обнаружения Covid-19 на рентгеновских снимках требует лишь однократной установки и связки с рентгеновским аппаратом, после чего может работать сколько угодно времени. Она способна определять состояние инфицирования лёгких значительно быстрее традиционных систем. Такого результата можно достичь двумя способами: с помощью библиотеки Python или посредством создания и тренировки ML-модели. В этой статье мы расскажем, как создать и обучить модель. В скором времени мы опубликуем статьи о других методах, в которых используется предварительно обученная модель.

Что для этого нужно

Решение задачи начнём с подбора компонентов.

Вместо HDMI-дисплея можно работать с дисплеем ноутбука, любым монитором или телевизором. Потребуется также рентгеновский снимок лёгких.

Предварительные требования

На Raspberry Pi должна быть установлена последняя версия ОС Raspbian. Также необходимо подготовить наборы данных для рентгеновских снимков лёгких, инфицированных и не инфицированных Covid-19. Такие снимки можно получить на сайте kaggle онлайн-ресурсе, где размещены рентгеновские снимки инфицированных пациентов, предоставленные сообществом экспертов и врачей. Аналогичные данные приведены на портале GitHub. Загрузим наборы данных рентгеновских снимков здоровых лёгких и лёгких, инфицированных Covid-19.

Теперь выберем платформу для создания ML-модели и обучим её обнаруживать на рентгеновских снимках инфицированные вирусом лёгкие. Здесь у нас есть разные варианты, например TensorFlow, PyTorch или онлайн-платформы: SensiML, Apache Spark, EDGE Impulse и так далее. Я выбрал EDGE Impulse.

В интерфейсе Edge Impulse переходим в меню создания проекта. Система спросит, как мы намереваемся использовать проект. Поскольку мы будем работать с проектом для создания ML-модели обработки изображений, выберем вариант, связанный с работой с изображениями.

Затем система спросит, как нужно классифицировать изображение (классификация одного или многих объектов). Здесь можно выбрать любой вариант. Я выбрал классификацию одного объекта (рис. 2). Щелчком мыши выберем только что созданный проект. Здесь система предложит указать, как его необходимо использовать. Выберем пункт Connect a development board (Подключение макетной платы). Через эту плату данные будут загружаться в проект (рис. 3).

1. Создание проекта1. Создание проекта2. Выбор типа для классификации2. Выбор типа для классификации3. Подключение макетной платы3. Подключение макетной платы

Установка на Raspberry Pi

Откроем окно терминала и с помощью приведённых ниже команд установим на Raspberry Pi зависимость для EDGE_Impulse:

curl -sL https://deb.nodesource.com/setup_12.x | sudo bash -sudo apt install -y gcc g++ make build-essential nodejs sox gstreamer1.0-tools gstreamer1.0-plugins-good gstreamer1.0-plugins-base gstreamer1.0-plugins-base-appsnpm config set user root && sudo npm install edge-impulse-linux -g --unsafe-perm

После установки запускаем Edge_Impulse командой edge-impulse-linux. Нам будет предложено ввести адрес электронной почты и пароль для EDGE Impulse. Вводим требуемые данные и входим в систему (рис. 4).

4. Вход в систему с помощью адреса электронной почты и пароля4. Вход в систему с помощью адреса электронной почты и пароля

Система попросит выбрать проект, к которому должно быть подключено устройство. Указываем название проекта (в моём случае это COVID-19 Detector) (рис. 5).

5. Выбор проекта5. Выбор проекта

Подготовка набора данных

Система выдаст URL-адрес для загрузки набора данных. Есть два варианта загрузки наборов данных рентгеновских снимков: либо через камеру Raspberry Pi (поместив рентгеновские снимки в кадр камеры), либо загрузив файлы фотографий средствами ПК/Raspberry Pi (рис. 6). Я рекомендую второй вариант.

Чтобы вручную загрузить с ПК наборы данных, то есть изображения рентгеновских снимков, выберем эти наборы в качестве тестовых и тренировочных изображений. Выбрав наборы, загрузим их для обучения и установим метки [в оригинале level, крайне вероятна сложная опечатка label, о пороге принятия решений речь идёт далее] для классификации изображений. Здесь я устанавливаю метку Covid-19 infected (Covid-инфицированные лёгкие) и Normal Lungs (Здоровые лёгкие) (рис. 8). Далее загружаем рентгеновские снимки здоровых лёгких и лёгких, инфицированных Covid-19.

6. Получение URL-адреса для создания модели6. Получение URL-адреса для создания модели7. Загрузка набора данных с использованием камеры Raspberry Pi для модели7. Загрузка набора данных с использованием камеры Raspberry Pi для модели8. Загрузка данных из файла изображения8. Загрузка данных из файла изображения

Если вы не успели загрузить изображения в соответствующие категории, с помощью функции Edit (Редактирование) можно позже добавить и/или обозначить их как "Инфицированное лёгкое" или "Здоровое лёгкое". Размечая набор данных, пожалуйста, будьте осторожны. Если пометить рентгеновские снимки здоровых лёгких как Covid-инфицированные, модель будет обучена неправильно, и её точность будет снижена. Перед первоначальной загрузкой наборов данных (с веб-сайта) необходимо создать две отдельные папки: одну для тренировочных, другую для тестовых наборов данных. В обеих папках создадим ещё две папки с названиями Infected lung X-ray (Рентген инфицированных лёгких) и Normal lung X-ray (Рентген здоровых лёгких) соответственно. Загрузим отдельно изображения из обеих папок и после загрузки распределим их по соответствующим категориям (рис. 9).

9. Установка уровня для рентгеновского снимка наборов данных для тренировки модели9. Установка уровня для рентгеновского снимка наборов данных для тренировки модели

Повторим те же шаги, загружая рентгеновские изображения инфицированных и здоровых лёгких для тестовых наборов данных. Опция загрузки изображений для тестовых наборов данных находится рядом с опцией тренировки (рис. 10).

10. Вкладка тренировочных данных10. Вкладка тренировочных данных

Тренируем модель

После загрузки рентгеновских снимков инфицированных и неинфицированных наборов данных модель готова к тренировке. Переходим на вкладку Impulse design, выбираем пункт Create Impulse и жмём кнопку Create Impulse (см. рис. 11). Затем добавляем блок обработки и блок обучения (рис. 12, 13).

11. Создание Impulse11. Создание Impulse12. Выбор блока обработки12. Выбор блока обработки13. Блок обучения.13. Блок обучения.

После добавления блоков обработки и обучения необходимо определить параметры [в оригинале the parameter в единственном числе опечатка]. Для этого нужно получить характеристики рентгеновского снимка и сохранить их (рис. 14, 15). Затем переходим к новой опции тренировки ML-модели. Для этого в экспертном или обычном режиме Keras выбираем тренировку создаваемой ML-модели. В нашем случае выбираем обычный режим. В этом режиме можно задать количество циклов обучения. При увеличении количества циклов обучения точность работы модели увеличится, так как она пройдёт через различные циклы обучения, но при этом также увеличится время, необходимое для обучения ML-модели. Здесь можно задать такие параметры, как порог принятия решений и скорость обучения, значения которых влияют на точность и время работы модели.

15. 3D-визуализация признаков модели15. 3D-визуализация признаков модели16. Генерация параметра16. Генерация параметра17. Настройка и создание Ml-модели17. Настройка и создание Ml-модели

В оригинале надпись к рисунку 15 Feathers graph for ML model., вероятнее всего Features graph for ML model, то есть визуализация пространства признаков модели в трёхмерном пространстве.

Тестирование

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

18. Обнаружение инфекции по рентгеновскому снимку во время тестирования18. Обнаружение инфекции по рентгеновскому снимку во время тестирования19. Классификация снимка для проверки точности модели19. Классификация снимка для проверки точности модели

Развёртывание модели

Теперь наша ML-модель готова к развёртыванию. Развернуть созданную ML-модель для обнаружения инфекции вируса Covid-19 на рентгеновском снимке лёгкого можно многими способами и на разном оборудовании. Поскольку мы используем Linux, выбираем Linux. Затем открываем окно терминала и запускаем программу.

edge-impulse-runner-linux

После этого Raspberry Pi начнёт загрузку ML-модели и запустит её. Получаем URL-адрес, по которому можно наблюдать видео с камеры Raspberry Pi в реальном времени. Помещаем рентгеновский снимок лёгкого перед камерой (снимок должен быть хорошо освещён). Модель определяет инфицированное лёгкое и через несколько секунд выдает результаты Covid-теста.

Развёртывание ML-модели обнаружения Covid может быть осуществлено и другими способами. Используя SDK, можно обнаруживать Covid, развёртывая ML-модели на Python. Кроме того, можно загрузить рентгеновский снимок непосредственно в набор тестовых данных, а затем запустить ML-модель в режиме живой классификации она сработает за считанные секунды.

20. Выбор платы для развёртывания20. Выбор платы для развёртывания21. Развёртывание ML-модели в Raspberry Pi21. Развёртывание ML-модели в Raspberry Pi22. Обнаружение инфицированного лёгкого по рентгеновскому изображению22. Обнаружение инфицированного лёгкого по рентгеновскому изображению24. Вывод результатов24. Вывод результатов23. Обнаружение инфекции по рентгеновскому снимку23. Обнаружение инфекции по рентгеновскому снимку24. Здоровые лёгкие идентифицируются как здоровые24. Здоровые лёгкие идентифицируются как здоровые25. Обнаружение вирусной инфекции внутри лёгкого на рентгеновском снимке25. Обнаружение вирусной инфекции внутри лёгкого на рентгеновском снимке

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

Такое сочетание Raspberry Pi в решении проблемы коронавируса одним людям покажется очевидным, но другим неожиданным. Область ML сегодня бурно развивается, расширяет сферу влияния, так что впереди нас ждёт ещё много больших и маленьких парадоксов и неожиданностей самого разного рода. Если вы не хотите оставаться в стороне от прогресса ML, то вы можете присмотреться к нашему курсу о машинном и глубоком обучении, который может стать для вас источником новых идей и решений.

Узнайте, как прокачаться и в других специальностях или освоить их с нуля:

Другие профессии и курсы

ПРОФЕССИИ

КУРС

Подробнее..

Твиттер Илона Маска в телеграме и с переводом на русский

16.06.2021 16:13:28 | Автор: admin

Хочу поделиться одной моей поделкой, возможно, кому-то она тоже будет полезна. В этой статье я поделюсь тем, что я сделал, чтобы читать Twitter-аккаунт Маска в удобном мне месте и имея под рукой перевод англоязычных твитов на русский.

Проблема

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

Идея

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

Подводные камни

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

Вторая проблема, отказ твиттера в выдаче доступа к API в описанных мной кейсах. То есть остается единственный вариант заниматься веб-скраппингом. Ну что ж.

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

Технологии

Я решил попробовать самостоятельно и начал гуглить что-то вроде "parsing twitter without API". Нашлось достаточно много решений, сразу скажу, что решениеtwint библиотека с открытым исходным кодом, которая вполне работоспособна и подошла под мою задачу.

Для того, чтобы перевести текст с английского на русский, я сначала было собирался использовать google translate, но понимал, что в нем ограниченное количество бесплатных переводов, решил что попробую использовать единственную известную мне нейросеть для перевода с английского на русскийfairseqот Facebook AI Research. Качество перевода показалось мне вполне приемлемым с точки зрения того, чтобы понять в чем суть твита, хотя оно и не было идеальным.

Все это я обернул в скрипт на языке программирования python и запустил на постоянную работу на своем сервере.

Примеры кода

Чтобы собрать данные из твиттера без использования выделенных девелоперских доступов, логинов, паролей и API, нужно сделать следующее:

Установить библиотеку twint

pip3 install twint

Запустить код формата

twint -u <name_of_twitter_user> -o output.csv --csv --since 2020-01-01 --retweets

Здесь есть важный момент, что запускается это все из-под bash, при том что у библиотеки есть python API (да и написана она на питоне), но при этом я потратил довольно много времени и оно ни в какую не заводилось. При этом если запускать из командной строки - все кроме автоматического перевода постов у меня работало.

Из функционала, который есть у библиотеки еще отмечу:

  • Возможность искать твиты пользователя по ключевому слову

twint -u username -s pineapple
  • Возможность находить твиты пользователя с указанием номеров телефонов и почт

twint -u username --email --phone
  • Поиск твитов вокруг определенной локации

twint -g="48.880048,2.385939,1km" -o file.csv --csv
  • Сохранение в Elasticsearch или SQLite

twint -u username -es localhost:9200twint -u username --database tweets.db
  • Сохранение фоловеров, подписок и избранных для пользователя

twint -u username --followerstwint -u username --followingtwint -u username --favorites

Данные сохраняются в csv файл, в котором присутствуют такие поля как (перечислю те, которые использовал сам, так как их много и большинство несут мало информации):

id - идентификатор сообщения

conversation_id - идентификатор беседы

created_at - дата создания сообщения

tweet - текст сообщения

mentions - упоминания пользователей твиттера ( список словарей)

urls - вставленные по правилам твиттера ссылки (например на youtube)

photos - ссылки на картинки

link - ссылка на твит

reply_to - список словарей с пользователямя, ответом на твиты которых является твит

У библиотеки есть также возможность перевода на другой язык, но она у меня совсем не заработала. Собственно по этой причине я искал другую возможность. Нашел я, как упоминал выше, открытую разработку Facebook AI Research - библиотеку fairseq, в которой можно скачать веса нейронки для перевода в частности из английского в русский и наоборот.

pip install hydra-core

Итого необходимо было установить:

pip install torch pip install hydra-core==1.0.0 omegaconf==2.0.1pip install fastBPE regex requests sacremoses subword_nmt 

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

import torch# Compare the results with English-Russian round-trip translation:en2ru = torch.hub.load('pytorch/fairseq', 'transformer.wmt19.en-ru.single_model',                        tokenizer='moses', bpe='fastbpe')ru2en = torch.hub.load('pytorch/fairseq', 'transformer.wmt19.ru-en.single_model',                        tokenizer='moses', bpe='fastbpe')paraphrase = ru2en.translate(  en2ru.translate('PyTorch Hub is an awesome interface!'))assert paraphrase == 'PyTorch is a great interface!'

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

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

Как пользоваться

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

Итого у меня получилсятелеграм-каналпод названием "Твиттер Илона Маска" (подписывайтесь, мне будет приятно, что это нужно кому-то еще , будет дополнительный стимул поддерживать в будущем), в котором можно

1) читать новые и старые посты Илона Маска

2) видеть перевод текста на русский язык

3) перейти по ссылке на исходный пост в твиттере

И все это без регистрации и смс:)

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

Подробнее..

Перевод Оптимизация платежей в Dropbox при помощи машинного обучения

19.06.2021 16:06:42 | Автор: admin

Представьте ситуацию: вам нужно воспользоваться оплаченным (как вы думаете) сервисом и вдруг оказывается, что он отключен за неуплату. Такая неприятность портит впечатление от бренда, снижая поток прибыли, а внезапно отключенный клиент может не вернуться к сервису. К старту курса о глубоком и машинном обучении делимся переводом о том, как эту проблему решили в Dropbox, где обнаружили, что внедрение ML в обработку клиентских платежей помогает пользователям оставаться довольными и работает лучше внедрённых за 14 лет политик биллинга.


Платежи в Dropbox

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

Всё это происходит за кулисами, когда пользователь Dropbox становится подписчиком. Как только платёж завершается и аккаунт становится платным, также пользователь вступает в платёжный жизненный цикл. Всё это от начала до конца обрабатывается нашей платёжной платформой.

Продление подписки и сбои

Обычно подписчики Dropbox платят за подписку ежемесячно или ежегодно. Если клиент дал нам полномочия снимать оплату, повторный платёж списывается с банковской карты клиента автоматически. Когда оплата проходит успешно, подписка продлевается без каких-либо действий со стороны пользователя.

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

Рисунок 1. Недобровольный отток происходит, когда истекает срок действия кредитной карты, или же она аннулирована, или на ней нет средств и т. д.Рисунок 1. Недобровольный отток происходит, когда истекает срок действия кредитной карты, или же она аннулирована, или на ней нет средств и т. д.

Чтобы определить время платежа от клиента, чья подписка не продлевается, наша платёжная платформа использовала статический набор из примерно 10 различных методов. Так сложилось исторически. Например, мы можем взимать плату с клиента каждые четыре дня, пока платёж не завершится успешно, в течение максимум 28 дней. Если платеж клиента к концу этого срока по-прежнему не выполнен, уровень его учётной записи в Dropbox понижается до бесплатной базовой учётной записи. Конечно, для активных пользователей и команд понижение уровня учётной записи создаёт неприятные впечатления, а для Dropbox недобровольный отток может обернуться упущенной выгодой.

Рисунок 2. Попытки обновленияРисунок 2. Попытки обновления

Сбои в оплате могут произойти по ряду причин. Среди них:

  • нехватка средств;

  • карта с истекшим сроком действия;

  • заблокированная карта возможно, сообщается о потере или краже;

  • непредсказуемые сбои обработки.

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

Зачем машинное обучение в работе с платежами?

Последние два года, чтобы выяснить, повлияет ли изменение времени оплаты на её успешность, Dropbox проводил A/B-тестирование. Чтобы разработать набор правил о том, когда взимать плату, эти тесты в значительной мере опирались на интуицию и знания людей в предметной области.

Команда платежей должна была вручную разделить пользователей на группы в зависимости от их признаков типа подписки, географического местоположения и т. д., а затем выполнить A/B-тест наших десяти или около того различных жёстко закодированных наборов правил, чтобы определить, какие из них лучше всего подходят для этих признаков. Затем команда платежей сохраняла оптимальный вариант политики для этой группы выставления счетов по умолчанию. Периодически команда проводила повторное тестирование, чтобы узнать, изменились ли для разных пользователей лучшие решения.

С другой стороны, этот подход доказал, что момент оплаты влияет на её успех, это позволило Dropbox поддерживать большее количество постоянных подписчиков. Но со временем многие правила перестали работать и достигли потолка эффективности. Кроме того, ручное обновление этих правил сложный, трудоёмкий процесс.

В стремлении уменьшить непроизвольный отток и необходимый для его поддержания объем работы, команда платежей в партнёрстве с командой прикладного машинного обучения для оптимизации выставления счетов экспериментировала с ML.

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

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

  • устранение ручного вмешательства и сложной логики на основе правил;

  • например, Повторяйте каждые X дней или Избегайте попыток оплаты в выходные;

  • глобальная оптимизация множества параметров для конкретных сегментов клиентов;

  • устойчивость к изменениям клиентов и рынка;

  • увеличение общего числа успешных платежей и сокращение времени сбора платежей.

Говоря коротко, применение ML к платежам сделало счастливее и клиентов, и нас.

Как мы сделали это

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

Эксперименты проводились с различными сегментами клиентов, а конкретно начиная с отдельных клиентов и команд в Северной Америке. Мы построили модель ранжирования с градиентным бустингом, обученную на таких признаках, как типы сбоев платежей, шаблоны использования учётной записи Dropbox и характеристики типа оплаты. Модель ранжирует попытки оплаты по прогнозируемой вероятности успеха для каждого окна оплаты.

Например, мы взяли окно в 8 дней, разделив его на часовые промежутки, так, в общей сложности получилось 192 отрезка времени. Чтобы найти самый протяжённый отрезок времени для попытки обновления, мы использовали наши модели. А также экспериментировали с дневными окнами по 6 и 4 часа.

Сначала эксперименты проводились с оптимизацией каждой попытки независимо. У нас была модель, оптимизирующая решение о том, когда взимать плату с клиента после неудачной первой оплаты. Если рекомендуемая попытка модели также проваливалась, в оставшейся части окна обновления мы по умолчанию возвращались к логике правил. A/B-тесты этой комбинации проводились на отдельных сегментах пользователей в США. Для таргетинга применялся внутренний сервис развёртывания функциональности Stormcrow. Модель стала работать лучше, и мы развернули её.

Нашей целью всегда была сквозная оптимизация неудачных попыток обновления. Процесс начинался с единственной моделью, и этот факт помог подтвердить, что ML применимо к решению задач такого типа. Тем не менее мы быстро поняли, что шаблон проектирования с отдельной моделью для каждой попытки оплаты только создал более сложную систему. Например, если бы мы повторили попытку оплаты 5 раз, прежде чем платёж удался, то так получили бы 5 моделей. И это противоречило цели применения ML снижению сложности биллинговой системы.

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

Именно эта модель сегодня проходит A/B-тестирование в производстве при помощи Stormcrow со случайным набором команд участников тестирования Dropbox. Результаты пока положительные.

Predict Service

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

Когда эксперименты только начинались, мы работали с платформой платежей, чтобы загрузить и запустить модели. Из-за добавленных зависимостей архитектура сильно раздулась. Задержки прогноза в среднем составляли около двух минут.

Чтобы упростить процесс, мы воспользовались созданным и управляемым командой платформы ML сервисом Predict Service, этот сервис управляет инфраструктурой для быстрого создания, развёртывания и масштабирования процессов машинного обучения в Dropbox. Применение Predict Service помогло сократить время ожидания при генерации прогнозов модели с нескольких минут до менее 300 мс для 99 % моделей. Переход на Predict Service также обеспечил возможность легкого масштабирования и чистое разделение двух систем.

С помощью этой системы машинного обучения платёжная платформа собирает все относящиеся к клиенту сигналы, запрашивает обслуживаемую через сервис Predict модель, чтобы получить лучшее время выставления счета, таким образом устраняя все наши разработанные и закодированные за 14 лет A/B-тестирования неоптимальные политики биллинга. Рабочий процесс этой системы построен следующим образом:

Белый цвет представляет компоненты платёжной платформы. Фиолетовым цветом обозначены компоненты системы машинного обученияБелый цвет представляет компоненты платёжной платформы. Фиолетовым цветом обозначены компоненты системы машинного обучения
  1. Получение прогноза о следующем лучшем времени списания средств. Когда попытка не удалась, платформа платежей, чтобы получить следующее лучшее время, запрашивает модуль Predict. Запрос выполняется с использованием идентификатора клиента и его типа.

  2. Получение сигналов клиентов. Модуль Predict собирает последние сигналы об использовании и о платежах клиентов, а также информацию о предыдущем сбое. Эти данные сохраняются в Edgestore (основной системе хранения метаданных в Dropbox) ежедневным заданием Airflow Job.

  3. Запрос прогноза. Собранные сигналы отправляются в Predict Service через вызов GRPC, который кодирует сигналы во фрейм данных о признаках, а затем отправляет их в модель.

  4. Генерация прогноза. Модель возвращает ранжированное наилучшее время оплаты. Этот прогноз отправляется обратно в модуль Predict, в свою очередь, результаты в биллинговую политику.

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

  6. Расписание следующего платежа. Как только сервис платежей получает наилучшее время списания средств, он учитывает это время при планировании следующей попытки оплаты и сохраняет в Edgestore.

ML-операции

При развёртывании наша задача не была выполнена. Мы применили передовые методы DevOps к нашим системам сбора данных и прогнозирования: автоматизировали ежедневные задания по сбору данных и установили мониторинг, чтобы он уведомлял о любых сбоях и задержках этих заданий.

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

Бизнес-метрики

  • Коэффициент одобрения счетов. Основная метрика, которую нужно улучшить. При каждом продлении подписки в Dropbox все платежи за продление отслеживаются как часть единого счёта. Эта метрика сообщает нам, было ли обновление подписки успешным.

  • Коэффициент успешности попыток. Эта метрика помогает отслеживать успешность каждой предпринятой от имени пользователя отдельной попытки оплаты. Может быть предпринята одна, две, четыре или более попыток. Этот показатель наряду с показателем утверждения счетов помогает отслеживать, как можно быстро продлить срок действия договора с клиентом.

Внутренний мониторинг модели

Это внутренний в отношении процесса обучения и используемого инструментария комплекс мер. На основе входных данных он помогает нам определить, насколько хорошо настроена модель, а также во время её работы в производственной среде помогает выявить любые проблемы модели. Как вспомогательные для диагностики измеряются следующие показатели онлайн-модели:

  • Охват: процент клиентов, получивших рекомендации от модели, в сравнении с подходом фиксированного интервала в 4 дня.

  • Количество сделанных моделью прогнозов: количество рекомендаций, которые модель успешно выполнила без каких-либо ошибок.

  • Задержка прогнозирования: сколько времени потребовалось модели для составления каждой рекомендации.

Мониторинг инфраструктуры

Наряду с мониторингом и оповещением для платёжной платформы и сервиса Predict мы также отслеживаем следующее, чтобы определить, насколько хорошо работает инфраструктура:

  • свежесть и задержки в конвейерах данных признаков;

  • доступность и задержка сервиса Predict;

  • доступность EdgeStore.

Для мониторинга нашей модели и метрик инфраструктуры мы используем дашборды Grafana и Vortex. Для бизнес-метрик мы используем Superset. Все эти живые метрики и дашборды помогают нам проактивно отслеживать ожидаемое поведение модели, позволяя принимать соответствующие меры, когда оно отклоняется.

Ответственность за мониторинг этих показателей разделена между командой инженеров по платежам и командой прикладного машинного обучения. У нас есть руководства по устранению неисправностей с чёткими путями эскалации, помогающие дежурным инженерам отладить любые проблемы. Поскольку ML был новым для команды инженеров по платежам, мы потратили время на объяснение принципов работы систем и интерпретацию результатов модели. Это помогло двум командам успешно сотрудничать в работе над проектом и обеспечить бесперебойную работу.

Дальнейшие шаги

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

Наша модель, ориентированная на индивидуальных клиентов, в настоящее время внедрена в производство. Модель оптимизации всего цикла обновления сейчас проходит A/B-тестирование. Компания стремится распространить оптимизацию через ML на всех наших клиентов.

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

Здесь мы видим, что в вопросах формальных правил искусственный интеллект может превосходить естественный, а значит, специалисты в области ИИ будут всё более востребованы бизнесом. Если вы не хотите оставаться в стороне от этой бурно развивающийся сферы технологий, то можете обратить внимание на наш курс о машинном и глубоком обучении, где студенты не только создают работающие модели ML и глубокие нейронные сети, но и получают все необходимые специалистам теоретические знания.

Узнайте, как прокачаться и в других специальностях или освоить их с нуля:

Другие профессии и курсы
Подробнее..

Перевод Оптимизация при помощи линейного поиска на Python

13.06.2021 18:05:09 | Автор: admin

Линейный поиск это алгоритм оптимизации, который может использоваться для целевых функций с одной или несколькими переменными. Он предоставляет возможность использовать алгоритм одномерной оптимизации, например поиск методом деления пополам (бисекции) для многомерной целевой функции, работая с линейным поиском для определения оптимального размера шага в каждом измерении от известной точки до оптимума. Мы уже делились переводами Джейсона Браунли, например статьёй о смешанных ансамблях, а в этом учебном руководстве, которое мы перевели к старту курса о машинном и глубоком обучении, рассказывается об основах: вы узнаете, как на Python с помощью линейного поиска выполнить оптимизацию.


Прочитав это руководство, вы узнаете:

  • что линейный поиск это алгоритм оптимизации для одномерных и многомерных задач оптимизации;

  • что библиотека SciPy предоставляет API выполнения линейного поиска, который требует знания о том, как вычисляется первая производная вашей целевой функции;

  • как выполнить линейный поиск для целевой функции и работать с результатом.

Давайте начнём.

Обзор

Этот учебный материал разделён на три части:

  1. Что такое линейный поиск?

  2. Линейный поиск на Python.

  3. Как выполняется линейный поиск? Он состоит из:

a) определения целевой функции;

б) выполнения линейного поиска;

в) работы со сбоями алгоритма.

Что такое линейный поиск?

Линейный поиск это алгоритм оптимизации для одномерной или многомерной оптимизации. Он требует начальной позиции в пространстве поиска, а также указания направления поиска. Затем он из начальной выбирает следующую позицию в пространстве поиска, которая приведёт к значению лучше или же к наилучшему значению целевой функции.

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

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

Один из подходов заключается в применении линейного поиска, выбирающего коэффициент шага, который минимизирует одномерную функцию [...]. Мы можем применить метод одномерной оптимизации по нашему выбору.

Алгоритмы оптимизации, 2019. С. 54.

Альфа коэффициент масштаба для направления, поэтому при поиске учитываются только значения в диапазоне от 0,0 до 1,0. Один шаг линейного поиска решает задачу минимизации, которая минимизирует целевую функцию для текущей позиции в сумме с масштабируемым направлением, то есть:

  • Минимизирует objective(position + alpha * direction).

Таким образом, линейный поиск работает в одном измерении за один раз и возвращает расстояние перемещения в выбранном направлении.

Каждая итерация метода линейного поиска вычисляет направление поиска pk, а затем решает, как далеко двигаться в этом направлении.

Численная оптимизация, 2006. С. 30.

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

Решение приблизительно или неточно и в зависимости от формы пространства поиска может не оказаться общим решением. Условия, при которых этот алгоритм подходит, называются условиями Вольфе. Теперь, когда мы знакомы с линейным поиском, давайте посмотрим, как выполнять его на Python.

Линейный поиск на Python

Выполнить линейный поиск на Python можно вручную, с помощью функции line_search(). Она поддерживает одномерную оптимизацию, а также многомерные задачи оптимизации. Эта функция принимает имя целевой функции и имя градиента для целевой функции, а также текущее положение в пространстве поиска и направление движения.

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

...result = line_search(objective, gradient, point, direction)

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

...# retrieve the alpha value found as part of the line searchalpha = result[0]

Альфа, начальная точка и направление могут использоваться при построении конечной точки линейного поиска.

...# construct the end point of a line searchend = point + alpha * direction

Для задач оптимизации с более чем одной входной переменной, например многомерной оптимизации, функция line_search() вернёт одно альфа-значение для всех измерений. Это значит, функция предполагает, что оптимум равноудалён от начальной точки во всех измерениях, такое ограничение существенно. Теперь, после ознакомления с тем, как в Python выполнять линейный поиск, давайте рассмотрим работающий пример.

Как выполняется линейный поиск?

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

Определение целевой функции

Во-первых, мы можем определить целевую функцию. Здесь поработаем с одномерной целевой функцией, а именно со сдвинутой на небольшую величину от нуля функцией x^2. Это выпуклая функция, она была выбрана потому, что её легко понять, а также легко вычислить первую производную.

  • objective(x) = (-5 + x)^2.

Обратите внимание, что линейный поиск не ограничивается одномерными или выпуклыми функциями. Реализация этой функции приведена ниже.

# objective functiondef objective(x):return (-5.0 + x)**2.0

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

  • gradient(x) = 2 * (-5 + x).

Градиент для каждого входного значения просто указывает наклон к оптимумам в каждой точке. Реализация функции градиента приведена ниже:

# gradient for the objective functiondef gradient(x):return 2.0 * (-5.0 + x)

Можно определить диапазон входных данных для x от -10 до 20 и вычислить целевое значение для каждого входного значения:

...# define ranger_min, r_max = -10.0, 20.0# prepare inputsinputs = arange(r_min, r_max, 0.1)# compute targetstargets = [objective(x) for x in inputs]

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

...# plot inputs vs objectivepyplot.plot(inputs, targets, '-', label='objective')pyplot.legend()pyplot.show()

Связав всё это воедино, получим такой код:

# plot a convex objective functionfrom numpy import arangefrom matplotlib import pyplot # objective functiondef objective(x):return (-5.0 + x)**2.0 # gradient for the objective functiondef gradient(x):return 2.0 * (-5.0 + x) # define ranger_min, r_max = -10.0, 20.0# prepare inputsinputs = arange(r_min, r_max, 0.1)# compute targetstargets = [objective(x) for x in inputs]# plot inputs vs objectivepyplot.plot(inputs, targets, '-', label='objective')pyplot.legend()pyplot.show()

Программа вычисляет входные значения (x) в диапазоне от -10 до 20 и создаёт график, показывающий знакомую U-образную форму параболы. Оптимум функции, по-видимому, находится в точке x=5,0, целевое значение 0,0.

Линейный график выпуклой целевой функцииЛинейный график выпуклой целевой функции

Выполнение линейного поиска

Затем можно выполнить линейный поиск по этой функции. Во-первых, мы должны определить отправную точку поиска и его направление. Здесь воспользуемся начальной точкой x=-5, расстояние от которой до оптимума около 10 единиц. Сделаем большой шаг вправо, в данном случае в 100 единиц (что значительно превышает оптимум), например, в положительном направлении. Напомним, что направление похоже на размер шага и поиск масштабирует размер шага, чтобы найти оптимум:

...# define the starting pointpoint = -5.0# define the direction to movedirection = 100.0# print the initial conditionsprint('start=%.1f, direction=%.1f' % (point, direction))# perform the line searchresult = line_search(objective, gradient, point, direction)

Затем поиск ищет оптимумы и возвращает альфа или расстояние, чтобы изменить направление. Из результата мы можем получить значение альфа, а также количество выполненных вычислений функций:

...# summarize the resultalpha = result[0]print('Alpha: %.3f' % alpha)print('Function evaluations: %d' % result[1])

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

...# define objective function minima end = point + alpha * direction# evaluate objective function minimaprint('f(end) = %.3f' % objective(end))

Затем, для развлечения, мы можем снова построить график функции и показать начальную точку в виде зелёного квадрата, а конечную точку в виде красного квадрата.

...# define ranger_min, r_max = -10.0, 20.0# prepare inputsinputs = arange(r_min, r_max, 0.1)# compute targetstargets = [objective(x) for x in inputs]# plot inputs vs objectivepyplot.plot(inputs, targets, '--', label='objective')# plot start and end of the searchpyplot.plot([point], [objective(point)], 's', color='g')pyplot.plot([end], [objective(end)], 's', color='r')pyplot.legend()pyplot.show()

Ниже приведён полный пример выполнения линейного поиска для выпуклой целевой функции:

# perform a line search on a convex objective functionfrom numpy import arangefrom scipy.optimize import line_searchfrom matplotlib import pyplot # objective functiondef objective(x):return (-5.0 + x)**2.0 # gradient for the objective functiondef gradient(x):return 2.0 * (-5.0 + x) # define the starting pointpoint = -5.0# define the direction to movedirection = 100.0# print the initial conditionsprint('start=%.1f, direction=%.1f' % (point, direction))# perform the line searchresult = line_search(objective, gradient, point, direction)# summarize the resultalpha = result[0]print('Alpha: %.3f' % alpha)print('Function evaluations: %d' % result[1])# define objective function minimaend = point + alpha * direction# evaluate objective function minimaprint('f(end) = f(%.3f) = %.3f' % (end, objective(end)))# define ranger_min, r_max = -10.0, 20.0# prepare inputsinputs = arange(r_min, r_max, 0.1)# compute targetstargets = [objective(x) for x in inputs]# plot inputs vs objectivepyplot.plot(inputs, targets, '--', label='objective')# plot start and end of the searchpyplot.plot([point], [objective(point)], 's', color='g')pyplot.plot([end], [objective(end)], 's', color='r')pyplot.legend()pyplot.show()

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

start=-5.0, direction=100.0Alpha: 0.100Function evaluations: 3f(end) = f(5.000) = 0.000

Наконец, создаётся график функции, показывающий зелёную начальную точку и красную цель.

Линейный график целевой функции с оптимумами и начальной точкой поискаЛинейный график целевой функции с оптимумами и начальной точкой поиска

Работа со сбоями алгоритма

Линейный поиск не гарантирует нахождения оптимумов функции. Он может не найти оптимумы, если задано значение направления, недостаточно большое, чтобы охватить их. Например, найти оптимумы будет невозможно, когда направление имеет значение 3. Продемонстрировать это можно на полном примере ниже:

# perform a line search on a convex objective function with a direction that is too smallfrom numpy import arangefrom scipy.optimize import line_searchfrom matplotlib import pyplot # objective functiondef objective(x):return (-5.0 + x)**2.0 # gradient for the objective functiondef gradient(x):return 2.0 * (-5.0 + x) # define the starting pointpoint = -5.0# define the direction to movedirection = 3.0# print the initial conditionsprint('start=%.1f, direction=%.1f' % (point, direction))# perform the line searchresult = line_search(objective, gradient, point, direction)# summarize the resultalpha = result[0]print('Alpha: %.3f' % alpha)# define objective function minimaend = point + alpha * direction# evaluate objective function minimaprint('f(end) = f(%.3f) = %.3f' % (end, objective(end)))

При выполнении примера поиск достигает предела альфа 1,0, что даёт конечную точку от -2 до 49. При f(5) = 0,0 от оптимумов очень далеко:

start=-5.0, direction=3.0Alpha: 1.000f(end) = f(-2.000) = 49.000

Кроме того, мы можем выбрать неправильное направление, ведущее только к вычислениям хуже стартовой точки. Здесь оно будет отрицательным в сторону от оптимума, например, вверх по склону от начальной точки:

...# define the starting pointpoint = -5.0# define the direction to movedirection = -3.0

Ожидается, что поиск не сойдётся, поскольку он не может найти какие-либо точки лучше начальной. Полный пример поиска, который не сходится, приведён ниже:

# perform a line search on a convex objective function that does not convergefrom numpy import arangefrom scipy.optimize import line_searchfrom matplotlib import pyplot # objective functiondef objective(x):return (-5.0 + x)**2.0 # gradient for the objective functiondef gradient(x):return 2.0 * (-5.0 + x) # define the starting pointpoint = -5.0# define the direction to movedirection = -3.0# print the initial conditionsprint('start=%.1f, direction=%.1f' % (point, direction))# perform the line searchresult = line_search(objective, gradient, point, direction)# summarize the resultprint('Alpha: %s' % result[0])

Выполнение программы приводит к предупреждению LineSearchWarning, указывающему на то, что поиск, как и ожидалось, не может сойтись. Альфа возвращённое в результате поиска значение равно None:

start=-5.0, direction=-3.0LineSearchWarning: The line search algorithm did not convergewarn('The line search algorithm did not converge', LineSearchWarning)Alpha: None

Дальнейшее чтение

Если вы хотите глубже погрузиться в тему, смотрите этот раздел.

Книги

API

Статьи

Резюме

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

  • что линейный поиск это алгоритм оптимизации для одномерных и многомерных задач оптимизации;

  • что библиотека SciPy предоставляет API выполнения линейного поиска, требующий знания о том, как вычисляется первая производная вашей целевой функции;

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

Применяемые в машинном обучении методы оптимизации, конечно же, не ограничиваются одним лишь линейным поиском, они многочисленны, разнообразны и у каждого есть свои недостатки и преимущества. Если вы хотите погрузиться в машинное обучение, изучить оптимизацию глубже, но не хотите ограничивать себя областью ML, вы можете обратить внимание на наш курс "Machine Learning и Deep Learning", партнёр которого, компания NVIDIA, не нуждается в представлении.

Узнайте, как прокачаться и в других специальностях или освоить их с нуля:

Другие профессии и курсы
Подробнее..

Автоматизация машинного обучения

18.06.2021 14:19:30 | Автор: admin

Datascience это не только fit-predict

Представим, что вы начали работать в компании, которая производит однообразные операции с бесконечными таблицами. Например, в крупном ретейлере или у ведущего оператора связи. Ежедневно перед вами ставят задачу выяснить, останется ли клиент с вами или хватит ли товара на полках до конца недели. Алгоритм выглядит просто. Вы берете выборку, изучаете бесконечные ряды признаков, удаляете мусор, генерируете новые признаки, собираете сводную таблицу. Подаете готовые данные в модель, настраиваете параметры и с нетерпением ждете заветных цифр итоговой метрики. Это повторяется день за днем. Затрачивая каждый день всего 60 минут на генерацию фич или подбор параметров, за месяц вы израсходуете минимум 20 часов. Это, без малого, целые сутки, за которые можно выполнить новую задачу, обучить нейросеть или прочесть несколько статей на arxive.

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

TL:DR

В статье мы рассмотрим применение трех решений оптимизации рабочего процесса: генератор признаков featuretools, подборщик гиперпараметров optuna и коробочное решение автоматического машинного обучения от H2O AutoML.

Сравним работу новых инструментов против классических pandas и sklearn. Не будем глубоко закапываться в код. Приведем основные моменты и оставим анализ полного функционала в качестве домашнего задания. Статья будет полезна DS-специалистам и аналитикам. Требуется понимание основных шагов процесса машинного обучения.

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

Нужно больше данных

Начнем с генерации признаков. Зачем вообще нужны новые фичи в машинном обучении? Они агрегируют данные, дают новую информацию для анализа и улучшают качество работы моделей. Например, методы groupby() и sum() это рядовые представители сгенерированных признаков. Классический подход инжиниринга данных изучить датасет вручную и написать код для каждого нового признака. Такой метод практичен, но отнимает много времени и велика вероятность ошибки. Особенно, когда однообразного кода становится много.

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

Инструмент предлагает два направления генерации фич:feature primitives(fp, англ. примитивные признаки) иdeep feature synthesis(dfs, англ. глубокий синтез признаков).Первый метод производит простые математические операции между определенными фичами. Второй, более сложный, позволяет соединять несколько примитивных операций над признаками. Каждая новая операция увеличивает глубину (depth) сложности алгоритма. Например, мы можем посчитать сначала все средние значения по определенному пользователю, а затем сразу суммировать их.

Переходим к практическому применению. Установка и импорт.

pip install featuretoolsimport featuretools as ft

Featuretools обладают очень удобным API и встроенной документацией. Для вызова возможных функций, просто запустите следующий код:

ft.primitives.list_primitives()

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

В нашем примере мы рассмотрим применение библиотеки с одним датасетом. В несколько строк кода мы создадим попарно умноженные и попарно сложенные исходные признаки. Работа featuretools начинается с создания класса EntitySet (англ. множество сущностей), которому обязательно присвоить id. Id используется системой для идентификации определенной сущности, когда вы работаете сразу с несколькими источниками. Затем последовательно добавляем в него имеющиеся датафреймы, в нашем случае единственный. И запускаем автоматическую генерацию признаков.

es = ft.EntitySet(id=data)                                                                           # Новый пустой EntitySetes.entity_from_dataframe(entity_id = january,                                    # Добавляем в него информацию      dataframe=df_train.drop(target, axis=1),      index=1)feature_matrix, feature_defs = ft.dfs(entityset=es,                                # Запускаем генерацию признаков          target_entity=january,          trans_primitives=[add_number, multiply_numeric],          verbose=1)

Вспомните, сколько строк кода вам пришлось бы писать для подобной обработки на pandas. А если бы мы захотели получить более тяжелые признаки? После генерации фич мы получаем pandas датафреймfeature_matrix,который можно сразу отправлять в модель. Ничего конвертировать дополнительно не нужно.

Всю мощь featuretools раскрывает при обработке нескольких таблиц. Подробный туториал с примерами кода есть наофициальном сайте. Рекомендую к ежедневному использованию.

Искусство легких настроек

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

Самые известные инструменты поиска гиперпараметров из sklearn GridSearchCVиRandomizedSearchCV. GridSearchCV перебирает все возможные комбинации параметров на кросс-валидации. RandomizedSearchCV сначала создает словарь с несколькими случайно выбранными параметрами из всех переданных значений. Затем алгоритм тестирует отобранные значения на моделях и выбирает лучшие. Первый метод является крайне ресурсозатратным. Второй быстрее, но мало применим к сложным ансамблям градиентного бустинга. Для обучения серьезных моделей классические алгоритмы не подходят.

Бытует мнение, что истинные профи не запускают подбор, а с первого раза вбивают нужные параметры, просто смотря на входные данные. Для рядовых специалистов есть несколько коробочных решений: optuna, hyperopt, scikit-optimization.

Разберем применения самого быстрого и самого молодого из них optuna.

Кроме настройки классических моделей sklearn и ансамблей бустинга (xgboost, lightgbm, catboost), фреймворк позволяет настраивать нейросети, написанные на pytorch, tensorflow, chainer и mxnet. В отличие от классических sklearn методов, optuna, вместе с другими новыми фреймворками, может обрабатывать непрерывные значения гиперпараметров. Например, альфа- или лямбда-регуляризации будут принимать любые значения с плавающей точкой в заданном диапазоне. Все это делает его одним из самых гибких инструментов настройки моделей глубокого обучения.

В своей работе optuna использует байесовские алгоритмы подбора с возможностью удаления заведомо проигрышного пространства заданных гиперпараметров из анализа. Рассмотрим практическое применение фреймворка. Как библиотека устроена под капотом на английском языке можно прочесть наarxiv.orgНа русском языке есть подробная статья наХабре. В ней вы найдете математическое описание методов оптимизации практически на пальцах.

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

# установка (при необходимости)pip install optuna# импорт библиотеки и средств визуализацииimport optunafrom optuna.visualization import plot_optimization_history, plot_param_importances

Посмотрим на работу модели без подбора гиперпараметров. Базовая метрика RMSE на приватном списке победителей = 0.73562. Запустим optuna и посмотрим на улучшение метрики за 5 итераций. Имеющиеся данные достаточно простые и мы не ждем глобального скачка качества после подбора гиперпараметров. Нам интересен сам процесс и скорость его работы.

Сетка гиперпараметров задается несколькими типами значений непрерывными, категориальными и количественными. Разница в их оформлении выглядит так:

reg_alpha: trial.suggest_loguniform(reg_alpha, 1e-3, 10.0)  # передаем нижнюю и верхнюю границы непрерывного параметраnum_leaves: trial.suggest_int(num_leaves, 1, 300)# передаем нижнее и верхнее значение числового параметраmax_depth: trial.suggest_categorical(max_depth, [-1,10,20]) # передаем список определенных значений категориального признака, которые надо проверить

Сам процесс обучения прописывается в две строки:

study = optuna.create_study(direction=minimize)  # минимизируем ошибкуstudy.optimize(objective, n_trials=5)       # objective  задание для поиска, 5  количество      # итераций оптимизации  

В приложенном к статьеkaggleноутбуке рассматривается базовый вариант запуска optuna. За 5 итераций удалось улучшить метрику на отложенной выборке на 0.1 пункта метрики RMSE. Процесс длился 5 минут. Интересно, сколько по времени работал бы традиционный GridSearchCV с данным количеством параметров. Метрика на финальном сабмите с оптимизацией = 0.7221, что лучше ручной настройки модели на сырых данных.

MachineLearningза 7 строк кода

Такой заголовок подошел бы для кликбейтовой рекламы в интернете. Но мы действительно создадим полный пайплайн обучения за минимальное количество блоков кода. Поможет нам в этомh2оот Amazon. Это библиотека содержит в себе все популярные модули обработки данных и машинного обучения. Разберем модуль автоматического машинного обучения automl. Фреймворк позволяет подбирать модель и параметры без участия специалиста.

Сегодня мы отправим в обработку сырой датафрейм в силу его простоты. В ежедневных задачах данным все же требуется небольшая предподготовка. Для нее удобно использоватьвстроенные алгоритмы. Начнем с установки и импорта модулей.

pip install f https://h2o-release.s3.amazonaws.com/h2o/latest_stable_Py.html h2o from h2o.automl import H2OAutoML# установим максимальный размер используемой оперативной памятиh2o.init(max_mem_size=16G)

Pandas больше не нужен! Можно загружать данные в рабочий ноутбук с помощью AutoML.

train = h20.import_file(../input/tabular-playground-series-jan-21/train.csv)test = h20.import_file(../input/tabular-playground-series-jan-21/test.csv)

Выберем признаки и таргет.

x = test.columns[1:]y = target

Запускаем машинное обучение. Библиотека будет последовательно обучать встроенные модели с различными гиперпараметрами. В нашем примере настроим количество моделей, random_state=47 и максимальное время обучения одной модели = 3100 секунд (3600 секунд по умолчанию)

aml = H20AutoML(max_models=2,  # Количество различных моделей для обучения                                 seed=SEED,                                  max_runtime_secs = 3100)   # Максимальное время обучения одной моделиaml.train(x=x, y=y, training_frame=train)# Ждем, когда заполнится строка обучения AutoML

Готово! Параметры подобраны, модели выбраны. Все сделано в автоматическом режиме. Можно посмотреть на метрики лучших моделей или вывести информацию о наилучшей. Подробный код смотрите в учебном ноутбуке наkaggle. Остается создать и отправить на валидацию предсказания модели. Это единственный случай, когда потребуется загруженный pandas.

preds = aml.predict(test) df_sub[target] = preds.as_data_frame().values_flatten() df_sub.to_csv(h2o_submission_5.csv, index=False)

Финальная метрика AutoMl на приватном списке победителей = 0.71487. Напомню, что метрика среднеквадратичной ошибки (root mean squared error, RMSE) отражает среднее отклонение предсказаний от реального значения. Простыми словами она должна быть минимальной и стремиться к нулю. RMSE у победителя соревнования = 0.69381. В пределах рутинных задач с которыми можно столкнуться в ежедневной работе разница так же невелика. Но время AutoML экономит значительно.

Заключение

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

Оптимизация параметров и автоматизированное обучение повысили качество финальной метрики RMSE в сравнении с базовой моделью, на 0.2 пункта. Для простых синтетических данных это адекватный результат, который демонстрирует применимость методов к серьезным задачам.

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

Подробнее..

Геопространственное моделирование с применением методов машинного обучения

18.06.2021 18:20:53 | Автор: admin


Всем привет! Меня зовут Константин Измайлов, я руководитель направления Data Science в Delivery Club. Мы работаем над многочисленными интересными и сложными задачами: от формирования классических аналитических отчетов до построения рекомендательных моделей в ленте приложения.

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

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

Статья написана по мотивам выступления с Евгением Макиным на конференции Highload++ Весна 2021. Для тех, кто любит видео, ищите его в конце статьи.

Бизнес-модель работы Delivery Club


Бизнес-модель Delivery Club состоит из двух частей:

  • ДДК (доставка Деливери Клаб): мы передаем заказ в ресторан и доставляем его клиенту, то есть ресторану остается только приготовить заказ к определенному времени, когда придет курьер.
  • МП (маркетплейс): мы передаем заказ в ресторан, а он своими силами доставляет заказ в пределах своей согласованной зоны.

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

При этом построить зону доставки это полбеды. Ведь география ДДК огромна и охватывает более 170 городов России с тысячами ресторанов в каждом из них, и у каждого ресторана свои индивидуальные особенности, которые необходимо учитывать при построении эффективного сервиса доставки.

Рисуем зону доставки ресторана


С одной стороны, мы хотим сохранить качество нашего сервиса, минимизируя время доставки до клиента: меньше зона курьер быстрее добирается от ресторана до клиента. При этом все партнеры разные, и к каждому должен быть индивидуальный подход. Например, кофейня хочет доставлять в близлежащий офис и супербыстро, иначе горячий кофе остынет и будет невкусным. А вот для ресторана, который специализируется на кейтеринге, время доставки хоть и важно, но остается вторичным фактором по сравнению с максимальным охватом потенциальных клиентов. Также не стоит забывать про курьеров, которым, к примеру, придется доставлять заказ на другую сторону реки :)



Как процесс выглядел раньше


Исторически сложилось, что за построение зон доставки отвечали координаторы операционной команды, которые вручную перерисовывали зоны для каждого из наших партнеров. Это было трудоемко, требовало должной сноровки и, самое главное, терпения. Несмотря на набор правил, в процессе присутствовала субъективная оценка. Даже можно было заметить индивидуальный стиль отрисовки зон, который прослеживался у каждого из менеджеров.

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



Стоит упомянуть и про SLA (Service Level Agreement соглашение о максимальной длительности отрисовки зоны доставки для одного партнера): онбординг партнера или подготовка его зоны для внедрения в нашу платформу составляли порядка 40 минут для одного заведения. Представьте, что к вам подключилась городская сеть с сотней ресторанов, а если это ещё и жаркий сезон, например, после проведения рекламной акции Вот наглядное доказательство неэффективности ручной отрисовки:

$T = R * SLA = 100 * 40\ минут =\ \sim 67\ часов\ ручной\ работы$


где $T$ время, которое будет затрачено на отрисовку всех зон доставки партнера,
$R$ количество ресторанов,
$SLA$ время на отрисовку одной зоны.

Проблемы ручной отрисовки зон:


  • непостоянство результата;
  • непрозрачная логика;
  • субъективизм в принятии решений о форме зоны и ее характеристиках;
  • однообразный подход без учета индивидуальных особенностей партнера;
  • крайне низкая скорость построения зон;
  • ручной труд менеджеров;
  • отсутствие гибкости в управлении характеристиками зон доставки (зона представляла собой единую нераздельную сущность, внутри которой нельзя было установить разную стоимость доставки в зависимости от расстояния между клиентом и рестораном).

Baseline


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

При этом оставались недостатки:

  • артефакты в зонах доставки (стандартный случай с переходом через реку);
  • единообразный подход к партнерам (не учитываются индивидуальные KPI партнеров).

Через Delivery Club ежедневно проходят сотни тысяч заказов, и артефакты зон доставки могли дать большую нагрузку на колл-центр.



Пробовали алгоритмы на основе Convex Hull и Alpha Shape, с их помощью можно было бы устранить артефакты. Например, используя границы водных массивов как опорные точки для построения форм удавалось обходить реки. Однако всё равно отсутствовал индивидуальный подход к партнерам.

Преимущества технологии H3


Мы стремились прийти к универсальному решению. Каких-то простых и популярных подходов найти не удалось, поэтому мы сосредоточились на разработке собственного алгоритма. А за основу взяли технологию H3 компании Uber.


Источник: eng.uber.com/h3

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

Также стоит отметить, что:

  1. Существует хорошая библиотека для работы с H3, которую и выбрала наша команда в качестве основного инструмента. Библиотека поддерживает многие языки программирования (Python, Go и другие), в которых уже реализованы основные функции для работы с гексагонами.
  2. Наша реляционная аналитическая база Postgres поддерживает работу с нативными функциями H3.
  3. При использовании гексагональной сетки благодаря ряду алгоритмов, работающих с индексами, можно очень быстро получить точную информацию о признаках в соседних ячейках, например, определить вхождение точки в гексагон.
  4. Преимуществом гексагонов является возможность хранить информацию о признаках не только в конкретных точках, но и в целых областях.

Алгоритм построения зон доставки


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


  2. Убираем точки-выбросы. Анализируем все постройки и сразу отбрасываем те, которые нас не интересуют. Например, какие-то мелкие нежилые объекты. Далее с помощью DBSCAN формируем кластеры точек и отбрасываем те, которые для нас не являются важными: например, если кластер находится далеко от ресторана или нам экономически невыгодно доставлять туда.


  3. Далее на основе очищенного набора точек применяем триангуляцию Делоне.


  4. Создаем сетку гексагонов H3.


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


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



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

    • минимизацию времени доставки при фиксированном покрытии;
    • максимизацию охвата пользователей при фиксированном времени доставки.

    Пример функции ошибки для минимизации времени доставки:

    ${L_{min}}_{time}\;=\;min(\sum_{i=1}^n\;({t_{rest}}_i)/n),$


    где $L_{min_ {time}}$ функция ошибки минимизации времени доставки с фиксированным покрытием,
    $t_{rest_ {i}}$ время от ресторана i до клиента,
    $n$ количество клиентов в зоне доставки.

  7. Далее строим временной градиент в получившихся зонах (с очищенными выбросами) и с заранее определенными интервалами (например, по 10-минутным отрезкам пешего пути).



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

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

Внедрение


Алгоритм написан, технические и нагрузочные тесты пройдены, остается самое важное: внедрить алгоритм в бизнес-процессы.

Здесь мы столкнулись с тем, что очень трудно было менять сложившиеся бизнес-процессы, которые были налажены годами. Операторы очень не хотели передавать свои KPI машине.

С другой стороны, сами партнеры с опаской подходили к новомодной фиче, которую им предлагали протестировать в бою. У них не было уверенности, что такую сложную задачу можно автоматизировать с сохранением прежнего качества. А те, кто отваживались, тестировали странно: предоставляли заведения с маленьким количеством заказов, для которых было достаточно трудно накопить историю для нахождения статистически значимых различий.

Но тут пришел COVID-19

  • тысячи ресторанов закрывают свои филиалы;
  • увеличиваются зоны доставки;
  • сокращается меню и меняется целевая аудитория;
  • меняется формат работы ресторанов;
  • поменялось еще и географическое распределение заказов: до начала пандемии много заказов приходило из офисов, а во время пандемии все сидели дома и заказывали оттуда.

Всё это отчасти помогло нам убедить партнёров активнее тестировать и переходить на алгоритм автоматического построения зон доставки. В итоге, за первую неделю после начала пандемии и введения ограничения для ресторанов порядка 50% наших партнеров перешло на новые зоны доставки, которые уже были построены автоматически, а в течение следующих 3 недель уже почти не осталось партнеров с зонами, построенными вручную.

Оценка


После решения всех горящих проблем нам нужно было немного отдышаться и понять, что мы вообще наделали. Для этого воспользовались A/B-тестом, а точнее его вариацией switch-back. Мы сравнивали зоны ресторанов с одинаковыми входными параметрами, оценивали GMV и время доставки, где в качестве контроля у нас были простые автоматически отрисованные зоны в виде окружностей и прямоугольников, либо зоны, отрисованные операторами вручную.



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

А время, затрачиваемое на построение зон для партнера из примера выше, теперь выглядит более оптимистично:

$T = 100 * 3,6\ секунды =\ \sim 6\ минут$


Ускорение в 670 раз!

Текущая ситуация и планы


Сервис работает в production. Зоны автоматически строятся по кнопке. Появился более гибкий инструмент для работы со стоимостью доставки для клиентов в зависимости от их удаленности от ресторана. 99,9% ресторанов (изредка ещё нужно ручное вмешательство) перешли на алгоритмические зоны, а наш алгоритм поспособствовал переходу бэкенда на H3.

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

Работа с географией и зонами открыла нам дорогу к новым проектам: рекомендации для размещения, гранулярная работа с зонами доставки, более точный прогноз длительности доставки для клиента, ценообразование.

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

Всем спасибо!

Подробнее..

Перевод ТОП-10 трендов в сфере данных и аналитики 2021. Версия Gartner

15.06.2021 10:13:02 | Автор: admin
Оракул технологического мира Gartner регулярно и охотно делится с обществом своими наблюдениями относительно текущих трендов. Эксперты компании составили подборку из 10 трендов в сфере данных и аналитики, которые стоит учитывать ИТ-лидерам в 2021 году от искусственного интеллекта до малых данных и применения графовых технологий.

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

Источник

Коротко о трендах


В предложенном материале Gartner выделяет ряд трендов в индустрии, связанной с машинным обучением и искусственным интеллектом. Не стоит ожидать, что статья откроет новые горизонты: в ней собраны те особенности и тренды, которые уже прошли этап новаторства, а также этап привлечения ранних последователей, однако если не обратить должного внимания на отмеченные тенденции, то можно опоздать даже попасть в категорию отстающих последователей. Кроме того, в статье явно прослеживаются рекламные и побудительные элементы, нацеленные на аудиторию, влияющую на инновации в своей области бизнеса, т.е. на основную аудиторию Gartner. В процессе перевода не удалось уйти от упомянутых элементов, однако рекомендуется к ним относиться снисходительно, т.к. эти рекламные вставки перемежаются ценной информацией. Некоторые из трендов напрямую связаны с изменениями в индустрии, к которым привела эпидемиологическая обстановка. Другие с растущей популярностью систем автоматического принятия решений и использованию ИИ в бизнес-аналитике. Отдельно хочется отметить тренд, связанный с графовыми методами, которые быстро развиваются и набирают все большую популярность. Тем не менее, некоторые из них носят скорее номинальный характер. Одним из таких номинальных трендов на первый взгляд кажется термин XOps, в котором Gartner объединяет направления DataOps, ModelOps и DevOps, комментируя свое видение следующим образом: Умножение дисциплин Ops, вытекающих из лучших практик DevOps, вызвало значительную путаницу на рынке. Тем не менее, их согласование может принести значительные преимущества организациям, которые способны гармонизировать эти дисциплины Практики XOps объединяют разработку, развертывание и обслуживание, чтобы создать общее понимание требований, передачу навыков и процессов для мониторинга и поддержки аналитики и артефактов ИИ. В этом, казалось бы, номинальном тренде, прослеживается мысль, отсылающая к теме Франкенштейна: мало состыковать отдельные рабочие части компании, т.к. они будут функционировать хаотично и не согласовано, жизнь и полезная активность начнется после того, как эти разрозненные части будут синхронизированы и гармонизированы. Но не буду раскрывать все карты сразу, предлагаю читателю самостоятельно ознакомиться с находками Gartner далее.

Как изменилась работа data-специалистов


По словам экспертов Gartner, на фоне COVID-19 организации, использующие традиционные методы аналитики, которые в значительной степени полагаются на большие объемы исторических данных, осознали одну важную вещь: многие из этих моделей больше не актуальны. По сути, пандемия изменила все, сделав множество данных бесполезными. В свою очередь, прогрессивные команды, занимающиеся обработкой данных и аналитикой, все больше переходят от традиционных методов искусственного интеллекта, основанных на больших данных, к классу аналитики, использующей малые или более разнообразные данные.

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

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

Каждая из тенденций соответствует одной из трех основных тем:

  1. Ускорение изменений в данных и аналитике: использование инноваций в области искусственного интеллекта, улучшенная возможность по компоновке, а также более гибкая и эффективная интеграция разнообразных источников данных.
  2. Операционализация ценности бизнеса посредством более эффективного использования XOps: позволяет лучше принимать решения и превращать данные и аналитику в неотъемлемую часть бизнеса.
  3. Принцип все распределено: предполагает гибкое соотнесение данных и идей для расширения возможностей более широкой аудитории людей и объектов.

Тренд 1. Продвинутый, ответственный, масштабируемый ИИ


Более умный, ответственный, масштабируемый ИИ позволит улучшить алгоритмы обучения, интерпретируемых систем и сократить время оценки. Организации начнут требовать гораздо большего от систем искусственного интеллекта, и им нужно будет выяснить, как масштабировать технологии до сих пор это было сложной задачей.

Хотя традиционные методы ИИ могут в значительной степени полагаться на исторические данные, учитывая, как COVID-19 изменил бизнес-ландшафт, исторические данные могут больше не иметь значения. Это означает, что технология ИИ должна быть способна работать с меньшим количеством данных с помощью методов малых данных и адаптивного машинного обучения. Эти системы ИИ также должны защищать приватность, соблюдать федеральные правила и минимизировать предвзятость для поддержки этичного ИИ.

Тренд 2. Составные данные и аналитика


Целью составных данных и аналитики является использование компонентов из множества данных, аналитики и решений ИИ для создания гибкого, удобного, адаптированного под потребности пользователей интерфейса, который позволит руководителям связывать аналитические данные с бизнес-действиями. Запросы клиентов Gartner показывают, что в большинстве крупных организаций имеется более одного стандартного корпоративного инструмента аналитики и бизнес-аналитики.

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

Тренд 3. Фабрика данных как основа


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

Фабрика данных сокращает время на проектирование интеграции на 30%, развертывание на 30% и поддержку на 70%, поскольку технологические разработки основаны на возможности использования / повторного использования и комбинирования различных стилей интеграции данных. Кроме того, фабрики данных могут использовать существующие навыки и технологии из data-хабов (data hubs), озер данных (data lakes) и хранилищ данных (data warehouses), а также внедрять новые подходы и инструменты для будущего.

Тренд 4. От больших данных к малым и широким данным


Малые и широкие данные, в отличие от больших данных, решают ряд проблем для организаций, которые сталкиваются со все более сложными вопросами, касающимися ИИ, и проблемами, связанными с редкими вариантами использования данных. Широкие данные с использованием методов X-аналитики позволяют анализировать и объединять многообразие малых и широких, неструктурированных и структурированных источников данных для повышения осведомленности о контексте и принимаемых решениях. Малые данные, как следует из названия, могут использовать модели данных, которые требуют меньше данных, но все же предлагают полезные инсайты.

Источник

Тренд 5. XOps


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

Тренд 5. XOps. Источник

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

Тренд 6. Проектирование интеллекта принятия решений


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

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

Тренд 7. Данные и аналитика как ключевая бизнес-функция


Руководители бизнеса начинают понимать важность использования данных и аналитики для ускорения инициатив цифрового бизнеса. Вместо того, чтобы быть второстепенной задачей, выполняемой отдельной командой, данные и аналитика переключаются на основную функцию. Однако руководители предприятий часто недооценивают сложность данных и в конечном итоге упускают возможности. Если директора по данным (CDO) участвуют в постановке целей и стратегий, они могут увеличить стабильное производство стоимости бизнеса в 2,6 раз.

Тренд 8. Графы в основе всего


Графовые подходы формируют основу современных данных и аналитики, предоставляя возможности для усиления и улучшения взаимодействия c пользователями, моделей машинного обучения и интерпретируемого ИИ. Хотя графические технологии не новы для данных и аналитики, произошел сдвиг в мышлении вокруг них, поскольку организации выявляют все больше вариантов их использования. Фактически, до 50% запросов клиентов Gartner о ИИ связаны с обсуждением использования graph-технологий.

Источник

Тренд 9. Расширение пользовательского опыта


Традиционно бизнес-пользователи были ограничены использованием преднастроенных панелей аналитики (dashboard) и ручных инструментов исследования данных. Чаще всего это предполагало, что панели аналитики ограничивались работой дата-аналитиков или гражданских специалистов по данным, которые изучали заранее определенные вопросы.

Однако Gartner полагает, что в дальнейшем эти информационные панели будут заменены автоматизированными, интерактивными, мобильными и динамически генерируемыми аналитическими данными, адаптированными к потребностям пользователей и доставляемыми в их точку потребления. И это, в свою очередь, означает переход знаний от ограниченного круга специалистов в области данных к любому сотруднику организации.

Тренд 10. Данные и аналитика впереди планеты всей


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

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

В заключение


Сложно переоценить слова Gartner: наблюдаемые тренды, безусловно, играют немаловажную роль в дальнейшей судьбе индустрии. В отмеченных трендах явно выделяется фокус на инженерную составляющую индустрии ИИ: поддержка и контроль качества моделей машинного обучения, ответственность и масштабируемость ИИ, повсеместность использования и т.д. Это наблюдение лишний раз подтверждает то, что долина разочарования, связанная с проектами по применению ИИ уже пройдена, мир принял технологию и теперь более актуальными становятся вопросы интеграции, оптимизации, контроля качества и надежности.

Нас, представителей компании CleverDATA, активно использующих ИИ в повседневной работе, отчасти уже коснулись упомянутые в статье тренды: мы пришли к аналогичным выводам через собственный опыт. Благодаря выделенным в статье тенденциям, у читателей есть возможность подготовиться к переменам в индустрии ИИ, например, освоить методы работы с графовыми нейронными сетями или взяться за освоение элементов профессии Data Engineer. Предусмотрительный работник готовит не только сани летом, но и актуальные в будущем навыки уже сейчас.
Подробнее..

Мы сделали наш публичный синтез речи еще лучше

18.06.2021 14:19:30 | Автор: admin

6cc6e0011d4d26aeded6f052080b1890


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


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


Если коротко:


  • Мы сделали наш вокодер в 4 раза быстрее;
  • Мы сделали пакетирование моделей более удобным;
  • Мы сделали мультиспикерную / мультязычную модель и "заставили" спикеров говорить на "чужих" языках;
  • Мы добавили в наши русские модели возможность автопростановки ударений и буквы ё с некоторыми ограничениями;
  • Теперь мы можем сделать голос с нормальным качеством на 15 минутах 1 часе (с теплого старта в принципе заводилось даже на 3-7 минутах) или на 5 часах аудио (с холодного старта). Но тут все очень сильно зависит от качества самого аудио и ряда деталей;
  • Мы привлекли коммьюнити к работе, и нам помогли сделать удобный интерфейс для записи. Мы начали работу над голосами на языках народностей СНГ (украинский, татарский, башкирский, узбекский, таджикский). Если вы хотите увидеть свой язык в числе спикеров пишите нам;
  • Мы продолжаем собирать обратную связь по применимости нашей системы для экранных интерфейсов чтения, и пока кажется, что нужно где-то еще всё ускорить в 5-10 раз, чтобы наши модели закрывали и этот кейс;

Справедливая критика


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


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


Хотя мы вроде явно написали про это и даже сделали warning в коде про то, что модель принимает только предложения (а не целые тексты или книги), все равно основной поток комментариев был именно про это. Также мало кто обратил внимание на раздел статьи про скорость работы моделей и батчи (если вы не видели его, прочитайте).


Я привел в комментариях к статье пару простейших примеров в виде ответов на вопросы (раз, два, три, четыре), но чтобы не возвращаться к этому правильный алгоритм работы с моделью такой:


  • Сначала текст надо разбить на предложения оптимальным способом для вашего домена;
  • Потом надо проставить ударения (новые модели русского языка автоматически проставляют ударение и там, где его нет, и букву ё, но можно поставить и руками);
  • Далее надо кормить модель батчами в соответствии с оптимальным сайзингом (например на 2 ядрах процессора оптимальнее всего использовать батч-сайз 1-2);

Упрощаем запуск


Вспоминая опыт с silero-vad, чтобы не множить сущности, мы опубликовали наш синтез в нашем репозитории silero-models. Это имело очевидные плюсы, но и ряд минусов:


  • Если запускать модель через интерфейс с torch.hub, то нахождение в одном репозитории с моделями распознавания речи требовало установки двух библиотек (omegaconf для парсинга yaml-конфига и torchaudio для чтения аудио). Сам синтез не имеет внешних зависимостей кроме стандартной библиотеки питона и PyTorch. Но, если судить только по гневным комментариям и сообщениям в личку, это оказалось слишком сложным даже если дисконтировать радикальные мнения (мне всерьез писали в личку люди, c энтузиазмом стремящиеся доказать что "питон говно"). Люди в итоге не обращали внимание как на интерактивное демо в colab, так и на standalone примеры. По этой причине через какое-то время я добавил пример # Minimal Example to Run Locally;
  • Вообще конечно в идеале для полностью независимого оффлайнового запуска нужно было просто скачать модель, взять этот скрипт загрузки модели, дополнить его своими функциями и убрать лишнее. Но это тоже оказалось слишком сложным и неочевидным;

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


Если вызов через torch.hub по сути особо и не изменился:


import torchlanguage = 'ru'speaker = 'kseniya_v2'sample_rate = 16000device = torch.device('cpu')model, example_text = torch.hub.load(repo_or_dir='snakers4/silero-models',                                     model='silero_tts',                                     language=language,                                     speaker=speaker)model.to(device)  # gpu or cpuaudio = model.apply_tts(texts=[example_text],                        sample_rate=sample_rate)

То полностью standalone вызов стал сильно проще:


import osimport torchdevice = torch.device('cpu')torch.set_num_threads(4)local_file = 'model.pt'if not os.path.isfile(local_file):    torch.hub.download_url_to_file('https://models.silero.ai/models/tts/ru/v2_kseniya.pt',                                   local_file)  model = torch.package.PackageImporter(local_file).load_pickle("tts_models", "model")model.to(device)example_batch = ['В недрах тундры выдры в г+етрах т+ырят в вёдра ядра кедров.',                 'Котики - это жидкость!',                 'М+ама М+илу м+ыла с м+ылом.']sample_rate = 16000audio_paths = model.save_wav(texts=example_batch,                             sample_rate=sample_rate)

Снижение требований по количеству часов и расширение базы голосов


Не секрет, что записывать 15 20 часов аудио это тяжелая и кропотливая работа. Мы проверили некоторые исследования, чтобы понять, сколько часов нужно реально для того, чтобы сделать адекватный голос. Например системы синтеза прошлого поколения требуют около 2-3 часов аудио.


У нас получились такие результаты:


Количество аудио Тип старта Результат
15 20 часов с холодного модели в репозитории
5-6 часов с холодного заводится, нормальное качество, примеры ниже
3 часа с холодного заводится, но речь уже не очень членораздельна
1 час с холодного не заводится совсем
-------------------------- ------------------------ ---------------------------------------------------------
5-6 часов с теплого, похожий голос заводится чуть быстрее, нормальное качество, примеры ниже
5-6 часов с теплого, другой пол заводится, нормальное качество, примеры ниже
5-6 часов с теплого старта с теплого, другой язык заводится, нормальное качество, примеры ниже
3 часа с теплого, похожий голос заводится, качество чуть хуже, примеры ниже
1 час с теплого, похожий голос заводится, качество чуть хуже, примеры ниже
3 15 минут с теплого, похожий голос заводится, на 3 минутах уже сильно проседает качество

С холодного старта, 6 часов:




С теплого старта, 6 часов:



С холодного старта, 3 часа:


Тут уже понятно, что для холодного старта 3 часов маловато.



С теплого старта, 3 часа:



С холодного старта, 1 час:


На холодном старте 1 час вообще уже не работает и генерирует хрип вместо голоса.



С теплого старта, 1 час:


На 1 часу с теплого старта продолжает работать.



С теплого старта, 3 15 минут:


И тут нам стало интересно, а какой же минимальный порог после которого с теплого старта перестает работать? На похожем голосе получилось, что нижняя граница лежит где-то в районе 3 минут.





На моем голосе из голосового чата:


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


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


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



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


Как и в случае с видео дипфейками, новостное поле искажает их реальный потенциал и возможности, и скорее ограничением на массовое создание "поддельного" контента является даже не столько спорная этическая и финансовая составляющая (я не уверен какой будет LTV у сервиса с поздравлениями, но это скорее всё-таки не про наш рынок), сколько банальное количество усилий, которое надо все равно вложить, чтобы получить хорошее звучание.


Немного усложним задачу:


Давайте теперь попробуем теплый старт на немецком языке, с непохожего голоса другого пола и с непохожего голоса с другого языка:





Хм, все работает с некими оговорками.


Прочие эксперименты:


Ну и напоследок попробуем заставить Ксению говорить по-немецки. Например, вот эту фразу: Mein Knig, das Fichtenbaum, Bundesausbildungsfrderungsgesetz, die Ubng..



Звучит как ученик на первом уроке немецкого языка. Но мы знаем как сделать это лучше. Кстати одна из небольших фич текущего релиза, что можно заставлять спикеров говорить на других языках. Это пока сильно неидеально работает, но все равно уже прикольно.


Чтобы не разбегались мысли подведем некоторый итог:


  • Для более менее качественного синтеза точно достаточно 5-6 часов качественного аудио (особенно, если новый язык сильно не похож на имеющиеся);
  • Если записи очень качественные и язык похож на имеющиеся, то в принципе можно опускаться и ниже, вплоть до 15 минут 1 часа;
  • Естественно интонации и эмоции модель выучивает из записанного корпуса, искусственно управлять интонациями мы пока не научились;
  • Именно качество аудиозаписей является критическим моментом для качества синтеза;
  • Мы пока не понимаем как добиться качества и интонаций сравнимых с Алисой, но есть подозрение, что дело в количестве часов (40 100 часов) и чистоте аудио;

Мультиязычная модель


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


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













Записываем голоса языков народностей СНГ


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


photo_2021-05-17_18-05-35


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


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


Ускорение модели


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


В текущей итерации у нас получилось ускорить вокодер примерно в 4 раза почти без потери качества (на глаз потеря 0.1 0.2 MOS в среднем) и достичь примерно таких цифр:


Модель 8 kHz 16 kHz
v1 только вокодер, 1 поток 18 8
v2 только вокодер, 1 поток 70 35

Это великолепный результат, но теперь осталось подтянуть все остальное. Но тут уже технически нам не хватает ресурсов, чтобы все 10 вещей делать одновременно. В релизе v2 также немного пришлось пожертвовать качеством отдельных моделей в силу ограниченности ресурсов.


Автоматическая простановка ударений


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


Основные проблемы:


  • Слов в русских и украинских языках реально много. Словарь может весить 100 200 мегабайт. Нужно не только добиться приемлемого качества и скорости работы моделей но и высокой степени сжатия моделей по сравнению со словарем;
  • В отличие от словаря, модели обладают хоть какой-то генерализацией;
  • У слов в языке сильно отличается частотность и надо соблюсти баланс между размером всей системы, точностью на всех когортах и стоимостью обладания системой;

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


  • Для самых популярных слов и словоформ (их примерно 130 тысяч), мы ставим ударение с точностью 99.9%;
  • Для слов со средней популярностью (их примерно 540 тысяч), мы ставим ударение с точностью 99.9%;
  • Примерно 1,300 слов мы включаем в словарь исключений (ошибки на остальных словах из этих когорт);
  • Для слов с низкой популярностью (длинный хвост, примерно 2 миллиона), мы ставим ударение с точностью 99%;
  • В каждой из этих категорий есть примерно 3% слов-омографов, которые мы пока не можем обработать (например зАмок замОк, хлОпок хлопОк). Такие слова наша модель специально пропускает, тем самым перенося бремя по простановке усредненного ударения на нашу модель синтеза;

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


Задача простановки буквы ё была также решена с некоторыми оговорками:


  • Омографы, связанные с буквой ё (например все всё, колеса колёса), не пропускаются, как в случае с ударениями. Выбрать подобные омографы из корпуса оказалось нетривиальной задачей (часто буква ё на письме игнорируется, поэтому отделить омограф от hard negative сложно без специально размеченных данных);
  • На словаре буквы ё мы ставим её с точностью 99% (как для hard positive, так и для hard negative слов);
  • Модель немного генерализовалась на слова, которых она не видела при обучении. Но всё-таки общее поведение на незнакомых словах не ставить ё;
  • Случается, что модель предсказывает ударение и букву ё на разные позиции. В таких ситуациях буква ё не будет проставлена;

Есть ещё одна проблема, не решённая на данном этапе: слова с побочными ударениями или несколькими буквами ё (например авиаметеослужба, премьер-министр, трёхколёсный).
Сейчас модель проставляет только одно ударение (и одну ё) в таких словах, но мы планируем в будущем исправить эти кейсы.


Несправедливая критика


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


Если перечислить самые популярные претензии:


  • Нет middleware под конкретный домен или платформу;
  • Нет приложения / интеграции в какое-то другое существующее приложение под какую-то платформу;
  • Слишком сложно, невозможно разобраться;
  • Алиса звучит лучше;

На первые ответ довольно простой middleware делаются на заказ как часть коммерческого продукта. Если вы хотите поддерживаемый клиент / интеграцию под какую-то платформу, то цена вопроса цена на поиск + зарплата команды разработки (1-2 человека), умноженная на требуемый срок поддержки.


Насчет Алисы вы сами выбираете дарить ли свои персональные данные корпорациям. Практика показывает, что де-факто никакой защиты от их недобросовестного поведения нет кроме сил конкуренции. Как говорится: "Если подписал не удивляйся". Также мы примерно приценивались к "звездным" голосам, там только бюджет на запись голоса уже в разы больше бюджета на весь некоммерческий релиз целиком (нам называли оценки в районе 0.25 0.5 миллиона долларов только за запись голоса).


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


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


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


Дальнейшие планы


Текущий релиз:


  • Ускорение вокодера в 4 раза;
  • Многоязычная мультиспикерная модель (и возможность говорить на другом языке с акцентом);
  • Снижено количество файлов и упрощено пакетирование;
  • Добавление автоматической простановки ударений и простановки буквы ё;
  • Снижение требований по количеству данных и начало работы над голосами народностей СНГ;

Следующие релизы:


  • Высота голоса и скорость;
  • Радикальное ускорение моделей (10+ раз);
  • Эмоции, управление интонацией;
  • Еще большее снижение требований по данным;
  • Добавление новых голосов по мере появления открытых голосов на других языках;
  • Добавление малых языков и языков народностей России и СНГ по мере сбора датасетов;
Подробнее..

Управляем звуком ПК от активности пользователя с помощью Python

17.06.2021 14:15:17 | Автор: admin

Настройка программного обеспечения

Без промедления начнём. Нам нужно установить следующее ПО:

  • Windows 10

  • Anaconda 3 (Python 3.8)

  • Visual Studio 2019 (Community) - объясню позже, зачем она понадобится.

Открываем Anaconda Prompt (Anaconda3) и устанавливаем следующие пакеты:

pip install opencv-pythonpip install dlibpip install face_recognition

И уже на этом моменте начнутся проблемы с dlib.

Решаем проблему с dlib

Я перепробовал все решения, что нашёл в интернете и они оказались неактуальными - раз, два, три, официальное руководство и видео есть. Поэтому будем собирать пакет вручную.

Итак, первая же ошибка говорит о том, что у нас не установлен cmake.

ERROR: CMake must be installed to build dlib
ERROR: CMake must be installed to build dlibERROR: CMake must be installed to build dlib

Не закрывая консоль, вводим следующую команду:

pip install cmake
Проблем при установке быть не должно

Пробуем установить пакет той же командой (pip install dlib), но на этот раз получаем новую ошибку:

Отсутствуют элементы Visual Studio

Ошибка явно указывает, что у меня, скорее всего, стоит студия с элементами только для C# - и она оказывается права. Открываем Visual Studio Installer, выбираем "Изменить", в вкладке "Рабочие нагрузки" в разделе "Классические и мобильные приложения" выбираем пункт "Разработка классических приложений на С++":

Пошагово
"Изменить""Изменить"Разработка классических приложений на С++Разработка классических приложений на С++Ждем окончания установкиЖдем окончания установки

Почему важно оставить все галочки, которые предлагает Visual Studio. У меня с интернетом плоховато, поэтому я решил не скачивать пакет SDK для Windows, на что получил следующую ошибку:

Не нашли компилятор

Я начал искать решение этой ошибки, пробовать менять тип компилятора (cmake -G " Visual Studio 16 2019"), но только стоило установить SDK, как все проблемы ушли.

Я пробовал данный метод на двух ПК и отмечу ещё пару подводных камней. Самое главное - Visual Studio должна быть 2019 года. У меня под рукой был офлайн установщик только 2017 - я мигом его поставил, делаю команду на установку пакета и получаю ошибку, что нужна свежая Microsoft Visual C++ версии 14.0. Вторая проблема была связана с тем, что даже установленная студия не могла скомпилировать проект. Помогла дополнительная установка Visual C++ 2015 Build Tools и Microsoft Build Tools 2015.

Открываем вновь Anaconda Prompt, используем ту же самую команду и ждём, когда соберется проект (около 5 минут):

Сборка
Всё прошло успешноВсё прошло успешно

Управляем громкостью

Вариантов оказалось несколько (ссылка), но чем проще - тем лучше. На русском язычном StackOverflow предложили использовать простую библиотеку от Paradoxis - ей и воспользуемся. Чтобы установить её, нам нужно скачать архив, пройти по пути C:\ProgramData\Anaconda3\Lib и перенести файлы keyboard.py, sound.py из архива. Проблем с использованием не возникало, поэтому идём дальше

Собираем события мыши

Самым популярным модулем для автоматизации управления мышью/клавиатурой оказался pynput. Устанавливаем так же через (pip install dlib). У модуля в целом неплохое описание - https://pynput.readthedocs.io/en/latest/mouse.html . Но у меня возникли сложности при получении событий. Я написал простую функцию:

from pynput import mousedef func_mouse():        with mouse.Events() as events:            for event in events:                if event == mouse.Events.Scroll or mouse.Events.Click:                    #print('Переместил мышку/нажал кнопку/скролл колесиком: {}\n'.format(event))                    print('Делаю половину громкости: ', time.ctime())                    Sound.volume_set(volum_half)                    break

Самое интересное, что если раскомментировать самую первую строчку и посмотреть на событие, которое привело выходу из цикла, то там можно увидеть Move. Если вы заметили, в условии if про него не слово. Без разницы, делал я только скролл колесиком или только нажатие любой клавиши мыши - все равно просто движение мыши приводит к выходу из цикла. В целом, мне нужно все действия (Scroll, Click, Move), но такое поведение я объяснить не могу. Возможно я где-то ошибаюсь, поэтому можете поправить.

А что в итоге?

Adam Geitgey, автор библиотеки face recognition, в своём репозитории имеет очень хороший набор примеров, которые многие используют при написании статей: https://github.com/ageitgey/face_recognition/tree/master/examples

Воспользуемся одним из них и получим следующий код, который можно скачать по ссылке: Activity.ipynb, Activity.py

Код
# Подключаем нужные библиотекиimport cv2import face_recognition # Получаем данные с устройства (веб камера у меня всего одна, поэтому в аргументах 0)video_capture = cv2.VideoCapture(0) # Инициализируем переменныеface_locations = []from sound import SoundSound.volume_up() # увеличим громкость на 2 единицыcurrent = Sound.current_volume() # текущая громкость, если кому-то нужноvolum_half=50  # 50% громкостьvolum_full=100 # 100% громкостьSound.volume_max() # выставляем сразу по максимуму# Работа со временем# Подключаем модуль для работы со временемimport time# Подключаем потокиfrom threading import Threadimport threading# Функция для работы с активностью мышиfrom pynput import mousedef func_mouse():        with mouse.Events() as events:            for event in events:                if event == mouse.Events.Scroll or mouse.Events.Click:                    #print('Переместил мышку/нажал кнопку/скролл колесиком: {}\n'.format(event))                    print('Делаю половину громкости: ', time.ctime())                    Sound.volume_set(volum_half)                    break# Делаем отдельную функцию с напоминаниемdef not_find():    #print("Cкрипт на 15 секунд начинается ", time.ctime())    print('Делаю 100% громкости: ', time.ctime())    #Sound.volume_set(volum_full)    Sound.volume_max()        # Секунды на выполнение    #local_time = 15    # Ждём нужное количество секунд, цикл в это время ничего не делает    #time.sleep(local_time)        # Вызываю функцию поиска действий по мышке    func_mouse()    #print("Cкрипт на 15 сек прошел")# А тут уже основная часть кодаwhile True:    ret, frame = video_capture.read()        '''    # Resize frame of video to 1/2 size for faster face recognition processing    small_frame = cv2.resize(frame, (0, 0), fx=0.50, fy=0.50)    rgb_frame = small_frame[:, :, ::-1]    '''    rgb_frame = frame[:, :, ::-1]        face_locations = face_recognition.face_locations(rgb_frame)        number_of_face = len(face_locations)        '''    #print("Я нашел {} лицо(лица) в данном окне".format(number_of_face))    #print("Я нашел {} лицо(лица) в данном окне".format(len(face_locations)))    '''        if number_of_face < 1:        print("Я не нашел лицо/лица в данном окне, начинаю работу:", time.ctime())        '''        th = Thread(target=not_find, args=()) # Создаём новый поток        th.start() # И запускаем его        # Пока работает поток, выведем на экран через 10 секунд, что основной цикл в работе        '''        #time.sleep(5)        print("Поток мыши заработал в основном цикле: ", time.ctime())                #thread = threading.Timer(60, not_find)        #thread.start()                not_find()        '''        thread = threading.Timer(60, func_mouse)        thread.start()        print("Поток мыши заработал.\n")        # Пока работает поток, выведем на экран через 10 секунд, что основной цикл в работе        '''        #time.sleep(10)        print("Пока поток работает, основной цикл поиска лица в работе.\n")    else:        #все хорошо, за ПК кто-то есть        print("Я нашел лицо/лица в данном окне в", time.ctime())        Sound.volume_set(volum_half)            for top, right, bottom, left in face_locations:        cv2.rectangle(frame, (left, top), (right, bottom), (0, 0, 255), 2)        cv2.imshow('Video', frame)        if cv2.waitKey(1) & 0xFF == ord('q'):        breakvideo_capture.release()cv2.destroyAllWindows()

Суть кода предельно проста: бегаем в цикле, как только появилось хотя бы одно лицо (а точнее координаты), то звук делаем 50%. Если не нашёл никого поблизости, то запускаем цикл с мышкой.

Тестирование в бою

Ожидание и реальность

Если вы посмотрели видео, то поняли, что результат ещё далёк от реальной эксплуатации.

Признаю честно - до этого момента никогда не сталкивался с многопоточностью на Python, поэтому "с наскоку" тему взять не удалось и результат по видео понятен. Есть неплохая статья на Хабре, описывающая различные методы многопоточности, применяемые в языке. Пока у меня решения нету по этой теме нету - будет повод разобраться лучше и дописать код/статью с учетом этого.

Так же возникает закономерный вопрос - а если вместо живого человека поставить перед монитором картинку? Да, она распознает, что, скорее всего, не совсем верно. Мне попался очень хороший материал по поводу определения живого лица в реальном времени - https://www.machinelearningmastery.ru/real-time-face-liveness-detection-with-python-keras-and-opencv-c35dc70dafd3/ , но это уже немного другой уровень и думаю новичкам это будет посложнее. Но эксперименты с нейронными сетями я чуть позже повторю, чтобы тоже проверить верность и повторяемость данного руководства.

Немаловажным фактором на качество распознавания оказывает получаемое изображение с веб-камеры. Предложение использовать 1/4 изображения (сжатие его) приводит только к ухудшению - моё лицо алгоритм распознать так и не смог. Для повышения качества предлагают использовать MTCNN face detector (пример использования), либо что-нибудь посложнее из абзаца выше.

Другая интересная особенность - таймеры в Питоне. Я, опять же, признаю, что ни разу до этого не было нужды в них, но все статьях сводится к тому, чтобы ставить поток в sleep(кол-во секунд). А если мне нужно сделать так, чтобы основной поток был в работе, а по истечению n-ое количества секунд не было активности, то выполнялась моя функция? Использовать демонов (daemon)? Так это не совсем то, что нужно. Писать отдельную программу, которая взаимодействует с другой? Возможно, но единство программы пропадает.

Заключение

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

P.S. Предлагаю вам, читатели, обсудить в комментариях статью - ваши идеи, замечания, уточнения.

Подробнее..

Распознавание эмоций в записях телефонных разговоров

21.06.2021 02:14:29 | Автор: admin

Технология распознавания эмоций в речи может может найти применение в огромном количестве задач. В частности, это позволит автоматизировать процесс мониторинга качества обслуживания клиентов call-центров.

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

1) Empath

В 2017 году был основан японский стартап Empath. Он создал платформу Web Empath, основанную на алгоритмах, обученных на десятках тысяч голосовых образцов японской медицинской технологической компании Smartmedical. Недостатком платформы является то, что она анализирует только голос и не пытается распознать речь.

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

Также важное влияние на форму проявления эмоционального состояния оказывают культурные и языковые особенности. И попытки многоязычной классификации эмоций демонстрируют значительное снижение эффективности их распознавания [1]. Тем не менее, такое решение имеет место быть, а компания имеет возможность предлагать свое решение клиентам по всему миру.

2) Центр речевых технологий

В составе программного продукта Smart Logger II компании ЦРТ есть модуль речевой аналитики QM Analyzer, позволяющий в автоматическом режиме отслеживать события на телефонной линии, речевую активность дикторов, распознавать речь и анализировать эмоции. Для анализа эмоционального состояния QM Analyzer измеряет физические характеристики речевого сигнала: амплитуда, частотные и временные параметры, ищет ключевые слова и выражения, характеризующие отношение говорящего к теме [2]. При анализе голоса первые несколько секунд система накапливает данные и оценивает, какой тон разговора был нормальным, и далее, отталкиваясь от него, фиксирует изменения тона в положительную или отрицательную сторону [3].

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

3) Neurodata Lab

Компания Neurodata Lab разрабатывает решения, которые охватывают широкий спектр направлений в области исследований эмоций и их распознавания по аудио и видео, в том числе технологии по разделению голосов, послойного анализа и идентификации голоса в аудиопотоке, комплексного трекинга движений тела и рук, а также детекции и распознавания ключевых точек и движений мышц лица в видеопотоке в режиме реального времени. В качестве одного из своих первых проектов команда Neurodata Lab собрала русскоязычную мультимодальную базу данных RAMAS комплексный набор данных об испытываемых эмоциях, включающий параллельную запись 12 каналов: аудио, видео, окулографию, носимые датчики движения и другие о каждой из ситуаций межличностного взаимодействия. В создании базы данных приняли участие актеры, воссоздающие различные ситуации повседневного общения [4].

На основе RAMAS с помощью нейросетевой технологии компания Neurodata Lab создала решение для контакт-центров, позволяющее распознавать эмоции в голосе клиентов и рассчитывать индекс удовлетворенности обслуживанием непосредственно во время разговора с оператором. При этом анализ осуществляется как на голосовом уровне, так и на семантическом, при переводе речи в текст. Система также учитывает дополнительные параметры: количество пауз в речи оператора, изменение громкости голоса и общее время разговора.

Однако стоит заметить, что база данных для обучения нейронной сети в данном решении была подготовлена специально с участием актеров. А, согласно исследованиям, переход от модельных эмоциональных баз к распознаванию эмоций в спонтанной речи ведет к заметному снижению эффективности работы алгоритмов [1].

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

Empath

ЦРТ

Neurodata Lab

Разрабатываемый сервис

семантический анализ

-

+

+

+

русский дата-сет

-

нет

+

+

дата-сет спонтанных эмоций

+

-

+

В качестве материалов для создания русскоязычного эмоционального дата-сета со спонтанной речью мне была предоставлена база записей телефонных разговоров от IT-компании Эм Си Арт.

Общий алгоритм работы разрабатываемого сервиса выглядит следующим образом.

Блок-схема алгоритма обработки звонкаБлок-схема алгоритма обработки звонка

При реализации были использованы следующие инструменты:

  1. Шумоочистка RNNoise_Wrapper

  2. Диаризация pyAudioAnalysis

  3. Транскрибация vosk-api

  4. Анализ эмоций текста dostoevsky

Для распознавания эмоций по голосу не нашлось подходящей библиотеки с открытым исходным кодом, поэтому модель для решения данной задачи будем создавать сами.

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

Я выбрала пять наиболее часто используемых признаков:

  • мел-частотные кепстральные коэффициенты (MFCC)

  • вектор цветности

  • мел-спектрограмма

  • спектральный контраст

  • тональный центроид (Tonnetz)

На основе выделенных из записей телефонных разговоров отрезков я составила 3 варианта дата-сетов с различным количеством выделяемых классов эмоций. Также для сравнения результатов обучения была взята берлинская база эмоциональной речи Emo-DB, созданная с привлечением профессиональных актеров.

Сначала я попробовала обучить простые классификаторы библиотеки scikit-learn:

  • SVC

  • RandomForestClassifier

  • GradientBoostingClassifier

  • KNeighborsClassifier

  • MLPClassifier

  • BaggingClassifier

В результате обучения на дата-сете Emo-DB получилось достичь точности распознавания 79%. Однако при тестировании полученной модели на размеченных мной записях телефонных разговоров, точность оказалась равной всего 23%. Это подтверждает тезисы о том, что при многоязычной классификации и переходе от модельных эмоций к спонтанным точность распознавания значительно снижается.

На составленных мной дата-сетах получилось достичь точности 55%.

База данных

Количество классов

Количество записей

Модель

Точность

Emo-DB

4

408

MLPClassifier

79.268%/22.983%

MCartEmo-admntlf

7

324

KNeighborsClassifier

49.231%

MCartEmo-asnef

5

373

GradientBoostingClassifier

49.333%

MCartEmo-pnn

3

421

BaggingClassifier

55.294%

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

Далее я попробовала обучить сверточную нейронную сеть на дата-сете MCartEmo-pnn. Оптимальной архитектурой оказалась следующая.

Точность распознавания такой сети составила 62.352%.

Далее я провела работу по расширению и фильтрации дата-сета, в результате чего количество записей увеличилось до 566. Модель заново была обучена на этих данных. По итогу точность распознавания увеличилась до 66.666%. Это говорит о необходимости дальнейшего расширения набора данных, что приведет к увеличению точности распознавания эмоций по голосу.

График истории обучения и матрица ошибок полученной CNNГрафик истории обучения и матрица ошибок полученной CNN

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

Сервис Gateway API производит аутентификацию пользователей по стандарту JSON Web Token и выполнять роль прокси-сервера, направляя запросы к функциональным микросервисам, находящимся в закрытом контуре.

Разработанный сервис был проинтегрирован с Битрикс24. Для этого было создано приложение Аналитика речи. В понятиях Битрикс24 это серверное приложение или приложение второго типа. Такие приложения могут обращаться к REST API Битрикс24, используя протокол OAuth 2.0, а также регистрировать свои обработчики событий. Поэтому достаточно было в сервере добавить роуты для установки приложения (по сути регистрация пользователя), удаления приложения (удаление аккаунта пользователя) и обработчик события OnVoximplantCallEnd, который сохраняет результаты анализа записей в карточках связанных со звонками CRM-сущностей. В качестве результатов приложение добавляет расшифровку записи к звонку и комментарий с оценкой успешности разговора по пятибалльной шкале с прикреплением графика изменения эмоционального состояния по каждому участнику разговора.

Заключение

В работе представлен результат исследования на тему распознавания эмоций в речи, в ходе которой на основе русскоязычных записей телефонных разговоров был создан дата-сет эмоциональной речи, на котором была обучена CNN. Точность распознавания составила 66.66%.
Был реализован веб-сервис, с помощью которого можно выполнять очистку аудиозаписей от шума, диаризацию, транскрибацию и анализ эмоций в аудиозаписи или текстовых сообщениях.
Сервис был доработан, чтобы его также можно было использовать как приложение Битрикс24.

Данная работа выполнялась по заказу компании Эм Си Арт в рамках ВКР бакалавра образовательной программы "Нейротехнологии и программирование" университета ИТМО. Также по этой теме у меня был доклад на X КМУ и была принята на публикацию в "Сборнике трудов Конгресса" статья.

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

Список источников

  1. Давыдов, А. Классификация эмоционального состояния диктора по голосу: проблемы и решения / А. Давыдов, В. Киселёв, Д. Кочетков // Труды международной конференции "Диалог 2011.". 2011. С. 178185.

  2. Smart Logger II. Эволюция систем многоканальной записи. От регистрации вызовов к речевой аналитике [Электронный ресурс]. Режим доступа: http://www.myshared.ru/slide/312083/.

  3. Smart logger-2 не дремлет. Эмоции операторов call-центров и клиентов под контролем [Электронный ресурс]. Режим доступа: https://piter.tv/event/_Smart_logger_2_ne_drem/.

  4. Perepelkina, O. RAMAS: Russian Multimodal Corpus of Dyadic Interaction for Studying Emotion Recognition / O. Perepelkina, E. Kazimirova, M. Konstantinova // PeerJ Preprints 6:e26688v1. 2018.

Подробнее..

Mushrooms (Machine Learning)

14.06.2021 12:13:27 | Автор: admin

Всем привет! Рассмотрим данные о грибах, предскажем их съедобность, построим корреляцию и многое другое.

Воспользуемся данными о грибах с Kaggle (исходный датафрейм) сhttps://www.kaggle.com/uciml/mushroom-classification, 2 дополнительных датафрейма приложу к статье.

Все операции проделаны наhttps://colab.research.google.com/notebooks/intro.ipynb

# Загружаем библиотекeу для работы с даннымиimport pandas as pd# для построения леса деревьев решений, обучения моделей и построения confusion_matrix:from sklearn.ensemble import RandomForestClassifierfrom sklearn.model_selection import GridSearchCVfrom sklearn.metrics import confusion_matrix# для работы с графикой:import matplotlib.pyplot as pltimport seaborn as sns# Загружаем наш датафреймmushrooms = pd.read_csv('/content/mushrooms.csv')#Просматриваем наши данныеmushrooms.head()# Что будет изображено после выполнения кода можете увидеть на картинке внизу:
#Краткая сводка данныхmushrooms.info()
#Информация о количестве строк и столбцовmushrooms.shape# Используем кодировщик данных LabelEncoder для преобразования наших категоральных или текстовых данных в числа (обязательно перед heatmap)# Если мы этого не сделаем, при обучении дерева у нас возникнет ошибка на этапе его обученияfrom sklearn.preprocessing import LabelEncoderle=LabelEncoder()for i in mushrooms.columns:    mushrooms[i]=le.fit_transform(mushrooms[i])# Посмотрим как преобразовались наши данныеmushrooms.head()
# Просмотрим корреляцию наших данных с помощью heatmapfig = plt.figure(figsize=(18, 14))sns.heatmap(mushrooms.corr(), annot = True, vmin=-1, vmax=1, center= 0, cmap= 'coolwarm', linewidths=3, linecolor='black')fig.tight_layout()plt.show()

Положительно коррелирующие значения: Сильная корреляция (veil-color,gill-spacing) = +0.9 Средняя корреляция (ring-type,bruises) = +0.69 Средняя корреляция (ring-type,gill-color) = +0.63 Средняя корреляция (spore-print-color,gill-size) = +0.62 Отрицательно коррелирующие значения Средняя корреляция (stalk-root,spore-print-color) = -0.54 Средняя корреляция (population,gill-spacing) = -0.53 Средняя корреляция (gill-color,class) = -0.53 Если в нашем исследование возьмем максимально тесно связанные коррелирующие значения, то получим максимально точные значения и точно обученную модель. В нашей задаче мы будем обучать модель по классу, представляя, что аналитик не воспользовался таблицей корреляции.

# Отбросим колонку, которую будем предсказывать.X = mushrooms.drop(['class'], axis=1)# Создадим переменную, которую будем предсказывать.y = mushrooms['class']# Создаем модель RandomForestClassifier.rf = RandomForestClassifier(random_state=0)# Задаем параметры модели, изначально когда мы не знаем оптимальных параметров для обучения леса задаем так#{'n_estimators': range(10, 51, 10), 'max_depth': range(1, 13, 2),#             'min_samples_leaf': range(1,8), 'min_samples_split': range(2,10,2)}parameters = {'n_estimators': [10], 'max_depth': [7],              'min_samples_leaf': [1], 'min_samples_split': [2]}# Обучение Random forest моделей GridSearchCV.GridSearchCV_clf = GridSearchCV(rf, parameters, cv=3, n_jobs=-1)GridSearchCV_clf.fit(X, y)# Определение наилучших параметров, и обучаем с ними дерево для получения лучшего уровня обучаемостиbest_clf = GridSearchCV_clf.best_params_# Просмотр оптимальных параметров.best_clf
# Создание confusion matrix (матрицу ошибок) по предсказаниям, полученным в прошлом шаге и правильным ответам с нового датасета.y_true = pd.read_csv ('/content/testing_y_mush.csv')sns.heatmap(confusion_matrix(y_true, predictions), annot=True, cmap="Blues")plt.show()

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

Далее мы проделаем операции для определения модели наилучшей точности нашем дф

# определим точность нашей модели from sklearn.metrics import accuracy_scoremr = accuracy_score(y_true, predictions)#Данные для тренировки и тестировки датафреймаfrom sklearn.model_selection import train_test_splitx_train, x_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, random_state = 0)#Логистическая регрессия#Тренируем модельfrom sklearn.linear_model import LogisticRegressionlr = LogisticRegression(max_iter = 10000)lr.fit(x_train,y_train)#Строим матрицу ошибокfrom sklearn.metrics import confusion_matrix,classification_reporty_pred = lr.predict(x_test)cm = confusion_matrix(y_test,y_pred)#Делаем проверку точностиlog_reg = accuracy_score(y_test,y_pred)#K ближайших соседей#Тренируем модельfrom sklearn.neighbors import KNeighborsClassifierknn = KNeighborsClassifier(n_neighbors = 5, metric = 'minkowski',p = 2)knn.fit(x_train,y_train)#Создаем матрицу ошибокfrom sklearn.metrics import confusion_matrix,classification_reporty_pred = knn.predict(x_test)cm = confusion_matrix(y_test,y_pred)#Делаем проверку точностиfrom sklearn.metrics import accuracy_scoreknn_1 = accuracy_score(y_test,y_pred)#Дерево решений#Тренируем модельfrom sklearn.tree import DecisionTreeClassifierdt = DecisionTreeClassifier(criterion = 'entropy')dt.fit(x_train,y_train)#Создаем матрицу ошибокfrom sklearn.metrics import confusion_matrix,classification_reporty_pred = dt.predict(x_test)cm = confusion_matrix(y_test,y_pred)#Делаем проверку точностиfrom sklearn.metrics import accuracy_scoredt_1 = accuracy_score(y_test,y_pred)#Простой вероятностный классификатор#Тренируем модельfrom sklearn.naive_bayes import GaussianNBnb = GaussianNB()nb.fit(x_train,y_train)#Создаем матрицу ошибокfrom sklearn.metrics import confusion_matrix,classification_reporty_pred = nb.predict(x_test)cm = confusion_matrix(y_test,y_pred)#Делаем проверку точностиfrom sklearn.metrics import accuracy_scorenb_1 = accuracy_score(y_test,y_pred)#Осущевстляем проверку точностейplt.figure(figsize= (16,12))ac = [log_reg,knn_1,nb_1,dt_1,mr]name = ['Логистическая регрессия','К ближайших соседей','Простой вероятностный классификатор','Дерево решений', 'Случайные деревья']sns.barplot(x = ac,y = name,palette='colorblind')plt.title("График точностей моделей", fontsize=20, fontweight="bold")

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

Подробнее..

Распознавание волейбольного мяча на видео с дрона

14.06.2021 16:14:45 | Автор: admin

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

Основным фидбеком было - что за каменный век использовать олдскульные технологии, обучаешь нейросеть и погнали. Я пробовал, но не взлетело - очень часто мяч настолько размыт, что его даже человеческим глазом не отличить от случайной кляксы, и даже простой бинарный классификатор не дает стабильных результатов, чего уже говорить о детекторах и прочих YOLO.

В общем, я это дело подзабросил, но вот весной завел себе дрон и конечно же первым делом приспособил его для волейбольных съемок. Если поднять его на нужную высоту (12-15 метров), в кадр влезает вся площадка, что дает прямо неограниченные возможности для анализа.

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

Шум и никакого мячаШум и никакого мяча

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

Для сравнения - похожая подача при старом подходе выглядит вот так:

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

      gray = cv.cvtColor(frame, cv.COLOR_BGR2GRAY)      gray = cv.GaussianBlur(gray, (5, 5),0)      mask = cv.Canny(gray, 50, 100)

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

      mask = backSub.apply(frame)      mask = cv.dilate(mask, None)      mask = cv.GaussianBlur(mask, (15, 15),0)      ret,mask = cv.threshold(mask,0,255,cv.THRESH_BINARY | cv.THRESH_OTSU)

И опять же для сравнения - тот же фрагмент с неподвижной камеры с границами:

Как видно, палка имеет два конца - на трясущемся дроне Канни-фильтр дает лучшие результаты, а на статичной камере - генерит лишний фон, на котором теряется мяч.

Но что будет, если эти два подхода совместить? Для видео с дрона Канни сначала оставит только нужные фигуры, а затем, можно удалить фон.

Получается картинка не без шума, но его можно уже фильтровать, и мяч уже видно.

Результат - гораздо лучше, шума меньше (но есть еще) и траектория мяча распознается достаточно четко.

Прошлые статьи на эту же тему

Подробнее..

Обучение с подкреплением в Super Mario Bros. Сравнение алгоритмов DQN и Dueling DQN

17.06.2021 10:17:44 | Автор: admin

Этой весной Питерская Вышка и JetBrains впервые провели проектную смену для старшеклассников Школу по практическому программированию и анализу данных. В течение пяти дней 50 участников со всей страны работали над групповыми проектами по машинному обучению, NLP, мобильной и web-разработке.

Первое место заняла команда Deep Q-Mario ребята создали нейронную сеть, которая использует reinforcement learning для обучения агента играть в Super Mario Bros. В этом посте они рассказывают, какие алгоритмы использовали и с какими проблемами столкнулись (например, в какой-то момент Марио просто отказался прыгать).

О нас

Мы Владислав и Дмитрий Артюховы, Артём Брежнев, Арсений Хлытчиев и Егор Юхневич учимся в 10-11 классах в разных школах Краснодара. С программированием каждый из нас знаком довольно давно, мы писали олимпиады на С++. Однако почти все члены команды раньше не работали на Python, а для написания проекта в короткий пятидневный срок он был необходим. Поэтому первым испытанием для нас стало преодоление слабой типизации Python и незнакомого синтаксиса. Но обо всем по порядку.

Немного теории

На школе Питерской Вышки нам предстояло создать нейронную сеть, которая использует reinforcement learning для обучения агента играть в Super Mario Bros.

Reinforcement Learning

В основе RL алгоритмов лежит принцип взаимодействия агента и среды. Обучение происходит примерно так: агент совершает в среде действие и получает награду (в нашем случае Марио умеет прыгать и перемещаться вправо); среда переходит в следующее состояние; агент опять совершает действие и получает награду; подобное повторяется, пока агент не попадет в терминальное состояние (например, смерть в игре).

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

Q-learning

В основу нашей модели лег алгоритм Q-learning. Q-learning это модель, которая обучает некоторую функцию полезности (Q-функцию). Эта функция на основании текущего состояния и конкретного действия агента вычисляет прогнозируемую награду за весь эпизод (Q-value).Агент совершает действия на основании некоторого свода правил политики. Политика нашего агента называется Epsilon-Greedy: с некоторой вероятностью агент совершает случайное действие, иначе он совершает действие, которое соответствует максимальному значению Q-функции.

# implementation of Epsilon-Greedy Policy:def act(state):rand_float = random.random() # returns random float in range: [0, 1)if rand_float <= EPS:action = random_action()else:action = model.get_action(state) # returns action that brings max Q-valuereturn action

В классической реализации алгоритма Q-learning формируется таблица из всех возможных состояний среды и всех возможных действий. Задача заключается в том, чтобы посчитать значения Q-values для каждой пары состояние действие.

Обучение происходит так: мы добавляем к рассматриваемому значению Q-функции разность между оптимальным значением и текущим значением данной функции:

Q(s_t,a_t):=Q(s_t,a_t)+(Q_{target}(s_t,a_t)-Q(s_t,a_t))Q_{target}(s_t,a_t)=r_t(s_t,a_t)+ maxQ(s_{t+1},a)

Где Q(s, a) значение Q-функции для состояния и действия;

Qtarget(s, a) это оптимальное, по нашему предположению, значение Q-функции, к которому мы пытаемся свести текущее значение Q-функции;

st, at состояние среды и выбранное действие в момент времени $t$;

rt(st, at) награда за текущее состояние среды и совершенное действие;

коэффициент дисконтирования. Он необходим для того, чтобы уменьшать "значимость" награды в последующих моментах времени;

коэффициент обучения. Он определяет насколько сильно мы изменим текущее значение Q-функции.

Deep Q-Learning

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

Deep Q-learningDeep Q-learning

Experience Replay Buffer

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

# implementation of transition collecting:transition = (state, action, next_state, reward, done)replay_buffer.append(transition)  

Target network

Для того, чтобы весь алгоритм обучения работал, необходимо иметь вторую нейронную сеть target model, которая определяет оптимальное значение Q-функции (Q-target) и является копией модели, взаимодействующей со средой (online model). Единственное отличие этих сетей друг от друга заключается в том, что веса target model обновляются несколько реже, чем у online model у нас это примерно каждый 500-й эпизод. Это нужно для корректного обучения модели: если online model будет производить вычисления Q-target и Q-функций самостоятельно, при изменении весов сети следующие значения Q-target и Q-функций изменятся примерно одинаково, то есть разница между ними останется такой же, и мы не будем сводиться к оптимальному значению.

Существуют два метода обновления весов target model: hard update и soft update. Первый копирует online model в target model каждую n-ую итерацию обучения. Во втором методе веса target model также пересчитываются при обучении, но медленнее, как взвешенное среднее весов двух сетей

Q_{target}:=Q_{target}+(Q_{agent}-Q_{target})

Работа над проектом

Стоит отметить, что до школы никто из нашей команды не делал проекты по машинному обучению. За несколько недель нам сообщили тему проекта, и мы заранее, еще в Краснодаре, начали готовиться. Мы читали статьи, смотрели видео по машинному обучению и нейронным сетям, изучали математику, которая нам может пригодиться. Поэтому можно сказать, что на смену приехали уже подготовленными. Конечно, мы не знали нюансов, но во время школы наш куратор Дмитрий Иванов каждый день давал задания, благодаря которым мы смогли разобраться с деталями.Первые дни после начала школы мы занимались тем, что изучали необходимую теорию по нейронным сетям и обучению с подкреплением вместе с Дмитрием. После настало время кодинга: первая наша попытка реализовать DQN (Deep Q-learning Network) алгоритм и научить агента играть в Марио успехом не увенчалась. После девяти часов обучения прогресса не было, и мы не знали, в чем, собственно, дело. После тщетных попыток дебаггинга на питоне, командой было принято единственное разумное решение переписать код с нуля, что принесло свои плоды. Имея рабочую реализацию DQN, мы решили на этом не останавливаться, а написать модификацию Dueling DQN, сравнить ее со стандартным алгоритмом и посмотреть, какой агент лучше покажет себя в игре после обучения.

Dueling DQN

Основная идея Dueling DQN заключается в том, что нейронная сеть предсказывает не значения Q для всех действий, а отдельно средневзвешенное значение Q-функции по всем действиям (так называемое V-value), а также преимущества для каждого действия, которые определяются как разность между Q-функцией и средневзвешенным значением (подробнее можно почитать здесь).

advantage(s,a)=Q(s,a)-V(s)Визуализация архитектуры модели Dueling DQN (где-то на просторах интернета)Визуализация архитектуры модели Dueling DQN (где-то на просторах интернета)

Дополнительный функционал

Помимо алгоритмов обучения, нам необходимо было сделать еще несколько полезных вспомогательных фич: saver, logger, plotting, visualization.

Saver

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

Logger and Plotting

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

Visualization

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

Возникшие проблемы

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

Возникшая проблема с трубамиВозникшая проблема с трубами

Мы считаем, что эта особенность поведения связана с тем, что отрицательная награда от исхода времени на прохождение эпизода была меньше, чем отрицательная награда от смерти Марио при ударе с врагом. Другими словами, Марио "считал", что завершить уровень из-за истечения времени для него более предпочтительно, чем смерть.Эта проблема действительно поставила нас в тупик: мы не знали, как заставить агента проходить уровень. Мы бились над решением в течение многих часов, пока Арсений Хлытчиев не придумал модификацию функции награды, названную Punishment-оптимизацией (за что мы всей командой выражаем Арсению благодарность!) Он предложил добавлять отрицательную награду за "простой" Марио, чтобы восстановить значимость передвижения агента вперед по уровню. Это улучшение оказало сильное влияние на поведение агента в среде: Марио больше не застревал перед трубами.

Решение проблемы с трубамиРешение проблемы с трубами

Результаты

К окончанию школы мы получили агента, который неплохо справлялся с частичным прохождением первого уровня игры: Марио сумел пройти около 50%. При этом каждый член команды сумел одолеть Марио, дойдя до второго уровня.

Лучший gameplay МариоЛучший gameplay Марио

Оба алгоритма DQN и Dueling DQN после обучения проходили примерно равную часть уровня. Но в силу того, что обычный DQN имел больше времени для обучения, его результат был немного лучше.Так как нашей целью было сравнить обычный алгоритм DQN с его модификацией, давайте проанализируем графики, которые мы получили.

Функция потери

 DQN (слева) и Dueling DQN (справа) DQN (слева) и Dueling DQN (справа)

На первый взгляд может показаться, что Dueling модификация показывает себя хуже, однако большое значение функции потери объясняется тем, что агент, обучающийся на Dueling DQN, в среднем проходил дальше по уровню, чем агент с обычной моделью обучения. В связи с этим среда для агента становилась неизвестной, и он чаще ошибался.

Функция награды

DQN (слева) и Dueling DQN (справа)DQN (слева) и Dueling DQN (справа)

Функция средней награды постепенно возрастает, это свидетельствует о том, что агенты узнают о среде больше, то есть проходят дальше по уровню. Из графиков видно, что агент с моделью обучения Dueling DQN в среднем получает такую же награду, что агент с DQN, однако модифицированной версии понадобилось практически в два раза меньше итераций, чтобы научиться получать такое среднее количество награды.

Заключение

Наш проект еще можно и нужно дорабатывать. Например, можно продолжить обучать агента, пока он не завершит уровень, подумать над другими оптимизациями алгоритма DQN и т.д. Но сейчас мы заняты другим: кто-то сдает ЕГЭ, кто-то готовится к летним школам по программированию, поэтому добавлять какие-либо изменения пока не планируем.

За время школы мы получили много опыта в командной разработке и базовые знания о машинном обучении, на основе которых можем создавать свои собственные ML-проекты. А еще мы познакомились с большим количеством интересных людей, которые также хотят развиваться в сфере IT. Поэтому хотим выразить безмерную благодарность организаторам смены, нашему куратору и всем, кто принимал участие в школе. Это был незабываемый и очень полезный опыт.

Подробнее..

Перевод Линейная алгебра для исследователей данных

15.06.2021 14:10:21 | Автор: admin
Иллюстрация: UCIИллюстрация: UCI

Наша [Ирвинга Капланского и Пола Халмоша] общая философия в отношении линейной алгебры такова: мы думаем в безбазисных терминах, пишем в безбазисных терминах, но когда доходит до серьезного дела, мы запираемся в офисе и вовсю считаем с помощью матриц.

Ирвинг Капланский

Для многих начинающих исследователей данных линейная алгебра становится камнем преткновения на пути к достижению мастерства в выбранной ими профессии.

kdnuggetskdnuggets

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

Произведения векторов

Для двух векторов x, y их скалярным или внутренним произведением xy

называется следующее вещественное число:

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

.

x^Ty = y^Tx

Для двух векторов x , y (не обязательно одной размерности) также можно определить внешнее произведение xy . Это матрица, значения элементов которой определяются следующим образом: (xy) = xy, то есть

След

Следом квадратной матрицы A , обозначаемым tr(A) (или просто trA), называют сумму элементов на ее главной диагонали:

След обладает следующими свойствами:

  • Для любой матрицы A : trA = trA.

  • Для любых матриц A,B : tr(A + B) = trA + trB.

  • Для любой матрицы A и любого числа t : tr(tA) = t trA.

  • Для любых матриц A,B, таких, что их произведение AB является квадратной матрицей: trAB = trBA.

  • Для любых матриц A,B,C, таких, что их произведение ABC является квадратной матрицей: trABC = trBCA = trCAB (и так далее данное свойство справедливо для любого числа матриц).

TimoElliottTimoElliott

Нормы

Норму x вектора x можно неформально определить как меру длины вектора. Например, часто используется евклидова норма, или норма l:

Заметим, что x=xx.

Более формальное определение таково: нормой называется любая функция f : n , удовлетворяющая четырем условиям:

  1. Для всех векторов x : f(x) 0 (неотрицательность).

  2. f(x) = 0 тогда и только тогда, когда x = 0 (положительная определенность).

  3. Для любых вектора x и числа t : f(tx) = |t|f(x) (однородность).

  4. Для любых векторов x, y : f(x + y) f(x) + f(y) (неравенство треугольника)

Другими примерами норм являются норма l

и норма l

Все три представленные выше нормы являются примерами норм семейства lp, параметризуемых вещественным числом p 1 и определяемых как

Нормы также могут быть определены для матриц, например норма Фробениуса:

Линейная независимость и ранг

Множество векторов {x,x,...,x} называют линейно независимым, если никакой из этих векторов не может быть представлен в виде линейной комбинации других векторов этого множества. Если же такое представление какого-либо из векторов множества возможно, эти векторы называют линейно зависимыми. То есть, если выполняется равенство

для некоторых скалярных значений ,, - , то мы говорим, что векторы x,...,x линейно зависимы; в противном случае они линейно независимы. Например, векторы

линейно зависимы, так как x = 2x + x.

Столбцовым рангом матрицы A называют число элементов в максимальном подмножестве ее столбцов, являющемся линейно независимым. Упрощая, говорят, что столбцовый ранг это число линейно независимых столбцов A. Аналогично строчным рангом матрицы является число ее строк, составляющих максимальное линейно независимое множество.

Оказывается (здесь мы не будем это доказывать), что для любой матрицы A столбцовый ранг равен строчному, поэтому оба этих числа называют просто рангом A и обозначают rank(A) или rk(A); встречаются также обозначения rang(A), rg(A) и просто r(A). Вот некоторые основные свойства ранга:

  • Для любой матрицы A : rank(A) min(m,n). Если rank(A) = min(m,n), то A называют матрицей полного ранга.

  • Для любой матрицы A : rank(A) = rank(A).

  • Для любых матриц A , B np: rank(AB) min(rank(A),rank(B)).

  • Для любых матриц A,B : rank(A + B) rank(A) + rank(B).

Ортогональные матрицы

Два вектора x, y называются ортогональными, если xy = 0. Вектор x называется нормированным, если ||x|| = 1. Квадратная м

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

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

Другими словами, результатом транспонирования ортогональной матрицы является матрица, обратная исходной. Заметим, что если U не является квадратной матрицей (U , n < m), но ее столбцы являются ортонормированными, то UU = I, но UU I. Поэтому, говоря об ортогональных матрицах, мы будем по умолчанию подразумевать квадратные матрицы.

Еще одно удобное свойство ортогональных матриц состоит в том, что умножение вектора на ортогональную матрицу не меняет его евклидову норму, то есть

для любых вектора x и ортогональной матрицы U .

TimoElliottTimoElliott

Область значений и нуль-пространство матрицы

Линейной оболочкой множества векторов {x,x,...,x} является множество всех векторов, которые могут быть представлены в виде линейной комбинации векторов {x,...,x}, то есть

Областью значений R(A) (или пространством столбцов) матрицы A называется линейная оболочка ее столбцов. Другими словами,

Нуль-пространством, или ядром матрицы A (обозначаемым N(A) или ker A), называют множество всех векторов, которые при умножении на A обращаются в нуль, то есть

Квадратичные формы и положительно полуопределенные матрицы

Для квадратной матрицы A и вектора x квадратичной формой называется скалярное значение x Ax. Распишем это выражение подробно:

Заметим, что

  • Симметричная матрица A называется положительно определенной, если для всех ненулевых векторов x справедливо неравенство xAx > 0. Обычно это обозначается как

    (или просто A > 0), а множество всех положительно определенных матриц часто обозначают

    .

  • Симметричная матрица A называется положительно полуопределенной, если для всех векторов справедливо неравенство x Ax 0. Это записывается как

    (или просто A 0), а множество всех положительно полуопределенных матриц часто обозначают

    .

  • Аналогично симметричная матрица A называется отрицательно определенной

  • , если для всех ненулевых векторов x справедливо неравенство xAx < 0.

  • Далее, симметричная матрица A называется отрицательно полуопределенной (

    ), если для всех ненулевых векторов x справедливо неравенство xAx 0.

  • Наконец, симметричная матрица A называется неопределенной, если она не является ни положительно полуопределенной, ни отрицательно полуопределенной, то есть если существуют векторы x, x такие, что

    и

    .

Собственные значения и собственные векторы

Для квадратной матрицы A комплексное значение и вектор x будут соответственно являться собственным значением и собственным вектором, если выполняется равенство

На интуитивном уровне это определение означает, что при умножении на матрицу A вектор x сохраняет направление, но масштабируется с коэффициентом . Заметим, что для любого собственного вектора x и скалярного значения с справедливо равенство A(cx) = cAx = cx = (cx). Таким образом, cx тоже является собственным вектором. Поэтому, говоря о собственном векторе, соответствующем собственному значению , мы обычно имеем в виду нормализованный вектор с длиной 1 (при таком определении все равно сохраняется некоторая неоднозначность, так как собственными векторами будут как x, так и x, но тут уж ничего не поделаешь).


Перевод статьи был подготовлен в преддверии старта курса "Математика для Data Science". Также приглашаем всех желающих посетить бесплатный демоурок, в рамках которого рассмотрим понятие линейного пространства на примерах, поговорим о линейных отображениях, их роли в анализе данных и порешаем задачи.


Подробнее..

Log-Sum-Exp Trick как свойства функций делают работу классификаторов реальной

19.06.2021 00:09:06 | Автор: admin


В этой статье мы рассмотрим, что такое классификатор, поговорим о мультиклассовой классификации с помощью нейронных сетей. Затем, ознакомившись с контекстом перейдем к основному топику поста к Log-Sum-Exp Trick. Напишем формулы и разберемся, как этот трюк помогает избежать переполнения чисел с плавающей точкой.

Логистическая регрессия


В классическом машинном обучение существует метод классификации под названием логистическая регрессия. Классификатор на основе логистической регрессии, в обычном случае, призван разделять данные на два класса. Например, классификатор электронной почты может выдавать значение 0.9, что может означать: 90% вероятность, письмо спам, и 10% не спам. При этом сумма вероятностей всегда даёт единицу. Логистическая регрессия выдаёт такие вероятности на основе выхода сигмоидной функции.

Здесь возникают следующие вопросы. Как действовать, если наши данные нужно разделять на более, чем два класса? Какую функцию использовать, для того, чтобы выходы классификатора можно было трактовать как вероятности?

Мультиклассовые нейронные сети


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

\frac{e^{x_i}}{\sum_{j=0}^N{e^{x_j}}} = p_i, \, i \in [0, N].

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



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

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

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

Проблема переполнения при вычислении Softmax


Логиты, приходящие из нейронной сети, могут принимать произвольные значения на вещественной прямой. Рассмотрим, что будет если в Python мы попытаемся вычислить софтмакс для третьей компоненты заданного вектора:

import numpy as npx = np.array([-100, 1000, -100, 5, 10, 0.001])exp_x = np.exp(x)print("Возведение в степень X: ", "\n", exp_x)print("Сумма экспонент: ", "\n", np.sum(exp_x))print("Третья вероятность: ", np.exp(x[3]) / np.sum(exp_x))

Вывод:

Возведение в степень X:   [3.72007598e-44            inf 3.72007598e-44 1.48413159e+02 2.20264658e+04 1.00100050e+00]Сумма экспонент:  infТретья вероятность:  0.0

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

Log-Sum-Exp


Для решения проблемы поставленной выше предлагается воспользоваться функцией

LSE(X) = \log\left(\sum_{j=0}^N e^{x_j}\right) Log-Sum-Exp функция.


Её определение вытекает из названия это последовательное применение к входному аргументу: экспоненты, суммирования и логарифмирования. Часто можно увидеть, что эту функцию называют дифференцируемым аналогом функции максимума от нескольких переменных. Дифференцируемость крайне полезное для поиска экстремумов свойство.

image (источник)
Как видно на картинке выше, lse(x) всюду гладкая, в отличии от max(x) функция.

Вариант Log-Sum-Exp без переполнения


Если попытаться вычислить функцию LSE от вектора x, то мы так же столкнемся с проблемой переполнения, поскольку в сумме участвуют экспоненты произвольной, возможно большой, степени. Путём преобразований, основанных на свойствах функции можно избавиться от проблемы переполнения.

Пусть результат выполнения LSE(x) имеет значение y, тогда мы можем записать следующее уравнение:


y = LSE(x) = \log{\sum_{j=0}^N{e^{x_j}}}.

Применим экспоненту к обоим частя уравнения:


e^y = \sum_{j=0}^N{e^{x_j}}.

Время трюка. Пусть c = \max_{j}(x_j), тогда вынесем из каждого слагаемого e^c:


e^y = e^c \sum_{j=0}^N{e^{x_j- c}}, \, \, | \,\, \log(\cdot)

y = c + \log\left(\sum_{j=0}^N{e^{x_j- c}} \right) = LSE(x).


Теперь проверим на практике новую формулу:

import numpy as npx = np.array([11, 12, -1000, 5, 10, 0.001])y = np.array([-1000, 1000, -1000, 5, 10, 0.001])def LSE_initial(x):  return np.log(np.sum(np.exp(x)))def LSE_modified(x):  c = np.max(x)  return c + np.log(np.sum(np.exp(x - c)))# с экспонентами, не приводящими к переполнениюprint('Исходная LSE(x): ', LSE_initial(x)) print('Преобразованная LSE(x): ', LSE_modified(x))# с экспонентой в 1000й степениprint('Исходная LSE(y): ', LSE_initial(y))print('Преобразованная LSE(y): ', LSE_modified(y))

Вывод:

Исходная LSE(x):  12.408216490736713Преобразованная LSE(x):  12.408216490736713Исходная LSE(y):  infПреобразованная LSE(y):  1000.0

Видно, что даже в случае, когда одно из слагаемых обращается в float inf, модифицированный вариант lse(x) даёт верный результат.

Log-Sum-Exp Trick


Теперь перейдём непосредственно к трюку. Наша исходная задача состоит в том, чтобы вычислить софтмакс для набора вещественных значений, не получая при этом неправильные результаты из-за переполнения.

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


Пусть p_i в формуле софтмакс равно 1, тогда наше равенство примет следующий вид:


\frac{e^{x_i}}{\sum_{j=0}^N{e^{x_j}}} = 1.

Применим ряд преобразований:


\frac{e^{x_i}}{\sum_{j=0}^N{e^{x_j}}} = 1, \,\, | \,\, * \sum_{j=0}^N{e^{x_j}}

e^{x_i} = \sum_{j=0}^N{e^{x_j}}, \,\, | \,\, \log(\cdot)

0 = x_i - \log{\sum_{j=0}^N{e^{x_j}}}, \,\, | \,\, \exp(\cdot)

1 = \exp\left( x_i - \log{\sum_{j=0}^N{e^{x_j}}} \right).

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


\frac{e^{x_i}}{\sum_{j=0}^N{e^{x_j}}} = \exp\left( x_i - LSE(x) \right).


Применим новую формулу для вычисления софтмакса:

import numpy as npx = np.array([11, 12, -1000, 5, 10, 0.001])y = np.array([-1000, 1000, -1000, 5, 10, 0.001])def softmax_initial(x):  return np.exp(x) / np.sum(np.exp(x))def LSE(x):  c = np.max(x)  return c + np.log(np.sum(np.exp(x - c)))def softmax_modified(x):  return np.exp(x - LSE(x))# с экспонентами, не приводящими к переполнениюprint('Исходный Softmax(x): ', softmax_initial(x)) print('Преобразованный Softmax(x): ', softmax_modified(x))print('Суммы вероятностей: {} {}\n'.format(np.sum(softmax_initial(x)), np.sum(softmax_modified(x))))# с экспонентой в 1000й степениprint('Исходный Softmax(y): ', softmax_initial(y))print('Преобразованный Softmax(y): ', softmax_modified(y))print('Суммы вероятностей: {} {}'.format(np.sum(softmax_initial(y)), np.sum(softmax_modified(y))))

Вывод:

Исходный Softmax(x):  [2.44579103e-01 6.64834933e-01 0.00000000e+00 6.06250985e-04 8.99756239e-02 4.08897394e-06]Преобразованный Softmax(x):  [2.44579103e-01 6.64834933e-01 0.00000000e+00 6.06250985e-04 8.99756239e-02 4.08897394e-06]Суммы вероятностей: 0.9999999999999999 1.0000000000000004Исходный Softmax(y):  [ 0. nan  0.  0.  0.  0.]Преобразованный Softmax(y):  [0. 1. 0. 0. 0. 0.]Суммы вероятностей: nan 1.0

Здесь, аналогично результатам с преобразованным lse(x), видно, что модифицированная версия софтмакса стабильнее, и не страдает от переполнения при вычислении. Сумма вероятностей, полученных из софтмакс, даёт единицу, на всех примерах векторов. Такое поведение и ожидается от этой функции.

Заключение


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

Статья была подготовлена в рамках курса Математика для Data Science. Также предлагаю всем желающим посмотреть запись бесплатного демоурока про линейные пространства и отображения.

СМОТРЕТЬ ДЕМОУРОК
Подробнее..

БСД, или как легко объяснить финансистам их же задачи в терминах машинного обучения

19.06.2021 20:20:08 | Автор: admin

Мне давно нравятся Байесовские сети доверия (БСД) своей наглядностью, что для учебного процесса, согласитесь, очень важно. Как-то в интернете, довольно неожиданно, наткнулся на очень удачную визуализацию простых финансовых задач, выполненную с использованием БСД. Мелькнула мысль вот это, пожалуй, можно будет объяснить не только нашим финансистам, но и филологам-психологам вкупе с врачами!

А началась эта история года три назад, когда руководство ВУЗа поставило нас, преподавателей перед выбором: либо мы по команде Все вдруг! вступаем в цифровую экономику, либо, пожав друг другу руки, тихо расходимся, кто куда сумеет. Причина демографическая яма, экономический кризис, успехи конкурентов и т. д.

Ну, что ж, как говорится, цифровая так цифровая! Тем из нас, у кого за плечами помимо финансового было еще и хорошее инженерное образование, всё это не показалось катастрофой, т.к. давно разжевано в учебниках и монографиях по машинному обучению, искусственному интеллекту, экспертным системам. Однако проблема усугублялась тем, что закон не препятствует поступлению филологов, психологов, врачей и еще кого хотите в финансовую магистратуру. В результате в учебной группе частенько оказывается филологов-психологов половина, а то и более. И вот тут, если вы не способны такой аудитории, что называется между делом, быстро дочитать теорию вероятностей хотя бы до теоремы Байеса, то дальше полный стоп, и никакой цифровизации.

После той находки в интернете какое-то время я целенаправленно перебирал готовые программные продукты, и, в конце концов, остановился на Нетике (Netica). Да, есть Hugin, есть MSBN, и еще с десяток им подобных, но, как всегда: один дорогой, другой сложный, третий и то и другое, и т. д. А вот Netica бесплатная (до 15 узлов переменных), простая, интуитивно понятная, студенты осваивают ее за одно занятие, и вперёд! Строить свою собственную, пусть простую, даже примитивную, но работающую с пинка экспертную систему, которая жужжит и щелкает у вас прямо на занятии.

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

Например, строим модель анализа решений инвестора об исполнении европейского опциона колл на акции. Такая БСД может быть для начала совсем небольшой хватит и двух узлов (см. рис. 1). В нашей узлы будут такими:

  • узел Рrice (или Р) описывает случайную переменную текущей цены акции;

  • узел Decision (или D) отображает варианты решения об исполнении опциона.

Узел Р является родительским, поэтому таблицу безусловных вероятностей (ТБВ) его состояния заполняем исходя из того, что инвестору, как лицу, принимающему решения (ЛПР), известно состояние рынка, и он, основываясь на собственном опыте, с вероятностью 60% ожидает, что цена Р акции будет расти и к дате истечения опциона превысит цену его исполнения Х. Он также считает, что вероятность точного совпадения цен Р и Х очень низка (10%), поэтому оставшиеся 30% составляют вероятность того, что цена Р акции окажется ниже цены исполнения опциона Х.

ТБВ для узла РТБВ для узла РРис. 1. Граф (а) и диаграммы вероятностей (б)Рис. 1. Граф (а) и диаграммы вероятностей (б)

Таблицу условных вероятностей (ТУВ) для узла D начнем заполнять исходя из того, что исполнение опциона выгодно инвестору только в том случае, если цена Р акции превысит цену исполнения опциона Х. Однако не будем забывать о том, что если это превышение будет незначительным, то оно не покроет премию, которую инвестор уже уплатил продавцу опциона. Таким образом, уверенность в исполнении опциона может в этом случае быть равна не 100%, а, допустим, 80% (1-я строка ТУВ).

ТУВ для узла DТУВ для узла D

Если P < X (2-я строка ТУВ), то исполнение невыгодно всегда, поэтому отказу от исполнения присваиваем 100% вероятность. Если Р точно равно Х (3-я строка ТУВ), то инвестору практически безразлично исполнять, или не исполнять опцион, поскольку и то и другое дает одинаковый экономический результат. В терминах вероятности такое состояние обычно записывают как 50/50.

После заполнения всех таблиц вероятностей в программе Netica инициализируем построенную БСД, и сразу получаем на стержневой диаграмме нижнего узла распределение полных вероятностей в исходном состоянии модели (рис. 1б). При подстановке значений вероятностей из ТБВ и ТУВ для проверки машины действительно получаем:

D yes = 0,8 0,6 + 0 0,3 + 0,5 0,1 = 0,53;

D no = 0,2 0,6 + 1 0,3 + 0,5 0,1 = 0,47.

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

Рис. 2. Результаты вероятностных выводовРис. 2. Результаты вероятностных выводов

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

На рис.2а в качестве примера прямого вывода показан клик на стержне случайного события P > X. После этого в узле вариантов решений значения полных вероятностей изменились, и соответствуют 80% вероятности исполнения нашего опциона. На рис. 2б показан один из обратных выводов, который показывает, что если опцион исполняется, то значит, с вероятностью 90,6% (но не 100%) цена акции превысила цену исполнения, но при этом всё же остается 9,43% вероятности того, что цена акции будет равна цене исполнения. Самый интересный вывод следует из рис. 2в. Он говорит о том, что отказ от исполнения опциона в построенной модели может произойти с вероятностью 63,8%, при этом отказ вероятен на 25,5% даже если цена акции превысит цену исполнения, и на 10,6% при равенстве цен!

Остается проверить машину используя те же табличные данные, найдем вручную апостериорные вероятности состояния цены акции при условии, что опцион был исполнен (Рис. 2б). Для этого придется вспомнить формулу теоремы Байеса в ее первозданном виде, и подставить в неё всё, что у нас есть:

P(P_{m}|D_{yes})=\frac{P(D_{yes}|P_{m})\times P(P_{m})}{P(D_{yes})}P(P_{m}|D_{yes})=\frac{0,6\times0,8}{0,6\times0,8+0\times0,3+0,5\times0,1}=0,9057P(P_{l}|D_{yes})=0P(P_{e}|D_{yes})=0,0943

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

На любые упрёки о допущениях, субъективных вероятностях, всегда отвечаю а вот, пожалуйста, теперь вы эксперт! Введите любые поправки, какие считаете нужными, и, не сходя с места, получите скорректированный результат. И поверьте, еще ни один не ушел без полной сатисфакции.

Модель слишком простая? сейчас же дополните её новыми узлами (переменными). Мало два? сделайте 22, и. т. д.

Подробнее..

Перевод Учимся понимать таблицы на меньшем объеме данных

21.06.2021 14:12:41 | Автор: admin

Задача распознавания семантического следования (textual entailment), или импликации (natural language inference), в текстах на естественном языке состоит в определении того, может ли часть текста (посылка, антецедент) подразумеваться или противоречить (или не противоречить) другому фрагменту текста (следствию, консеквенту). Хотя эта проблема часто считается важным тестом на понимание в системах машинного обучения (ML) и была глубоко изучена для простых текстов, гораздо меньше усилий было приложено для применения таких моделей к структурированным данным, таким как веб-сайты, таблицы, базы данных и т. д. Тем не менее, распознавание семантического следования особенно актуально, когда содержимое таблицы необходимо точно суммировать и представить пользователю, и важно для таких приложений, где необходима высокая точность: в вопросно-ответных системах и виртуальных ассистентах.


В статье Understanding tables with intermediate pre-training, опубликованной в материалах EMNLP 2020, авторы представили первые методы предварительного обучения, адаптированные для анализа таблиц и позволяющие моделям учиться лучше, быстрее и на меньшем объеме данных. Авторы основываются на своей более ранней модели TAPAS, которая была расширением двунаправленной модели на основе Трансформера BERT со специальными эмбеддингами для поиска ответов в таблицах. Новые методы предварительного обучения на TAPAS позволяют получить лучшие метрики на нескольких наборах данных, включающих таблицы. В TabFact, например, сокращается разрыв между результатами выполнения этого задания моделью и человеком примерно на 50%. Авторы также систематически тестируют методы выбора подходящих входных данных для повышения эффективности, что позволяет достичь четырёхкратного увеличения скорости и снижения объема затрачиваемой памяти при сохранении 92% результатов. Все модели для разных задач и размеров выпущены в репозитории GitHub, где их можно опробовать в Colab-ноутбуке.


Семантическое следование


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


image5


Таблица вместе с несколькими предложениями из TabFact. Содержание таблицы может быть использовано для подтверждения или опровержения предложения.


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


TAPAS


Архитектура модели TAPAS использует модель BERT для кодирования предложения и плоской таблицы, считываемой строка за строкой. Для кодирования структуры таблицы используются специальные эмбеддинги. Векторный вывод первого токена используется для прогнозирования вероятности семантического следования.


Поскольку единственная информация в обучающих примерах это двоичное значение (т.е. правильно или неправильно), обучение модели для понимания того, присутствует ли в предложении семантическое следование, является нетривиальной задачей. Это в очередной раз подтверждает сложность достижения генерализации в глубоком обучении, особенно в случае достаточно скудных средств выражения необходимого значения в обучающих данных. Видя отдельные предполагаемые или опровергнутые примеры, модель может легко уловить ложные закономерности в данных. Например, в предложении Greg Norman and Billy Mayfair tie in rank (Грег Норман и Билли Мейфэр занимают равное положение) модель может выделить слово tie (галстук/связывать, разделять) вместо истинного сравнение положения Нормана и Мейфэра, необходимого для успешного применения модели за пределами исходных обучающих данных.


Методы предварительного обучения


Предварительное обучение можно использовать для прогрева моделей, предоставляя им большие объемы легко доступных неразмеченных данных. Однако предварительное обучение обычно включает в себя в основном простой текст, а не табличные данные. Фактически, TAPAS изначально был предварительно обучен с использованием простой задачи маскированного языкового моделирования, которая не предназначалась для приложений с табличными данными. Чтобы улучшить результаты модели для табличных данных, авторы представили две новые задачи бинарной классификации предварительного обучения: контрфактическую (counterfactual) и синтетическую (synthetic). Эти задачи могут применяться в качестве второго этапа предварительного обучения (часто называемого промежуточным предварительным обучением intermediate pre-training).


В контрфактической задаче авторы используют предложения из Википедии, в которых упоминается сущность (человек, место или предмет), которая также появляется в данной таблице. Затем в половине предложений сущности заменяются на альтернативные. Чтобы убедиться, что утверждение реалистично, мы выбираем замену среди сущностей в том же столбце таблицы. Модель обучается распознавать, было ли изменено утверждение или нет. Эта задача предварительного обучения включает в себя миллионы таких примеров, и, хотя их интерпретация не так уж сложна, они, как правило, все равно будут звучать естественно.


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


image1


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


Результаты


Авторы оценивают успешность контрфактического и синтетического методов предварительного обучения на наборе данных TabFact, сравнивая с базовой моделью TAPAS и двумя предыдущими моделями, которые показали хорошие результаты в области семантического следования, LogicalFactChecker (LFC) и Structure Aware Transformer (SAT). Базовая модель TAPAS демонстрирует более высокие метрики по сравнению с LFC и SAT, но предварительно обученная модель (TAPAS + CS) работает значительно лучше, достигая наивысших метрик.


Авторы также применили TAPAS + CS для вопросно-ответных задач на наборе данных SQA, для которого модель должна находить ответы из содержимого таблиц в формате диалога. Включение в задачи предварительного обучения CS повышает метрики более чем на 4 балла, демонстрируя, что этот подход также способен к генерализации за пределами семантического следования.


Accuracy


Результаты на TabFact (слева) и SQA (справа). Используя синтетические наборы данных и наборы контрфактических данных, авторы с большим отрывом достигают наивысших метрик в обеих задачах.


Данные и эффективность вычислений


Другой аспект контрфактической и синтетической задач предварительного обучения заключается в том, что, поскольку модели уже обучены для двоичной классификации, их можно применять без какой-либо тонкой настройки на TabFact. Авторы исследуют, что происходит с каждой из моделей при обучении только на поднаборе данных (или даже без них). Не увидев ни одного обучающего пример, модель TAPAS + CS способна соперничать с сильной базовой моделью Table-Bert, а при обучении всего на 10% данных, результаты становятся сопоставимы с предыдущими наивысшими метриками для этой задачи.


image7


Точность (Accuracy) на валидационном наборе данных в зависимости от объема обучающих примеров


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


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


Например, все модели, описанные выше, используют последовательности из 512 токенов, что примерно соответствует обычному порогу для модели Трансформера (хотя недавние методы повышения эффективности, такие как Reformer или Performer, доказали свою эффективность при масштабировании размера ввода). Предлагаемые в статье методы выбора столбцов позволяют ускорить обучение, сохраняя при этом высокую точность TabFact. Для 256 входных токенов получается очень небольшое снижение точности, но теперь модель можно предварительно обучить, дообучить на конкретную задачу и делать предсказания до двух раз быстрее. С 128 токенами модель по-прежнему превосходит предыдущую современную модель с еще более значительным ускорением в 4 раза быстрее по всем направлениям.


image6


Точность (Accuracy) на TabFact с использованием разной длины последовательности за счет сокращения входа с помощью предложенного метода выбора столбца.


Используя метод выбора столбцов и новые методы предварительного обучения, появляется возможность создавать модели анализа таблиц, которым требуется меньше данных и меньшая вычислительная мощность для получения лучших результатов.


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


Авторы


Подробнее..

Стоит ли смотреть в сторону Data science?

21.06.2021 16:11:25 | Автор: admin

Очень много людей с самым разным бэкграундом, и не всегда даже техническим, в последнее время пытаются перейти в data science. И такой ажиотажный интерес - проблема для всех, кто рассматривает переквалификацию в эту сферу. Потому что рынок труда может оказаться наводнён соискателям без опыта.

С точки зрения потенциального работодателя: вы скорее возьмёте выпускника технического вуза, или человека за 40, без профильного опыта, решившего изменить свою жизнь? Ведь вузы тоже ведут активную подготовку профильных специалистов (пусть и не всегда высокого уровня). На рынке явный недостаток специалистов с профильным опытом. Но среди людей с непрофильным опытом - вероятно, больше шансов найти работу у выпускников с профильным образованием.

Написать этот пост меня натолкнуло то, что полгода назад я начал писать цикл статей о самообучении и переквалификации в data sceince. В итоге, за 5 месяцев мне написало больше сотни людей с разными вопросами по такой переквалификации. И, вероятно, многие недооценивают объем знаний, который необходимо получить для этого. В этом, наверное, виноваты и заголовки моих постов "с нуля до senior data scientist за 2 года". Как мне подсказали в комментариях к тому посту - мой начальный уровень был отнюдь не нулевой (был топовым разработчиком 1С).

Почему в data science сложно попасть

Это очень много учебы и практики

Идеальный data scientist - специалист максимально высокой квалификации, знающий, одновременно:

  • всё что должны знать "простые аналитики" (SQL+визуализация данных)

  • хороший Python программист

  • с неплохим владением английским (подавляющее большинство инструментов имеют документацию только на английском языке; многие книги и курсы не переведены, или переводятся с большой задержкой)

  • с отличным знанием хотя бы основ теории вероятностей (в идеале - значительно глубже + линал, мат.анализ)

  • хорошие коммуникативные навыки и понимание бизнеса (невозможно эффективно обрабатывать данные из предметной области, если вы её не понимаете)

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

Это дорого

Даже с большим опытом работы в ИТ я потратил 8 месяцев, не работая и проедая все свои накопления, только на учебу, оставаясь без работы. Мой уровень ЗП в первый год после того как я нашёл работу, был ниже, чем до этого. Только спустя 2 года я вышел на тот же уровень дохода. И это всё было достаточно страшновато, при том что у меня были своя квартира, большая финансовая подушка, и отсутствие финансово зависимых родственников. Большая часть людей, желающих поменять свою жизнь, не могут себе позволить такую траекторию переобучения, по её финансовым ограничениям.

Аналитика - сестра Data science

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

Отличие аналитика: нет нужды хорошо знать статистику, нет нужны заниматься машинным обучением.

Статистика нужна тогда, когда ценность небольшого улучшения так огромна, что важно научиться различать небольшие отличия в эффективности, разделяя реальные различия, от случайных колебания.

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

То есть аналитик нужен тогда, когда не нужно различать колебания эффективности, измеряемые в процентах, и когда не нужно анализ делать полностью автоматическим. Требуется меньше точность/автоматизированность. Ценны: скорость проведения анализа, его правильность, умение понятно и убедительно объяснить свои результаты. При этом, ценность аналитика может быть очень высока, т.к. подобные разовые анализы данных могут использоваться для принятия различных стратегических решений.

Суть решаемых задач аналитка: разобраться в данных, понять их и найти интересные закономерности, представить результаты в удобном и понятном для коллег виде (обычно, графики и презентации).
Ключевой набор навыков для подобной работы: это прирожденные "аналитические способности" + знания базовых инструментов (SQL+Excel) + инструменты визуализации данных (Tableau, PowerBI).

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

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

На позиции "аналитика" нужно будет создавать точно такие же отчеты. Но, чаще, задачей аналитика будет самому в чём-то разобраться и самому решить какими графиками эту информацию представить. Эта работа более творческая, требующая большей самоотдачи. По уровню зарплат оба типа позиций сопоставими. Но с позиций аналитика талантливные специалисты чаще могут вырасти в менеджеров и отвественных за какие-то большие объемы задач. Надо помнить, что данное деление не всегда видно из названия позиций, т.к. "аналатиком" могут назвать и человека, не занимающегося самостоятельным анализом данных, и толького готовящим их для других людей (в виде отчетов и графиков).

Рекомендуемый набор знаний для аналитика:

  • SQL + Excel

  • Tableau / PowerBI

  • Когортный анализ (принципы)

  • Понимать парадокс симпсона, чтобы не делать ошибок, к которым он приводит

  • Нужно знать основы теории вероятностей:

    • вероятности зависимых и независимых событий, условные вероятности

    • разные статистики: среднее, медиана, мода, стандартное отклонение.

  • знать что такое АВ-тесты: понимать принципы, калькуляторы есть онлайн

  • знать основы regexp. Например, веб-аналитике он используется в инструментах типа google analytics

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

Маркетинговая/веб аналитика

Анализ и визуализация даных по продажам - огромная сфера. Большая часть подобных вакансий, с интересными и более творческими задачами - в онлайн компаниях.

Знания специфические для веб-маркетинга (помимо обще-аналитических):

  • Понимание принципов работы контекстной рекламы (основные метрики и схемы оплаты).

  • Знание как работают UTM метки.

  • Понимание основных принципов юнит-экономики.

  • Желательно знание основ HTML

  • Популярные инструменты: Google Analytics, Яндекс.Метрика (эти инструменты можно учить уже выйдя на первую работу)

Продуктовая аналитика

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

Data engineer - брат для Data scientist

Огромная часть задач в анализе данных, особенно в более продвинутом (статистический анализ, машинное обучение) требует хорошо организованных данных.
Задачи по организации данных занимают более половины всех усилий по работе с данными. Часть данных задач традиционная и решается специалистами по базам данных. Другая часть требует подготовки данных для обеспечения большего удобства и скорости анализа данных. Данными задачами занимаются дата инжененеры. Т.к. часто данная часть задач не решается ими полностью - квалифицированный дата сайентист должен уметь решать подобные задачи.

Но, потенциально, дата инженер, это не урезанный дата сайентист. К инженерам предъявляется больше требований по полной автоматизации процесса, по обеспечению высокого качества данных (без пропадания их кусков), высокой скорости их подготовки и доступности, настройке систем, способных обрабатывать данные о миллионах операций, товаров и клиентов за считанные секунды/минуты.
По сути, это позиция программиста, с акцентом на технологии и инструменты, позволяющие разрабывать системы работы с данными. И вместо создания интерфейса (как у фронтенд разработчиков, или разработчиков для андройд/ iOS), или какой-то бизнес-логики (бэкенд) - их продукт это система, автоматизировано готовящая данные для анализа. И поддержание это системы для работы коллег, занимающихся анализом данных.

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

  • SQL

  • Python (Java, Kotlin)

  • bash

  • Docker, Kubernets

Эта сфера отлично подходит для людей, которым интересно писать системы обработки больших данных (big data) и которым менее интересно придумывать как повысить эффективность бизнеса, стараясь убедить в этом каких-то коллег.

Ищете работу, которая вам подходит

Цель этого поста - показать что есть море вариантов интересной работы.

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

Идти в дата сайенс без опыта работы в ИТ, примерно как захотеть стать нейрохирургом, без опыта работы в медицине. Разумнее получить хотя бы часть более простых смежных навыков, начать работать в этой сфере, и далее расти в ней "естествнным путём", получая релевантный опыт не только из теории, но и из регулярной практики работы.

Подробнее..

Категории

Последние комментарии

  • Имя: Билал
    04.12.2024 | 19:28
  • Имя: Murshin
    13.06.2024 | 14:01
    Нейросеть-это мозг вселенной.Если к ней подключиться,то можно получить все знания,накопленные Вселенной,но этому препятствуют аннуннаки.Аннуннаки нас от неё отгородили,установив в головах барьер. Подр Подробнее..
  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru