معاينة مختبر آمنة
Genetic
هذي معاينة منقّحة للقراءة فقط؛ ما فيه أي شيء يشتغل داخل الصفحة.
قراءة فقط
معاينة الدفتر
Genetic
> **ملاحظة بيئة التشغيل المدمجة:** هالمعاينة تستخدم عيّنة صغيرة وثابتة وآمنة من ناحية الحقوق عشان تكون النتايج قابلة للتكرار. النتايج بالحجم الكامل تحتاج مجموعة البيانات أو النموذج الموثّق بالدرس داخل بيئة خارجية معتمدة.
# الخوارزميات الجينية
هالدفتر جزء من منهج الذكاء الاصطناعي للمبتدئين.
import random
import matplotlib.pyplot as plt
import numpy as np
import math
import time
random.seed(2026)
np.random.seed(2026)## النظرية باختصار
تعتمد **الخوارزميات الجينية** (Genetic Algorithms, GA) على **نهج تطوري** في الذكاء الاصطناعي: نطوّر مجموعة من الحلول عشان نوصل للحل الأمثل لمشكلة معيّنة. اقترحها [John Henry Holland (جون هنري هولاند)](https://en.wikipedia.org/wiki/John_Henry_Holland) سنة 1975.
تقوم الخوارزميات الجينية على هالأفكار:
* نقدر نمثّل الحلول الصالحة للمشكلة على شكل **جينات**.
* **التهجين** (crossover) يخلينا ندمج حلّين ونطلع منهم بحل جديد صالح.
* نستخدم **الاختيار** عشان ننتقي الحلول الأفضل بحسب **دالة الملاءمة** (fitness function).
* ندخل **الطفرات** (mutations) عشان نغيّر مسار التحسين ونطلعه من الحد الأدنى المحلي.
إذا بغينا نطبّق خوارزمية جينية، نحتاج نسوي الآتي:
* نلقى طريقة نرمّز فيها حلول المشكلة باستخدام **جينات** $g\in\Gamma$
* نعرّف على مجموعة الجينات $\Gamma$ **دالة الملاءمة** $\mathrm{fit}: \Gamma\to\mathbb{R}$، بحيث تدل القيم الأصغر على حلول أفضل.
* نعرّف آلية **تهجين** تدمج جينين وتنتج جينًا جديدًا صالحًا $\mathrm{crossover}: \Gamma^2\to\Gamma$.
* نعرّف آلية **طفرة** $\mathrm{mutate}: \Gamma\to\Gamma$.
في حالات كثيرة، يكون التهجين والطفرة خوارزميات بسيطة تتعامل مع الجينات كسلاسل رقمية أو متجهات بتّية.
التطبيق التفصيلي للخوارزمية الجينية يختلف من حالة للثانية، لكن بنيتها العامة تمشي كذا:
1. نختار مجموعة حلول أولية $G\subset\Gamma$.
2. نختار عشوائيًا العملية اللي بننفّذها في هالخطوة: تهجين أو طفرة.
3. **التهجين**:
* نختار عشوائيًا جينين $g_1, g_2 \in G$.
* نحسب ناتج التهجين $g=\mathrm{crossover}(g_1,g_2)$.
* إذا كان $\mathrm{fit}(g)<\mathrm{fit}(g_1)$ أو $\mathrm{fit}(g)<\mathrm{fit}(g_2)$، نستبدل الجين المقابل في المجموعة بـ $g$.
4. **الطفرة**: نختار جينًا عشوائيًا $g\in G$ ونستبدله بـ $\mathrm{mutate}(g)$.
5. نكرر من الخطوة 2 لين تصير قيمة $\mathrm{fit}$ صغيرة بما يكفي، أو نوصل للحد الأعلى من عدد الخطوات.
عادةً نستخدم GA في هالمهام:
1. تحسين الجداول الزمنية.
1. الوصول لأفضل طريقة للتعبئة.
1. الوصول لأفضل طريقة للقص.
1. تسريع البحث الشامل.
## المسألة 1: تقسيم الكنز بإنصاف
**المطلوب**:
لقى شخصان كنزًا فيه قطع ألماس بأحجام مختلفة، وبالتالي قيمها مختلفة. يبغون يقسمون الكنز قسمين بحيث يكون فرق القيمة بينهما 0، أو أقل فرق ممكن.
**التعريف الرياضي**:
عندنا مجموعة أعداد $S$، ونبي نقسمها إلى مجموعتين جزئيتين $S_1$ و$S_2$ بحيث $$\left|\sum_{i\in S_1}i - \sum_{j\in S_2}j\right|\to\min$$ و$S_1\cup S_2=S$، و$S_1\cap S_2=\emptyset$.
أول شيء، خلونا نعرّف المجموعة $S$:
N = 200
S = np.array([random.randint(1,10000) for _ in range(N)])
print(S)بنرمّز كل حل ممكن للمسألة بمتجه ثنائي $B\in\{0,1\}^N$. القيمة في الموضع $i$ تحدد لأي مجموعة، $S_1$ أو $S_2$، ينتمي العدد رقم $i$ من المجموعة الأصلية $S$. ودالة `generate` بتولّد لنا هالمتجهات الثنائية عشوائيًا.
def generate(S):
return np.array([random.randint(0,1) for _ in S])
b = generate(S)
print(b)خلونا الحين نعرّف دالة `fit` اللي تحسب «تكلفة» الحل، وهي الفرق بين مجموعي المجموعتين $S_1$ و$S_2$:
def fit(B,S=S):
c1 = (B*S).sum()
c2 = ((1-B)*S).sum()
return abs(c1-c2)
fit(b)الحين نحتاج نعرّف دالتي الطفرة والتهجين:
* في الطفرة، نختار بتًا واحدًا عشوائيًا ونعكسه: من 0 إلى 1 أو العكس.
* وفي التهجين، ناخذ بعض البتات من متجه والباقي من متجه ثاني. بنستخدم دالة `generate` نفسها عشان نحدد عشوائيًا أي بت ناخذه من كل قناع دخل.
def mutate(b):
x = b.copy()
i = random.randint(0,len(b)-1)
x[i] = 1-x[i]
return x
def xover(b1,b2):
x = generate(b1)
return b1*x+b2*(1-x)خلونا ننشئ مجموعة الحلول الأولية $P$ بحجم `pop_size`:
pop_size = 30
P = [generate(S) for _ in range(pop_size)]الحين نجي للدالة الرئيسة اللي تنفّذ عملية التطور. يمثّل `n` عدد خطوات التطور، وفي كل خطوة:
* باحتمال 30% نسوي طفرة، ونستبدل العنصر صاحب أسوأ قيمة في `fit` بالعنصر الناتج من الطفرة.
* وباحتمال 70% نسوي تهجينًا.
ترجّع الدالة أفضل حل، أي الجين المقابل له، ومعه سجل أقل قيمة لدالة الملاءمة في المجموعة عند كل تكرار.
def evolve(P,S=S,n=2000):
res = []
for _ in range(n):
f = min([fit(b) for b in P])
res.append(f)
if f==0:
break
if random.randint(1,10)<3:
i = random.randint(0,len(P)-1)
b = mutate(P[i])
i = np.argmax([fit(z) for z in P])
P[i] = b
else:
i = random.randint(0,len(P)-1)
j = random.randint(0,len(P)-1)
b = xover(P[i],P[j])
if fit(b)<fit(P[i]):
P[i]=b
elif fit(b)<fit(P[j]):
P[j]=b
else:
pass
i = np.argmin([fit(b) for b in P])
return (P[i],res)
(s,hist) = evolve(P)
print(s,fit(s))مثل ما تشوفون، قدرنا نقلّل دالة `fit` بشكل واضح! والرسم الجاي يبيّن كيف تتغير `fit` لمجموعة الحلول كاملة خلال العملية.
plt.plot(hist)
plt.show()## المسألة 2: مسألة الملكات N
**المطلوب**:
نبي نحط $N$ ملكة على رقعة شطرنج حجمها $N\times N$، من دون ما تهاجم أي ملكة غيرها.
بالبداية، بنحل المسألة من دون خوارزمية جينية باستخدام البحث الكامل. نقدر نمثّل حالة الرقعة بقائمة $L$، بحيث تكون القيمة رقم $i$ في القائمة هي الموضع الأفقي للملكة في الصف رقم $i$. وبهالتمثيل، كل حل فيه ملكة وحدة في كل صف، وكل صف فيه ملكة.
هدفنا نلقى أول حل للمسألة ثم نوقف البحث. وتقدرون توسّعون هالدالة بسهولة عشان تولّد كل المواضع الممكنة للملكات.
N = 8
def checkbeats(i_new,j_new,l):
for i,j in enumerate(l,start=1):
if j==j_new:
return False
else:
if abs(j-j_new) == i_new-i:
return False
return True
def nqueens(l,N=8,disp=True):
if len(l)==N:
if disp: print(l)
return True
else:
for j in range(1,N+1):
if checkbeats(len(l)+1,j,l):
l.append(j)
if nqueens(l,N,disp): return True
else: l.pop()
return False
nqueens([],8)الحين خلونا نقيس كم يستغرق إيجاد حل لمسألة 20 ملكة:
course_started_at = time.perf_counter()
course_result = nqueens([], 12, False)
print(f"Solved={course_result}; elapsed={time.perf_counter() - course_started_at:.4f}s")الحين بنحل المسألة نفسها باستخدام **الخوارزمية الجينية**. هالحل مستوحى من [هالتدوينة](https://kushalvyas.github.io/gen_8Q.html).
بنمثّل كل حل بالقائمة نفسها وطولها $N$، وبنخلي دالة `fit` هي عدد الملكات اللي تهاجم بعضها:
def fit(L):
x=0
for i1,j1 in enumerate(L,1):
for i2,j2 in enumerate(L,1):
if i2>i1:
if j2==j1 or (abs(j2-j1)==i2-i1): x+=1
return xبما إن حساب دالة الملاءمة يستهلك وقتًا، بنخزّن كل حل داخل مجموعة الحلول ومعه قيمة دالة الملاءمة. خلونا نولّد المجموعة الأولية:
def generate_one(N):
x = np.arange(1,N+1)
np.random.shuffle(x)
return (x,fit(x))
def generate(N,NP):
return [generate_one(N) for _ in range(NP)]
generate(8,5)الحين نحتاج نعرّف دالتي الطفرة والتهجين. في التهجين، نقص الجينين عند نقطة عشوائية ثم نوصل جزءًا من الجين الأول بجزء من الجين الثاني.
def mutate(G):
x=random.randint(0,len(G)-1)
G[x]=random.randint(1,len(G))
return G
def xover(G1,G2):
x=random.randint(0,len(G1))
return np.concatenate((G1[:x],G2[x:]))
xover([1,2,3,4],[5,6,7,8])بنحسّن اختيار الجينات بحيث نعطي الجينات ذات دالة الملاءمة الأفضل فرصًا أكثر. يعني احتمال اختيار الجين بيعتمد على دالة ملاءمته:
def choose_rand(P):
N=len(P[0][0])
mf = N*(N-1)//2 # max fitness fn
z = [mf-x[1] for x in P]
tf = sum(z) # total fitness
w = [x/tf for x in z]
p = np.random.choice(len(P),2,False,p=w)
return p[0],p[1]
def choose(P):
def ch(w):
p=[]
while p==[]:
r = random.random()
p = [i for i,x in enumerate(P) if x[1]>=r]
return random.choice(p)
N=len(P[0][0])
mf = N*(N-1)//2 # max fitness fn
z = [mf-x[1] for x in P]
tf = sum(z) # total fitness
w = [x/tf for x in z]
p1=p2=0
while p1==p2:
p1 = ch(w)
p2 = ch(w)
return p1,p2الحين خلونا نعرّف حلقة التطور الرئيسة. بنغيّر المنطق شوي عن المثال السابق عشان نشوف إن فيه مساحة للابتكار. بنكرر لين نلقى الحل المثالي، أي لما تكون دالة الملاءمة = 0. وفي كل خطوة ناخذ الجيل الحالي وننتج جيلًا جديدًا بالحجم نفسه. تسوي دالة `nxgeneration` هالعملية بالخطوات الجاية:
1. نستبعد الحلول الأقل ملاءمة؛ ودالة `discard_unfit` هي اللي تسوي هالخطوة.
1. نضيف للجيل بعض الحلول العشوائية الجديدة.
1. نملأ جيلًا جديدًا حجمه `gen_size`، ونكرر الخطوات الجاية لكل جين جديد:
- نختار جينين عشوائيين باحتمال يتناسب مع دالة الملاءمة.
- نحسب ناتج التهجين.
- نطبّق طفرة باحتمال `mutation_prob`.
mutation_prob = 0.1
def discard_unfit(P):
P.sort(key=lambda x:x[1])
return P[:len(P)//3]
def nxgeneration(P):
gen_size=len(P)
P = discard_unfit(P)
P.extend(generate(len(P[0][0]),3))
new_gen = []
for _ in range(gen_size):
p1,p2 = choose_rand(P)
n = xover(P[p1][0],P[p2][0])
if random.random()<mutation_prob:
n=mutate(n)
nf = fit(n)
new_gen.append((n,nf))
'''
if (nf<=P[p1][1]) or (nf<=P[p2][1]):
new_gen.append((n,nf))
elif (P[p1][1]<P[p2][1]):
new_gen.append(P[p1])
else:
new_gen.append(P[p2])
'''
return new_gen
def genetic(N, pop_size=100, max_generations=500):
P = generate(N,pop_size)
mf = min([x[1] for x in P])
n=0
while mf > 0 and n < max_generations:
#print("Generation {0}, fit={1}".format(n,mf))
n+=1
mf = min([x[1] for x in P])
P = nxgeneration(P)
mi = np.argmin([x[1] for x in P])
return P[mi]
genetic(8)اللافت إننا في أغلب المرات نلقى الحل بسرعة، لكن أحيانًا وبشكل نادر يوصل التحسين إلى حد أدنى محلي وتعلق العملية مدة طويلة. لازم نحسب حساب هالشيء لما نقيس متوسط الوقت: غالبًا تكون الخوارزمية الجينية أسرع من البحث الكامل، لكنها في بعض الحالات تأخذ وقتًا أطول. عشان نتعامل مع هالمشكلة، عادةً من المناسب نحط حدًا لعدد الأجيال؛ وإذا ما لقينا الحل، نبدأ من الصفر.
course_started_at = time.perf_counter()
course_result = genetic(10)
print(f"Fitness={course_result[1]}; elapsed={time.perf_counter() - course_started_at:.4f}s")حذفنا المخرجات وعدّادات التشغيل والودجات والمحتوى النشط وقت الاستيراد. شغّل الدفاتر بس في بيئة خارجية تثق فيها.
سجّل تطبيقك
التسجيل اختياري، يفيدك تتذكر وش طبّقت، ولا يمنع إكمال الدورة.