UserPreferences

PythonSelfOrganizingMap


1. Python Self Organizing Map (psom) -- Project Overview

Using psom can be a little bit confusing, so my thought is to make this page be a general resource for psom related information.

psom can be imported from pyrobot.brain
psom is a python interface to the opensource c library som_pak; I modified the som_pak-3.1 source code (creating a project I called som_pak-dev), from which swig generates the low-level interface csom.so, and psom is really just a wrapper around this.

One of the key features of psom is that it can be trained dynamically.

Consult the DevelopmentalRobotics page for more info on SOMs and som_pak.

2. Getting Started

If you are really in a hurry to get started, what follows is a (very) brief tutorial to orient you to the basic features of psom.

First, make sure you have the latest version of pyro installed, and that building csom.so worked properly (you can always go into the pyrobot/brain/psom/ directory and type make, and that should work...

Next,soms need to be initialized. You need to somehow specify what the initial model vectors are before you start som training. Currently, psom supports three methods:

In the following example, we are going to assume the second method.

Suppose we have a robot, and we wish to create a SOM where the data vectors contain information about the sensor values and motor driver values at various instants of time. Just to make this concrete, suppose we have 8 infared sensors and 2 motor values (translate and rotate) for a total of 10 entries in our data vectors

Furthermore, suppose that we have collected a (rather large) handful of sample data values based on the behaviour of some low-level controller, and that these samples are stored as lists of floats.

Now, before creating the psom, we will create a dataset object from which to initialize the psom, which is done by creating another kind of object, a vector, which for now you can think of as a wrapper for a list of floats (the associated float list can be accessed by calling vector.get_elts()). To create a dataset do:

from pyrobot.brain import psom

vec0 = psom.vector(list0)
mydata = psom.dataset(vec0)
mydata.addvec(psom.vector(list1))
mydata.addvec(psom.vector(list2))
....
and so on. It is not too difficult to envision using some form of for or while construct to make this seem a bit more reasonable.

You can also create a dataset from an existing file. The file should be formatted as follows: the first line should contain a single integer which represents the length of the data vectors; each subsequent line should contain a list of numbers of that length separated by white space. Then to create a dataset from an existing file do:

mydata = psom.dataset(file="sample.dat")

Now we wish to initialize our som, i.e. create the psom proper. We decide that we want a hexagonal topology with a gaussian neighborhood function (the default), and that we want a map that has x-dimension 20 and y-dimension 15. Then all we need to do is:

mysom = psom.psom(20,15,data=mydata)

and now we have a psom (equivalently, we could have done something like mysom = psom(20,15,'hexa','gaussian',data=mydata...)

Now, in the normal world of soms, the som is trained all in one fell-swoop, where you specify an initial alpha (learning-rate) and neighborhood radius and then, with a large (say 50,000) handful of training samples tell the map to train itself; in the process, alpha will decay to zero and radius will decay to 1.0. In order to get equivalent behaviour from the psom, we must make a guess as to how many training samples we expect to feed it, as well as specify initial alpha and radius. This is done by calling psom.init_training(). Suppose we expect to train with 500 samples, and want an initial alpha of 0.02 and an initial radius of 3.0. Then we would do this:

mysom.init_training(0.02,3.0,500)
Now we can use the psom to drive our robot. We will use extensive use of psom.train(), which takes a psom.vector as an argument and returns a psom.vector corresponding to the model vector it mapped to. So if we imagine that we already have functions to get and process our data vectors when represented as lists of floats (call them robot.get_list()} and robot.use_list() we could do something like this:
vec = psom.vector(robot.get_list())
model = mysom.train(vec)
robot.use_list(model.get_elts())
...
The map will automatically adjust itself (that is, after all, the whole point of a self-organizing map...), and alpha and radius will automatically decay, such that after completing 500 iterations alpha will be zero and radius will be 1.0 (this behaviour can be modified when creating the psom by specifying alpha_mode='inverse_t' and/or radius_mode='inverse_t in the constructor parameters).

3. Getting Fancier

What follows are further details on some of the psom library's classes and methods, and fun and exciting ways to use them.

There are five classes contained in the psom library; they are psom, dataset, vector, point, and activations. all five classes have a display() method to get basic printed output.

3.1. psom initialization

The psom constructor looks like this:

def __init__(self, xdim='unset', ydim='unset', topol='hexa', neigh='gaussian',
                alpha_mode='linear', radius_mode='linear', rmin=0.0,
                rmax=1.0, data='unset', file='unset'):

As noted previously, there are currently two ways supported to initialize a psom, ie. two ways to use the psom constructor, reading a pre-existing som from a .cod file and random initialization based on a pre-existing dataset.

To specify file-based initialization, you need to specify the file parameter by doing mysom = psom( ..., file="myfile.cod" ), and to specify initialization from a dataset you need to specify the data parameter by doing something like mysom = psom( ..., data=mydataset)

In the case of file-based initialization, the package uses the .cod file format, which is the code-vector (ie. model-vector) file format supported by som_pak. The header line to the file specifies the vector dimension, map dimensions (xdim and ydim) and topology and neighborhood type (topol and neigh), so if you are using file-based initialization you do not need to specify any of these parameters (so in some cases saying mysom = psom(file="myfile.cod") will be sufficient)

In dataset based initialization, xdim and ydim must be specified. topol clearly defaults to 'hexa', but if desired you can set it to 'rect', and similarly neigh may be set to 'bubble'.

In both cases, the alpha and radius decay modes default to linear, but both can be set to inverse_t if desired by setting the alpha_mode and radius_mode parameters

hmm... this sort of thing should probably be in the docstring comments, so maybe i'll work on that for a bit. check out [WWW]psom docstring funworld, though last i checked it was kinda broken

4. An example

This now uses the VisPsom class to give a graphical representation of the som.
from pyrobot.brain import Brain
from pyrobot.brain import psom
from pyrobot.brain.psom import vis
import time

def saveListToFile(ls, file):
    for i in range(len(ls)):
        file.write(str(ls[i]) + " ")
    file.write("\n")

class SonarSom(Brain):
    """
    Written by Cassandra Telenko and Lisa Meeden
    
    This class is designed to work with the Pioneer simulated robot.
    The goal of this class is to dynamically create a som of sonar data
    from a robot doing simple navigation with obstacle avoidance.
    
    It operates in two modes: CollectData and Run.  The robot begins
    in CollectData mode, and saves samples of its front sonar values
    to a file called sonar.dat while it is navigating.  After a
    designated number of steps (given by maxCollectSteps), the sample
    data file is closed and used to initialize a som.  Then the robot
    goes into Run mode and uses its sonar values to train the som for
    a designated number of steps (given by maxRunSteps).  
    """
       
    mode='CollectData'
    maxCollectSteps = 100
    maxRunSteps = 500
    currStep = 0
    minDistance = .9
    dat = open("sonar.dat", "w")
    # must write data pattern length at the top of the file
    dat.write(str(8)+"\n")
    mysom=1
    
    def step(self):
        if self.mode=='CollectData':
            if self.currStep<self.maxCollectSteps:
                instance=self.scout()
                saveListToFile(instance, self.dat)
                self.currStep = self.currStep + 1
            else:
                self.dat.close()
                self.stop()
                print "done collecting sample data"
                mydat = psom.dataset(file="sonar.dat")
                self.mysom = vis.VisPsom(8,12,data=mydat) 
                self.mysom.init_training(0.02,5.0,self.maxRunSteps)
                self.mode='RunSom'
                self.currStep=0
                print "done initializing som"
        elif self.mode=='RunSom':
            instance=self.scout()
            model = self.mysom.train(psom.vector(instance))
            print "Input:\n" + str(instance) + "\nModel: "
            model.display()
            self.currStep=self.currStep+1
            if self.currStep==self.maxRunSteps:
                self.mode='Done'
                print "done training, press stop in pyro now"
        else:
            self.stop()

    def scout(self):
        self.avoidObstacles()
        time.sleep(0.25)
        instance = range(0,8)
        for i in range(0,8):
            instance[i] = self.robot.sonar[0][i].value
        return instance
             
    def avoidObstacles(self):
        if self.checkFront():
            self.move(0,.3)
        elif self.checkLeft():
            self.move(0, -.3)
        elif self.checkRight():
            self.move(0, .3)
        else:
            self.move(.3,0)

    def checkLeft(self):
        for i in range(0,3):
            if self.robot.sonar[0][i].value < self.minDistance:
                return 1
            return 0

    def checkRight(self):
        for i in range(5,8):
            if self.robot.sonar[0][i].value < self.minDistance:
                return 1
            return 0
        
    def checkFront(self):
        right = self.robot.sonar[0][3].value
        left= self.robot.sonar[0][4].value
        if right < self.minDistance or left < self.minDistance:
            return 1
        else:
            return 0

def INIT(engine):
   return SonarSom('SonarSom', engine)

4.1. A second Example (slightly improved version of above)

Modified the above program to use generic range sensors (intead of sonar)...so it can run on a Pioneer (it will use sonar sensors) as well as a Khepera (will use IR sensors). The program is more or less generic, save of one instance where a new feature needs to be added to the API. It also uses an improved version of obstacle avoidance (that uses random amounts for movement). Some other simple cosmetic changes in the output produced.

It is really cool to watch the SOM learn as the robot is moving around. One can clearly see where the SOM is for each 'major' robot behavior (like going straight, turning right or left).

from pyrobot.brain import Brain
from pyrobot.brain import psom
from pyrobot.brain.psom import vis
import time
from random import random, seed

def saveListToFile(ls, file):
    for i in range(len(ls)):
        file.write(str(ls[i]) + " ")
    file.write("\n")

class RangeSom(Brain):
    """
    Written by Cassandra Telenko and Lisa Meeden
    
    This class is designed to work with the Pioneer simulated robot.
    The goal of this class is to dynamically create a som of sonar data
    from a robot doing simple navigation with obstacle avoidance.
    
    It operates in two modes: CollectData and Run.  The robot begins
    in CollectData mode, and saves samples of its front sonar values
    to a file called sonar.dat while it is navigating.  After a
    designated number of steps (given by maxCollectSteps), the sample
    data file is closed and used to initialize a som.  Then the robot
    goes into Run mode and uses its sonar values to train the som for
    a designated number of steps (given by maxRunSteps).

    Modified by Deepak Kumar to make it generic to any robot's range sensors.
    """
    def __init__(self, name='somBrain', engine = 0):
        Brain.__init__(self, name, engine)
        self.maxCollectSteps = 100
        self.maxRunSteps = 500
        self.currStep = 0
        self.minDistance = .85
        self.dataFileName = "range.dat"
        self.dat = open(self.dataFileName, "w")
        self.mysom=1
        self.sensorCount = self.robot.range.count
        self.dat.write(str(self.sensorCount)+"\n") # write data pattern
        self.mode = 'CollectData'
        print "done initializing: Sensor count = " + str(self.sensorCount)
            
    def step(self):
        if self.mode=='CollectData':
            if self.currStep<self.maxCollectSteps:
                instance=self.scout()
                saveListToFile(instance, self.dat)
                self.currStep = self.currStep + 1
                if not (self.currStep % 10):
                    print ".",
            else:
                self.dat.close()
                self.stop()
                print "done collecting sample data"
                mydat = psom.dataset(file=self.dataFileName)
                self.mysom = vis.VisPsom(8,12,data=mydat) 
                self.mysom.init_training(0.02,5.0,self.maxRunSteps)
                self.mode='RunSom'
                self.currStep=0
                print "done initializing som"
        elif self.mode=='RunSom':
            instance=self.scout()
            model = self.mysom.train(psom.vector(instance))
            #print "Input:\n" + str(instance) + "\nModel: "
            print "Input:" + str(map(lambda i: '%4.3f' % i, instance))+" #"+str(self.currStep)
            print "Model:" + str(map(lambda i: '%4.3f' % i, model.get_elts()))
            #model.display()
            self.currStep=self.currStep+1
            if self.currStep==self.maxRunSteps:
                self.mode='Done'
                print "done training, press stop in pyro now"
        else:
            self.stop()

    def scout(self):
        self.avoidObstacles()
        time.sleep(0.25)
        instance = range(self.sensorCount) 
        for i in range(self.sensorCount):
            instance[i] = self.robot.range[i].value
        return instance
             
    def avoidObstacles(self):
        if self.checkFront():
            if (random() < 0.5):
                self.move(0, -random())
            else:
                self.move(0, random())
        elif self.checkLeft():
            self.move(0, -random())
        elif self.checkRight():
            self.move(0, random())
        else:
            self.move(.2,0)

    def checkLeft(self):
        d = min([s.value for s in self.robot.range["front-left"]])
        if d < self.minDistance:
            return 1
        else:
            return 0

    def checkRight(self):
        d = min([s.value for s in self.robot.range["front-right"]])
        if d < self.minDistance:
            return 1
        else:
            return 0
        
    def checkFront(self):
        d = min([s.value for s in self.robot.range["front"]])
        if d < self.minDistance:
            return 1
        else:
            return 0

def INIT(engine):
    seed()
    return RangeSom('RangeSom', engine)

And here is another version with the fixes alluded to above, and some optimizations. I think this works correctly on the Pioneer where the one above only mapped the first 8 sensors.

from pyrobot.brain import Brain 
from pyrobot.brain import psom 
from pyrobot.brain.psom import vis 
import time 
from random import random, seed 
 
def saveListToFile(ls, file): 
    for i in range(len(ls)): 
        file.write(str(ls[i]) + " ") 
    file.write("\n") 
 
class RangeSom(Brain): 
    """ 
    Written by Cassandra Telenko and Lisa Meeden 
     
    This class is designed to work with the Pioneer simulated robot. 
    The goal of this class is to dynamically create a som of sonar data 
    from a robot doing simple navigation with obstacle avoidance. 
     
    It operates in two modes: CollectData and Run.  The robot begins 
    in CollectData mode, and saves samples of its front sonar values 
    to a file called sonar.dat while it is navigating.  After a 
    designated number of steps (given by maxCollectSteps), the sample 
    data file is closed and used to initialize a som.  Then the robot 
    goes into Run mode and uses its sonar values to train the som for 
    a designated number of steps (given by maxRunSteps). 
 
    Modified by Deepak Kumar and Doug Blank to make it generic to any 
    robot's range sensors. 
    """ 
    def __init__(self, name='somBrain', engine = 0): 
        Brain.__init__(self, name, engine) 
        self.maxCollectSteps = 100 
        self.maxRunSteps = 500 
        self.currStep = 0 
        self.minDistance = .85 
        self.dataFileName = "range.dat" 
        self.dat = open(self.dataFileName, "w") 
        self.mysom=1 
        self.sensorCount = len(self.robot.range["front-all"])
        self.dat.write(str(self.sensorCount)+"\n") # write data pattern 
        self.mode = 'CollectData' 
        print "done initializing: Sensor count = " + str(self.sensorCount) 
             
    def step(self): 
        if self.mode=='CollectData': 
            if self.currStep<self.maxCollectSteps: 
                instance=self.scout() 
                saveListToFile(instance, self.dat) 
                self.currStep = self.currStep + 1 
                if not (self.currStep % 10): 
                    print ".", 
            else: 
                self.dat.close() 
                self.stop() 
                print "done collecting sample data" 
                mydat = psom.dataset(file=self.dataFileName) 
                self.mysom = vis.VisPsom(8,12,data=mydat)  
                self.mysom.init_training(0.02,5.0,self.maxRunSteps) 
                self.mode='RunSom' 
                self.currStep=0 
                print "done initializing som" 
        elif self.mode=='RunSom': 
            instance=self.scout() 
            model = self.mysom.train(psom.vector(instance)) 
            #print "Input:\n" + str(instance) + "\nModel: " 
            print "Input:" + str(map(lambda i: '%4.3f' % i, instance))+\
                  " #"+str(self.currStep) 
            print "Model:" + str(map(lambda i: '%4.3f' % i, model.get_elts())) 
            #model.display() 
            self.currStep=self.currStep+1 
            if self.currStep==self.maxRunSteps: 
                self.mode='Done' 
                print "done training, press stop in pyro now" 
        else: 
            self.stop() 
 
    def scout(self): 
        self.avoidObstacles() 
        time.sleep(0.25) 
        return [s.value for s in self.robot.range["front-all"]]
              
    def avoidObstacles(self):
        minDist = self.minDistance
        if min(self.get('robot/range/front/value')) < minDist:
            if (random() < 0.5): 
                self.move(0, -random()) 
            else: 
                self.move(0, random()) 
        elif min([s.value for s in self.robot.range["front-left"]]) < minDist:
            self.move(0, -random()) 
        elif min([s.value for s in self.robot.range["front-right"]]) < minDist:
            self.move(0, random()) 
        else: 
            self.move(.2,0) 
 
def INIT(engine): 
    seed() 
    return RangeSom('RangeSom', engine) 

4.2. Yet Another Example, without Vis, but with two Soms running Simultaneously

from pyrobot.brain import Brain
from pyrobot.brain import psom
import random
import time

def printList(ls, file):
    for i in range(len(ls)):
        file.write(str(ls[i]) + " ")
    file.write("\n")

class BothSoms(Brain):
    """ 
    Written by Cassandra Telenko and Lisa Meeden 
    
    This class is designed to work with the Pioneer simulated robot. 
    The goal of this class is to dynamically create a som of sonar data 
    from a robot doing simple navigation with obstacle avoidance. 
     
    It operates in two modes: CollectData and Run.  The robot begins 
    in CollectData mode, and saves samples of its front sonar values 
    to a file called ir.dat while it is navigating.  After a 
    designated number of steps (given by maxCollectSteps), the sample 
    data file is closed and used to initialize a som.  Then the robot 
    goes into Run mode and uses its sonar values to train the som for 
    a designated number of steps (given by maxRunSteps). 
        
    With Doug and Deepak's avoidObstacles.

    Modified By Cassandra Telenko:
    It does the Above with Motor Values as well. The samples are saved to            motor.dat.
    """ 

    mode='CollectData'
    maxSteps = 50
    currStep = 0
    minDistance = .9
    sensorDat = open("ir.dat", "w")
    sensorDat.write(str(8)+"\n")
    motorDat = open("motor.dat","w")
    motorDat.write(str(2)+"\n")
    myMotorSom=1
    mySensorSom=1

    def step(self):
        if self.mode=='CollectData':
            if self.currStep<self.maxSteps:
                instance=self.scout()
                sensorinstance=instance[0]
                motorinstance=instance[1]
                printList(sensorinstance, self.sensorDat)
                printList(motorinstance, self.motorDat)
                self.currStep = self.currStep + 1
            else:
                self.sensorDat.close()
                self.motorDat.close()
                print "done collecting sample data"
                self.stop()
                #Initializes the Sensor Som
                mySensorDat = psom.dataset(file="ir.dat")
                self.mySensorSom = psom.psom(5,7,data=mySensorDat) 
                self.mySensorSom.init_training(0.02,3.0,self.maxSteps)
                #Initializes the Motor Som
                myMotorDat = psom.dataset(file="motor.dat")
                self.myMotorSom = psom.psom(5,7,data=myMotorDat) 
                self.myMotorSom.init_training(0.02,3.0,self.maxSteps)
                self.mode='RunSom'
                self.currStep=0
                print "done initializing"
        elif self.mode=='RunSom':
            instance=self.scout()
            model1 = self.mySensorSom.train(psom.vector(instance[0]))
            model2 = self.myMotorSom.train(psom.vector(instance[1]))
            print "Sensor Input:\n" + str(instance[0]) + "\nSensor Model: "
            model1.display()
            print "Motor Input:\n" + str(instance[1]) + "\nMotor Model: "
            model2.display()
            self.currStep=self.currStep+1
            if self.currStep==self.maxSteps:
                self.mode='Done'
                print "done training"
        else:
            self.stop()

    def scout(self):
        motorInstance=self.avoidObstacles()
        time.sleep(0.25)
        sensorInstance = range(0,8)
        for i in range(0,8):
            sensorInstance[i] = self.robot.sonar[0][i].value
        return sensorInstance,motorInstance
            
 
    def avoidObstacles(self): 
        minDist = self.minDistance
        happy=random.random()
        if min([s.value for s in self.robot.range["front"]]) < minDist:
            if (random.random() < 0.5):  
                self.move(0, -happy)
                return [0,-happy]
            else:  
                self.move(0, happy)
                return [0,happy]
                
        elif min([s.value for s in self.robot.range["front-left"]]) < minDist:
            self.move(0, -happy)
            return [0,-happy]
        elif min([s.value for s in self.robot.range["front-right"]]) < minDist:
            self.move(0,happy)
            return [0,happy]
        else:  
            self.move(.2,0)
            return [.2,0]
            
   
def INIT(engine):
   return BothSoms('BothSoms', engine)

5. Feedback

bugs, problems, and unfortunate quirks:

features you would like to see

I'm not sure if csom or psom has this functionality, but I think it would be nice: 
would like to know (and see in the vis) how many training vectors (and also testing
vectors) have been mapped to a particular cell so far. 

For example, it would be cool to see a count in each cell that showed how many 
vectors had been mapped to that cell since, say, the last time "Clear" had been
clicked. This could be done in a color other than black or white on top of the
cell's fill color.

This could be done by having a matrix of counters. Would be nice to be able to access
this info programmatically too. 

Also, it would be nice to visualize the errors that a testing vector produces
with all of the model vectors (as I suggested a couple of weeks ago). I see that
Daniel has code for returning a couple of variations of the errors, but it
would be nice to see that in real time too.

Thanks!

-Doug