KazStem
KazStem is an ambiguity-preserving morphological analyzer and generator for
Kazakh. Its primary command is kazstem; the compatibility commands
qazmorph and mystem-kz, and the existing Python import package qazmorph,
remain available. KazStem exposes a MyStem-like command-line interface and a
typed Python API, but its implementation is independent of MyStem.
Features
- lossless Unicode tokenization with exact character offsets and reconstruction;
- complete finite-state analysis lattices for known Kazakh words;
- formally bounded productive noun, adjective, and verb hypotheses for OOV words;
- lemma, UPOS, UD features, ordered raw tags, morphemes, provenance, and scores;
- fast candidate-constrained Constraint Grammar disambiguation that can abstain;
- optional Stanza-based neural ranking that cannot invent analyses outside the FST;
- dictionary generation from exact
(lemma, tags)analyses; - MyStem-style positional files and
-n/-c/-w/-l/-i/-g/-s/-e/-doptions; - text, MyStem-shaped JSON/XML, lossless JSONL v2, and CoNLL-U output;
- JSONL/TSV fixlists, grammar filtering, Unicode normalization, and bounded OOV caching;
- reproducible, content-addressed resources with formal graph, provenance, and post-run integrity verification;
- versioned evaluation and performance reports with explicit coverage, abstention, alignment, and completeness accounting.
The production core is hybrid:
flowchart LR
T["Kazakh text"] --> E["lossless stream escaping"]
E --> F["bidirectional finite-state morphology"]
F --> L["legal analysis lattice"]
U["open-class FST"] -->|"only for OOV tokens"| L
L --> C["Constraint Grammar fast mode"]
L --> N["candidate-constrained neural mode"]
L --> O["text / JSONL / XML / CoNLL-U"]
C --> O
N --> O
- The finite-state lexicon and two-level morphophonology come from the
independently licensed
apertium-kazproject at a pinned commit. - Known words retain every distinct raw/morpheme reading, even when several readings collapse to the same lossy UD projection.
- Version 0.2 makes the consuming surface partition strictly atomic: every
whitespace run is a separate gap token and no lexical token contains
Unicode whitespace. Dictionary expressions such as
жоқ қойremain fully available as immutable, overlappingAnalysisSpanlattice records, so fixing tokenization never discards their original raw candidates. - An isolated productive FST proposes noun, adjective, and verb analyses for
unseen NFC Kazakh-Cyrillic stems of at most 32 code points, excluding
three-or-more-character elongation/noise runs. The unknown lemma must
identity-copy a nonempty surface prefix before entering one of the upstream
N1, A1, V-TV, or V-IV continuations. The only nonidentity root paths are one
explicitly bounded stem-final п→б, к→г, or қ→ғ voicing pair after a nonempty
identity prefix. A formal build gate rejects every reachable input-epsilon
cycle. Output is capped, ranked, and cached; it never
adds speculative readings to dictionary words. Non-Kazakh/overlong OOVs
deterministically receive an explicit unknown analysis. A timed-out, failed,
or cyclically truncated lookup discards its incomplete output, warns once for
that cached OOV, and likewise falls back to explicit unknown. That negative
result is cached only for the lifetime of its
Analyzer; a new instance retries the lookup. - Fast contextual mode applies Kazakh Constraint Grammar and retains ambiguity when the grammar cannot justify a unique answer.
- Optional neural mode uses a Kazakh contextual model only to rank candidates
licensed by the FST. CLI
--format jsonlemits versioned consuming token records and non-consuming analysis-span records with every visible candidate and selected index;-cis still required to include nonlexical token rows. - The same grammar generates surface forms from an exact lexical analysis.
Correctness contract
No analyzer can truthfully guarantee 100% correctness for every possible Kazakh sentence: real homonymy, annotation disagreement, code-switching, names, typos, and new vocabulary make that claim ill-defined. This project uses three testable guarantees instead:
- All enabled tests in the versioned regression suite pass; any intentionally skipped contract is named as an unsupported limitation in the results.
- Every compiled generator pair is accepted by the analyzer, enforced by the
formal build gate
generation - analysis = ∅. Corpus diagnostics, genuinely held-out evaluations, and performance are reported separately. - The productive analyzer is finite-valued for every finite surface: the compiled graph has no reachable input-epsilon cycle, and immutable no-cap probes must complete without HFST's cycle marker while retaining all four configured open-class continuations.
Unweighted FST readings have score: null. KazStem does not invent
probabilities from lexicon order. OOV and neural scores are normalized only
within their returned candidate lattice.
Reproducible reference build
The repository contains source code rather than compiled Kazakh FSTs or neural weights. The audited bootstrap currently targets Ubuntu 24.04 on x86-64 and installs an unprivileged, version-locked HFST/CG toolchain under the checkout.
git clone https://github.com/amir-nuriyev/KazStem.git
cd KazStem
bash scripts/bootstrap_h100.sh
export QAZMORPH_RESOURCE_DIR="$PWD/.qazmorph/resources"
export PYTHONPATH="$PWD/src"
python3 -m unittest discover
The bootstrap uses pinned apertium-kaz source and exact Ubuntu package
versions to install an unprivileged HFST/CG toolchain under .qazmorph/. It
does not require sudo. Every downloaded Debian archive is checked against a
checked-in filename/package/version/architecture/size/SHA-256 lock before it is
extracted; every extracted toolchain file, build input, and output is then
SHA-256 manifested. The build also formally proves that the generator relation
is a subset of the analyzer relation by requiring generation - analysis to be
empty. Compiled resources live in an immutable content-addressed directory;
.qazmorph/resources is atomically switched only after the new bundle verifies.
Earlier complete bundles are kept for inspection or functional rollback.
Resource-manifest v2 bundles retain their legacy cyclic standard-lookup
guesser, so QazMorph deliberately disables productive OOV guessing for them
and returns explicit unknowns instead. Manifest v3 bundles require the embedded
finite-relation proof and use the optimized .hfstol guesser.
Runtime verification re-hashes every compiled resource artifact and every file
and symlink in the extracted HFST/CG toolchain. Official runs require both
content-addressed bundles to be sealed read-only and reject an unverified
executable override. It is not a byte-closed operating-system image: ELF
dependencies supplied by the H100 host (for example libc and ICU) remain
outside that manifest. Reports record this boundary as byte_closed: false,
the host platform, and whether ambient LD_LIBRARY_PATH, LD_PRELOAD, or
LD_AUDIT made the run non-official; LD_PRELOAD/LD_AUDIT values are hashed
rather than exposed.
Legacy v2 resources remain usable through standard lookup, but are labeled
non-official because their historical toolchain was not sealed read-only.
For packaged source and Python-wheel downloads, see
GitHub Releases. The wheel
installs the kazstem, qazmorph, and mystem-kz console commands. A complete
analysis run also needs the verified HFST/CG toolchain and compiled Kazakh
resource bundle described above; release assets state their platform and
runtime requirements explicitly.
Optional neural mode:
bash scripts/bootstrap_neural_h100.sh
.qazmorph/neural-venv/bin/qazmorph --neural -cin <<'EOF'
Қазақстанның кітаптарымыздан жазбағанбыз.
EOF
Neural setup is similarly staged. It locks the uv executable, every Kazakh
Stanza model file by byte size and SHA-256, the project sources, and exact
versions for the selected venv and host packages named in the lock. It also
checks the declared Python/Torch/CUDA host runtime and records every Python
distribution visible to the resulting interpreter. This is not a byte-for-byte
wheel-locked closure: the venv inherits system site packages, unselected visible host
dependencies are recorded rather than locked, and dependency wheel bytes are
not pinned. The active model and environment links move only after their
manifests verify. The active venv and verified model bundles remain under the
checkout's .qazmorph/ directory; ephemeral installer caches are not runtime
inputs.
CLI
The invocation follows MyStem's file convention:
kazstem [options] [input-file] [output-file]
Omitted filenames and - mean standard input/output.
printf 'Қазақстанның кітаптарымыздан.' \
| kazstem -cin --weight
printf 'Оқу инемен құдық қазғандай.' \
| kazstem -d --format jsonl
printf 'Оқу инемен құдық қазғандай.' \
| .qazmorph/neural-venv/bin/kazstem --neural --format conllu
Important options:
| Option | Meaning |
|---|---|
-n |
one emitted segment per line |
-c |
retain gaps and punctuation |
-w |
dictionary readings only |
-l |
omit surface forms in text output |
-i |
include UPOS and UD features |
-g |
merge readings sharing a lemma; requires -i |
-s |
emit text {\s} or XML sentence boundaries; requires -c |
-d |
apply Constraint Grammar; unresolved ambiguity remains visible |
--neural |
contextually rank the legal FST lattice |
--format text|json|jsonl|xml|conllu |
select output format |
--fixlist PATH |
override entries with JSONL or three-column TSV |
--filter-gram TAG[,TAG] |
require raw tags, UPOS, or Feature=Value |
--generate-all |
retain up to 256 safety-bounded OOV hypotheses |
--weight |
display scores when a scoring layer actually supplied them |
--ud-profile universal|ktb |
select the UD view; ktb is explicit corpus compatibility |
json is the compact MyStem-shaped schema (text, analysis, lex, optional
gr/qual/wt): an array by default and one object per line with -n.
jsonl is QazMorph's richer qazmorph.jsonl-record.v2 schema. Every row has
record_type: token|analysis_span and consumes_input. Token rows carry exact
offsets, raw tags, projection-profile provenance, all candidates, and selection; span rows retain
overlapping multi-token FST cohorts without consuming the surface twice. Use
-c (and do not use -w) when consuming token rows must reconstruct the full
input; without -c, nonlexical token rows are omitted. Span records remain
available whenever their filtered candidate set is nonempty.
Physical line endings are retained in MyStem text mode even without -c.
XML output validates every emitted text and attribute value against XML 1.0;
forbidden controls such as NUL produce a controlled error. Text, JSON, and
JSONL remain lossless for those caller code points.
The precise option-by-option contract, including deliberately unsupported combinations, is in docs/MYSTEM_COMPATIBILITY.md.
Python API
from qazmorph import Analyzer
with Analyzer(
resource_dir=".qazmorph/resources",
ud_profile="universal",
) as analyzer:
document = analyzer.analyze("Қазақстанның кітаптарымыздан")
for token in document.tokens:
for reading in token.analyses:
print(token.text, reading.lemma, reading.upos, reading.feature_map)
for span in document.analysis_spans:
print(span.text, span.start, span.end, [r.raw for r in span.analyses])
assert "кітаптарға" in analyzer.generate("кітап", ["n", "pl", "dat"])
Every token or analysis-span reading retains:
- exact surface offsets and optional NFC-normalized surface;
- lemma, lexical UPOS, ordered raw tags, and raw FST reading;
- every joined morpheme with its own UPOS/features;
- a legal UD v2 projection;
- provenance (
lexicon,guesser,fixlist, deterministicrule, orunknown); - nullable score and explicit guessed/orthographic flags;
- a separate contextual UPOS when syntax changes lexical
VERBtoAUX.
The default universal profile retains the legal UD distinctions supported by
the raw tags. The opt-in ktb profile changes only the lossy UD view to match
Kazakh-KTB conventions. Document.ud_profile and JSONL record which projection
was used; raw readings and morphemes remain unchanged.
On consuming atomic tokens, explicit raw usage tags can license additive UD
candidates without weakening the primary projection: adj+subst → NOUN,
n+attr → ADJ, and adj+advl → ADV. Compiled bare-decimal <num> readings
similarly receive an ordinal view, plus KTB's combined Card,Ord view only in
the ktb profile. Original FST candidates remain the stable prefix; these
rule-projection aliases never create OOV coverage or force an otherwise
unresolved top-1 choice. Non-consuming phrase spans remain the exact backend
raw lattice and provenance sequence.
See docs/CONTRACT.md for the schema and edge-case rules.
Evaluation and performance
The official public annotated corpus is UD Kazakh-KTB (CC BY-SA 4.0). It is
small—1,078 sentences and 10,536 syntactic words—and its maintainers recommend
merging its files and using ten-fold jackknife evaluation instead of treating
the filenames as a canonical train/test split. Full-corpus FST and CG results
are diagnostics, not held-out claims: KTB's morphology follows Apertium
principles, and this project uses Apertium resources. The packaged Stanza
Kazakh models are themselves KTB-trained, so a neural score on full KTB has
training overlap and is reported only as a contaminated diagnostic. The
official 31-sentence train sample can serve as a tiny Stanza-unseen audit, but
it is still KTB and is not independent gold. The evaluator keeps alignment
failures in the end-to-end denominator. Evaluator v4 reports lattice recall,
lexical selected top-1, and neural context-projected UD top-1 separately because
only the lexical selected fields are candidate-recall-bounded.
An official neural report additionally requires the checked-in model and live
environment manifests to re-verify, the selected model bundle to match the
environment, and requested CPU/GPU placement to match the actual pipeline.
python3 scripts/evaluate_ud.py \
--resource-dir "$QAZMORPH_RESOURCE_DIR" \
.qazmorph/evaluation/UD_Kazakh-KTB-r2.18/kk_ktb-ud-*.conllu
python3 scripts/benchmark.py \
--resource-dir "$QAZMORPH_RESOURCE_DIR" \
--runs 20
See the verified H100 results, benchmark instructions, and evaluation protocol. Corpus/model payloads and detailed machine-readable reports are retained outside the source repository; the checked-in results page records their aggregate metrics and immutable hashes.
Data policy
The 25 GB Multidomain Kazakh Dataset is raw text rather than morphological gold.
It is useful for frequency estimates, OOV mining, domain robustness, and
throughput workloads, but it must not be presented as correctness labels.
scripts/prepare_multidomain_sample.py streams a revision-pinned prefix on the
evaluation host and writes a content manifest; scripts/evaluate_raw.py then measures
lossless reconstruction, operational coverage, bounded-OOV diagnostics, and
throughput without inventing gold accuracy. Keep the payload out of the source
repository and audit upstream component licenses independently before any
redistribution.
Scope and limitations
- The current primary orthography is Kazakh Cyrillic. Digits, punctuation, abbreviations, and many Latin proper names are covered. Arabic-script Kazakh needs the upstream transliteration composition and is not claimed yet.
- Constraint Grammar is high precision but intentionally abstains when rules do not decide. Neural mode is optional and must earn its place on genuinely independent held-out data.
- CoNLL-U export emits only atomic, non-whitespace FORM rows and does not encode
overlapping whitespace-MWE spans or expand joined morphemes into
multiword-token rows. The upstream readings do not expose reliable morpheme
surface spans. JSONL v2 retains the complete token and phrase-span lattice;
use
-cwithout-wwhen consuming rows must reconstruct the input. --generate-allremains safety bounded to keep API response size predictable, even though the productive analyzer is formally finite-valued per input.- Productive OOV lookup retains at most the first 512 raw FST readings (2,048
for
--generate-all) and has a two-second complete-query bound. Reaching an explicit reading/byte cap retains that deterministic prefix; a wall-clock timeout or HFST cycle marker never exposes a partial lattice. - The productive root relation deliberately does not hallucinate lexical vowel-drop alternations, and it cannot recover a loan whose harmony class is not inferable from its letters. Such analyses remain available when licensed by the compiled dictionary; unsupported OOV alternation probes are tracked explicitly in the resource-build audit.
Licensing
This project is GPL-3.0-or-later. The compiled runtime incorporates GPL-3.0
apertium-kaz resources. Its extracted HFST/CG toolchain contains components
under GPL, LGPL, Apache-2.0, Expat, ICU, and public-domain terms; the exact
component notices and neural-model licensing caveat are listed in
THIRD_PARTY.md.
The MyStem binary is not used, inspected, decompiled, or redistributed. Yandex's license expressly restricts reverse engineering; this implementation relies on public behavior documentation, the published algorithm paper, and independent Kazakh resources.
Citation
GitHub exposes a Cite this repository action from CITATION.cff. BibTeX users can cite CITATION.bib:
@software{nuriyev_kazstem_2026,
author = {Amir Nuriyev},
title = {KazStem: an ambiguity-preserving Kazakh morphological analyzer and generator},
year = {2026},
version = {0.2.1},
url = {https://github.com/amir-nuriyev/KazStem},
license = {GPL-3.0-or-later}
}
The repository also includes CodeMeta and Zenodo metadata. A GitHub URL alone does not guarantee Google Scholar indexing; archive a tagged release with Zenodo to obtain a DOI, then replace the URL-only citation with that DOI without changing authorship.
Metadata
Release files for kazstem 0.2.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| kazstem-0.2.1.tar.gz | 213.4 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| kazstem-0.2.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 285.2 kB
Release files / kazstem-0.2.1.tar.gz
| Download URL | kazstem-0.2.1.tar.gz |
|---|---|
| Size | 213.4 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
06bcb52f36e14328f011f7f97e92ca764ddbba630981e85adbcde1cd44b20315
|
|
BLAKE2b-256 checksum How to use checksums |
7e80a5cf376405fe173460523571f90a7de35f905628c05771ffd285e4c49f9d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 10, 2026.
Transparency logRelease files / kazstem-0.2.1-py3-none-any.whl
| Download URL | kazstem-0.2.1-py3-none-any.whl |
|---|---|
| Size | 71.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
5b69cb0b3f113605bb9c1681ab325cff5c8334b6a83f772fc05cdb4b5b400657
|
|
BLAKE2b-256 checksum How to use checksums |
503f80c5bf814049553d51af549185a5a4a402eca40436665fb3e4c10f36f51d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 10, 2026.
Transparency log