guest@blog.cmj.tw: ~/posts $

AI


Industry X.0

AI 是個很有趣的領域,他充滿了各種數學理論以及演算法。AI 對我來說就是一種策略 (Strategy) 上的一種方式:如何根據數學模型與演算法,決定接下來的行動。 用踩地雷 (Minesweeper) 0 為例,就可以發展出若干種策略來進行遊戲

全亂數

最簡單也是最無效的策略就是全亂數的決策方式:這種決策方式表示不考慮任何已知的情報, 用隨機的方式決定接下來的行動。在 Minesweeper 的例子中,可以使用 random 函數中的 choice 來隨機選取目前未知的可能性 (safeStep)。因為不考慮任何已知的情報, 故可以快速地估算出成功的機率約為 $\prod_{k=0}^{S-n}\frac{S-n-k}{S-k}$_。

在 S=289、n=18 的情況下,成功的機率約為 $5.6 * 10^{-29}$。 在 S=49、n=5 的情況下,成功的機率約為 $5.2 * 10^{-7}$。 然而在實際運作中,我們可以先排除掉已經知道的結果,所以機率會比估算值還要高。在 AlgoFullRandom 的情況下,大約有 6% 的機率可以獲勝 (4/49/500 round)。

class AI(object):
    def AlgoFullRandom(cls, debug=False):
        import random

        agent = Minesweeper(1, 1, cls.size)
        while len(agent):
            if debug:
                print agent
            agent.step(*random.choice(agent.safeStep))

基本策略

基本策略表示我們會參考到目前已知的線索,而非利用全亂數的方式來實作策略:在這個策略中, 我們計算預計觸發的位子中,附近八格的已知線索來計算這個位子是炸彈的機率。 計算的方式利用 $\frac{未知的位子}{已知的炸彈數}$ 來計算機率。根據這個策略, 我們可以獲得遠高於全亂數的結果:在 (4/49/500 round) 的情況下,我們獲勝的機率已經高達 7 成, 更嚴苛的 (11/121/500 round) 的情況下,也有 27% 的獲勝機會。

def AlgoSimpleStrategy(cls, debug=False, default=0.3):
    def ratio(cls, x, y):
        fn = lambda x, y: cls[x, y]/float(cls.empty(x, y))
        nr  = [[_x, _y] for _x in range(x-1, x+2) for _y in (y-1, y+1) if 0 < cls[_x, _y]]
        nr += [[_x, y] for _x in (x-1, x+1) if 0 < cls[_x, y]]
        if nr:
            return max([fn(*_) for _ in nr])
        else:
            return default

    agent = Minesweeper(1, 1, cls.size, debug=debug)
    while len(agent):
        if debug: print agent, len(agent)
        steps = [[_, ratio(agent, *_)] for _ in agent.safeStep]
        steps = sorted(steps, key=lambda x: x[1])
        agent.step(*steps[0][0])

Minesweeper Sample Code

class Minesweeper(object):
    '''
    >>> for _ in range(100):
    ... 	agent = Minesweeper(1, 1, debug=True)
    '''
    def __init__(cls, x, y, size=16, ratio=0.1, debug=False):
        from random import randint

        cls.size, cls.debug = size, debug
        cls._board_ = [['_' for w in range(size)] for h in range(size)]
        bomb = int(ratio*size**2)

        while 0 != bomb:
            pos = [randint(0, size-1) for _ in range(2)]
            if pos == [x, y]:
                continue
            cls._board_[pos[0]][pos[1]] = 'X'
            bomb -= 1
        cls.step(x, y)
    def __str__(cls, hidden=True):
        FMT = '{0} {1}{0}'	## Board, data

        s = cls.size

        header = ['{0}-'.format('-' if 0 != idx%5 else idx%10) for idx in range(cls.size)]
        header = FMT.format('+', ''.join(header))

        data = [['_ ' if hidden and 'X' == _ else '{0} '.format(_) for _ in line] for line in cls._board_]
        data = [''.join(line) for line in data]
        data = [FMT.format('|' if 0 != idx%5 else '{0}'.format(idx%10), line) for idx, line in enumerate(data)]
        data = '\n'.join(data)
        return '{0}\n{1}\n{0}'.format(header, data)
    def __call__(cls):
        try:
            nr = len(cls.safeStep)
            while len(cls) != nr:
                print agent
                try:
                    x, y = raw_input('{0} / {1}> '.format(len(cls), nr)).split()
                except Exception as e:
                    continue
                cls.step(int(x), int(y))
                nr = len(cls.safeStep)
            print 'Win'
        except KeyboardInterrupt as e:
            print 'Ctrl+C'
    def __len__(cls):
        if not hasattr(cls, "bombs"):
            cls.bombs = sum([1 for line in cls._board_ for _ in line if 'X' == _])
        return len(cls.safeStep) - cls.bombs
    def __getitem__(cls, args):
        x, y = args
        def _fn_(x):
            if x.isdigit():
                return int(x)
            elif '_' == x or 'X' == x:
                return -1
            elif ' ' == x:
                return 0
            else:
                raise IOError('[{0}]'.format(x))
        if 0 <= x < cls.size and 0 <= y < cls.size:
            return _fn_(cls._board_[x][y])
        else:
            return 0

    @property
    def safeStep(cls):
        if not hasattr(cls, '_safeStep_'):
            _safeStep_ = [[x, y] for x in range(cls.size) for y in range(cls.size) if 0 > cls[x, y]]

        _safeStep_ = [[x, y] for x, y in _safeStep_ if 0 > cls[x, y]]
        return _safeStep_

    def empty(cls, x, y):
        nr  = [1 for _x in range(x-1, x+2) for _y in (y-1, y+1) if 0 > cls[_x, _y]]
        nr += [1 for _x in (x-1, x+1) if 0 > cls[_x, y]]
        return sum(nr)
    def check(cls, x, y):
        if x < 0 or x >= cls.size or y <0 or y >= cls.size:
            return False
        return 'X' == cls._board_[x][y]
    def step(cls, x, y):
        if 0 == cls._step_(x, y):
            for _x in range(x-1, x+2):
                for _y in range(y-1, y+2):
                    if [_x, _y] == [x, y]:
                        continue
                    cls.step(_x, _y)
    def _step_(cls, x, y):
        if x < 0 or x >= cls.size or y <0 or y >= cls.size:
            ''' Out-of boundary'''
            return -1
        elif 'X' == cls._board_[x][y]:
            cls._board_[x][y] = '@'
            if cls.debug:
                print cls.__str__(False)
            raise IOError("Bomb")
        elif 0 <= cls[x, y]:
            ''' Already done '''
            return -1
        else:
            cnt = [cls.check(_x, _y) for _x in range(x-1, x+2) for _y in range(y-1, y+2)]
            cnt = sum(cnt)
            if cnt:
                cls._board_[x][y] = str(cnt)
            else:
                cls._board_[x][y] = ' '
            return cnt