【问题标题】:AI How to model genetic programming for BattleshipsAI 如何为战舰建模基因编程
【发布时间】:2016-06-19 15:39:23
【问题描述】:

我有一个关于遗传编程的问题。我将为game called Battleships 研究遗传算法。

我的问题是:我将如何决定人工智能发展的“决策”模型?它是如何工作的?

我已经阅读了多篇论文和多个答案,它们只是谈论使用不同的模型,但找不到具体的东西,不幸的是,我显然需要解决这个问题。

我希望它在多次迭代中进化并“学习”最有效的方法,但不确定如何保存这些“决定”(我知道文件,但如何“编码”?) 以一种很好的方式,因此它将学会对先前的操作采取立场并基于当前董事会状态的信息。

我一直在考虑一个“树状结构”供 AI 决策依据,但我实际上不知道如何开始。

如果有人能指出我正确的方向(链接?一些伪代码?类似的东西),那将非常感激,我尝试尽可能多地使用谷歌,观看多个关于主题,但我认为我只需要在正确的方向上轻轻一点。

我也可能只是不知道究竟要搜索什么,这就是为什么我对我实现它的内容和方式的结果一无所知。

【问题讨论】:

标签: java algorithm artificial-intelligence genetic-algorithm genetic-programming


【解决方案1】:

我会建议你另一种方法。这种方法是基于船舶所在位置的可能性。我将向您展示一个较小版本的游戏示例(相同的想法适用于所有其他版本)。在我的示例中,它是 3x3 区域,并且只有一个 1x2 船舶。

现在您选择一个空白区域,并将船放在所有可能的位置(存储船的部分在矩阵元素中的次数)。如果您要为船 1x2 执行此操作,您将获得以下内容

1 2 1
1 2 1
1 2 1

船舶可以在另一个方向2x1,这将为您提供以下矩阵:

1 1 1
2 2 2
1 1 1

总结你会得到概率矩阵:

2 3 2
3 4 3
2 3 2

这意味着最可能的位置是中间位置(我们有 4 个)。这里是你应该拍摄的地方。


现在让假设你撞到了船的部分。如果你重新计算似然矩阵,你会得到:

0 1 0
1 W 1
0 1 0

它会告诉您下一次拍摄的 4 个不同的可能位置。

例如,如果您错过了上一步,您将获得以下矩阵:

2 2 2
2 M 2
2 2 2

这是基本思想。您尝试重新定位船只的方式取决于如何定位船只的规则以及每次移动后您获得的信息。可以是missed/gotmissed/wounded/killed

【讨论】:

  • 因为这是对以下问题的回答:“什么是后勤/统计上最好的拍摄位置”,我很感激,但我实际上希望 AI 自己解决这个问题,使用数千多代迭代:) 真正的问题仍然是,我如何才能让它存储它已经收集的信息,如果认为有用的话,它可以被使用,以及它怎么能如果为后代选择它,是否可以扩展?
  • @user3071234 我明白你想要什么。问题是我认为 GA 在这里不是一个好方法。我认为我描述的概率方法是最好的方法。在这里看到一个好的 GA 方法会很有趣。
  • 我知道概率方法很可能是最好的方法,但我实际上想“实践”遗传算法方法,并选择了它作为一个合适的开始,不要太关注立即打开,因为它所要做的就是选择要射击的瓷砖,但仍然足够使用,因为健身功能很容易,只需选择与发射总数相比,哪一个击中最多。但正如我所说,我不确定如何开始,因此非常感谢代码示例和/或伪代码! :) (不过,谢谢您的意见!)
  • 我认为这种方法可能存在问题,因为船只不是随机放置的(好吧,除非它们是在这种情况下),它们是由一个聪明的对手放置的,他可能遵循某种策略.所以在这种情况下,他实际上会尝试避开中心,正是因为如果随机放置,它最有可能是一艘船的位置。
  • 事实上,我记得最好的策略之一是将大部分较长的船沿边界放置,因为它可以最大限度地减少被周围未使用空间“浪费”的电路板部分,并放置所有的随机在中间。当然,沿边界的人很快就会被淘汰,但是除了幸运的猜测之外,没有可靠的策略来追捕 1s
【解决方案2】:

回答第一部分:遗传算法的基础是拥有一组参与者,其中一些参与者可以复制。选择最适者进行繁殖,后代是稍微突变的父母的副本。这是一个非常简单的概念,但是要对其进行编程,您必须具有可以随机选择和动态修改的操作。对于战舰模拟,我创建了一个名为 Shooter 的类,因为它在某个位置“射击”。这里的假设是第一个位置已经被击中,射手现在正试图击沉战舰。

public class Shooter implements Comparable<Shooter> {
    private static final int NUM_SHOTS = 100;
    private List<Position> shots;
    private int score;

    // Make a new set of random shots.
    public Shooter newShots() {
        shots = new ArrayList<Position>(NUM_SHOTS);
        for (int i = 0; i < NUM_SHOTS; ++i) {
            shots.add(newShot());
        }
        return this;
    }
    // Test this shooter against a ship
    public void testShooter(Ship ship) {
        score = shots.size();
        int hits = 0;
        for (Position shot : shots) {
            if (ship.madeHit(shot)) {
                if (++hits >= ship.getSize())
                    return;
            } else {
                score = score - 1;
            }
        }
    }

    // get the score of the testShotr operation
    public int getScore() {
        return score;
    }
    // compare this shooter to other shooters.
    @Override
    public int compareTo(Shooter o) {
        return score - o.score;
    }
    // getter
    public List<Position> getShots() {
        return shots;
    }
    // reproduce this shooter
    public Shooter reproduce() {
        Shooter offspring = new Shooter();
        offspring.mutate(shots);
        return offspring;
    }
    // mutate this shooter's offspring
    private void mutate(List<Position> pShots) {
        // copy parent's shots (okay for shallow)
        shots = new ArrayList<Position>(pShots);
        // 10% new mutations, in random locations
        for (int i = 0; i < NUM_SHOTS / 10; i++) {
            int loc = (int) (Math.random() * 100);
            shots.set(loc, newShot());
        }
    }
    // make a new random move
    private Position newShot() {
        return new Position(((int) (Math.random() * 6)) - 3, ((int) (Math.random() * 6)) - 3);
    }
}

这里的想法是Shooter 最多有 100 次射击,在 X 轴 +-3 和 Y 轴 +-3 之间随机选择。是的,100 次射击是多余的,但是,不管怎样。将Ship 传递给Shooter.testShooter,它会自己评分,100 是最好的分数,0 是最差的。

这个Shooter 演员有reproducemutate 方法将返回其10% 的镜头随机变异的后代。一般的想法是,最好的Shooters 已经“学会”尽快以交叉模式(“+”)射击,因为一艘船以四种方式之一(北、南、东、西)。

运行模拟的程序ShooterSimulation 非常简单:

public class ShooterSimulation {
    private int NUM_GENERATIONS = 1000;
    private int NUM_SHOOTERS = 20;
    private int NUM_SHOOTERS_NEXT_GENERATION = NUM_SHOOTERS / 10;

    List<Shooter> shooters = new ArrayList<Shooter>(NUM_SHOOTERS);
    Ship ship;

    public static void main(String... args) {
        new ShooterSimulation().run();
    }

    // do the work
    private void run() {
        firstGeneration();
        ship = new Ship();
        for (int gen = 0; gen < NUM_GENERATIONS; ++gen) {
            ship.newOrientation();
            testShooters();
            Collections.sort(shooters);
            printAverageScore(gen, shooters);
            nextGeneration();
        }
    }

    // make the first generation
    private void firstGeneration() {
        for (int i = 0; i < NUM_SHOOTERS; ++i) {
            shooters.add(new Shooter().newShots());
        }
    }

    // test all the shooters
    private void testShooters() {
        for (int mIdx = 0; mIdx < NUM_SHOOTERS; ++mIdx) {
            shooters.get(mIdx).testShooter(ship);
        }
    }

    // print the average score of all the shooters
    private void printAverageScore(int gen, List<Shooter> shooters) {
        int total = 0;
        for (int i = 0, j = shooters.size(); i < j; ++i) {
            total = total + shooters.get(i).getScore();
        }
        System.out.println(gen + " " + total / shooters.size());
    }

    // throw away the a tenth of old generation
    // replace with offspring of the best fit
    private void nextGeneration() {
        for (int l = 0; l < NUM_SHOOTERS_NEXT_GENERATION; ++l) {
            shooters.set(l, shooters.get(NUM_SHOOTERS - l - 1).reproduce());
        }
    }
}

代码从 run 方法读取为伪代码:创建 firstGeneration 然后迭代若干代。对于每一代,给船设置一个newOrientation,然后做testShooters,用Collections.sort对测试结果进行排序。 printAverageScore 的测试,然后构建 nextGeneration。借助平均分列表,您可以咳咳,进行“分析”。

结果图如下所示:

如您所见,它一开始的平均分数很低,但学起来很快。然而,船的方位不断变化,除了随机分量外,还会产生一些噪声。一次又一次的突变会使团队有点混乱,但随着团队整体的提高,突变会越来越少。

挑战,也是许多论文可以肯定的原因,是让更多的东西变得可变,尤其是以建设性的方式。例如,镜头的数量可以是可变的。或者,将镜头列表替换为根据最后一个镜头是命中还是未命中而分枝的树可能会改善情况,但这很难说。这就是“决策”逻辑考虑的用武之地。拥有一个随机镜头列表还是根据先前镜头决定采用哪个分支的树更好?更高级别的挑战包括预测哪些变化将使该群体学习得更快并且更不容易受到不良突变的影响。

最后,考虑可能有多个组,例如,一组是战列舰猎人,一组是潜艇猎人。每个组虽然由相同的代码组成,但可以“进化”不同的内部“遗传学”,使他们能够专门从事他们的任务。

无论如何,和往常一样,从简单的地方开始,边做边学,直到你变得足够好,可以回去阅读论文。

PS> 也需要这个:

public class Position {
    int x;
    int y;
    Position(int x, int y ) {this.x=x; this.y=y;}

    @Override
    public boolean equals(Object m) {
        return (((Position)m).x==x && ((Position)m).y==y);
    }
}

UDATE:添加了Ship 类,修复了一些错误:

public class Ship {
    List<Position> positions;

    // test if a hit was made
    public boolean madeHit(Position shot) {
        for (Position p: positions) {
            if ( p.equals(shot)) return true;
        }
        return false;
    }

    // make a new orientation
    public int newOrientation() {
        positions = new ArrayList<Position>(3);
        // make a random ship direction.
        int shipInX=0, oShipInX=0 , shipInY=0, oShipInY=0;

        int orient = (int) (Math.random() * 4);
        if( orient == 0 ) {
            oShipInX = 1;
            shipInX = (int)(Math.random()*3)-3;
        }
        else if ( orient == 1 ) {
            oShipInX = -1;
            shipInX = (int)(Math.random()*3);
        }
        else if ( orient == 2 ) {
            oShipInY = 1;
            shipInY = (int)(Math.random()*3)-3;
        }
        else if ( orient == 3 ) {
            oShipInY = -1;
            shipInY = (int)(Math.random()*3);
        }

        // make the positions of the ship
        for (int i = 0; i < 3; ++i) {
            positions.add(new Position(shipInX, shipInY));
            if (orient == 2 || orient == 3)
                shipInY = shipInY + oShipInY;
            else
                shipInX = shipInX + oShipInX;
        }
        return orient;
    }

    public int getSize() {
        return positions.size();
    }
}

【讨论】:

  • 这很酷,而且你用相当简单的方式描述它的方式很棒,谢谢!通过广泛的搜索,我发现神经网络可以使用遗传算法进行训练,从而具有可以做出决策的玩家的“自我学习”模型,您认为这是一种可能性吗?
  • 我不能权威地发表评论,但通常神经网络是trained,而不是evolved。反馈给网络,网络调整网络中节点的权重。最近的进展是增加了一个额外的网络层来解释底层网络的响应,但一般来说,人们会继续训练同一个网络,而不是因为性能不佳而终止。 GA 可以帮助在底层配置之间进行选择,当然,为什么不呢。我听说过 java h20 作为神经网络软件,但还没有尝试过。
【解决方案3】:

回答第二部分:遗传算法本身并不是目的,它是实现目的的手段。在这个战舰例子的情况下,最终是做出最好的Shooter。我在之前版本的程序中添加了a行来输出最佳射手的射门模式,发现有问题:

Best shooter = Shooter:100:[(0,0), (0,0), (0,0), (0,-1), (0,-3), (0,-3), (0,-3), (0,0), (-2,-1) ...]

此模式中的前三个镜头位于坐标 (0,0) 处,在此应用程序中保证命中,即使它们击中同一个点。多次击中同一个地点是违反战舰规则的,所以这个“最好”的射手是最好的,因为它学会了作弊!

因此,显然该程序需要改进。为此,我更改了 Ship 类,如果位置已被击中,则返回 false。

public class Ship {
    // private class to keep track of hits
    private class Hit extends Position {
        boolean hit = false;
        Hit(int x, int y) {super(x, y);}
    }
    List<Hit> positions;

    // need to reset the hits for each shooter test.
    public void resetHits() {
        for (Hit p: positions) {
            p.hit = false;
        }
    }
    // test if a hit was made, false if shot in spot already hit
    public boolean madeHit(Position shot) {
        for (Hit p: positions) {
            if ( p.equals(shot)) {
                if ( p.hit == false) {
                    p.hit = true;
                    return true;
                }
                return false;
            }
        }
        return false;
    }

    // make a new orientation
    public int newOrientation() {
        positions = new ArrayList<Hit>(3);
        int shipInX=0, oShipInX=0 , shipInY=0, oShipInY=0;
        // make a random ship orientation.
        int orient = (int) (Math.random() * 4.0);
        if( orient == 0 ) {
            oShipInX = 1;
            shipInX = 0-(int)(Math.random()*3.0);
        }
        else if ( orient == 1 ) {
            oShipInX = -1;
            shipInX = (int)(Math.random()*3.0);
        }
        else if ( orient == 2 ) {
            oShipInY = 1;
            shipInY = 0-(int)(Math.random()*3.0);
        }
        else if ( orient == 3 ) {
            oShipInY = -1;
            shipInY = (int)(Math.random()*3.0);
        }

        // make the positions of the ship
        for (int i = 0; i < 3; ++i) {
            positions.add(new Hit(shipInX, shipInY));
            if (orient == 2 || orient == 3)
                shipInY = shipInY + oShipInY;
            else
                shipInX = shipInX + oShipInX;
        }
        return orient;
    }

    public int getSize() {
        return positions.size();
    }
}

在我这样做之后,我的射手停止了“作弊”,但这让我开始思考一般的得分。该应用程序的早期版本所做的是根据错过的投篮次数进行评分,因此如果没有投篮命中,射手可以获得完美的分数。然而,这是不现实的,我真正想要的是射门最少的射手。我更改了射手以跟踪平均拍摄次数:

public class Shooter implements Comparable<Shooter> {
    private static final int NUM_SHOTS = 40;
    private List<Position> shots;
    private int aveScore;

    // Make a new set of random shots.
    public Shooter newShots() {
        shots = new ArrayList<Position>(NUM_SHOTS);
        for (int i = 0; i < NUM_SHOTS; ++i) {
            shots.add(newShot());
        }
        return this;
    }
    // Test this shooter against a ship
    public int testShooter(Ship ship) {
        int score = 1;
        int hits = 0;
        for (Position shot : shots) {
            if (ship.madeHit(shot)) {
                if (++hits >= ship.getSize())
                    return score;
            }
            score++;
        }
        return score-1;
    }
    // compare this shooter to other shooters, reverse order
    @Override
    public int compareTo(Shooter o) {
        return o.aveScore - aveScore;
    }
    ... the rest is the same, or getters and setters.
}

我还意识到,我必须对每个射手进行多次测试,才能获得对战列舰的平均射击次数。为此,我对每个射手分别进行了多次测试。

// test all the shooters
private void testShooters() {
    for (int i = 0, j = shooters.size(); i<j;  ++i) {
        Shooter current = shooters.get(i);
        int totalScores = 0;
        for (int play=0; play<NUM_PLAYS; ++play) {
            ship.newOrientation();
            ship.resetHits();
            totalScores = totalScores + current.testShooter(ship);
        }
        current.setAveScore(totalScores/NUM_PLAYS);
    }
}

现在,当我运行模拟时,我会得到平均值的平均值作为输出。该图通常如下所示: 同样,射手们学得很快,但随机变化需要一段时间才能降低平均水平。现在我最好的Shooter 更有意义了:

Best=Shooter:6:[(1,0), (0,0), (0,-1), (2,0), (-2,0), (0,1), (-1,0), (0,-2), ...

所以,遗传算法正在帮助我设置我的Shooter 的配置,但正如这里的另一个答案指出的那样,只要考虑一下就可以取得好的结果。考虑一下,如果我的神经网络有 10 个可能的设置,每个设置有 100 个可能的值,那就是 10^100 个可能的设置,并且应该如何设置这些设置的理论可能比战舰射击理论更难一些。在这种情况下,遗传算法可以帮助确定最佳设置并检验当前理论。

【讨论】:

    【解决方案4】:

    回答第三部分: 如您所见,Genetic Algorithm 通常不是最难的部分。同样,它是一段简单的代码,实际上是为了练习另一段代码,演员。在这里,actor 在 Shooter 类中实现。这些参与者通常以Turning Machines 的方式建模,因为参与者对一组输入有一组定义的输出。 GA 帮助您确定state table 的最佳配置。在之前对这个问题的回答中,Shooter 实现了一个概率矩阵,就像@SalvadorDali 在他的回答中所描述的那样。

    彻底测试之前的Shooter,我们发现它可以做的最好的事情是:

    BEST Ave=5, Min=3, Max=9
    Best=Shooter:5:[(1,0), (0,0), (2,0), (-1,0), (-2,0), (0,2), (0,1), (0,-1), (0,-2), (0,1)]
    

    这表明击沉一艘 3X3 战列舰平均需要 5 次射击,最少 3 次,最多 9 次。 9 个镜头的位置显示为 X/Y 坐标对。问题“这可以做得更好吗?”取决于人类的聪明才智。遗传算法不能为我们编写新的演员。我想知道 decision tree 是否可以比概率矩阵做得更好,所以我实现了一个来尝试一下:

    public class Branch {
        private static final int MAX_DEPTH = 10;
        private static final int MUTATE_PERCENT = 20;
        private Branch hit;
        private Branch miss;    
        private Position shot;
    
        public Branch() {
            shot = new Position(
                (int)((Math.random()*6.0)-3), 
                (int)((Math.random()*6.0)-3)
            );
        }
    
        public Branch(Position shot, Branch hit, Branch miss) {
            this.shot = new Position(shot.x, shot.y);
            this.hit = null; this.miss = null;
            if ( hit != null ) this.hit = hit.clone();
            if ( miss != null ) this.miss = miss.clone();
        }
    
        public Branch clone() {
            return new Branch(shot, hit, miss);
        }
    
        public void buildTree(Counter c) {
            if ( c.incI1() > MAX_DEPTH ) {
                hit = null;
                miss = null;
                c.decI1();
                return;
            } else {
                hit = new Branch();
                hit.buildTree(c);
                miss = new Branch();
                miss.buildTree(c);
            }
            c.decI1();
        }
    
        public void shoot(Ship ship, Counter c) {
            c.incI1();
            if ( ship.madeHit(shot)) {
                if ( c.incI2() == ship.getSize() ) return;
                if ( hit != null ) hit.shoot(ship, c);
            }
            else {
                if ( miss != null ) miss.shoot(ship, c);
            }
        }
    
        public void mutate() {
            if ( (int)(Math.random() * 100.0) < MUTATE_PERCENT) {
                shot.x = (int)((Math.random()*6.0)-3);
                shot.y = (int)((Math.random()*6.0)-3);
            }
            if ( hit != null ) hit.mutate();
            if ( miss != null ) miss.mutate();
        }
    
        @Override
        public String toString() {
            StringBuilder sb = new StringBuilder();
            sb.append(shot.toString());
            if ( hit != null ) sb.append("h:"+hit.toString());
            if ( miss != null ) sb.append("m:"+miss.toString());
            return sb.toString();
        }
    }
    

    Branch 类是 node 中的 decision tree (好吧,也许名字不好)。每次击球时,选择的下一个分支取决于击球是否命中。

    对射手进行了一些修改以使用新的decisionTree

    public class Shooter implements Comparable<Shooter> {
        private Branch decisionTree;
        private int aveScore;
    
        // Make a new random decision tree.
        public Shooter newShots() {
            decisionTree = new Branch();
            Counter c = new Counter();
            decisionTree.buildTree(c);
            return this;
        }
        // Test this shooter against a ship
        public int testShooter(Ship ship) {
            Counter c = new Counter();
            decisionTree.shoot(ship, c);
            return c.i1;
        }
        // compare this shooter to other shooters, reverse order
        @Override
        public int compareTo(Shooter o) {
            return o.aveScore - aveScore;
        }
        // mutate this shooter's offspring
        public void mutate(Branch pDecisionTree) {
            decisionTree = pDecisionTree.clone();
            decisionTree.mutate();
        }
    
        // min, max, setters, getters
        public int getAveScore() {
            return aveScore;
        }
        public void setAveScore(int aveScore) {
            this.aveScore = aveScore;
        }
        public Branch getDecisionTree() {
            return decisionTree;
        }
        @Override
        public String toString() {
            StringBuilder ret = new StringBuilder("Shooter:"+aveScore+": [");
            ret.append(decisionTree.toString());
            return ret.append(']').toString();
        }
    }
    

    细心的读者会注意到,虽然方法本身发生了变化,但Shooter 需要实现的方法与之前的Shooters 没有什么不同。这意味着主要的GA 模拟除了与突变相关的一行之外没有改变,这可能可以解决:

    Shooter child = shooters.get(l);
    child.mutate( shooters.get(NUM_SHOOTERS - l - 1).getDecisionTree());
    

    典型的模拟运行图现在如下所示:

    如您所见,使用Decision Tree 演变的最终最佳平均分数比Probability Matrix 演变的最佳平均分数低一杆。另请注意,这组Shooters 已经花费了大约 800 代来训练到最佳状态,大约是更简单的概率矩阵Shooters 的两倍。最佳决策树Shooter 给出了这样的结果:

    BEST Ave=4, Min=3, Max=6
    Best=Shooter:4: [(0,-1)h:(0,1)h:(0,0) ... ]
    

    这里,不仅平均出手少了一杆,而且最大出手数比概率矩阵Shooter低1/3。

    在这一点上,需要一些非常聪明的人来确定这个演员是否已经达到了问题域的理论最优值,即,这是你试图击沉一艘 3X3 船所能做的最好的事情吗?考虑到这个问题的答案在真正的战舰游戏中会变得更加复杂,因为它有几艘不同大小的战舰。您将如何构建一个演员,将哪些船已经沉没的知识整合到randomly chosen and dynamically modified 的动作中?这就是理解图灵机(也称为 CPU)变得重要的地方。

    PS>你还需要这个类:

    public class Counter {
        int i1;
        int i2;
    
        public Counter() {i1=0;i2=0;}
    
        public int incI1() { return ++i1; }
        public int incI2() { return ++i2; }
        public int decI1() { return --i1; }
        public int decI2() { return --i2; }
    }
    

    【讨论】:

      猜你喜欢
      • 2010-12-10
      • 1970-01-01
      • 2013-04-17
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-11-30
      • 1970-01-01
      相关资源
      最近更新 更多