Upload folder using huggingface_hub
Browse files- .DS_Store +0 -0
- M1.h5 +3 -0
- README.md +126 -0
- model.py +146 -0
- model.yaml +11 -0
- requirements.txt +2 -0
.DS_Store
ADDED
|
Binary file (6.15 kB). View file
|
|
|
M1.h5
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:e12389b383ddcd573405078048eaeea3fd8c370dea067283a28eea0e9aa206c1
|
| 3 |
+
size 20625576
|
README.md
CHANGED
|
@@ -1,3 +1,129 @@
|
|
| 1 |
---
|
| 2 |
license: mit
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
license: mit
|
| 3 |
+
library_name: tensorflow
|
| 4 |
+
tags:
|
| 5 |
+
- othello
|
| 6 |
+
- reversi
|
| 7 |
+
- game-ai
|
| 8 |
+
- transformer
|
| 9 |
+
- mixture-of-experts
|
| 10 |
---
|
| 11 |
+
|
| 12 |
+
## Transformer Reversi AI
|
| 13 |
+
This Reversi AI is a Transformer-based model that uses Mixture-of-Experts architecture for decision-making instead of CNNs.
|
| 14 |
+
|
| 15 |
+
## Architecture
|
| 16 |
+
|
| 17 |
+
- Parameters: 1.68M
|
| 18 |
+
- Model size: 20.6MB
|
| 19 |
+
|
| 20 |
+
- Input: `(8, 8, 2)`
|
| 21 |
+
- Own stones
|
| 22 |
+
- Opponent stones
|
| 23 |
+
- Board representation: 64 tokens
|
| 24 |
+
- Embedding dimension: 128
|
| 25 |
+
- Transformer blocks: 4
|
| 26 |
+
- Attention heads: 4
|
| 27 |
+
- MHA experts: 2
|
| 28 |
+
- FFN experts: 2
|
| 29 |
+
- Routing: softmax-based dynamic routing
|
| 30 |
+
- Iterative routing steps: 2
|
| 31 |
+
- Normalization: Pre-LN LayerNorm
|
| 32 |
+
- FFN activation: GELU
|
| 33 |
+
|
| 34 |
+
### Diagram
|
| 35 |
+
```
|
| 36 |
+
Input (8×8×2)
|
| 37 |
+
↓
|
| 38 |
+
64 Tokens
|
| 39 |
+
↓
|
| 40 |
+
Dense 128
|
| 41 |
+
↓
|
| 42 |
+
Token + Row + Column + Move Embeddings
|
| 43 |
+
↓
|
| 44 |
+
DynamicAssembly × 4
|
| 45 |
+
├─ MHA Expert × 2
|
| 46 |
+
└─ FFN Expert × 2
|
| 47 |
+
↓
|
| 48 |
+
├─ Policy Head → 64 moves
|
| 49 |
+
└─ Value Head → Win-rate
|
| 50 |
+
```
|
| 51 |
+
|
| 52 |
+
### DynamicAssembly
|
| 53 |
+
|
| 54 |
+
Each DynamicAssembly block contains a shared pool of MHA and FFN
|
| 55 |
+
experts. At each iteration step, the mean-pooled board representation
|
| 56 |
+
is combined with a step embedding and passed to a router.
|
| 57 |
+
|
| 58 |
+
The router produces a probability distribution over the expert pool.
|
| 59 |
+
All experts are evaluated and their outputs are combined using the
|
| 60 |
+
router probabilities.
|
| 61 |
+
|
| 62 |
+
### Policy Head
|
| 63 |
+
|
| 64 |
+
The policy head predicts a probability distribution over the 64
|
| 65 |
+
board positions.
|
| 66 |
+
|
| 67 |
+
### Value Head
|
| 68 |
+
|
| 69 |
+
The value head predicts a scalar value in `[-1, 1]`, representing
|
| 70 |
+
the estimated game outcome.
|
| 71 |
+
|
| 72 |
+
## Training
|
| 73 |
+
|
| 74 |
+
This model was trained using self-play game records generated by a previous CNN-based Othello AI.
|
| 75 |
+
|
| 76 |
+
### Self-Play data generation
|
| 77 |
+
- MCTS simulations: 600
|
| 78 |
+
- MCTS batch size: 100
|
| 79 |
+
- PUCT constant: 2.3
|
| 80 |
+
- Total games: 10,000
|
| 81 |
+
|
| 82 |
+
### Model Training
|
| 83 |
+
- Optimizer: AdamW
|
| 84 |
+
- Clipnorm: 1.0
|
| 85 |
+
- Weight decay: 0.05
|
| 86 |
+
- Learning rate: 6.0e-5
|
| 87 |
+
- Learning rate scheduler: ReduceLROnPlateau
|
| 88 |
+
- Training batch size: 256
|
| 89 |
+
- Epochs: 75
|
| 90 |
+
|
| 91 |
+
## Evaluation
|
| 92 |
+
|
| 93 |
+
A 63M-parameter ResNet CNN model was used as the previous model.
|
| 94 |
+
|
| 95 |
+
### Win rate
|
| 96 |
+
- vs Random : 100% (100 / 100 games)
|
| 97 |
+
- vs Prev model : 63% (63 / 100 games)
|
| 98 |
+
|
| 99 |
+
### Average final stone count
|
| 100 |
+
- vs Random : 58
|
| 101 |
+
- vs Prev model : 47
|
| 102 |
+
|
| 103 |
+
## Usage / Quick start
|
| 104 |
+
```python
|
| 105 |
+
import numpy as np
|
| 106 |
+
import tensorflow as tf
|
| 107 |
+
from huggingface_hub import hf_hub_download
|
| 108 |
+
from model import TokenAndPositionEmbedding, MHA, FFN, DynamicAssembly
|
| 109 |
+
|
| 110 |
+
model_path = hf_hub_download(repo_id="rsu/Reversi-Transformer-1.6M_v2025-12-17", filename="M1.h5", repo_type="model")
|
| 111 |
+
|
| 112 |
+
custom_objects = {
|
| 113 |
+
"TokenAndPositionEmbedding": TokenAndPositionEmbedding,
|
| 114 |
+
"MHA": MHA,
|
| 115 |
+
"FFN": FFN,
|
| 116 |
+
"DynamicAssembly": DynamicAssembly,
|
| 117 |
+
}
|
| 118 |
+
model = tf.keras.models.load_model(model_path, custom_objects=custom_objects, compile=False)
|
| 119 |
+
|
| 120 |
+
board = np.zeros((1, 8, 8, 2), dtype=np.float32)
|
| 121 |
+
board[0, 3, 3, 1] = 1.0
|
| 122 |
+
board[0, 3, 4, 1] = 1.0
|
| 123 |
+
board[0, 3, 3, 1] = 1.0
|
| 124 |
+
board[0, 4, 4, 1] = 1.0
|
| 125 |
+
|
| 126 |
+
policy, value = model(board, training=False)
|
| 127 |
+
print("Policy (64 moves prob): ", policy.numpy())
|
| 128 |
+
print("Value (win rate est [-1, 1]): ", value.numpy().item())
|
| 129 |
+
```
|
model.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import tensorflow as tf
|
| 2 |
+
import keras
|
| 3 |
+
from keras import layers, models, ops
|
| 4 |
+
|
| 5 |
+
class TokenAndPositionEmbedding(layers.Layer):
|
| 6 |
+
def __init__(self, d_model, moves=64, **kwargs):
|
| 7 |
+
if 'position' in kwargs:
|
| 8 |
+
kwargs.pop('position')
|
| 9 |
+
super(TokenAndPositionEmbedding, self).__init__(**kwargs)
|
| 10 |
+
self.d_model = d_model
|
| 11 |
+
self.moves = moves
|
| 12 |
+
self.row_embedding = layers.Embedding(8, d_model, name="row_emb")
|
| 13 |
+
self.col_embedding = layers.Embedding(8, d_model, name="col_emb")
|
| 14 |
+
self.time_embedding = layers.Embedding(moves + 1, d_model, name="time_emb")
|
| 15 |
+
|
| 16 |
+
def call(self, inputs):
|
| 17 |
+
x, board = inputs
|
| 18 |
+
|
| 19 |
+
positions = tf.range(start=0, limit=64, delta=1, dtype=tf.int32)
|
| 20 |
+
r_emb = self.row_embedding(positions // 8)
|
| 21 |
+
c_emb = self.col_embedding(positions % 8)
|
| 22 |
+
|
| 23 |
+
stone_count = tf.reduce_sum(board, axis=[1, 2, 3])
|
| 24 |
+
current_moves = tf.cast(stone_count, tf.int32) - 4
|
| 25 |
+
current_moves = tf.maximum(current_moves, 0)
|
| 26 |
+
|
| 27 |
+
t_emb = self.time_embedding(current_moves)
|
| 28 |
+
t_emb = tf.expand_dims(t_emb, axis=1)
|
| 29 |
+
|
| 30 |
+
return x + tf.cast(r_emb, x.dtype) + tf.cast(c_emb, x.dtype) + tf.cast(t_emb, x.dtype)
|
| 31 |
+
|
| 32 |
+
class MHA(layers.Layer):
|
| 33 |
+
def __init__(self, d_model, num_heads, rate=0.2, **kwargs):
|
| 34 |
+
super().__init__(**kwargs)
|
| 35 |
+
self.att = layers.MultiHeadAttention(num_heads=num_heads, key_dim=d_model//num_heads)
|
| 36 |
+
self.layernorm = layers.LayerNormalization(epsilon=1e-6)
|
| 37 |
+
self.dropout = layers.Dropout(rate)
|
| 38 |
+
|
| 39 |
+
def call(self, x, training=False):
|
| 40 |
+
x_f32 = tf.cast(x, tf.float32)
|
| 41 |
+
normed_inputs = self.layernorm(x_f32)
|
| 42 |
+
attn_output = self.att(
|
| 43 |
+
query = normed_inputs,
|
| 44 |
+
value = normed_inputs,
|
| 45 |
+
key = normed_inputs,
|
| 46 |
+
training = training
|
| 47 |
+
)
|
| 48 |
+
attn_output = self.dropout(attn_output, training=training)
|
| 49 |
+
return x_f32 + tf.cast(attn_output, tf.float32)
|
| 50 |
+
|
| 51 |
+
class FFN(layers.Layer):
|
| 52 |
+
def __init__(self, d_model, rate=0.2, **kwargs):
|
| 53 |
+
super().__init__(**kwargs)
|
| 54 |
+
ff_dim = d_model * 4
|
| 55 |
+
self.ffn = models.Sequential([layers.Dense(ff_dim, activation='gelu'),layers.Dense(d_model)])
|
| 56 |
+
self.layernorm = layers.LayerNormalization(epsilon=1e-6)
|
| 57 |
+
self.dropout = layers.Dropout(rate)
|
| 58 |
+
|
| 59 |
+
def call(self, x, training=False):
|
| 60 |
+
x_f32 = tf.cast(x, tf.float32)
|
| 61 |
+
normed_inputs = self.layernorm(x_f32)
|
| 62 |
+
ffn_output = self.ffn(normed_inputs)
|
| 63 |
+
ffn_output = self.dropout(ffn_output, training=training)
|
| 64 |
+
return x_f32 + tf.cast(ffn_output, tf.float32)
|
| 65 |
+
|
| 66 |
+
class DynamicAssembly(layers.Layer):
|
| 67 |
+
def __init__(self, d_model, num_heads, num_mha=4, num_ffn=4, steps=8, rate=0.2, **kwargs):
|
| 68 |
+
super().__init__(**kwargs)
|
| 69 |
+
self.d_model = d_model
|
| 70 |
+
self.steps = steps
|
| 71 |
+
self.num_options = num_mha + num_ffn
|
| 72 |
+
self.layer_pool = []
|
| 73 |
+
for i in range(num_mha):
|
| 74 |
+
self.layer_pool.append(MHA(d_model, num_heads, rate, name=f"pool_mha_{i}"))
|
| 75 |
+
for i in range(num_ffn):
|
| 76 |
+
self.layer_pool.append(FFN(d_model, rate, name=f"pool_ffn_{i}"))
|
| 77 |
+
self.router_dense = layers.Dense(self.num_options, name="router")
|
| 78 |
+
self.step_embedding = layers.Embedding(steps, d_model)
|
| 79 |
+
self.last_probs = []
|
| 80 |
+
|
| 81 |
+
def call(self, x, training=False):
|
| 82 |
+
if not training:
|
| 83 |
+
self.last_probs = []
|
| 84 |
+
|
| 85 |
+
for i in range(self.steps):
|
| 86 |
+
step_vec = self.step_embedding(tf.convert_to_tensor([i]))
|
| 87 |
+
x_pooled = tf.reduce_mean(x, axis=1)
|
| 88 |
+
|
| 89 |
+
router_input = x_pooled + tf.cast(step_vec, x_pooled.dtype)
|
| 90 |
+
logits = self.router_dense(router_input)
|
| 91 |
+
probs = tf.nn.softmax(logits, axis=-1)
|
| 92 |
+
|
| 93 |
+
if not training:
|
| 94 |
+
self.last_probs.append(probs)
|
| 95 |
+
|
| 96 |
+
outputs = [layer(x, training=training) for layer in self.layer_pool]
|
| 97 |
+
stacked_outputs = tf.stack(outputs, axis=1)
|
| 98 |
+
|
| 99 |
+
probs_bc = tf.expand_dims(probs, axis=-1)
|
| 100 |
+
probs_bc = tf.expand_dims(probs_bc, axis=-1)
|
| 101 |
+
probs_bc = tf.cast(probs_bc, stacked_outputs.dtype)
|
| 102 |
+
|
| 103 |
+
weighted_sum = tf.reduce_sum(stacked_outputs * probs_bc, axis=1)
|
| 104 |
+
|
| 105 |
+
x = tf.cast(weighted_sum, x.dtype)
|
| 106 |
+
return x
|
| 107 |
+
|
| 108 |
+
def build_model(config):
|
| 109 |
+
d_model = config.get('embed_dim', 32)
|
| 110 |
+
num_blocks = config.get('block', 2)
|
| 111 |
+
num_heads = config.get('head', 4)
|
| 112 |
+
num_mha = config.get('num_mha', 2)
|
| 113 |
+
num_ffn = config.get('num_ffn', 2)
|
| 114 |
+
steps = config.get('steps', 3)
|
| 115 |
+
dropout_rate = config.get('dropout', 0.2)
|
| 116 |
+
input_shape = (8, 8, 2)
|
| 117 |
+
|
| 118 |
+
inputs = layers.Input(shape=input_shape, dtype=tf.float32)
|
| 119 |
+
x = layers.Reshape((64, 2))(inputs)
|
| 120 |
+
x = layers.Dense(d_model)(x)
|
| 121 |
+
x = TokenAndPositionEmbedding(d_model, 64)([x, inputs])
|
| 122 |
+
|
| 123 |
+
for _ in range(num_blocks):
|
| 124 |
+
x = DynamicAssembly(d_model, num_heads, num_mha=num_mha, num_ffn=num_ffn, steps=steps, rate=dropout_rate)(x)
|
| 125 |
+
|
| 126 |
+
# Policy Head
|
| 127 |
+
policy_x = layers.Dense(d_model, activation='relu', name="policy_hidden")(x)
|
| 128 |
+
policy_logits = layers.Dense(1, name="policy_logits")(policy_x)
|
| 129 |
+
policy_logits = layers.Reshape((64,))(policy_logits)
|
| 130 |
+
policy_head = layers.Activation('softmax', name='p', dtype='float32')(policy_logits)
|
| 131 |
+
|
| 132 |
+
# Value Head (M1.h5 actual structure: Conv1D -> Flatten -> Dense(128))
|
| 133 |
+
value_x = layers.Conv1D(8, 1, activation='relu')(x)
|
| 134 |
+
value_x = layers.Flatten()(value_x)
|
| 135 |
+
value_x = layers.Dense(128, activation='relu', name="value_hidden")(value_x)
|
| 136 |
+
value_head = layers.Dense(1, activation='tanh', name='v', dtype='float32')(value_x)
|
| 137 |
+
|
| 138 |
+
model = models.Model(inputs=inputs, outputs=[policy_head, value_head])
|
| 139 |
+
|
| 140 |
+
return model
|
| 141 |
+
|
| 142 |
+
if __name__ == '__main__':
|
| 143 |
+
conf = {'embed_dim': 128, 'block': 4, 'head': 4, 'num_mha': 2, 'num_ffn': 2, 'steps': 2}
|
| 144 |
+
model = build_model(conf)
|
| 145 |
+
model.summary()
|
| 146 |
+
print(f"Total Params: {model.count_params()}")
|
model.yaml
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
default_model: m1
|
| 2 |
+
|
| 3 |
+
m1:
|
| 4 |
+
arch: moe_1
|
| 5 |
+
embed_dim: 128
|
| 6 |
+
block: 4
|
| 7 |
+
head: 4
|
| 8 |
+
num_mha: 2
|
| 9 |
+
num_ffn: 2
|
| 10 |
+
steps: 2
|
| 11 |
+
model_save_path: "models/TF/M1.h5"
|
requirements.txt
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
tensorflow
|
| 2 |
+
numpy
|