Difference between revisions of "Kohonen Map"

From Robowiki
Jump to navigation Jump to search
m
 
(6 intermediate revisions by 2 users not shown)
Line 1: Line 1:
Nat contacted me wanting to know bout Kohonen Maps. To be honest there isn't actually a whole lot to them. I wrote some psudocode for you all to convert into something usable in java. Its been a long time since I wrote java.
+
This is about all I can remember on how Kohonen maps work, you may tweak this if you like. Fix any bugs that keeps it from actually working in a robot, etc.
  
This is all from memory, prototype gained score based on the weights and the way I updated the weights and the dimensionality of my map, and probably some other things. I use a 1 dimensional map in this snippet, but you can use as many dimensions as you like, the higher you go the more interlinks the data gets when it preforms its distribution.
+
<syntaxhighlight>
 +
package chase.meh;
  
Also keep in mind you can also 'wrap' the 'edges' of the map, and you can get better data that way, however make sure to limit the size of your gauss function, otherwise you will be calculating the wrap forever. (I find once the neighborhood functions dips below 0.01 to 0.1 its mostly unneeded to try and update it). Edge wrappings gets exceedingly computationally expensive as you go up in dimensions.
+
/**
 
+
* This is a basic stand alone Kohonen map for use in Succubus, this allows me to worry
I found around 10,000 to 100,000 total nodes didn't do half bad, and really due to the nature of kohonen maps, you probably do not need to go much higher.
+
* about Kohonen Map stuff in the Kohonen Map, and robot stuff in the robot.
 
+
*
I would love to see someone integrate this into movement. :)
+
* @author Chase
 
+
*/
ALSO: Initialize those nodes with random static if possible.
+
public class KohonenMap {
 
+
private KohonenNode[] map;
[[User:Chase-san|Chase]] 15:44, 1 November 2009 (UTC)
+
private int mapSize;
 
+
private long time;
<pre>
+
private double[] weights;
//KOHONEN MAP, PSUDOCODE FOR YOU AND ME!
+
 
+
/**
//Making an array, 25 line in this case
+
* The dimensions in the dim, are only used with the neighborhood function. The true
int gridSize = 25;
+
* dimensionality that makes Kohonen Maps work is the input and output weights.<br><br>
array Node grid[gridSize];
+
* The input weights are used to determine the BMU, while the output weights are unused
 
+
* @param dim Sets the size of the internal map. Where the length is the number of dimensions and
 
+
* each index determines the size of that particular dimension. So {8,4} would initialize a 2
//IMPORTANT: As much I would love to see a pattern matcher version of this, its kinda unrealistic
+
* dimensional map, 8 wide, and 4 high.
//based on how kohonen maps work.
+
* @param inputL The lower range of the input weights that each node contains. The number of entries
class Node {
+
* determines the number of input weights.
static int nearest; //1 instance of
+
* @param inputU The upper range of the input weights that each node contains. The number of entries
float gf; //Guess Factor
+
* determines the number of input weights.
 +
* @param outputL The lower range of the output weights that each node contains. The number of entries
 +
* determines the number of output weights.
 +
* @param outputU The upper range of the output weights that each node contains. The number of entries
 +
* determines the number of output weights.
 +
*/
 +
public KohonenMap(int[] dim, int input, int output, double[] weight) {
 +
mapSize = 1;
 +
for(int i=0; i<dim.length; ++i)
 +
mapSize *= dim[i];
 +
 +
map = new KohonenNode[mapSize];
 +
 +
//Logarithms have this thing against zero :)
 +
time = 1;
 +
 +
weights = weight.clone();
 +
 +
/**
 +
* Initialize each node.
 +
*/
 +
int tDim[] = new int[dim.length];
 +
for(int i=0; i<mapSize; ++i) {
 +
map[i] = new KohonenNode(input,output);
 +
 +
/** !! VERY IMPORTANT !! VERY IMPORTANT !!
 +
* Here we are setting up the neighborhood,
 +
* so it doesn't need to be calculated later
 +
* which makes things much simpler.
 +
*/
 +
map[i].my_pos = tDim.clone();
 +
 +
++tDim[0];
 +
for(int j=0;j<dim.length-1;++j) {
 +
if(tDim[j] >= dim[j]) {
 +
tDim[j+1]++;
 +
tDim[j] = 0;
 +
}
 +
}
 +
}
 +
}
 +
 +
/**
 +
* Returns the best matching unit for the given data
 +
* @param input Data used to locate the BMU
 +
* @return the best matching BMU for this input data
 +
*/
 +
public KohonenNode getBMU(double input[]) {
 +
KohonenNode out = null;
 +
double distance = Double.POSITIVE_INFINITY;
 +
for(int i=0;i<mapSize;++i) {
 +
KohonenNode n = map[i];
 +
double dist = distance(input,n.input);
 +
if(dist < distance) {
 +
distance = dist;
 +
out = n;
 +
}
 +
}
 +
return out;
 +
}
 +
 +
/**
 +
* Updates this KohonenMap with the given data.
 +
* @param input The input data that will be updated to the map
 +
* @param output The output data that will be updated to the map
 +
*/
 +
public void update(double input[], double output[]) {
 +
KohonenNode bmu = getBMU(input);
 +
for(int i=0;i<mapSize;++i) {
 +
map[i].update(bmu.my_pos, input, output);
 +
}
 +
++time;
 +
}
 +
 +
/**
 +
* Sets the internal time used for weighting the update data by time.
 +
* The internal time automatically gets incremented with each update,
 +
* calling this is unnecessary.
 +
* @param time
 +
*/
 +
public void setTime(long time) {
 +
if(time < 1) time = 1;
 +
this.time = time;
 +
}
 +
 +
public long getTime() {
 +
return this.time;
 +
}
 
 
float weight0; //Distance?
+
/**
float weight1; //Velocity?
+
* This is a KohonenNode, it just contains the data
float weight2; //Angle
+
* @author Chase
//...
+
*/
}
+
public class KohonenNode {
 
+
/** this is the location of this node in the neighborhood. */
//There are much better ways to do this aswell
+
private int[] my_pos;
 
+
//I do not include the GF because thats the target
+
public double[] input;
//'dimension' for reduced resolution :)
+
public double[] output;
float distance(Node a, Node b) {
+
return abs(a.weight0 - b.weight0) +
+
/**
abs(a.weight1 - b.weight1) +
+
* @param inputL The lower range of the input weights that this node contains. The number of entries
abs(a.weight2 - b.weight2);
+
* determines the number of input weights.  
}
+
* @param inputU The upper range of the input weights that this node contains. The number of entries
 
+
* determines the number of input weights.  
Node getNearestNode(Node n) {
+
* @param outputL The lower range of the output weights that this node contains. The number of entries
Node near;
+
* determines the number of output weights.
float dist = INFINITY;
+
* @param outputU The upper range of the output weights that this node contains. The number of entries
 
+
* determines the number of output weights.
//find nearest Node to the given data
+
*/
//Do this however you like
+
public KohonenNode(int inputs, int outputs) {
for(int i=0; i<gridSize; ++i) {
+
input = new double[inputs];
float newDist = distance(n,grid[i]);
+
output = new double[outputs];
if(newDist < dist) {
+
near = grid[i];
+
/** !! VERY IMPORTANT !! VERY IMPORTANT !!
dist = newDist;
+
* Randomize the startup data
near.nearest = i;
+
*/
 +
for(int i=0;i<input.length;++i) {
 +
input[i] = (Math.random()-0.5)*2.0;
 +
}
 +
for(int i=0;i<output.length;++i) {
 +
output[i] = (Math.random()-0.5)*2.0;
 +
}
 +
}
 +
 +
 +
private double gaussian(double u) {
 +
/** This normalizes it so that 0 = 1 */
 +
final double oos2p = (1.0/Math.sqrt(2.0*Math.PI));
 +
return oos2p*Math.exp(-0.5*u*u);
 +
}
 +
 +
private double weight(double current, double target, double neighborhood, double time) {
 +
return current + neighborhood * time * (target - current);
 +
}
 +
 +
private void update(int[] bmu, double input[], double output[]) {
 +
double distance = neighborDistance(my_pos,bmu);
 +
//double neighborhood = Math.exp(-(distance*distance));
 +
double neighborhood = gaussian(distance);
 +
double timeFunc = 1.0/(Math.log(time)+0.01);
 +
//double timeFunc = Math.exp(-(time*time)/2.0);
 +
 +
/** Beyond this point its benefit is negligible. */
 +
//if(neighborhood*timeFunc < 0.001) return;
 +
if(distance > 4) return;
 +
 +
/** Update the input and output weights */
 +
for(int i=0;i<input.length;++i) {
 +
this.input[i] = weight(this.input[i], input[i], neighborhood, timeFunc);
 +
}
 +
for(int i=0;i<output.length;++i) {
 +
this.output[i] = weight(this.output[i], output[i], neighborhood, timeFunc);
 +
}
 
}
 
}
 
}
 
}
 
//You could also make a short list of 'nearest' nodes
 
//and run some kind of other function to determine the
 
//best amoung them for updating or for firing
 
return near;
 
}
 
 
float recalcWeight(float current, float target, float neighborhood, float time) {
 
return current + neighborhood * time * (target - current);
 
}
 
 
void updateMap(Node new, int time) {
 
//find nearest Node to our new data
 
Node nearest = getNearestNode(new);
 
 
//Save the nearest index for later
 
int n = nearest.nearest;
 
 
 
//Now we have two options, we can either set the given node to our
+
/**
//new data, or use some kinda of 'weighting' system to 'tune' it
+
* The weight distance function.
 
+
*/
//I am just going to set it
+
public double distance(double[] p, double[] q) {
//This is psudocode so I can cheat
+
if(p == null || q == null) return Double.POSITIVE_INFINITY;
nearest = new;
+
double k, d = 0;
 
+
for(int i = 0; i < p.length; i++) {
//now we run a guassian function on 'nearby' nodes to 'pull' their values
+
k = p[i] - q[i];
//to be closer to the value of this node. The guassian depends on the other
+
d += k*k*weights[i];
//nodes 'distance' from the given node on the grid.
+
}
 
+
return d;
//Also its a good idea to weight the strength of this pull by the number of elapsed
+
}
//rounds, so it would learn faster early on and only slightly tune the data
+
/**
//later. This is a very important concept, otherwise the map is mostly useless.
+
* The neighborhood distance function.
 
+
*/
//Note that my neighborhood function isn't actually guassian at all, but
+
public static final double neighborDistance(int[] p, int[] q) {
//linear, guassian however works much better. :)
+
double k, d = 0;
for(int i = 0; i<gridSize; ++i) {
+
for(int i = 0; i < p.length; i++) {
int dist = abs(n-i);   
+
k = p[i] - q[i];
grid[i].gf = recalcWeight( grid[i].gf, new.gf, (0.75/dist), 1.0/ln(time) );
+
d += k*k;  
//Do this for the other weights aswell...
+
}
 +
return d;
 
}
 
}
 
}
 
}
 
+
</syntaxhighlight>
float getBestGF(Node current) {
 
return getNearestNode(current).gf;
 
}
 
 
 
//That is about all there is to it.
 
</pre>
 

Latest revision as of 02:50, 6 December 2011

This is about all I can remember on how Kohonen maps work, you may tweak this if you like. Fix any bugs that keeps it from actually working in a robot, etc.

package chase.meh;

/**
 * This is a basic stand alone Kohonen map for use in Succubus, this allows me to worry
 * about Kohonen Map stuff in the Kohonen Map, and robot stuff in the robot.
 * 
 * @author Chase
 */
public class KohonenMap {
	private KohonenNode[] map;
	private int mapSize;
	private long time;
	private double[] weights;
	
	/**
	 * The dimensions in the dim, are only used with the neighborhood function. The true
	 * dimensionality that makes Kohonen Maps work is the input and output weights.<br><br>
	 * The input weights are used to determine the BMU, while the output weights are unused
	 * @param dim Sets the size of the internal map. Where the length is the number of dimensions and
	 * each index determines the size of that particular dimension. So {8,4} would initialize a 2
	 * dimensional map, 8 wide, and 4 high.
	 * @param inputL The lower range of the input weights that each node contains. The number of entries
	 * determines the number of input weights. 
	 * @param inputU The upper range of the input weights that each node contains. The number of entries
	 * determines the number of input weights. 
	 * @param outputL The lower range of the output weights that each node contains. The number of entries
	 * determines the number of output weights.
	 * @param outputU The upper range of the output weights that each node contains. The number of entries
	 * determines the number of output weights.
	 */
	public KohonenMap(int[] dim, int input, int output, double[] weight) {
		mapSize = 1;
		for(int i=0; i<dim.length; ++i)
			mapSize *= dim[i];
		
		map = new KohonenNode[mapSize];
		
		//Logarithms have this thing against zero :)
		time = 1;
		
		weights = weight.clone();
		
		/**
		 * Initialize each node.
		 */
		int tDim[] = new int[dim.length];
		for(int i=0; i<mapSize; ++i) {
			map[i] = new KohonenNode(input,output);
			
			/** !! VERY IMPORTANT !! VERY IMPORTANT !!
			 * Here we are setting up the neighborhood,
			 * so it doesn't need to be calculated later
			 * which makes things much simpler.
			 */
			map[i].my_pos = tDim.clone();
			
			++tDim[0];
			for(int j=0;j<dim.length-1;++j) {
				if(tDim[j] >= dim[j]) {
					tDim[j+1]++;
					tDim[j] = 0;
				}
			}
		}
	}
	
	/**
	 * Returns the best matching unit for the given data
	 * @param input Data used to locate the BMU
	 * @return the best matching BMU for this input data
	 */
	public KohonenNode getBMU(double input[]) {
		KohonenNode out = null;
		double distance = Double.POSITIVE_INFINITY;
		for(int i=0;i<mapSize;++i) {
			KohonenNode n = map[i];
			double dist = distance(input,n.input);
			if(dist < distance) {
				distance = dist;
				out = n;
			}
		}
		return out;
	}
	
	/**
	 * Updates this KohonenMap with the given data.
	 * @param input The input data that will be updated to the map
	 * @param output The output data that will be updated to the map
	 */
	public void update(double input[], double output[]) {
		KohonenNode bmu = getBMU(input);
		for(int i=0;i<mapSize;++i) {
			map[i].update(bmu.my_pos, input, output);
		}
		++time;
	}
	
	/**
	 * Sets the internal time used for weighting the update data by time.
	 * The internal time automatically gets incremented with each update,
	 * calling this is unnecessary.
	 * @param time
	 */
	public void setTime(long time) {
		if(time < 1) time = 1;
		this.time = time;
	}
	
	public long getTime() {
		return this.time;
	}
	
	/**
	 * This is a KohonenNode, it just contains the data
	 * @author Chase
	 */
	public class KohonenNode {
		/** this is the location of this node in the neighborhood. */
		private int[] my_pos;
		
		public double[] input;
		public double[] output;
		
		/**
		 * @param inputL The lower range of the input weights that this node contains. The number of entries
		 * determines the number of input weights. 
		 * @param inputU The upper range of the input weights that this node contains. The number of entries
		 * determines the number of input weights. 
		 * @param outputL The lower range of the output weights that this node contains. The number of entries
		 * determines the number of output weights.
		 * @param outputU The upper range of the output weights that this node contains. The number of entries
		 * determines the number of output weights.
		 */
		public KohonenNode(int inputs, int outputs) {
			input = new double[inputs];
			output = new double[outputs];
			
			/** !! VERY IMPORTANT !! VERY IMPORTANT !!
			 * Randomize the startup data
			 */
			for(int i=0;i<input.length;++i) {
				input[i] = (Math.random()-0.5)*2.0;
			}
			for(int i=0;i<output.length;++i) {
				output[i] = (Math.random()-0.5)*2.0;
			}
		}
		
		
		private double gaussian(double u) {
			/** This normalizes it so that 0 = 1 */
			final double oos2p = (1.0/Math.sqrt(2.0*Math.PI));
			return oos2p*Math.exp(-0.5*u*u);
		}
		
		private double weight(double current, double target, double neighborhood, double time) {
			return current + neighborhood * time * (target - current);
		}
		
		private void update(int[] bmu, double input[], double output[]) {
			double distance = neighborDistance(my_pos,bmu);
			//double neighborhood = Math.exp(-(distance*distance));
			double neighborhood = gaussian(distance);
			double timeFunc = 1.0/(Math.log(time)+0.01);
			//double timeFunc = Math.exp(-(time*time)/2.0);
			
			/** Beyond this point its benefit is negligible. */
			//if(neighborhood*timeFunc < 0.001) return;
			if(distance > 4) return;
			
			/** Update the input and output weights */
			for(int i=0;i<input.length;++i) {
				this.input[i] = weight(this.input[i], input[i], neighborhood, timeFunc);
			}
			for(int i=0;i<output.length;++i) {
				this.output[i] = weight(this.output[i], output[i], neighborhood, timeFunc);
			}
		}
	}
	
	/**
	 * The weight distance function.
	 */
	public double distance(double[] p, double[] q) {
		if(p == null || q == null) return Double.POSITIVE_INFINITY;
		double k, d = 0;
		for(int i = 0; i < p.length; i++) {
			k = p[i] - q[i];
			d += k*k*weights[i];
		}
		return d;
	}
	/**
	 * The neighborhood distance function.
	 */
	public static final double neighborDistance(int[] p, int[] q) {
		double k, d = 0;
		for(int i = 0; i < p.length; i++) {
			k = p[i] - q[i];
			d += k*k; 
		}
		return d;
	}
}