تمثيل حالة اللوحة
قبل تصميم ذكاء اصطناعي للغو، يجب أولاً حل مشكلة أساسية: كيف نجعل الحاسوب "يفهم" اللوحة؟
تبدو هذه المشكلة بسيطة، لكنها تتضمن هياكل البيانات والخوارزميات وتصميم مدخلات الشبكة العصبية على مستويات متعددة. ستبدأ هذه المقالة من المصفوفة ثنائية الأبعاد الأساسية، وتقدم تدريجياً طرق تمثيل اللوحة المختلفة التي يستخدمها ذكاء الغو الاصطناعي، وأخيراً توضح كيف قام AlphaGo بترميز اللوحة إلى صيغة يمكن للشبكة العصبية معالجتها.
تمثيل المصفوفة ثنائية الأبعاد: الطريقة الأكثر سهولة
المفهوم الأساسي
لوحة الغو هي شبكة مربعات 19×19، والطريقة الأكثر سهولة للتمثيل هي استخدام المصفوفة ثنائية الأبعاد:
import numpy as np
# ثوابت اللوحة
BOARD_SIZE = 19
EMPTY = 0
BLACK = 1
WHITE = 2
# تهيئة لوحة فارغة
board = np.zeros((BOARD_SIZE, BOARD_SIZE), dtype=np.int8)
# وضع حجر: أسود في D4 (3, 3)
board[3, 3] = BLACK
# وضع حجر: أبيض في Q16 (15, 3)
board[15, 3] = WHITE
نظام الإحداثيات
يستخدم الغو نظامين شائعين للإحداثيات:
1. الإحداثيات الرقمية (للبرمجة)
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
0 . . . . . . . . . . . . . . . . . . .
1 . . . . . . . . . . . . . . . . . . .
2 . . . . . . . . . . . . . . . . . . .
3 . . . ● . . . . . + . . . . . ○ . . .
...
2. إحداثيات الحروف والأرقام (لسجلات المباريات)
A B C D E F G H J K L M N O P Q R S T
19 . . . . . . . . . . . . . . . . . . .
18 . . . . . . . . . . . . . . . . . . .
17 . . . . . . . . . . . . . . . . . . .
16 . . . ● . . . . . + . . . . . ○ . . .
...
ملاحظة: الإحداثيات الحرفية تتخطى "I" (لتجنب الخلط مع الرقم 1).
دوال تحويل الإحداثيات
def coord_to_index(coord):
"""
تحويل إحداثيات السجل إلى فهرس المصفوفة
مثال: 'D4' -> (3, 15)
"""
letters = 'ABCDEFGHJKLMNOPQRST' # تخطي I
col = letters.index(coord[0].upper())
row = BOARD_SIZE - int(coord[1:])
return row, col
def index_to_coord(row, col):
"""
تحويل فهرس المصفوفة إلى إحداثيات السجل
مثال: (3, 15) -> 'Q16'
"""
letters = 'ABCDEFGHJKLMNOPQRST'
return f"{letters[col]}{BOARD_SIZE - row}"
تحليل التعقيد المكاني
التعقيد المكاني لتمثيل المصفوفة ثنائية الأبعاد:
| طريقة التمثيل | حجم كل خانة | المساحة الإجمالية |
|---|---|---|
int8 | 1 بايت | 361 بايت |
int32 | 4 بايت | 1.4 كيلوبايت |
float64 | 8 بايت | 2.9 كيلوبايت |
بالنسبة للحواسيب الحديثة، هذه الذاكرة ليست مشكلة على الإطلاق. لكن عند الحاجة لتخزين عدد كبير من حالات اللوحة (مثل شجرة بحث MCTS)، يمكن لاستخدام int8 تقليل استخدام الذاكرة بشكل كبير.
العمليات الأساسية
class Board:
def __init__(self, size=19):
self.size = size
self.board = np.zeros((size, size), dtype=np.int8)
self.current_player = BLACK
self.ko_point = None # نقطة الكو المحظورة
self.move_history = []
def is_valid_move(self, row, col):
"""التحقق من قانونية الحركة"""
# 1. الموقع داخل اللوحة
if not (0 <= row < self.size and 0 <= col < self.size):
return False
# 2. الموقع فارغ
if self.board[row, col] != EMPTY:
return False
# 3. ليست نقطة كو محظورة
if self.ko_point == (row, col):
return False
# 4. ليست انتحاراً (إلا إذا كان يمكن الأسر)
# ... يتطلب فحصاً أكثر تعقيداً
return True
def place_stone(self, row, col):
"""وضع حجر"""
if not self.is_valid_move(row, col):
return False
self.board[row, col] = self.current_player
self.move_history.append((row, col))
# معالجة الأسر
captures = self._remove_captured_stones(row, col)
# تحديث نقطة الكو
self._update_ko(row, col, captures)
# تبديل اللاعب
self.current_player = WHITE if self.current_player == BLACK else BLACK
return True
Zobrist Hashing: التعرف السريع على الأوضاع المتكررة
خلفية المشكلة
في ذكاء الغو الاصطناعي، نحتاج في كثير من الأحيان لتحديد "هل وضعان متماثلان":
- قاعدة الكو: لا يمكن إعادة اللوحة إلى وضع الحركة السابقة
- الكو الثلاثي الدوري: معالجة قاعدة خاصة
- جدول التبديل: تذكر الأوضاع التي تم البحث فيها
- ذاكرة التخزين المؤقت للشبكة العصبية: تجنب الحسابات المكررة
إذا قارنا خانة بخانة في كل مرة، نحتاج إلى وقت O(361). هل هناك طريقة أسرع؟
مبدأ Zobrist Hashing
Zobrist Hashing (المعروف أيضاً بمفاتيح Zobrist) هي طريقة تجزئة ذكية، اقترحها Albert Zobrist في عام 1970. فكرتها الأساسية هي:
- إنشاء رقم عشوائي مسبقاً لكل مجموعة "موقع × لون"
- قيمة تجزئة اللوحة = XOR لجميع الأرقام العشوائية للمواقع التي بها أحجار
- وضع/إزالة حجر يتطلب عملية XOR واحدة فقط لتحديث قيمة التجزئة
جدول الأرقام العشوائية
import numpy as np
# إنشاء جدول الأرقام العشوائية
# استخدام بذرة ثابتة لضمان قابلية التكرار
np.random.seed(42)
# رقم عشوائي لكل موقع وكل لون
# zobrist[color][row][col]
zobrist_table = np.random.randint(
0, 2**64,
size=(3, BOARD_SIZE, BOARD_SIZE), # 3: EMPTY, BLACK, WHITE
dtype=np.uint64
)
# رقم عشوائي لمن يلعب
zobrist_player = np.random.randint(0, 2**64, dtype=np.uint64)
خصائص عملية XOR
عملية XOR (الأو الحصرية) لها عدة خصائص مهمة تجعلها مناسبة جداً لهذا التطبيق:
- الانعكاسية: A ⊕ A = 0 (نفس الرقم XOR نفسه يساوي 0)
- القابلية للعكس: A ⊕ B ⊕ B = A (XOR مرتين يساوي لا شيء)
- التبديلية: A ⊕ B = B ⊕ A
- التجميعية: (A ⊕ B) ⊕ C = A ⊕ (B ⊕ C)
هذا يعني:
- وضع حجر:
hash ^= zobrist[color][row][col] - إزالة حجر:
hash ^= zobrist[color][row][col](نفس العملية!)
التنفيذ
class ZobristBoard:
def __init__(self, size=19):
self.size = size
self.board = np.zeros((size, size), dtype=np.int8)
self.current_player = BLACK
self.hash = np.uint64(0)
# تهيئة جدول الأرقام العشوائية
self._init_zobrist_table()
def _init_zobrist_table(self):
np.random.seed(12345)
self.zobrist = np.random.randint(
0, 2**64,
size=(3, self.size, self.size),
dtype=np.uint64
)
self.zobrist_player = np.random.randint(0, 2**64, dtype=np.uint64)
def place_stone(self, row, col, color):
"""وضع حجر وتحديث قيمة التجزئة"""
# إزالة الحالة القديمة (إذا كان هناك حجر)
old_color = self.board[row, col]
if old_color != EMPTY:
self.hash ^= self.zobrist[old_color, row, col]
# إضافة الحجر الجديد
self.board[row, col] = color
self.hash ^= self.zobrist[color, row, col]
def remove_stone(self, row, col):
"""إزالة حجر وتحديث قيمة التجزئة"""
color = self.board[row, col]
if color != EMPTY:
self.hash ^= self.zobrist[color, row, col]
self.board[row, col] = EMPTY
def switch_player(self):
"""تبديل اللاعب وتحديث قيمة التجزئة"""
self.hash ^= self.zobrist_player
self.current_player = WHITE if self.current_player == BLACK else BLACK
def get_hash(self):
"""الحصول على قيمة تجزئة الوضع الحالي"""
return self.hash
ميزة التحديث التزايدي
باستخدام Zobrist Hashing، تحديث قيمة التجزئة يستغرق وقت O(1) فقط:
| العملية | المقارنة التقليدية | Zobrist |
|---|---|---|
| مقارنة وضعين | O(361) | O(1) |
| التحديث بعد وضع حجر | O(361) | O(1) |
| التحديث بعد إزالة حجر | O(361) | O(k)، k هو عدد الأحجار المُزالة |
احتمال التصادم
باستخدام تجزئة 64 بت، احتمال التصادم منخفض للغاية:
P(تصادم) ≈ n² / 2^65
حيث n هو عدد الأوضاع المختلفة. حتى مع مليار وضع، احتمال التصادم حوالي 10^-3 فقط.
في الواقع، العديد من برامج الغو تستخدم Zobrist Hashing 64 بت بدون التحقق من التصادم، لأن احتمال التصادم منخفض لدرجة يمكن تجاهلها.
تمثيل السلسلة (Union-Find): مشكلة الاتصال
مفهوم السلسلة
في الغو، السلسلة (String أو Chain) هي مجموعة من الأحجار المتصلة من نفس اللون. "أنفاس" السلسلة تحدد بقاءها أو موتها.
في الشكل أعلاه، أربعة أحجار سوداء تشكل سلسلة واحدة، عدد أنفاس هذه السلسلة يحدد بقاءها.
لماذا نحتاج إدارة سلسلة فعالة؟
كل حركة قد:
- تنشئ سلسلة جديدة
- تدمج سلاسل متعددة
- تقلل أنفاس سلسلة العدو
- تأسر السلاسل بدون أنفاس
تحتاج هذه العمليات للتنفيذ بكفاءة، لأنه في MCTS، قد نحتاج لإجراء عشرات الآلاف من محاكاة الحركات في الثانية.
هيكل بيانات Union-Find
Union-Find (المعروف أيضاً بـ Disjoint Set Union, DSU) هو هيكل بيانات كلاسيكي لمعالجة مشاكل "الاتصال":
class UnionFind:
def __init__(self, size):
# parent[i] يشير إلى العقدة الأصل لـ i
# إذا كان parent[i] = i، فإن i هي العقدة الجذر
self.parent = list(range(size))
self.rank = [0] * size
def find(self, x):
"""
إيجاد العقدة الجذر للمجموعة التي ينتمي إليها x (مع ضغط المسار)
"""
if self.parent[x] != x:
self.parent[x] = self.find(self.parent[x]) # ضغط المسار
return self.parent[x]
def union(self, x, y):
"""
دمج المجموعات التي ينتمي إليها x و y (مع الدمج حسب الرتبة)
"""
root_x = self.find(x)
root_y = self.find(y)
if root_x == root_y:
return # بالفعل في نفس المجموعة
# الدمج حسب الرتبة: ربط الشجرة الأصغر بالشجرة الأكبر
if self.rank[root_x] < self.rank[root_y]:
self.parent[root_x] = root_y
elif self.rank[root_x] > self.rank[root_y]:
self.parent[root_y] = root_x
else:
self.parent[root_y] = root_x
self.rank[root_x] += 1
التعقيد الزمني
التعقيد الزمني لـ Union-Find يُعبر عنه باستخدام دالة أكرمان العكسية α(n):
| العملية | التعقيد الزمني |
|---|---|
| find | O(α(n)) ≈ O(1) |
| union | O(α(n)) ≈ O(1) |
α(n) تنمو ببطء شديد، لأي n عملي، α(n) ≤ 4. لذا يمكن اعتبارها وقت ثابت.
تطبيق Union-Find في الغو
class GoStringManager:
def __init__(self, size=19):
self.size = size
num_points = size * size
# هيكل Union-Find
self.parent = list(range(num_points))
self.rank = [0] * num_points
# أنفاس كل سلسلة (مفهرسة بالعقدة الجذر)
self.liberties = [set() for _ in range(num_points)]
# أحجار كل سلسلة (مفهرسة بالعقدة الجذر)
self.stones = [set() for _ in range(num_points)]
def _index(self, row, col):
return row * self.size + col
def _neighbors(self, row, col):
"""الحصول على الجيران الأربعة"""
neighbors = []
if row > 0: neighbors.append((row - 1, col))
if row < self.size - 1: neighbors.append((row + 1, col))
if col > 0: neighbors.append((row, col - 1))
if col < self.size - 1: neighbors.append((row, col + 1))
return neighbors
def find(self, idx):
if self.parent[idx] != idx:
self.parent[idx] = self.find(self.parent[idx])
return self.parent[idx]
def add_stone(self, row, col, color, board):
"""
إضافة حجر ومعالجة الاتصال والأسر
"""
idx = self._index(row, col)
# إنشاء سلسلة جديدة
self.stones[idx] = {idx}
self.liberties[idx] = set()
# حساب أنفاس هذا الحجر
for nr, nc in self._neighbors(row, col):
nidx = self._index(nr, nc)
if board[nr, nc] == 0: # النقطة الفارغة هي نفس
self.liberties[idx].add(nidx)
# الدمج مع الجيران من نفس اللون
for nr, nc in self._neighbors(row, col):
nidx = self._index(nr, nc)
if board[nr, nc] == color:
self._merge_strings(idx, nidx)
# تقليل أنفاس سلاسل العدو
opponent = 3 - color # BLACK=1, WHITE=2
captured = []
for nr, nc in self._neighbors(row, col):
nidx = self._index(nr, nc)
if board[nr, nc] == opponent:
root = self.find(nidx)
self.liberties[root].discard(idx)
if len(self.liberties[root]) == 0:
captured.append(root)
return captured
def _merge_strings(self, idx1, idx2):
"""دمج سلسلتين"""
root1 = self.find(idx1)
root2 = self.find(idx2)
if root1 == root2:
return
# الدمج حسب الرتبة
if self.rank[root1] < self.rank[root2]:
root1, root2 = root2, root1
self.parent[root2] = root1
if self.rank[root1] == self.rank[root2]:
self.rank[root1] += 1
# دمج مجموعات الأنفاس والأحجار
self.liberties[root1] |= self.liberties[root2]
self.stones[root1] |= self.stones[root2]
# إزالة موقع الذات من الأنفاس (الذات ليست نفساً لنفسها)
for stone_idx in self.stones[root2]:
self.liberties[root1].discard(stone_idx)
حساب الأنفاس
حساب عدد أنفاس السلسلة هو من أكثر العمليات شيوعاً في الغو:
def count_liberties(self, row, col, board):
"""حساب عدد أنفاس السلسلة التي ينتمي إليها حجر معين"""
idx = self._index(row, col)
root = self.find(idx)
return len(self.liberties[root])
باستخدام Union-Find، هذه العملية هي O(1) (بافتراض أن find هي O(1)).
تطور ترميز الميزات
من الميزات اليدوية التقليدية إلى الميزات البسيطة لـ AlphaGo Zero، شهد ترميز ميزات ذكاء الغو الاصطناعي تطوراً كبيراً.
الميزات اليدوية لـ GNU Go
برامج الغو المبكرة (مثل GNU Go) استخدمت كمية كبيرة من الميزات المصممة يدوياً:
def extract_features_gnugo(board, point):
"""
استخراج الميزات اليدوية بأسلوب GNU Go
"""
features = {}
# 1. أنماط الشكل (أشكال محلية محددة مسبقاً)
features['pattern'] = match_pattern(board, point)
# 2. ميزات تكتيكية
features['can_capture'] = check_capture(board, point)
features['can_connect'] = check_connect(board, point)
features['creates_eye'] = check_eye_creation(board, point)
# 3. ميزات محلية
features['liberties_after'] = count_liberties_after_move(board, point)
features['enemy_liberties'] = count_enemy_liberties(board, point)
# 4. ميزات عالمية
features['distance_to_edge'] = min(
point[0], point[1],
18 - point[0], 18 - point[1]
)
# ... المزيد من الميزات
return features
مشاكل هذا الأسلوب:
- يتطلب الكثير من المعرفة الخبيرة البشرية
- الميزات قد تكون غير مكتملة
- صعوبة اكتشاف أنماط جديدة
48 مستوى ميزات لـ AlphaGo
AlphaGo (نسخة Nature 2016) استخدم 48 مستوى ميزات كمدخلات للشبكة العصبية:
def encode_alphago_features(board, player, history):
"""
48 مستوى ميزات لـ AlphaGo 2016
Returns:
موتر بأبعاد (19, 19, 48)
"""
features = np.zeros((19, 19, 48), dtype=np.float32)
# المستوى 1: مواقع الأحجار السوداء
features[:, :, 0] = (board == BLACK)
# المستوى 2: مواقع الأحجار البيضاء
features[:, :, 1] = (board == WHITE)
# المستوى 3: النقاط الفارغة
features[:, :, 2] = (board == EMPTY)
# المستوى 4: كلها 1 (حد الانحياز)
features[:, :, 3] = 1
# المستويات 5-12: ترميز عدد الأنفاس (مستوى لكل 1-8 أنفاس)
for i, num_libs in enumerate(range(1, 9)):
for r in range(19):
for c in range(19):
if board[r, c] != EMPTY:
libs = count_liberties(board, r, c)
if libs == num_libs:
features[r, c, 4 + i] = 1
# المستويات 13-20: عدد أنفاس الخصم بعد الأسر
# ...
# المستويات 21-28: عدد أنفاسي بعد الحركة
# ...
# المستويات 29-36: تاريخ آخر 8 حركات (مستوى لكل حركة)
for i, (r, c) in enumerate(history[-8:]):
features[r, c, 28 + i] = 1
# المستويات 37-44: حكم المطاردة
# ...
# المستويات 45-48: متعلقة بالتماثل
# ...
# المستوى 49: دور من يلعب (كلها 1 للأسود، كلها 0 للأبيض)
if player == BLACK:
features[:, :, 47] = 1
return features
تصنيف 48 مستوى
| الفئة | عدد المستويات | الوصف |
|---|---|---|
| مواقع الأحجار | 3 | أسود، أبيض، فارغ |
| ثابت | 1 | كلها 1 |
| عدد الأنفاس | 8 | 1-8 أنفاس |
| عدد الأنفاس بعد الأسر | 8 | عدد الأنفاس المفترض بعد الحركة |
| عدد الأنفاس بعد الحركة | 8 | عدد الأنفاس المفترض بعد الحركة |
| التاريخ | 8 | آخر 8 حركات |
| المطاردة | 8 | تحليل المطاردة المعقد |
| أخرى | 4 | الدور، التماثل، إلخ |
17 مستوى ميزات لـ AlphaGo Zero
AlphaGo Zero (2017) بسّط ترميز الميزات بشكل كبير، باستخدام 17 مستوى فقط:
def encode_alphago_zero_features(board_history, player):
"""
17 مستوى ميزات لـ AlphaGo Zero
Args:
board_history: آخر 8 أوضاع (بما في ذلك الحالي)
player: اللاعب الحالي
Returns:
موتر بأبعاد (19, 19, 17)
"""
features = np.zeros((19, 19, 17), dtype=np.float32)
# المستويات 1-8: تاريخ الأحجار السوداء (آخر 8 حركات)
for i, board in enumerate(board_history[-8:]):
features[:, :, i] = (board == BLACK)
# المستويات 9-16: تاريخ الأحجار البيضاء (آخر 8 حركات)
for i, board in enumerate(board_history[-8:]):
features[:, :, 8 + i] = (board == WHITE)
# المستوى 17: دور من يلعب (كلها 1 للأسود، كلها 0 للأبيض)
if player == BLACK:
features[:, :, 16] = 1
return features
بساطة 17 مستوى
| الفئة | عدد المستويات | الوصف |
|---|---|---|
| تاريخ الأحجار السوداء | 8 | مواقع الأحجار السوداء في آخر 8 خطوات زمنية |
| تاريخ الأحجار البيضاء | 8 | مواقع الأحجار البيضاء في آخر 8 خطوات زمنية |
| اللاعب الحالي | 1 | كلها 1 أو كلها 0 |
لماذا يمكن أن تكون بهذه البساطة؟
الرؤية الأساسية: دع الشبكة العصبية تتعلم الميزات بنفسها.
AlphaGo Zero أثبت أنه:
- لا حاجة لحساب عدد الأنفاس يدوياً (الشبكة يمكنها استنتاجه من مواقع الأحجار)
- لا حاجة لتحليل المطاردة (الشبكة يمكنها تعلم التعرف على المطاردة)
- لا حاجة لتنبؤ أسر معقد (الشبكة يمكنها فهم نتائج الحركات)
هذا يجسد الميزة الأساسية للتعلم العميق: التعلم من طرف إلى طرف.
تصور مستويات الميزات
دعونا نرى كيف تبدو هذه المستويات فعلياً:
اللوحة الأصلية: مستوى الأسود: مستوى الأبيض:
. . . . . . 0 0 0 0 0 0 0 0 0 0 0 0
. ● ○ . . . 0 1 0 0 0 0 0 0 1 0 0 0
. . ● ○ . . -> 0 0 1 0 0 0 + 0 0 0 1 0 0
. . . ● . . 0 0 0 1 0 0 0 0 0 0 0 0
. . . . . . 0 0 0 0 0 0 0 0 0 0 0 0
كل مستوى هو مصفوفة ثنائية (0 أو 1)، والشبكة العصبية يمكنها استخراج الأنماط من هذه المستويات من خلال عمليات الالتواء.
معالجة التماثل: 8 تحويلات
تماثل الغو
لوحة الغو لها 8 أنواع من التماثل:
- 4 دورات: 0°، 90°، 180°، 270°
- 4 انعكاسات مع دوران: انعكاس أفقي + 4 دورات
هذا يشكل زمرة ثنائية السطوح D4.
التمثيل الرياضي
لتكن (x, y) إحداثيات على اللوحة (المركز في (9, 9))، يمكن تمثيل 8 تحويلات كالتالي:
| التحويل | الصيغة |
|---|---|
| الهوية | (x, y) |
| دوران 90° | (-y, x) |
| دوران 180° | (-x, -y) |
| دوران 270° | (y, -x) |
| انعكاس أفقي | (-x, y) |
| انعكاس رأسي | (x, -y) |
| انعكاس قطري | (y, x) |
| انعكاس قطري معاكس | (-y, -x) |
التنفيذ البرمجي
def get_symmetries(board):
"""
الحصول على 8 تحويلات تماثلية للوحة
Returns:
قائمة من 8 لوحات
"""
symmetries = []
# الأصلية
symmetries.append(board.copy())
# دوران 90°
symmetries.append(np.rot90(board, k=1))
# دوران 180°
symmetries.append(np.rot90(board, k=2))
# دوران 270°
symmetries.append(np.rot90(board, k=3))
# انعكاس أفقي
symmetries.append(np.fliplr(board))
# انعكاس رأسي
symmetries.append(np.flipud(board))
# انعكاس قطري
symmetries.append(board.T)
# انعكاس قطري معاكس
symmetries.append(np.rot90(board.T, k=2))
return symmetries
def apply_symmetry(board, sym_index):
"""
تطبيق تحويل التماثل رقم sym_index
"""
return get_symmetries(board)[sym_index]
def inverse_symmetry(move, sym_index, board_size=19):
"""
تحويل إحداثيات الحركة المحولة إلى الإحداثيات الأصلية
"""
row, col = move
if sym_index == 0: # الهوية
return row, col
elif sym_index == 1: # دوران 90°
return col, board_size - 1 - row
elif sym_index == 2: # دوران 180°
return board_size - 1 - row, board_size - 1 - col
elif sym_index == 3: # دوران 270°
return board_size - 1 - col, row
elif sym_index == 4: # انعكاس أفقي
return row, board_size - 1 - col
elif sym_index == 5: # انعكاس رأسي
return board_size - 1 - row, col
elif sym_index == 6: # انعكاس قطري
return col, row
elif sym_index == 7: # انعكاس قطري معاكس
return board_size - 1 - col, board_size - 1 - row
زيادة البيانات
عند تدريب الشبكة العصبية، يمكن استخدام التماثل لزيادة البيانات:
def augment_training_data(board, policy, value):
"""
توسيع عينة تدريب واحدة إلى 8 عينات
"""
augmented = []
for sym_index in range(8):
# تحويل اللوحة
aug_board = apply_symmetry(board, sym_index)
# تحويل السياسة (متجه 361 بُعداً)
policy_2d = policy.reshape(19, 19)
aug_policy_2d = apply_symmetry(policy_2d, sym_index)
aug_policy = aug_policy_2d.flatten()
# القيمة لا تتغير
aug_value = value
augmented.append((aug_board, aug_policy, aug_value))
return augmented
هذا يزيد حجم بيانات التدريب 8 مرات، وبشكل مجاني تماماً (لا حاجة لجمع بيانات جديدة).
استخدام التماثل أثناء الاستدلال
AlphaGo يستخدم التماثل أيضاً في المباريات الفعلية:
def predict_with_symmetry(model, board):
"""
تعزيز التنبؤ باستخدام التماثل
"""
policies = []
values = []
for sym_index in range(8):
# تحويل المدخلات
aug_board = apply_symmetry(board, sym_index)
# تنبؤ الشبكة العصبية
policy, value = model.predict(aug_board)
# تحويل السياسة إلى نظام الإحداثيات الأصلي
policy_2d = policy.reshape(19, 19)
original_policy = inverse_symmetry_2d(policy_2d, sym_index)
policies.append(original_policy.flatten())
values.append(value)
# متوسط جميع التنبؤات
avg_policy = np.mean(policies, axis=0)
avg_value = np.mean(values)
return avg_policy, avg_value
هذا الأسلوب يمكن أن يحسن قليلاً دقة واستقرار التنبؤ.
ملخص طرق تمثيل اللوحة
| الطريقة | الاستخدام | التعقيد | الميزات |
|---|---|---|---|
| المصفوفة ثنائية الأبعاد | التخزين الأساسي | مساحة O(361) | بسيطة ومباشرة |
| Zobrist Hashing | التعرف على الأوضاع | استعلام O(1) | تجزئة فعالة |
| Union-Find | إدارة السلسلة | عملية O(α(n)) | معالجة الاتصال |
| مستويات الميزات | مدخلات الشبكة العصبية | O(361×عدد المستويات) | ترميز معلومات غنية |
| تحويلات التماثل | زيادة البيانات | 8× حجم البيانات | توسيع مجاني |
هذه التقنيات تعمل معاً لتشكل البنية التحتية لذكاء الغو الاصطناعي الحديث.
توافق الرسوم المتحركة
المفاهيم الأساسية في هذه المقالة وأرقام الرسوم المتحركة المقابلة:
| الرقم | المفهوم | المقابل الفيزيائي/الرياضي |
|---|---|---|
| 🎬 A1 | تمثيل المصفوفة ثنائية الأبعاد | المصفوفات، البيانات المتناثرة |
| 🎬 A2 | Zobrist Hashing | دوال التجزئة، عملية XOR |
| 🎬 A8 | ترميز مستويات الميزات | الموترات، المدخلات متعددة القنوات |
| 🎬 A5 | معالجة التماثل | نظرية الزمر، زمرة ثنائية السطوح |
قراءات إضافية
- المقالة السابقة: حدود الأساليب التقليدية — من Minimax إلى MCTS
- المقالة التالية: شرح تفصيلي لـ Policy Network — كيف تتنبأ الشبكة العصبية بالحركات
- تمرين عملي: تشغيل أول ذكاء غو اصطناعي في 30 دقيقة — تجربة عملية بنفسك
المراجع
- Zobrist, A. L. (1970). "A new hashing method with application for game playing." ICCA journal.
- Tarjan, R. E. (1975). "Efficiency of a Good But Not Linear Set Union Algorithm." Journal of the ACM, 22(2), 215-225.
- Silver, D., et al. (2016). "Mastering the game of Go with deep neural networks and tree search." Nature, 529, 484-489. — 48 مستوى ميزات لـ AlphaGo
- Silver, D., et al. (2017). "Mastering the game of Go without human knowledge." Nature, 550, 354-359. — 17 مستوى ميزات لـ AlphaGo Zero