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

Алгоритмы

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

16.09.2020 18:22:11 | Автор: admin
Привет, Хабр! Сегодня я хочу познакомить вас с Андреем Чумаченко, руководителем сообщества по спортивному программированию в Иркутске и титулованным участником соревнований по программированию, в том числе ICPC и Всесибирской олимпиады имени И.В. Поттосина.

Мы поговорили с Андреем про спортивное программирование, подготовку к соревнованиям и про его работу тренером. Под катом полезные и вредные советы участникам соревнований, вопросы мотивации, истории с соревнований, отношение к ЕГЭ и школе спортивного программирования в Иркутске.


Финал студенческого командного соревнования по программированию ICPC, 2016 год (источник: ICPC Live)

Андрей Чумаченко основатель и руководитель сообщества по программированию в Иркутске, студент магистратуры ИГУ по фундаментальной информатике, призер полуфинала студенческого чемпионата мира по программированию ICPC 2018, 2019 (среди стран СНГ), призер Всесибирской олимпиады имени И.В. Поттосина 2018, 2019 (среди стран СНГ), победитель четвертьфинала студенческого чемпионата мира по программированию (среди студентов Восточной Сибири) 2018, 2019, победитель Универсиады Алтая по программированию 2019 (среди студентов и школьников России), финалист чемпионата БГУИР по программированию 2018, 2019.

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


С чем его едят


Что такое спортивное программирование и какие задачи там сейчас решают?

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

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

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


2016 год, студенты УрФУ только что выиграли международный чемпионат по программированию Challenge 24 в Будапеште. Тогда в десятку лидеров вошли семь команд из России (источник: codeforces.com)

Какой язык сегодня самый популярный в спортивном программировании? Моя подруга в московском Политехе на прикладной информатике (janka2330) изучала спортивное программирование как предмет и сдавала зачет. Говорит, было круто. Они соревновались с однокурсниками и сдавали задачи на spoj.pl (spoj.com), а писали на Ruby on Rails.

Язык сильно зависит от соревнований. Чаще всего я встречаю С++, также популярны Java, Python. Еще новичок Kotlin в последнее время набирает обороты. Ruby on Rails, да и просто Ruby, редко используют, но на некоторых соревнованиях они были в списке поддерживаемых языков. Я сам всегда пишу на C++, и мои ученики тоже. Мне он кажется наиболее удобным, когда надо быстро что-то закодить.


Языки, которые чаще других используют на соревнованиях


Как готовиться, чтобы победить


Расскажите про подготовку к соревнованиям. У вас наверняка есть свои секреты.

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

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

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


Архив олимпиадных задач codeforces.com

Еще заглядываем на acm.timus.ru крупнейший в России архив задач по программированию с автоматической проверяющей системой. На YouTube сейчас много чего появилось, но на постоянной основе мы его пока не используем. Если интересно, могу посоветовать оттуда крутого польского программиста под ником Errichto, у него свой канал, там можно накопать кучу полезностей.


Второй канал польского программиста под ником Errichto на YouTube

Ну и книги, конечно, как без них: Искусство программирования Дональда Кнута, например. Или Олимпиадные задачи по программированию. Руководство по подготовке к соревнованиям Стивена Скиены и Мигеля Ревиллы.

Лайфхаки для участника


Ок, а что может помешать выиграть на олимпиаде по спортивному программированию?

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

Еще очень вредно тренироваться в ночь перед соревнованиями, тем более если не готовился в течение года.

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

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

А чтобы победить, что нужно делать?

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

Когда идти в программисты


Расскажите про свой путь в спортивном программировании.

Я считаю, что довольно поздно стал погружаться в эту тему: только в старших классах школы я серьезно начал изучать C++ и участвовать в олимпиадах, которые проводили университеты Иркутска. Затем, уже поступив в ИГУ, я встретил преподавателя, который поддерживал движение по спортивному программированию, и начал заниматься с ним. Так, потихоньку, спортивное программирование для меня перестало быть просто хобби, я занялся этим серьезно, стал активно участвовать в соревнованиях и дорос до тренера.


Андрей разбирает одну из олимпиадных задач на августовских сборах в Иркутске

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

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

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

Поэтому чем раньше, тем лучше.

ЕГЭ больная тема


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

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

Что же касается ЕГЭ, то это больная тема. Тут я могу наговорить материала на еще одну статью.

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

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

Сейчас в вузы можно поступать и по результатам олимпиад. И это круто для топовых школьников, для 10%. А что делать с остальными? Только ЕГЭ.

Да, есть простые олимпиады третьего уровня из перечня, но они часто еще более несуразны или содержат задачи из ЕГЭ.

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

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

А вы не видите аналогичную шаблонность в оценивании знаний на олимпиадах по программированию? Или там все по-другому?

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

Почему школьники учат спортивное программирование


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

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

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

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

Что вы делаете в рамках своего сообщества спортивного программирования?

Сегодня я тренирую студентов, и мы ездим на олимпиады и соревнования по спортивному программированию. Среди моих учеников призеры четвертьфинала студенческого чемпионата мира по программированию (среди студентов Восточной Сибири) 2019 года, призеры сибирской площадки полуфинала студенческого чемпионата мира по программированию ICPC 2019 года, призеры Универсиады Алтая по программированию 2019, финалисты олимпиады имени Поттосина 2018 и 2019 годов.

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

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

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


Августовский интенсив по спортивному программированию в иркутской Точке

Был у меня такой случай


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

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

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

Поделитесь интересными историями с соревнований.

Все наши забавные истории обычно связаны с задачами. Например, я однажды долго мучился над решением, потому что не заметил, что фразу no solution нужно было вывести с переставленными в одном месте буквами no soluiton.

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



Минутка рекламы про наш акселератор AI-проектов

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

Отбор и программа преакселерационной подготовки бесплатные. А если вы пишете о своем проекте на Хабре вам плюс в отборочный рейтинг! О самых интересных проектах расскажем в нашем блоге.

Подробнее..

Исправление кратных ошибок при кодировании сообщений

04.09.2020 20:08:16 | Автор: admin

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


Теоретические положения


Идея использования организованной избыточности в сообщениях привела Р. Хемминга к построению корректирующего кода описанного здесь. Линейный корректирующий (n, k)-код характеризуется проверочной (mn) матрицей H. Требования к матрице просты: число строк совпадает с числом проверочных символов, ее столбцы должны быть отличны от нулевого и все различны. Более того, значения столбцов описывают номера позиций, занимаемых в кодовом слове символами слова, являющимися элементами конечного поля.

Часто для установления ошибочности переданного слова декодер использует вычисление синдрома, вычисляемого для этого слова. Синдром равен сумме столбцов этой матрицы, умноженных на компоненты вектора ошибки. Если в H имеется m строк и код позволяет исправлять одиночные ошибки, то длина блока (кодового слова) не превышает $ n2^m-1 $. Важна также выполнимость требуемой удаленности кодовых слов друг от друга.

Коды Хемминга достигают этой границы. Каждая позиция кодового слова кода Хемминга может быть занумерована двоичным вектором, совпадающим с соответствующим столбцом матрицы H. При этом синдром будет совпадать непосредственно с номером позиции, в которой произошла ошибка (если она только одна) или с двоичной суммой номеров (если ошибок несколько).
Идея векторной нумерации весьма плодотворна. Далее будем полагать $ n2^m-1 $ и, что i-я позиция слова занумерована числом i.

Нумерацию в двоичном виде,(т.е. такое представление) называют локатором позиции. Допустим, что требуется исправлять все двойные и одиночные ошибки. Видимо, для этого потребуется большая избыточность кода, т.е. матрица H должна иметь больше строк (вдвое большое число). Поэтому будем формировать матрицу H с 2m строками и с $ n2^m-1 $ столбцами, и эти столбцы разумно выбирать различными. В качестве первых m строк будем брать прежнюю матрицу кода Хемминга. Это базисные векторы-слова пространства слов.

Пример 1. Пусть m = 5 и n = 31. Желательно было бы получить (n, k)-код, исправляющий двойные ошибки, с проверочной матрицей Н в виде:

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

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

Пример 2 .
00000 0
00010 1
00011 х

$11011 х^4 +х^3+х+1$
Сумма и разность таких многочленов соответствует сумме и разности векторов:
0 0 = 0, 0 1 = 1, 1 0 = 1, 1 1 = 0, знаки имеют в двоичном случае совпадающий смысл. Не так с умножением, показатель степени результата умножения может превысить 4.

Пример 3 .
$(х^3+х+1)х^4+х^3+х+1)=х^7+х^6+х^5+3х^4+2х^3+х^2+2х+1=$
$=х^7+х^6+х^5+х^4+х^2+1$.

Необходим метод понижения степеней больше 4.
Он называется (редукцией) построением вычетов по модулю неприводимого многочлена M(x) степени 5; метод состоит в переходе от многочленов произведений к их остаткам от деления на $M(x)=x^5+x^2 + 1$


Так что
$х^7+х^6+х^5+х^4+х^2+1 =(х^2+х + 1)(х^5 +х^2 +1) +х^3 +х^2 +х$ или $х^7+х^6+х^5+х^4+х^2+1 (х^3+х^2 + x)mod(х^5 +х^2 +1).$

Символ читается сравнимо с.
В общем виде A(x) a(x)mod M(x)
Тогда и только тогда, когда существует такой многочлен C(x), что
A(x)= M(x)C(x) +a(x) коэффициенты многочленов приводятся по модулю два:
A(x) a(x)mod(2,M(x)).

Важные свойства сравнений


Если а(x) А(x)mod M(x) и b(x) B(x)mod M(x), то
а(x) b(x) А(x) B(x)mod M(x) и
а(x)b(x) А(x)B(x)mod M(x).

Более того, если степени многочленов а(х) и А(х) меньше степени М(х), то из формулы
а(x) А(x)mod(2,M(x)) следует, что а(x) = А(x).
Различных классов вычетов существует 2 в степени degM(x) т.е. столько, сколько существует различных многочленов степени, меньшей m, т.е. сколько может быть различных остатков при делении. С делением еще больше сложностей.

Алгоритм деления


Для чисел.
Для данных a и M существуют однозначно определенные числа q и A, такие, что а =qM + A, 0 A M,
Для многочленов с коэффициентами из данного поля.
Для данных a(x) и M(x) существуют однозначно определенные многочлены q(x) и A(x), такие, что a(x) = q(x)M(x) + A(x), degA(x) <deg M(x).
Возможность деления многочленов обеспечивается алгоритмом Евклида.

Для чисел пример расширенного НОД описан здесь
Для заданных a и b существуют такие числа A и B, что aA +bB = (a,b), где (a,b) НОД чисел a и b.
Для многочленов с коэффициентами из данного поля.
Для заданных a(x) и b(x) существуют такие многочлены A(x) и B(x), что
a(x)A(x) + b(x)B(x) = (a(x), b(x)),
где (a(x), b(x)) нормированный общий делитель a(x) и b(x) наибольшей степени.
Если а(х) и М(х) имеют общий делитель d(x) 1, то деление на a(x) по mod M(x) не всегда возможно.

Очевидно, что деление на a(x) эквивалентно умножению на A(x).
Так как если (a(x), b(x))= 1 =НОД, то согласно алгоритму Евклида, существуют такие A(x) и B(x), что a(x)A(x) + b(x)B(x) = 1, так, что a(x)A(x) 1mod b(x). Проверка того, что двоичный многочлен является неприводимым над полем GF ($2^5$), выполняется непосредственным делением на всевозможные делители со степенями, меньшими, чем deg M(x).

Пример 4. $M(x) = x^5+x^2 +1$ делим на х и на (х + 1)
на линейные делители. Результат деления не нуль. Делим на квадратные делители $х^2 , х^2 +х=х(х +1),х^2 +1, х^2 +2х +1= (х +1)^2, х^2 + х + 1.$. Они выдают остатки, не равные нулю. Делителей степени 3 не существует, так как их произведение дает степень 6.
Таким образом, многочлены можно складывать, вычитать, умножать и делить по модулю $M(x) = x^5+x^2 +1$.

Переходим к поиску функции для проверочной матрицы H, задающей код с исправлением двойных ошибок с блоковой длиной 31 и скоростью 21/31; 31-21=10 =2t проверочных символов = 10. Такая функция должна иметь своими корнями номера ошибочных позиций в кодовом слове, т.е. при подстановке в эту функцию номеров позиций, обращает ее в нуль.

Поиск функции


Предположим, что 1 и 2 номера искаженных символов (позиций) слова. Используя двоичную запись чисел 1 и 2 можно представить эти номера в виде классов вычетов по модулю M(x) т.е. установить соответствие i (i)(x) двоичные многочлены степени < 5.

Первые 5 проверочных условий определяют 1 + 2; второе множество проверочных уравнений должны определять f(1) + f(2).
Декодер должен определить 1 и 2 по заданной системе:

Какой же должна быть функция f(x)?

Простейшая функция это умножение на константу f() (х)modM(x).
Но тогда 2 = 1, т.е. уравнения системы зависимы. Новая пятерка проверочных условий декодеру не даст ничего нового.
Аналогично и функция f() = + не изменяет ситуацию, так как 2 = 1.
Пробуем степенные функции: сначала возьмем $f() = ^2$. При этом


Эти уравнения также зависимы, так как

$$display$$1^2 =(1 + 2)^2=1^2 + 212 + 2^2 = 1^2 + 2^2 =2$$display$$


Таким образом, второе уравнение является квадратом первого.
Пробуем $f() = ^3$. Уравнения декодера меняют вид:
Откуда $inline$2 =1^3 + 2^3=(1 + 2)(1^2 -12+ 2^2)=1(12-1^2).$inline$

Так что при 10 имеем

Значит, 1 и 2 удовлетворяют уравнению

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

Так как в поле двоичных многочленов по модулю M(x) данное уравнение имеет точно 2 корня, то декодер всегда сможет найти два нужных локатора.
Если произошла только одна ошибка, то 1=1 и $1^2 = 2$. Следовательно, в этом случае единственная ошибка удовлетворяет уравнению + 1 = 0 или 1+ 1-1= 0.

Наконец, декодер всегда производит декодирование, если ошибок не произошло, то в этом случае 1 + 2 = 0 .
Более удобно (на практике) оперировать не непосредственно с многочленом, корнями которого являются локаторы ошибок, а с многочленом, корни которого взаимны к локаторам; т.е. являются к ним мультипликативным обратными величинами.
Ясно, что при не более чем двух ошибках декодер может определить номера ошибок. Если же искажаются три или более символов, то произойдет ошибка декодирования или отказ от декодирования.
Таким образом, функция $f(x) = x^3$ подходит для построения нижних пяти строк проверочной матрицы Н двоичного кода с длиной кодовых слов 31 и 10-ю проверочными символами, исправляющего все двойные ошибки.
Первые пять проверок задают сумму номеров ошибок (S1); вторые пять проверок задают сумму кубов номеров ошибок (S3).
Процедура декодирования состоит из трех основных шагов:
1. каждое полученное кодовое слово проверяется и вычисляются S1 и S3;
2. находится многочлен локаторов ошибок от (z);
3. вычисляются взаимные величины для корней (z) и изменяются символы в соответствующих позициях полученного слова.
Подробнее..

Самописная криптуха. Vulnerable by design

08.09.2020 12:21:09 | Автор: admin

Автор: Иннокентий Сенновский (rumata888)


Как заинтересовать студента скучным предметом? Придать учебе форму игры. Довольно давно кто-то придумал такую игру в области безопасности Capture the Flag, или CTF. Так ленивым студентам было интереснее изучать, как реверсить программы, куда лучше вставить кавычки и почему проприетарное шифрование это как с разбегу прыгнуть на грабли.


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


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


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


Финальный scoreboard CTFZone 2020


Финальный scoreboard CTFZone 2020


Содержание


  1. Необязательное введение: объясняем CTF за 2 минуты
  2. Как мы поняли, что нам нужна крипта
  3. Как мы выбрали стек
  4. Как мы нашли идею для задания
    4.1. А. Что такое Гамильтоновость графов
    4.2. Б. Как идентифицировать себя с помощью Гамильтонова цикла
  5. Как мы построили задание
    5.1. Протокол: вид сверху
    5.2. Протокол: внутренности
    5.3. Последняя уязвимость
  6. Как мы разрабатывали таск
  7. Как мы тестировали таск
  8. Как мы боролись с читерством
  9. Заключение

Необязательное введение: объясняем CTF за 2 минуты


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


Различают два вида турниров: jeopardy и attack-defense.


Турниры jeopardy проходят онлайн. Он устроены по формату американской телевикторины Jeopardy!, в российской версии известной как Своя игра. Участникам предлагают разнообразные задания, за выполнение которых начисляют очки. Чем сложнее задание, тем дороже правильный ответ. Выигрывает команда, у которой больше всего очков.


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


На старте соревнования AD команды получают vulnboxes виртуальные машины или хосты с уязвимыми сервисами, которые необходимо защищать. У всех команд vulnboxes одинаковые. Задача участников защитить свои хосты, при этом сохранить их доступность для сервера проверки (checker). То есть нельзя обеспечить безопасность хоста, просто закрыв к нему доступ.


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


Итак, перед каждой командой стоят следующие цели:


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

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


  • web,
  • pwn,
  • misc,
  • PPC,
  • forensic,
  • reverse,
  • crypto (то есть криптография, а не криптовалюта).

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


Как мы поняли, что нам нужна крипта


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


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


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


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


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


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


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


Как мы выбрали стек


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


Мы подумали, что для квалификационного этапа DEF CON это несерьезно, поэтому для нашего стека решили использовать Python + C (звучит убийственно, знаю). Большая часть функциональности была реализована на языке C, тогда как Python обеспечивал удобную обертку для манипуляций с сокетами, а также функциональность сервера.


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


Как мы нашли идею для задания


Мы не хотели, чтобы в задании было много уязвимостей, известных любому участнику CTF, поэтому решили остановиться на менее стандартном решении Zero Knowledge Proofs of Knowledge (ZKPoK), то есть протоколе доказательства с нулевым разглашением. Идея заключается в том, чтобы доказать, что вам известна какая-либо секретная информация, не раскрывая эту информацию. Было решено использовать ZKPoK в качестве схемы идентификации: если сервер проверки сможет что-либо доказать, он получает флаг. В основу нашей схемы была положена гамильтоновость графов.


А. Что такое Гамильтоновость графов


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


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


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




Но сначала немного вводной информации.


Мы решили представить наши графы с помощью матриц смежности. Это квадратные матрицы, в которых две вершины, соединенные ребром, обозначаются единицей в соответствующей ячейке, и нулем, если они не соединяются. Например, имеется 4 вершины: A, B, C, D. A соединена с B, C соединена с D.


Матрица будет выглядеть следующим образом:
Матрица


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


Итак, если вы создаете граф с известным Гамильтоновым циклом, первым делом нужно перемешать массив вершин: например, ABCD BADC. Из этого массива вы определяете циклический порядок следования вершин (контур): BADCB.




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


Матрица


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


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


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


Матрица смежности


Как видно, каждому ребру (x, y) соответствует (y, x), и я добавил ребро между B и D.


Б. Как идентифицировать себя с помощью Гамильтонова цикла


Схема простая:


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

Часть с доказательством проходит в три этапа, не считая подготовительного, при этом мы принимаем роль Доказывающего (Prover), а сервер выполняет роль Проверяющего (Verifier).


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


Умножение и транспонирование матриц


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


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


Обязательства должны обладать двумя важными свойствами:


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

2. Этап вызова. После того как Доказывающий направил свои обязательства, Проверяющий произвольно выбирает бит вызова (challenge) $b \in \{0,1\}$ и отправляет Доказывающему. В ответ на это Доказывающий направляет информацию, в которой открывается или первый (если $b=0$) или второй (если $b=1$) контейнер обязательства.


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


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


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


Последнее можно легко доказать путем моделирования. Представим, что мы как Доказывающий можем обратить часть этапов в протоколе и изменить обязательство уже после получения бита вызова $b$. Зная $b$, мы всегда можем обмануть Проверяющего: если $b=0$, сгенерировать случайную перестановку и изоморфный граф на ее основе, а если $b=1$, сгенерировать граф с новым гамильтоновым циклом. Далее мы принимаем обязательства в отношении первого или второго согласно известному $b$, Проверяющий выпускает такой же бит вызова еще раз и проверяет открытое обязательство. Так как в этом случае доказательства были сгенерированы не на основе гамильтонового цикла на изначальном графе, Проверяющий не может выжать какую-либо информацию из доказательства (в случае с реальным Доказывающим, то есть не в ходе моделирования).


Как вы, наверное, поняли, всегда есть 50-процентная вероятность, что злонамеренный Доказывающий успешно проведет атаку, не имея информации об исходном цикле. Если это выполнить только один раз, о безопасности не будет и речи. Но такая последовательность и не предполагает разового выполнения. Цель состоит в том, чтобы Проверяющий и Доказывающий проделали все этапы несколько раз. При одном повторе вероятность обмана составит 25%, при двух уже 12,5% и т. д. Всегда можно задать количество повторов, которые вписываются в ваши границы риска.


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


P.S. Если вы мало что поняли из сказанного выше или хотите больше узнать о Zero Knowledge, почитайте блог д-ра Мэтью Грина Zero Knowledge Proofs: An illustrated primer. Он объясняет концепцию куда понятнее меня.


Как мы построили задание


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


Протокол: вид сверху


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


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

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


Кстати, здесь и была заложена первая ошибка: флаг таким способом нельзя было украсть, но можно было произвести DoS-атаку на команду противника. Мы выбрали схему подписи PKCS#1 v1.5. Она в целом не является уязвимой, за исключением некоторых ее реализаций, которые могут быть уязвимы к атаке Блейхенбахера на подпись с экспонентой 3 (Bleichenbacher's e=3 signature attack). Чтобы создать возможности для проведения такой атаки, мы выбрали значение 3 для открытой экспоненты открытого ключа и реализовали уязвимый алгоритм снятия дополнения (очевидно, что SAFE_VERSION macro не был определен):


 uint8_t* badPKCSUnpadHash(uint8_t* pDecryptedSignature, uint32_t dsSize){    uint32_t i;    if (dsSize<MIN_PKCS_SIG_SIZE) return NULL;    if ((pDecryptedSignature[0]!=0)||(pDecryptedSignature[1]!=1))return NULL;    i=2;    while ((i<dsSize) && (pDecryptedSignature[i]==0xff)) i=i+1;    if (i==2 || i>=dsSize) return NULL;    if (pDecryptedSignature[i]!=0) return NULL;    i=i+1;    if ((i>=dsSize)||((dsSize-i)<SHA256_SIZE)) return NULL;    #ifdef SAFE_VERSION    //Check that there are no bytes left, apart from hash itself    //(We presume that the caller did not truncate the signature afteк exponentiation    // and the dsSize is the equal to modulus size in bytes    if ((dsSize-i)!=SHA256_SIZE) return NULL;    #endif    return pDecryptedSignature+i;}

После настройки графа сервер проверки каждые 30 секунд делал следующее:


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

Предполагалось, что в случае верного доказательства сервер команды отдает флаг.


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


Протокол: внутренности


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


Наш языковой стек Python + C. Мы создали библиотеку C, содержащую 95% функциональности приложения. Затем мы создали классы поддержки на Python для Доказывающего и Проверяющего. Они включали указатели на соответствующие структуры и обертывали вызовы библиотеки. (Кстати, будьте внимательны при работе с void_p в ctypes. В 64-битных системах он может быть урезан до 32 бит при передаче в качестве аргумента в функцию).


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


verifier=Verifier(4,4,7)

Аргументы были следующие:


  1. Желательное количество вершин в графе.
  2. Количество одновременных доказательств.
  3. Выбранные алгоритмы обязательств.

Разберем их по порядку.


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


Что касается максимально разрешенного количества вершин, то мы остановились на 256. Не стали увеличивать его по двум причинам:


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

Количество одновременных доказательств. Как и в случае с вершинами, четырех параллельных доказательств недостаточно, чтобы говорить о какой-то безопасности. Вероятность правильного угадывания битов доказательства составляет $\frac{1}{16}$. Можно пробовать несколько раз и после ряда попыток все получится.


Максимальное значение, поддерживаемое функцией инициализации, составляло 64, что означало вероятность читерства $\frac{1}{2^{64}}$. При наших времени раунда и мощностях злоумышленника практически никаких шансов.


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


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


  • обязательства, в основе которых лежат "хеши" CRC32;
  • обязательства, в основе которых лежат хеши SHA-256;
  • обязательства, в основе которых лежит шифрование при помощи AES-128 в режиме CBC.

Правильными значениями для данной битовой маски будут $1-7$, так что командам необходимо выбрать хотя бы один алгоритм. Сервер проверки выбирает их в следующей последовательности: CRC32, SHA-256, AES. Так, если доступны CRC32 и AES, приоритетным будет CRC32.


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


  1. Доказывающий запрашивает конфигурацию доказательства у Проверяющего (количество доказательств и поддерживаемые алгоритмы).
  2. Проверяющий в ответ направляет конфигурацию.
  3. Доказывающий создает доказательства, обязательства из доказательств и отправляет 1. Доказывающему обязательства по количеству доказательств proof_count.
  4. Проверяющий направляет Доказывающему вызов (случайные биты в количестве proof_count).
  5. В ответ Доказывающий направляет информацию, которая раскрывает обязательства в соответствии с вызовом.
  6. Проверяющий проверяет корректность доказательств и, если все верно, возвращает флаг.
  7. Доказывающий на этом останавливается либо начинает заново с шага 3.

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


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


В случае с обязательствами CRC32 и SHA-256 перестановка, графы с перестановленными вершинами и перестановленные циклические матрицы упаковываются. Упаковывание состоит в том, что размер квадратной матрицы размещается в пределах одного двухбайтного слова (uint16_t), а битовый поток плотно упаковывается в байты таким образом, чтобы каждый байт содержал 8 бит из ячеек исходного представления матрицы. После того, как матрицы упакованы, к каждому упакованному массиву применяется выбранная хеш-функция. В итоге на одно доказательство приходится три хеша:


$Hash(Pack(permutation)) | Hash(Pack(permuted\_graph)) | Hash(Pack(permuted\_cycle))$


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


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


$Enc(Pack(permutation),K_1) | Enc(Pack(permuted\_cycle),K_2) | Pack(permuted\_graph)$


После получения бита вызова $b$ от Проверяющего Доказывающий направляет ему $K_b$. Проверяющий дешифрует соответствующий зашифрованный текст и проверяет достоверность доказательства.


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


CRC32, однако, не в полной мере удовлетворяет условиям скрытия, и это четвертая ошибка. Для поиска противоречия можно подобрать CRC32 с ориентировочным уровнем сложности $2^{32}$. Но его можно обнаружить еще быстрее при помощи подхода Meet-in-the-Middle (Встреча посередине), ориентировочный уровень сложности которого составляет $2^{17}$.
Обновление: Как мне подсказали, можно еще быстрее. Пример здесь. Но узнать про MitM всё равно полезно, т.к. этот метод можно применить и в других местах.


Атака Meet-in-the-Middle может быть использована, потому что можно обратить раунды CRC32. Допустим, у нас есть


$y=CRC32(x_0)$


и надо найти некое $x_1$ таким образом, чтобы


$CRC32(x_1)=y$


Давайте зафиксируем длину искомого $x_1$, пусть она будет 6 байтов (позже это нам пригодится). CRC32 состоит из трех фаз:


  1. Инициализация $Init$.
  2. Цикл прохода по байтам сообщения $t_{i+1}=Round(t_i,b_i)$, где ($t_i$ внутреннее состояние, а $b_i$ байт).
  3. Постпроцессинг $Finish$.

Итак, для 6 байтов:


$CRC32_6(x)=Finish(Round(Round(Round(Round(Round(Round(Init()\\,b_1),b_2),b_3),b_4),b_5),b_6))$


или произведение функций от $t$:
Произведение функций от t
Теперь можно разбить $CRC32_6$ на две части:


$CRC32_6=CRC32_{FH}CRC32_{SH}$


где


$CRC32_{FH}=InitRound_{b_1}Round_{b_2}Round_{b_3}$


$CRC32_{SH}=Round_{b_4}Round_{b_5}Round_{b_6}Finish$


Мы разделили CRC32 пополам. Более того, функции $Round_{b_i}$ и $Finish$ являются обратимыми, что делает $CRC32_{SH}$ тоже обратимой:


$CRC32_{SH\_INV}=CRC32^{1}_{SH}$


Теперь вместо подбора $CRC32_6$ мы:


  1. Рассчитываем около $2^{17}$ значений $CRC32_{FH}$ с различными $b_1b_2b_3$ и помещаем эти значения в хеш-таблицу, чтобы можно было посмотреть результаты $b_1b_2b_3$ для каждого значения за константное время.
  2. Рассчитываем значения $CRC32_{SH\_INV}(y)$ для различных $b_4b_5b_6$. После расчета каждого значения проверяем, отражено ли оно в хеш-таблице. Как только мы найдем одно из них, прекращаем процесс. Шанс найти одно значение при каждой попытке находится в диапазоне $\frac{1}{2^{16}}-\frac{1}{2^{15}}$.
  3. Получаем значение: $t=CRC32_{FH}(b_1,b_2,b_3)=CRC32_{SH\_INV}(y,b_6,b_5,b_4)$, и это означает, что $y=CRC32(b_1b_2b_3b_4b_5b_6)$, и мы обнаружили коллизию.

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


$SIZE (2\space bytes)\space |\space PACKED\_DATA$


Очень важно, что при проверке HASHES Проверяющий хеширует весь этот объект но, когда матрицы распаковываются, алгоритм принимает только биты $size^2$ из $packed\_data$, и все лишнее отбрасывается. Итак, мы можем добавить еще 6 байтов:


$ SIZE (2\space bytes)\space |\space PACKED\_DATA \space|\space e_1\space|\space e_2\space|\space e_3\space|\space e_4\space|\space e_5\space|\space e_6$


И произвести такую же атаку, но теперь $CRC32_{FH}$ будет обрабатывать


$ SIZE (2\space bytes) \space |\space PACKED\_DATA \space|\space e_1\space|\space e_2\space|\space e_3$


тогда как $CRC32_{SH\_INV}$ будет обрабатывать


$e_4\space|\space e_5\space|\space e_6$


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


Последняя уязвимость


Мы рассказали о работе всей системы и рассмотрели 4 запланированных уязвимости. Однако была еще и пятая ошибка. Биты вызова, предоставляемые Доказывающим, были из небезопасного источника ГПСЧ (PRNG или генератор псевдослучайных чисел).


Обычно в таких случаях выбор падает на небезопасный примитив языка C rand, но нам хотелось чего-то более интересного. В последние годы было опубликовано несколько исследований о Legendre PRF, поэтому мы решили реализовать его в качестве ГПСЧ вместо избитого Вихря Мерсенна.


Идея, положенная в основу этого ГПСЧ, это поле Галуа $GF(p)$, то есть фактор-модулей простого числа $p$. Каждый элемент этого поля либо является квадратичным вычетом, либо нет. Таким образом, в поле $\frac{(p-1)}{2}$ квадратичных вычетов (если исключить 0) и столько же неквадратичных вычетов.


Если вы случайным образом выбираете какой-либо элемент этого поля, за исключением элемента $0$, вероятность того, что он представляет собой квадратичный вычет, будет равна $\frac{1}{2}$. Довольно легко проверить, является ли данный элемент (исключая нулевой $r0$) квадратичным вычетом или нет. Для этого необходимо вычислить его символ Лежандра. Если для элемента $r$ выполняется равенство $r^\frac{p1}{2}=1\space mod \space p$, тогда он является квадратичным вычетом. Так как вероятность для каждого элемента $r$ представлять собой квадратичный вычет составляет 50%, а квадратичные вычеты соседствуют с неквадратичными в случайном порядке, то можно создать искомый ГПСЧ.


Мы инициализируем ГПСЧ с произвольным элементом $a\in GF(p)$. Алгоритм в Python будет выглядеть следующим образом:


def LegendrePRNG(a,p):    if a==0:        a+=1    while True:        next_bit=pow(a,(p-1)//2,p)        if next_bit==1:            yield 1        else:            yield 0        a=(a+1)%p        if a==0:            a+=1

Мы специально выбрали 32-битный $p$, чтобы команды смогли его использовать в атаке Meet-in-the-Middle. Идея заключается в том, чтобы получить $2^{16}+31$ бит из ГПСЧ, направляя постоянные запросы на вызовы. Как только вы их получите, можно конвертировать этот битовый поток в $2^{16}$ 32-битовых целых чисел, преобразуя каждую последовательность из 32 бит внутри потока в целое число. Неважно, какой порядок использовать big-endian или little-endian, главное использовать один и тот же везде.


Поместите эти целые числа в словарь в виде ключей, а значения будут означать их позиции в битовом потоке. Теперь инициализируйте свой собственный экземпляр Legendre PRNG. Выберите $a=1$ и сгенерируйте 32 псевдослучайных бита при помощи ГПСЧ. Преобразуйте эти биты в целое число и проверьте, есть ли оно в словаре (предполагается, что алгоритмическая сложность поиска будет приближаться к константе).


Если его там нет, измените инициализацию на $a=1+2^{16}$ и повторите. Увеличивайте $a$ шагом $2^{16}$ до тех пор, пока не найдете совпадение. Когда вы его обнаружите, то узнаете, каким было значение внутреннего состояния ГПСЧ Проверяющего несколько шагов назад. Обновите $a$ так, чтобы оно соответствовало текущему внутреннему состоянию ГПСЧ и вы успешно клонировали ГПСЧ Проверяющего. Если вы заранее знаете, какие будут вызовы, то обмануть Проверяющего не составит труда.


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


Итак, в сервисе присутствуют следующие уязвимости:


  1. Атака через подделку подписи Bleichenbachers e=3.
  2. Небезопасные параметры Проверяющего.
  3. Включенный режим симуляции.
  4. Обязательство CRC32.
  5. Небезопасный ГПСЧ.

Как мы разрабатывали таск


Писать задание было довольно весело, так как некоторые задачи пришлось решать нестандартно.


Например, на ранних этапах мы решили, что не будем добавлять никаких зависимостей, за исключением стандартных библиотек. Из-за этого нам пришлось использовать Linux Usermode Kernel CryptoAPI для хеширования и шифрования.


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


Допустим, имеется граф или циклическая матрица $M$ и матрица перестановки $P$. Чтобы найти перестановленную матрицу $M_p$, нужно произвести следующий расчет: $M_p=P^TMP$. Матрицы перестановки это особый случай. В каждом ряду и каждом столбце матрицы перестановки встречается только одна ячейка со значением $1$, во всех остальных ячейках значение равно $0$. То же самое относится к транспонированной матрице перестановки. Давайте умножим такую матрицу $P$ на $M$, $R=PM$.


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


  1. Взять первую строку $P$.
  2. Искать положение $1$, скажем, это будет $j$.
  3. Взять $j$-й ряд $M$ и скопировать в первый ряд $R$.
  4. Повторить эти действия с другими рядами.

Давайте попробуем на примере:


Пример


Сначала мы ищем в первой строке матрицы $P'$ (той, что слева) первую и единственную единицу.


Матрица


Единица находится на второй позиции, считая с нуля. Поэтому берем вторую строку $M'$ и копируем в первую строку итоговой матрицы $R$.


Матрица


Повторяем то же самое со второй строкой $P'$. Ищем единицу.


Матрица


Она на первой позиции, поэтому берем первую строку $M'$ и кладем на место второй строки в итоговой матрице.


Матрица


Так можно легко получить итоговую матрицу.


Итоговая матрица


Поскольку мы знаем, что в каждом ряду значение $1$ встречается только один раз, нам также известно, что только $j$-е значение каждого столбца будет влиять на полученную в результате ячейку. Кроме того, используя memcpy для копирования, мы ускоряем умножение с помощью SIMD, так как memcpy задействует его для более быстрого копирования. Этот метод позволил значительно ускорить вычисления.


Финальная версия сервиса была встроена в докер-контейнер в два этапа. Сначала библиотека Zero Knowledge собиралась отдельно, затем скомпилированная библиотека копировалась в финальную версию контейнера вместе с оберткой на Python, серверным скриптом и открытым ключом сервера проверки в формате PEM. Все символы, за исключением импорта и экспорта, были удалены из библиотеки, чтобы командам пришлось реверсить основную функциональность.


Как мы тестировали таск


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


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


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


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


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


Вот что мы сделали, чтобы снизить этот риск:


  • Во-первых, мы полностью воссоздали взаимодействие между Доказывающим и Проверяющим на языке C со всеми обязательствами, с наименьшим и наибольшим возможным количеством вершин и доказательств. Тест создавался с использованием ASAN (Address Sanitizer), с помощью которого мы нашли несколько утечек.
  • Во-вторых, мы выписали все интерфейсы, которые будут принимать входные данные из недоверенных источников, будь то на стороне Проверяющего или Доказывающего. Для каждого из этих интерфейсов мы сохранили входные значения, получаемые при взаимодействии сервера проверки с сервером команды. Затем мы написали обвязки под Libfuzzer для каждой точки входа, включая всю необходимую инициализацию и очистку.
    Однако невозможно было так просто подготовить нашу библиотеку к фаззингу: при данном виде тестирования все должно быть по максимуму детерминировано. Поэтому мы заменили получение значений из /dev/urandom вызовами к randrand, а также заменили инициализацию ГПСЧ (не Legendre PRF, а основного) на srand(0). Также было очевидно, что в ходе фаззинга нельзя получить правильный хеш или шифротекст с корректными матрицами. Поэтому мы выключили все хеш-проверки и заменили AES-шифрование простым копированием данных для фаззинга.
    В общей сложности фаззинг длился несколько дней, и за это время мы обнаружили множество ошибок. Но после этого мы чувствовали себя немного уверенней, когда отдавали нашу библиотеку на анализ соревнующимся командам.

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


Как мы боролись с читерством


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


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


  1. Простое доказательство. Сервер проверки просто доказывает, что знает цикл. Если что-то идет неправильно, то у команды отнимаются очки SLA (соглашения об уровне сервиса).
  2. Сервер проверки создает валидные обязательства, но перед отправкой на сервер команды повреждает их случайным образом, чтобы Доказывающий счел полученное обязательство ошибочным. Он повторяет цикл неверных доказательств в течение нескольких секунд. Затем он реализует правильное взаимодействие и получает флаг. Если в какой-то момент от сервера команды придет неожиданный ответ или сервер оборвет соединение, то команда теряет очки SLA.
  3. Сервер проверки инициализирует протокол и направляет валидное обязательство. Далее он дает команду Проверяющему на отправку вызова в цикле на протяжении пары секунд. По истечении этого времени он выбирает последний вызов, создает пакет, который раскрывает обязательство, и направляет его на сервер команды. Если сервер отдает флаг, значит, все сделано правильно. За любые отклонения от ожиданий сервера проверки отнимаются очки SLA.

У одной из команд (Bushwhackers) показатель SLA составил только 65%, потому что они провалили вторую стратегию. Кстати, все причины неудач выводились на scoreboard, чтобы команды могли сделать вывод об ошибочности определенных стратегий защиты.


Заключение


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


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


Если у вас будет желание, можно собрать и запустить наш таск самостоятельно по ссылке https://github.com/bi-zone/CTFZone-2020-Finals-LittleKnowledge. Мы выложили его в открытый доступ, чтобы любой мог попробовать его решить. Там есть подробные комментарии по всем функциям внутри библиотеки, так что вы можете пойти путем команд (без информации о внутренностях библиотеки) либо посмотреть исходный код. Также доступна примитивная реализация сервера проверки. Учитывая все усилия, надеемся, что этот труд не пропадет. Возможно, кто-нибудь возьмет наш материал на вооружение при подготовке к CTF.


Спасибо, что прочитали до конца, и удачи!

Подробнее..

Код Рида-Соломона

10.09.2020 12:19:55 | Автор: admin

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

Так, например, для определенного Рида-Соломона кода (РС-кода) необходимо установить:
длину n кодового слова (блока);
количество k информационных и N-k проверочных символов;
неприводимый многочлен р(х), задающий конечное поле GF(2r);
примитивный элемент конечного поля;
порождающий многочлен g(x);
параметр j кода;
используемое перемежение;
последовательность передачи кодовых слов или символов в канал и еще некоторые другие.

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

Описание РС-кода и его характеристик


Для удобства и лучшего уяснения сущности устройства РС-кода и процесса кодирования вначале приведем основные понятия и термины (элементы) кода.
Рида Соломона коды (РС-код) можно интерпретировать как недвоичные коды БЧХ (Боуза Чоудхури Хоквингема), значения кодовых символов которых взяты из поля GF(2r), т. е. r информационных символов отображаются отдельным элементом поля. Коды Рида Соломона это линейные недвоичные систематические циклические коды, символы которых представляют собой r-битовые последовательности, где r целое положительное число, большее 1.

Коды Рида Соломона (n, k) определены на r-битовых символах при всех n и k, для которых:
0 < k < n < 2r + 2, где
k число информационных символов, подлежащих кодированию,
n число кодовых символов в кодируемом блоке.

Для большинства (n, k)-кодов Рида Соломона; (n, k) = (2r1, 2r12t), где
t количество ошибочных символов, которые может исправить код, а
nk = 2t число контрольных символов.

Код Рида Соломона обладает наибольшим минимальным расстоянием (числом символов, которыми отличаются последовательности), возможным для линейного кода. Для кодов Рида Соломона минимальное расстояние определяется следующим образом: dmin = nk +1.

Определение. РС-кодом над полем GF(q=рm), с длиной блока n = qm-1, исправляющим t ошибок, является множество всех кодовых слов u(n) над GF(q), для которых 2t последовательных компонентов спектра с номерами $m_0,m_0 +1,...,m_0+2t-1$ равны 0.

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

Информационный многочлен Q. Задает текст сообщения, которое делится на блоки (слова) постоянной длины и оцифровывается. Это то, что подлежит передаче в системе связи.
Порождающий многочлен g(x) РС-кода многочлен, который преобразует информационные многочлены (сообщения) в кодовые слова путем перемножения Qg(x)= С =u(n) над GF(q).

Проверочный многочлен h(x) позволяет устанавливать наличие искаженных символов в слове.
Синдромный многочлен S(z). Многочлен, содержащий компоненты соответствующие ошибочным позициям. Вычисляется для каждого принятого декодером слова.
Многочлен ошибок E. Многочлен с длиной равной кодовому слову, с нулевыми значениями во всех позициях, кроме тех, что содержат искажения символов кодового слова.

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

Неприводимый многочлен поля р(x). Конечные поля существуют не при любом числе элементов, а только в случае, если число элементов является простым числом р или степенью q=рm простого числа. В первом случае поле называется простым (его элементы-вычеты чисел по модулю простого числа р), во втором-расширением соответствующего простого поля (его q элементов-многочленов степени m-1 и менее это вычеты многочленов по модулю неприводимого над простым полем многочлена р(x) степени m)

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

Таблица П Характеристики элементов конечного поля расширения GF(24), неприводимый многочлен p(x) = x4+x+1, примитивный элемент =0010= 210


Пример 1. Над конечным полем GF(24), задан неприводимый многочлен поля p(x) = x4 + x + 1, примитивный элемент =2, и задан (n, k)- код Рида-Соломона (РС-код). Кодовое расстояние этого кода равно d = n k + 1 = 7. Такой код может исправлять до трёх ошибок в блоке (кодовом слове) сообщения.

Порождающий многочлен g(z) кода имеет степень m =n-k = 15-9 = 6 (его корнями являются 6 элементов поля GF(24) в десятичном представлении, а именно элементы 2, 3, 4, 5, 6, 7) и определяется соотношением, т.е. многочленом от z с коэффициентами (элементами) из GF(24) в десятичном представлении при i = 1(1)6. В рассматриваемом РС-коде 29 = 512 кодовых слов.

Кодирование сообщений РС-кодом


В таблице П эти корни имеют и степенное представление $^1=2, ^2=3,...,^6=7$.
.
Здесь z- абстрактная переменная, а -примитивный элемент поля, через его степени выражены все (16) элементы поля. Многочленное представление элементов поля использует переменную х.
Вычисление порождающего многочлена g(x)=АВ РС-кода выполним частями (по три скобки):

Векторное представление (через коэффициенты g(z) элементами поля в десятичном представлении) порождающего многочлена имеет вид
g(z) = G<7>= (1, 11, 15, 5, 7, 10, 7).

После формирования порождающего многочлена РС-кода, ориентированного на обнаружение и исправление ошибок, задается сообщение. Сообщение представляется в цифровом виде (например, ASCII- кодом), от которого переходят к многочленному или векторному представлению.

Информационный вектор (слово сообщения) имеет k компонентов из (n, k). В примере k = 9, вектор получается 9-компонентный, все компоненты это элементы поля GF(24) в десятичном представлении Q<9> = (11, 13, 9, 6, 7, 15, 14, 12, 10).

Из этого вектора формируется кодовое слово u<15> вектор с 15 компонентами. Кодовые слова, как и сами коды, бывают систематическими и несистематическими. Несистематическое кодовое слово получают умножением информационного вектора Q на вектор, соответствующий порождающему многочлену
.

После преобразований получаем несистематическое кодовое слово (вектор) в виде
Qg = <11, 15, 3, 9, 6, 14, 7, 5, 12, 15, 14, 3, 3, 7, 1>.
При систематическом кодировании сообщение (информационный вектор) представляют многочленом Q(z) в форме Q(z)=q(z)g(z) + R(z), где степень degR(z)<m = 6. После этого к вектору Q справа приписывается остаток R (всё в десятичном виде). Это делается так.

Многочлен Q сдвигают в сторону старших разрядов на величину m = n k, что достигается путём умножения Q(z) на Zn k (в примере Zn k = Z 6) и выполняют после сдвига деление Q(z)Zn k на g(z). В результате находят остаток от деления R(z). Все операции выполняют над полем GF(24)
(11, 13, 9, 6, 7, 15, 14, 12, 10, 0, 0, 0, 0, 0, 0) =
=(1, 11, 15, 5, 7, 10, 7)(11, 15, 9, 10,12,10,10,10, 3) + (1, 2, 3, 7, 13, 9) = GS + R.

Остаток при делении многочленов вычисляется обычным способом (уголком см.здесь Пример 6). Деление выполняется по образцу: Пусть Q = 26, g(z) = 7 тогда 26 = 73 +R(z), R(z)=26 -73 =26-21 = 5. Вычисление остатка R(z) от деления многочленов. Приписываем к вектору Q справа остаток R.

Получаем u<15> кодовое слово в систематическом виде. Этот вид явно содержит информационное сообщение в k старших разрядах кодового слова
u<15> = (11,13,9,6,7,15,14,12,10; 1, 2, 3, 7, 13, 9).
Разряды вектора нумеруются справа налево от 0(1)14. Шесть младших разрядов справа являются проверочными.

Декодирование кодов Рида-Соломона


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

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

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

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

Обнаружение искажений.


Синдромный $S = (S_v,S_{v+1},...,S_{m+v-1})$, где $v{0, 1}$ вектор последовательно определяется для каждого из полученных декодером на его входе кодовых слов. При нулевых значениях компонентов вектора синдрома $Sj = 0, j=v,v+1,...,m + v - 1$, декодер считает, что в принятом слове ошибки нет. Если же хотя бы для одного $j1,Sj0$, то декодер делает вывод о наличии ошибок в кодовом векторе и приступает к их выявлению, что является 1-м шагом работы декодера.

Вычисление синдромного многочлена
Умножение на приемной стороне кодового слова С на проверочную матрицу Н может давать в результате два исхода:
синдромный вектор S=0, что соответствует отсутствию ошибок в векторе C;
синдромный вектор S0, что означает наличие ошибок (одной или более) в компонентах вектора C.

Интерес представляет второй случай.
Кодовый вектор с ошибками представлен в виде C(E) =C + E, E вектор ошибок. Тогда $(C +E)H^t = CH^t + EH^t = 0 + EH^t = S$
Компоненты Sj синдрома определяются либо соотношением суммирования
для n = q-1 и j = 1(1)m = n-k, либо схемой Горнера:
$inline$S_j = C_0 +^j(C_1 +^j(C_2 +...+^j(C_{n-2} +^jC_{n-1})...))$inline$

Пример 2. Пусть вектор ошибок имеет вид Е =<0 0 0 0 12 0 0 0 0 0 0 8 0 0 0>. Он искажает в кодовом векторе символы на 3-й и 10-й позициях. Значения ошибок соответственно 8 и 12 эти значения также являются элементами поля GF(24) и заданы в десятичном (табл. П) представлении. В векторе Е нумерация позиций от младших справа налево, начиная с 0(1)14.

Сформируем теперь кодовый вектор с двумя ошибками в 3-ем разряде и в 10-ом со значениями 8 и 12 соответственно. Это осуществляется суммированием в поле GF(24) по правилам арифметики этого поля. Суммирование элементов поля с нулем не изменяет их значения. Ненулевые значения (элементы поля) суммируются после преобразования их к многочленному представлению, как обычно суммируются многочлены, но коэффициенты при неизвестной приводятся по mod 2.

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

Ниже показано вычисление искажённых ошибками значений в 10 и 3 позициях кодового слова:
$inline$(7+12) ^6+^11 =x^3 +x^2 +x^3 +x^2 +x^1 =^1 = 2,$inline$
$inline$(3 + 8) ^2+ ^7 =x^2 +x^3 +x^1 + 1 =^{12}=13.$inline$

Декодер вычисления выполняет по общей формуле для компонентов Sj, j=1(1)m. Здесь (в модели) используем соотношение $S=EH^t$, так как E задаём (моделируем) в программе сами, то ненулевые слагаемые получаются только при i = 3 и i = 10.


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

Проверочная матрица РС кода


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

Сама матрица формируется специальным образом. Первые две строки очевидны, третья строка и все последующие получены вычитанием из предыдущей (второй) строки отрезка чисел натурального ряда 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15 по mod 15. При возникновении нулевого значения оно заменяется числом 15, отрицательные вычеты преобразуются в положительные.


Каждая матрица соответствует своему порождающему многочлену для систематического и несистематического кодирования.

Определение коэффициентов синдромного многочлена


Далее будем определять коэффициенты синдромного многочлена при j=1(1)6.
Относительно кодового слова с длиной $n<q=p^r$, поступающего на вход декодера мы допускаем, что оно искажено ошибками.

Относительно вектора ошибок для его выявления необходимо знать следующее:
количество искаженных позиций у кодового слова
$vv_{max}=0.5m$;
номера (положение) искаженных позиций в кодовом слове $ _i: _i=0(1)n-1$;
значения (величины) искажений $inline$e_; e_GF(2^4)$inline$.
Как вычисляется и используется далее синдромный вектор (многочлен) S? Его роль при декодировании кодовых слов очень значительна. Покажем это с иллюстрацией на числовом примере.

Пример 3. (Вычисление компонентов синдромного вектора $S_{<6>}$ )

то в итоге имеем $S_{<6>}=(S_1,S_2,S_3,S_4,S_5,S_6)$ =<8,13,7,13,15,15>.

Для дальнейшего рассмотрения введем новые понятия. Величину $x_i = ^{_i}$будем называть локатором ошибок, здесь искаженный символ кодового слова на позиции $_i$, примитивный элемент поля GF(24).
Множество локаторов ошибок конкретного кодового слова рассматривается далее как коэффициенты многочлена локаторов ошибок (z), корнями $z_i$ которого являются значения $x_i ^{-1}$, обратные локаторам.

При этом выражения $(1-zx_i)=0 $обращаются в нуль.
$inline$(z) = (1-zx_1)(1-zx_2)...(1-zx_v) =_vz^v +_{v-1}z^{v-1} +...+_1z +_0$inline$
всегда свободный член уравнения всегда свободный член уравнения $_0 =1$.
Степень многочлена локаторов ошибок равна v количеству ошибок и не превышает величины $vv_{max}=0.5m$.

Все искаженные символы находятся на разных позициях слова, следовательно, среди локаторов $x_i = ^i$,, не может быть повторяющихся элементов поля, а многочлен (z)=0 не имеет кратных корней.
Величины ошибок для удобства записи переобозначим символом $Y_i = e_i$. Для коэффициентов синдромного многочлена ранее рассматривались нелинейные уравнения. В нашем случае v=1 начало отсчета компонентов синдрома.


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

Преобразование к системе линейных уравнений
В уравнение $_i$ многочлена локаторов ошибок подставляется значение его корней $z =x_i^{-1}$. При этом многочлен обращается в нуль. Образуется тождество, обе части которого умножаем на $y_ix_i^{j+v}$, получаем:
$inline$y_i(_vx_i^{j}+_{v-1}x_i^{j+1}+...+_1x_i^{j+v-1}+x_i^{j+v})=0,1iv, 1jv$inline$.

Таких равенств получаем $vv =v^2$.
Суммируем эти равенства по всем $ i, 1iv$, при которых эти равенства выполняются. Так как многочлен (z) имеет v корней $x_i^{-1}$, раскроем скобки и перенесем коэффициенты $_i$ за знак суммы:


В этом равенстве согласно системе нелинейных уравнений, приведенной
ранее, каждая сумма равна одному из компонентов вектора синдрома. Отсюда заключает, что относительно коэффициентов $_v, _{v-1},...,_1$ можно выписать систему уже линейных уравнений.


Знаки при вычислениях над двоичным полем опускаются, так как со-ответствуют +. Полученная система линейных уравнений является ганкелевой и ей соответствует матрица с размерами $v(v+1)$ бит.

Эта матрица не вырождена, если число ошибок в кодовом слове C(E) строго равно $v , v 0.5(n-k)$, т.е. способность помехоустойчивости данного кода не нарушилась.

Решение системы линейных уравнений


Полученная система линейных уравнений в качестве неизвестных содержит коэффициенты $_i =1(1)t$ многочлена локаторов ошибок для кодового слова C(E). Известными считаются вычисленные ранее компоненты синдромного вектора $S_j, j=1(1)m$. Здесь t количество ошибок в слове, m количество проверочных позиций в слове.
Существуют разные методы решения сформированной системы.

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

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

Метод (ПГЦ) прост и хорош, но для малого количества исправляемых ошибок, С-метод сложен для реализации на ЭВМ и ограниченно опубликован (освещен) в источниках, хотя С-метод как и ТБМ-метод по известному многочлену синдромов S(z) обеспечивает решение уравнения Падэ над полем Галуа. Это уравнение сформировано для многочлена локаторов ошибок (z) и многочлена (z), в теории кодирования называется ключевым уравнением Падэ:
$S(z)(z) = (z)(mod z^m)$.

Решением ключевого уравнения является совокупность $x_i^{-1}$ корней многочлена (z), и соответственно локаторов $x_i =^{_i}$, т.е. позиции ошибок. Значения (величины) ошибок $e_i$ определяются из формулы Форни в виде

где $_z^{'}(^{-i})$ и $ (^{-i})$ значения многочленов (z) и (z) в точке $z =^{-i}$, обратной корню многочлена (z);
i позиция ошибки;$_z^{'}(z)$ формальная производная многочлена (z) по z;

Формальная производная многочлена в конечном поле


Имеются различия и сходство для производной по переменной в поле вещественных чисел и формальной производной в конечном поле. Рассмотрим многочлен

$a_i$ это элементы поля, i = 1(1)n.
Элементы поля. Задан код над вещественным полем GF(24). Производная по z имеет вид: .
В бесконечном вещественном поле операции умножить на n и суммировать n раз совпадают. Для конечных полей производная определяется иначе.
Производная по аналогии определяется соотношением:

где ((i)) = 1+1+...+1, (i) раз, суммируемых по правилам конечного поля: знак + обозначает операцию суммировать столько-то раз, т.е. элемент $a_2z$ повторить 2 раза, элемент $a_3z^2$ повторить 3 раза, элемент $a_nz^{n-1}$ повторить n раз.

Ясно, что эта операция не совпадает с операции умножения в конечном поле. В частности, в полях GF(2r) сумма четного числа одинаковых слагаемых берется по mod2 и обнуляется, а нечетного равна самому слагаемому без изменений. Следовательно, в поле GF(2r) производная получает вид

вторая и старшие четные производные в этом поле равны нулю.

Из алгебры известно, если многочлен имеет кратные корни (кратность р ), то производная многочлена будет иметь этот же корень, но с кратностью р-1. Если р = 1, то f(z) и f '(z) не имеет общего корня. Следовательно, если многочлен и его производная имеют общий делитель, то существует кратный корень. Все корни производной f '(z) эти корни кратные в f(z).

Метод решения ключевого уравнения


ТМБ (Тренча-Берлекэмпа-Месси) метод решения ключевого уравнения.
Итеративный алгоритм обеспечивает определение многочленов (z) и (z), и решение уравнения Падэ (ключевого).

Исходные данные: коэффициенты многочлена $S_1,S_2,...,S_n$ степени n-1.
Цель. Определение в явном (аналитическом) виде многочленов (z) и (z).
В алгоритме используются обозначения: j номер шага, $v_j$ степень многочлена, $inline$_j(z) =_{ji}z^i +_{ji-1}z^{i-1}+...+_{j1}z+_{j0}$inline$ разложение многочлена по степеням $z, k_j, L_j, _j(z)$ и $ _j(z)$ промежуточные переменные и функции на j-м шаге алгоритма;

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


Пример 4. Выполнение итеративного алгоритма для вектора
S=(8,13,7,13,15,15). Определяются многочлены $(z) =_n(z)$ и $(z) = _n(z)$.
Таблица 2 Расчет многочленов локаторов ошибок




Итак $inline$_j^(z)=14z^2+13z+1$inline$, $inline$_j^(z)$inline$=7z+8.
Многочлен локаторов ошибок (z) над полем GF(24) с неприводимым многочленом p(x) = x4 + x + 1 имеет корни
$z_1 = ^{-i_1} = 13 = 4^{-1}$ и $z_2 =^{-i_2} = 6 = 11^{-1}$, в этом легко убедиться непосредственной проверкой, т.е. $inline$i_1= 3, i_2 =10, 13 = ^{12}, 1 =^{12}^{3}$inline$ и $^{12} =^{-3}=>13=4^{-1}$. Подстановка корней в
$inline$(z=13)=14(13)^2+1313+1=^{13}(^{12})^2+(^{12})^2+^0= ^{37}+^{24} +^{0}$inline$=
=$^{7}+^{9}+^0 =x^3+x+1=0(mod2)$;
$inline$(z = 6)=14(6)^2+136+1 = ^{13}(^{5})^2+(^{5})^2+^{0}$inline$=
=$^{8}+^{2} +^{0} = x^2 +1+x^2 +1 = 0(mod2)$.

Взяв формальную производную от (z), получаем _2(z) =214+13 =13, так как 14z берется в сумме 2 раза и по mod 2 обращается в нуль.
С использованием формулы Форни найдем выражения для расчета величин ошибок $e_i$.


Подстановкой значений i = 3 и i = 10 позиций в последнее выражение
находим
$е_3 = 10^{15-3}+11 =^{6}+^{10}$= =$x^3+x^2+x^2+x+1=x^3+x+1=^{7}=>8$;
$inline$е_{10} = 10^{15-10}+11 =^{9}^{5}+^{10}=^{14}+^{10}$inline$= =$x^3+x^2+x=^{11}=>12$.

Архитектура построения программного комплекса


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

Схема функционирования программного комплекса

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

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

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

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


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

Рисунок 4. Результаты загрузки файла сообщения

Рисунок 5. Результаты кодирования файла

Рисунок 6. Вывод сообщения с внесенными в него ошибками.

Рисунок 7. Вывод результатов декодирования и сообщения с внесенными в него ошибками

Рисунок 8. Вывод декодированного сообщения.

Заключение


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

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

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

Список используемой литературы


1. Блейхут Р. Теория и практика кодов, контролирующих ошибки. М.: Мир, 1986. 576 с.
2. Мак-Вильямс Ф. Дж, Слоэн Н. Дж. А. Теория кодов, исправляющих ошибки. М.: Связь, 1979. 744 с.
3. Берлекэмп Э. Алгебраическая теория кодирования. М.: Мир, 1971. 478 с.
4. Габидулин Э.М., Афанасьев В.Б. Кодирование в радиоэлектронике. М.: Радио и связь, 1986. 176 с., ил.
5. Вернер М. Основы кодирования. Учебник для ВУЗов. М.: Техносфера, 2004. 288 с.
6. Трифонов П.В. Адаптивное кодирование в многочастотных системах. Диссертация на соискание ученой степени кандидата технических наук. СПб: Санкт-Петербургский государственный политехнический университет, 2005. 147 с.
7. Фомичев С. М., Абилов А.В. Обзор математических моделей каналов связи и их применение в телекоммуникационных системах. Ижевск: Ижевский государственный технический университет, 2001. 60 с.
8. Касами Т., Токура Н., Ивадари Е., Инагаки Я. Теория кодирования. М.: Мир, 1978. 576 с.
9. Муттер В. М. Основы помехоустойчивой телепередачи информации. Л.: Энергоатомиздат. Ленинградское отделение, 1990. 288 с.
10. Ваулин А. Е., Смирнов С.И. Моделирование помехозащищенного канала передачи сообщения в системе связи/Сборник алгоритмов и программ типовых задач. Вып.26. под редакцией ктн доц. И.А. Кудряшова . СПб.: ВКА им А.Ф. Можайского, 2007. стр. 121-130.
11. Карпушев С.И Конспект лекций по алгебре (часть 2. Абстрактная
алгебра). ВИКУ им. А. Ф. Можайского, 2002. 97 с.
12. Зайцев И. Е. Методика определения параметров помехоустойчивого каскадного кодирования. Л.: ВИКИ, 1987 120 с.
Подробнее..

Принятие решений. Пример

21.09.2020 14:20:44 | Автор: admin

Прочие статьи цикла
Принятие решений
Принятие решений. Пример

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

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

Исходное множество альтернатив их измерение и оценивание


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

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

Пусть некоторое свойство множества альтернатив из выражается числом, т. е. существует отображение : Е1, где Е1 множество действительных чисел. Тогда такое свойство характеризуют показателем, а число z = (x) называют значением (оценкой) альтернативы по показателю.

Для проведения оценивания альтернатив необходимо произвести измерение показателей.

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

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

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

Из этих примеров можно (и важно) сделать вывод о том, что показатели объем и температура относятся к различным типам свойств, над значениями z = (x) которых допустимы или недопустимы определенные преобразования f(z)=f((x)).

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

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

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



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

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

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

Из этих cоображений такая шкала называется шкалой наименований.
Допустимые преобразования значений в этой шкале это все взаимно однозначные функции: f(x) f(y) <=> x y.

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

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

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

Ответить на вопрос насколько или во сколько раз один минерал тверже другого не удается. Допустимые преобразования порядковой шкалы состоят из всех монотонно возрастающих функций, обладающих свойством: x y => f(x) f(y).

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

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

Другой пример, шкала длин (расстояний) пространственных интервалов определяется путем совмещения нуля линейки с одной точкой, а отсчет делается у другой точки. К этому типу шкал относятся и шкалы температур по Цельсию, Фаренгейту, Реомюру.

В этих шкалах допустимы линейные преобразования, (x y)/(z -v); x y; в них применимы процедуры для отыскания математического ожидания, стандартного отклонения, коэффициента асимметрии и смещенных моментов.

4. Шкала разностей(балльная) Шкалы разностей отличаются от шкал порядка тем, что по шкале интервалов можно уже судить не только о том, что размер больше другого, но и на сколько больше в сущности это та же абсолютная шкала, но ее значения сдвинуты на некоторую величину относительно абсолютных значений (x y) < (z -v); x y;

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

Пусть в этой шкале выполняется измерение длины предмета. При этом можно переходить от измерения в метрах к измерению в сантиметрах, уменьшая единицу измерения в 100 раз. Очевидно, что в этом случае отношение длин L(A) и L(B) двух предметов А и В, измеренных в одинаковых единицах, не изменится при изменении единиц измерения.

Значения показателя признака, измеренные в этой шкале позволяют
отвечать на вопрос во сколько раз интенсивнее признак проявляется у одного предмета, чем у другого. С этой целью необходимо рассмотреть отношение значений L(A)/L(B) = k.

Если отношение больше единицы (k >1), значение показателя признака у первого объекта А в k раз больше чем у В, если k < 1, то значение показателя признака у объекта В в 1/ k раз больше чем у А. Допустимым преобразованием показателя является умножение на целое положительное число и только оно.

6. Абсолютная шкала. Наиболее простой из всех шкал является шкала, допускающая только одно преобразование f(x) = x. Этой ситуации соответствует единственный способ измерения показателя свойства объекта, а именно простой пересчет предметов.

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

Задача принятия решения. Получение матрицы отношений


Перечислим возможные постановки ЗПР, к ним относятся:

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

На основе анализа измерений показателей свойств альтернатив в различных шкалах результаты измерений могут быть представлены различными способами [1, 5].

1. Классификационная таблица. Таблица получается при проведении измерений в номинальных шкалах и представляет собой таблицу, строками которой являются: наименование объекта, а столбцами наименования классов $X_1, X_2, X_3,$ и т. д. В столбцах класс 1, класс 2 и т. д. ставится 1, если объект принадлежит данному классу и 0 если нет (табл. Классы объектов).

В таблице классов объекты $x_1 ,x_2 X_1, x_3 X_2, x_4 X_3.$

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

Строится $А_{[nn]}^p$ квадратная матрица. Ее i-я строка соответствует i-му элементу $x_i$ множества , а j-й столбец элементу $x_j$. На пересечении i-той строки и j-го столбца ставится единица, если объект $x_i$ предпочтительнее объекта $x_j$, нуль, если объект $x_j$ предпочтительнее объекта $x_i$, 1/2, если объекты $x_i$ и $x_j$ безразличны, и ничего не ставится если объекты несравнимы $x_i$ и $x_j$ нельзя сравнить.

Пример такого отношения предпочтений представлен матрицей ниже


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

В столбцах $p_1, p_2, p_3, p_4$ таблицы Отношение предпочтения размещены значения показателей свойств, по которым оцениваются объекты $x_1,x_2,x_3,x_4,x_5,x_6$ и $x_7$.

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

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


Из матрицы отношения предпочтения $А_{[44]}^p$ для 4-х альтернатив представленной в табл. Отношения предпочтения получится матрица $А_{[44]}^p$, которая выгляди следующим образом:


Отображение матрицы показателей в матрицу отношения предпочтения осуществляется следующим образом: $a_{i,j}=1$, если:
1)число показателей, по которым объект $x_i$ предпочтительнее объекта $x_j$ больше, чем число показателей, по которым объект $x_j$ предпочтительнее объекта $x_i$;

2) для объекта $x_i$ ни один из показателей не принимает наименьшего из возможных значений.

3) из условия 1 следует, что те показатели, по которым объект $x_i$ не хуже объекта $x_j$, составляют большинство среди всех рассматриваемых показателей.

Однако при выполнении этого условия может оказаться, что по тем показателям, по которым объект $x_i$ хуже объекта $x_j$, разница значительна; чтобы уменьшить число таких случаев при отдаче предпочтения в пользу х, вводится условие 2.

Методы решения задачи принятия решения


Пусть после получения исходных данных мы располагаем отношением R на множестве альтернатив $= {(x_1, ...,x_n)}$. И стоит задача принятия решения. Основной метод линейное упорядочение (ранжирование) альтернатив, т. е. выстраивание альтернатив в цепочку по убыванию их ценности, пригодности, важности и тому подобное, от самой хорошей до самой плохой.

Отношение R может быть:

  1. полным нетранзитивным отношением;
  2. отношением частичного порядка;
  3. линейным порядком.

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

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

Ранжирование альтернатив. Пусть отношение [, R] полное и нетранзитивное. Свойство полноты говорит о том, что все альтернативы $ = {(x_1, ...,x_n)}$ из множества сравнимы между собой. Наличие нетранзитивности возможно только в том случае, если граф G[, R] предпочтения содержит контуры.

Необходимо преобразовать структуру графа отношения так, чтобы были устранены логические противоречия в форме контуров. Если предположить что в отношении R имеется контур $x_1, x_2, ,x_k, x_1,$ то при ранжировании альтернатив $x_1$ должна быть расположена выше $x_2, x_3, ,x_k, x_1,$, что приводит к противоречию.
Введем следующее утверждение [1,5].

Пусть В' и В" два произвольных контура графа вида G[, R], тогда если некоторый элемент $x_i$ B' доминирует элемент $x_j$ B'', то и любой элемент $x_1$ B' доминирует любой элемент $x_k$ B''.

Это предложение предоставляет возможность разбиения множества R на m подмножеств $B_1, B_2, ,B_m$, таких что $inline$B_i B_j=, i,j [1, m]; UB_i =.$inline$
Итак, задача ранжирования альтернатив множества распадается на два этапа:
1) выделение контуров графа, т.е. разбиение множества на подмножества $B_1, B_2, ,B_m$ и групповое упорядочение этих подмножеств;
2) ранжирование элементов контуров, выделенных на первом этапе.

Алгоритм выделения контуров графа

Для нахождения контуров графа существует простой алгоритм [1]. Пусть $А_{[nn]}$ матрица смежности графа G[, R], а $Е_{[nn]}$единичная матрица. Образуем $Е_{[nn]}+ А_{[nn]}$, $(Е_{[nn]}+ А_{[nn]})^2$, $(Е_{[nn]}+ А_{[nn]})^3$, последовательность степеней матриц, элементы которых выражают количество путей длины не более 1, 2, 3 Для некоторого значения m n получим следующее равенство (установившуюся матрицу):
$inline$(Е_{[nn]}+ А_{[nn]})^m=(Е_{[nn]}+А_{[nn]})^{m+1}$inline$.

Из теории графов известно [10], что каждой системе всех одинаковых строк установившейся матрицы соответствует подмножество вершин графа, лежащих в одном контуре. Группируя соответствующие вершины в классы, получим разбиение исходного множества на подмножества $B_1, B_2, ,B_m$.

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

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

Пусть на множестве задано отношение предпочтения R матрицей $А_{[66]}$.


Граф отношения R изображен на рис. Г.


Рис. Г. Граф нетранзитивного отношения R

Для осуществления первого этапа ранжирования элементов множества необходимо выделить контуры графа G[, R]. Это делается возведением матрицы смежности графа в последовательные степени, пока не выполнится совпадение матриц.

Получаем $(Е_{[66]}+ А_{[66]})$, $(Е_{[66]}+ А_{[66]})^2$, $(Е_{[66]}+ А_{[66]})^3$.
Далее последовательно вычисляем возрастающие степени матрицы, суммируя их с единичной матрицей соответствующей размерности до тех пор пока матрица не перестанет изменяться:


Так как $inline$(Е_{[66]}+ А_{[66]})^2 =(Е_{[66]}+ А_{[66]})^3$inline$, можно сделать вывод, что $inline$(Е_{[66]}+ А_{[66]})^2 = (Е_{[66]}+ А_{[66]})^k$inline$, при k 2. Из анализа матрицы $(Е_{[66]}+ А_{[66]})^2$ следует, что строки, соответствующие элементам $x_1,x_4,x_6$ совпадают, это говорит о принадлежности этих элементов одному контуру графа G[, R].

Элементы $x_1,x_4,x_6$ образуют множество $B_1 =(x_1,x_4,x_6)$. Другой контур образован элементами $x_2,x_3,x_5$, которые входят в множество $B_2 =(x_2,x_3,x_5)$.

Таким образом, мы провели разбиение множества на m=2 класса $B_1, B_2$. Проведем групповое упорядочение этих подмножеств. Для этого необходимо найти какой-нибудь элемент $x_i B_1$, который доминирует элемент $x_j B_2$.

Это будет означать превосходство подмножества $B_1$ над $B_2$. В нашем примере $x_1 B_1$ доминирует над $x_2 B_2$. Следовательно, подмножество $B_1$ доминирует над $B_2$. Графическое представление доминирования в разбиении изображено на рис. КК.


Рис. КК. Ранжирование контуров, выделенных на первом этапе

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

Обозначим через $ B_{h_{[nn]}}$ матрицу смежности h-го контура. Введем понятие $p^i(k)$-силы порядка k элемента i, значение которой вычисляется как сумма элементов i-й строки в матрице $ B_{h_{[nn]}}^k$ (1).

Пусть $ b_{h_{[i,j]}}^k$ элемент, стоящий в i-й строке и j-м столбце матрицы, тогда


Под относительной силой k-го порядка элемента i понимают дробь

При неограниченном возрастании k (k ), число $_i(k)$ стремится к некоторому пределу , который мы в дальнейшем будем называть силой элемента i. Вектор $П_{[n]} = (_1,...,_n)$ называется предельным вектором.

Вследствие теоремы Перрона-Фробениуса [1] предел всегда существует. Собственный нормированный вектор матрицы смежности контура совпадет с ее предельным вектором. Следовательно, вектор $П_{[n]} = (_1,...,_n)$ (2)

может быть найден без вычислений интегрированных сил $p^i(k)$, путем решения системы линейных уравнений
$B_{h_{[nn]}} П_{[n]} = П_{[n]}$, (3)
где наибольший неотрицательный действительный корень характеристического уравнения
$det(Е_{[nn]}- B_{h_{[nn]}}) = 0$ (4)
Необходимо отметить, что нормированный собственный вектор неотрицательной неразложимой матрицы не меняется при умножении матрицы на число s > 0, а также при суммировании ее с матрицей вида sE.

Затем элементы контура упорядочиваются по уменьшению значений соответствующих компонентов вектора $П_{[n]}$, т.е. элемент i доминирует элемент j тогда, когда $_i >_j$.

Осуществим ранжировку элементов множества $B_1 =(x_1,x_4,x_6)$. Построим матрицу предпочтений для этого множества


Вектор интегрированных сил 1-го порядка для элементов $(x_1, x_4, x_6)$ выглядит (1,1,2), вектор относительный сил П(k) = (1/4,1/4,2/4).
Ранжирование элементов $(x_1, x_4, x_6)$по силе 1-го порядка представлено на рис. Р.


Рис. Р. Ранжирование элементов

Найдем векторы, характеризующие силы 2-го, 3-го, 4-го и 5-го порядков.


Графическое представление ранжирования изображено на рис. П.



Рис.Ц Цепь

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

В результате совмещения ранжировки элементов множества В1 и В2 переходим к окончательному упорядочению элементов множества (рис. Ц).

Линейное доупорядочение строгих частичных порядков


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

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

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

Теорема (Шпильрайн [5, 10]). Всякий порядок R на множестве можно продолжить до линейного порядка на этом множестве.

Следствие из теоремы Шпильрайна: всякое линейное доупорядочение подмножества $_i $ может быть продолжено до линейного доупорядочения всего упорядоченного множества .

Если X подмножество в , состоящее из несравнимых альтернатив, то всякое линейное упорядочение X может быть продолжено до линейного упорядочения всего множества . При этом порядок R выражается через линейные порядки $R_i$.

В силу теоремы Шпильрайна, на множестве существует нумерация $x_1, x_2,...,x_n $ элементов этого множества. Нумерация n-элементного упорядоченного множества , с заданным на нем отношением порядка R, есть взаимно-однозначное отображение множества в себя, т.е. в {1, 2, ..., n}, при котором большему относительно порядка элементу соответствует больший номер [5]. Далее под ранжированием элементов будем понимать любое линейное доупорядочение этого порядка. Отметим, что нумерация упорядоченного множества представляет собой его измерение.

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

Для упорядоченного множества , с заданным на нем порядком R, элемент х' множества называется максимальным, если не существует строго большего его элемента х, т.е. если x>x' не выполняется ни для какого x . Элемент x'' называется наибольшим элементом упорядоченного множества [, R], если он больше любого другого элемента х, т.е. для всякого x , x''>x [5].

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

При всякой нумерации n-элементного множества , номер N приписывается максимальному элементу. Все нумерации множества можно получить, если известны все нумерации всех подмножеств, получающихся из удалением одного из таких максимальных элементов. К каждому подмножеству применим тот же прием [7]. Рассмотрим алгоритм построения всех нумераций упорядоченного множества [, R].

1. Строится вспомогательный граф [, R] упорядоченного множества [, R], вершины которого удовлетворяют условиям:

а) являются подмножествами ;

б) для любых двух подмножеств X, Y верно: (X,Y R) тогда, когда
подмножество Y может быть получено из подмножества X удалением одного из его максимальных элементов (рис. А и АА).


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

Задача заключается в нахождении всех линейных доупорядочений частичного порядка, диаграмма которого представлена на рис. A. В этом отношении нет информации, например, о том, доминирует ли альтернатива $x_1 $альтернативу $х_2$ или наоборот, а также аналогично для пар $(х_3, х_6);(х_4, х_5)$. Это и означает, что А частичное упорядочение. Достроить до линейного упорядочения существует много (22) вариантов, которые желательно привести к единственному. Это возможно с привлечением дополнительной информации об альтернативах, получаемой при детальном изучении ситуации.

1. Строим вспомогательный граф [, R], начинаем с множества $(x_1, x_2, x_3,x_4, x_5, x_6)$ и снизу. Цифра около дуги графа указывает удалением какого максимального элемента получено подмножество, в которое эта стрелка направлена (рис. AA).

2. Формируем табл. AAA для нахождения всех нумераций подмножеств, являющихся вершинами графа [, R]. Заполнение таблицы выполняется построчно сверху вниз. Каждая строка есть нумерация подмножества, записанного в левой колонке таблицы (табл. AAA).

3. При составлении нумерации подмножества X, состоящего из k элементов надо переписать все записанные ранее (для предыдущего подмножества) нумерации подмножеств Y R(x) и присвоить номер тому элементу, который дополняет Y до X.


В последнем (нижнем) блоке (табл. AAA) находятся все нумерации линейных доупорядочений множества . Графическое представление этих доупорядочений изображено на рис. AAA.


Рис. AAA. Графическое представление доупорядочений

Необходимо отметить, что всего линейных порядков на множестве из 6 элементов 6! или 720, а линейных доупорядочений множества с отношением, заданным графом, изображенным на рис. AA, всего 22. Это также достаточно много для принятия решения.
Существуют ли возможности для сокращения количества таких вариантов? Да существуют.
Для уменьшения числа линейных доупорядочений, необходимо воспользоваться дополнительной информацией.

Дополнительная информация


Пусть [, R] исходное отношение, тогда дополнительную информацию можно представить в виде отношения на множестве , где условие (x,y) , т.е. (x>y) интерпретируется как сообщение о том, что объект х доминирует объект у;
отношение можно рассматривать тогда, как множество подобных сообщений информации о доминировании, заданной в виде бинарного отношения , возможно два случая при использовании дополнительной информации:

  1. граф отношения R содержит контуры;
  2. граф отношения R не содержит контуров.

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

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

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

Например, если поступит информация о том, что $x_2$ доминирует $x_4$, т.е. $x_2>x_4$, то число линейных доупорядочений сократиться с 22 до 19, а если поступит информация: $x_1>x_2$, то число линейных доупорядочений сократиться в два раза. Таким образом, возникает вопрос: какая информация будет наиболее ценна, или при добавлении, какого отношения число доупорядочений уменьшится в наибольшей степени?

Для решения этой задачи для всех пар элементов $(x_i, x_j)$ множества , которые не входят в отношение R, в нижнем блоке табл. ААА, необходимо вычислить $n_{ij}$ сколько раз номер элемента $x_i$ больше номера элемента $x_j$, т.е. элемент $x_i$ стоит выше элемента $x_j$ и $n_{ji}$ сколько раз элемент $x_j$ стоит выше элемента $x_i$.
Степень ценности дополнительной информации об отношении в этой паре, будет тем выше, чем меньше разность $|n_{ij} - n_{ji}|$. Большее из чисел $n_{ij}, n_{ji}$ будет равняться числу доупорядочений множества [, R]. Для рассматриваемого примера получим сводку характеристик графического представления доупорядочений


Из анализа таблицы следует, что наиболее полезной будет являться
информация об отношении в парах $(x_1, x_2)$ и $(x_4, x_5)$. Получение дополнительной информации об отношении в любой из этих пар приводит к сокращению числа линейных доупорядочений в два раза.

Заключение


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

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

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

Список использованной литературы


1. Берж К. Теория графов и ее применение. М.: ИЛ, 1962. 320 с.
2. Ваулин А. Е. Дискретная математика в задачах компьютерной безопасности. Ч. I. СПб.: ВКА имени А. Ф. Можайского, 2015. 219 с.
3. Ваулин А. Е. Дискретная математика в задачах компьютерной безопасности.Ч. II. СПб.: ВКА имени А. Ф. Можайского, 2017. 151 с.
4. Ваулин А. Е. Методы исследования информационных вычислительных комплексов. Вып. 2. Л.: ВИКИ им. А. Ф. Можайского, 1984. 129 с.
5. Ваулин А. Е. Методология и методы анализа информационных вычислительных комплексов. Вып.1. Л.: ВИКИ им. А. Ф. Можайского, 1981. 117 с.
6. Ваулин А. Е. Методы цифровой обработки данных: дискретные ортогональные преобразования. СПб.: ВИККИ им. А. Ф. Можайского, 1993. 106 с.
7. Кузьмин В. Б. Построение групповых решений в пространствах четких и нечетких бинарных отношений. М.: Наука,1982. 168 с.
8. Макаров И. М. и др.Теория выбора и принятия решений. М.: Физматлит, 1982. 328 с.52.
9. Розен В.В. Цель оптимальность решение. М.: Радио и связь,1982.169 с.
10. Szpilraijn E Sur Textension de l'ordre partiel. Fundam. math.,1930, vol.16,pp.386-389.
Подробнее..

Immutable Trie найди то, не знаю что, но быстро, и не мусори

20.09.2020 10:09:00 | Автор: admin
Про префиксное дерево (Trie) написано немало, в том числе и на Хабре. Вот пример, как оно может выглядеть:


И даже реализаций в коде, в том числе на JavaScript, для него существует немало от каноничной by John Resig и разных оптимизированных версий до серии модулей в NPM.

Зачем же нам понадобилось использовать его для сервиса по сбору и анализу планов PostgreSQL, да еще и велосипедить какую-то новую реализацию?..

Склеенные логи


Давайте посмотрим, на небольшой кусок лога сервера PostgreSQL:

2020-09-11 14:49:53.281 MSK [80927:619/4255507] [explain.tensor.ru] 10.76.182.154(59933) pgAdmin III - ???????????????????? ???????????????? LOG:  duration: 0.016 ms  plan:Query Text: explain analyzeSELECT*FROMpg_classWHERErelname = 'мамамылараму';Index Scan using pg_class_relname_nsp_index on pg_class  (cost=0.29..2.54 rows=1 width=265) (actual time=0.014..0.014 rows=0 loops=1)  Index Cond: (relname = 'мамамылараму'::name)  Buffers: shared hit=2

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

SHOW log_line_prefix;-- "%m [%p:%v] [%d] %r %a "

Потребуется совсем немного магии регулярных выражений
const reTS = "\\d{4}(?:-\\d{2}){2} \\d{2}(?::\\d{2}){2}"  , reTSMS = reTS + "\\.\\d{3}"  , reTZ   = "(?:[A-Za-z]{3,5}|GMT[+\\-]\\d{1,2})";var re = {// : log_line_prefix      '%a' : "(?:[\\x20-\\x7F]{0,63})"    , '%u' : "(?:[\\x20-\\x7F]{0,63})"    , '%d' : "[\\x20-\\x7F]{0,63}?"    , '%r' : "(?:(?:\\d{1,3}(?:\\.\\d{1,3}){3}|[\\-\\.\\_a-z0-9])\\(\\d{1,5}\\)|\\[local\\]|)"    , '%h' : "(?:(?:\\d{1,3}(?:\\.\\d{1,3}){3}|[\\-\\.\\_a-z0-9])|\\[local\\]|)"    , '%p' : "\\d{1,5}"    , '%t' : reTS + ' ' + reTZ    , '%m' : reTSMS + ' ' + reTZ    , '%i' : "(?:SET|SELECT|DO|INSERT|UPDATE|DELETE|COPY|COMMIT|startup|idle|idle in transaction|streaming [0-9a-f]{1,8}\/[0-9a-f]{1,8}|)(?: waiting)?"    , '%e' : "[0-9a-z]{5}"    , '%c' : "[0-9a-f]{1,8}\\.[0-9a-f]{1,8}"    , '%l' : "\\d+"    , '%s' : "\\d{4}(?:-\\d{2}){2} \\d{2}(?::\\d{2}){2} [A-Z]{3}"    , '%v' : "(?:\\d{1,9}\\/\\d{1,9}|)"    , '%x' : "\\d+"    , '%q' : ""    , '%%' : "%"// : log_min_messages    , '%!' : "(?:DEBUG[1-5]|INFO|NOTICE|WARNING|ERROR|LOG|FATAL|PANIC)"// : log_error_verbosity    , '%@' : "(?:DETAIL|HINT|QUERY|CONTEXT|LOCATION|STATEMENT)"    };re['%#'] = "(?:" + re['%!'] + "|" + re['%@'] + ")";// преобразуем log_line_prefix в RegExp для разбора строкиlet lre = self.settings['log_line_prefix'].replace(/([\[\]\(\)\{\}\|\?\$\\])/g, "\\\$1") + '%#:  ';self.tokens = lre.match(new RegExp('(' + Object.keys(re).join('|') + ')', 'g'));let matcher = self.tokens.reduce((str, token) => str.replace(token, '(' + re[token] + ')'), lre);self.matcher = new RegExp('^' + matcher, '');

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

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

Увы, нет. В нашем примере такой строкой окажется хвост раму'::name) от распавшейся на части multiline string. Как быть?

Use Trie, Luke!


Но заметим, что план обязан начинаться с одного из узлов: Seq Scan, Index Scan, Sort, Aggregate, ... ни много, ни мало, а 133 разных варианта, исключая CTE, InitPlan и SubPlan, которые не могут быть корневыми.

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

Immutable Trie


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

  • компактность
    Возможных элементов у нас десятки/сотни вполне ограниченной длины, поэтому не может возникнуть ситуации большого количества очень длинных почти совпадающих ключей, отличающихся только в последнем символе. Самый длинный из наших ключей, наверное 'Parallel Index Only Scan Backward'.
  • иммутабельность
    То есть элементы добавляются только при инициализации. В дальнейшем процессе его существования они уже не добавляются и не удаляются.
  • пропускная способность
    Мы хотим тратить минимум операций на проверку совпадения каждого элемента. Иначе можно было бы просто последовательно сравнивать каждый элемент с началом строки, пока не найдется нужный.
  • отсутствие сайд-эффектов
    Операции поиска не должны создавать новых объектов в памяти, которые потом пришлось бы зачищать Garbage Collector'у.

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

В этом нам помогут две полезные функции:


Строим карту


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

Insert
Index Scan
Index Scan Backward
Index Only Scan
Index Only Scan Backward


Хм да у них же есть одинаковый префикс In!

// определение минимальной длины ключа и Longest Common Prefixlet len, lcp;for (let key of keys) {  // первый элемент  if (lcp === undefined) {    len = key.length;    lcp = key.slice(off);    continue;  }  len = Math.min(len, key.length);  // пропускаем, если уже "обнулили" LCP или он совпадает для этого элемента  if (lcp == '' || key.startsWith(lcp, off)) {    continue;  }  // усечение LCP при несовпадении префикса  for (let i = 0; i < lcp.length; i++) {    if (lcp.charCodeAt(i) != key.charCodeAt(off + i)) {      lcp = lcp.slice(0, i);      break;    }  }}

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

Insert
Index Scan
Index Scan Backward
Index Only Scan
Index Only Scan Backward


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

// перебираем варианты выбора номера тестируемого символаlet grp = new Set();res.pos = {};for (let i = off + lcp.length; i < len; i++) {  // группируем ключи по соответствующим значениям [i]-символа  let chr = keys.reduce((rv, key) => {    if (key.length < i) {      return rv;    }    let ch = key.charCodeAt(i);    rv[ch] = rv[ch] || [];    rv[ch].push(key);    return rv;  }, {});  // не обрабатываем повторно уже встречавшуюся комбинацию распределений ключей по группам  let cmb = Object.values(chr)    .map(seg => seg.join('\t'))    .sort()    .join('\n');  if (grp.has(cmb)) {    continue;  }  else {    grp.add(cmb);  }  res.pos[i] = chr;}

Метрика оптимума


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

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

То есть, если мы взяли в строке 3-й символ и обнаружили там 's', то потом нам надо сравнить с помощью startsWith, в худшем случае, еще 6 символов, чтобы убедиться, что там именно Insert.
Итого: 1 (.charCodeAt(2)) + 6 (.startsWith('Insert')) = 7 сравнений.

А вот если там обнаружился 'd', то надо взять еще 7-й, чтобы узнать, будет там 'O' или 'S'. И после этого нам все равно придется перебором проверить список был ли это 'Index Scan Backward' (+19 сравнений) или только 'Index Scan' (+10 сравнений).

Причем, если в строке будет 'Index Scan Backward', то мы используем всего 19 сравнений, а вот если 'Index Scan' тогда 19 + 10 = 29.
Итого: 1 (.charCodeAt(2)) + 1 (.charCodeAt(6)) + 19 + 29 (.startsWith(...)) = 50 сравнений только по этой подветке.

В итоге, для нашего примера оптимальная карта будет выглядеть так:

{  '$pos' : 2 // проверяем 3-й символ, '$chr' : Map {    100 => {         // 'd'      '$pos' : 6 // проверяем 7-й символ    , '$chr' : Map {        79 => [ 'Index Only Scan Backward', 'Index Only Scan' ] // 'O'      , 83 => [ 'Index Scan Backward', 'Index Scan' ]           // 'S'      }    }  , 115 => 'Insert' // 's'  }}

Вжух!


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

// заполнение карты поискаconst fill = (obj, off, hash) => {  off = off || 0;  hash = hash || {};  let keys = obj.src;  // проверка наличия такого списка ключей среди уже обработанных  let H = keys.join('\n');  hash[off] = hash[off] || {};  if (hash[off][H]) {    obj.res = hash[off][H];    return;  }  obj.res = {};  hash[off][H] = obj.res;  let res = obj.res;  // ситуация единственного ключа - возможна только при стартовом вызове  if (keys.length == 1) {    res.lst = [...keys];    res.cmp = res.lst[0].length;    return;  }  // определение минимальной длины ключа и Longest Common Prefix  let len, lcp;  for (let key of keys) {    // первый элемент    if (lcp == undefined) {      len = key.length;      lcp = key.slice(off);      continue;    }    len = Math.min(len, key.length);    // пропускаем, если уже "обнулили" LCP или он совпадает для этого элемента    if (lcp == '' || key.startsWith(lcp, off)) {      continue;    }    // усечение LCP при несовпадении префикса    for (let i = 0; i < lcp.length; i++) {      if (lcp.charCodeAt(i) != key.charCodeAt(off + i)) {        lcp = lcp.slice(0, i);        break;      }    }  }  // если один из ключей является общим префиксом  if (off + lcp.length == len) {    let cmp = 0;    // для двух ключей оптимальный вариант поиска - список    if (keys.length == 2) {      res.lst = [...keys];    }    // выносим "за скобки" слишком короткие ключи    else {      res.src = keys.filter(key => key.length > off + lcp.length);      res.lst = keys.filter(key => key.length <= off + lcp.length);    }    // поиск по списку проходит, начиная с самого длинного ключа, и стоит дорого    res.lst.sort((x, y) => y.length - x.length); // s.length DESC    cmp += res.lst.reduce((rv, key, idx, keys) => rv + (keys.length - idx + 1) * key.length, 0);    // если есть продолжение - копаем дальше    if (res.src && res.src.length) {      fill(res, off + lcp.length + 1, hash);      cmp += res.res.cmp;    }    res.cmp = cmp + 1;    return;  }  // перебираем варианты выбора номера тестируемого символа  let grp = new Set();  res.pos = {};  for (let i = off + lcp.length; i < len; i++) {    // группируем ключи по соответствующим значениям [i]-символа    let chr = keys.reduce((rv, key) => {      if (key.length < i) {        return rv;      }      let ch = key.charCodeAt(i);      rv[ch] = rv[ch] || [];      rv[ch].push(key);      return rv;    }, {});    // не обрабатываем повторно уже встречавшуюся комбинацию распределений ключей по группам    let cmb = Object.values(chr)      .map(seg => seg.join('\t'))      .sort()      .join('\n');    if (grp.has(cmb)) {      continue;    }    else {      grp.add(cmb);    }    let fl = true;    let cmp = 0;    for (let [ch, keys] of Object.entries(chr)) {      // упаковываем группы из единственного ключа      if (keys.length == 1) {        let key = keys[0];        chr[ch] = key;        cmp += key.length;      }      // для групп из нескольких ключей запускаем рекурсию      else {        fl = false;        chr[ch] = {src : keys};        fill(chr[ch], i + 1, hash);        cmp += chr[ch].res.cmp;      }    }    res.pos[i] = {      chr    , cmp    };    // запоминаем позицию "лучшего" символа    if (res.cmp === undefined || cmp + 1 < res.cmp) {      res.cmp = cmp + 1;      res.bst = i;    }    // если за каждым символом остался конкретный ключ, то другие варианты нам не нужны    if (fl) {      res.bst = i;      for (let j = off; j < i; j++) {        delete res.pos[j];      }      break;    }  }};// сжатие карты поиска в минимальный форматconst comp = obj => {  // удаляем служебные ключи  delete obj.src;  delete obj.cmp;  if (obj.res) {    let res = obj.res;    if (res.pos !== undefined) {      // сохраняем только оптимальный вариант проверяемого символа      obj.$pos = res.bst;      let $chr = res.pos[res.bst].chr;      Object.entries($chr).forEach(([key, val]) => {        // упаковываем содержимое ключа        comp(val);        // если внутри символа только список - "поднимаем" его на уровень выше        let keys = Object.keys(val);        if (keys.length == 1 && keys[0] == '$lst') {          $chr[key] = val.$lst;        }      });      // преобразуем объект с ключами-строками в Map с ключами-числами      obj.$chr = new Map(Object.entries($chr).map(([key, val]) => [Number(key), val]));    }    // при наличии списка "поднимаем" вложенные результаты на уровень выше    if (res.lst !== undefined) {      obj.$lst = res.lst;      delete res.lst;      if (res.res !== undefined) {        comp(res);        Object.assign(obj, res);      }    }    delete obj.res;  }};// поиск по карте - циклы вместо рекурсии и замыканийconst find = (str, off, map) => {  let curr = map;  do {    // по символу в позиции    let $pos = curr.$pos;    if ($pos !== undefined) {      let next = curr.$chr.get(str.charCodeAt(off + $pos));      if (typeof next === 'string') {   // значение ключа        if (str.startsWith(next, off)) {          return next;        }      }      else if (next instanceof Array) { // список ключей на проверку        for (let key of next) {          if (str.startsWith(key, off)) {            return key;          }        }      }      else if (next !== undefined) {    // вложенный map, если есть        curr = next;        continue;      }    }    // ищем по дополнительному списку, если он есть    if (curr.$lst) {      for (let key of curr.$lst) {        if (str.startsWith(key, off)) {          return key;        }      }    }    return;  }  while (true);};function ImmutableTrie(keys) {  this.map = {src : keys.sort((x, y) => x < y ? -1 : +1)};  fill(this.map);  comp(this.map);}const p = ImmutableTrie.prototype;p.get = function(line, off) {  return find(line, off || 0, this.map);};p.has = function(line, off) {  return this.get(line, off) !== undefined;};module.exports = ImmutableTrie;

Как можно заметить, при поиске в таком Immutable Trie ни одно животное не пострадало не создается никаких новых объектов в памяти, за которыми потом хотел бы придти GC.

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

const nodeIT = new ImmutableTrie(...);nodeIT.get('  ->  Parallel Seq Scan on abc', 6); // 'Parallel Seq Scan'

Ну а когда мы уже определились, где план начинается, ровно таким же способом (но уже с помощью Trie названий атрибутов) мы определяем строки, которые являются началом атрибута узла, а какие продолжение multiline string и склеиваем их:

Index Scan using pg_class_relname_nsp_index on pg_class  (cost=0.29..2.54 rows=1 width=265) (actual time=0.014..0.014 rows=0 loops=1)  Index Cond: (relname = 'мама\nмыла\nраму'::name)  Buffers: shared hit=2

Ну а в таком виде его разбирать уже намного проще.
Подробнее..

Как управлять CNC-роутером, не привлекая внимания

27.09.2020 18:10:26 | Автор: admin
Мой CNC-роутер служил верой и правдой два года, но что-то пошло не так слетела прошивка, а был это woodpecker 0.9.

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

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



Собственно идея контроллера для CNC-машины довольна проста и интересна. Есть несколько потоков обработки один читает данные (gcode) и разбирает их, второй превращает команды в блоки исполнения и третий (stepper) собственно исполняет эти блоки. Вот об этом третьем потоке и пойдет речь.

Степпер имеет дело со списком отдельных команд вида сделай (X,Y,Z) шагов для всех трёх (как минимум) шаговых двигателей, причем за указанное время и в заданном направлении (ну это так упрощенно). Надо сказать, что шаговый двигатель со своим драйвером довольно простая в управлении штука задаешь (0 или 1) направление вращения и затем по положительному перепаду входа (0 -> 1) двигатель пытается сделать один шаг (а всего на оборот обычно 200 шагов). Данные уже подготовлены, так что надо просто как-то соотнести 3 целых числа с заданным временем.

В оригинале у автора использован контроллер atmega328p, но практически без изменений все легко переносится на arm (например, stm32). Но вот сам алгоритм не может не вызывать вопросов.

С одной стороны, использован весьма совершенный алгоритм Брезенхэма, а точнее его разновидность Adaptive Multi-Axis Step-Smoothing. Но с другой стороны, как-то это все сложно и главное, плавность хода шагового мотора и точность работы роутера прямо зависят от точности выдачи сигналов управления. В данном случае это обуславливается частотой на которой работает таймер и временем обработки прерываний а это дает не более 40-50 кГц в лучшем случае, а обычно и того менее ну то есть точность задания управления 20-50 мксек.

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

Так как я рассматривал переход на cortex-m (ну точнее на stm32h750, который я очень люблю и который очень подешевел), то такая задача может быть решена вовсе без привлечения CPU только лишь с использованием двух каналов DMA и одного 32-битного счетчика.

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

Получится что-то вроде такого.

Обработка по прерыванию переключение на новый буфер (двойная буфферизация).

#define MAX_PGM 32typedef struct _pgm_buffer {        uint32_t data[MAX_PGM];        uint32_t delta[MAX_PGM];} pgm_buffer;pgm_buffer buf[2];uint32_t current_buf = 1;uint32_t flags = 0;void program_down(DMA_HandleTypeDef *_hdma) {        TIM2->CR1 &= ~TIM_CR1_CEN;        if ((flags & BUF_RUNNING) == 0)                return;        current_buf ^= 1;        DMA1_Channel5->CCR &= ~1;        DMA1_Channel2->CCR &= ~1;        DMA1_Channel5->CNDTR = MAX_PGM;        DMA1_Channel2->CNDTR = MAX_PGM;        DMA1_Channel5->CMAR = (uint32_t) (buf[current_buf].delta);        DMA1_Channel2->CMAR = (uint32_t) (buf[current_buf].data);        DMA1_Channel5->CCR |= 1;        DMA1_Channel2->CCR |= 1;        TIM2->CNT = 0;        TIM2->ARR = 8;        TIM2->EGR |= TIM_EGR_UG;        TIM2->CR1 |= TIM_CR1_CEN;}


Инициировать можно так:

       HAL_DMA_RegisterCallback(&hdma_tim2_up, HAL_DMA_XFER_CPLT_CB_ID,                        program_down);        HAL_DMA_Start_IT(&hdma_tim2_up, buf, &GPIOA->BSRR, MAX_PGM);        DMA1_Channel5->CCR &= ~1;        DMA1_Channel5->CPAR = &TIM2->ARR;        DMA1_Channel5->CCR |= 1;        TIM2->CCR1 = 1;        TIM2->DIER |= TIM_DIER_UDE | TIM_DIER_CC1DE;        flags |= BUF_RUNNING;


Ну а старт это:

        program_down(NULL);


Что это дает? Давайте подсчитаем на примере того же stm32h750. Таймер (TIM2) там работает на частоте 200 МГц, минимальное время задержки два такта, но DMA не может переслать данные быстрее 50МГц, то есть между двумя командами на переключение порта можно положить (с учетом возможной занятости шины) 40 нсек (25МГц) это в 1000 раз лучше исходной реализации!

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

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

Собственно выводы.

В мастерской лежит готовый CNC-станок с размером 1.2 метра на 0.8 метра с двигателями и драйверами, но без контроллера. Похоже, надо завершить работу и попробовать на нем, насколько это будет эпично. Если сделаю обязательно напишу продолжение. А пока я не понимаю, почему это контроллеры делают на atmega и они пищат на всех 3d-принтерах и cnc-роутерах на этих грубых прерываниях

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

Где порешать аналитические задачи от команд Яндекса? Контест и разбор

21.09.2020 18:14:10 | Автор: admin
Сегодня начинается пробный раунд чемпионата по программированию Yandex Cup. Это означает, что можно с помощью системы Яндекс.Контест решать задачи, подобные тем, которые будут в квалификационном раунде. Пока результат ни на что влияет.

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

A. Посчитать лгунов в стране

Решить в Контесте

В государстве живёт 10 000 человек. Они делятся на правдолюбов и лгунов. Правдолюбы говорят правду с вероятностью 80%, а лгуны с вероятностью 40%. Государство решило подсчитать правдолюбов и лгунов на основе опроса 100 жителей. Каждый раз случайно выбранного человека спрашивают: Вы лгун? и записывают ответ. Однако один человек может поучаствовать в опросе несколько раз. Если житель уже участвовал в опросе он отвечает то же самое, что и в первый раз. Мы знаем, что правдолюбов 70%, а лгунов 30%. Какая вероятность того, что государство недооценит количество лгунов, т. е. опрос покажет, что лгунов меньше 30%? Дайте ответ в процентах с точкой в качестве разделителя, результат округлите до сотых (пример ввода: 00.00).

Решение
1. Посчитаем вероятность получить ответ Да на вопрос Вы лгун?.

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

От правдолюбов, которых не спрашивали до этого: 0,2 * доля правдолюбов, которых не спрашивали.
От лгунов, которых не спрашивали до этого: 0,4 * доля лгунов, которых не спрашивали.
От правдолюбов, которых уже спрашивали до этого и которые ответили Да: 1,0 * доля правдолюбов, которых уже спрашивали и которые ответили Да.
От лгунов, которых уже спрашивали до этого и которые ответили Да: 1,0 * доля лгунов, которых уже спрашивали и которые ответили Да.

Посчитаем по шагам вероятность получить ответ Да от правдолюбов:

1. 0,2 * % правдолюбов.
2. 0,2 * (% правдолюбов % опрошенных правдолюбов) + 0,2 * (% опрошенных правдолюбов) = 0,2 * % правдолюбов.
3. Аналогично шагу 2.

То есть на каждом шаге вероятность получить ответ Да, я лгун от правдолюбов составляет 0,2 и не зависит от того, сколько правдолюбов опросили до этого. Точно так же для лгунов.

Таким образом, вероятность получить ответ Да от правдолюбов и лгунов: 0,2 * 0,7 + 0,4 * 0,3 = 0,26.

2. Посчитаем вероятность недооценить количество лгунов.

Количество лгунов, которое получит государство по результатам опроса, это биномиальное распределение с параметрами n = 100, p = 0,26.

Количеством успехов в нашем случае будет 30 (30% от 100 опрошенных). Если мы посмотрим на функцию распределения в этой точке, то получим P (x < 30) = 0,789458. Посчитать можно вот тут: stattrek.com/online-calculator/binomial.aspx.

Ответ в процентах, округлённых до сотых: 78,95.

B. Театральный сезон и телефоны

Решить в Контесте

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

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

Формат ввода

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

Формат вывода

Число уникальных номеров.

Решение
Технические особенности данных

Подробный вариант решения лежит в main.py.

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

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

1. 8-(801)-111-11-11
2. 8-801-111-11-11
3. 8801-111-11-11
4. 8-8011111111
5. +88011111111
6. 8-801-flowers, вместо цифр буквы (распространено в США)

Как предполагается обнаружить эти особенности:

1. Форматы в пунктах 14 видны при первом взгляде на данные и удаляются стандартными методами вроде replace.
2. Формат 5 легко отфильтровать, проверив число символов в телефонах после форматирования пункта 1. Во всех номерах будет 11 символов, кроме этого формата.
3. Пункт 6 самый неочевидный, надо догадаться проверить наличие нечисловых символов в номере телефона. Надеюсь, что смысл этих букв участник быстро найдёт в интернете.

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

Код. Как генерировались данные

Этот раздел для тех, кому надо разобраться в устройстве кода или изменить сгенерированные логи в ticket_logs.csv. Все действия сложены в logs_generator.py. Как запустить:

python logs_generator.py

На выходе получается файл ticket_logs.csv.

Конфигурационный файл config.yaml

В файле собраны все параметры, которые влияют на создание файла ticket_logs.csv:

  • zones коды зон, которые используются в генерируемых телефонных номерах.
  • seven_letter_words слова, которые используются для создания телефонных номеров с буквами.
  • letters_to_numbers_dict словарь соответствия цифр на клавиатуре телефона и алфавита. Вряд ли он изменится.
  • performances список спектаклей и их весов. Чем выше вес, тем чаще спектакль будет в логах ticket_logs.csv.

Полезные константы в файле logs_generator.py:

USERS_COUNT = 1000  # количество пользователей (можно сверять в решении main.py результат)RESULT_FILE_LOCATION = 'ticket_logs.csv'  # куда сохранять созданные логи

Как формируются телефонные номера

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

from yaml import load, FullLoaderfrom phone_numbers.phone_numbers_generator import PhonesGeneratorwith open('config.yaml') as f:    config = load(f, Loader=FullLoader)PhonesGenerator(config).generate_number()

Метод вернёт словарь с набором телефонных номеров. Пример:

{
'base': '8804academy', 'case_1': '8-(804)-aca-de-my', 'case_2': '8-804-aca-de-my',
'case_3': '8804-aca-de-my', 'case_4': '+8804academy', 'case_5': '8-804-academy',
'case_6': '8-804-2223369'
}


При многократном вызове метода generate_number в первую очередь отдаются номера с буквами. Слова в случайном порядке берутся из файла config.yaml, ключ seven_letter_words. Когда слова заканчиваются, то отдаются только числовые номера. Но можно и сразу генерировать числовые, для этого достаточно указать параметр generate_number(with_letters=False):

{
'base': '88062214016', 'case_1': '8-(806)-221-40-16', 'case_2': '8-806-221-40-16',
'case_3': '8806-221-40-16', 'case_4': '+88062214016', 'case_5': '8-806-2214016',
'case_6': '8-806-2214016'
}


В logs_generator.py из этого набора случайно выбирается от одного до некоторого набора вариантов. Подходящие варианты для числовых номеров задаёт константа PHONE_CASES, для буквенных PHONE_CASES_WITH_LETTERS в файле logs_generator.py. Сами форматы определяют методы build_case_1_number, ..., build_case_6_number в классе PhonesGenerator. Они же добавляются в конце метода generate_number.

Как генерируются названия спектаклей

Список спектаклей и их весов сложен в файле config.yaml. Чем выше вес, тем чаще спектакль будет в логах ticket_logs.csv. Этот процесс заложен в функции random_performance в logs_generator.py. Состав спектаклей:

  • Оперы: Севильский цирюльник, Волшебная флейта, Норма, Травиата, Евгений Онегин, Аида, Кармен, Свадьба Фигаро, Риголетто.
  • Балеты: Жизель, Лебединое озеро, Щелкунчик, Спящая красавица, Ромео и Джульетта, Дон Кихот, Баядерка, Спартак.
  • Мюзиклы: Вестсайдская история, TODD, Юнона и Авось, Ночь перед Рождеством, Чикаго, Ла-Ла Ленд, Нотр-Дам де Пари, Кошки.

Недостатки

Код класса PhonesGenerator слишком завязан на число символов в номере это можно улучшить.

C. Рассчитать pFound

Решить в Контесте

В архиве содержится три текстовых файла:

  • qid_query.tsv id запроса и текст запроса, разделённые табуляцией;
  • qid_url_rating.tsv id запроса, URL документа, релевантность документа запросу;
  • hostid_url.tsv id хоста и URL документа.

Нужно вывести текст запроса с максимальным значением метрики pFound, посчитанной по топ-10 документов. Выдача по запросу формируется по следующим правилам:
  • С одного хоста может быть только один документ на выдаче. Если для запроса есть несколько документов с одним и тем же id хоста берется максимально релевантный документ (а если несколько документов максимально релевантны, берется любой).
  • Документы по запросу сортируются по убыванию релевантности.
  • Если у нескольких документов с разных хостов релевантность одинакова, их порядок может быть произвольным.

Формула для расчёта pFound:

pFound = $\sum_{i=1}^{10}$pLook[i] pRel[i]
pLook[1] = 1
pLook[i] = pLook[i 1] (1 pRel[i 1]) (1 pBreak)
pBreak = 0,15

Формат вывода

Текст запроса с максимальным значением метрики. Например, для open_task.zip правильный ответ:
гугл переводчик

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

import pandas as pd# считываем данныеqid_query = pd.read_csv("hidden_task/qid_query.tsv", sep="\t", names=["qid", "query"])qid_url_rating = pd.read_csv("hidden_task/qid_url_rating.tsv", sep="\t", names=["qid", "url", "rating"])hostid_url = pd.read_csv("hidden_task/hostid_url.tsv", sep="\t", names=["hostid", "url"])# делаем join двух таблиц, чтобы было просто брать url с максимальным рейтингомqid_url_rating_hostid = pd.merge(qid_url_rating, hostid_url, on="url")def plook(ind, rels): if ind == 0: return 1    return plook(ind-1, rels)*(1-rels[ind-1])*(1-0.15)def pfound(group): max_by_host = group.groupby("hostid")["rating"].max() # максимальный рейтинг хоста top10 = max_by_host.sort_values(ascending=False)[:10] # берем топ-10 урлов с наивысшим рейтингом pfound = 0    for ind, val in enumerate(top10): pfound += val*plook(ind, top10.values) return pfoundqid_pfound = qid_url_rating_hostid.groupby('qid').apply(pfound) # группируем по qid и вычисляем pfoundqid_max = qid_pfound.idxmax() # берем qid с максимальным pfoundqid_query[qid_query["qid"] == qid_max]

D. Спортивный турнир

Решить в Контесте
Ограничение по времени на тест 2 с
Ограничение по памяти на тест 256 МБ
Ввод стандартный ввод или input.txt
Вывод стандартный вывод или output.txt
Пока Маша была в отпуске, её коллеги организовали турнир по шахматам по олимпийской системе. За отдыхом Маша не обращала особого внимания на эту затею, так что она еле может вспомнить, кто с кем играл (про порядок игр даже речи не идёт). Внезапно Маше пришла в голову мысль, что неплохо бы привезти из отпуска сувенир победителю турнира. Маша не знает, кто победил в финальной игре, но сможет без труда вычислить, кто в нём играл, если только она правильно запомнила играющие пары. Помогите ей проверить, так ли это, и определить возможных кандидатов в победители.

Формат ввода

В первой строке находится целое число 3n2161,n=2k1 количество прошедших игр. В последующих n строках по две фамилии игроков (латинскими заглавными буквами) через пробел. Фамилии игроков различны. Все фамилии уникальны, однофамильцев среди коллег нет.

Формат ввода

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

Пример 1
Ввод Вывод
7
GORBOVSKII ABALKIN
SIKORSKI KAMMERER
SIKORSKI GORBOVSKII
BYKOV IURKOVSKII
PRIVALOV BYKOV
GORBOVSKII IURKOVSKII
IURKOVSKII KIVRIN
IURKOVSKII GORBOVSKII
Пример 2
Ввод Вывод
3
IVANOV PETROV
PETROV BOSHIROV
BOSHIROV IVANOV
NO SOLUTION
Примечания

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

Схема первого теста из условия:



Решение
Из количества игрn = 2^k 1легко получить количество раундов турнираk.Обозначим количество игр, которые сыгралi-й участник, черезn_i.Очевидно, что финалисты сыграли максимальное количество раз (они единственные играли во всехkраундах).Теперь научимся проверять, что данный нам набор встреч между участниками возможен в турнире по олимпийской системе.Заметим, что игра между участникамиiиjмогла произойти только в раундеmin(n_i, n_j),поскольку этот раунд был последним для кого-то из них (раунды для удобства нумеруются с единицы).Назовём псевдораундом номерrмножество игр(i, j), для которыхmin(n_i, n_j) = r. Проверку корректности будем делать в соответствии с таким утверждением:

Утверждение.Набор из2^k 1игр задаёт турнир по олимпийской системе тогда и только тогда,когда:

1. В каждом псевдораунде все участники различны.
2. Количество игр в псевдораунде r равно 2^{k r}.

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

Во-первых, докажем, что каждый участник турнира играл в первом псевдораунде.Рассмотрим произвольного игрока,пусть он участвовал вqиграх.В каждом псевдораунде он мог сыграть не более одного раза,причём в псевдораундах послеq-го он не мог играть ни разу.Значит, он должен был сыгратьпо одному разу в каждом из псевдораундов1, 2, ..., q.Это, в частности, означает, что все люди сыграли в первомпсевдораунде, а всего игроков2^k.Теперь докажем, что в каждой из2^{k1}игр первого псевдораунда был ровно один участниксn_i = 1.Как минимум один такой участник в каждой игре должен быть по определению псевдораунда.

С другой стороны,есть не менее2^{k1}человек сn_i > 1 это участники следующего псевдораунда.Следовательно, людей сn_i = 1было ровно2^{k1}, по одному на каждую игру.Теперь легко понять, как должен выглядеть первый раундискомого турнира: назначим в каждой игре первого псевдораунда проигравшим участника сn_i = 1,а победителем участника сn_i > 1.Множество игр между победителями удовлетворяет условиюдляk1(после выбрасывания игр из первого псевдораунда всеn_iуменьшились на 1).Следовательно, этомумножеству соответствует турнир по олимпийской системе.

import sysimport collectionsdef solve(fname):    games = []    for it, line in enumerate(open(fname)):        line = line.strip()        if not line:            continue        if it == 0:            n_games = int(line)            n_rounds = n_games.bit_length()        else:            games.append(line.split())    gamer2games_cnt = collections.Counter()    rounds = [[] for _ in range(n_rounds + 1)]    for game in games:        gamer_1, gamer_2 = game        gamer2games_cnt[gamer_1] += 1        gamer2games_cnt[gamer_2] += 1    ok = True    for game in games:        gamer_1, gamer_2 = game        game_round = min(gamer2games_cnt[gamer_1], gamer2games_cnt[gamer_2])        if game_round > n_rounds:            ok = False            break        rounds[game_round].append(game)    finalists = list((gamer for gamer, games_cnt in gamer2games_cnt.items() if games_cnt == n_rounds))    for cur_round in range(1, n_rounds):        if len(rounds[cur_round]) != pow(2, n_rounds - cur_round):            ok = False            break        cur_round_gamers = set()        for gamer_1, gamer_2 in rounds[cur_round]:            if gamer_1 in cur_round_gamers or gamer_2 in cur_round_gamers:                ok = False                break            cur_round_gamers.add(gamer_1)            cur_round_gamers.add(gamer_2)    print ' '.join(finalists) if ok else 'NO SOLUTION'def main():    solve('input.txt')if name == '__main__':    main()



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

Определяем пульс по вебкамере в 50 строчек кода

01.09.2020 22:17:56 | Автор: admin

Привет Хабр.

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

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

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

Получаем данные с камеры

Сначала мы должны получить поток с вебкамеры, для чего воспользуемся OpenCV. Код является кроссплатформенным, и может работать как под Windows, так и под Linux/OSX.

import cv2import ioimport timecap = cv2.VideoCapture(0)cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1920)cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080)cap.set(cv2.CAP_PROP_FPS, 30)while(True):    ret, frame = cap.read()    # Our operations on the frame come here    img = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)    # Display the frame    cv2.imshow('Crop', crop_img)    if cv2.waitKey(1) &amp; 0xFF == ord('q'):        breakcap.release()cv2.destroyAllWindows()

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

x, y, w, h = 800, 500, 100, 100crop_img = img[y:y + h, x:x + w]cv2.imshow('Crop', crop_img)

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

Обработка

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

heartbeat_count = 128heartbeat_values = [0]*heartbeat_countheartbeat_times = [time.time()]*heartbeat_countwhile True:    ...    # Update the list    heartbeat_values = heartbeat_values[1:] + [np.average(crop_img)]    heartbeat_times = heartbeat_times[1:] + [time.time()]

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

Остается вывести график на экран в реальном времени:

fig = plt.figure()ax = fig.add_subplot(111)while(True):    ...    ax.plot(heartbeat_times, heartbeat_values)    fig.canvas.draw()    plot_img_np = np.fromstring(fig.canvas.tostring_rgb(), dtype=np.uint8, sep='')    plot_img_np = plot_img_np.reshape(fig.canvas.get_width_height()[::-1] + (3,))    plt.cla()        cv2.imshow('Graph', plot_img_np)

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

Собственно и все.

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

Возможно, из заголовка не совсем очевидно, но камера не прикладывается к коже, мы просто анализируем общую картинку с человеком на экране. И удивительно, что даже на таком расстоянии изменение оттенка кожи вполне уверенно фиксируется камерой! Разумеется, по клеточкам считать не точно, примерный пульс получился около 75bpm. Для сравнения, результат с поверенного китайскими мастерами пульсоксиметра:

Заключение

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

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

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

Spoiler
import numpy as npfrom matplotlib import pyplot as pltimport cv2import ioimport timecap = cv2.VideoCapture(0)cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1920)cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 1280)cap.set(cv2.CAP_PROP_FPS, 30)# Image cropx, y, w, h = 800, 500, 100, 100heartbeat_count = 128heartbeat_values = [0]*heartbeat_countheartbeat_times = [time.time()]*heartbeat_count# Matplotlib graph surfacefig = plt.figure()ax = fig.add_subplot(111)while(True):    # Capture frame-by-frame    ret, frame = cap.read()    # Our operations on the frame come here    img = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)    crop_img = img[y:y + h, x:x + w]    # Update the data    heartbeat_values = heartbeat_values[1:] + [np.average(crop_img)]    heartbeat_times = heartbeat_times[1:] + [time.time()]    # Draw matplotlib graph to numpy array    ax.plot(heartbeat_times, heartbeat_values)    fig.canvas.draw()    plot_img_np = np.fromstring(fig.canvas.tostring_rgb(), dtype=np.uint8, sep='')    plot_img_np = plot_img_np.reshape(fig.canvas.get_width_height()[::-1] + (3,))    plt.cla()    # Display the frames    cv2.imshow('Crop', crop_img)    cv2.imshow('Graph', plot_img_np)    if cv2.waitKey(1) & 0xFF == ord('q'):        breakcap.release()cv2.destroyAllWindows()

И как обычно, всем удачных экспериментов

Подробнее..

Перевод Текстовый индекс по котировкам в памяти на Go

09.09.2020 14:04:11 | Автор: admin

Недавно понадобилось реализовать поиск по началу строки, по сути WHERE name LIKE 'начало%'. Это был поиск по названию биржевых символов (AAPL, AMZN, EUR/USD и пр.). Хотелось, чтобы поиск работал быстро, и не нагружал лишний раз БД. В итоге пришел к реализации поиска по дереву в памяти, об этом и расскажу.

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

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

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

Структура дерева

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

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

  • AAA (BetaShares Australian High Interest Cash ETF, ASX),

  • AAA (All Active Asset Capital LTD, LSE).

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

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

Построение дерева для поиска

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

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

  1. Все разделители заменяются на пробелы.

  2. Двойные пробелы заменяются на одинарные.

  3. Обрезаются пробелы по краям строк.

  4. Строки переводятся в нижний регистр.

  5. Схожие символы приводятся к единому формату, например, a.

  6. Исключаются стоп-слова (опционально).

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

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

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

Тут на красной линии расположены на каждой вершине дерева элементы [A], [A], [P], [L]. В квадратных скобках обозначены ключи, которые используются на каждой вершине для индексации. Без скобок обозначены полные ключи, которые получаются при проходе от корня до вершины.

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

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

Поиск по дереву

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

  1. Выполним предварительную обработку искомой строки тем же алгоритмом как обрабатывали ключи перед индексацией (см. выше).

  2. Найдем в дереве элемент, в котором ключ совпадает с искомой строкой.

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

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

  • АА

  • ААА

  • АAAL

  • AAALF

  • AAAP

  • AAB

  • AAP

  • AAPJ

  • AAPL

  • AAPT

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

Поиск подходящего биржевого символа

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

  • Если вводят в поиске EUR, то в выдаче должны быть EUR, EUR/USD, USD/EUR. То есть поиск должен работать не только с начала строки, но и с начала каждого слова в строке.

  • Поиск должен работать не только по названию символа, но также и по названию компании. Например, при вводе в поиске APL, надо выдать в результатах APL, AAPL (Apple).

  • Первыми выдавать в поиске популярные символы.

Чтобы для EUR, в выдачу попали не только EUR, EUR/USD, но и USD/EUR, решил класть в индекс по несколько экземпляров значений с разными ключами: подстроки начиная с каждого слова индексируемой строки. Например, при индексации строки USD/EUR, в индекс попадают следующие ключи: usd eur, eur. При индексации строки Grupo Financiero Galicia SA в индекс попадают ключи Grupo Financiero Galicia SA, Financiero Galicia SA, Galicia SA, SA.

Также чтобы учесть вышеописанные нюансы, понадобилось выполнять поиск в 4 этапа.

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

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

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

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

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

  1. Индекс SearchSymbolIndex по символам со всех финансовых рынков.

  2. Индекс SearchPopularIndex только по популярным символам (10% от всех).

  3. Индекс SearchInstrumentIndex по названию компании.

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

var searchedData []searchindex.SearchDatasearchedData = r.SearchSymbolIndex.Search(searchindex.SearchParams{    Text: key,    OutputSize: outputSize,    Matching: searchsymbol.Strict,})searchedData = r.SearchPopularIndex.Search(searchindex.SearchParams{    Text: key,    OutputSize: outputSize,    Matching: searchsymbol.Beginning,    StartValues: searchedData,})searchedData = r.SearchSymbolIndex.Search(searchindex.SearchParams{    Text: key,    OutputSize: outputSize,    Matching: searchindex.Beginning,    StartValues: searchedData,})searchedData = r.SearchInstrumentIndex.Search(searchindex.SearchParams{    Text: key,    OutputSize: outputSize,    Matching: searchindex.Beginning,    StartValues: searchedData,})

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

searchindex.Strict поиск точных совпадений.

searchindex.Beginning поиск совпадений по началу строки.

Итого

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

Больших бенчмарков по производительности не делал, но на моих данных в 55000 строк создание трех индексов занимает примерно 2 секунды, это с учетом выборки из БД и дополнительных действий. А поиск в 4 последовательные итерации в трех индексах выполняется за 100-200 наносекунд (это если исключить время на обработку http запроса и считать только время поиска), что для моей задачи более чем достаточно.

Код в виде готового пакета: https://github.com/twelvedata/searchindex

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

Подробнее..

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

11.09.2020 10:13:26 | Автор: admin
image

Сегодня мы расскажем, как разрабатывали систему поиска скважин-кандидатов для гидравлического разрыва пласта (ГРП) с использованием машинного обучения (далее ML) и что из этого вышло. Разберёмся, зачем делать гидравлический разрыв пласта, при чём здесь ML, и почему наш опыт может оказаться полезен не только нефтяникам.

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

Зачем делать ГРП мы писали в наших предыдущих статьях тут и тут.

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

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

A. Автоматизировать обработку и анализ большого потока данных.
B. Уменьшить затраты и не упускать выгоду.
C. Сделать такую систему быстро и эффективно.

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

Как происходит подбор скважин на ГРП традиционным способом

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

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

image
Текущий процесс подбора скважин-кандидатов

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

Постановка задачи

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

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

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

image

Успешность ГРП это бинарная целевая переменная. Она определяется с помощью фактического значения прироста дебита нефти, который получили после ГРП. Если прирост больше некоего порога, определённого экспертом в доменной области, то значение признака успешности равно единице, в противном случае оно равно нулю. Таким образом мы формируем разметку для решения задачи классификации.

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

Для задачи регрессии были выбраны следующие метрики:

image

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

Для задачи классификации выбраны следующие метрики:

image
(вики),

Площадь под кривой ROCAUC (вики).

Ошибки, с которыми мы столкнулись

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

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

image

Ошибка 2 отсутствие глубокого понимания данных.

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

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

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

1. ТЕХНИЧЕСКАЯ ЧАСТЬ

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

image

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

2.1 ETL = Загрузка данных

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

image
Скриншот одной из трансформаций

Основные плюсы:

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

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

Для каждого факта ГРП выгружается более 400 параметров, описывающих работу скважины на момент проведения мероприятия, работу соседних скважин, а также информацию о проведённых ранее ГРП. Далее происходит преобразование и предобработка данных.

В качестве хранилища обработанных данных мы выбрали PostgreSQL. У него большой набор методов для работы с json. Так как мы храним итоговые датасеты в данном формате это стало решающим фактором.

Проект машинного обучения связан с постоянным изменением входных данных в силу добавления новых признаков, поэтому в качестве схемы базы данных используется Data Vault (ссылка на вики). Эта схема построения хранилищ позволяет быстро добавлять новые данные об объекте и не нарушать целостность таблиц и запросов.

2.2 Сервисы данных и моделей

После причёсывания и расчёта нужных показателей данные заливаются в БД. Здесь они хранятся и ждут, когда датасайнтист возьмёт их для создания ML модели. Для этого существует DataService сервис, написанный на Python и использующий gRPC протокол. Он позволяет получать датасеты и их метаданные (типы признаков, их описание, размер датасета и т. п.), загружать и выгружать прогнозы, управлять параметрами фильтрации и деления на train/test. Прогнозы в базе хранятся в формате json, что позволяет быстро получать данные и хранить не только значение прогноза, но и влияние каждого признака на этот конкретный прогноз.

image
Пример proto файла для сервиса данных.

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

image
Примерно так выглядит структура нашего сервиса моделей.

2.3 ML модель

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

Изначально рассматривалась возможность использования готовых AutoML библиотек, однако существующие решения оказались недостаточно гибкими для нашей задачи и не обладали всем нужным функционалом сразу (по просьбам трудящихся можем написать отдельную статью о нашем AutoML). Отметим лишь, что разработанный нами фреймворк содержит классы, используемые для предобработки датасета, генерации и отбора признаков. В качестве моделей машинного обучения используется привычный набор алгоритмов, которые наиболее успешно применялись нами ранее: реализации градиентного бустинга из библиотек xgboost, catboost, случайный лес из Sklearn, полносвязная нейронная сеть на Pytorch и т. д. После обучения AutoML возвращает sklearn пайплайн, который включает в себя упомянутые классы, а также ML модель, которая показала наилучший результат на кросс-валидации по выбранной метрике.

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

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

2.4 Интерфейс

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

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

image
Интерфейс модуля в приложении.

image
Так выглядит карточка скважины в приложении.

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

image

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

Также пользователь может посмотреть на аналоги интересующей скважины. Поиск аналогов реализован на стороне клиента с помощью алгоритма K-d дерево.

image

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

2. КАК М УЛУЧШАЛИ ML МОДЕЛЬ

image

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

1. Изменение метода заполнения пропусков

В самых первых моделях мы заполняли почти все пропуски в признаках средним, кроме категориальных для них использовалась самое часто встречающееся значение. В дальнейшем, при совместной работе аналитиков и эксперта, в доменной области удалось подобрать наиболее подходящие значения для заполнения пропусков в 80% признаков. Также мы испробовали ещё несколько методов заполнения пропусков с использованием библиотек sklearn и missingpy. Наилучшие результаты дали заполнение константой и KNNImputer до 5% MAPE.

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

2. Генерация признаков

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

image
Проверка гипотез, выдвигаемых командой, помогает вводить новые признаки.

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

image
Процесс создания признака на основе выделения кластеров.

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

3. Отбор признаков

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

А теперь про полученные метрики...

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

image

Стоит отметить, что результат выполнения ГРП также зависит от ряда внешних факторов, которые не прогнозируются. Поэтому о снижении МАРЕ до 0 говорить нельзя.

Заключение

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

Компания открыта для экспериментов, поэтому из списка были выбраны около 20 скважин, и на них провели операции гидроразрыва. Отклонение прогноза с фактическим значением запускного дебита нефти (МАРЕ) составило около 10%. И это очень неплохой результат!

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

Пишите вопросы и комментарии постараемся на них ответить.

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

SQL HowTo курсорный пейджинг с неподходящей сортировкой

05.09.2020 22:11:28 | Автор: admin
Этот пост родился как расширенный ответ на умозрительную задачу, обозначенную в статье Хроники пэйджинга.

Пусть у нас есть реестр документов, с которым работают операторы или бухгалтеры в СБИС, вроде такого:



Традиционно, при подобном отображении используется или прямая (новые снизу) или обратная (новые сверху) сортировка по дате и порядковому идентификатору, назначаемому при создании документа ORDER BY dt, id или ORDER BY dt DESC, id DESC.

Типичные возникающие при этом проблемы я уже рассматривал в статье PostgreSQL Antipatterns: навигация по реестру. Но что если пользователю зачем-то захотелось нетипичного например, отсортировать одно поле так, а другое этак ORDER BY dt, id DESC? Но второй индекс мы создавать не хотим ведь это замедление вставки и лишний объем в базе.

Можно ли решить эту задачу, эффективно используя только индекс (dt, id)?

Давайте сначала нарисуем, как упорядочен наш индекс:



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

Теперь предположим, что мы находимся в точке (A, 2) и хотим прочитать следующие 6 записей в сортировке ORDER BY dt, id DESC:



Ага! Мы выбрали какой-то кусочек из первого узла A, другой кусочек из последнего узла C и все записи из узлов между ними (B). Каждый такой блок вполне успешно ложится на чтение по индексу (dt, id), несмотря на не вполне подходящий порядок.

Давайте попробуем сконструировать такой запрос:

  • сначала читаем из блока A влево от стартовой записи получаем N записей
  • дальше читаем L - N вправо от значения A
  • находим в последнем блоке максимальный ключ C
  • отфильтровываем из предыдущей выборки все записи этим ключом и вычитываем его справа

А теперь попробуем изобразить в коде и проверить на модели:

CREATE TABLE doc(  id    serial, dt    date);CREATE INDEX ON doc(dt, id); -- наш индекс-- случайно "раскидаем" документы по последнему годуINSERT INTO doc(dt)SELECT  now()::date - (random() * 365)::integerFROM  generate_series(1, 10000);

Чтобы не вычислять количество уже прочитанных записей и разность между ним и целевым количеством, заставим это делать PostgreSQL с помощью хака UNION ALL и LIMIT:

(  ... LIMIT 100)UNION ALL(  ... LIMIT 100)LIMIT 100

Теперь соберем начитку следующих 100 записей с целевой сортировкой (dt, id DESC) от последнего известного значения:

WITH src AS (  SELECT    '{"dt" : "2019-09-07", "id" : 2331}'::json -- "опорный" ключ), pre AS (  (    ( -- читаем не более 100 записей "влево" от опорной точки из "левого" значения A      SELECT        *      FROM        doc      WHERE        dt = ((TABLE src) ->> 'dt')::date AND        id < ((TABLE src) ->> 'id')::integer      ORDER BY        dt DESC, id DESC      LIMIT 100    )    UNION ALL    ( -- дочитываем до 100 записей "вправо" от исходного значения "верхнего" ключа A -> B, C      SELECT        *      FROM        doc      WHERE        dt > ((TABLE src) ->> 'dt')::date      ORDER BY        dt, id      LIMIT 100    )  )  LIMIT 100)-- находим крайний правый ключ C в том, что прочитали, maxdt AS (  SELECT    max(dt)  FROM    pre  WHERE    dt > ((TABLE src) ->> 'dt')::date)(  ( -- вычищаем "левые" записи по ключу C    SELECT      *    FROM      pre    WHERE      dt <> (TABLE maxdt)    LIMIT 100  )  UNION ALL  ( -- дочитываем "правые" записи по ключу C до 100 штук    SELECT      *    FROM      doc    WHERE      dt = (TABLE maxdt)    ORDER BY      dt DESC, id DESC    LIMIT 100  )  LIMIT 100)ORDER BY  dt, id DESC; -- накладываем целевую сортировку, поскольку записи по B у нас лежат не в том порядке

Посмотрим, что получилось в плане:


[посмотреть на explain.tensor.ru]

  • Итак, по первому ключу A = '2019-09-07' мы прочитали 3 записи.
  • К ним дочитали еще 97 по B и C за счет точного попадания в Index Scan.
  • Среди всех записей отфильтровали 18 по максимальному ключу C.
  • Дочитали 23 записи (вместо 18 искомых из-за Bitmap Scan) по максимальному ключу.
  • Все пересортировали и отсекли целевые 100 записей.
  • и на все это ушло меньше миллисекунды!

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

GetHashCode() и философский камень, или краткий очерк о граблях

11.09.2020 20:19:02 | Автор: admin

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

Возможно, такая хорошая осведомленность о предмете нашей беседы, может сослужить и плохую службу, вселяя ложное чувство уверенности: "Это ж так просто! Что тут может пойти не так?"

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

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

Хэш-таблица для самых маленьких

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

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

Теплая ламповая хэш-таблицаТеплая ламповая хэш-таблица

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

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

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

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

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

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

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

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

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

Ну а теперь перейдем к реальным (ну или почти реальным) примерам.

Хэш, кеш и EF

На коленке написанная подсистема по работе с документами. Документ - это такая простая штука вида

public class Document{  public Int32 Id {get; set;}  public String Name {get; set;}  ...}

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

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

HashSet<Document> _openDocuments;

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

var newDocument = new Document(); // document is created_openDocuments.Add(newDocument); // document is open, nobody else can edit it.context.Documents.Add(newDocument);await context.SaveChangesAsync(); // so it's safe to write the document to the DB

Как вы думаете, чему равно значение переменной test в следующей строке, которая выполнится сразу после написанного выше кода?

Boolean test = _openDocuments.Contains(newDocument);

Разумеется, false, иначе бы этой статьи тут не было. Дьявол обычно кроется в деталях, а в нашем случае - в политике EF и в троеточиях объявления класса Document.

Для EF свойство Id выступает в роли первичного ключа, поэтому заботливая ORM по умолчанию мапит его на автоинкрементное поле базы данных. Таким образом, в момент создания объекта его Id равен 0, а сразу после записи в базу ему присваевается какое-то осмысленное значение:

var newDocument = new Document(); // newDocument.Id == 0_openDocuments.Add(newDocument);context.Documents.Add(newDocument);await context.SaveChangesAsync(); // newDocument.Id == 42

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

public class Document{public Int32 Id {get; set;}public String Name {get; set;}  public override int GetHashCode() {    return Id; }}

А вот теперь пазл складывается: записали мы в хэш-таблицу объект с хэш-кодом 0, а позже попросили объект с кодом 42.

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

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

Квадратно-гнездовой метод

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

private static IEnumerable<Size> FilterRectangles(IEnumerable<Size> rectangles){HashSet<Size> result = new HashSet<Size>();foreach (var rectangle in rectangles)    result.Add(rectangle);return result;}

Вроде бы и работает, но вовремя заметили, что производительность фильтрации как-то тяготеет к O(n^2), а не к более приятному O(n). Но постойте, классики Computer Science, ошибаться, конечно, могут, но не так фатально.

HashSet опять же самая обычная, да и Size - весьма тривиальная структура из FCL. Хорошо, что догадались проверить, какие же хэш-коды генерируются:

    var a = new Size(20,20).GetHashCode(); // a == 0     var b = new Size(30,30).GetHashCode(); // b == 0

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

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

var a = new SizeF(20,20).GetHashCode(); // a == 346948956var b = new SizeF(30,30).GetHashCode(); // b == 346948956

Нет, a и b теперь не равны примитивному нулю! Теперь это истинно случайное значение 346948956...

Вместо заключения

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

var a = Int64.MinValue.GetHashCode(); // a == 0var b = Int64.MaxValue.GetHashCode(); // a == 0

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

А будут ли выводы? Ну, давайте:

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

  2. При написании хэш-функции рекомендуется хорошенько подумать... либо использовать специальные кодогенераторы (см. в сторону Resharper).

  3. Верить никому нельзя. Мне - можно.

Подробнее..

Математика нужна программистам, или задача, которую мне пришлось решать

19.09.2020 00:21:00 | Автор: admin

Всем привет!

Я работаю над WebRTC - фреймворком для аудио-видео конференций (или звонков? проще говоря - real time communication). В этой статье я хочу описать интересную задачу и как она была решена. В задаче, по сути, потребовалось минимизировать lcm нескольких вещественных чисел с дополнительными ограничениями. Пришлось применить совсем чуть чуть теории чисел или хотя бы логики.

Если вам интересна только задача - то можете смело проматывать до секции "Формулировка задачи". Следующая секция объясняет откуда она такая взялась и в чем ее смысл.

Введение

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

Но нельзя просто задать желаемые разрешения, нет - это было бы слишком просто. Дело в том, что источник (например, камера в хроме) может выдавать видео какого угодно разрешения. А еще есть механизм обратной связи и при высокой нагрузке на CPU входящее разрешение снижается. Короче говоря, пользователь задает коэффициенты масштабированияS_i \ge 1.0. Потом входящий кадр сжимается в заданное количество раз, кодируется и отправляется по сети получателям.

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

Самый эффективный способ этого добиться, это требовать от источника, чтобы разрешение делилось на некоторое заданное число: alignment. Например, для стандартных коэффициентов {1.0, 2.0, 4.0} и требования четности для энкодера, можно легко попросить у источникаalignment=8. Источник чуть-чуть обрежет изображения. Это приведет к незначительному искажению соотношения сторон у видео, зато сделает переключение между потоками незаметным. В итоге, входящее разрешение, кратное 8 можно спокойно делить в 1, 2 или 4 раза и получать четное разрешение, которое енкодер с радостью закодирует.

Но что делать, если заданы коэффициенты {1, 1.7, 2.3}? Минимальное целое, "делящееся" нацело на все эти коэффициенты - 391. А чтобы результат был четным, нужно вообще взять 782. Согласитесь, это весьма нагло требовать от источника выдавать разрешение, делящееся на 782. Это значит, что даже VGA (640x480) видео уже не послать вообще никак. На практике - максимально допустимое выравнивание, которое мы можем попросить должно быть ограничено, чтобы, во-первых, допускать маленькие разрешения и, во-вторых, не очень сильно искажать соотношение сторон.

Но, раз уж мы уже немного искажаем настройки пользователя, округляя входящее разрешение, то почему бы и не округлить чуть чуть коэффициенты масштабирования? Например, можно было бы взять коэффициенты {1, 1.6, 2.4} вместо {1, 1.7, 2.3} и получить необходимую делимость в 48 (сильно лучше 782). Если еще больше поменять коэффициенты, то можно получить и меньшее выравнивание.

Формулировка задачи

Дано: d \in \mathbb{N},\ S_i \ge 1, S_i \in \mathbb{R}, i=1..n

Найти: A \in \mathbb{N},\ A \le MaxA,\ S'_i \in \mathbb{R} ,\ S'_i \ge 1,\ i=1..n

При условии:

\sum_{i=1}^n\left(S_i -S'_i\right)^2 \rightarrow min\frac{A}{S'_i \cdot d} \in \mathbb{N}, i=1..n \ \ \ \ \ \ \ \ \ (1)

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

Решение

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

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

Поскольку в условии A/(S'_i \cdot d), A, d \in \mathbb{N} , то получается, что S'_i \in \mathbb{Q} илиS'_i = N_i/D_i. Потому что только рациональные числа можно домножить и поделить на целое и получить в итоге целое.

Можно потребовать, чтобы дробь была неприводимая: GCD(N_i, D_i) = 1

Подставим дробь в (1) и получим \frac{A \cdot D_i}{N_i \cdot d} \in \mathbb{N} откуда следует, что

N_i \cdot d \vert A \cdot D_i \ \ \ \ \ \ \ (2)

(запись означает: левая часть делит правую).

Тут немного теории чисел или просто логики. N_i взаимно просто сD_i по условию, но делит правую часть. Значит N_i целиком содержится в оставшемся множителе или N_i \vert A , отсюда можно записать

A=N_i \cdot f,\ f \in \mathbb{N} \ \ \ \ \ \ (3)

Далее, домножим обе части уравнения (2) на f:

f \cdot N_i \cdot d \vert f\cdot A \cdot D_i

Подставим выражение (3) для A выше:

A \cdot d \vert f \cdot A \cdot D_i

сократим A

d \vert f \cdot D_i

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

f \cdot D_i = k \cdot d, k \in \mathbb{N} \ \ \ \ \ \ \ \ \ \ (4)

Теперь вспомним выражение для S'_i в виде дроби и домножим числитель и знаменатель на f и применим (3) и (4):

S'_i = \frac{N_i\cdot f}{ D_i \cdot f} = \frac{A}{f \cdot D_i} = \frac{A}{k \cdot d},\ \ k \in \mathbb{N} \ \ \ \ \ \ \ \ (5)

Добавив к этому условие, что коэффициенты S'_i не могут быть меньше 1 (ведь растягивать изображения смысла вообще нет) мы получим:

k \le \frac{A}{d} \ \ \ \ \ \ \ (6)

Таким образом, из условия (1) мы получили (5) и (6), которые говорят, что искомый коэффициент должен быть представим в виде дроби у которой числитель равен A , а знаменатель делиться на d и не превосходит числитель. При чем любая такая дробь нам подходит. Из (6) следует что таких дробей мало, а значит их все можно перебрать.

А можно и не перебирать. Ведь целевая функция, если рассматривать непрерывнуюk выпуклая и имеет минимум, равный 0, в точке k^*=\frac{A}{S_i \cdot d} . Значит, достаточно рассмотреть 2 ближайших целых значения k=min\{\lfloor k^* \rfloor ,\ \lceil k^* \rceil\} . Все, что левее левой точки - хуже ее, ведь она сама левее минимума выпуклой функции. Аналогично и с правой точкой. Еще надо аккуратно проверить, что эти точки положительные и удовлетворяют (6).

Итого, получаем такое решение (тут для простоты нет проверок входных данных):

const int kMaxAlignment = 16;// Находит лучшее приближение scale_factor (S_i) при заданном // выравнивании энкодера (d) и результирующем выравнивании источника (A).// Ошибка приближения прибавляется к error_acc.float GetApprox(int encoder_alignment, int requested_alignment,                 float scale_factor, float *error_acc) {  int k = static_cast<int> ((requested_alignment + 0.0) /                             (encoder_alignment * scale_factor));  float best_error = 1e90;  float best_approx = 1.0;  for (int i = 0; i < 2; i++, k++) {    if (k == 0 || k * encoder_alignment > requested_alignment) continue;    float approx = (requested_alignment +0.0) / (k * encoder_alignment);    float error = (approx - scale_factor) * (approx - scale_factor);    if (error < best_error) {      best_error = error;      best_approx = approx;    }  }  *error_acc += best_error;  return best_approx;}// Решает задачу. Возвращает измененные коэффициенты (S'_i)// и результирующее выравнивание (A) в параметре requested_alignment.std::vector<float> CalulateAlignmentAndScaleFactors(    int encoder_alignment, std::vector<float> scale_factors,     int *requested_alignment) {  float best_error = 1e90;  int best_alignment = 1;  std::vector<float> best_factors;  std::vector<float> cur_factors;    for (int a = 1; a <= kMaxAlignment; ++a) {    float cur_error = 0;    cur_factors.clear();    for (float factor: scale_factors) {      float approx = GetApprox(encoder_alignment, a, factor, &cur_error);      cur_factors.push_back(approx);    }    if (cur_error < best_error) {      best_error = cur_error;      best_factors = cur_factors;      best_alignment = a;    }  }  *requested_alignment = best_alignment;  return best_factors;}

Заключение

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

Да, без математики еще можно убедить себя, что выданные этим кодом коэффициенты будут подходить под условие задачи (числитель делит вычисленное выравнивание, поэтому все поделиться нацело, а знаменатель дает делимость на необходимое выравнивание для энкодера). Но без цепочки рассуждений (1) => (4),(5) вообще неясно, как этот код находит оптимальное решение.

Подробнее..

Квантовый хакинг, вычисления, алгоритмы и машинное обучение на практике дайджест Университета ИТМО

20.09.2020 12:13:41 | Автор: admin
Это подборка текстовых материалов и тематических подкастов с участием представителей Университета ИТМО студентов, аспирантов, научных сотрудников и преподавателей. Мы обсуждаем научные статьи, делимся личным опытом разработки проектов различного уровня и говорим о возможностях для развития, которыми располагает первый неклассический.


Фото Claudio Schwarz | @purzlbaum (Unsplash.com)

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

Как ускорить открытия в области квантовой физики. Алексей Мельников, представляющий сразу несколько организаций, в том числе и Университет ИТМО, специализируется на квантовом машинном обучении и системах ИИ для ученых. В прошлом году он получил престижную премию Национальной академии наук США Cozzarelli Prize за работу по теме Active learning machine learns to create new quantum experiments. Рассказываем простыми словами о том, как подход, предложенный Алексеем и его коллегами, помогает ускорить научные исследования в области квантовой физики: так, в процессе дизайна экспериментов применяется машинное обучение с подкреплением, что позволяет в разы повысить эффективность подготовки к осуществлению опытов.

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

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


Как определить подлинность подписи. Рассказ о статье Юрия Рождественского, д.ф.-м.н. и профессора факультета фотоники и оптоинформатики, и его коллег Семена Рудого и Татьяны Вовк. Речь идет о вычислении фрактальной размерности Минковского для траектории кончика пера. В материале объясняем все простыми словами и приводим примеры.

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

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


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



Что еще у нас есть на Хабре:



Подробнее..

ANYKS Spell-checker

20.09.2020 22:11:04 | Автор: admin
image

Здравствуйте, это моя третья статья на хабре, ранее я писал статью о языковой модели ALM. Сейчас, я хочу познакомить вас с системой исправления опечаток ASC (реализованной на основе ALM).

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

Список возможностей:


  1. Исправление ошибок в словах с разницей до 4-х дистанций по Левенштейну.
  2. Исправление опечаток в словах (вставка, удаление, замещение, перестановка) символов.
  3. Ёфикация с учётом контекста.
  4. Простановка регистра первой буквы слова, для (имён собственных и названий) с учётом контекста.
  5. Разбиение объединённых слов на отдельные слова, с учётом контекста.
  6. Выполнение анализа текста без корректировки исходного текста.
  7. Поиск в тексте наличия (ошибок, опечаток, неверного контекста).


Поддерживаемые операционные системы:


  • MacOS X
  • FreeBSD
  • Linux


Написана система на С++11, есть порт для Python3

Готовые словари


Название Размер (Гб) Оперативная память (Гб) Размер N-грамм Язык
wittenbell-3-big.asc 1.97 15.6 3 RU
wittenbell-3-middle.asc 1.24 9.7 3 RU
mkneserney-3-middle.asc 1.33 9.7 3 RU
wittenbell-3-single.asc 0.772 5.14 3 RU
wittenbell-5-single.asc 1.37 10.7 5 RU

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


Для проверки работы системы использовались данные соревнованияисправления опечаток 2016 года от Dialog21. Для тестирования использовался обученный бинарный словарь:wittenbell-3-middle.asc
Проводимый тест Precision Recall FMeasure
Режим исправления опечаток 76.97 62.71 69.11
Режим исправления ошибок 73.72 60.53 66.48

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

Материалы использовавшиеся в тестировании


  • test.txt- Текст для тестирования
  • correct.txt- Текст корректных вариантов
  • evaluate.py- Скрипт Python3 для расчёта результатов коррекции


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

Для сравнения возьмём систему исправления опечаток, которую я упоминал выше JamSpell.

ASC vs JamSpell


Установка
ASC
$ git clone --recursive https://github.com/anyks/asc.git$ cd ./asc$ mkdir ./build$ cd ./build$ cmake ..$ make

JamSpell
$ git clone https://github.com/bakwc/JamSpell.git$ cd ./JamSpell$ mkdir ./build$ cd ./build$ cmake ..$ make


Обучение
ASC

train.json
{  "ext": "txt",  "size": 3,  "alter": {"е":"ё"},  "debug": 1,  "threads": 0,  "method": "train",  "allow-unk": true,  "reset-unk": true,  "confidence": true,  "interpolate": true,  "mixed-dicts": true,  "only-token-words": true,  "locale": "en_US.UTF-8",  "smoothing": "wittenbell",  "pilots": ["а","у","в","о","с","к","б","и","я","э","a","i","o","e","g"],  "corpus": "./texts/correct.txt",  "w-bin": "./dictionary/3-middle.asc",  "w-vocab": "./train/lm.vocab",  "w-arpa": "./train/lm.arpa",  "mix-restwords": "./similars/letters.txt",  "alphabet": "абвгдеёжзийклмнопрстуфхцчшщъыьэюяabcdefghijklmnopqrstuvwxyz",  "bin-code": "ru",  "bin-name": "Russian",  "bin-author": "You name",  "bin-copyright": "You company LLC",  "bin-contacts": "site: https://example.com, e-mail: info@example.com",  "bin-lictype": "MIT",  "bin-lictext": "... License text ...",  "embedding-size": 28,  "embedding": {      "а": 0, "б": 1, "в": 2, "г": 3, "д": 4, "е": 5,      "ё": 5, "ж": 6, "з": 7, "и": 8, "й": 8, "к": 9,      "л": 10, "м": 11, "н": 12, "о": 0, "п": 13, "р": 14,      "с": 15, "т": 16, "у": 17, "ф": 18, "х": 19, "ц": 20,      "ч": 21, "ш": 21, "щ": 21, "ъ": 22, "ы": 23, "ь": 22,      "э": 5, "ю": 24, "я": 25, "<": 26, ">": 26, "~": 26,      "-": 26, "+": 26, "=": 26, "*": 26, "/": 26, ":": 26,      "%": 26, "|": 26, "^": 26, "&": 26, "#": 26, "'": 26,      "\\": 26, "0": 27, "1": 27, "2": 27, "3": 27, "4": 27,      "5": 27, "6": 27, "7": 27, "8": 27, "9": 27, "a": 0,      "b": 2, "c": 15, "d": 4, "e": 5, "f": 18, "g": 3,      "h": 12, "i": 8, "j": 6, "k": 9, "l": 10, "m": 11,      "n": 12, "o": 0, "p": 14, "q": 13, "r": 14, "s": 15,      "t": 16, "u": 24, "v": 21, "w": 22, "x": 19, "y": 17, "z": 7  }}

$ ./asc -r-json ./train.json

Приведу также пример на языке Python3
import ascasc.setSize(3)asc.setAlmV2()asc.setThreads(0)asc.setLocale("en_US.UTF-8")asc.setOption(asc.options_t.uppers)asc.setOption(asc.options_t.allowUnk)asc.setOption(asc.options_t.resetUnk)asc.setOption(asc.options_t.mixDicts)asc.setOption(asc.options_t.tokenWords)asc.setOption(asc.options_t.confidence)asc.setOption(asc.options_t.interpolate)asc.setAlphabet("абвгдеёжзийклмнопрстуфхцчшщъыьэюяabcdefghijklmnopqrstuvwxyz")asc.setPilots(["а","у","в","о","с","к","б","и","я","э","a","i","o","e","g"])asc.setSubstitutes({'p':'р','c':'с','o':'о','t':'т','k':'к','e':'е','a':'а','h':'н','x':'х','b':'в','m':'м'})def statusArpa1(status):    print("Build arpa", status)def statusArpa2(status):    print("Write arpa", status)def statusVocab(status):    print("Write vocab", status)def statusIndex(text, status):    print(text, status)def status(text, status):    print(text, status)asc.collectCorpus("./texts/correct.txt", asc.smoothing_t.wittenBell, 0.0, False, False, status)asc.buildArpa(statusArpa1)asc.writeArpa("./train/lm.arpa", statusArpa2)asc.writeVocab("./train/lm.vocab", statusVocab)asc.setCode("RU")asc.setLictype("MIT")asc.setName("Russian")asc.setAuthor("You name")asc.setCopyright("You company LLC")asc.setLictext("... License text ...")asc.setContacts("site: https://example.com, e-mail: info@example.com")asc.setEmbedding({     "а": 0, "б": 1, "в": 2, "г": 3, "д": 4, "е": 5,     "ё": 5, "ж": 6, "з": 7, "и": 8, "й": 8, "к": 9,     "л": 10, "м": 11, "н": 12, "о": 0, "п": 13, "р": 14,     "с": 15, "т": 16, "у": 17, "ф": 18, "х": 19, "ц": 20,     "ч": 21, "ш": 21, "щ": 21, "ъ": 22, "ы": 23, "ь": 22,     "э": 5, "ю": 24, "я": 25, "<": 26, ">": 26, "~": 26,     "-": 26, "+": 26, "=": 26, "*": 26, "/": 26, ":": 26,     "%": 26, "|": 26, "^": 26, "&": 26, "#": 26, "'": 26,     "\\": 26, "0": 27, "1": 27, "2": 27, "3": 27, "4": 27,     "5": 27, "6": 27, "7": 27, "8": 27, "9": 27, "a": 0,     "b": 2, "c": 15, "d": 4, "e": 5, "f": 18, "g": 3,     "h": 12, "i": 8, "j": 6, "k": 9, "l": 10, "m": 11,     "n": 12, "o": 0, "p": 14, "q": 13, "r": 14, "s": 15,     "t": 16, "u": 24, "v": 21, "w": 22, "x": 19, "y": 17, "z": 7}, 28)asc.saveIndex("./dictionary/3-middle.asc", "", 128, statusIndex)

JamSpell

$ ./main/jamspell train ../test_data/alphabet_ru.txt ../test_data/correct.txt ./model.bin


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

spell.json
{    "debug": 1,    "threads": 0,    "method": "spell",    "spell-verbose": true,    "confidence": true,    "mixed-dicts": true,    "asc-split": true,    "asc-alter": true,    "asc-esplit": true,    "asc-rsplit": true,    "asc-uppers": true,    "asc-hyphen": true,    "asc-wordrep": true,    "r-text": "./texts/test.txt",    "w-text": "./texts/output.txt",    "r-bin": "./dictionary/3-middle.asc"}

$ ./asc -r-json ./spell.json

Пример на языке Python3
import ascasc.setAlmV2()asc.setThreads(0)asc.setOption(asc.options_t.uppers)asc.setOption(asc.options_t.ascSplit)asc.setOption(asc.options_t.ascAlter)asc.setOption(asc.options_t.ascESplit)asc.setOption(asc.options_t.ascRSplit)asc.setOption(asc.options_t.ascUppers)asc.setOption(asc.options_t.ascHyphen)asc.setOption(asc.options_t.ascWordRep)asc.setOption(asc.options_t.mixDicts)asc.setOption(asc.options_t.confidence)def status(text, status):    print(text, status)asc.loadIndex("./dictionary/3-middle.asc", "", status)f1 = open('./texts/test.txt')f2 = open('./texts/output.txt', 'w')for line in f1.readlines():    res = asc.spell(line)    f2.write("%s\n" % res[0])f2.close()f1.close()

JamSpell

Так-как версия для Python у меня не собралась, пришлось написать небольшое приложение на C++
#include <fstream>#include <iostream>#include <jamspell/spell_corrector.hpp>// Если используется BOOST#ifdef USE_BOOST_CONVERT#include <boost/locale/encoding_utf.hpp>// Если нужно использовать стандартную библиотеку#else#include <codecvt>#endifusing namespace std;/** * convert Метод конвертирования строки utf-8 в строку * @param  str строка utf-8 для конвертирования * @return     обычная строка */const string convert(const wstring & str){// Результат работы функцииstring result = "";// Если строка переданаif(!str.empty()){// Если используется BOOST#ifdef USE_BOOST_CONVERT// Объявляем конвертерusing boost::locale::conv::utf_to_utf;// Выполняем конвертирование в utf-8 строкуresult = utf_to_utf <char> (str.c_str(), str.c_str() + str.size());// Если нужно использовать стандартную библиотеку#else// Устанавливаем тип для конвертера UTF-8using convert_type = codecvt_utf8 <wchar_t, 0x10ffff, little_endian>;// Объявляем конвертерwstring_convert <convert_type, wchar_t> conv;// wstring_convert <codecvt_utf8 <wchar_t>> conv;// Выполняем конвертирование в utf-8 строкуresult = conv.to_bytes(str);#endif}// Выводим результатreturn result;}/** * convert Метод конвертирования строки в строку utf-8 * @param  str строка для конвертирования * @return     строка в utf-8 */const wstring convert(const string & str){// Результат работы функцииwstring result = L"";// Если строка переданаif(!str.empty()){// Если используется BOOST#ifdef USE_BOOST_CONVERT// Объявляем конвертерusing boost::locale::conv::utf_to_utf;// Выполняем конвертирование в utf-8 строкуresult = utf_to_utf <wchar_t> (str.c_str(), str.c_str() + str.size());// Если нужно использовать стандартную библиотеку#else// Объявляем конвертер// wstring_convert <codecvt_utf8 <wchar_t>> conv;wstring_convert <codecvt_utf8_utf16 <wchar_t, 0x10ffff, little_endian>> conv;// Выполняем конвертирование в utf-8 строкуresult = conv.from_bytes(str);#endif}// Выводим результатreturn result;}/** * safeGetline Функция извлечения строки из текста * @param  is файловый поток * @param  t  строка для извлечения текста * @return    файловый поток */istream & safeGetline(istream & is, string & t){// Очищаем строкуt.clear();istream::sentry se(is, true);streambuf * sb = is.rdbuf();for(;;){int c = sb->sbumpc();switch(c){ case '\n': return is;case '\r':if(sb->sgetc() == '\n') sb->sbumpc();return is;case streambuf::traits_type::eof():if(t.empty()) is.setstate(ios::eofbit);return is;default: t += (char) c;}}}/*** main Главная функция приложения*/int main(){// Создаём корректорNJamSpell::TSpellCorrector corrector;// Загружаем модель обученияcorrector.LoadLangModel("model.bin");// Открываем файл на чтениеifstream file1("./test_data/test.txt", ios::in);// Если файл открытif(file1.is_open()){// Строка чтения из файлаstring line = "", res = "";// Открываем файл на чтениеofstream file2("./test_data/output.txt", ios::out);// Если файл открытif(file2.is_open()){// Считываем до тех пор пока все удачноwhile(file1.good()){// Считываем строку из файлаsafeGetline(file1, line);// Если текст получен, выполняем коррекциюif(!line.empty()){// Получаем исправленный текстres = convert(corrector.FixFragment(convert(line)));// Если текст получен, записываем его в файлif(!res.empty()){// Добавляем перенос строкиres.append("\n");// Записываем результат в файлfile2.write(res.c_str(), res.size());}}}// Закрываем файлfile2.close();}// Закрываем файлfile1.close();}    return 0;}

Компилируем и запускаем
$ g++ -std=c++11 -I../JamSpell -L./build/jamspell -L./build/contrib/cityhash -L./build/contrib/phf -ljamspell_lib -lcityhash -lphf ./test.cpp -o ./bin/test$ ./bin/test


Результаты


Получение результатов
$ python3 evaluate.py ./texts/test.txt ./texts/correct.txt ./texts/output.txt


ASC
Precision Recall FMeasure
92.13 82.51 87.05

JamSpell
Precision Recall FMeasure
77.87 63.36 69.87

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

Принцип обучения который предлагаю я


  1. Собираем языковую модель на грязных данных
  2. Удаляем все редко встречающиеся слова и N-граммы в собранной языковой модели
  3. Добавляем одиночные слова для более правильной работы системы исправления опечаток.
  4. Собираем бинарный словарь

Приступим


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

Сборка корпуса с помощью ALM
collect.json
{"size": 3,"debug": 1,"threads": 0,"ext": "txt","method": "train","allow-unk": true,"mixed-dicts": true,"only-token-words": true,"smoothing": "wittenbell","locale": "en_US.UTF-8","w-abbr": "./output/alm.abbr","w-map": "./output/alm.map","w-vocab": "./output/alm.vocab","w-words": "./output/words.txt","corpus": "./texts/corpus","abbrs": "./abbrs/abbrs.txt","goodwords": "./texts/whitelist/words.txt","badwords": "./texts/blacklist/garbage.txt","mix-restwords": "./texts/similars/letters.txt","alphabet": "абвгдеёжзийклмнопрстуфхцчшщъыьэюяabcdefghijklmnopqrstuvwxyz"}

$ ./alm -r-json ./collect.json

  • size Мы собираем N-граммы длиной 3
  • debug Выводим индикатор выполнения сбора данных
  • threads Для сборки используем все доступные ядра
  • ext Указываем расширение файлов в каталоге которые пригодны для обучения
  • allow-unk Разрешаем хранить токенunkв языковой модели
  • mixed-dicts Разрешаем исправлять слова с замещёнными буквами из других языков
  • only-token-words Собираем не целиком N-граммы как есть а только последовательности нормальных слов
  • smoothing Используем алгоритм сглаживания wittenbell (на данном этапе он не применяется, но какой-то алгоритм сглаживания указать нужно)
  • locale Устанавливаем локаль окружения (можно не указывать)
  • w-abbr Сохраняем собранные суффиксы цифровых аббревиатур
  • w-map Сохраняем карту последовательности как промежуточный результат
  • w-vocab Сохраняем собранный словарь
  • w-words Сохраняем список собранных уникальных слов (на всякий случай)
  • corpus Используем для сборки каталог с текстовыми данными корпуса
  • abbrs Используем в обучении, общеупотребимые аббревиатуры, такие как (США, ФСБ, КГБ ...)
  • goodwords Используем заранее подготовленный белый список слов
  • badwords Используем заранее подготовленный чёрный список слов
  • mix-restwords Используем файл с похожими символами разных языков
  • alphabet Указываем алфавит используемый при обучении (алфавит всегда должен быть указан один и тот же)

Версия на Python
import alm# Мы собираем N-граммы длиной 3alm.setSize(3)# Для сборки используем все доступные ядраalm.setThreads(0)# Устанавливаем локаль окружения (можно не указывать)alm.setLocale("en_US.UTF-8")# Указываем алфавит используемый при обучении (алфавит всегда должен быть указан один и тот же)alm.setAlphabet("абвгдеёжзийклмнопрстуфхцчшщъыьэюяabcdefghijklmnopqrstuvwxyz")# Устанавливаем похожие символы разных языковalm.setSubstitutes({'p':'р','c':'с','o':'о','t':'т','k':'к','e':'е','a':'а','h':'н','x':'х','b':'в','m':'м'})# Разрешаем хранить токен <unk> в языковой моделиalm.setOption(alm.options_t.allowUnk)# Разрешаем исправлять слова с замещёнными буквами из других языковalm.setOption(alm.options_t.mixDicts)# Собираем не целиком N-граммы  как есть а только последовательности нормальных словalm.setOption(alm.options_t.tokenWords)# Используем алгоритм сглаживания wittenbell (на данном этапе он не применяется, но какой-то алгоритм сглаживания указать нужно)alm.init(alm.smoothing_t.wittenBell)# Используем в обучении, общеупотребимые аббревиатуры, такие как (США, ФСБ, КГБ ...)f = open('./abbrs/abbrs.txt')for abbr in f.readlines():    abbr = abbr.replace("\n", "")    alm.addAbbr(abbr)f.close()# Используем заранее подготовленный белый список словf = open('./texts/whitelist/words.txt')for word in f.readlines():    word = word.replace("\n", "")    alm.addGoodword(word)f.close()# Используем заранее подготовленный чёрный список словf = open('./texts/blacklist/garbage.txt')for word in f.readlines():    word = word.replace("\n", "")    alm.addBadword(word)f.close()def status(text, status):    print(text, status)def statusWords(status):    print("Write words", status)def statusVocab(status):    print("Write vocab", status)def statusMap(status):    print("Write map", status)def statusSuffix(status):    print("Write suffix", status)# Выполняем сборку языковой моделиalm.collectCorpus("./texts/corpus", status)# Выполняем сохранение списка собранных уникальных словalm.writeWords("./output/words.txt", statusWords)# Выполняем сохранение словаряalm.writeVocab("./output/alm.vocab", statusVocab)# Выполняем сохранение карты последовательностиalm.writeMap("./output/alm.map", statusMap)# Выполняем сохранение списка суффиксов цифровых аббревиатурalm.writeSuffix("./output/alm.abbr", statusSuffix)

Таким образом, мы собираем все наши корпуса

Прунинг собранного корпуса с помощью ALM
prune.json
{    "size": 3,    "debug": 1,    "allow-unk": true,    "method": "vprune",    "vprune-wltf": -15.0,    "locale": "en_US.UTF-8",    "smoothing": "wittenbell",    "r-map": "./corpus1/alm.map",    "r-vocab": "./corpus1/alm.vocab",    "w-map": "./output/alm.map",    "w-vocab": "./output/alm.vocab",    "goodwords": "./texts/whitelist/words.txt",    "badwords": "./texts/blacklist/garbage.txt",    "alphabet": "абвгдеёжзийклмнопрстуфхцчшщъыьэюяabcdefghijklmnopqrstuvwxyz"}

$ ./alm -r-json ./prune.json

  • size Мы используем N-граммы длиной 3
  • debug Выводим индикатор выполнения прунинга словаря
  • allow-unk Разрешаем хранить токенunkв языковой модели
  • vprune-wltf Минимально-разрешённый вес слова в словаре (все, что ниже удаляется)
  • locale Устанавливаем локаль окружения (можно не указывать)
  • smoothing Используем алгоритм сглаживания wittenbell (на данном этапе он не применяется, но какой-то алгоритм сглаживания указать нужно)
  • r-map Кара последовательности собранная на предыдущем этапе
  • r-vocab Словарь собранный на предыдущем этапе
  • w-map Сохраняем карту последовательности как промежуточный результат
  • w-vocab Сохраняем собранный словарь
  • goodwords Используем заранее подготовленный белый список слов
  • badwords Используем заранее подготовленный чёрный список слов
  • alphabet Указываем алфавит используемый при обучении (алфавит всегда должен быть указан один и тот же)

Версия на Python
import alm# Мы собираем N-граммы длиной 3alm.setSize(3)# Для сборки используем все доступные ядраalm.setThreads(0)# Устанавливаем локаль окружения (можно не указывать)alm.setLocale("en_US.UTF-8")# Указываем алфавит используемый при обучении (алфавит всегда должен быть указан один и тот же)alm.setAlphabet("абвгдеёжзийклмнопрстуфхцчшщъыьэюяabcdefghijklmnopqrstuvwxyz")# Разрешаем хранить токен <unk> в языковой моделиalm.setOption(alm.options_t.allowUnk)# Используем алгоритм сглаживания wittenbell (на данном этапе он не применяется, но какой-то алгоритм сглаживания указать нужно)alm.init(alm.smoothing_t.wittenBell)# Используем заранее подготовленный белый список словf = open('./texts/whitelist/words.txt')for word in f.readlines():    word = word.replace("\n", "")    alm.addGoodword(word)f.close()# Используем заранее подготовленный чёрный список словf = open('./texts/blacklist/garbage.txt')for word in f.readlines():    word = word.replace("\n", "")    alm.addBadword(word)f.close()def statusPrune(status):    print("Prune data", status)def statusReadVocab(text, status):    print("Read vocab", text, status)def statusWriteVocab(status):    print("Write vocab", status)def statusReadMap(text, status):    print("Read map", text, status)def statusWriteMap(status):    print("Write map", status)# Выполняем загрузкусловаряalm.readVocab("./corpus1/alm.vocab", statusReadVocab)# Выполняем загрузку карты последовательностиalm.readMap("./corpus1/alm.map", statusReadMap)# Выполняем прунинг словаряalm.pruneVocab(-15.0, 0, 0, statusPrune)# Выполняем сохранение словаряalm.writeVocab("./output/alm.vocab", statusWriteVocab)# Выполняем сохранение карты последовательностиalm.writeMap("./output/alm.map", statusWriteMap)


Объединение собранных данных с помощью ALM
merge.json
{    "size": 3,    "debug": 1,    "allow-unk": true,    "method": "merge",    "mixed-dicts": "true",    "locale": "en_US.UTF-8",    "smoothing": "wittenbell",    "r-words": "./texts/words",    "r-map": "./corpus1",    "r-vocab": "./corpus1",    "w-map": "./output/alm.map",    "w-vocab": "./output/alm.vocab",    "goodwords": "./texts/whitelist/words.txt",    "badwords": "./texts/blacklist/garbage.txt",    "mix-restwords": "./texts/similars/letters.txt",    "alphabet": "абвгдеёжзийклмнопрстуфхцчшщъыьэюяabcdefghijklmnopqrstuvwxyz"}

$ ./alm -r-json ./merge.json

  • size Мы используем N-граммы длиной 3
  • debug Выводим индикатор выполнения загрузки данных
  • allow-unk Разрешаем хранить токенunkв языковой модели
  • mixed-dicts Разрешаем исправлять слова с замещёнными буквами из других языков
  • locale Устанавливаем локаль окружения (можно не указывать)
  • smoothing Используем алгоритм сглаживания wittenbell (на данном этапе он не применяется, но какой-то алгоритм сглаживания указать нужно)
  • r-words Указываем каталог или файл с словами которые нужно добавить в словарь
  • r-map Указываем каталог с файлами карт последовательности, собранных и пропруненных на предыдущих этапах
  • r-vocab Указываем каталог с файлами словарей, собранных и пропруненных на предыдущих этапах
  • w-map Сохраняем карту последовательности как промежуточный результат
  • w-vocab Сохраняем собранный словарь
  • goodwords Используем заранее подготовленный белый список слов
  • badwords Используем заранее подготовленный чёрный список слов
  • alphabet Указываем алфавит используемый при обучении (алфавит всегда должен быть указан один и тот же)

Версия на Python
import alm# Мы собираем N-граммы длиной 3alm.setSize(3)# Для сборки используем все доступные ядраalm.setThreads(0)# Устанавливаем локаль окружения (можно не указывать)alm.setLocale("en_US.UTF-8")# Указываем алфавит используемый при обучении (алфавит всегда должен быть указан один и тот же)alm.setAlphabet("абвгдеёжзийклмнопрстуфхцчшщъыьэюяabcdefghijklmnopqrstuvwxyz")# Устанавливаем похожие символы разных языковalm.setSubstitutes({'p':'р','c':'с','o':'о','t':'т','k':'к','e':'е','a':'а','h':'н','x':'х','b':'в','m':'м'})# Разрешаем хранить токен <unk> в языковой моделиalm.setOption(alm.options_t.allowUnk)# Разрешаем исправлять слова с замещёнными буквами из других языковalm.setOption(alm.options_t.mixDicts)# Используем алгоритм сглаживания wittenbell (на данном этапе он не применяется, но какой-то алгоритм сглаживания указать нужно)alm.init(alm.smoothing_t.wittenBell)# Используем заранее подготовленный белый список словf = open('./texts/whitelist/words.txt')for word in f.readlines():    word = word.replace("\n", "")    alm.addGoodword(word)f.close()# Используем заранее подготовленный чёрный список словf = open('./texts/blacklist/garbage.txt')for word in f.readlines():    word = word.replace("\n", "")    alm.addBadword(word)f.close()# Используем файл с словами которые нужно добавить в словарьf = open('./texts/words.txt')for word in f.readlines():    word = word.replace("\n", "")    alm.addWord(word)f.close()def statusReadVocab(text, status):    print("Read vocab", text, status)def statusWriteVocab(status):    print("Write vocab", status)def statusReadMap(text, status):    print("Read map", text, status)def statusWriteMap(status):    print("Write map", status)# Выполняем загрузку словаряalm.readVocab("./corpus1", statusReadVocab)# Выполняем загрузку карты последовательностиalm.readMap("./corpus1", statusReadMap)# Выполняем сохранение словаряalm.writeVocab("./output/alm.vocab", statusWriteVocab)# Выполняем сохранение карты последовательностиalm.writeMap("./output/alm.map", statusWriteMap)


Обучение языковой модели с помощью ALM
train.json
{    "size": 3,    "debug": 1,    "allow-unk": true,    "reset-unk": true,    "interpolate": true,    "method": "train",    "locale": "en_US.UTF-8",    "smoothing": "wittenbell",    "r-map": "./output/alm.map",    "r-vocab": "./output/alm.vocab",    "w-arpa": "./output/alm.arpa",    "w-words": "./output/words.txt",    "alphabet": "абвгдеёжзийклмнопрстуфхцчшщъыьэюяabcdefghijklmnopqrstuvwxyz"}

$ ./alm -r-json ./train.json

  • size Мы используем N-граммы длиной 3
  • debug Выводим индикатор обучения языковой модели
  • allow-unk Разрешаем хранить токенunkв языковой модели
  • reset-unk Выполняем сброс значения частоты, дляunkтокена в языковой модели
  • interpolate Выполнять интерполяцию при расчётах частот
  • locale Устанавливаем локаль окружения (можно не указывать)
  • smoothing Используем алгоритм сглаживания wittenbell
  • r-map Указываем файл карты последовательности, собранной на предыдущих этапах
  • r-vocab Указываем файл словаря, собранного на предыдущих этапах
  • w-arpa Указываем адрес файла ARPA, для сохранения
  • w-words Указываем адрес файла, для сохранения уникальных слов (на всякий случай)
  • alphabet Указываем алфавит используемый при обучении (алфавит всегда должен быть указан один и тот же)

Версия на Python
import alm# Мы собираем N-граммы длиной 3alm.setSize(3)# Для сборки используем все доступные ядраalm.setThreads(0)# Устанавливаем локаль окружения (можно не указывать)alm.setLocale("en_US.UTF-8")# Указываем алфавит используемый при обучении (алфавит всегда должен быть указан один и тот же)alm.setAlphabet("абвгдеёжзийклмнопрстуфхцчшщъыьэюяabcdefghijklmnopqrstuvwxyz")# Устанавливаем похожие символы разных языковalm.setSubstitutes({'p':'р','c':'с','o':'о','t':'т','k':'к','e':'е','a':'а','h':'н','x':'х','b':'в','m':'м'})# Разрешаем хранить токен <unk> в языковой моделиalm.setOption(alm.options_t.allowUnk)# Выполняем сброс значения частоты токена <unk> в языковой моделиalm.setOption(alm.options_t.resetUnk)# Разрешаем исправлять слова с замещёнными буквами из других языковalm.setOption(alm.options_t.mixDicts)# Разрешаем выполнять интерполяцию при расчётахalm.setOption(alm.options_t.interpolate)# Используем алгоритм сглаживания wittenbell (на данном этапе он не применяется, но какой-то алгоритм сглаживания указать нужно)alm.init(alm.smoothing_t.wittenBell)def statusReadVocab(text, status):    print("Read vocab", text, status)def statusReadMap(text, status):    print("Read map", text, status)def statusBuildArpa(status):    print("Build ARPA", status)def statusWriteMap(status):    print("Write map", status)def statusWriteArpa(status):    print("Write ARPA", status)def statusWords(status):    print("Write words", status)# Выполняем загрузку словаряalm.readVocab("./output/alm.vocab", statusReadVocab)# Выполняем загрузку карты последовательностиalm.readMap("./output/alm.map", statusReadMap)# Выполняем расчёты частот языковой моделиalm.buildArpa(statusBuildArpa)# Выполняем запись языковой модели в файл ARPAalm.writeArpa("./output/alm.arpa", statusWriteArpa)# Выполняем сохранение словаряalm.writeWords("./output/words.txt", statusWords)


Обучение spell-checker ASC
train.json
{"size": 3,"debug": 1,"threads": 0,"confidence": true,"mixed-dicts": true,"method": "train","alter": {"е":"ё"},"locale": "en_US.UTF-8","smoothing": "wittenbell","pilots": ["а","у","в","о","с","к","б","и","я","э","a","i","o","e","g"],"w-bin": "./dictionary/3-single.asc","r-abbr": "./output/alm.abbr","r-vocab": "./output/alm.vocab","r-arpa": "./output/alm.arpa","abbrs": "./texts/abbrs/abbrs.txt","goodwords": "./texts/whitelist/words.txt","badwords": "./texts/blacklist/garbage.txt","alters": "./texts/alters/yoficator.txt","upwords": "./texts/words/upp","mix-restwords": "./texts/similars/letters.txt","alphabet": "абвгдеёжзийклмнопрстуфхцчшщъыьэюяabcdefghijklmnopqrstuvwxyz","bin-code": "ru","bin-name": "Russian","bin-author": "You name","bin-copyright": "You company LLC","bin-contacts": "site: https://example.com, e-mail: info@example.com","bin-lictype": "MIT","bin-lictext": "... License text ...","embedding-size": 28,"embedding": {    "а": 0, "б": 1, "в": 2, "г": 3, "д": 4, "е": 5,    "ё": 5, "ж": 6, "з": 7, "и": 8, "й": 8, "к": 9,    "л": 10, "м": 11, "н": 12, "о": 0, "п": 13, "р": 14,    "с": 15, "т": 16, "у": 17, "ф": 18, "х": 19, "ц": 20,    "ч": 21, "ш": 21, "щ": 21, "ъ": 22, "ы": 23, "ь": 22,    "э": 5, "ю": 24, "я": 25, "<": 26, ">": 26, "~": 26,    "-": 26, "+": 26, "=": 26, "*": 26, "/": 26, ":": 26,    "%": 26, "|": 26, "^": 26, "&": 26, "#": 26, "'": 26,    "\\": 26, "0": 27, "1": 27, "2": 27, "3": 27, "4": 27,    "5": 27, "6": 27, "7": 27, "8": 27, "9": 27, "a": 0,    "b": 2, "c": 15, "d": 4, "e": 5, "f": 18, "g": 3,    "h": 12, "i": 8, "j": 6, "k": 9, "l": 10, "m": 11,    "n": 12, "o": 0, "p": 14, "q": 13, "r": 14, "s": 15,    "t": 16, "u": 24, "v": 21, "w": 22, "x": 19, "y": 17, "z": 7}}

$ ./asc -r-json ./train.json

  • size Мы используем N-граммы длиной 3
  • debug Выводим индикатор обучения опечаточника
  • threads Для сборки используем все доступные ядра
  • confidence Разрешаем загружать данные из ARPA так-как они есть, без перетокенизации
  • mixed-dicts Разрешаем исправлять слова с замещёнными буквами из других языков
  • alter Альтернативные буквы (буквы которые замещают другие буквы в словаре, в нашем случае, это буква Ё)
  • locale Устанавливаем локаль окружения (можно не указывать)
  • smoothing Используем алгоритм сглаживания wittenbell (на данном этапе он не применяется, но какой-то алгоритм сглаживания указать нужно)
  • pilots Устанавливаем список пилотных слов (слова состоящие из одной буквы)
  • w-bin Устанавливаем адрес для сохранения бинарного контейнера
  • r-abbr Указываем каталог с файлами, собранных суффиксов цифровых аббревиатур на предыдущих этапах
  • r-vocab Указываем файл словаря, собранного на предыдущих этапах
  • r-arpa Указываем файл ARPA, собранный на предыдущем этапе
  • abbrs Используем в обучении, общеупотребимые аббревиатуры, такие как (США, ФСБ, КГБ ...)
  • goodwords Используем заранее подготовленный белый список слов
  • badwords Используем заранее подготовленный чёрный список слов
  • alters Используем файл со словами содержащими альтернативные буквы, которые используются всегда однозначно (синтаксис файла аналогичен списку похожих букв в разных алфавитах)
  • upwords Используем файл со списком слов, которые всегда употребляются с заглавной буквы (названия, имена, фамилии...)
  • mix-restwords Используем файл с похожими символами разных языков
  • alphabet Указываем алфавит используемый при обучении (алфавит всегда должен быть указан один и тот же)
  • bin-code Устанавливаем код языка в словаре
  • bin-name Устанавливаем название словаря
  • bin-author Устанавливаем имя автора словаря
  • bin-copyright Устанавливаем копирайт словаря
  • bin-contacts Устанавливаем контактные данные автора словаря
  • bin-lictype Устанавливаем тип лицензии словаря
  • bin-lictext Устанавливаем текст лицензии словаря
  • embedding-size Устанавливаем размер блока внутреннего эмбеддинга
  • embedding Устанавливаем параметры блока внутреннего эмбеддинга (не обязательно, влияет на точность подбора кандидатов)

Версия на Python
import asc# Мы собираем N-граммы длиной 3asc.setSize(3)# Для сборки используем все доступные ядраasc.setThreads(0)# Устанавливаем локаль окружения (можно не указывать)asc.setLocale("en_US.UTF-8")# Разрешаем исправлять регистр у слов в начале предложенийasc.setOption(asc.options_t.uppers)# Разрешаем хранить токен <unk> в языковой моделиasc.setOption(asc.options_t.allowUnk)# Выполняем сброс значения частоты токена <unk> в языковой моделиasc.setOption(asc.options_t.resetUnk)# Разрешаем исправлять слова с замещенными буквами из других языковasc.setOption(asc.options_t.mixDicts)# Разрешаем загружать данные из ARPA так-как они есть, без перетокенизацииasc.setOption(asc.options_t.confidence)# Указываем алфавит используемый при обучении (алфавит всегда должен быть указан один и тот же)asc.setAlphabet("абвгдеёжзийклмнопрстуфхцчшщъыьэюяabcdefghijklmnopqrstuvwxyz")# Указываем список пилотных слов (слова которые состоят из одной буквы)asc.setPilots(["а","у","в","о","с","к","б","и","я","э","a","i","o","e","g"])# Устанавливаем похожие символы разных языковasc.setSubstitutes({'p':'р','c':'с','o':'о','t':'т','k':'к','e':'е','a':'а','h':'н','x':'х','b':'в','m':'м'})# Загружаем файл заранее подготовленный белый список словf = open('./texts/whitelist/words.txt')for word in f.readlines():    word = word.replace("\n", "")    asc.addGoodword(word)f.close()# Загружаем файл заранее подготовленный чёрный список словf = open('./texts/blacklist/garbage.txt')for word in f.readlines():    word = word.replace("\n", "")    asc.addBadword(word)f.close()# Загружаем файл суффиксов цифровых аббревиатурf = open('./output/alm.abbr')for word in f.readlines():    word = word.replace("\n", "")    asc.addSuffix(word)f.close()# Загружаем файл общеупотребимые аббревиатуры, такие как (США, ФСБ, КГБ ...)f = open('./texts/abbrs/abbrs.txt')for abbr in f.readlines():    abbr = abbr.replace("\n", "")    asc.addAbbr(abbr)f.close()# Загружаем файл со списком слов, которые всегда употребляются с заглавной буквы (названия, имена, фамилии...)f = open('./texts/words/upp/words.txt')for word in f.readlines():    word = word.replace("\n", "")    asc.addUWord(word)f.close()# Устанавливаем альтернативную буквуasc.addAlt("е", "ё")# Загружаем файл со словами содержащими альтернативные буквы, которые используются всегда однозначно (синтаксис файла аналогичен списку похожих букв в разных алфавитах)f = open('./texts/alters/yoficator.txt')for words in f.readlines():    words = words.replace("\n", "")    words = words.split('\t')    asc.addAlt(words[0], words[1])f.close()def statusIndex(text, status):    print(text, status)def statusBuildIndex(status):    print("Build index", status)def statusArpa(status):    print("Read arpa", status)def statusVocab(status):    print("Read vocab", status)# Выполняем загрузку данные языковой модели из файла ARPAasc.readArpa("./output/alm.arpa", statusArpa)# Выполняем загрузку словаряasc.readVocab("./output/alm.vocab", statusVocab)# Устанавливаем код языка в словареasc.setCode("RU")# Устанавливаем тип лицензии словаряasc.setLictype("MIT")# Устанавливаем название словаряasc.setName("Russian")# Устанавливаем имя автора словаряasc.setAuthor("You name")# Устанавливаем копирайт словаряasc.setCopyright("You company LLC")# Устанавливаем текст лицензии словаряasc.setLictext("... License text ...")# Устанавливаем контактные данные автора словаряasc.setContacts("site: https://example.com, e-mail: info@example.com")# Устанавливаем параметры блока внутреннего эмбеддинга (не обязательно, влияет на точность подбора кандидатов)asc.setEmbedding({    "а": 0, "б": 1, "в": 2, "г": 3, "д": 4, "е": 5,    "ё": 5, "ж": 6, "з": 7, "и": 8, "й": 8, "к": 9,    "л": 10, "м": 11, "н": 12, "о": 0, "п": 13, "р": 14,    "с": 15, "т": 16, "у": 17, "ф": 18, "х": 19, "ц": 20,    "ч": 21, "ш": 21, "щ": 21, "ъ": 22, "ы": 23, "ь": 22,    "э": 5, "ю": 24, "я": 25, "<": 26, ">": 26, "~": 26,    "-": 26, "+": 26, "=": 26, "*": 26, "/": 26, ":": 26,    "%": 26, "|": 26, "^": 26, "&": 26, "#": 26, "'": 26,    "\\": 26, "0": 27, "1": 27, "2": 27, "3": 27, "4": 27,    "5": 27, "6": 27, "7": 27, "8": 27, "9": 27, "a": 0,    "b": 2, "c": 15, "d": 4, "e": 5, "f": 18, "g": 3,    "h": 12, "i": 8, "j": 6, "k": 9, "l": 10, "m": 11,    "n": 12, "o": 0, "p": 14, "q": 13, "r": 14, "s": 15,    "t": 16, "u": 24, "v": 21, "w": 22, "x": 19, "y": 17, "z": 7}, 28)# Выполняем сборку индекса бинарного словаряasc.buildIndex(statusBuildIndex)# Выполняем сохранение индекса бинарного словаряasc.saveIndex("./dictionary/3-middle.asc", "", 128, statusIndex)


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

Пример работы
spell.json
{    "ad": 13,    "cw": 38120,    "debug": 1,    "threads": 0,    "method": "spell",    "alter": {"е":"ё"},    "asc-split": true,    "asc-alter": true,    "confidence": true,    "asc-esplit": true,    "asc-rsplit": true,    "asc-uppers": true,    "asc-hyphen": true,    "mixed-dicts": true,    "asc-wordrep": true,    "spell-verbose": true,    "r-text": "./texts/test.txt",    "w-text": "./texts/output.txt",    "upwords": "./texts/words/upp",    "r-arpa": "./dictionary/alm.arpa",    "r-abbr": "./dictionary/alm.abbr",    "abbrs": "./texts/abbrs/abbrs.txt",    "alters": "./texts/alters/yoficator.txt",    "mix-restwords": "./similars/letters.txt",    "goodwords": "./texts/whitelist/words.txt",    "badwords": "./texts/blacklist/garbage.txt",    "pilots": ["а","у","в","о","с","к","б","и","я","э","a","i","o","e","g"],    "alphabet": "абвгдеёжзийклмнопрстуфхцчшщъыьэюяabcdefghijklmnopqrstuvwxyz",    "embedding-size": 28,    "embedding": {        "а": 0, "б": 1, "в": 2, "г": 3, "д": 4, "е": 5,        "ё": 5, "ж": 6, "з": 7, "и": 8, "й": 8, "к": 9,        "л": 10, "м": 11, "н": 12, "о": 0, "п": 13, "р": 14,        "с": 15, "т": 16, "у": 17, "ф": 18, "х": 19, "ц": 20,        "ч": 21, "ш": 21, "щ": 21, "ъ": 22, "ы": 23, "ь": 22,        "э": 5, "ю": 24, "я": 25, "<": 26, ">": 26, "~": 26,        "-": 26, "+": 26, "=": 26, "*": 26, "/": 26, ":": 26,        "%": 26, "|": 26, "^": 26, "&": 26, "#": 26, "'": 26,        "\\": 26, "0": 27, "1": 27, "2": 27, "3": 27, "4": 27,        "5": 27, "6": 27, "7": 27, "8": 27, "9": 27, "a": 0,        "b": 2, "c": 15, "d": 4, "e": 5, "f": 18, "g": 3,        "h": 12, "i": 8, "j": 6, "k": 9, "l": 10, "m": 11,        "n": 12, "o": 0, "p": 14, "q": 13, "r": 14, "s": 15,        "t": 16, "u": 24, "v": 21, "w": 22, "x": 19, "y": 17, "z": 7    }}

$ ./asc -r-json ./spell.json

Версия на Python
import asc# Для сборки используем все доступные ядраasc.setThreads(0)# Разрешаем исправлять регистр у слов в начале предложенийasc.setOption(asc.options_t.uppers)# Разрешаем выполнять сплитыasc.setOption(asc.options_t.ascSplit)# Разрешаем выполнять Ёфикациюasc.setOption(asc.options_t.ascAlter)# Разрешаем выполнять сплит слов с ошибкамиasc.setOption(asc.options_t.ascESplit)# Разрешаем удалять лишние пробелы между словамиasc.setOption(asc.options_t.ascRSplit)# Разрешаем выполнять корректировку регистров словasc.setOption(asc.options_t.ascUppers)# Разрешаем выполнять сплит по дефисамasc.setOption(asc.options_t.ascHyphen)# Разрешаем удалять повторяющиеся словаasc.setOption(asc.options_t.ascWordRep)# Разрешаем исправлять слова с замещенными буквами из других языковasc.setOption(asc.options_t.mixDicts)# Разрешаем загружать данные из ARPA так-как они есть, без перетокенизацииasc.setOption(asc.options_t.confidence)# Указываем алфавит используемый при обучении (алфавит всегда должен быть указан один и тот же)asc.setAlphabet("абвгдеёжзийклмнопрстуфхцчшщъыьэюяabcdefghijklmnopqrstuvwxyz")# Указываем список пилотных слов (слова которые состоят из одной буквы)asc.setPilots(["а","у","в","о","с","к","б","и","я","э","a","i","o","e","g"])# Устанавливаем похожие символы разных языковasc.setSubstitutes({'p':'р','c':'с','o':'о','t':'т','k':'к','e':'е','a':'а','h':'н','x':'х','b':'в','m':'м'})# Загружаем файл заранее подготовленный белый список словf = open('./texts/whitelist/words.txt')for word in f.readlines():    word = word.replace("\n", "")    asc.addGoodword(word)f.close()# Загружаем файл заранее подготовленный чёрный список словf = open('./texts/blacklist/garbage.txt')for word in f.readlines():    word = word.replace("\n", "")    asc.addBadword(word)f.close()# Загружаем файл суффиксов цифровых аббревиатурf = open('./output/alm.abbr')for word in f.readlines():    word = word.replace("\n", "")    asc.addSuffix(word)f.close()# Загружаем файл общеупотребимые аббревиатуры, такие как (США, ФСБ, КГБ ...)f = open('./texts/abbrs/abbrs.txt')for abbr in f.readlines():    abbr = abbr.replace("\n", "")    asc.addAbbr(abbr)f.close()# Загружаем файл со списком слов, которые всегда употребляются с заглавной буквы (названия, имена, фамилии...)f = open('./texts/words/upp/words.txt')for word in f.readlines():    word = word.replace("\n", "")    asc.addUWord(word)f.close()# Устанавливаем альтернативную буквуasc.addAlt("е", "ё")# Загружаем файл со словами содержащими альтернативные буквы, которые используются всегда однозначно (синтаксис файла аналогичен списку похожих букв в разных алфавитах)f = open('./texts/alters/yoficator.txt')for words in f.readlines():    words = words.replace("\n", "")    words = words.split('\t')    asc.addAlt(words[0], words[1])f.close()def statusArpa(status):    print("Read arpa", status)def statusIndex(status):    print("Build index", status)# Выполняем загрузку данные языковой модели из файла ARPAasc.readArpa("./dictionary/alm.arpa", statusArpa)# Устанавливаем характеристики словаря (38120 слов полученных при обучении и 13 документов используемых в обучении)asc.setAdCw(38120, 13)# Устанавливаем параметры блока внутреннего эмбеддинга (не обязательно, влияет на точность подбора кандидатов)asc.setEmbedding({    "а": 0, "б": 1, "в": 2, "г": 3, "д": 4, "е": 5,    "ё": 5, "ж": 6, "з": 7, "и": 8, "й": 8, "к": 9,    "л": 10, "м": 11, "н": 12, "о": 0, "п": 13, "р": 14,    "с": 15, "т": 16, "у": 17, "ф": 18, "х": 19, "ц": 20,    "ч": 21, "ш": 21, "щ": 21, "ъ": 22, "ы": 23, "ь": 22,    "э": 5, "ю": 24, "я": 25, "<": 26, ">": 26, "~": 26,    "-": 26, "+": 26, "=": 26, "*": 26, "/": 26, ":": 26,    "%": 26, "|": 26, "^": 26, "&": 26, "#": 26, "'": 26,    "\\": 26, "0": 27, "1": 27, "2": 27, "3": 27, "4": 27,    "5": 27, "6": 27, "7": 27, "8": 27, "9": 27, "a": 0,    "b": 2, "c": 15, "d": 4, "e": 5, "f": 18, "g": 3,    "h": 12, "i": 8, "j": 6, "k": 9, "l": 10, "m": 11,    "n": 12, "o": 0, "p": 14, "q": 13, "r": 14, "s": 15,    "t": 16, "u": 24, "v": 21, "w": 22, "x": 19, "y": 17, "z": 7}, 28)# Выполняем сборку индекса бинарного словаряasc.buildIndex(statusIndex)f1 = open('./texts/test.txt')f2 = open('./texts/output.txt', 'w')for line in f1.readlines():    res = asc.spell(line)    f2.write("%s\n" % res[0])f2.close()f1.close()



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

Из песочницы Table-Makers Dilemma, или почему почти все трансцендентные элементарные функции округляются неправильно

21.09.2020 12:22:00 | Автор: admin
С удивлением обнаружил, что на русском языке трудно отыскать информацию по данной проблеме, как будто мало кого волнует, что математические библиотеки, используемые в современных компиляторах, иногда не дают корректно-округлённого результата. Меня эта ситуация волнует, так как я как раз занимаюсь разработкой таких математических библиотек. В иностранной литературе эта проблема освещена хорошо, вот я и решил в научно-популярной форме изложить её на русском языке, опираясь на западные источники и пока ещё небольшой личный опыт.



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




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

  • Трансцендентные элементарные функции (exp, sin, log, cosh и другие), работающие с арифметикой с плавающей запятой, округляются некорректно, иногда они допускают ошибку в последнем бите.
  • Причина ошибок не всегда кроется в лени или низкой квалификации разработчиков, а в одном фундаментальном обстоятельстве, преодолеть которое современная наука пока не смогла.
  • Существуют костыли, которые позволяют худо-бедно справляться с обсуждаемой проблемой в некоторых случаях.
  • Некоторые функции, которые должны делать вроде бы одно и то же, могут выдавать различный результат в одной и той же программе, например, exp2(x) и pow(2.0, x).

Чтобы понять содержание статьи, вам нужно быть знакомым с форматом чисел с плавающей запятой IEEE-754. Будет достаточно, если вы хотя бы просто понимаете что, например, вот это: 0x400921FB54442D18 число пи в формате с удвоенной точностью (binary64, или double), то есть просто понимаете, что я имею в виду этой записью; я не требую уметь на лету делать подобные преобразования. А про режимы округления я вам напомню в этой статье, это важная часть повествования. Ещё желательно знать программистский английский, потому что будут термины и цитаты из западной литературы, но можете обойтись и онлайн-переводчиком.

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

#include <stdio.h>#include <cmath>int main() {  float x = 0.00296957581304013729095458984375f;  // Аргумент, записанный точно.  float z;  z = exp2f(x);  // z = 2**x одним способом.  printf ("%.8f\n", z);  // Вывод результата с округлением до 8 знаков после точки.  z = powf(2.0f, x);  // z = 2**x другим способом  printf ("%.8f\n", z);  // Такой же вывод.  return 0;}

Число x намеренно записано с таким количеством значащих цифр, чтобы оно было точнопредставимым в типе float, то есть чтобы компилятор преобразовал его в бинарный код без округления. Ведь вы прекрасно знаете, что некоторые компиляторы не умеют округлять без ошибок (если не знаете, укажите в комментариях, я напишу отдельную статью с примерами). Далее в программе нам нужно вычислить 2x, но давайте сделаем это двумя способами: функция exp2f(x), и явное возведение двойки в степень powf(2.0f, x). Результат, естественно, будет различным, потому что я же сказал выше, что не могут элементарные функции работать правильно во всех случаях, а я специально подобрал пример, чтобы это показать. Вот что будет на выходе:

1.002060531.00206041

Эти значения мне выдали четыре компилятора: Microsoft C++ (19.00.23026), Intel C++ 15.0, GCC (6.3.0) и Clang (3.7.0). Они отличаются одним младшим битом. Вот шестнадцатеричный код этих чисел:

0x3F804385  // Правильно0x3F804384  // Неправильно

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

Функция Аргумент MS C++ Intel C++ GCC Clang
log10(x) 2.60575359533670695e129 0x40602D4F53729E44 0x40602D4F53729E45* 0x40602D4F53729E44 0x40602D4F53729E44
expm1(x) -1.31267823646623444e-7 0xBE819E53E96DFFA9* 0xBE819E53E96DFFA8 0xBE819E53E96DFFA8 0xBE819E53E96DFFA8
pow(10.0, x) 3.326929759608827789e-15 0x3FF0000000000022 0x3FF0000000000022 0x3FF0000000000022 0x3FF0000000000022
logp1(x) -1.3969831951387235e-9 0xBE17FFFF4017FCFF* 0xBE17FFFF4017FCFE 0xBE17FFFF4017FCFE 0xBE17FFFF4017FCFE


Надеюсь, у вас не сложилось впечатления, что я специально взял какие-то прямо совсем уникальные тесты, которые с трудом можно отыскать? Если сложилось, то давайте состряпаем на коленках полный перебор всех возможных дробных аргументов для функции 2x для типа данных float. Понятно, что нас интересуют только значения x между 0 и 1, потому что другие аргументы будут давать результат, отличающийся только значением в поле экспоненты и интереса не представляют. Сами понимаете:

$$display$$2^x = 2^{[x]}\cdot2^{\{x\}}.$$display$$



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

MS C++ Intel C++ GCC Clang
1910726 (0,97%) 90231 (0,05%) 0 0


Из программы ниже понятно, что число проверяемых аргументов x составило 197612997 штук. Получается, что, например, Microsoft С++ неверно вычисляет функцию 2x для почти одного процента из них. Не радуйтесь, уважаемые любители GCC и Clang, просто именно эта функция в данных компиляторах реализована правильно, но полно ошибок в других.

Код полного перебора
#include <stdio.h>#include <cmath>    // Этими макросами мы обращаемся к битовому представлению чисел float и double#define FAU(x) (*(unsigned int*)(&x))#define DAU(x) (*(unsigned long long*)(&x))    // Эта функция вычисляет 2**x точно до последнего бита для 0<=x<=1.    // Страшный вид, возможно, не даёт сразу увидеть, что     // здесь вычисляется аппроксимирующий полином 10-й степени.    // Промежуточные расчёты делаются в double (ошибки двойного округления тут не мешают).    // Не нужно пытаться оптимизировать этот код через FMA-инструкции,     // практика показывает, что будет хуже, но... процессоры бывают разными.float __fastcall pow2_minimax_poly_double (float x) {  double a0, a1, a2, a3, a4, a5, a6, a7, a8, a9, a10;  DAU(a0) = 0x3ff0000000000001;  DAU(a1) = 0x3fe62e42fefa3763;  DAU(a2) = 0x3fcebfbdff845acb;  DAU(a3) = 0x3fac6b08d6a26a5b;  DAU(a4) = 0x3f83b2ab7bece641;  DAU(a5) = 0x3f55d87e23a1a122;  DAU(a6) = 0x3f2430b9e07cb06c;  DAU(a7) = 0x3eeff80ef154bd8b;  DAU(a8) = 0x3eb65836e5af42ac;  DAU(a9) = 0x3e7952f0d1e6fd6b;  DAU(a10)= 0x3e457d3d6f4e540e;  return (float)(a0+(a1+(a2+(a3+(a4+(a5+(a6+(a7+(a8+(a9+a10*x)*x)*x)*x)*x)*x)*x)*x)*x)*x);} int main() {  unsigned int n = 0;  // Счётчик ошибок.  // Цикл по всем возможным значениям x в интервале (0,1)  // Старт цикла: 0x33B8AA3B = 0.00000008599132428344091749750077724456787109375  // Это минимальное значение, для которого 2**x > 1.0f  // Конец цикла: 0x3F800000 = 1.0 ровно.  for (unsigned int a=0x33B8AA3B; a<0x3F800000; ++a) {     float x;    FAU(x) = a;    float z1 = exp2f (x);// Подопытная функция.    float z2 = pow2_minimax_poly_double (x);// Точный ответ.    if (FAU(z1) != FAU(z2)) {// Побитовое сравнение.      // Закомментируйте это, чтобы не выводить все ошибки на экран (их может быть много).      //fprintf (stderr, "2**(0x%08X) = 0x%08X, but correct is 0x%08X\n", a, FAU(z1), FAU(z2));      ++n;    }  }  const unsigned int N = 0x3F800000-0x33B8AA3B;  // Сколько всего аргументов было проверено.  printf ("%u wrong results of %u arguments (%.2lf%%)\n", n, N, (float)n/N*100.0f);  return 0;} 


Не буду утомлять читателя этими примерами, здесь главное было показать, что современные реализации трансцендентных функций могут неправильно округлять последний бит, причём разные компиляторы ошибаются в разных местах, но ни один не будет работать правильно. Кстати, Стандарт IEEE-754 эту ошибку в последнем бите разрешает (о чём скажу ниже), но мне всё равно кажется странным вот что: ладно double, это большой тип данных, но ведь float можно проверить полным перебором! Так ли сложно это было сделать? Совсем не сложно, и я уже показал пример.

В коде нашего перебора есть самописная функция правильного вычисления 2x с помощью аппроксимирующего полинома 10-й степени, и написана она была за несколько минут, потому что такие полиномы выводятся автоматически, например, в системе компьютерной алгебры Maple. Достаточно задать условие, чтобы полином обеспечивал 54 бита точности (именно для этой функции, 2x). Почему 54? А вот скоро узнаете, сразу после того как я расскажу суть проблемы и поведую о том, почему для типа данных учетверённой точности (binary128) в принципе сейчас невозможно создать быстрые и правильные трансцендентные функции, хотя попытки атаковать эту проблему в теории уже есть.

Округление по-умолчанию и проблема, с ним связанная


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

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

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

  1. Округлить 1,001001. Третий бит после запятой равен 1, но дальше есть ещё 6-й бит, равный 1, значит округление будет вверх, потому что исходное число ближе к 1,01, чем к 1,00.
  2. Округлить 1,001000. Теперь округляем вниз, потому что мы находимся ровно посередине между 1,00 и 1,01, но именно у первого варианта последний бит будет чётным.
  3. Округлить 1,011000. Мы посередине между 1,01 и 1,10. Придётся округлять вверх, потому что чётный последний бит именно у большего числа.
  4. Округлить 1,010111. Округляем вниз, потому что третий бит равен нулю и мы ближе к 1,01, чем к 1,10.

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

1,0010000000000000000000000000000000000001

Вам сейчас очевидно, что округление должно быть вверх, то есть к числу 1,01. Однако вы смотрите на число, в котором после запятой 40 битов. А что если ваш алгоритм не смог обеспечить 40 битов точности и достигает, скажем, только 30 битов? Тогда он выдаст другое число:

1,001000000000000000000000000000

Не подозревая, что на 40-й позиции (которую алгоритм рассчитать не в состоянии) будет заветная единичка, вы округлите это число книзу и получите 1,00, что неправильно. Вы неправильно округлили последний бит в этом и состоит предмет нашего обсуждения. Из сказанного выходит, что для того чтобы получить всего лишь 2-й бит правильным, вам придётся вычислять функцию до 40 битов! Вот это да! А если паровоз из нулей окажется ещё длиннее? Вот об этом и поговорим в следующем разделе.

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

Суть проблемы округления последнего значащего бита


Проблема проявляется по двум причинам. Первая намеренный отказ от трудоёмкого вычисления в пользу скорости. В этом случае лишь бы заданная точностью соблюдалась, а какие там будут биты в ответе дело второстепенное. Вторая причина Table Makers Dilemma, которая является основным предметом нашего разговора. Рассмотрим обе причины подробнее.

Первая причина


Вы, конечно, понимаете, что вычисление трансцендентных функций реализовано какими-то приближёнными методами, например, методом аппроксимирующих полиномов или даже (редко) разложением в ряд. Чтобы вычисления происходили как можно быстрее, разработчики соглашаются выполнить как можно меньшее количество итераций численного метода (или взять полином как можно меньшей степени), лишь бы алгоритм допускал погрешность не превосходящую половину ценности последнего бита мантиссы. В литературе это пишется как 0.5ulp (ulp = unit in the last place).

Например, если речь идёт о числе x типа float на интервале (0,5; 1) величина ulp = 2-23. На интервале (1;2) ulp = 2-22. Иными словами, если x находится на интервале (0;1) то 2x будет на интервале (1,2), и чтобы обеспечить точность 0.5ulp, нужно, грубо говоря, выбрать EPS = 2-23 (так мы будем обозначать константу эпсилон, в простонародье именуемую погрешность, или точность, кому как нравится, не придирайтесь, пожалуйста).

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

Для тех кто не понимает, приведу пример в десятичной системе счисления. Перед вами два числа: 1,999999 и 2,0. Допустим, что первое это то, что получил программист, а второе это эталон, что должно было бы получиться, будь у нас безграничные возможности. Разница между ними всего лишь одна миллионная, то есть ответ рассчитан с погрешностью EPS=10-6. Однако ни одной правильной цифры в этом ответе нет. Плохо ли это? Нет, с точки зрения прикладной программы это фиолетово, программист округлит ответ, скажем, до двух знаков после запятой и получит 2,00 (например, речь шла о валюте, $2,00), ему больше и не надо, а то, что он заложил в свою программу EPS=10-6, то молодец, взял запас на погрешность промежуточных вычислений и решил задачу правильно.

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

Напомню, это было первое направление проблемы: последние биты ответа могут быть неправильными потому, что это намеренное решение. Главное оставить точность 0.5ulp (или выше). Поэтому численный алгоритм подбирается только из этого условия, лишь бы он работал предельно быстро. При этом Стандарт разрешает реализацию элементарных функций без корректного округления последнего бита. Цитирую [1, раздел 12.1] (англ.):
The 1985 version of the IEEE 754 Standard for Floating-Point Arithmetic did not specify anything concerning the elementary function. This was because it has been believed for years that correctly rounded functions would be much too slow at least for some input arguments. The situation changed since then and the 2008 version of the standard recommends (yet does not require) that some functions be correctly rounded.

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

список функций


Вторая причина


Наконец-то мы перешли к теме разговора: Table Maker's Dilemma (сокращённо TMD). Её название я не смог адекватно перевести на русский язык, оно было введено Уильямом Кэхэном (отцом-основателем IEEE-754) в статье [2]. Возможно, если вы прочитаете статью, то поймёте, почему название именно такое. Если кратко, то суть дилеммы в том, нам нужно получить абсолютно точное округление функции z=f(x), как если бы у нас в распоряжении была бесконечная битовая запись идеально посчитанного результата z. Но всем ясно, что бесконечную последовательность мы не можем получить. А сколько битов тогда взять? Выше я показал пример, когда нам нужно увидеть 40 битов результата, чтобы получить хотя бы 2 корректных бита после округления. А суть проблемы TMD в том, что мы заранее не знаем, до скольки битов рассчитать величину z, чтобы получить правильными столько битов после округления, сколько нам требуется. А что если их будет сто или тысяча? Мы не знаем заранее!

Например, как я сказал, для функции 2x, для типа данных float, где дробная часть мантиссы имеет всего 23 бита, нам надо выполнить расчёт с точностью 2-54, чтобы округление произошло правильно для всех без исключения возможных аргументов x. Эту оценку нетрудно получить полным перебором, но для большинства других функций, особенно для типа double или long double (ставь класс, если знаешь что это), подобные оценки неизвестны.

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

Мы начали с числа x = 0.00296957581304013729095458984375, это точнопредставимое число в типе данных float, то есть оно записано так, чтобы его конвертирование в двоичную систему float выполнялось без округления. Мы вычисляем 2x, и если бы у нас был калькулятор с бесконечной точностью, то мы должны были бы получить (Чтобы вы могли проверять меня, расчёт выполнен в онлайн-системе WolframAlpha):

1,0020604729652405753669743044108123031635398201893943954577320057

Переведём это число в двоичную систему, скажем, 64 бита будет достаточно:

1,000000001000011100001001000000000000000000000000000001101111101

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

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

1,00000000100001110000100011111111111111111111111111111?

Тогда округление будет вниз.

Мы этого не знаем, пока наша точность не достигнет 54-го бита после запятой. Когда 54-й бит будет известен точно, мы будем знать точно, к какому из двух ближайших чисел мы на самом деле ближе. Подобные числа называются hardest-to-round-points [1, раздел 12.3] (критические для округления точки), а число 54 называется hardness-to-round (трудоёмкость округления) и в цитируемой книге обозначается буквой m.

Трудоёмкость округления (m) это число битов, минимально необходимое для того, чтобы для всех без исключения аргументов некоторой функции f(x) и для заранее выбранного диапазона функция f(x) округлялась корректно до последнего бита (для разных режимов округления может быть разное значение m). Иными словами, для типа данных float и для аргумента x из диапазона (0;1) для режима округления к ближайшему чётному трудоёмкость округления m=54. Это значит что абсолютно для всех x из интервала (0;1) мы можем заложить в алгоритм одну и ту же точность ESP=2-54, и все результаты будут округлены корректно до 23-х битов после двоичной запятой.

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

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

Костыли


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

Первый костыль


Читателю может показаться, что ответ очевиден: взять арифметику с бесконечной точностью и заложить заведомо избыточное число битов, а если будет мало, то заложить ещё и пересчитать. В общем-то правильно. Так и делается, когда скорость и ресурсы компьютера не играют особой роли. У этого подхода есть имя: многоуровневая стратегия Зива (Zivs multilevel strategy) [1, раздел 12.3]. Суть её предельно проста. Алгоритм должен поддерживать расчёты на нескольких уровнях: быстрый предварительный расчёт (в большинстве случаев он же оказывается финальным), более медленный, но более точный расчёт (спасает в большинстве критических случаев), ещё более медленный, но ещё более точный расчёт (когда совсем худо пришлось) и так далее.

В подавляющем большинстве случаев достаточно взять точность чуть выше чем 0.5ulp, но если появился паровоз, то увеличиваем её. Пока паровоз сохраняется, наращиваем точность до тех пор, пока не будет строго понятно, что дальнейшие флуктуации численного метода на этот паровоз не повлияют. Так, скажем, в нашем случае если мы достигли ESP=2-54, то на 54-й позиции появляется единица, которая как бы охраняет паровоз из нулей и гарантирует, что уже не произойдёт вычитания величины, больше либо равной 2-53 и нули не превратятся в единицы, утащив за собой бит округления в ноль.

Это было научно-популярное изложение, всё то же самое с теоремой Зива (Zivs rounding test), где показано как быстро, одним действием проверять достигли ли мы желаемой точности, можно прочитать в [1, глава 12], либо в [3, раздел 10.5].

Проблема этого подхода очевидна. Нужно проектировать алгоритм вычисления каждой трансцендентной функции f(x) так, чтобы по ходу пьесы можно было увеличивать точность расчётов. Для программной реализации это ещё не так страшно, например, метод Ньютона позволяет, грубо говоря, удваивать число точных битов после запятой на каждой итерации. Можно удваивать до тех пор, пока не станет достаточно, хотя это довольно трудоёмкий процесс, надо признаться и не везде метод Ньютона оправдан, потому что требует вычисления обратной функции f-1(x), что в некоторых случаях может оказаться ничуть не проще вычисления самой f(x). Для аппаратной реализации стратегия Зива совершенно не годится. Алгоритм, зашитый в процессоре, должен выполнить ряд действий с уже предустановленным числом битов и это достаточно проблематично реализовать, если мы этого числа заранее не знаем. Взять запас? А сколько?

Вероятностный подход к решению проблемы [1, раздел 12.6] позволяет оценить величину m (напомню, это число битов, которого достаточно для корректного округления). Оказывается, что длина паровоза в вероятностном смысле чуть больше длины мантиссы числа. Таким образом, в большинстве случаев будет достаточно брать m чуть более чем вдвое больше величины мантиссы и только в очень редких случаях придётся брать ещё больше. Цитирую авторов указанной работы: we deduce that in practice, m must be slightly greater than 2p (у них p длина мантиссы вместе с целой частью, то есть p=24 для float). Далее в тексте они показывают, что вероятность ошибки при такой стратегии близка к нулю, но всё-таки положительна, и подтверждают это экспериментами.

Тем не менее, всё равно остаются случаи, когда величину m приходится брать ещё больше, и худший случай заранее неизвестен. Теоретические оценки худшего случая существуют [1, раздел 12.7.2], но они дают немыслимые миллионы битов, это никуда не годится. Вот таблица из цитируемой работы (это для функции exp(x) на интервале от -ln(2) до ln(2)):

p m
24 (binary32) 1865828
53 (binary64) 6017142
113 (binary128) 17570144


Второй костыль


На практике величина m не будет такой ужасно-большой. И чтобы определить худший случай применяется второй костыль, который называется исчерпывающий предподсчёт. Для типа данных float (32 бита), если функция f имеет один аргумент (x), то мы можем запросто прогнать все возможные значения x. Проблема возникнет только с функциями, у которых больше одного аргумента (среди них pow(x, y)), для них ничего такого придумать не удалось. Проверив все возможные значения x, мы вычислим свою константу m для каждой функции f(x) и для каждого режима округления. Затем алгоритмы расчёта, которые нужно реализовать аппаратно, проектируются так, чтобы обеспечить точность 2-m. Тогда округление f(x) будет гарантированно-корректным во всех случаях.

Для типа double (64 бита) простой перебор практически невозможен. Однако ведь перебирают! Но как? Ответ даётся в [4]. Расскажу о нём очень кратко.

Область определения функции f(x) разбивается на очень маленькие сегменты так, чтобы внутри каждого сегмента можно было заменить f(x) на линейную функцию вида b-ax (коэффициенты a и b, конечно же, разные для разных сегментов). Размер этих сегментов вычисляется аналитически, чтобы такая линейная функция действительно была бы почти неотличима от исходной в каждом сегменте.

Далее после некоторых операций масштабирования и сдвига мы приходим к следующей задаче: может ли прямая линия b-ax пройти достаточно близко от целочисленной точки?

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

Тем не менее, перебор аргументов функции f(x) сокращается во много-много раз и делает возможным обнаруживать критические точки для чисел типа double (binary64) и long double (80 битов!). Делается это на суперкомпьютерах и, конечно же, видеокартах в свободное от майнинга время. Тем не менее, что делать с типом данных binary128 пока никто не знает. Напомню, что дробная часть мантиссы таких чисел занимает 112 битов. Поэтому в иностранной литературе по данному поводу пока можно отыскать только полуфилософские рассуждения, начинающиеся с we hope... (мы надеемся...).

Подробности метода, который позволяет быстро определить прохождение линии вблизи от целочисленных точек, здесь излагать неуместно. Желающим познать процесс более тщательно рекомендую смотреть в сторону проблемы поиска расстояния между прямой и Z2, например, в статье [5]. В ней описан усовершенствованный алгоритм, который по ходу построения напоминает знаменитый алгоритм Евклида для нахождения наибольшего общего делителя. Приведу одну и ту же картинку из [4] и [5], где изображена дальнейшая трансформация задачи:

image

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

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

Функция x f(x) (обрезанное) 53-й бит и последующие
log2(x) 1.B4EBE40C95A01P0 1.8ADEAC981E00DP-1 10531011...
cosh(x) 1.7FFFFFFFFFFF7P-23 1.0000000000047P0 11890010...
ln(1+x) 1.8000000000003P-50 1.7FFFFFFFFFFFEP-50 10991000...


Как читать таблицу? Величина x указана в шестнадцатеричной нотации числа с плавающей запятой double. Сначала, как положено, идёт лидирующая единица, затем 52 бита дробной части мантиссы и буква P. Эта буква означает умножить на два в степени далее идёт степень. Например, P-23 означает, что указанную мантиссу нужно умножить на 2-23.

Далее представьте, что вычисляется функция f(x) с бесконечной точностью и у неё отрезают (без округления!) первые 53 бита. Именно эти 53 бита (один из них до запятой) указываются в столбце f(x). Последующие биты указаны в последнем столбце. Знак степени у битовой последовательности в последнем столбце означает число повторений бита, то есть, например, 10531011 означает, что сначала идёт бит, равный 1, затем 53 нуля и далее 1011. Потом троеточие, которое означает, что остальные биты нам, в общем-то, и не нужны вовсе.

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

Зачем это нужно?


Прекрасный вопрос! Ведь я выше неоднократно высказался о том, что почти 100% программистам не нужно знать элементарную функцию с точностью до корректно-округлённого последнего бита (часто им и половина битов не нужна), зачем учёные гоняют суперкомпьютеры и составляют таблицы ради решения бесполезной задачи?

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

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

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


[1] Jean-Michel Muller, Elementary Functions: Algorithms and Implementation, 2016

[2] William Kahan, A Logarithm Too Clever by Half, 2004

[3] Jean-Michel Muller, Handbook of floating-point arithmetic, 2018

[4] Vincent Lefvre, Jean-Michel Muller, Toward Correctly Rounded Transcendentals, IEEE TRANSACTIONS ON COMPUTERS, VOL. 47, NO. 11, NOVEMBER 1998. pp. 1235-1243

[5] Vincent Lefvre. New Results on the Distance Between a Segment and Z2. Application to the Exact Rounding. 17th IEEE Symposium on Computer Arithmetic Arith17, Jun 2005, Cape Cod, MA,
United States. pp.68-75

[6] Vincent Lefvre, Jean-Michel Muller, Worst Cases for Correct Rounding of the Elementary Functions in Double Precision, Rapport de recherche (INSTITUT NA TIONAL DE RECHERCHE EN INFORMA TIQUE ET EN AUTOMA TIQUE) n4044 Novembre 2000 19 pages.
Подробнее..

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

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

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

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


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

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

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


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

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


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

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

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

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

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

Получается:

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

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

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

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

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

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


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

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

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


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

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

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

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



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

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

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

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


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



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



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



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

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

Заключение


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

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

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

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

image

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



Подробнее..

Как нарисовать звезду (и не только) в полярных координатах

22.09.2020 08:08:59 | Автор: admin
Вопрос о формуле для многоугольника в полярных координатах регулярно возникает на тематических ресурсах и так же регулярно остаётся без внятного ответа. В лучшем случае попадается решение через функцию остатка от деления что не является чистым с математической точки зрения, поскольку не позволяет производить над функцией аналитические преобразования. Видимо, настоящие математики слишком заняты решением проблем тысячелетия и поисками простого доказательства теоремы Ферма, чтобы обращать внимание на подобные банальные задачи. К счастью, в этом вопросе воображение важнее знания, и для решения этой задачи не нужно быть профессором топологических наук достаточно знания школьного уровня.



Формула для равностороннего многоугольника в полярных координатах выглядит очень просто

$\rho = \frac{\cos \left(\frac{2 \sin ^{-1}(k)+\pi m}{2 n}\right)}{\cos \left(\frac{2 \sin ^{-1}(k \cos (n \phi ))+\pi m}{2 n}\right)}$


и имеет следующие параметры:

$\phi$ угол;
$n$ количество выпуклых вершин;
$m$ определяет, через какое количество вершин стороны будут лежать на одной прямой. Для него допустимы и отрицательные значения от знака будет зависеть, в какую сторону будет выгибаться звезда;
$k$ жёсткость при $k=0$ мы получим окружность вне зависимости от прочих параметров, при $ k=1$ многоугольник с прямыми линиями, при промежуточных значениях от $0$ до $1$ промежуточные фигуры между окружностью и многоугольником.

С этой формулой можно нарисовать звезду двумя путями:

1) $n=5, m=3$


2) $n=5/4,m=0$. В этом случае требуется сделать два оборота вместо одного:


Параметр $m$ влияет на многоугольник следующим образом (здесь он изменяется от -1 до 5):


Параметр $k$ в анимации:


Комплексная форма и модификации


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

$\rho = \frac{4^{1/n} \left(\sqrt{1-k^2}+i k\right)^{-1/n} \left(1+\left(\sqrt{1-k^2}+i k\right)^{2/n} e^{\frac{i \pi m}{n}}\right) \left(\sqrt{1-k^2 \cos ^2(n \phi )}+i k \cos (n \phi )\right)^{1/n}}{4^{1/n}+e^{\frac{i \pi m}{n}} \left(2 \sqrt{1-k^2 \cos ^2(n \phi )}+2 i k \cos (n \phi )\right)^{2/n}}$


На первый взгляд это может показаться бессмысленным, поскольку формула стала чуть более громоздкой но не стоит спешить с выводами. Во-первых, в ней отсутствует арксинус, что полностью меняет математический смысл формулы и позволяет по-другому посмотреть на построение звёздчатого многоугольника. Во-вторых, из неё также можно получить компактные формулы для частных случаев, например $\frac{i^t \left(i^{n t}\right)^{1/n}}{1+\left(i^{n t}\right)^{2/n}}$. В-третьих (и самое интересное), её можно творчески модифицировать и получать другие, неожиданные формы. Для того, чтобы появление возможной мнимой компоненты в радиусе не вызывало неоднозначности при вычислении, можно её сразу привести к декартовым координатам умножением на $e^{i \phi }$. Вот примеры некоторых модификаций:

$\frac{(-1)^{2/3} e^{i \phi } \sqrt[3]{\sqrt{\sin ^2(3 \phi )}+i \cos (3 \phi )}}{(-1)^{2/3}+2^{2/3} \left(\sqrt{\sin ^2(3 \phi )}+i \cos (3 \phi )\right)^{2/3}}$



$\frac{e^{i \phi } \sqrt{\sqrt{\sin ^2(2 \phi )}+i \cos (2 \phi )}}{\sqrt{2} \left(\sqrt{\sin ^2(2 \phi )}+i \cos (2 \phi )\right)^{3/2}+e^{i/2}}$



$\frac{e^{\frac{1}{4} i (4 \phi +\pi )}}{2 \sqrt{\sqrt{\sin ^2(2 \phi )}+\sqrt[4]{-1} \cos (2 \phi )}+(-1-i)}$



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

Квадрокруги и прочее


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

$\rho = \frac{2}{\sqrt{2+\sqrt{2+2 \cos (4 \phi )}}}$


или

$\rho = \sqrt{\frac{2}{1+\sqrt{1-\sin ^2(2 \phi )}}}$


(выбирайте, какая больше нравится).

В чуть более развёрнутом случае можно определить промежуточные фигуры между кругом и квадратом через точку $(k,k)$ на плоскости

$\rho = \sqrt{\frac{2}{1+\sqrt{1-\frac{\left(2 k^2-1\right) \sin ^2(2 \phi )}{k^4}}}}$




Можно также добавить вариативности этим фигурам с сохранением условия прохождения их через точку $(k,k)$ модулируя непосредственно сам параметр $k$ в зависимости от угла таким образом, чтобы при прохождении через диагонали его множитель был равен единице. Например, подставив вместо $k$ функцию $\frac{k}{1-z \cos ^2(2 \phi )}$, мы получим дополнительный параметр $z$, которым можно регулировать дополнительные изгибы. В частности, для $z=1/4$ получится следующее:



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

$\rho = \sqrt{\frac{4 a^2 b^2}{\left(\left(b^2-a^2\right) \cos (2 \phi )+a^2+b^2\right) \left(\sqrt{1-\frac{4 a^2 b^2 k \sin ^2(2 \phi )}{\left(\left(b^2-a^2\right) \cos (2 \phi )+a^2+b^2\right)^2}}+1\right)}}$


И даже посчитать его площадь (через эллиптические интегралы):

$S=4 a b\frac{((k-1) K(k)+E(k))}{k}$

Примечание
Для крайних значений $k$ ($0$ и $1$) эта функция имеет особые точки, которые можно посчитать через предел и они ожидаемо будут равны $\pi a b$ и $4 a b$.

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



А здесь площадь расширяется по экспоненциальному закону:



Переход к декартовым координатам


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

$0=\sqrt{x^2+y^2}-\frac{\cos \left(\frac{2 \sin ^{-1}(k)+\pi m}{2 n}\right)}{\cos \left(\frac{2 \sin ^{-1}\left(k \cos \left(n \tan ^{-1}(x,y)\right)\right)+\pi m}{2 n}\right)}$


или делением

$1=\frac{\sqrt{x^2+y^2} \cos \left(\frac{2 \sin ^{-1}\left(k \cos \left(n \tan ^{-1}(x,y)\right)\right)+\pi m}{2 n}\right)}{\cos \left(\frac{2 \sin ^{-1}(k)+\pi m}{2 n}\right)}$


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

Примечание
Здесь также нужно помнить, что в точке (0,0) возникает неопределенность из-за деления на ноль которая, впрочем, легко разрешается через предел (который будет равным $-\cos \left(\frac{2 \sin ^{-1}(k)+\pi m}{2 n}\right) \sec \left(\frac{2 \sin ^{-1}\left(k \cos \left(\frac{\pi n}{2}\right)\right)+\pi m}{2 n}\right)$ в первом случае и нулю во втором).

Выражение $\cos \left(n \tan ^{-1}(x,y)\right)$ также можно упростить до $\frac{(x+i y)^n+(x-i y)^n}{2 \left(x^2+y^2\right)^{n/2}}$, коэффициенты числителя которого при разложении образуют знакочередующий вариант последовательности A034839.

Значение формулы из правой части уравнения (во 2-м случае) будет меняться от $0$ до $1$ если точка $(x,y)$ попадает внутрь фигуры, и от $1$ до бесконечности если нет. Выбирая различные функции для преобразования её в яркость, можно получать различные варианты растеризации. Для экспоненты ($e^{-x-1}$ для первого и $e^{-x}$ для второго варианта) получим
или, если с насыщением

Можно использовать классический фильтр нижних частот $\frac{1}{x^p+1}$, в котором $p$ порядок фильтра, определяющий степень затухания.

Для первого варианта:

И для второго:

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

Помимо растеризации как таковой, можно задавать и деформации например, сжать шахматную доску в круг:


Или даже натянуть её на сферу:
формула

$x=\frac{u}{\sqrt{\frac{2}{1+ \left| \frac{u^2-v^2}{u^2+v^2} \right|}}}$


$y=\frac{v}{\sqrt{1+\frac{2}{\left| \frac{u^2-v^2}{u^2+v^2}\right| }}}$


$z=\sqrt{1-\frac{1}{2} u^2 \left(1+\left| \frac{u^2-v^2}{u^2+v^2}\right|\right)-\frac{1}{2} v^2 \left(1+\left| \frac{u^2-v^2}{u^2+v^2}\right|\right)}$




Appendix: как была получена формула


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

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

1) начинаем с самого простого случая задаче начертить прямую в полярных координатах. Для этого нужно решить уравнение $r \cos (\phi )=1$, решение которого очевидно $r\to \sec (\phi )$.



2) далее аргумент секанса нужно зациклить, чтобы обеспечить изломы в прямой. Именно на этом этапе другие решения используют грязный хак в виде остатка от деления. Здесь же используется последовательное взятие прямой и обратной функции синуса $\sin ^{-1}(\sin (\phi ))$



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

$\frac{\partial \sin ^{-1}(\sin (\phi ))}{\partial \phi }=\frac{\cos (\phi )}{\sqrt{1-\sin ^2(\phi )}}$




Благодаря этой же записи можно упростить функцию квадрата в полярных координатах до более эстетического вида, используя представление тригонометрический функций в комплексном виде. В Wolfram Mathematica это можно сделать с помощью функций TrigToExp и ExpToTrig:
код
Sec[1/2 ArcSin[k Sin[2 \[Phi]]]]^2//TrigToExp//ExpToTrig//Sqrt[#]&//FullSimplify

$\frac{2}{\sqrt{2+2 \sqrt{1-k^2 \sin ^2(2 \phi )}}}$


Благодаря этой же записи можно получить гладкие промежуточные фигуры между кругом и квадратом с помощью дополнительного множителя $k$, благодаря которому аргумент арксинуса не дотягивает до единицы $\sin ^{-1}(k \sin (\phi ))$:


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

$\sqrt{\frac{2}{1+\sqrt{1-k' \sin ^2( \frac{\pi}{2} )}}}=k$


код
Solve[(Sqrt[2/(1+Sqrt[1-k Sin[2 \[Phi]]^2])] /. \[Phi]->Pi/4)==x, k] /. x->k

$k'\to \frac{4 \left(k^2-1\right)}{k^4}$



3) параметры $n$ и $m$ были просто добавлены творческим способом и их влияние исследовалось экспериментально, по факту.

4) Прямоугольник легко получить перейдя к параметрическому виду и растягиванием осей

$x=a \cos (t) \sqrt{\frac{2}{1+\sqrt{1-\sin ^2(2 t)}}}$


$y=b \sin (t) \sqrt{\frac{2}{1+\sqrt{1-\sin ^2(2 t)}}}$


Но после этого $t$ уже не будет значить угол, теперь $t$ это просто параметр, который описывает вектор через его проекции на координатные оси. Чтобы перейти обратно к полярным координатам нужно найти длину вектора (через корень суммы квадратов), угол (через арктангенс отношения), выразить этот угол через $\phi$ и подставить получившееся выражение вместо $t$.
код
With[{r = Sqrt[2/(1 + Sqrt[
1 - Sin[2 t]^2])]}, {Sqrt[(a r Cos[t])^2 + (b r Sin[t])^2],
ArcTan[(b r Sin[t])/(a r Cos[t])]}] // Simplify

$\left\{\sqrt{2} \sqrt{\frac{a^2 \cos ^2(t)+b^2 \sin ^2(t)}{\sqrt{\cos ^2(2 t)}+1}},\tan ^{-1}\left(\frac{b \tan (t)}{a}\right)\right\}$



Solve[ArcTan[(b Tan[t])/a]==\[Phi], t]

$t\to \tan ^{-1}\left(\frac{a \tan (\phi )}{b}\right)$



Sqrt[2] Sqrt[(a^2 Cos[t]^2 + b^2 Sin[t]^2)/(1 + Sqrt[Cos[2 t]^2])]
/. t -> ArcTan[(a Tan[\[Phi]])/b] // Simplify



$\sqrt{2} \sqrt{\frac{a^2 b^2 \sec ^2(\phi )}{\left(a^2 \tan ^2(\phi )+b^2\right) \left(\sqrt{\cos ^2\left(2 \tan ^{-1}\left(\frac{a \tan (\phi )}{b}\right)\right)}+1\right)}}$


Упростить такую формулу уже посложнее, и для этого потребуется несколько этапов:

1) перейти к декартовым координатам заменой $\phi \to \tan ^{-1}(x,y)$;
2) перейти к экспоненциальному виду;
3) упростить;
4) сделать обратную замену $x\to \cos (\phi )$ и $y\to \sin (\phi )$;
5) опять перейти к экспоненциальному виду;
6) упростить.

В результате получим такую формулу:
код
Sqrt[2] Sqrt[(a^2 b^2 Sec[\[Phi]]^2) /
((1 + Sqrt[Cos[2 ArcTan[(a Tan[\[Phi]])/b]]^2])
(b^2 + a^2 Tan[\[Phi]]^2))] /. \[Phi] -> ArcTan[x, y]
// TrigToExp // Simplify
// # /. {x -> Cos[\[Phi]], y -> Sin[\[Phi]]} &
// TrigToExp // Simplify // FullSimplify

$2 \sqrt{\frac{a^2 b^2}{\left(\left(b^2-a^2\right) \cos (2 \phi )+a^2+b^2\right) \left(1+\sqrt{\frac{\left(\left(a^2+b^2\right) \cos (2 \phi )-a^2+b^2\right)^2}{\left(\left(b^2-a^2\right) \cos (2 \phi )+a^2+b^2\right)^2}}\right)}}$





Заключение


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

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

Конференция GraphAI World 2020 графовые алгоритмы и машинное обучение

25.09.2020 22:13:02 | Автор: admin
Graph+AI World

28-30 сентября пройдёт конференция Graph+AI World 2020 для людей, не равнодушных к графовым технологиям и машинному обучению. Мероприятие будет проходить онлайн в течение трех дней, участие бесплатное.

Организатором выступила компания TigerGraph, создатель одноименной Графовой БД, а в программе будут доклады от спикеров из различных компаний: Intel, KPMG, AT&T, Forbes, Intuit, UnitedHealth Group, Jaguar Land Rover, Xilinx, Xandr, Futurist Academy и др.

Зачем участвовать Руководителю или Инженеру и присоединиться к одному из 3000 участников из 110 Fortune 500 компаний? Добро пожаловать под кат.

Для тех, кто сразу хочет принять участие, ссылка на регистрацию.


Конференция Graph +AI World направлена на повышение эффективности проектов AI и машинного обучения через использование Графовых алгоритмов.

Почему Графовые алгоритмы?


graph
Мы используем графовые базы данных каждый день и, вероятно, не догадываемся об этом. Facebook, Instagram и Twitter используют графовые базы данных и аналитику, чтобы понять, как пользователи связаны друг с другом, и связать их с нужным контентом. Каждый раз, когда вы выполняете поиск в Google, вы используете knowledge graph от Google. Рекомендации продуктов на Amazon люди, которые купили этот товар, также купили или эти товары часто покупают вместе? Всё это также связано с аналитическими запросами к графовым базам данных.

Если сравнивать различные типы баз данных, можно выделить основные тенденции:

RDB

NoSQL

графовые базы данных

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

Key-value базы данных
Требуется множественное сканирование массива таблиц
  • Отсутствует четкая схема;
  • Высокая производительность для простых транзакций;
  • Низкая производительность для глубокой аналитики.

Графовые базы данных
Предварительно соединенные бизнес-сущности отсутствует необходимость связывать объекты.
  • Гибкая схема;
  • Высокая производительность для сложных транзакций;
  • Высокая производительность для глубокой аналитики.



Таким образом, если Ваши данные имеют множество связей между собой, логично использовать Графовые базы данных вместо множественных Join запросов, которые на больших объемах будут не настолько эффективны. Кроме того, никто не отменял Теорию графов для Data Science ;)

Ключевые спикеры


Graph + AI World 2020 Key Speakers

  • UnitedHealth Group создали крупнейшую Графовую БД в индустрии здравоохранения для связи, анализа и предоставления рекомендаций в реальном времени о траектории лечения для 50 миллионов пациентов.
  • Jaguar Land Rover сократили время запросов по своей сложной модели цепочек поставок с 3-х недель до 45 минут, что позволило им точно планировать и быстро реагировать на неопределенность спроса и предложения в связи с пандемией Covid-19.
  • Intuit используют knowledge graph как фундаментальную технологию для экспертной платформы, управляемой AI.

Программа


У конференции звездная повестка дня, наполненная учебными и сертификационными сессиями 28 сентября (предварительный день) и бизнес-кейсами, вариантами использования и техническими сессиями 29 и 30 сентября. Некоторые сессии выделил ниже.

28 Сентября



Introduction to Graph Algorithms for Machine Learning Certification
Графовые алгоритмы являются важными строительными блоками для анализа связанных данных и машинного обучения, чтобы получить более глубокое понимание этих данных. Графовые алгоритмы могут использоваться непосредственно для обучение без учителя или для обогащения обучающих выборок для обучения с учителем. На этом занятии будет представлена новая программа обучения и сертификации TigerGraph для применения Графовых алгоритмов для машинного обучения: обзор контента, видео, демонстрация и процесс сертификации.


Hands-on Workshop: Accelerating Machine Learning with Graph Algorithms
На этом семинаре вы сможете применить несколько различных подходов к машинному обучению с данными на базе графов.

После настройки вашей графовой БД (в облаке и бесплатно) мы сделаем следующее:
  • Обучение без учителя с помощью графовых алгоритмов
  • Извлечение признаков и обогащение графов
  • Внешнее обучение и интеграция с notebooks
  • In-database ML техники для графов

У нас будет несколько наборов данных для разных случаев.

29 Сентября




Application of Graph Model in Fintech and Risk Management
FinTell построила граф с десятками миллиардов ребер и узлов на основе 1,5 миллиардов активных мобильных устройств в месяц. Графовая модель помогает FinTell предоставлять превосходное качество услуг по управлению рисками финансовых институтов.


Building a State of the Art Fraud Detection System with Graph + AI

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



Executive Roundtable Transforming Media & Entertainment With Graph + AI

Графовые базы данных используются для идентификации, связывания и объединения повторяющихся сущностей клиентов и для построения проницательный 360 взгляд на клиентов. Обычно это приводит к более высоким доходам в результате более точных и эффективных рекомендаций по продуктам и услугам. Присоединяйтесь к руководителям Ippen Digital и Xandr (входит в состав AT&T), чтобы узнать, как графы и машинное обучение меняют медиа и сферу развлечений.

30 Сентября




Supply Chain & Logistics Management with Graph DB & AI
Промышленное производство сталкивается с серьезными проблемами, связанными с огромным количеством деталей, компонентов и материалов, которые необходимо закупать у множества глобально распределенных поставщиков, а затем обрабатывать и собирать на множестве этапов, что значительно затрудняет отслеживание от поставщика до конечного продукта. Это также включает в себя логистику, то есть типы транспорта, местоположения, продолжительность, стоимость и т. д.
Используя Графовые БД для обеспечения прозрачности сложных и распределенных данных, в сочетании с прогнозной аналитикой, производители могут эффективно решать эти проблемы. Одновременно оптимизируя планирование производства: обеспечение доступности деталей, минимизация потери качества, улучшение сборки и доставки в целом.



Recommendation Engine with In-Database Machine Learning
Рекомендательные системы используются в различных сервисах, таких как потоковое видео, интернет-магазины и социальные сети. В промышленном применении база данных может содержать сотни миллионов пользователей и элементов. Обучение модели в базе данных также позволяет избежать экспорта данных графа из СУБД на другие платформы машинного обучения и, таким образом, лучше поддерживать непрерывное обновление модели рекомендаций по изменяющимся обучающим данным.

Также на конференции будут подведены итоги хакатона Graphathon 2020.

Регистрация


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

Присоединяйтесь к Graph + AI World!

До встречи на конференции)
Подробнее..

Категории

Последние комментарии

© 2006-2020, personeltest.ru