Difference between revisions of "Kohonen Map"

From Robowiki
Jump to navigation Jump to search
(Updated with real actual code (I did test, but it compiles))
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.
 
 
 
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.
 
 
 
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.
 
 
 
I would love to see someone integrate this into movement. :)
 
 
 
ALSO: Initialize those nodes with random static if possible.  
 
  
 
[[User:Chase-san|Chase]] 15:44, 1 November 2009 (UTC)
 
[[User:Chase-san|Chase]] 15:44, 1 November 2009 (UTC)
  
 
<pre>
 
<pre>
//KOHONEN MAP, PSUDOCODE FOR YOU AND ME!
+
package chase.meh;
  
//Making an array, 25 line in this case
+
/**
int gridSize = 25;
+
* This is a basic stand alone Kohonen map for use in Succubus, this allows me to worry
array Node grid[gridSize];
+
* about Kohonen Map stuff in the Kohonen Map, and robot stuff in the robot.
 
+
*
 
+
* @author Chase
//IMPORTANT: As much I would love to see a pattern matcher version of this, its kinda unrealistic
+
*/
//based on how kohonen maps work.
+
public class KohonenMap {
class Node {
+
private KohonenNode[] map;
static int nearest; //1 instance of
+
private int mapSize;
float gf; //Guess Factor
+
private long time;
 +
 +
/**
 +
* 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, double[] inputL, double[] inputU,
 +
double[] outputL, double[] outputU) {
 +
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;
 +
 +
/**
 +
* Initialize each node.
 +
*/
 +
int tDim[] = new int[dim.length];
 +
for(int i=0; i<dim.length; ++i) {
 +
map[i] = new KohonenNode(inputL,inputU,outputL,outputU);
 +
 +
/** !! 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;
 +
}
 
 
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(double[] inputL, double[] inputU,
for(int i=0; i<gridSize; ++i) {
+
double[] outputL, double[] outputU) {
float newDist = distance(n,grid[i]);
+
input = new double[inputL.length];
if(newDist < dist) {
+
output = new double[outputL.length];
near = grid[i];
+
dist = newDist;
+
/** !! VERY IMPORTANT !! VERY IMPORTANT !!
near.nearest = i;
+
* Randomize the startup data
 +
*/
 +
for(int i=0;i<input.length;++i) {
 +
double range = inputU[i]-inputL[i];
 +
range = Math.sqrt(range*range);
 +
input[i] = Math.random()*range+inputL[i];
 +
 +
range = outputU[i]-outputL[i];
 +
range = Math.sqrt(range*range);
 +
output[i] = Math.random()*range+outputL[i];
 +
}
 +
}
 +
 +
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 = 1+neighborDistance(my_pos,bmu);
 +
double neighborhood = Math.pow(Math.E, -(distance*distance)/2.0);
 +
double timeFunc = Math.log(time);
 +
 +
/** Beyond this point its benefit is negligible. */
 +
if(neighborhood*timeFunc < 0.0001) 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 static final 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;
//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;
 
}
 
}
 
}
 
}
  
float getBestGF(Node current) {
 
return getNearestNode(current).gf;
 
}
 
 
//That is about all there is to it.
 
 
</pre>
 
</pre>

Revision as of 13:03, 8 November 2009

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.

Chase 15:44, 1 November 2009 (UTC)

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;
	
	/**
	 * 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, double[] inputL, double[] inputU,
			double[] outputL, double[] outputU) {
		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;
		
		/**
		 * Initialize each node.
		 */
		int tDim[] = new int[dim.length];
		for(int i=0; i<dim.length; ++i) {
			map[i] = new KohonenNode(inputL,inputU,outputL,outputU);
			
			/** !! 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;
	}
	
	/**
	 * 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(double[] inputL, double[] inputU,
				double[] outputL, double[] outputU) {
			input = new double[inputL.length];
			output = new double[outputL.length];
			
			/** !! VERY IMPORTANT !! VERY IMPORTANT !!
			 * Randomize the startup data
			 */
			for(int i=0;i<input.length;++i) {
				double range = inputU[i]-inputL[i];
				range = Math.sqrt(range*range);
				input[i] = Math.random()*range+inputL[i];
				
				range = outputU[i]-outputL[i];
				range = Math.sqrt(range*range);
				output[i] = Math.random()*range+outputL[i];
			}
		}
		
		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 = 1+neighborDistance(my_pos,bmu);
			double neighborhood = Math.pow(Math.E, -(distance*distance)/2.0);
			double timeFunc = Math.log(time);
			
			/** Beyond this point its benefit is negligible. */
			if(neighborhood*timeFunc < 0.0001) 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 static final 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;
		}
		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;
	}
}