Skip to content

Commit 9e3bea3

Browse files
authored
Add genetic programming (#1116)
* Add genetic programming * Fix test name * Reset displayed estimated function * Change loss function and increate test retry time
1 parent a436483 commit 9e3bea3

6 files changed

Lines changed: 430 additions & 1 deletion

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ for (let i = 0; i < n; i++) {
124124
| clustering | (Soft / Kernel / Genetic / Weighted / Bisecting) k-means, k-means++, k-medois, k-medians, x-means, G-means, (DC) DP-means, LBG, ISODATA, Fuzzy c-means, Possibilistic c-means, k-harmonic means, MacQueen, Hartigan-Wong, Phillips, Elkan, Hamelry, Drake, Yinyang, Agglomerative (complete linkage, single linkage, group average, Ward's, centroid, weighted average, median), DIANA, Monothetic, Mutual kNN, (Blurring / Weighted Blurring) Mean shift, DBSCAN, OPTICS, DTSCAN, HDBSCAN, DENCLUE, DBCLASD, BRIDGE, CLUES, PAM, CLARA, CLARANS, BIRCH, CURE, ROCK, C2P, STING, PLSA, Latent dirichlet allocation, GMM, VBGMM, Affinity propagation, Spectral clustering, Mountain, (Growing) SOM, GTM, (Growing) Neural gas, Growing cell structures, LVQ, ART, SVC, CAST, CHAMELEON, COLL, CLIQUE, PROCLUS, ORCLUS, FINDIT, DOC, FastDOC, DiSH, LMCLUS, NMF, Autoencoder |
125125
| classification | (Fisher's) Linear discriminant, Quadratic discriminant, Mixture discriminant, Least squares, (Multiclass / Kernel) Ridge, (Complement / Negation / Universal-set / Selective) Naive Bayes (gaussian), AODE, (Fuzzy / Weighted) k-nearest neighbor, Radius neighbor, Nearest centroid, ENN, ENaN, NNBCA, ADAMENN, DANN, IKNN, Decision tree, Random forest, Extra trees, GBDT, XGBoost, ALMA, (Aggressive) ROMMA, (Bounded) Online gradient descent, (Budgeted online) Passive aggressive, RLS, (Selective-sampling) Second order perceptron, AROW, NAROW, Confidence weighted, CELLIP, IELLIP, Normal herd, Stoptron, (Kernelized) Pegasos, MIRA, Forgetron, Projectron, Projectron++, Banditron, Ballseptron, (Multiclass) BSGD, ILK, SILK, (Multinomial) Logistic regression, (Multinomial) Probit, SVM, Gaussian process, HMM, CRF, Bayesian Network, LVQ, (Average / Multiclass / Voted / Kernelized / Selective-sampling / Margin / Shifting / Budget / Tighter / Tightest) Perceptron, PAUM, RBP, ADALINE, MADALINE, MLP, ELM, LMNN, OneR |
126126
| semi-supervised classification | k-nearest neighbor, Radius neighbor, Label propagation, Label spreading, k-means, GMM, S3VM, Ladder network |
127-
| regression | Least squares, Ridge, Lasso, Elastic net, RLS, Bayesian linear, Poisson, Least absolute deviations, Huber, Tukey, Least trimmed squares, Least median squares, Lp norm linear, SMA, Deming, Segmented, LOWESS, LOESS, spline, Naive Bayes, Gaussian process, Principal components, Partial least squares, Projection pursuit, Quantile regression, k-nearest neighbor, Radius neighbor, IDW, Nadaraya Watson, Priestley Chao, Gasser Muller, RBF Network, RVM, Decision tree, Random forest, Extra trees, GBDT, XGBoost, SVR, MARS, MLP, ELM, GMR, Isotonic, Ramer Douglas Peucker, Theil-Sen, Passing-Bablok, Repeated median |
127+
| regression | Least squares, Ridge, Lasso, Elastic net, RLS, Bayesian linear, Poisson, Least absolute deviations, Huber, Tukey, Least trimmed squares, Least median squares, Lp norm linear, SMA, Deming, Segmented, LOWESS, LOESS, spline, Naive Bayes, Gaussian process, Principal components, Partial least squares, Projection pursuit, Quantile regression, k-nearest neighbor, Radius neighbor, IDW, Nadaraya Watson, Priestley Chao, Gasser Muller, RBF Network, RVM, Decision tree, Random forest, Extra trees, GBDT, XGBoost, SVR, MARS, MLP, ELM, GMR, Isotonic, Ramer Douglas Peucker, Theil-Sen, Passing-Bablok, Repeated median, Genetic programming |
128128
| interpolation | Nearest neighbor, IDW, (Spherical) Linear, Brahmagupta, Logarithmic, Cosine, (Inverse) Smoothstep, Cubic, (Centripetal) Catmull-Rom, Hermit, Polynomial, Lagrange, Trigonometric, Spline, RBF Network, Akima, Natural neighbor, Delaunay |
129129
| learning to rank | Ordered logistic, Ordered probit, PRank, OAP-BPM, RankNet |
130130
| anomaly detection | Percentile, MAD, Tukey's fences, Grubbs's test, Thompson test, Tietjen Moore test, Generalized ESD, Hotelling, MT, MCD, k-nearest neighbor, LOF, COF, ODIN, LDOF, INFLO, LOCI, LoOP, RDF, LDF, KDEOS, RDOS, NOF, RKOF, ABOD, PCA, OCSVM, (Multivariate) KDE, GMM, Isolation forest, Autoencoder, GAN |

js/model_selector.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -336,6 +336,7 @@ const AIMethods = [
336336
{ value: 'theil_sen', title: 'Theil-Sen' },
337337
{ value: 'passing_bablok', title: 'Passing Bablok' },
338338
{ value: 'rmr', title: 'Repeated Median' },
339+
{ value: 'genetic_programming', title: 'Genetic Programming' },
339340
],
340341
},
341342
},

js/view/genetic_programming.js

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import GeneticProgramming from '../../lib/model/genetic_programming.js'
2+
import Controller from '../controller.js'
3+
4+
export default function (platform) {
5+
platform.setting.ml.usage = 'Click and add data point. Next, click "Fit" button.'
6+
platform.setting.ml.reference = {
7+
author: 'J. R. Koza',
8+
title: 'Genetic Programming as a Means for Programming Computers by Natural Selection',
9+
year: 1994,
10+
}
11+
const controller = new Controller(platform)
12+
let model = null
13+
const fitModel = () => {
14+
if (!model) {
15+
model = new GeneticProgramming()
16+
model.init(platform.trainInput, platform.trainOutput)
17+
}
18+
model.fit()
19+
20+
const pred = model.predict(platform.testInput(4))
21+
platform.testResult(pred)
22+
expr.value = model.bestPrograms[0].toString()
23+
}
24+
25+
controller
26+
.stepLoopButtons()
27+
.init(() => {
28+
model = null
29+
expr.value = ''
30+
platform.init()
31+
})
32+
.step(fitModel)
33+
.epoch()
34+
const expr = controller.div().text({ label: 'estimated function: ' })
35+
}

lib/model/genetic_programming.js

Lines changed: 287 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,287 @@
1+
class Node {
2+
constructor(value) {
3+
this.value = value
4+
this.children = []
5+
}
6+
7+
get length() {
8+
if (typeof this.value === 'function' || this.value instanceof VectorizedFunction) {
9+
return this.value.length
10+
}
11+
return 0
12+
}
13+
14+
addChild(child) {
15+
this.children.push(child)
16+
}
17+
18+
copy() {
19+
const node = new Node(this.value)
20+
node.children = this.children.map(c => c.copy())
21+
return node
22+
}
23+
24+
evaluate(env) {
25+
if (typeof this.value === 'function') {
26+
return this.value(...this.children.map(child => child.evaluate(env)))
27+
}
28+
if (this.value instanceof VectorizedFunction) {
29+
return this.value.evaluate(...this.children.map(child => child.evaluate(env)))
30+
}
31+
if (Object.hasOwn(env, this.value)) {
32+
return env[this.value]
33+
}
34+
return this.value
35+
}
36+
37+
toString() {
38+
if (this.value instanceof VectorizedFunction) {
39+
return this.value.toString(...this.children.map(child => child.toString()))
40+
}
41+
if (typeof this.value === 'function') {
42+
return `${this.value.name}(${this.children.map(child => child.toString()).join(', ')})`
43+
}
44+
return `${this.value}`
45+
}
46+
}
47+
48+
class VectorizedFunction {
49+
constructor(func, name) {
50+
this._originalfunc = func
51+
this._name = name
52+
this._vectorizedFunc = (...args) => {
53+
if (args.every(v => !Array.isArray(v))) {
54+
return this._originalfunc(...args)
55+
}
56+
const length = args.reduce((l, v) => (Array.isArray(v) ? Math.max(l, v.length) : l), 1)
57+
const result = []
58+
for (let i = 0; i < length; i++) {
59+
result[i] = this._originalfunc(...args.map(v => (Array.isArray(v) ? v[i] : v)))
60+
}
61+
return result
62+
}
63+
}
64+
65+
get length() {
66+
return this._originalfunc.length
67+
}
68+
69+
get name() {
70+
return this._originalfunc.name || this._name
71+
}
72+
73+
evaluate(...args) {
74+
return this._vectorizedFunc(...args)
75+
}
76+
77+
toString(...args) {
78+
return `${this.name}(${args.join(', ')})`
79+
}
80+
}
81+
82+
class BinaryFunction extends VectorizedFunction {
83+
toString(...args) {
84+
return `(${args.join(` ${this.name} `)})`
85+
}
86+
}
87+
88+
class Program {
89+
constructor(root) {
90+
this._p = root
91+
}
92+
93+
static create(funcs, variables, depth = 2) {
94+
const root = new Node(funcs[Math.floor(Math.random() * funcs.length)])
95+
let stack = [root]
96+
for (let i = 0; i < depth; i++) {
97+
const newStack = []
98+
for (const node of stack) {
99+
for (let j = 0; j < node.length; j++) {
100+
const child = new Node(funcs[Math.floor(Math.random() * funcs.length)])
101+
node.addChild(child)
102+
newStack.push(child)
103+
}
104+
}
105+
stack = newStack
106+
}
107+
for (const node of stack) {
108+
for (let j = 0; j < node.length; j++) {
109+
if (Math.random() < 0.5) {
110+
node.addChild(new Node(variables[Math.floor(Math.random() * variables.length)]))
111+
} else {
112+
const x = Math.random()
113+
const y = Math.random()
114+
const X = Math.sqrt(-2 * Math.log(x)) * Math.cos(2 * Math.PI * y)
115+
node.addChild(new Node(X))
116+
}
117+
}
118+
}
119+
const p = new Program(root)
120+
p.normalize()
121+
return p
122+
}
123+
124+
*nodes() {
125+
const stack = [this._p]
126+
while (stack.length > 0) {
127+
const node = stack.shift()
128+
stack.push(...node.children)
129+
yield node
130+
}
131+
}
132+
133+
normalize() {
134+
const nodes = [...this.nodes()]
135+
for (let i = nodes.length - 1; i >= 0; i--) {
136+
const node = nodes[i]
137+
if (node.children.some(c => typeof c.value !== 'number')) {
138+
continue
139+
}
140+
node.value = node.evaluate({})
141+
}
142+
}
143+
144+
mix(other) {
145+
const cp = new Program(this._p.copy())
146+
const thisNodes = [...cp.nodes()]
147+
const otherNodes = [...other.nodes()]
148+
const thisIdx = Math.floor(Math.random() * thisNodes.length)
149+
const otherIdx = Math.floor(Math.random() * otherNodes.length)
150+
thisNodes[thisIdx].value = otherNodes[otherIdx].value
151+
thisNodes[thisIdx].children = otherNodes[otherIdx].children.map(c => c.copy())
152+
cp.normalize()
153+
return cp
154+
}
155+
156+
evaluate(env) {
157+
return this._p.evaluate(env)
158+
}
159+
160+
toString() {
161+
return this._p.toString()
162+
}
163+
}
164+
165+
const functions = {
166+
'+': new BinaryFunction((a, b) => a + b, '+'),
167+
'-': new BinaryFunction((a, b) => a - b, '-'),
168+
'*': new BinaryFunction((a, b) => a * b, '*'),
169+
'/': new BinaryFunction((a, b) => a / b, '/'),
170+
}
171+
172+
/**
173+
* Genetic Programming
174+
*/
175+
export default class GeneticProgramming {
176+
// Genetic Programming as a Means for Programming Computers by Natural Selection
177+
// https://www.genetic-programming.com/jkpdf/scjournallong.pdf
178+
// https://qiita.com/shinjikato/items/f482637d1976a0ca6b7c
179+
/**
180+
* @param {('+' | '-' | '*' | '/' | function (number, number): number)[]} [funcs] Functions to use
181+
* @param {number} [size] Number of populations per generation
182+
*/
183+
constructor(funcs = ['+', '-', '*', '/'], size = 100) {
184+
this._progs = []
185+
this._funcs = funcs.map(func => {
186+
if (typeof func === 'string') {
187+
return functions[func]
188+
}
189+
return new VectorizedFunction(func)
190+
})
191+
this._variables = []
192+
this._size = size
193+
this._loss = (y, y_pred) => {
194+
if (Array.isArray(y_pred)) {
195+
return y.reduce((s, v, i) => s + (v - y_pred[i]) ** 2, 1) / y.length
196+
}
197+
return y.reduce((s, v) => s + (v - y_pred) ** 2, 1) / y.length
198+
}
199+
}
200+
201+
/**
202+
* @returns {Program[]} Best programs for each outputs
203+
*/
204+
get bestPrograms() {
205+
return this._progs.map(p => p[0].p)
206+
}
207+
208+
/**
209+
* Initialize model.
210+
* @param {Array<Array<number>>} x Training data
211+
* @param {Array<Array<number>>} y Target values
212+
*/
213+
init(x, y) {
214+
this._x = x
215+
this._y = y
216+
this._variables = Array.from(this._x[0], (_, i) => `x[${i}]`)
217+
this._outDim = y[0].length
218+
219+
this._inputs = {}
220+
for (let i = 0; i < this._variables.length; i++) {
221+
this._inputs[this._variables[i]] = this._x.map(xi => xi[i])
222+
}
223+
this._outputs = []
224+
for (let i = 0; i < this._outDim; i++) {
225+
this._outputs[i] = this._y.map(v => v[i])
226+
}
227+
for (let d = 0; d < this._outDim; d++) {
228+
this._progs[d] = []
229+
for (let i = 0; i < this._size * 2; i++) {
230+
const p = Program.create(this._funcs, this._variables)
231+
this._progs[d].push({
232+
p,
233+
loss: this._loss(this._outputs[d], p.evaluate(this._inputs)),
234+
})
235+
}
236+
this._progs[d].sort((a, b) => a.loss - b.loss)
237+
this._progs[d] = this._progs[d].slice(0, this._size)
238+
}
239+
}
240+
241+
/**
242+
* Fit model.
243+
*/
244+
fit() {
245+
for (let d = 0; d < this._outDim; d++) {
246+
const newProgs = [...this._progs[d]]
247+
for (let i = 0; i < this._size; i++) {
248+
const sump = this._progs[d].reduce((s, p) => s + 1 / p.loss, 0)
249+
let r = Math.random() * sump
250+
for (let j = 0; j < this._size; j++) {
251+
r -= 1 / this._progs[d][i].loss
252+
if (r <= 0) {
253+
const p = this._progs[d][i].p.mix(this._progs[d][j].p)
254+
newProgs.push({
255+
p,
256+
loss: this._loss(this._outputs[d], p.evaluate(this._inputs)),
257+
})
258+
break
259+
}
260+
}
261+
}
262+
newProgs.sort((a, b) => a.loss - b.loss)
263+
this._progs[d] = newProgs.slice(0, this._size)
264+
}
265+
return this._progs.reduce((s, v) => s + v[0].loss, 0) / this._outDim
266+
}
267+
268+
/**
269+
* Returns predicted values.
270+
* @param {Array<Array<number>>} x Sample data
271+
* @returns {Array<Array<number>>} Predicted values
272+
*/
273+
predict(x) {
274+
const inputs = {}
275+
for (let i = 0; i < x[0].length; i++) {
276+
inputs[`x[${i}]`] = x.map(xi => xi[i])
277+
}
278+
const result = Array.from({ length: x.length }, () => [])
279+
for (let d = 0; d < this._outDim; d++) {
280+
const od = this._progs[d][0].p.evaluate(inputs)
281+
for (let i = 0; i < x.length; i++) {
282+
result[i][d] = Array.isArray(od) ? od[i] : od
283+
}
284+
}
285+
return result
286+
}
287+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { getPage } from '../helper/browser'
2+
3+
describe('regression', () => {
4+
/** @type {Awaited<ReturnType<getPage>>} */
5+
let page
6+
beforeEach(async () => {
7+
page = await getPage()
8+
const taskSelectBox = page.locator('#ml_selector dl:first-child dd:nth-child(5) select')
9+
await taskSelectBox.selectOption('RG')
10+
const modelSelectBox = page.locator('#ml_selector .model_selection #mlDisp')
11+
await modelSelectBox.selectOption('genetic_programming')
12+
})
13+
14+
afterEach(async () => {
15+
await page?.close()
16+
})
17+
18+
test('initialize', async () => {
19+
const methodMenu = page.locator('#ml_selector #method_menu')
20+
const buttons = methodMenu.locator('.buttons')
21+
22+
const epoch = buttons.locator('[name=epoch]')
23+
await expect(epoch.textContent()).resolves.toBe('0')
24+
})
25+
26+
test('learn', async () => {
27+
const methodMenu = page.locator('#ml_selector #method_menu')
28+
const buttons = methodMenu.locator('.buttons')
29+
30+
const epoch = buttons.locator('[name=epoch]')
31+
await expect(epoch.textContent()).resolves.toBe('0')
32+
const methodFooter = page.locator('#method_footer')
33+
await expect(methodFooter.textContent()).resolves.toBe('')
34+
35+
const initButton = buttons.locator('input[value=Initialize]')
36+
await initButton.dispatchEvent('click')
37+
const stepButton = buttons.locator('input[value=Step]:enabled')
38+
await stepButton.dispatchEvent('click')
39+
40+
await expect(epoch.textContent()).resolves.toBe('1')
41+
await expect(methodFooter.textContent()).resolves.toMatch(/^RMSE:[0-9.]+$/)
42+
})
43+
})

0 commit comments

Comments
 (0)