Как исправить дилемму клеточных автоматов / пространственных заключенных, которая не воспроизводится должным образом - PullRequest
3 голосов
/ 14 мая 2019

Я пытаюсь воспроизвести результаты статьи (если вам это интересно, ее «Новак и май 1992 года: эволюционные игры и пространственный хаос»), которая создает набор фракталов, выполняя дилемму заключенных на сетке nxn (например, , https://www.researchgate.net/figure/Spatial-version-of-the-Prisoners-Dilemma-for-symmetrical-initial-conditions-Nowak_fig3_277476479), но мои результаты не такие, какими они должны быть. Идея состоит в том, что сетка полностью заполняется кооператорами, за исключением одного объекта Defector, который находится в центре сетки. Разные взаимодействия приводят к разным выплатам: взаимные перебежчики дают выплату 0, взаимные кооператоры выплачивают 1 каждый, а дефектор против кооператора выдает выплату b для перебежчика и 0 для кооператора, где b> 1. Все объекты в сетки играют друг против друга и получают оценку в соответствии с приведенной выше структурой выплат. После каждого поколения каждый объект в узле заменяется соседом с наивысшей оценкой. Поскольку стратегия дефекторов является превосходящей стратегией, она должна вторгаться в совокупность Кооператоров и создавать указанные фрактальные изображения, как это делают клеточные автоматы.

Основной способ, которым я пытался это сделать (также основная область, с которой у меня были проблемы), - это использовать функцию replace_pop, показанную ниже. После каждого раунда программа проходит по сетке и заменяет любой объект на узле соседним объектом, который имеет более высокий балл. Я думал, что этого было бы достаточно, но, как можно заметить, даже через несколько поколений, существует некоторая форма репликации, но не совсем так, как это должно происходить, что затрудняет точное определение того, что именно идет не так. При N = 1 (N - число поколений) результат кажется правильным, так как соседние (соседние слева, справа, сверху и снизу) кооператоры становятся дефектами, но по мере увеличения N изображение просто сбивается с пути.

Я также повторно инициализировал счет каждого объекта до 0 после каждого поколения, чтобы обеспечить правильную репликацию. Однако, если этого не сделать, популяция развивается таким же образом, как в случае N = 1, описанном выше, но для всех последующих поколений, что является своеобразным, потому что должны быть перебежчики, которые имеют более высокие оценки, чем окружающие кооператоры. Я не уверен, где я иду не так? Мой код ниже (извините за включение всего этого, но я не знаю, где именно проблема). Я довольно новичок в Python и Stack, поэтому любая помощь будет оценена.

import random
import matplotlib.pyplot as plt

row = 99
col = 99

class Cooperator:
    def __init__(self):
        self.score = 0
        self.id = 'C'

class Defector:
    def __init__(self):
        self.score = 0
        self.id = 'D'

class Grid:
    def __init__(self, rowsize, colsize):
        self.rowsize = rowsize
        self.colsize = colsize

    def make_grid(self):
        n = self.rowsize
        m = self.colsize
        arr = [[0 for j in range(m)] for i in range(n)]
        return arr

    def populate_grid(self):
        empty_grid = self.make_grid()
        for i in range(self.rowsize):
            for j in range(self.colsize):
                empty_grid[i][j] = Cooperator()
        empty_grid[i//2][j//2] = Defector()
        return empty_grid

    def shuffle_population(self):
        populated_grid = self.populate_grid()
        for i in range(self.rowsize):
            random.shuffle(populated_grid[i])
        return populated_grid

def von_neumann_neighbourhood(array, row, col, wrapped=True):
    """gets von neumann neighbours for a specfic point on grid with or without wrapping"""
    neighbours = []
    #conditions for in bound points
    if row + 1 <= len(array) - 1:
        neighbours.append(array[row + 1][col])

    if row - 1 >= 0:
        neighbours.append(array[row - 1][col])

    if col + 1 <= len(array[0]) - 1:
        neighbours.append(array[row][col + 1])

    if col - 1 >= 0:    
        neighbours.append(array[row][col - 1])
    #if wrapped is on, conditions for out of bound points
    if row - 1 < 0 and wrapped == True:
        neighbours.append(array[-1][col])

    if col - 1 < 0 and wrapped == True:
        neighbours.append(array[row][-1])

    if row + 1 > len(array) - 1 and wrapped == True:
        neighbours.append(array[0][col])

    if col + 1 > len(array[0]) - 1 and wrapped == True:
        neighbours.append(array[row][0])

    return neighbours

def play_round(array, row, col):
    b = 1.70
    player = array[row][col]
    neighbours = von_neumann_neighbourhood(array, row, col)
    for neighbour in neighbours:
        if player.id == 'C' and neighbour.id == 'C':
            player.score += 1
            neighbour.score += 1
        if player.id == 'D' and neighbour.id == 'D':
            player.score += 0
            neighbour.score += 0
        if player.id == 'D' and neighbour.id == 'C':
            player.score += b
            neighbour.score += 0
        if player.id == 'C' and neighbour.id == 'D':
            player.score += 0
            neighbour.score += b

def replace_pop(array, row, col):   
    neighbour_score = 0
    type_neighbour = ""
    neighbours = von_neumann_neighbourhood(array, row, col)
    player_score = array[row][col].score

    for neighbour in neighbours:
        if neighbour.score > neighbour_score:
            neighbour_score = neighbour.score
            type_neighbour = neighbour.id
    if player_score < neighbour_score:
        if type_neighbour == "C":
            array[row][col] = Cooperator()
        if type_neighbour == "D":
            array[row][col] = Defector()

N = 1
last_gen = []

def generations(N, row, col, array):
    for gen in range(N):    
        for z in range(row):
            for x in range(col):
                play_round(array, z, x)

        for r in range(row):
            last_gen.append([])
            for c in range(col):
                last_gen[r].append(lattice[r][c].id)
                replace_pop(array, r, c)

        for obj in lattice:
            for ob in obj:
                ob.score = 0

lattice = Grid(row, col).populate_grid()
generations(N, row, col, lattice)

heatmap_stuff = []
for z in range(row):
    heatmap_stuff.append([])
    for v in range(col):
        if lattice[z][v].id == 'C' and last_gen[z][v] == 'C':
            heatmap_stuff[z].append(1)
        if lattice[z][v].id == 'D' and last_gen[z][v] == 'D':
            heatmap_stuff[z].append(0)
        if lattice[z][v].id == 'C' and last_gen[z][v] == 'D':
            heatmap_stuff[z].append(3)
        if lattice[z][v].id == 'D' and last_gen[z][v] == 'C':
            heatmap_stuff[z].append(4)

plt.imshow(heatmap_stuff, interpolation='nearest')
plt.colorbar()
plt.show()

Редактировать: я обновил код в соответствии с предложениями Ильмари. Хотя результаты выглядят лучше, а также возвращают реальный фрактал в режиме реального времени, результаты все еще не оптимальны, что наводит меня на мысль, что в другом месте может быть ошибка, поскольку ячейки, похоже, обновляются правильно. Ниже приведен обновленный код, который я добавил / заменил в предыдущем коде.

def get_moore_neighbours(grid, row, col):
    neighbours = []
    for x, y in (
            (row - 1, col), (row + 1, col), (row, col - 1),
            (row, col + 1), (row - 1, col - 1), (row - 1, col + 1),
            (row + 1, col - 1), (row + 1, col + 1)):
        if not (0 <= x < len(grid) and 0 <= y < len(grid[x])):
            # out of bounds
            continue
        else:
            neighbours.append(grid[x][y])
    return neighbours

def calculate_score(grid, row, col):
    b = 1.85
    player = grid[row][col]
    neighbours = get_moore_neighbours(grid, row, col)
    for neighbour in neighbours:
        if player.id == 'C' and neighbour.id == 'C':
            player.score += 1
            neighbour.score += 1
        if player.id == 'D' and neighbour.id == 'D':
            player.score += 0
            neighbour.score += 0
        if player.id == 'D' and neighbour.id == 'C':
            player.score += b
            neighbour.score += 0
        if player.id == 'C' and neighbour.id == 'D':
            player.score += 0
            neighbour.score += b
    return player.score

def best_neighbor_type(grid, row, col): 
    neighbour_score = 0
    type_neighbour = ""
    neighbours = get_moore_neighbours(grid, row, col)
    player_score = grid[row][col].score

    for neighbour in neighbours:
        if neighbour.score > neighbour_score:
            neighbour_score = neighbour.score
            type_neighbour = neighbour.id
    if player_score < neighbour_score:
        if type_neighbour == "C":
            return 'C'
        if type_neighbour == "D":
            return 'D'
    if player_score >= neighbour_score:
        return grid[row][col].id

N = 15
heatmap_data = Grid(row, col).make_grid()
lattice = Grid(row, col).populate_grid()
dbl_buf = Grid(row, col).populate_grid()

for gen in range(N):    
    for r in range(row):
        for c in range(col):
            lattice[r][c].score = calculate_score(lattice, r, c)

    for r in range(row):
        for c in range(col):
            dbl_buf[r][c].id = best_neighbor_type(lattice, r, c)

    for r in range(row):
        for c in range(col):
            if lattice[r][c].id == 'C' and dbl_buf[r][c].id == 'C':
                heatmap_data[r][c] = 1
            if lattice[r][c].id == 'D' and dbl_buf[r][c].id == 'D':
                heatmap_data[r][c] = 2
            if lattice[r][c].id == 'C' and dbl_buf[r][c].id == 'D':
                heatmap_data[r][c] = 3
            if lattice[r][c].id == 'D' and dbl_buf[r][c].id == 'C':
                heatmap_data[r][c] = 4

    plt.imshow(heatmap_data, interpolation='nearest')
    plt.pause(0.01)

    (lattice, dbl_buf) = (dbl_buf, lattice)

plt.show()

1 Ответ

3 голосов
/ 14 мая 2019

Глядя на ваш код, выскакивают несколько вопросов:

  1. Вы никогда не сбрасываете массив last_gen между поколениями, поэтому вы постоянно добавляете в него новые (пустые) строки и делаете первые row строки длиннее и длиннее. Это почти наверняка ошибка.

  2. Вы также никогда не будете использовать массив last_gen для чего-либо, кроме генерации тепловой карты. В частности, ваша replace_pop() функция изменяет тот же массив (с креативным именем array), из которого она читает соседние состояния.

Вторая проблема означает, что поведение вашего кода будет зависеть от порядка, в котором вы перебираете ячейки для вызова replace_pop() в каждом поколении, поскольку замена одной ячейки другим соседом повлияет на соседство всех его соседи, которые еще не были обновлены в этом поколении.

В клеточном автомате, подобном описанному в статье, которую вы цитируете, все клетки должны эффективно обновлять свое состояние одновременно, так, чтобы изменения состояния каждой клетки не становились видимыми для ее соседей до следующего поколения.

На практике самый простой способ реализовать этот вид «одновременного» обновления - это использовать двойную буферизацию, когда вы сначала копируете состояние всех ячеек во второй массив, а затем обновляете первый массив на основе вашей копии. только что сделал. Или, более эффективно, просто поменяйте местами (ссылки на) массивы вместо того, чтобы копировать один в другой. Код будет выглядеть примерно так:

lattice = Grid(row, col).populate_grid()
dbl_buf = Grid(row, col)

for gen in range(N):    
    for r in range(row):
        for c in range(col):
            lattice[r][c].score = calculate_score(lattice, r, c)

    # This is probably the best spot for generating output, since both
    # buffers contain consistent and up-to-date IDs and scores here.

    for r in range(row):
        for c in range(col):
            dbl_buf[r][c].id = best_neighbor_type(lattice, r, c)

    (lattice, dbl_buf) = (dbl_buf, lattice)

, где функция calculate_score() возвращает оценку данной ячейки в решетке на основе типов ее соседей, а функция best_neighbor_id() возвращает идентификатор типа соседа с наивысшей оценкой соседа ячейки в решетке.


Приложение: Ваша реализация calculate_score() в вашем обновленном коде имеет некоторые ошибки:

  1. вы начинаете вычисления с предыдущего значения счета (которое фактически происходит от двух поколений назад из-за двойной буферизации),
  2. вы избыточно записываете счет непосредственно в таблицу внутри функции, а не просто возвращаете счет вызывающей стороне, и
  3. вы также избыточно обновляете счет соседей ячейки, что приводит к тому, что некоторые взаимодействия учитываются дважды.

Однако настоящая причина, по которой вы получаете другие результаты, чем в статье Nowak & May, заключается в концептуальном различии: в документе предполагается, что ячейки также играют в игру сами с собой, эффективно давая сотрудникам прирост очков на одно очко. Ваша реализация не включает это, что приводит к разной динамике для одних и тех же значений параметров.

В любом случае, вот как я бы переписал функцию:

def calculate_score(grid, row, col):
    neighbours = get_moore_neighbours(grid, row, col)
    player = grid[row][col]
    score = 0
    if player.id == 'C': score += 1  # self-interaction!
    for neighbour in neighbours:
        if player.id == 'C' and neighbour.id == 'C':
            score += 1
        if player.id == 'D' and neighbour.id == 'C':
            score += b
    return score

С этим изменением ваш код генерирует очень похожие шаблоны, как в статье Nowak & May:

Screenshot

Кстати, я не уверен, как Nowak & May справится с краями решетки, что может привести к расхождению рисунков, как только они достигнут края. Ваша реализация эффективно исключает любых соседей за пределами краев из вычисления оценки, как если бы решетка была окружена нераспространяющимися дефекторами.

...