Python Memory Error und lange Laufzeit bei Such-Algorithmus

MaToBe

Lt. Junior Grade
Registriert
Dez. 2008
Beiträge
459
Liebe CB'ler,

aktuell lerne ich mit einem Online-Kurs Python. Die erste Projekt-Aufgabe: das 8-Puzzle Game mit verschiedenen Tree-Search Algorithmen lösen. Bisher habe ich mich an Breath-First-Search und Deep-First-Search gewagt. Ersteres funktioniert auch sehr gut. Lösungen werden immer gefunden, die Laufzeit ist nur leicht länger der Referenzwert des Profs, ...

Bei Deep-Search (dfs) sieht es leider anders aus. Mein Alogrithmus such bis zu einer halben Minute lang und bricht dann mit "Memory Error" ab.

Mein Ansatz: das 8-Puzzle wird in ein numpy-array geladen. Das zu verschiebende Kästchen ist die 0. Bei jeder neuer Puzzlezusammensetzung wird die 0 gesucht, dann geschaut in welche Richtungen das Kästchen verschoben werden kann. Alle daraus resultierenden Puzzle werden in die FiFo-Schlange (bfs) oder den LiFo-Stack (dfs) gespeichert, WENN diese nicht schon in der Schlange/Stack sind oder nicht schon untersucht wurden.

Meiner Meinung nach ist diese Überprüfung "Ist Puzzle schon bekannt" genau das Problem. Bei dfs reden wir über eine 5-6 stellige Zahl von Puzzlen. Wie kann ich überprüfen, wo die Komplexität in meinem Code liegt?
Hat jemand eine Idee, wo ich verschlanken und verbessern kann? Würdet mir sehr helfen.

Code:
import sys
import numpy as np
import math
import copy
import time
from collections import deque


def move_up(board, zero):
    if zero[0, 0] != 0:
        move_board = copy.deepcopy(board)
        temp = move_board[zero[0, 0] - 1, zero[0, 1]]
        move_board[zero[0, 0] - 1, zero[0, 1]] = 0
        move_board[zero[0, 0], zero[0, 1]] = temp
        return move_board


def move_down(board, zero):
    if zero[0, 0] != math.sqrt(board_size) - 1:
        move_board = copy.deepcopy(board)
        temp = move_board[zero[0, 0] + 1, zero[0, 1]]
        move_board[zero[0, 0] + 1, zero[0, 1]] = 0
        move_board[zero[0, 0], zero[0, 1]] = temp
        return move_board


def move_left(board, zero):
    if zero[0, 1] != 0:
        move_board = copy.deepcopy(board)
        temp = move_board[zero[0, 0], zero[0, 1]-1]
        move_board[zero[0, 0], zero[0, 1]-1] = 0
        move_board[zero[0, 0], zero[0, 1]] = temp
        return move_board


def move_right(board, zero):
    if zero[0, 1] != math.sqrt(board_size) - 1:
        move_board = copy.deepcopy(board)
        temp = move_board[zero[0, 0], zero[0, 1]+1]
        move_board[zero[0, 0], zero[0, 1]+1] = 0
        move_board[zero[0, 0], zero[0, 1]] = temp
        return move_board


def new_node(board, path, depth, cost):
    return Node(board, path, depth, cost)


class Node:
    def __init__(self, board, path, depth, cost):
        self.board = board
        self.path = path
        self.depth = depth
        self.cost = cost


def bfs(start_board):
    # starting parameters
    bfs_queue = deque()
    explored_boards = set()
    max_queue = 0
    nodes_expanded = 0

    # start timer for search
    start_time = time.time()

    # put initial board in queue and in explored_boards
    explored_boards.add(tuple(map(tuple, initial_board)))
    bfs_queue.append(new_node(start_board, [], 0, 0))

    while len(bfs_queue) != 0:
        # save max length of fringe
        if max_queue < len(bfs_queue):
            max_queue = len(bfs_queue)

        # pull new board instance to test
        explore = bfs_queue.popleft()

        if np.array_equal(explore.board, goal_board):
            return explore, bfs_queue, start_time, nodes_expanded, max_queue
        else:
            print('Depth: ' + str(explore.depth) + ' | Nodes: ' + str(nodes_expanded) +
                  ' | Time: ' + str(time.time()-start_time))
            # search for child nodes
            nodes_expanded += 1
            zero = np.argwhere(explore.board == 0)

            up = move_up(explore.board, zero)
            if (up is not None) and (tuple(map(tuple, up)) not in explored_boards):
                explored_boards.add(tuple(map(tuple, up)))
                bfs_queue.append(new_node(up, explore.path + ['Up'], explore.depth + 1, 0))

            down = move_down(explore.board, zero)
            if (down is not None) and (tuple(map(tuple, down)) not in explored_boards):
                explored_boards.add(tuple(map(tuple, down)))
                bfs_queue.append(new_node(down, explore.path + ['Down'], explore.depth + 1, 0))

            left = move_left(explore.board, zero)
            if (left is not None) and (tuple(map(tuple, left)) not in explored_boards):
                explored_boards.add(tuple(map(tuple, left)))
                bfs_queue.append(new_node(left, explore.path + ['Left'], explore.depth + 1, 0))

            right = move_right(explore.board, zero)
            # Hier spare ich 1x Tuple anlegen, hat jedoch keinen Einfluss auf mein Problem
            tuple_right = tuple(map(tuple, right))
            if (right is not None) and (tuple_right not in explored_boards):
                explored_boards.add(tuple_right)
                bfs_queue.append(new_node(right, explore.path + ['Right'], explore.depth + 1, 0))


def dfs(start_board):
    # starting parameters
    dfs_stack = []
    explored_boards = set()
    max_stack = 0
    nodes_expanded = 0

    # start timer for search
    start_time = time.time()

    # put initial board in queue and in explored_boards
    explored_boards.add(tuple(map(tuple, initial_board)))
    dfs_stack.append(new_node(start_board, [], 0, 0))

    while len(dfs_stack) != 0:
        # save max length of fringe
        if max_stack < len(dfs_stack):
            max_stack = len(dfs_stack)

        # pull new board instance to test
        explore = dfs_stack.pop()
        explored_boards.add(tuple(map(tuple, explore.board)))

        if np.array_equal(explore.board, goal_board):
            return explore, dfs_stack, start_time, nodes_expanded, max_stack
        else:
            print('Depth: ' + str(explore.depth) + ' | Nodes: ' + str(nodes_expanded) +
                  ' | Time: ' + str(time.time()-start_time))
            # search for child nodes
            nodes_expanded += 1
            zero = np.argwhere(explore.board == 0)

            right = move_right(explore.board, zero)
            if (right is not None) and (tuple(map(tuple, right)) not in explored_boards):
                explored_boards.add(tuple(map(tuple, right)))
                board_right = new_node(right, explore.path + ['Right'], explore.depth + 1, 0)
                dfs_stack.append(board_right)

            left = move_left(explore.board, zero)
            if (left is not None) and (tuple(map(tuple, left)) not in explored_boards):
                explored_boards.add(tuple(map(tuple, left)))
                board_left = new_node(left, explore.path + ['Left'], explore.depth + 1, 0)
                dfs_stack.append(board_left)

            down = move_down(explore.board, zero)
            if (down is not None) and (tuple(map(tuple, down)) not in explored_boards):
                explored_boards.add(tuple(map(tuple, down)))
                board_down = new_node(down, explore.path + ['Down'], explore.depth + 1, 0)
                dfs_stack.append(board_down)

            up = move_up(explore.board, zero)
            if (up is not None) and (tuple(map(tuple, up)) not in explored_boards):
                explored_boards.add(tuple(map(tuple, up)))
                board_up = new_node(up, explore.path + ['Up'], explore.depth + 1, 0)
                dfs_stack.append(board_up)


# Catch input parameters
program_name = sys.argv[0]
sorting_alg = sys.argv[1]
board_input = sys.argv[2]

# create initial board matrix with integer values
board_list = board_input.split(',')
board_size = int(len(board_list))
initial_board = np.reshape(np.array(board_list), (-1, int(math.sqrt(board_size))))
initial_board = initial_board.astype(np.integer)

# create goal board based on input arguments for reference
goal_board = np.reshape(np.array(list(range(0, board_size))), (-1, int(math.sqrt(board_size))))
goal_board = goal_board.astype(np.integer)

# program executes based on wanted sorting algorithm
if sorting_alg == 'bfs':
    result = bfs(initial_board)
    print('path_to_goal: ' + str(result[0].path))
    print('cost_of_path: ' + str(len(result[0].path)))
    print('nodes_expanded: ' + str(result[3]))
    print('fringe_size: ' + str(len(result[1])))
    print('max_fringe_size: ' + str(result[4]))
    print('search_depth: ' + str(result[0].depth))
    print('max_search_depth: ')
    print('running_time: ' + str(time.time() - result[2]))
    print('max_ram_usage: ')
if sorting_alg == 'dfs':
    result = dfs(initial_board)
    print('path_to_goal: ' + str(result[0].path))
    print('cost_of_path: ' + str(len(result[0].path)))
    print('nodes_expanded: ' + str(result[3]))
    print('fringe_size: ' + str(len(result[1])))
    print('max_fringe_size: ' + str(result[4]))
    print('search_depth: ' + str(result[0].depth))
    print('max_search_depth: ')
    print('running_time: ' + str(time.time() - result[2]))
    print('max_ram_usage: ')
if sorting_alg == 'ast':
    print('Function not implemented yet!')
if sorting_alg == 'ida':
    print('Function not implemented yet!')
 
Hi, ich kann leider nichts zu deinem Problem beitragen.
Würde aber gerne wissen, was das für ein Online-Kurs ist? Die Algorithmen hören sich sehr interessant an und würde das mir gerne auch mal ansehen. Vielen Dank vorab schon mal
 
Bei Deep-Search (dfs) sieht es leider anders aus. Mein Alogrithmus such bis zu einer halben Minute lang und bricht dann mit "Memory Error" ab.

Ohne den Code gelesen zu haben: Das deutet darauf hin, dass die Abbruchbedingung bei deiner Tiefensuche nicht greift und das Programm in einer endlose Rekursion landet (die schließlich doch endet, eben weil kein Speicher mehr verfügbar ist). Darauf könntest du deine Fehlersuche konzentrieren.

EDIT: Ich sehe gerade, dein Algorithmus ist ja gar nicht rekursiv. Muss also was anderes sein >.<
 
Zuletzt bearbeitet:
@NullPointer: Das kann es in der Tat nicht sein. In den Referenzwerten des Profs wird angegeben, dass das Ergebnis bei ca. 180.000 "nodes" = Boards zu finden ist. Mein Algorithmus kommt gar nicht so weit, da vorher Memory Error. Irgendetwas in meinem Code muss extrem ressourcenlastig sein...

@fuji: Ich suche dir mal morgen die verschiedenen Links zu Angeboten raus.
 
Zuletzt bearbeitet:
MaToBe schrieb:
Wie kann ich überprüfen, wo die Komplexität in meinem Code liegt?

Ich habe mir den Code noch nicht genauer angeschaut, aber für diese Fragestellung benutzt man für gewöhnlich einen Profiler. Für Python gibt es u.a. cProfile, den du wie folgt aufrufen kannst:
Code:
python -m cProfile -s <ordering> <scriptfile> <parameters> …

scriptfile und parameters sind der Name deiner Programmdatei und deren Parameter (hier wohl sorting_algo und board_input)

Der lässt das Programm dann laufen und zeigt dir am Schluss eine Statistik in der z.B. drin steht wie oft die Funktionen aufgerufen wurden, wieviel Zeit in jeder Funktion verbraucht wurde usw.
Du kannst die Einträge ordnen lassen z.B. nach ncalls (Anzahl Funktionsaufrufe), tottime (Zeit in Funktion ohne Unterfunktionen) oder cumtime (Zeit in Funktion inkl. Unterfunktionen).

Edit1:
Noch ein Hinweis: Textausgabe ist relativ zeitaufwendig. Daher ist es sinnvoll alle Nicht-Fehlermeldungen optional abschaltbar zu machen.

Edit2:
Zeile 86 und 141 enthält einen Fehler, der Parameter soll sicherlich kein bool sein, oder?

Edit3:
Dein Speicherproblem kommt daher, dass du bei jedem Zug eine Kopie des Spielfeldes anlegst. Ich hatte bei einem Test nach 40s schon >160k Spielfelder im RAM (bei nur ~40k mögl. Permutationen). Dazu kamen zu dem Zeitpunkt auch schon mehr als 70k Nodes.

Edit4:
Wenn ich das richtig sehe, kannst du beliebig viele Nodes bekommen, da du nicht überprüfst, ob es nicht schon ein Node mit der identischen Board-Konfiguration gibt. Irgendwann würde das Programm zwar terminieren, weil durch explored_board die Zahl der möglichen Board-Konfigurationen abnimmt, aber solange eine Konfiguration noch nicht untersucht wurde, kann diese beliebig oft kopiert und in Nodes auf dem Stack/in der Queue landen.
 
Zuletzt bearbeitet:
Ich hab das Programm bei mir ausgeführt. Es lief durch - entweder ich hab eine leichtere Startposition erwischt, oder einfach mehr RAM als du (laut 'time -l' brauchte es 2.67 Gigabyte). Hier ist die Ausgabe: http://pastebin.com/PtSjiBkZ

Zum Vergleich: Die Breitensuche fand eine Lösung der Länge 17. Der Tiefensuchalgorithmus scheint sehr ineffizient für die Lösung dieser Art Puzzle zu sein.
 
@NullPointer: Der Referenzwert des Profs ist für:
BFS
$ python driver.py bfs 1,2,5,3,4,0,6,7,8
path_to_goal: ['Up', 'Left', 'Left']
cost_of_path: 3
nodes_expanded: 10
fringe_size: 11
max_fringe_size: 12
search_depth: 3
max_search_depth: 4
running_time: 0.00188088
max_ram_usage: 0.07812500

und DFS
$ python driver.py bfs 1,2,5,3,4,0,6,7,8
path_to_goal: ['Up', 'Left', 'Left']
cost_of_path: 3
nodes_expanded: 181437
fringe_size: 2
max_fringe_size: 42913
search_depth: 3
max_search_depth: 66125
running_time: 5.01608433
max_ram_usage: 4.23940217

Die Tiefensuche ist in der Tat für diese Art Puzzle sehr schlecht, das soll die Übung ja unter anderem vermitteln. Aber die Werte von mir (und auch von dir bestätigt) bei meinem Code sind ja Welten davon entfernt...

@Limit:
Zu 1.: Habe die Textausgabe mal deaktiviert, bringt aber keinen spürbaren Vorteil.
Zu 2.: Genau das sagt mir mein IDE Programm auch, aber der Code funktioniert! Und ich verstehe auch nicht genau, wie ich es "richtiger" schreiben soll.
Zu 3.: Das ist genau das Problem. Nur wie bekommt man diese große Menge an Spielzügen besser abgebildet? Wie weiter oben sichtbar braucht die Lösung bei DFS 181.437 Spielfelder. Bei mir ist leider schon bei etwa 50.000 Schluss.
zu 4.: Das prüfe ich genau hier:
Code:
if (right is not None) and (tuple(map(tuple, right)) not in explored_boards):
                explored_boards.add(tuple(map(tuple, right)))
                board_right = new_node(right, explore.path + ['Right'], explore.depth + 1, 0)
                dfs_stack.append(board_right)
Nur wenn der Spielzug erlaubt (right is not None) und der Spielzug noch nicht in explored_boards ist, wird der Spielzug im Stack abgelegt.
Deinem Hinweis zum Performance-Tracking gehe ich morgen mal nach. Danke.
 
Dein Programm findet auf meinem Rechner genau die gleiche Lösung wie vom Prof als Referenz vorgegeben - allerdings braucht es dafür ca. 10 Gigabyte Speicher :)
 
Habe 16 GB RAM. Dann frage ich mich, warum bei mir der Abbruch mit Memory Error erfolgt ... Bist du auf Linux oder Windows unterwegs?
 
Mac OS, auch mit 16 GB.
 
@MaToBe 32-Bit-Version von Python statt der 64-Bit-Version installiert?

Abgesehen davon behebt das natürlich nicht das Problem in deinem Code, auch wenn er dann durchläuft. Gibt es keine Referenzlösung?

Edit: Bei mir läuft es unter macOS mit Python 3.6 (64 Bit) ebenfalls mit knapp einem Verbrauch von rund 10 GB RAM durch (mit den Settings des Profs).

Zur Lösung deines Problems: Vllt. einfach die Werte, die du bei move_up usw. in das geänderte Board schreiben würdest in ein Tupel hauen, das dann in den Stack stecken und bei jeder Iteration dein Board mit diesen Werten verändern?
 
Zuletzt bearbeitet:
Was ich oben über das mögl. Problem geschrieben habe war Unsinn. Gestern Abend war es wohl zu spät für mich um klar zu denken ;)
Jedenfalls habe ich mal meine eigene Version implementiert und mir ist dabei ein Problem aufgefallen, dass vermutlich auch bei dir auftaucht und das sogar in verschärfter Form.

Du speicherst in jedem Knoten den Pfad. Wenn er lange suchen muss bevor er die Lösung findet, kann der Pfad sehr lang werden. Bei dem Prof-Beispiel und Tiefensuche müsste die erste Lösung eine Pfadlänge von 105318 haben, d.h. du hast hochgerechnet auf alle Nodes im Worst-Case etwa (105318 * 105319) / 2 Einträge, was bei durchschnittlich 3,75 Byte / Eintrag ungeführ 19GB zur Speicherung der Pfade ergibt.
Als einfachste Lösung könntest du statt up, down, left, right einfach mal nur u, d, l, r benutzen. Das alleine sollte die benötigte Speichermenge deutlich reduzieren. Noch weniger Speicher brauchst du, wenn du es nicht also Liste von Strings speicherst, sondern einfach als eine einzige große Integer-Zahl.
 
@fujitsustaR*: Mache aktuell den MicroMaster "Artificial Intelligence" von edX. Da hier unter anderem als 1ste Projektarbeit direkt diese Aufgabe gestellt wurde, mache ich nebenher noch "Applied Data Science with Python" von coursera, um mehr Python Wissen aufzubauen.

@all: Danke für die Hinweise. Habe nun herausgefunden, dass der wahre Übeltäter das Speichern von "path_to_goal" ist. Hier speichere ich für bis zu 180k Knoten jeweils den kompletten Weg zu diesem Knoten als Liste von Strings. Nehme ich diese Listenspeicherungen testweise raus, löst mein Programm das Puzzel richtig. Im Vergleich zum "perfekten" Ergebnis des Profs brauche ich 26 Sekunden (vs 5 Sekunden).

Nun natürlich die Frage: Alternativ könnte ich ja jedem Knoten sein Elternknoten als Parameter mitgeben. So würde dann auch die ganze Baumstruktur abgebildet werden. Nur: Wie bekomme ich dann den Pfad heraus? Habe schon einige Iterationsschleifen probiert, aber keine hat geklappt.

@Limit: Ja, genau das ist mir auch klar geworden. Den unschönen Weg über direktes Speichern im jeweiligen Knoten bin ich anfangs gegangen, weil ich es nicht hinbekommen habe, den Baum zu durchsuchen und dann bei gefundenem Ergebnis wieder "rückwärts hochzuwandern". Das wäre bestimmt die eleganteste, richtige Lösung. Jemand einen Idee für einen Pseudo-Algorithmus, wie so etwas aussehen könnte?
 
Zuletzt bearbeitet:
Ich habe der Einfachheit halber den Pfad auch in jedem Knoten gespeichert, allerdings nicht als Liste von Strings, sondern als eine einzelne Integer-Zahl. Damit komme ich bei der Tiefensuche auf eine Laufzeit von 3,5s. Was ich noch anders gemacht habe war das Speichern des Boards, dass bei mir nur ein einfaches Tuple ist. Das spart Speicher und führt zu weniger Cache-Misses, ist aber ansonsten nicht so wichtig.

Zum speichern des Pfades als Integer:
Code:
…
dfs_stack.append(new_node(start_board, [], 0, 0))
…
board_right = new_node(right, explore.path + ['Right'], explore.depth + 1, 0)
…
board_left = new_node(left, explore.path + ['Left'], explore.depth + 1, 0)
…
Das und natürlich auch board_up/down könntest du ersetzen durch:
Code:
…
dfs_stack.append(new_node(start_board, 0, 0, 0))
…
board_right = new_node(right, explore_path * 10 + 1, explore.depth + 1, 0)
…
board_left = new_node(left, explore_path * 10 + 2, explore.depth + 1, 0)
…

Dabei stünde dann im Pfad eine 1 für rechts, 2 für links, 3 für hoch, 4 für runter.
Anstatt ["up", "left", "left"] wäre der Pfad dann einfach die Zahl 322. Diese kannst du dann für die Lösung mit Division und Modulo aufspiltten um eine Textausgabe zu bekommen.

Wie sich die Version mit dem Elternknoten im Vergleich dazu schlägt bin ich mir nicht ganz sicher, denn um den Pfad zurückverfolgen zu können, musst du alle Knoten bis zum Ende speichern. Bei deiner Version kannst du sie (die Knoten ;)) nach dem "Poppen" und Erzeugen neuer Knoten wegschmeißen, weil alle Folgeknoten den Pfad schon dabei haben.
 
Zuletzt bearbeitet:
Du brauchst ja nur einen einzigen Pfad - den, bei dem du am Ende ankommst, wenn np.array_equal(explore.board, goal_board) zutrifft. Würde es demnach nicht ausreichen, den aktuell besuchten Pfad in einer globalen Variable abzulegen statt jedem Knoten seinen eigenen Pfad mitzugeben? Jedes Mal, wenn du einen Schritt in die Tiefe gehst, append()est du den passenden String an den Pfad, und wenn du einen Schritt wieder raufgehst, pop()st du den letzten String wieder runter.

EDIT: Nee, so, wie dein Algorithmus aufgebaut ist, funktioniert das nicht...
 
Zuletzt bearbeitet:
Limit schrieb:
Damit komme ich bei der Tiefensuche auf eine Laufzeit von 3,5s. Was ich noch anders gemacht habe war das Speichern des Boards, dass bei mir nur ein einfaches Tuple ist. Das spart Speicher und führt zu weniger Cache-Misses, ist aber ansonsten nicht so wichtig.

Code:
…
dfs_stack.append(new_node(start_board, 0, 0, 0))
…
board_right = new_node(right, explore_path * 10 + 1, explore.depth + 1, 0)
…
board_left = new_node(left, explore_path * 10 + 2, explore.depth + 1, 0)
…
Wie sich die Version mit dem Elternknoten im Vergleich dazu schlägt bin ich mir nicht ganz sicher, denn um den Pfad zurückverfolgen zu können, musst du alle Knoten bis zum Ende speichern. Bei deiner Version kannst du sie (die Knoten ;)) nach dem "Poppen" und Erzeugen neuer Knoten wegschmeißen, weil alle Folgeknoten den Pfad schon dabei haben.
Danke für die Anmerkungen. Wenn du das Board als Tupel - nicht mehr als numpy-array - gespeichert hast, wie überprüfst du dann, welcher der möglichen Züge erlaubt ist?
Warum explore_path * 10? Einfach + 1 / 2 / 3 / 4 geht nicht?

Ich habe die Version mit "Elternknoten" umgesetzt. Komme damit auf das richtige Ergebnis mit einer Laufzeit von 11 Sekunden (vs 5 Sekunden vom Prof). Du kommst auf 3,5 Sekunden? Demnach sogar besser als "die Lösung"?! Falls dem so sei, muss ich mir wirklich überlege, deinen Ansatz noch mal anzuwenden. Allerdings glaube ich, dass der Prof eher den "Baum Ansatz" sehen möchte....

Meine Lösung:
Bereitstellung des Lösungsweges:
Code:
def find_path(goal_node):
    path_to_goal = [goal_node.move]
    parent_node = goal_node.parent
    while parent_node.parent is not None:
        path_to_goal.insert(0, parent_node.move)
        parent_node = parent_node.parent
    return path_to_goal
Klasse der Knoten:
Code:
def new_node(board, parent, move, depth, cost):
    return Node(board, parent, move, depth, cost)


class Node:
    def __init__(self, board, parent, move, depth, cost):
        self.board = board
        self.parent = parent
        self.move = move
        self.depth = depth
        self.cost = cost
Auszug DFS Suche
Code:
    while len(dfs_stack) != 0:
        # save max length of fringe
        if max_stack < len(dfs_stack):
            max_stack = len(dfs_stack)

        # pull new board instance to test
        explore = dfs_stack.pop()
        explored_boards.add(tuple(map(tuple, explore.board)))

        if np.array_equal(explore.board, goal_board):
            goal_to_path = find_path(explore)
            return explore, dfs_stack, start_time, nodes_expanded, max_stack, goal_to_path
        else:
            # print('Depth: ' + str(explore.depth) + ' | Nodes: ' + str(nodes_expanded) +
            #      ' | Time: ' + str(time.time()-start_time))
            # search for child nodes
            nodes_expanded += 1
            zero = np.argwhere(explore.board == 0)

            right = move_right(explore.board, zero)
            if (right is not None) and (tuple(map(tuple, right)) not in explored_boards):
                explored_boards.add(tuple(map(tuple, right)))
                board_right = new_node(right, explore, 'Right', explore.depth + 1, 0)
                dfs_stack.append(board_right)

Danke schon Mal für eure Hilfe! Jetzt ist das Problem gelöst, alles weitere ist Optimierung!
 
MaToBe schrieb:
Danke für die Anmerkungen. Wenn du das Board als Tupel - nicht mehr als numpy-array - gespeichert hast, wie überprüfst du dann, welcher der möglichen Züge erlaubt ist?

Im Prinzip genauso wie du. Ich habe halt noch eine kleine Hilfsfunktion, die mir die Nullstelle aus dem Tupel berechnet (configuration = board, Position ist einfach ein nur ein named_tuple):
Code:
def find_zero_position(configuration):
    index = configuration.index(0)
    return Position(int(index // N), int(index % N))

MaToBe schrieb:
Warum explore_path * 10? Einfach + 1 / 2 / 3 / 4 geht nicht?
Wenn du nur +1/+2/… nimmst, kannst du aus dem Ergebnis den Pfad nicht rekonstruieren, da er nicht eindeutig ist:
Bei meiner Methode würde aus up, left, left -> 322, bei der einfache Addition würde daraus 7. Diese 7 könnte aber sowohl 7x right oder 1x right und 1x down oder 2x left und 1x up oder … bedeuten.
Wenn du Chars statt Integers nimmst, kannst du einfach + "1" / + "2" / … benutzen. Der Speicherverbrauch wäre identisch, allerdings die Laufzeit schlechter, weil er bei jedem Anhängen einer Ziffer neuen Speicher allokieren und dann den kompletten String kopieren muss. Bei der Integer-Variante reicht es im best-case nur ein Bit zu ändern.
Man könnte die Optimierung sogar noch weiter treiben und mit den Bit-Operatoren (<<, >>, &, |, ~, ^) die Ziffern jeweils in 4Bits packen und so den Speicherverbrauch nochmals halbieren.

MaToBe schrieb:
Ich habe die Version mit "Elternknoten" umgesetzt. Komme damit auf das richtige Ergebnis mit einer Laufzeit von 11 Sekunden (vs 5 Sekunden vom Prof). Du kommst auf 3,5 Sekunden? Demnach sogar besser als "die Lösung"?! Falls dem so sei, muss ich mir wirklich überlege, deinen Ansatz noch mal anzuwenden. Allerdings glaube ich, dass der Prof eher den "Baum Ansatz" sehen möchte....
Hast du noch deine Meldungen bei jedem Schleifendurchlauf an? Bei mir dauert es ~3,5s ohne Meldungen und 9,5s mit.
 
Zuletzt bearbeitet:
nein, die Meldung ist aus. Sind also 11 Sekunden für meine gegen 3,5 für deine. Danke für das Sparing, hat sehr geholfen!

Werde deine Lösung mal testweise nachstellen, aber wahrscheinlich trotzdem meine einreichen. In der Aufgabenbeschreibung steht sowas wie "as seen in the lecture" bei den zu implementierenden Algorithmen. Da kommt das Benutzen der Elternnote näher an die Vorlesung ran.
 
Falls du dir meinen Code mal anschauen willst, stelle ich ihn mal hier rein.

Code:
import logging
import math

from argparse import ArgumentParser
from collections import deque, namedtuple

Node = namedtuple("Node", "configuration path path_length cost")
Position = namedtuple("Position", "row column")


def main(search_algorithm, start_configuration):
    logger.info("search algorithm = '%s', size=%d×%d, start configuration = %s" % (
        search_algorithm, N, N, start_configuration))

    nodes = deque()
    pop = nodes.pop
    add = nodes.append if search_algorithm == "dfs" else nodes.appendleft

    known_configurations = set()

    known_configurations.add(tuple(start_configuration))
    add(Node(start_configuration, 0, 0, 0))

    while nodes:
        node = pop()
        row, column = find_zero_position(node.configuration)
        logger.info("#nodes=%d, #known_configurations=%d, fetched: %s, zero_position=%d,%d" % (
            len(nodes), len(known_configurations), node.configuration, row, column))

        if node.configuration == tuple(sorted(node.configuration)):
            logger.info("result: %s, path='%s'" % (str(node), node.path))
            break

        if row > 0:
            new_configuration = list(node.configuration)
            new_configuration[index(row, column)] = new_configuration[index(row - 1, column)]
            new_configuration[index(row - 1, column)] = 0
            new_configuration = tuple(new_configuration)
            if new_configuration not in known_configurations:
                known_configurations.add(new_configuration)
                add(Node(new_configuration, node.path * 10 + 1, node.path_length + 1, 0))
                logger.debug("added…")
            else:
                logger.debug("skipped…")
                del new_configuration

        if column < N - 1:
            new_configuration = list(node.configuration)
            new_configuration[index(row, column)] = new_configuration[index(row, column + 1)]
            new_configuration[index(row, column + 1)] = 0
            new_configuration = tuple(new_configuration)
            if new_configuration not in known_configurations:
                known_configurations.add(new_configuration)
                add(Node(new_configuration, node.path * 10 + 2, node.path_length + 1, 0))
                logger.debug("added…")
            else:
                del new_configuration
                logger.debug("skipped…")

        if row < N - 1:
            new_configuration = list(node.configuration)
            new_configuration[index(row, column)] = new_configuration[index(row + 1, column)]
            new_configuration[index(row + 1, column)] = 0
            new_configuration = tuple(new_configuration)
            if new_configuration not in known_configurations:
                known_configurations.add(new_configuration)
                add(Node(new_configuration, node.path * 10 + 3, node.path_length + 1, 0))
                logger.debug("added…")
            else:
                del new_configuration
                logger.debug("skipped…")

        if column > 0:
            new_configuration = list(node.configuration)
            new_configuration[index(row, column)] = new_configuration[index(row, column - 1)]
            new_configuration[index(row, column - 1)] = 0
            new_configuration = tuple(new_configuration)
            if new_configuration not in known_configurations:
                known_configurations.add(new_configuration)
                add(Node(new_configuration, node.path * 10 + 4, node.path_length + 1, 0))
                logger.debug("added…")
            else:
                del new_configuration
                logger.debug("skipped…")


def find_zero_position(configuration):
    index = configuration.index(0)
    return Position(int(index // N), int(index % N))


def index(row, column):
    return row * N + column


def path_to_string(path):
    temp = {1: "up", 2: "right", 3: "down", 4: "left"}
    masks = (10 ** j for j in range(int(math.log(path, 10)), -1, -1))
    return ", ".join((temp[(path // i) % 10] for i in masks))


parser = ArgumentParser()
parser.add_argument("-a", "--algo", dest="algo", choices=["bfs", "dfs"], default="bfs")
parser.add_argument("-c", "--start-config", dest="start_config", nargs="+", type=int, default=[1, 2, 0, 3])
parser.add_argument("-v", "--verbose", dest="verbose", action="count", default=0)
args = parser.parse_args()

log_levels = {
    0: logging.ERROR, 1: logging.WARNING,
    2: logging.INFO, 3: logging.DEBUG
}

logger = logging.getLogger("puzzle")
handler = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s :: %(name)s :: %(levelname)s :: %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)

logger.setLevel(log_levels[args.verbose])

N = int(math.sqrt(len(args.start_config)))
main(args.algo, args.start_config)
 

Ähnliche Themen

Antworten
5
Aufrufe
1.199
  • Geschlossen
2
Antworten
31
Aufrufe
12.010
Zurück
Oben