local help = [[
ezr3-dtlz.lua: drive ezr3 with an EXTERNAL MODEL, no csv.
DTLZ1-7 rows are born with "?" goals; disty computes them
on demand, folding each new label into the column summaries
so normalization sharpens as spending grows. This file is
the seam where outsiders plug in their own (maybe very
expensive) models. Demos at the bottom; run with --all.
options:
model=dtlz2 one of dtlz1..dtlz7
M=2 objectives, all minimized
Nx=6 decision variables, all in 0..1
pool=1000 rows per fresh pool]]
local abs,cos,sin,pi = math.abs, math.cos, math.sin, math.pifind 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)Each maps x in [0,1]^Nx to M objectives to minimize. The
last Nx-M+1 x’s (xm) set distance from the true front;
the first M-1 shape position along it. The signature
above each function is a reading aid, not a check: x is
the list of decision variables, f the list of
objectives, and names after the wide gap in an argument
list are LOCALS.
g1(xm:list) -> num
Multi-modal distance (dtlz1, dtlz3).
function g1(xm)
return 100 * (#xm + sum(xm, function(v)
return (v-.5)^2 - cos(20*pi*(v-.5)) end)) endg2(xm:list) -> num
Unimodal distance (dtlz2, dtlz4, dtlz5).
function g2(xm)
return sum(xm, function(v) return (v-.5)^2 end) endg6(xm:list) -> num
Biased distance (dtlz6).
function g6(xm)
return sum(xm, function(v) return v^0.1 end) endsphere(M:int, g:num, th:list) -> list
The cos/sin product shared by dtlz2 through dtlz6.
function sphere(M,g,th, f,v)
f = {}
for i = 0, M-1 do
v = 1 + g
for j = 1, M-1-i do v = v * cos(th[j]) end
if i > 0 then v = v * sin(th[M-i]) end
push(f, v) end
return f enddtlz1(x:list, M:int) -> list
Linear front: the objectives sum to .5(1+g).
function dtlz1(x,M, g,f,v)
g, f = g1(slice(x, M)), {}
for i = 0, M-1 do
v = 0.5 * (1 + g)
for j = 1, M-1-i do v = v * x[j] end
if i > 0 then v = v * (1 - x[M-i]) end
push(f, v) end
return f enddtlz2(x:list, M:int) -> list
Spherical front.
function dtlz2(x,M)
return sphere(M, g2(slice(x, M)),
map(slice(x, 1, M-1), function(v)
return v * pi/2 end)) enddtlz3(x:list, M:int) -> list
Spherical, multi-modal.
function dtlz3(x,M)
return sphere(M, g1(slice(x, M)),
map(slice(x, 1, M-1), function(v)
return v * pi/2 end)) enddtlz4(x:list, M:int) -> list
Spherical, biased sampling.
function dtlz4(x,M)
return sphere(M, g2(slice(x, M)),
map(slice(x, 1, M-1), function(v)
return v^100 * pi/2 end)) enddegen(x:list, M:int, g:num) -> list
The degenerate curve behind dtlz5 and dtlz6.
function degen(x,M,g, th)
th = {x[1] * pi/2}
for i = 2, M-1 do
th[i] = pi/(4*(1+g)) * (1 + 2*g*x[i]) end
return sphere(M, g, th) enddtlz5(x:list, M:int) -> list – degenerate, unimodal.
function dtlz5(x,M) return degen(x, M, g2(slice(x, M))) enddtlz6(x:list, M:int) -> list – degenerate, biased.
function dtlz6(x,M) return degen(x, M, g6(slice(x, M))) enddtlz7(x:list, M:int) -> list
Disconnected front.
function dtlz7(x,M, k,g,f,h)
k = #x - M + 1
g = 1 + 9/k * sum(slice(x, M), function(v) return v end)
f = slice(x, 1, M-1)
h = M - sum(f, function(fi)
return fi/(1+g) * (1 + sin(3*pi*fi)) end)
push(f, (1+g) * h)
return f endThe lazy-label seam (t.model + TBL.label) lives in ezr3 now: all this file adds is pool-making and the models.
Dtlz() -> TBL
A Tbl over one fresh, blank pool: x cells random, y cells
still “?”.
function Dtlz( u,r)
u = {names()}
for _ = 1, the.pool do
r = {}
for _ = 1, the.Nx do push(r, rand()) end
for _ = 1, the.M do push(r, "?") end
push(u, r) end
u = Tbl(u)
u.model = _ENV[the.model]
return u endnames() -> list
The header: X1..XNx, then F1-..FM- (all minimized).
function names( u)
u = {}
for j = 1, the.Nx do push(u, "X"..j) end
for m = 1, the.M do push(u, "F"..m.."-") end
return u endTBL:baseline() -> row
Mean of every goal over the whole pool. These are raw
model calls: nothing folds into the summaries, so the
baseline never sharpens the ruler it is measured against.
function TBL.baseline(i, nums,f)
nums = map(i.cols.y, function() return Num() end)
for _,r in ipairs(i.rows) do
f = i.model(map(i.cols.x,
function(c) return r[c.at] end), #i.cols.y)
for j,v in ipairs(f) do nums[j]:add(v) end end
return map(nums, "mid") endinstance(t:TBL, row:row)
Print one row: its x, then its f, then its disty.
function instance(t,row)
print(" x " .. show(slice(row, 1, the.Nx)))
print((" f %s (disty %.3f, lower=better)"):format(
show(slice(row, the.Nx + 1)), t:disty(row))) endeg = {}lua ezr-dtlz.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-dtlz.lua --fronts
Known geometry: dtlz1 leaves sum f = .5(1+g); dtlz2
leaves sum f^2 = (1+g)^2.
eg["--fronts"] = function( x,f,s,g)
for _ = 1, 100 do
x = {}
for j = 1, 6 do x[j] = rand() end
g = g1(slice(x, 2))
f = dtlz1(x, 2)
s = sum(f, function(v) return v end)
assert(abs(s - 0.5*(1+g)) < 1e-9 * (1+g))
g = g2(slice(x, 2))
f = dtlz2(x, 2)
s = sum(f, function(v) return v*v end)
assert(abs(s - (1+g)^2) < 1e-9 * (1+g)^2) end
print"100 rounds: dtlz1 linear, dtlz2 spherical: ok" endlua ezr-dtlz.lua --label
Goals appear on demand, and only on demand.
eg["--label"] = function( t,r)
t = Dtlz()
r = t.rows[1]
assert(r[t.cols.y[1].at] == "?") -- born blank
t:disty(r) -- the seam fires
assert(r[t.cols.y[1].at] ~= "?") -- now labelled
print("labelled: " .. show(slice(r, the.Nx + 1)))
assert(t.cols.y[1].n == 1) end -- and folded inlua ezr-dtlz.lua --models
All 7 models run; 50 labels each, and the ruler sharpens
as they arrive.
eg["--models"] = function( t,d,lo,hi,best)
for _,m in ipairs{"dtlz1","dtlz2","dtlz3","dtlz4",
"dtlz5","dtlz6","dtlz7"} do
the.model = m
t = Dtlz()
for j = 1, 50 do t:disty(t.rows[j]) end -- label 50, then
lo, hi = 2, -1 -- rescore all on the warmed ruler
for j = 1, 50 do
d = t:disty(t.rows[j])
if d < lo then lo, best = d, t.rows[j] end
if d > hi then hi = d end end
print(("%-6s disty %.3f .. %.3f best f %s mean f %s")
:format(m, lo, hi, show(slice(best, the.Nx + 1)),
show(t:baseline())))
for _,y in ipairs(t.cols.y) do -- finite, and
assert(best[y.at] == best[y.at] -- not negative
and best[y.at] >= 0) end
assert(0 <= lo and lo < hi and hi <= 1) -- real spread
assert(t.cols.y[1].n == 50) end -- 50 labels in
the.model = "dtlz2" end -- restore the defaultlua ezr-dtlz.lua --pure
Rank the whole pool, with no train/test split.
eg["--pure"] = function( t,lab)
t = Dtlz()
lab = t:acquirer(the.budget - the.check)
print("mean f, all " .. #t.rows .. " rows: "
.. show(t:baseline()))
print("best found (one instance):")
instance(t, lab[1])
assert(t:disty(lab[1]) <= t:disty(lab[#lab])) endlua ezr-dtlz.lua --why
Which x-ranges reach the good goals?
eg["--why"] = function( t,lab)
t = Dtlz()
lab = t:acquirer(the.budget - the.check)
Tree(t, lab):show(t)
assert(#lab <= the.budget) endlua ezr-dtlz.lua --generalize
The best pick, made on rows never seen before.
eg["--generalize"] = function( t,best)
t = Dtlz()
best = t:holdout()
print("mean f, all " .. #t.rows .. " rows: "
.. show(t:baseline()))
instance(t, best)
assert(t:disty(best) <= 1) endlua ezr-dtlz.lua --wins
Grade each search against the fully labelled pool:
100 = the true best, 0 = the median row, negative = worse.
eg["--wins"] = function( t,lab,W,w)
for _,m in ipairs{"dtlz1","dtlz2","dtlz3","dtlz4",
"dtlz5","dtlz6","dtlz7"} do
the.model = m
t = Dtlz()
lab = t:acquirer(the.budget - the.check)
W = t:wins() -- labels the whole pool
w = W(lab[1])
print(("%-6s win %4.0f (%s labels vs %s)"):format(
m, w, #lab, #t.rows))
assert(-100 <= w and w <= 100) end
the.model = "dtlz2" end -- restore the defaultgo(eg)
return _ENV