Sentence Meaning and Natural Language Inference › Worked Example: A Small NLI Battery
Language AI HandbookAll chapters Sentence Meaning and Natural Language Inference › Worked Example: A Small NLI BatterySection view · Sections in this chapter9 / 12 ↓
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 will now build a small NLI battery and run an explicit lexical baseline. This deterministic example uses Python, NumPy, pandas, and Matplotlib from the locked book environment; it downloads no models or data. The baseline intentionally ignores word order and lexical hierarchies so that the battery exposes its limitations.
The battery is intentionally small. It is a teaching tool, not a benchmark. The goal is to inspect controlled failures and practice computing evaluation metrics. The manually chosen score weights are not learned or tuned on a held-out split. Softmax normalizes those scores to sum to one; it does not calibrate them as empirical confidence estimates.
Setting Up
The hidden setup at the start of the chapter seeds the book's plotting utilities once. Each figure applies the shared book style when it renders.
Next, import the small numerical and plotting dependencies. The tokenization below lowercases text and extracts ASCII letter/digit runs; it is deliberately limited to these English teaching examples.
PythonIn [5]Expand
import re
import matplotlib.pyplot as plt
import numpy as np
import pandas as pdA Transparent Lexical Baseline
The baseline uses hypothesis-word overlap, a mismatch in negation words, and a small list of contrasting terms. These are fallible cues: opposite words need not denote incompatible events, and a negation word need not have the same scope in both sentences. The coefficients below are fixed illustrative choices, so the scores describe this heuristic rather than a trained NLI system.
PythonIn [6]Expand
LABELS = ["contradiction", "entailment", "neutral"]
NEGATIONS = {
"not",
"no",
"never",
"none",
"nobody",
"nothing",
"neither",
"nor",
"without",
}
ANTONYM_PAIRS = [
("open", "closed"),
("hot", "cold"),
("up", "down"),
("before", "after"),
("true", "false"),
("always", "never"),
("more", "less"),
("increase", "decrease"),
("buy", "sell"),
("win", "lose"),
]
STOPWORDS = {
"a",
"an",
"the",
"is",
"are",
"was",
"were",
"be",
"been",
"being",
"of",
"to",
"in",
"on",
"at",
"for",
"with",
"and",
"or",
"that",
"this",
"it",
"as",
"by",
"from",
}
def simple_tokens(text):
return re.findall(r"[a-z0-9]+", text.lower())PythonIn [7]Expand
class LexicalNLIBaseline:
"""Fixed teaching heuristic, without training or model downloads."""
def predict(self, pairs):
rows = []
for premise, hypothesis in pairs:
pt = set(simple_tokens(premise))
ht = set(simple_tokens(hypothesis))
content = ht - STOPWORDS
overlap = len(content & pt) / max(1, len(content))
neg_p = any(token in NEGATIONS for token in pt)
neg_h = any(token in NEGATIONS for token in ht)
antonym_hit = any(
(a in pt and b in ht) or (b in pt and a in ht)
for a, b in ANTONYM_PAIRS
)
ent_logit = 0.2 + 1.6 * overlap
con_logit = 0.1
if neg_p != neg_h:
con_logit += 1.1
if antonym_hit:
con_logit += 1.3
neu_logit = 0.7 + 1.0 * (1.0 - overlap)
logits = np.array([con_logit, ent_logit, neu_logit])
probs = np.exp(logits - logits.max())
rows.append(probs / probs.sum())
return np.vstack(rows) if rows else np.empty((0, len(LABELS)))Instantiating the Baseline
The output columns always use LABELS in the declared order: contradiction, entailment, neutral. There is no external model configuration or environment-dependent fallback.
PythonIn [8]Expand
nli_model = LexicalNLIBaseline()
model_labels = LABELSPrediction Functions
The batch function checks that every premise has a hypothesis. It returns a normalized heuristic score for each label and selects the largest score. Exact ties follow the declared label order because the first maximum is selected. An empty batch returns an empty list.
PythonIn [9]Expand
def predict_nli_batch(premises, hypotheses):
if len(premises) != len(hypotheses):
raise ValueError("Each premise needs exactly one hypothesis.")
pairs = list(zip(premises, hypotheses))
raw = nli_model.predict(pairs)
results = []
for row in raw:
probs = {label: float(row[i]) for i, label in enumerate(LABELS)}
prediction = max(LABELS, key=lambda label: probs[label])
results.append({"probs": probs, "prediction": prediction})
return results
def predict_nli(premise, hypothesis):
return predict_nli_batch([premise], [hypothesis])[0]Running the Battery
The battery covers simple entailment, contradiction, neutral pairs, lexical ambiguity, event structure, and downward monotonicity. Gold labels use the intended readings stated here. Hold referents and relevant times fixed when descriptions are intended to corefer. In the bank example, the premise uses the financial sense and the hypothesis the geographical sense, so they concern different entities. The train-arrival pair refers to one arrival, and the meeting-time pair to one scheduled start; different events would change the judgment. The child-and-park example uses an outdoor park reading, and the museum pair uses one recurring weekly schedule with a binary daily open/closed status. The toy battery has no training/test split because no parameters are fitted.
PythonIn [10]Expand
examples = [
{
"id": 1,
"premise": "A young child is riding a bicycle in the park.",
"hypothesis": "A child is outdoors.",
"gold": "entailment",
},
{
"id": 2,
"premise": "The museum is open every day.",
"hypothesis": "The museum is closed on Mondays.",
"gold": "contradiction",
},
{
"id": 3,
"premise": "A woman is playing the violin.",
"hypothesis": "A woman is performing in an orchestra.",
"gold": "neutral",
},
{
"id": 4,
"premise": "All mammals are warm-blooded, and dolphins are mammals.",
"hypothesis": "Dolphins are warm-blooded.",
"gold": "entailment",
},
{
"id": 5,
"premise": "The train arrived before noon.",
"hypothesis": "The train arrived after noon.",
"gold": "contradiction",
},
{
"id": 6,
"premise": "The bank raised interest rates.",
"hypothesis": "The river bank is steep.",
"gold": "neutral",
},
{
"id": 7,
"premise": "No dogs are allowed in the store.",
"hypothesis": "No puppies are allowed in the store.",
"gold": "entailment",
},
{
"id": 8,
"premise": "The door is open.",
"hypothesis": "The door is not open.",
"gold": "contradiction",
},
{
"id": 9,
"premise": "The lawyer questioned the witness.",
"hypothesis": "The witness questioned the lawyer.",
"gold": "neutral",
},
{
"id": 10,
"premise": "Alice sold Bob a car.",
"hypothesis": "Bob bought a car from Alice.",
"gold": "entailment",
},
{
"id": 11,
"premise": "The keys are in the drawer.",
"hypothesis": "The keys are in the kitchen.",
"gold": "neutral",
},
{
"id": 12,
"premise": "The meeting starts at 3 p.m.",
"hypothesis": "The meeting starts at 4 p.m.",
"gold": "contradiction",
},
]
examples_df = pd.DataFrame(examples)PythonIn [11]Expand
batch_predictions = predict_nli_batch(
examples_df["premise"].tolist(),
examples_df["hypothesis"].tolist(),
)
results_df = examples_df.copy()
results_df["prediction"] = [item["prediction"] for item in batch_predictions]
for label in LABELS:
results_df[f"p_{label}"] = [
item["probs"][label] for item in batch_predictions
]
results_df["correct"] = results_df["gold"] == results_df["prediction"]ConsoleOut [12]Expand
id gold prediction p_contradiction p_entailment p_neutral correct 1 entailment neutral 0.155 0.381 0.465 False 2 contradiction contradiction 0.403 0.207 0.390 True 3 neutral neutral 0.155 0.293 0.552 True 4 entailment entailment 0.121 0.660 0.220 True 5 contradiction contradiction 0.379 0.379 0.242 True 6 neutral neutral 0.155 0.293 0.552 True 7 entailment entailment 0.143 0.524 0.334 True 8 contradiction entailment 0.343 0.367 0.290 False 9 neutral entailment 0.121 0.660 0.220 False 10 entailment entailment 0.143 0.524 0.334 True 11 neutral neutral 0.155 0.381 0.465 True 12 contradiction entailment 0.139 0.552 0.309 False Accuracy on this toy battery: 0.667
The table shows the baseline's normalized scores and agreement with the hand labels. A large score does not establish correct inference or explain a decision. Inspecting errors can suggest patterns for controlled follow-up tests, but one output cannot establish a systematic failure or show that a correct answer was accidental. Example 5 has equal contradiction and entailment scores; its correct label comes from the declared tie-breaking order rather than a separation between those two scores.
The next chart uses the same twelve predictions, shows the normalized score assigned to the chosen label, and marks agreement with the hand label. A larger score does not establish better calibration or semantic understanding.
VisualizationOut [14]Expand

Directional Entailment
The first probe isolates lexical specificity: a poodle barking entails a dog barking, while the reverse is neutral. The second probe assumes the same restaurant, time, and a binary open/closed status. Under that explicit convention, "open" and "not closed" are equivalent. The lexical baseline is not expected to implement either semantic rule correctly.
PythonIn [15]Expand
directional_pairs = [
("A poodle barked.", "A dog barked."),
("A dog barked.", "A poodle barked."),
("The restaurant is open.", "The restaurant is not closed."),
("The restaurant is not closed.", "The restaurant is open."),
]
directional_preds = predict_nli_batch(
[p for p, h in directional_pairs],
[h for p, h in directional_pairs],
)
directional_df = pd.DataFrame(
directional_pairs, columns=["premise", "hypothesis"]
)
directional_df["prediction"] = [
item["prediction"] for item in directional_preds
]
directional_df["p_entailment"] = [
item["probs"]["entailment"] for item in directional_preds
]
directional_df["p_neutral"] = [
item["probs"]["neutral"] for item in directional_preds
]
directional_df["p_contradiction"] = [
item["probs"]["contradiction"] for item in directional_preds
]ConsoleOut [16]Expand
premise hypothesis prediction p_entailment p_neutral p_contradiction
A poodle barked. A dog barked. neutral 0.381 0.465 0.155
A dog barked. A poodle barked. neutral 0.381 0.465 0.155
The restaurant is open. The restaurant is not closed. contradiction 0.114 0.216 0.670
The restaurant is not closed. The restaurant is open. contradiction 0.149 0.182 0.669These probes change the premise-hypothesis direction; they do not swap grammatical roles inside a sentence. Compare the output with the stated semantic targets. Equal heuristic scores can expose a missing distinction, while differing scores alone do not establish correct inference. The chart shows the baseline's entailment score for each ordered pair.
VisualizationOut [18]Expand

Presupposition Probes
Presuppositions often project under negation in the readings considered here. A model that only learns surface entailment may treat the negated premise differently from the positive one, even though the presupposition survives in both.
PythonIn [19]Expand
presupposition_pairs = [
("The king of France is bald.", "There is a king of France."),
("The king of France is not bald.", "There is a king of France."),
("John stopped smoking.", "John used to smoke."),
("John did not stop smoking.", "John used to smoke."),
]
presupposition_preds = predict_nli_batch(
[p for p, h in presupposition_pairs],
[h for p, h in presupposition_pairs],
)
presupposition_df = pd.DataFrame(
presupposition_pairs, columns=["premise", "hypothesis"]
)
presupposition_df["prediction"] = [
item["prediction"] for item in presupposition_preds
]
presupposition_df["p_entailment"] = [
item["probs"]["entailment"] for item in presupposition_preds
]
presupposition_df["p_neutral"] = [
item["probs"]["neutral"] for item in presupposition_preds
]
presupposition_df["p_contradiction"] = [
item["probs"]["contradiction"] for item in presupposition_preds
]ConsoleOut [20]Expand
premise hypothesis prediction p_entailment p_neutral p_contradiction
The king of France is bald. There is a king of France. entailment 0.475 0.376 0.148
The king of France is not bald. There is a king of France. entailment 0.367 0.290 0.343
John stopped smoking. John used to smoke. neutral 0.293 0.552 0.155
John did not stop smoking. John used to smoke. neutral 0.223 0.421 0.356The three-way label set does not separately identify presupposition. If both positive and negated premises receive entailment labels, the outputs alone do not establish which cues or meaning representations produced them. Additional controlled probes are needed to investigate the mechanism.
Stress Tests
The stress battery targets lexical overlap, negation, and downward monotonicity. The last two examples are a minimal pair: "No dogs are allowed" entails "No puppies are allowed," but "No puppies are allowed" does not entail "No dogs are allowed."
PythonIn [21]Expand
stress_examples = [
{
"premise": "The lawyer questioned the witness.",
"hypothesis": "The witness questioned the lawyer.",
"gold": "neutral",
},
{
"premise": "The lawyer questioned the witness.",
"hypothesis": "The lawyer questioned someone.",
"gold": "entailment",
},
{
"premise": "The door is open.",
"hypothesis": "The door is closed.",
"gold": "contradiction",
},
{
"premise": "The door is not open.",
"hypothesis": "The door is closed.",
"gold": "entailment",
},
{
"premise": "No dogs are allowed in the store.",
"hypothesis": "No puppies are allowed in the store.",
"gold": "entailment",
},
{
"premise": "No puppies are allowed in the store.",
"hypothesis": "No dogs are allowed in the store.",
"gold": "neutral",
},
]
stress_df = pd.DataFrame(stress_examples)
stress_preds = predict_nli_batch(
stress_df["premise"].tolist(),
stress_df["hypothesis"].tolist(),
)
stress_df["prediction"] = [item["prediction"] for item in stress_preds]
stress_df["correct"] = stress_df["gold"] == stress_df["prediction"]ConsoleOut [22]Expand
premise hypothesis gold prediction correct
The lawyer questioned the witness. The witness questioned the lawyer. neutral entailment False
The lawyer questioned the witness. The lawyer questioned someone. entailment entailment True
The door is open. The door is closed. contradiction contradiction True
The door is not open. The door is closed. entailment contradiction False
No dogs are allowed in the store. No puppies are allowed in the store. entailment entailment True
No puppies are allowed in the store. No dogs are allowed in the store. neutral entailment False
Stress-test accuracy: 0.500The lawyer-witness example is the classic lexical overlap trap. The premise and hypothesis share almost all content words, but the grammatical roles are reversed. A model that attends to argument order should predict neutral. A model that counts overlapping words may predict entailment. The minimal pair about dogs and puppies tests the other end of the same idea: downward monotonicity means the inference runs from the broader term to the narrower one, not the other way around.
Metrics and Figures
We can compute a confusion matrix and macro-F1 on the toy battery. With only twelve hand-chosen examples, these numbers describe this battery; they are not a representative estimate of population performance. The point is to practice reporting per-class behavior, not to rank models.
PythonIn [23]Expand
confusion = np.zeros((len(LABELS), len(LABELS)), dtype=int)
for gold, pred in zip(results_df["gold"], results_df["prediction"]):
confusion[LABELS.index(gold), LABELS.index(pred)] += 1
def macro_f1(y_true, y_pred, labels):
scores = []
for label in labels:
tp = sum(t == label and p == label for t, p in zip(y_true, y_pred))
fp = sum(t != label and p == label for t, p in zip(y_true, y_pred))
fn = sum(t == label and p != label for t, p in zip(y_true, y_pred))
precision = tp / (tp + fp) if tp + fp else 0.0
recall = tp / (tp + fn) if tp + fn else 0.0
f1 = (
2 * precision * recall / (precision + recall)
if precision + recall
else 0.0
)
scores.append(f1)
return float(np.mean(scores))
accuracy = float((results_df["gold"] == results_df["prediction"]).mean())
f1 = macro_f1(
results_df["gold"].tolist(), results_df["prediction"].tolist(), LABELS
)ConsoleOut [24]Expand
Accuracy: 0.667
Macro F1: 0.672
Confusion matrix (rows = gold, columns = predicted):
Contradiction Entailment Neutral
Contradiction 2 2 0
Entailment 0 3 1
Neutral 0 1 3The confusion matrix shows the observed label errors. Two of four gold contradictions and one of four gold neutrals are predicted as entailment. These counts describe this heuristic on the battery; they do not establish a general error distribution for NLI models. Macro-F1 gives each class equal weight.
The next figure compares the three normalized heuristic scores for four selected examples. It shows which labels the fixed rules favor, without treating score magnitude as calibrated confidence.
PythonIn [25]Expand
plot_ids = [1, 2, 4, 7]
plot_df = results_df[results_df["id"].isin(plot_ids)].copy().sort_values("id")VisualizationOut [26]Expand

The second figure shows the confusion matrix. It is a compact way to see which labels the model confuses.
VisualizationOut [27]Expand

With twelve examples, the confusion matrix is sparse. That is normal for a teaching battery. For an application evaluation, choose a sampling design and sample size that fit the target population and required uncertainty, then report category-level results with an appropriate uncertainty analysis.
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!