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.
- desimate-0.1.0/PKG-INFO +175 -0
- desimate-0.1.0/README.md +151 -0
- desimate-0.1.0/pyproject.toml +37 -0
- desimate-0.1.0/pyproject.toml.orig +36 -0
- desimate-0.1.0/src/desimate/__init__.py +77 -0
- desimate-0.1.0/src/desimate/charencoders/__init__.py +38 -0
- desimate-0.1.0/src/desimate/charencoders/hashing.py +70 -0
- desimate-0.1.0/src/desimate/charencoders/onehot.py +73 -0
- desimate-0.1.0/src/desimate/charencoders/ord.py +58 -0
- desimate-0.1.0/src/desimate/clusterizers/__init__.py +47 -0
- desimate-0.1.0/src/desimate/clusterizers/agglomerative.py +485 -0
- desimate-0.1.0/src/desimate/clusterizers/hdbscan.py +64 -0
- desimate-0.1.0/src/desimate/config.py +15 -0
- desimate-0.1.0/src/desimate/distances/__init__.py +94 -0
- desimate-0.1.0/src/desimate/distances/euclidean.py +63 -0
- desimate-0.1.0/src/desimate/distances/levenshtein.py +19 -0
- desimate-0.1.0/src/desimate/encoders/__init__.py +96 -0
- desimate-0.1.0/src/desimate/encoders/cnned/LICENSE +21 -0
- desimate-0.1.0/src/desimate/encoders/cnned/README.md +3 -0
- desimate-0.1.0/src/desimate/encoders/cnned/__init__.py +315 -0
- desimate-0.1.0/src/desimate/encoders/cnned/datasets.py +47 -0
- desimate-0.1.0/src/desimate/encoders/cnned/networks.py +120 -0
- desimate-0.1.0/src/desimate/encoders/datasets.py +21 -0
- desimate-0.1.0/src/desimate/encoders/neuroseed/LICENSE +21 -0
- desimate-0.1.0/src/desimate/encoders/neuroseed/README.md +3 -0
- desimate-0.1.0/src/desimate/encoders/neuroseed/__init__.py +281 -0
- desimate-0.1.0/src/desimate/encoders/neuroseed/datasets.py +128 -0
- desimate-0.1.0/src/desimate/encoders/neuroseed/networks.py +308 -0
- desimate-0.1.0/src/desimate/encoders/neuroseed/utils.py +82 -0
- desimate-0.1.0/src/desimate/encoders/transformers/__init__.py +24 -0
- desimate-0.1.0/src/desimate/main.py +864 -0
- desimate-0.1.0/src/desimate/parsers/__init__.py +125 -0
- desimate-0.1.0/src/desimate/parsers/markdown.py +283 -0
- desimate-0.1.0/src/desimate/parsers/text.py +47 -0
- desimate-0.1.0/src/desimate/utils/__init__.py +14 -0
- desimate-0.1.0/src/desimate/utils/console.py +23 -0
- desimate-0.1.0/src/desimate/utils/timer.py +16 -0
- desimate-0.1.0/src/desimate/utils/torchutils.py +7 -0
desimate-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+
[](https://github.com/sgl-umons/desimate/blob/master/LICENSE.txt)
|
|
28
|
+
[](https://badgen.net/github/release/sgl-umons/desimate/stable)
|
|
29
|
+
[](https://pypi.org/project/desimate)
|
|
30
|
+
[](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).
|
desimate-0.1.0/README.md
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# DeSiMaTe: Detecting Similar Markdown Text
|
|
2
|
+
|
|
3
|
+
[](https://github.com/sgl-umons/desimate/blob/master/LICENSE.txt)
|
|
4
|
+
[](https://badgen.net/github/release/sgl-umons/desimate/stable)
|
|
5
|
+
[](https://pypi.org/project/desimate)
|
|
6
|
+
[](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])
|