pylelemmatize 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pylelemmatize/__init__.py +52 -0
- pylelemmatize/abstract_mapper.py +345 -0
- pylelemmatize/all_charsets.py +282 -0
- pylelemmatize/char_distance.py +108 -0
- pylelemmatize/charset_ascii.py +21 -0
- pylelemmatize/charset_greek.py +133 -0
- pylelemmatize/charset_iso.py +43 -0
- pylelemmatize/charset_mes.py +145 -0
- pylelemmatize/charset_mufi.py +101 -0
- pylelemmatize/demapper_lstm.py +475 -0
- pylelemmatize/demapper_lstm_ctc.py +314 -0
- pylelemmatize/fast_mapper.py +123 -0
- pylelemmatize/htr_postcorrection.py +263 -0
- pylelemmatize/main_scripts.py +137 -0
- pylelemmatize/many_to_more.py +321 -0
- pylelemmatize/mapper_ds.py +236 -0
- pylelemmatize/philogeny.py +156 -0
- pylelemmatize/pylelemmatize_torch.py +33 -0
- pylelemmatize/substitution_augmenter.py +342 -0
- pylelemmatize/util.py +164 -0
- pylelemmatize/version.py +1 -0
- pylelemmatize-0.1.0.dist-info/METADATA +107 -0
- pylelemmatize-0.1.0.dist-info/RECORD +27 -0
- pylelemmatize-0.1.0.dist-info/WHEEL +5 -0
- pylelemmatize-0.1.0.dist-info/entry_points.txt +16 -0
- pylelemmatize-0.1.0.dist-info/top_level.txt +2 -0
- pylelemmatize_torch/__init__.py +6 -0
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from typing import Dict, Literal, Optional
|
|
2
|
+
from .char_distance import char_similarity
|
|
3
|
+
from .main_scripts import main_alphabet_extract_corpus_alphabet, main_alphabet_evaluate_merges
|
|
4
|
+
from .abstract_mapper import AbstractLemmatizer, GenericLemmatizer, fast_cer, fast_numpy_to_str, fast_str_to_numpy
|
|
5
|
+
from .fast_mapper import LemmatizerBMP
|
|
6
|
+
#from .charset import allbmp_encoding_alphabet_strings, allnonbmp_encoding_alphabet_strings, main_map_test_corpus_on_alphabets
|
|
7
|
+
|
|
8
|
+
#from .charset import chr_iso, chr_mes, chr_ascii, chr_mufi_bmp, chr_mufi_nonbmp
|
|
9
|
+
from .all_charsets import Charsets
|
|
10
|
+
charsets = Charsets()
|
|
11
|
+
|
|
12
|
+
from .util import extract_transcription_from_page_xml, main_extract_transcription_from_page_xml, print_err
|
|
13
|
+
import sys
|
|
14
|
+
from .philogeny import main_char_similarity_tree
|
|
15
|
+
from .version import version
|
|
16
|
+
from .substitution_augmenter import CharConfusionMatrix
|
|
17
|
+
__version__ = version
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
default_unknown_chr = "�"
|
|
21
|
+
|
|
22
|
+
def create_lemmatizer(src_alphabet_str: str, dst_alphabet_str: Optional[str]=None,
|
|
23
|
+
mapper_type: Literal["fast", "generic", "guess"] = "guess", unknown_chr: str = default_unknown_chr,
|
|
24
|
+
override_map: Optional[Dict[str, str]] = None, min_similarity: float = .3) -> GenericLemmatizer:
|
|
25
|
+
if mapper_type == "guess":
|
|
26
|
+
if LemmatizerBMP.alphabet_in_bmp(src_alphabet_str) and LemmatizerBMP.alphabet_in_bmp(dst_alphabet_str):
|
|
27
|
+
mapper_type = "fast"
|
|
28
|
+
else:
|
|
29
|
+
mapper_type = "generic"
|
|
30
|
+
if mapper_type == "fast":
|
|
31
|
+
return LemmatizerBMP.from_alphabet_mapping(src_alphabet_str, dst_alphabet_str, unknown_chr=unknown_chr,
|
|
32
|
+
override_map=override_map, min_similarity=min_similarity)
|
|
33
|
+
elif mapper_type == "generic":
|
|
34
|
+
return GenericLemmatizer.from_alphabet_mapping(src_alphabet_str, dst_alphabet_str, unknown_chr=unknown_chr,
|
|
35
|
+
override_map=override_map, min_similarity=min_similarity)
|
|
36
|
+
else:
|
|
37
|
+
raise ValueError(f"Unknown mapper type: {mapper_type}")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
__all__ = ["create_lemmatizer", "AbstractLemmatizer", "GenericLemmatizer", "LemmatizerBMP",
|
|
41
|
+
"char_similarity", "fast_cer", "fast_numpy_to_str", "fast_str_to_numpy",
|
|
42
|
+
"Charsets", "charset",
|
|
43
|
+
"extract_transcription_from_page_xml", "print_err",
|
|
44
|
+
"CharConfusionMatrix", "__version__"]
|
|
45
|
+
|
|
46
|
+
if "torch" in sys.modules or "sphinx" in sys.modules: # to allow doc generation without torch
|
|
47
|
+
from .mapper_ds import Seq2SeqDs
|
|
48
|
+
from .demapper_lstm import DemapperLSTM, main_train_one2one, main_infer_one2one, main_report_demapper
|
|
49
|
+
__all__.extend(["Seq2SeqDs", "DemapperLSTM", "main_train_one2one", "main_infer_one2one", "main_report_demapper"])
|
|
50
|
+
else:
|
|
51
|
+
print("Warning: Torch is not loaded. Seq2SeqDs will not be available.", file=sys.stderr)
|
|
52
|
+
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
from difflib import SequenceMatcher
|
|
2
|
+
import sys
|
|
3
|
+
|
|
4
|
+
from typing import Any, Dict, Generator, Literal, Optional, Set, Tuple, Union
|
|
5
|
+
from collections import defaultdict
|
|
6
|
+
import numpy as np
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
import unicodedata
|
|
9
|
+
from .char_distance import char_similarity
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def fast_str_to_numpy(s: str, dtype=np.uint16) -> np.ndarray:
|
|
13
|
+
if dtype == np.uint16:
|
|
14
|
+
return np.frombuffer(s.encode('utf-16le'), dtype=dtype)
|
|
15
|
+
elif dtype == np.uint32:
|
|
16
|
+
return np.frombuffer(s.encode('utf-32le'), dtype=dtype)
|
|
17
|
+
elif dtype == np.uint64:
|
|
18
|
+
return np.frombuffer(s.encode('utf-64le'), dtype=dtype)
|
|
19
|
+
elif dtype == np.uint8:
|
|
20
|
+
return np.frombuffer(s.encode('utf-8'), dtype=dtype)
|
|
21
|
+
else:
|
|
22
|
+
raise ValueError(f"Unsupported dtype: {dtype}")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def fast_numpy_to_str(np_arr: np.ndarray) -> str:
|
|
26
|
+
if np_arr.dtype == np.uint16:
|
|
27
|
+
return np_arr.tobytes().decode('utf-16le')
|
|
28
|
+
elif np_arr.dtype == np.uint32:
|
|
29
|
+
return np_arr.tobytes().decode('utf-32le')
|
|
30
|
+
elif np_arr.dtype == np.uint64:
|
|
31
|
+
return np_arr.tobytes().decode('utf-64le')
|
|
32
|
+
elif np_arr.dtype == np.uint8:
|
|
33
|
+
return np_arr.tobytes().decode('utf-8')
|
|
34
|
+
else:
|
|
35
|
+
raise ValueError(f"Unsupported dtype: {np_arr.dtype}")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def fast_cer(pred: str, true: str) -> float:
|
|
39
|
+
np_pred = fast_str_to_numpy(pred)
|
|
40
|
+
np_true = fast_str_to_numpy(true)
|
|
41
|
+
return np.mean(np_pred != np_true)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class AbstractLemmatizer(ABC):
|
|
47
|
+
"""Abstract base class for lemmatizers that map characters from a source alphabet to a destination alphabet.
|
|
48
|
+
|
|
49
|
+
Attributes:
|
|
50
|
+
src_alphabet_str (str): The source alphabet string.
|
|
51
|
+
dst_alphabet_str (str): The destination alphabet string.
|
|
52
|
+
unknown_chr (str): The character used for unknown mappings. Default is "�".
|
|
53
|
+
normalize_unicode (Callable[[str], str]): Function to normalize Unicode strings.
|
|
54
|
+
"""
|
|
55
|
+
def _densify_unicode(self: Any, text: str) -> str:
|
|
56
|
+
"""
|
|
57
|
+
Convert a Unicode string to its composed (dense) form using NFC normalization.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
text (str): The input Unicode string.
|
|
61
|
+
|
|
62
|
+
Returns:
|
|
63
|
+
str: The composed (dense) Unicode string.
|
|
64
|
+
"""
|
|
65
|
+
return unicodedata.normalize('NFC', text)
|
|
66
|
+
|
|
67
|
+
def _decompose_unicode(self: Any, text: str) -> str:
|
|
68
|
+
"""
|
|
69
|
+
Convert a Unicode string to its decomposed (sparse) form using NFD normalization.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
text (str): The input Unicode string.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
str: The decomposed (sparse) Unicode string.
|
|
76
|
+
"""
|
|
77
|
+
return unicodedata.normalize('NFD', text)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _null_unicode(self: Any, text: str) -> str:
|
|
81
|
+
"""
|
|
82
|
+
Normalize a Unicode string to its composed (dense) form using NFC normalization.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
text (str): The input Unicode string.
|
|
86
|
+
|
|
87
|
+
Returns:
|
|
88
|
+
str: The normalized Unicode string.
|
|
89
|
+
"""
|
|
90
|
+
return text
|
|
91
|
+
|
|
92
|
+
@classmethod
|
|
93
|
+
def __create_mappers(cls, mapping_dict, unknown_chr) -> Tuple[defaultdict, np.ndarray, Dict[int, str], Dict[str, int], np.ndarray, np.ndarray, np.ndarray]:
|
|
94
|
+
max_ord = max(ord(c) for c in mapping_dict.keys() | mapping_dict.values() | {unknown_chr})
|
|
95
|
+
assert max_ord < 256**3, f"All characters must be whithin resoble size (256^3). found {max_ord}."
|
|
96
|
+
|
|
97
|
+
if unknown_chr in mapping_dict:
|
|
98
|
+
assert mapping_dict[unknown_chr] == unknown_chr, "unknown_chr must map to itself in the mapping_dict."
|
|
99
|
+
del mapping_dict[unknown_chr] # Remove the unknown character from the mapping to avoid confusion
|
|
100
|
+
|
|
101
|
+
src_alphabet_str = ''.join(sorted(mapping_dict.keys()))
|
|
102
|
+
dst_alphabet_str = ''.join(sorted(set(mapping_dict.values())))
|
|
103
|
+
|
|
104
|
+
chr2chr = defaultdict(lambda: unknown_chr)
|
|
105
|
+
chr2chr.update(mapping_dict)
|
|
106
|
+
|
|
107
|
+
dense2src_dst = [(n + 1, (c, chr2chr[c])) for n, c in enumerate(src_alphabet_str)]
|
|
108
|
+
|
|
109
|
+
src_str = [(s,s) for _, (s, _) in dense2src_dst]
|
|
110
|
+
src_str = ''.join([s for _, s in sorted(src_str)])
|
|
111
|
+
|
|
112
|
+
dst_str = [(s, d) for _, (s, d) in dense2src_dst]
|
|
113
|
+
dst_str = [(s, d) for s, d in dst_str if d != unknown_chr] # Remove unknown characters from the destination string
|
|
114
|
+
dst_str = ''.join([d for _, d in sorted(dst_str)])
|
|
115
|
+
|
|
116
|
+
src_full_str = unknown_chr + src_str
|
|
117
|
+
|
|
118
|
+
srcchr2dense = {s: n for n, s in enumerate(src_full_str)}
|
|
119
|
+
|
|
120
|
+
np_chrord2dense = np.zeros(max_ord, dtype=np.uint16)
|
|
121
|
+
np_dense2chrord = np.zeros(max_ord, dtype=np.uint16)
|
|
122
|
+
for c, n in srcchr2dense.items():
|
|
123
|
+
np_chrord2dense[ord(c)] = n
|
|
124
|
+
np_dense2chrord[n] = ord(chr2chr[c])
|
|
125
|
+
return src_alphabet_str, dst_alphabet_str, np_chrord2dense, np_dense2chrord
|
|
126
|
+
|
|
127
|
+
@classmethod
|
|
128
|
+
def fast_alphabet_extraction(cls, text: str) -> str:
|
|
129
|
+
np_text = fast_str_to_numpy(text)
|
|
130
|
+
uniq = np.unique(np_text)
|
|
131
|
+
return fast_numpy_to_str(uniq)
|
|
132
|
+
|
|
133
|
+
def __set_unicode_normalization(self, normalize_unicode: Literal["Dense", "Composite", None] = "Dense"):
|
|
134
|
+
"""
|
|
135
|
+
np_text = fast_str_to_numpy(text)
|
|
136
|
+
Set the Unicode normalization method.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
normalize_unicode (Literal["Dense", "Composite", None]): The normalization type.
|
|
140
|
+
- "Dense" for NFC normalization (composed form).
|
|
141
|
+
- "Composite" for NFD normalization (decomposed form).
|
|
142
|
+
- None for no normalization.
|
|
143
|
+
"""
|
|
144
|
+
self.__unicode_normalization = normalize_unicode
|
|
145
|
+
if normalize_unicode.lower() == "dense":
|
|
146
|
+
self.normalize_unicode = self._densify_unicode
|
|
147
|
+
elif normalize_unicode == "composite":
|
|
148
|
+
self.normalize_unicode = self._decompose_unicode
|
|
149
|
+
elif normalize_unicode is None:
|
|
150
|
+
self.normalize_unicode = self._null_unicode
|
|
151
|
+
else:
|
|
152
|
+
raise ValueError(f"Unknown normalization type: {normalize_unicode}")
|
|
153
|
+
|
|
154
|
+
def __init__(self, unicode_normalization: Literal["Dense", "Composite", None] = "Dense", unknown_chr: str = "�"):
|
|
155
|
+
super().__init__()
|
|
156
|
+
self.__set_unicode_normalization(unicode_normalization)
|
|
157
|
+
self.__unknown_chr = unknown_chr
|
|
158
|
+
|
|
159
|
+
@property
|
|
160
|
+
def unicode_normalization(self) -> Literal["Dense", "Composite", None]:
|
|
161
|
+
return self.__unicode_normalization
|
|
162
|
+
|
|
163
|
+
@abstractmethod
|
|
164
|
+
def __call__(self, text: str) -> str:
|
|
165
|
+
"""Convert text to the alphabet representation."""
|
|
166
|
+
pass
|
|
167
|
+
|
|
168
|
+
@property
|
|
169
|
+
@abstractmethod
|
|
170
|
+
def src_alphabet_str(self) -> str:
|
|
171
|
+
pass
|
|
172
|
+
|
|
173
|
+
@property
|
|
174
|
+
@abstractmethod
|
|
175
|
+
def dst_alphabet_str(self) -> str:
|
|
176
|
+
pass
|
|
177
|
+
|
|
178
|
+
@property
|
|
179
|
+
def unknown_chr(self) -> str:
|
|
180
|
+
return self.__unknown_chr
|
|
181
|
+
|
|
182
|
+
@abstractmethod
|
|
183
|
+
def __repr__(self):
|
|
184
|
+
raise NotImplementedError("Subclasses must implement __repr__ method.")
|
|
185
|
+
|
|
186
|
+
@property
|
|
187
|
+
def alphabet_tsv(self) -> str:
|
|
188
|
+
title = " #\tUnicode Number\tUnicode x10\tUnicode x16\tPython String"
|
|
189
|
+
lines = [title]
|
|
190
|
+
for n, c in enumerate(self.unknown_chr + self.dst_alphabet_str):
|
|
191
|
+
lines.append(f"{n}\t{unicodedata.name(c)}\t{ord(c)}\t{ord(c):x}\t{repr(c)}")
|
|
192
|
+
return "\n".join(lines)
|
|
193
|
+
|
|
194
|
+
@property
|
|
195
|
+
def mapping_tsv(self) -> str:
|
|
196
|
+
title = "Source Character\tDestination Character"
|
|
197
|
+
lines = [title]
|
|
198
|
+
for src, dst in sorted(self.mapping_dict.items()):
|
|
199
|
+
lines.append(f"{repr(src)}\t{repr(dst)}")
|
|
200
|
+
return "\n".join(lines)
|
|
201
|
+
|
|
202
|
+
def __ne__(self, other: Any) -> bool:
|
|
203
|
+
if not isinstance(other, AbstractLemmatizer):
|
|
204
|
+
return True
|
|
205
|
+
else:
|
|
206
|
+
return self.mapping_tsv != other.mapping_tsv
|
|
207
|
+
|
|
208
|
+
def __eq__(self, other: Any) -> bool:
|
|
209
|
+
if not isinstance(other, AbstractLemmatizer):
|
|
210
|
+
return False
|
|
211
|
+
else:
|
|
212
|
+
return self.mapping_tsv == other.mapping_tsv
|
|
213
|
+
|
|
214
|
+
def __len__(self) -> int:
|
|
215
|
+
"""Return the size of the destination alphabet."""
|
|
216
|
+
return len(self.dst_alphabet_str) + 1
|
|
217
|
+
|
|
218
|
+
def get_unigram(self, text: str) -> Tuple[np.ndarray, np.ndarray, Dict[int, str]]:
|
|
219
|
+
# adding all characters atleast once to make np.unique count zero counts
|
|
220
|
+
#src_alphabet_str, _, __np_chrord2dense, __np_dense2chrord = self.__create_mappers(self.mapping_dict, self.unknown_chr)
|
|
221
|
+
np_text = fast_str_to_numpy(self.unknown_chr + self.src_alphabet_str + text)
|
|
222
|
+
mapped_np_text = self.__np_chrord2dense[np_text]
|
|
223
|
+
values, counts = np.unique(mapped_np_text, return_counts=True)
|
|
224
|
+
counts = counts - 1 # removing the counts of the added characters
|
|
225
|
+
labels = np.array([c for c in fast_numpy_to_str(self.__np_dense2chrord[values])], dtype=np.str_)
|
|
226
|
+
return values, counts, labels
|
|
227
|
+
|
|
228
|
+
def get_cer(self, pred: str, true: str) -> float:
|
|
229
|
+
np_pred = fast_str_to_numpy(pred)
|
|
230
|
+
np_true = fast_str_to_numpy(true)
|
|
231
|
+
mapped_np_pred = self.__npint2int[np_pred]
|
|
232
|
+
mapped_np_true = self.__npint2int[np_true]
|
|
233
|
+
return np.mean(mapped_np_pred != mapped_np_true)
|
|
234
|
+
|
|
235
|
+
def get_encoding_information_loss(self, text: str) -> float:
|
|
236
|
+
np_text = fast_str_to_numpy(text)
|
|
237
|
+
mapped_np_text = self.__npint2int[np_text]
|
|
238
|
+
return np.mean(np_text != mapped_np_text)
|
|
239
|
+
|
|
240
|
+
# def get_unigram(self, text: str) -> Tuple[np.ndarray, np.ndarray, Dict[int, str]]:
|
|
241
|
+
# # adding all characters atleast once to make np.unique count zero counts
|
|
242
|
+
# src_alphabet_str, _, __np_chrord2dense, __np_dense2chrord = AbstractLemmatizer.__create_mappers(self.mapping_dict, self.unknown_chr)
|
|
243
|
+
# np_text = fast_str_to_numpy(self.unknown_chr + src_alphabet_str + text)
|
|
244
|
+
# mapped_np_text = __np_chrord2dense[np_text]
|
|
245
|
+
# values, counts = np.unique(mapped_np_text, return_counts=True)
|
|
246
|
+
# counts = counts - 1 # removing the counts of the added characters
|
|
247
|
+
# labels = np.array([c for c in fast_numpy_to_str(__np_dense2chrord[values])], dtype=np.str_)
|
|
248
|
+
# labels[0] = self.unknown_chr # Ensure the unknown character is included
|
|
249
|
+
# print("\n\nLABELS:\n", labels, "\n\n\n")
|
|
250
|
+
# return values, counts, labels
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
class GenericLemmatizer(AbstractLemmatizer):
|
|
254
|
+
@classmethod
|
|
255
|
+
def from_alphabet_mapping(cls, src_alphabet_str: str, dst_alphabet_str: Optional[str] = None,
|
|
256
|
+
unknown_chr: str = "�", override_map: Optional[Dict[str, str]] = None,
|
|
257
|
+
min_similarity: float = .25, verbose: int = 0) -> 'GenericLemmatizer':
|
|
258
|
+
if dst_alphabet_str is None:
|
|
259
|
+
mapping_dict = {c:c for c in src_alphabet_str}
|
|
260
|
+
if unknown_chr not in mapping_dict:
|
|
261
|
+
mapping_dict[unknown_chr] = unknown_chr
|
|
262
|
+
return cls(mapping_dict=mapping_dict, unknown_chr=unknown_chr)
|
|
263
|
+
mapping_dict = defaultdict(lambda: unknown_chr) # Default to unknown character
|
|
264
|
+
if override_map is not None:
|
|
265
|
+
mapping_dict.update(override_map) # Map destination characters to themselves
|
|
266
|
+
remain_sources = []
|
|
267
|
+
for c in src_alphabet_str:
|
|
268
|
+
if c not in override_map:
|
|
269
|
+
remain_sources.append(c)
|
|
270
|
+
else:
|
|
271
|
+
remain_sources = list(src_alphabet_str)
|
|
272
|
+
if unknown_chr not in dst_alphabet_str:
|
|
273
|
+
dst_alphabet_str = unknown_chr + dst_alphabet_str
|
|
274
|
+
s_map = np.zeros((len(remain_sources), len(dst_alphabet_str)))
|
|
275
|
+
unknown_chr_dst_idx = dst_alphabet_str.index(unknown_chr)
|
|
276
|
+
for src_n, src_c in enumerate(remain_sources):
|
|
277
|
+
for dst_n, dst_c in enumerate(dst_alphabet_str):
|
|
278
|
+
s_map[src_n, dst_n] = char_similarity(src_c, dst_c)
|
|
279
|
+
|
|
280
|
+
too_small = s_map < min_similarity
|
|
281
|
+
s_map[too_small] = 0 # Apply minimum similarity threshold
|
|
282
|
+
s_map[:, unknown_chr_dst_idx]+= .00001 # Set similarity to unknown character to 0
|
|
283
|
+
|
|
284
|
+
if verbose:
|
|
285
|
+
print(f"Similarity matrix:\n{s_map}\n", file=sys.stderr)
|
|
286
|
+
|
|
287
|
+
dst_symbol_array = np.array(list(dst_alphabet_str), dtype=np.str_)
|
|
288
|
+
src_symbol_array = np.array(list(remain_sources), dtype=np.str_)
|
|
289
|
+
best_dst_idx = np.argmax(s_map, axis=1)
|
|
290
|
+
|
|
291
|
+
if verbose:
|
|
292
|
+
print(f"Best destination indices: {best_dst_idx}\n", file=sys.stderr)
|
|
293
|
+
print(f"Source symbols: {src_symbol_array}\n", file=sys.stderr)
|
|
294
|
+
print(f"Destination symbols: {dst_symbol_array}\n", file=sys.stderr)
|
|
295
|
+
if verbose > 1:
|
|
296
|
+
for n in range(s_map.shape[0]):
|
|
297
|
+
print(f"Source: {repr(src_symbol_array[n])} -> Best Destination: {repr(dst_symbol_array[best_dst_idx[n]])} with similarity {s_map[n, best_dst_idx[n]]:.4f}", file=sys.stderr)
|
|
298
|
+
mapping_dict.update({src_symbol_array[n]: dst_symbol_array[best_dst_idx[n]] for n in range(len(remain_sources))})
|
|
299
|
+
mapping_dict = {k: v for k, v in sorted(mapping_dict.items())}
|
|
300
|
+
if verbose:
|
|
301
|
+
print(f"Mapping dictionary: {mapping_dict}\n", file=sys.stderr)
|
|
302
|
+
if unknown_chr not in mapping_dict:
|
|
303
|
+
mapping_dict[unknown_chr] = unknown_chr
|
|
304
|
+
else:
|
|
305
|
+
assert mapping_dict[unknown_chr] == unknown_chr, f"unknown_chr must map to itself in the mapping_dict. Found {repr(mapping_dict[unknown_chr])} instead of {repr(unknown_chr)}."
|
|
306
|
+
mapping_dict = {str(k): str(v) for k, v in sorted(mapping_dict.items())}
|
|
307
|
+
return cls(mapping_dict=mapping_dict, unknown_chr=unknown_chr)
|
|
308
|
+
|
|
309
|
+
def copy_removing_unused_inputs(self, txt: str) -> Any:
|
|
310
|
+
txt = self.normalize_unicode(txt)
|
|
311
|
+
txt = self.fast_alphabet_extraction(txt)
|
|
312
|
+
mapping_dict = self.mapping_dict.copy()
|
|
313
|
+
reduced_mapping_dict = {}
|
|
314
|
+
for k in txt:
|
|
315
|
+
if k in mapping_dict:
|
|
316
|
+
reduced_mapping_dict[k] = mapping_dict[k]
|
|
317
|
+
return self.__class__(mapping_dict=reduced_mapping_dict, unknown_chr=self.unknown_chr, unicode_normalization=self.unicode_normalization)
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def __init__(self, mapping_dict = {}, unknown_chr: str = "�", unicode_normalization: Literal["Dense", "Composed", None] = "Dense"):
|
|
321
|
+
super().__init__(unicode_normalization=unicode_normalization, unknown_chr=unknown_chr)
|
|
322
|
+
#print(f"Creating GenericLemmatizer with mapping_dict={repr(mapping_dict)} and unknown_chr={repr(unknown_chr)}")
|
|
323
|
+
self.mapping_dict = mapping_dict.copy()
|
|
324
|
+
|
|
325
|
+
def len(self) -> int:
|
|
326
|
+
"""Return the size of the destination alphabet."""
|
|
327
|
+
return len(self.dst_alphabet_str) + 1
|
|
328
|
+
|
|
329
|
+
def __call__(self, text: str) -> str:
|
|
330
|
+
text = self.normalize_unicode(text) # This might be the bottleneck in a fast implementation.
|
|
331
|
+
return ''.join(self.mapping_dict.get(c, self.unknown_chr) for c in text)
|
|
332
|
+
|
|
333
|
+
@property
|
|
334
|
+
def src_alphabet_str(self) -> str:
|
|
335
|
+
return ''.join(sorted(self.mapping_dict.keys()))
|
|
336
|
+
|
|
337
|
+
@property
|
|
338
|
+
def dst_alphabet_str(self) -> str:
|
|
339
|
+
res = ''.join(sorted(set(self.mapping_dict.values())))
|
|
340
|
+
if self.unknown_chr not in res:
|
|
341
|
+
res += self.unknown_chr
|
|
342
|
+
return res
|
|
343
|
+
|
|
344
|
+
def __repr__(self):
|
|
345
|
+
return f"GenericLemmatizer(mapping_dict={repr(self.mapping_dict)}, unknown_chr={repr(self.unknown_chr)})"
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
from typing import Dict, List
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
from .charset_iso import get_encoding_dicts as get_iso_encoding_dicts
|
|
5
|
+
from .charset_mes import get_encoding_dicts as get_mes_encoding_dicts
|
|
6
|
+
from .charset_ascii import get_encoding_dicts as get_ascii_encoding_dicts
|
|
7
|
+
from .charset_mufi import get_encoding_dicts as get_mufi_encoding_dicts
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
from .abstract_mapper import fast_cer, GenericLemmatizer
|
|
12
|
+
from .fast_mapper import LemmatizerBMP
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Charsets:
|
|
16
|
+
"""A class that provides access to various character sets as unicode strings.
|
|
17
|
+
|
|
18
|
+
This class is a singleton and is instantiated as `charset` in the `pylelemmatize` package.
|
|
19
|
+
|
|
20
|
+
Usage::
|
|
21
|
+
from pylelemmatize import Charsets
|
|
22
|
+
charsets = Charsets()
|
|
23
|
+
print(charsets.mes1) # prints the MES1 character set as a string
|
|
24
|
+
"""
|
|
25
|
+
def __init__(self):
|
|
26
|
+
self.lib_cache__ = {}
|
|
27
|
+
self.alphabets = ["mes1", "mes2", "mes3a", "mes3b",
|
|
28
|
+
"iso_8859_1", "iso_8859_2", "iso_8859_3", "iso_8859_4",
|
|
29
|
+
"iso_8859_5", "iso_8859_6", "iso_8859_7", "iso_8859_8",
|
|
30
|
+
"iso_8859_9", "iso_8859_10", "iso_8859_11", "iso_8859_12", "iso_8859_13",
|
|
31
|
+
"iso_8859_14", "iso_8859_15", "iso_8859_16",
|
|
32
|
+
"ascii_lowercase", "ascii_uppercase", "ascii_letters", "digits",
|
|
33
|
+
"hexdigits", "octdigits", "punctuation",
|
|
34
|
+
"bmp_mufi", "bmp_pua_mufi", "nonbmp_mufi",
|
|
35
|
+
"nonbmp_pua_mufi"]
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def mes_charsets(self):
|
|
39
|
+
if self.lib_cache__.get("mes") is None:
|
|
40
|
+
self.lib_cache__["mes"] = get_mes_encoding_dicts()
|
|
41
|
+
return self.lib_cache__["mes"]
|
|
42
|
+
@property
|
|
43
|
+
def mes1(self):
|
|
44
|
+
return self.mes_charsets["mes1"]
|
|
45
|
+
@property
|
|
46
|
+
def mes2(self):
|
|
47
|
+
return self.mes_charsets["mes2"]
|
|
48
|
+
@property
|
|
49
|
+
def mes3a(self):
|
|
50
|
+
return self.mes_charsets["mes3a"]
|
|
51
|
+
@property
|
|
52
|
+
def mes3b(self):
|
|
53
|
+
return self.mes_charsets["mes3b"]
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def iso_charsets(self):
|
|
57
|
+
if self.lib_cache__.get("iso") is None:
|
|
58
|
+
self.lib_cache__["iso"] = get_iso_encoding_dicts()
|
|
59
|
+
return self.lib_cache__["iso"]
|
|
60
|
+
@property
|
|
61
|
+
def iso_8859_2(self):
|
|
62
|
+
return self.iso_charsets["iso8859_2"]
|
|
63
|
+
@property
|
|
64
|
+
def iso_8859_3(self):
|
|
65
|
+
return self.iso_charsets["iso8859_3"]
|
|
66
|
+
@property
|
|
67
|
+
def iso_8859_4(self):
|
|
68
|
+
return self.iso_charsets["iso8859_4"]
|
|
69
|
+
@property
|
|
70
|
+
def iso_8859_5(self):
|
|
71
|
+
return self.iso_charsets["iso8859_5"]
|
|
72
|
+
@property
|
|
73
|
+
def iso_8859_6(self):
|
|
74
|
+
return self.iso_charsets["iso8859_6"]
|
|
75
|
+
@property
|
|
76
|
+
def iso_8859_7(self):
|
|
77
|
+
return self.iso_charsets["iso8859_7"]
|
|
78
|
+
@property
|
|
79
|
+
def iso_8859_8(self):
|
|
80
|
+
return self.iso_charsets["iso8859_8"]
|
|
81
|
+
@property
|
|
82
|
+
def iso_8859_9(self):
|
|
83
|
+
return self.iso_charsets["iso8859_9"]
|
|
84
|
+
@property
|
|
85
|
+
def iso_8859_10(self):
|
|
86
|
+
return self.iso_charsets["iso8859_10"]
|
|
87
|
+
@property
|
|
88
|
+
def iso_8859_11(self):
|
|
89
|
+
return self.iso_charsets["iso8859_11"]
|
|
90
|
+
@property
|
|
91
|
+
def iso_8859_12(self):
|
|
92
|
+
return self.iso_charsets["iso8859_12"]
|
|
93
|
+
@property
|
|
94
|
+
def iso_8859_13(self):
|
|
95
|
+
return self.iso_charsets["iso8859_13"]
|
|
96
|
+
@property
|
|
97
|
+
def iso_8859_14(self):
|
|
98
|
+
return self.iso_charsets["iso8859_14"]
|
|
99
|
+
@property
|
|
100
|
+
def iso_8859_15(self):
|
|
101
|
+
return self.iso_charsets["iso8859_15"]
|
|
102
|
+
@property
|
|
103
|
+
def iso_8859_16(self):
|
|
104
|
+
return self.iso_charsets["iso8859_16"]
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def ascii_charsets(self):
|
|
108
|
+
if self.lib_cache__.get("ascii") is None:
|
|
109
|
+
self.lib_cache__["ascii"] = get_ascii_encoding_dicts()
|
|
110
|
+
return self.lib_cache__["ascii"]
|
|
111
|
+
@property
|
|
112
|
+
def ascii(self):
|
|
113
|
+
return self.ascii_charsets["ascii"]
|
|
114
|
+
@property
|
|
115
|
+
def ascii_lowercase(self):
|
|
116
|
+
return self.ascii_charsets["ascii_lowercase"]
|
|
117
|
+
@property
|
|
118
|
+
def ascii_uppercase(self):
|
|
119
|
+
return self.ascii_charsets["ascii_uppercase"]
|
|
120
|
+
@property
|
|
121
|
+
def ascii_letters(self):
|
|
122
|
+
return self.ascii_charsets["ascii_letters"]
|
|
123
|
+
@property
|
|
124
|
+
def digits(self):
|
|
125
|
+
return self.ascii_charsets["digits"]
|
|
126
|
+
@property
|
|
127
|
+
def hexdigits(self):
|
|
128
|
+
return self.ascii_charsets["hexdigits"]
|
|
129
|
+
@property
|
|
130
|
+
def octdigits(self):
|
|
131
|
+
return self.ascii_charsets["octdigits"]
|
|
132
|
+
@property
|
|
133
|
+
def punctuation(self):
|
|
134
|
+
return self.ascii_charsets["punctuation"]
|
|
135
|
+
|
|
136
|
+
@property
|
|
137
|
+
def mufibmp_charsets(self):
|
|
138
|
+
if self.lib_cache__.get("mufibmp") is None:
|
|
139
|
+
self.lib_cache__["mufibmp"], self.lib_cache__["mufinonbmp"] = get_mufi_encoding_dicts()
|
|
140
|
+
return self.lib_cache__["mufibmp"]
|
|
141
|
+
@property
|
|
142
|
+
def mufinonbmp_charsets(self):
|
|
143
|
+
if self.lib_cache__.get("mufinonbmp") is None:
|
|
144
|
+
self.lib_cache__["mufibmp"], self.lib_cache__["mufinonbmp"] = get_mufi_encoding_dicts()
|
|
145
|
+
return self.lib_cache__["mufinonbmp"]
|
|
146
|
+
@property
|
|
147
|
+
def mufibmp(self):
|
|
148
|
+
return self.mufibmp_charsets["bmp_mufi"]
|
|
149
|
+
@property
|
|
150
|
+
def mufibmp_pua(self):
|
|
151
|
+
return self.mufibmp_charsets["bmp_pua_mufi"]
|
|
152
|
+
@property
|
|
153
|
+
def mufinonbmp(self):
|
|
154
|
+
return self.mufinonbmp_charsets["nonbmp_mufi"]
|
|
155
|
+
@property
|
|
156
|
+
def mufinonbmp_pua(self):
|
|
157
|
+
return self.mufinonbmp_charsets["nonbmp_pua_mufi"]
|
|
158
|
+
|
|
159
|
+
def __getitem__(self, item):
|
|
160
|
+
return getattr(self, item)
|
|
161
|
+
|
|
162
|
+
def __iter__(self):
|
|
163
|
+
return iter(self.alphabets)
|
|
164
|
+
|
|
165
|
+
def __len__(self):
|
|
166
|
+
return len(self.alphabets)
|
|
167
|
+
|
|
168
|
+
def __contains__(self, item):
|
|
169
|
+
return item in self.alphabets
|
|
170
|
+
|
|
171
|
+
def keys(self):
|
|
172
|
+
return self.alphabets
|
|
173
|
+
|
|
174
|
+
def items(self):
|
|
175
|
+
return [(k, getattr(self, k)) for k in self.alphabets]
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def main_map_test_corpus_on_alphabets():
|
|
179
|
+
import fargv, glob, time, sys, tqdm
|
|
180
|
+
from matplotlib import pyplot as plt
|
|
181
|
+
from .util import generate_corpus
|
|
182
|
+
|
|
183
|
+
def plot_covverage(corpus_str, encoding_alphabet_strings, alphabet_to_cer: Dict[str, float], alphabet_to_missing: Dict[str, str], alphabet_to_unfound: Dict[str, str], save_plot_path: str, show_plot: bool):
|
|
184
|
+
fig, ax = plt.subplots(3, 1, figsize=(15, 10))
|
|
185
|
+
alphabet_to_missing_ratio = {k: 100*len(v)/len(corpus_str) for k, v in alphabet_to_missing.items() if len(v) > 0}
|
|
186
|
+
alphabet_to_unfound_ratio = {k: len(v) for k, v in alphabet_to_unfound.items() if len(v) > 0}
|
|
187
|
+
ax[0].bar(alphabet_to_cer.keys(), [100*v for v in alphabet_to_cer.values()])
|
|
188
|
+
ax[0].set_xticklabels(alphabet_to_cer.keys(), rotation=45)
|
|
189
|
+
ax[0].set_title("CER %")
|
|
190
|
+
|
|
191
|
+
ax[1].bar(alphabet_to_missing_ratio.keys(), alphabet_to_missing_ratio.values())
|
|
192
|
+
ax[1].set_xticklabels(alphabet_to_missing_ratio.keys(), rotation=45)
|
|
193
|
+
ax[1].set_title("Character Recall %")
|
|
194
|
+
|
|
195
|
+
ax[2].bar(alphabet_to_unfound_ratio.keys(), alphabet_to_unfound_ratio.values())
|
|
196
|
+
ax[2].set_xticklabels(alphabet_to_unfound_ratio.keys(), rotation=45)
|
|
197
|
+
ax[2].set_yscale('log')
|
|
198
|
+
ax[2].set_title("Charset uncovered chars # (waste)")
|
|
199
|
+
|
|
200
|
+
plt.subplots_adjust(hspace=.6)
|
|
201
|
+
if save_plot_path != "":
|
|
202
|
+
plt.savefig(save_plot_path)
|
|
203
|
+
if show_plot:
|
|
204
|
+
plt.show()
|
|
205
|
+
|
|
206
|
+
t = time.time()
|
|
207
|
+
p = {
|
|
208
|
+
"alphabets": ["ascii", f"A comma separated listof encodings must be a subset of {repr(allbmp_encoding_alphabet_strings.keys())}. Or 'all'"],
|
|
209
|
+
"corpus_glob": "",
|
|
210
|
+
"corpus_files": set([]),
|
|
211
|
+
"verbose": False,
|
|
212
|
+
"strip_xml": True,
|
|
213
|
+
"all_is_xml": False,
|
|
214
|
+
"output_tsv": "",
|
|
215
|
+
"hide_plot": False,
|
|
216
|
+
"save_plot_path": ""
|
|
217
|
+
}
|
|
218
|
+
args, _ = fargv.fargv(p)
|
|
219
|
+
|
|
220
|
+
if args.alphabets == "all":
|
|
221
|
+
args.alphabets = list(allbmp_encoding_alphabet_strings.keys())
|
|
222
|
+
else:
|
|
223
|
+
args.alphabets = [a.strip() for a in args.alphabets.split(',')]
|
|
224
|
+
|
|
225
|
+
if args.corpus_glob != "":
|
|
226
|
+
glob_files = set(glob.glob(args.corpus_glob))
|
|
227
|
+
else:
|
|
228
|
+
glob_files = set([])
|
|
229
|
+
if len(args.corpus_files) > 0:
|
|
230
|
+
corpus_files = args.corpus_files
|
|
231
|
+
else:
|
|
232
|
+
corpus_files = set([])
|
|
233
|
+
|
|
234
|
+
all_corpus_strs = []
|
|
235
|
+
for file_contnets in generate_corpus(glob_files | corpus_files, verbose=args.verbose,
|
|
236
|
+
strip_xml=args.strip_xml, treat_all_file_as_xml=args.all_is_xml):
|
|
237
|
+
all_corpus_strs.append(file_contnets)
|
|
238
|
+
found_corpus_str = ''.join(all_corpus_strs)
|
|
239
|
+
|
|
240
|
+
if args.verbose:
|
|
241
|
+
print(f"{time.time() - t :.2f}: Loaded corpus", file=sys.stderr)
|
|
242
|
+
|
|
243
|
+
if args.verbose:
|
|
244
|
+
alphabet_names = tqdm.tqdm(args.alphabets)
|
|
245
|
+
else:
|
|
246
|
+
alphabet_names = args.alphabets
|
|
247
|
+
|
|
248
|
+
alphabet_to_cer = {}
|
|
249
|
+
alphabet_to_missing = {}
|
|
250
|
+
alphabet_to_unfound = {}
|
|
251
|
+
for alphabet_name in alphabet_names:
|
|
252
|
+
if alphabet_name not in allbmp_encoding_alphabet_strings:
|
|
253
|
+
raise ValueError(f"Alphabet {alphabet_name} not in {allbmp_encoding_alphabet_strings.keys()}")
|
|
254
|
+
|
|
255
|
+
alphabet_str = allbmp_encoding_alphabet_strings[alphabet_name]
|
|
256
|
+
if max(alphabet_str) > '\uffff':
|
|
257
|
+
#alphabet = Alphabet(alphabet_str=alphabet_str, vectorized_mapper_sz=max(256**2, 1 + ord(max(alphabet_str)))) # This is a hack to ensure that the vectorized mapper is large enough for all characters in MUFI
|
|
258
|
+
alphabet = GenericLemmatizer.from_alphabet_mapping(alphabet_str, mapping_dict=None)
|
|
259
|
+
else:
|
|
260
|
+
#alphabet = AlphabetBMP(alphabet_str=alphabet_str) # This is a hack to ensure that the vectorized mapper is large enough for all characters in MUFI
|
|
261
|
+
alphabet = LemmatizerBMP.from_alphabet_mapping(alphabet_str)
|
|
262
|
+
|
|
263
|
+
mapped_corpus_str = alphabet(found_corpus_str)
|
|
264
|
+
|
|
265
|
+
if len(found_corpus_str) > 0:
|
|
266
|
+
alphabet_to_cer[alphabet_name] = fast_cer(found_corpus_str, mapped_corpus_str)
|
|
267
|
+
alphabet_to_missing[alphabet_name] = ''.join(sorted(set(found_corpus_str) - set(alphabet_str)))
|
|
268
|
+
alphabet_to_unfound[alphabet_name] = ''.join(sorted(set(alphabet_str) - set(found_corpus_str)))
|
|
269
|
+
|
|
270
|
+
else:
|
|
271
|
+
alphabet_to_cer[alphabet_name] = 0
|
|
272
|
+
alphabet_to_missing[alphabet_name] = ""
|
|
273
|
+
alphabet_to_unfound[alphabet_name] = alphabet_str
|
|
274
|
+
|
|
275
|
+
if args.verbose:
|
|
276
|
+
for k, v in alphabet_to_cer.items():
|
|
277
|
+
print(f"{k}: CER {100*v:.2f}% Missing: {len(alphabet_to_missing[k])}, Redundant: {len(alphabet_to_unfound[k])} {repr(alphabet_to_missing[k])}", file=sys.stderr)
|
|
278
|
+
print(f"Computed all for {len(found_corpus_str)} characters in {time.time() -t :.2f}", file=sys.stderr)
|
|
279
|
+
|
|
280
|
+
if not args.hide_plot or args.save_plot_path != "":
|
|
281
|
+
found_corpus_alphabet_str = ''.join(sorted(set(found_corpus_str)))
|
|
282
|
+
plot_covverage(found_corpus_alphabet_str, allbmp_encoding_alphabet_strings, alphabet_to_cer, alphabet_to_missing, alphabet_to_unfound, args.save_plot_path, not args.hide_plot)
|