DeSiMaTe 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.
Files changed (38) hide show
  1. desimate-0.1.0/PKG-INFO +175 -0
  2. desimate-0.1.0/README.md +151 -0
  3. desimate-0.1.0/pyproject.toml +37 -0
  4. desimate-0.1.0/pyproject.toml.orig +36 -0
  5. desimate-0.1.0/src/desimate/__init__.py +77 -0
  6. desimate-0.1.0/src/desimate/charencoders/__init__.py +38 -0
  7. desimate-0.1.0/src/desimate/charencoders/hashing.py +70 -0
  8. desimate-0.1.0/src/desimate/charencoders/onehot.py +73 -0
  9. desimate-0.1.0/src/desimate/charencoders/ord.py +58 -0
  10. desimate-0.1.0/src/desimate/clusterizers/__init__.py +47 -0
  11. desimate-0.1.0/src/desimate/clusterizers/agglomerative.py +485 -0
  12. desimate-0.1.0/src/desimate/clusterizers/hdbscan.py +64 -0
  13. desimate-0.1.0/src/desimate/config.py +15 -0
  14. desimate-0.1.0/src/desimate/distances/__init__.py +94 -0
  15. desimate-0.1.0/src/desimate/distances/euclidean.py +63 -0
  16. desimate-0.1.0/src/desimate/distances/levenshtein.py +19 -0
  17. desimate-0.1.0/src/desimate/encoders/__init__.py +96 -0
  18. desimate-0.1.0/src/desimate/encoders/cnned/LICENSE +21 -0
  19. desimate-0.1.0/src/desimate/encoders/cnned/README.md +3 -0
  20. desimate-0.1.0/src/desimate/encoders/cnned/__init__.py +315 -0
  21. desimate-0.1.0/src/desimate/encoders/cnned/datasets.py +47 -0
  22. desimate-0.1.0/src/desimate/encoders/cnned/networks.py +120 -0
  23. desimate-0.1.0/src/desimate/encoders/datasets.py +21 -0
  24. desimate-0.1.0/src/desimate/encoders/neuroseed/LICENSE +21 -0
  25. desimate-0.1.0/src/desimate/encoders/neuroseed/README.md +3 -0
  26. desimate-0.1.0/src/desimate/encoders/neuroseed/__init__.py +281 -0
  27. desimate-0.1.0/src/desimate/encoders/neuroseed/datasets.py +128 -0
  28. desimate-0.1.0/src/desimate/encoders/neuroseed/networks.py +308 -0
  29. desimate-0.1.0/src/desimate/encoders/neuroseed/utils.py +82 -0
  30. desimate-0.1.0/src/desimate/encoders/transformers/__init__.py +24 -0
  31. desimate-0.1.0/src/desimate/main.py +864 -0
  32. desimate-0.1.0/src/desimate/parsers/__init__.py +125 -0
  33. desimate-0.1.0/src/desimate/parsers/markdown.py +283 -0
  34. desimate-0.1.0/src/desimate/parsers/text.py +47 -0
  35. desimate-0.1.0/src/desimate/utils/__init__.py +14 -0
  36. desimate-0.1.0/src/desimate/utils/console.py +23 -0
  37. desimate-0.1.0/src/desimate/utils/timer.py +16 -0
  38. desimate-0.1.0/src/desimate/utils/torchutils.py +7 -0
@@ -0,0 +1,175 @@
1
+ Metadata-Version: 2.3
2
+ Name: DeSiMaTe
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Author: Guillaume Cardoen
6
+ Author-email: Guillaume Cardoen <Guillaume.CARDOEN@umons.ac.be>
7
+ Requires-Dist: click>=8.5.0
8
+ Requires-Dist: duckdb>=1.5.6
9
+ Requires-Dist: hdbscan>=0.8.44
10
+ Requires-Dist: joblib>=1.6.0
11
+ Requires-Dist: numpy>=2.5.3
12
+ Requires-Dist: pandas>=3.0.6
13
+ Requires-Dist: pyarrow>=25.0.1
14
+ Requires-Dist: rapidfuzz>=3.14.6
15
+ Requires-Dist: rich>=15.0.0
16
+ Requires-Dist: scikit-learn>=1.9.1
17
+ Requires-Dist: scipy-stubs~=1.18.1
18
+ Requires-Dist: sentence-transformers>=6.2.0
19
+ Requires-Dist: torch>=2.14.1
20
+ Requires-Dist: unidecode>=1.4.0
21
+ Requires-Dist: wenmode>=0.15.1
22
+ Requires-Python: >=3.14
23
+ Description-Content-Type: text/markdown
24
+
25
+ # DeSiMaTe: Detecting Similar Markdown Text
26
+
27
+ [![License](https://badgen.net/pypi/license/desimate)](https://github.com/sgl-umons/desimate/blob/master/LICENSE.txt)
28
+ [![Last release](https://badgen.net/github/release/sgl-umons/desimate/stable)](https://badgen.net/github/release/sgl-umons/desimate/stable)
29
+ [![PyPI](https://badgen.net/pypi/v/desimate)](https://pypi.org/project/desimate)
30
+ [![Last commit](https://badgen.net/github/last-commit/sgl-umons/desimate)](https://github.com/sgl-umons/desimate/commits/)
31
+
32
+ An automated tool for detecting similar text in large collections of documents, written in Python.
33
+ `desimate` is designed to be used as a CLI tool.
34
+ Given a set of Markdown documents, it splits each of them into fragments (paragraphs, lists, tables, code blocks, etc.), stores them in a DuckDB database, embeds them with a learned string-embedding model, and clusters the fragments according to their similarity.
35
+
36
+ The pipeline is composed of the following steps, each available as a dedicated subcommand:
37
+
38
+ 1. `run`: run the complete pipeline on a set of documents. It is the easiest way for quick results. Alternatively, you can use subcommands to control each step of the pipeline:
39
+ 2. `parse`: parse the given documents into fragments at a configurable granularity, and store them in a DuckDB database.
40
+ 3. `create-training-dataset`: sample values from the database into a JSON Lines file, to be used for training.
41
+ 4. `train`: train a string-embedding model (a character-level convolutional network such as NeuroSEED or CNN-ED) such that the Euclidean distance between two embeddings approximates the Levenshtein distance between the embedded strings.
42
+ 5. `embed`: embed the fragments with the trained model (syntactic embedding), or with a given sentence transformer (semantic embedding), and store the results in the database.
43
+ 6. `cluster`: group the fragments into clusters of near-duplicates, either directly with the Levenshtein distance (small datasets) or with a grid-based agglomerative clustering that uses the embeddings as a cheap first filter and the Levenshtein distance to refine the results (large datasets).
44
+
45
+ This project is developed by Guillaume Cardoen at the Software Engineering Lab of the University of Mons (Belgium).
46
+
47
+ ## Installation
48
+
49
+ `desimate` requires a Python 3.14 or newer interpreter. The easiest way to install `desimate` is to install it from PyPI
50
+ ```
51
+ pip install desimate
52
+ ```
53
+
54
+ Another easy way to install `desimate` is via `pip` from this GitHub repository
55
+ ```
56
+ pip install git+https://github.com/sgl-umons/desimate
57
+ ```
58
+
59
+ Alternatively, you can clone this repository and install it locally
60
+ ```
61
+ git clone https://github.com/sgl-umons/desimate
62
+ cd desimate
63
+ pip install . # or uv sync and uv run
64
+ ```
65
+
66
+ You may wish to use `desimate` in a virtual environment
67
+ ```
68
+ python -m venv venv
69
+ source venv/bin/activate
70
+ pip install desimate
71
+ ```
72
+
73
+ ## Usage
74
+
75
+ After installation, the `desimate` command-line tool should be available in your shell. All commands accept a `--config` option (defaulting to `./config.toml`) pointing to a TOML file whose content is used as default for the command options:
76
+
77
+ ```
78
+ Usage: desimate [OPTIONS] COMMAND [ARGS]...
79
+
80
+ Options:
81
+ --config PATH The path to a TOML configuration file.
82
+ --help Show this message and exit.
83
+
84
+ Commands:
85
+ cluster Cluster the values stored in the database.
86
+ create-training-dataset Sample values from the database, and write
87
+ them in a JSON Lines file.
88
+ embed Embed the values stored in the database (or
89
+ a given input file) with a model.
90
+ parse Parse the given inputs, and store the
91
+ extracted fragments in the database.
92
+ process Run the full pipeline (parse, embed and
93
+ cluster) in a single command.
94
+ train Train a string-embedding model.
95
+ ```
96
+
97
+ The easiest way to use desimate is through the `process` subcommand.
98
+ ```
99
+ Usage: desimate process [OPTIONS] [INPUTS]...
100
+
101
+ Run the full pipeline in a single command: parse the given inputs (if any is
102
+ given), embed the values with the given model, and cluster the values of the
103
+ database. The embedding step is only run if `--model` is given, and the
104
+ clustering step is only run if `--distance` is given.
105
+
106
+ Positional arguments:
107
+ [INPUTS]... The files or directories (given one or multiple times;
108
+ directories are traversed recursively) to parse.
109
+
110
+ Options:
111
+ -i, --input-list FILE A file containing a list of additional inputs
112
+ (one path per line) to add to the inputs.
113
+ -p, --parser [markdown|text] The parser to use at which the fragments are
114
+ extracted.
115
+ -g, --granularity TEXT The granularity at which the fragments are
116
+ extracted.
117
+ -m, --model FILE The trained model used to embed the values.
118
+ -f, --focus-on TEXT Only keep the fragments of the given type (can
119
+ be given multiple times); every other fragment
120
+ is removed from the database. (Everything is
121
+ kept is not given.
122
+ -o, --output FILE The path where the cluster labels will be
123
+ saved as a NumPy array.
124
+ -d, --distance INTEGER The maximum distance for the clustering. When
125
+ given, the clustering step is run.
126
+ --syntactic Use the syntactic embedding, i.e., a trained
127
+ model (default).
128
+ --semantic Use the semantic embedding, i.e., a sentence
129
+ transformer.
130
+ --batch-size INTEGER The batch size used for the inference
131
+ (default: 1024).
132
+ --db, --database FILE The DuckDB database used throughout the
133
+ pipeline. [required]
134
+ --help Show this message and exit.
135
+ ```
136
+
137
+ For Markdown, each granularity is a list of Markdown AST node types at which the traversal stops, i.e., the fragment types that will be extracted. The default configuration defines two granularities:
138
+ - `paragraph`: fragments are paragraphs, lists and tables (default);
139
+ - `md-line`: fragments are the smallest line-level elements (text, list items, table cells and table rows).
140
+
141
+ Other granularities (`line` and `file`) are available through the `text` parser, which read the file as pure text.
142
+
143
+
144
+
145
+ ### Examples
146
+
147
+ As an example, the following commands will parse every Markdown file of the `all_content` directory into the `my_dataset.db` database, keeping only the `paragraph`, `table`, `list` and `code` fragments.
148
+
149
+ The fragments can be parsed, embedded and clustered, with a maximum distance of 15, in a single command:
150
+
151
+ ```bash
152
+ desimate process all_content --db my_dataset.db --model model.torch -d 15 -o clusters.parquet -f paragraph -f table -f list -f code
153
+ ```
154
+
155
+ Alternatively, you can use subcommands to do each step at a time.
156
+
157
+ ```bash
158
+ desimate parse all_content --db my_dataset.db -f paragraph -f table -f list -f code
159
+ ```
160
+
161
+ A training set can then be sampled from the database:
162
+
163
+ ```bash
164
+ desimate create-training-dataset training.jsonl --count 25000 --db my_dataset.db
165
+ ```
166
+
167
+ An embedding model can be trained on this dataset:
168
+
169
+ ```bash
170
+ desimate train nscnn --input training.jsonl --output model.torch --n-training 20000 --n-query 5000
171
+ ```
172
+
173
+ ## License
174
+
175
+ Distributed under [GNU Lesser General Public License v3](https://github.com/sgl-umons/desimate/blob/master/LICENSE.txt).
@@ -0,0 +1,151 @@
1
+ # DeSiMaTe: Detecting Similar Markdown Text
2
+
3
+ [![License](https://badgen.net/pypi/license/desimate)](https://github.com/sgl-umons/desimate/blob/master/LICENSE.txt)
4
+ [![Last release](https://badgen.net/github/release/sgl-umons/desimate/stable)](https://badgen.net/github/release/sgl-umons/desimate/stable)
5
+ [![PyPI](https://badgen.net/pypi/v/desimate)](https://pypi.org/project/desimate)
6
+ [![Last commit](https://badgen.net/github/last-commit/sgl-umons/desimate)](https://github.com/sgl-umons/desimate/commits/)
7
+
8
+ An automated tool for detecting similar text in large collections of documents, written in Python.
9
+ `desimate` is designed to be used as a CLI tool.
10
+ Given a set of Markdown documents, it splits each of them into fragments (paragraphs, lists, tables, code blocks, etc.), stores them in a DuckDB database, embeds them with a learned string-embedding model, and clusters the fragments according to their similarity.
11
+
12
+ The pipeline is composed of the following steps, each available as a dedicated subcommand:
13
+
14
+ 1. `run`: run the complete pipeline on a set of documents. It is the easiest way for quick results. Alternatively, you can use subcommands to control each step of the pipeline:
15
+ 2. `parse`: parse the given documents into fragments at a configurable granularity, and store them in a DuckDB database.
16
+ 3. `create-training-dataset`: sample values from the database into a JSON Lines file, to be used for training.
17
+ 4. `train`: train a string-embedding model (a character-level convolutional network such as NeuroSEED or CNN-ED) such that the Euclidean distance between two embeddings approximates the Levenshtein distance between the embedded strings.
18
+ 5. `embed`: embed the fragments with the trained model (syntactic embedding), or with a given sentence transformer (semantic embedding), and store the results in the database.
19
+ 6. `cluster`: group the fragments into clusters of near-duplicates, either directly with the Levenshtein distance (small datasets) or with a grid-based agglomerative clustering that uses the embeddings as a cheap first filter and the Levenshtein distance to refine the results (large datasets).
20
+
21
+ This project is developed by Guillaume Cardoen at the Software Engineering Lab of the University of Mons (Belgium).
22
+
23
+ ## Installation
24
+
25
+ `desimate` requires a Python 3.14 or newer interpreter. The easiest way to install `desimate` is to install it from PyPI
26
+ ```
27
+ pip install desimate
28
+ ```
29
+
30
+ Another easy way to install `desimate` is via `pip` from this GitHub repository
31
+ ```
32
+ pip install git+https://github.com/sgl-umons/desimate
33
+ ```
34
+
35
+ Alternatively, you can clone this repository and install it locally
36
+ ```
37
+ git clone https://github.com/sgl-umons/desimate
38
+ cd desimate
39
+ pip install . # or uv sync and uv run
40
+ ```
41
+
42
+ You may wish to use `desimate` in a virtual environment
43
+ ```
44
+ python -m venv venv
45
+ source venv/bin/activate
46
+ pip install desimate
47
+ ```
48
+
49
+ ## Usage
50
+
51
+ After installation, the `desimate` command-line tool should be available in your shell. All commands accept a `--config` option (defaulting to `./config.toml`) pointing to a TOML file whose content is used as default for the command options:
52
+
53
+ ```
54
+ Usage: desimate [OPTIONS] COMMAND [ARGS]...
55
+
56
+ Options:
57
+ --config PATH The path to a TOML configuration file.
58
+ --help Show this message and exit.
59
+
60
+ Commands:
61
+ cluster Cluster the values stored in the database.
62
+ create-training-dataset Sample values from the database, and write
63
+ them in a JSON Lines file.
64
+ embed Embed the values stored in the database (or
65
+ a given input file) with a model.
66
+ parse Parse the given inputs, and store the
67
+ extracted fragments in the database.
68
+ process Run the full pipeline (parse, embed and
69
+ cluster) in a single command.
70
+ train Train a string-embedding model.
71
+ ```
72
+
73
+ The easiest way to use desimate is through the `process` subcommand.
74
+ ```
75
+ Usage: desimate process [OPTIONS] [INPUTS]...
76
+
77
+ Run the full pipeline in a single command: parse the given inputs (if any is
78
+ given), embed the values with the given model, and cluster the values of the
79
+ database. The embedding step is only run if `--model` is given, and the
80
+ clustering step is only run if `--distance` is given.
81
+
82
+ Positional arguments:
83
+ [INPUTS]... The files or directories (given one or multiple times;
84
+ directories are traversed recursively) to parse.
85
+
86
+ Options:
87
+ -i, --input-list FILE A file containing a list of additional inputs
88
+ (one path per line) to add to the inputs.
89
+ -p, --parser [markdown|text] The parser to use at which the fragments are
90
+ extracted.
91
+ -g, --granularity TEXT The granularity at which the fragments are
92
+ extracted.
93
+ -m, --model FILE The trained model used to embed the values.
94
+ -f, --focus-on TEXT Only keep the fragments of the given type (can
95
+ be given multiple times); every other fragment
96
+ is removed from the database. (Everything is
97
+ kept is not given.
98
+ -o, --output FILE The path where the cluster labels will be
99
+ saved as a NumPy array.
100
+ -d, --distance INTEGER The maximum distance for the clustering. When
101
+ given, the clustering step is run.
102
+ --syntactic Use the syntactic embedding, i.e., a trained
103
+ model (default).
104
+ --semantic Use the semantic embedding, i.e., a sentence
105
+ transformer.
106
+ --batch-size INTEGER The batch size used for the inference
107
+ (default: 1024).
108
+ --db, --database FILE The DuckDB database used throughout the
109
+ pipeline. [required]
110
+ --help Show this message and exit.
111
+ ```
112
+
113
+ For Markdown, each granularity is a list of Markdown AST node types at which the traversal stops, i.e., the fragment types that will be extracted. The default configuration defines two granularities:
114
+ - `paragraph`: fragments are paragraphs, lists and tables (default);
115
+ - `md-line`: fragments are the smallest line-level elements (text, list items, table cells and table rows).
116
+
117
+ Other granularities (`line` and `file`) are available through the `text` parser, which read the file as pure text.
118
+
119
+
120
+
121
+ ### Examples
122
+
123
+ As an example, the following commands will parse every Markdown file of the `all_content` directory into the `my_dataset.db` database, keeping only the `paragraph`, `table`, `list` and `code` fragments.
124
+
125
+ The fragments can be parsed, embedded and clustered, with a maximum distance of 15, in a single command:
126
+
127
+ ```bash
128
+ desimate process all_content --db my_dataset.db --model model.torch -d 15 -o clusters.parquet -f paragraph -f table -f list -f code
129
+ ```
130
+
131
+ Alternatively, you can use subcommands to do each step at a time.
132
+
133
+ ```bash
134
+ desimate parse all_content --db my_dataset.db -f paragraph -f table -f list -f code
135
+ ```
136
+
137
+ A training set can then be sampled from the database:
138
+
139
+ ```bash
140
+ desimate create-training-dataset training.jsonl --count 25000 --db my_dataset.db
141
+ ```
142
+
143
+ An embedding model can be trained on this dataset:
144
+
145
+ ```bash
146
+ desimate train nscnn --input training.jsonl --output model.torch --n-training 20000 --n-query 5000
147
+ ```
148
+
149
+ ## License
150
+
151
+ Distributed under [GNU Lesser General Public License v3](https://github.com/sgl-umons/desimate/blob/master/LICENSE.txt).
@@ -0,0 +1,37 @@
1
+ [project]
2
+ name = "DeSiMaTe"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ requires-python = ">=3.14"
7
+ dependencies = [
8
+ "click>=8.5.0",
9
+ "duckdb>=1.5.6",
10
+ "hdbscan>=0.8.44",
11
+ "joblib>=1.6.0",
12
+ "numpy>=2.5.3",
13
+ "pandas>=3.0.6",
14
+ "pyarrow>=25.0.1",
15
+ "rapidfuzz>=3.14.6",
16
+ "rich>=15.0.0",
17
+ "scikit-learn>=1.9.1",
18
+ "scipy-stubs~=1.18.1",
19
+ "sentence-transformers>=6.2.0",
20
+ "torch>=2.14.1",
21
+ "unidecode>=1.4.0",
22
+ "wenmode>=0.15.1",
23
+ ]
24
+
25
+ [[project.authors]]
26
+ name = "Guillaume Cardoen"
27
+ email = "Guillaume.CARDOEN@umons.ac.be"
28
+
29
+ [project.scripts]
30
+ desimate = "desimate:main.cli"
31
+
32
+ [build-system]
33
+ requires = ["uv_build>=0.12.19,<0.13.0"]
34
+ build-backend = "uv_build"
35
+
36
+ [dependency-groups]
37
+ dev = []
@@ -0,0 +1,36 @@
1
+ [project]
2
+ name = "DeSiMaTe"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "Guillaume Cardoen", email = "Guillaume.CARDOEN@umons.ac.be" }
8
+ ]
9
+ requires-python = ">=3.14"
10
+ dependencies = [
11
+ "click>=8.5.0",
12
+ "duckdb>=1.5.6",
13
+ "hdbscan>=0.8.44",
14
+ "joblib>=1.6.0",
15
+ "numpy>=2.5.3",
16
+ "pandas>=3.0.6",
17
+ "pyarrow>=25.0.1",
18
+ "rapidfuzz>=3.14.6",
19
+ "rich>=15.0.0",
20
+ "scikit-learn>=1.9.1",
21
+ "scipy-stubs~=1.18.1",
22
+ "sentence-transformers>=6.2.0",
23
+ "torch>=2.14.1",
24
+ "unidecode>=1.4.0",
25
+ "wenmode>=0.15.1",
26
+ ]
27
+
28
+ [project.scripts]
29
+ desimate = "desimate:main.cli"
30
+
31
+ [build-system]
32
+ requires = ["uv_build>=0.12.19,<0.13.0"]
33
+ build-backend = "uv_build"
34
+
35
+ [dependency-groups]
36
+ dev = []
@@ -0,0 +1,77 @@
1
+ from abc import ABC, abstractmethod
2
+
3
+ from rich.console import Group
4
+ from rich.live import Live
5
+
6
+ from desimate.config import config
7
+ from desimate.utils.console import TaskSpeed, get_progress
8
+
9
+
10
+ class Step(ABC):
11
+ """
12
+ Generic step class representing a step in the detection process.
13
+ """
14
+
15
+ def __init__(self) -> None:
16
+ pass
17
+
18
+ def enter(self) -> None:
19
+ pass
20
+
21
+ def execute(self, *args, **kwargs):
22
+ self.enter()
23
+ r = self.run(*args, **kwargs)
24
+ self.exit()
25
+ return r
26
+
27
+ @abstractmethod
28
+ def run(self, *args, **kwargs) -> None:
29
+ """
30
+ Create and add every needed table to the database.
31
+ Run the step, and flush every table.
32
+ """
33
+
34
+ def exit(self) -> None:
35
+ pass
36
+
37
+ def __call__(self, *args, **kwargs):
38
+ return self.execute(*args, **kwargs)
39
+
40
+ def get_config(self):
41
+ return config
42
+
43
+
44
+ class ProgressStep(Step, ABC):
45
+ def __init__(self, show_progress: bool = True):
46
+ super().__init__()
47
+ self.show_progress = show_progress
48
+ self.progress = get_progress()
49
+
50
+ def enter(self) -> None:
51
+ super().enter()
52
+ if self.show_progress:
53
+ self.progress.start()
54
+
55
+ def exit(self) -> None:
56
+ super().exit()
57
+ if self.show_progress:
58
+ self.progress.stop()
59
+
60
+
61
+ class StatusProgressStep(ProgressStep, ABC):
62
+ def __init__(self, show_progress: bool = True):
63
+ super().__init__(show_progress)
64
+ self.live = Live(Group(self.progress), refresh_per_second=10)
65
+
66
+ def enter(self) -> None:
67
+ # super().enter()
68
+ if self.show_progress:
69
+ self.live.start()
70
+
71
+ def update_status(self, status: str):
72
+ self.live.update(Group(status, self.progress))
73
+
74
+ def exit(self) -> None:
75
+ # super().exit()
76
+ if self.show_progress:
77
+ self.live.stop()
@@ -0,0 +1,38 @@
1
+ from abc import abstractmethod
2
+
3
+ from desimate import Step
4
+ import numpy as np
5
+
6
+
7
+ class CharacterEncoder(Step):
8
+ """
9
+ This class represents the logic for transforming a single character
10
+ into a vector representing this character.
11
+ Possible representation might include onehot encoding, UTF-8 padded to
12
+ 4-bytes, etc.
13
+
14
+ Before using this encoder, you should first call `train` with your data.
15
+ You can then use `run` to encode your string or list of character.
16
+ """
17
+
18
+ def __init__(self, max_length: int | None = None):
19
+ super().__init__()
20
+ self.max_length = max_length
21
+
22
+ @abstractmethod
23
+ def train(self, texts: list[str]):
24
+ pass
25
+
26
+ @abstractmethod
27
+ def run(self, text: str | list[str]) -> np.ndarray:
28
+ """
29
+ Vectorize a str (or list of characters) towards a representative
30
+ vectorial embedding.
31
+
32
+ Returns a 2D array where the embeddings are in the same order of the input.
33
+ """
34
+ pass
35
+
36
+ @abstractmethod
37
+ def __len__(self):
38
+ raise NotImplementedError()
@@ -0,0 +1,70 @@
1
+ from functools import lru_cache
2
+
3
+ import numpy as np
4
+ from sklearn.feature_extraction import FeatureHasher
5
+ from hashlib import sha256
6
+
7
+ from desimate.charencoders import CharacterEncoder
8
+
9
+
10
+ class HashingCE(CharacterEncoder):
11
+ """
12
+ Encode a character using the hashing trick.
13
+ """
14
+
15
+ def __init__(self, max_length: int | None = None, length: int | None = None):
16
+ super().__init__(max_length)
17
+ self.config = self.get_config()
18
+ self.length = length or self.config.get("length") or 128
19
+ if not length:
20
+ raise ValueError("No length was given...")
21
+
22
+ def train(self, texts: list[str]):
23
+ if self.max_length is None:
24
+ self.max_length = max((len(_t) for _t in texts))
25
+
26
+ def run(self, text: str | list[str]) -> np.ndarray:
27
+ feature_hasher = FeatureHasher(n_features=self.length, input_type="string")
28
+ encode = np.zeros((self.max_length, self.length), dtype=np.float32)
29
+ text_length = min(len(text), self.max_length)
30
+ encode[:text_length] = feature_hasher.transform(
31
+ [[_c] for _c in text]
32
+ ).toarray()[:text_length]
33
+ return encode
34
+
35
+ def __len__(self):
36
+ return self.length
37
+
38
+
39
+ class HashingCE2(CharacterEncoder):
40
+ """
41
+ Encode a character using the hashing trick.
42
+ Unlike `HashingCE`, it does not rely on `FeatureHasher`,
43
+ but on a BLOOM filter.
44
+ """
45
+
46
+ def __init__(self, max_length: int | None = None, length: int = 64):
47
+ super().__init__(max_length)
48
+ self.length = length
49
+
50
+ def train(self, texts: list[str]):
51
+ if self.max_length is None:
52
+ self.max_length = max((len(_t) for _t in texts))
53
+
54
+ @lru_cache(maxsize=128)
55
+ def _hash(self, c):
56
+ digest = sha256(c.encode("utf-8")).digest()
57
+ h1 = int.from_bytes(digest[:8], "little") % self.length
58
+ h2 = int.from_bytes(digest[8:16], "little") % self.length
59
+ h3 = int.from_bytes(digest[16:24], "little") % self.length
60
+ return h1, h2, h3
61
+
62
+ def run(self, text: str | list[str]) -> np.ndarray:
63
+ encode = np.zeros((self.max_length, self.length), dtype=np.float32)
64
+ for _i, _c in enumerate(text):
65
+ _h1, _h2, _h3 = self._hash(_c)
66
+ encode[_i, [_h1, _h2, _h3]] = 1
67
+ return encode
68
+
69
+ def __len__(self):
70
+ return self.length
@@ -0,0 +1,73 @@
1
+ import logging
2
+
3
+ import numpy as np
4
+ from unidecode import unidecode
5
+
6
+ from desimate.charencoders import CharacterEncoder
7
+
8
+ logger = logging.getLogger(__name__)
9
+
10
+
11
+ class OneHotCE(CharacterEncoder):
12
+ """
13
+ Encode a character using a OneHot representation.
14
+ An alphabet is locally maintained.
15
+ You should call `train` first to construct this alphabet.
16
+ In case a character in the text is not in the alphabet, it is
17
+ silently discarded.
18
+ """
19
+
20
+ def __init__(self, max_length: int | None = None):
21
+ super().__init__(max_length)
22
+ self.alphabet = {}
23
+
24
+ def train(self, texts: list[str]):
25
+ characters = set()
26
+ max_length = 0
27
+ for text in texts:
28
+ max_length = max(max_length, len(text))
29
+ characters.update(text)
30
+ if self.max_length is None:
31
+ self.max_length = max_length
32
+ self.alphabet = {_c: _i for _i, _c in enumerate(characters)}
33
+ logger.info(
34
+ "Total of %d characters found, forming the OneHot alphabet.",
35
+ len(self.alphabet),
36
+ )
37
+
38
+ def text_to_signal(self, text: list[str]):
39
+ return [self.alphabet[_c] for _c in text if _c in self.alphabet]
40
+
41
+ def run(self, text: str | list[str]) -> np.ndarray:
42
+ _text = text[: self.max_length]
43
+ if isinstance(_text, str):
44
+ _text = list(_text)
45
+ sig = self.text_to_signal(_text)
46
+
47
+ encode = np.zeros((self.max_length, len(self.alphabet)), dtype=np.float32)
48
+ encode[np.arange(len(sig), dtype=int), np.array(sig, dtype=int)] = 1.0
49
+ return encode
50
+
51
+ def __len__(self):
52
+ return len(self.alphabet)
53
+
54
+
55
+ class AsciiOneHotCE(OneHotCE):
56
+ """
57
+ Encode a character using a OneHot representation.
58
+ To simplify the alphabet, first transform the text towards
59
+ ASCII only character using `unidecode`.
60
+
61
+ """
62
+
63
+ def __init__(self, max_length: int | None = None):
64
+ super().__init__(max_length)
65
+
66
+ def _apply(self, c):
67
+ return unidecode(c).lower()
68
+
69
+ def train(self, texts: list[str]):
70
+ super().train([self._apply(_t) for _t in texts])
71
+
72
+ def run(self, text: str | list[str]) -> np.ndarray:
73
+ return super().run([self._apply(_t) for _t in text])