简单游戏的遗传编程,非专家可行吗?

时间:2015-01-21 21:57:52

标签: python artificial-intelligence genetic-algorithm

我一直在认真地尝试创建一个genetic program,它将以可接受的方式演变为玩tic-tac-toe。我正在使用基因组生成一个函数,然后将板作为输入并输出结果......但它不起作用。

这个程序可以用少于500行的代码(包括空行和文档)编写吗?也许我的问题是我正在生成过于简单的AI。

我的研究

重要免责声明

  • 这不是任何形式的作业,只是为了学习一些很酷的个人项目。
  • 这不是'给我codz plz',我正在寻找高层建议。我明确地不希望现成的解决方案作为答案。

请给我一些帮助和洞察这个应用于这个特定简单问题的'遗传编程'概念。

@OnABauer:我认为我正在使用遗传编程因为引用维基百科

  

在人工智能中,遗传编程(GP)是一种   基于进化算法的生物学方法启发   进化以找到执行用户定义任务的计算机程序。

我正在尝试生成一个程序(在这种情况下是函数),它将执行播放tic-tac-toe的任务,你可以看到它,因为最重要的genetic_process函数的输出是一个基因组然后将转换为函数,因此,如果我理解正确,这是遗传编程,因为输出是一个函数。

程序内省和可能的错误/问题

代码运行时没有错误也没有崩溃。问题是,最终我得到的是一个无能的人工智能,它将试图非法行动并因每次失败而受到惩罚。它并不比随机更好。

可能是因为我的AI功能如此简单:只需对没有条件的方块的存储值进行计算。

高级别描述

  • 你的染色体代表什么?

我的cromosome显示了一系列函数,这些函数将用于减少存储为trinary的电路板数组。好的,让我举个例子: *染色体是:amMMMdsa(染色体长度必须为8)。    1.第一步是在顶部查找名为LETTERS_TO_FUNCTIONS之后将其转换为函数,这给出了函数:[op.add,op.mul,op.mod,op.mod,op.mod,op.floordiv,op.sub,op.add]

  1. 第二步是将董事会转变为三元代表。所以让我们说董事会是"OX XOX "我们会得到[2,3,1,1,3,2,3,1,1]

  2. 第三步是使用上面获得的函数减少三次繁殖。下面的功能最好地解释了这一点:

    def reduce_by_all_functions(numbers_list,functions_list):
        """
        Applies all the functions in a list to a list of numbers.
    
        >>> reduce_by_all_functions([3,4,6],[op.add,op.sub])
        1
    
        >>> reduce_by_all_functions([6,2,4],[op.mul,op.floordiv])
        3
        """
    
        if len(functions_list) != len(numbers_list) - 1:
            raise ValueError("The functions must be exactly one less than the numbers")
        result = numbers_list[0]
        for index,n in enumerate(numbers_list[1:]):
            result = functions_list[index](result,n)
        return result
    

    因此得出结果:0这意味着ai决定进入第一个方格

    • 你的健身功能是什么?
  3. 幸运的是,这很容易回答。

    def ai_fitness(genome,accuracy):
        """
        Returns how good an ai is by letting it play against a random ai many times.
        The higher the value, the best the ai
        """
        ai = from_genome_to_ai(genome)
        return decide_best_ai(ai,random_ai,accuracy)
    
    • 你的突变是如何起作用的?

    儿子从父亲那里获得了80%的基因,从母亲那里获得了20%的基因。除此之外没有任何随机变异。

      

    如何使用reduce_by_all_functions()?我明白了   拿一块板和一条染色体并返回一个数字。那个怎么样   使用的数字,它代表什么意思,以及......它为什么存在   返回modulo 9?

    reduce_by_all_functions()用于实际应用先前由染色体获得的函数。数字是ai想要的方格。它是模9,因为它必须在0到8之间,因为电路板是9个空格。

    到目前为止我的代码:

    import doctest
    import random
    import operator as op
    
    SPACE = ' '
    MARK_OF_PLAYER_1 = "X"
    MARK_OF_PLAYER_2 = "O"
    EMPTY_MARK = SPACE
    
    BOARD_NUMBERS = """
    The moves are numbered as follows:
    
             0 | 1 | 2
             ---------
             3 | 4 | 5
             ---------
             6 | 7 | 8
    """
    
    WINNING_TRIPLETS = [ (0,1,2), (3,4,5), (6,7,8),
                         (0,3,6), (1,4,7), (2,5,8),
                         (0,4,8), (2,4,6) ]
    
    LETTERS_TO_FUNCTIONS = {
        'a': op.add,
        'm': op.mul,
        'M': op.mod,
        's': op.sub,
        'd': op.floordiv
    }
    
    def encode_board_as_trinary(board):
        """
        Given a board, replaces the symbols with the numbers
        1,2,3 in order to make further processing easier.
    
        >>> encode_board_as_trinary("OX  XOX  ")
        [2, 3, 1, 1, 3, 2, 3, 1, 1]
    
        >>> encode_board_as_trinary("   OOOXXX")
        [1, 1, 1, 2, 2, 2, 3, 3, 3]
        """
        board = ''.join(board)
        board = board.replace(MARK_OF_PLAYER_1,'3')
        board = board.replace(MARK_OF_PLAYER_2,'2')
        board = board.replace(EMPTY_MARK,'1')
        return list((int(square) for square in board))
    
    def create_random_genome(length):
        """
        Creates a random genome (that is a sequences of genes, from which
        the ai will be generated. It consists of randoom letters taken
        from the keys of LETTERS_TO_FUNCTIONS
    
        >>> random.seed("EXAMPLE")
    
        # Test is not possible because even with the same
        # seed it gives different results each run...
        """
        letters = [letter for letter in LETTERS_TO_FUNCTIONS]
        return [random.choice(letters) for _ in range(length)]
    
    def reduce_by_all_functions(numbers_list,functions_list):
        """
        Applies all the functions in a list to a list of numbers.
    
        >>> reduce_by_all_functions([3,4,6],[op.add,op.sub])
        1
    
        >>> reduce_by_all_functions([6,2,4],[op.mul,op.floordiv])
        3
        """
    
        if len(functions_list) != len(numbers_list) - 1:
            raise ValueError("The functions must be exactly one less than the numbers")
        result = numbers_list[0]
        for index,n in enumerate(numbers_list[1:]):
            result = functions_list[index](result,n)
        return result
    
    def from_genome_to_ai(genome):
        """
        Creates an AI following the rules written in the genome (the same as DNA does).
        Each letter corresponds to a function as written in LETTERS_TO_FUNCTIONS.
        The resulting ai will reduce the board using the functions obtained.
    
        >>> ai = from_genome_to_ai("amMaMMss")
    
        >>> ai("XOX   OXO")
        4
        """
        functions = [LETTERS_TO_FUNCTIONS[gene] for gene in genome]
        def ai(board):
            return reduce_by_all_functions(encode_board_as_trinary(board),functions) % 9
        return ai
    
    def take_first_empty_ai(board):
        """
        Very simple example ai for tic-tac-toe
        that takes the first empty square.
    
        >>> take_first_empty_ai(' OX O XXO')
        0
    
        >>> take_first_empty_ai('XOX O XXO')
        3
        """
        return board.index(SPACE)
    
    def get_legal_moves(board):
        """
        Given a tic-tac-toe board returns the indexes of all
        the squares in which it is possible to play, i.e.
        the empty squares.
    
        >>> list(get_legal_moves('XOX O XXO'))
        [3, 5]
    
        >>> list(get_legal_moves('X   O XXO'))
        [1, 2, 3, 5]
        """
        for index,square in enumerate(board):
            if square == SPACE:
                yield index
    
    def random_ai(board):
        """
        The simplest possible tic-tac-toe 'ai', just
        randomly choses a random move.
    
        >>> random.seed("EXAMPLE")
    
        >>> random_ai('X   O XXO')
        3
        """
        legal_moves = list(get_legal_moves(board))
        return random.choice(legal_moves)
    
    
    def printable_board(board):
        """
        User Interface function:
        returns an easy to understand representation
        of the board.
    
        """
        return """
                   {} | {} | {}
                   ---------
                   {} | {} | {}
                   ---------
                   {} | {} | {}""".format(*board)
    
    
    def human_interface(board):
        """
        Allows the user to play tic-tac-toe.
        Shows him the board, the board numbers and then asks
        him to select a move.
        """
        print("The board is:")
        print(printable_board(board))
        print(BOARD_NUMBERS)
        return(int(input("Your move is: ")))
    
    def end_result(board):
        """
        Evaluates a board returning:
    
        0.5 if it is a tie
        1 if MARK_OF_PLAYER_1 won # default to 'X'
        0 if MARK_OF_PLAYER_2 won # default to 'O'
    
        else if nothing of the above applies return None
    
        >>> end_result('XXX   OXO')
        1
    
        >>> end_result(' O X X  O')
        None
    
        >>> end_result('OXOXXOXOO')
        0.5
    
        """
    
        if SPACE not in board:
            return 0.5
        for triplet in WINNING_TRIPLETS:
            if all(board[square] == 'X'  for square in triplet):
                return 1
            elif all(board[square] == 'O'  for square in triplet):
                return 0
    
    def game_ended(board):
        """
        Small syntactic sugar function to if the game is ended
        i.e. no tie nor win occured
        """
        return end_result(board) is not None
    
    def play_ai_tic_tac_toe(ai_1,ai_2):
        """
        Plays a game between two different ai-s, returning the result.
        It should be noted that this function can be used also to let the user
        play against an ai, just call it like: play_ai_tic_tac_toe(random_ai,human_interface)
    
        >>> play_ai_tic_tac_toe(take_first_empty_ai,take_first_empty_ai)
        1
        """
    
        board = [SPACE for _ in range(9)]
        PLAYER_1_WIN = 1
        PLAYER_1_LOSS = 0
        while True:
            for ai,check in ( (ai_1,MARK_OF_PLAYER_1), (ai_2,MARK_OF_PLAYER_2) ):
                move = ai(board)
    
                # If move is invalid you lose
                if board[move] != EMPTY_MARK:
                    if check == MARK_OF_PLAYER_1:
                        return PLAYER_1_LOSS
                    else:
                        return PLAYER_1_WIN
    
                board[move] = check
                if game_ended(board):
                    return end_result(board)
    
    def loop_play_ai_tic_tac_toe(ai_1,ai_2,games_number):
        """
        Plays games number games between ai_1 and ai_2
        """
        return sum(( play_ai_tic_tac_toe(ai_1,ai_2)) for _ in range(games_number))
    
    def decide_best_ai(ai_1,ai_2,accuracy):
        """
        Returns the number of times the first ai is better than the second:
        ex. if the ouput is 1.4, the first ai is 1.4 times better than the second.
    
        >>> decide_best_ai(take_first_empty_ai,random_ai,100) > 0.80
        True
        """
        return sum((loop_play_ai_tic_tac_toe(ai_1,ai_2,accuracy//2),
                   loop_play_ai_tic_tac_toe(ai_2,ai_1,accuracy//2))) / (accuracy // 2)
    
    def ai_fitness(genome,accuracy):
        """
        Returns how good an ai is by lettting it play against a random ai many times.
        The higher the value, the best the ai
        """
        ai = from_genome_to_ai(genome)
        return decide_best_ai(ai,random_ai,accuracy)
    
    def sort_by_fitness(genomes,accuracy):
        """
        Syntactic sugar for sorting a list of genomes based on the fitness.
        High accuracy will yield a more accurate ordering but at the cost of more
        computation time.
        """
        def fit(genome):
            return ai_fitness(genome,accuracy)
    
        return list(sorted(genomes, key=fit, reverse=True))
        # probable bug-fix because high fitness means better individual
    
    def make_child(a,b):
        """
        Returns a mix of cromosome a and cromosome b.
        There is a bias towards cromosome a because I think that
        a too weird soon is going to be bad.
        """
        result = []
        for index,char_a in enumerate(a):
            char_b = b[index]
            if random.random() > 0.8:
                result.append(char_a)
            else:
                result.append(char_b)
        return result
    
    def genetic_process(population_size,generation_number,accuracy,elite_number):
        """
        A full genetic process yielding a good tic-tac-toe ai. # not yet
    
        @ Parameters:
             @ population_size: the number of ai-s that you allow to be alive
                  at once
             @ generation_number: the number of generations of the gentetic
             @ accuracy: how well the ai-s are ordered,
                  low accuracy means that a good ai may be considered bad or
                  viceversa. High accuarcy is computationally costly
             @ elite_number: the number of best programmes that get to reproduce
                  at each generation.
    
        @ Return:
              @ A genome for a tic-tac-toe ai
        """
        pool = [create_random_genome(9-1) for _ in range(population_size)]
        for generation in range(generation_number):
            best_individuals = sort_by_fitness(pool,accuracy)[:elite_number]
            the_best = best_individuals[0]
            for good_individual in best_individuals:
                pool.append(make_child(the_best,good_individual))
            pool = sort_by_fitness(pool,accuracy)[:population_size]
        return the_best
    
    def _test():
        """
        Tests all the script by running the
    
        >>> 2 + 2 # code formatted like this
        4
        """
        doctest.testmod()
    
    def main():
        """
        A simple demo to let the user play against a genetic opponent.
        """
        print("The genetic ai is being created... please wait.")
        genetic_ai = from_genome_to_ai(genetic_process(50,4,40,25))
        play_ai_tic_tac_toe(genetic_ai,human_interface)
    
    if __name__ == '__main__':
        main()
    

1 个答案:

答案 0 :(得分:5)

首先,我有义务说Tic Tac Toe真的太简单了,不能合理地用遗传程序攻击。你根本不需要GP的力量来赢得Tic Tac Toe;您可以使用强力查找表或简单的游戏树来解决它。

那就是说,如果我理解正确,你的基本观点就是:

1)创建长度为8的染色体,其中每个基因是算术运算,并且8基因染色体作为板评估函数作用于每个板。也就是说,染色体接受一个板表示,并吐出一个代表该板的优点的数字。

不完全清楚这是你正在做的事情,因为你的电路板表示每个都是9个整数(仅限1,2,3),但你的例子是按照&#34给出的赢得三重奏"这是2个整数(0到8)。

2)启动AI,在AI轮到它时,它应该得到所有合法移动的列表,为每个合法移动评估每个染色体的板子......并取这个数字,模9,并将其作为下一步行动?当然,那里有一些代码来处理这种移动是非法的情况......

3)让一堆这些染色体表示要么发挥标准实现,要么相互发挥作用,并根据获胜次数确定适合度。

4)一旦评估了整整一代染色体,就要创造新一代染色体。我不清楚你是如何从池中选择父母的,但是一旦选择了父母,就可以通过80-20规则从父母那里获取单个基因来生产孩子。

您的整体高级策略是合理的,但执行中存在许多概念和实施缺陷。首先,让我们谈谈完全可观察的游戏以及为他们制作AI的简单方法。如果游戏非常简单(例如Tic Tac Toe),你可以简单地制作一个蛮力极小极大游戏树such as this。 TTT很简单,甚至你的手机也可以很快地一直到树的底部。您甚至可以使用查找表通过强力解决它:只需列出所有电路板位置和每个电路板的响应。

当游戏变得更大时 - 想想棋子,国际象棋,去 - 这已不再适用,其中一个方法是开发所谓的棋盘评估功能。这是一个占据董事会位置并返回一个数字的函数,通常对于一个玩家更好,对另一个玩家更好。然后,人们执行搜索到某个可接受的深度,并针对最高(比较)的评估板功能。

这引出了一个问题:我们如何提出董事会评估功能?最初,有人要求游戏专家为您开发这些功能。 Chellapilla和Fogel有一个很棒的paper,它类似于你想要为棋子做的事情 - 他们使用神经网络来确定棋盘评估函数,而且,关键的是,这些神经网络被编码为基因组并进化。然后将它们用于搜索深度4树。最终结果与人类玩家竞争非常激烈。

你应该阅读那篇论文。

我认为你要做的事情是非常相似的,除了不将神经网络编码为染色体,你要尝试编写一个非常有限的代数表达式,总是形式:

((((((((arg1 op1 arg2) op2 arg3) op3 arg4) op4 arg5) op5 arg6) op6 arg7) op7 arg8) op8 arg)

...然后你用mod 9来选择一个动作。

现在让我们谈谈遗传算法,遗传程序和新生儿的创造。进化技术的整体思想是结合两种希望良好的解决方案的最佳属性,希望它们能够更好,而不会陷入局部最大化。

通常,这是通过touranment选择,交叉和变异来完成的。锦标赛选择意味着按比例选择父母的健康状况。交叉意味着将染色体分成两个通常连续的区域,并从一个亲本获得一个区域而从另一个亲本获得另一个区域。 (为什么是连续的?因为Holland's Schema Theorem)突变意味着偶尔改变基因,作为维持种群多样性的手段。

现在让我们来看看你正在做什么:

1)你的评估板功能 - 你的染色体变成的功能 - 作用于电路板位置 - 受到高度限制并且非常随意。将1,2和3分配为这些数字的韵律或理由并不多,但这可能没什么问题。更大的缺陷是你的功能是整个功能空间的一个非常有限的部分。它们的长度始终相同,并且解析树看起来总是一样。

没有理由期望在这个限制性空间中有任何有用的东西。所需要的是提出一种方案,该方案允许更一般的解析树集,包括交叉和变异方案。您应该通过John Koza查找一些论文或书籍,以获取有关此主题的想法。

请注意,Chellapilla和Fogel也有固定的功能形式,但它们的染色体比它们的板表示要大得多。跳棋板有32个可玩空间,每个空间可以有5个状态。但他们的神经网络有大约85个节点,染色体包含这些节点的连接权重 - 数百甚至数千个值。

2)然后就是整个模9的东西。我不明白为什么你这样做。不要这样做。你只是在争论染色体中可能存在的任何信息。

3)你创造新孩子的功能很糟糕。即使作为遗传算法,您也应该将染色体分成两部分(在随机点)并从一侧获取一部分父母,而另一部分从另一部分获取另一部分。对于你正在做的遗传编程,有一些类似的策略可以在解析树上进行交叉。见Koza。

您必须包含突变,否则您几乎肯定会得到次优结果。

4a)如果你通过对抗一个称职的人工智能来评估健康状况,那么你就会意识到你的染色体永远不会胜利。他们会失败,或者他们会画画。一个称职的AI永远不会失败。此外,你的AI可能会一直失去,而初期几代人可能都是同等(灾难性)的穷人。让你从这个洞里走出来并不是不可能的,但这很难。

4b)另一方面,如果像Chellapilla和Fogel一样,你对自己玩AI,那么你最好确定AI可以玩X或O.否则你就是永远不会取得任何进展。

5)最后,即使解决了所有这些问题,我也不相信这会得到很好的结果。请注意,检查器示例搜索深度为4,这在跳棋游戏中可能持续20或30次移动并不是一个很大的视野。

TTT只能持续9次。

如果您不进行搜索树并且只是寻求最高的评估板功能,那么您可能会得到一些有效的信息。你可能不会。我不确定。如果搜索到深度4,您也可以跳到完全搜索到9级并按常规执行此操作。