pylelemmatize 0.1.0__tar.gz
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-0.1.0/PKG-INFO +107 -0
- pylelemmatize-0.1.0/README.md +77 -0
- pylelemmatize-0.1.0/pyproject.toml +3 -0
- pylelemmatize-0.1.0/setup.cfg +4 -0
- pylelemmatize-0.1.0/setup.py +48 -0
- pylelemmatize-0.1.0/src/pylelemmatize/__init__.py +52 -0
- pylelemmatize-0.1.0/src/pylelemmatize/abstract_mapper.py +345 -0
- pylelemmatize-0.1.0/src/pylelemmatize/all_charsets.py +282 -0
- pylelemmatize-0.1.0/src/pylelemmatize/char_distance.py +108 -0
- pylelemmatize-0.1.0/src/pylelemmatize/charset_ascii.py +21 -0
- pylelemmatize-0.1.0/src/pylelemmatize/charset_greek.py +133 -0
- pylelemmatize-0.1.0/src/pylelemmatize/charset_iso.py +43 -0
- pylelemmatize-0.1.0/src/pylelemmatize/charset_mes.py +145 -0
- pylelemmatize-0.1.0/src/pylelemmatize/charset_mufi.py +101 -0
- pylelemmatize-0.1.0/src/pylelemmatize/demapper_lstm.py +475 -0
- pylelemmatize-0.1.0/src/pylelemmatize/demapper_lstm_ctc.py +314 -0
- pylelemmatize-0.1.0/src/pylelemmatize/fast_mapper.py +123 -0
- pylelemmatize-0.1.0/src/pylelemmatize/htr_postcorrection.py +263 -0
- pylelemmatize-0.1.0/src/pylelemmatize/main_scripts.py +137 -0
- pylelemmatize-0.1.0/src/pylelemmatize/many_to_more.py +321 -0
- pylelemmatize-0.1.0/src/pylelemmatize/mapper_ds.py +236 -0
- pylelemmatize-0.1.0/src/pylelemmatize/philogeny.py +156 -0
- pylelemmatize-0.1.0/src/pylelemmatize/pylelemmatize_torch.py +33 -0
- pylelemmatize-0.1.0/src/pylelemmatize/substitution_augmenter.py +342 -0
- pylelemmatize-0.1.0/src/pylelemmatize/util.py +164 -0
- pylelemmatize-0.1.0/src/pylelemmatize/version.py +1 -0
- pylelemmatize-0.1.0/src/pylelemmatize.egg-info/PKG-INFO +107 -0
- pylelemmatize-0.1.0/src/pylelemmatize.egg-info/SOURCES.txt +31 -0
- pylelemmatize-0.1.0/src/pylelemmatize.egg-info/dependency_links.txt +1 -0
- pylelemmatize-0.1.0/src/pylelemmatize.egg-info/entry_points.txt +16 -0
- pylelemmatize-0.1.0/src/pylelemmatize.egg-info/requires.txt +8 -0
- pylelemmatize-0.1.0/src/pylelemmatize.egg-info/top_level.txt +2 -0
- pylelemmatize-0.1.0/src/pylelemmatize_torch/__init__.py +6 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pylelemmatize
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A set utilities for hadling alphabets of corpora and OCR/HTR datasets
|
|
5
|
+
Home-page: https://github.com/anguelos/pylelemmatize
|
|
6
|
+
Author: Anguelos Nicolaou
|
|
7
|
+
Author-email: anguelos.nicolaou@gmail.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.6
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
Requires-Dist: numpy
|
|
14
|
+
Requires-Dist: unidecode
|
|
15
|
+
Requires-Dist: fargv
|
|
16
|
+
Requires-Dist: matplotlib
|
|
17
|
+
Requires-Dist: scipy
|
|
18
|
+
Requires-Dist: tqdm
|
|
19
|
+
Requires-Dist: networkx
|
|
20
|
+
Requires-Dist: lxml
|
|
21
|
+
Dynamic: author
|
|
22
|
+
Dynamic: author-email
|
|
23
|
+
Dynamic: classifier
|
|
24
|
+
Dynamic: description
|
|
25
|
+
Dynamic: description-content-type
|
|
26
|
+
Dynamic: home-page
|
|
27
|
+
Dynamic: requires-dist
|
|
28
|
+
Dynamic: requires-python
|
|
29
|
+
Dynamic: summary
|
|
30
|
+
|
|
31
|
+
# PyLeLemmatize
|
|
32
|
+
|
|
33
|
+
[](https://pypi.org/project/pylelemmatize/)
|
|
34
|
+
[](https://pypi.org/project/pylelemmatize/)
|
|
35
|
+
[](https://github.com/anguelos/pylelemmatize/actions/workflows/tests.yml)
|
|
36
|
+
[](https://pylelemmatize.readthedocs.io/en/latest/)
|
|
37
|
+
[](https://github.com/anguelos/pylelemmatize/blob/main/LICENSE)
|
|
38
|
+
|
|
39
|
+
A fast, modular lemmatization toolkit for Python.
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
PyLeLemmatize is a Python package for lemmatizing text. It provides a simple and efficient way to reduce large characters to simpler ones.
|
|
44
|
+
|
|
45
|
+
## Installation
|
|
46
|
+
|
|
47
|
+
### Install from GitHub with pip
|
|
48
|
+
|
|
49
|
+
To install PyLemmatize directly from GitHub using pip, run the following command:
|
|
50
|
+
|
|
51
|
+
```sh
|
|
52
|
+
pip install git+https://github.com/yourusername/pylelemmatize.git
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Install from GitHub with code
|
|
56
|
+
|
|
57
|
+
To install PyLemmatize from the source code, follow these steps:
|
|
58
|
+
|
|
59
|
+
1. Clone the repository:
|
|
60
|
+
2. Navigate to the project directory:
|
|
61
|
+
3. Install the package
|
|
62
|
+
|
|
63
|
+
```sh
|
|
64
|
+
git clone https://github.com/yourusername/pylelemmatize.git
|
|
65
|
+
cd pylelemmatize
|
|
66
|
+
pip install -e ./
|
|
67
|
+
# If you dont want a development install, do pip install ./
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Usage
|
|
71
|
+
|
|
72
|
+
### Command Line Invocation
|
|
73
|
+
|
|
74
|
+
#### Evaluate Merges
|
|
75
|
+
|
|
76
|
+
```sh
|
|
77
|
+
ll_evaluate_merges -h # get help string with the cli interface
|
|
78
|
+
ll_evaluate_merges -corpus_glob './sample_data/wienocist_charter_1/wienocist_charter_1*'
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Attention the merge CER is not symetric at all!
|
|
82
|
+
```
|
|
83
|
+
# The following gives a CER of 0.0591
|
|
84
|
+
ll_evaluate_merges -corpus_glob './sample_data/wienocist_charter_1/wienocist_charter_1*' -merges '[("I", "J"), ("i", "j")]'
|
|
85
|
+
# While the following gives a CER of 0.0007
|
|
86
|
+
ll_evaluate_merges -corpus_glob './sample_data/wienocist_charter_1/wienocist_charter_1*' -merges '[("J", "I"), ("j", "i")]'
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
#### Extract corpus alphabet
|
|
90
|
+
```sh
|
|
91
|
+
ll_extract_corpus_alphabet -h # get help string with the cli interface
|
|
92
|
+
ll_extract_corpus_alphabet -corpus_glob './sample_data/wienocist_charter_1/wienocist_charter_1*'
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
#### Test corpus on alphabets
|
|
96
|
+
```sh
|
|
97
|
+
ll_test_corpus_on_alphabets -h # get help string with the cli interface
|
|
98
|
+
ll_test_corpus_on_alphabets -corpus_glob './sample_data/wienocist_charter_1/wienocist_charter_1*' -alphabets 'bmp_mufi,ascii,mes1,iso8859_2' -verbose
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## Contributing
|
|
102
|
+
|
|
103
|
+
Contributions are welcome!
|
|
104
|
+
|
|
105
|
+
## License
|
|
106
|
+
|
|
107
|
+
This project is licensed under the MIT License.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# PyLeLemmatize
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/pylelemmatize/)
|
|
4
|
+
[](https://pypi.org/project/pylelemmatize/)
|
|
5
|
+
[](https://github.com/anguelos/pylelemmatize/actions/workflows/tests.yml)
|
|
6
|
+
[](https://pylelemmatize.readthedocs.io/en/latest/)
|
|
7
|
+
[](https://github.com/anguelos/pylelemmatize/blob/main/LICENSE)
|
|
8
|
+
|
|
9
|
+
A fast, modular lemmatization toolkit for Python.
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
PyLeLemmatize is a Python package for lemmatizing text. It provides a simple and efficient way to reduce large characters to simpler ones.
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
### Install from GitHub with pip
|
|
18
|
+
|
|
19
|
+
To install PyLemmatize directly from GitHub using pip, run the following command:
|
|
20
|
+
|
|
21
|
+
```sh
|
|
22
|
+
pip install git+https://github.com/yourusername/pylelemmatize.git
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Install from GitHub with code
|
|
26
|
+
|
|
27
|
+
To install PyLemmatize from the source code, follow these steps:
|
|
28
|
+
|
|
29
|
+
1. Clone the repository:
|
|
30
|
+
2. Navigate to the project directory:
|
|
31
|
+
3. Install the package
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
git clone https://github.com/yourusername/pylelemmatize.git
|
|
35
|
+
cd pylelemmatize
|
|
36
|
+
pip install -e ./
|
|
37
|
+
# If you dont want a development install, do pip install ./
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Usage
|
|
41
|
+
|
|
42
|
+
### Command Line Invocation
|
|
43
|
+
|
|
44
|
+
#### Evaluate Merges
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
ll_evaluate_merges -h # get help string with the cli interface
|
|
48
|
+
ll_evaluate_merges -corpus_glob './sample_data/wienocist_charter_1/wienocist_charter_1*'
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Attention the merge CER is not symetric at all!
|
|
52
|
+
```
|
|
53
|
+
# The following gives a CER of 0.0591
|
|
54
|
+
ll_evaluate_merges -corpus_glob './sample_data/wienocist_charter_1/wienocist_charter_1*' -merges '[("I", "J"), ("i", "j")]'
|
|
55
|
+
# While the following gives a CER of 0.0007
|
|
56
|
+
ll_evaluate_merges -corpus_glob './sample_data/wienocist_charter_1/wienocist_charter_1*' -merges '[("J", "I"), ("j", "i")]'
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
#### Extract corpus alphabet
|
|
60
|
+
```sh
|
|
61
|
+
ll_extract_corpus_alphabet -h # get help string with the cli interface
|
|
62
|
+
ll_extract_corpus_alphabet -corpus_glob './sample_data/wienocist_charter_1/wienocist_charter_1*'
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
#### Test corpus on alphabets
|
|
66
|
+
```sh
|
|
67
|
+
ll_test_corpus_on_alphabets -h # get help string with the cli interface
|
|
68
|
+
ll_test_corpus_on_alphabets -corpus_glob './sample_data/wienocist_charter_1/wienocist_charter_1*' -alphabets 'bmp_mufi,ascii,mes1,iso8859_2' -verbose
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Contributing
|
|
72
|
+
|
|
73
|
+
Contributions are welcome!
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
This project is licensed under the MIT License.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
version = open('src/pylelemmatize/version.py').read().split('=')[1].strip().strip('"')
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
setup(
|
|
7
|
+
name='pylelemmatize',
|
|
8
|
+
version=version,
|
|
9
|
+
package_dir={"": "src"},
|
|
10
|
+
packages=find_packages(where="src"),
|
|
11
|
+
install_requires=[
|
|
12
|
+
'numpy', 'unidecode', 'fargv', 'matplotlib', 'scipy', 'tqdm', 'networkx', 'lxml'
|
|
13
|
+
],
|
|
14
|
+
author='Anguelos Nicolaou',
|
|
15
|
+
author_email='anguelos.nicolaou@gmail.com',
|
|
16
|
+
description='A set utilities for hadling alphabets of corpora and OCR/HTR datasets',
|
|
17
|
+
long_description=open('README.md', encoding='utf-8').read(),
|
|
18
|
+
long_description_content_type='text/markdown',
|
|
19
|
+
url='https://github.com/anguelos/pylelemmatize',
|
|
20
|
+
classifiers=[
|
|
21
|
+
'Programming Language :: Python :: 3',
|
|
22
|
+
'License :: OSI Approved :: MIT License',
|
|
23
|
+
'Operating System :: OS Independent',
|
|
24
|
+
],
|
|
25
|
+
license_files=["LICENSE*", "LICENSES/*"],
|
|
26
|
+
python_requires='>=3.6',
|
|
27
|
+
entry_points={
|
|
28
|
+
'console_scripts': [
|
|
29
|
+
'll_remap_alphabet=pylelemmatize:main_remap_alphabet',
|
|
30
|
+
'll_render_char_similarity_tree=pylelemmatize:main_char_similarity_tree',
|
|
31
|
+
'll_infer_one2one=pylelemmatize:main_infer_one2one',
|
|
32
|
+
'll_train_one2one=pylelemmatize:main_train_one2one',
|
|
33
|
+
'll_train_one2one_report=pylelemmatize:main_report_demapper',
|
|
34
|
+
'll_extract_corpus_alphabet=pylelemmatize:main_alphabet_extract_corpus_alphabet',
|
|
35
|
+
'll_test_corpus_on_alphabets=pylelemmatize:main_map_test_corpus_on_alphabets',
|
|
36
|
+
'll_evaluate_merges=pylelemmatize:main_alphabet_evaluate_merges',
|
|
37
|
+
'll_extract_transcription_from_page_xml=pylelemmatize.util:main_extract_transcription_from_page_xml',
|
|
38
|
+
'll_many_to_more=pylelemmatize.many_to_more:many_to_more_main',
|
|
39
|
+
'll_many_to_more_evaluate=pylelemmatize.many_to_more:many_to_more_evaluate_main',
|
|
40
|
+
|
|
41
|
+
'll_create_postcorrection_tsv=pylelemmatize.htr_postcorrection:main_create_postcorrection_tsv',
|
|
42
|
+
|
|
43
|
+
'll_textline_full_cer=pylelemmatize.substitution_augmenter:main_textline_full_cer',
|
|
44
|
+
'll_postcorrection=pylelemmatize.htr_postcorrection:main_postcorrection_infer',
|
|
45
|
+
'll_postcorrection_train=pylelemmatize.htr_postcorrection:main_train_substitution_only_postcorrection',
|
|
46
|
+
],
|
|
47
|
+
},
|
|
48
|
+
)
|
|
@@ -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)})"
|