Part of Language AI Handbook
Explains how finite-state transducers and neural sequence-to-sequence models perform morphological analysis and generation.
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
Morphological Analysis and Generation
In the previous chapter, you learned how languages build words from morphemes, whether by changing grammatical features or creating new words. This chapter turns that linguistic knowledge into computational machinery. We want systems that can do two things with morphologically complex words. Analysis takes a surface form like the Turkish word evlerinizden ("from your houses") and recovers its internal structure: stem ev, plural, possessive second person plural, ablative case. Generation runs the machine in reverse: given the stem ev and a bundle of grammatical features, produce the correct surface form.
This matters because a huge fraction of the world's languages pack what English does with separate words into single word forms. In Finnish, one inflected noun can mark number and case while also carrying possessive or clitic information that would take several English words to express. Systems that treat every word as an opaque atom, as the bag-of-words models from Part II did, drown in sparsity: thousands of distinct surface forms share a single stem, and a statistical model sees each of those forms too rarely to learn anything reliable about it. Solving morphology means recovering the shared structure underneath, so that evidence about one surface form transfers to the thousands of forms built on the same stem.
We will study this machinery through two dominant paradigms. First, finite-state morphology: hand-engineered transducers that encode lexicons and morphophonological rules as inspectable regular relations. Then neural morphology: character-level encoder-decoder models that learn inflection from examples. The code section implements compact rule-based demonstrations of the finite-state ideas and a small neural model that learns Turkish-style vowel harmony from synthetic data, then tests it on stems excluded from training. This lets us compare their assumptions and failure modes without pretending that one small experiment decides which paradigm wins in general.
The Two Directions of Morphological Processing
Before diving into algorithms, let's be precise about the tasks, because "morphology" in NLP bundles several distinct problems that are easy to conflate.
Analysis (surface to analysis). Given a surface word form, recover one or more candidate analyses. An analysis typically pairs a lemma with a bundle of morphosyntactic features, written here in a Universal Dependencies-style FEATS notation:
- evlerinizden (ev, Number=Plur, Person[psor]=2, Number[psor]=Plur, Case=Abl)
- flies (fly, Number=Plur) or (fly, Person=3, Tense=Pres, VerbForm=Fin)
- watched (watch, Tense=Past, VerbForm=Fin)
Analysis can be ambiguous. The same string may decompose in more than one way, and an analyzer can enumerate those possibilities before a later component selects one. Choosing among them often requires context, which connects directly to the sequence labeling machinery you built in Part VI: hidden Markov models, Viterbi decoding, and conditional random fields. A part-of-speech tagger can help disambiguate whether flies in a particular sentence is the plural noun or the inflected verb.
Generation (analysis to surface). Given a lemma plus features, produce one or more licensed surface strings. This is the direction spell checkers, text-to-speech front ends, and machine translation systems need when they must produce a correctly inflected word in the target language. It is also the direction language models implicitly perform every time they generate an inflected form. Generation is usually less ambiguous than analysis, though a feature bundle can license variants. It is where systematic linguistic knowledge pays off most visibly: get the harmony rule wrong and every affected suffixed form is wrong.
Lemmatization is a frequently used special case of analysis: map a surface form to its dictionary form (its lemma), discarding inflectional detail. It is closely related to the stemming you saw in earlier chapters, but the goals differ in ways that matter for downstream systems.
A stemmer (like the Porter stemmer) chops affixes with string heuristics. It may produce non-words (studies studi) and never distinguishes ambiguity. A lemmatizer targets a dictionary form and may use context: in "The flies were buzzing," flies lemmatizes to fly as a noun, while in "She flies to Rome," it lemmatizes to the same base form as a verb. Stemming is fast and lexicon-free. Lemmatization is often preferable when a downstream task needs linguistically interpretable forms, though its predictions are not error-free and the choice remains task-dependent.
Why not sidestep all of this with the subword tokenizers from Part V? To a first approximation, they do solve a morphology-shaped problem by fragmenting rare words into reusable pieces. BPE greedily merges frequent symbol pairs, while SentencePiece is a toolkit that supports BPE and unigram language-model tokenization. Neither objective requires the resulting units to be linguistically correct morphemes. A BPE vocabulary trained on English may capture the frequent suffix -ing while slicing cancellation into fragments that do not follow its morphological structure. For some language-tokenizer combinations, morphologically rich words split into substantially more tokens, which stretches context windows and changes how much evidence fits into a fixed-length input. We will return to this tension in the limitations section and when we discuss multilingual models in Part XXVI: Multilingual Language Models and Cross-Lingual Transfer.
Finite-State Morphology
Finite-state morphology is the classical answer, developed in the 1980s and 1990s by Kimmo Koskenniemi, Lauri Karttunen, and others. It remains an important approach in morphological analyzers, spell checkers, and hyphenation systems. Its core claim is elegant: many practically useful accounts of word structure can be expressed with finite-state automata extended to emit symbols in two alphabets at once.
Building on your knowledge of regular expressions from Part I, Chapter 3, recall that a finite-state automaton recognizes a regular language: it reads a string and answers yes or no. A finite-state transducer (FST) generalizes this from a recognizer to a translator: it reads a string and produces another string.
A finite-state transducer is a machine where is a finite set of states, is the input (upper) alphabet, is the output (lower) alphabet, is the start state, is the set of final states, and the transition relation is . Each arc consumes at most one symbol from and emits at most one symbol from , allowing the input and output strings to have different lengths. An FST defines a regular relation: a set of paired strings rather than a set of single strings.
The power of this formalism for morphology comes from its closure properties. Regular relations remain regular under composition and union, and they can also be inverted: the inverse of an analyzer relation can serve as its generator relation. Because analysis and generation use the same encoded relation, they cannot drift apart as two separately maintained implementations might. The guarantee is relative to the grammar: the machine is consistent with what its lexicon and constraints encode, not automatically with every fact of the natural language. A lexicon transducer can also feed a cascade of rewrite-rule transducers. If is the lexicon transducer and are sequential rule transducers, the whole relation is their composition .
where:
- : the lexicon transducer, which maps lexical strings (stems plus morpheme labels) to intermediate strings
- : the phonological rule transducers, applied in order, with first
- : the composed transducer, which maps a lexical string directly to its surface form
Composition is what lets the pipeline behave like one relation: each transducer's output is paired with the next transducer's input, and the composite relation is still regular, so it can be inverted. Practically, this means you can develop the lexicon and each rule as separate, testable artifacts, then compile them into one transducer for efficient lookup.
The Two-Level Model
Koskenniemi's two-level morphology, the foundational design, models a word as a pairing between a lexical string (morphemes with their identity intact) and a surface string (what is written). Between the two levels sit correspondence rules that map pairs of symbols in parallel: rather than rewriting a string sequentially, rule by rule, two-level rules constrain, at each position, which lexical symbol may correspond to which surface symbol.
For the English noun plural, the lexicon would generate the lexical string watch +N +PL, conceptually w a t c h ^ s with a morpheme boundary, and the constraints must license the surface form watches. In two-level notation, one writes rules over symbol pairs, where each symbol in the rule is an upper:lower pair. A schematic sibilant-epenthesis constraint looks like this:
where:
- : the correspondence pair, with nothing in the lexical string and a surface e on the other side
- : a symbol class matching one sibilant consonant; in a complete grammar, the environment also identifies the plural morpheme boundary
- : the position of the correspondence itself
- : the identity correspondence for the plural s, which passes through unchanged
- : the biconditional operator
Read this as: "the pair (nothing in the lexical string, an e on the surface) corresponds exactly in the environment where a sibilant precedes the plural s." The biconditional operator means the rule fires exactly in this environment and nowhere else. Different operators encode different logical strengths, and choosing the right one for each rule is part of the grammarian's job:
- surface constraint: the correspondence is permitted only in this environment and is blocked everywhere else (this rules out illicit occurrences).
- obligatory: whenever the left side occurs in this environment, it must be rewritten.
- if and only if: both of the above.
Two-level constraints are evaluated in parallel over the same lexical–surface correspondence; they do not feed or bleed one another through an ordering. For party parties, a complete grammar would constrain the lexical y to correspond to surface i in the plural environment and would license surface e before the plural s. Both constraints describe the same aligned pair rather than successively rewriting party+s. That distinguishes the method from an ordered rewrite cascade.
Sequential rewrite transducers are also useful, and they compose. Because an FST denotes a relation rather than necessarily a function, composition is set-valued:
where:
- : an input string, for example a lexical string produced by the lexicon
- : the first rule applied, for example the y-to-i rule
- : the second rule applied, for example the epenthesis rule
- : one possible intermediate string licensed by
- : the set of outputs licensed by for that intermediate string
In other words, the composite transducer retains every path whose intermediate output from matches an input to . It produces one combined relation, but that relation need not be deterministic. Building the analyzer means compiling the lexicon (which morphemes exist and in what order, a constraint called morphotactics) and the alternation constraints (how morphemes change in contact, called morphophonology) into a transducer that can be used in both directions.
Morphophonology as Regular Relations
Why is it legitimate to model many sound alternations with finite-state machines? Contexts like "preceded by a sibilant" or "followed by a suffix starting with a front vowel" are the kinds of finite-context patterns regular expressions capture. Kaplan and Kay's classic 1994 treatment showed that the standard, non-self-applying interpretation of finite-context rewrite rules denotes regular relations; FST closure under composition then preserves regularity for a finite cascade. The restrictions matter: arbitrary cyclic or self-feeding rule application is not covered by the claim.
This gives finite-state morphology a precise, grammar-relative guarantee. An analyzer returns exactly the pairs licensed by its compiled lexicon and constraints. It can still accept a non-word if the grammar overgenerates or miss a real word if the lexicon or rules are incomplete. Neural models offer a different tradeoff: their architecture does not hard-code the same closed set of licensed pairs, so they can produce forms outside the observed language patterns.
Worked Example: Analyzing Finnish taloissa
Let's walk through a small but real example: the Finnish word taloissa, meaning "in the houses." Finnish nouns inflect for number and case, and the case suffixes obey vowel harmony, so a single noun stem generates dozens of forms whose suffix vowels depend on the stem.
The lexicon says a noun stem like talo ("house") inflects by concatenating a number marker and a case marker. The lexical string contains the stem plus abstract markers, some of which (like the plural i and the archiphoneme A inside the case suffix) are later rewritten by the rules:
| Lexical string | Morphemes | Gloss |
|---|---|---|
talo +PL +INE | talo, i, ssa | in the houses |
talo +SG +INE | talo, ∅, ssa | in the house |
talo +PL +NOM | talo, t, ∅ | houses |
Notice that the plural has an i allomorph (Finnish uses i or j before most cases) and that the case suffix ssa contains the back vowel a, because the last vowel of the stem is o, a back vowel. A front-vowel stem selects ssä. The form käsissä from käsi additionally involves plural-stem alternation and vowel deletion; harmony alone selects the suffix vowel but does not derive the complete form. In two-level notation, harmony can be expressed with an archiphoneme, a symbol in the lexical alphabet that has not yet decided which vowel it will become:
- : the archiphoneme in the lexical string, the underspecified vowel of the case suffix
- / : the correspondence pair, mapping the underspecified to the back surface vowel or the front surface vowel
- / : a symbol class matching one harmony-triggering back vowel (, , ) or front vowel (, , )
- : zero or more harmony-neutral vowels ( or ), such as the plural allomorph , which neither trigger harmony nor block it
- : zero or more intervening consonants between the triggering vowel (after any neutral vowels) and the archiphoneme
- : the position of the or correspondence itself
- : the biconditional, meaning the pair is rewritten exactly in this environment and nowhere else
The lexical string talo i ssA (the A being the archiphoneme inside the case suffix) passes through the harmony rule, which skips the harmony-neutral plural i and commits the A to a because the nearest preceding stem vowel is back. The composed transducer then aligns the two strings character by character:
Lexical: t a l o i s s A
Surface: t a l o i s s a → taloissawhere:
- row 1 (
Lexical): the lexical stringtalo i ssA, with the archiphonemeAstanding in for the not-yet-harmonized first vowel of the inessive suffix - row 2 (
Surface): the surface stringtaloissa, after the harmony rule commitsAtoa - characters at the same position are aligned in columns, showing that each lexical symbol corresponds to exactly one surface symbol (the
+PLand+INElabels are grammatical features rather than surface characters)
The harmony constraint licenses A:a because the preceding harmony-triggering stem vowel is o, a back vowel. For a front-vowel stem it licenses A:ä; separate alternation constraints account for the stem changes in forms such as käsissä.
Running the same transducer in the analysis direction, the input taloissa is scanned left to right: talo matches a noun stem, i matches the plural allomorph, ssa matches the inessive allomorph, and the end of input is reached in a final state. The machine emits talo +N +PL +INE. If we fed it talon, it would emit talo +N +SG +GEN ("of the house"); with no suffix, talo yields talo +N +SG +NOM. Ambiguity arises when multiple lexical paths accept the same surface string. Resolving among such analyses requires sentence context, which is exactly the role Viterbi-style decoding over a sequence tagger plays, as you saw in Part VI, Chapter 6.
Weighting and Ranking
A plain FST answers a yes/no question per string pair: is this analysis licensed? Real systems want the most plausible analysis, because a raw enumeration of licensed analyses is often long and mostly useless without a ranking. Weighted FSTs attach a weight to each arc, typically a negative log probability:
where:
- : one accepting route through the transducer, from the start state to a final state, consuming the input string and emitting a candidate analysis
- : a single weighted transition on that path
- : the probability associated with taking that arc, so that is its cost
- : the sum of the arc costs over the whole path
When a path probability is factorized into arc probabilities, which may be conditional on the state and history encoded by the machine, multiplication becomes addition under the negative logarithm. An arc with probability zero has infinite cost. The best analysis of a surface form is then the minimum-weight accepting path, found by the same dynamic programming idea as Viterbi decoding from Part VI, Chapter 6, just over a transducer instead of an HMM. Weights can come from a lexicon of word frequencies, a corpus, or a character-level language model, and they let the analyzer rank flie+s below fly+s without hard-coding a rejection.
To make the ranking concrete, we first attach toy corpus frequencies to the candidate analyses of flies and convert them to path costs.
import matplotlib.pyplot as plt
import numpy as np
fly_candidates = {
"fly +N +PL": 0.60, # plural of the noun, dominant in corpus counts
"flie +N +PL": 0.02, # spurious stem from the epenthesis deletion
}
fly_weights = {k: float(-np.log(p)) for k, p in fly_candidates.items()}
Because the correct reading carries the bulk of the corpus mass, its cost is far below the spurious analysis, which is exactly the separation a weighted FST exploits. Note that the spurious path remains available at finite cost: weighting softens the all-or-nothing character of the unweighted machine while preserving the complete enumeration of licensed analyses.
In one downstream design, a disambiguating sequence tagger consumes these costs alongside sentence context and selects among the candidate analyses. Other pipelines expose candidates without their original FST weights or combine scores differently. Either way, keeping a candidate at finite cost leaves it available when context favors it.
Building Analyzers in Practice
Several mature toolchains implement this pipeline. Xerox tools (lexc for lexicons, twolc for two-level rules) are the historical standard. HFST and foma are widely used open-source implementations, and pynini brings weighted FSTs into Python. GiellaLT, developed by the Giellatekno community, supports language-technology resources for Sámi and other languages. UniMorph provides a cross-lingual schema and inflection datasets, while UniDive coordinates work on universal and cross-lingual morphology; they are related resources, not FST analyzer toolchains. A high-quality analyzer for a new language can require substantial expert work: enumerating the lexicon, writing morphotactic continuation classes, and testing morphophonological constraints. That cost helped motivate data-driven approaches, which we examine next.
Neural Morphology as Sequence-to-Sequence Learning
The neural reframing treats inflection as string-to-string transduction learned from data. Given a training set of triples (lemma, morphosyntactic features, target form), a character-level encoder-decoder model learns the mapping end to end, with no hand-written rules. The landmark evaluations here are the SIGMORPHON shared tasks (2016 onward) and the CoNLL-SIGMORPHON 2017–2018 shared tasks, which established character-level encoder-decoder networks as the standard for what is often called morphological reinflection: given watch and the features V;PST, produce watched.
Why characters rather than words? Because the whole point is to generalize to unseen stems. A character-level model can inflect a stem it has never encountered, by combining what it has learned about the -ed pattern with the phonology of the stem it reads, so screeched is reachable even if screech never appeared in training. This is the same subword insight that powered FastText's character n-gram embeddings in Part IV, Chapter 8, applied to a transduction task instead of a representation task.
A minimal architecture looks like this:
- an encoder: an embedding layer over the character vocabulary feeding a GRU or LSTM, which compresses the input string (say, the lemma plus a tag string like
N;PL) into a sequence of hidden states; - a decoder: a second GRU that generates the output string one character at a time, attending over encoder states;
- an optional alignment or copy mechanism: because the output usually shares most of its characters with the input, models can benefit from tracking input positions or explicitly copying characters. Aharoni and Goldberg (2017) used hard monotonic alignment with action-based character decoding rather than an explicit pointer-copy operation. Makarov and Clematide (2018) combined a related transition-based approach with imitation learning in a strong SIGMORPHON system.
The learned model can capture morphophonological patterns implicitly, without hand-written rules. Given suitable English training data, a character model can learn mappings such as walk walked, carry carried, and stop stopped. Some experimental models overregularize irregular forms under particular data and evaluation conditions, but that behavior is not guaranteed and our Turkish-style experiment below does not test English irregulars.
There is a second neural family worth distinguishing. Instead of one word at a time, contextual lemmatizers and morphological taggers operate on whole sentences. Their architectures vary: contextual encoders can predict part-of-speech and morphological features, while lemma components may use lookup tables, edit rules, sequence models, or other language-specific mechanisms. Context helps distinguish flies the noun from flies the verb even though both share the lemma fly. Stanza and spaCy expose these analyses through a common pipeline interface, but their internal lemmatization architectures are not identical. We will meet transformer encoders in Part XXIV.
Finally, there is unsupervised morphological segmentation: given only raw text, look for recurring pieces shared by forms such as walks, walking, and walked. Morfessor formulates this as a compression problem under the Minimum Description Length principle, balancing the cost of a lexicon of morphs against the cost of encoding the corpus with them. The objective is designed for morphological segmentation, but it does not guarantee that every learned segment is a linguist's morpheme. BPE from Part V, Chapter 2 is a cousin with a different objective: it merges frequent symbol pairs and may cross morpheme boundaries.
Worked Example: Two Models of Turkish Vowel Harmony
Let's make the contrast concrete with a single regular phenomenon. Turkish suffixes harmonize with the last vowel of the stem: after a front vowel (e, i, ö, ü) the plural is -ler, and after a back vowel (a, ı, o, u) it is -lar. The harmony rule produces two surface suffixes for nouns covered by this regular pattern; loans and lexicalized exceptions can require additional handling.
The finite-state solution. We write the plural morpheme lexically as lAr, where A is the harmony archiphoneme, plus two two-level rules:
where:
- : the archiphoneme, the underspecified vowel written lexically inside the plural suffix
lAr - / : the correspondence pair, realized as the front surface vowel e or the back surface vowel a
- / : the symbol class of the stem's last vowel (e, i, ö, ü or a, ı, o, u)
- : zero or more consonants between the stem's last vowel and the archiphoneme
- : the position of the or correspondence itself
- : the biconditional, so the rewrite happens exactly in this environment and nowhere else
The lexicon licenses ev +PL, and the constraints select evler. The same regular harmony mapping applies productively to stems in the modeled classes, including many novel regular stems, because it references a phonological class rather than memorizing each word. Loans, proper names, consonant alternations, and lexical exceptions require additional treatment. The machinery also extends to the ablative -den/-dan, the genitive -in/-ın/-ün/-un, and buffer consonants such as the y in araba+y+a.
The neural solution. We assemble a training set: (stem, PL, surface) triples like (ev, PL, evler) and (okul, PL, okullar), plus hundreds of other stems, and train a character-level encoder-decoder. The + delimiter explicitly marks where the stem ends and the feature tag begins. What the model must infer from the examples is how much of the stem to reproduce and which surface suffix follows it. Our attention model learns this behavior without an explicit pointer-copy operation.
The code section below demonstrates both ideas, though its rule-based functions are ordinary Python rather than compiled FSTs. The interesting differences appear at the edges: an explicit rule handles a new regular stem without training examples, while the neural model may fail on an unfamiliar character pattern. Conversely, extending a hand-built grammar requires engineering the new behavior, while a neural model can learn it when enough representative examples are available. We will quantify generalization within our synthetic dataset.
Code Implementation
We will build three artifacts: a rule-based English plural analyzer and generator, a rule-based Turkish inflector, and a small neural sequence-to-sequence inflector trained on synthetic Turkish-style data that we then test on held-out stems. The Python rules illustrate finite-state behavior without compiling an FST library, including how reversing local rules can yield multiple candidate analyses.
A Hand-Rolled Transducer: English Plurals
First, the setup cell. It seeds all randomness and imports what we need for the entire chapter.
import random
import matplotlib.pyplot as plt
import numpy as np
import torch
import torch.nn as nn
from torch.utils.data import DataLoader, TensorDataset
torch.manual_seed(42)
random.seed(42)
np.random.seed(42)Now the generator side. This simplified rule-based function covers four cases: irregular stems bypass the general rules, sibilant-final stems take -es, consonant-final y becomes ies, and everything else takes plain -s. A compiled finite-state grammar could encode the same input-output relation, but this Python function is not itself a two-level grammar.
IRREGULAR_PLURALS = {
"child": "children",
"foot": "feet",
"goose": "geese",
"man": "men",
"mouse": "mice",
"tooth": "teeth",
}
VOWELS = "aeiou"
def is_vowel(ch):
return ch in VOWELS
def plural_noun(lemma):
"""Generate the English plural surface form from a lemma."""
if lemma in IRREGULAR_PLURALS:
return IRREGULAR_PLURALS[lemma]
if lemma.endswith(("s", "x", "z", "ch", "sh")):
return lemma + "es" # sibilant epenthesis
if lemma.endswith("y") and len(lemma) > 1 and not is_vowel(lemma[-2]):
return lemma[:-1] + "ies" # y -> i, then epenthetic e
return lemma + "s"The analyzer direction is where relation-like behavior shows itself. We reverse the applicable -ies or -es rule, then also test the independent final-s reversal, collecting all surviving analyses rather than committing to one.
IRREGULAR_SINGULARS = {p: l for l, p in IRREGULAR_PLURALS.items()}
def analyze_plural(form):
"""Propose all analyses consistent with the plural alternation rules."""
analyses = []
if form in IRREGULAR_SINGULARS:
analyses.append(IRREGULAR_SINGULARS[form] + " +N +PL")
if form.endswith("ies") and form not in IRREGULAR_SINGULARS:
analyses.append(form[:-3] + "y +N +PL")
elif form.endswith("es") and form not in IRREGULAR_SINGULARS:
analyses.append(form[:-2] + " +N +PL")
if form.endswith("s"):
analyses.append(form[:-1] + " +N +PL")
return analysescat -> cats watch -> watches kiss -> kisses party -> parties day -> days child -> children cats <- ['cat +N +PL'] watches <- ['watch +N +PL', 'watche +N +PL'] flies <- ['fly +N +PL', 'flie +N +PL'] parties <- ['party +N +PL', 'partie +N +PL'] children <- ['child +N +PL']
cat -> cats
watch -> watches
kiss -> kisses
party -> parties
day -> days
child -> children
cats <- ['cat +N +PL']
watches <- ['watch +N +PL', 'watche +N +PL']
flies <- ['fly +N +PL', 'flie +N +PL']
parties <- ['party +N +PL', 'partie +N +PL']
children <- ['child +N +PL']Look carefully at the analysis side. watches correctly yields watch, but it also yields the spurious watche. And flies yields fly +N +PL (correct, as in insects) alongside the non-word flie +N +PL. This is not a bug in our rules; it is the behavior expected when a transducer reverses local rules without a stem lexicon. Pruning requires knowledge of which stems occur in the lexicon. Real systems therefore compose the alternation rules with lexical constraints and may use weights to rank the survivors. The noun and verb readings of flies still require sentence context; sequence models from Part VI provide that layer.
To see this overgeneration at a glance, we count how many analyses the rules propose for each surface form.
probe_forms = [
"cats",
"watches",
"flies",
"parties",
"children",
"foxes",
"days",
]
analysis_counts = [len(analyze_plural(f)) for f in probe_forms]
The chart makes the pattern concrete: an applicable -ies or -es reversal can coexist with the separate final-s reversal, and neither path knows whether the recovered stem is a real word. Plain -s forms such as cats and days, and the irregular children, which bypasses the rules entirely, produce one analysis in this toy grammar.
The Turkish Harmony Generator
Now the rule-based Turkish inflector. We model vowel harmony as an explicit allomorphy table keyed by the stem's final vowel, plus buffer consonants (y after a vowel in the dative, n in the genitive). We deliberately omit Turkish consonant assimilation (real kitap → kitabın, with p becoming b); we will note what that omission costs.
FRONT = set("eiöü")
VOWELS = set("aeıioöuü")
def last_vowel(word):
for ch in reversed(word):
if ch in VOWELS:
return ch
return None
## Genitive allomorphy is four-way in real Turkish: -in/-ın/-ün/-un
GEN_ALLOMORPH = {
"e": "in",
"i": "in",
"ö": "ün",
"ü": "ün",
"a": "ın",
"ı": "ın",
"o": "un",
"u": "un",
}
def inflect(noun, tag): # Turkish noun inflection
"""Inflect a Turkish noun for a small set of cases, honoring vowel harmony."""
v = last_vowel(noun)
if v is None:
raise ValueError(f"no vowel in stem: {noun}")
front = v in FRONT
if tag == "PL":
return noun + ("ler" if front else "lar")
if tag == "LOC":
return noun + ("de" if front else "da")
if tag == "DAT":
link = (
"y" if noun[-1] in VOWELS else ""
) # buffer y after vowel-final stems
return noun + link + ("e" if front else "a")
if tag == "GEN":
link = (
"n" if noun[-1] in VOWELS else ""
) # buffer n after vowel-final stems
return noun + link + GEN_ALLOMORPH[v]
raise ValueError(f"unknown tag: {tag}")ev PL=evler, LOC=evde, DAT=eve, GEN=evin okul PL=okullar, LOC=okulda, DAT=okula, GEN=okulun araba PL=arabalar, LOC=arabada, DAT=arabaya, GEN=arabanın kitap PL=kitaplar, LOC=kitapda, DAT=kitapa, GEN=kitapın
ev PL=evler, LOC=evde, DAT=eve, GEN=evin
okul PL=okullar, LOC=okulda, DAT=okula, GEN=okulun
araba PL=arabalar, LOC=arabada, DAT=arabaya, GEN=arabanın
kitap PL=kitaplar, LOC=kitapda, DAT=kitapa, GEN=kitapınThe harmony table commits the suffix vowel according to the stem's last vowel, and vowel-final stems get their buffer consonants. The same functions handle ev, araba, and kitap without a per-stem suffix table because they reference phonological classes. The example also exposes what the toy grammar omits. Real Turkish voices final p before a vowel-initial suffix (kitaba, kitabın) and devoices the locative suffix after voiceless p (kitapta). Those consonant alternations are not implemented, so the code emits kitapa, kitapın, and kitapda.
We can also visualize the harmony mapping directly, as a grid over stem-final vowels and suffix vowels.
harmony_vowels = list(GEN_ALLOMORPH.keys())
suffix_vowels = ["a", "e", "ı", "i", "o", "ö", "u", "ü"]
harmony_matrix = np.zeros((len(harmony_vowels), len(suffix_vowels)))
for r, v in enumerate(harmony_vowels):
chosen = GEN_ALLOMORPH[v][0]
harmony_matrix[r, suffix_vowels.index(chosen)] = 1.0
Each row has exactly one marked cell, which is the visual signature of a deterministic rule: no stem-final vowel is left underspecified, and the rows partition into a front block and a back block. This is precisely the structure the archiphoneme rule pair encodes, and the same grid is what the neural model must induce implicitly from the training pairs.
Building the Synthetic Parallel Corpus
For the neural model, we need training pairs. We generate 400 Turkish-like stems from consonant-vowel syllables constrained to one harmony class, an intentionally simplified approximation to the dominant pattern in many native Turkish roots. Loans and several exceptional patterns are outside this generator. We hold out 80 stems entirely from training. Each stem pairs with four inflection tags, giving 1,280 training pairs and 320 held-out pairs. The input is the stem and tag joined by +, and the target is the surface form.
CONSONANTS = "bcdfgklmnprstz"
FRONT_V = "eiöü"
BACK_V = "aıou"
TAGS = ["PL", "LOC", "DAT", "GEN"]
def make_syllable(harmony):
vowel = random.choice(FRONT_V if harmony == "front" else BACK_V)
onset = random.choice(CONSONANTS)
coda = random.choice(CONSONANTS) if random.random() < 0.5 else ""
return onset + vowel + coda
def make_stem():
harmony = random.choice(["front", "back"])
return "".join(
make_syllable(harmony) for _ in range(random.choice([2, 2, 3]))
)
stems, seen = [], set()
while len(stems) < 400:
s = make_stem()
if s not in seen:
seen.add(s)
stems.append(s)
random.shuffle(stems)
train_stems, test_stems = stems[:320], stems[320:]
def make_pairs(stem_list):
return [(f"{s}+{t}", inflect(s, t)) for s in stem_list for t in TAGS]
train_pairs = make_pairs(train_stems)
test_pairs = make_pairs(test_stems)Stems: 400 total, 320 train, 80 held out Training pairs: 1280 Samples: gidsöri+PL -> gidsöriler gidsöri+LOC -> gidsöride gidsöri+DAT -> gidsöriye gidsöri+GEN -> gidsörinin lacpo+PL -> lacpolar lacpo+LOC -> lacpoda
Each held-out stem was never seen in training, so accuracy on the test pairs measures transfer to unseen stems rather than recall of exact training strings. The test stems still come from the same synthetic generator as the training stems, so this is an in-distribution stem split; it does not measure performance under a different language or stem distribution. Shared-task evaluations such as SIGMORPHON likewise use held-out lemma–feature–form triples, though their datasets and splits are substantially richer than this demonstration.
Encoding and the Seq2Seq Model
Next, the character vocabulary and tensor preparation. We pad everything to fixed lengths computed from the training data.
specials = ["<pad>", "<sos>", "<eos>"]
all_chars = sorted(
{c for inp, _ in train_pairs for c in inp}
| {c for _, t in train_pairs for c in t}
)
itos = list(dict.fromkeys(specials + sorted(all_chars)))
stoi = {c: i for i, c in enumerate(itos)}
PAD, SOS, EOS = 0, 1, 2
def encode(text):
return [stoi[c] for c in text]
def pad(ids, length):
return ids + [PAD] * (length - len(ids))Vocabulary size (chars + specials): 37 Inventory: <pad><sos><eos>+ACDEGLNOPTabcdefgiklmnoprstuyzöüı
The inventory is small: specials, the tag letters (P, L, O, C, D, A, T, G, E, N), +, consonants, and all eight Turkish vowels including dotless ı. That is the entire alphabet the model must reason over.
max_in = max(len(inp) for inp, _ in train_pairs)
max_out = max(len(tgt) for _, tgt in train_pairs) + 1 # +1 for <eos>
def build_tensors(pairs):
lin = max(max_in, max(len(inp) for inp, _ in pairs))
lout = max(max_out, max(len(t) for _, t in pairs) + 1)
src = torch.tensor([pad(encode(inp), lin) for inp, _ in pairs])
dec_in = torch.tensor([pad([SOS] + encode(t), lout) for _, t in pairs])
target = torch.tensor([pad(encode(t) + [EOS], lout) for _, t in pairs])
return src, dec_in, target
src, dec_in, target = build_tensors(train_pairs)src shape: (1280, 13), target shape: (1280, 13)
The shapes confirm that all 1,280 training pairs are padded to one common source length and one common target length, the fixed geometry the batched GRUs require. The decoder consumes <sos> followed by the gold target characters (teacher forcing) while predicting a one-step-shifted target. The model combines a GRU encoder with an attention-equipped GRU decoder and an output projection. Attention gives each output step direct access to every encoded input character rather than forcing the complete stem and tag through one fixed vector. That architecture supplies a route for copying, but this chapter does not include a no-attention ablation that would isolate its causal contribution.
class Seq2Seq(nn.Module):
def __init__(self, vocab_size, emb_dim=32, hid_dim=64, pad_idx=0):
super().__init__()
self.emb = nn.Embedding(vocab_size, emb_dim, padding_idx=pad_idx)
self.encoder = nn.GRU(emb_dim, hid_dim, batch_first=True)
self.decoder = nn.GRU(emb_dim + hid_dim, hid_dim, batch_first=True)
self.out = nn.Linear(2 * hid_dim, vocab_size)
def decode_step(self, token, hidden, encoded, source_mask):
scores = torch.bmm(encoded, hidden[-1].unsqueeze(2)).squeeze(2)
scores = scores.masked_fill(~source_mask, -1e9)
attention = torch.softmax(scores, dim=1)
context = torch.bmm(attention.unsqueeze(1), encoded).squeeze(1)
decoder_input = torch.cat([self.emb(token), context], dim=1)
output, hidden = self.decoder(decoder_input.unsqueeze(1), hidden)
logits = self.out(torch.cat([output.squeeze(1), context], dim=1))
return logits, hidden
def forward(self, src, dec_in, tf_ratio=0.7):
encoded, h = self.encoder(self.emb(src))
source_mask = src != PAD
B = src.size(0)
token = torch.full((B,), SOS, dtype=torch.long)
hidden = h
logits = []
for t in range(dec_in.size(1)):
inp = dec_in[:, t] if random.random() < tf_ratio else token
step_logits, hidden = self.decode_step(
inp, hidden, encoded, source_mask
)
logits.append(step_logits)
token = step_logits.argmax(-1)
return torch.stack(logits, dim=1)Model parameters: 55,877
The printed parameter count is modest by modern standards and sufficient for this synthetic harmony task. That result illustrates the compactness of the particular mapping we constructed; it does not imply that all of morphology is equally small or simple.
Now training. We re-seed before construction so the model's random initialization is reproducible regardless of how many random draws happened earlier. Mini-batch gradient descent gives the model enough parameter updates to learn the copying and harmony operations while keeping the example quick to execute; we record the mean loss per epoch for the first figure.
torch.manual_seed(42)
model = Seq2Seq(len(itos))
criterion = nn.CrossEntropyLoss(ignore_index=PAD)
optimizer = torch.optim.Adam(model.parameters(), lr=3e-3)
loader_generator = torch.Generator().manual_seed(42)
train_loader = DataLoader(
TensorDataset(src, dec_in, target),
batch_size=128,
shuffle=True,
generator=loader_generator,
)
losses = []
for epoch in range(60):
model.train()
epoch_loss = 0.0
for batch_src, batch_dec_in, batch_target in train_loader:
optimizer.zero_grad()
logits = model(batch_src, batch_dec_in, tf_ratio=1.0)
loss = criterion(
logits.reshape(-1, len(itos)), batch_target.reshape(-1)
)
loss.backward()
torch.nn.utils.clip_grad_norm_(model.parameters(), 1.0)
optimizer.step()
epoch_loss += loss.item() * batch_src.size(0)
losses.append(epoch_loss / len(train_loader.dataset))Initial loss: 3.298 Final loss: 0.002
The loss falls from its initial value to near zero, which is what we expect on a task with a tight, learnable regularity. This near-zero training loss is good news but not yet evidence of generalization, because the model could simply be memorizing the 1,280 training pairs. But the number that matters is exact-match accuracy on stems the model never saw, since a memorizer would ace training and fail there. Let's build the greedy decoder and evaluate.
@torch.no_grad()
def greedy_decode(model, src, max_len):
model.eval()
encoded, h = model.encoder(model.emb(src))
source_mask = src != PAD
B = src.size(0)
token = torch.full((B,), SOS, dtype=torch.long)
hidden = h
outputs = torch.full((B, max_len), PAD, dtype=torch.long)
finished = torch.zeros(B, dtype=torch.bool)
for t in range(max_len):
logits, hidden = model.decode_step(token, hidden, encoded, source_mask)
token = logits.argmax(-1)
outputs[:, t] = token
finished |= token == EOS
if finished.all():
break
return outputs
def ids_to_string(ids):
chars = []
for idx in ids.tolist():
if idx == EOS:
break
if idx != PAD:
chars.append(itos[idx])
return "".join(chars)
def evaluate(pairs):
src_t, _, _ = build_tensors(pairs)
max_len = max(max_out, max(len(t) for _, t in pairs) + 1)
outputs = greedy_decode(model, src_t, max_len)
correct = sum(
int(ids_to_string(outputs[i]) == gold)
for i, (_, gold) in enumerate(pairs)
)
return correct / len(pairs)
train_acc = evaluate(train_pairs)
test_acc = evaluate(test_pairs)Exact-match accuracy on training stems: 100.0% Exact-match accuracy on held-out stems: 64.7%
Exact-match accuracy on training stems: 100.0%
Exact-match accuracy on held-out stems: 64.7%These two numbers are the key empirical result of this chapter. Perfect training accuracy says the model fit the pairs, which is expected but uninformative on its own. The held-out number is the real test: those 80 stems and their 320 inflected forms never appeared in training. An exact-match score of 64.7% is far above what an exact-stem memorizer could achieve, so the model has learned reusable copying and harmony behavior. The 35.3-point gap from training accuracy is equally important: this small attention model has not induced a perfectly systematic rule, and exact copying remains brittle on unfamiliar strings.
Visualizing Training and Generalization
All computation is done, so we can render the training curve and the per-tag generalization comparison from precomputed values. First, the loss curve.

The curve drops steeply in the first epochs and then stays near zero, with a few small local fluctuations. Training loss alone does not reveal which linguistic behavior the model learned, so the held-out comparison matters more.
def accuracy_by_tag(pairs):
return {tag: evaluate([p for p in pairs if tag in p[0]]) for tag in TAGS}
seen_acc = accuracy_by_tag(train_pairs)
unseen_acc = accuracy_by_tag(test_pairs)
Read the suffix differences with care. All four seen-stem bars reach 100%, while held-out accuracy remains between roughly 63% and 68%. The aggregate scores are similar across suffixes, so no single tag stands out as uniquely difficult. They do not, by themselves, identify the main source of errors; the sample below supplies one concrete stem-copying failure. More representative data and a model designed for monotonic transduction would be sensible follow-up experiments, not conclusions supported by this run. The rule-based inflector handles every stem generated by this synthetic setup because its behavior is defined symbolically.
Finally, let's eyeball a few predictions on held-out stems.
sample = random.sample(test_pairs, 6)
src_t, _, _ = build_tensors(sample)
outs = greedy_decode(model, src_t, max(max_out, 14))
predictions = [ids_to_string(outs[i]) for i in range(len(sample))]rocon+GEN predicted=roconun gold=roconun correct riri+PL predicted=ririler gold=ririler correct nükdötöf+PL predicted=nükdötler gold=nükdötöfler WRONG zirzüc+PL predicted=zirzücler gold=zirzücler correct toptaco+DAT predicted=toptacoya gold=toptacoya correct pıla+GEN predicted=pılanın gold=pılanın correct
rocon+GEN predicted=roconun gold=roconun correct
riri+PL predicted=ririler gold=ririler correct
nükdötöf+PL predicted=nükdötler gold=nükdötöfler WRONG
zirzüc+PL predicted=zirzücler gold=zirzücler correct
toptaco+DAT predicted=toptacoya gold=toptacoya correct
pıla+GEN predicted=pılanın gold=pılanın correctFive of these six held-out predictions match exactly. The displayed error deletes part of the stem in nükdötöfler while retaining the correct plural suffix; it is a copying failure, not a harmony or buffer-consonant error. Six examples are useful for inspection but too few to characterize a general error profile.
Contextual Analysis in Practice
Our neural model works on isolated words, which is enough for many analysis tasks. Sentence context becomes important when several analyses survive and must be disambiguated. Many practical pipelines therefore combine contextual part-of-speech or feature prediction with a lemmatization component, but the component order and algorithms differ across toolkits and languages. The code below shows spaCy's user-facing pattern; we mark it eval: false because it requires a separately downloaded model package.
import spacy
nlp = spacy.load("en_core_web_sm")
doc = nlp("The flies were buzzing while she flies to Copenhagen.")
for token in doc:
print(f"{token.text:12s} lemma={token.lemma_:12s} pos={token.pos_}")Running it (after the one-time local install of the model package) shows the two occurrences of flies receive the same lemma but different part-of-speech tags: fly as a plural noun in the first clause, fly as a verb in the second. That distinction comes from sentence context, which an isolated-word analyzer lacks. spaCy's internal pipeline is not the weighted-FST-plus-HMM/CRF architecture from Part VI. Such a combination is one possible design: an FST enumerates lexical analyses, and a contextual sequence model ranks them.
Finite-State versus Neural: A Comparison
Having built both demonstrations, we can compare the approaches across the dimensions in Table fst-vs-neural.
| Dimension | Finite-state | Neural seq2seq |
|---|---|---|
| Resource required | Expert-written lexicons and rules | Parallel (lemma, features, form) data |
| Correctness | Exact relative to the encoded grammar | Statistical; errors depend on data and model |
| Reversibility | The relation can be inverted | Reverse mapping usually needs separate training or decoding |
| Ambiguous input | Can enumerate all licensed analyses | A basic decoder returns one best output; alternatives require explicit decoding |
| Unseen stems | Handled when the grammar licenses the stem and its alternations | Generalizes from training distribution |
| New suffix or language | New linguistic engineering | New representative data and training |
| Interpretability | Rules are human-readable | Requires probing and analysis |
| Non-concatenative morphology (Semitic root-and-pattern) | Supported with specialized encodings | Learnable from string pairs when data and architecture suffice |
Arabic and Hebrew illustrate why the last row needs nuance. Root-and-pattern morphology interleaves consonantal roots and vocalic patterns: the Arabic root k-t-b is related to forms such as kataba and kitāb. Finite-state systems can model such patterns with appropriate lexical representations and transducers. Neural character models can learn them from examples, but results depend on the data coverage and representation as well as the evaluation conditions.
The paradigms are complementary. Finite-state machines provide grammar-relative guarantees and can be inverted; their behavior on encoded stems and rules is transparent, but coverage requires expert labor. Neural models learn from data and can generalize to unseen strings, though they may produce illegal-looking or unattested forms. Hybrid systems are an active engineering and research pattern: a neural model can propose an analysis or generation, while an FST supplies lexical candidates or hard constraints.
Limitations and Impact
Finite-state morphology's deepest limitation is not mathematical but human: the lexicon bottleneck. Irregular forms, exceptional stems, continuation classes, and morphophonological alternations must be represented and tested. The engineering effort grows with lexical breadth and alternation complexity, whether the language has a few broad inflectional classes with many irregular lexemes or richer systems of noun classes and stem alternations. Coverage gaps are silent failures: an analyzer may return no analysis for an out-of-lexicon word, and it cannot by itself decide whether the word is rare, new, a typo, or a proper noun. Weighting ranks existing paths but does not create missing ones. Two-level constraints avoid sequential rule ordering, but complex constraint interactions, lexical exceptions, and incomplete test coverage can still produce subtle bugs. Formal correctness remains conditional on the encoded grammar.
Neural morphology trades that brittleness for different problems. It needs parallel data, which is scarce for many morphologically rich languages. It can generate forms that violate phonotactic or inflectional patterns, particularly for rare feature combinations, and a plain seq2seq architecture does not distinguish licensed from unlicensed outputs. Exact-match accuracy also hides the distribution of errors: a score of 90 percent does not identify which constructions make up the remaining 10 percent. When reading inflection benchmarks, inspect the split design, frequency distribution, per-feature results, and irregular or otherwise difficult subsets rather than relying on one aggregate number.
The impact of this chapter's machinery is broad and often invisible. Spell checkers and hyphenators use several architectures; so do text-to-speech systems, search normalization, and machine-translation pipelines. These systems may be finite-state, statistical, neural, or hybrid, with morphological analysis as one component. For neural language models, morphology is entangled with the tokenization you studied in Part V: a vocabulary poorly matched to a morphologically rich language can split words into more tokens, increasing sequence length and changing the evidence available for learning inflectional patterns. We will revisit that problem for multilingual models in Part XXVI.
Summary
You now have a working command of both paradigms of morphological computation.
- Morphological analysis maps surface words to candidate (lemma, feature bundle) analyses, and generation maps analyses to licensed forms. Either direction can be relational rather than one-to-one; weights and context help rank alternatives.
- Finite-state transducers model morphotactics and morphophonology as regular relations. They are closed under composition and inversion, so one encoded relation can support analysis and generation. Their guarantees are exact relative to the grammar. Two-level constraints describe lexical–surface correspondences in parallel rather than as an ordered cascade.
- Weighted FSTs rank competing analyses with negative log probabilities and pick the minimum-weight path, a transducer-relative of Viterbi decoding.
- Neural sequence-to-sequence models learn inflection from character-level parallel data. Attention or explicit copy mechanisms can help preserve stems, but generalization must be measured on held-out data.
- The paradigms are complementary: rule systems offer grammar-relative guarantees and interpretability, neural systems learn coverage from data, and hybrid designs can combine candidates, learned scores, and hard constraints.
Key Parameters
The key parameters for the neural seq2seq inflector are:
- emb_dim = 32: Dimension of the character embeddings.
- hid_dim = 64: Hidden-state size of both GRUs. The decoder's attention can access the full sequence of encoder states, so this is not a single-vector stem bottleneck.
- tf_ratio = 1.0: Probability of teacher forcing at each training decoder step in this experiment. Lower values expose the decoder to its own earlier predictions, but do not by themselves guarantee better copying.
- lr = 3e-3: Learning rate for the Adam optimizer used for the 60-epoch run.
In the next chapter we climb one level of linguistic structure, from the inside of words to the arrangement of words into phrases, with grammars and constituency structure. When we reach dependency syntax and Universal Dependencies later in this part, you will see these morphological feature bundles attached to the tokens for which the relevant features are annotated; a token's FEATS field may also be empty.
Quiz
Ready to test your understanding? Take this quick quiz to reinforce what you've learned about morphological analysis, finite-state transducers, and neural inflection.
Morphological Analysis and Generation Quiz
Reference
Citation details
Cite or share this article.
Continue with the full handbook
This chapter is part of Language AI Handbook. Use the handbook page to browse the complete table of contents and continue reading in sequence.
Explore Language AI HandbookStay up to date
Get articles, book updates, and news delivered to your inbox.
No spam, unsubscribe anytime.
Join the community
Sign in to remove popups, track your reading progress, and join the discussion.

Comments
No comments yet. Be the first to share your thoughts!