Commits

Mikhail Korobov committed 433337e

предсказатель для метода .tag теперь работает так же, как и для .parse

Comments (0)

Files changed (3)

docs/internals/dict.rst

     "О"     105
     "Ы"     110
 
-Этот способ неоптимален, т.к. в словарях OpenCorpora_ у большинства
-сравнительных прилагательных есть формы на ПО-::
+Этот способ неоптимален, т.к. в словарях OpenCorpora_, например,
+у большинства сравнительных прилагательных есть формы на ПО-::
 
     375081
     ЧЕЛОВЕКОЛЮБИВЕЙ         554
     "Е"      556     "ПО"
     "Й"      557     "ПО"
 
-Окончания и префиксы в парадигмах повторяются и хорошо
+Окончания и префиксы в парадигмах повторяются, и хорошо
 бы их не хранить по многу раз, поэтому все возможные окончания
 хранятся в массиве, а в парадигме указывается только номер окончания;
 с префиксами то же самое.

pymorphy2/tagger.py

 
             total_cnt = 1 # smoothing; XXX: isn't max_cnt better?
             for fixed_suffix, parse in para_data:
-                for cnt, para_id, idx in reversed(parse):
+                for cnt, para_id, idx in parse:
 
                     tag = self._build_tag_info(para_id, idx)
 
                     result.append(parse)
 
             if total_cnt > 1:
+                # parses are sorted inside paradigms, but they are unsorted overall
+                sorted_parses = sorted(result, reverse=True)
                 result = [
                     (fixed_word, tag, normal_form, cnt/total_cnt * ESTIMATE_DECAY)
-                    for (fixed_word, tag, normal_form, cnt) in sorted(result, reverse=True)
+                    for (fixed_word, tag, normal_form, cnt) in sorted_parses
                 ]
                 break
 
         res = self._tag_as_known(word)
         if not res:
             res = self._tag_as_word_with_known_prefix(word)
+
         if not res:
-            res = self._tag_as_word_with_unknown_prefix(word)
-        if not res:
-            res = self._tag_as_word_with_known_suffix(word)
+            seen = set()
+            res = self._tag_as_word_with_unknown_prefix(word, seen)
+            res.extend(self._tag_as_word_with_known_suffix(word, seen))
+
         return res
 
     def _tag_as_known(self, word):
         word_prefixes = self._dictionary.prediction_prefixes.prefixes(word)
         for pref in word_prefixes:
             unprefixed_word = word[len(pref):]
-            res.extend(self.tag(unprefixed_word))
-        return res
 
-    def _tag_as_word_with_unknown_prefix(self, word):
-        res = []
-        for _, truncated_word in _split_word(word):
-            res.extend(self._tag_as_known(truncated_word))
-            # XXX: remove non-productive classes?
+            for tag in self.tag(unprefixed_word):
+                if get_POS(tag) in self._non_productive_classes:
+                    continue
+                res.append(tag)
 
         return res
 
-    def _tag_as_word_with_known_suffix(self, word):
+
+    def _tag_as_word_with_unknown_prefix(self, word, _seen_tags=None):
+        if _seen_tags is None:
+            _seen_tags = set()
+
+        res = []
+        for _, unprefixed_word in _split_word(word):
+            for tag in self._tag_as_known(unprefixed_word):
+
+                if get_POS(tag) in self._non_productive_classes:
+                    continue
+
+                if tag in _seen_tags:
+                    continue
+                _seen_tags.add(tag)
+
+                res.append(tag)
+
+        return res
+
+
+    def _tag_as_word_with_known_suffix(self, word, _seen_tags=None):
+        if _seen_tags is None:
+            _seen_tags = set()
+
         result = []
         for i in 5,4,3,2,1:
             end = word[-i:]
             para_data = self._dictionary.prediction_suffixes.similar_item_values(end, self._ee)
 
+            found = False
             for parse in para_data:
                 for cnt, para_id, idx in parse:
+                    tag = self._build_tag_info(para_id, idx)
+                    if get_POS(tag) in self._non_productive_classes:
+                        continue
+
+                    found = True
+                    if tag in _seen_tags:
+                        continue
+
+                    _seen_tags.add(tag)
                     result.append(
-                        (cnt, self._build_tag_info(para_id, idx))
+                        (cnt, tag)
                     )
 
-            if result:
+            if found:
                 result = [tpl[1] for tpl in sorted(result, reverse=True)] # remove counts
                 break
 
         return result
 
+
     # ==== dictionary access utilities ===
 
     def _build_tag_info(self, para_id, idx):

tests/test_tagger.py

 
 morph = tagger.Morph.load()
 
-def with_test_data(data):
+TEST_DATA = [
+    ('КОШКА', ['КОШКА']),
+    ('КОШКЕ', ['КОШКА']),
+
+    # в pymorphy 0.5.6 результат парсинга - наоборот, сначала СТАЛЬ, потом СТАТЬ
+    ('СТАЛИ', ['СТАТЬ', 'СТАЛЬ']),
+
+    ('НАИСТАРЕЙШИЙ', ['СТАРЫЙ']),
+
+    ('КОТЁНОК', ['КОТЁНОК']),
+    ('КОТЕНОК', ['КОТЁНОК']),
+    ('ТЯЖЕЛЫЙ', ['ТЯЖЁЛЫЙ']),
+    ('ЛЕГОК', ['ЛЁГКИЙ']),
+
+    ('ОНА', ['ОНА']),
+    ('ЕЙ', ['ОНА']),
+    ('Я', ['Я']),
+    ('МНЕ', ['Я']),
+
+    ('НАИНЕВЕРОЯТНЕЙШИЙ', ['ВЕРОЯТНЫЙ']),
+    ('ЛУЧШИЙ', ['ХОРОШИЙ']),
+    ('НАИЛУЧШИЙ', ['ХОРОШИЙ']),
+    ('ЧЕЛОВЕК', ['ЧЕЛОВЕК']),
+    ('ЛЮДИ', ['ЧЕЛОВЕК']),
+
+    ('КЛЮЕВУ', ['КЛЮЕВ']),
+    ('КЛЮЕВА', ['КЛЮЕВ']),
+
+    ('ГУЛЯЛ', ['ГУЛЯТЬ']),
+    ('ГУЛЯЛА', ['ГУЛЯТЬ']),
+    ('ГУЛЯЕТ', ['ГУЛЯТЬ']),
+    ('ГУЛЯЮТ', ['ГУЛЯТЬ']),
+    ('ГУЛЯЛИ', ['ГУЛЯТЬ']),
+    ('ГУЛЯТЬ', ['ГУЛЯТЬ']),
+
+    ('ГУЛЯЮЩИЙ', ['ГУЛЯТЬ']),
+    ('ГУЛЯВШИ', ['ГУЛЯТЬ']),
+    ('ГУЛЯЯ', ['ГУЛЯТЬ']),
+    ('ГУЛЯЮЩАЯ', ['ГУЛЯТЬ']),
+    ('ЗАГУЛЯВШИЙ', ['ЗАГУЛЯТЬ']),
+
+    ('КРАСИВЫЙ', ['КРАСИВЫЙ']),
+    ('КРАСИВАЯ', ['КРАСИВЫЙ']),
+    ('КРАСИВОМУ', ['КРАСИВЫЙ']),
+    ('КРАСИВЫЕ', ['КРАСИВЫЙ']),
+
+    ('ДЕЙСТВИЕ', ['ДЕЙСТВИЕ']),
+]
+
+PREFIX_PREDICTION_DATA = [
+    ('ПСЕВДОКОШКА', ['ПСЕВДОКОШКА']),
+    ('ПСЕВДОКОШКОЙ', ['ПСЕВДОКОШКА']),
+
+    ('СВЕРХНАИСТАРЕЙШИЙ', ['СВЕРХСТАРЫЙ']),
+    ('СВЕРХНАИСТАРЕЙШИЙ', ['СВЕРХСТАРЫЙ']),
+    ('КВАЗИПСЕВДОНАИСТАРЕЙШЕГО', ['КВАЗИПСЕВДОСТАРЫЙ']),
+    ('НЕБЕСКОНЕЧЕН', ['НЕБЕСКОНЕЧНЫЙ']),
+
+    ('МЕГАКОТУ', ['МЕГАКОТ']),
+    ('МЕГАСВЕРХНАИСТАРЕЙШЕМУ', ['МЕГАСВЕРХСТАРЫЙ']),
+]
+
+PREDICTION_TEST_DATA = [
+    ('ТРИЖДЫЧЕРЕЗПИЛЮЛЮОКНАМИ', ['ТРИЖДЫЧЕРЕЗПИЛЮЛЮОКНО']),
+    ('РАЗКВАКАЛИСЬ', ['РАЗКВАКАТЬСЯ']),
+    ('КАШИВАРНЕЕ', ['КАШИВАРНЫЙ']),
+    ('ДЕПЫРТАМЕНТОВ', ['ДЕПЫРТАМЕНТ', 'ДЕПЫРТАМЕНТОВЫЙ']),
+    ('ИЗМОХРАТИЛСЯ', ['ИЗМОХРАТИТЬСЯ']),
+
+    ('БУТЯВКОЙ', ['БУТЯВКА', 'БУТЯВКОЙ']), # и никаких местоимений!
+    ('САПАЮТ', ['САПАТЬ']), # и никаких местоимений!
+]
+
+NON_PRODUCTIVE_BUGS_DATA = [
+    ('БЯКОБЫ', 'PRCL'),
+    ('БЯКОБЫ', 'CONJ'),
+    ('ПСЕВДОЯКОБЫ', 'PRCL'),
+    ('ПСЕВДОЯКОБЫ', 'CONJ'),
+]
+
+def with_test_data(data, second_param_name='parse_result'):
     return pytest.mark.parametrize(
-        ("word", "parse_result"),
+        ("word", second_param_name),
         data
     )
 
 class TestNormalForms(object):
 
-    TEST_DATA = [
-        ('КОШКА', ['КОШКА']),
-        ('КОШКЕ', ['КОШКА']),
-
-        # в pymorphy 0.5.6 результат парсинга - наоборот, сначала СТАЛЬ, потом СТАТЬ
-        ('СТАЛИ', ['СТАТЬ', 'СТАЛЬ']),
-
-        ('НАИСТАРЕЙШИЙ', ['СТАРЫЙ']),
-
-        ('КОТЁНОК', ['КОТЁНОК']),
-        ('КОТЕНОК', ['КОТЁНОК']),
-        ('ТЯЖЕЛЫЙ', ['ТЯЖЁЛЫЙ']),
-        ('ЛЕГОК', ['ЛЁГКИЙ']),
-
-        ('ОНА', ['ОНА']),
-        ('ЕЙ', ['ОНА']),
-        ('Я', ['Я']),
-        ('МНЕ', ['Я']),
-
-        ('НАИНЕВЕРОЯТНЕЙШИЙ', ['ВЕРОЯТНЫЙ']),
-        ('ЛУЧШИЙ', ['ХОРОШИЙ']),
-        ('НАИЛУЧШИЙ', ['ХОРОШИЙ']),
-        ('ЧЕЛОВЕК', ['ЧЕЛОВЕК']),
-        ('ЛЮДИ', ['ЧЕЛОВЕК']),
-
-        ('КЛЮЕВУ', ['КЛЮЕВ']),
-        ('КЛЮЕВА', ['КЛЮЕВ']),
-
-        ('ГУЛЯЛ', ['ГУЛЯТЬ']),
-        ('ГУЛЯЛА', ['ГУЛЯТЬ']),
-        ('ГУЛЯЕТ', ['ГУЛЯТЬ']),
-        ('ГУЛЯЮТ', ['ГУЛЯТЬ']),
-        ('ГУЛЯЛИ', ['ГУЛЯТЬ']),
-        ('ГУЛЯТЬ', ['ГУЛЯТЬ']),
-
-        ('ГУЛЯЮЩИЙ', ['ГУЛЯТЬ']),
-        ('ГУЛЯВШИ', ['ГУЛЯТЬ']),
-        ('ГУЛЯЯ', ['ГУЛЯТЬ']),
-        ('ГУЛЯЮЩАЯ', ['ГУЛЯТЬ']),
-        ('ЗАГУЛЯВШИЙ', ['ЗАГУЛЯТЬ']),
-
-        ('КРАСИВЫЙ', ['КРАСИВЫЙ']),
-        ('КРАСИВАЯ', ['КРАСИВЫЙ']),
-        ('КРАСИВОМУ', ['КРАСИВЫЙ']),
-        ('КРАСИВЫЕ', ['КРАСИВЫЙ']),
-
-        ('ДЕЙСТВИЕ', ['ДЕЙСТВИЕ']),
-    ]
-
-    PREFIX_PREDICTION_DATA = [
-        ('ПСЕВДОКОШКА', ['ПСЕВДОКОШКА']),
-        ('ПСЕВДОКОШКОЙ', ['ПСЕВДОКОШКА']),
-
-        ('СВЕРХНАИСТАРЕЙШИЙ', ['СВЕРХСТАРЫЙ']),
-        ('СВЕРХНАИСТАРЕЙШИЙ', ['СВЕРХСТАРЫЙ']),
-        ('КВАЗИПСЕВДОНАИСТАРЕЙШЕГО', ['КВАЗИПСЕВДОСТАРЫЙ']),
-        ('НЕБЕСКОНЕЧЕН', ['НЕБЕСКОНЕЧНЫЙ']),
-
-        ('МЕГАКОТУ', ['МЕГАКОТ']),
-        ('МЕГАСВЕРХНАИСТАРЕЙШЕМУ', ['МЕГАСВЕРХСТАРЫЙ']),
-    ]
-
-    PREDICTION_TEST_DATA = [
-        ('ТРИЖДЫЧЕРЕЗПИЛЮЛЮОКНАМИ', ['ТРИЖДЫЧЕРЕЗПИЛЮЛЮОКНО']),
-        ('РАЗКВАКАЛИСЬ', ['РАЗКВАКАТЬСЯ']),
-        ('КАШИВАРНЕЕ', ['КАШИВАРНЫЙ']),
-        ('ДЕПЫРТАМЕНТОВ', ['ДЕПЫРТАМЕНТ', 'ДЕПЫРТАМЕНТОВЫЙ']),
-        ('ИЗМОХРАТИЛСЯ', ['ИЗМОХРАТИТЬСЯ']),
-
-        ('БУТЯВКОЙ', ['БУТЯВКА', 'БУТЯВКОЙ']), # и никаких местоимений!
-        ('САПАЮТ', ['САПАТЬ']), # и никаких местоимений!
-    ]
-
     @with_test_data(TEST_DATA)
     def test_normal_forms(self, word, parse_result):
         assert morph.normal_forms(word) == parse_result
         assert morph.normal_forms(word) == parse_result
 
 
+class TestTagMethod(object):
+
+    def _tagged_as(self, parse, cls):
+        return any(tagset.get_POS(p)==cls for p in parse)
+
+    def assertNotTaggedAs(self, word, cls):
+        parse = morph.tag(word)
+        assert not self._tagged_as(parse, cls), (parse, cls)
+
+    def _tags_from_parses(self, parses):
+        return [p[1] for p in parses]
+
+    @with_test_data(TEST_DATA)
+    def test_tag_is_on_par_with_parse(self, word, parse_result): #parse_result is unused here
+        assert set(morph.tag(word)) == set(self._tags_from_parses(morph.parse(word)))
+
+    @with_test_data(PREDICTION_TEST_DATA)
+    def test_tag_is_on_par_with_parse__prediction(self, word, parse_result): #parse_result is unused here
+        assert set(morph.tag(word)) == set(self._tags_from_parses(morph.parse(word)))
+
+    @with_test_data(PREFIX_PREDICTION_DATA)
+    def test_tag_is_on_par_with_parse__prefix_prediction(self, word, parse_result): #parse_result is unused here
+        assert set(morph.tag(word)) == set(self._tags_from_parses(morph.parse(word)))
+
+    @with_test_data(NON_PRODUCTIVE_BUGS_DATA, 'cls')
+    def test_no_nonproductive_forms(self, word, cls):
+        self.assertNotTaggedAs(word, cls)
+
+
 class TestParse(object):
 
     def _parsed_as(self, parse, cls):
         return any(tagset.get_POS(p[1])==cls for p in parse)
 
+    def _parse_cls_first_index(self, parse, cls):
+        for idx, p in enumerate(parse):
+            if tagset.get_POS(p[1]) == cls:
+                return idx
+
     def assertNotParsedAs(self, word, cls):
         parse = morph.parse(word)
         assert not self._parsed_as(parse, cls), (parse, cls)
 
-    def test_no_nonproductive_forms(self):
-        self.assertNotParsedAs('БЯКОБЫ', 'PRCL')
-        self.assertNotParsedAs('БЯКОБЫ', 'CONJ')
-
-    def test_no_nonproductive_forms2(self):
-        self.assertNotParsedAs('ПСЕВДОЯКОБЫ', 'PRCL')
-        self.assertNotParsedAs('ПСЕВДОЯКОБЫ', 'CONJ')
+    @with_test_data(NON_PRODUCTIVE_BUGS_DATA, 'cls')
+    def test_no_nonproductive_forms(self, word, cls):
+        self.assertNotParsedAs(word, cls)
 
     def test_no_duplicate_parses(self):
         parse = morph.parse('БУТЯВКОЙ')
         data = [variant[:3] for variant in parse]
         assert len(set(data)) == len(data), parse
 
+    def test_parse_order(self):
+        parse = morph.parse('ПРОДЮСЕРСТВО')
+        assert self._parsed_as(parse, 'NOUN')
+        assert self._parsed_as(parse, 'ADVB')
+        assert self._parse_cls_first_index(parse, 'NOUN') < self._parse_cls_first_index(parse, 'ADVB')
+
 
 class TestTagWithPrefix(object):