
На Гитхабе: https://github.com/suprathermal/System-II.
На (временном) запасном аэродроме: https://1drv.ms/u/s!Aix7Hvq263uagjTGTAOBM7OTEPpk?e=P9D7VQ.
Или сохраняем вот эту картинку, я ниже объясню, как её перекодировать в zip проекта.

Про что это?
За прошедшие лет 30 технологии создали множество площадок, помогающих людям выражать эмоции и объединяться на их основе. Но я не знаю ни одного места, предоставляющего людям эффективные инструменты для коллективного рационального поиска истины.
Итог: мир захлёстывают течения, раздуваемые в соцсетях всевозможными манипуляторами. Рациональные же люди разъединены и редко успевают вырабатывать более-менее взвешенные совместные позиции по большинству вопросов.
Хочется написать систему, которая усиливала бы не споры, а их сходимость к ответу. Где найти истину было бы по крайней мере обычно возможно. В которой могли бы участвовать аргументы всех: человека, его гнуснейшего политического оппонента, и искусственного интеллекта со дна датацентра. В которой информация бы не цензурировалась прежде полного её провала улучшить хоть какое-то предсказание. Систему, где решения вычисляет не имеющий коррупционных интересов НИКТО, и в идеале не имеющую центра, который можно подавить или выключить.
Три года назад я очертил проблему и мысли по её решению в статье “цифровой вытрезвитель[1]“
Сегодня я выкладываю прототип возможного решения под Телеграм. Его можно запустить на (почти) любой машине, пригласить участников в бот, и… решать вопросы. По крайней мере, я на это надеюсь.
Disclaimer (Открещивание)
Это не БТР и не BWM. Комфорта не ожидайте. Это, повторюсь, прототип. Он реализует лишь один подход из возможных. Он наверняка с багами. Он намеренно написан примитивнее, чем было можно. Он несовершенен с точки зрения производительности, красоты, функциональности, удобства (по каковой причине вряд ли я буду чинить что-то, кроме самых страшных багов). Не исключено, что 80% его кода окажется ненужным.
Он делает другое:
1. Показывает, что каждый принцип, стоящий за дизайном, можно отобразить в коде.
2. Показывает, что эти элементы можно собрать в рабочее целое.
Моя цель — убедить вас, что эта задача в принципе имеет решение, а потому над ней не бесполезно работать.
А дальше… Дальше надо гонять эту штуку на разных сценариях. Смотреть, где она работает, где не очень. Но делать это в одиночку — всё равно, что обыгрывать самого себя в шахматы. Нужны другие игроки. Нужны вопросы и сценарии, про которые я забыл подумать.
Поэтому следующий ход — ваш. Берите. Изучайте. Пробуйте. Я не просто разрешаю переписывать и улучшать этот прототип. Я активно надеюсь на это (в рамках лицензии MIT). Только постарайтесь не поломать основные принципы, про них сказано подробнее в документации.
Идея…
…довольно проста.
Как работает голосование?
Каждый участник придумывает свой “правильный” ответ на задачу. Полученные ответы объединяются, обычно усреднением.
Способы построения этих ответов бывают кривы и неполны, а потом ещё и применяются с ошибками, но это не главная беда. Можно показать, что при некоторых разумных допущениях голосование всё-таки сходится к правильному ответу.
Главный его недостаток в том, что почти вся мыслительная продукция создаётся участниками “с нуля”. А потому успешные достижения одних мало переиспользуются другими. Отчего большая часть промежуточных ответов имеет ужасное качество, а метод, хоть и сходится, делает это с жутким скрежетом и чрезвычайно медленно. Дебаты отчасти улучшают эту ситуацию, но слабо масштабируются. Ведь все N >> 1 человек не могут рассмотреть все M >> 1 моделей, придуманных обществом.
Хотелось бы лучше.
И тут возникает мысль: “усреднять” надо не после получения ответов, а до.
То есть, сначала объединить способы рассуждения и данные в максимально сильную модель, а уж после вычислить ею ответ.
Это можно сделать многими способами. Я избрал, наверное, не самый общий и гибкий из них, зато, пожалуй, самый простой. Работает он так:
Когда люди разумные пытаются предсказать исход неочевидной ситуации, они часто используют высказывания двух видов:
-
Аргументы. “Случается засуха, если наблюдается восточный ветер.”
-
Примеры. “А вот в Габоне восточный ветер не наблюдался, а засуха была”.
Есть масса причин, по которым спор может не сойтись к ответу, но важнейшая из них — это выход за пределы человеческой памяти и внимания. Уже при десятке высказываний аргументы начинают путаться, а сверить каждый из них с каждым примером один человек обычно вообще не может.
Но может ML. Он готов перебрать хоть миллиард вариантов. Надо только их формализовать.
Итак, допустим, у нас есть ситуация, по которой требуется получить предсказание. Её примеры и аргументы можно логически организовать в обучающую таблицу следующим образом:

В каждой клеточке — цифра. Например, степень согласия аргумента с примером. Или количественное значение параметра, фигурирующего в аргументе, в конкретном примере.
Для иллюстрации предположим, что спор идёт о предсказании победы в военном конфликте Игриды и Распри. Тогда заполненная таблица может принять нижеприведённый вид, где примеры — это прошлые военные конфликты пар стран “A-B” с уже известным завершением, а аргументы имеют вид “было ли у страны А больше того-то, чем у страны B?”:

Люди, желающие узнать ответ для Игриды и Распри, заполняют эту таблицу. Потом мы суём её верхнюю часть в ML, тренируем его, и просим предсказать ответ для нижней строчки. Который и будет, при ряде разумных допущений, наилучшим возможным ответом при имеющихся данных.
Многие задачи удобно описываются такой структурой. Температура кипения вещества? Примерами будут другие вещества с известными температурами кипения, аргументами — параметры вроде молекулярной массы, от которых она может зависеть. Предсказание решения суда? Примеры — прошлые прецеденты. Аргументы — законы. Хотим предсказать, разведётся ли пара через пять лет? Примеры — другие пары людей. Аргументы — особенности их отношений, отмечаемые окружающими.
Получается, конечно, голосование. Только вместо усреднения ответов мы объединяем знания от всех голосующих, а уже потом вычисляем по ним ответ.
И эти знания, кстати, не обязаны поступать лишь от людей. LLM-ки вполне могут участвовать в процессе. Более того, протокол в принципе позволяет извлекать объективно верные ответы и из галлюцинирующих и уходящих от вопроса нейросетей, ибо разработан для работы с людьми, которые тоже сплошь и рядом этим занимаются.
Инженерные трудности
Разумеется, на практике этот сферический конь не долетит и до середины ручья из-за множества проблем:
-
В таблице могут оказаться пропуски. Как минимум, по причине недоступности данных. Большинство ML-алгоритмов переносят пропуски плохо.
-
Даже один участник дискуссии может намеренно внести в таблицу такие комбинации примеров, аргументов и данных, что на выходе получится любой нужный ему ответ.
-
В таблице может оказаться шум, спам и мусор. Как случайный, так и намеренный. Как на уровне единичных клеточек, так и в виде полностью негодных примеров или аргументов.
-
Даже честное мнение участников дискуссии по поводу конкретных цифр в клеточках может резко разойтись.
-
Ответ без границ погрешности — мусор.
-
Мало данных. Нейронные сети требуют для работы хотя бы сотен записей. А у нас вся таблица может оказаться эдак 9 на 5.
-
Примеры могут иметь несопоставимую значимость. Случайное наблюдение и повторённый в сотнях лабораторий опыт явно не должны одинаково влиять на ответ, но здесь оба занимают по одной строчке и рассматриваются ML-ом как равноценные.
-
Процесс заполнения таблицы на практике нетривиален. На первый взгляд вопрос технический, но как его организовать, чтобы работало?
-
А ещё по-хорошему надо бы, чтобы процесс был анонимным, не имеющим кнопки “Выкл”, и чтобы решения в нём принимал НИКТО, но это уже из области пожеланий.
Технические решения
Я не утверждаю, что эти решения — единственно возможные и уж тем более наилучшие. Просто я кое-что пробовал и делюсь наблюдениями.
Итак, по порядку:
1. В таблице могут оказаться пропуски. Как минимум, по причине недоступности данных.
Надо применить регрессор, устойчивый к пропуску данных. Из готовых “питонских” решений таков HistGradientBoostingRegressor[4]. На малых данных он слабоват, зато работает прямо “из коробки”.
Другое решение реализовано в модуле SparseRF.py. Это мета-регрессор, перебирающий все “недырявые” подмножества матрицы, затем вписывающий в каждое из них переданный ему регрессор, определённым образом взвешивающий полученные им промежуточные предсказания, и объединяющий их в ответ. Хорошо переносит пропуски до 20-30% данных, развивает недурные результаты в тестах. Но имеет существенный недостаток: сложность O(2N) от размерности задачи, с практическим потолком где-то в районе 12-ти и сильным торможением уже на 5-7 переменных. Поэтому на практике, пока размерность задачи мала, я выбираю SparseRF, а дальше — HistGradientBoostingRegressor.
Соблазнительные, но обычно опасные альтернативы:
-
Выкидывать строчки с пропусками. Учитывая, что реальная матрица легко может иметь кучу “дыр”, выкинуть придётся (почти) всё.
-
Заполнение пропущенных значений. Широко используемый и крайне скользкий путь. Если эти значения вычисляются на основе внешних к матрице данных, внесите их в матрицу сразу! А если нет, то мы, получается, заполняем клетки значениями, вычислимыми из самой же матрицы. То есть не вносим никакой новой информации. Что имеет смысл, если заполняющий регрессор сильнее, чем решающий основную задачу. Почему бы их тогда не поменять местами? И ведь, как бы мы ни старались, в таблице всё равно окажутся значения не “истинные”, а “слегка другие”. И понять, насколько это “слегка” искажает ответ, на практике тем сложнее, чем больше мы старались сделать это “слегка” “слегкее”.
-
[Some text was replaced with its SHA256 value. Reason: “Впрочем, возможны и иные варианты:”, Hash:8A-F0-96-54-08-D6-FE-F1-CC-1D-76-59-3E-9F-39-EE-C2-88-49-91-80-76-7B-5A-3D-4B-1E-99-DF-CB-20-FC]
2. Даже один участник дискуссии может намеренно внести в таблицу такие комбинации примеров, аргументов и данных, что на выходе получится любой нужный ему ответ.
Используем разделение полномочий. Человек, предложивший аргумент или пример, сам цифры в клеточках для них заполнять не должен. Более того, никто не должен вносить в одиночку крупные куски данных с предсказуемой структурой.
Как-то так:
-
Участник А предложил аргумент? Пусть пересечения его с примерами заполнят случайно выбранные другие участники.
-
Участник Б предложил пример? Пусть пересечения его с аргументами, опять же, заполнят другие случайные люди.
-
Доля входных данных от каждого участника не должна превышать некоторого порога. Чтобы нельзя было, контролируя эти данные, контролировать ответ.
-
Надо, чтобы никто не мог предложить аргумент (“фичу”), который “в одно рыло” предсказывает ответ — и которым, соответственно, легко будет манипулировать.
Но основная идея — это именно разделить предложение примеров, аргументов, и заполнение цифр между участниками дискуссии. Это, по крайней мере в теории и в среднем, гарантирует устойчивость решений к намеренным вбросам.
3. В таблице может оказаться шум, спам и мусор. Как случайный, так и намеренный. Как на уровне единичных клеточек, так и в виде целиком негодных примеров или аргументов.
Здесь принцип простой: если некоторая группа данных статистически достоверно ухудшает качество решения (измеренное, скажем, через R2), то это — спам. Она несёт отрицательную информацию. Выкинуть её.
То есть, в процессе вычислений, периодически по отношению к каждому “элементу” данных (примеру, аргументу, участнику) применяется эта процедура. Замеченные в спаме “элементы” выкидываются.
“Простой”, хм. Нет, конечно. Ведь даже вклад фичи (“столбца”) можно измерить кучей способов (выкидывание, labels shuffle, SHAP). А если это “строка”? А если это группа цифр, фич, и примеров от одного пользователя, буквой “кси” раскиданная по всей матрице?
И всё-таки я остановился на методе выкидывания:
-
Строим решение с полной матрицей.
-
Записываем его качество (например, среднюю абсолютную ошибку на тренировочных данных, или R2)
-
Временно выкидываем элемент (пользователя, пример, аргумент, данные из одного источника).
-
Строим решение.
-
Опять измеряем его качество.
-
Смотрим, насколько оно упало.
-
Проделываем это несколько раз в бутстрапе, чтобы подавить случайные корреляции.
Этот метод неидеален. Он груб и может выкинуть “честные” данные. Скажем, если две фичи несут близкую смысловую нагрузку, то выкидывание первой из них просто “перенесёт вес” на вторую и качество решения почти не изменится. Это как с двумя ногами: если поднять одну, скорее всего, не упадёшь — но это не значит, что поднятая нога ничего не делала :)
Тем не менее, у метода есть плюсы. Он легко обобщается на любые конфигурации данных. Он прост в написании и понятен в работе. Ну и временно потерять пусть даже важную информацию не так страшно, как перманентно впустить в модель мусор. Ведь хороший аргумент, скорее всего, будет повторён.
Разумеется, возможны и лучшие решения:
-
[Some text was replaced with its SHA256 value. Reason: “вот так”, Hash:20-C5-4B-E8-E4-81-02-BA-1E-B2-AD-6F-DD-33-E9-A3-F5-64-57-06-2F-AD-E3-3B-C9-51-12-A3-4F-C7-89-E0]
-
[Some text was replaced with its SHA256 value. Reason: “или так”, Hash:99-C3-B1-3F-CB-52-49-07-0C-5C-70-8D-97-D3-7B-CD-5E-66-DF-2B-22-C5-BF-95-9B-4E-DA-BC-BB-DF-64-2F]
4. Даже честное мнение участников дискуссии по поводу конкретных цифр в клеточках может резко разойтись.
Поэтому единицей хранения в каждой ячейке должна быть не цифра, а распределение. Охарактеризованное набором предложенных участниками цифр.
То есть, мы не просто допускаем ввод более чем одной цифры на ячейку, но вполне намеренно желаем этого. Как-то так:

Затем проходим по матрице и случайно надёргиваем из каждой строчки некоторое количество (в идеале много) возможных комбинаций параметров:

Выкидываем повторяющиеся и вносим каждую оставшуюся строчку в тренировочный набор.
[Some text was replaced with its SHA256 value. Reason: “У этой задачи тоже есть и лучшее решение, если не лень покодировать.”, Hash:13-F7-C8-39-60-75-78-18-0D-93-65-F6-80-38-E0-C6-2C-7B-51-2E-61-9A-84-51-09-FC-71-E4-03-F5-9F-48]
Мы не отбраковываем ничьи ответы до моделирования. Нет, мы включаем их в процесс и создаём модель, схватывающую в пределе весь спектр мнений в населении. Но вот потом мы смотрим, какие данные помогли создать связную картину, а какие помешали. Никакой цензуры. Данные объявляются мусором только после провала самых тщательных попыток ну хоть что-нибудь с их помощью предсказать.
Заметим, что в отношении значений меток (т.е. известных исходов прошлых примеров) этот метод неприменим по причинам фундаментальным. ML, в принципе, может отличать истину от лжи по противоречиям последней с “твёрдыми” истинами, пусть бы и противоречиям нетривиальным. Но для этого в задаче должны присутствовать “твёрдые” истины. То есть такие, с которыми если не все, то хотя бы большинство участников твёрдо согласны. Но если в группе нет единого мнения даже по вопросам вроде “Солнце — светит?”, то нет возможности и опереться хоть на какие-то общие утверждения так, чтобы все могли прошагать от них к выводам.
Поэтому с метками фактически устраиваем усреднение. Смотрим на разброс предложенных меток по вопросу. Если он слишком велик и участников много, можно попытаться выкинуть 1-2 “выброса”. Если разброс по-прежнему велик, сбрасываем всё и повторяем сбор информации. Если мал, берём среднее и объявляем его меткой. Цель этого процесса — не “установить истинное значение” метки (мы его не можем знать), а впустить в обсуждение только оценки меток с высоким внутренним согласием в группе. Чтобы иметь реперы для всем понятной апелляции к ним. И только.
(Люди при этом всё равно могут коллективно ошибаться! Когда таких ошибок не очень много, ML в принципе может “перевернуть” их и выделить позитивный сигнал. Но иногда и он не способен установить консенсус и тогда должен максимально рано отказаться от задачи).
5. Ответ без границ погрешности — мусор.
Это наименьшая из проблем. Стандартный ответ на это — resampling, bootstrap & cross-validation. То есть, разбиваем тренировочную матрицу на train/test случайно, ну, скажем, в пропорции 80/20. Тренируем. Предсказываем тест, вычисляем метрики качества. Предсказываем интересующую нас ситуацию. Повторяем это много раз. Усредняем предсказания, вычисляем их разброс и ошибку.
На малых данных с этим, однако, возникает большая трудность. Если точек для обучения всего штук так 13, то выкинуть три из них — непозволительная роскошь. Потому что не факт, что на оставшихся 10-ти регрессор обучится так же, как на всех 13-ти. Если это обучение вообще состоится.
Поэтому применяем jackknifing[5]. Который есть тот же resampling, но с выбором лишь одного элемента. А ошибка предсказания тогда — медиана ошибок предсказаний 12-ти (если точек было 13) моделей.
Да, почему в измерениях качества регрессора не стоит использовать среднее от ошибок (или среднеквадратичный разброс ответов), а хотя бы медиану и MAD[6], объяснять надо? Во многих регрессорах ответ возникает, логически, как результат деления X/Y, где обе величины могут быть зашумлены. Соответственно, если Y из-за шумов иногда приближается к нулю, то “хвост” ошибок таких регрессоров будет распределён по Парето[7] c α = 1.0. У этого распределения нет среднего. Ошибка такого регрессора, измеренная путём усреднения, будет становиться тем хуже, чем больше вы прогоняете тестов, надеясь “точнее” её измерить. От чего легко тронуться умом, особенно если время поджимает.
[Some text was replaced with its SHA256 value. Reason: “На самом деле даже медиана не гарантирует правильного измерения, но это тема размером на отдельную статью”, Hash:9A-09-8D-23-BC-06-3E-2A-67-7C-D0-F1-D7-D1-AB-F7-18-1D-41-6E-FA-E6-9A-D7-2F-78-FF-BE-C2-6D-CE-DA]
6. Мало данных. Нейронные сети требуют для работы хотя бы сотен записей. А у нас вся таблица может оказаться эдак 9 на 5.
Разумеется, это не сетями надо считать. А с помощью Random Forest или его родственников вроде XGBT или Boosted Trees. На малых цифровых таблицах с шумами и плохими данными они работают (и это известный факт, см. напр. [8]) даже лучше сетей. После оборачивания в мета-регрессоры, стабильные к пропускам, могут приличный результат показать так же Ridge, KernelRidge, и даже KNearestNeigbors.
(Мне думается, что быстрый прогресс в Big Data привёл к некоторой недооценке возможностей “тяжёлых” алгоритмов на “малых” данных. До рывка ML это было трудно считать, и ими занимались мало. После — скачком выросли объёмы данных, и все сфокусировались на вытачивании пользы из миллионов и миллиардов записей при помощи примерно линейных алгоритмов. В итоге квадрат “30 точек, дорогие алгоритмы” так и остался недоразработанным. А там сидят многие полезные задачи, повседневно формулируемые людьми и для людей.)
7. Примеры могут иметь несопоставимую значимость. Случайное наблюдение и повторённый в сотнях лабораторий опыт явно не должны одинаково влиять на ответ, но здесь оба занимают по одной строчке и рассматриваются ML-ом как равноценные.
Я не уверен, что нашёл наилучшее или даже хорошее решение. Но вижу его таким.
Да, первое, что приходит в голову при столкновении с такой проблемой — это выдать наблюдениям разные веса. Но откуда их взять? Даже когда участники согласны, что наблюдения из ИЯФ имеют больший вес, чем от Васи Пупкина, оценить количественно эту разницу сложно.
Можно попытаться вычислить вес наблюдения, применив рекурсивно тот же самый процесс дискуссии, что здесь разработан. Но сложность этого алгоритма легко может превысить экспоненциальную. Скорее же всего, он просто не сойдётся.
Поэтому я остановился на другом подходе: источники. У каждого наблюдения есть источник. Например, “наука”, “CNN”, “слухи”, “личное наблюдение”. Даже если эта маркировка не очень точна, она позволит модели группировать данные и вычислять степень полезности каждой группы для решения задачи. И, таким образом, выкидывать совсем уж негодные источники. Что и означает неявное придание больших весов тем, что более достоверны информационно.
Потенциально это открывает и возможность манипуляции со стороны злонамеренного участника. Не будучи в состоянии “прогнуть” модель в целом, он может, например, отвечать на вопросы явнейшим мусором, сообщая в качестве источника “наука”, и таким образом надеясь дискредитировать её в глазах ML.
Защита от этого основана на проверках на мусор и на выкидывании “плохих” данных в иерархическом порядке: пример > пользователь > аргумент > источник. То есть, если пользователь начинает вводить мусор, то в первую очередь система выкинет плохие примеры (но оставив возможность ввести их в игру и заполнить заново), а во вторую, при достаточном усердии со стороны пользователя… его самого :)
Защита эта, конечно, имеет лишь вероятностный характер и потому в принципе может быть обойдена.
Другой вариант, оставленный на откуп пользователям — это искусственно ввести дополнительную колонку, описывающую именно надёжность примера. В него можно вносить, скажем, десятичный логарифм количества людей, которых нужно обмануть, чтобы “перевернуть” консенсус об исходе примера. Так, если речь идёт о наблюдении НЛО, показавшемся исключительно Васе Пупкину, то эта фича будет нулём (ибо обмануть надо только Васю, а Log10(1) = 0). Если НЛО видела деревня в 1000 человек, то нужно вписать тройку. А если наблюдение звучит как “квантовая механика в общем и целом работает”, то надо ставить минимум 9.5. Потому что иначе отключатся сотовые телефоны в руках у трёх миллиардов человек на свете :)
8. Процесс заполнения таблицы на практике. На первый взгляд вопрос технический, но как его организовать, чтобы работало?
Нельзя же просто дать людям ссылку на Google Sheets и ждать, что всё получится. Как минимум из-за проблем 2-4.
Нужен протокол, который задаёт участникам по 1-2 вопроса, соблюдая ограничения п.2 и п.4, и объединяет ответы в таблицу, желательно даже не показывая её целиком до конца обсуждения. Собственно, на продумывание и создание этого протокола у меня и ушло больше половины времени.
Здесь, конечно, важно разделить транспорт и генерацию вопросов:
-
Транспорт — это лишь среда, через которую вопросы доходят до участников и возвращаются их ответы. Транспорт не должен знать о генерации вопросов ничего. Это должен быть вставляемый в систему модуль, заменимый любым другим с тем же интерфейсом. В предлагаемой имплементации поддерживаются Telegram и FileTransport. (Последний — просто обёртка CVS-файла и эмулирует заполнение матрицы из него синтетическими участниками, для тестирования.) В теории же транспортом может быть и email, и FTP, и объявления на аукционе, и хоть голубиная почта.
-
Генерация вопросов. Это механизм, который, глядя на уже имеющуюся матрицу, и, возможно, на некоторые статистики от участников, вычисляет, какие следующие вопросы и кому лучше всего задать для продолжения заполнения/расширения таблицы. Он, наоборот, о транспорте должен знать (почти) ничего.
Так вот, алгоритм генерации нетривиален. После кучи проб, хаков, патчей и переделок я сумел написать одну его имплементацию, в файле QManager.py. Она изрядно несовершенна. Я надеюсь, кто-нибудь напишет лучше. Нынешняя работает так:
-
Если есть примеры без меток — генерировать высокоприоритетные вопросы про их метки, пока они не заполнятся.
-
Проверить, нужны ли новые аргументы. В принципе, они нужны всегда, но есть ситуации, когда вот прямо сейчас не надо. Например, если аргументов уже значительно больше, чем примеров. Или если в матрице слишком много пропущенных данных. Или мало меток. В таких случаях мы с большой (но не 100%-й!) вероятностью пропускаем запрос новых аргументов. Когда не пропускаем, то генерим вопрос “а приведите аргумент/фичу, способствующие решению задачи?”
-
Проверить, нужны ли новые примеры. Примеры нужны ещё более, чем всегда, но тоже с исключениями. Например, если матрица всё ещё слишком “дырявая”. В подобных ситуациях запрос нового примера генерится с какой-то малой вероятностью, обычно же со 100%-й.
-
Проверить, есть ли в матрице пропуски. Если есть, попробовать задать вопрос о них.
-
(Опционально) просить новых данных для случайного элемента в матрице, даже уже заполненного. С целью интеграции всех возможных мнений и (в дальнем пределе) исключения ошибочных вбросов.
При выборе пользователей, которым задаются вопросы, надо следить, чтобы ни один из них не вносил существенно большую пропорцию ответов в каждую категорию данных, нежели допускается случайным совпадением. Во избежание возникновения ситуаций, когда один участник контролирует, например, 51% примеров или аргументов.
На этом, пожалуй, хватит.
Ибо текста уже много. Однако это — лишь примерно треть описания проекта. Кому интересно, прочитать всё можно, взяв проект с Гитхаба[2] или запасного (временного) миррора[3].
А картинка?
Да, в порядке шутки картинка содержит в себе проект, закодированный в PNG. Раскодировать его можно так:
-
Сохраняете её в локальную папку как “src.png”
-
В той же папке исполняете следующий код на питоне:
from PIL import Image
img = Image.open("src.png")
raw_data = img.tobytes()
size = struct.unpack("<I", raw_data[:4])[0]
file_data = raw_data[4:4+size]
with open("src.zip, "wb") as f: f.write(file_data)
На выходе получаете src.zip всего проекта.
Ссылки
Всем спасибо!
Автор: eugeneb0