local help = [[
ezr3-apps.lua: skills built on the ezr3 substrate, one
short function per book chapter. Residents: knn prediction
(Fortune Teller), anomaly (Bouncer), naive bayes (ER
Nurse), kmeans/kpp (Curator), ga/de/sa/ls/race (Drag Race:
the ch22 shootout, ported from attic/luamine/lapps.lua).
Demos at the bottom; run with --all.
options:
k=5 knn: neighbours polled per guess
kluster=8 kmeans, kpp: clusters wanted
iter=10 kmeans: assign/recentre passes
L=1 nb: Laplace smoothing on syms
m=2 nb: m-estimate prior weight
wait=10 nb: rows seen before scoring starts
np=20 ga/de: population size
gens=10 ga/de: generations
muts=2 mutants: x cells re-picked per kid
F=0.5 de: extrapolation scale
cr=0.9 de: crossover rate, per dim
budget1=200 sa/ls: evals per run
repeats=5 race: runs per optimizer]]
local abs,exp,log,pi = math.abs, math.exp, math.log, math.pi
local min,max,floor = math.min, math.max, math.floorfind ezr.lua beside this file, whatever the cwd
package.path = (arg and arg[0] or ""):gsub("[^/]*$","")
.. "?.lua;" .. package.path
local _ENV = setmetatable({}, {__index = require"ezr"})
if setfenv then setfenv(1, _ENV) end
the = the:also(help)Guess a row’s goals from the rows it looks like. No model
is fitted; the neighbours are the model. The signature
above each function is a reading aid, not a check: row
is a list of cells, col is a NUM or a SYM, and names
after the wide gap in an argument list are LOCALS, never
arguments.
TBL:around(row:row, rows?:list) -> list
The rows, sorted nearest first.
function TBL.around(i,row,rows)
return keysort(rows or i.rows,
function(r) return i:distx(row, r) end) endTBL:knn(row:row, k?:int) -> num
Guess row’s disty from its k nearest neighbours.
function TBL.knn(i,row,k, t)
t = i:around(row)
return adds(map(slice(t, 1, k or the.k), i:Y())).mu endWho does not belong? Loneliness is the whole test: how far is the nearest other row?
TBL:anomaly() -> fun
Score rows 0..1; 1 = loneliest. The gap being scored is
the distance to the nearest other row.
function TBL.anomaly(i, dn,gap)
gap = function(r, lo,d)
lo = 1e32
for _,z in ipairs(i.rows) do
if z ~= r then
d = i:distx(r, z); if d < lo then lo = d end end end
return lo end
dn = Num()
for _,r in ipairs(i.rows) do dn:add(gap(r)) end
return function(r) return dn:norm(gap(r)) end endSort arrivals into classes, fast, one row at a time. Test then train: every row is guessed before it is learned, so the accuracy needs no held-out split.
like(col:col, v:any, prior:num) -> num
P(v|col): an m-estimate for syms, a gaussian pdf for nums.
function like(col,v,prior, z)
if not col.mu then
return ((col.has[v] or 0) + the.L*prior)
/ (col.n + the.L) end
z = 2 * col:div()^2 + 1e-32
return exp(-(v - col.mu)^2 / z) / (pi * z)^0.5 endTBL:likes(row:row, nrows:int, nh:int) -> num
Log likelihood of row under this klass-table.
function TBL.likes(h,row,nrows,nh, prior,out,v)
prior = (#h.rows + the.m) / (nrows + the.m*nh)
out = log(prior)
for _,c in ipairs(h.cols.x) do
v = row[c.at]
if v ~= "?" then
v = like(c, v, prior)
if v > 0 then out = out + log(v) end end end
return out endmostlikes(h:dict, row:row, nrows:int, nh:int) -> any
Which klass-table likes this row the most?
function mostlikes(h,row,nrows,nh, best,bs,s)
bs = -1e32
for k,hk in pairs(h) do
s = hk:likes(row, nrows, nh)
if s > bs then bs, best = s, k end end
return best endTBL:classify(wait?:int) -> list
Test, then train: one pass, no held-out split. Scoring
starts once wait rows have been seen. Returns a list of
{got, want} pairs.
function TBL.classify(i,wait, at,h,seen,nh,want)
wait, at = wait or the.wait, i.cols.klass.at
h, seen, nh = {}, {}, 0
for j,row in ipairs(i.rows) do
want = row[at]
if j >= wait and nh > 0 then
push(seen, {mostlikes(h, row, #i.rows, nh), want}) end
if not h[want] then h[want] = i:clone(); nh = nh + 1 end
h[want]:add(row) end
return seen endacc(seen:list) -> num
Fraction of the {got,want} pairs that agree.
function acc(seen, n)
n = 0
for _,p in ipairs(seen) do
if p[1] == p[2] then n = n + 1 end end
return n / (#seen + 1e-32) endGroup the rows without labelling any of them. kmeans finds the groups; kpp picks the starting centroids far apart, so kmeans does not start in a corner.
nearest(i:TBL, cents:list, r:row) -> int – local.
Index of r’s closest centroid.
local function nearest(i,cents,r, lo,d,at)
lo = 1e32
for j,c in ipairs(cents) do
d = i:distx(c, r)
if d < lo then lo, at = d, j end end
return at endassign(i:TBL, cents:list) -> list – local.
Each row into its centroid’s clone.
local function assign(i,cents, out)
out = map(cents, function() return i:clone() end)
for _,r in ipairs(i.rows) do
out[nearest(i, cents, r)]:add(r) end
return out endrecentre(clusters:list) -> list – local.
Middles of the non-empty clusters.
local function recentre(clusters, cents)
cents = {}
for _,c in ipairs(clusters) do
if #c.rows > 0 then push(cents, c:mids()) end end
return cents endTBL:kmeans(k?:int, iter?:int) -> list
k clusters: iter rounds of assign, then recentre.
function TBL.kmeans(i,k,iter, cents)
cents = some(i.rows, k or the.kluster)
for _ = 1, iter or the.iter do
cents = recentre(assign(i, cents)) end
return assign(i, cents) endd2(i:TBL, cents:list, r:row) -> num – local.
Squared distance from r to its nearest centroid.
local function d2(i,cents,r, lo,d)
lo = 1e32
for _,c in ipairs(cents) do
d = i:distx(r, c); lo = min(lo, d*d) end
return lo endwpick(ws:list) -> int – local.
Index j, with chance ws[j]/sum(ws).
local function wpick(ws, all,r,c)
all = sum(ws, function(w) return w end)
r, c = rand() * all, 0
for j,w in ipairs(ws) do
c = c + w
if r <= c then return j end end
return #ws endTBL:kpp(k?:int) -> list
k centroids, far apart: d^2-weighted picks from random
pools.
function TBL.kpp(i,k, cents,pool,ws)
cents = {some(i.rows, 1)[1]}
while #cents < (k or the.kluster) do
pool = some(i.rows, min(the.few, #i.rows))
ws = map(pool, function(r) return d2(i, cents, r) end)
push(cents, pool[wpick(ws)]) end
return cents endClassic optimizers, racing. Mutants invent x values, so
their y cells are unknown: snap grades a mutant by its
nearest real row, and guess is that neighbour’s disty.
TBL:dominates(r1:row, r2:row) -> bool
True when r1 is no worse on all goals and better on at
least one. Goals that do not exist yet get bought here
(via disty), so this works on model-backed pools too.
function TBL.dominates(i,r1,r2, d1,d2,better,worse)
if i.model then i:disty(r1); i:disty(r2) end
better, worse = false, false
for _,y in ipairs(i.cols.y) do
d1 = abs(y:norm(r1[y.at]) - y.heaven)
d2 = abs(y:norm(r2[y.at]) - y.heaven)
if d1 < d2 then better = true end
if d1 > d2 then worse = true end end
return better and not worse endTBL:snap(row:row) -> row
The nearest real row to a made-up one.
function TBL.snap(i,row, lo,d,out)
lo = 1e32
for _,r in ipairs(i.rows) do
d = i:distx(row, r)
if d < lo then lo, out = d, r end end
return out endTBL:guess(row:row) -> num
A mutant’s worth: its neighbour’s disty.
function TBL.guess(i,row)
return i:disty(i:snap(row)) endgauss() -> num – local.
Unit normal, the cheap way (Irwin-Hall).
local function gauss( g)
g = -6; for _ = 1, 12 do g = g + rand() end; return g endwkey(d:dict) -> any – local.
A key, picked with a chance weighted by its count.
local function wkey(d, all,r,c)
all = sum(d, function(n) return n end)
r, c = rand() * all, 0
for _,k in ipairs(keys(d)) do
c = c + d[k]
if r <= c then return k end end endpick(col:col, v:any) -> any
A new cell value, near v: syms by frequency, nums by a
gaussian step, clamped to +-3sd.
function pick(col,v, sd)
if col.has then return wkey(col.has) end
v = v ~= "?" and v or col.mu
sd = col:div()
return max(col.mu - 3*sd,
min(col.mu + 3*sd, v + sd * gauss())) endTBL:mutate(row:row, n?:int) -> row
Copy row, then re-pick n random x cells.
function TBL.mutate(i,row,n, out,xs)
out, xs = copy(row), shuffle(i.cols.x)
for j = 1, min(n or the.muts, #xs) do
out[xs[j].at] = pick(xs[j], out[xs[j].at]) end
return out endTBL:extrapolate(a:row, b:row, c:row) -> row
A de kid: a + F*(b - c), per dimension, with probability
cr. Numbers wrap inside +-4sd.
function TBL.extrapolate(i,a,b,c, out,keep,va,vb,vc,v,lo,s)
out = copy(a)
keep = i.cols.x[rand(#i.cols.x)]
for _,col in ipairs(i.cols.x) do
if col ~= keep and rand() < the.cr then
va, vb, vc = a[col.at], b[col.at], c[col.at]
if va == "?" then out[col.at] = "?"
elseif col.has then
out[col.at] = rand() < the.F and vb or va
elseif vb == "?" or vc == "?" then out[col.at] = va
else
v = va + the.F * (vb - vc)
lo = col.mu - 4*col:div()
s = 8*col:div() + 1e-32
out[col.at] = lo + (v - lo) % s end end end
return out endtourn(i:TBL, pop:list) -> row – local.
2-way select: keep the dominator.
local function tourn(i,pop, a,b)
a, b = pop[rand(#pop)], pop[rand(#pop)]
return i:dominates(i:snap(b), i:snap(a)) and b or a endcross(i:TBL, mum:row, dad:row) -> row – local.
One-point crossover.
local function cross(i,mum,dad, kid,cut)
kid, cut = copy(mum), rand(#i.cols.x)
for j,c in ipairs(i.cols.x) do
if j > cut then kid[c.at] = dad[c.at] end end
return kid endTBL:ga() -> row
Evolve np rows, gens times: domination tournament, cross,
mutate.
function TBL.ga(i, pop,kids)
pop = slice(shuffle(i.rows), 1, the.np)
for _ = 1, the.gens do
kids = {}
for _ = 1, the.np do
push(kids, i:mutate(cross(i, tourn(i, pop),
tourn(i, pop)))) end
pop = kids end
return i:snap(keysort(pop, function(r)
return i:guess(r) end)[1]) endTBL:de() -> row
de/rand/1: a kid replaces its parent, but only when it is
better.
function TBL.de(i, pop,es,t,kid,d,at)
pop = slice(shuffle(i.rows), 1, the.np)
es = map(pop, function(r) return i:guess(r) end)
for _ = 1, the.gens do
for j = 1, #pop do
t = some(pop, 3)
kid = i:extrapolate(t[1], t[2], t[3])
d = i:guess(kid)
if d < es[j] then pop[j], es[j] = kid, d end end end
at = 1
for j = 2, #es do if es[j] < es[at] then at = j end end
return i:snap(pop[at]) endclimb(i:TBL, accept:fun) -> row – local.
The (1+1) loop: mutate s, maybe accept the mutant, and
keep the best mutant ever seen.
local function climb(i,accept, s,e,b,eb,kid,d)
s = i.rows[rand(#i.rows)]
e = i:guess(s)
b, eb = s, e
for h = 1, the.budget1 do
kid = i:mutate(s)
d = i:guess(kid)
if d < eb then b, eb = kid, d end
if accept(e, d, h) then s, e = kid, d end end
return i:snap(b) endTBL:ls() -> row
Greedy local search: better, or bust.
function TBL.ls(i)
return climb(i, function(e,d) return d < e end) endTBL:sa() -> row
Simulated annealing: metropolis says yes to some bad
moves, and more rarely as the budget cools.
function TBL.sa(i)
return climb(i, function(e,d,h)
return d < e or rand() < exp((e - d) /
(1 - h/the.budget1 + 1e-32)) end) endTBL:race(repeats?:int) -> dict, dict
All four optimizers, plus best-of-np random rows, ranked
by same/ranks.
function TBL.race(i,repeats, d)
d = {ga={}, de={}, sa={}, ls={}, any={}}
for _ = 1, repeats or the.repeats do
push(d.ga, i:disty(i:ga()))
push(d.de, i:disty(i:de()))
push(d.sa, i:disty(i:sa()))
push(d.ls, i:disty(i:ls()))
push(d.any, i:disty(
keysort(some(i.rows, the.np), i:Y())[1])) end
return d, ranks(d) endeg = {}lua ezr-apps.lua --all
All the demos; fail if any of them do.
eg["--all"] = function( bad)
bad = 0
for _,k in ipairs(keys(eg)) do
if k ~= "--all" and run(eg, k) == false then
bad = bad + 1 end end
print("failures: " .. bad)
assert(bad == 0) endlua ezr-apps.lua --knn
Neighbours beat the global mean as a guess.
eg["--knn"] = function( t,y,mu,e1,e2,r)
t = Tbl(csv())
y = t:Y()
mu = adds(map(t.rows, y)).mu
e1, e2 = 0, 0
for _ = 1, 32 do
r = t.rows[rand(#t.rows)]
e1 = e1 + abs(t:knn(r) - y(r))
e2 = e2 + abs(mu - y(r)) end
print(("knn err %.3f vs mean-guess err %.3f")
:format(e1/32, e2/32))
assert(e1 < e2) endlua ezr-apps.lua --detect
Anomaly scores: someone in here is lonely.
eg["--detect"] = function( t,det,ss)
t = Tbl(csv())
det = t:anomaly()
ss = sorted(map(t.rows, det))
print(show{lo=ss[1], mid=ss[floor(#ss/2)+1], hi=ss[#ss]})
assert(0 <= ss[1] and ss[#ss] <= 1)
assert(ss[#ss] > 0.5) endlua ezr-apps.lua --nb
Test-then-train naive bayes, on diabetes.
eg["--nb"] = function( t,seen,a)
t = Tbl(csv"$MOOT/classify/diabetes.csv")
seen = t:classify()
a = acc(seen)
print(("diabetes: acc %.2f over %s guesses"):format(
a, #seen))
assert(a > 0.65) endlua ezr-apps.lua --kmeans
Clustering conserves rows.
eg["--kmeans"] = function( t,cs,n)
t = Tbl(csv())
cs = t:kmeans()
n = 0
for _,c in ipairs(cs) do n = n + #c.rows end
print("cluster sizes " ..
show(sorted(map(cs, function(c) return #c.rows end))))
assert(n == #t.rows and #cs <= the.kluster) endlua ezr-apps.lua --kpp
The seeds come out spread out.
eg["--kpp"] = function( t,cents,d)
t = Tbl(csv())
cents = t:kpp()
d = 1e32
for j = 1, #cents do
for k = j+1, #cents do
d = min(d, t:distx(cents[j], cents[k])) end end
print(("%s kpp seeds, min gap %.3f"):format(#cents, d))
assert(#cents == the.kluster and d > 0) endlua ezr-apps.lua --dominate
If a dominates b then a’s d2h is less; and often, nobody
wins.
eg["--dominate"] = function( t,y,n,tie,a,b,w)
t, y = Tbl(csv()), nil
y = t:Y()
n, tie = 0, 0
for _ = 1, 64 do
a = t.rows[rand(#t.rows)]
b = t.rows[rand(#t.rows)]
if t:dominates(a, b) then w = y(a) < y(b)
elseif t:dominates(b, a) then w = y(b) < y(a)
else w, tie = true, tie + 1 end
if w then n = n + 1 end end
print(("dominate agrees with d2h %s/64; "..
"indecisive on %s pairs"):format(n, tie))
assert(n == 64 and tie > 0) endlua ezr-apps.lua --ga
The evolved best beats the median row, easily.
eg["--ga"] = function( t,mid)
t = Tbl(csv())
mid = sorted(map(t.rows, t:Y()))[floor(#t.rows/2)]
print("ga best d2h " .. show(t:disty(t:ga())))
assert(t:disty(t:ga()) < mid) endlua ezr-apps.lua --de
Same test, differential evolution.
eg["--de"] = function( t,mid)
t = Tbl(csv())
mid = sorted(map(t.rows, t:Y()))[floor(#t.rows/2)]
print("de best d2h " .. show(t:disty(t:de())))
assert(t:disty(t:de()) < mid) endlua ezr-apps.lua --sa
Same test, simulated annealing.
eg["--sa"] = function( t,mid)
t = Tbl(csv())
mid = sorted(map(t.rows, t:Y()))[floor(#t.rows/2)]
print("sa best d2h " .. show(t:disty(t:sa())))
assert(t:disty(t:sa()) < mid) endlua ezr-apps.lua --ls
Same test, greedy local search.
eg["--ls"] = function( t,mid)
t = Tbl(csv())
mid = sorted(map(t.rows, t:Y()))[floor(#t.rows/2)]
print("ls best d2h " .. show(t:disty(t:ls())))
assert(t:disty(t:ls()) < mid) endlua ezr-apps.lua --race
The ch22 dragrace: 5 repeats each, ranked by same/ranks.
eg["--race"] = function( t,d,r,mid,mids)
t = Tbl(csv())
d, r = t:race()
mid = sorted(map(t.rows, t:Y()))[floor(#t.rows/2)]
mids = kap(d, function(k,v)
return k .. "=" .. show(sorted(v)[floor(#v/2)+1]) end)
print("median best d2h: " ..
table.concat(sorted(mids), " "))
print("rank 0: " .. show(sorted(r.winners)))
assert(#r.winners >= 1)
for _,v in pairs(d) do
assert(sorted(v)[floor(#v/2)+1] < mid) end endgo(eg)
return _ENV