Skip to content

Commit 0ed7eff

Browse files
authored
KNNRegressor (#136)
* KNNRegressor RST & SC examples * fixed sc code formatting * separate discussion and description!
1 parent b55a9ec commit 0ed7eff

File tree

2 files changed

+112
-62
lines changed

2 files changed

+112
-62
lines changed

doc/KNNRegressor.rst

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,46 +3,47 @@
33
:sc-categories: Regression
44
:sc-related: Classes/FluidKNNClassifier, Classes/FluidDataSet
55
:see-also:
6-
:description:
7-
A nearest-neighbour regressor. A continuous value is predicted for each point as the (weighted) average value of its nearest neighbours.
8-
9-
https://scikit-learn.org/stable/modules/neighbors.html#regression
10-
6+
:description: Regression between DataSets using weighted average of neighbours
7+
:discussion:
8+
9+
KNNRegressor is a supervised machine learning algorithm for regression. In order to make predictions, the KNNRegressor must first be ``fit`` with an input :fluid-obj:`DataSet` of data points, each of which is paired (by means of a shared identifier) with another data point in an output DataSet.
1110

11+
It uses an internal ``KDTree`` to find an input point's ``numNeighbours`` nearest neighbours in an input dataset. The output returned is a weighted average of those neighbours' values from the output DataSet.
12+
13+
The output DataSet must have only 1 dimension.
1214

1315
:control numNeighbours:
1416

15-
number of neigbours to consider in mapping, min 1
17+
Number of neighbours to consider when interpolating the regressed value. The default is 3.
1618

1719
:control weight:
1820

19-
Whether to weight neighbours by distance when producing new point
20-
21+
Whether to weight neighbours by distance when producing new point. The default is 1 (true).
2122

2223
:message fit:
2324

24-
:arg sourceDataSet: Source data
25+
:arg sourceDataSet: input :fluid-obj:`DataSet`
2526

26-
:arg targetDataSet: Target data
27+
:arg targetDataSet: output :fluid-obj:`DataSet` containing only one dimension.
2728

2829
:arg action: Run when done
2930

30-
Map a source :fluid-obj:`DataSet` to a one-dimensional target; both DataSets need to have the same number of points.
31+
Map an input :fluid-obj:`DataSet` to a one-dimensional output DataSet.
3132

3233
:message predict:
3334

34-
:arg sourceDataSet: data to regress
35+
:arg sourceDataSet: input :fluid-obj:`DataSet`
3536

36-
:arg targetDataSet: output data
37+
:arg targetDataSet: a :fluid-obj:`DataSet` to write the predictions into
3738

3839
:arg action: Run when done
3940

40-
Apply learned mapping to a :fluid-obj:`DataSet` and write to an output DataSet
41+
Apply learned mapping to a :fluid-obj:`DataSet` and write predictions to an output DataSet
4142

4243
:message predictPoint:
4344

4445
:arg buffer: data point
4546

4647
:arg action: Run when done
4748

48-
Apply learned mapping to a data point in a |buffer|
49+
Apply learned mapping to a data point in a |buffer| the predicted value is returned.

example-code/sc/KNNRegressor.scd

Lines changed: 96 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,113 @@
11

22
code::
33

4-
//Make a simple mapping between a ramp and a sine cycle, test with an exponentional ramp
4+
// Making an input dataset of a ramp from 0-1 and an output dataset of a sine wave
5+
// we'll have the KNNRegressor learn the relationship between the inputs and outputs
6+
// so that any input value provided will return where on the sine wave (what amplitude)
7+
// the output should be
58
(
6-
~source = FluidDataSet(s);
7-
~target = FluidDataSet(s);
8-
~test = FluidDataSet(s);
9-
~output = FluidDataSet(s);
10-
~tmpbuf = Buffer.alloc(s,1);
11-
~regressor = FluidKNNRegressor(s);
9+
~size = 128;
10+
~ds_ramp = FluidDataSet(s).load(
11+
Dictionary.newFrom([
12+
\cols,1,
13+
\data,Dictionary.newFrom(
14+
~size.collect{arg i;
15+
[i,i/~size]; // linear: 128 steps from 0 to (slightly less than) 1
16+
}.flat
17+
)
18+
])
19+
);
20+
21+
~ds_sine = FluidDataSet(s).load(
22+
Dictionary.newFrom([
23+
\cols,1,
24+
\data,Dictionary.newFrom(
25+
~size.collect{
26+
arg i;
27+
[i,sin(2pi*i/~size)]; // sine wave
28+
}.flat;
29+
)
30+
])
31+
);
1232
)
1333

14-
//Make source, target and test data
34+
// fit to make the KNNRegressor learn the relationship between inputs and outputs
1535
(
16-
~sourcedata = 128.collect{|i|i/128};
17-
~targetdata = 128.collect{|i| sin(2*pi*i/128) };
18-
~testdata = 128.collect{|i|(i/128)**2};
19-
20-
~source.load(
21-
Dictionary.with(
22-
*[\cols -> 1,\data -> Dictionary.newFrom(
23-
~sourcedata.collect{|x, i| [i.asString, [x]]}.flatten)])
24-
);
36+
~regressor = FluidKNNRegressor(s).fit(~ds_ramp,~ds_sine);
37+
)
2538

26-
~target.load(
27-
d = Dictionary.with(
28-
*[\cols -> 1,\data -> Dictionary.newFrom(
29-
~targetdata.collect{|x, i| [i.asString, [x]]}.flatten)]);
30-
);
39+
// predicting with input dataset should give us what we expect: something that
40+
// looks like a sine wave.
41+
(
42+
~predictions = FluidDataSet(s);
43+
~regressor.predict(~ds_ramp,~predictions,{
44+
~predictions.dump({
45+
arg dict;
46+
var array = Array.newClear(~size);
47+
dict["data"].keysValuesDo{
48+
arg id, v;
49+
array[id.asInteger] = v[0];
50+
};
51+
{array.plot}.defer;
52+
});
53+
});
54+
)
3155

32-
~test.load(
33-
Dictionary.with(
34-
*[\cols -> 1,\data -> Dictionary.newFrom(
35-
~testdata.collect{|x, i| [i.asString, [x]]}.flatten)])
56+
// now instead of using the linear ramp to derive the output, we'll use a warped ramp: an exponetial curve
57+
// make that dataset to use as input:
58+
(
59+
~ds_exponential = FluidDataSet(s).load(
60+
Dictionary.newFrom([
61+
\cols,1,
62+
\data,Dictionary.newFrom(
63+
~size.collect{arg i;
64+
[i,(i/~size)**2];
65+
}.flat;
66+
)
67+
])
3668
);
69+
)
3770

38-
~targetdata.plot;
39-
~source.print;
40-
~target.print;
41-
~test.print;
42-
71+
// use the regressor to make predictions based on that input:
72+
(
73+
~regressor.predict(~ds_exponential,~predictions,{
74+
~predictions.dump({
75+
arg dict;
76+
var array = Array.newClear(~size);
77+
dict["data"].keysValuesDo{
78+
arg id, v;
79+
array[id.asInteger] = v[0];
80+
};
81+
array.postln;
82+
{array.plot}.defer;
83+
});
84+
});
4385
)
4486

45-
// Now make a regressor and fit it to the source and target, and predict against test
46-
//grab the output data whilst we're at it, so we can inspect
87+
// just for fun let's use the sine wave ds as input...
88+
// notice that all the negative values of the sine wave
89+
// (the second half) are going to have the same three nearest
90+
// neighbours and therefore the same value for their prediction!
4791
(
48-
~outputdata = Array(128);
49-
~regressor.fit(~source, ~target);
50-
~regressor.predict(~test, ~output, 1, action:{
51-
~output.dump{|x| 128.do{|i|
52-
~outputdata.add(x["data"][i.asString][0])
53-
}};
92+
~regressor.predict(~ds_sine,~predictions,{
93+
~predictions.dump({
94+
arg dict;
95+
var array = Array.newClear(~size);
96+
dict["data"].keysValuesDo{
97+
arg id, v;
98+
array[id.asInteger] = v[0];
99+
};
100+
array.postln;
101+
{array.plot}.defer;
102+
});
54103
});
55104
)
56105

57-
//We should see a single cycle of a chirp
58-
~outputdata.plot;
106+
::
107+
strong::single point transform on arbitrary value::
108+
code::
109+
~inbuf = Buffer.loadCollection(s,[0.3]);
59110

60-
// single point transform on arbitrary value
61-
~inbuf = Buffer.loadCollection(s,[0.5]);
62111
~regressor.predictPoint(~inbuf,{|x|x.postln;});
63112
::
64113

@@ -70,11 +119,11 @@ code::
70119
{
71120
var input = Saw.kr(2).linlin(-1,1,0,1);
72121
var trig = Impulse.kr(ControlRate.ir/10);
73-
var inputPoint = LocalBuf(1);
74-
var outputPoint = LocalBuf(1);
122+
var inputPoint = LocalBuf(1);
123+
var outputPoint = LocalBuf(1);
75124
BufWr.kr(input,inputPoint,0);
76-
~regressor.kr(trig,inputPoint,outputPoint);
77-
BufRd.kr(1,outputPoint,0);
125+
~regressor.kr(trig,inputPoint,outputPoint);
126+
BufRd.kr(1,outputPoint,0);
78127
}.scope
79128
)
80129

0 commit comments

Comments
 (0)