Web-сервер машинного обучения «ВКФ-решатель»

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



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

    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 не используется из-за известных проблем с безопасностью.

    Есть несколько плюсов у предлагаемого варианта:

    1. он асинхронный из-за использования aiohttp;
    2. он допускает обработку шаблонов Jinja2;
    3. он работает с пулом соединений к БД через aiomysql;
    4. он обеспечивает запуск независимых вычислительных процессов через aiojobs.aiohttp.spawn.

    Укажем и на очевидные минусы (по сравнению с Django):

    1. нет Object Relational Mapping (ORM);
    2. труднее организуется использование proxy-сервера Nginx;
    3. нет 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 файлах:

    1. app.py — файл запуска приложения
    2. control.py — файл с процедурами работы с ВКФ-решателем
    3. models.py — файл с классами обработки данных и работы с БД
    4. settings.py — файл с настройками приложения
    5. 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()
    

    Я сократил для настоящей заметки этот файл, выкинув классы, обслуживающие служебные маршруты:

    1. класс Auth привязан к корневому маршруту '/' и выводит форму запроса на идентификацию пользователя. Если пользователь не зарегистрирован, имеется кнопка SignIn, которая перенаправляет пользователя по маршруту '/signin'. Если же пользователь с введенными логином и паролем обнаруживается, то он перенаправляется по маршруту '/user/{name}'.
    2. класс SignIn обрабатывает маршрут '/signin' и после успешной регистрации возвращает пользователя на корневой маршрут.
    3. класс Select обрабатывает маршруты '/user/{name}' и запрашивает, какой эксперимент и на каком этапе пользователь желает провести. После проверки наличия такого эксперимента БД пользователь перенаправляется на маршрут '/vkf/{name}' или '/experiment/{name}' (если эксперимента еще было зарегистрировано).

    Оставшиеся классы обрабатывают маршруты, отвечающие за этапы машинного обучения:

    1. класс Prepare обрабатывает маршруты '/experiment/{name}' и собирает имена служебных таблиц и числовые параметры, необходимые для запуска процедур ВКФ-метода. После сохранения этой информации в БД пользователь перенаправляется на маршрут '/vkf/{name}'.
    2. класс Generate обрабатывает маршруты '/vkf/{name}' и запускает различные этапы процедуры индукции ВКФ-метода в зависимости от подготовленности данных экспертом.
    3. класс 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)
    


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

    1. Класс User соответствует посетителю сайта. Он позволяет зарегистрироваться и зайти в систему как эксперт.
    2. Класс Expert позволяет выбрать один из экспериментов.

    Оставшиеся классы соответствуют главным процедурам:

    1. Класс Experiment позволяет задать имена ключевых и вспомогательных таблиц и параметры, необходимые для проведения ВКФ-экспериментов.
    2. Класс Solver отвечает за индуктивное обобщение в ВКФ-методе.
    3. Класс 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-подобный) синтаксис, чтобы сделать логику максимально прозрачной (и эффективной).

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

    1. Загрузка данных в БД для дискретных признаков осуществляется сейчас с помощью дополнительной библиотеки vkfencoder.cpython-36m-x86_64-linux-gnu.so (web-интерфейс для нее делают студенты, а сам автор вызывает соответствующие методы напрямую, так как пока работает на локальном хосте). Для непрерывных признаков идет работа по внесению соответствующих методов в vkfencoder.cpython-36m-x86_64-linux-gnu.so.
    2. Показ гипотез пока осуществляется сторонними программами-клиентами MariaDB (автор использует DBeaver 7.1.1 Community, но существует большое число аналогов). Студенты разрабатывают прототип системы с использованием фреймворка Django, где технология ORM позволит создать просмотр гипотез в удобном для экспертов виде.

    5. Об авторе и истории создания метода


    Автор занимается задачами интеллектуального анализа данных более 30 лет. После окончания механико-математического факультета МГУ им М.В. Ломоносова он был приглашен в группу исследователей под руководством д.т.н., проф. В.К. Финна (ВИНИТИ АН СССР). Виктор Константинович с начала 80-х годов прошлого века исследует правдоподобные рассуждения и их формализацию средствами многозначных логик.

    Ключевыми идеями, предложенными В.К. Финном, можно считать следующие:

    1. использование бинарной операции сходства (первоначально, операция пересечения в Булевой алгебре);
    2. идея отбрасывания порожденного сходства группы обучающих примеров, если оно вкладывается в описание примера противоположного знака (контр-примера);
    3. идея предсказания исследуемого (целевого) свойства новых примеров путем учета доводов за и против;
    4. идея проверки полноты множества гипотез через нахождения причин (среди порожденных сходств) для наличия/отсутствия целевого свойства у обучающих примеров.

    Следует отметить, что В.К. Финн приписывает некоторые из своих идей зарубежным авторам. Пожалуй, только логика аргументации с полным правом считается им придуманной самостоятельно. Идею учета контр-примеров В.К. Финн заимствовал, по его словам, у К.Р. Поппера. А истоки проверки полноты индуктивного обобщения относятся им к (совершенно туманным, на мой взгляд) работам американского математика и логика Ч.С. Пирса. Порождение гипотез о причинах с помощью операции сходства он считает заимствованным из идей британского экономиста, философа и логика Д.С. Милля. Поэтому созданный им комплекс идей он озаглавил «ДСМ-метод» в честь Д.С. Милля.

    Странно, но возникший в конце 70-х годов XX века в трудах проф. Рудольфа Вилле (ФРГ) гораздо более полезный раздел алгебраической теории решеток «Анализ формальных понятий» (АФП) не пользуется у В.К. Финна уважением. На мой взгляд, причина этого — неудачное название, которое у него, как у человека, закончившего сначала философский факультет, а потом инженерный поток механико-математического факультета МГУ, вызывает отторжение.

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

    Сейчас группа В.К. Финна работает в ВЦ им. А.А. Дородницына РАН ФИЦ ИУ РАН и на отделении интеллектуальных систем Российского Государственного Гуманитарного Университета.

    Подробнее с математикой ВКФ-решателя можно ознакомится по диссертации автора или его видео-лекциям в Ульяновском Государственном Университете (за организацию лекций и обработку их записей автор благодарен А.Б. Веревкину и Н.Г. Баранец).

    Полный пакет исходных файлов хранится на Bitbucket.

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

    Наконец, последнее замечание: автор начал изучать Python 6 апреля 2020 года. До этого единственным языком, на котором он программировал, был C++. Но это обстоятельство не снимает с него обвинений в возможной неаккуратности кода.

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

    Similar posts

    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 9

      0
      Честно говоря, ничего не понятно

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

      Вот честно, я сперва бы хотел узнать в чём состоит метод и как он работает (хотя бы поверхностно), посмотрел бы на результаты в сравнении с другими методами. И если это меня удовлетворяет, я бы уже начал читать про реализацию. А тут ссылка на сам метод в конце статьи, сравнения с другими методами не приведено (сказано что нейронные сети тоже достигают 100% точности, может тогда и задача слабовата?). Да и будь я грибником, 50 правил это многовато)

      Понятно желание рассказать про плод своей работы. Но это тоже нужно делать правильно. Иначе это не будет нужно никому.
        0
        Спасибо за комментарий. Попробую объяснить. Начну со целей, планов и закончу реализацией.
        1) Аудитория — программисты. Грузить их цепями Маркова и продвинутыми разделами комбинаторики не хотелось. Хочется составить план, следуя которому они смогут воспроизвести эксперименты и убедятся, что все работает.
        2) План — переход от конкретики к деталям (по мере заинтересованности). Эта статья — вторая в серии. Первая (ссылка имеется в самом начале) — для людей, которые хотят все потрогать руками, но лезть в программирование не хочется. Вторая — для программистов, которые предпочтут приспособить библиотеку для своих нужд (и других массивов). Третья (когда будет готова) расскажет об алгоритмических следствиях математических теорем.
        3) Реализация — описание реального запуска библиотеки на двух совсем разноплановых массивах данных.
        Предлагается попробовать. Данные берутся из стандартного репозитория UCI. Кодирование описано (а кодирование грибов можно придумать и свое собственное). Исходники выложены на bitbucket (и, надеюсь, будут скоро выложены на savannah.nongnu.org).
          0
          Прошу прощения, я попутал: это первая статья серии, вторая — здесь
          0
          Теперь отвечу по конкретным замечаниям:
          1) Есть мои видеолекции для студентов (ссылка приведена в первой части работы). Диссертация — скорее знак того, что под этим лежит наука (с доказательтвами теорем и оценками параметров).
          2) Сравнение с другими методами предполагает метрики. Если брать количественные (особенно, в областях с внутренними симметриями), боюсь, лучше сверточных сетей никто ничего не придумает. Аппроксиматоры будут лучше всех, они на это и нацелены! Если же ограничиться объясняемым ИИ Matt Turek. Explainable Artificial Intelligence (XAI), то все методы окажутся переборными (или вероятностными). Но, например, по вероятностным методам индуктивного логического программирования (ИЛП) за последние 5 лет нет ни одной работы в arXiv.org! Хотя наблюдается возврат интереса к классическому ИЛП: для JICAI2020 заказан большой обзорный доклад (черновой вариант можно найти на arXiv.org).
          3) Метод является вероятностным расширением Анализа формальных понятий
          Sébastien Ferré, Marianne Huchard, Mehdi Kaytoue, Sergei O. Kuznetsov, Amedeo Napoli.
          Formal Concept Analysis: From Knowledge Discovery to Knowledge Processing // Marquis
          P., Papini O., Prade H. (eds) A Guided Tour of Artificial Intelligence Research. Vol. 2,
          Springer, Cham, 2020, p. 411-445
          Автор делал доклад на международном семинаре FCA4AI (проходившем в рамках JICAI2019 в Макао).
            0
            Наконец, затрону совсем частные вопросы:
            1) С деревьями решений есть только два сходства: символьное представление гипотез о причинах и использование энтропийных принципов для дискретизании непрерывных признаков. Зато нет проблемы учета порядка появления признаков (во множестве все признаки равноправны!).
            2) Я довольно часто сталкивался в своих демонстрациях с ситуацией, когда 50 правил для грибника хватало. Можно ли их уменьшить? Наверняка можно, но я этим не занимался. Так как проверятся включение одного подмножества признаков (фрагмента гипотезы о причине) в другое множество признаков (описание тестового примера), то запрограммировать жадный алгоритм минимизации числа гипотез (накрытие) легко. Но никто не может гарантировать, что будет получен глобальный минимум. А делать экспоненциальный алгоритм остовного дерева ради частной задачи не хочется совершенно.
            3) Я использовал наивный подход к описанию грибов. Eсли привлечь знания из микологии, то, скорее всего, результаты будут улучшены. В обсуждаемой статье описано, как построить свое собственное кодирование, которое потом подсовывается универсальной машине обучения. Если есть интерес, вперед!
            4) Еще более интересны эксперименты к непрерывными признаками (у меня качество вин). Я только недавно придумал, как с ними работать, но в любом случае машина обучения не изменяется.

            0

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

              0
              В общем, нормально написано, но конкретные детали для того, чтобы это реально пошло — не увидел.
              Интерфейс библиотеки должен быть приближен к обычным данным, т. е. брать на вход обычную матрицу данных (pandas-стандарт) и давать по ней результат (какие колонки-значения-диапазоны дают правильную конъюнкцию). все остальное — не важно.
              Но это только часть дела. Как аналитик я не пойму почему такие сложности дают лишь массив конъюнкций. Чтобы это и впрямь было интересно, библиотека должна определять оптимальную формулу-дерево для детектирования любой группы. как по мне я не вижу в этом особых сложностей, а оптимальная формула группы с наперед заданной точностью — это то, что нужно. Возможно я это пропустил и это реализовано, но без этого Ваша разработка годится только для ограниченного применения.
                0
                Спасибо, Вы правы. Но Вы должны учитывать, что я един в двух лицах: и математик, разрабатывающий и исследующий алгоритмы, и программист, реализующий их в библиотеке. Надеюсь, что мои студенты присоединятся к программированию, для них Ваше замечание про Pandas будет полезно.
                Вы правильно указали на следующий теоретический шаг: нужно одностадийный процесс нахождения причинно-следственных зависимостей уложить в (необязательно древовидную) систему представления знаний. Для Анализа Формальных Понятий этим занимались в Техническом Университете Дрездена (у проф. Бернарда Гантера и проф. Франца Баадера) с помощью логик описания (description logics). Но Бернарда пару лет назад отправили на пенсию. Я сам планирую двигаться в этом направлении.
                Что касается сложности — это, к сожалению, неизбежно: мы используем или вероятностные алгоритмы (для которых я кое-что умею доказывать) или жадные алгоритмы (как в обучении деревьям решений, например). Но во втором случае мы получим неоптимальность, неустойчивость относительно расширения выборки и переобучение. Комитетное обучение (как пример, случайный лес) — попытка справиться с этими проблемами, но цена — потеря объяснямости. Более того, тут опять вылезают вероятностные алгоритмы.
                  0
                  Тоже работаю за команду. Если у Вас есть промежуточный результат в виде множества конъюнкий, построить по ним по формулу не представляется особо сложной задачей. Студентам-головастикам вполне под силу. Хотя я зашел бы с другой стороны — кластерного анализа. Там сложностей меньше, и сразу понятно, где конъюкции, где дизъюнкции и как именно дробить диапазоны. Надо будет подумать, не сделать ли самому)

              Only users with full accounts can post comments. Log in, please.