diff --git a/release-packaging/Classes/FluidKNNClassifier.sc b/release-packaging/Classes/FluidKNNClassifier.sc index ef6a7b6..5f1fbac 100644 --- a/release-packaging/Classes/FluidKNNClassifier.sc +++ b/release-packaging/Classes/FluidKNNClassifier.sc @@ -1,18 +1,27 @@ FluidKNNClassifier : FluidManipulationClient { + + *new {|server| + var uid = UniqueID.next; + ^super.new(server,uid)!?{|inst|inst.init(uid);inst} + } + + init {|uid| + id = uid; + } + fit{|dataset, labelset, action| - this.pr_sendMsg(\fit,[dataset.asString, labelset.asString], action); + this.prSendMsg(\fit,[dataset.asSymbol, labelset.asSymbol], action); } - predict{ |dataset, labelset, k, action| - this.pr_sendMsg(\predict, - [dataset.asString, labelset.asString, k], - action, [string(FluidMessageResponse,_,_)] - ); + predict{ |dataset, labelset, k, uniform = 0, action| + this.prSendMsg(\predict, + [dataset.asSymbol, labelset.asSymbol, k, uniform], + action); } - predictPoint { |buffer, k, action| - this.pr_sendMsg(\predictPoint, - [buffer.asUGenInput, k], action, + predictPoint { |buffer, k, uniform = 0, action| + this.prSendMsg(\predictPoint, + [buffer.asUGenInput, k,uniform], action, [number(FluidMessageResponse,_,_)] ); } diff --git a/release-packaging/Classes/FluidKNNRegressor.sc b/release-packaging/Classes/FluidKNNRegressor.sc index d3716b0..74c0023 100644 --- a/release-packaging/Classes/FluidKNNRegressor.sc +++ b/release-packaging/Classes/FluidKNNRegressor.sc @@ -1,20 +1,29 @@ FluidKNNRegressor : FluidManipulationClient { + + *new {|server| + var uid = UniqueID.next; + ^super.new(server,uid)!?{|inst|inst.init(uid);inst} + } + + init {|uid| + id = uid; + } + fit{|sourceDataset, targetDataset, action| - this.pr_sendMsg(\fit, - [sourceDataset.asString, targetDataset.asString], + this.prSendMsg(\fit, + [sourceDataset.asSymbol, targetDataset.asSymbol], action ); } - predict{ |sourceDataset, targetDataset, k, action| - this.pr_sendMsg(\predict, - [sourceDataset.asString, targetDataset.asString, k], - action, - [string(FluidMessageResponse,_,_)]); + predict{ |sourceDataset, targetDataset, k, uniform = 0, action| + this.prSendMsg(\predict, + [sourceDataset.asSymbol, targetDataset.asSymbol, k, uniform], + action); } - predictPoint { |buffer, k, action| - this.pr_sendMsg(\predictPoint, [buffer.asUGenInput, k], action, + predictPoint { |buffer, k, uniform = 0, action| + this.prSendMsg(\predictPoint, [buffer.asUGenInput, k,uniform], action, [number(FluidMessageResponse,_,_)]); } } diff --git a/release-packaging/HelpSource/Classes/FluidKNNClassifier.schelp b/release-packaging/HelpSource/Classes/FluidKNNClassifier.schelp new file mode 100644 index 0000000..eac0ecb --- /dev/null +++ b/release-packaging/HelpSource/Classes/FluidKNNClassifier.schelp @@ -0,0 +1,161 @@ +TITLE:: FluidKNNClassifier +summary:: Classify data with K Nearest Neighbours +categories:: Classification, KNN +related:: Classes/FluidKNNRegressor, Classes/FluidDataSet, Classes/FluidLabelSet + +DESCRIPTION:: + +CLASSMETHODS:: + +METHOD:: new +Create a new KNNClassifier +ARGUMENT:: server +The server to make the model on + +INSTANCEMETHODS:: + +METHOD:: fit +Fit the model to a source link::Classes/FluidDataSet:: and a target link::Classes/FluidLabelSet:: . These need to be the sime size +ARGUMENT:: dataset +Source data +ARGUMENT:: labelset +Labels for the source data +ARGUMENT:: action +Run when done + +METHOD:: predict +Given a fitted model, predict labels for a link::Classes/FluidDataSet:: and write these to a link::Classes/FluidLabelSet:: +ARGUMENT:: dataset +data to predict labels for +ARGUMENT:: labelset +place to write labels +ARGUMENT:: k +the number of neighours to consider +ARGUMENT:: uniform +true / false: whether the neighbours shold be weighted by distance +ARGUMENT:: action +Run when done + +METHOD:: predictPoint +Given a fitted model, predict labels for a data point in a link::Classes/Buffer:: and return these to the caller +ARGUMENT:: buffer +A data point +ARGUMENT:: k +Number of neighbours to consider +ARGUMENT:: uniform +true / false: whether the neighbours shold be weighted by distance +ARGUMENT:: action +Run when done, passes predicted label as argument + +EXAMPLES:: + +code:: +//A dataset of example points, and a label set of corresponding labels +//+ +//A dataset of test data and a labelset for predicted labels +( +~source= FluidDataSet(s,\knnclassify_help_examples); +~labels = FluidLabelSet(s,\knnclassify_help_labels); +~test = FluidDataSet(s,\knnclassify_help_test); +~mapping = FluidLabelSet(s,\knnclassify_help_mapping); +) + +//Make some clumped 2D points and place into a dataset +( +~examplepoints = [[0.5,0.5],[-0.5,0.5],[0.5,-0.5],[-0.5,-0.5]]; +~examplelabels = [\red,\orange,\green,\blue]; +~source.clear; +~labels.clear; +~tmpbuf = Buffer.alloc(s,2); +fork{ + s.sync; + ~examplepoints.do{|x,i| + (""++(i+1)++"/4").postln; + ~tmpbuf.setn(0,x); + ~source.addPoint(i,~tmpbuf); + ~labels.addLabel(i,~examplelabels[i]); + s.sync + } +} +) + +//Make some random, but clustered test points +( +~testpoints = (4.collect{64.collect{(1.sum3rand) + [1,-1].choose}.clump(2)}).flatten(1) * 0.5; +~test.clear; +fork { + s.sync; + ~testpoints.do{|x,i| + ~tmpbuf.setn(0,x); + ~test.addPoint(i,~tmpbuf); + s.sync; + if(i==(~testpoints.size - 1)){"Generated test data".postln;} + } +} +) + +//Make a new KNN classifier model, fit it to the example dataset and labels, and then run preduction on the test data into our mapping label set +( +fork{ + ~classifier = FluidKNNClassifier(s); + s.sync; + ~classifier.fit(~source,~labels); + ~classifier.predict(~test, ~mapping, 1); + s.sync; +} +) + +//Dims of kmeans should match dataset +~kmeans.cols + +//Return labels of clustered points +( +~assignments = Array.new(~testpoints.size); +fork{ + ~testpoints.do{|x,i| + ~mapping.getLabel(i,action:{|l| + ~assignments.add(l); + }); + s.sync; + if(i==(~testpoints.size - 1)){"Got assignments".postln;} + }; + ~assignments.postln; +} +) + +//Visualise: we're hoping to see colours neatly mapped to quandrants... +( +c = IdentityDictionary(); + +c.add(\red->Color.red); +c.add(\blue->Color.blue); +c.add(\green->Color.green); +c.add(\orange-> Color.new255(255, 127, 0)); + +e = 200 * ((~examplepoints + 1) * 0.5).flatten(1).unlace; +d = ((~testpoints + 1) * 0.5).flatten(1).unlace; +// d = [20.collect{1.0.rand}, 20.collect{1.0.rand}]; +w = Window("scatter", Rect(128, 64, 200, 200)); +~colours = [Color.blue,Color.red,Color.green,Color.magenta]; +w.drawFunc = { + Pen.use { + e[0].size.do{|i| + var r = Rect(e[0][i],e[1][i],10,10); + Pen.fillColor = c[~examplelabels[i]]; + Pen.fillOval(r); + }; + d[0].size.do{|i| + var x = (d[0][i]*200); + var y = (d[1][i]*200); + var r = Rect(x,y,5,5); + Pen.fillColor = c[~assignments[i].asSymbol].alpha_(0.3); + Pen.fillOval(r); + } + } +}; +w.refresh; +w.front; +) + + +:: diff --git a/release-packaging/HelpSource/Classes/FluidKNNRegressor.schelp b/release-packaging/HelpSource/Classes/FluidKNNRegressor.schelp new file mode 100644 index 0000000..7eb7625 --- /dev/null +++ b/release-packaging/HelpSource/Classes/FluidKNNRegressor.schelp @@ -0,0 +1,108 @@ +TITLE:: FluidKNNRegressor +summary:: Regression with K Nearest Neighbours +categories:: Regression +related:: Classes/FluidKNNClassifier, Classes/FluidDataSet + +DESCRIPTION:: + +CLASSMETHODS:: + +METHOD:: new +Create a new KNN regressor on the server +ARGUMENT:: server +The server to run this model on. + +INSTANCEMETHODS:: + +METHOD:: fit +Map a source link::Classes/FluidDataSet:: to a target; they must be the same size, but can have different dimesionality +ARGUMENT:: sourceDataset +Source data +ARGUMENT:: targetDataset +Target data +ARGUMENT:: action +Run when done + + +METHOD:: predict +Apply learned mapping to a link::Classes/FluidDataSet:: and write to an output dataset +ARGUMENT:: sourceDataset +data to regress +ARGUMENT:: targetDataset +output data +ARGUMENT:: k +number of neigbours to consider in mapping, min 1 +ARGUMENT:: uniform +Whether to weight neighbours by distance when producing new point +ARGUMENT:: action +Run when done + +METHOD:: predictPoint +Apply learned mapping to a data point in a link::Classes/Buffer:: +ARGUMENT:: buffer +data point +ARGUMENT:: k +number of neigbours to consider in mapping, min 1 +ARGUMENT:: uniform +Whether to weight neighbours by distance when producing new point +ARGUMENT:: action +Run when done + +EXAMPLES:: + +code:: + +//Make a simple mapping between a ramp and a sine cycle, test with an exponentional ramp +( +~source = FluidDataSet(s,\knn_regress_src); +~target = FluidDataSet(s,\knn_regress_tgt); +~test = FluidDataSet(s,\knn_regress_test); +~output = FluidDataSet(s,\knn_regress_out); +~tmpbuf = Buffer.alloc(s,1); +) + +//Make source, target and test data +( +~sourcedata = 128.collect{|i|i/128}; +~targetdata = 128.collect{|i| sin(2*pi*i/128) }; +fork{ + 128.do{ |i| + ((i + 1).asString ++ "/128").postln; + ~tmpbuf.setn(0,i/128); + ~source.addPoint(i,~tmpbuf); + s.sync; + ~tmpbuf.setn(0,sin(2*pi*i/128)); + ~target.addPoint(i,~tmpbuf); + s.sync; + ~tmpbuf.setn(0,(i/128)**2); + ~test.addPoint(i,~tmpbuf); + s.sync; + if(i==127){"Source, target and test generated".postln}; + } +} +) + +// Now make a regressor and fit it to the source and target, and predict against test +//grab the output data whilst we're at it, so we can inspect +( +~outputdata = Array(128); +fork{ + ~regressor = FluidKNNRegressor(s); + s.sync; + ~regressor.fit(~source,~target); + ~regressor.predict(~test,~output,1); + s.sync; + 128.do{|i| + ~output.getPoint(i,~tmpbuf,{ + ~tmpbuf.loadToFloatArray(action:{|x| + ~outputdata.addAll(x) + }) + }); + s.sync; + if(i==127){"Model fitted, output generated".postln}; + } +} +) +//We should see a single cycle of a chirp +~outputdata.plot; +:: \ No newline at end of file