Lexical Semantics and Word Senses › Implementing Word-Sense Disambiguation
Language AI HandbookAll chapters Lexical Semantics and Word Senses › Implementing Word-Sense DisambiguationSection view · Sections in this chapter7 / 11 ↓
Reading tools Definitions, links & notes
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
We can now build a small WSD pipeline with NLTK and scikit-learn. The goal is to illustrate the mechanics, not to produce a state-of-the-art system. We will inspect WordNet, implement simplified Lesk, build a gloss vector baseline, and compare against the first-WordNet-sense proxy on a tiny hand-built test set. The code is intentionally simple so that each step is visible. Before production use, evaluate on a larger representative target-domain set and choose the preprocessing and model according to the measured requirements. The teaching value comes from seeing how the pieces fit together.
Use the book's Python 3.11 environment for the notebook kernel. Run the notebook with its kernel working directory in the repository checkout so the setup cell can find the shared plotting helper. Install the required WordNet data before running the notebook if it is missing. The commented download calls show one way to fetch resources over the network. The hidden setup cell at the start of the chapter loads the shared plotting helper and sets the seed.
PythonIn [7]Expand
import re
from collections import Counter
import nltk
# nltk.download("wordnet", quiet=True) # Run once locally, then comment out
# nltk.download("omw-1.4", quiet=True) # Run once locally, then comment out
from nltk.corpus import wordnet as wn
from nltk.stem import PorterStemmerThe examples below require WordNet 3.0. The first lookup checks the installed release before inspecting the noun senses of bank. The first listed noun synset is bank.n.01, the riverbank sense. We use the resource order for the first-WordNet-sense baseline. The sense numbers alone give no reliable estimate of corpus frequencies. The glosses show how WordNet separates the financial, river, and other meanings.
PythonIn [8]Expand
if wn.get_version() != "3.0":
raise ValueError("These examples require WordNet 3.0")
bank_synsets = wn.synsets("bank", pos=wn.NOUN)ConsoleOut [9]Expand
Number of noun synsets for 'bank': 10 bank.n.01 sloping land (especially the slope beside a body of water) depository_financial_institution.n.01 a financial institution that accepts deposits and channels the money into lending activities bank.n.03 a long ridge or pile bank.n.04 an arrangement of similar objects in a row or in tiers bank.n.05 a supply or stock held in reserve for future use (especially in emergencies) bank.n.06 the funds held by a gambling house or the dealer in some gambling games bank.n.07 a slope in the turn of a road or track; the outside is higher than the inside in order to reduce the effects of centrifugal force savings_bank.n.02 a container (usually with a slot in the top) for keeping money at home bank.n.09 a building in which the business of banking transacted bank.n.10 a flight maneuver; aircraft tips laterally about its longitudinal axis (especially in turning)
The output lists each synset name and its definition. You can see that WordNet distinguishes the sloping land sense, the financial institution sense, the building sense, the flight maneuver sense, and several others. This is exactly the kind of fine-grained inventory that makes WSD both informative and difficult. The number of senses is not a property of the word in isolation. It is a property of the word relative to the inventory. A different inventory might list fewer or more senses. The output therefore shows both the richness of WordNet and the challenge of choosing among its categories.
We can also inspect the lexical structure of a synset: its lemma names, examples, and hypernyms.
ConsoleOut [10]Expand
Synset: bank.n.01 Lemmas: bank Examples: ['they pulled the canoe up on the bank', 'he sat on the bank of the river and watched the currents'] Hypernyms: ['slope.n.01'] Synset: depository_financial_institution.n.01 Lemmas: depository_financial_institution, bank, banking_concern, banking_company Examples: ['he cashed a check at the bank', 'that bank holds the mortgage on my home'] Hypernyms: ['financial_institution.n.01'] Synset: bank.n.03 Lemmas: bank Examples: ['a huge bank of earth'] Hypernyms: ['ridge.n.01']
The lemma names show the synonyms that WordNet groups under the concept. The examples provide usage contexts. The hypernyms place the sense in a hierarchy. For the financial sense, the direct hypernym is financial_institution.n.01, whose lemmas include financial organization. For the riverbank sense, the direct hypernym is slope.n.01. The hypernyms are useful because they provide additional vocabulary and because they place the sense in a broader semantic network. A Lesk system can use hypernym glosses as extra evidence. A graph-based system can use hypernym links to connect senses. A human reader can use the hierarchy to understand how WordNet organizes the lexicon.
We can count candidate synsets for selected word forms in the installed inventory. The ten forms below were chosen to illustrate variation, so their counts do not estimate the distribution across the vocabulary. Counts also combine parts of speech and depend on resource granularity. To study a population-level relationship between usage frequency and sense count, define a sampling frame and collect those quantities separately.
PythonIn [11]Expand
words = [
"bank",
"run",
"light",
"plant",
"bass",
"spring",
"head",
"line",
"charge",
"table",
]
polysemy = {w: len(wn.synsets(w)) for w in words}
polysemy_sorted = sorted(polysemy.items(), key=lambda item: -item[1])ConsoleOut [12]Expand
run 57 synsets light 47 synsets head 42 synsets charge 40 synsets line 36 synsets bank 18 synsets spring 11 synsets plant 10 synsets bass 9 synsets table 8 synsets
The counts vary among these selected forms. Run has dozens of candidate synsets across nouns and verbs; bass has fewer, including the fish and low-sound meanings. Candidate count is not sense frequency. A form can have many inventory senses while one dominates a corpus, or few senses with balanced frequencies. These ten chosen forms therefore do not establish the vocabulary-wide ambiguity distribution or predict an MFS baseline's accuracy.
Before implementing Lesk, we need a tokenizer and a stemmer. We use a simple regex tokenizer and the Porter stemmer. The stopword list is deliberately small and general. For the intended domain and language, check which cues the stopword policy removes and change it when those checks show a need. A larger stopword list can reduce spurious matches but may remove words that are useful for disambiguation. A domain-specific stopword list might remove words that are common in the domain but uninformative. The tokenizer also matters. A regex tokenizer that extracts alphabetic sequences is simple, but it ignores punctuation and multiword expressions. A richer preprocessing pipeline can preserve relevant hyphens, handle contractions, and identify named-entity spans. For a teaching example, the simple tokenizer is sufficient.
PythonIn [13]Expand
STOPWORDS = set(
"""
a an the and or but if while of to in on at by for with from as is are was were be been being
this that these those it its he she they them his her their you your we our i my me
not no nor so than then there here when where who whom which what how why
""".split()
)
stemmer = PorterStemmer()
def content_words(text):
tokens = re.findall(r"[a-z]+", text.lower())
return [stemmer.stem(t) for t in tokens if t not in STOPWORDS]The content_words function lowercases the text, extracts ASCII alphabetic strings, removes its stopword set, and applies Porter stemming. This matches some inflections but may conflate distinct words or return a string that is not a conventional lemma. In the installed NLTK example, both universe and university stem to univers. Lemmatization instead aims at a lexicographic base form; methods can use lexical entries, morphology, part of speech, or context. Compare the actual normalization errors and runtime on the intended data before choosing a method.
Now we implement simplified Lesk. For each candidate synset, we build a gloss document from its definition, examples, and optionally its hypernym definitions. We compute the overlap with the context and sort the candidates by score. The function discards the target’s stem from the context set. Any other token with the same stem loses its feature too. If universe appears in the sentence and university is the target, discarding univers removes their shared feature. This suppresses a self-match when a candidate signature contains the target stem. The function returns the matching stems as well as the count, which makes the result interpretable. A human can inspect the evidence and see why a sense was chosen.
PythonIn [14]Expand
def lesk_scores(word, sentence, use_hypernyms=True):
context = set(content_words(sentence))
context.discard(stemmer.stem(word.lower()))
scores = []
for syn in wn.synsets(word):
gloss_text = syn.definition() + " " + " ".join(syn.examples())
if use_hypernyms:
for hypernym in syn.hypernyms():
gloss_text += " " + hypernym.definition()
gloss = set(content_words(gloss_text))
overlap = context & gloss
scores.append((syn, len(overlap), sorted(overlap)))
return sorted(scores, key=lambda item: (-item[1], item[0].name()))The function returns a list of tuples containing the synset, the overlap count, and the overlapping words. It sorts by descending score, then orders tied senses by synset name. The simplified_lesk wrapper takes the first ranked sense. Ordering ties by name makes the example reproducible without adding semantic evidence. If candidates are available and every overlap is zero, the wrapper still returns the first candidate by name. A system could use a prior or another source of evidence to break ties.
The helper has no target index or span: it returns one ranking for a word form in the supplied sentence. If that sentence contains two uses of the same form with different intended readings, the interface cannot label them separately. The six cases below each contain one target use. Occurrence-level prediction needs a target-alignment policy, such as marked spans or token-aligned outputs.
Let's apply the function to two sentences, one financial and one riverbank.
PythonIn [15]Expand
example_sentences = [
"She deposited her paycheck at the bank before noon.",
"The fisherman sat on the bank of the river and watched the current.",
]
lesk_examples = [
(sentence, lesk_scores("bank", sentence)[:3])
for sentence in example_sentences
]ConsoleOut [16]Expand
She deposited her paycheck at the bank before noon. deposit.v.02 score=2 overlap=['deposit', 'paycheck'] bank.n.09 score=1 overlap=['deposit'] depository_financial_institution.n.01 score=1 overlap=['deposit'] The fisherman sat on the bank of the river and watched the current. bank.n.01 score=4 overlap=['current', 'river', 'sat', 'watch'] bank.n.03 score=0 overlap=[] bank.n.04 score=0 overlap=[]
In the locked WordNet 3.0 example, the first sentence ranks the verb sense deposit.v.02 highest, with overlaps deposit and paycheck. Both matches come from the verb’s WordNet usage example, which closely resembles the input sentence. The second ranks the noun sense bank.n.01 highest, with sat, river, watch, and current. The first result exposes the cost of admitting all parts of speech when the target occurrence is a noun. Keeping only nouns with the same scoring rule would leave bank.n.09 and depository_financial_institution.n.01 tied at 1. The name-based tie-break would pick the building sense, bank.n.09. In the river sentence, the correct noun leads. In the financial sentence, the wrong verb also leads, so a score gap alone cannot establish that the top sense is correct. When the available cues are weak, several candidates can have similar scores. The shared words show what counted. Check them against the sentence and the candidate definition to see whether they support the intended reading.
We can wrap the ranking in a simple classifier that returns the top sense.
PythonIn [17]Expand
def simplified_lesk(word, sentence):
ranked = lesk_scores(word, sentence)
if not ranked:
raise ValueError(f"No WordNet candidates for {word!r}")
return ranked[0][0]This small knowledge-based WSD system requires no sense-tagged training data and estimates no probabilistic model. It still has configuration choices: preprocessing, signature expansion, candidate scope, and tie-breaking. It is a useful baseline and a transparent teaching tool. The score counts shared stems without modeling syntax or word order. Direct hypernym links can still affect which definitions enter the signature. It depends on the quality of the glosses and the preprocessing. But it illustrates the core idea: compare the context with the sense definitions and choose the best match.
Next, we build a gloss vector baseline. Instead of counting exact overlaps, we represent each gloss as a TF-IDF vector and compare it with the context vector using cosine similarity. This weights exact vocabulary matches and normalizes their vector lengths. It does not match unseen synonyms or reconcile inflections unless we add suitable preprocessing. With term frequency held fixed, the default inverse-document-frequency factor is larger for a term occurring in fewer gloss documents. The vectorizer then normalizes each row, so document frequency alone does not determine every final feature weight. The configured English stopword filter removes words such as the and of before weighting. For a retained token such as river, TF-IDF supplies a corpus-dependent weight; that weight alone does not establish semantic relevance. Cosine similarity compares the direction of the two sparse vectors, but shared vector features still require matching tokens with the vectorizer used here.
PythonIn [18]Expand
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity
def gloss_documents(word):
docs = []
synsets = []
for syn in wn.synsets(word):
docs.append(syn.definition() + " " + " ".join(syn.examples()))
synsets.append(syn)
return docs, synsets
docs, synsets = gloss_documents("bank")
vectorizer = TfidfVectorizer(stop_words="english")
gloss_matrix = vectorizer.fit_transform(docs)
query = "She deposited her paycheck at the bank before noon."
query_vector = vectorizer.transform([query])
similarities = cosine_similarity(query_vector, gloss_matrix).ravel()
order = similarities.argsort()[::-1]ConsoleOut [19]Expand
deposit.v.02 similarity=0.558 put into a bank account bank.v.03 similarity=0.211 do business with a bank or keep an account at a bank bank.v.02 similarity=0.168 enclose with a bank
The vector method ranks senses by cosine similarity between the context and the gloss. It is still a knowledge-based method, but it uses weighted sparse vectors whose nonzero similarity still depends on matching token features. The similarity scores are not probabilities, and they are not calibrated. They are ranking scores. A score of 0.3 does not mean a 30% chance that the sense is correct. It means that the context and the gloss have a certain cosine similarity in the TF-IDF space. This example sends the full query, including bank, to the vectorizer and builds candidate documents from definitions and examples without hypernyms. Only bank and paycheck survive the projection into the fitted vocabulary. The two other displayed verbs share only bank with the query, which gives both positive cosine scores. The wrong verb deposit.v.02 ranks first, with a displayed cosine of 0.558. An isolated comparison of the scoring rules would keep the inputs fixed. The method can still fail if the gloss does not contain the relevant words or if the context is too short. It can also be sensitive to the vectorizer's tokenization and stopword settings.
Now we need an evaluation with a fixed denominator. We pin this demonstration to WordNet 3.0 and list explicit gold synset names. In the installed WordNet 3.0 data, searching bank definitions for the string bank matches several candidate entries. Such a search does not identify one gold sense, and a missing match must not silently remove a test case. Before scoring a case, the evaluator resolves its gold synset name and checks that the synset is a candidate for the target word.
PythonIn [20]Expand
def first_wordnet_sense(word, sentence=None):
candidates = wn.synsets(word)
if not candidates:
raise ValueError(f"No WordNet candidates for {word!r}")
return candidates[0]
def evaluate(method, test_cases):
if wn.get_version() != "3.0":
raise ValueError("This fixed gold set requires WordNet 3.0")
if not test_cases:
raise ValueError("The test set must not be empty")
correct = 0
rows = []
for word, sentence, gold_name in test_cases:
gold = wn.synset(
gold_name
) # Missing identifiers raise an explicit error.
if gold not in wn.synsets(word):
raise ValueError(
f"Gold {gold_name!r} is not a candidate for {word!r}"
)
pred = method(word, sentence)
is_correct = pred == gold
correct += int(is_correct)
rows.append((word, sentence, gold.name(), pred.name(), is_correct))
return correct, len(test_cases), rowsThe evaluator now either scores every supplied case or raises an error. The predictor should return a synset from the installed WordNet 3.0 reader. Both demonstration predictors select from the target’s candidate list. The evaluator checks gold membership. If another predictor returns a valid WordNet synset outside the target’s candidates, it counts as an incorrect prediction. Synset-name strings are unsupported. The evaluator compares the prediction with the resolved gold synset before calling the prediction’s name() method for the result row. It returns the correct count, the intended test-set size, and row details for error analysis. Synset names are useful identifiers within this pinned resource; they are not a guarantee that an identifier has identical meaning across WordNet releases.
We define six test cases covering the financial, riverbank, and flight maneuver senses of bank, and the industrial and biological senses of plant. These hand-built examples illustrate the evaluation procedure. Their intended gold readings are stated explicitly, while the all-POS candidate list still lets the scorer choose an inappropriate verb. A representative evaluation would need a sampling protocol covering the words, parts of speech, and usage conditions relevant to the application. Here we compare only these six cases with the first-WordNet-sense proxy.
PythonIn [21]Expand
test_cases = [
(
"bank",
"She deposited her paycheck at the bank before noon.",
"depository_financial_institution.n.01",
),
(
"bank",
"The fisherman sat on the bank of the river and watched the current.",
"bank.n.01",
),
(
"bank",
"The pilot felt the bank of the aircraft as it turned.",
"bank.n.10",
),
(
"plant",
"The plant manufactures electric cars on three assembly lines.",
"plant.n.01",
),
(
"plant",
"Biologists classify the plant as a living organism that absorbs sunlight.",
"plant.n.02",
),
(
"plant",
"Water the plant every morning and keep it near the window.",
"plant.n.02",
),
]
lesk_correct, lesk_total, lesk_rows = evaluate(simplified_lesk, test_cases)
first_sense_correct, first_sense_total, first_sense_rows = evaluate(
first_wordnet_sense, test_cases
)
lesk_accuracy = lesk_correct / lesk_total
first_sense_accuracy = first_sense_correct / first_sense_totalConsoleOut [22]Expand
Simplified Lesk: 4/6 = 0.667 First WordNet sense: 2/6 = 0.333 ERR word=bank gold=depository_financial_institution.n.01 pred=deposit.v.02 OK word=bank gold=bank.n.01 pred=bank.n.01 OK word=bank gold=bank.n.10 pred=bank.n.10 OK word=plant gold=plant.n.01 pred=plant.n.01 OK word=plant gold=plant.n.02 pred=plant.n.02 ERR word=plant gold=plant.n.02 pred=establish.v.02
In WordNet 3.0 this implementation gets four of the six cases correct; the first-WordNet-sense baseline gets two correct. Both use the same six gold labels and admit all parts of speech. The financial bank sentence is a Lesk error: a verb sense wins despite the noun target. The set was selected for illustration, so these fractions describe these examples only. They do not estimate performance on a representative WSD benchmark.
In the last plant case, Lesk selects establish.v.02. The gold label is the biological sense plant.n.02. To investigate the error, inspect the candidate scores and shared words for that sentence. A model that has seen many examples of plant in gardening contexts can learn that water and window are associated with the biological sense, even if the gloss does not mention them. The example shows the limits of gloss-based knowledge. Glosses are compact and may not include the words that are most diagnostic in real usage. Supervised data can fill that gap. A classifier that uses contextual token representations can also draw on words in the sentence that are absent from the gloss.
Let's visualize the Lesk scores for the financial sentence. The figure shows which of the five displayed candidates receive lexical support. A bar chart is useful here because the scores are discrete counts. The length of each bar shows how many content words overlap between the sentence and the candidate signature, which here combines its definition, examples, and direct hypernym definitions. The figure makes the sparse nature of Lesk evidence visible. In the full candidate list, many senses have zero overlap. A few senses have some overlap. For this sentence the highest-scoring candidate is the incorrect verb deposit.v.02, so a high overlap score alone does not establish a correct sense.
PythonIn [23]Expand
score_sentence = "She deposited her paycheck at the bank before noon."
ranked_plot_scores = lesk_scores("bank", score_sentence)[:5]
score_labels = [syn.name() for syn, _, _ in ranked_plot_scores][::-1]
score_values = [score for _, score, _ in ranked_plot_scores][::-1]
score_colors = ["primary"] * (len(score_labels) - 1) + ["teal"]
score_ticks = list(range(max(score_values) + 1))VisualizationOut [24]Expand

The chart exposes a candidate-selection problem: the code admits verb senses, so a verb can outrank the noun intended in this sentence. Other candidates have zero overlap. The figure makes the sparse, discrete nature of Lesk evidence visible. TF-IDF can produce fractional scores, but it can also produce exact zeros when the vectors share no token features. Both methods expose lexical support; neither represents synonymy automatically. The choice between them depends on the task and the resource.
We can also visualize the candidate counts for our ten selected forms. Their all-POS synset counts vary, and noun and verb senses are counted together. This deliberately ambiguous sample shows differences among the selected forms. It cannot establish a vocabulary-wide distribution, a sense-frequency prior, or the accuracy of an MFS baseline; those require different measurements.
PythonIn [25]Expand
polysemy_items = sorted(polysemy.items(), key=lambda item: item[1])
words_sorted = [word for word, _ in polysemy_items]
counts_sorted = [count for _, count in polysemy_items]VisualizationOut [26]Expand

Finally, we compare the accuracy of simplified Lesk and the first-WordNet-sense proxy on the tiny test set. The comparison is not a real benchmark. The set is small and selected for illustration, so it cannot estimate representative benchmark performance. But the comparison illustrates the evaluation logic. For a study of context against training-set frequency, include a training-data MFS baseline as described above. Always report the inventory, the test set, and the evaluation conditions. A lower aggregate score does not establish an accuracy gain over the evaluated baseline; other benefits require separate evidence. A system that beats a proxy on a tiny set may still fail on a larger set. The comparison is a starting point, not a conclusion.
PythonIn [27]Expand
method_names = ["First WordNet sense", "Simplified Lesk"]
toy_accuracies = [first_sense_accuracy, lesk_accuracy]
accuracy_labels = [f"{value:.1%}" for value in toy_accuracies]VisualizationOut [28]Expand

These heights come from the six examples scored with WordNet 3.0. The evaluator rejects other releases, so this run cannot compare versions. The methodological point is to name the baseline actually evaluated, include a training-data MFS comparison when testing the value of context, and report the inventory, the test set, and the evaluation conditions. A figure with two bars is not evidence of state-of-the-art performance. It is evidence that the pipeline runs and that the comparison is possible. The real value of the implementation is the transparency. You can inspect the synsets, the glosses, the overlap words, and the scores. You can see where the method succeeds and where it fails. That transparency is useful for learning and for debugging.
Comments
Add to the conversation
A question, a correction, or an example from your own work.
No comments yet. Be the first to share your thoughts!