Pular para o conteúdo principal

Arquitetura de Rede Neural Detalhada

Este artigo analisa em profundidade a arquitetura completa da rede neural do KataGo, desde a codificação de recursos de entrada até o design de múltiplas saídas.


Visão Geral da Arquitetura

O KataGo utiliza um design de rede neural única com múltiplas saídas:


Codificação de Recursos de Entrada

Visão Geral dos Planos de Recursos

O KataGo usa 22 planos de recursos (19×19×22), cada plano é uma matriz 19×19:

PlanoConteúdoDescrição
0Pedras próprias1 = tem pedra própria, 0 = não tem
1Pedras do oponente1 = tem pedra do oponente, 0 = não tem
2Pontos vazios1 = vazio, 0 = tem pedra
3-10Estados históricosMudanças do tabuleiro nas últimas 8 jogadas
11Ponto de Ko1 = aqui é Ko proibido, 0 = pode jogar
12-17Codificação de liberdadesGrupos com 1, 2, 3... liberdades
18-21Codificação de regrasRegras Chinesas/Japonesas, komi, etc.

Empilhamento de Estados Históricos

Para que a rede neural entenda a dinâmica da posição, o KataGo empilha os estados das últimas 8 jogadas:

# Codificação de estados históricos (conceitual)
def encode_history(game_history, current_player):
features = []

for t in range(8): # Últimas 8 jogadas
if t < len(game_history):
board = game_history[-(t+1)]
# Codifica pedras próprias/do oponente naquele momento
features.append(encode_board(board, current_player))
else:
# Histórico insuficiente, preenche com zeros
features.append(np.zeros((19, 19)))

return np.stack(features, axis=0)

Codificação de Regras

O KataGo suporta múltiplas regras, informando a rede neural através de planos de recursos:

# Codificação de regras (conceitual)
def encode_rules(rules, komi):
rule_features = np.zeros((4, 19, 19))

# Tipo de regra (one-hot)
if rules == "chinese":
rule_features[0] = 1.0
elif rules == "japanese":
rule_features[1] = 1.0

# Komi normalizado
normalized_komi = komi / 15.0 # Normalizado para [-1, 1]
rule_features[2] = normalized_komi

# Jogador atual
rule_features[3] = 1.0 if current_player == BLACK else 0.0

return rule_features

Rede Principal: Torre Residual

Estrutura do Bloco Residual

O KataGo usa a estrutura Pre-activation ResNet:

Exemplo de Código

class ResidualBlock(nn.Module):
def __init__(self, channels):
super().__init__()
self.bn1 = nn.BatchNorm2d(channels)
self.conv1 = nn.Conv2d(channels, channels, 3, padding=1)
self.bn2 = nn.BatchNorm2d(channels)
self.conv2 = nn.Conv2d(channels, channels, 3, padding=1)

def forward(self, x):
residual = x

out = self.bn1(x)
out = F.relu(out)
out = self.conv1(out)

out = self.bn2(out)
out = F.relu(out)
out = self.conv2(out)

return out + residual # Conexão residual

Camada de Pooling Global

Uma das inovações-chave do KataGo: adicionar pooling global nos blocos residuais, permitindo que a rede veja informações globais:

class GlobalPoolingBlock(nn.Module):
def __init__(self, channels):
super().__init__()
self.conv = nn.Conv2d(channels, channels, 3, padding=1)
self.fc = nn.Linear(channels, channels)

def forward(self, x):
# Caminho local
local = self.conv(x)

# Caminho global
global_pool = x.mean(dim=[2, 3]) # Pooling médio global
global_fc = self.fc(global_pool)
global_broadcast = global_fc.unsqueeze(2).unsqueeze(3)
global_broadcast = global_broadcast.expand(-1, -1, 19, 19)

# Fusão
return local + global_broadcast

Por que precisamos de pooling global?

Convoluções tradicionais só veem localmente (campo receptivo 3×3). Mesmo empilhando muitas camadas, a percepção de informações globais ainda é limitada. O pooling global permite que a rede "veja" diretamente:

  • A diferença no número de pedras do jogo inteiro
  • A distribuição de influência global
  • A avaliação geral da posição

Design das Cabeças de Saída

Policy Head (Cabeça de Política)

Produz a probabilidade de jogar em cada posição:

class PolicyHead(nn.Module):
def __init__(self, in_channels):
super().__init__()
self.conv = nn.Conv2d(in_channels, 2, 1) # Convolução 1×1
self.bn = nn.BatchNorm2d(2)
self.fc = nn.Linear(2 * 19 * 19, 362) # 361 + pass

def forward(self, x):
out = F.relu(self.bn(self.conv(x)))
out = out.view(out.size(0), -1)
out = self.fc(out)
return F.softmax(out, dim=1) # Distribuição de probabilidade

Formato de saída: Vetor de 362 dimensões

  • Índices 0-360: Probabilidade de jogar em 361 posições do tabuleiro
  • Índice 361: Probabilidade de passar

Value Head (Cabeça de Valor)

Produz a taxa de vitória da posição atual:

class ValueHead(nn.Module):
def __init__(self, in_channels):
super().__init__()
self.conv = nn.Conv2d(in_channels, 1, 1)
self.bn = nn.BatchNorm2d(1)
self.fc1 = nn.Linear(19 * 19, 256)
self.fc2 = nn.Linear(256, 1)

def forward(self, x):
out = F.relu(self.bn(self.conv(x)))
out = out.view(out.size(0), -1)
out = F.relu(self.fc1(out))
out = torch.tanh(self.fc2(out)) # Saída de -1 a +1
return out

Formato de saída: Valor único [-1, +1]

  • +1: Vitória certa própria
  • -1: Vitória certa do oponente
  • 0: Equilíbrio

Score Head (Cabeça de Pontuação)

Exclusivo do KataGo, prevê a diferença final de pontos:

class ScoreHead(nn.Module):
def __init__(self, in_channels):
super().__init__()
self.conv = nn.Conv2d(in_channels, 1, 1)
self.bn = nn.BatchNorm2d(1)
self.fc1 = nn.Linear(19 * 19, 256)
self.fc2 = nn.Linear(256, 1)

def forward(self, x):
out = F.relu(self.bn(self.conv(x)))
out = out.view(out.size(0), -1)
out = F.relu(self.fc1(out))
out = self.fc2(out) # Saída sem restrição
return out

Formato de saída: Valor único (pontos)

  • Positivo: Próprio à frente
  • Negativo: Oponente à frente

Ownership Head (Cabeça de Território)

Prevê a quem pertence cada ponto no final:

class OwnershipHead(nn.Module):
def __init__(self, in_channels):
super().__init__()
self.conv1 = nn.Conv2d(in_channels, 32, 1)
self.bn = nn.BatchNorm2d(32)
self.conv2 = nn.Conv2d(32, 1, 1)

def forward(self, x):
out = F.relu(self.bn(self.conv1(x)))
out = torch.tanh(self.conv2(out)) # Cada ponto de -1 a +1
return out.view(out.size(0), -1) # Achatado para 361

Formato de saída: Vetor de 361 dimensões, cada valor em [-1, +1]

  • +1: Este ponto pertence ao território próprio
  • -1: Este ponto pertence ao território do oponente
  • 0: Área neutra ou disputada

Diferenças em Relação ao AlphaZero

AspectoAlphaZeroKataGo
Cabeças de saída2 (Policy + Value)4 (+ Score + Ownership)
Pooling globalNãoSim
Recursos de entrada17 planos22 planos (incluindo codificação de regras)
Blocos residuaisResNet padrãoPre-activation + Pooling global
Suporte a múltiplas regrasNãoSim (através de codificação de recursos)

Escala do Modelo

O KataGo oferece modelos de diferentes escalas:

ModeloBlocos ResiduaisCanaisParâmetrosCenário de Uso
b10c12810128~5MCPU, testes rápidos
b18c38418384~75MGPU comum
b40c25640256~95MGPU avançada
b60c32060320~200MGPU topo de linha

Convenção de nomes: b{blocos}c{canais}


Implementação Completa da Rede

class KataGoNetwork(nn.Module):
def __init__(self, num_blocks=18, channels=384):
super().__init__()

# Convolução inicial
self.initial_conv = nn.Conv2d(22, channels, 3, padding=1)
self.initial_bn = nn.BatchNorm2d(channels)

# Torre residual
self.residual_blocks = nn.ModuleList([
ResidualBlock(channels) for _ in range(num_blocks)
])

# Blocos de pooling global (inseridos a cada poucos blocos residuais)
self.global_pooling_blocks = nn.ModuleList([
GlobalPoolingBlock(channels) for _ in range(num_blocks // 6)
])

# Cabeças de saída
self.policy_head = PolicyHead(channels)
self.value_head = ValueHead(channels)
self.score_head = ScoreHead(channels)
self.ownership_head = OwnershipHead(channels)

def forward(self, x):
# Convolução inicial
out = F.relu(self.initial_bn(self.initial_conv(x)))

# Torre residual
gp_idx = 0
for i, block in enumerate(self.residual_blocks):
out = block(out)

# Insere pooling global após cada 6 blocos residuais
if (i + 1) % 6 == 0 and gp_idx < len(self.global_pooling_blocks):
out = self.global_pooling_blocks[gp_idx](out)
gp_idx += 1

# Cabeças de saída
policy = self.policy_head(out)
value = self.value_head(out)
score = self.score_head(out)
ownership = self.ownership_head(out)

return {
'policy': policy,
'value': value,
'score': score,
'ownership': ownership
}

Leitura Adicional