Repository: bst-mug/acres

Issue:
Calculate out-of-dictionary acronyms
If we're not dealing with disambiguation (#10), then, if existent, a domain-dictionary would suffice.

Check if any exists for clinical german and, if yes, estimate the amount of out-of-dictionary acronyms. If the amount is large (or if the dictionary introduces extra ambiguity), an unsupervised approach like ours is reasonable.

Discussion:

Patch:
diff --git a/acres/evaluation/evaluation.py b/acres/evaluation/evaluation.py
index d6a596f..09c345a 100644
--- a/acres/evaluation/evaluation.py
+++ b/acres/evaluation/evaluation.py
@@ -71,8 +71,8 @@ def analyze(contextualized_acronym: topic_list.Acronym, true_expansions: Set[str
     # Normalize true expansions so that we can match them to the training data
     true_expansions = set(map(text.clean, true_expansions))
 
-    # XXX We currently do not support contextualized word2vec
-    if strategy == resolver.Strategy.WORD2VEC:
+    # XXX We currently support context only for n-grams
+    if strategy != resolver.Strategy.NGRAM:
         left_context = ""
         right_context = ""
 
diff --git a/acres/model/expansion_standard.py b/acres/model/expansion_standard.py
index 433e90c..84f74e9 100644
--- a/acres/model/expansion_standard.py
+++ b/acres/model/expansion_standard.py
@@ -56,18 +56,25 @@ def write_results(filename: str, acronyms: Dict[str, Dict[str, int]]) -> None:
 
         # Write all old expansions
         _write_expansions(acronym, old_expansions, file)
+        previous_expansions = old_expansions.keys()
+
+        # Write all expansions in the dictionary
+        dictionary = resolver.resolve(acronym, "", "", resolver.Strategy.DICTIONARY)
+        dictionary = [exp for exp in dictionary if exp not in previous_expansions]
+        _write_expansions(acronym, dict.fromkeys(dictionary, -1), file)
+        previous_expansions |= set(dictionary)
 
         k = 5
 
         # Write up to k filtered expansions not in old
-        filtered_expansions = [exp for exp in filtered_expansions[:k] if
-                               exp not in old_expansions.keys()]
-        _write_expansions(acronym, dict.fromkeys(filtered_expansions, -1), file)
+        filtered_expansions = [exp for exp in filtered_expansions[:k]
+                               if exp not in previous_expansions]
+        _write_expansions(acronym, dict.fromkeys(filtered_expansions, -2), file)
+        previous_expansions |= set(filtered_expansions)
 
         # Write up to k remaining expansions
-        expansions = [exp for exp in expansions[:k] if
-                      exp not in old_expansions.keys() and exp not in set(filtered_expansions)]
-        _write_expansions(acronym, dict.fromkeys(expansions, -2), file)
+        expansions = [exp for exp in expansions[:k] if exp not in previous_expansions]
+        _write_expansions(acronym, dict.fromkeys(expansions, -3), file)
 
     file.close()
 
diff --git a/acres/preprocess/resource_factory.py b/acres/preprocess/resource_factory.py
index 1f7e319..9ca2824 100644
--- a/acres/preprocess/resource_factory.py
+++ b/acres/preprocess/resource_factory.py
@@ -9,12 +9,14 @@ import pickle
 from typing import Dict, Set, List, Tuple, Any
 
 from gensim.models import Word2Vec
-# from gensim.models import FastText
 
 from acres.nn import train
 from acres.preprocess import dumps
+from acres.stats import dictionary
 from acres.util import functions
 
+# from gensim.models import FastText
+
 logger = logging.getLogger(__name__)
 
 PICKLE_FOLDER = "models/pickle/"
@@ -34,6 +36,7 @@ NN_MODEL = None  # type: Word2Vec
 NGRAMSTAT = {}  # type: Dict[int, Tuple[int,str]]
 CHARACTER_NGRAMS = {}  # type: Dict[str, int]
 WORD_NGRAMS = {}  # type: Dict[str, int]
+DICTIONARY = {}  # type: Dict[str, List[str]]
 
 
 def get_log_corpus_filename() -> str:
@@ -286,6 +289,20 @@ def get_nn_model(ngram_size: int = 3, min_count: int = 1, net_size: int = 100, a
     return NN_MODEL
 
 
+def get_dictionary() -> Dict[str, List[str]]:
+    """
+    Lazy load the sense inventory.
+
+    :return:
+    """
+    global DICTIONARY
+
+    if not DICTIONARY:
+        DICTIONARY = dictionary.parse("resources/acro_full_reference.txt")
+
+    return DICTIONARY
+
+
 def reset() -> None:
     """
     Resets global variables to force model recreation.
diff --git a/acres/resolution/resolver.py b/acres/resolution/resolver.py
index b785cc2..e9c7470 100644
--- a/acres/resolution/resolver.py
+++ b/acres/resolution/resolver.py
@@ -4,6 +4,7 @@ from typing import List, Dict, Tuple
 from acres.ngram import finder
 from acres.nn import test
 from acres.rater import rater
+from acres.stats import dictionary
 from acres.util import text
 
 
@@ -13,10 +14,12 @@ class Strategy(Enum):
     """
     NGRAM = 1
     WORD2VEC = 2
+    DICTIONARY = 3
 
 
 NGRAM_CACHE = {}  # type: Dict[Tuple, List[str]]
 WORD2VEC_CACHE = {}  # type: Dict[Tuple, List[str]]
+DICTIONARY_CACHE = {}  # type: Dict[Tuple, List[str]]
 
 
 def cached_resolve(acronym: str, left_context: str, right_context: str,
@@ -35,7 +38,8 @@ def cached_resolve(acronym: str, left_context: str, right_context: str,
     """
     switcher = {
         Strategy.NGRAM: NGRAM_CACHE,
-        Strategy.WORD2VEC: WORD2VEC_CACHE
+        Strategy.WORD2VEC: WORD2VEC_CACHE,
+        Strategy.DICTIONARY: DICTIONARY_CACHE
     }
 
     cache = switcher.get(strategy)
@@ -85,7 +89,8 @@ def resolve(acronym: str, left_context: str, right_context: str, strategy: Strat
     """
     switcher = {
         Strategy.NGRAM: finder.robust_find_embeddings,
-        Strategy.WORD2VEC: test.find_candidates
+        Strategy.WORD2VEC: test.find_candidates,
+        Strategy.DICTIONARY: dictionary.expand
     }
 
     func = switcher.get(strategy)
diff --git a/acres/stats/dictionary.py b/acres/stats/dictionary.py
index 05a675f..8d13a9a 100644
--- a/acres/stats/dictionary.py
+++ b/acres/stats/dictionary.py
@@ -1,15 +1,57 @@
 """
 Metrics from large German acronym / definition list
 """
-from typing import List, Tuple, Optional
+from typing import List, Tuple, Optional, Dict
 
-from acres.rater import rater
+from acres.preprocess import resource_factory
 from acres.rater import expansion
+from acres.rater import rater
 from acres.util import acronym as acro_util
 from acres.util import functions
 
 
-def dump_sample(filename: str, max_len: int = 15, min_len: int = 1) -> List[Tuple[str, str]]:
+def expand(acronym: str, left_context: str = "", right_context: str = "") -> List[str]:
+    """
+
+    :param acronym:
+    :param left_context:
+    :param right_context:
+    :return:
+    """
+    dictionary = resource_factory.get_dictionary()
+    if acronym not in dictionary:
+        return []
+    return dictionary[acronym]
+
+
+def _tuple2dictionary(senses: List[Tuple[str, str]]) -> Dict[str, List[str]]:
+    """
+    Convert the sense inventory as a list of tuples to a Python dictionary for better search \
+    performance.
+
+    :param senses:
+    :return:
+    """
+    dictionary = {}  # type: Dict[str, List[str]]
+    for acro, full in senses:
+        # Initialize sub-list
+        if acro not in dictionary:
+            dictionary[acro] = []
+        dictionary[acro].append(full)
+    return dictionary
+
+
+def parse(filename: str) -> Dict[str, List[str]]:
+    """
+    Parse a tab-separated sense inventory as a Python dictionary.
+
+    :param filename:
+    :return:
+    """
+    return _tuple2dictionary(_dump_sample(filename))
+
+
+def _dump_sample(filename: str, max_len: int = 15, min_len: int = 1) -> List[Tuple[str, str]]:
     """
 
     :param filename:
@@ -97,7 +139,7 @@ def analyze_file(filename: str) -> None:
     :param filename:
     :return:
     """
-    senses = dump_sample(filename, 3, 3)
+    senses = _dump_sample(filename, 3, 3)
     for (acro, full) in senses:
         if not acro_util.is_acronym(acro):
             print(acro + " is not an acronym according to our definition")


Test patch:
diff --git a/tests/benchmark/test_benchmark.py b/tests/benchmark/test_benchmark.py
index 1a0f71a..c922348 100644
--- a/tests/benchmark/test_benchmark.py
+++ b/tests/benchmark/test_benchmark.py
@@ -168,3 +168,13 @@ def test_evaluation():
     absolute_tolerance = 0.02
     assert precision == pytest.approx(0.81, abs=absolute_tolerance)
     assert recall == pytest.approx(0.73, abs=absolute_tolerance)
+
+    (precision, recall) = evaluation.do_analysis("resources/topic_list.tsv",
+                                                 "resources/detection_standard.tsv",
+                                                 "resources/expansion_standard.tsv",
+                                                 resolver.Strategy.DICTIONARY,
+                                                 evaluation.Level.TYPE,
+                                                 10, True)
+    absolute_tolerance = 0.02
+    assert precision == pytest.approx(0.68, abs=absolute_tolerance)
+    assert recall == pytest.approx(0.42, abs=absolute_tolerance)
diff --git a/tests/stats/test_dictionary.py b/tests/stats/test_dictionary.py
index 0b4ea78..35892af 100644
--- a/tests/stats/test_dictionary.py
+++ b/tests/stats/test_dictionary.py
@@ -3,7 +3,7 @@ from acres.stats import dictionary
 
 def test_dump_sample():
     expected = [('AA', 'Alopecia areata'), ('γ-GT', 'γ-Glutamyltransferase')]
-    actual = dictionary.dump_sample("resources/acro_full_reference.txt")
+    actual = dictionary._dump_sample("resources/acro_full_reference.txt")
     assert set(expected).issubset(actual)