Computation for Linguists

spaCy & WordNet

Dr. Andrew M. Byrd

2025-11-07

Review

  • What did you learn last time?

Recap from Last Time

import spacy
nlp = spacy.load("en_core_web_sm")

text = "Dr. Byrd's students can't wait to analyze PIE roots!"

doc = nlp(text)
[t.text for t in doc]

Recap from Last Time

import spacy
import pandas as pd

# Load English model
nlp = spacy.load("en_core_web_sm")

text = "Dr. Byrd's students can't wait to analyze PIE roots!"
doc = nlp(text)

# Create list of dicts, one per token
data = []
for t in doc:
    data.append({
        "text": t.text,
        "lemma": t.lemma_,
        "POS": t.pos_,
        "tag": t.tag_,
        "stop": t.is_stop,
        "is_punct": t.is_punct
    })

# Make DataFrame
df = pd.DataFrame(data)
print(df)

Recap from Last Time

import spacy

content_lemmas = [t.lemma_.lower() for t in doc
                  if not (t.is_stop or t.is_punct or t.is_space or t.like_num)]

Recap from Last Time

from collections import Counter
import pandas as pd

freq = Counter(content_lemmas)
df_freq = (pd.DataFrame(freq.items(), columns=["lemma", "count"])
           .sort_values("count", ascending=False))
df_freq.head(10)

Recap from Last Time

import matplotlib.pyplot as plt

top10 = df_freq.head(10)
plt.figure()
plt.bar(top10["lemma"], top10["count"])
plt.title("Top 10 Content Lemmas")
plt.xlabel("Lemma")
plt.ylabel("Count")
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()

Recap from Last Time

import spacy 
import pandas as pd

# Load English model
nlp = spacy.load("en_core_web_sm")

text2 = "President Pitzer, Mr. Vice President, Governor Connally, ladies and gentlemen: I am delighted to be here today. We meet in an hour of change and challenge."
doc2 = nlp(text2)
[s.text for s in doc2.sents]

Recap from Last Time

import spacy
from nltk.corpus import wordnet as wn
from nltk.corpus.reader.wordnet import NOUN, VERB, ADJ, ADV

# Load spaCy
nlp = spacy.load("en_core_web_sm")

# Sentence with both noun and verb "bear"
text = "The bears bear their burdens bravely."
doc = nlp(text)

# Map spaCy POS tags to WordNet POS tags -- this is a **function**, we'll get to these soon
def get_wordnet_pos(spacy_pos):
    if spacy_pos.startswith("N"):
        return NOUN
    elif spacy_pos.startswith("V"):
        return VERB
    elif spacy_pos.startswith("J"):
        return ADJ
    elif spacy_pos.startswith("R"):
        return ADV
    return None

# Loop through tokens and look up WordNet entries
for token in doc:
    wn_pos = get_wordnet_pos(token.tag_)
    lemma = token.lemma_.lower()

    if wn_pos and not token.is_stop and not token.is_punct:
        synsets = wn.synsets(lemma, pos=wn_pos)
        print(f"\n{token.text.upper()} ({token.pos_}) → lemma: {lemma}")
        for s in synsets[:3]:  # show just the first 3 senses
            print(f"  - {s.definition()}  [examples: {s.examples()}]")

Recap from Last Time

import spacy
from spacy_wordnet.wordnet_annotator import WordnetAnnotator

nlp = spacy.load("en_core_web_sm")
# Attach the WordNet annotator; it will use the NLTK WordNet data you downloaded
nlp.add_pipe("spacy_wordnet", after="tagger")

Review Activity

  • Copy the below string, and after filtering out stopwords and non-words

    • identify the top 10 most frequent words.

    • select the first sentence using spaCy’s attitude .sents

    • parse each word by meaning using WordNet.

getty = "Four score and seven years ago our fathers brought forth on this continent, a new nation, conceived in Liberty, and dedicated to the proposition that all men are created equal.Now we are engaged in a great civil war, testing whether that nation, or any nation so conceived and so dedicated, can long endure. We are met on a great battle-field of that war. We have come to dedicate a portion of that field, as a final resting place for those who here gave their lives that that nation might live. It is altogether fitting and proper that we should do this.But, in a larger sense, we can not dedicate—we can not consecrate—we can not hallow—this ground. The brave men, living and dead, who struggled here, have consecrated it, far above our poor power to add or detract. The world will little note, nor long remember what we say here, but it can never forget what they did here. It is for us the living, rather, to be dedicated here to the unfinished work which they who fought here have thus far so nobly advanced. It is rather for us to be here dedicated to the great task remaining before us—that from these honored dead we take increased devotion to that cause for which they gave the last full measure of devotion—that we here highly resolve that these dead shall not have died in vain—that this nation, under God, shall have a new birth of freedom—and that government of the people, by the people, for the people, shall not perish from the earth."

Wordnet-onyms

WordNet is More than Synsets

  1. antonyms

  2. hypernyms

  3. hyponyms

  4. meronyms

  5. holonyms

  6. entailments

Finding Antonyms

def get_antonyms(word): # this is also a function
    antonyms = set() # A set is like a list, but prevents duplicates
    for syn in wn.synsets(word):
        for lemma in syn.lemmas():  # analyzes each lemma associated with the token
            for ant in lemma.antonyms():
                antonyms.add(ant.name())
    return list(antonyms)

get_antonyms("slow")
  • Let’s try some other words to run in our get_antonyms() function.

Finding Hypernyms & Hyponyms

  • A hypernym encompasses its hyponym
    • animal > mammal > canine > dog, etc.
import spacy
from spacy.tokens import Token
from nltk.corpus import wordnet as wn

nlp = spacy.load("en_core_web_sm")

# Register a getter-based extension
def get_synsets(token):
    # You can filter by POS if you want, e.g., pos=wn.NOUN for nouns
    return wn.synsets(token.lemma_.lower())

Token.set_extension("synsets", getter=get_synsets, force=True)

doc = nlp("dog")
token = doc[0]

word_synsets = token._.synsets  # now this works
for synset in word_synsets:
    hypernym_names = [h.name() for h in synset.hypernyms()]
    hyponym_names  = [h.name() for h in synset.hyponyms()]
    print(f"\nSense: {synset.name()}")
    print(f"  Hypernyms: {hypernym_names}")
    print(f"  Hyponyms:  {hyponym_names}")

Part Meronyms & Holonyms

  • We can also extend this script to meronyms & holonyms
    • Meronyms: car → wheel
    • Holonyms: wheel → car
import spacy
from spacy.tokens import Token
from nltk.corpus import wordnet as wn

# Creating a function to "get" synsets
def get_synsets(token):
    # You can filter by POS if you want, e.g., pos=wn.NOUN for nouns
    return wn.synsets(token.lemma_.lower())

Token.set_extension("synsets", getter=get_synsets, force=True)

doc = nlp("dog")
token = doc[0]

word_synsets = token._.synsets  # now this works
for synset in word_synsets:
    hypernym_names = [h.name() for h in synset.hypernyms()]
    hyponym_names  = [h.name() for h in synset.hyponyms()]
    mero_names = [h.name() for h in synset.part_meronyms()]
    holo_names = [h.name() for h in synset.part_holonyms()]
    print(f"\nSense: {synset.name()}")
    print(f"  Hypernyms: {hypernym_names}")
    print(f"  Hyponyms:  {hyponym_names}")
    print(f"  Part Meronyms: {mero_names}")
    print(f"  Part Holonyms:  {holo_names}")

Entailments

  • And entailments:
    • Entailments: snore → sleep
import spacy
from spacy.tokens import Token
from nltk.corpus import wordnet as wn

# Creating a function to "get" synsets
def get_synsets(token):
    # You can filter by POS if you want, e.g., pos=wn.NOUN for nouns
    return wn.synsets(token.lemma_.lower())

Token.set_extension("synsets", getter=get_synsets, force=True)

doc = nlp("dog")
token = doc[0]

word_synsets = token._.synsets  # now this works
for synset in word_synsets:
    hypernym_names = [h.name() for h in synset.hypernyms()]
    hyponym_names  = [h.name() for h in synset.hyponyms()]
    mero_names = [h.name() for h in synset.part_meronyms()]
    holo_names = [h.name() for h in synset.part_holonyms()]
    ent_names = [h.name() for h in synset.entailments()]
    print(f"\nSense: {synset.name()}")
    print(f"  Hypernyms: {hypernym_names}")
    print(f"  Hyponyms:  {hyponym_names}")
    print(f"  Part Meronyms: {mero_names}")
    print(f"  Part Holonyms:  {holo_names}")
    print(f"  Entailments:  {ent_names}")

Activity:

  • Copy the above code and analyze (1) a noun, (2) a verb, and (3) an adjective of your choice.

Similarity Scores

Wu–Palmer (WUP) Similarity

  • Goal: measure how semantically close two WordNet concepts (synsets) are.
  • Idea: the more specific their common ancestor, the more similar they are.

Wu–Palmer (WUP) Similarity

Key Terms

  • LCS (Lowest Common Subsumer): the most specific shared ancestor in WordNet’s hierarchy.
  • Depth: how far a synset is from the root (the more specific, the deeper).

\(wup\_similarity(s_1, s_2) = \frac{2 \times depth(LCS)}{depth(s_1) + depth(s_2)}\)

Wu–Palmer (WUP) Similarity

  • Let’s compare two synsets dog.n.01 and cat.n.01; these are s\(_1\), s\(_2\), respectively
  • Remember LCS (Lowest Common Subsumer) is the most specific shared ancestor in WordNet’s hierarchy
  • And depth determines how deep that entity is within the WordNet hierarchy

\(wup\_similarity(s_1, s_2) = \frac{2 \times depth(LCS)}{depth(s_1) + depth(s_2)}\)

Semantic Similarity

dog = nlp("dog")[0]._.wordnet.synsets()[0]
comparison = nlp("cat")[0]._.wordnet.synsets()[0]

print("wup:",  dog.wup_similarity(comparison))   # Wu–Palmer similarity

Wu–Palmer (WUP) Similarity

Interpretation

  • Values range from 0 → 1
    • 1.0 → identical meaning
    • 0.0 → completely unrelated

Wu–Palmer (WUP) Similarity

entity.n.01
 └── physical_entity.n.01
      └── object.n.01
           └── whole.n.02
                └── living_thing.n.01
                     └── organism.n.01
                          └── animal.n.01
                               └── chordate.n.01
                                    └── vertebrate.n.01
                                         └── mammal.n.01
                                              └── carnivore.n.01
                                                   └── feline.n.01
                                                        └── cat.n.01

Wu–Palmer (WUP) Similarity

entity.n.01
 └── physical_entity.n.01
      └── object.n.01
           └── whole.n.02
                └── living_thing.n.01
                     └── organism.n.01
                          └── animal.n.01
                               └── chordate.n.01
                                    └── vertebrate.n.01
                                         └── mammal.n.01
                                              └── carnivore.n.01
                                                   └── canine.n.02
                                                        └── dog.n.01

Wu–Palmer (WUP) Similarity

\(wup\_similarity(dog, cat) = \frac{2 \times 7}{8 + 8} = \frac{14}{16} = 0.875\)

Activity:

  • Compute the WUP Similarity of:
    • two nouns
    • two verbs
    • two adjectives
    • two words that aren’t the same part of speech

Returning to our Verbs of Violence

Returning to our verbs of violence

import spacy
from spacy.matcher import PhraseMatcher
from nltk.corpus import wordnet as wn

nlp = spacy.load("en_core_web_sm")

# WordNet helpers (via NLTK) – returns only verb synsets
def verb_senses(word):
    return wn.synsets(word, pos='v')  # verb synsets only

# Collects all (recursive) hyponyms for a verb synset (troponyms in WN terms).
# This function recursively collects all *hyponyms* (a.k.a. “troponyms” for verbs)
# of a given synset. A hyponym is a more *specific* instance of an action.
# Example: the verb “attack.v.01” has hyponyms like “bomb.v.01”, “invade.v.01”, etc.
def all_verb_hyponyms(root):
      seen, stack = set(), [root]
    while stack:
        cur = stack.pop()
        if cur in seen:
            continue
        seen.add(cur)
        # For verbs, .hyponyms() are the troponyms
        stack.extend(cur.hyponyms())
    return {s for s in seen if s.pos() == 'v'}

# This function extracts lemma names (the “canonical” word forms)
# from a set of synsets, e.g. 'strike.v.01' → ['strike', 'hit', 'smite', ...].
# Optionally filters out multiword expressions like "shoot_down".
def lemmas_from_synsets(synsets, keep_multiword=False):
    out = set()
    for s in synsets:
        for lem in s.lemmas():
            name = lem.name().lower()
            if not keep_multiword and "_" in name:
                continue
            out.add(name.replace("_", " "))
    return out

# Build a violence lexicon from WordNet using a few intuitive seeds
seed_verbs = ["attack", "assault", "hit", "strike", "punch", "kick", "stab", "shoot", "beat"]
base_synsets = []
for w in seed_verbs:
    ss = verb_senses(w)
    if ss:
        # take the most “central” sense by picking the one with most hyponyms
        ss_scored = sorted(ss, key=lambda s: len(s.hyponyms()), reverse=True)
        base_synsets.append(ss_scored[0])

# expand via hyponyms (troponyms)
expanded = set()
for s in base_synsets:
    expanded |= all_verb_hyponyms(s)

# collect lemmas (single-token by default)
violent_verb_lemmas = sorted(lemmas_from_synsets(expanded, keep_multiword=False) | set(seed_verbs))
print(f"{len(violent_verb_lemmas)} violent verb lemmas (sample):", violent_verb_lemmas[:25])

# 4) spaCy PhraseMatcher by lemma — IMPORTANT: run full pipeline on patterns
matcher = PhraseMatcher(nlp.vocab, attr="LEMMA")
patterns = list(nlp.pipe(violent_verb_lemmas))  # not make_doc: we need lemmas
matcher.add("VIOLENCE", patterns)

An automated list of words

violent_vs = ['assail', 'assault', 'atom-bomb', 'atomise', 'atomize', 'attack', 'backbite', 'backhand', 'bait', 'bastinado', 'bat', 'batter', 'bayonet', 'beak', 'beat', 'beef', 'beetle', 'beleaguer', 'bellyache', 'bemoan', 'beset', 'besiege', 'best', 'better', 'bewail', 'birdie', 'bitch', 'blast', 'bleat', 'blindside', 'blitz', 'blockade', 'bogey', 'bomb', 'bombard', 'bounce', 'break', 'buffet', 'bulldog', 'bunker', 'bunt', 'bust', 'butt', 'cannon', 'cannonade', 'carom', 'carry', 'charge', 'cheat', 'checkmate', 'chicane', 'chip', 'chop', 'chouse', 'circumvent', 'clap', 'clobber', 'clout', 'coldcock', 'complain', 'connect', 'counterattack', 'counterstrike', 'crab', 'cream', 'croak', 'croquet', 'crump', 'crush', 'cuff', 'dab', 'deck', 'declaim', 'deplore', 'desecrate', 'dishonor', 'dishonour', 'dive-bomb', 'double', 'down', 'dribble', 'drive', 'drub', 'dump', 'dunk', 'eagle', 'ebb', 'eliminate', 'exceed', 'firebomb', 'floor', 'fly', 'foul', 'full', 'gang-rape', 'gas', 'glide-bomb', 'gnarl', 'gripe', 'grizzle', 'grouch', 'ground', 'grouse', 'grumble', 'hammer', 'headbutt', 'heel', 'hen-peck', 'hew', 'hit', 'hole', 'holler', 'hook', 'hydrogen-bomb', 'immobilise', 'immobilize', 'infest', 'invade', 'inveigh', 'jab', 'jockey', 'jump', 'kick', 'kill', 'knap', 'knife', 'knock', 'knuckle', 'kvetch', 'lament', 'lash', 'lick', 'loft', 'master', 'mate', 'molest', 'murmur', 'mutter', 'nag', 'nuke', 'occupy', 'out-herod', 'outbrave', 'outcry', 'outdo', 'outdraw', 'outfight', 'outflank', 'outfox', 'outgeneral', 'outgo', 'outgrow', 'outmaneuver', 'outmanoeuvre', 'outmarch', 'outmatch', 'outpace', 'outperform', 'outplay', 'outpoint', 'outrage', 'outrange', 'outroar', 'outsail', 'outscore', 'outsell', 'outshine', 'outshout', 'outsmart', 'outstrip', 'outwear', 'outweigh', 'outwit', 'overcome', 'overmaster', 'overpower', 'overreach', 'overrun', 'overwhelm', 'paste', 'pat', 'pattern-bomb', 'peck', 'pelt', 'pepper', 'percuss', 'pick', 'pip', 'pitch', 'plain', 'play', 'plug', 'poniard', 'pop', 'profane', 'protest', 'pull', 'punch', 'putt', 'quetch', 'racket', 'raid', 'rail', 'rap', 'rape', 'ravish', 'reassail', 'repine', 'report', 'retaliate', 'rout', 'rush', 'savage', 'sclaff', 'scold', 'scoop', 'screw', 'set', 'shaft', 'shame', 'shank', 'shell', 'shoot', 'sic', 'sideswipe', 'single', 'skip-bomb', 'slam-dunk', 'slap', 'sledge', 'sledgehammer', 'slice', 'smash', 'snag', 'snap', 'snick', 'spread-eagle', 'spreadeagle', 'spur', 'squawk', 'stab', 'steamroll', 'steamroller', 'storm', 'strafe', 'strike', 'stroke', 'subdue', 'submarine', 'surmount', 'surpass', 'surprise', 'surround', 'tap', 'teargas', 'thrash', 'thresh', 'tip', 'toe', 'top', 'torpedo', 'triple', 'trounce', 'trump', 'undercut', 'upstage', 'urticate', 'vanquish', 'violate', 'volley', 'whang', 'whine', 'whip', 'whomp', 'worst', 'yammer', 'yawp', 'zap']

Rule-Based Matching (verbs of violence, e.g.)

  • Convert the string on the previous slide into a list:
  • Then return to the code from before. Add a couple additional violent verbs to the VP to see if the list is comprehensive enough.
import spacy
from spacy.matcher import PhraseMatcher

nlp = spacy.load("en_core_web_sm")

patterns = [nlp(v) for v in violent_verb_lemmas]
matcher = PhraseMatcher(nlp.vocab, attr="LEMMA")
doc = nlp("They punched, kicked, and attacked the intruder before fleeing.")
matcher.add("VIOLENCE", patterns)
[(doc[s:e].text, doc[s:e].lemma_) for _, s, e in matcher(doc)]