Поиск с подкреплением на ориентированных взвешенных графах

Мир абстракции выхолощено чист. Реальность и не полна, и изменяема.

В студенческие 90-е годы пришлось переходить границу в районе г. Печеры Псковской обл. Был ноябрь. Спустился ранним утром с холма. Перед речкой болотистая местность. Приходилось местами раздеваться и, держа одежду в одной руке, или переходить по пояс, или переплывать. Слышал по лаю собак, как наши погранцы пошли по следу. Но ни собакам, ни служивым в воду лезть уже не хотелось. Потом вечер. Темно. Ушел по полярной звезде к жд на северо-запад. Назад через 2 месяца почти также.

Хорошо, когда есть чёткие ориентиры. Искать маршруты, не имея всей информации — типичная и востребованная задача. На ориентированных взвешенных графах с ней лучше всего справляется алгоритм Дейкстры. Но этот алгоритм обхода «волной» не совсем похож на то, как движется человек.

Задача статьи показать как можно скрестить поиск с подкреплением и взвешенные ориентированные графы.

При такой постановке успешность будет зависеть от величины награды (величины ориентира) и дисконта ребер графа. Если величина награды больше суммы стоимости ребер по маршруту, то никаких проблем ориентирования не возникает. Это похоже на то, как вначале производится поиск и, если цель найдена, на этом месте ставим гору Алтая Белуху. По сути, получаем на выходе топографическую карту местности. Градиент которой всегда показывает направление к цели.

Высота над уровнем моря: 4 506 м

Высота над уровнем моря: 4 506 м

В противном случае могут возникнуть отрицательные значения по q. И, как следствие, циклы без достижимости к цели, если следовать правилу: из следующих вершин выбирать с наибольшим значением.

Возникает вопрос: если цель все же найдена, но нет возможности построить такую карту, то можно ли все же проложить хоть какой-то маршрут, пусть и неоптимальный? Конечно, есть. Удалим в таком еще loop-ы наподобие того, например, как здесь: [1, 2, 3, 5, 2, 4, 9] → [1, 2, 4, 9]

import random

zero = 0 
roundLim = 3 #величина округления числа
visited = {} #для отслеживания количества посещений состояний (вершин)
cGraph = {} #пошагово переносятся состояния и переходы графа G
qGraph = {} #пошагово обновляются q: currentSate: {nextState:q})
routes = {} #хранит маршруты, найденные в процессе поиска targetState
limit = 100 #глубина поиска в графе
epoch = 100 #количество попыток поиска цели
targetCost = 4506 #величина награды (reward)
#г. Белуха - высота над уровнем моря: 4 506 м
targetState = 19 #целевое состояние (для сгенерированного графа ниже)

k = len(str(targetCost))
gamma = int('9'*k)/int('1'+'0'*k)#коэффициент дисконтирования

def set_q(currentState, nextStates):
    for nextState in nextStates:
        cMax = zero
        if nextState in qGraph:
            nnextStates = [nnextState for nnextState in qGraph[nextState]]
            for nnextState in nnextStates:
                if gamma*qGraph[nextState][nnextState] > cMax:
                    cMax = gamma*qGraph[nextState][nnextState]
            if cMax - cGraph[currentState][nextState] > zero:
                qGraph[currentState][nextState] = \
                  round(cMax - gamma*cGraph[currentState][nextState], \
                  roundLim)
            else: qGraph[currentState][nextState] = zero
        if nextState == targetState:
            qGraph[currentState][nextState] = targetCost - \
              cGraph[currentState][nextState]
            return
        
def get_path(currentState):
    lim = limit
    path = []
    while lim > 0 and currentState != None:
        path += [currentState];
        visited[currentState] +=1
        lim -=1
        if currentState == targetState:
            return path  
        nextStateQ = best_q(currentState)     
        if len(path) ==1:
            currentState = best_q(currentState)
            continue
        currentState = nextStateQ
    return []
 
def get_first_route(startState):
    for state in routes:
        if targetState in routes[state] and startState in routes[state]:
            startStateInd = routes[state].index(startState)
            return routes[state][startStateInd:]
            
def best_q(currentState):
    try:
        toSorted = [(nextState, qGraph[currentState][nextState]) \
          for nextState in qGraph[currentState]]
        nextState = sorted(toSorted, key = lambda t: t[1], \
          reverse=True)[0][0]
        return  nextState
    except: 
        return 
    
def next_states(currentState):
    if currentState not in cGraph:
        cGraph[currentState] = G[currentState]
        qGraph[currentState] = {}
        for nextState in cGraph[currentState]:
            qGraph[currentState][nextState] = zero
    if currentState not in visited:
        visited[currentState] = 1
    else: 
        visited[currentState] += 1
    nextStates = [nextState for nextState in G[currentState]]
    for nextState in nextStates:
       if nextState not in visited:
           visited[nextState] = zero
    set_q(currentState, nextStates)
    return nextStates  
    
def less_visited(currentState, nextStates):
    if not nextStates: return
    toSorted = [(nextState, visited[nextState]) \
      for nextState in nextStates]
    sortedStates = sorted(toSorted, key = lambda t: t[1])
    minVisited = [nextState for nextState in sortedStates \
      if sortedStates[0][1] == nextState[1]]
    nextState = random.choice(minVisited)[0]
    visited[nextState] +=1
    return nextState
    
def search_target(currentState, route):
    lim = limit
    route.append(currentState)
    nextStates = next_states(currentState)
    while lim > 0:
        if not nextStates: 
            return []
        lim -=1
        if currentState == targetState:
            return route 
        currentState = less_visited(currentState, nextStates)
        route.append(currentState)
        nextStates = next_states(currentState)    
    return []
    
def delete_loop(route):
    sRoute = []
    i = 0
    while i < len(route)-1:
        if route[i] in route[i+1:]:
            i = len(route) - route[::-1].index(route[i]) -1 
        sRoute.append(route[i])
        i +=1
    if route[-1] not in sRoute: 
        sRoute.append(route[-1])
    return sRoute

#сгенерирован с помощью pyrgg.    
G = {
  0 : {6: 7, 14: 7, 0: 16},
  16 : {3: 9, 17: 1},
  6 : {18: 0, 11: 7, 17: 3},
  14 : {18: 9, 17: 3, 8: 4},
  1 : {17: 5, 2: 2, 12: 2, 3: 1, 5: 6, 18: 4, 1: 12},
  12 : {7: 2, 19: 6, 3: 8, 2: 2},
  18 : {10: 8, 2: 6},
  5 : {16: 9, 2: 6, 13: 2, 9: 2, 19: 9, 15: 2, 10: 4},
  3 : {15: 5, 19: 2, 8: 0, 10: 6},
  17 : {},
  2 : {11: 5},
  11 : {0: 0, 13: 0, 17: 9},
  10 : {13: 3, 12: 7, 2: 6},
  8 : {9: 0, 15: 6, 14: 9},
  19 : {15: 6, 0: 6},
  15 : {16: 6},
  4 : {8: 2, 18: 4, 11: 9, 16: 0, 10: 8, 15: 5, 14: 0, 9: 6, 5: 9, 4: 14},
  9 : {13: 8, 18: 7, 12: 8, 17: 0, 2: 4, 16: 0},
  13 : {18: 7},
  7 : {19: 6, 9: 7, 7: 12},
} 
    
def main():
    gStates = [state for state in G]    
    route = []
    startState = 0
    for i in range(1, epoch+1):
        initState = random.choice(gStates)
        #initState = startState
        rRoute = search_target(initState, route.copy())
        if rRoute:
            routes[rRoute[0]] = delete_loop(rRoute)     
    currentState = startState 
    path = get_path(currentState)
    #print(rRoute, "\n\n", visited, "\n\n", cGraph, "\n\n", qGraph)  
    if path:
        print("\npath =", path)
    else:
        path = get_first_route(startState)
        print("get_first_route =", path)
    
main()

Наиболее частотный найденный путь: path = [0, 14, 8, 9, 12, 3, 19]

Изменим targetCost. Приравняем пяти, например. В этом случае придется извлекать из памяти «то, что есть». Маршрут может быть не найден вообще. Или найден хоть какой-то, например, такой: path = [0, 14, 8, 15, 16, 3, 19]

иллюстрации

иллюстрации

Для сравнения … из мира истины.

import math

graph = {
0 : {6: 7, 14: 7, 0: 16} ,
16 : {3: 9, 17: 1} ,
6 : {18: 0, 11: 7, 17: 3} ,
14 : {18: 9, 17: 3, 8: 4} ,
1 : {17: 5, 2: 2, 12: 2, 3: 1, 5: 6, 18: 4, 1: 12} ,
12 : {7: 2, 19: 6, 3: 8, 2: 2} ,
18 : {10: 8, 2: 6} ,
5 : {16: 9, 2: 6, 13: 2, 9: 2, 19: 9, 15: 2, 10: 4} ,
3 : {15: 5, 19: 2, 8: 0, 10: 6} ,
17 : {} ,
2 : {11: 5} ,
11 : {0: 0, 13: 0, 17: 9} ,
10 : {13: 3, 12: 7, 2: 6} ,
8 : {9: 0, 15: 6, 14: 9} ,
19 : {15: 6, 0: 6} ,
15 : {16: 6} ,
4 : {8: 2, 18: 4, 11: 9, 16: 0, 10: 8, 15: 5, 14: 0, 9: 6, 5: 9, 4: 14} ,
9 : {13: 8, 18: 7, 12: 8, 17: 0, 2: 4, 16: 0} ,
13 : {18: 7} ,
7 : {19: 6, 9: 7, 7: 12} ,
}

source = 0
destination = 19

unvisited = graph
shortest_distances = {}
route = []
path_nodes = {}

for nodes in unvisited:
    shortest_distances[nodes] = math.inf
shortest_distances[source] = 0

while unvisited:
    min_node = None
    for current_node in unvisited:
        if min_node is None:
            min_node = current_node
        elif shortest_distances[min_node] \
            > shortest_distances[current_node]:
            min_node = current_node
    for (node, value) in unvisited[min_node].items():
        if value + shortest_distances[min_node] \
            < shortest_distances[node]:
            shortest_distances[node] = value \
                + shortest_distances[min_node]
            path_nodes[node] = min_node
    unvisited.pop(min_node)
node = destination

while node != source:
    try:
        route.insert(0, node)
        node = path_nodes[node]
    except Exception:
        print('Path not reachable')
        break
route.insert(0, source)

print(route) #[0, 14, 8, 9, 16, 3, 19]
print("\n", shortest_distances)

PS Удачи.

© Habrahabr.ru