Yapay Zeka alanındaki heyecan verici ilerlemelerden biri Google DeepMind şirketinin AlphaGo programının 17 kez Go şampiyonu olmuş Lee Sedol’u yenmesiydi. Fakat DeepMind orada durmadı, mimariyi geliştirerek AlphaGo’yu 100-0 yenecek AlphaGo Zero’yu geliştirdi! Yeni mimarinin ilginç tarafı YZ’nin hiç dış veriye ihtiyaç duymadan eğitilmiş olması. AGZ sıfır kabiliyet ile başlıyor (clean slate), ve kendisiyle oynaya oynaya Go şampiyonlarını yenecek hale geliyor. Bu ve ek ilerlemeleri bu yazıda paylaşıyoruz.
Mimari
Genel olarak AG ve AGZ’nin benzer bazı özellikleri var. Bunlardan ilki Monte Carlo Ağaç Aramasının (Monte Carlo Tree Search -MCTS-) bir derin YSA ile genişletilerek kabiliyetinin ilerletilmiş olması. MCTS konusunu işledik, herhangi bir tahta pozisyonundan başlayarak simülasyon yapılır, ve kazanç / kayıp verisi yukarı alınarak karar mekanizması için kullanılır. Fakat simülasyon yapılırken ve her tahta pozisyonundan hamle seçenekleri üretilirken ne kadar derine inilecek? Oyun bitene kadar inilirse bu özellikle Go gibi bir oyunda çok derin ve geniş bir ağaç ortaya çıkartabilir, bilgisayarlar performans açısından böyle bir ağaçla zorlanırlar. Çözüm belli bir tahtaya bakarak o oyunun kazanılıp kazanılmayacağı hakkında “sezgisel’’ bir karar verebilmek. Bu işi örüntü tanıma üzerinden YSA çok güzel yapabilir. Önceden (kendisiyle oynarken) elde edilen oyun verisine bakarak, tahta pozisyonları ve o oyunun kazanılıp kazanılmadığı verisiyle eğitilen”değer YSA’sı’’ artık yeni bir tahtayı görünce o durumun kazanç şansının olup olmadığını -1,+1 arasında bir değer ile hesaplayabilir. Bu durumda MCTS’in herhangi bir dalda oyunu sonuna kadar simüle etmesine gerek yoktur, belli bir seviye sonra durup YSA’ya kazanç şansını sorar, bu değeri kullanır.
İkinci özellik bir ilke / siyaset / strateji YSA’sı kullanmak, ilke YSA’sı bir tahtayı girdi olarak alıp, yapılabilecek tüm hamleler için bir kazanç olasılığı üretebilir, ilke YSA’sin çıktısı potansiyel olarak tüm tahta hücreleri olabilir [1,3]. MCTS bu çıktıları kazanma olasılığı daha yüksek olan hamle ağaç dallarını simüle etmek için kullanacaktır.
Yazının geri kalanında 9x9 Go boyutlarını referans alacağız, AG ve AGZ 19x19 oyunu üzerinde işliyor.
Bir not düşelim, YSA “bir tahtayı girdi alıyor’’ dedik ama girdi verisi oldukca zenginleştirilmiş halde, sadece (9,9) boyutunda tek bir tahta değil, (9,9,17) boyutunda yani 17 katmanlı bir veri”balyası’’. Bu balyayı hazırlamak için herhangi bir oyun anında tahtaya bakılır, her renk için (siyah/beyaz) 8 katman yaratılır, bu katmanlardan biri o rengin o andaki tahtadaki pozisyonu, diğer 7’si hemen önceki 7 adımdaki aynı rengin pozisyonları, aynı şekilde diğer renk için 8 katman, ve ek bir katman sıranın kimde olduğu. Tüm bunlar istiflenerek toplam 17 katman yaratılır. Herhangi bir anda oyunun durumu bu tensor üzerinden belirtilir. Verinin bu şekilde zenginleştirilmesinin sebebi herhalde zamansal bağlantıları yakalamaya uğraşmak.
AlphaGo Zero
AGZ’nin yaptığı ilerlemeler ise şunlar [3]: AlphaGo değer ve ilke için iki ayrı ağ kullanıyordu. AGZ’de değer ve ilke YZ’leri birleştirilerek tek bir YSA ile iki çıktı üretilmesi sağlanıyor, bu yapıya esprili bir şekilde “iki başlı canavar (two-headed monster)’’ ismi de veriliyor. Bu mimari biraz garip gelebilir, çünkü çoğu uygulamada genellikle tek bir çıktı kullanılır, mesela bir resimde kedi olup olmadığını tahmin edecek bir YZ tek bir ikisel çıktı ile bunu yapabilir, ya da resmin belli kategorilere ait olup olmadığı tek bir vektör çıktısı ile, bu vektörde obje olasılıkları vardır. Fakat biraz düşününce iki farklı çıktılı yapının niye işlediğini anlayabiliriz, sonuçta YZ bir fonksiyonu yaklaşık olarak temsil etmeye çabalar, eğitim sırasında kafa #1 fonksiyonu bir tahmin üretir, ve eğitim o kafanın yaptığı tahmine sonuç veren parametreleri günceller, aynı şekilde kafa #2 için düzeltme yapılır.
İkinci bir AGZ ilerlemesi artıksal ağ (residual network) adı verilen bir derin YSA yapısı kullanmak. Artıksal ağlar pür evrişimsel ağların daha gelişmiş hali, bu yapılarda evrişimsel ağda bölge atlaması yapılarak sonraki bölgelere direk / kısayol bağlantıları koyuluyor [6]. Örnek bir yapı altta,
Üçüncü ilerleme ise AGZ’nin kendisine karşı oynayarak kendini eğitmesi (AG için başkaların oynadığı oyunların kayıtları kullanıldı). DeepMind bilimcileri araştırmaları sırasında şunu anladılar: hangi baz mimariyle başlarsak başlayalım, MCTS onu daha iyileştirir. Bu durumda herhangi bir YZ alınır ki başta ağırlık değerleri rasgele yani hiç bir doğru karar vermez, ama bu YZ, MCTS ile daha iyi performans gösterecektir, bu sırada oyundan eğitim verisi toplanır ve bu veri YZ’yi iyileştirmek için ağa uygulanır, ve işlem başa dönerek devam edilir. Bu sayede en kötü performanstan en iyisine doğru ilerlemek mümkündür.
Eğitim verisinde hedefin ne olduğunu daha iyi vurgulamak gerekirse, değer kafası için bu tek bir değer, ilke kafası için tüm 9x9 tahta pozisyonlarında hangi hamlenin yapılmış olduğu, o hücre 1 diğerleri 0 değerinde olacak. Bu şekilde eğitilen YSA, ilke tahmini üreteceği zaman tüm hücrelerde olasılık değerleri üretecektir.
Alttaki kod [4]’u baz almıştır. Eğitmek için train.py
kullanılır, eğitim sırasında en son YSA belli aralıklarla sürekli
kaydedilecektir, eğitim sonunda ya da yeterince eğitilince işlem
durulabilir, ve gnugo_play.py
kaydedilen aynı modeli
kullanarak GnuGo [1] programına karşı oynatılabilir. YSA olarak biz
ResNet yerine daha basit bir yapı kullandık,
import simplenet
simplenet.PolicyValue.create_network()
Tensor("input_1:0", shape=(?, 17, 9, 9), dtype=float32)
Tensor("conv2d/BiasAdd:0", shape=(?, 17, 9, 64), dtype=float32)
Tensor("batch_normalization/batchnorm/add_1:0", shape=(?, 17, 9, 64), dtype=float32)
Tensor("conv2d_2/BiasAdd:0", shape=(?, 17, 9, 128), dtype=float32)
Tensor("batch_normalization_2/batchnorm/add_1:0", shape=(?, 17, 9, 128), dtype=float32)
------------- value -------------------
Tensor("conv2d_3/BiasAdd:0", shape=(?, 17, 9, 2), dtype=float32)
Tensor("activation_3/Relu:0", shape=(?, 17, 9, 2), dtype=float32)
Tensor("flatten/Reshape:0", shape=(?, 306), dtype=float32)
policy_output Tensor("activation_4/Softmax:0", shape=(?, 82), dtype=float32)
------------- policy -------------------
Tensor("conv2d_4/BiasAdd:0", shape=(?, 17, 9, 1), dtype=float32)
Tensor("activation_5/Relu:0", shape=(?, 17, 9, 1), dtype=float32)
Tensor("flatten_2/Reshape:0", shape=(?, 153), dtype=float32)
Tensor("dense_2/BiasAdd:0", shape=(?, 256), dtype=float32)
Tensor("activation_6/Relu:0", shape=(?, 256), dtype=float32)
Tensor("dense_3/BiasAdd:0", shape=(?, 1), dtype=float32)
Tensor("dense_3/BiasAdd:0", shape=(?, 1), dtype=float32)
Out[1]: <tensorflow.python.keras._impl.keras.engine.training.Model at 0x7fa2dd30de50>
import numpy as np, resource, sys
from operator import itemgetter
1500)
sys.setrecursionlimit(0x10000000, resource.RLIM_INFINITY])
resource.setrlimit(resource.RLIMIT_STACK, [0x100000)
sys.setrecursionlimit(
class TreeNode(object):
"""MCTS agacindaki bir dugum. Her dugum kendi Q degerini, onceki
olasiligi P'yi, ve kac kez ziyaret edildigi sayisiyla duzeltilmis
onsel skoru u'yu biliyor.
"""
def __init__(self, parent, prior_p):
self._parent = parent
self._children = {}
self._n_visits = 0
self._W = 0
self._Q = 0
self._u = prior_p
self._P = prior_p
def printing(self):
print(self._children)
for _, child in self._children.iteritems():
child.printing()
def expand(self, action_priors):
for action, prob in action_priors:
if action not in self._children:
self._children[action] = TreeNode(self, prob)
def select(self):
"""Cocuklar arasinda maksimum aksiyon Q + u(P)'yi vereni sec
"""
return max(self._children.iteritems(),
=lambda act_node: act_node[1].get_value())
key
def update(self, leaf_value, c_puct):
"""Dugumu altindaki cocuklardan gelen irdeleme uzerinden guncelle
Arguments:
leaf_value -- mevcut oyuncu perspektifinden alt agacin degeri
c_puct -- (0, inf) arasinda bir sayi, bu dugumun skoru
uzerinde Q ve P'nun etkisini izafi olarak ayarlar
Returns:
None
"""
self._n_visits += 1
self._W += leaf_value
self._Q = self._W / self._n_visits
if not self.is_root():
self._u = c_puct * self._P * np.sqrt(self._parent._n_visits) / (1 + self._n_visits)
def update_recursive(self, leaf_value, c_puct):
if self._parent:
self._parent.update_recursive(leaf_value, c_puct)
self.update(leaf_value, c_puct)
def get_value(self):
return self._Q + self._u
def is_leaf(self):
return self._children == {}
def is_root(self):
return self._parent is None
class MCTS(object):
def __init__(self, value_fn, policy_fn, c_puct=5, n_playout=1600):
self._root = TreeNode(None, 1.0)
self._value = value_fn
self._policy = policy_fn
self._c_puct = c_puct
self._n_playout = n_playout
def _playout(self, state, self_play):
= self._root
node if not node.is_leaf() and self_play:
= [0.03 for _ in range(len(node._children.items()))]
tmp = np.random.dirichlet(tmp,1)[0]
etas = 0
j for action, child_node in node._children.iteritems():
= 0.75*child_node._P + 0.25*etas[j]
child_node._P += 1
j
while True:
if node.is_leaf():
= self._policy(state)
action_probs # Check for end of game.
if len(action_probs) == 0:
break
if node.is_root() and self_play:
= [0.03 for _ in range(len(action_probs))]
tmp = np.random.dirichlet(tmp,1)[0]
etas = 0
j = []
new_action_probs for action, prob in action_probs:
= 0.75*prob + 0.25*etas[j]
prob
new_action_probs.append((action, prob))+= 1
j = new_action_probs
action_probs
node.expand(action_probs)break
# Greedily select next move.
= node.select()
action, node
state.do_move(action)
# Alt dugumunun degerini YSA'yi kullanarak hesapla
= self._value(state)
leaf_value
self._c_puct)
node.update_recursive(leaf_value,
def get_move(self, state, temperature, self_play):
for n in range(self._n_playout):
= state.copy()
state_copy self._playout(state_copy, self_play)
if temperature > 0:
= self._root._children.items()
childrens = map(list, zip(*childrens))
actions, next_states = [next_state._n_visits for next_state in next_states]
tmp = np.power(tmp,1./temperature)
exponentiated_n_visits = np.divide(exponentiated_n_visits, np.sum(exponentiated_n_visits))
pi = range(len(childrens))
child_idx = np.random.choice(child_idx, p = pi)
child_idx return actions[child_idx]
else : # when temperature is infinitesimal
return max(self._root._children.iteritems(),
=lambda act_node: act_node[1]._n_visits)[0]
key
def update_with_move(self, last_move):
if last_move in self._root._children:
self._root = self._root._children[last_move]
self._root._parent = None
else:
self._root = TreeNode(None, 1.0)
class MCTSPlayer(object):
def __init__(self, value_function, policy_function, c_puct=5, n_playout=1600, evaluating=True, self_play=False):
self.mcts = MCTS(value_function, policy_function, c_puct, n_playout)
self.move_count = 0
self.evaluating = evaluating
self.self_play = self_play
if self.evaluating:
= 0.
temperature else:
= 1.
temperature self.temperature = temperature
self.is_human = False # for playing a game in play.py
def get_move(self, state, self_play=False):
= [move for move in state.get_legal_moves()]
sensible_moves if len(sensible_moves) > 0:
= self.mcts.get_move(state, self.temperature, self.self_play)
move if not self_play:
self.mcts.update_with_move(move)
self.move_count += 1
if not self.evaluating :
if self.move_count == 2:
self.temperature = 0.
return move
self.move_count += 1
if not self.evaluating:
if self.move_count == 2:
self.temperature = 0.
return go.PASS_MOVE
import os, glob, pickle, go
import json, re, util
import numpy as np
from shutil import copy
from mcts import MCTSPlayer
from util import flatten_idx, pprint_board
from tensorflow.contrib.keras import optimizers as O
from tensorflow.contrib.keras import callbacks as C
from tensorflow.contrib.keras import backend as K
import resnet
import simplenet
def self_play_and_save(player, opp_player):
= []
state_list = []
pi_list = []
player_list
= go.GameState(size=9, komi=0)
state
= go.BLACK
player_color = player
current = opp_player
other
= 0
step while not state.is_end_of_game:
= current.get_move(state, self_play=True)
move
= current.mcts._root._children.items()
childrens
= map(list, zip(*childrens))
actions, next_states = [next_state._n_visits for next_state in next_states]
_n_visits if not move == go.PASS_MOVE:
if step < 25: # temperature is considered to be 1
= np.divide(_n_visits, np.sum(_n_visits))
distribution else:
= np.argmax(_n_visits)
max_visit_idx = np.zeros(np.shape(_n_visits))
distribution = 1.0
distribution[max_visit_idx] else: # to prevent the model from overfitting to PASS_MOVE
= np.zeros(np.shape(_n_visits))
distribution
= zip(actions, distribution)
pi
pi_list.append(pi)
state_list.append(state.copy())
current.mcts.update_with_move(move)
state.do_move(move)
other.mcts.update_with_move(move)= other, current
current, other += 1
step
= state.get_winner()
winner print 'winner', winner
# oyun bitti kimin kazandigini biliyoruz, mesela siyah kazandiysa
# odulleri hamle bazinda +1,-1,+1,.. olacak sekilde ata, beyaz
# kazandiysa -1,+1,-1 seklinde. Siyah olunca +1 cunku oyuna hep siyah
# basliyor.
if winner == go.BLACK:
= [(-1.)**j for j in range(len(state_list))]
reward_list else : # winner == go.WHITE:
= [(-1.)**(j+1) for j in range(len(state_list))]
reward_list return state_list, pi_list, reward_list
def self_play_and_train(cmd_line_args=None):
# iki farkli ag yarat, egitim bunlardan ilkini gunceller.
# belli bir sure sonra oteki YSA ilkinin kaydettigi veriden guncellenir,
# ve ikisi tekrar esit hale gelir, bu boyle devam eder.
= simplenet.PolicyValue(simplenet.PolicyValue.create_network())
policy = simplenet.PolicyValue(simplenet.PolicyValue.create_network())
opp_policy
def lr_scheduler(epoch):
if epoch == 5000:
.001)
K.set_value(model.optimizer.lr, elif epoch == 7000:
.0001)
K.set_value(model.optimizer.lr, return K.get_value(model.optimizer.lr)
= C.LearningRateScheduler(lr_scheduler)
change_lr = O.SGD(lr=.01, momentum=0.9)
sgd compile(loss=['categorical_crossentropy','mean_squared_error'],
policy.model.=sgd)
optimizer
= 50
batch_size = 10 # her oyundan kac veri noktasi alalim
n_pick
for epoch in range(1000):
= []
state_list2 = []
pi_list2 = []
reward_list2
while True: # batch_size kadar veri toplayincaya kadar oyna
try:
= MCTSPlayer(policy.eval_value_state,
player
policy.eval_policy_state,=50, evaluating=False,
n_playout=True)
self_play
= MCTSPlayer(opp_policy.eval_value_state,
opp_player
opp_policy.eval_policy_state,=50, evaluating=False,
n_playout=True)
self_play
= self_play_and_save(
state_list, pi_list, reward_list
opp_player, player
)
# oyunda atilan tum adimlar, sonuclar state_list,
# pi_list, reward_list listesi icinde. Simdi rasgele
# n_pick tane veri noktasi her oyun kaydindan cekip
# cikartilir.
= [np.random.choice(range(10,len(state_list)),replace=False) \
idxs for i in range(n_pick)]
print 'picked results', idxs
for idx in idxs:
state_list2.append(state_list[idx])
pi_list2.append(pi_list[idx])
reward_list2.append(reward_list[idx])
if len(state_list2) >= batch_size: break
except:
print 'exception'
continue
= np.zeros((batch_size, 9*9+1))
pout = np.zeros((batch_size, 1))
vout = [pout, vout]
Y = np.zeros((batch_size, 17, 9, 9))
X
for i in range(len(state_list2)):
= reward_list2[i]
vout[i,:] = util.get_board(state_list2[i])
X[i, :] = util.to_pi_mat(pi_list2[i])
pout[i,:]
policy.model.fit(X, Y)
if epoch % 5 == 0:
print 'saving'
policy.save()
if epoch % 50 == 0:
print 'birincinin en son kayitli agirliklarindan bu agi guncelle'
opp_policy.load()
if __name__ == '__main__':
self_play_and_train()
from tensorflow.contrib.keras import regularizers as R
from tensorflow.contrib.keras import models as M
from tensorflow.contrib.keras import layers as L
from tensorflow.contrib.keras import backend as K
from util import flatten_idx, random_transform, idx_transformations
import numpy as np, util, random
= "/tmp/alphago-zero.h5"
mfile
class PolicyValue:
def __init__(self, model):
self.model = model
def save(self):
self.model.save_weights(mfile)
def load(self):
self.model.load_weights(mfile)
def eval_policy_state(self, state):
= util.get_board(state).reshape(1, 17, 9, 9)
x = self.model.predict(x)[0][0]
probs1 = probs1[1:].reshape(9,9)
probs2 = [(action,probs2[action]) \
res_probs for action in state.get_legal_moves() if action]
None, probs1[0]))
res_probs.append((return res_probs
def eval_value_state(self, state):
= util.get_board(state).reshape(1, 17, 9, 9)
x return self.model.predict(x)[1][0][0]
@staticmethod
def create_network(**kwargs):
= L.Input(shape=(17, 9, 9))
model_input print model_input
= L.Convolution2D(
convolution_path =(),
input_shape=64,
filters=3,
kernel_size='linear',
activation='same',
padding=R.l2(.0001),
kernel_regularizer=R.l2(.0001))(model_input)
bias_regularizerprint convolution_path
= L.BatchNormalization(
convolution_path =R.l2(.0001),
beta_regularizer=R.l2(.0001))(convolution_path)
gamma_regularizerprint convolution_path
= L.Activation('relu')(convolution_path)
convolution_path
= L.Convolution2D(
convolution_path =(),
input_shape=128,
filters=3,
kernel_size='linear',
activation='same',
padding=R.l2(.0001),
kernel_regularizer=R.l2(.0001))(convolution_path)
bias_regularizerprint convolution_path
= L.BatchNormalization(
convolution_path =R.l2(.0001),
beta_regularizer=R.l2(.0001))(convolution_path)
gamma_regularizerprint convolution_path
= L.Activation('relu')(convolution_path)
convolution_path
print '------------- value -------------------'
# policy head
= L.Convolution2D(
policy_path =(),
input_shape=2,
filters=1,
kernel_size='linear',
activation='same',
padding=R.l2(.0001),
kernel_regularizer=R.l2(.0001))(convolution_path)
bias_regularizerprint policy_path
= L.BatchNormalization(
policy_path =R.l2(.0001),
beta_regularizer=R.l2(.0001))(policy_path)
gamma_regularizer= L.Activation('relu')(policy_path)
policy_path print policy_path
= L.Flatten()(policy_path)
policy_path print policy_path
= L.Dense(
policy_path 9*9)+1,
(=R.l2(.0001),
kernel_regularizer=R.l2(.0001))(policy_path)
bias_regularizer= L.Activation('softmax')(policy_path)
policy_output print 'policy_output', policy_output
print '------------- policy -------------------'
# value head
= L.Convolution2D(
value_path =(),
input_shape=1,
filters=1,
kernel_size='linear',
activation='same',
padding=R.l2(.0001),
kernel_regularizer=R.l2(.0001))(convolution_path)
bias_regularizerprint value_path
= L.BatchNormalization(
value_path =R.l2(.0001),
beta_regularizer=R.l2(.0001))(value_path)
gamma_regularizer= L.Activation('relu')(value_path)
value_path print value_path
= L.Flatten()(value_path)
value_path print value_path
= L.Dense(
value_path 256,
=R.l2(.0001),
kernel_regularizer=R.l2(.0001))(value_path)
bias_regularizerprint value_path
= L.Activation('relu')(value_path)
value_path print value_path
= L.Dense(
value_path 1,
=R.l2(.0001),
kernel_regularizer=R.l2(.0001))(value_path)
bias_regularizerprint value_path
= L.Activation('tanh')(value_path)
value_output print value_path
return M.Model(inputs=[model_input], outputs=[policy_output, value_output])
İki YSA kendisine karşı oynarken aynı ağırlıklara sahip iki farklı YSA ile başlıyoruz, ve oyun sonuçlarını kullanarak sadece birini güncelliyoruz. Eğer her toptan demeti (minibatch) ile her iki YSA’yı güncelleseydik birbirlerinden farklılaşmaları zorlaşabilirdi. Ama döngünün daha ileri bir noktasında esitleme yapariz yine de, ilk YSA’yı güncellenenin ağırlıklarını diskten okuyarak güncelliyoruz, ve böyle devam ediyor. AGZ tasarımcıları “en iyi’’ olan ağırlıklara geçmeden yeni YSA’nın diğerini yüzde 55’ten fazla yenmesi şartını koymuşlar, tam donanımla testleri yapanlar bunu takip ederse iyi olur.
Üstteki mimari birkaç saat eğitim sonrası GnuGo (kendi YZ’si olan bir
dış Go programı) başlangıç, orta seviyelerini yenebiliyor. Okuyucular,
grafik kartlı güçlü bir mimaride, ya üstteki YSA’yı derinleştirerek
(daha fazla evrişim filtresi ekleyerek), ya da resnet.py
ile, ve n_play
’i arttırarak daha fazla simülasyonla
sonuçları daha da iyileştirmeye uğraşabilirler.
Kaynaklar
[1] Bayramlı, Go Oyunu, GnuGo, https://burakbayramli.github.io/dersblog/sk/2018/02/go-gnugo.html
[2] Silver, Mastering the game of Go with deep neural networks and tree search, https://www.nature.com/articles/nature16961
[3] Weidman, The 3 Tricks That Made AlphaGo Zero Work, https://hackernoon.com/the-3-tricks-that-made-alphago-zero-work-f3d47b6686ef
[4] Yi, A reproduction of Alphago Zero in ‘Mastering the game of Go without human knowledge’, https://github.com/sangyi92/alphago_zero
[5] AlphaGo Zero Cheat Sheet, https://applied-data.science/static/main/res/alpha_go_zero_cheat_sheet.png
[6] Kristiadi, Residual Net, https://wiseodd.github.io/techblog/2016/10/13/residual-net/