В mystem и aot подходы почти одинаковые — по паре автоматов для поиска/итерации по словам спереди и сзади. Отличие, как я понял (судя по бумагам — исходников mystem нет) — в словарях (видимо, в mystem они актуальнее), в эвристике + в aot автоматы вручную строится, а в mystem, судя по бумаге, пара trie.
Было бы здорово) Мельком глянул, они вроде довольно похожи на rutags, и в авторах Anna Feldman тоже — это я к тому, что, возможно, проще всего будет multext <-> positional сделать, multext <-> aot тогда само заработает.
Но если есть готовый код уже для multext <-> aot, то он совсем не помешает, т.к. явное преобразование может помочь избежать потерь информации при преобразовании через посредника.
Там сейчас README нет, если какие-то вопросы по реализации — пиши.
Кстати, урывками пилю github.com/kmike/russian-tagsets — библиотечку для преобразования между различными форматами морф. признаков. Там внутри простой движок для цепочек преобразований (например, если библиотека знает, как из формата A перевести в формат B, и как из B в C, то A->C тоже как-то заработает без явного указания). Сейчас готова более-менее поддержка преобразований туда-сюда между тегами aot, "rutags" и ДИАЛОГ-2010. Начал добавлять туда opencorpora, но моя «урывка» закончилась и не доделал по-нормальному.
Я это к чему — если делаете преобразование между тегами любых 2 форматов, рассмотрите возможность сделать это в рамках russian-tagsets в виде pull-request'а. В качестве бонуса — заработает преобразование в другие форматы, если получается цепочка.
Права на тексты и права на разметку — немного разные вещи. Даже если бы НКРЯ захотел распространять разметку, встал бы сложный впрос: как ее распространять, не распространяя сами тексты. Сейчас выкрутились так: дают скачать случайную выборку предложений с морфологической разметкой (т.е. вроде как сами тексты как цельные произведения не распространяют, только предложения из них). Но условия распространения этой выборки тоже очень мутные (похоже, что использовать можно только в исследовательских целях, т.е. «в стол», хотя написано «свободно»), а на письма с просьбами уточнить не отвечают никому ничего.
Как и что это все значит с точки зрения закона — это только суд сказать может, как мне кажется. Короче, вопрос мутный уже много лет, и, похоже, таким и останется, уж извините за пессимизм. В OpenCorpora очень правильно сделали, что только лицензионно «чистые» тексты в корпус добавляют изначально, чтоб похожая проблема не возникла.
Дальше сугубо IMHO. Расстраивает одно — непонятно, как и зачем серьезно заниматься NLP (и синтаксисом в частности) простому смертному. Писать правила — это нужна команда лингвистов, чтоб использовать машинное обучение — нужны корпусы, а тут НКРЯ вне конкуренции сейчас. Доступ к НКРЯ (применительно к задачам машинного обучения) не свободный (хоть корпус вроде же национальный — но понятно, причины на это есть, наверное), а потом еще и результаты обучения использовать свободно нельзя (для себя все делать, в стол, как другим-то людям результатами пользоваться?). По слухам — на изменение ситуации надежды почти нет. И при этом именно в НКРЯ вкладывают свои усилия РАН, университеты и т.д.
Отсюда следствие — для того, чтоб общедоступную компьютерную лингвистику русскоязычную вперед двигать, нужен открытый корпус; мне кажется, алгоритмы сейчас даже — вопрос вторичный.
Такой открытый корпус есть, и довольно активно развивается: opencorpora.org/ — пользуясь случаем, приглашаю всех, кто может, присоединиться к его развитию, без краудсорсинга тут не справиться.
В случае с django ORM можно еще вот что попробовать:
— Если тормозит создание моделей User — не создавать их, использовать values или values_list.
— Если тормозит клонирование QuerySet — не использовать его или использовать по минимуму, вынося за циклы.
— Если тормозит построение запроса — поковыряться в исходниках и строить запрос отдельно и использовать метод raw (см. docs.djangoproject.com/en/dev/topics/db/sql/#performing-raw-queries ). При этом запрос можно все еще строить с помощью ORM через qs.query.sql_with_params(). Параметры потом менять, как нужно и если нужно, и передавать в User.objects.raw(). Если это в хитрую утилиту оформить, то и ORM сохранится, и месяц на отладку не понадобится. Я сам не пробовал, но мне кажется, что должно получиться.
Ну это уже чисто психологическое: в 2 раза, в 5 раз) Абстракция может что-то тормозить на 10% и поэтому не подходить для задачи, а может тормозить в 100 раз и подходить при этом прекрасно. Меня не парит, что цикл в питоне в сотню раз медленнее, чем в C, т.к. обычно это не проблема, а узкое место можно при необходимости и на C переписать. Так и скорость ORM в вебе обычно тоже не проблема.
Впрочем, это не означает, что разбираться в инструментах и улучшать их не нужно. Так что за бенчмарки плюс. Ну и обычно так бывает: если не знать, как твои инструменты работают на уровень-два ниже, а относиться к ним исключительно как к черным ящикам, ерунду можно написать, т.к. абстракции текут. Это легкий троллинг по поводу рассуждений об «архитектурных элементах» — взять профайлер, почитать исходники да разобраться, в чем конкретно дело, вот и решится загадка.
Можно попробовать разобраться, что где происходит.
В django ORM явные кандидаты на тормоза — постоянные копирования QuerySet (которые нужны для модной ленивости запросов) + сигналы post_init для полей (которые нужны для модных полей вроде ImageField или GFK — но платят за это все, если в проекте хоть где-то есть хоть одна модель с ImageField или GFK) + генератор SQL на объектах (что нужно для поддержки всех баз данных и сложных запросов).
Что происходит:
1) qs = User.objects.all()
Метод .all() создает экземпляр QuerySet. QuerySet создает экземпляр Query. Экземпляр Query — достаточно большой питоний объект, у него где-то 50 полей (словарей, tuple, экземпляров других классов), создающихся в конструкторе.
2) qs[0]
При слайсинге сначала создается новый QuerySet (копируется исходный — в.т.ч. все из Query); у query выставляются новые лимиты: от [0, 1).
Затем по Query (который к базе не привязан) db.mysql.compiler.SQLCompiler собирает запрос для mysql (там всяческа логика на основе тех 50 полей из query).
Т.е. в тесте построение запроса и копирование QuerySet за цикл, можно сказать, не вынесено.
После этого запрос выполняется и для полученного результата строится экземпляр джанго-модели User, в процессе чего шлются сигналы pre_init и post_init. Если в проекте где-то используются ImageField или GFK (например, django.contrib.auth GFK используется), то у этих сигналов есть слушатели, «короткий» путь не работает, и при каждой отправке сигнала для всех слушателей проверяется, живы ли weakref-ы и тот ли sender.
.values() вдобавок к .all() убирает издержки по созданию экземпляра User (кстати, лучше просто написать было User.objects.values('username')). Для чистоты эксперимента можно еще User.objects.values_list('username', flat=True) попробовать, чтоб убрать влияние получения лишних данных.
Что где конкретно тормозит — профайлер покажет)
Но можно немного и без профайлера порассуждать. Т.к. values() вместо all() убирает издержки по созданию экземпляров User, то тормозит, наверное, постоянное копирование QuerySet и/или сбор Query в конкретный запрос под mysql.
Можно попробовать провернуть какой-нибудь хак и убрать копирование QuerySet/Query, чтоб проверить, тормозит это копирование или построение sql-запроса для бэкенда (код не проверял!):
qs = User.objects.values_list('username', flat=True)
qs.query.set_limits(0, 1) # получаем элементы из полудиапазона [0; 1)
for i in range(10000):
user = list(qs.iterator())[0] # iterator нужен, чтоб значения из кэша не брались
Если провести другой тест, в котором будет получаться 10000 результатов из одного запроса, проверяться будет скорость построения джанго-моделей (там values/values_list должен здорово помочь), т.к. sql строиться 1 раз должен будет.
Но точный ответ, понятное дело, только профайлер даст — узкое место всегда оказывается не там, где думаешь :)
Можно. Нет желания сделать pull request?) Или сам могу добавить.
Мне вот только гугл говорит, что tmpfile из stdio может требовать административных прав под windows, так что я бы не стал его использовать; может быть лучше использовать менее прямой вариант с питоним tempfile.
Угу, про костыль с временными файлами тоже думал. В принципе, этот вариант даже получше, чем явная сериализация items, т.к. требует меньше оперативной памяти.
Понятное дело, было бы проще, если б file-like объекты все поддерживало) Но там одним изменением fileutils не отделаешься, т.к. вся библиотека работает с FILE* и подразумевает наличие «настоящего» файла (tail.c/tail_fread, darray.c/da_fread).
Кроме того, хорошо бы отвязываться от апстрима по минимуму — если можно в C-код изменений не вносить, то не вносить их; если и вносить, то такие, которые можно в апстрим пропихнуть (== неспецифичные для питона) — например, фикс для MSVC2008 так в транке libdatrie появился недавно.
В том, чтоб встать на путь правки самой библиотеки, есть плюсы — например, можно было бы попробовать минимизировать преобразования PyUnicode <-> AlphaChar* + возможно, сделать TrieData == PyObject*. Кстати, ruby-расширения для libdatrie на этот путь и встало. Но и минусы очевидны — меньше пользы для мировой цивилизации, сложнее обновляться, больше кода поддерживать.
Поэтому конкретно для «правильной» сериализации я бы предпочел дополнительные функции (на С или Cython), а не правку библиотеки.
* реализовать больше методов стандартного питоньего словаря (простор есть — от простых get и pop до хитрых viewitems);
* подумать, как убрать AlphaMap из публичного интерфейса (и datrie.new, наверное, тоже);
* добавить поддержку ключей-байтовых строк для второго питона (обязательно проверив на бенчмарках, что это ничего не затормозило для юникодных строк) — скорее всего, просто декодировать их в юникод из ascii; если затормозило — не добавлять, а явно задокументировать «не будет этого», отрицательный результат — тоже результат;
* реализовать аналог lookup c deepsearch='choose' из Trie::Trie. В datrie это, видимо, выльется в 3 метода first_key(prefix), first_item(prefix), first_value(prefix). С другой стороны, эти методы могут быть не очень нужны, если iteritems(prefix) будет реализован, т.к. они станут просто чуть более эффективной версией next(trie.iteritems(prefix)). С третьей стороны, они все равно могут быть полезны, т.к. они будут более эффективны + еще неизвестно, когда iteritems получится сделать.
* скрипт для бенчмарков довольно неудобный — все запускается сразу и ждать долговато; какой-то интерфейс командной строки к нему был бы очень кстати;
* любые дополнительные бенчмарки — это хорошо (и не только скорости: например, нормальный скрипт замера оперативной памяти — см. psutil и github.com/fabianp/memory_profiler ); вдруг что-то интересно узнать самому?;
* можно в самой libdatrie реализовать итерацию по дереву с каким-то iterator-object, и функциями в духе «next_state(const Trie* trie, IteratorState* state)» и послать патч автору libdatrie; после этого можно переписывать BaseTrie._walk_prefixes и добавлять публичный питоний интерфейс для гуляния по дереву. Theppitak Karoonboonyanan собирался в libdatrie поддержку итерации сам добавить, но не факт, что скоро;
* возможно, __len__ у Trie может быть быстрее (т.к. в отличие от BaseTrie у нас есть self.values — но как это будет работать совместно с __delitem__?)
* попробовать прогнать тесты на narrow-сборке питона (там сейчас довольно ковбойский метод кодирования питоньего юникода в AlphaChar*, в портабельности которого я до конца не уверен);
* ну или просто использовать библиотеку для своих нужд — недостающие фичи или проблемы сами вылезут со временем)
Я, в принципе, за кривые решения, если они позволяют что-то сделать полезное и их потом можно заменить на «прямые» без смены интерфейса) Но тут вылезает досадная неприятность с обновлением со старой версии + польза не очевидна, поэтому в библиотеку этот вариант добавлять не хочется.
Метод .items() без префиксов сейчас тормозной (раз в 20-30 медленнее, чем у словаря); для сохранения и загрузки придется дублировать trie в памяти в «развернутом» виде (а с текущими save-load можно большой список слов в trie загрузить без загрузки всех отдельных слов как питоньих строк); у AlphaMap есть еще параметр ranges, который может быть использовать удобнее, чем alphabet (и который тут никак не передать).
Но мне нравится) Необходимость создавать отдельный AlphaMap хорошо бы все равно из API убирать.
Вот только если потом пиклинг переписать по-нормальному, то станет нельзя загружать старые trie, сохраненные этим вариантом. Способы обхода есть (например, распиклить trie старой версией, сохранить в файл через save и загрузить через load новой), но мне пока неочевидно, что игра стоит свеч, т.к. не очень представляю, зачем именно пиклинг может понадобиться. А правда, зачем?
Тем, что для реализации pickle-протокола для datrie.Trie пришлось бы писать свою сериализацию trie в байты (на С или Cython).
Не то чтобы это было очень сложно, но работы хватит, за час не напишу, наверное. В libdatrie уже есть поддержка сохранения trie в файл на диске, поэтому в datrie реализовано сохранение в файл: методы save и write. Метод write не документирован, но позволяет писать trie в открытый питоний file.
Поддержка pickle была бы хорошей фичей, но ее пока нет, т.к. ее делать было сложнее, чем просто сохранение в файл. Патчи, понятное дело, приветствуются)
Это смотря как писать. if PY3:… else:… — это дурной тон и почти никогда не нужно. Без ветвления 2-3 часто можно обойтись, а когда нельзя — оно выносится в отдельный модуль (типа compat.py) и не засоряет код.
pymongo (о нем ведь речь) еще недавно поддерживал (о ужас!) python 2.3, ясно-понятно там куча хаков будет даже без поддержки 3го питона. 2.4-то поддерживать невесело. По-моему писать код, совместимый с 2.3 — 2.7 посложнее будет, чем 2.6 — 3.2, и сам код жутковатым будет, в отличие от варианта 2.6-3.2.
Но если есть готовый код уже для multext <-> aot, то он совсем не помешает, т.к. явное преобразование может помочь избежать потерь информации при преобразовании через посредника.
Там сейчас README нет, если какие-то вопросы по реализации — пиши.
Кстати, в планах — обязательно добавить в russian-tagsets консольную утилиту, чтоб можно было библиотеку использовать без написания питоньих скриптов.
Я это к чему — если делаете преобразование между тегами любых 2 форматов, рассмотрите возможность сделать это в рамках russian-tagsets в виде pull-request'а. В качестве бонуса — заработает преобразование в другие форматы, если получается цепочка.
Как и что это все значит с точки зрения закона — это только суд сказать может, как мне кажется. Короче, вопрос мутный уже много лет, и, похоже, таким и останется, уж извините за пессимизм. В OpenCorpora очень правильно сделали, что только лицензионно «чистые» тексты в корпус добавляют изначально, чтоб похожая проблема не возникла.
Дальше сугубо IMHO. Расстраивает одно — непонятно, как и зачем серьезно заниматься NLP (и синтаксисом в частности) простому смертному. Писать правила — это нужна команда лингвистов, чтоб использовать машинное обучение — нужны корпусы, а тут НКРЯ вне конкуренции сейчас. Доступ к НКРЯ (применительно к задачам машинного обучения) не свободный (хоть корпус вроде же национальный — но понятно, причины на это есть, наверное), а потом еще и результаты обучения использовать свободно нельзя (для себя все делать, в стол, как другим-то людям результатами пользоваться?). По слухам — на изменение ситуации надежды почти нет. И при этом именно в НКРЯ вкладывают свои усилия РАН, университеты и т.д.
Отсюда следствие — для того, чтоб общедоступную компьютерную лингвистику русскоязычную вперед двигать, нужен открытый корпус; мне кажется, алгоритмы сейчас даже — вопрос вторичный.
Такой открытый корпус есть, и довольно активно развивается: opencorpora.org/ — пользуясь случаем, приглашаю всех, кто может, присоединиться к его развитию, без краудсорсинга тут не справиться.
— Если тормозит создание моделей User — не создавать их, использовать values или values_list.
— Если тормозит клонирование QuerySet — не использовать его или использовать по минимуму, вынося за циклы.
— Если тормозит построение запроса — поковыряться в исходниках и строить запрос отдельно и использовать метод raw (см. docs.djangoproject.com/en/dev/topics/db/sql/#performing-raw-queries ). При этом запрос можно все еще строить с помощью ORM через qs.query.sql_with_params(). Параметры потом менять, как нужно и если нужно, и передавать в User.objects.raw(). Если это в хитрую утилиту оформить, то и ORM сохранится, и месяц на отладку не понадобится. Я сам не пробовал, но мне кажется, что должно получиться.
Впрочем, это не означает, что разбираться в инструментах и улучшать их не нужно. Так что за бенчмарки плюс. Ну и обычно так бывает: если не знать, как твои инструменты работают на уровень-два ниже, а относиться к ним исключительно как к черным ящикам, ерунду можно написать, т.к. абстракции текут. Это легкий троллинг по поводу рассуждений об «архитектурных элементах» — взять профайлер, почитать исходники да разобраться, в чем конкретно дело, вот и решится загадка.
В django ORM явные кандидаты на тормоза — постоянные копирования QuerySet (которые нужны для модной ленивости запросов) + сигналы post_init для полей (которые нужны для модных полей вроде ImageField или GFK — но платят за это все, если в проекте хоть где-то есть хоть одна модель с ImageField или GFK) + генератор SQL на объектах (что нужно для поддержки всех баз данных и сложных запросов).
Что происходит:
1) qs = User.objects.all()
Метод .all() создает экземпляр QuerySet. QuerySet создает экземпляр Query. Экземпляр Query — достаточно большой питоний объект, у него где-то 50 полей (словарей, tuple, экземпляров других классов), создающихся в конструкторе.
2) qs[0]
При слайсинге сначала создается новый QuerySet (копируется исходный — в.т.ч. все из Query); у query выставляются новые лимиты: от [0, 1).
Затем по Query (который к базе не привязан) db.mysql.compiler.SQLCompiler собирает запрос для mysql (там всяческа логика на основе тех 50 полей из query).
Т.е. в тесте построение запроса и копирование QuerySet за цикл, можно сказать, не вынесено.
После этого запрос выполняется и для полученного результата строится экземпляр джанго-модели User, в процессе чего шлются сигналы pre_init и post_init. Если в проекте где-то используются ImageField или GFK (например, django.contrib.auth GFK используется), то у этих сигналов есть слушатели, «короткий» путь не работает, и при каждой отправке сигнала для всех слушателей проверяется, живы ли weakref-ы и тот ли sender.
.values() вдобавок к .all() убирает издержки по созданию экземпляра User (кстати, лучше просто написать было User.objects.values('username')). Для чистоты эксперимента можно еще User.objects.values_list('username', flat=True) попробовать, чтоб убрать влияние получения лишних данных.
Что где конкретно тормозит — профайлер покажет)
Но можно немного и без профайлера порассуждать. Т.к. values() вместо all() убирает издержки по созданию экземпляров User, то тормозит, наверное, постоянное копирование QuerySet и/или сбор Query в конкретный запрос под mysql.
Можно попробовать провернуть какой-нибудь хак и убрать копирование QuerySet/Query, чтоб проверить, тормозит это копирование или построение sql-запроса для бэкенда (код не проверял!):
Если провести другой тест, в котором будет получаться 10000 результатов из одного запроса, проверяться будет скорость построения джанго-моделей (там values/values_list должен здорово помочь), т.к. sql строиться 1 раз должен будет.
Но точный ответ, понятное дело, только профайлер даст — узкое место всегда оказывается не там, где думаешь :)
Мне вот только гугл говорит, что tmpfile из stdio может требовать административных прав под windows, так что я бы не стал его использовать; может быть лучше использовать менее прямой вариант с питоним tempfile.
Понятное дело, было бы проще, если б file-like объекты все поддерживало) Но там одним изменением fileutils не отделаешься, т.к. вся библиотека работает с FILE* и подразумевает наличие «настоящего» файла (tail.c/tail_fread, darray.c/da_fread).
Кроме того, хорошо бы отвязываться от апстрима по минимуму — если можно в C-код изменений не вносить, то не вносить их; если и вносить, то такие, которые можно в апстрим пропихнуть (== неспецифичные для питона) — например, фикс для MSVC2008 так в транке libdatrie появился недавно.
В том, чтоб встать на путь правки самой библиотеки, есть плюсы — например, можно было бы попробовать минимизировать преобразования PyUnicode <-> AlphaChar* + возможно, сделать TrieData == PyObject*. Кстати, ruby-расширения для libdatrie на этот путь и встало. Но и минусы очевидны — меньше пользы для мировой цивилизации, сложнее обновляться, больше кода поддерживать.
Поэтому конкретно для «правильной» сериализации я бы предпочел дополнительные функции (на С или Cython), а не правку библиотеки.
* реализовать больше методов стандартного питоньего словаря (простор есть — от простых get и pop до хитрых viewitems);
* подумать, как убрать AlphaMap из публичного интерфейса (и datrie.new, наверное, тоже);
* добавить поддержку ключей-байтовых строк для второго питона (обязательно проверив на бенчмарках, что это ничего не затормозило для юникодных строк) — скорее всего, просто декодировать их в юникод из ascii; если затормозило — не добавлять, а явно задокументировать «не будет этого», отрицательный результат — тоже результат;
* реализовать аналог lookup c deepsearch='choose' из Trie::Trie. В datrie это, видимо, выльется в 3 метода first_key(prefix), first_item(prefix), first_value(prefix). С другой стороны, эти методы могут быть не очень нужны, если iteritems(prefix) будет реализован, т.к. они станут просто чуть более эффективной версией next(trie.iteritems(prefix)). С третьей стороны, они все равно могут быть полезны, т.к. они будут более эффективны + еще неизвестно, когда iteritems получится сделать.
* скрипт для бенчмарков довольно неудобный — все запускается сразу и ждать долговато; какой-то интерфейс командной строки к нему был бы очень кстати;
* любые дополнительные бенчмарки — это хорошо (и не только скорости: например, нормальный скрипт замера оперативной памяти — см. psutil и github.com/fabianp/memory_profiler ); вдруг что-то интересно узнать самому?;
* можно в самой libdatrie реализовать итерацию по дереву с каким-то iterator-object, и функциями в духе «next_state(const Trie* trie, IteratorState* state)» и послать патч автору libdatrie; после этого можно переписывать BaseTrie._walk_prefixes и добавлять публичный питоний интерфейс для гуляния по дереву. Theppitak Karoonboonyanan собирался в libdatrie поддержку итерации сам добавить, но не факт, что скоро;
* возможно, __len__ у Trie может быть быстрее (т.к. в отличие от BaseTrie у нас есть self.values — но как это будет работать совместно с __delitem__?)
* попробовать прогнать тесты на narrow-сборке питона (там сейчас довольно ковбойский метод кодирования питоньего юникода в AlphaChar*, в портабельности которого я до конца не уверен);
* ну или просто использовать библиотеку для своих нужд — недостающие фичи или проблемы сами вылезут со временем)
Можно в вики добавить, вдруг кому пригодится)
Метод .items() без префиксов сейчас тормозной (раз в 20-30 медленнее, чем у словаря); для сохранения и загрузки придется дублировать trie в памяти в «развернутом» виде (а с текущими save-load можно большой список слов в trie загрузить без загрузки всех отдельных слов как питоньих строк); у AlphaMap есть еще параметр ranges, который может быть использовать удобнее, чем alphabet (и который тут никак не передать).
Но мне нравится) Необходимость создавать отдельный AlphaMap хорошо бы все равно из API убирать.
Вот только если потом пиклинг переписать по-нормальному, то станет нельзя загружать старые trie, сохраненные этим вариантом. Способы обхода есть (например, распиклить trie старой версией, сохранить в файл через save и загрузить через load новой), но мне пока неочевидно, что игра стоит свеч, т.к. не очень представляю, зачем именно пиклинг может понадобиться. А правда, зачем?
Не то чтобы это было очень сложно, но работы хватит, за час не напишу, наверное. В libdatrie уже есть поддержка сохранения trie в файл на диске, поэтому в datrie реализовано сохранение в файл: методы save и write. Метод write не документирован, но позволяет писать trie в открытый питоний file.
Поддержка pickle была бы хорошей фичей, но ее пока нет, т.к. ее делать было сложнее, чем просто сохранение в файл. Патчи, понятное дело, приветствуются)
pymongo (о нем ведь речь) еще недавно поддерживал (о ужас!) python 2.3, ясно-понятно там куча хаков будет даже без поддержки 3го питона. 2.4-то поддерживать невесело. По-моему писать код, совместимый с 2.3 — 2.7 посложнее будет, чем 2.6 — 3.2, и сам код жутковатым будет, в отличие от варианта 2.6-3.2.