import math
import random
from collections import Counter, defaultdict
def tokenize(text):
return text.lower().replace(".", " .").replace(",", " ,").split()
def train_bigrams(sentences):
bigrams = Counter()
unigrams = Counter()
unigram_contexts = defaultdict(set)
for sentence in sentences:
padded = [""] + sentence + [""]
for i, w in enumerate(padded):
unigrams[w] += 1
if i > 0:
prev = padded[i - 1]
bigrams[(prev, w)] += 1
unigram_contexts[w].add(prev)
return bigrams, unigrams, unigram_contexts
def kneser_ney_prob(bigrams, unigram_contexts, context_totals, unique_follow, total_unique_bigrams, prev, w, discount=0.75):
count = bigrams.get((prev, w), 0)
denom = context_totals.get(prev, 0)
continuation = len(unigram_contexts.get(w, set())) / max(total_unique_bigrams, 1)
if denom == 0:
return continuation or 1e-9
first = max(count - discount, 0) / denom
lam = discount * len(unique_follow[prev]) / denom
return first + lam * continuation
def laplace_prob(bigrams, unigrams, vocab_size, prev, w):
num = bigrams.get((prev, w), 0) + 1
den = unigrams.get(prev, 0) + vocab_size
return num / den
def perplexity(prob_fn, sentences):
total_log = 0.0
total = 0
for sentence in sentences:
padded = [""] + sentence + [""]
for i in range(1, len(padded)):
p = prob_fn(padded[i - 1], padded[i])
total_log += math.log(max(p, 1e-12))
total += 1
return math.exp(-total_log / total)
def sample_sentence(prob_fn, vocab, max_len=15, seed=0):
rng = random.Random(seed)
tokens = [""]
for _ in range(max_len):
probs = [(w, prob_fn(tokens[-1], w)) for w in vocab if w != ""]
total = sum(p for _, p in probs)
r = rng.random() * total
acc = 0.0
for w, p in probs:
acc += p
if r <= acc:
tokens.append(w)
break
if tokens[-1] == "":
return tokens[1:-1]
return tokens[1:]
def main():
raw = [
"the cat sat on the mat .",
"the cat ran across the room .",
"the dog sat by the window .",
"a cat chased the mouse .",
"the dog ran after the cat .",
"a mouse hid under the table .",
"the cat watched the birds .",
"the dog chased the ball .",
"a bird sat on the branch .",
"the cat slept on the couch .",
]
train = [tokenize(s) for s in raw]
test = [tokenize("the cat sat on the couch ."), tokenize("the dog watched the mouse .")]
bigrams, unigrams, unigram_contexts = train_bigrams(train)
vocab = list(unigrams.keys())
vocab_size = len(vocab)
context_totals = Counter()
unique_follow = defaultdict(set)
for (prev, w), c in bigrams.items():
context_totals[prev] += c
unique_follow[prev].add(w)
total_unique_bigrams = sum(len(ctx_set) for ctx_set in unigram_contexts.values())
def kn(prev, w):
return kneser_ney_prob(bigrams, unigram_contexts, context_totals, unique_follow, total_unique_bigrams, prev, w)
def lap(prev, w):
return laplace_prob(bigrams, unigrams, vocab_size, prev, w)
print(f"vocab size: {vocab_size}")
print(f"train sentences: {len(train)}")
print(f"test sentences: {len(test)}")
print()
print(f"perplexity (Laplace): {perplexity(lap, test):.2f}")
print(f"perplexity (Kneser-Ney): {perplexity(kn, test):.2f}")
print()
print("=== sampled sentences (Kneser-Ney, 3 seeds) ===")
for seed in [1, 7, 42]:
sentence = sample_sentence(kn, vocab, seed=seed)
print(f" seed={seed}: {' '.join(sentence)}")
if __name__ == "__main__":
main()