— чем exconsole от pdb.post_mortem отличается, и почему pdb.post_mortem внутри не используется?
— почему репозиторий нестандартно организован — вместо setup.py — setup.py.in? Из-за этого же нельзя dev-версию через pip прямо из репозитория поставить, + нужно какие-то make-файлы запускать, чтоб готовый к установке пакет получить.
К примеру, для русского языка это дерево вместе с дополнительными данными занимает порядка 2 мегабайт. Во-первых, по сравнению с 75 мегабайтами это, безусловно, успех.
Хм, интересно. Для уточнения — цифры тут про одно дерево или про способ хранения, описанный далее в статье?
У меня были такие эксперименты: берем 3млн слов (весь словарь OpenCorpora), загружаем их в Trie, получаем 100-200M занятой памяти (в зависимости от реализации дерева).
Я пробовал только реализации префиксных деревьев с «полным» алфавитом; уменьшение алфавита до 33 символов может максимум раз в 7 объем памяти уменьшить (на практике меньше), так что для основанного на указателях дерева, похожего на то, что тут описано, вместо 200Мб должно около 25-30Мб получиться. Дальше, у меня была кодировка utf8; при использовании однобайтовой кодировки до 15-20Мб сжаться все может (в 2 раза не выйдет, т.к. Trie хорошо жмет utf8). Если посчитать, что «несколько сот тысяч» — это раз в десять меньше, чем 3млн, и что размер Trie линейно зависит от количества слов (опять очень грубо), то 2Мб и правда как раз получается :) Хотя выглядит цифра все равно немного странно, по прикидкам все же больше должно быть. Это точно не цифра, которая получается с двумя префиксными деревьями?
Что интересно, 3млн слов, закодированные в utf8 и загруженные «как есть» в DAFSA/DAWG (брал готовую реализацию dawgdic) с алфавитом 256 символов, тоже мегабайта 2-3 занимают (7 с грамматической информацией и информацией о парадигмах для склонения), и разбор потом проще/быстрее, т.к. не нужно с двух концов идти и сопоставлять результаты.
В русском языке есть фишка — слова, оканчивающиеся одинаково, вероятно, имеют одинаковую форму и парадигму, поэтому DAWG может и эту информацию эффективно сжимать и не «разваливается» в Trie, если ее просто приписывать в конец слов.
Использование locals() для строковой интерполяции мне кажется очень удобным. Питону не хватает простой интерполяции локальных переменных. В Groovy и CoffeeScript, например, можно написать что-то вроде "{name} is {some_value}". Но я думаю, что этот Python-вариант вполне сойдёт: "{name} is {some_value}".format(**locals()).
В текущей реализации namedtuple всех этих locals() нет, и правильно.
Вы, наверное, заметили, что __slots__ определяется как пустой кортеж. Питон в таком случае не использует для экземпляров словари в качестве пространств имён, что немного экономит ресурсы. Благодаря неизменяемости, которая наследуется от родительского класса (tuple), и невозможности добавлять новые атрибуты (потому что __slots__ = ()), экземпляры namedtuple-типов являются объектами-значениями, что позволяет использовать их, например, в качестве ключей в словарях.
__slots__ не имеет никакого отношения к тому, можно ли использовать объект в качестве ключей в словарях, или нет. Да и неизменяемость, в общем-то, не обязательна. См. docs.python.org/3/glossary.html#term-hashable.
Баум доказал теоретически, что алгоритм Велша работает: каждая итерация гарантировано увеличивает целевую функцию. Так что целевая функция будет расти. Другое дело, что нет никакой гарантии, что в итоге мы достигнем глобального максимума, а не локального.
Для борьбы с этим мне известны 4 подхода; ни один из них не позволяет гарантировано найти глобальный максимум, но они позволяют хотя бы найти параметры получше.
Первый подход — пробовать много разных случайных начальных точек, и в конце выбирать лучший результат. Вроде в обработке речи это используют. Баум-Велш сам по себе «тормоз», так что сильно много начальных точек выбирать может быть непрактично.
Второй подход — выбирать начальное приближение исходя из каких-то эвристик, основанных на соображениях из конкретной предметной области.
Третий подход — предварительно натренировать HMM на небольшом количестве обучающих примеров (=supervised), и затем просто пытаться эти параметры улучшить с помощью Баума-Велша на неразмеченных данных (=unsupervised). В статье это как-то явно не выделено; Баум-Велш — алгоритм для обучения без учителя, ему не нужны размеченные данные.
Четвертый подход — зафиксировать часть параметров HMM исходя из каких-то особенностей предметной области, и обновлять только оставшиеся параметры Баум-Велшем. Например, иногда можно зафиксировать значения P(наблюдаемый символ | состояние) исходя из каких-то соображений, а Баумом-Велшем обновлять (в случае HMM первого порядка) только P(состояние | предыдущее состояние) — в таком случае задача оказывается проще.
Или отдавать *почти* тот же шаблон, если запрос аяксовый.
Шаблон 1: myform.inc.html — собственно форма;
Шаблон 2: myform.html — страничка с формой; подключает myform.inc.html через include.
Если запрос обычный, отдаем myform.html; если аяксовый — myform.inc.html. Нет дублирования кода, клиент предельно простой, не нужно из ответа выдирать форму (ответ и есть форма, если запрос аяксовый), легко добавлять аяксовость имеющимся вьюхам.
Если завести в проекте какое-нибудь соглашение об именовании «обычных» шаблонов и аяксовых, то можно хелпер написать, который будет правильный шаблон отдавать, и аякс почти-что сам заработает: используем везде хелпер (втч где нет аякса); пишем немного js-кода на клиенте; потом в html помечаем нужные формы и они начинают отправляться аяксом.
Из загашников, таким когда-то хелпером пользовались:
from django.template.response import TemplateResponse
class show(TemplateResponse):
"""
TemplateResponse subclass with extra goodies. It is intended to be used
as a better replacement of `django.shortcuts.render`.
"""
def resolve_template(self, template):
"""
Accepts only path-to-template (unlike TemplateResponse method).
For ajax requests it tries a template in 'ajax' subfolder first.
E.g. if request is ajax then
show(request, 'myapp/index.html')
will try 'myapp/ajax/index.html' first and fallback to 'myapp/index.html'
if ajax template is not found.
"""
assert isinstance(template, basestring)
if self._request.is_ajax():
if '/' in template:
path, name = template.rsplit('/', 1)
ajax_template = "/".join((path, 'ajax', name))
else:
ajax_template = "ajax/"+template
template = [ajax_template, template]
return super(show, self).resolve_template(template)
Sentry тоже в тырнет все загружает (хоть на свой сервер, хоть в getsentry.com), и в код все эти catcher.collect(e) писать не нужно, т.к. клиент работает просто как обработчик для стандартного logging. Это означает, что не только исключения писать можно, но и ворнинги. И что можно записывать какие-то события, возникающие в сторонних библиотеках (там, где код править нехорошо). А на сервере похожие исключения объединяются по группам (если их очень много — то используется семплинг), можно отмечать решенные проблемы, ну и т.д. HTML рендерится на сервере — не нужно его генерить на клиенте и гонять по сети. В python-catcher отчеты по http загружаются — это не очень хорошо, т.к. это тоже может упасть с исключением или повесить процесс; в sentry для этого есть вариант с UDP.
Из преимуществ python-catcher — возможно, отсутствие умного сервера, это да.
Пара минусов del.core/init.core по сравнению с __del__/__init__:
1) del.core читается «удалить ядро», init.core — «инициализировать ядро». В __del__ таких лишних смыслов нет.
2) в синтаксис языка добавляется новая сущность — точка в объявлении метода. Это усложнение, которое, к тому же, ко всяким неоднозначностям ведет:
class Foo:
def core(self):
print('Foo')
class FileObject:
def del.core(self):
print('FileObject')
def del(self):
return Foo()
и теперь вызываем file_object.del.core() — что будет напечатано?
__магические__ методы решают вполне осязаемые практические проблемы простым (и imho красивым) способом, зачем что-то усложнять? Переименование __next__ в next.core никаких практических проблем не решает.
Вырвиглазность повышает читаемость кода и позволяет улучшать язык обратно-совместимым способом.
Если чуть подробнее, этот стиль наименования:
1. позволяет вводить в язык новые магические методы без риска сломать старый код. Программисты на питоне в своем коде названия __name__ не используют (по соглашению, да и сложно так назвать что-то случайно), а значит новый магический метод ничего не сломает. Без подобного соглашения нужно быть телепатом, чтоб писать код, который не сломается в будущих версиях интерпретатора. Ср., например, с toJSON в javascript — когда в браузерах поддержка этого метода появилась, куча кода перестала работать.
2. при чтении кода сразу видно, что метод магический, и вызываться он, скорее всего, будет неявно. Вызовы остальных методов обычно можно грепом найти. Кстати, название магического метода часто подсказывает, как он может вызываться: если метод называется __foo__, то во многих случаях он будет вызван с помощью функции foo(obj). Например, функции getattr, setattr, len, iter, next, reversed, copy, deepcopy, hash вызывают соответствующие __методы__.
Не, не чтд. Из того, что производная целевой функции равна нулю, следует, что мы нашли локальный экстремум, но не следует, что мы нашли глобальный. Про погрешность не понял, при чем она тут.
Да банально, для организации кода — в scrapy удобно все делать на генераторах, а без yield from повторно использовать код на генераторах неудобно. Ну, например,
# вместо
for non_hcf_item in self._hcf_process_spider_result(result, spider):
yield non_hcf_item
# хочется писать
yield from self._hcf_process_spider_result(result, spider)
Для парсеров — получше работа с генераторами (yield from тот же, в scrapy часто его не хватает, даже без tulip) + нормальное отображение юникода в консоли (например, list юникодных строк можно быстро глянуть без необходимости писать цикл с print'ами) + urlencode работает с юникодом + модуль csv работает с юникодом + lru_cache из коробки есть + много других подобных мелочей.
Для сайтиков — ну, например, включенная рандомизация хэша по умолчанию, что несколько повышает безопасность. Ну и семантика импортов получше, u перед юникодными строками писать не надо, от object все явно наследовать не надо, super без аргументов (позволяет, например, класс переименовывать проще).
В 2.х ничего плохого нет, там все то же, понятно дело, написать можно (с чуть большим количеством кода), но вцелом 3.x поприятнее во многих мелочах; если получается его использовать, то стараюсь использовать. Иногда не получается: например scrapy, boto и fabric еще не портированы.
У нас был опыт, на одном сайте (года 2 назад было дело, или 3 уже) сначала не было ничего такого (кому хочется все эти навязчивые попапы делать), потом сделали, % регистраций скакнул вверх раз в 5 и так и остался стабильно выше (раза в 3). Это означало, например, что реклама раза в 3 дешевле стала. Что-то менее злое тогда не придумали, и работало остальное все как-то плохо. Оскал капитализма, короче говоря.
Хм, в статье про VIRT и RSS ничего не увидел. Видел про своп, но это немного другая проблема. Ткнете в нужное место?
Ну и ulimit — это не решение, а костыль :) Причем это костыль только многопоточности (=="… в том числе ОС"); при других вариантов использования виртуальной памяти (заммэпить файл, например?) это не помогает ведь? В итоге минное поле получается какое-то — ставишь программу и не знаешь, будет ли она работать нормально или отожрет гиг памяти просто так.
Почему в топиках, защищающих OpenVZ, никогда не пишут про вопиющую жесть — в OpenVZ в каждом контейнере ограничивается виртуальная память вместо RES/RSS? Или это как-то поправлено в последнее время?
Виртуальная память «бесплатна» везде, кроме OpenVZ (на реальных машинах и XEN/KVM), и многие программы ее выделяют с учетом того, что она «бесплатна» — ну java-машина, например, какая-нибудь. В том числе и ОС: для каждого потока место под стек выделяется заранее в виртуальной памяти (8Мб обычно).
Что это означает — под OpenVZ многопоточные программы могут занимать занимать в несколько раз больше памяти, чем на реальных машинах. И так и происходит, один и тот же простейший стек (LAMP из коробки) может на реальной машине требовать в 2 раза меньше памяти, чем под OpenVZ. Проблема не надуманная — мне кажется, что, например, дурная слава многопоточного Apache идет процентов на 50 из-за контейнеров с OpenVZ.
Т.е. для клиента цифры «512M RAM» под OpenVZ — это такое маркетинговое недоговаривание (т.е. вранье). 512-то 512, но это совсем не те 512, что просто на компьютере или при виртуализаци XEN/KVM (больше похоже на «недоделанные» 256). Даже если не вспоминать про OOM Killer. При этом ни разу не видел, чтоб писали как-то по-другому или поясняли этот вопрос.
… или еще примитивней — сделать наследника BaseAnalyzerUnit и реализовать нужные методы (tag, parse, lexeme, normalized). Хоть даже вот так (чтоб суть видна была; код организовать по-другому лучше, конечно):
class AlyoshenkaAnalyzer(BaseAnalyzerUnit):
# слова "Алёшенька" в словаре нет и оно из-за этого разбирается как имя женского рода
# пусть хотя бы метод tag работает
def tag(word, word_lower, seen_tags):
if word_lower in {"алешенька", "алёшенька"}:
return [self.morph.TagClass('NOUN,masc,anim,Name sing,nomn')]
return []
Для других методов там посложнее; у наследника DictionaryAnalyzer плюс тот, что никакого кода писать не нужно, и работать все будет быстро.
phpMorphy вроде неплох (это, насколько помню, был почти 1-в-1 клон lemmatizer с aot.ru) — но против переписывания pymorphy2, конечно же, ничего не имею против :)
— чем exconsole от pdb.post_mortem отличается, и почему pdb.post_mortem внутри не используется?
— почему репозиторий нестандартно организован — вместо setup.py — setup.py.in? Из-за этого же нельзя dev-версию через pip прямо из репозитория поставить, + нужно какие-то make-файлы запускать, чтоб готовый к установке пакет получить.
Хм, интересно. Для уточнения — цифры тут про одно дерево или про способ хранения, описанный далее в статье?
У меня были такие эксперименты: берем 3млн слов (весь словарь OpenCorpora), загружаем их в Trie, получаем 100-200M занятой памяти (в зависимости от реализации дерева).
Я пробовал только реализации префиксных деревьев с «полным» алфавитом; уменьшение алфавита до 33 символов может максимум раз в 7 объем памяти уменьшить (на практике меньше), так что для основанного на указателях дерева, похожего на то, что тут описано, вместо 200Мб должно около 25-30Мб получиться. Дальше, у меня была кодировка utf8; при использовании однобайтовой кодировки до 15-20Мб сжаться все может (в 2 раза не выйдет, т.к. Trie хорошо жмет utf8). Если посчитать, что «несколько сот тысяч» — это раз в десять меньше, чем 3млн, и что размер Trie линейно зависит от количества слов (опять очень грубо), то 2Мб и правда как раз получается :) Хотя выглядит цифра все равно немного странно, по прикидкам все же больше должно быть. Это точно не цифра, которая получается с двумя префиксными деревьями?
Что интересно, 3млн слов, закодированные в utf8 и загруженные «как есть» в DAFSA/DAWG (брал готовую реализацию dawgdic) с алфавитом 256 символов, тоже мегабайта 2-3 занимают (7 с грамматической информацией и информацией о парадигмах для склонения), и разбор потом проще/быстрее, т.к. не нужно с двух концов идти и сопоставлять результаты.
В русском языке есть фишка — слова, оканчивающиеся одинаково, вероятно, имеют одинаковую форму и парадигму, поэтому DAWG может и эту информацию эффективно сжимать и не «разваливается» в Trie, если ее просто приписывать в конец слов.
В текущей реализации namedtuple всех этих locals() нет, и правильно.
__slots__ не имеет никакого отношения к тому, можно ли использовать объект в качестве ключей в словарях, или нет. Да и неизменяемость, в общем-то, не обязательна. См. docs.python.org/3/glossary.html#term-hashable.
Для борьбы с этим мне известны 4 подхода; ни один из них не позволяет гарантировано найти глобальный максимум, но они позволяют хотя бы найти параметры получше.
Первый подход — пробовать много разных случайных начальных точек, и в конце выбирать лучший результат. Вроде в обработке речи это используют. Баум-Велш сам по себе «тормоз», так что сильно много начальных точек выбирать может быть непрактично.
Второй подход — выбирать начальное приближение исходя из каких-то эвристик, основанных на соображениях из конкретной предметной области.
Третий подход — предварительно натренировать HMM на небольшом количестве обучающих примеров (=supervised), и затем просто пытаться эти параметры улучшить с помощью Баума-Велша на неразмеченных данных (=unsupervised). В статье это как-то явно не выделено; Баум-Велш — алгоритм для обучения без учителя, ему не нужны размеченные данные.
Четвертый подход — зафиксировать часть параметров HMM исходя из каких-то особенностей предметной области, и обновлять только оставшиеся параметры Баум-Велшем. Например, иногда можно зафиксировать значения P(наблюдаемый символ | состояние) исходя из каких-то соображений, а Баумом-Велшем обновлять (в случае HMM первого порядка) только P(состояние | предыдущее состояние) — в таком случае задача оказывается проще.
Шаблон 1: myform.inc.html — собственно форма;
Шаблон 2: myform.html — страничка с формой; подключает myform.inc.html через include.
Если запрос обычный, отдаем myform.html; если аяксовый — myform.inc.html. Нет дублирования кода, клиент предельно простой, не нужно из ответа выдирать форму (ответ и есть форма, если запрос аяксовый), легко добавлять аяксовость имеющимся вьюхам.
Если завести в проекте какое-нибудь соглашение об именовании «обычных» шаблонов и аяксовых, то можно хелпер написать, который будет правильный шаблон отдавать, и аякс почти-что сам заработает: используем везде хелпер (втч где нет аякса); пишем немного js-кода на клиенте; потом в html помечаем нужные формы и они начинают отправляться аяксом.
Из загашников, таким когда-то хелпером пользовались:
Sentry тоже в тырнет все загружает (хоть на свой сервер, хоть в getsentry.com), и в код все эти catcher.collect(e) писать не нужно, т.к. клиент работает просто как обработчик для стандартного logging. Это означает, что не только исключения писать можно, но и ворнинги. И что можно записывать какие-то события, возникающие в сторонних библиотеках (там, где код править нехорошо). А на сервере похожие исключения объединяются по группам (если их очень много — то используется семплинг), можно отмечать решенные проблемы, ну и т.д. HTML рендерится на сервере — не нужно его генерить на клиенте и гонять по сети. В python-catcher отчеты по http загружаются — это не очень хорошо, т.к. это тоже может упасть с исключением или повесить процесс; в sentry для этого есть вариант с UDP.
Из преимуществ python-catcher — возможно, отсутствие умного сервера, это да.
1) del.core читается «удалить ядро», init.core — «инициализировать ядро». В __del__ таких лишних смыслов нет.
2) в синтаксис языка добавляется новая сущность — точка в объявлении метода. Это усложнение, которое, к тому же, ко всяким неоднозначностям ведет:
и теперь вызываем file_object.del.core() — что будет напечатано?
__магические__ методы решают вполне осязаемые практические проблемы простым (и imho красивым) способом, зачем что-то усложнять? Переименование __next__ в next.core никаких практических проблем не решает.
Если чуть подробнее, этот стиль наименования:
1. позволяет вводить в язык новые магические методы без риска сломать старый код. Программисты на питоне в своем коде названия __name__ не используют (по соглашению, да и сложно так назвать что-то случайно), а значит новый магический метод ничего не сломает. Без подобного соглашения нужно быть телепатом, чтоб писать код, который не сломается в будущих версиях интерпретатора. Ср., например, с toJSON в javascript — когда в браузерах поддержка этого метода появилась, куча кода перестала работать.
2. при чтении кода сразу видно, что метод магический, и вызываться он, скорее всего, будет неявно. Вызовы остальных методов обычно можно грепом найти. Кстати, название магического метода часто подсказывает, как он может вызываться: если метод называется __foo__, то во многих случаях он будет вызван с помощью функции foo(obj). Например, функции getattr, setattr, len, iter, next, reversed, copy, deepcopy, hash вызывают соответствующие __методы__.
Для сайтиков — ну, например, включенная рандомизация хэша по умолчанию, что несколько повышает безопасность. Ну и семантика импортов получше, u перед юникодными строками писать не надо, от object все явно наследовать не надо, super без аргументов (позволяет, например, класс переименовывать проще).
В 2.х ничего плохого нет, там все то же, понятно дело, написать можно (с чуть большим количеством кода), но вцелом 3.x поприятнее во многих мелочах; если получается его использовать, то стараюсь использовать. Иногда не получается: например scrapy, boto и fabric еще не портированы.
Ну и ulimit — это не решение, а костыль :) Причем это костыль только многопоточности (=="… в том числе ОС"); при других вариантов использования виртуальной памяти (заммэпить файл, например?) это не помогает ведь? В итоге минное поле получается какое-то — ставишь программу и не знаешь, будет ли она работать нормально или отожрет гиг памяти просто так.
Виртуальная память «бесплатна» везде, кроме OpenVZ (на реальных машинах и XEN/KVM), и многие программы ее выделяют с учетом того, что она «бесплатна» — ну java-машина, например, какая-нибудь. В том числе и ОС: для каждого потока место под стек выделяется заранее в виртуальной памяти (8Мб обычно).
Что это означает — под OpenVZ многопоточные программы могут занимать занимать в несколько раз больше памяти, чем на реальных машинах. И так и происходит, один и тот же простейший стек (LAMP из коробки) может на реальной машине требовать в 2 раза меньше памяти, чем под OpenVZ. Проблема не надуманная — мне кажется, что, например, дурная слава многопоточного Apache идет процентов на 50 из-за контейнеров с OpenVZ.
Т.е. для клиента цифры «512M RAM» под OpenVZ — это такое маркетинговое недоговаривание (т.е. вранье). 512-то 512, но это совсем не те 512, что просто на компьютере или при виртуализаци XEN/KVM (больше похоже на «недоделанные» 256). Даже если не вспоминать про OOM Killer. При этом ни разу не видел, чтоб писали как-то по-другому или поясняли этот вопрос.
Для других методов там посложнее; у наследника DictionaryAnalyzer плюс тот, что никакого кода писать не нужно, и работать все будет быстро.