Download src/python/symbolic_bench.py from Snapkitty/symbolic-morphology: direct link, hf CLI and curl.
- Browser
- Download file 10.7 kB
-
https://huggingface.co/Snapkitty/symbolic-morphology/resolve/main/src/python/symbolic_bench.py
- Command line
-
hf download hf://Snapkitty/symbolic-morphology/src/python/symbolic_bench.py
-
curl -L -o symbolic_bench.py https://huggingface.co/Snapkitty/symbolic-morphology/resolve/main/src/python/symbolic_bench.py
10.7 kB
| #!/usr/bin/env python3 | |
| # Symbolic Morphology Engine β Latin verb morphology from raw letters to Boolean grammar | |
| # Copyright (C) 2026 Ahmad Ali Parr | |
| # | |
| # This program is free software: you can redistribute it and/or modify | |
| # it under the terms of the GNU Affero General Public License as published by | |
| # the Free Software Foundation, either version 3 of the License, or | |
| # (at your option) any later version. | |
| # | |
| # This program is distributed in the hope that it will be useful, | |
| # but WITHOUT ANY WARRANTY; without even the implied warranty of | |
| # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
| # GNU Affero General Public License for more details. | |
| # | |
| # You should have received a copy of the GNU Affero General Public License | |
| # along with this program. If not, see <https://www.gnu.org/licenses/>. | |
| """ | |
| Burt's symbolic Latin morphology learner β float-based Elman RNN. | |
| Benchmark version: measures training time, inference latency, quality. | |
| """ | |
| import math, random, time | |
| # ββ Primitives ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def zeros(n): return [0.0] * n | |
| def zeros2(r, c): return [[0.0] * c for _ in range(r)] | |
| def sigmoid(z): | |
| if z >= 0.0: | |
| e = math.exp(-z); return 1.0 / (1.0 + e) | |
| e = math.exp(z); return e / (1.0 + e) | |
| def matTvec(M, v): | |
| c = len(M[0]); out = [0.0] * c | |
| for i in range(len(M)): | |
| vi = v[i] | |
| if vi != 0.0: | |
| row = M[i] | |
| for j in range(c): out[j] += row[j] * vi | |
| return out | |
| def outer_add(M, a, b): | |
| for i in range(len(a)): | |
| ai = a[i] | |
| if ai != 0.0: | |
| row = M[i] | |
| for j in range(len(b)): row[j] += ai * b[j] | |
| # ββ Features ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| FEATURES = ["PERSON_1","PERSON_2","PERSON_3","SINGULAR","PLURAL", | |
| "PRESENT","IMPERFECT","FUTURE","PERFECT", | |
| "INDICATIVE","SUBJUNCTIVE","IMPERATIVE","ACTIVE","PASSIVE"] | |
| K = len(FEATURES) | |
| FIDX = {f: i for i, f in enumerate(FEATURES)} | |
| def make_target(person, number, tense, mood, voice): | |
| y = [0.0] * K | |
| y[FIDX["PERSON_%d" % person]] = 1.0 | |
| y[FIDX["SINGULAR" if number == "SG" else "PLURAL"]] = 1.0 | |
| y[FIDX[tense]] = 1.0; y[FIDX[mood]] = 1.0; y[FIDX[voice]] = 1.0 | |
| return y | |
| # ββ Corpus (same structure as Burt's) βββββββββββββββββββββββββββββββ | |
| PRES_IND_ACT = { | |
| 1: ["o","as","at","amus","atis","ant"], | |
| 2: ["eo","es","et","emus","etis","ent"], | |
| 3: ["o","is","it","imus","itis","unt"], | |
| 4: ["io","is","it","imus","itis","iunt"], | |
| } | |
| IMPF_IND_ACT = { | |
| 1: ["abam","abas","abat","abamus","abatis","abant"], | |
| 2: ["ebam","ebas","ebat","ebamus","ebatis","ebant"], | |
| 3: ["ebam","ebas","ebat","ebamus","ebatis","ebant"], | |
| 4: ["iebam","iebas","iebat","iebamus","iebatis","iebant"], | |
| } | |
| PERF_IND_ACT = ["i","isti","it","imus","istis","erunt"] | |
| VERBS = [ | |
| (1,"am","amav"), (1,"laud","laudav"), (1,"port","portav"), (1,"voc","vocav"), | |
| (2,"mon","monu"), (2,"vid","vid"), | |
| (3,"reg","rex"), (3,"duc","dux"), (3,"mitt","mis"), | |
| (4,"aud","audiv"), (4,"ven","ven"), | |
| ] | |
| def pn(i): return (i % 3) + 1, ("SG" if i < 3 else "PL") | |
| def build_corpus(): | |
| corpus = [] | |
| for conj, stem, pstem in VERBS: | |
| for i, e in enumerate(PRES_IND_ACT[conj]): | |
| p, n = pn(i) | |
| corpus.append((stem+e, make_target(p,n,"PRESENT","INDICATIVE","ACTIVE"))) | |
| for i, e in enumerate(IMPF_IND_ACT[conj]): | |
| p, n = pn(i) | |
| corpus.append((stem+e, make_target(p,n,"IMPERFECT","INDICATIVE","ACTIVE"))) | |
| for i, e in enumerate(PERF_IND_ACT): | |
| p, n = pn(i) | |
| corpus.append((pstem+e, make_target(p,n,"PERFECT","INDICATIVE","ACTIVE"))) | |
| return corpus | |
| # ββ Model: Elman RNN ββββββββββββββββββββββββββββββββββββββββββββββββ | |
| class MorphRNN: | |
| def __init__(self, vocab, D=8, H=16, seed=7): | |
| rng = random.Random(seed) | |
| self.vocab = vocab; self.D = D; self.H = H | |
| s = lambda: rng.gauss(0, 0.4) | |
| self.E = [[s() for _ in range(D)] for _ in range(len(vocab))] | |
| sc_x = 1.0/math.sqrt(D); sc_h = 1.0/math.sqrt(H) | |
| self.Wxh = [[rng.gauss(0,sc_x) for _ in range(D)] for _ in range(H)] | |
| self.Whh = [[rng.gauss(0,sc_h) for _ in range(H)] for _ in range(H)] | |
| self.bh = zeros(H) | |
| self.Why = [[rng.gauss(0,sc_h) for _ in range(H)] for _ in range(K)] | |
| self.by = zeros(K) | |
| def forward(self, word): | |
| E,Wxh,Whh,bh,Why,by = self.E,self.Wxh,self.Whh,self.bh,self.Why,self.by | |
| H,D = self.H,self.D | |
| h = zeros(H); cache = [] | |
| for ch in word: | |
| ci = self.vocab[ch]; x = E[ci] | |
| pre = [0.0]*H | |
| for j in range(H): | |
| s = bh[j] | |
| for d in range(D): s += Wxh[j][d]*x[d] | |
| for k in range(H): s += Whh[j][k]*h[k] | |
| pre[j] = s | |
| h_new = [math.tanh(v) for v in pre] | |
| cache.append((ci,x,h,h_new)); h = h_new | |
| logits = [by[k] + sum(Why[k][j]*h[j] for j in range(H)) for k in range(K)] | |
| return h, logits, cache | |
| def backward(self, word, y, grads): | |
| H,D = self.H,self.D | |
| h,logits,cache = self.forward(word) | |
| p = [sigmoid(z) for z in logits] | |
| loss = sum(-(y[k]*math.log(max(1e-12,p[k]))+(1-y[k])*math.log(max(1e-12,1-p[k]))) for k in range(K))/K | |
| dlogits = [(p[k]-y[k])/K for k in range(K)] | |
| outer_add(grads["Why"], dlogits, h) | |
| for k in range(K): grads["by"][k] += dlogits[k] | |
| dh = matTvec(self.Why, dlogits) | |
| gE,gWxh,gWhh,gbh = grads["E"],grads["Wxh"],grads["Whh"],grads["bh"] | |
| for t in range(len(cache)-1,-1,-1): | |
| ci,x,h_prev,h_new = cache[t] | |
| dz = [dh[j]*(1.0-h_new[j]*h_new[j]) for j in range(H)] | |
| for j in range(H): | |
| d = dz[j] | |
| if d == 0.0: continue | |
| for dd in range(D): gWxh[j][dd] += d*x[dd] | |
| for k in range(H): gWhh[j][k] += d*h_prev[k] | |
| gbh[j] += d | |
| dh = matTvec(self.Whh, dz) | |
| dE = matTvec(self.Wxh, dz) | |
| for dd in range(D): gE[ci][dd] += dE[dd] | |
| return loss, p | |
| def step(self, grads, lr): | |
| for P,G in [(self.E,grads["E"]),(self.Wxh,grads["Wxh"]),(self.Whh,grads["Whh"]), | |
| (self.Why,grads["Why"])]: | |
| for i in range(len(P)): | |
| for j in range(len(P[i])): P[i][j] -= lr*G[i][j] | |
| for i in range(len(self.bh)): self.bh[i] -= lr*grads["bh"][i] | |
| for i in range(len(self.by)): self.by[i] -= lr*grads["by"][i] | |
| def zero_grads(self): | |
| return {"E":zeros2(len(self.vocab),self.D),"Wxh":zeros2(self.H,self.D), | |
| "Whh":zeros2(self.H,self.H),"bh":zeros(self.H), | |
| "Why":zeros2(K,self.H),"by":zeros(K)} | |
| # ββ Benchmark βββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def main(): | |
| t0 = time.time() | |
| print("=" * 65) | |
| print("PYTHON FLOAT-BASED RNN β BENCHMARK") | |
| print("=" * 65) | |
| corpus = build_corpus() | |
| all_chars = sorted(set("".join(w for w,_ in corpus))) | |
| vocab = {c:i for i,c in enumerate(all_chars)} | |
| print(f" Corpus: {len(corpus)} words, {K} features, vocab: {len(vocab)} chars") | |
| model = MorphRNN(vocab, D=8, H=16, seed=7) | |
| nparam = sum(len(r) for row in model.E for r in [row]) + \ | |
| sum(len(r) for row in model.Wxh for r in [row]) + \ | |
| sum(len(r) for row in model.Whh for r in [row]) + \ | |
| len(model.bh) + \ | |
| sum(len(r) for row in model.Why for r in [row]) + \ | |
| len(model.by) | |
| print(f" Params: D=8, H=16, K={K}, total={nparam}") | |
| print() | |
| # BENCHMARK 1: Training | |
| EPOCHS = 3000 | |
| print(f" BENCHMARK 1: TRAINING ({EPOCHS} epochs x {len(corpus)} examples)") | |
| print(" " + "-" * 55) | |
| for epoch in range(EPOCHS + 1): | |
| total_loss = 0.0 | |
| for word, target in corpus: | |
| grads = model.zero_grads() | |
| loss, _ = model.backward(word, target, grads) | |
| total_loss += loss | |
| model.step(grads, 0.3 * (1 - epoch/EPOCHS) + 0.02 * (epoch/EPOCHS)) | |
| if epoch % 500 == 0 or epoch == EPOCHS: | |
| correct = sum(1 for w,t in corpus | |
| if all((1 if sigmoid(z)>=0.5 else 0)==int(t[k]) | |
| for k,z in enumerate( | |
| [model.by[k]+sum(model.Why[k][j]*model.forward(w)[0][j] | |
| for j in range(model.H)) for k in range(K)]))) | |
| elapsed = time.time() - t0 | |
| print(f" Epoch {epoch:>5} | Loss: {total_loss/len(corpus):.6f} | {elapsed:.2f}s") | |
| train_time = time.time() - t0 | |
| total_ex = (EPOCHS+1)*len(corpus) | |
| print(f"\n Training: {train_time:.2f}s, {total_ex} examples, {total_ex/train_time:.0f} ex/sec") | |
| # BENCHMARK 2: Inference | |
| print(f"\n BENCHMARK 2: INFERENCE LATENCY") | |
| N_INF = 500 | |
| inf_t0 = time.time() | |
| for _ in range(N_INF): | |
| for word, _ in corpus: | |
| model.forward(word) | |
| inf_time = time.time() - inf_t0 | |
| total_inf = N_INF * len(corpus) | |
| print(f" {total_inf} inferences in {inf_time:.2f}s") | |
| print(f" Per inference: {inf_time*1e6/total_inf:.1f} Β΅s") | |
| print(f" Throughput: {total_inf/inf_time:.0f} inferences/sec") | |
| # BENCHMARK 3: Forward+Backward | |
| print(f"\n BENCHMARK 3: FORWARD+BACKWARD LATENCY") | |
| N_FB = 500 | |
| fb_t0 = time.time() | |
| for _ in range(N_FB): | |
| for word, target in corpus: | |
| grads = model.zero_grads() | |
| model.backward(word, target, grads) | |
| fb_time = time.time() - fb_t0 | |
| total_fb = N_FB * len(corpus) | |
| print(f" {total_fb} passes in {fb_time:.2f}s") | |
| print(f" Per pass: {fb_time*1e6/total_fb:.1f} Β΅s") | |
| print(f" Throughput: {total_fb/fb_time:.0f} passes/sec") | |
| # BENCHMARK 4: Quality | |
| print(f"\n BENCHMARK 4: GENERALIZATION QUALITY") | |
| train_ok = 0 | |
| for word, target in corpus: | |
| _,logits,_ = model.forward(word) | |
| p = [sigmoid(z) for z in logits] | |
| if all((1 if p[k]>=0.5 else 0)==int(target[k]) for k in range(K)): | |
| train_ok += 1 | |
| print(f" Training: {train_ok}/{len(corpus)} perfect words") | |
| print(f"\n TOTAL TIME: {time.time()-t0:.1f}s") | |
| print("=" * 65) | |
| if __name__ == "__main__": | |
| main() | |