Сейчас в глазах обычной публики машинное обучение прочно ассоциируется с различными вариантами обучения нейронных сетей. Если первоначально это были полносвязные сети, потом заместившиеся сверточными и рекуррентными, то теперь это стало совсем экзотическими вариантами типа GAN и LTSM-сетей. Кроме все больших объемов выборок, требуемых для их обучения, они еще страдают невозможностью объяснить, почему было принято то или иное решение. Но существуют и структурные подходы к машинному обучению, программная реализация одного из которых описана в настоящей статье.
![](https://webcf.waybackmachine.org/web/20240830172445im_/https://habrastorage.org/r/w780q1/webt/zk/vs/b9/zkvsb9zh3kabo0u-izlokug_xkk.jpeg)
Это отечественный подход к машинному обучению, получивший название ВКФ-метод машинного обучения, основанного на теории решеток. История возникновения и выбор названия объясняется в самом конце настоящей статьи.
Первоначально вся система была создана автором на С++ как консольное приложение, затем была соединена с БД под управлением СУБД MariaDB (с использованием библиотеки mariаdb++), потом превращена в СPython-библиотеку (с использованием пакета pybind11).
В качестве тестовых данных были выбраны несколько массивов для тестирования алгоритмов машинного обучения из репозитория Университета Калифорнии в г. Ирвайн.
На массиве Mushrooms, содержащем описания 8124 грибов Северной Америки, система показала 100% результат. Точнее, датчиком случайных чисел исходные данные были разделены на обучающую выборку (2088 съедобных и 1944 ядовитых гриба) и тестовую выборку (2120 съедобных и 1972 ядовитых). После вычисления около 100 гипотез о причинах съедобности, все тестовые примеры были предсказаны правильно. Так как алгоритм использует спаренную цепь Маркова, то достаточное число гипотез может варьироваться. Довольно часто оказывалось достаточным породить 50 случайных гипотез. Замечу, что при порождении причин ядовитости, число требуемых гипотез группируется вокруг 120, тем не менее, все тестовые примеры и в этом случае предсказываются правильно. На Kaggle.com имеется соревнование Mushroom Classification, где довольно много авторов достигли 100% точности. Но большинство из решений — нейронные сети. Наш же подход позволяет грибнику выучить всего около 50 правил. Так как большинство признаков несущественны, то и каждая гипотеза будет конъюнкцией малого числа значений существенных признаков, что позволяет их легко запомнить. После этого грибник может идти за грибами, не боясь взять поганку или пропустить съедобный гриб.
Вот пример одной из гипотез, на основании которой можно считать, что гриб съедобный:
[('gill_attachment', 'free'), ('gill_spacing', 'close'), ('gill_size', 'broad'), ('stalk_shape', 'enlarging'), ('stalk_surface_below_ring', 'scaly'), ('veil_type', 'partial'), ('veil_color', 'white'), ('ring_number', 'one'), ('ring_type', 'pendant')]
Обращаю внимание, что только 9 из 22 признаков приведены в списке, так как по остальным 13 признакам сходства у съедобных грибов, породивших эту причину, не наблюдается.
Другим массивом был SPECT Hearts. Там точность предсказания тестовых примеров достигала 86.1%, что оказалось несколько больше результатов (84%) системы машинного обучения CLIP3, основанной на обучении покрытия примеров с помощью целочисленного программирования, применяемой авторами массива. Полагаю, что из-за структуры описания томограмм сердца, которые там уже предварительно закодированы бинарными признаками, существенно улучшить качество прогноза не представляется возможным.
Автор совсем недавно придумал (и программно реализовал) расширение своего подхода на обработку данных, описываемых непрерывными (числовыми) признаками. В некотором аспекте его подход аналогичен системе C4.5 обучения деревьев решений. Тестирование этого варианта проходило на массиве Wine Quality. Этот массив описывает качество португальских вин. Результаты обнадеживающие: если брать высококачественные красные вина, то гипотезы полностью объясняют их высокие оценки.
В настоящее время усилиями студентов отделения интеллектуальных систем РГГУ создается серия web-серверов для разного типа задач (с использованием связки Nginx + Gunicorn + Django).
Однако я решил описать здесь свой личный вариант (с использованием связки aiohttp, aiojobs и aiomysql). Модуль aiomcache не используется из-за известных проблем с безопасностью.
Есть несколько плюсов у предлагаемого варианта:
Укажем и на очевидные минусы (по сравнению с Django):
Каждый из двух вариантов нацелен на разные стратегии работы с web-сервером. Синхронная стратегия (на Django) нацелена на однопользовательский режим, при котором эксперт в каждый момент времени работает с единственной БД. Хотя вероятностные процедуры ВКФ-метода замечательно распараллеливаются, тем не менее, теоретически не исключен случай, когда процедуры машинного обучения будут занимать значительное время. Поэтому обсуждаемый в настоящей заметке вариант нацелен на несколько экспертов, каждый из которых может одновременно работать (в разных вкладках браузера) с разными БД, отличающимися не только данными, но и способами их представления (разные решетки на значениях дискретных признаков, разные значимые регрессии и число порогов для непрерывных). Тогда при запуске ВКФ-эксперимента в одной вкладке, эксперт может переключиться на другую, где будет подготавливать или анализировать эксперимент с другими данными и/или параметрами.
Для учета нескольких пользователей, экспериментов и разных этапов, на которых они находятся, имеется служебная база данных (vkf) с двумя таблицами (users, experiments). Если таблица user хранит login и password всех зарегистрированных пользователей, то experiments кроме имен вспомогательных и главных таблиц каждого эксперимента сохраняет статус заполненности этих таблиц. Мы отказались от aiohttp_session, так как все равно потребуется использовать proxy-сервер Nginx для защиты критических данных.
Вот структура таблицы experiments:
Следует отметить, что имеются некоторые последовательности подготовки данных для ВКФ-экспериментов, которые, к сожалению, радикально отличаются для дискретного и непрерывного случаев. Случай смешанных признаков соединяет требования обоих типов.
дискретный: => goodLattices (полуавтоматический)
дискретный: goodLattices => goodEncoder (автоматический)
дискретный: goodEncoder => goodTrains (полуавтоматический)
дискретный: goodEncoder, goodTrains => goodHypotheses (автоматический)
дискретный: goodEncoder => goodTests (полуавтоматический)
дискретный: goodTests, goodEncoder, goodHypotheses => (автоматический)
непрерывный: => goodVerges (ручной)
непрерывный: goodVerges => goodTrains (ручной)
непрерывный: goodTrains => goodComplex (автоматический)
непрерывный: goodComplex, goodTrains => goodHypotheses (автоматический)
непрерывный: goodVerges => goodTests (ручной)
непрерывный: goodTests, goodComplex, goodHypotheses => (автоматический)
Сама библиотека машинного обучения имеет имя vkf.cpython-36m-x86_64-linux-gnu.so под Linux или vkf.cp36-win32.pyd под Windows. (36 — это версия Python, для которого эта библиотека собиралась).
Термин «автоматический» означает работу этой библиотеки, «полуавтоматический» означает работу вспомогательной библиотеки vkfencoder.cpython-36m-x86_64-linux-gnu.so. Наконец, «ручной» режим — это вызов программ, специально обрабатывающих данные конкретного эксперимента и переносимых сейчас в библиотеку vkfencoder.
При создании web-сервера мы используем подход «View/Model/Control»
Код на языке Python размещен в 5 файлах:
Файл app.py имеет стандартный вид:
Не думаю, что здесь что-то нуждается в пояснениях. Следующий по порядку включения в проект файл — views.py:
Я сократил для настоящей заметки этот файл, выкинув классы, обслуживающие служебные маршруты:
Оставшиеся классы обрабатывают маршруты, отвечающие за этапы машинного обучения:
Для передачи большого числа параметров в форму vkf.html используется конструкция из aiohttp_jinja2
Отметим также использование вызова spawn из пакета aiojobs.aiohttp:
Это необходимо для безопасного вызова сопроцедур из классов, определенных в файле models.py, обрабатывающих данные пользователя и экспериментов, хранимые в БД под управлением СУБД MariaDB:
Опять некоторые вспомогательные классы скрыты:
Оставшиеся классы соответствуют главным процедурам:
Важно отметить использование конструкции create_pool() пакета aiomysql. Она позволяет работать с БД в несколько соединений. Для ожидания окончания выполнения также нужны процедуры ensure_future() и gather() из модуля asyncio.
При чтении из таблицы конструкция row = cur.fetchone() возвращает future, поэтому row.result() выдает запись БД, из которой могут быть извлечены значения полей (например, str(row.result()[2]) извлекает имя таблицы с кодированием значений дискретных признаков).
Ключевые параметры системы импортируются из файла .env или (при его отсутствии) из файла settings.py.
Важно заметить, что localhost нужно указывать по ip-адресу, иначе aiomysql попытается соединиться с БД через Unix socket, что может не работать под Windows. Наконец, воспроизведем последний файл (control.py):
Я сохранил этот файл полностью, так как здесь видны названия, порядок вызова и аргументы процедур ВКФ-метода из библиотеки vkf.cpython-36m-x86_64-linux-gnu.so. Все аргументы после dbname могут быть опущены, так как значения по умолчанию в CPython-библиотеке установлены со стандартными значениями.
Предвосхищая вопрос профессиональных программистов о том, почему логика управления ВКФ-экспериментом вынесена наружу (через многочисленные if), а не спрятана через полиморфизм в типы, следует ответить так: к сожалению, динамическая типизация языка Python не позволяет переложить решение о типе используемого объекта на систему, то есть в любом случае возникнет эта последовательность вложенных if. Поэтому автор предпочел использовать явный (C-подобный) синтаксис, чтобы сделать логику максимально прозрачной (и эффективной).
Позволю себе прокомментировать отсутствующие компоненты:
Автор занимается задачами интеллектуального анализа данных более 30 лет. После окончания механико-математического факультета МГУ им М.В. Ломоносова он был приглашен в группу исследователей под руководством д.т.н., проф. В.К. Финна (ВИНИТИ АН СССР). Виктор Константинович с начала 80-х годов прошлого века исследует правдоподобные рассуждения и их формализацию средствами многозначных логик.
Ключевыми идеями, предложенными В.К. Финном, можно считать следующие:
Следует отметить, что В.К. Финн приписывает некоторые из своих идей зарубежным авторам. Пожалуй, только логика аргументации с полным правом считается им придуманной самостоятельно. Идею учета контр-примеров В.К. Финн заимствовал, по его словам, у К.Р. Поппера. А истоки проверки полноты индуктивного обобщения относятся им к (совершенно туманным, на мой взгляд) работам американского математика и логика Ч.С. Пирса. Порождение гипотез о причинах с помощью операции сходства он считает заимствованным из идей британского экономиста, философа и логика Д.С. Милля. Поэтому созданный им комплекс идей он озаглавил «ДСМ-метод» в честь Д.С. Милля.
Странно, но возникший в конце 70-х годов XX века в трудах проф. Рудольфа Вилле (ФРГ) гораздо более полезный раздел алгебраической теории решеток «Анализ формальных понятий» (АФП) не пользуется у В.К. Финна уважением. На мой взгляд, причина этого — неудачное название, которое у него, как у человека, закончившего сначала философский факультет, а потом инженерный поток механико-математического факультета МГУ, вызывает отторжение.
Как продолжатель дела своего учителя, автор свой подход назвал в его честь «ВКФ-метод». Впрочем, имеется и другая расшифровка — вероятностно-комбинаторный формальный метод машинного обучения, основанного на теории решеток.
Сейчас группа В.К. Финна работает в ВЦ им. А.А. Дородницына РАН ФИЦ ИУ РАН и на отделении интеллектуальных систем Российского Государственного Гуманитарного Университета.
Подробнее с математикой ВКФ-решателя можно ознакомится по диссертации автора или его видео-лекциям в Ульяновском Государственном Университете (за организацию лекций и обработку их записей автор благодарен А.Б. Веревкину и Н.Г. Баранец).
Полный пакет исходных файлов хранится на Bitbucket.
Исходные файлы (на С++) для библиотеки vkf находятся в процессе согласования их размещения на savannah.nongnu.org. При положительном решении, ссылка на скачивание будет добавлена сюда.
Наконец, последнее замечание: автор начал изучать Python 6 апреля 2020 года. До этого единственным языком, на котором он программировал, был C++. Но это обстоятельство не снимает с него обвинений в возможной неаккуратности кода.
Автор выражает сердечную благодарность Татьяне А. Волковой robofreak за поддержку, конструктивные предложения и критические замечания, позволившие существенно улучшить изложение (и даже значительно улучшить код). Впрочем, ответственность за оставшиеся ошибки и принятые решения (даже вопреки ее советам) несет исключительно автор.
![](https://webcf.waybackmachine.org/web/20240830172445/https://habrastorage.org/webt/zk/vs/b9/zkvsb9zh3kabo0u-izlokug_xkk.jpeg)
Это отечественный подход к машинному обучению, получивший название ВКФ-метод машинного обучения, основанного на теории решеток. История возникновения и выбор названия объясняется в самом конце настоящей статьи.
1. Описание метода
Первоначально вся система была создана автором на С++ как консольное приложение, затем была соединена с БД под управлением СУБД MariaDB (с использованием библиотеки mariаdb++), потом превращена в СPython-библиотеку (с использованием пакета pybind11).
В качестве тестовых данных были выбраны несколько массивов для тестирования алгоритмов машинного обучения из репозитория Университета Калифорнии в г. Ирвайн.
На массиве Mushrooms, содержащем описания 8124 грибов Северной Америки, система показала 100% результат. Точнее, датчиком случайных чисел исходные данные были разделены на обучающую выборку (2088 съедобных и 1944 ядовитых гриба) и тестовую выборку (2120 съедобных и 1972 ядовитых). После вычисления около 100 гипотез о причинах съедобности, все тестовые примеры были предсказаны правильно. Так как алгоритм использует спаренную цепь Маркова, то достаточное число гипотез может варьироваться. Довольно часто оказывалось достаточным породить 50 случайных гипотез. Замечу, что при порождении причин ядовитости, число требуемых гипотез группируется вокруг 120, тем не менее, все тестовые примеры и в этом случае предсказываются правильно. На Kaggle.com имеется соревнование Mushroom Classification, где довольно много авторов достигли 100% точности. Но большинство из решений — нейронные сети. Наш же подход позволяет грибнику выучить всего около 50 правил. Так как большинство признаков несущественны, то и каждая гипотеза будет конъюнкцией малого числа значений существенных признаков, что позволяет их легко запомнить. После этого грибник может идти за грибами, не боясь взять поганку или пропустить съедобный гриб.
Вот пример одной из гипотез, на основании которой можно считать, что гриб съедобный:
[('gill_attachment', 'free'), ('gill_spacing', 'close'), ('gill_size', 'broad'), ('stalk_shape', 'enlarging'), ('stalk_surface_below_ring', 'scaly'), ('veil_type', 'partial'), ('veil_color', 'white'), ('ring_number', 'one'), ('ring_type', 'pendant')]
Обращаю внимание, что только 9 из 22 признаков приведены в списке, так как по остальным 13 признакам сходства у съедобных грибов, породивших эту причину, не наблюдается.
Другим массивом был SPECT Hearts. Там точность предсказания тестовых примеров достигала 86.1%, что оказалось несколько больше результатов (84%) системы машинного обучения CLIP3, основанной на обучении покрытия примеров с помощью целочисленного программирования, применяемой авторами массива. Полагаю, что из-за структуры описания томограмм сердца, которые там уже предварительно закодированы бинарными признаками, существенно улучшить качество прогноза не представляется возможным.
Автор совсем недавно придумал (и программно реализовал) расширение своего подхода на обработку данных, описываемых непрерывными (числовыми) признаками. В некотором аспекте его подход аналогичен системе C4.5 обучения деревьев решений. Тестирование этого варианта проходило на массиве Wine Quality. Этот массив описывает качество португальских вин. Результаты обнадеживающие: если брать высококачественные красные вина, то гипотезы полностью объясняют их высокие оценки.
2. Выбор платформы
В настоящее время усилиями студентов отделения интеллектуальных систем РГГУ создается серия web-серверов для разного типа задач (с использованием связки Nginx + Gunicorn + Django).
Однако я решил описать здесь свой личный вариант (с использованием связки aiohttp, aiojobs и aiomysql). Модуль aiomcache не используется из-за известных проблем с безопасностью.
Есть несколько плюсов у предлагаемого варианта:
- он асинхронный из-за использования aiohttp;
- он допускает обработку шаблонов Jinja2;
- он работает с пулом соединений к БД через aiomysql;
- он обеспечивает запуск независимых вычислительных процессов через aiojobs.aiohttp.spawn.
Укажем и на очевидные минусы (по сравнению с Django):
- нет Object Relational Mapping (ORM);
- труднее организуется использование proxy-сервера Nginx;
- нет Django Template Language (DTL).
Каждый из двух вариантов нацелен на разные стратегии работы с web-сервером. Синхронная стратегия (на Django) нацелена на однопользовательский режим, при котором эксперт в каждый момент времени работает с единственной БД. Хотя вероятностные процедуры ВКФ-метода замечательно распараллеливаются, тем не менее, теоретически не исключен случай, когда процедуры машинного обучения будут занимать значительное время. Поэтому обсуждаемый в настоящей заметке вариант нацелен на несколько экспертов, каждый из которых может одновременно работать (в разных вкладках браузера) с разными БД, отличающимися не только данными, но и способами их представления (разные решетки на значениях дискретных признаков, разные значимые регрессии и число порогов для непрерывных). Тогда при запуске ВКФ-эксперимента в одной вкладке, эксперт может переключиться на другую, где будет подготавливать или анализировать эксперимент с другими данными и/или параметрами.
Для учета нескольких пользователей, экспериментов и разных этапов, на которых они находятся, имеется служебная база данных (vkf) с двумя таблицами (users, experiments). Если таблица user хранит login и password всех зарегистрированных пользователей, то experiments кроме имен вспомогательных и главных таблиц каждого эксперимента сохраняет статус заполненности этих таблиц. Мы отказались от aiohttp_session, так как все равно потребуется использовать proxy-сервер Nginx для защиты критических данных.
Вот структура таблицы experiments:
- id int(11) NOT NULL PRIMARY KEY
- expName varchar(255) NOT NULL
- encoder varchar(255)
- goodEncoder tinyint(1)
- lattices varchar(255)
- goodLattices tinyint(1)
- complex varchar(255)
- goodComplex tinyint(1)
- verges varchar(255)
- goodVerges tinyint(1)
- vergesTotal int(11)
- trains varchar(255) NOT NULL
- goodTrains tinyint(1)
- tests varchar(255)
- goodTests tinyint(1)
- hypotheses varchar(255) NOT NULL
- goodHypotheses tinyint(1)
- type varchar(255) NOT NULL
Следует отметить, что имеются некоторые последовательности подготовки данных для ВКФ-экспериментов, которые, к сожалению, радикально отличаются для дискретного и непрерывного случаев. Случай смешанных признаков соединяет требования обоих типов.
дискретный: => goodLattices (полуавтоматический)
дискретный: goodLattices => goodEncoder (автоматический)
дискретный: goodEncoder => goodTrains (полуавтоматический)
дискретный: goodEncoder, goodTrains => goodHypotheses (автоматический)
дискретный: goodEncoder => goodTests (полуавтоматический)
дискретный: goodTests, goodEncoder, goodHypotheses => (автоматический)
непрерывный: => goodVerges (ручной)
непрерывный: goodVerges => goodTrains (ручной)
непрерывный: goodTrains => goodComplex (автоматический)
непрерывный: goodComplex, goodTrains => goodHypotheses (автоматический)
непрерывный: goodVerges => goodTests (ручной)
непрерывный: goodTests, goodComplex, goodHypotheses => (автоматический)
Сама библиотека машинного обучения имеет имя vkf.cpython-36m-x86_64-linux-gnu.so под Linux или vkf.cp36-win32.pyd под Windows. (36 — это версия Python, для которого эта библиотека собиралась).
Термин «автоматический» означает работу этой библиотеки, «полуавтоматический» означает работу вспомогательной библиотеки vkfencoder.cpython-36m-x86_64-linux-gnu.so. Наконец, «ручной» режим — это вызов программ, специально обрабатывающих данные конкретного эксперимента и переносимых сейчас в библиотеку vkfencoder.
3. Детали реализации
При создании web-сервера мы используем подход «View/Model/Control»
Код на языке Python размещен в 5 файлах:
- app.py — файл запуска приложения
- control.py — файл с процедурами работы с ВКФ-решателем
- models.py — файл с классами обработки данных и работы с БД
- settings.py — файл с настройками приложения
- views.py — файл с визуализацией и обработкой маршрутов (routes).
Файл app.py имеет стандартный вид:
#! /usr/bin/env python
import asyncio
import jinja2
import aiohttp_jinja2
from settings import SITE_HOST as siteHost
from settings import SITE_PORT as sitePort
from aiohttp import web
from aiojobs.aiohttp import setup
from views import routes
async def init(loop):
app = web.Application(loop=loop)
# install aiojobs.aiohttp
setup(app)
# install jinja2 templates
aiohttp_jinja2.setup(app,
loader=jinja2.FileSystemLoader('./template'))
# add routes from api/views.py
app.router.add_routes(routes)
return app
loop = asyncio.get_event_loop()
try:
app = loop.run_until_complete(init(loop))
web.run_app(app, host=siteHost, port=sitePort)
except:
loop.stop()
Не думаю, что здесь что-то нуждается в пояснениях. Следующий по порядку включения в проект файл — views.py:
import aiohttp_jinja2
from aiohttp import web#, WSMsgType
from aiojobs.aiohttp import spawn#, get_scheduler
from models import User
from models import Expert
from models import Experiment
from models import Solver
from models import Predictor
routes = web.RouteTableDef()
@routes.view(r'/tests/{name}', name='test-name')
class Predict(web.View):
@aiohttp_jinja2.template('tests.html')
async def get(self):
return {'explanation': 'Please, confirm prediction!'}
async def post(self):
data = await self.request.post()
db_name = self.request.match_info['name']
analogy = Predictor(db_name, data)
await analogy.load_data()
job = await spawn(self.request, analogy.make_prediction())
return await job.wait()
@routes.view(r'/vkf/{name}', name='vkf-name')
class Generate(web.View):
#@aiohttp_jinja2.template('vkf.html')
async def get(self):
db_name = self.request.match_info['name']
solver = Solver(db_name)
await solver.load_data()
context = { 'dbname': str(solver.dbname),
'encoder': str(solver.encoder),
'lattices': str(solver.lattices),
'good_lattices': bool(solver.lattices),
'verges': str(solver.verges),
'good_verges': bool(solver.good_verges),
'complex': str(solver.complex),
'good_complex': bool(solver.good_complex),
'trains': str(solver.trains),
'good_trains': bool(solver.good_trains),
'hypotheses': str(solver.hypotheses),
'type': str(solver.type)
}
response = aiohttp_jinja2.render_template('vkf.html',
self.request, context)
return response
async def post(self):
data = await self.request.post()
step = data.get('value')
db_name = self.request.match_info['name']
if step is 'init':
location = self.request.app.router['experiment-name'].url_for(
name=db_name)
raise web.HTTPFound(location=location)
solver = Solver(db_name)
await solver.load_data()
if step is 'populate':
job = await spawn(self.request, solver.create_tables())
return await job.wait()
if step is 'compute':
job = await spawn(self.request, solver.compute_tables())
return await job.wait()
if step is 'generate':
hypotheses_total = data.get('hypotheses_total')
threads_total = data.get('threads_total')
job = await spawn(self.request, solver.make_induction(
hypotheses_total, threads_total))
return await job.wait()
@routes.view(r'/experiment/{name}', name='experiment-name')
class Prepare(web.View):
@aiohttp_jinja2.template('expert.html')
async def get(self):
return {'explanation': 'Please, enter your data'}
async def post(self):
data = await self.request.post()
db_name = self.request.match_info['name']
experiment = Experiment(db_name, data)
job = await spawn(self.request, experiment.create_experiment())
return await job.wait()
Я сократил для настоящей заметки этот файл, выкинув классы, обслуживающие служебные маршруты:
- класс Auth привязан к корневому маршруту '/' и выводит форму запроса на идентификацию пользователя. Если пользователь не зарегистрирован, имеется кнопка SignIn, которая перенаправляет пользователя по маршруту '/signin'. Если же пользователь с введенными логином и паролем обнаруживается, то он перенаправляется по маршруту '/user/{name}'.
- класс SignIn обрабатывает маршрут '/signin' и после успешной регистрации возвращает пользователя на корневой маршрут.
- класс Select обрабатывает маршруты '/user/{name}' и запрашивает, какой эксперимент и на каком этапе пользователь желает провести. После проверки наличия такого эксперимента БД пользователь перенаправляется на маршрут '/vkf/{name}' или '/experiment/{name}' (если эксперимента еще было зарегистрировано).
Оставшиеся классы обрабатывают маршруты, отвечающие за этапы машинного обучения:
- класс Prepare обрабатывает маршруты '/experiment/{name}' и собирает имена служебных таблиц и числовые параметры, необходимые для запуска процедур ВКФ-метода. После сохранения этой информации в БД пользователь перенаправляется на маршрут '/vkf/{name}'.
- класс Generate обрабатывает маршруты '/vkf/{name}' и запускает различные этапы процедуры индукции ВКФ-метода в зависимости от подготовленности данных экспертом.
- класс Predict обрабатывает маршруты '/tests/{name}' и запускает процедуру ВКФ-метода предсказания по аналогии.
Для передачи большого числа параметров в форму vkf.html используется конструкция из aiohttp_jinja2
response = aiohttp_jinja2.render_template('vkf.html', self.request, context)
return response
Отметим также использование вызова spawn из пакета aiojobs.aiohttp:
job = await spawn(self.request,
solver.make_induction(hypotheses_total, threads_total))
return await job.wait()
Это необходимо для безопасного вызова сопроцедур из классов, определенных в файле models.py, обрабатывающих данные пользователя и экспериментов, хранимые в БД под управлением СУБД MariaDB:
import aiomysql
from aiohttp import web
from settings import AUX_NAME as auxName
from settings import AUTH_TABLE as authTable
from settings import AUX_TABLE as auxTable
from settings import SECRET_KEY as secretKey
from settings import DB_HOST as dbHost
from control import createAuxTables
from control import createMainTables
from control import computeAuxTables
from control import induction
from control import prediction
class Experiment():
def __init__(self, dbName, data, **kw):
self.encoder = data.get('encoder_table')
self.lattices = data.get('lattices_table')
self.complex = data.get('complex_table')
self.verges = data.get('verges_table')
self.verges_total = data.get('verges_total')
self.trains = data.get('training_table')
self.tests = data.get('tests_table')
self.hypotheses = data.get('hypotheses_table')
self.type = data.get('type')
self.auxname = auxName
self.auxtable = auxTable
self.dbhost = dbHost
self.secret = secretKey
self.dbname = dbName
async def create_db(self, pool):
async with pool.acquire() as conn:
async with conn.cursor() as cur:
await cur.execute("CREATE DATABASE IF NOT EXISTS " +
str(self.dbname))
await conn.commit()
await createAuxTables(self)
async def register_experiment(self, pool):
async with pool.acquire() as conn:
async with conn.cursor() as cur:
sql = "INSERT INTO " + str(self.auxname) + "." +
str(self.auxtable)
sql += " VALUES(NULL, '"
sql += str(self.dbname)
sql += "', '"
sql += str(self.encoder)
sql += "', 0, '" #goodEncoder
sql += str(self.lattices)
sql += "', 0, '" #goodLattices
sql += str(self.complex)
sql += "', 0, '" #goodComplex
sql += str(self.verges_total)
sql += "', 0, " #goodVerges
sql += str(self.verges_total)
sql += ", '"
sql += str(self.trains)
sql += "', 0, '" #goodTrains
sql += str(self.tests)
sql += "', 0, '" #goodTests
sql += str(self.hypotheses)
sql += "', 0, '" #goodHypotheses
sql += str(self.type)
sql += "')"
await cur.execute(sql)
await conn.commit()
async def create_experiment(self, **kw):
pool = await aiomysql.create_pool(host=self.dbhost,
user='root', password=self.secret)
task1 = self.create_db(pool=pool)
task2 = self.register_experiment(pool=pool)
tasks = [asyncio.ensure_future(task1),
asyncio.ensure_future(task2)]
await asyncio.gather(*tasks)
pool.close()
await pool.wait_closed()
raise web.HTTPFound(location='/vkf/' + self.dbname)
class Solver():
def __init__(self, dbName, **kw):
self.auxname = auxName
self.auxtable = auxTable
self.dbhost = dbHost
self.dbname = dbName
self.secret = secretKey
async def load_data(self, **kw):
pool = await aiomysql.create_pool(host=dbHost,
user='root', password=secretKey, db=auxName)
async with pool.acquire() as conn:
async with conn.cursor() as cur:
sql = "SELECT * FROM "
sql += str(auxTable)
sql += " WHERE expName='"
sql += str(self.dbname)
sql += "'"
await cur.execute(sql)
row = cur.fetchone()
await cur.close()
pool.close()
await pool.wait_closed()
self.encoder = str(row.result()[2])
self.good_encoder = bool(row.result()[3])
self.lattices = str(row.result()[4])
self.good_lattices = bool(row.result()[5])
self.complex = str(row.result()[6])
self.good_complex = bool(row.result()[7])
self.verges = str(row.result()[8])
self.good_verges = bool(row.result()[9])
self.verges_total = int(row.result()[10])
self.trains = str(row.result()[11])
self.good_trains = bool(row.result()[12])
self.hypotheses = str(row.result()[15])
self.good_hypotheses = bool(row.result()[16])
self.type = str(row.result()[17])
async def create_tables(self, **kw):
await createMainTables(self)
pool = await aiomysql.create_pool(host=self.dbhost, user='root',
password=self.secret, db=self.auxname)
async with pool.acquire() as conn:
async with conn.cursor() as cur:
sql = "UPDATE "
sql += str(self.auxtable)
sql += " SET encoderStatus=1 WHERE dbname='"
sql += str(self.dbname)
sql += "'"
await cur.execute(sql)
await conn.commit()
await cur.close()
pool.close()
await pool.wait_closed()
raise web.HTTPFound(location='/vkf/' + self.dbname)
async def compute_tables(self, **kw):
await computeAuxTables(self)
pool = await aiomysql.create_pool(host=self.dbhost, user='root',
password=self.secret, db=self.auxname)
async with pool.acquire() as conn:
async with conn.cursor() as cur:
sql = "UPDATE "
sql += str(self.auxtable)
sql += " SET complexStatus=1 WHERE dbname='"
sql += str(self.dbname)
sql += "'"
await cur.execute(sql)
await conn.commit()
await cur.close()
pool.close()
await pool.wait_closed()
raise web.HTTPFound(location='/vkf/' + self.dbname)
async def make_induction(self, hypotheses_total, threads_total, **kw):
await induction(self, hypotheses_total, threads_total)
pool = await aiomysql.create_pool(host=self.dbhost, user='root',
password=self.secret, db=self.auxname)
async with pool.acquire() as conn:
async with conn.cursor() as cur:
sql = "UPDATE "
sql += str(self.auxtable)
sql += " SET hypothesesStatus=1 WHERE dbname='"
sql += str(self.dbname)
sql += "'"
await cur.execute(sql)
await conn.commit()
await cur.close()
pool.close()
await pool.wait_closed()
raise web.HTTPFound(location='/tests/' + self.dbname)
class Predictor():
def __init__(self, dbName, data, **kw):
self.auxname = auxName
self.auxtable = auxTable
self.dbhost = dbHost
self.dbname = dbName
self.secret = secretKey
self.plus = 0
self.minus = 0
async def load_data(self, **kw):
pool = await aiomysql.create_pool(host=dbHost, user='root',
password=secretKey, db=auxName)
async with pool.acquire() as conn:
async with conn.cursor() as cur:
sql = "SELECT * FROM "
sql += str(auxTable)
sql += " WHERE dbname='"
sql += str(self.dbname)
sql += "'"
await cur.execute(sql)
row = cur.fetchone()
await cur.close()
pool.close()
await pool.wait_closed()
self.encoder = str(row.result()[2])
self.good_encoder = bool(row.result()[3])
self.complex = str(row.result()[6])
self.good_complex = bool(row.result()[7])
self.verges = str(row.result()[8])
self.trains = str(row.result()[11])
self.tests = str(row.result()[13])
self.good_tests = bool(row.result()[14])
self.hypotheses = str(row.result()[15])
self.good_hypotheses = bool(row.result()[16])
self.type = str(row.result()[17])
async def make_prediction(self, **kw):
if self.good_tests and self.good_hypotheses:
await induction(self, 0, 1)
await prediction(self)
message_body = str(self.plus)
message_body += " correct positive cases. "
message_body += str(self.minus)
message_body += " correct negative cases."
raise web.HTTPException(body=message_body)
else:
raise web.HTTPFound(location='/vkf/' + self.dbname)
Опять некоторые вспомогательные классы скрыты:
- Класс User соответствует посетителю сайта. Он позволяет зарегистрироваться и зайти в систему как эксперт.
- Класс Expert позволяет выбрать один из экспериментов.
Оставшиеся классы соответствуют главным процедурам:
- Класс Experiment позволяет задать имена ключевых и вспомогательных таблиц и параметры, необходимые для проведения ВКФ-экспериментов.
- Класс Solver отвечает за индуктивное обобщение в ВКФ-методе.
- Класс Predictor отвечает за предсказания по аналогии в ВКФ-методе.
Важно отметить использование конструкции create_pool() пакета aiomysql. Она позволяет работать с БД в несколько соединений. Для ожидания окончания выполнения также нужны процедуры ensure_future() и gather() из модуля asyncio.
pool = await aiomysql.create_pool(host=self.dbhost,
user='root', password=self.secret)
task1 = self.create_db(pool=pool)
task2 = self.register_experiment(pool=pool)
tasks = [asyncio.ensure_future(task1),
asyncio.ensure_future(task2)]
await asyncio.gather(*tasks)
pool.close()
await pool.wait_closed()
При чтении из таблицы конструкция row = cur.fetchone() возвращает future, поэтому row.result() выдает запись БД, из которой могут быть извлечены значения полей (например, str(row.result()[2]) извлекает имя таблицы с кодированием значений дискретных признаков).
pool = await aiomysql.create_pool(host=dbHost, user='root',
password=secretKey, db=auxName)
async with pool.acquire() as conn:
async with conn.cursor() as cur:
await cur.execute(sql)
row = cur.fetchone()
await cur.close()
pool.close()
await pool.wait_closed()
self.encoder = str(row.result()[2])
Ключевые параметры системы импортируются из файла .env или (при его отсутствии) из файла settings.py.
from os.path import isfile
from envparse import env
if isfile('.env'):
env.read_envfile('.env')
AUX_NAME = env.str('AUX_NAME', default='vkf')
AUTH_TABLE = env.str('AUTH_TABLE', default='users')
AUX_TABLE = env.str('AUX_TABLE', default='experiments')
DB_HOST = env.str('DB_HOST', default='127.0.0.1')
DB_HOST = env.str('DB_PORT', default=3306)
DEBUG = env.bool('DEBUG', default=False)
SECRET_KEY = env.str('SECRET_KEY', default='toor')
SITE_HOST = env.str('HOST', default='127.0.0.1')
SITE_PORT = env.int('PORT', default=8080)
Важно заметить, что localhost нужно указывать по ip-адресу, иначе aiomysql попытается соединиться с БД через Unix socket, что может не работать под Windows. Наконец, воспроизведем последний файл (control.py):
import os
import asyncio
import vkf
async def createAuxTables(db_data):
if db_data.type is not "discrete":
await vkf.CAttributes(db_data.verges, db_data.dbname,
'127.0.0.1', 'root', db_data.secret)
if db_data.type is not "continuous":
await vkf.DAttributes(db_data.encoder, db_data.dbname,
'127.0.0.1', 'root', db_data.secret)
await vkf.Lattices(db_data.lattices, db_data.dbname,
'127.0.0.1', 'root', db_data.secret)
async def createMainTables(db_data):
if db_data.type is "continuous":
await vkf.CData(db_data.trains, db_data.verges,
db_data.dbname, '127.0.0.1', 'root', db_data.secret)
await vkf.CData(db_data.tests, db_data.verges,
db_data.dbname, '127.0.0.1', 'root', db_data.secret)
if db_data.type is "discrete":
await vkf.FCA(db_data.lattices, db_data.encoder,
db_data.dbname, '127.0.0.1', 'root', db_data.secret)
await vkf.DData(db_data.trains, db_data.encoder,
db_data.dbname, '127.0.0.1', 'root', db_data.secret)
await vkf.DData(db_data.tests, db_data.encoder,
db_data.dbname, '127.0.0.1', 'root', db_data.secret)
if db_data.type is "full":
await vkf.FCA(db_data.lattices, db_data.encoder,
db_data.dbname, '127.0.0.1', 'root', db_data.secret)
await vkf.FData(db_data.trains, db_data.encoder, db_data.verges,
db_data.dbname, '127.0.0.1', 'root', db_data.secret)
await vkf.FData(db_data.tests, db_data.encoder, db_data.verges,
db_data.dbname,'127.0.0.1', 'root', db_data.secret)
async def computeAuxTables(db_data):
if db_data.type is not "discrete":
async with vkf.Join(db_data.trains, db_data.dbname, '127.0.0.1',
'root', db_data.secret) as join:
await join.compute_save(db_data.complex, db_data.dbname,
'127.0.0.1', 'root', db_data.secret)
await vkf.Generator(db_data.complex, db_data.trains, db_data.verges,
db_data.dbname, db_data.dbname, db_data.verges_total, 1,
'127.0.0.1', 'root', db_data.secret)
async def induction(db_data, hypothesesNumber, threadsNumber):
if db_data.type is not "discrete":
qualifier = await vkf.Qualifier(db_data.verges,
db_data.dbname, '127.0.0.1', 'root', db_data.secret)
beget = await vkf.Beget(db_data.complex, db_data.dbname,
'127.0.0.1', 'root', db_data.secret)
if db_data.type is not "continuous":
encoder = await vkf.Encoder(db_data.encoder, db_data.dbname,
'127.0.0.1', 'root', db_data.secret)
async with vkf.Induction() as induction:
if db_data.type is "continuous":
await induction.load_continuous_hypotheses(qualifier, beget,
db_data.trains, db_data.hypotheses, db_data.dbname,
'127.0.0.1', 'root', db_data.secret)
if db_data.type is "discrete":
await induction.load_discrete_hypotheses(encoder,
db_data.trains, db_data.hypotheses, db_data.dbname,
'127.0.0.1', 'root', db_data.secret)
if db_data.type is "full":
await induction.load_full_hypotheses(encoder, qualifier, beget,
db_data.trains, db_data.hypotheses, db_data.dbname,
'127.0.0.1', 'root', db_data.secret)
if hypothesesNumber > 0:
await induction.add_hypotheses(hypothesesNumber, threadsNumber)
if db_data.type is "continuous":
await induction.save_continuous_hypotheses(qualifier,
db_data.hypotheses, db_data.dbname, '127.0.0.1', 'root',
db_data.secret)
if db_data.type is "discrete":
await induction.save_discrete_hypotheses(encoder,
db_data.hypotheses, db_data.dbname, '127.0.0.1', 'root',
db_data.secret)
if db_data.type is "full":
await induction.save_full_hypotheses(encoder, qualifier,
db_data.hypotheses, db_data.dbname, '127.0.0.1', 'root',
db_data.secret)
async def prediction(db_data):
if db_data.type is not "discrete":
qualifier = await vkf.Qualifier(db_data.verges,
db_data.dbname, '127.0.0.1', 'root', db_data.secret)
beget = await vkf.Beget(db_data.complex, db_data.dbname,
'127.0.0.1', 'root', db_data.secret)
if db_data.type is not "continuous":
encoder = await vkf.Encoder(db_data.encoder,
db_data.dbname, '127.0.0.1', 'root', db_data.secret)
async with vkf.Induction() as induction:
if db_data.type is "continuous":
await induction.load_continuous_hypotheses(qualifier, beget,
db_data.trains, db_data.hypotheses, db_data.dbname,
'127.0.0.1', 'root', db_data.secret)
if db_data.type is "discrete":
await induction.load_discrete_hypotheses(encoder,
db_data.trains, db_data.hypotheses, db_data.dbname,
'127.0.0.1', 'root', db_data.secret)
if db_data.type is "full":
await induction.load_full_hypotheses(encoder, qualifier, beget,
db_data.trains, db_data.hypotheses, db_data.dbname,
'127.0.0.1', 'root', db_data.secret)
if db_data.type is "continuous":
async with vkf.TestSample(qualifier, induction, beget,
db_data.tests, db_data.dbname, '127.0.0.1', 'root',
db_data.secret) as tests:
#plus = await tests.correct_positive_cases()
db_data.plus = await tests.correct_positive_cases()
#minus = await tests.correct_negative_cases()
db_data.minus = await tests.correct_negative_cases()
if db_data.type is "discrete":
async with vkf.TestSample(encoder, induction,
db_data.tests, db_data.dbname, '127.0.0.1', 'root',
db_data.secret) as tests:
#plus = await tests.correct_positive_cases()
db_data.plus = await tests.correct_positive_cases()
#minus = await tests.correct_negative_cases()
db_data.minus = await tests.correct_negative_cases()
if db_data.type is "full":
async with vkf.TestSample(encoder, qualifier, induction,
beget, db_data.tests, db_data.dbname, '127.0.0.1',
'root', db_data.secret) as tests:
#plus = await tests.correct_positive_cases()
db_data.plus = await tests.correct_positive_cases()
#minus = await tests.correct_negative_cases()
db_data.minus = await tests.correct_negative_cases()
Я сохранил этот файл полностью, так как здесь видны названия, порядок вызова и аргументы процедур ВКФ-метода из библиотеки vkf.cpython-36m-x86_64-linux-gnu.so. Все аргументы после dbname могут быть опущены, так как значения по умолчанию в CPython-библиотеке установлены со стандартными значениями.
4. Комментарии
Предвосхищая вопрос профессиональных программистов о том, почему логика управления ВКФ-экспериментом вынесена наружу (через многочисленные if), а не спрятана через полиморфизм в типы, следует ответить так: к сожалению, динамическая типизация языка Python не позволяет переложить решение о типе используемого объекта на систему, то есть в любом случае возникнет эта последовательность вложенных if. Поэтому автор предпочел использовать явный (C-подобный) синтаксис, чтобы сделать логику максимально прозрачной (и эффективной).
Позволю себе прокомментировать отсутствующие компоненты:
- Загрузка данных в БД для дискретных признаков осуществляется сейчас с помощью дополнительной библиотеки vkfencoder.cpython-36m-x86_64-linux-gnu.so (web-интерфейс для нее делают студенты, а сам автор вызывает соответствующие методы напрямую, так как пока работает на локальном хосте). Для непрерывных признаков идет работа по внесению соответствующих методов в vkfencoder.cpython-36m-x86_64-linux-gnu.so.
- Показ гипотез пока осуществляется сторонними программами-клиентами MariaDB (автор использует DBeaver 7.1.1 Community, но существует большое число аналогов). Студенты разрабатывают прототип системы с использованием фреймворка Django, где технология ORM позволит создать просмотр гипотез в удобном для экспертов виде.
5. Об авторе и истории создания метода
Автор занимается задачами интеллектуального анализа данных более 30 лет. После окончания механико-математического факультета МГУ им М.В. Ломоносова он был приглашен в группу исследователей под руководством д.т.н., проф. В.К. Финна (ВИНИТИ АН СССР). Виктор Константинович с начала 80-х годов прошлого века исследует правдоподобные рассуждения и их формализацию средствами многозначных логик.
Ключевыми идеями, предложенными В.К. Финном, можно считать следующие:
- использование бинарной операции сходства (первоначально, операция пересечения в Булевой алгебре);
- идея отбрасывания порожденного сходства группы обучающих примеров, если оно вкладывается в описание примера противоположного знака (контр-примера);
- идея предсказания исследуемого (целевого) свойства новых примеров путем учета доводов за и против;
- идея проверки полноты множества гипотез через нахождения причин (среди порожденных сходств) для наличия/отсутствия целевого свойства у обучающих примеров.
Следует отметить, что В.К. Финн приписывает некоторые из своих идей зарубежным авторам. Пожалуй, только логика аргументации с полным правом считается им придуманной самостоятельно. Идею учета контр-примеров В.К. Финн заимствовал, по его словам, у К.Р. Поппера. А истоки проверки полноты индуктивного обобщения относятся им к (совершенно туманным, на мой взгляд) работам американского математика и логика Ч.С. Пирса. Порождение гипотез о причинах с помощью операции сходства он считает заимствованным из идей британского экономиста, философа и логика Д.С. Милля. Поэтому созданный им комплекс идей он озаглавил «ДСМ-метод» в честь Д.С. Милля.
Странно, но возникший в конце 70-х годов XX века в трудах проф. Рудольфа Вилле (ФРГ) гораздо более полезный раздел алгебраической теории решеток «Анализ формальных понятий» (АФП) не пользуется у В.К. Финна уважением. На мой взгляд, причина этого — неудачное название, которое у него, как у человека, закончившего сначала философский факультет, а потом инженерный поток механико-математического факультета МГУ, вызывает отторжение.
Как продолжатель дела своего учителя, автор свой подход назвал в его честь «ВКФ-метод». Впрочем, имеется и другая расшифровка — вероятностно-комбинаторный формальный метод машинного обучения, основанного на теории решеток.
Сейчас группа В.К. Финна работает в ВЦ им. А.А. Дородницына РАН ФИЦ ИУ РАН и на отделении интеллектуальных систем Российского Государственного Гуманитарного Университета.
Подробнее с математикой ВКФ-решателя можно ознакомится по диссертации автора или его видео-лекциям в Ульяновском Государственном Университете (за организацию лекций и обработку их записей автор благодарен А.Б. Веревкину и Н.Г. Баранец).
Полный пакет исходных файлов хранится на Bitbucket.
Исходные файлы (на С++) для библиотеки vkf находятся в процессе согласования их размещения на savannah.nongnu.org. При положительном решении, ссылка на скачивание будет добавлена сюда.
Наконец, последнее замечание: автор начал изучать Python 6 апреля 2020 года. До этого единственным языком, на котором он программировал, был C++. Но это обстоятельство не снимает с него обвинений в возможной неаккуратности кода.
Автор выражает сердечную благодарность Татьяне А. Волковой robofreak за поддержку, конструктивные предложения и критические замечания, позволившие существенно улучшить изложение (и даже значительно улучшить код). Впрочем, ответственность за оставшиеся ошибки и принятые решения (даже вопреки ее советам) несет исключительно автор.