morphbpe-pl 0.3.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.
- morphbpe_pl-0.3.0/PKG-INFO +195 -0
- morphbpe_pl-0.3.0/README.md +177 -0
- morphbpe_pl-0.3.0/morphbpe/__init__.py +22 -0
- morphbpe_pl-0.3.0/morphbpe/cli.py +84 -0
- morphbpe_pl-0.3.0/morphbpe/hf_tokenizer.py +232 -0
- morphbpe_pl-0.3.0/morphbpe/morfeusz_backend.py +86 -0
- morphbpe_pl-0.3.0/morphbpe/morph_metrics.py +225 -0
- morphbpe_pl-0.3.0/morphbpe/tokenizer.py +614 -0
- morphbpe_pl-0.3.0/morphbpe_pl.egg-info/PKG-INFO +195 -0
- morphbpe_pl-0.3.0/morphbpe_pl.egg-info/SOURCES.txt +19 -0
- morphbpe_pl-0.3.0/morphbpe_pl.egg-info/dependency_links.txt +1 -0
- morphbpe_pl-0.3.0/morphbpe_pl.egg-info/entry_points.txt +2 -0
- morphbpe_pl-0.3.0/morphbpe_pl.egg-info/requires.txt +8 -0
- morphbpe_pl-0.3.0/morphbpe_pl.egg-info/top_level.txt +1 -0
- morphbpe_pl-0.3.0/pyproject.toml +31 -0
- morphbpe_pl-0.3.0/setup.cfg +4 -0
- morphbpe_pl-0.3.0/tests/test_byte_fallback.py +95 -0
- morphbpe_pl-0.3.0/tests/test_hf_tokenizer.py +71 -0
- morphbpe_pl-0.3.0/tests/test_morph_metrics.py +86 -0
- morphbpe_pl-0.3.0/tests/test_new_features.py +177 -0
- morphbpe_pl-0.3.0/tests/test_tokenizer.py +137 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: morphbpe-pl
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: MorphBPE: Morphologically-aware BPE tokenizer for Polish
|
|
5
|
+
Author: Piotr Styła
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
9
|
+
Classifier: Natural Language :: Polish
|
|
10
|
+
Requires-Python: >=3.10
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
Requires-Dist: numpy>=1.24
|
|
13
|
+
Requires-Dist: tqdm>=4.60
|
|
14
|
+
Provides-Extra: morfeusz
|
|
15
|
+
Requires-Dist: morfeusz2; extra == "morfeusz"
|
|
16
|
+
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
18
|
+
|
|
19
|
+
# MorphBPE
|
|
20
|
+
|
|
21
|
+
**Morphologically-aware BPE tokenizer for Polish.**
|
|
22
|
+
|
|
23
|
+
MorphBPE extends Byte-Pair Encoding with morphological boundary penalties.
|
|
24
|
+
Merges that cross morpheme boundaries receive a frequency penalty controlled by `alpha`,
|
|
25
|
+
preserving suffix (or prefix) structure during tokenization.
|
|
26
|
+
|
|
27
|
+
Based on: Styła (2026), *MorphBPE: morfologicznie informowany BPE dla polszczyzny*.
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install morphbpe
|
|
33
|
+
|
|
34
|
+
# With Morfeusz2 support (recommended):
|
|
35
|
+
pip install morphbpe[morfeusz]
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## Quick Start
|
|
39
|
+
|
|
40
|
+
### Python API
|
|
41
|
+
|
|
42
|
+
```python
|
|
43
|
+
from morphbpe import MorphBPETokenizer
|
|
44
|
+
|
|
45
|
+
# Train
|
|
46
|
+
tok = MorphBPETokenizer(vocab_size=32000, alpha=2.0, constraint="suffix")
|
|
47
|
+
tok.train(["list of", "training texts", ...])
|
|
48
|
+
|
|
49
|
+
# Encode / decode
|
|
50
|
+
ids = tok.encode("prezydent podpisał ustawę")
|
|
51
|
+
text = tok.decode(ids)
|
|
52
|
+
|
|
53
|
+
# Subword tokens
|
|
54
|
+
tokens = tok.encode_word("ustawodawstwo")
|
|
55
|
+
# ['ustaw', 'odaw', 'stwo'] (example)
|
|
56
|
+
|
|
57
|
+
# Save / load
|
|
58
|
+
tok.save("my-tokenizer")
|
|
59
|
+
tok = MorphBPETokenizer.load("my-tokenizer")
|
|
60
|
+
|
|
61
|
+
# Metrics
|
|
62
|
+
print(tok.fertility(test_texts))
|
|
63
|
+
print(tok.morph_edit_distance(test_texts))
|
|
64
|
+
mean, lo, hi = tok.morph_edit_distance_ci(test_texts)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### CLI
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# Train
|
|
71
|
+
morphbpe train corpus.txt --vocab-size 32000 --alpha 2.0 --constraint suffix -o my-model
|
|
72
|
+
|
|
73
|
+
# Encode
|
|
74
|
+
echo "prezydent podpisał ustawę" | morphbpe encode my-model
|
|
75
|
+
|
|
76
|
+
# Info
|
|
77
|
+
morphbpe info my-model
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Key Parameters
|
|
81
|
+
|
|
82
|
+
| Parameter | Default | Description |
|
|
83
|
+
|-----------|---------|-------------|
|
|
84
|
+
| `vocab_size` | 1000 | Target vocabulary size |
|
|
85
|
+
| `alpha` | 1.0 | Penalty strength: 0=BPE, 1=block, >1=over-penalization |
|
|
86
|
+
| `constraint` | `"suffix"` | Boundary type: `suffix`, `prefix`, or `both` |
|
|
87
|
+
|
|
88
|
+
**Recommended for Polish:** `alpha=2.0, constraint="suffix"` (see paper, §Penalty Sweep).
|
|
89
|
+
|
|
90
|
+
## How It Works
|
|
91
|
+
|
|
92
|
+
During BPE training, each candidate merge is scored by its corpus frequency.
|
|
93
|
+
MorphBPE multiplies the frequency of boundary-crossing merges by `(1 - alpha)`:
|
|
94
|
+
|
|
95
|
+
- `alpha=0.0` → no penalty → standard BPE
|
|
96
|
+
- `alpha=0.5` → 50% frequency reduction for crossing merges
|
|
97
|
+
- `alpha=1.0` → full block (frequency = 0)
|
|
98
|
+
- `alpha=2.0` → negative score → crossing merges actively deprioritized
|
|
99
|
+
|
|
100
|
+
Boundaries are detected using [Morfeusz2](http://morfeusz.sgjp.pl/) (Polish morphological analyzer)
|
|
101
|
+
or a heuristic fallback for environments without Morfeusz2.
|
|
102
|
+
|
|
103
|
+
## Results
|
|
104
|
+
|
|
105
|
+
### Intrinsic Evaluation (from paper, v=1000)
|
|
106
|
+
|
|
107
|
+
| Metric | BPE | MorphBPE α=1.0 | Δ |
|
|
108
|
+
|--------|-----|----------------|---|
|
|
109
|
+
| Fertility (v=1000) | 1.935 | 1.935 | +0.1% |
|
|
110
|
+
| MorphED (v=1000) | 1.838 | 1.712 | **−6.8%** |
|
|
111
|
+
| CE LSTM (v=1000) | 3.744 | 3.646 | **−2.6%** |
|
|
112
|
+
| CE Transformer (v=1000) | 3.525 | 3.431 | **−2.7%** |
|
|
113
|
+
|
|
114
|
+
Over-penalization (`alpha=2.0`) reduces CE by an additional 1.6–3.1%.
|
|
115
|
+
|
|
116
|
+
### Extrinsic Evaluation (vocab 8k–32k, fine-grained alpha sweep)
|
|
117
|
+
|
|
118
|
+
Corpus: Sejm speeches (8000 train / 500 test), seq_len=32, 3 epochs. See `plots/` for visualizations.
|
|
119
|
+
|
|
120
|
+
| Vocab | α | Model | CE | PPL |
|
|
121
|
+
|-------|---|-------|----|-----|
|
|
122
|
+
| 8k | 0.0 (BPE) | LSTM | 4.1073 | 60.78 |
|
|
123
|
+
| 8k | 0.0 (BPE) | Transformer | 3.9140 | 50.10 |
|
|
124
|
+
| 8k | 0.5 | LSTM | 4.0433 | 57.01 |
|
|
125
|
+
| 8k | 0.5 | Transformer | 3.8552 | 47.24 |
|
|
126
|
+
| 8k | 1.0 | LSTM | 3.9243 | 50.62 |
|
|
127
|
+
| 8k | 1.0 | Transformer | 3.7743 | 43.57 |
|
|
128
|
+
| 8k | 1.5 | LSTM | 3.8849 | 48.66 |
|
|
129
|
+
| 8k | 1.5 | Transformer | 3.7112 | 40.90 |
|
|
130
|
+
| 8k | 2.0 | LSTM | 3.7758 | 43.63 |
|
|
131
|
+
| 8k | 2.0 | Transformer | 3.6319 | 37.79 |
|
|
132
|
+
| 8k | 2.5 | LSTM | 3.7871 | 44.13 |
|
|
133
|
+
| 8k | 2.5 | Transformer | **3.6210** | **37.37** |
|
|
134
|
+
| 16k | 0.0 (BPE) | LSTM | 4.1494 | 63.40 |
|
|
135
|
+
| 16k | 0.0 (BPE) | Transformer | 3.9473 | 51.80 |
|
|
136
|
+
| 16k | 0.5 | LSTM | 4.1132 | 61.14 |
|
|
137
|
+
| 16k | 0.5 | Transformer | 3.8966 | 49.24 |
|
|
138
|
+
| 16k | 1.0 | LSTM | 3.9870 | 53.89 |
|
|
139
|
+
| 16k | 1.0 | Transformer | 3.7789 | 43.77 |
|
|
140
|
+
| 16k | 1.5 | LSTM | 3.9287 | 50.84 |
|
|
141
|
+
| 16k | 1.5 | Transformer | 3.7188 | 41.21 |
|
|
142
|
+
| 16k | 2.0 | LSTM | 3.7984 | 44.63 |
|
|
143
|
+
| 16k | 2.0 | Transformer | 3.6272 | 37.61 |
|
|
144
|
+
| 16k | 2.5 | LSTM | 3.8305 | 46.09 |
|
|
145
|
+
| 16k | 2.5 | Transformer | 3.6276 | 37.62 |
|
|
146
|
+
| 32k | 0.0 (BPE) | LSTM | 4.2140 | 67.62 |
|
|
147
|
+
| 32k | 0.0 (BPE) | Transformer | 3.9605 | 52.49 |
|
|
148
|
+
| 32k | 0.5 | LSTM | 4.1380 | 62.68 |
|
|
149
|
+
| 32k | 0.5 | Transformer | 3.9135 | 50.07 |
|
|
150
|
+
| 32k | 1.0 | LSTM | 4.0106 | 55.18 |
|
|
151
|
+
| 32k | 1.0 | Transformer | 3.7803 | 43.83 |
|
|
152
|
+
| 32k | 1.5 | LSTM | 3.9687 | 52.92 |
|
|
153
|
+
| 32k | 1.5 | Transformer | 3.7210 | 41.31 |
|
|
154
|
+
| 32k | 2.0 | LSTM | 3.8427 | 46.65 |
|
|
155
|
+
| 32k | 2.0 | Transformer | 3.6347 | 37.89 |
|
|
156
|
+
| 32k | 2.5 | LSTM | 3.8777 | 48.31 |
|
|
157
|
+
| 32k | 2.5 | Transformer | 3.6403 | 38.10 |
|
|
158
|
+
|
|
159
|
+
**Key findings:**
|
|
160
|
+
|
|
161
|
+
- **Monotonic improvement up to α=2.0**: CE decreases monotonically with α up to 2.0 for all three vocab sizes (8k, 16k, 32k) across both models
|
|
162
|
+
- **α=2.0 is the sweet spot**: α=2.5 shows diminishing returns — improvements plateau or reverse (especially at 16k and 32k), confirming α=2.0 as the optimal penalty strength
|
|
163
|
+
- **Best overall**: 8k α=2.5 Transformer (CE=3.6210, PPL=37.37) edges out 8k α=2.0 (CE=3.6319), but 16k and 32k α=2.5 are worse than α=2.0
|
|
164
|
+
- **α=1.5 sweet spot for efficiency**: 8k α=1.5 Transformer (CE=3.7112, PPL=40.90) beats 32k BPE Transformer (CE=3.9605, PPL=52.49) — 4x smaller vocab, 22% lower PPL. *Caveat: CE/PPL across different vocab sizes are not strictly comparable without bits-per-character normalization (see Methodological caveats below).*
|
|
165
|
+
- **CE reduction vs BPE**: α=0.5 → 0.9–1.8%; α=1.0 → 3.6–4.8%; α=1.5 → 5.2–6.0%; α=2.0 → 7.2–8.8%; α=2.5 → 7.2–8.1% (plateau)
|
|
166
|
+
- **Transformer > LSTM**: 3.8–6.2% CE reduction across all configs
|
|
167
|
+
- **Vocab size**: 8k and 16k perform similarly at α=2.0; 32k slightly worse due to sparse coverage on the small pilot corpus
|
|
168
|
+
|
|
169
|
+
### Cost-Benefit Analysis
|
|
170
|
+
|
|
171
|
+
Comparing MorphBPE 8k α=2.0 against a 4× larger BPE 32k tokenizer reveals a **trade-off**, not uniform dominance:
|
|
172
|
+
|
|
173
|
+
| Metric | BPE 32k | MorphBPE 8k α=2.0 | Δ | Winner |
|
|
174
|
+
|--------|---------|---------------------|---|--------|
|
|
175
|
+
| Vocab size | 32,000 | 8,000 | **4× smaller** | MorphBPE |
|
|
176
|
+
| Fertility (↓ better) | 1.020 | 1.149 | **+12.6%** | BPE 32k |
|
|
177
|
+
| MorphED (↓ better) | 0.389 | 0.456 | **+17.2%** | BPE 32k |
|
|
178
|
+
| CE Transformer (↓ better) | 3.9605 | 3.632 | **−8.3%** | MorphBPE |
|
|
179
|
+
| PPL Transformer (↓ better) | 52.49 | 37.79 | **−28.0%** | MorphBPE |
|
|
180
|
+
|
|
181
|
+
**Interpretation**: MorphBPE 8k α=2.0 wins decisively on language-model quality (CE/PPL) and vocabulary size, but at a cost — its fertility is 12.6% higher and its MorphED is 17.2% higher (worse) than BPE 32k. This is **not Pareto dominance**: the two configs trade off intrinsic segmentation quality (where the larger BPE 32k vocab wins on MorphED/fertility) against downstream LM performance and memory footprint (where MorphBPE 8k wins).
|
|
182
|
+
|
|
183
|
+
The higher MorphED and fertility for MorphBPE 8k are partly expected: it has 4× fewer tokens, so it must split words into more (and less morphologically-clean) pieces than a 32k vocab. The interesting result is that despite worse intrinsic metrics, the morphologically-informed 8k tokenizer still yields **lower LM cross-entropy** than 32k BPE.
|
|
184
|
+
|
|
185
|
+
> **Methodological caveats** (this is a small-corpus pilot — single runs, no seeds):
|
|
186
|
+
> 1. **CE/PPL are not directly comparable across vocab sizes.** Different tokenization granularity (fertility) affects per-token entropy independently of segmentation quality. A rigorous "8k beats 32k" claim requires bits-per-character, not raw CE/PPL. *(Planned: add bits-per-character.)*
|
|
187
|
+
> 2. **Small corpus (8k sentences) favors smaller vocabularies** regardless of morphology — 32k is under-trained with rare tokens on this little data, which may partly explain MorphBPE 8k's edge over BPE 32k.
|
|
188
|
+
> 3. **No repeats/seeds** — differences on the order of thousandths of CE (e.g. 8k vs 16k at α=2.0) have no estimated variance.
|
|
189
|
+
> 4. Conclusions about transfer to production-scale models are premature with an 8k-sentence corpus and seq_len=32.
|
|
190
|
+
|
|
191
|
+
See `plots/` for visualizations: `pareto_ce_vs_vocab.png`, `fertility_vs_morphed.png`, `efficiency_ratio.png`.
|
|
192
|
+
|
|
193
|
+
## License
|
|
194
|
+
|
|
195
|
+
MIT
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# MorphBPE
|
|
2
|
+
|
|
3
|
+
**Morphologically-aware BPE tokenizer for Polish.**
|
|
4
|
+
|
|
5
|
+
MorphBPE extends Byte-Pair Encoding with morphological boundary penalties.
|
|
6
|
+
Merges that cross morpheme boundaries receive a frequency penalty controlled by `alpha`,
|
|
7
|
+
preserving suffix (or prefix) structure during tokenization.
|
|
8
|
+
|
|
9
|
+
Based on: Styła (2026), *MorphBPE: morfologicznie informowany BPE dla polszczyzny*.
|
|
10
|
+
|
|
11
|
+
## Installation
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install morphbpe
|
|
15
|
+
|
|
16
|
+
# With Morfeusz2 support (recommended):
|
|
17
|
+
pip install morphbpe[morfeusz]
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quick Start
|
|
21
|
+
|
|
22
|
+
### Python API
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from morphbpe import MorphBPETokenizer
|
|
26
|
+
|
|
27
|
+
# Train
|
|
28
|
+
tok = MorphBPETokenizer(vocab_size=32000, alpha=2.0, constraint="suffix")
|
|
29
|
+
tok.train(["list of", "training texts", ...])
|
|
30
|
+
|
|
31
|
+
# Encode / decode
|
|
32
|
+
ids = tok.encode("prezydent podpisał ustawę")
|
|
33
|
+
text = tok.decode(ids)
|
|
34
|
+
|
|
35
|
+
# Subword tokens
|
|
36
|
+
tokens = tok.encode_word("ustawodawstwo")
|
|
37
|
+
# ['ustaw', 'odaw', 'stwo'] (example)
|
|
38
|
+
|
|
39
|
+
# Save / load
|
|
40
|
+
tok.save("my-tokenizer")
|
|
41
|
+
tok = MorphBPETokenizer.load("my-tokenizer")
|
|
42
|
+
|
|
43
|
+
# Metrics
|
|
44
|
+
print(tok.fertility(test_texts))
|
|
45
|
+
print(tok.morph_edit_distance(test_texts))
|
|
46
|
+
mean, lo, hi = tok.morph_edit_distance_ci(test_texts)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### CLI
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
# Train
|
|
53
|
+
morphbpe train corpus.txt --vocab-size 32000 --alpha 2.0 --constraint suffix -o my-model
|
|
54
|
+
|
|
55
|
+
# Encode
|
|
56
|
+
echo "prezydent podpisał ustawę" | morphbpe encode my-model
|
|
57
|
+
|
|
58
|
+
# Info
|
|
59
|
+
morphbpe info my-model
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Key Parameters
|
|
63
|
+
|
|
64
|
+
| Parameter | Default | Description |
|
|
65
|
+
|-----------|---------|-------------|
|
|
66
|
+
| `vocab_size` | 1000 | Target vocabulary size |
|
|
67
|
+
| `alpha` | 1.0 | Penalty strength: 0=BPE, 1=block, >1=over-penalization |
|
|
68
|
+
| `constraint` | `"suffix"` | Boundary type: `suffix`, `prefix`, or `both` |
|
|
69
|
+
|
|
70
|
+
**Recommended for Polish:** `alpha=2.0, constraint="suffix"` (see paper, §Penalty Sweep).
|
|
71
|
+
|
|
72
|
+
## How It Works
|
|
73
|
+
|
|
74
|
+
During BPE training, each candidate merge is scored by its corpus frequency.
|
|
75
|
+
MorphBPE multiplies the frequency of boundary-crossing merges by `(1 - alpha)`:
|
|
76
|
+
|
|
77
|
+
- `alpha=0.0` → no penalty → standard BPE
|
|
78
|
+
- `alpha=0.5` → 50% frequency reduction for crossing merges
|
|
79
|
+
- `alpha=1.0` → full block (frequency = 0)
|
|
80
|
+
- `alpha=2.0` → negative score → crossing merges actively deprioritized
|
|
81
|
+
|
|
82
|
+
Boundaries are detected using [Morfeusz2](http://morfeusz.sgjp.pl/) (Polish morphological analyzer)
|
|
83
|
+
or a heuristic fallback for environments without Morfeusz2.
|
|
84
|
+
|
|
85
|
+
## Results
|
|
86
|
+
|
|
87
|
+
### Intrinsic Evaluation (from paper, v=1000)
|
|
88
|
+
|
|
89
|
+
| Metric | BPE | MorphBPE α=1.0 | Δ |
|
|
90
|
+
|--------|-----|----------------|---|
|
|
91
|
+
| Fertility (v=1000) | 1.935 | 1.935 | +0.1% |
|
|
92
|
+
| MorphED (v=1000) | 1.838 | 1.712 | **−6.8%** |
|
|
93
|
+
| CE LSTM (v=1000) | 3.744 | 3.646 | **−2.6%** |
|
|
94
|
+
| CE Transformer (v=1000) | 3.525 | 3.431 | **−2.7%** |
|
|
95
|
+
|
|
96
|
+
Over-penalization (`alpha=2.0`) reduces CE by an additional 1.6–3.1%.
|
|
97
|
+
|
|
98
|
+
### Extrinsic Evaluation (vocab 8k–32k, fine-grained alpha sweep)
|
|
99
|
+
|
|
100
|
+
Corpus: Sejm speeches (8000 train / 500 test), seq_len=32, 3 epochs. See `plots/` for visualizations.
|
|
101
|
+
|
|
102
|
+
| Vocab | α | Model | CE | PPL |
|
|
103
|
+
|-------|---|-------|----|-----|
|
|
104
|
+
| 8k | 0.0 (BPE) | LSTM | 4.1073 | 60.78 |
|
|
105
|
+
| 8k | 0.0 (BPE) | Transformer | 3.9140 | 50.10 |
|
|
106
|
+
| 8k | 0.5 | LSTM | 4.0433 | 57.01 |
|
|
107
|
+
| 8k | 0.5 | Transformer | 3.8552 | 47.24 |
|
|
108
|
+
| 8k | 1.0 | LSTM | 3.9243 | 50.62 |
|
|
109
|
+
| 8k | 1.0 | Transformer | 3.7743 | 43.57 |
|
|
110
|
+
| 8k | 1.5 | LSTM | 3.8849 | 48.66 |
|
|
111
|
+
| 8k | 1.5 | Transformer | 3.7112 | 40.90 |
|
|
112
|
+
| 8k | 2.0 | LSTM | 3.7758 | 43.63 |
|
|
113
|
+
| 8k | 2.0 | Transformer | 3.6319 | 37.79 |
|
|
114
|
+
| 8k | 2.5 | LSTM | 3.7871 | 44.13 |
|
|
115
|
+
| 8k | 2.5 | Transformer | **3.6210** | **37.37** |
|
|
116
|
+
| 16k | 0.0 (BPE) | LSTM | 4.1494 | 63.40 |
|
|
117
|
+
| 16k | 0.0 (BPE) | Transformer | 3.9473 | 51.80 |
|
|
118
|
+
| 16k | 0.5 | LSTM | 4.1132 | 61.14 |
|
|
119
|
+
| 16k | 0.5 | Transformer | 3.8966 | 49.24 |
|
|
120
|
+
| 16k | 1.0 | LSTM | 3.9870 | 53.89 |
|
|
121
|
+
| 16k | 1.0 | Transformer | 3.7789 | 43.77 |
|
|
122
|
+
| 16k | 1.5 | LSTM | 3.9287 | 50.84 |
|
|
123
|
+
| 16k | 1.5 | Transformer | 3.7188 | 41.21 |
|
|
124
|
+
| 16k | 2.0 | LSTM | 3.7984 | 44.63 |
|
|
125
|
+
| 16k | 2.0 | Transformer | 3.6272 | 37.61 |
|
|
126
|
+
| 16k | 2.5 | LSTM | 3.8305 | 46.09 |
|
|
127
|
+
| 16k | 2.5 | Transformer | 3.6276 | 37.62 |
|
|
128
|
+
| 32k | 0.0 (BPE) | LSTM | 4.2140 | 67.62 |
|
|
129
|
+
| 32k | 0.0 (BPE) | Transformer | 3.9605 | 52.49 |
|
|
130
|
+
| 32k | 0.5 | LSTM | 4.1380 | 62.68 |
|
|
131
|
+
| 32k | 0.5 | Transformer | 3.9135 | 50.07 |
|
|
132
|
+
| 32k | 1.0 | LSTM | 4.0106 | 55.18 |
|
|
133
|
+
| 32k | 1.0 | Transformer | 3.7803 | 43.83 |
|
|
134
|
+
| 32k | 1.5 | LSTM | 3.9687 | 52.92 |
|
|
135
|
+
| 32k | 1.5 | Transformer | 3.7210 | 41.31 |
|
|
136
|
+
| 32k | 2.0 | LSTM | 3.8427 | 46.65 |
|
|
137
|
+
| 32k | 2.0 | Transformer | 3.6347 | 37.89 |
|
|
138
|
+
| 32k | 2.5 | LSTM | 3.8777 | 48.31 |
|
|
139
|
+
| 32k | 2.5 | Transformer | 3.6403 | 38.10 |
|
|
140
|
+
|
|
141
|
+
**Key findings:**
|
|
142
|
+
|
|
143
|
+
- **Monotonic improvement up to α=2.0**: CE decreases monotonically with α up to 2.0 for all three vocab sizes (8k, 16k, 32k) across both models
|
|
144
|
+
- **α=2.0 is the sweet spot**: α=2.5 shows diminishing returns — improvements plateau or reverse (especially at 16k and 32k), confirming α=2.0 as the optimal penalty strength
|
|
145
|
+
- **Best overall**: 8k α=2.5 Transformer (CE=3.6210, PPL=37.37) edges out 8k α=2.0 (CE=3.6319), but 16k and 32k α=2.5 are worse than α=2.0
|
|
146
|
+
- **α=1.5 sweet spot for efficiency**: 8k α=1.5 Transformer (CE=3.7112, PPL=40.90) beats 32k BPE Transformer (CE=3.9605, PPL=52.49) — 4x smaller vocab, 22% lower PPL. *Caveat: CE/PPL across different vocab sizes are not strictly comparable without bits-per-character normalization (see Methodological caveats below).*
|
|
147
|
+
- **CE reduction vs BPE**: α=0.5 → 0.9–1.8%; α=1.0 → 3.6–4.8%; α=1.5 → 5.2–6.0%; α=2.0 → 7.2–8.8%; α=2.5 → 7.2–8.1% (plateau)
|
|
148
|
+
- **Transformer > LSTM**: 3.8–6.2% CE reduction across all configs
|
|
149
|
+
- **Vocab size**: 8k and 16k perform similarly at α=2.0; 32k slightly worse due to sparse coverage on the small pilot corpus
|
|
150
|
+
|
|
151
|
+
### Cost-Benefit Analysis
|
|
152
|
+
|
|
153
|
+
Comparing MorphBPE 8k α=2.0 against a 4× larger BPE 32k tokenizer reveals a **trade-off**, not uniform dominance:
|
|
154
|
+
|
|
155
|
+
| Metric | BPE 32k | MorphBPE 8k α=2.0 | Δ | Winner |
|
|
156
|
+
|--------|---------|---------------------|---|--------|
|
|
157
|
+
| Vocab size | 32,000 | 8,000 | **4× smaller** | MorphBPE |
|
|
158
|
+
| Fertility (↓ better) | 1.020 | 1.149 | **+12.6%** | BPE 32k |
|
|
159
|
+
| MorphED (↓ better) | 0.389 | 0.456 | **+17.2%** | BPE 32k |
|
|
160
|
+
| CE Transformer (↓ better) | 3.9605 | 3.632 | **−8.3%** | MorphBPE |
|
|
161
|
+
| PPL Transformer (↓ better) | 52.49 | 37.79 | **−28.0%** | MorphBPE |
|
|
162
|
+
|
|
163
|
+
**Interpretation**: MorphBPE 8k α=2.0 wins decisively on language-model quality (CE/PPL) and vocabulary size, but at a cost — its fertility is 12.6% higher and its MorphED is 17.2% higher (worse) than BPE 32k. This is **not Pareto dominance**: the two configs trade off intrinsic segmentation quality (where the larger BPE 32k vocab wins on MorphED/fertility) against downstream LM performance and memory footprint (where MorphBPE 8k wins).
|
|
164
|
+
|
|
165
|
+
The higher MorphED and fertility for MorphBPE 8k are partly expected: it has 4× fewer tokens, so it must split words into more (and less morphologically-clean) pieces than a 32k vocab. The interesting result is that despite worse intrinsic metrics, the morphologically-informed 8k tokenizer still yields **lower LM cross-entropy** than 32k BPE.
|
|
166
|
+
|
|
167
|
+
> **Methodological caveats** (this is a small-corpus pilot — single runs, no seeds):
|
|
168
|
+
> 1. **CE/PPL are not directly comparable across vocab sizes.** Different tokenization granularity (fertility) affects per-token entropy independently of segmentation quality. A rigorous "8k beats 32k" claim requires bits-per-character, not raw CE/PPL. *(Planned: add bits-per-character.)*
|
|
169
|
+
> 2. **Small corpus (8k sentences) favors smaller vocabularies** regardless of morphology — 32k is under-trained with rare tokens on this little data, which may partly explain MorphBPE 8k's edge over BPE 32k.
|
|
170
|
+
> 3. **No repeats/seeds** — differences on the order of thousandths of CE (e.g. 8k vs 16k at α=2.0) have no estimated variance.
|
|
171
|
+
> 4. Conclusions about transfer to production-scale models are premature with an 8k-sentence corpus and seq_len=32.
|
|
172
|
+
|
|
173
|
+
See `plots/` for visualizations: `pareto_ce_vs_vocab.png`, `fertility_vs_morphed.png`, `efficiency_ratio.png`.
|
|
174
|
+
|
|
175
|
+
## License
|
|
176
|
+
|
|
177
|
+
MIT
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""MorphBPE: Morphologically-aware BPE tokenizer for Polish."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.3.0"
|
|
4
|
+
|
|
5
|
+
from morphbpe.tokenizer import (
|
|
6
|
+
MorphBPETokenizer,
|
|
7
|
+
pre_tokenize,
|
|
8
|
+
SPECIAL_TOKENS,
|
|
9
|
+
BYTE_TOKENS,
|
|
10
|
+
BYTE_TOKENS_INV,
|
|
11
|
+
)
|
|
12
|
+
from morphbpe.morph_metrics import evaluate_all, evaluate_lexeme
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"MorphBPETokenizer",
|
|
16
|
+
"pre_tokenize",
|
|
17
|
+
"SPECIAL_TOKENS",
|
|
18
|
+
"BYTE_TOKENS",
|
|
19
|
+
"BYTE_TOKENS_INV",
|
|
20
|
+
"evaluate_all",
|
|
21
|
+
"evaluate_lexeme",
|
|
22
|
+
]
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Command-line interface for MorphBPE."""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from morphbpe.tokenizer import MorphBPETokenizer
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def main():
|
|
10
|
+
parser = argparse.ArgumentParser(
|
|
11
|
+
prog="morphbpe",
|
|
12
|
+
description="MorphBPE: Morphologically-aware BPE tokenizer for Polish",
|
|
13
|
+
)
|
|
14
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
15
|
+
|
|
16
|
+
# ── train ────────────────────────────────────────────────────────
|
|
17
|
+
p_train = sub.add_parser("train", help="Train a tokenizer from a text file")
|
|
18
|
+
p_train.add_argument("corpus", help="Path to plain-text corpus (one doc per line)")
|
|
19
|
+
p_train.add_argument("-v", "--vocab-size", type=int, default=1000)
|
|
20
|
+
p_train.add_argument("-a", "--alpha", type=float, default=2.0)
|
|
21
|
+
p_train.add_argument(
|
|
22
|
+
"-c", "--constraint", choices=["suffix", "prefix", "both"], default="suffix"
|
|
23
|
+
)
|
|
24
|
+
p_train.add_argument(
|
|
25
|
+
"-o", "--output", default="morphbpe-model", help="Output directory"
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
# ── encode ───────────────────────────────────────────────────────
|
|
29
|
+
p_enc = sub.add_parser("encode", help="Encode text using a trained tokenizer")
|
|
30
|
+
p_enc.add_argument("model", help="Tokenizer directory")
|
|
31
|
+
p_enc.add_argument(
|
|
32
|
+
"text", nargs="?", default=None, help="Text to encode (or stdin)"
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
# ── info ─────────────────────────────────────────────────────────
|
|
36
|
+
p_info = sub.add_parser("info", help="Show tokenizer info")
|
|
37
|
+
p_info.add_argument("model", help="Tokenizer directory")
|
|
38
|
+
|
|
39
|
+
args = parser.parse_args()
|
|
40
|
+
|
|
41
|
+
if args.command == "train":
|
|
42
|
+
_cmd_train(args)
|
|
43
|
+
elif args.command == "encode":
|
|
44
|
+
_cmd_encode(args)
|
|
45
|
+
elif args.command == "info":
|
|
46
|
+
_cmd_info(args)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _cmd_train(args):
|
|
50
|
+
tok = MorphBPETokenizer(
|
|
51
|
+
vocab_size=args.vocab_size,
|
|
52
|
+
alpha=args.alpha,
|
|
53
|
+
constraint=args.constraint,
|
|
54
|
+
)
|
|
55
|
+
tok.train_from_file(args.corpus)
|
|
56
|
+
tok.save(args.output)
|
|
57
|
+
print(f"Saved to {args.output}/")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _cmd_encode(args):
|
|
61
|
+
tok = MorphBPETokenizer.load(args.model)
|
|
62
|
+
if args.text:
|
|
63
|
+
lines = [args.text]
|
|
64
|
+
else:
|
|
65
|
+
lines = sys.stdin.read().splitlines()
|
|
66
|
+
|
|
67
|
+
for line in lines:
|
|
68
|
+
tokens = []
|
|
69
|
+
for word in line.split():
|
|
70
|
+
tokens.extend(tok.encode_word(word))
|
|
71
|
+
print(" ".join(tokens))
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _cmd_info(args):
|
|
75
|
+
tok = MorphBPETokenizer.load(args.model)
|
|
76
|
+
print(f"Vocab size: {tok.get_vocab_size()}")
|
|
77
|
+
print(f"Merges: {len(tok.merges)}")
|
|
78
|
+
print(f"Alpha: {tok.alpha}")
|
|
79
|
+
print(f"Constraint: {tok.constraint}")
|
|
80
|
+
print(f"Morfeusz: {tok._backend.has_morfeusz}")
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if __name__ == "__main__":
|
|
84
|
+
main()
|