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

Python

Чистый Cython VS nvc жжем металлические пластины на GPU для сравнения скорости

11.01.2021 20:23:30 | Автор: admin
image

image
Будем греть металлические пластины на GPU

Все знают, что Python не блещет скоростью сам по себе. На мой взгляд язык прекрасен своей читабельностью, но основная ниша его применения там, где вы большую часть времени ожидаете ввода/вывода каких-то данных. Условно, вы можете написать суперпроизводительный код на Rust или С, но 99% времени он будет просто ждать.

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

Но мы пойдем чуть дальше попробуем распараллелить вычисления на CUDA и задействуем странный, но работающий гибрид C++, stdpar и компилятора nvc++ от Nvidia. Ну и заодно попробуем оценить быстродействие. Возьмем две задачи: сортировку чисел и метод Якоби, которым будем рассчитывать нагрев металлической пластины.

Вызываем C++ из Python


Наш код сортировки будет иметь следующий вид:

# distutils: language=c++from libcpp.algorithm cimport sortdef cppsort(int[:] x):    sort(&x[0], &x[-1] + 1)

В первой строчке мы явно указываем, что Cython должен использовать C++, а не дефолтный C. Во второй мы импортируем функцию сортировки из C++, а дальше следует сама логика. Помещаем код в файл cppsort.pyx. Обратите внимание, что расширение отличается от привычного py, так как мы будем его компилировать или выполнять cythonize в терминологии Cython.

Компиляцию можно выполнить вручную или включить в setup.py, где мы полноценно описываем подготовку нашего окружения.

В setup.py это выглядит примерно так:

from setuptools import setupfrom Cython.Build import cythonizesetup(    ext_modules = cythonize("cppsort.pyx"))

Но мы можем и просто выполнить это в командной строке:

cythonize -i cppsort.pyx

Под капотом произойдет примерно следующее:

image

  1. Cython транслирует python код в C++ и сгенерирует валидный cppsort.cpp.
  2. C++ компилятор (в данном случае g++) компилирует C++ код в Python extension module
  3. Python extension module импортируется в код как обычный питоновский модуль.

После компиляции можем импортировать и сразу протестировать сортировку:

from cppsort import cppsortimport numpy as npx = np.array([4, 3, 2, 1], dtype="int32")print(x)cppsort(x)print(x)

Массив [4, 3, 2, 1] успешно отсортируется в [1, 2, 3, 4] с помощью C++ std::sort.

А давайте на GPU?


Стандартные библиотечные алгоритмы C++ могут вызываться с указанием аргумента parallel execution policy. Этот аргумент говорит компилятору о том, что вы хотите раскидать алгоритм на параллельные процессы.

При этом C++ имеет несколько вариантов этой политики:

  1. std::execution::seq: Последовательное выполнение. Параллельность запрещена.
  2. std::execution::unseq: Векторизированное выполнение в рамках вызвавшего потока.
  3. std::execution::par: Параллельное выполнение в одном и более потоках.
  4. std::execution::par_unseq: Параллельное выполнение в одном и более потоках. Каждый поток будет по возможности векторизирован.

При этом вы сами должны следить за race condition и deadlock. Стандартный компилятор g++ постарается распределить вычисления на ядра CPU. Но мы можем взять проприетарный компилятор от Nvidia nvc++ и скормить ему опцию "-stdpar". stdpar это C++ Standard Parallelism от Nvidia с выполнением параллельного кода на GPU.

Перепишем код, с учетом необходимости создавать локальную копию массива, так как GPU не сможет получить доступ к массиву, расположенному в рамках NumPy.

from libcpp.algorithm cimport sort, copy_nfrom libcpp.vector cimport vectorfrom libcpp.execution cimport pardef cppsort(int[:] x):    cdef vector[int] temp    temp.resize(len(x))    copy_n(&x[0], len(x), temp.begin())    sort(par, temp.begin(), temp.end())    copy_n(temp.begin(), len(x), &x[0])

image

Теперь это нужно снова скомпилировать, но уже с использованием nvc++. В этот раз напишем нормальный setup.py и вызовем его:

CC=nvc++ python setup.py build_ext --inplace

Импортируем в код и пробуем вызвать:

x = np.array([4, 3, 2, 1], dtype="int32")cppsort(x) # этот кусок выполняется на GPU

Производительность


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

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


image

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

Вычисляем нагрев пластины


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

image

Код для симуляции
"""simulates heat equation on rectangle returning a heat map at a number of timesboundary and initial conditions are 0, source represents burner on a stoveThis program is based on the script FEniCS tutorial demo program: Diffusion of a Gaussian hill.       u'= Laplace(u) + f  in a square domain  u = u_D = 0            on the boundary  u = u_0 = 0            at t = 0  u_D = f = stove burner flameThis program succesfully runs in the fenics docker image, see the book Solving PDEs in Python.to animate: convert -delay 4 -loop 100 heatequation10*.png heatstovelinn.gifto crop:convert heatstovelinn.gif -coalesce -repage 0x0 -crop 810x810+95+15 +repage heatstovelin.gif"""from fenics import *import timeimport matplotlib.pyplot as pltfrom matplotlib import cm# Create mesh and define function spacenx = ny = 100mesh = RectangleMesh(Point(-2, -2), Point(2, 2), nx, ny)V = FunctionSpace(mesh, 'P', 1)# Define boundary, source, initialdef boundary(x, on_boundary):    return on_boundarybc = DirichletBC(V, Constant(0), boundary)u_0 = interpolate(Constant(0), V)f = Expression('exp(-sqrt(pow((a*pow(x[0], 2) + a*pow(x[1], 2)-a*1),2)))', degree=2, a=5) #steep guassian centred on the unit spherefinal_time = 0.035num_pics = 72for i in range(num_pics):    T =   final_time*(i+1.0)/(num_pics+1)      #solve time even space    #T = final_time*1.1**(i-num_pics+1)        #solve time log  space    num_steps = 30    dt = T / num_steps # time step size    # Define variational problem    u = TrialFunction(V)    v = TestFunction(V)    F = u*v*dx + dt*dot(grad(u), grad(v))*dx - (u_0 + dt*f)*v*dx    a, L = lhs(F), rhs(F)    # Time-stepping    u = Function(V)    t = 0    for n in range(num_steps):        t += dt              #step        solve(a == L, u, bc) #solve        u_0.assign(u)        #update    #plot solution    plot(u,cmap=cm.hot,vmin=0,vmax=0.07)    plt.axis('off')    plt.savefig('heatequation10%s.png'%(i+10),figsize=(8, 8), dpi=220,bbox_inches='tight', pad_inches=0,transparent=True)


Напишем аналогичный солвер на Cython для последующей компиляции по CUDA:

# distutils: language=c++# cython: cdivision=Truefrom libcpp.algorithm cimport swapfrom libcpp.vector cimport vectorfrom libcpp cimport bool, floatfrom libc.math cimport fabsfrom algorithm cimport for_each, any_of, copyfrom execution cimport par, seq cdef cppclass avg:    float *T1    float *T2    int M, N     avg(float* T1, float *T2, int M, int N):        this.T1, this.T2, this.M, this.N = T1, T2, M, N    inline void call "operator()"(int i):        if (i % this.N != 0 and i % this.N != this.N-1):            this.T2[i] = (                this.T1[i-this.N] + this.T1[i+this.N] + this.T1[i-1] + this.T1[i+1]) / 4.0cdef cppclass converged:    float *T1    float *T2    float max_diff     converged(float* T1, float *T2, float max_diff):        this.T1, this.T2, this.max_diff = T1, T2, max_diff     inline bool call "operator()"(int i):        return fabs(this.T2[i] - this.T1[i]) > this.max_diff def jacobi_solver(float[:, :] data, float max_diff, int max_iter=10_000):    M, N  = data.shape[0], data.shape[1]    cdef vector[float] local    cdef vector[float] temp    local.resize(M*N)    temp.resize(M*N)    cdef vector[int] indices = range(N+1, (M-1)*N-1)    copy(seq, &data[0, 0], &data[-1, -1], local.begin())    copy(par, local.begin(), local.end(), temp.begin())    cdef int iterations = 0    cdef float* T1 = local.data()    cdef float* T2 = temp.data()     keep_going = True    while keep_going and iterations < max_iter:        iterations += 1        for_each(par, indices.begin(), indices.end(), avg(T1, T2, M, N))        keep_going = any_of(par, indices.begin(), indices.end(), converged(T1, T2, max_diff))        swap(T1, T2)     if (T2 == local.data()):        copy(seq, local.begin(), local.end(), &data[0, 0])    else:        copy(seq, temp.begin(), temp.end(), &data[0, 0])    return iterations

image

image

В итоге отрыв GPU получается еще более существенным.

Минусы


  1. Написание такого кода несколько сложнее, чем чистого варианта на Python и требует понимания принципов работы параллельных вычислений на GPU.
  2. Требуется копирование данных в отдельный массив для передачи на GPU, куда видеокарта не имеет доступа. Это может быть проблемой при работе с очень большими массивами.

Подробнее..

Перевод Googles Certificate Transparency как источник данных для предотвращения атак

12.01.2021 14:15:46 | Автор: admin

Мы подготовили перевод статьи Райана Сирса об обработке логов Googles Certificate Transparency, состоящей из двух частей. В первой части дается общее представление о структуре логов и приводится пример кода на Python для парсинга записей из этих логов. Вторая часть посвящена получению всех сертификатов из доступных логов и настройке системы Google BigQuery для хранения и организации поиска по полученным данным.

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

Часть 1. Parsing Certificate Transparency Logs Like a Boss

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

Одним из источников, которые мы интегрировали (и определённо одним из лучших), стал Certificate Transparency Log (CTL) - проект, начатый Беном Лори и Адамом Ленгли в Google. По сути CTL - это лог, содержащий неизменяемый список сертификатов, выпущенных CA, который хранится в дереве Меркла, что позволяет при необходимости криптографически проверить каждый сертификат. [*]

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

```pythonimport requestsimport jsonimport localelocale.setlocale(locale.LC_ALL, 'en_US')ctl_log = requests.get('https://www.gstatic.com/ct/log_list/log_list.json').json()total_certs = 0human_format = lambda x: locale.format('%d', x, grouping=True)for log in ctl_log['logs']:log_url = log['url']try:log_info = requests.get('https://{}/ct/v1/get-sth'.format(log_url), timeout=3).json()total_certs += int(log_info['tree_size'])except:continueprint("{} has {} certificates".format(log_url, human_format(log_info['tree_size'])))print("Total certs -> {}".format(human_format(total_certs))) ```

На выходе получаем:

ct.googleapis.com/pilot has 92,224,404 certificatesct.googleapis.com/aviator has 46,466,472 certificatesct1.digicert-ct.com/log has 1,577,183 certificatesct.googleapis.com/rocketeer has 89,391,361 certificatesct.ws.symantec.com has 3,562,198 certificatesctlog.api.venafi.com has 94,797 certificatesvega.ws.symantec.com has 200,401 certificatesctserver.cnnic.cn has 5,081 certificatesctlog.wosign.com has 1,387,492 certificatesct.startssl.com has 293,374 certificatesct.googleapis.com/skydiver has 1,249,079 certificatesct.googleapis.com/icarus has 48,585,765 certificatesTotal certs -> 285,037,607

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

Spoiler

Комментарий переводчика

Стоит отметить, что API выдает количество записей в логе, значительную часть которых составляют PreCerts (о них далее) и не представляют особого интереса. Также важно учитывать, что сертификаты могут дублироваться между разными логами, к примеру, данный сертификат присутствует одновременно в 6 различных логах, поддерживаемых Chrome. Таким образом, реальное число сертификатов значительно меньше, чем суммарное число записей в логах.

Тем не менее, на момент написания перевода, учитывая только логи, удовлетворяющие политике Google и включенные в Chrome, в списке присутствует 46 логов, содержащие в сумме 6,861,473,804 записей, что уже потребует значительных ресурсов для полной обработки.

Анатомия CTL

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

```json// curl -s 'https://ct1.digicert-ct.com/log/ct/v1/get-entries?start=0&end=0' | jq .{  "entries": [    {      "leaf_input": "AAAAAAFIyfaldAAAAAcDMIIG/zCCBeegAwIBAgI...",      "extra_data": "AAiJAAS6MIIEtjCCA56gAwIBAgIQDHmpRLCMEZU..."    }  ]}```

Каждая запись содержит поля `leaf_input` и `extra_data` в формате base64. Обращаясь к RFC6962 видим, что `leaf_input` - закодированная структура MerkleTreeLeaf, а `extra_data` - PrecertChainEntry.

Про PreCerts

Мне потребовалось довольно много времени, чтобы разобраться, что вообще такое PreCert (можете попробовать сами, почитайте RFC, и, по всей видимости, я такой не один. Сохраню вам кучу времени на раздумья и поиски в гугле и сформулирую назначение PreCerts следующим образом:

PreCerts это отдельный тип сертификата, выпускаемого CA до того, как тот выпустит настоящий сертификат. Фактически, это копия исходного сертификата, но содержащая специальное расширение x509 v3, называемое `poison` и отмеченное как критическое. Таким образом, сертификат не будет валидирован как платформами, распознающими это расширение, и знающими что это PreCert, так и платформами, которые это расширение не распознают.

Мой опыт в ИБ говорит о том, что такая мера не сильно эффективна, хотя бы потому, что баги при парсинге x509/ASN.1 встречаются довольно часто и отдельные реализации могут быть уязвимы к различным махинациям, которые в конечном счете позволят валидировать PreCert. Я понимаю, зачем это было сделано, но складывается ощущение, что полностью убрать PreCerts и оставлять в CTL только сертификаты, действительно выпущенные CA, было бы намного разумнее.

Парсим бинарные структуры

Как для человека, занимающегося реверс-инжинирингом и время от времени участвующего в разных CTF, задача парсинга бинарных структур для меня не нова. Большинство людей в таких случаях обращаются к модулю `struct`, но много лет назад, во время работы на Филлипа Мартина, он познакомил меня с отличной библиотекой Construct, которая заметно упрощает парсинг подобных структур. Ниже приведены структуры, которые я использовал для парсинга, а также пример их использования для обработки записей:

```pythonfrom construct import Struct, Byte, Int16ub, Int64ub, Enum, Bytes, Int24ub, this, GreedyBytes, GreedyRange, Terminated, EmbeddedMerkleTreeHeader = Struct(    "Version"         / Byte,    "MerkleLeafType"  / Byte,    "Timestamp"       / Int64ub,    "LogEntryType"    / Enum(Int16ub, X509LogEntryType=0, PrecertLogEntryType=1),    "Entry"           / GreedyBytes)Certificate = Struct(    "Length" / Int24ub,    "CertData" / Bytes(this.Length))CertificateChain = Struct(    "ChainLength" / Int24ub,    "Chain" / GreedyRange(Certificate),)PreCertEntry = Struct(    "LeafCert" / Certificate,    Embedded(CertificateChain),    Terminated)``````pythonimport jsonimport base64import ctl_parser_structuresfrom OpenSSL import cryptoentry = json.loads("""{  "entries": [    {      "leaf_input": "AAAAAAFIyfaldAAAAAcDMIIG/zCCBeegAwIBAgIQ...",      "extra_data": "AAiJAAS6MIIEtjCCA56gAwIBAgIQDHmpRLCMEZUg..."    }  ]}""")['entries'][0]leaf_cert = ctl_parser_structures.MerkleTreeHeader.parse(base64.b64decode(entry['leaf_input']))print("Leaf Timestamp: {}".format(leaf_cert.Timestamp))print("Entry Type: {}".format(leaf_cert.LogEntryType))if leaf_cert.LogEntryType == "X509LogEntryType":    # В случае, если запись - обычный X509 сертификат    cert_data_string = ctl_parser_structures.Certificate.parse(leaf_cert.Entry).CertData    chain = [crypto.load_certificate(crypto.FILETYPE_ASN1, cert_data_string)]    # Парсим структуру `extra_data` чтобы получить оставшуюся часть цепочки    extra_data = ctl_parser_structures.CertificateChain.parse(base64.b64decode(entry['extra_data']))    for cert in extra_data.Chain:        chain.append(crypto.load_certificate(crypto.FILETYPE_ASN1, cert.CertData))else:    #  В случае, если запись - PreCert    extra_data = ctl_parser_structures.PreCertEntry.parse(base64.b64decode(entry['extra_data']))    chain = [crypto.load_certificate(crypto.FILETYPE_ASN1, extra_data.LeafCert.CertData)]    for cert in extra_data.Chain:        chain.append(            crypto.load_certificate(crypto.FILETYPE_ASN1, cert.CertData)        )

Получаем массив X509 сертификатов из цепочки с сертификатом из leaf_input в качестве первого элемента

```

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

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

Часть 2. Retrieving, Storing and Querying 250M+ Certificates Like a Boss

Сбор сертификатов

В соответствии с RFC, для получения записей из логов используется эндпоинт `get-entries`. К сожалению, задача осложняется ограничением на максимальное количество записей, которое можно получить в одном запросе (контролируется параметрами `start` и `end`), и большинство логов позволяют получить лишь 64 записи за раз. Однако CTL от Google, составляющие большинство всех логов, используют максимальный размер запроса в 1024 записи.

Spoiler

Комментарий переводчика

На момент написания перевода большинство логов Google (Argon, Xenon, Aviator, Icarus, Pilot, Rocketeer, Skydiver) предоставляют лишь по 32 записи для каждого запроса, но для ускорения получения записей можно одновременно отправлять несколько запросов на один и тот же лог, насколько позволяет пропускная способность, но работает такой подход не для всех логов.

Отдельные логи предоставляют возможность получать по 1024 и более записей за раз, но большинство CTL, помимо Google, выдает по 256 записей за один запрос.

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

Так как не было никаких инструментов, которые позволили бы легко и безболезненно получить и распарсить все CTL (помимо не особо примечательной утилиты от Google, было решено потратить немного времени и написать свой инструмент, который соответствовал бы всем нашим потребностям. Результатом стал Axeman, который использует asyncio и замечательную библиотеку aioprocessing для загрузки, парсинга и сохранения сертификатов в несколько CSV файлов, ограничиваясь при этом только скоростью интернет-соединения.

Эксплуатация облака

После получения инстанса (_прим. перев._ так в Google Cloud называются VM) c 16 ядрами, 32Гб памяти и SSD на 750Гб (спасибо Google за бесплатные 300$ на счете для новых аккаунтов!), я запустил Axeman, который загрузил все сертификаты меньше чем за сутки и сохранил результаты в `/tmp/certificates/$CTL_DOMAIN/`

Где хранить все эти данные?

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

  • позволяют дешево хранить большой объем данных

  • обеспечивают быстрый поиск

  • позволяют легко обновлять данные

Вариантов было несколько, но с точки зрения стоимости, почти все рассмотренные варианты (AWS RDS, Heroku Postgres, Google Cloud SQL) были весьма затратны. К счастью, так как наши данные в принципе никогда не изменяются, у нас появляется дополнительная гибкость в выборе платформы для размещения данных.

В целом, это как раз тот тип поиска по данным, который прекрасно ложится на модель map/reduce с использованием, к примеру, Spark или Hadoop Pig. Просматривая предложения различных провайдеров в категории big data (хотя в нашей задаче данных явно мало для включения в эту категорию), я наткнулся на Google BigQuery, который удовлетворяет всем обозначенным параметрам.

Скармливаем данные BigQuery

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

Когда бакет готов, используем `gsutil` для транспортировки всех сертификатов в хранилище Google (а затем в BigQuery). После настройки аккаунта командой `gsutil config`, запускаем процесс загрузки:

```gsutil -o GSUtil:parallel_composite_upload_threshold=150M \       -m cp \       /tmp/certificates/* \       gs://all-certificates```

И видим следующий результат в нашем бакете:

Далее создаем новый датасет в BigQuery:

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

Так как схема нужна всякий раз при импорте очередного лога, воспользуемся опцией Edit as Text. Использованная схема:

```[    {        "name": "url",        "type": "STRING",        "mode": "REQUIRED"    },    {        "mode": "REQUIRED",        "name": "cert_index",        "type": "INTEGER"    },    {        "mode": "REQUIRED",        "name": "chain_hash",        "type": "STRING"    },    {        "mode": "REQUIRED",        "name": "cert_der",        "type": "STRING"    },    {        "mode": "REQUIRED",        "name": "all_dns_names",        "type": "STRING"    },    {        "mode": "REQUIRED",        "name": "not_before",        "type": "FLOAT"    },    {        "mode": "REQUIRED",        "name": "not_after",        "type": "FLOAT"    }]```

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

Что в итоге получилось

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

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

```SQLSELECT  all_dns_namesFROM  [ctl-lists:certificate_data.scan_data]WHERE  (REGEXP_MATCH(all_dns_names,r'\b?xn\-\-'))  AND NOT all_dns_names CONTAINS 'cloudflare'```

И всего через 15 секунд получаем результат со всеми punycode доменами из всех известных CTL!

Рассмотрим другой пример. Попробуем получить все сертификаты доменов Coinbase, записанные в Certificate Transparency:

```SQLSELECT  all_dns_namesFROM  [ctl-lists:certificate_data.scan_data]WHERE  (REGEXP_MATCH(all_dns_names,r'.*\.coinbase.com[\s$]?'))```

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

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

Небольшая загадка

Проводя исследования, я обнаружил нечто странное. Домен `flowers-to-the-world.com` постоянно возникал в различных логах. Практически каждый лог имел огромное число сертификатов, содержащих этот домен:

```SQLSELECT  url,  COUNT(*) AS total_certsFROM  [ctl-lists:certificate_data.scan_data]WHERE  (REGEXP_MATCH(all_dns_names,r'.*flowers-to-the-world.*'))GROUP BY  urlORDER BY  total_certs DESC```

Whois позволяет определить, что этот домен принадлежит Google, так что мне интересно, не является ли это частью какой-то тестовой рутины. Если вы инженер Google, который может разузнать что-то у товарищей, занимающихся Certificate Transparency, было бы очень интересно услышать об этом.

Spoiler

Ответ инженера Google в комментариях под оригинальным постом

Привет, Райан. Пишет Пол Хэдфилд из команды Certificate Transparency.

`flowers-to-the-world.com` действительно принадлежит Google. Мы используем этот домен чтобы генерировать сертификаты, которые затем добавляются в каждый CTL в рамках периодической проверки логов на соответствие стандартам RFC6962. Проверка проводится с целью удостоверится, что логи работают в соответствии со стандартами и имеют приемлемый аптайм.

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

Если проследить полную цепочку при добавлении сертификата с `flower-to-the-world.com`, можно увидеть, что она заканчивается в корне со следующим эмитентом: C=GB, ST=London, O=Google UK Ltd., OU=Certificate Transparency, CN=Merge Delay Monitor Root

Надеюсь, это помогло.

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

В России, насколько нам известно, это первый такой продукт. В США и Китае у нас есть сильные конкуренты, но мы надеемся превзойти их по некоторым параметрам. Например, актуальность данных уже сейчас наша реализация поискового движка позволяет включать в поисковую выдачу данные, от сканов не старше минуты. На сегодняшний день Netlas.io доступен в формате "раннего доступа". Если есть желание потестировать переходите на сайт и регистрируйтесь.

Подробнее..

Пора избавляться от мышки или Hand Pose Estimation на базе LiDAR за 30 минут

12.01.2021 14:15:46 | Автор: admin
image

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

Предлагаю начать с посмотра коротенького видео, на котором видно, как можно за пару вечеров накидать простейшее управления курсором мышки на основе Object Detection, Hand Pose Estimation и камеры Intel Realsense L515. Конечно, оно далеко от идеала, но кажется, что осталось совсем немного подтянуть технологии и появятся принципиально новые способы управлять устройствами.



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

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

Основная идея это двигать мышь, передвигая не всю руку, а только указательный палец, что позволит не отрывая рук от клавиатуры, бегать по меню, нажимать кнопки и в совокупности с горячими клавишами превратиться в настоящего клавиатурного ninja! А что будет если добавить жесты пролистывания или скоролла? Думаю будет бомба! Но до этого момента нам еще придётся подождать пару-тройку лет)

Начнём собирать наш протитип манипулятора будущего



Что понадобится:
1. Камера с LiDAR Intel Realsense L515.
2. Умение программировать на python
3. Совсем чуть-чуть вспомнить школьную математику
4. Крепление для камеры на монитор ака штатив

Крепим камеру на шатив с алиэкспресс, он оказался очень удобный, лёгкий и дешевый =)
image
image

Разбираемся, как и на чём делать прототип



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

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

Во-первых, там все уже есть из коробки установка и запуск потребует минут 30, с учётом всех пререквизитов.

Во-вторых, благодаря мощной команде разработчиков, они не только бирут State Of Art в Hand Pose Estimation, но и дают лёгкое в понимание API.

В-третьих, сеть готова работать на CPU, так что порог входа минимален.

Наверное, вы спросите почему я не зашёл вот сюда и не воспользовался репозиториями победителей этого соревнования. На самом деле я довольно подробно изучил их решение, они вполне prod-ready, никаких стаков миллионов сеток и т.д. Но самая большая проблема, как мне кажется это то, что они работают с изображением глубины. Так как это академики, они не гнушались все данные конвертировать через матлаб, кроме того, разрешение, в котором были отсняты глубины, мне показались маленьким. Это могло сильно сказаться на результате. Поэтому, кажется, что проще всего получить ключевые точки на RGB картинке и по XY координатам взять значение по оси Z в Depth Frame. Сейчас не стоит задача сильно что-то оптимизировать, так что сделаем так, как это быстрее с точки зрения разработки.

Вспоминаем школьную математику



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

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

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

image

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

Установка бибилотеки для работы с камерой


Пожалуй, это самая сложная часть этой работы. Как оказалось, софт для камеры под Ubuntu очень сырой, librealsense просто завален все возможными багами, глюками и танцами с бубном.
До сих пор мне не удалось победить странное поведение камеры, иногда она не подгружает параметры при запуске.
Камера работает только один раз после рестарта компьютера!!! Но есть решение: перед каждым запуском делать програмно hard reset камеры, резет usb, и, может быть, всё будет хорошо. Кстати для Windows 10 там все нормально. Странно разработчики себе представляют роботов на базе винды =)

Чтобы под Ubuntu 20 у вас завелся realsense, сделайте так:
$ sudo apt-get install libusb-1.0-0-devThen rerun cmake and make install. Here is a complete recipe that worked for me:$ sudo apt-get install libusb-1.0-0-dev$ git clone https://github.com/IntelRealSense/librealsense.git$ cd librealsense/$ mkdir build && cd build$ cmake ../ -DFORCE_RSUSB_BACKEND=true -DBUILD_PYTHON_BINDINGS=true -DCMAKE_BUILD_TYPE=release -DBUILD_EXAMPLES=true -DBUILD_GRAPHICAL_EXAMPLES=true$ sudo make uninstall && make clean && make && sudo make install


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

Продолжаем разбираться в тонкостях работы нейросети



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


Это, как вы поняли, не тряска моих рук, на праздниках я выпил всего одну кружку New England DIPA =) Все дело в постоянных флуктациях ключевых точек и Z-координаты на основе значений, получаемых от лидара.
Посмотим вблизи:


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

Еще, как мне кажется, играет роль специфика разметки. Врядли можно сделать на таком колличестве кадров одинаковую разметку, не говоря уже о том, что разрешение кадра везде разное и не очень большое. Также мы не видим мерцание света, которое, вероятнеё всего, не постоянно из-за разного периода работы и величины экспозиции камеры. И еще сеть возращает бутерброд из heatmap, равный количеству ключевых точек на экране, размер этого тензора BxNx96x96, где N это кол-во ключевых точек, и, конечно же, после threshold и resize к оригинальному размеру кадра, мы получаем то что получаем =(

Прмер визуализации heatmap:
image

Обзор кода


Весь код находится в этом репозитории и он очень короткий. Давайте разберём основной файл, а остальное вы посмотрите сами.
import cv2import mediapipe as mpimport numpy as npimport pyautoguiimport pyrealsense2.pyrealsense2 as rsfrom google.protobuf.json_format import MessageToDictfrom mediapipe.python.solutions.drawing_utils import _normalized_to_pixel_coordinatesfrom pynput import keyboardfrom utils.common import get_filtered_values, draw_cam_out, get_right_indexfrom utils.hard_reset import hardware_resetfrom utils.set_options import set_short_rangepyautogui.FAILSAFE = Falsemp_drawing = mp.solutions.drawing_utilsmp_hands = mp.solutions.hands# инициализируем mediapipe для Hand Pose Estimationhands = mp_hands.Hands(max_num_hands=2, min_detection_confidence=0.9) def on_press(key):    if key == keyboard.Key.ctrl:        pyautogui.leftClick()    if key == keyboard.Key.alt:        pyautogui.rightClick()def get_color_depth(pipeline, align, colorizer):    frames = pipeline.wait_for_frames(timeout_ms=15000) # ождидаем фрейм от камеры    aligned_frames = align.process(frames)     depth_frame = aligned_frames.get_depth_frame()    color_frame = aligned_frames.get_color_frame()    if not depth_frame or not color_frame:        return None, None, None    depth_image = np.asanyarray(depth_frame.get_data())    depth_color_image = np.asanyarray(colorizer.colorize(depth_frame).get_data())    color_image = np.asanyarray(color_frame.get_data())    depth_color_image = cv2.cvtColor(cv2.flip(cv2.flip(depth_color_image, 1), 0), cv2.COLOR_BGR2RGB)    color_image = cv2.cvtColor(cv2.flip(cv2.flip(color_image, 1), 0), cv2.COLOR_BGR2RGB)    depth_image = np.flipud(np.fliplr(depth_image))    depth_color_image = cv2.resize(depth_color_image, (1280 * 2, 720 * 2))    color_image = cv2.resize(color_image, (1280 * 2, 720 * 2))    depth_image = cv2.resize(depth_image, (1280 * 2, 720 * 2))    return color_image, depth_color_image, depth_imagedef get_right_hand_coords(color_image, depth_color_image):    color_image.flags.writeable = False    results = hands.process(color_image)    color_image.flags.writeable = True    color_image = cv2.cvtColor(color_image, cv2.COLOR_RGB2BGR)    handedness_dict = []    idx_to_coordinates = {}    xy0, xy1 = None, None    if results.multi_hand_landmarks:        for idx, hand_handedness in enumerate(results.multi_handedness):            handedness_dict.append(MessageToDict(hand_handedness))        right_hand_index = get_right_index(handedness_dict)        if right_hand_index != -1:            for i, landmark_list in enumerate(results.multi_hand_landmarks):                if i == right_hand_index:                    image_rows, image_cols, _ = color_image.shape                    for idx, landmark in enumerate(landmark_list.landmark):                        landmark_px = _normalized_to_pixel_coordinates(landmark.x, landmark.y,                                                                       image_cols, image_rows)                        if landmark_px:                            idx_to_coordinates[idx] = landmark_px            for i, landmark_px in enumerate(idx_to_coordinates.values()):                if i == 5:                    xy0 = landmark_px                if i == 7:                    xy1 = landmark_px                    break    return color_image, depth_color_image, xy0, xy1, idx_to_coordinatesdef start():    pipeline = rs.pipeline() # инициализируем librealsense    config = rs.config()     print("Start load conf")    config.enable_stream(rs.stream.depth, 1024, 768, rs.format.z16, 30)    config.enable_stream(rs.stream.color, 1280, 720, rs.format.bgr8, 30)    profile = pipeline.start(config)     depth_sensor = profile.get_device().first_depth_sensor()    set_short_range(depth_sensor) # загружаем параметры для работы на маленьком расстоянии    colorizer = rs.colorizer()    print("Conf loaded")    align_to = rs.stream.color    align = rs.align(align_to) # совокупляем карту глубины и цветную картинку    try:        while True:            color_image, depth_color_image, depth_image = get_color_depth(pipeline, align, colorizer)            if color_image is None and color_image is None and color_image is None:                continue            color_image, depth_color_image, xy0, xy1, idx_to_coordinates = get_right_hand_coords(color_image,                                                                                                 depth_color_image)            if xy0 is not None or xy1 is not None:                z_val_f, z_val_s, m_xy, c_xy, xy0_f, xy1_f, x, y, z = get_filtered_values(depth_image, xy0, xy1)                pyautogui.moveTo(int(x), int(3500 - z))  # 3500 хард код специфичый для моего монитора                if draw_cam_out(color_image, depth_color_image, xy0_f, xy1_f, c_xy, m_xy):                    break    finally:        hands.close()        pipeline.stop()hardware_reset() # делаем ребут камеры и ожидаем её появленияlistener = keyboard.Listener(on_press=on_press) # устанавливаем слушатель нажатия кнопок клавиатурыlistener.start()start() # запуск программы


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

В самом начале происходит инициализация mediapipe, камеры, загрузка настроек камеры для работы short range и вспомогательных переменных. Следом идёт магия под названием alight depth to color эта функция ставит в соответсвие каждой точки из RGB картинки, точку на Depth Frame, то есть даёт нам возможность получать по координатам XY, значение Z.

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

Далее мы берём из всего предсказания только точки под номером 5 и 7 правой руки.
image

Осталось дело за малым полученные координаты фильтруем с помощью скользящего среднего. Можно было конечно применить более серьзные алгоритмы фильтрации, но взглянув на их визуализацию и подёргав разные рычажки, стало понятно, что для демо вполне хватит и скользящего среднего с глубиной 5 фреймов, хочу заметить что для XY вполне хватало и 2-3-х, но вот с Z дела обстоят хуже.
deque_l = 5x0_d = collections.deque(deque_l * [0.], deque_l)y0_d = collections.deque(deque_l * [0.], deque_l)x1_d = collections.deque(deque_l * [0.], deque_l)y1_d = collections.deque(deque_l * [0.], deque_l)z_val_f_d = collections.deque(deque_l * [0.], deque_l)z_val_s_d = collections.deque(deque_l * [0.], deque_l)m_xy_d = collections.deque(deque_l * [0.], deque_l)c_xy_d = collections.deque(deque_l * [0.], deque_l)x_d = collections.deque(deque_l * [0.], deque_l)y_d = collections.deque(deque_l * [0.], deque_l)z_d = collections.deque(deque_l * [0.], deque_l)def get_filtered_values(depth_image, xy0, xy1):    global x0_d, y0_d, x1_d, y1_d, m_xy_d, c_xy_d, z_val_f_d, z_val_s_d, x_d, y_d, z_d    x0_d.append(float(xy0[1]))    x0_f = round(mean(x0_d))    y0_d.append(float(xy0[0]))    y0_f = round(mean(y0_d))    x1_d.append(float(xy1[1]))    x1_f = round(mean(x1_d))    y1_d.append(float(xy1[0]))    y1_f = round(mean(y1_d))    z_val_f = get_area_mean_z_val(depth_image, x0_f, y0_f)    z_val_f_d.append(float(z_val_f))    z_val_f = mean(z_val_f_d)    z_val_s = get_area_mean_z_val(depth_image, x1_f, y1_f)    z_val_s_d.append(float(z_val_s))    z_val_s = mean(z_val_s_d)    points = [(y0_f, x0_f), (y1_f, x1_f)]    x_coords, y_coords = zip(*points)    A = np.vstack([x_coords, np.ones(len(x_coords))]).T    m, c = lstsq(A, y_coords)[0]    m_xy_d.append(float(m))    m_xy = mean(m_xy_d)    c_xy_d.append(float(c))    c_xy = mean(c_xy_d)    a0, a1, a2, a3 = equation_plane()    x, y, z = line_plane_intersection(y0_f, x0_f, z_val_s, y1_f, x1_f, z_val_f, a0, a1, a2, a3)    x_d.append(float(x))    x = round(mean(x_d))    y_d.append(float(y))    y = round(mean(y_d))    z_d.append(float(z))    z = round(mean(z_d))    return z_val_f, z_val_s, m_xy, c_xy, (y0_f, x0_f), (y1_f, x1_f), x, y, z


Создаем deque c длинной 5 фреймов и усредняем все подряд =) Дополнительно расчитываем y = mx+c, Ax+By+Cz+d=0, уравнение для прямой луч на RGB картинке и уравнение плоскости монитора, оно у нас получается y=0.

Итоги



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

Благодарности



Спасибо сообществу ods.ai, без него невозможно развиваться!
Подробнее..

Стилометрия, или как отличить Акунина от Булгакова с помощью 50 строк кода?

12.01.2021 02:13:34 | Автор: admin

Привет, Хабр.

Довольно интересным направлением "прикладной статистики" и NLP (Natural Languages Processing а вовсе не то что многие сейчас подумали) является анализ текста. Появилось это направление задолго до компьютеров, и имело вполне практическую цель: определить автора того или иного текста. С помощью ПК это впрочем, гораздо легче и удобнее, да и результаты получаются весьма интересные. Посмотрим, какие закономерности можно выявить с помощью совсем простого кода на Python.

Для тех кому интересно, продолжение под катом.

История

Одной из первых практических задач было определение авторства политических текстов TheFederalist Papers, написанных в США в 1780 годах. Их авторами было несколько человек, но кто есть кто, окончательно было неизвестно. Первый подход к построению кривой распределения длины слов был предпринят еще в 1851 г, и можно представить, какой это был объем работы. Сейчас, слава богу, всё проще. Я рассмотрю простейший способ анализа с помощью несложных расчетов и пакета Natural Language Toolkit, что в совокупности с matplotlib позволяет получить интересные результаты буквально в несколько строк кода. Мы посмотрим, как все это можно визуализировать, и какие закономерности можно увидеть.

Те, кому интересны результаты, главу "код" могут пропустить.

Код

Перейдем к практическому примеру. Возьмем для анализа следующий текст:

s = """Ежик сидел  на горке под  сосной и смотрел на освещенную        лунным светом долину, затопленную туманом. Красиво было так, что        он время от времени вздрагивал: не снится ли ему все это?"""

Подключим библиотеку nltk:

import nltknltk.data.find('tokenizers/punkt')tokens = nltk.word_tokenize(s)

Массив tokens содержит все слова и знаки пунктуации строки:

['Ежик', 'сидел', 'на', 'горке', 'под', 'сосной', 'и', 'смотрел', 'на',  'освещенную', 'лунным', 'светом', 'долину', ',', 'затопленную', 'туманом', '.'  ...]

Отфильтруем массив, удалив из него знаки препинания:

import stringremove_punctuation = str.maketrans('', '', string.punctuation)tokens_ = [x for x in [t.translate(remove_punctuation).lower() for t in tokens] if len(x) > 0]

Теперь мы можем получить первый статистический параметр: лексическое разнообразие текста. Это соотношение числа уникальных слов к их общему количеству.

text = nltk.Text(tokens_)lexical_divercity = (len(set(text)) / len(text)) * 100

Для данного текста этот параметр равен 96.6%.

Несложно получить среднюю длину слова:

words = set(tokens_)word_chars = [len(word) for word in words]mean_word_len = sum(word_chars) / float(len(word_chars))

Множество set(tokens_) дает нам неповторяющийся список слов, далее мы просто вычисляем среднее, разделив сумму на количество. Для этого текста средняя длина слова равна 4.86.

Средняя длина предложения вычисляется с помощью метода sent_tokenize в NLTK, который, как очевидно из названия, разбивает текст на предложения.

import numpy as npsentences = nltk.sent_tokenize(s)sentence_word_length = [len(sent.split()) for sent in sentences]mean_sentence_len = np.mean(sentence_word_length)

Для нашего текста длина предложения составляет 15 слов.

И последний параметр - частотность появления различных симолов. У каждого автора может быть свой стиль использования запятых, вопросов и кавычек, разных несклоняемых частей речи ("что", "в"). Для примера посчитаем частоту использования запятых на 1000 символов текста:

fdist = nltk.probability.FreqDist(nltk.Text(tokens))commas_per_thousand = (fdist[","] * 1000) / fdist.N()

Для данного текста параметр составляет 57.14 запятых на 1000 символов.

Последнее, что нам нужно сделать - загружать текст из файла.

import codecstry:    doc = codecs.open(file_name, 'r', 'cp1251').read()except:    doc = codecs.open(file_name, 'r', 'utf-8').read()

Как можно видеть, здесь есть два варианта. Часть файлов, скачанных из онлайн-библиотек, хранятся в кодировке 1251. Другая часть файлов сохранена методом copy-paste в Блокноте, и имеет более современную кодировку UTF-8. Вышеприведенный метод сначала пытается открыть файл как 1251, в случае неудачи мы считаем что это UTF-8, на практике такого подхода оказалось вполне достаточно.

Визуализация

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

Лексическое разнообразие и средняя длина слова не дают какой-либо заметной разницы:

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

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

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

Количество запятых на 1000 слов также отличается, и это очевидно - в длинном предложении их, очевидно, и должно быть больше:

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

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

Мы же рассмотрим пример попроще. Популярная в СССР детская книга "Улица младшего сына" имеет двух авторов, Лев Кассиль и Макс Поляновский. На графике хорошо видно статистическое различие по Lexical Diversity. Можно предположить что начало книги писал один автор, а закончил другой:

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

Но разумеется, может это и просто совпадение, теория вероятности такое, в принципе, допускает...

Заключение

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

Для желающих поэкспериментировать самостоятельно, исходный код для Python 3.7 приведен под спойлером.

text_process.py
import nltk, codecsimport numpy as npimport pandas as pdimport matplotlib.pyplot as pltfrom typing import Optional, Listimport stringimport globimport sys, osdef get_articles_from_blob(folder: str):    data = []    for path in glob.glob(folder + os.sep + "*"):        print(path)        data += get_articles_from_folder(path)    return datadef get_articles_from_folder(folder: str):    data = []    for path in glob.glob(folder + os.sep + "*.txt"):        data += get_data_from_file(path)    return [(folder.split(os.sep)[-1], data)]def get_data_from_file(file_name: str):    print("Get data for %s" % file_name)    try:        doc = codecs.open(file_name, 'r', 'cp1251').read()    except:        doc = codecs.open(file_name, 'r', 'utf-8').read()    chunk_size = 25000    data = []    for part in [doc[i:i+chunk_size] for i in range(0, len(doc) - (len(doc) % chunk_size), chunk_size)]:        data.append(get_data_from_str(part[part.find(' '):part.rfind(' ')]))    return datadef get_data_from_str(doc: str):    tokens = nltk.word_tokenize(doc)    remove_punctuation = str.maketrans('', '', string.punctuation)    tokens_ = [x for x in [t.translate(remove_punctuation).lower() for t in tokens] if len(x) > 0]    text = nltk.Text(tokens_)    lexical_divercity = (len(set(text)) / len(text)) * 100    words = set(tokens_)    word_chars = [len(word) for word in words]    mean_word_len = sum(word_chars) / float(len(word_chars))    sentences = nltk.sent_tokenize(doc)    sentence_word_length = [len(sent.split()) for sent in sentences]    mean_sentence_len = np.mean(sentence_word_length)    fdist = nltk.probability.FreqDist(nltk.Text(tokens))    commas_per_thousand = (fdist[","] * 1000) / fdist.N()    return (lexical_divercity, mean_word_len, mean_sentence_len, commas_per_thousand)def plot_data(data):    plt.rcParams["figure.figsize"] = (12, 5)    fig, ax = plt.subplots()    plt.title('Lexical diversity')    for author, author_data in data:        plt.plot(list(map(lambda val: val[0], author_data)), label=author)    plt.ylim([40, 70])    # plt.title('Mean Word Length')    # for author, author_data in data:    #     plt.plot(list(map(lambda val: val[1], author_data)), label=author)    # plt.ylim([4, 8])    # plt.title('Mean Sentence Length')    # for author, author_data in data:    #     plt.plot(list(map(lambda val: val[2], author_data)), label=author)    # plt.ylim([0, 30])    # plt.title("Commas per thousand")    # for author, author_data in data:    #     plt.plot(list(map(lambda val: val[3], author_data)), label=author)    plt.legend(loc='upper right')    plt.tight_layout()    plt.show()if __name__ == "__main__":    # Download punkt tokenizer    try:        nltk.data.find('tokenizers/punkt')    except LookupError:        nltk.download('punkt')    # Process text files    # data = get_articles_from_blob("Folder")  # Folder/AuthorXX/Text.txt    data = get_articles_from_folder("folder_here")  # Folder with files    plot_data(data)
Подробнее..

Приложение для конвертирования jpg файлов в pdfфайл

12.01.2021 12:17:05 | Автор: admin

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

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

Аналогичных программ в интернете не так много и, в основном, они платные.

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

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

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

Программы написаны на языке python

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

Здесь нам понадобиться модуль os и PIL

import osimport PIL.Imagedef img2pdf(fname):    filename = fname    name = filename.split('.')[0]    im = PIL.Image.open(filename)    if not os.path.exists('im2pdf_output'):        os.makedirs('im2pdf_output')    newfilename = ''.join(['im2pdf_output/',name,'.pdf'])    PIL.Image.Image.save(im, newfilename, "PDF", resolution = 100.0)    print("processed successfully: {}".format(newfilename))files = [f for f in os.listdir('./') if f.endswith('.jpg')]for fname in files:    img2pdf(fname)

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

Для этого нам понадобится модуль PyPDF2

from PyPDF2 import PdfFileMergerpdfs = []t=1for i in range(8):    f=str(t)+".pdf"    pdfs.append(f)    t=t+1print(pdfs)merger = PdfFileMerger()for pdf in pdfs:    merger.append(pdf)merger.write("result.pdf")merger.close()

Подробное видео о данных программах представлено ниже.

Здесь представлена ссылка на скачивание файлов
СКАЧАТЬ

Подробнее..

Соревнование KAGGLE по определению риска дефолта заемщика. Разработка признаков

14.01.2021 08:18:31 | Автор: admin

Введение: Соревнование от финансовой группы HOME CREDIT по определению риска дефолта заемщика

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

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

В рамках этой статьи я рассмотрю два простых метода построения признаков:

  • Полиномиальные признаки;

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

ПОЛИНОМИАЛЬНЕ ПРИЗНАКИ

Одним из простых методов построения признаков является созданиеполиномиальных признаков. В этом методе создаются признаки, которые являются степенями существующих признаков, а также определенными взаимодействиями между существующими признаками. Например, мы можем создать переменные EXTSOURCE1 ^ 2 и EXTSOURCE2 ^ 2, а также такие переменные, как EXTSOURCE1 * EXTSOURCE2, EXTSOURCE1 * EXTSOURCE2 ^ 2, EXTSOURCE1 ^ 2 x EXTSOURCE2 ^ 2 и так далее. Эти признаки, представляющие собой комбинацию нескольких отдельных переменных, называются условиями взаимодействия, поскольку они фиксируют взаимодействия между переменными. Другими словами, хотя две переменные сами по себе могут не иметь сильного влияния на цель, объединение их вместе в одну взаимодействующую переменную может показать связь с целью. Термины взаимодействия обычно используются в статистических моделях, чтобы уловить влияние нескольких переменных, но в машинном обучении используются не очень часто. Тем не менее, мы можем попробовать несколько вариантов, чтобы увидеть, могут ли они помочь нашей модели предсказать, вернет ли клиент ссуду или нет.

Следующим программным кодом я создам полиномиальные признаки, используя переменные EXTSOURCE и переменную DAYSBIRTH.В Scikit-Learn есть полезный классPolynomialFeatures, который создает полиномы и условия взаимодействия до определенной степени. Достаточно использовать степень 3, чтобы увидеть результат (при создании полиномиальных признаков, нежелательно использовать слишком высокую степень, потому что количество признаков экспоненциально масштабируется со степенью, а также можно столкнуться спроблемами переобучения).

poly_features = app_train[['EXT_SOURCE_1', 'EXT_SOURCE_2', 'EXT_SOURCE_3', 'DAYS_BIRTH', 'TARGET']]poly_features_test = app_test[['EXT_SOURCE_1', 'EXT_SOURCE_2', 'EXT_SOURCE_3', 'DAYS_BIRTH']]from sklearn.preprocessing import Imputerimputer = Imputer(strategy = 'median')poly_target = poly_features['TARGET']poly_features = poly_features.drop(columns = ['TARGET'])poly_features = imputer.fit_transform(poly_features)poly_features_test = imputer.transform(poly_features_test)from sklearn.preprocessing import PolynomialFeatures# Создаем объект PolynomialFeatures, указав степень взаимодействия, равную 3poly_transformer = PolynomialFeatures(degree = 3)poly_transformer.fit(poly_features)poly_features = poly_transformer.transform(poly_features)poly_features_test = poly_transformer.transform(poly_features_test)print('Polynomial Features shape: ', poly_features.shape)

Polynomial Features shape: (307511, 35)

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

poly_transformer.get_feature_names(input_features = ['EXT_SOURCE_1', 'EXT_SOURCE_2', 'EXT_SOURCE_3', 'DAYS_BIRTH'])[:15]

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

poly_features = pd.DataFrame(poly_features,                              columns = poly_transformer.get_feature_names(['EXT_SOURCE_1', 'EXT_SOURCE_2',                                                                            'EXT_SOURCE_3', 'DAYS_BIRTH']))poly_features['TARGET'] = poly_target# Найдем корреляцию с целевой меткойpoly_corrs = poly_features.corr()['TARGET'].sort_values()print(poly_corrs.head(10))print(poly_corrs.tail(5))

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

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

poly_features_test = pd.DataFrame(poly_features_test,                                   columns = poly_transformer.get_feature_names(['EXT_SOURCE_1', 'EXT_SOURCE_2',                                                                                 'EXT_SOURCE_3', 'DAYS_BIRTH']))poly_features['SK_ID_CURR'] = app_train['SK_ID_CURR']app_train_poly = app_train.merge(poly_features, on = 'SK_ID_CURR', how = 'left')poly_features_test['SK_ID_CURR'] = app_test['SK_ID_CURR']app_test_poly = app_test.merge(poly_features_test, on = 'SK_ID_CURR', how = 'left')app_train_poly, app_test_poly = app_train_poly.align(app_test_poly, join = 'inner', axis = 1)print('Training data with polynomial features shape: ', app_train_poly.shape)print('Testing data with polynomial features shape:  ', app_test_poly.shape)

Training data with polynomial features shape: (307511, 275)

Testing data with polynomial features shape: (48744, 275)

ПРИЗНАКИ, ОСНОВАННЕ НА ПОНИМАНИИ ПРЕДМЕТНОЙ ОБЛАСТИ

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

  • CREDITINCOMEPERCENT: процент суммы кредита относительно дохода клиента;

  • ANNUITYINCOMEPERCENT: процент аннуитетного платежа кредита относительно дохода клиента;

  • CREDITTERM: продолжительность платежа в месяцах;

  • DAYSEMPLOYED_PERCENT: процент отработанных дней по отношению к возрасту клиента.

app_train_domain = app_train.copy()app_test_domain = app_test.copy()app_train_domain['CREDIT_INCOME_PERCENT'] = app_train_domain['AMT_CREDIT'] / app_train_domain['AMT_INCOME_TOTAL']app_train_domain['ANNUITY_INCOME_PERCENT'] = app_train_domain['AMT_ANNUITY'] / app_train_domain['AMT_INCOME_TOTAL']app_train_domain['CREDIT_TERM'] = app_train_domain['AMT_ANNUITY'] / app_train_domain['AMT_CREDIT']app_train_domain['DAYS_EMPLOYED_PERCENT'] = app_train_domain['DAYS_EMPLOYED'] / app_train_domain['DAYS_BIRTH']app_test_domain['CREDIT_INCOME_PERCENT'] = app_test_domain['AMT_CREDIT'] / app_test_domain['AMT_INCOME_TOTAL']app_test_domain['ANNUITY_INCOME_PERCENT'] = app_test_domain['AMT_ANNUITY'] / app_test_domain['AMT_INCOME_TOTAL']app_test_domain['CREDIT_TERM'] = app_test_domain['AMT_ANNUITY'] / app_test_domain['AMT_CREDIT']app_test_domain['DAYS_EMPLOYED_PERCENT'] = app_test_domain['DAYS_EMPLOYED'] / app_test_domain['DAYS_BIRTH']

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

plt.figure(figsize = (12, 20))for i, feature in enumerate(['CREDIT_INCOME_PERCENT', 'ANNUITY_INCOME_PERCENT', 'CREDIT_TERM', 'DAYS_EMPLOYED_PERCENT']):    plt.subplot(4, 1, i + 1)    sns.kdeplot(app_train_domain.loc[app_train_domain['TARGET'] == 0, feature], label = 'target == 0')    sns.kdeplot(app_train_domain.loc[app_train_domain['TARGET'] == 1, feature], label = 'target == 1')    plt.title('Distribution of %s by Target Value' % feature)    plt.xlabel('%s' % feature); plt.ylabel('Density');plt.tight_layout(h_pad = 2.5)

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

BASELINE

Для наивного решения можно было бы указать одно и то же значение для всех примеров из тестового набора. Нас просят предсказать вероятность невыплаты ссуды, поэтому можно предположить вероятность 0,5 для всех наблюдений на тестовой выборке. Это даст нам площадь под кривой ошибок (AUC ROC), равную 0,5. Но лучше воспользоваться для нашего бейзлайна чуть более сложной моделью: логистической регрессией.

РЕАЛИЗАЦИЯ ЛОГИСТИЧЕСКОЙ РЕГРЕССИИ

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

from sklearn.preprocessing import MinMaxScaler, Imputerif 'TARGET' in app_train:    train = app_train.drop(columns = ['TARGET'])else:    train = app_train.copy()features = list(train.columns)test = app_test.copy()# Заполняем пропущенные значения медианным значением признакаimputer = Imputer(strategy = 'median')# Масштабируем признаки в диапазон 0-1scaler = MinMaxScaler(feature_range = (0, 1))imputer.fit(train)train = imputer.transform(train)test = imputer.transform(app_test)scaler.fit(train)train = scaler.transform(train)test = scaler.transform(test)print('Training data shape: ', train.shape)print('Testing data shape: ', test.shape)

Training data shape: (307511, 240)

Testing data shape: (48744, 240)

Для первой модели я использую LogisticRegressionиз библиотеки Scikit-Learn. Единственное изменение, которое потребуется в настройки модели по умолчанию, уменьшупараметр регуляризацииC, контролирующий степень переобучения (более низкое значение должно уменьшить переобучение). Это даст чуть лучшие результаты, чем LogisticRegression с параметрами по умолчанию, но все равно установит низкую планку для любых будущих моделей.

Сначала создаем модель, затем обучаем ее с помощью метода.fit, а затем делаем прогнозы на основе тестовых данных с помощью метода.predict_proba(нам нужны вероятности, а не целые числа 0 или 1).

from sklearn.linear_model import LogisticRegression# Создаем модель и тренируем ееlog_reg = LogisticRegression(C = 0.0001)log_reg.fit(train, train_labels)

Теперь, когда модель обучена, можно использовать ее для прогнозирования. В задаче требуется спрогнозировать вероятность невыплаты ссуды, поэтому использую метод моделиpredict.proba. Он возвращает массив m*2, где m количество наблюдений. Первый столбец это вероятность того, что цель будет равна 0, а второй вероятность того, что цель будет равна 1 (поэтому для каждой строки два столбца в сумме должны быть равными 1). Нам нужна вероятность того, что ссуда не будет погашена, поэтому выберу второй столбец.

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

log_reg_pred = log_reg.predict_proba(test)[:, 1]

Прогнозы должны быть в формате, показанном в файле samplesubmission.csv, где есть только два столбца: SKID_CURR и TARGET. Создадим фрейм данных в этом формате из набора тестов и прогнозов.

submit = app_test[['SK_ID_CURR']]submit['TARGET'] = log_reg_predsubmit.head()

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

submit.to_csv('log_reg_baseline.csv', index = False)

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

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

При отправке результата на оценку базовый показатель данной модели должен быть приблизительно равен 0,671.

УЛУЧШЕННАЯ МОДЕЛЬ: СЛУЧАЙНЙ ЛЕС

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

from sklearn.ensemble import RandomForestClassifierrandom_forest = RandomForestClassifier(n_estimators = 100, random_state = 50, verbose = 1, n_jobs = -1)random_forest.fit(train, train_labels)# Определяем важность признаковfeature_importance_values = random_forest.feature_importances_feature_importances = pd.DataFrame({'feature': features, 'importance': feature_importance_values})# Формируем предсказания для тестовых данныхpredictions = random_forest.predict_proba(test)[:, 1]

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

ДЕЛАЕМ ПРОГНОЗ, ИСПОЛЬЗУЯ СПЕЦИАЛЬНЕ ПРИЗНАКИ

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

poly_features_names = list(app_train_poly.columns)imputer = Imputer(strategy = 'median')poly_features = imputer.fit_transform(app_train_poly)poly_features_test = imputer.transform(app_test_poly)scaler = MinMaxScaler(feature_range = (0, 1))poly_features = scaler.fit_transform(poly_features)poly_features_test = scaler.transform(poly_features_test)random_forest_poly = RandomForestClassifier(n_estimators = 100, random_state = 50, verbose = 1, n_jobs = -1)random_forest_poly.fit(poly_features, train_labels)# Формируем предсказания на тестовых данныхpredictions = random_forest_poly.predict_proba(poly_features_test)[:, 1]
app_train_domain = app_train_domain.drop(columns = 'TARGET')domain_features_names = list(app_train_domain.columns)imputer = Imputer(strategy = 'median')domain_features = imputer.fit_transform(app_train_domain)domain_features_test = imputer.transform(app_test_domain)scaler = MinMaxScaler(feature_range = (0, 1))domain_features = scaler.fit_transform(domain_features)domain_features_test = scaler.transform(domain_features_test)random_forest_domain = RandomForestClassifier(n_estimators = 100, random_state = 50, verbose = 1, n_jobs = -1)random_forest_domain.fit(domain_features, train_labels)feature_importance_values_domain = random_forest_domain.feature_importances_feature_importances_domain = pd.DataFrame({'feature': domain_features_names, 'importance': feature_importance_values_domain})predictions = random_forest_domain.predict_proba(domain_features_test)[:, 1]

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

ИНТЕРПРЕТАЦИЯ МОДЕЛИ: ВАЖНОСТЬ ПРИЗНАКОВ

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

def plot_feature_importances(df):    df = df.sort_values('importance', ascending = False).reset_index()    df['importance_normalized'] = df['importance'] / df['importance'].sum()    plt.figure(figsize = (10, 6))    ax = plt.subplot()    ax.barh(list(reversed(list(df.index[:15]))),             df['importance_normalized'].head(15),             align = 'center', edgecolor = 'k')    ax.set_yticks(list(reversed(list(df.index[:15]))))    ax.set_yticklabels(df['feature'].head(15))    plt.xlabel('Normalized Importance'); plt.title('Feature Importances')    plt.show()    return df# Отображаем важность признаков без учета специальных признаковfeature_importances_sorted = plot_feature_importances(feature_importances)

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

feature_importances_domain_sorted = plot_feature_importances(feature_importances_domain)

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

ВВОД

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

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

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

  1. Разберитесь в проблеме и данных

  2. Очистите и отформатируйте данные (в основном это сделали для нас организаторы конкурса)

  3. Проведите исследовательский анализ данных

  4. Создайте базовую модель

  5. Улучшить модель

  6. Интерпретируйте модель

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

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

При подготовке статьи были использованы материалы из открытых источников:источник1,источник2.

Подробнее..

Перевод Constraint Programming или как решить задачу коммивояжёра, просто описав её

15.01.2021 14:04:54 | Автор: admin

Пожалуй, наиболее популярной парадигмой программирования является императивное программирование. Но это не единственный вид программирования, широко известны функциональное и логическое программирование. Constraint Programming (Программирование в ограничениях/Ограниченное программирование) не так популярно. Но это очень мощный инструмент для решения комбинаторных задач. Вместо реализации алгоритма, который решает задачу, с последующей тратой кучи времени на его отладку, рефакторинг и оптимизацию, программирование с ограничениями позволяет вам просто описать модель в специальном синтаксисе, а особая программа (решатель - solver) найдет решение за вас (или скажет, если их нет). Впечатляет, не правда ли? Мне кажется, каждый программист должен знать о такой возможности.

Minizinc

Вероятно, наиболее часто используемым инструментом программирования ограничений (по крайней мере, в образовательных целях) является minizinc. Он предоставляет IDE для объявления моделей и несколько встроенных решателей для поиска ответа. Вы можете скачать программу с официального сайта.

Простая модель в Minizinc

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

  • равенство должно выполняться

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

Например, решим следующее уравнение:

    S E N D+   M O R E= M O N E Y

Структура модели

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

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

Ограничения - это ограничения :), которым должны удовлетворять значения переменных.

Объявление модели

Приступим к собственно программированию. Здесь у нас есть 8 переменных (S, E, N, D, M, O, R, Y), они являются цифрами, поэтому они могут принимать значения от 0 до 9 (S и M от 1 до 9, потому что число не может начаться с нуля).

В синтаксисе minizinc это объявляется следующим образом:

var 1..9: S;var 0..9: E;var 0..9: N;var 0..9: D;var 1..9: M;var 0..9: O;var 0..9: R;var 0..9: Y;

Далее следует указать равенство, в minizinc оно задается самым обычным образом:

constraint                1000 * S + 100 * E + 10 * N + D                        + 1000 * M + 100 * O + 10 * R + E           == 10000 * M + 1000 * O + 100 * N + 10 * E + Y;

Мы также должны указать, что каждая переменная имеет свои собственные значения и не должно быть переменных с одинаковым значением. Minizinc имеет специальное ограничение alldifferent, но оно не определено в стандартной библиотеке, необходимо подключить его специальной директивой include "alldifferent.mzn";.

И последнее, что необходимо сделать, это объявить, каким образом решать модель, есть 3 варианта: удовлетворить (satisfy), минимизировать (minimize) и максимизировать (maximize) какое-то значение, я думаю, их названия говорят сами за себя :).

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

import zython as znclass MoneyModel(zn.Model):    def __init__(self):        self.S = zn.var(range(1, 10))        self.E = zn.var(range(0, 10))        self.N = zn.var(range(0, 10))        self.D = zn.var(range(0, 10))        self.M = zn.var(range(1, 10))        self.O = zn.var(range(0, 10))        self.R = zn.var(range(0, 10))        self.Y = zn.var(range(0, 10))        self.constraints = [(self.S * 1000 + self.E * 100 + self.N * 10 + self.D +                             self.M * 1000 + self.O * 100 + self.R * 10 + self.E ==                             self.M * 10000 + self.O * 1000 + self.N * 100 + self.E * 10 + self.Y),                             zn.alldifferent((self.S, self.E, self.N, self.D, self.M, self.O, self.R, self.Y))]model = MoneyModel()result = model.solve_satisfy()print(" ", result["S"], result["E"], result["N"], result["D"])print(" ", result["M"], result["O"], result["R"], result["E"])print(result["M"], result["O"], result["N"], result["E"], result["Y"])

Minizinc может выполнить модель и найти решение:

   9567+  1085= 10652

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

Заключение для первой части

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

Интеграция с Python

minizinc-python решает вторую проблему, предоставляя способ вызова моделей minizinc из python, библиотека будет запускать minizinc, сериализовать ваш ввод и разбирать вывод, но программист по-прежнему должен писать довольно много строк кода. Мы можем посмотреть на пример решения квадратного уравнения:

Spoiler

У автора публикации сломался хабр и перестал выдавать список языков для подсветки синтаксиса в drop-down меню, если кто-то знает, как починить, буду очень признателен.

import minizinc# Create a MiniZinc modelmodel = minizinc.Model()model.add_string("""var -100..100: x;int: a; int: b; int: c;constraint a*(x*x) + b*x = c;solve satisfy;""")# Transform Model into a instancegecode = minizinc.Solver.lookup("gecode")inst = minizinc.Instance(gecode, model)inst["a"] = 1inst["b"] = 4inst["c"] = 0# Solve the instanceresult = inst.solve(all_solutions=True)for i in range(len(result)):    print("x = {}".format(result[i, "x"]))

Лично для меня проблематично запомнить и воссоздать такой пример, а модель minizinc (которая представляет собой всего 4 строки кода) представлена в виде string, поэтому IDE и python не могут подсветить синтаксис и предоставить любую помощь и статические проверки для вас.

Zython

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

Это то, что делает zython (miniZinc pYTHON). Это самый простой способ погрузиться в программирование с ограничениями*.

Spoiler

* из того, что я знаю

* по крайней мере, если вы разработчик на Python. :)

Чтобы начать работу с zython, у вас должны быть установлены python 3.6+ и minizinc в путь по умолчанию или доступен в $PATH. После этого можно скачать сам пакет и проверить установку

pip install zythonpython>>> import zython as zn

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

Send More Money

Сначала разберём уже известную модель задачу "Send More Money"

import zython as znclass MoneyModel(zn.Model):    def __init__(self):        self.S = zn.var(range(1, 10))        self.E = zn.var(range(0, 10))        self.N = zn.var(range(0, 10))        self.D = zn.var(range(0, 10))        self.M = zn.var(range(1, 10))        self.O = zn.var(range(0, 10))        self.R = zn.var(range(0, 10))        self.Y = zn.var(range(0, 10))        self.constraints = [(self.S * 1000 + self.E * 100 + self.N * 10 + self.D +                             self.M * 1000 + self.O * 100 + self.R * 10 + self.E ==                             self.M * 10000 + self.O * 1000 + self.N * 100 + self.E * 10 + self.Y),                             zn.alldifferent((self.S, self.E, self.N, self.D, self.M, self.O, self.R, self.Y))]model = MoneyModel()result = model.solve_satisfy()print(" ", result["S"], result["E"], result["N"], result["D"])print(" ", result["M"], result["O"], result["R"], result["E"])print(result["M"], result["O"], result["N"], result["E"], result["Y"])

Она должен вернуть тот же результат, что и раньше.

Однако кажется, всё еще довольно много кода. Но если мы посмотрим внимательнее, мы увидим, что это в основном объявление переменных и арифметическое уравнение, zython выполняет всю грязную работу, например, поиск решателя, создания экземпляра, его параметризацию, запуск модели и передачу решения в скрипт на python. Все, что вы делаете, это само программирование. Кроме того, zython предоставляет синтаксис Python для определения модели, что позволяет IDE выделять ваш код и проверять его на наличие ошибок перед запуском. Zython же дополнительно осуществляет проверки во время выполнения.

Генерация судоку

Создадим поле для судоку. Для этого необходимо использовать zn.Array. Массив может быть как переменной, так и параметром. Так как на момент запуска значения в ячейках поля судоку неизвестны и должны быть найдены в данном случае создаётся массив переменных.

import zython as znclass MyModel(zn.Model):    def __init__(self):        self.a = zn.Array(zn.var(range(1, 10)), shape=(9, 9))        self.constraints = \            [zn.forall(range(9),                       lambda i: zn.alldifferent(self.a[i])),             zn.forall(range(9),                       lambda i: zn.alldifferent(self.a[:, i])),             zn.forall(range(3),                       lambda i: zn.forall(range(3),                                           lambda j: zn.alldifferent(self.a[i * 3: i * 3 + 3, j * 3: j * 3 + 3]))),            ]model = MyModel()result = model.solve_satisfy()print(result["a"])

Решение, выданное моделью будет зависить от версии minizinc, мне выдало следующее:

Задача коммивояжёра

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

import zython as znclass TSP(zn.Model):    def __init__(self, distances):        self.distances = zn.Array(distances)        self.path = zn.Array(zn.var(range(len(distances))),                             shape=len(distances))        self.cost = (self._cost(distances))        self.constraints = [zn.circuit(self.path)]    def _cost(self, distances):        return (zn.sum(range(1, len(distances)),                       lambda i: self.distances[self.path[i - 1],                                                self.path[i]]) +                self.distances[self.path[len(distances) - 1],                               self.path[0]])distances = [[0, 6, 4, 5, 8],             [6, 0, 4, 7, 6],             [4, 4, 0, 3, 4],             [5, 7, 3, 0, 5],             [8, 6, 4, 5, 0]]model = TSP(distances)result = model.solve_minimize(model.cost)print(result)

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

Заключение для второй части

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

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

Конструктивная критика, выражение своего мнения в комментариях, баг репорты, feature request'ы и PR одобряются.

Удачи в изучении программирования с ограничениями.

Подробнее..

Перевод Строим надёжную конкурентность с FSP и моделированием процессов

15.01.2021 18:06:16 | Автор: admin

Делаем систему параллелизма надёжнее


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

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


Сгенерированная инструментом LTSA диаграмма состояний


Что это за язык FSP?


Finite state processes (FSP) это абстрактный язык, на котором разрабатывают системы конкурентных процессов.

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

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

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

Как с помощью алгебры процессов (FSP) описать конкурентные процессы


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

Вначале проанализируем принтер



  • В принтер помещается только три листа, студенты могут распечатать до трёх документов;
  • Если листов нет, принтер нужно заполнить ими.

const MIN_SHEET_COUNT=1const MAX_SHEET_COUNT=3range DOC_COUNT=MIN_SHEET_COUNT .. MAX_SHEET_COUNTrange SHEET_STACK=0 .. MAX_SHEET_COUNTPRINTER(SHEETS_AVAILABLE = MAX_SHEET_COUNT) =  ( start -> PRINTER_AVAILABLE[MAX_SHEET_COUNT]),  PRINTER_AVAILABLE[sheets_available: SHEET_STACK] = if   (sheets_available > 0)  then (acquire -> print[DOC_COUNT] -> release -> PRINTER_AVAILABLE[sheets_available - 1])  else (empty -> refill_printer -> release -> PRINTER_AVAILABLE[MAX_SHEET_COUNT]).

Процесс PRINTER

Когда пользователь (студент или техник) получает принтер, принтер печатает документ, отдаёт его пользователю и возвращается в исходное состояние. Это называется повторяющимся поведением.

Как анимировать процесс


Чтобы анимировать процесс, сначала скомпилируйте (compile [1]) код, затем перейдите на вкладку draw. Нажмите кнопку animatе [2].


Анимация процесса PRINTER

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

Печатающий документы студент


Код на FSP можно написать при помощи условных процессов (if, then, else). DOCSTOPRINT = 3 это переданный процессу параметр 3. Процесс PRINT начинается с 0. Doc_count это метка индексированного действия, которая ведёт к этому действию: PRINT [doc_count].

STUDENT(DOCS_TO_PRINT = 3) =  PRINT[0],PRINT[doc_count: 0 .. DOCS_TO_PRINT] = if (doc_count < DOCS_TO_PRINT)then ( acquire -> print -> release -> PRINT[doc_count + 1]  )else ( terminate -> END ).

Процесс STUDENT с условным процессом

Тот же самый процесс можно написать и с помощью защищённых процессов.

STUDENT(DOCS_TO_PRINT = 3) =  PRINT[0],PRINT[doc_count: 0 .. DOCS_TO_PRINT] = (  when (doc_count < DOCS_TO_PRINT)  acquire -> print -> release -> PRINT[doc_count + 1] |  when (document_count == DOCUMENTS_TO_PRINT) terminate -> END ).

Процесс STUDENT с защищённым процессом


Анимация процесса STUDENT

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


TECHNICIAN = (empty -> refill_printer -> release -> TECHNICIAN | terminate -> END) .

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


Анимация процесса техника

Наконец, давайте посмотрим на составной процесс


  • Составной процесс должен быть синхронизирован со всеми подпроцессами, для этого можно определить набор действий с именем PRINT_Actions.
  • terminate/s1.terminate это перемаркированное действие. С помощью мы переназначаем действие s1.terminate, чтобы просто завершить процесс. В противном случае аниматор покажет s1.terminate, s2.terminateиtcn.terminate.
  • Чтобы синхронизировать пользователей с PRINTER, можно использовать взаимоисключающий общий ресурс.

|| SHARED_PRINTER = (s1: STUDENT(2) || s2: STUDENT(3) || tcn : TECHNICIAN || All_Users :: PRINTER)

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

const MIN_SHEET_COUNT=1const MAX_SHEET_COUNT=3range DOC_COUNT=MIN_SHEET_COUNT .. MAX_SHEET_COUNTrange SHEET_STACK=0 .. MAX_SHEET_COUNTset All_Users = {s1, s2, tcn}set PRINT_Actions = {acquire, print, release, empty}PRINTER(SHEETS_AVAILABLE = MAX_SHEET_COUNT) = PRINTER_AVAILABLE[MAX_SHEET_COUNT],PRINTER_AVAILABLE[sheets_available: SHEET_STACK] = if   (sheets_available > 0)then ( acquire -> print -> release -> PRINTER_AVAILABLE[sheets_available - 1]  )else ( empty -> release -> PRINTER_AVAILABLE[MAX_SHEET_COUNT] ).STUDENT(DOCS_TO_PRINT = 1) =  PRINT[0],PRINT[doc_count: 0 .. DOCS_TO_PRINT] = if   (doc_count < DOCS_TO_PRINT)then ( acquire -> print -> release -> PRINT[doc_count + 1]  )else ( terminate -> END )+ PRINT_Actions.TECHNICIAN = (empty -> refill_printer -> release -> TECHNICIAN | terminate -> END) + PRINT_Actions.|| SHARED_PRINTER = (s1: STUDENT(2) || s2: STUDENT(3) || tcn : TECHNICIAN || All_Users :: PRINTER) / {terminate/s1.terminate,terminate/s2.terminate,terminate/tcn.terminate}.

Составной процесс системы принтера

Я надеюсь, что этот материал поможет вам в изучении параллелизма на FSP.




Подробнее..

Перевод Как определять собственные классы исключений в Python

16.01.2021 18:16:46 | Автор: admin
Привет, Хабр!

Ваш интерес к новой книге "Секреты Python Pro" убедил нас, что рассказ о необычностях Python заслуживает продолжения. Сегодня предлагаем почитать небольшой туториал о создании кастомных (в тексте собственных) классах исключений. У автора получилось интересно, сложно не согласиться с ним в том, что важнейшим достоинством исключения является полнота и ясность выдаваемого сообщения об ошибке. Часть кода из оригинала в виде картинок.

Добро пожаловать под кат


Создание собственных классов ошибок


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

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

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

Собственный класс исключений MyCustomError


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



В вышеприведенном классе MyCustomError есть два волшебных метода, __init__ и __str__, автоматически вызываемых в процессе обработки исключений. Метод Init вызывается при создании экземпляра, а метод str при выводе экземпляра на экран. Следовательно, при выдаче исключения два этих метода обычно вызываются сразу друг за другом. Оператор вызова исключения в Python переводит программу в состояние ошибки.

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

В нашем случае можно сказать, что, если конструктору MyCustomError были переданы какие-либо аргументы, то мы берем первый переданный аргумент и присваиваем его атрибуту message в объекте. Если ни одного аргумента передано не было, то атрибуту message будет присвоено значение None.

В первом примере исключение MyCustomError вызывается без каких-либо аргументов, поэтому атрибуту message этого объекта присваивается значение None. Будет вызван метод str, который выведет на экран сообщение MyCustomError message has been raised.



Исключение MyCustomError выдается без каких-либо аргументов (скобки пусты). Иными словами, такая конструкция объекта выглядит нестандартно. Но это просто синтаксическая поддержка, оказываемая в Python при выдаче исключения.

Во втором примере MyCustomError передается со строковым аргументом We have a problem. Он устанавливается в качестве атрибута message у объекта и выводится на экран в виде сообщения об ошибке, когда выдается исключение.



Код для класса исключения MyCustomError находится здесь.

class MyCustomError(Exception):    def __init__(self, *args):        if args:            self.message = args[0]        else:            self.message = None    def __str__(self):        print('calling str')        if self.message:            return 'MyCustomError, {0} '.format(self.message)        else:            return 'MyCustomError has been raised'# выдача MyCustomErrorraise MyCustomError('We have a problem')


Класс CustomIntFloatDic


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

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

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

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

Для начала определю класс CustomIntFloatDict, наследующий от встроенного класса dict. dict передается в списке аргументов, которые заключены в скобки и следуют за именем класса CustomIntFloatDict.

Если создан экземпляр класса CustomIntFloatDict, причем, параметрам ключа и значения не передано никаких аргументов, то они будут установлены в None. Выражение if интерпретируется так: если или ключ равен None, или значение равно None, то с объектом будет вызван метод get_dict(), который вернет атрибут empty_dict; такой атрибут у объекта указывает на пустой список. Помните, что атрибуты класса доступны у всех экземпляров класса.



Назначение этого класса позволить пользователю передать список или кортеж с ключами и значениями внутри. Если пользователь вводит список или кортеж в поисках ключей и значений, то два эти перебираемых множества будут сцеплены при помощи функции zip языка Python. Подцепленная переменная, указывающая на объект zip, поддается перебору, а кортежи поддаются распаковке. Перебирая кортежи, я проверяю, является ли val экземпляром класса int или float. Если val не относится ни к одному из этих классов, я выдаю собственное исключение IntFloatValueError и передаю ему val в качестве аргумента.

Класс исключений IntFloatValueError


При выдаче исключения IntFloatValueError мы создаем экземпляр класса IntFloatValueError и одновременно выводим его на экран. Это означает, что будут вызваны волшебные методы init и str.

Значение, спровоцировавшее выдаваемое исключение, устанавливается в качестве атрибута value, сопровождающего класс IntFloatValueError. При вызове волшебного метода str пользователь получает сообщение об ошибке, информирующее, что значение init в CustomIntFloatDict является невалидным. Пользователь знает, что делать для исправления этой ошибки.



Классы исключений IntFloatValueError и KeyValueConstructError



Если ни одно исключение не выдано, то есть, все val из сцепленного объекта относятся к типам int или float, то они будут установлены при помощи __setitem__(), и за нас все сделает метод из родительского класса dict, как показано ниже.


Класс KeyValueConstructError


Что произойдет, если пользователь введет тип, не являющийся списком или кортежем с ключами и значениями?

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

Если пользователь не укажет ключи и значения как список или кортеж, то будет выдано исключение KeyValueConstructError. Цель этого исключения проинформировать пользователя, что для записи ключей и значений в объект CustomIntFloatDict, список или кортеж должен быть указан в конструкторе init класса CustomIntFloatDict.

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

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

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



Установка ключа и значения в CustomIntFloatDict


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

__setitem__ это волшебный метод, вызываемый при установке ключа и значения в словаре. В нашей реализации setitem мы проверяем, чтобы значение относилось к типу int или float, и только после успешной проверки оно может быть установлено в словаре. Если проверка не пройдена, то можно еще раз воспользоваться классом исключения IntFloatValueError. Здесь можно убедиться, что, попытавшись задать строку bad_value в качестве значения в словаре test_4, мы получим исключение.



Весь код к этому руководству показан ниже и выложен на Github.

# Создаем словарь, значениями которого могут служить только числа типов int и float  class IntFloatValueError(Exception):    def __init__(self, value):        self.value = value    def __str__(self):        return '{} is invalid input, CustomIntFloatDict can only accept ' \               'integers and floats as its values'.format(self.value)class KeyValueContructError(Exception):    def __init__(self, key, value):        self.key = key        self.value = value    def __str__(self):        return 'keys and values need to be passed as either list or tuple' + '\n' + \                ' {} is of type: '.format(self.key) + str(type(self.key)) + '\n' + \                ' {} is of type: '.format(self.value) + str(type(self.value))class CustomIntFloatDict(dict):    empty_dict = {}    def __init__(self, key=None, value=None):        if key is None or value is None:            self.get_dict()        elif not isinstance(key, (tuple, list,)) or not isinstance(value, (tuple, list)):            raise KeyValueContructError(key, value)        else:            zipped = zip(key, value)            for k, val in zipped:                if not isinstance(val, (int, float)):                    raise IntFloatValueError(val)                dict.__setitem__(self, k, val)    def get_dict(self):        return self.empty_dict    def __setitem__(self, key, value):        if not isinstance(value, (int, float)):            raise IntFloatValueError(value)        return dict.__setitem__(self, key, value)# тестирование # test_1 = CustomIntFloatDict()# print(test_1)# test_2 = CustomIntFloatDict({'a', 'b'}, [1, 2])# print(test_2)# test_3 = CustomIntFloatDict(('x', 'y', 'z'), (10, 'twenty', 30))# print(test_3)# test_4 = CustomIntFloatDict(('x', 'y', 'z'), (10, 20, 30))# print(test_4)# test_4['r'] = 1.3# print(test_4)# test_4['key'] = 'bad_value'


Заключение


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

Мы опубликовали современный Voice Activity Detector и не только

14.01.2021 10:07:09 | Автор: admin

image


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


Для решения задачи детекции голоса (Voice Activity Detector, VAD) существует довольно популярный инструмент от Google webRTC VAD. Он нетребовательный по ресурсам и компактный, но его основной минус состоит в неустойчивости к шуму, большом числе ложноположительных срабатываний и невозможности тонкой настройки. Понятно, что если переформулировать задачу не в детекцию голоса, а в детекцию тишины (тишина это отсутствие и голоса и шума), то она решается весьма тривиальными способами (порогом по энергии, например), но с теми же минусами и ограничениями. Что самое неприятное зачастую такие решения являются хрупкими и какие-то хардкодные пороги не переносятся на другие домены.


Изначально мы хотели сделать простой и быстрый внутренний инструмент для себя и наших партнеров для детекции произнесенных чисел без привлечения полноценного STT (фишка изначально была именно в портативности засчет использования современных фреймворков типа PyTorch и ONNX), но в итоге оказалось, что можно сделать не только детектор чисел, но и качественный, быстрый и портативный VAD и классификатор языков, который и опубликовали бесплатно для всех желающих тут под лицензией MIT. За подробностями прошу под кат.


Основные фишки нашего решения


Что же умеет делать наш "VAD"?


  • Именно сам VAD находит в аудио участки, где люди говорят;
  • Number detector находит в аудио участки, где люди произносят цифры;
  • Language classifier классифицирует языки;
  • Это все сейчас работает на 4 языках (Русский, Английский, Немецкий, Испанский), но с высокой степенью вероятности именно сам VAD будет работать и на других родственных им языках (небольшой квест для Хабра если вы говорите на каком-то экзотическом языке, запишите свой голос, прогоните VAD и поделитесь результатом!);

Основные "фишки" на данный момент:


  • Поддержка 4 языков;
  • Именно VAD сильно выигрывает у WebRTC по качеству;
  • Натренирован на огромных речевых и шумовых корпусах;
  • Ест мало ресурсов и памяти, работает на 1 потоке процессора;
  • Его скорости достаточно для edge и мобильных применений;
  • Построен на базе современных и портативных технологий (PyTorch, ONNX);
  • В отличие от WebRTC скорее является детектором голоса, а не детектором тишины;
  • Мы выложили чекпойнты как для PyTorch (JIT), так и для ONNX;

Возможные применения


  • Детекция конца фразы;
  • Подготовка и очистка голосовых корпусов;
  • Часть пайплайна для анонимизации речевых корпусов (по-хорошему еще надо уметь искать имена, но это совсем другая проблема, и она довольно специфична для решаемой задачи и требует наличия и тонкой настройки STT);
  • Детекция наличия голоса для применения на мобильных и edge устройствах;
  • Компактность и наличие ONNX позволяет запускать его с большим количеством доступных бекендов;
  • VAD кушает данные с частотой дискретизации 16 kHz, но он научен не бояться и данных с 8 kHz;

Примеры


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


  • Все примеры есть как для PyTorch так и для ONNX;
  • Для самого важного алгоритма VAD мы привели примеры как для работы с целыми отдельными файлами, так и для однопоточного / многопоточного стриминга;
  • Для остальных приведены только примеры по работе с отдельными файлами. Но имея VAD уже несложно длинные файлы разделить на короткие;
  • Примеры специально приводятся в виде простейшего тулкита, который легко будет адаптировать на свой язык с минимальными усилиями (обработка целых файлов тривиальна, стриминг в 1 поток несложный, несколько потоков немного сложноват из-за механизма окон);

Самый просто пример, где мы натравливаем VAD на файл:


import torchtorch.set_num_threads(1)model, utils = torch.hub.load(repo_or_dir='snakers4/silero-vad',                              model='silero_vad',                              force_reload=True)(get_speech_ts, _, read_audio, _, _, _) = utilsfiles_dir = torch.hub.get_dir() + '/snakers4_silero-vad_master/files'wav = read_audio(f'{files_dir}/en.wav')speech_timestamps = get_speech_ts(wav, model,                                  num_steps=4)print(speech_timestamps)

Как работает VAD


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


  • Аудио разделяется на кусочки длиной например 250 мс. Можно конечно порезать и короче, но по нашему опыту все паузы менее 100 мс являются малозначимыми и получается очень много шума, если пытаться поделить по 30-50мс. По просьбам интересующихся мы также привели график зависимости качества от длины кусочка тут (мы сравнили 100 мс и 250 мс);
  • VAD держит в памяти прошлый кусочек (или нули в начале стрима);
  • Эти кусочки по 500 мс (или по 200 мс) делятся на 4 или 8 окон внахлест и модель применяется к каждому такому окну;
  • Вероятности выдаваемые моделью усредняются по всем таким окнам;
  • Дальше эта вероятность используется, чтобы или "войти" в речь или из нее "выйти". Базовые оптимальные гипер-параметры приведены в коде примеров;

Скорость и задержка


Все замеры скорости мы делали на 1 потоке процессора AMD Ryzen Threadripper 3960X. Для этого мы использовали такие настройки:


torch.set_num_threads(1) # pytorchort_session.intra_op_num_threads = 1 # onnxort_session.inter_op_num_threads = 1 # onnx

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


  • num_steps число таких окон "внахлест";
  • number of audio streams число одновременно обрабатываемых потоков аудио;
  • По сути получается, что модель всегда видит батч длины равной num_steps * number of audio streams;

Получаются такие задержки:


Batch size Pytorch latency, ms Onnx latency, ms
2 9 2
4 11 4
8 14 7
16 19 12
40 36 29
80 64 55
120 96 85
200 157 137

Попробуем теперь измерить пропускную способность в секундах аудио, обрабатываемых за одну секунду на 1 потоке процессора:


Batch size num_steps Pytorch model RTS Onnx model RTS
40 4 68 86
40 8 34 43
80 4 78 91
80 8 39 45
120 4 78 88
120 8 39 44
200 4 80 91
200 8 40 46

Качество


По логике процесса, описанного выше, мы измеряли качество нашего VAD по сути просто присваивая некую усредненную вероятность каждому кусочку аудио и сравнивая ее с истинными метками. Но как добавить к сравнению WebRTС, он же выдает просто 0 или 1?


WebRTC принимает на вход фреймы аудио и отдает 0 или 1. По-умолчанию используется длина фрейма в 30 мс, то есть каждый кусочек аудио в 250 мс мы делится примерно на 8 таких фреймов. Это неидеально, но мы просто интерпретируем среднее из таких 0 и 1 как вероятность.


В итоге получается вот такой результат:


image


Тонкая настройка и остальные алгоритмы


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

Подробнее..

Перевод Как преобразовать аудиоданные в изображения

14.01.2021 14:20:31 | Автор: admin

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


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

Сегодня, специально к старту нового потока курса по машинному обучению делюсь с вами статьей, в которой авторы, в качестве примера определяют вид птиц по их пению. Они находят в записях, сделанных в естественных условиях, фрагменты с пением птиц, и классифицируют виды. Преобразовав аудиоданные в данные изображений и применив модели компьютерного зрения, авторы этой статьи получили серебряную медаль (как лучшие 2 %) на соревновании Kaggle Cornell Birdcall Identification.




Обработка аудио как изображений


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

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

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

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


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


Объяснение параметров звуковых волн

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


Объяснение спектрограммы

Чтобы понять, как частоты отражаются в спектрограммах, посмотрите на трёхмерную визуализацию, которая демонстрирует амплитуду с помощью дополнительного измерения. По оси X отложено время, а по оси Y значения частот. Ось z это амплитуда звуков частоты координаты y в момент координаты x. По мере увеличения значения z цвет меняется с синего на красный, получается цвет, который мы видели в предыдущем примере 2D-спектрограммы.


Визуализация трёхмерной спектрограммы

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

Шкала мел и её спектрограмма


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

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

$m = 2595 * log(1+f/700).$


Спектрограмма на мел-шкале это просто спектрограмма с частотами, измеренными в мел.

Как мы используем спектрограмму?


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

import librosay, sr = librosa.load('img-tony/amered.wav', sr=32000, mono=True)melspec = librosa.feature.melspectrogram(y, sr=sr, n_mels = 128)melspec = librosa.power_to_db(melspec).astype(np.float32)

Где y обозначает необработанные данные волны, sr обозначает частоту дискретизации аудио-сэмпла, а n_mels определяет количество полос мел в сгенерированной спектрограмме. При использовании метода melspectrogram вы также можете установить параметры метода f_min и f_max. Можно установить Then и преобразовать спектрограмму в спектрограмму мел, выражающую амплитуду на прямоугольной шкале, к децибелам с помощью метода power_to_db.

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

import librosa.displaylibrosa.display.specshow(melspec, x_axis='time',  y_axis='mel', sr=sr, fmax=16000)

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

from torchlibrosa.stft import Spectrogram, LogmelFilterBankspectrogram_extractor = Spectrogram()logmel_extractor = LogmelFilterBank()y = spectrogram_extractor(y)y = self.logmel_extractor(y)

Резюме


В заключение скажу, что мы можем воспользоваться преимуществами последних достижений компьютерного зрения в задачах, связанных со звуком, путём преобразования данных аудиоклипов в данные изображения. Мы достигаем этого с помощью спектрограмм, показывающих сведения о частоте, амплитуде и времени аудиоданных в изображении. Использование шкалы мел и спектрограммы шкалы мел помогает компьютерам имитировать человеческий слух, чтобы различать звуки разных частот. Для генерации спектрограмм мы могли бы воспользоваться библиотекой librosa или torchlibrosa для ускорения GPU на Python. Рассматривая таким образом задачи, связанные со звуком, мы можем создавать эффективные модели глубокого обучения для выявления и классификации звуков, так же, как, например, врачи диагностируют сердечные заболевания с помощью ЭКГ.




Подробнее..

Как мы в СберМаркете боремся с товарами-призраками

14.01.2021 12:05:51 | Автор: admin
Так могла бы выглядеть наша команда, но мы на удаленкеТак могла бы выглядеть наша команда, но мы на удаленке

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

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

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

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

Как работает СберМаркет

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

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

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

Идея алгоритма

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

Исторически 70% таких артикулов имеют очень высокий показатель ненайденных товаров за последние дни продаж более 50%. Значит, нужно срочно заблокировать позицию в каталоге, чтобы клиенты не смогли купить то, что мы не можем привезти.

Выше представлена динамика неуспешных сборок по товару из категории фрукты в одном магазине. С 21 июня по 1 июля показатель ненайденных товаров был 100%. Хотя ретейлер передавал нам другие данные.

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

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

И начали писать его на Python.

Как работает алгоритм

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

  2. Из базы данных наших заказов вытаскиваем историю последних сборок. Для каждого ненайденного товара берём историю THRESHOLD_ORDERS.

  3. Если в последних THRESHOLD_ORDERS заказах процент ненайденных товаров более THRESHOLD_CANCELLATION процентов, то эту позицию нужно заблокировать.

  4. Если товар ещё ни разу не блокировался или с момента последней блокировки уже прошло DAYS_NO_CANC дней, то товар блокируется на HIDE_1 дней.

  5. Если с момента последней блокировки товара прошло менее DAYSNOCANC дней, то:

    ~ товар блокируется на HIDE_2 дней, если текущая блокировка ставится второй раз подряд;
    ~ товар блокируется на HIDE_3 дней, если уже блокировался более двух раз подряд.

Пример параметров алгоритма для магазина:

'params': {  'DAYS_NO_CANC': 4,  'HIDE_1': 2,  'HIDE_2': 11,  'HIDE_3': 9,  'THRESHOLD_CANCELLATION': 0.4,  'THRESHOLD_ORDERS': 3  }

Параметры алгоритма подбирались отдельно для каждого магазина на исторических данных с использованием библиотеки hyperopt на Python. В процессе оптимизации максимизировалась метрика F-мера с beta 0.7.

Зачем так усложнять

Почему нельзя просто всегда блокировать товары на N дней? Зачем нам параметры DAYS_NO_CANC, HIDE_1, HIDE_2 и HIDE_3?

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

Кейс 1: товары нужно блокировать на маленький срок

У ретейлера A в магазине закончились бананы. Новую поставку смогут выложить в торговый зал через 1-2 дня. Бананы необходимо заблокировать на максимально маленький срок после этого они точно будут доступны для клиентов и сборщиков.

Кейс 2: товары нужно блокировать на большой срок

У ретейлера B случился сбой в поставке яблочного сока. Возможно, новая партия доедет до магазина через 2-3 недели. Нет смысла блокировать товар на маленький срок, так как после такой блокировки клиенты все равно смогут заказать товар, но сборщик не сможет его собрать.

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

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

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

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

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

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

Чего вам не хватает в сервисах доставки продуктов или у нас? Делитесь в комментариях подумаем над решением.

Подробнее..

Перевод Как быть билингвом в Data Science

09.01.2021 20:18:44 | Автор: admin
В этой статье я хочу продемонстрировать R Markdown удобную надстройку для программирования вашего проекта как на R, так и на Python, позволяющую программировать некоторые элементы вашего проекта на двух языках и управлять объектами, созданными на одном языке, с помощью другого языка. Это может быть полезно потому, что:

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





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


Для работы понадобятся эти компоненты:

  1. Конечно, R и Python.
  2. IDE RStudio (можно сделать это в других IDE, но в RStudio проще).
  3. Ваш любимый менеджер среды для Python (здесь я использую conda).
  4. Пакеты rmarkdown и reticulate, установленные в R.

При написании документов R Markdown мы будем работать в среде RStudio, но при этом перемещаться между фрагментами кода, написанными на R и на Python. Покажу пару простых примеров.

Настройка среды Python


Если вы знакомы с программированием на Python, то вы знаете, что любая выполняемая на Python работа, должна ссылаться на конкретную, содержащую все необходимые для работы пакеты среду. Есть много способов управления пакетами в Python, два самых популярных virtualenv и conda. Здесь я предполагаю, что мы используем conda и что он установлен в качестве менеджера среды Python.
Вы можете использовать пакет reticulate в R для настройки окружений conda через командную строку R, если хотите (используя такие функции, как conda_create()), но как обычный программист Python я предпочитаю настраивать свои среды вручную.

Предположим, мы создаём среду conda с именем r_and_python и устанавливаем в неё pandas и statsmodels. Итак, команды в терминале:

conda create -name r_and_pythonconda activate r_and_pythonconda install pandasconda install statsmodels

После установки pandas, statsmodels (и любых других пакетов, которые могут вам понадобиться) настройка среды завершена. Теперь запустите conda info в терминале и выберите путь к вашей среде. Он понадобится вам на следующем шаге.

Настройка вашего проекта R для работы с R и Python


Мы запустим проект R в RStudio, но хотим иметь возможность запускать Python в этом же проекте. Чтобы убедиться, что код Python работает нужной нам среде, необходимо установить системную переменную среды RETICULATE_PYTHON для исполняемого файла Python в этой среде. Это будет путь, который вы выбрали в предыдущем разделе, за которым следует /bin/python3.

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

Sys.setenv(RETICULATE_PYTHON=path_to_environment/bin/python3")

Замените pathtoenvironment на путь, который вы выбрали в предыдущем разделе. Сохраните файл .Rprofile и перезапустите сеанс R. Каждый раз, когда вы перезапускаете сеанс или проект, запускается .Rprofile, настраивающий вашу среду Python. Если вы хотите проверить это, вы можете запустить строку Sys.getenv (RETICULATE_PYTHON).

Написание кода первый пример


Теперь вы можете настроить в своём проекте документ R Markdown .Rmd и писать код на двух разных языках. Сначала нужно загрузить библиотеку reticulate в ваш первый фрагмент кода.

```{r}library(reticulate)```

Теперь, когда вы захотите написать код на Python, можно обернуть его обычными обратными кавычками, но пометить как фрагмент кода Python с помощью {python}, а когда захотите писать на R воспользуйтесь {r}.

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

```{python}import pandas as pdimport statsmodels.api as smimport statsmodels.formula.api as smf# obtain ugtests dataurl = http://peopleanalytics-regression-book.org/data/ugtests.csv"ugtests = pd.read_csv(url)# define modelmodel = smf.ols(formula = Final ~ Yr3 + Yr2 + Yr1, data = ugtests)# fit modelfitted_model = model.fit()# see results summarymodel_summary = fitted_model.summary()print(model_summary)```



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

Не бойтесь. Можно получить доступ ко всем объектам python, которые вы создали в общем списке с названием py. Поэтому, если создается блок R внутри вашего документа R Markdown, коллеги получат доступ к параметрам вашей модели:

```{r}py$fitted_model$params```



или первые несколько остатков:

```{r}py$fitted_model$resid[1:5]```



Теперь можно легко выполнить некоторую диагностику модели, например построить график остатков вашей модели типа квантиль-квантиль:

```{r}qqnorm(py$fitted_model$resid)```



Написание кода второй пример


Вы анализировали некоторые данные о быстрых знакомствах на Python и создали фрейм данных pandas со всеми данными в нём. Для простоты загрузим данные и посмотрим на них:

```{python}import pandas as pdurl = http://peopleanalytics-regression-book.org/data/speed_dating.csv"speed_dating = pd.read_csv(url)print(speed_dating.head())```


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

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

И снова ничего не бойтесь, отправьте проект коллеге и он напишет решение на R.

```{r}library(lme4)speed_dating <- py$speed_datingiid_intercept_model <- lme4:::glmer(dec ~ agediff + samerace + attr + intel + prob + (1 | iid), data = speed_dating, family = binomial)coefficients <- coef(iid_intercept_model)$iid```

Теперь вы можете получить код и посмотреть на коэффициенты. Также можно получить доступ к объектам R в Python внутри общего объекта r.

```{python}coefs = r.coefficientsprint(coefs.head())```


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

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

Примеры данных в документе взяты из моего Справочника по моделированию регрессии в People Analytics.

image


Подробнее..

Перевод Анимации градиентного спуска и ландшафта потерь нейронных сетей на Python

10.01.2021 14:07:00 | Автор: admin
Во время изучения различных алгоритмов машинного обучения я наткнулся на ландшафт потерь нейронных сетей с их горными территориями, хребтами и долинами. Эти ландшафты потерь сильно отличались от выпуклых и гладких ландшафтов потерь, с которыми я столкнулся при использовании линейной и логистической регрессий. Здесь мы создадим ландшафты потерь нейронных сетей и анимированного градиентного спуска с помощью датасета MNIST.


Рисунок 1 Ландшафт потерь свёрточной нейронной сети с 56 слоями (VGG-56, источник)



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

Искусственная нейронная сеть, с которой мы будем работать, состоит из одного входного слоя (с 784 узлами), двух скрытых слоёв (с 50 и 500 узлами соответственно) и одного выходного слоя (с 10 узлами). Мы будем повсеместно использовать сигмовидную функцию в качестве функции активации. Нейронная сеть не будет подвержена предвзятости. Обучающие данные состоят из изображений 28x28 пикселей, рукописных цифр в диапазоне от 0 до 9 из набора данных MNIST. Технически мы могли бы выбрать любой из 784*50+50*500+500*10=69,200 весов, которые мы используем в нашей нейронной сети. Я произвольно решил использовать веса w250, 5 (2) и w251,5(2), которые соединяют 250-й и 251-й узлы второго скрытого слоя с 6-м выходным нейроном соответственно. В нашей модели 6-й выходной нейрон возвращает активацию для модели, прогнозируя наличие цифры 5 на изображении. На рисунке 2 схематично показана архитектура нейронной сети, с которой мы будем работать. Из соображений ясности некоторые связи между нейронами и большая часть весовых аннотаций были намеренно опущены.


Рисунок 2 Архитектура нейронной сети

Мы импортируем MNIST в скрипт на Python. Рукописные цифры набора данных MNIST представлены в виде изображений в оттенках серого, поэтому мы можем нормализовать входные данные путём масштабирования значений пикселей из диапазона 0-255 в диапазон 0-1,2 в нашем коде, следовательно, мы делим x-значения на 255.

# Import librariesimport numpy as npimport gzipfrom sklearn.preprocessing import OneHotEncoderimport matplotlib.pyplot as pltfrom mpl_toolkits.mplot3d import Axes3Dfrom scipy.special import expitimport celluloidfrom celluloid import Camerafrom matplotlib import animation # Open MNIST-files: def open_images(filename):    with gzip.open(filename, "rb") as file:             data=file.read()        return np.frombuffer(data,dtype=np.uint8, offset=16).reshape(-1,28,28).astype(np.float32) def open_labels(filename):    with gzip.open(filename,"rb") as file:        data = file.read()        return np.frombuffer(data,dtype=np.uint8, offset=8).astype(np.float32)     X_train=open_images("C:\\Users\\tobia\\train-images-idx3-ubyte.gz").reshape(-1,784).astype(np.float32) X_train=X_train/255 # rescale pixel values to 0-1y_train=open_labels("C:\\Users\\tobia\\train-labels-idx1-ubyte.gz")oh=OneHotEncoder(categories='auto') y_train_oh=oh.fit_transform(y_train.reshape(-1,1)).toarray() # one-hot-encoding of y-values

Чтобы создать ландшафты потерь, создадим график поверхности стоимости по отношению к вышеупомянутым весам w_250, 5(2) и w_251,5(2). Для этого мы определим среднеквадратичную функцию стоимости ошибки по отношению к весам w_a и w_b. Стоимости нашей модели J эквивалентны усреднённой сумме квадратичных ошибок между прогнозом модели и фактическим значением каждого из 10 выходных нейронов нашего обучающего набора данных с размером N:



С y и pred, представляющим матрицы фактических и прогнозируемых значений y соответственно. Прогнозируемые значения вычисляются путём прямого распространения входных данных через нейронную сеть на конечный слой. Вывод каждого слоя служит входными данными для следующего слоя. Входная матрица умножается на весовую матрицу соответствующего слоя. После этого сигмоидная функция применяется для получения выходных данных этого конкретного слоя. Весовые матрицы инициализируются малыми случайными числами с помощью генератора псевдослучайных чисел numpy. С помощью seed мы гарантируем воспроизводимость результатов. После этого подставляем два веса, которые могут изменяться в зависимости от аргументов функции w_a и w_b. Мы разработали функцию затрат на Python следующим образом:

hidden_0=50 # number of nodes of first hidden layerhidden_1=500 # number of nodes of second hidden layer# Set up cost function:def costs(x,y,w_a,w_b,seed_):          np.random.seed(seed_) # insert random seed         w0=np.random.randn(hidden_0,784)  # weight matrix of 1st hidden layer        w1=np.random.randn(hidden_1,hidden_0) # weight matrix of 2nd hidden layer        w2=np.random.randn(10,hidden_1) # weight matrix of output layer        w2[5][250] = w_a # set value for weight w_250,5(2)        w2[5][251] = w_b # set value for weight w_251,5(2)        a0 = expit(w0 @ x.T)  # output of 1st hidden layer        a1=expit(w1 @ a0)  # output of 2nd hidden layer        pred= expit(w2 @ a1) # output of final layer        return np.mean(np.sum((y-pred)**2,axis=0)) # costs w.r.t. w_a and w_b

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

# Set range of values for meshgrid: m1s = np.linspace(-15, 17, 40)   m2s = np.linspace(-15, 18, 40)  M1, M2 = np.meshgrid(m1s, m2s) # create meshgrid # Determine costs for each coordinate in meshgrid: zs_100 = np.array([costs(X_train[0:100],y_train_oh[0:100].T                                 ,np.array([[mp1]]), np.array([[mp2]]),135)                         for mp1, mp2 in zip(np.ravel(M1), np.ravel(M2))])Z_100 = zs_100.reshape(M1.shape) # z-values for N=100zs_10000 = np.array([costs(X_train[0:10000],y_train_oh[0:10000].T                                 ,np.array([[mp1]]), np.array([[mp2]]),135)                         for mp1, mp2 in zip(np.ravel(M1), np.ravel(M2))])Z_10000 = zs_10000.reshape(M1.shape) # z-values for N=10,000# Plot loss landscapes: fig = plt.figure(figsize=(10,7.5)) # create figureax0 = fig.add_subplot(121, projection='3d' )ax1 = fig.add_subplot(122, projection='3d' )fontsize_=20 # set axis label fontsizelabelsize_=12 # set tick label size# Customize subplots: ax0.view_init(elev=30, azim=-20)ax0.set_xlabel(r'$w_a$', fontsize=fontsize_, labelpad=9)ax0.set_ylabel(r'$w_b$', fontsize=fontsize_, labelpad=-5)ax0.set_zlabel("costs", fontsize=fontsize_, labelpad=-30)ax0.tick_params(axis='x', pad=5, which='major', labelsize=labelsize_)ax0.tick_params(axis='y', pad=-5, which='major', labelsize=labelsize_)ax0.tick_params(axis='z', pad=5, which='major', labelsize=labelsize_)ax0.set_title('N:100',y=0.85,fontsize=15) # set title of subplot ax1.view_init(elev=30, azim=-30)ax1.set_xlabel(r'$w_a$', fontsize=fontsize_, labelpad=9)ax1.set_ylabel(r'$w_b$', fontsize=fontsize_, labelpad=-5)ax1.set_zlabel("costs", fontsize=fontsize_, labelpad=-30)ax1.tick_params(axis='y', pad=-5, which='major', labelsize=labelsize_)ax1.tick_params(axis='x', pad=5, which='major', labelsize=labelsize_)ax1.tick_params(axis='z', pad=5, which='major', labelsize=labelsize_)ax1.set_title('N:10,000',y=0.85,fontsize=15)# Surface plots of costs (= loss landscapes):  ax0.plot_surface(M1, M2, Z_100, cmap='terrain', #surface plot                             antialiased=True,cstride=1,rstride=1, alpha=0.75)ax1.plot_surface(M1, M2, Z_10000, cmap='terrain', #surface plot                             antialiased=True,cstride=1,rstride=1, alpha=0.75)plt.tight_layout()plt.show()


Рисунок 3 Ландшафты с различными размерами образцов

На рисунке 3 показаны два примерных ландшафта потерь с одинаковыми весами (w_250, 5 (2) и w_251,5(2)) и одинаковыми случайными начальными весами. Левый участок поверхности создавался с помощью первых 100 изображений набора данных MNIST, в то время как участок справа был создан с помощью первых 10 000 изображений. Если мы присмотримся к левому графику, то увидим некоторые типичные черты невыпуклых ландшафтов потерь: локальные минимумы, плато, хребты (иногда также называемые седловыми точками) и глобальный минимум. Однако термин минимум следует использовать с осторожностью, поскольку мы видим только заданный диапазон значений, вместе с тем не проводился тест первой производной.


Рисунок 4

Градиентный спуск


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



где J градиент нашей функции стоимости, w вес всей модели, e соответствующая эпоха и скорость обучения.

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



где w определяется как вес между j-м узлом слоя до и i-м узлом текущего слоя, который является выходным слоем в нашем случае. Вход i-го нейрона в выходном слое просто обозначается как in () и эквивалентен сумме активаций слоя до умножения на их соответствующие веса соединения, ведущие к этому узлу. Выход * i*-го нейрона в выходном слое обозначается как out () и соответствует (in ()). Решая уравнение выше, мы получаем:



с * out (), соответствующим активации j-го узла в слое, перед которым в выходном слое через w. соединен с n-м узлом. Переменная target обозначает целевой вывод для каждого из 10 выходных нейронов. Ссылаясь на рисунок 2, out () будет соответствовать активации h или h, в зависимости от того веса, от которого мы намереваемся вычислить частную производную. Отличное объяснение, включая подробный математический вывод, можно найти здесь [4].

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

# Store values of costs and weights in lists: weights_2_5_250=[] weights_2_5_251=[] costs=[] seed_= 135 # random seedN=100 # sample size # Set up neural network: class NeuralNetwork(object):    def __init__(self, lr=0.01):        self.lr=lr        np.random.seed(seed_) # set random seed        # Intialize weight matrices:         self.w0=np.random.randn(hidden_0,784)          self.w1=np.random.randn(hidden_1,hidden_0)        self.w2=np.random.randn(10,hidden_1)        self.w2[5][250] = start_a # set starting value for w_a        self.w2[5][251] = start_b # set starting value for w_b        def train(self, X,y):        a0 = expit(self.w0 @ X.T)          a1=expit(self.w1 @ a0)          pred= expit(self.w2 @ a1)        # Partial derivatives of costs w.r.t. the weights of the output layer:         dw2= (pred - y.T)*pred*(1-pred)  @ a1.T / len(X)   # ... averaged over the sample size        # Update weights:         self.w2[5][250]=self.w2[5][250] - self.lr * dw2[5][250]         self.w2[5][251]=self.w2[5][251] - self.lr * dw2[5][251]         costs.append(self.cost(pred,y)) # append cost values to list        def cost(self, pred, y):        return np.mean(np.sum((y.T-pred)**2,axis=0))    # Initial values of w_a/w_b: starting_points = [  (-9,15),(-10.1,15),(-11,15)] for j in starting_points:    start_a,start_b=j    model=NeuralNetwork(10) # set learning rate to 10    for i in range(10000):  # 10,000 epochs                    model.train(X_train[0:N], y_train_oh[0:N])         weights_2_5_250.append(model.w2[5][250]) # append weight values to list        weights_2_5_251.append(model.w2[5][251]) # append weight values to list# Create sublists of costs and weight values for each starting point: costs = np.split(np.array(costs),3) weights_2_5_250 = np.split(np.array(weights_2_5_250),3)weights_2_5_251 = np.split(np.array(weights_2_5_251),3)

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

fig = plt.figure(figsize=(10,10)) # create figureax = fig.add_subplot(111,projection='3d' ) line_style=["dashed", "dashdot", "dotted"] #linestylesfontsize_=27 # set axis label fontsizelabelsize_=17 # set tick label fontsizeax.view_init(elev=30, azim=-10)ax.set_xlabel(r'$w_a$', fontsize=fontsize_, labelpad=17)ax.set_ylabel(r'$w_b$', fontsize=fontsize_, labelpad=5)ax.set_zlabel("costs", fontsize=fontsize_, labelpad=-35)ax.tick_params(axis='x', pad=12, which='major', labelsize=labelsize_)ax.tick_params(axis='y', pad=0, which='major', labelsize=labelsize_)ax.tick_params(axis='z', pad=8, which='major', labelsize=labelsize_)ax.set_zlim(4.75,4.802) # set range for z-values in the plot# Define which epochs to plot:p1=list(np.arange(0,200,20))p2=list(np.arange(200,9000,100))points_=p1+p2camera=Camera(fig) # create Camera objectfor i in points_:    # Plot the three trajectories of gradient descent...    #... each starting from its respective starting point    #... and each with a unique linestyle:    for j in range(3):         ax.plot(weights_2_5_250[j][0:i],weights_2_5_251[j][0:i],costs[j][0:i],                linestyle=line_style[j],linewidth=2,                color="black", label=str(i))        ax.scatter(weights_2_5_250[j][i],weights_2_5_251[j][i],costs[j][i],                   marker='o', s=15**2,               color="black", alpha=1.0)    # Surface plot (= loss landscape):    ax.plot_surface(M1, M2, Z_100, cmap='terrain',                              antialiased=True,cstride=1,rstride=1, alpha=0.75)    ax.legend([f'epochs: {i}'], loc=(0.25, 0.8),fontsize=17) # set position of legend    plt.tight_layout()     camera.snap() # take snapshot after each iteration    animation = camera.animate(interval = 5, # set delay between frames in milliseconds                          repeat = False,                          repeat_delay = 0)animation.save('gd_1.gif', writer = 'imagemagick', dpi=100)  # save animation   


Рисунок 5 Траектории градиентного спуска

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

fig = plt.figure(figsize=(10,10)) # create figureax0=fig.add_subplot(2, 1, 1) ax1=fig.add_subplot(2, 1, 2) # Customize subplots: ax0.set_xlabel(r'$w_a$', fontsize=25, labelpad=0)ax0.set_ylabel(r'$w_b$', fontsize=25, labelpad=-20)ax0.tick_params(axis='both', which='major', labelsize=17)ax1.set_xlabel("epochs", fontsize=22, labelpad=5)ax1.set_ylabel("costs", fontsize=25, labelpad=7)ax1.tick_params(axis='both', which='major', labelsize=17)contours_=21 # set the number of contour linespoints_=np.arange(0,9000,100) # define which epochs to plotcamera = Camera(fig) # create Camera objectfor i in points_:    cf=ax0.contour(M1, M2, Z_100,contours_, colors='black', # contour plot                     linestyles='dashed', linewidths=1)    ax0.contourf(M1, M2, Z_100, alpha=0.85,cmap='terrain') # filled contour plots         for j in range(3):        ax0.scatter(weights_2_5_250[j][i],weights_2_5_251[j][i],marker='o', s=13**2,               color="black", alpha=1.0)        ax0.plot(weights_2_5_250[j][0:i],weights_2_5_251[j][0:i],                linestyle=line_style[j],linewidth=2,                color="black", label=str(i))                ax1.plot(costs[j][0:i], color="black", linestyle=line_style[j])    plt.tight_layout()    camera.snap()    animation = camera.animate(interval = 5,                          repeat = True, repeat_delay = 0)  # create animation animation.save('gd_2.gif', writer = 'imagemagick')  # save animation as gif


Рисунок 6 Траектории градиентного спуска в 2D

Обе анимации показывают, что градиентный спуск может застревать в локальных минимумах, седловых точках или плато с невыпуклыми ландшафтами потерь. Для преодоления некоторых из этих препятствий были реализованы многочисленные варианты градиентного спуска (ADAGRAD, Adam и др.). Однако я хотел бы прояснить, что не все ландшафты потерь настолько невыпуклые в определённом диапазоне значений для w_a и w_b. Выпуклость ландшафта потерь зависит, среди прочего, от количества скрытых слоев, при этом глубокие нейронные сети приводят к сильно невыпуклым ландшафтам потерь.

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


Рисунок 7 N=500, w20030(1), w20031(1) (создано автором, код)


Рисунок 8 N=1000, w55(1), w56(1) (создано автором, код)

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

Я надеюсь, что вам понравилось! Полный код Jupyter Notebook можно найти на моём GitHub.

Приложение



Представленное изображение

Ссылки
База данных MNIST

  1. Li, Hao, et al. Visualizing the loss landscape of neural nets. Advances in neural information processing systems. 2018.
  2. Как нормализовать, центрировать и стандартизировать пиксели изображения в Keras
  3. Почему сложно обучать нейронную сеть
  4. Пример пошагового обратного распространения ошибки
  5. Staib, Matthew & J. Reddi, Sashank & Kale, Satyen & Kumar, Sanjiv & Sra, Suvrit. (2019). Escaping Saddle Points with Adaptive Gradient Methods.
  6. Dauphin, Yann et al. Identifying and attacking the saddle point problem in high-dimensional non-convex optimization. NIPS (2014).
  7. Choromanska, A., Henaff, M., Mathieu, M., Arous, G. B., & LeCun, Y. (2015). The loss surfaces of multilayer networks. Journal of Machine Learning Research, 38, 192204.


image



Подробнее..

Клиент-серверный IPC на Python multiprocessing

11.01.2021 16:10:01 | Автор: admin

Статья отражает личный опыт разработки CLI приложения для Linux.

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

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

Введение

Межпроцессное взаимодействие (англ. inter-process communication, IPC) обмен данными между потоками одного или разных процессов. Реализуется посредством механизмов, предоставляемых ядром ОС или процессом, использующим механизмы ОС и реализующим новые возможности IPC. Википедия

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

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

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

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

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

Предпосылки параллелизма

Если ваша программа не использует привилегированные системные вызовы, вам не нужен суперпользователь, а значит можно писать монолит без параллелизма.

В противном случае вам придётся запускать свою программу под рутом.

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

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

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

При этом вы можете запросить у процесса в руте исполнение системного вызова из пользовательского процесса при помощи одного из методов IPC.


Таблица методов межпроцессного взаимодействия

Метод

Реализуется ОС или процессом

Неименованный канал

Все ОС, совместимые со стандартом POSIX.

Разделяемая память

Все ОС, совместимые со стандартом POSIX.

Очередь сообщений (Message queue)

Большинство ОС.

Сигнал

Большинство ОС; в некоторых ОС, например, в Windows, сигналы доступны только в библиотеках, реализующих стандартную библиотеку языка Си, и не могут использоваться для IPC.

Почтовый ящик

Некоторые ОС.

Сокет

Большинство ОС.

Именованный канал

Все ОС, совместимые со стандартом POSIX.

Проецируемый в память файл (mmap)

Все ОС, совместимые со стандартом POSIX. При использовании временного файла возможно возникновение гонки. ОС Windows также предоставляет этот механизм, но посредством API, отличающегося от API, описанного в стандарте POSIX.

Обмен сообщениями (без разделения)

Используется в парадигме MPI, Java RMI, CORBA и других.

Файл

Все ОС.

Семафор

Все ОС, совместимые со стандартом POSIX.

Канал

Все ОС, совместимые со стандартом POSIX.


Для своего приложения я выбрал сокеты и написал API для коммуникации между процессами.

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

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

Историческая справка

Традиционно процессы, которые запускаются при загрузке системы и остаются активными в фоне, классифицируются как daemon. Имена исполняемых файлов таких программ по соглашению заканчиваются на d. Пример: systemd.

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

Известны и другие примеры: ssh и sshd.

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

Структура проекта

Для сервера и клиента я использую одинаковую структуру.

. core  api.py  __init__.py main.py

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

Реализация API клиента

from multiprocessing.connection import Clientfrom multiprocessing.connection import Listener# адрес сервера (процесса в руте) для исходящих# запросовdaemon = ('localhost', 6000)# адрес клиента (этого процесса) для входящих# ответов от сервераcli = ('localhost', 6001)def send(request: dict) -> bool or dict:    """    Принимает словарь аргументов удалённого метода.    Отправляет запрос, после чего открывет сокет    и ждет на нем ответ от сервера.    """    with Client(daemon) as conn:        conn.send(request)    with Listener(cli) as listener:        with listener.accept() as conn:            try:                return conn.recv()            except EOFError:                return Falsedef hello(name: str) -> send:    """    Формирует уникальный запрос и вызывает функцию    send для его отправки.    """    return send({        "method": "hello",        "name": name    })

В модуле connection пакета multiprocessing есть два класса, реализующих API высокого уровня над низкоуровнивым аналогом стандартной библиотеки socket.

Client класс, который содержит методы отправки дейтаграмм.

Listener принимает дейтаграммы.

Отправляемые запросы содержат название целевого метода сервера.

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

Использование API

В main.py я импортирую модуль api для дальнейшего использования.

from core import apiresponse = api.hello("World!")print(response)

Этот код представлен для демонстрации. В работе я использовал Сlick Framework для создания СLI приложения с опциями, которые вызывают методы API.

Реализация API сервера

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

def hello(request: dict) -> str:    """    Привилегированный системный вызов.    """    return " ".join(["Hello", request["name"])

Использование API

from core import apifrom multiprocessing.connection import Listenerfrom multiprocessing.connection import Client# адрес сервера (этого процесса) для входящих запросовdaemon = ('localhost', 6000)# адрес клиента для исходящих ответовcli = ('localhost', 6001)while True:    with Listener(daemon) as listener:        with listener.accept() as conn:            request = conn.recv()            if request["method"] == "hello":                response = api.hello(request)            with Client(cli) as conn:                conn.send(response)

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

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

Дополнительно

Советую снабдить свой сервер пакетом systemd, который позволяет программам на Python писать лог в journald.

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

Спасибо за внимание!

Подробнее..

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

11.01.2021 18:14:50 | Автор: admin


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

MuZero


DeepMind неожиданно опубликовали статью о MuZero, алгоритме, который способен играть как в популярные логические настольные игры вроде шахмат, Сёги и Го, так и в видеоигры Atari вроде Pac-Man.

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

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

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



Infinite Nature


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

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

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



Time Travel Rephotography


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



pi-GAN


Еще одна GAN-модель, которая генерирует 3D представление объекта из нескольких неразмеченных двухмерных изображений. В демо показано, как модель можно использовать для вращения головы, подобно тому как ранее демонстрировали Nvidia в Maxine.



Neural Scene Flow Fields


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



YolactEdge


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

ModNet


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

Svoice


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


Hypersim


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

ArtLine


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

На этом все, вот таким на удивление насыщенным оказался декабрь. Начало года тоже обещает быть интересным. Нам уже не терпится посмотреть, что в январе появится на основе Dall-E от OpenAI. Как говорится, stay tuned!
Подробнее..

Реализация распределённых вычислений на языке python с использованием технологии docker

13.01.2021 12:17:24 | Автор: admin

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

Одно из первых упоминаний распределенных вычислений относится к 1973 году. Сотрудники научно-исследовательского центра Xerox PARC Джон Шох и Джон Хапп написали программу, которая рассылала себя по другим работающими компьютерам через локальную сеть PARC.

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

Ещё одним значимым событием было создание проекта SETI@Home (Search for Extra-Terrestrial Intelligence at Home) для поиска внеземного разума путём анализа данных с радиотелескопов, в том числе на домашних компьютерах участников. Данный проект был запущен в 1999 году и оста новлен в 2020-м. Эта распределенная система была построена на платформе BOINC, созданной в университете Беркли.

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

Ещё одной важной областью применения распределённых вычислений является обработка больших данных с использованием методов машинного обучения и Data Mining. В качестве языка программирования для этой цели в последние годы на лидирующие позиции выходит язык Python. По состоянию на март 2020 года, согласно рейтингу TIOBE, Python находится на третьем месте, хотя ещё в 2015 году занимал лишь седьмое.

Одной из известных проблем языка Python является относительно низкая производительность в сравнении с компилируемыми языками такими как C++. Данный недостаток является дополнительным поводом применять параллельное и распределённое программирование в процессе разработки.

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

Основной инструмент кластеризации для Docker носит название Docker Swarm. Он позволяет объединять узлы в единое кластерное пространство и распространять контейнеры по этому кластеру. Представленная в данной статье разработка основывается на изолированных Docker-контейнерах исполнителях.

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

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

Здесь стоит учитывать, что потоки в Python по умолчанию являются не совсем полноценными в отличие, например, от потоков в языке C++. Дело в том, что интерпретатор Python (CPython) написан на языке C с использованием некоторых библиотек, не являющихся потокобезопасными. Из-за этого используемый в CPython механизм GIL (Global Interpreter Lock) не позволяет потокам исполняться по-настоящему параллельно даже на многоядерных процессорах. Вместо этого интерпретатор постоянно переключается между потоками с интервалом примерно 5 миллисекунд. В связи с этим, на современных компьютерах в большинстве случаев более эффективным вариантом будет использование стратегии множественных процессов.

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

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

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

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

Тестирование проводилось на двух объединённых в локальную сеть компьютерах (6 ядер на первом и 2 на втором) с суммарным объёмом ОЗУ 20 ГБ, под управлением ОС Linux.

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

Результаты выполнения представлены в следующей таблице.

Стратегия/Задача

Расчет факториала

Вычисление про- стых чисел

Множественные процессы (8 процессов)

7.3525 с

39.3731 c

Многопоточный (8 потоков)

54.3255 c

42.0415 с

Последовательная

43.4656 c

41.4426 c

Асинхронная

43.5361 с

43.9102 c

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

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

Одним из возможных применений разработки является организация распределённой проверки решений при проведении соревнований по программированию.

Подробнее..

Голосовой ассистент на Python (Виталий alfa 2.0)

14.01.2021 20:16:39 | Автор: admin

Привет хабр! Меня зовут Глеб Пряхин, мне 14 лет, я написал голосового ассистента на python 3 и скомпилировал его в exe.

(Ссылка на скомпилированный вариант)

Прошу протестировать помощника и позадавать ему вопросы,

если у вас появились вопросы или вы хотите написать в поддержку: в комментах под постом, или на эл. почту glebilic@gmail.com.


Приступим к коду! Для начала импортируем модули:

import pyttsx3import osimport timeimport datetimeimport speech_recognition as srimport randomimport webbrowserimport sounddevice as sd

Для добавления голосов скачиваем и устанавливаем RHvoice.

Теперь ассистент читает файл с кол-во просмотров и подгружает файл tts, где хранится нумер голоса. Переменная num123 = количеству просмотров, а tts1 = нумеру голоса в синтезаторе речи.

f = open("sp.txt", "r")num123 = f.read(1000)num123 = int(num123)f.closef = open("tts.txt", "r")tts1 = int(f.read(1))f.close()#синтез речиtts = pyttsx3.init()speak_engine = pyttsx3.init()voices = speak_engine.getProperty('voices')speak_engine.setProperty('voice', voices[tts1].id)

Простейшая шапка для программы:

os.system('cls' if os.name == 'nt' else 'clear')print("ВИТАЛИЙ 2.0 ALFA \nBy Глеб Пряхин\n2021 \n-загрузка...")

Теперь мы читаем файл name и понимаем, что ужас! Наш пациент, ой то есть клиент не зарегистрировался в системе! Вызываем форму регистрации:

f = open('name.txt', 'r')if f.read(1) == "":    os.system('cls' if os.name == 'nt' else 'clear')    tts.say("Добрый день, я Виталий, я здесь, что бы вывести ваше взаимодействие с компьютером на новый, продуктивный уровень. Давайте знакомится. Как вас зовут?")    tts.runAndWait()    r = sr.Recognizer()    with sr.Microphone(device_index = 1) as source:        print(' ')        r.adjust_for_ambient_noise(source, duration=0.5) #настройка посторонних шумов        print('...')        audio = r.listen(source)        print(' ')    try:        query = r.recognize_google(audio, language = 'ru-RU')        name = query.lower()        print(f'Вы сказали: {query.lower()}')                except:        print('-')    tts.say("Вас зовут")    tts.runAndWait()    tts.say(name)    tts.runAndWait()    tts.say("Подтвердите пожалуйста.")    tts.runAndWait()    r = sr.Recognizer()    with sr.Microphone(device_index = 1) as source:        print(' ')        r.adjust_for_ambient_noise(source, duration=0.5) #настройка посторонних шумов        print('...')        audio = r.listen(source)        print(' ')    try:        query = r.recognize_google(audio, language = 'ru-RU')        ver = query.lower()        print(f'Вы сказали: {query.lower()}')                except:        print('-')    if "да" in ver or "подтверждаю" in ver:        f = open('name.txt', 'w')        f.write(name.title())        f.close()        tts.say("Готово! Теперь давайте поболтаем!")        tts.runAndWait()        else:        tts.say("напишите свое имя на клавиатуре")        tts.runAndWait()        name = input("Ваше имя: ")        tts.say("Готово! Теперь давайте поболтаем!")        tts.runAndWait()        f = open('name.txt', 'w')        f.write(name.title())        f.close()

Теперь, мы читаем кол-во просмотров, и понимаем что это "первый раз", поэтому начинаем читать инструктаж:

if num123 == 0:        tts.say("Но сначала я хочу научить основным командам: И так вот список моих команд:")        tts.runAndWait()        tts.say("Тут текст инструктажа")        tts.runAndWait()

Подготавливаем программу ко входу в центральный цикл:

#тут мы добавляем просмотр к счетчику просмотровf = open("sp.txt", "w")num123 = num123 + 1num123 = str(num123)num123 = f.write(num123)f.close#а тут читаем имя и создаем рандомное числоf = open('name.txt', 'r')name = f.read(10)r = random.randint(1,10)

В прошлом блоке мы сгенерировали рандомное число и занесли его в переменную r, теперь создадим elif`ки и зададим переменные cont, они понадобятся нам позже:

cont = ""if r == 1:    tts.say("Добрый день")    tts.runAndWait()    tts.say(name)    tts.runAndWait()    tts.say("Как дела?")    tts.runAndWait()elif r == 2:    tts.say("Привет")    tts.runAndWait()    tts.say(name)    tts.runAndWait()    tts.say("Чем могу помочь?")    tts.runAndWait()elif r == 3:    tts.say("Привет привет")    tts.runAndWait()    tts.say(name)    tts.runAndWait()    tts.say("Чем займемся?")    tts.runAndWait()elif r == 4:    tts.say("Добрый день")    tts.runAndWait()    tts.say(name)    tts.runAndWait()    tts.say("Хотите открою почту?")    tts.runAndWait()    cont = ("почта")elif r == 5:    tts.say("Добрый день")    tts.runAndWait()    tts.say(name)    tts.runAndWait()    tts.say("Открыть ютуб?")    tts.runAndWait()    cont = "ютуб"elif r == 6:    tts.say("Привет")    tts.runAndWait()    tts.say(name)    tts.runAndWait()    tts.say("Посмотрим кино?")    tts.runAndWait()    cont = "кино"elif r == 7:    tts.say("Добрый день")    tts.runAndWait()    tts.say(name)    tts.runAndWait()    tts.say("Что хотите узнать?")    tts.runAndWait()elif r == 8:    tts.say("Приветики")    tts.runAndWait()    tts.say(name)    tts.runAndWait()    tts.say("Хотите почитать последние новости?")    tts.runAndWait()    cont = "новости"elif r == 9:    tts.say("Добрый день")    tts.runAndWait()    tts.say(name)    tts.runAndWait()    tts.say("Где вы были?")    tts.runAndWait()elif r == 10:    tts.say("Добрый день")    tts.runAndWait()    tts.say(name)    tts.runAndWait()    tts.say("Как дела?")    tts.runAndWait()

Входим в цикл:

while True:    cikl = cikl + 1    ca = 0    ra = random.randint(1,10)    an = ""    #распознание    r = sr.Recognizer()    with sr.Microphone(device_index = 1) as source:        print(' ')        r.adjust_for_ambient_noise(source, duration=0.5) #настройка посторонних шумов        print('...')        audio = r.listen(source)        print(' ')    try:        query = r.recognize_google(audio, language = 'ru-RU')        an = query.lower()        print(f'Вы сказали: {query.lower()}')                except:        print('-')

И создадим первую команду "да", помните переменную cont? Так вот она отличает ответ "да" на вопрос "открыть ютуб?", и "да" на вопрос "Включить новости?", если контекста нет, то он просто ответит стандартным ответом.

 #да    if "да" in an and len(an) == 2 or "давай" in an or "почему-бы и нет" in an:        ca = 1        if cont == "почта":            f = open('email.txt', 'r')            if f.read(1) == "":                tts.say("Я совсем забыл, на каком сервисе зарегистрирована ваша почта! Пожайлуста выберете на экране нужную.")                tts.runAndWait()                a = 1                while True:                    v = input("Вставьте ссылку для почтового сервиса.")                    f = open('email.txt', 'w')                    if "https" in v:                        web = v                        f.write(web)                        f.close()                        break            tts.say("открываю почту")            f = open('email.txt', 'r')            web = f.read(97)            f.close()            tts.runAndWait()            webbrowser.open(web)                elif cont == "ютуб":            tts.say("Хорошо, включаю его")            tts.runAndWait()            webbrowser.open('https://www.youtube.com/')        elif cont == "кино":            tts.say("Давайте подберем что нибудь на око")            tts.runAndWait()            webbrowser.open('https://okko.tv/')        elif cont == "новости":            tts.say("Открываю евроньюс!")            tts.runAndWait()            webbrowser.open('https://www.youtube.com/watch?v=E3rH3KdVWcc')                elif cont == "ютубпр":            tts.say("Вот, надеюсь вам понравится")            tts.runAndWait()            webbrowser.open('https://www.youtube.com/channel/UCy0uukwm4dOSFCGyfp8g2sw')        else:            tts.say("Это очень хорошо.")            tts.runAndWait()

Теперь перейдем к интернет командам. Я приведу по 1 примеру на каждый их вид:

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

elif "вк " in an or "вконтакте" in an:        ca = 1        tts.say("Включаю вконтакте")        tts.runAndWait()        webbrowser.open("https:/vk.com")

Команда, которая выполняет поиск по сайту:

elif "найди в интернете" in an:        ca = 1        tts.say("Выполняю поиск по запросу")        tts.runAndWait()        tts.say(an[an.find("ете")+3:])        tts.runAndWait()        sear = an[an.find("ете")+3:]        webbrowser.open("https://www.google.com/search?q=" + sear)

Команда открывающая сайт, который сохранен в файл, и может изменятся:

elif "почт" in an:        ca = 1        f = open('email.txt', 'r')        if f.read(1) == "":            tts.say("Я совсем забыл, на каком сервисе зарегистрирована ваша почта! Пожайлуста выберете на экране нужную.")            tts.runAndWait()            a = 1            while True:                v = input("Вставьте ссылку для почтового сервиса.")                f = open('email.txt', 'w')                if "https" in v:                    web = v                    f.write(web)                    f.close()                    break

Далее перейдем к командам для компа:

Команда на выполнение команды в командной строке:

elif "если я скажу это" in an:        ca = 1        os.system('то помощник выполнит эту команду через командную строку')        tts.say("И скажет это")        tts.runAndWait()        tts.say(name) #а потом имя произнесет.        tts.runAndWait()

Команда на выключение компа:

 elif "выключи компьютер" in an or "заверши работу" in an:        ca = 1        tts.say("Досвидания")        tts.runAndWait()        tts.say(name)        tts.runAndWait()        tts.say("До новых встреч. Идет завершение работы.")        tts.runAndWait()        os.system('cls' if os.name == 'nt' else 'clear')        print("Выключение через 10 секунд.")        time.sleep(1)        os.system('cls' if os.name == 'nt' else 'clear')        print("Выключение через 09 секунд.")        time.sleep(1)        os.system('cls' if os.name == 'nt' else 'clear')        print("Выключение через 08 секунд.")        time.sleep(1)        os.system('cls' if os.name == 'nt' else 'clear')        print("Выключение через 07 секунд.")        time.sleep(1)        os.system('cls' if os.name == 'nt' else 'clear')        print("Выключение через 06 секунд.")        time.sleep(1)        os.system('cls' if os.name == 'nt' else 'clear')        print("Выключение через 05 секунд.")        time.sleep(1)        os.system('cls' if os.name == 'nt' else 'clear')        print("Выключение через 04 секунд.")        time.sleep(1)        os.system('cls' if os.name == 'nt' else 'clear')        print("Выключение через 03 секунд.")        time.sleep(1)        os.system('cls' if os.name == 'nt' else 'clear')        print("Выключение через 02 секунд.")        time.sleep(1)        os.system('cls' if os.name == 'nt' else 'clear')        print("Выключение через 01 секунд.")        time.sleep(1)        os.system('cls' if os.name == 'nt' else 'clear')        print("Выключение...")        time.sleep(1)        os.system('shutdown -s')

Заметки. Ассистент умеет хранить заметки, вот какой код работает для этого:

#память        elif "запомни" in an or "напомни" in an:        ca = 1        f = open("z1.txt", "a")        if an[len("запомни"):] == "":            tts.say("Заметка не может быть пустой! Если хотите создать новую, скажите запомни и то что вы хотите сохранить.")            tts.runAndWait()            print("Ошибка! Вы пытаетесь создать пустую заметку!")        else:            tts.say("Я запомнил, что бы прочитать эту заметку скажите, что ты помнишь?")            tts.runAndWait()            an45 = an[len("запомни"):] + ","            f.write(an45)            f.close()    elif "помн" in an:        ca = 1        f = open("z1.txt", "r")        if f.read(1) == "":            tts.say("Похоже у вас еще нет заметок. Если хотите создать новую, скажите запомни и то что вы хотите сохранить.")            tts.runAndWait()        else:            st = f.read()            print(st)            tts.say("И так вот что я помню:")            tts.runAndWait()            tts.say(st)            tts.runAndWait()            tts.say("Если хотите их удалить, скажите удалить все заметки.")            tts.runAndWait()            f.close()    elif "удалить все заметки" in an or "удали все заметки" in an:        ca = 1        print("Вы уверены?")        tts.say("Вы хотите удалить все заметки? Подтвердите пожайлуста.")        tts.runAndWait()        #распознание        r = sr.Recognizer()        with sr.Microphone(device_index = 1) as source:            print(' ')            r.adjust_for_ambient_noise(source, duration=0.5) #настройка посторонних шумов            print('...')            audio = r.listen(source)            print(' ')        try:            query = r.recognize_google(audio, language = 'ru-RU')            an = query.lower()            print(f'Вы сказали: {query.lower()}')                    except:            print('-')        if an == "да" or "подтверждаю" in an or "утверждаю" in an:            ca = 1            print("Удаление...")            f = open("z1.txt", "w")            f.write("")            tts.say("Удаление заметок завершено.")            tts.runAndWait()        else:            print("Отмена...")            tts.say("Подтверждение не получено, заметки не удалены. Ну вы меня и напугали...")            tts.runAndWait()        f.close()

Еще пара полезных функций:

elif "настройки" in an:ca = 1        tts1 = input("введите номер голоса:")        f = open("tts.txt", "w")        f.write(tts1)        f.close() elif "замолчи" in an or "стоп" in an:        ca = 1        tts.say("Хорошо, микрофон выключен. Для продолжения работы нажмите энтр")        tts.runAndWait()        an4925479864 = input("[ПАУЗА] Нажмите enter: ")        tts.say("Привет-привет, чем займемся?.")        tts.runAndWait()

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

elif "виталий активируй диалоги" in an:        ca = 1        tts.say("Возможность диалогов активирована, О чём поговорим?")        tts.runAndWait()        f = open('dialogset.txt', 'w')        f.write("1")        f.close()    elif "виталий выключи диалоги" in an:        ca = 1        tts.say("Возможность диалогов отключена.")        tts.runAndWait()        f = open('dialogset.txt', 'w')        f.write("0")        f.close()    f = open('dialogset.txt', 'r')    an4897987 = f.read(1)    f.close()        if an4897987 == "1":        rsm = random.randint(1,3)        if "привет" in an or "здрав" in an:            ca = 1            if rsm == 1:                tts.say("Привет, чем могу пом очь?.")                tts.runAndWait()            elif rsm == 2:                tts.say("Добрый день.")                tts.runAndWait()            elif rsm == 3:                tts.say("Хэлоу.")                tts.runAndWait()         #пример фразы смол толк`а        elif "в1" in an or "в2" in an:            ca = 1            if rsm == 1:                tts.say("1в ответа")                tts.runAndWait()            elif rsm == 2:                tts.say("2в ответа.")                tts.runAndWait()            elif rsm == 3:                tts.say("3в ответа")                tts.runAndWait()                elif "как" in an and "дел" in an:            ca = 1            if rsm == 1:                tts.say("Как сказала-бы Алиса, у меня всё хорошо, но немного одиноко, обращайтесь ко мне по-чаще.")                tts.runAndWait()            elif rsm == 2:                tts.say("У меня прекрасно! Заходил на официальный канал проекта. Там очень интересно. Хотите посмотреть?")                tts.runAndWait()                cont = "ютубпр"                cikl = 0            elif rsm == 3:                tts.say("У меня всё прекрасно! А у вас?")                tts.runAndWait()           elif "хорошо" in an or "прекрасно" in an:            ca = 1            if rsm == 1:                tts.say("Я рад за вас, чем займемся?")                tts.runAndWait()            elif rsm == 2:                tts.say("Я рад за вас, у меня тоже всё хорошо.")                tts.runAndWait()            elif rsm == 3:                tts.say("Я рад за вас.")                tts.runAndWait()

А если ошибка? Или, что бот будет делать в случае, если не найдет ответа? Вот что. Кстати ассистент понимает, что не выполнил за цикл ни одной команды, если переменная ca = 0.

if an == "":            print("")            ca = 1    if ca == 0:        print("ошибка")        if ra == 1 or ra ==2:            tts.say("Ну наверное...")            tts.runAndWait()        elif ra == 3 or ra == 4:            tts.say("Даже не знаю.")            tts.runAndWait()        elif ra == 5 or ra == 6:            tts.say("Незнаю что на это ответить.")            tts.runAndWait()        elif ra == 7 or ra == 8:            tts.say("Я вас не то что бы понял, но по смыслу понял")            tts.runAndWait()        elif ra == 9 or ra == 10:            tts.say("Наверное я вас не правильно понял.")            tts.runAndWait()

Осталось только "обнулить", нет не этого, а переменный, что бы не было повторных сработок:

    an = ""    if cikl == 2:        cikl = 0        cont = ""

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

Вот ссылка на готовую и скомпилированную версию.

Жду ваших советов, идей и критики в комментариях под постом и на почте: glebilic@gmail.com.

Пока!

Подробнее..

Как быстро получить много данных от Битрикс24 через REST API

17.01.2021 12:19:01 | Автор: admin

Нередко при работе с Bitrix24 REST API возникает необходимость быстро получить содержимое определенных полей всех элементов какого-то списка (например, лидов). Традиционный способ для этого - обращение к серверу через метод *.list (например, crm.lead.list для лидов) с параметром select, перечисляющим список требуемых полей.

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

Стратегии

Ниже мы описываем три стратегии, которые мы условно назвали "ID filter", "Start increment' и "List + get".

Первые две стратегии ("ID filter" и "Start increment") предложены в официальной документации Битрикс24, но мы ниже предлагаем их "докрутить".

ID filter

Запросы отправляются к серверу последовательно с параметром "order": {"ID": "ASC"} (сортировка по возрастанию ID), и в каждом последующем запросе используются результаты предыдущего (фильтрация по ID, где ID > максимального ID в результатах предыдущего запроса).

При этом для ускорения используется параметр start = -1 для отключения затратной по времени операции расчета общего количества записей (поле total), которое по умолчанию возвращается в каждом ответе сервера при вызове методов вида *.list.

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

Start increment

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

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

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

Объединение запросов в батчи

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

Параллельная отправка батчей к серверу

Примеры кода в официальной документации Битрикс24 REST API везде предлагают последовательную отправку запросов и описывают лишь ограничения на скорость отправки запросов. Но параллельная отправка запросов возможна и позволяет сильно ускорить обмен информацией с сервером.

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

Именно такая стратегия сейчас заложена в метод get_all() в питоновской библиотеке fast_bitrix24 (пиарюсь - библиотеку написал я).

List + get

Составная стратегия, при которой при помощи стратегии "Start increment" от сервера получается сначала список всех ID по методу *.list (с указанием, что нужны только ID - 'select': ['ID']) , а потом через метод *.get получается содержимое всех полей для каждого ID. При этом в обоих шагах используются описанные выше способы ускорения "Объединение запросов в батчи" и "Параллельная отправка батчей".

Тест

Чтобы проверить эффективность этих стратегий, мы провели тест (код теста).

Тест запрашивает страницы лидов (метод crm.lead.list) через 3 вышеописанные стратегии (при этом стратегия "ID filter" реализована в один поток - с начала списка ID). Для каждой стратегии запрашиваются 1, 50, 100 и 200 страниц и замеряется время выполнения запроса.

Тест использует библиотеку fast_bitrix24 для автоматического контроля скорости запросов к серверу Битрикс24.

Тест проводим на 7-й версии REST API на списке в ~35000 лидов.

Результаты теста

Getting 1 pages:ID filter: 0.3 sec.Start increment: 0.73 sec.Getting ID list for the 'list+get' strategy, method crm.lead: 2.17 sec.List + get: 2.61 sec.Getting 50 pages:ID filter: 12.8 sec.Start increment: 21.39 sec.List + get: 1.84 sec.Getting 100 pages:ID filter: 49.67 sec.Start increment: 39.97 sec.List + get: 3.28 sec.Getting 200 pages:ID filter: 99.67 sec.Start increment: 78.05 sec.List + get: 6.36 sec.

Выводы

В целом, стратегии, использующие батчи и параллельные запросы ("Start increment" и "List + get"), показали себя лучше.

Однако при этом, к моему удивлению, стратегия "List + get" оказалась на порядок продуктивнее остальных, даже несмотря на то, что в ней приходится пробегаться по всему списку два раза. (Возможно, эту статью увидят разработчики Битрикс24 и объяснят этот феномен?)

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

Подробнее..

Распознавание Ворониных на фотографиях от концепции к делу

17.01.2021 16:22:22 | Автор: admin

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

Откуда родилась задача

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

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

Что будем использовать

Первым шагом к решению проблемы стал выбор инструментария. После изучения нескольких популярных решений и подходов, выбор пал на библиотеку face-recognition. О других реализациях распознавания нестандартных объектов постараюсь рассказать в следующих статьях (дайте знать, если тема распознавания грибов на фото трогает вашу душу).

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

Устанавливается face-recognition довольно легко, единственная важная оговорка - пользователям Mac и Linux (в моем случае была Ubuntu 20.04) необходимо вручную установить dlib. Процесс тоже довольно подробно описан в документации.

Немного о том, как все работает

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

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

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

От слов к реализации

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

На серверной стороне python с fast api. Эта давно зарекомендовавшая себя пара и тут продемонстрировала все свои красоту и удобство.

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

Далее мы загружаем в систему все наши тестовые фотографии, чтобы выделить кодировки лиц:

kostik = face_recognition.load_image_file("samples/kostik.jpg")kostik_encoding = face_recognition.face_encodings(kostik)[0]vera = face_recognition.load_image_file("samples/vera.jpg")vera_encoding = face_recognition.face_encodings(vera)[0]

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

def start_comparing(encoding):compare_result = face_recognition.compare_faces([kostik_encoding], encoding)if compare_result[0]:return "Костик"else:return compare_vera(encoding)def compare_vera(encoding):compare_result = face_recognition.compare_faces([vera_encoding], encoding)if compare_result[0]:return "Вера"else:return compare_nikolay(encoding)

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

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

Ну и в конце нужно реализовать обычный API-поинт, который умел бы принимать фото и возвращать результат:

@app.post("/recognize/")def create_file(file: UploadFile = File(...)):with open("samples/test.jpeg", "wb") as buffer:    shutil.copyfileobj(file.file, buffer)unknown_image = face_recognition.load_image_file("samples/test.jpeg")try:unknown_encoding = face_recognition.face_encodings(unknown_image)[0]result = start_comparing(unknown_encoding)except Exception as e:result = "Тут нет лица"else:passfinally:passreturn {"filename": file.filename, "result": result}

Получаем файл изображения, сохраняем его, кодируем и запускаем алгоритм сравнения. Если вы все сделали правильно, не забыли про CORS и написали веб-приложение (или использовали мое, с 2 формами), то теперь у вас готовая система, которая умеет отличать Галю от Лени. Итоговое решение можно найти тут. Если вдруг кто-то заинтересуется и будет скучать томными вечерами, то присылайте пулл-реквесты - буду рад!

Выводы

Теперь немного выводов:

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

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

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

  • Навык импорта библиотеки в python и последующее использование ее методов не делает из программиста Senior Data Sciene Engineer. Область искусственного интеллекта куда как сложнее и глубже.

  • Математику все же забывать не стоит :)

Подробнее..

Категории

Последние комментарии

© 2006-2021, personeltest.ru