Sunday, March 29, 2015

Curve Fitting

Curve Fitting, Over Fitting, and Under Fitting

There is always a relationship between two things. There is always an input and an output. If we could find this relationship, we could predict the output from any input. This is the main purpose of curve fitting, to be able to predict an output from the observations that we see. Curve fitting fits a curve into the given observations. The more the curve is able to fit into the observations the more likely that this curve is the hidden relationship.

What is an observation? An observation is anything that you observe. However, in the real world, it's never possible to measure observations with a 100% accuracy. There will be some fluctuations such as measurement errors or even random changes in the environment that can affect the observations. These random factors that we don't know of and cause fluctuations is called, noise. An observation is a combination of the effect plus the noise.

How would we want to fit the curve into these observations where effect and noise are mixed together? We don't want to fit the curve into the noise since the noise is random and can completely fluctuate. We only want to fit the curve to the effect, but not the noise. However, since we cannot distinguish between them easily, then in this post, we will talk about techniques to handle this.

In this example, let's talk about a polynomial curve fitting. For example, a third degree polynomial is a function in the form of: y = a*x^3 + b*x^2 + c*x + d. As the degree of the polynomial increases, the power of fitting the observations also increases because you would have more parameters to adjust to improve the fitness. For example, in a second degree curve you would have 3 coefficients while, in a third degree curve you would have 4 coefficients. In other words, polynomials with higher degrees contain polynomials with lower degrees by just setting the higher degree coefficients to be 0.

However, would the polynomial get so powerful that it not only fits the real effect, but starts fitting the noise as well? Let's do an experiment to find out. In this experiment, I will do the following:

  1. Generate the observations based on a 2nd degree polynomial ( this is the underlying relationship of the real effect that we want to discover)
  2. Generate a random noise using a gaussian distribution which requires the mean (kept at the value 0) and the standard deviation (the noise fluctuates more when you increase the standard deviation)
  3. Fit curves with different degrees and see which curve fits the best (find the root of the mean of the square of the training error [difference between the observation and the curve prediction]). For example, to fit observations with a degree 2 polynomial, you would adjust the wideness of the parabola, the slope, and the level in order to find the minimal training error.
  4. Generate some additional observations based on the original formula (This is used to find the testing error)
  5. Compare the training error and testing error across different degrees of polynomial to determine which degree is best fitting (only fit the effect but not the noise)

Without Noise:

Observations that I generated based on second degree polynomial (3*x^2 + 4*x + 20) with no fluctuation(red).



These graphs have observations without noise in them, meaning that there is no fluctuation. The curve in each graph below is the curve fitted in the given observations. The dots represent the observations that I get without any noise fluctuation:








Since the observations had no noise and fluctuation and was generated using a second degree polynomial, the second degree curve perfectly fits the observations. All degrees higher the second degree curve would also fit the observations perfectly since having more coefficients to adjust would not hurt. The higher degree coefficients could just be set to 0. As you can see in the Degree 5 curve graph the top 3 highest coefficients are 0 since they are not needed in a second degree polynomial. However, since the observations were made from a second degree polynomial, the first degree polynomial did not have enough adjusting power to be able to fit the observations correctly. Therefore there is training error in the degree 1 graph.

With Noise:

These graphs have observations with noise in them, which means that there is fluctuation. The curve in each graph below is the curve fitted in the given observations. In order to fit the observations, the coefficients of the polynomial would be adjusted until the minimal training error is found. In order to minimize the training error, all you have to do is minimize the sum of the square of the differences in each point.

The goal of this experiment is to find a curve that would fit the initial observations(red dots) the best. These observations are made up of the real effect added with some noise. Unfortunately, if the curve fits the red dots well, we do not know whether or not it is just fitting the real effect or also fitting the noise. Because of this, we next add some additional observations such that the curve would not have a chance to fit the white dots. This would allow us to figure out whether or not we are just fitting the real effect or fitting the noise too. In an overfitting situation, the curve would fit the red dots with much less error than the white dots.






As you can see however, when there is noise in the observations, the higher degree curves would start fitting into the noise. This would cause the training error to be of small value. However, the noise is regenerated again, the curve would be completely inaccurate. As shown in the x axis label, the 8th degree graph has the greatest testing error while the 2nd degree graph has the least testing error as we used a degree 2 polynomial to generate the observations.


This graph shows the direct comparison between the training error(in red) and the testing error(in blue). As you can see, the training error decreases as the degree of the polynomial gets higher and higher. However, the testing error is the greatest at degree eight polynomial and lowest at degree two since the observations were generated from degree two polynomial.

COMPLETE CODE : 

import random
import pylab
import numpy
import math

def pfunc(a, b, c):
    x = [i*5 for i in range(0, 10)]
    #x = sorted([random.randrange(0, 51) for i in range(0, 10)])
    estx = range(0, 51)
    negylim = -4000
    posylim = 10000
    y = [(a*num**2 + b*num + c)+random.gauss(0, 1000) for num in x]
    y1 = [(a*num**2 + b*num + c)+random.gauss(0, 1000) for num in x]
    pylab.figure('Plot1')
    pylab.ylim(negylim, posylim)
    pylab.plot(x, y, 'ro')
    
    #Degree 1
    l, m = pylab.polyfit(x, y, 1)
    esty = [(l*num + m) for num in estx]
    estlessy = [(l*num + m) for num in x]
    pylab.figure('Plot2')
    pylab.xlabel('Training RMS Error: '
                +str(round(math.sqrt(numpy.mean([(y[i]-estlessy[i])**2 
                for i in range(0, len(estlessy))])), 3))
                + ', Testing RMS Error: '
                +str(round(math.sqrt(numpy.mean([(y1[i]-estlessy[i])**2
                for i in range(0, len(estlessy))])), 3)))
    pylab.title('Degree 1, coefficient: |' + str(round(l)) + '|' + str(round(m)) + '|')
    pylab.ylim(negylim, posylim)
    pylab.plot(x, y, 'ro')
    pylab.plot(x, y1, 'ow')
    pylab.plot(estx, esty, 'g')
    
    #Degree 2
    l, m, n = pylab.polyfit(x, y, 2)
    esty = [(l*num**2 + m*num + n) for num in estx]
    estlessy = [(l*num**2 + m*num + n) for num in x]
    pylab.figure('Plot3')
    pylab.xlabel('Training RMS Error: '
                +str(round(math.sqrt(numpy.mean([(y[i]-estlessy[i])**2
                for i in range(0, len(estlessy))])), 3))
                + ', Testing RMS Error: '
                +str(round(math.sqrt(numpy.mean([(y1[i]-estlessy[i])**2
                for i in range(0, len(estlessy))])), 3)))
    pylab.title('Degree 2, coefficient: |' 
                + str(round(l)) + '|' 
                + str(round(m)) + '|' 
                + str(round(n)) + '|')
    pylab.ylim(negylim, posylim)
    pylab.plot(x, y, 'ro')
    pylab.plot(x, y1, 'ow')
    pylab.plot(estx, esty)
    
    #Degree 5
    l, m, n, o, p, q = pylab.polyfit(x, y, 5)
    esty = [(l*num**5 + m*num**4 + n*num**3 + o*num**2 + p*num + q) 
            for num in estx]
    estlessy = [(l*num**5 + m*num**4 + n*num**3 + o*num**2 + p*num + q) 
                for num in x]
    pylab.figure('Plot4')
    pylab.xlabel('Training RMS Error: '
                +str(round(math.sqrt(numpy.mean([(y[i]-estlessy[i])**2 
                for i in range(0, len(estlessy))])), 3))
                + ', Testing RMS Error: '
                +str(round(math.sqrt(numpy.mean([(y1[i]-estlessy[i])**2
                for i in range(0, len(estlessy))])), 3)))
    pylab.title('Degree 5, coefficient: |' 
                + str(round(l)) + '|' 
                + str(round(m)) + '|' 
                + str(round(n)) + '|' 
                + str(round(o)) + '|' 
                + str(round(p)) + '|' 
                + str(round(q)) + '|')
    pylab.ylim(negylim, posylim)
    pylab.plot(x, y, 'ro')
    pylab.plot(x, y1, 'ow')
    pylab.plot(estx, esty, 'y')
    
    #Degree 8
    l, m, n, o, p, q, r, s, t = pylab.polyfit(x, y, 8)
    esty = [(l*num**8 + m*num**7 + n*num**6 + o*num**5 
            + p*num**4 + q*num**3 + r*num**2 + s*num + t) 
            for num in estx]
    estlessy = [(l*num**8 + m*num**7 + n*num**6 + o*num**5 
                + p*num**4 + q*num**3 + r*num**2 + s*num + t) 
                for num in x]
    pylab.figure('Plot5')
    pylab.xlabel('Training RMS Error: '
                +str(round(math.sqrt(numpy.mean([(y[i]-estlessy[i])**2 
                for i in range(0, len(estlessy))])), 3))
                + ', Testing RMS Error: '
                + str(round(math.sqrt(numpy.mean([(y1[i]-estlessy[i])**2
                for i in range(0, len(estlessy))])), 3)))
    pylab.title('Degree 8, coefficient: |' 
                + str(round(l)) + '|' 
                + str(round(m)) + '|' 
                + str(round(n)) + '|' 
                + str(round(o)) + '|' 
                + str(round(p)) + '|' 
                + str(round(q)) + '|' 
                + str(round(r)) + '|' 
                + str(round(s)) + '|' 
                + str(round(t)) + '|')
    pylab.ylim(negylim, posylim)
    pylab.plot(x, y, 'ro')
    pylab.plot(x, y1, 'ow')
    pylab.plot(estx, esty, 'm')
    
    
pfunc(3, 4, 20)

Thursday, March 19, 2015

Monty Hall Problem


Monty Hall Problem


The Monty Hall Problem is a simple problem that has stumped many of the smartest people. In this problem there is a host and a player. There are three doors. Two doors contain goats and one door contains a car. Your goal is to achieve that car. The host of the game already knows which door has which object. You first choose one of the doors. The host will then reveal one door from the remaining two doors and the door he reveals must have a goat. You are given a choice to switch to the other door or stay with your original choice. The question is, will you switch?


Most people think that once the host has revealed a door with a goat in it, that the other two doors have a half-half chance of containing the car and that it doesn't matter whether or not you switch doors. However, this is a common misconception.

Let's say you pick door 1. Your first choice will have a 1/3 probability of being the car. There is a likelihood of 2/3 that the car is in door 2 and door 3. However, once the host reveals door 3 that is goat. The probability of the car being in the door 3 is 0 and the 2/3 probability is shifted to door 2. Now, door 2 has a 2/3 probability of containing the car while door 1 only has a 1/3 probability of containing the car. Thus, you should always switch doors.


To actually visualize this problem better, let's build a simulation.

I'm going to build a class called Monty Hall to simulate one round of the game.

class Monty_Hall:

    def __init__(self):
        self.game = ['car', 'goat', 'goat']
        # position of choice
        self.choice_pos = None
        # position of monty's choice
        self.monty_choice_pos = None
        # doors you can choose from
        self.options = range(0, 3)
        # shuffle the doors
        random.shuffle(self.game)
        # doors monty can choose from
        self.monty_options = [door_num for door_num in range(0, 3)
                          if self.game[door_num] == 'goat']


The first thing in this game is that the player needs to choose a certain door.

    def choose(self, door_pos):
        self.choice_pos = door_pos
        try:
            self.monty_options.remove(self.choice_pos)
        except ValueError:
            pass
        self.options.remove(self.choice_pos)


I assign the door position parameter of this method into the variable of the position of my choice and removes that choice from my options.

The second thing in this game is for Monty to reveal one of the other two doors that has a goat in it.

    def monty_choose(self):
        self.monty_choice_pos = random.choice(self.monty_options)
        self.options.remove(self.monty_choice_pos)

Now the player is offered a choice to switch to a different door.

There are three strategies:
  1. The player always stays with his original choice
  2. The player always switch from his original choice to the other door
  3. The player randomly decide whether or not to switch doors
    def stay(self):
        pass

    def switch(self):
        self.choice_pos = random.choice(self.options)
        self.options.remove(self.choice_pos)

    def random_switch(self):
        if random.random() < 0.5:
            self.switch()


Now, I create a function to simulate the play of this game one time with the specified strategy and outputs whether the player achieve the car or not.

def simulate(strategy):
    a = Monty_Hall()
    a.choose(random.choice(range(0, 3)))
    a.monty_choose()
    if strategy == 'stay':
        a.stay()
    elif strategy == 'switch':
        a.switch()
    elif strategy == 'random':
        a.random_switch()
    if a.game[a.choice_pos] == 'car':
        return True
    else:
        return False

Lastly, I create a function to run this simulation many times and compare the rate of winning the car for each strategy.

def run_simulation(num_times):
    results = {'stay_wins': 0, 'stay_losses': 0,
               'switch_wins': 0, 'switch_losses': 0,
               'random_wins': 0, 'random_losses': 0}
    for i in range(0, num_times):
        for strat in ['stay', 'switch', 'random']:
            if simulate(strat):
                results[strat + '_wins'] += 1
            else:
                results[strat + '_losses'] += 1

    for strat in ['stay', 'switch', 'random']:
        print strat + ' win ratio: ' + str(results[strat+'_wins']/float(num_times))

Let's run this code on the console.

>>> run_simulation(1000)
stay win ratio: 0.332
switch win ratio: 0.675
random win ratio: 0.505

As you can see, the rate of success if you always stay with your original choice is about 33% (1/3).
The rate of success if you randomly choose between staying and switching doors is about 50% (1/2).
The rate of success if you always switch to the last door is about 66% (2/3).

COMPLETE PROGRAM

import random

class Monty_Hall:

    def __init__(self):
        self.game = ['car', 'goat', 'goat']
        # position of choice
        self.choice_pos = None
        # position of monty's choice
        self.monty_choice_pos = None
        # doors you can choose from
        self.options = range(0, 3)
        # shuffle the doors
        random.shuffle(self.game)
        # doors monty can choose from
        self.monty_options = [door_num for door_num in range(0, 3)
                          if self.game[door_num] == 'goat']

    def choose(self, door_pos):
        self.choice_pos = door_pos
        try:
            self.monty_options.remove(self.choice_pos)
        except ValueError:
            pass
        self.options.remove(self.choice_pos)

    def monty_choose(self):
        self.monty_choice_pos = random.choice(self.monty_options)
        self.options.remove(self.monty_choice_pos)

    def stay(self):
        pass

    def switch(self):
        self.choice_pos = random.choice(self.options)
        self.options.remove(self.choice_pos)

    def random_switch(self):
        if random.random() < 0.5:
            self.switch()

def simulate(strategy):
    a = Monty_Hall()
    a.choose(random.choice(range(0, 3)))
    a.monty_choose()
    if strategy == 'stay':
        a.stay()
    elif strategy == 'switch':
        a.switch()
    elif strategy == 'random':
        a.random_switch()
    if a.game[a.choice_pos] == 'car':
        return True
    else:
        return False

def run_simulation(num_times):
    results = {'stay_wins': 0, 'stay_losses': 0,
               'switch_wins': 0, 'switch_losses': 0,
               'random_wins': 0, 'random_losses': 0}
    for i in range(0, num_times):
        for strat in ['stay', 'switch', 'random']:
            if simulate(strat):
                results[strat + '_wins'] += 1
            else:
                results[strat + '_losses'] += 1

    for strat in ['stay', 'switch', 'random']:
        print strat + ' win ratio: ' + str(results[strat+'_wins']/float(num_times))

Tuesday, March 17, 2015

15 Puzzle Game - (In Python)


15 Puzzle Game


This game is the 15 Puzzle Game. In this game, there is a 4*4 board with 15 numbers and an empty square. The numbers are then shuffled randomly. The goal of the game is to move the numbers in such a way that the numbers are ordered again as shown in the picture below. The rules are simple. You can only move tiles into the empty tile space.



First of all we need to make a class to represent this board:

class Board:

    def __init__(self):
        # 4 by 4 board
        # self.board is the goal board when first initialized
        self.board = [range(1, 5),
                      range(5, 9), 
                      range(9, 13),
                      range(13, 16) + ['*']]
        # self.goal is later used as a comparison to solve the mixed board
        self.goal = [] 
        for i in self.board:
            self.goal.append(tuple(i))
        self.goal = tuple(self.goal)
        # self.empty is the position of empty block
        self.empty = [3, 3]

Now we need a way to print this graph on the console of Python:


    def __repr__(self):
        string = ''
        for row in self.board:
            for num in row:
                if len(str(num)) == 1:
                    string += '   ' + str(num)
                elif len(str(num)) > 1:
                    string += '  ' + str(num)
            string += '\n'
        return string

When you print the board method on the console, it should look like this:

>>> board = Board()
>>> print board
   1   2   3   4
   5   6   7   8
   9  10  11  12
  13  14  15   *

An important thing to remember when you are writing __repr__ is that what you return MUST be a string.

Now, after we have a way to represent our board, we need to be able to do the basic things the player can do. I need to be able to move the empty block up, down, right, and left.


    def move_up(self): # move empty block up
        try:
            if self.empty[0] != 0:
                tmp = self.board[self.empty[0]-1][self.empty[1]]
                self.board[self.empty[0]-1][self.empty[1]] = '*'
                self.board[self.empty[0]][self.empty[1]] = tmp
                self.empty = [self.empty[0]-1, self.empty[1]]
        except IndexError:
            pass

    def move_down(self): # move empty block down
        try:
            tmp = self.board[self.empty[0]+1][self.empty[1]]
            self.board[self.empty[0]+1][self.empty[1]] = '*'
            self.board[self.empty[0]][self.empty[1]] = tmp
            self.empty = [self.empty[0]+1, self.empty[1]]
        except IndexError:
            pass

    def move_right(self): # move empty block right
        try:
            tmp = self.board[self.empty[0]][self.empty[1]+1]
            self.board[self.empty[0]][self.empty[1]+1] = '*'
            self.board[self.empty[0]][self.empty[1]] = tmp
            self.empty = [self.empty[0], self.empty[1]+1]
        except IndexError:
            pass

    def move_left(self): # move empty block left
        try:
            if self.empty[1] != 0:
                tmp = self.board[self.empty[0]][self.empty[1]-1]
                self.board[self.empty[0]][self.empty[1]-1] = '*'
                self.board[self.empty[0]][self.empty[1]] = tmp
                self.empty = [self.empty[0], self.empty[1]-1]
        except IndexError:
            pass



These 4 methods are the methods in the class that are going to be used to move the empty block. This is when the position of the empty block comes in handy.

We use a try and except to make sure we don't move off the board.

If you have an array and you take the index of [-1], you get the last element of that array. We don't want that to happen and therefore we put an if statement to make sure that the empty block is not at the top of the board.

If it passes these condition, then we can actually move the empty block. We subtract 1 from the row and make that index to be the empty block. We swap the empty block with the previous number that the new empty block has just replaced. We do this for all 4 directions.

After moving the board should look like this:

>>> board = Board()
>>> print board
   1   2   3   4
   5   6   7   8
   9  10  11  12
  13  14  15   *

>>> board.move_up()
>>> board.move_left()
>>> print board
   1   2   3   4
   5   6   7   8
   9  10   *  11
  13  14  15  12


We have gotten the basic movements down. Let's make this a real game. In order to do that, we need a way to shuffle the whole board such that there is a game to solve.

    def shuffle(self, steps):
        for i in range(0, steps):
            direction = random.randrange(1, 5)
            if direction == 1:
                self.move_up()
            elif direction == 2:
                self.move_right()
            elif direction == 3:
                self.move_left()
            elif direction == 4:
                self.move_down()

This method will be used to mix the board randomly. I generate a random number between the range 1 to 5 including 1 and excluding 5 which gives me a total of 4 possible numbers. I associate these 4 numbers to each move (up, down, left, right).

Board after shuffled 100 times:

>>> board = Board()
>>> board.shuffle(100)
>>> print board
   1   2   4  12
   9   5   6   3
  10   8   *   7
  13  14  11  15

Now that we have a game to play, here comes the complicated part: to be able to write a function to solve this. The way we're going to solve this is using breadth first search which finds the shortest way to get to the goal.

Breadth First Search

There are three types of searches : Breadth First Search, Uniform Cost Search, and Depth First Search. In depth first search, you do not care about whether or not there are nodes of different levels. Depth first search always goes deeper down the node tree. Breadth First Search on the other hand explores the node with the most shallow level. As a result, breadth first search reaches the shortest path but depth first search is more efficient on memory. Uniform cost search is for weighted graphs when you go explore the next node that costs the least. For this program, the most applicable search is the Breath First Search since we want to find the shortest path to reach our goal.

Implementation of Breath First Search:




    def solve(self):
        start = self.convert_to_tuple(self.board)
        pred = {}
        visited = []
        frontier = Queue.Queue()
        frontier.put(start)
        
        while frontier.qsize() > 0:
            tmp = frontier.get()
            
            if tmp == self.goal:
                path = []
                while tmp != start:
                    path.append(pred[tmp][1])
                    tmp = pred[tmp][0]
                return path[::-1]
            
            if tmp not in visited:
                visited.append(tmp)
                tmpboard = self.match(tmp)
                tmpboard.move_up()
                if self.convert_to_tuple(tmpboard.board) != tmp:
                    frontier.put(self.convert_to_tuple(tmpboard.board))
                    if not pred.has_key(self.convert_to_tuple(tmpboard.board)):
                        pred[self.convert_to_tuple(tmpboard.board)]=[tmp, 'up']

                
                tmpboard = self.match(tmp)
                tmpboard.move_down()
                if self.convert_to_tuple(tmpboard.board) != tmp:
                    frontier.put(self.convert_to_tuple(tmpboard.board))
                    if not pred.has_key(self.convert_to_tuple(tmpboard.board)):
                        pred[self.convert_to_tuple(tmpboard.board)]=[tmp, 'down']

                        
                tmpboard = self.match(tmp)
                tmpboard.move_right()
                if self.convert_to_tuple(tmpboard.board) != tmp:
                    frontier.put(self.convert_to_tuple(tmpboard.board))
                    if not pred.has_key(self.convert_to_tuple(tmpboard.board)):
                        pred[self.convert_to_tuple(tmpboard.board)]=[tmp, 'right']

                
                tmpboard = self.match(tmp)
                tmpboard.move_left()
                if self.convert_to_tuple(tmpboard.board) != tmp:
                    frontier.put(self.convert_to_tuple(tmpboard.board))
                    if not pred.has_key(self.convert_to_tuple(tmpboard.board)):
                        pred[self.convert_to_tuple(tmpboard.board)]=[tmp, 'left']

        raise Exception('There is no solution.')

In this method called solve, we will output a list of strings that give you directions in order to solve the board ('up', 'down', 'left', 'right'). First thing you have to notice is that you MUST import Queue with the line:

import Queue

Visualizing the Problem

In this program, each state that the board is in is a node. We are comparing node to node and checking until our current node is the same as the goal board, which means all the numbers are in order.

The Variables:

  • start stores the original list used for the board when we initialized this class and is the first node that is explored
  • pred is used in the method at the very end to create the directions to solve the board
  • visited stores all the nodes that we have already checked/visited and explored through
  • frontier is a Queue that stores our plan for which node to explore next.
  • NOTE :  Frontier is a Queue which means first in first out

Parts

The First Part:
The first part of this method is the first if statement which checks whether or not our current node is the goal node that we are trying to achieve. If it is, we use our current dictionary and create a list of directions and return that list.

The Second Part:

The second part of this method is the second if statement which adds new nodes to our Queue such that we an explore them. First of all, we have to make sure that we are not visiting the same node twice so we check if our current node is in the visited array. Then there are 4 miniparts for each direction. These miniparts don't vary a lot. From one node of the board I can get to 4 new nodes by moving 4 different directions. I store these 4 possible nodes into the Queue such that I can explore them in the future. Then I add the directions to the dictionary such that, when I do find the solution, I can refer back to the dictionary 'pred' and create a list of directions.

NOTE : Some methods in the solve method are not shown for simplicity. At the bottom of this blog, you will be able to see the whole program with these small methods.

COMPLETE PROGRAM:

import Queue
import random

class Board:

    def __init__(self):
        self.board = [range(1, 5), range(5, 9), range(9, 13), range(13, 16) + ['*']]
        self.goal = []
        for i in self.board:
            self.goal.append(tuple(i))
        self.goal = tuple(self.goal)
        self.empty = [3, 3]

    def __repr__(self):
        string = ''
        for row in self.board:
            for num in row:
                if len(str(num)) == 1:
                    string += '   ' + str(num)
                elif len(str(num)) > 1:
                    string += '  ' + str(num)
            string += '\n'
        return string

    def convert_to_tuple(self, ar):
        result = []
        for i in ar:
            result.append(tuple(i))
        return tuple(result)

    def convert_to_list(self, tup):
        result = []
        for i in tup:
            result.append(list(i))
        return result

    def match(self, copy):
        a = Board()
        a.board = copy
        for row in range(0, 4):
            for col in range(0, 4):
                if a.board[row][col] == '*':
                    a.empty = [row, col]
        result = []
        for i in a.board:
            result.append(list(i))
        a.board = result
        return a

    def move_up(self): # move empty block up
        try:
            if self.empty[0] != 0:
                tmp = self.board[self.empty[0]-1][self.empty[1]]
                self.board[self.empty[0]-1][self.empty[1]] = '*'
                self.board[self.empty[0]][self.empty[1]] = tmp
                self.empty = [self.empty[0]-1, self.empty[1]]
        except IndexError:
            pass

    def move_down(self): # move empty block down
        try:
            tmp = self.board[self.empty[0]+1][self.empty[1]]
            self.board[self.empty[0]+1][self.empty[1]] = '*'
            self.board[self.empty[0]][self.empty[1]] = tmp
            self.empty = [self.empty[0]+1, self.empty[1]]
        except IndexError:
            pass

    def move_right(self): # move empty block right
        try:
            tmp = self.board[self.empty[0]][self.empty[1]+1]
            self.board[self.empty[0]][self.empty[1]+1] = '*'
            self.board[self.empty[0]][self.empty[1]] = tmp
            self.empty = [self.empty[0], self.empty[1]+1]
        except IndexError:
            pass

    def move_left(self): # move empty block left
        try:
            if self.empty[1] != 0:
                tmp = self.board[self.empty[0]][self.empty[1]-1]
                self.board[self.empty[0]][self.empty[1]-1] = '*'
                self.board[self.empty[0]][self.empty[1]] = tmp
                self.empty = [self.empty[0], self.empty[1]-1]
        except IndexError:
            pass

    def shuffle(self, steps):
        for i in range(0, steps):
            direction = random.randrange(1, 5)
            if direction == 1:
                self.move_up()
            elif direction == 2:
                self.move_right()
            elif direction == 3:
                self.move_left()
            elif direction == 4:
                self.move_down()

    def solve(self):
        start = self.convert_to_tuple(self.board)
        pred = {}
        visited = []
        frontier = Queue.Queue()
        frontier.put(start)
        
        while frontier.qsize() > 0:
            tmp = frontier.get()
            
            if tmp == self.goal:
                path = []
                while tmp != start:
                    path.append(pred[tmp][1])
                    tmp = pred[tmp][0]
                return path[::-1]
            
            if tmp not in visited:
                visited.append(tmp)
                tmpboard = self.match(tmp)
                tmpboard.move_up()
                if self.convert_to_tuple(tmpboard.board) != tmp:
                    frontier.put(self.convert_to_tuple(tmpboard.board))
                    if not pred.has_key(self.convert_to_tuple(tmpboard.board)):
                        pred[self.convert_to_tuple(tmpboard.board)]=[tmp, 'up']

                
                tmpboard = self.match(tmp)
                tmpboard.move_down()
                if self.convert_to_tuple(tmpboard.board) != tmp:
                    frontier.put(self.convert_to_tuple(tmpboard.board))
                    if not pred.has_key(self.convert_to_tuple(tmpboard.board)):
                        pred[self.convert_to_tuple(tmpboard.board)]=[tmp, 'down']

                        
                tmpboard = self.match(tmp)
                tmpboard.move_right()
                if self.convert_to_tuple(tmpboard.board) != tmp:
                    frontier.put(self.convert_to_tuple(tmpboard.board))
                    if not pred.has_key(self.convert_to_tuple(tmpboard.board)):
                        pred[self.convert_to_tuple(tmpboard.board)]=[tmp, 'right']

                
                tmpboard = self.match(tmp)
                tmpboard.move_left()
                if self.convert_to_tuple(tmpboard.board) != tmp:
                    frontier.put(self.convert_to_tuple(tmpboard.board))
                    if not pred.has_key(self.convert_to_tuple(tmpboard.board)):
                        pred[self.convert_to_tuple(tmpboard.board)]=[tmp, 'left']

        raise Exception('There is no solution.')

Trial:

>>> board = Board()
>>> board.shuffle(20)
>>> print board
   1   2   3   4
   *   5   7   8
  10   6  11  12
   9  13  14  15

>>> board.solve()
['right', 'down', 'left', 'down', 'right', 'right', 'right']
>>> board.move_right()
>>> board.move_down()
>>> board.move_left()
>>> board.move_down()
>>> board.move_right()
>>> board.move_right()
>>> board.move_right()
>>> print board
   1   2   3   4
   5   6   7   8
   9  10  11  12
  13  14  15   *