Solving Dijkstra's algorithm - passing costs / parents with two edges - python

I have a graph like this:
# graph table
graph = {}
graph['start'] = {}
graph['start']['a'] = 5
graph['start']['b'] = 2
graph['a'] = {}
graph['a']['c'] = 4
graph['a']['d'] = 2
graph['b'] = {}
graph['b']['a'] = 8
graph['b']['d'] = 7
graph['c'] = {}
graph['c']['d'] = 6
graph['c']['finish'] = 3
graph['d'] = {}
graph['d']['finish'] = 1
graph['finish'] = {}
And I am trying to find the fastest way from S to F.
In the first example in the book only one edge was connected to one node, in this example for example, node D has 3 weights and a cost table was used:
costs = {}
infinity = float('inf')
costs['a'] = 5
costs['b'] = 2
costs['c'] = 4
costs['d'] = # there is 3 costs to node D, which one to select?
costs['finish'] = infinity
And a parents table:
parents = {}
parents['a'] = 'start' # why not start and b since both `S` and `B` can be `A` nodes parent?
parents['b'] = 'start'
parents['c'] = 'a'
parents['d'] = # node D can have 3 parents
parents['finish'] = None
But this also works, by works I mean no error is thrown, so do I only have to name the parents from the first node S?
parents = {}
parents['a'] = 'start'
parents['b'] = 'start'
parents['finish'] = None
The code:
processed = []
def find_lowest_cost_node(costs):
lowest_cost = float('inf')
lowest_cost_node = None
for node in costs:
cost = costs[node]
if cost < lowest_cost and node not in processed:
lowest_cost = cost
lowest_cost_node = node
return lowest_cost_node
node = find_lowest_cost_node(costs)
while node is not None:
cost = costs[node]
neighbors = graph[node]
for n in neighbors.keys():
new_cost = cost + neighbors[n]
if costs[n] > new_cost:
costs[n] = new_cost
parents[n] = node
processed.append(node)
node = find_lowest_cost_node(costs)
def find_path(parents, finish):
path = []
node = finish
while node:
path.insert(0, node)
if parents.__contains__(node):
node = parents[node]
else:
node = None
return path
path = find_path(parents, 'finish')
distance = costs['finish']
print(f'Path is: {path}')
print(f'Distance from start to finish is: {distance}')
I get:
Path is: ['finish']
Distance from start to finish is: inf
Where is my mistake and how should I add cost and parent to a node which can be visited from more than 1 node?
Edit
I do believe this is not the best approach for this problem, the best in practice solution / recommendations are welcome.

You do not need to initialise the cost table with more than costs['start'] = 0 or the parents dictionary with more than parents = {}. That is what your algorithm is going to create for you!
The only other change you need to make is to your while loop. It just needs to check whether the new node has already been detected before. If so then we check to see whether the new path is shorter and update as required; if not then we establish the new path.
while node is not None:
cost = costs[node]
neighbors = graph[node]
for n in neighbors.keys():
new_cost = cost + neighbors[n]
if n in costs:
if costs[n] > new_cost:
costs[n] = new_cost
parents[n] = node
else:
costs[n] = new_cost
parents[n] = node
processed.append(node)
node = find_lowest_cost_node(costs)
I think there are much neater ways to deal with graphs but this is the minimal change needed to make your code work as required. Hope it's helpful!

Related

Why doesn't this Monte Carlo Tree Search algorithm work properly?

PROBLEM
I'm writing a Monte-Carlo tree search algorithm to play chess in Python. I replaced the simulation stage with a custom evaluation function. My code looks perfect but for some reason acts strange. It recognizes instant wins easily enough but cannot recognize checkmate-in-2 moves and checkmate-in-3 moves positions. Any ideas?
WHAT I'VE TRIED
I tried giving it more time to search but it still cannot find the best move even when it leads to a guaranteed win in two moves. However, I noticed that results improve when I turn off the custom evaluation and use classic Monte Carlo Tree Search simulation. (To turn off custom evaluation, just don't pass any arguments into the Agent constructor.) But I really need it to work with custom evaluation because I am working on a machine learning technique for board evaluation.
I tried printing out the results of the searches to see which moves the algorithm thinks are good. It consistently ranks the best move in mate-in-2 and mate-in-3 situations among the worst. The rankings are based on the number of times the move was explored (which is how MCTS picks the best moves).
MY CODE
I've included the whole code because everything is relevant to the problem. To run this code, you may need to install python-chess (pip install python-chess).
I've struggled with this for more than a week and it's getting frustrating. Any ideas?
import math
import random
import time
import chess
import chess.engine
class Node:
def __init__(self, state, parent, action):
"""Initializes a node structure for a Monte-Carlo search tree."""
self.state = state
self.parent = parent
self.action = action
self.unexplored_actions = list(self.state.legal_moves)
random.shuffle(self.unexplored_actions)
self.colour = self.state.turn
self.children = []
self.w = 0 # number of wins
self.n = 0 # number of simulations
class Agent:
def __init__(self, custom_evaluation=None):
"""Initializes a Monte-Carlo tree search agent."""
if custom_evaluation:
self._evaluate = custom_evaluation
def mcts(self, state, time_limit=float('inf'), node_limit=float('inf')):
"""Runs Monte-Carlo tree search and returns an evaluation."""
nodes_searched = 0
start_time = time.time()
# Initialize the root node.
root = Node(state, None, None)
while (time.time() - start_time) < time_limit and nodes_searched < node_limit:
# Select a leaf node.
leaf = self._select(root)
# Add a new child node to the tree.
if leaf.unexplored_actions:
child = self._expand(leaf)
else:
child = leaf
# Evaluate the node.
result = self._evaluate(child)
# Backpropagate the results.
self._backpropagate(child, result)
nodes_searched += 1
result = max(root.children, key=lambda node: node.n)
return result
def _uct(self, node):
"""Returns the Upper Confidence Bound 1 of a node."""
c = math.sqrt(2)
# We want every WHITE node to choose the worst BLACK node and vice versa.
# Scores for each node are relative to that colour.
w = node.n - node.w
n = node.n
N = node.parent.n
try:
ucb = (w / n) + (c * math.sqrt(math.log(N) / n))
except ZeroDivisionError:
ucb = float('inf')
return ucb
def _select(self, node):
"""Returns a leaf node that either has unexplored actions or is a terminal node."""
while (not node.unexplored_actions) and node.children:
# Pick the child node with highest UCB.
selection = max(node.children, key=self._uct)
# Move to the next node.
node = selection
return node
def _expand(self, node):
"""Adds one child node to the tree."""
# Pick an unexplored action.
action = node.unexplored_actions.pop()
# Create a copy of the node state.
state_copy = node.state.copy()
# Carry out the action on the copy.
state_copy.push(action)
# Create a child node.
child = Node(state_copy, node, action)
# Add the child node to the list of children.
node.children.append(child)
# Return the child node.
return child
def _evaluate(self, node):
"""Returns an evaluation of a given node."""
# If no custom evaluation function was passed into the object constructor,
# use classic simulation.
return self._simulate(node)
def _simulate(self, node):
"""Randomly plays out to the end and returns a static evaluation of the terminal state."""
board = node.state.copy()
while not board.is_game_over():
# Pick a random action.
move = random.choice(list(board.legal_moves))
# Perform the action.
board.push(move)
return self._calculate_static_evaluation(board)
def _backpropagate(self, node, result):
"""Updates a node's values and subsequent parent values."""
# Update the node's values.
node.w += result.pov(node.colour).expectation()
node.n += 1
# Back up values to parent nodes.
while node.parent is not None:
node.parent.w += result.pov(node.parent.colour).expectation()
node.parent.n += 1
node = node.parent
def _calculate_static_evaluation(self, board):
"""Returns a static evaluation of a *terminal* board state."""
result = board.result(claim_draw=True)
if result == '1-0':
wdl = chess.engine.Wdl(wins=1000, draws=0, losses=0)
elif result == '0-1':
wdl = chess.engine.Wdl(wins=0, draws=0, losses=1000)
else:
wdl = chess.engine.Wdl(wins=0, draws=1000, losses=0)
return chess.engine.PovWdl(wdl, chess.WHITE)
def custom_evaluation(node):
"""Returns a static evaluation of a board state."""
board = node.state
# Evaluate terminal states.
if board.is_game_over(claim_draw=True):
result = board.result(claim_draw=True)
if result == '1-0':
wdl = chess.engine.Wdl(wins=1000, draws=0, losses=0)
elif result == '0-1':
wdl = chess.engine.Wdl(wins=0, draws=0, losses=1000)
else:
wdl = chess.engine.Wdl(wins=0, draws=1000, losses=0)
return chess.engine.PovWdl(wdl, chess.WHITE)
# Evaluate material.
material_balance = 0
material_balance += len(board.pieces(chess.PAWN, chess.WHITE)) * +100
material_balance += len(board.pieces(chess.PAWN, chess.BLACK)) * -100
material_balance += len(board.pieces(chess.ROOK, chess.WHITE)) * +500
material_balance += len(board.pieces(chess.ROOK, chess.BLACK)) * -500
material_balance += len(board.pieces(chess.KNIGHT, chess.WHITE)) * +300
material_balance += len(board.pieces(chess.KNIGHT, chess.BLACK)) * -300
material_balance += len(board.pieces(chess.BISHOP, chess.WHITE)) * +300
material_balance += len(board.pieces(chess.BISHOP, chess.BLACK)) * -300
material_balance += len(board.pieces(chess.QUEEN, chess.WHITE)) * +900
material_balance += len(board.pieces(chess.QUEEN, chess.BLACK)) * -900
# TODO: Evaluate mobility.
mobility = 0
# Aggregate values.
centipawn_evaluation = material_balance + mobility
# Convert evaluation from centipawns to wdl.
wdl = chess.engine.Cp(centipawn_evaluation).wdl(model='lichess')
static_evaluation = chess.engine.PovWdl(wdl, chess.WHITE)
return static_evaluation
m1 = chess.Board('8/8/7k/8/8/8/5R2/6R1 w - - 0 1') # f2h2
# WHITE can win in one move. Best move is f2-h2.
m2 = chess.Board('8/6k1/8/8/8/8/1K2R3/5R2 w - - 0 1')
# WHITE can win in two moves. Best move is e2-g2.
m3 = chess.Board('8/8/5k2/8/8/8/3R4/4R3 w - - 0 1')
# WHITE can win in three moves. Best move is d2-f2.
agent = Agent(custom_evaluation)
result = agent.mcts(m2, time_limit=30)
print(result)

How to extract all ancestors of a nodes and leaves in a decision tree?

I don't know how difficult is this question. Suppose we train a decision tree and then I want to records all nodes and leaves in the tree, what are their ancestors (set of p for parents), and which of their ancestors are divided based on x<a (set A_l) and x>a (set A_r).
Actually, nodes can be classified as follows and I want to extract all of this information:
t = 1,...,T set of all nodes
p(t): parent of node t
A(t): set of all ancestors of node t
A_L(t): the set of ancestors of t whose left branch has been followed on the path from the root node to t
A_R(t): the set of ancestors of t whose left branch has been followed on the path from the root node to t
T_b: the set of branch nodes
T_l: the set of leaf nodes
I wrote a code and I could store all left and right children of a node, but I don't know how to determine the aforementioned sets.
class Nodes():
no_nodes = 0
def __init__(self,tree,no_total_nodes,node):
self.no_nodes = no_total_nodes
self.current_node= node
self.left_child = all_left_children(tree,self.current_node)
self.right_child = all_right_children(tree,self.current_node)
where
def func_right_node(tree,start_node):
node = tree.children_right[start_node]
return node
def func_left_node(tree,start_node):
node = tree.children_left[start_node]
return node
def all_right_children(tree,start_node):
List=[]
while True:
r_node = func_right_node(tree,start_node)
if r_node!= -1:
List.append(r_node)
start_node = r_node
elif r_node== -1:
break
return List
def all_left_children(tree,start_node):
List=[]
while True:
r_node = func_left_node(tree,start_node)
if r_node!= -1:
List.append(r_node)
start_node = r_node
elif r_node== -1:
break
return List
and all nodes and leaves are stores as follows:
TREE = decision_tree.tree_
total_number_nodes = decision_tree.tree_.node_count
All_Nodes = []
All_Leaves = []
for i in range(total_number_nodes):
All_Nodes.append(Nodes(TREE,total_number_nodes,i))
if not All_Nodes[i].left_child:
All_Leaves.append(All_Nodes[i])

MCTS : RecursionError: maximum recursion depth exceeded while calling a Python object

For this Monte-Carlo Tree Search python coding, why do I have RecursionError: maximum recursion depth exceeded while calling a Python object ?
Is this normal for MCTS which needs to keep expanding ?
Or did I miss any other bugs which I am still tracing at this moment ?
As for puct_array , see PUCT formula for explanation
import numpy as np
import random
# Reference :
# https://www.reddit.com/r/learnmachinelearning/comments/fmx3kv/empirical_example_of_mcts_calculation_puct_formula/
# PUCT formula : https://colab.research.google.com/drive/14v45o1xbfrBz0sG3mHbqFtYz_IrQHLTg#scrollTo=1VeRCpCSaHe3
# https://en.wikipedia.org/wiki/Monte_Carlo_tree_search#Exploration_and_exploitation
cfg_puct = np.sqrt(2) # to balance between exploitation and exploration
puct_array = [] # stores puct ratio for every child nodes for argmax()
# determined by PUCT formula
def find_best_path(parent):
if (parent == root) | (len(parent.nodes) == 0):
return parent
for N in parent.nodes:
puct_array.append(N.puct)
max_index = np.argmax(puct_array)
# leaf node has 0 child node
is_leaf_node = (len(parent.nodes[max_index].nodes) == 0)
if is_leaf_node:
return parent.nodes[max_index]
return parent.nodes[max_index]
class Mcts:
def __init__(self, parent):
# https://www.tutorialspoint.com/python_data_structure/python_tree_traversal_algorithms.htm
# https://www.geeksforgeeks.org/sum-parent-nodes-child-node-x/
self.parent = parent # this is the parent node
self.nodes = [] # creates an empty list with no child nodes initially
self.data = 0 # can be of any value, but just initialized to 0
self.visit = 1 # when a node is first created, it is counted as visited once
self.win = 0 # because no play/simulation had been performed yet
self.loss = 0 # because no play/simulation had been performed yet
self.puct = 0 # initialized to 0 because game had not started yet
# this function computes W/N ratio for each node
def compute_total_win_and_visits(self, total_win=0, visits=0):
if self.win:
total_win = total_win + 1
if self.visit:
visits = visits + 1
if self.nodes: # if there is/are child node(s)
for n in self.nodes: # traverse down the entire branch for each child node
n.compute_total_win_and_visits(total_win, visits)
return total_win, visits # same order (W/N) as in
# https://i.imgur.com/uI7NRcT.png inside each node
# Selection stage of MCTS
def select(self):
# traverse recursively all the way down from the root node
# to find the path with the highest W/N ratio (this ratio is determined using PUCT formula)
# and then select that leaf node to do the new child nodes insertion
leaf = find_best_path(self) # returns a reference pointer to the desired leaf node
leaf.insert() # this leaf node is selected to insert child nodes under it
# Expansion stage of MCTS
# Insert Child Nodes for a leaf node
def insert(self):
num_of_possible_game_states = 8 # assuming that we are playing tic-tac toe
for S in range(num_of_possible_game_states):
self.nodes.append(Mcts(self)) # inserts child nodes
self.nodes[len(self.nodes) - 1].simulate()
# Simulation stage of MCTS
def simulate(self):
# will replace the simulation stage with a neural network in the future
self.win = random.randint(0, 1) # just for testing purpose, so it is either win (1) or lose (0)
self.loss = ~self.win & random.randint(0, 1) # 'and' with randn() for tie/draw situation
self.backpropagation(self.win, self.loss)
# Backpropagation stage of MCTS
def backpropagation(self, win, loss):
# traverses upwards to the root node
# and updates PUCT ratio for each parent nodes
# computes the PUCT expression Q+U https://slides.com/crem/lc0#/9
if self.parent == 0:
num_of_parent_visits = 0
else:
num_of_parent_visits = self.parent.visit
total_win_for_all_child_nodes, num_of_child_visits = self.compute_total_win_and_visits(0, 0)
self.visit = num_of_child_visits
# traverses downwards all branches (only for those branches involved in previous play/simulation)
# and updates PUCT values for all their child nodes
self.puct = (total_win_for_all_child_nodes / num_of_child_visits) + \
cfg_puct * np.sqrt(num_of_parent_visits) / (num_of_child_visits + 1)
if self.parent == root: # already reached root node
self.select()
else:
self.parent.visit = self.parent.visit + 1
if win:
if self.parent.parent: # grandparent node (same-coloured player) exists
self.parent.parent.win = self.parent.parent.win + 1
if (win == 0) & (loss == 0): # tie is between loss (0) and win (1)
self.parent.win = self.parent.win + 0.5 # parent node (opponent player)
if self.parent.parent: # grandparent node (same-coloured player) exists
self.parent.parent.win = self.parent.parent.win + 0.5
self.parent.backpropagation(win, loss)
# Print the Tree
def print_tree(self, child):
for x in child.nodes:
print(x.data)
if x.nodes:
self.print_tree(x.nodes)
root = Mcts(0) # we use parent=0 because this is the head/root node
root.select()
print(root.print_tree(root))
Problem solved using
import sys
sys.setrecursionlimit(100000)
See the most up-to-date code here.

Inserting into n-child tree in Python

I am trying to implement a tree for the travelling salesperson problem. My particular tree has 5 destinations which are fully connected to each other.
One of the destinations is guaranteed to always be the starting destination and that you are only allowed to visit each destination once with the exception of the starting destination which you have to return to (ie if you have [1,2,3,4,5] with 1 the starting destination, a possible sequence of moves would be 1-3-5-2-4-1)
I tried implementing a tree in python with the following code (I brute forced it since I know the maximum depth is going to be 5).
class Node(object):
def __init__(self,value, city, children = [None, None, None, None]):
self.value = value
self.city = city
self.children = children
class Tree(object):
def __init__(self):
self.root = None
def insert(self,value,city):
newNode = Node(value,city)
if self.root is None:
self.root = newNode
else:
self._insert(1, newNode)
def _insert(self,depth, newNode):
if depth is 1:
for x in range(0,4):
if self.root.children[x] is None:
self.root.children[x] = newNode
return
elif self.root.children[3] is not None:
self._insert(2, newNode)
return
if depth is 2:
for x in range(0,4):
for y in range(0,3):
if self.root.children[x].children[y] is None:
self.root.children[x].children[y] = newNode
return
elif self.root.children[3].children[2] is not None:
self._insert(3, newNode)
return
if depth is 3:
for w in range(0,4):
for x in range(0,3):
for y in range(0,2):
if self.root.children[w].children[x].children[y] is None:
self.root.children[w].children[x].children[y] = newNode
return
elif self.root.children[3].children[2].children[1] is not None:
self._insert(4,newNode)
return
if depth is 4:
for w in range(0,4):
for x in range(0,3):
for y in range(0,2):
for z in range(0,1):
if self.root.children[w].children[x].children[y].children[z] is None:
self.root.children[w].children[x].children[y].children[z] = newNode
return
elif self.root.children[3].children[2].children[1].children[0] is not None:
self._insert(5,newNode)
return
if depth is 5:
for w in range(0,4):
for x in range(0,3):
for y in range(0,2):
for z in range(0,1):
for u in range(0,1):
if self.root.children[w].children[x].children[y].children[z].children[u] is None:
self.root.children[w].children[x].children[y].children[z].children[u] = newNode
return
elif self.root.children[3].children[2].children[1].children[0].children[0] is not None and w is 3 and x is 2 and y is 1 and z is 0 and u is 0:
print "The table is full"
def delete(self):
self.root = None
x = Tree()
x.insert(0, "Pretoria")
x.insert(60, "Johannesburg")
x.insert(1200, "Cape Town")
x.insert (600, "Durban")
x.insert(400, "Bloemfontein")
x.insert(1400, "Port Elizabeth")
My root and first level populate correctly but all the children nodes of the second, third, fourth and fifth level all populate exactly the same as the first level. When I checked their memory, they all populated the exact same memory space and I have no idea why. This happens when the following line of code runs:
x.insert(1400, "Port Elizabeth")
The tree for some reason is fully populated at this point despite only having 5 entries.
I tried using pointers at first but the same issue crops up.
Long story short, how would one go about inserting into an n-ary tree with decreasing n as you increase in depth?
This particular tree has the following attributes:
Root: 4 children per node (1 node with 4 children)
Level 1: 3 children per node (4 nodes with 3 children)
Level 2: 2 children per node (12 nodes with 2 children)
level 3: 1 child per node (24 nodes with 1 child)
level 4: 1 child per node (24 nodes with 1 child) (this is final destination in the TSP)

Python - Dijsktra's Algorithm Distance Problem

I've run into a problem with my code, i'm not able to calculate the distance to a node from the starting node. I have a text file of the form:
1,2,3,4,5,6,7,8,9
1,2,3,4,5,6,7,8,9
This represents the node distances in the graph. Here is my code, unfortunately, despite trying a few different methods I still keep coming up with various error messages.
infinity = 1000000
invalid_node = -1
startNode = 0
class Node:
distFromSource = infinity
previous = invalid_node
visited = False
def populateNodeTable():
nodeTable = []
index =0
f = open('route.txt', 'r')
for line in f:
node = map(int, line.split(','))
nodeTable.append(Node())
print nodeTable[index].previous
print nodeTable[index].distFromSource
index +=1
nodeTable[startNode].distFromSource = 0
return nodeTable
def tentativeDistance(currentNode, nodeTable):
nearestNeighbour = []
for currentNode in nodeTable:
# if Node[currentNode].distFromSource + currentDistance = startNode + currentNode
# currentDistance = currentNode.distFromSource + nodeTable.currentNode
currentNode.previous = currentNode
currentNode.length = currentDistance
currentNode.visited = True
currentNode +=1
nearestNeighbour.append(currentNode)
print nearestNeighbour
return nearestNeighbour
def shortestPath (nearestNeighbour)
shortestPath = []
f = open ('spf.txt', 'r')
f.close()
currentNode = startNode
if __name__ == "__main__":
populateNodeTable()
tentativeDistance(currentNode,populateNodeTable())
The lines starting with '#' in my tentativeDistance function is the section giving me trouble. I've looked at some other implementations on the web though they confuse me
I have been programming the Dijkstra's Algorithm in Python a few months ago; its tested and it should work:
def dijkstra(u,graph):
n = graph.numNodes
l = { u : 0 } ; W = graph.V()
F = [] ; k = {}
for i in range(0,n):
lv,v = min([ (l[lk],lk) for lk in l.keys() if lk in W ])
W.remove(v)
if v!=u: F.append(k[v])
for v1 in [ v2 for v2 in graph.G(v) if v2 in W ]:
if v1 not in l or l[v]+graph.w(v,v1) < l[v1]:
l[v1] = l[v] + graph.w(v,v1)
k[v1] = (v,v1)
return l,F
You need a class Graph with Method V() (which yields the graphs nodes), w(v1,v2) (which yields the weight of the edge (v1,v2)), remove (which removes an edge from a graph) and attribute numNodes (which yields the number of nodes in the graph) and G(v) which yields the neighborhood of node v.

Categories