Ana içeriğe atla

Genetik Algoritma: Python Uygulamasıyla Kapsamlı Rehber

Genetik algoritma, aday çözümlerden oluşan bir popülasyonu yinelemeli olarak iyileştirerek doğal seçilimi taklit eden ve en iyi çözümleri bulan bir arama tekniğidir.
Güncel 16 Nis 2026  · 15 dk. oku

Kamyonlar için teslimat rotalarını optimize etmeye çalıştığınızı hayal edin. Her kamyonun birçok olası rotası var ve sizin de çok sayıda durağı olan pek çok kamyonunuz var. Olası kombinasyonların sayısı baş döndürücü olabilir ve en iyi çözümü bulmak samanlıkta iğne aramaya benzeyebilir.

Bu tür problemleri çözmek için biyolojiden bir kavram ödünç alabiliriz: evrim. Biyolojide doğal seçilim, evrimi yönlendiren bir güçtür. Doğanın problemi, bir organizmanın hayatta kalmasını ve üremesini sağlayan özelliklerin en iyi kombinasyonlarını bulmaktır. Bunu, farklı özellik setlerini birbiriyle yarıştırarak ve en iyi kombinasyonları seçerek yapar. 

Benzer şekilde, farklı çözümleri birbirine karşı yarıştıran ve seçim yoluyla onları en iyi çözüme doğru “evrimleştiren” bir algoritma oluşturmak için bu kavramı kullanabiliriz. Bunu yapan algoritmalara genetik algoritmalar (GA) denir.

Yapay Zeka Uygulamaları Geliştirin

OpenAI API'sını kullanarak AI uygulamaları oluşturmayı öğrenin.
Ücretsiz olarak beceri geliştirmeye başlayın

Doğal evrimden ilham alan GA’lar, çözüm uzayını verimli bir şekilde keşfederek, birçok hareketli parçaya sahip karmaşık problemler için bile optimal ya da optimale yakın çözümleri bulur. Doğal seçilim sürecini taklit ederek GA’lar, zaman içinde çözümleri evrimleştirir ve performanslarını yinelemeli olarak iyileştirir.

Genetik Algoritmaların Biyolojik Arka Planı

Genetik algoritmalar, doğal seçilim ve genetik süreçlerinden esinlenmiştir. Canlı organizmaların nesiller boyunca çevrelerine uyum sağlamak için nasıl evrimleştiğini taklit ederler. GA’ların arkasındaki temel biyolojik ilkeleri anlamak, bu algoritmaların nasıl çalıştığını kavramamıza ve onları daha stratejik kullanmamıza yardımcı olur.

Doğal seçilim

Doğal seçilim, elverişli özelliklerin daha iyi kombinasyonlarına sahip bireylerin hayatta kalma ve üreme olasılığının daha yüksek olmasına neden olan süreçtir. Zamanla bu avantajlı özellikler popülasyonda daha yaygın hale gelir.

GA bağlamında bu kavram, üreme için daha yüksek uygunluk (fitness) puanına sahip bireylerin seçilmesiyle yansıtılır. Böylece elverişli özelliklerini, yani “genlerini”, bir sonraki nesle aktarırlar.

Genler, çaprazlama ve mutasyon

Bir biyolojik organizmanın özellikleri genleri tarafından kodlanır. Genler, kromozomlara düzenlenmiş DNA’nın bir parçasıdır.

GA’lar aynı kavram ve terminolojinin çoğunu ödünç alır. Bir probleme yönelik olası çözümler genellikle dizgiler veya diziler olarak kodlanan “kromozomlar” şeklinde gösterilir. Kromozomdaki her bir öğe, çözümün belirli bir özelliğini belirleyen bir gene karşılık gelir.

Çaprazlama ya da rekombinasyon, iki ebeveyn kromozomunun genetik materyalinin bölümlerini birleştirerek her bir ebeveynden parçalar içeren yeni bir kromozom oluşturan genetik bir işlemdir. Bu yeni kromozom karışımı çocuğa aktarılır. Bu yüzden annenizin göz rengini, babanızın saç rengini alabilirsiniz.

GA’larda çaprazlama, yeni bireyler oluşturmak için ebeveyn kromozomlarının segmentlerini değiştirmeyi içerir. Bu işlem, değişkenlik katar ve yavruların her iki ebeveynden de yararlı özellikler miras almasını sağlar.

Mutasyon, bir organizmanın genlerinde rastgele değişiklikler oluşturarak yeni özelliklere yol açabilir.

Seçilim, mutasyon ve çaprazlama olmak üzere üç evrim kavramının görsel açıklaması.

Genetik algoritmalarda mutasyon, bir kromozomdaki bir veya daha fazla genin rastgele değiştirilmesini içerir. Bu, popülasyon içinde genetik çeşitliliği korumaya yardımcı olur ve algoritmanın çözüm uzayının yeni bölgelerini keşfetmesini sağlar.

Uygunluk değerlendirmesi

Doğada bir bireyin uygunluğu, hayatta kalma ve üreme yeteneğiyle belirlenir. Üremeden önce ölürseniz uygunluğunuz sıfırdır. 12 torununuz varsa uygunluğunuz 2 torunu olan birinden daha yüksektir.

GA’larda uygunluk benzer olsa da biraz farklıdır. Bir uygunluk fonksiyonu, bir çözümün eldeki problemi ne kadar iyi çözdüğünü değerlendirir. Daha yüksek uygunluk puanlarına sahip çözümlerin üreme için seçilme olasılığı daha yüksektir; bu da daha iyi çözümlerin nesiller boyunca yayılmasını sağlar.

Bir Genetik Algoritmanın Bileşenleri

Genetik algoritmalar evrimden ilham almıştır. Bu nedenle bir GA’nın bileşenleri, biyolojik karşılıklarıyla benzer ad ve işlevlere sahiptir. Bu bileşenlerin her biri genellikle kendi fonksiyonu olarak kodlanır.

Popülasyon

Bir genetik algoritmadaki popülasyon, genellikle bireyler olarak adlandırılan aday çözümler kümesidir. Her birey bir kromozom olarak temsil edilir ve ikili dizgiler, diziler veya diğer veri yapıları olarak kodlanabilir.

Örneğin, her bir kromozom, optimize etmemiz gereken bir fonksiyona ait bir dizi girdi değerini temsil edebilir. Ya da her bir kromozom, dağıtım sürücüleri için bir kamyon rotasını temsil edebilir.

Uygunluk fonksiyonu

Uygunluk fonksiyonu, ilgilendiğimiz problemi çözme konusundaki her bireyin yeteneğini değerlendirir. Daha iyi çözümler için daha yüksek değerler atar. Uygunluk fonksiyonu, algoritmayı daha iyi çözümlere doğru yönlendirir.

Örneğin, matematiksel fonksiyon optimizasyonu örneğimizde uygunluk fonksiyonu, verilen girdiler için fonksiyonun değerini döndürebilir. Kamyon rotaları örneğimizde ise uygunluk fonksiyonu teslimatı tamamlama hızını döndürebilir.

Seçim fonksiyonu

Seçim fonksiyonu, uygunluklarına dayanarak popülasyondan üremek üzere bireyleri seçer. Uygunluğu daha yüksek bireylerin üreme için seçilme olasılığı daha fazladır. Bu, en iyi uyum sağlayan bireylerin hayatta kalma ve üreme olasılığının daha yüksek olduğu doğal seçilimi taklit eder.

Birkaç farklı seçim yöntemi vardır. Yaygın yöntemler arasında rulet tekeri seçimi, turnuva seçimi ve sıralama tabanlı seçim bulunur.

Rulet tekeri seçiminde bireyler, uygunluklarıyla orantılı bir olasılığa göre seçilir; rulet tekerinin çevrilmesine benzer.

Turnuva seçiminde, rastgele bir alt küme seçilir ve bu alt kümedeki en uygun birey seçilir.

Sıralama tabanlı seçimde, bireyler uygunluklarına göre sıralanır. Seçim olasılıkları, ham uygunluk değerleri yerine bu sıralamalara göre atanır.

Her yöntemin kendi avantajları vardır ve hangisinin seçileceği, eldeki problemin özel gereksinimlerine göre belirlenmelidir.

Çaprazlama fonksiyonu

Çaprazlama, iki bireyden bilgi alarak yavrular oluşturur. Amaç, her iki ebeveynden de yararlı özellikleri miras almaktır. Yaygın çaprazlama teknikleri arasında tek noktalı çaprazlama, çok noktalı çaprazlama, uniform (düzgün) çaprazlama ve harman (blend) çaprazlama yer alır.

Tek noktalı çaprazlama, rastgele bir nokta seçilmesini ve bu noktada iki ebeveynden genetik materyalin değiştirilerek iki yavru oluşturulmasını içerir. Çok noktalı çaprazlama, ebeveynler arasında segmentlerin değişimi için birden fazla nokta kullanarak daha karmaşık genetik kombinasyonlara olanak tanır.

Uniform çaprazlama, her gen için hangi ebeveynin katkı sağlayacağına rastgele karar vererek yüksek düzeyde genetik çeşitlilik sağlar. Harman çaprazlama ise, rastgele bir harmanlama faktörü kullanarak ebeveynlerin genlerini harmanlayıp yavrular üretir. Hangi tekniğin kullanılacağına ilişkin karar, problemin karmaşıklığına ve yavrularda istenen çeşitlilik düzeyine bağlı olmalıdır.

Mutasyon fonksiyonu

Mutasyon, yavrunun genetik materyalinde rastgele değişiklikler yapar. Bu, popülasyondaki çeşitliliği korur ve çözüm uzayının yeni bölgelerinin keşfini sağlar. Mutasyonlar, ikili bir dizgide bir biti çevirmek kadar basit olabileceği gibi, kodlama şemasına bağlı olarak daha karmaşık değişiklikler de içerebilir. 

Genetik Algoritma Süreci

Bir genetik algoritma, optimal çözümleri bulmak için doğal evrimsel süreçleri taklit eden bir dizi adımdan geçer. Bu adımlar, popülasyonun nesiller boyunca evrimleşmesini ve çözümlerin kalitesinin artmasını sağlar. Bir genetik algoritmanın genel işleyişi şöyledir:

Adım 1: Başlatma

Önce rasgele bireylerden oluşan bir başlangıç popülasyonu üretiriz. Bu adım, algoritmaya başlamak için çeşitli olası çözümler kümesi oluşturur. 

Adım 2: Değerlendirme

Sonra popülasyondaki her bireyin uygunluğunu hesaplamamız gerekir. Burada her çözümün ne kadar iyi olduğunu değerlendirmek için uygunluk fonksiyonunu kullanırız.

Adım 3: Seçim

Seçim ölçütlerini kullanarak bireyleri uygunluklarına göre üreme için seçeriz. Bu adım, hangi bireylerin ebeveyn olacağını belirler.

Adım 4: Çaprazlama

Sırada çaprazlama var. Seçilen ebeveynlerin genetik materyalini birleştirerek, çaprazlama tekniklerini uygulayıp yeni çözümler veya yavrular üretiriz.

Adım 5: Mutasyon

Popülasyonumuzdaki çeşitliliği korumak için yavrularda rastgele mutasyonlar uygularız.

Adım 6: Yer değiştirme

Ardından, hangi bireylerin bir sonraki nesle geçeceğine karar vererek eski popülasyonun bir kısmını veya tamamını yeni yavrularla değiştiririz.

Adım 7: Tekrarla

Önceki 2-6 adımları, belirli sayıda nesil boyunca veya bir sonlandırma koşulu sağlanana kadar döngü halinde tekrarlanır. Bu döngü, popülasyonun zaman içinde evrimleşmesini sağlar ve umarız iyi bir çözüme ulaşır.

Python Örneği

Artık genetik algoritmaların ne olduğunu ve genel olarak nasıl çalıştığını anladığımıza göre, basit bir optimizasyon problemini çözmek için kendi genetik algoritmamızı oluşturalım.

Grafiğe döküldüğünde y=ax2+bx+c denklemi bir parabol oluşturur. a, b ve c değerlerinin, en düz parabolü oluşturacak kombinasyonunu bulmak için bir genetik algoritma kullanacağız. İşte elde edeceğimiz sonucun bir ön izlemesi:

Kodlu örnekteki çözümlerle denklemlerin grafiği.

Kütüphaneleri içe aktarın

Fonksiyonumuzun başında gerekli kütüphaneleri içe aktaracağız. Başlangıç popülasyonumuz için rastgele değerler üretmek üzere ‘random’ kullanacağız. Numpy bence matematikle uğraşırken genel olarak faydalıdır. 

Ayrıca, kodunuzun düşündüğünüz şeyi yaptığından emin olmak için her zaman grafikler çizmeniz gerektiğine inanırım. Bu yüzden grafik oluşturmak için ‘matplotlib.pyplot’ içe aktaracağız.

Ayrıca, simülasyon sırasında neler olduğunu daha iyi anlamak için (nispeten az sayıda nesil varsayarak) her nesilden önemli sonuçları yazdırmayı severim. Bunun için de ‘PrettyTable’ içe aktaracağız.

import random
import matplotlib.pyplot as plt
import numpy as np
from prettytable import PrettyTable

Uygunluk fonksiyonunu tanımlayın

Sırada, her bir çözümün uygunluğunu değerlendirecek uygunluk fonksiyonunu tanımlamamız gerekiyor. Bu durumda, grafikte en düz U-şeklini bulmak istiyoruz. Bu yüzden tepe noktasındaki y değerini ve x=-1 ile x=1 noktalarındaki değerleri hesapladım. 

Ardından grafiğin eğriliğini, bu üç noktadaki y değerleri arasındaki fark olarak hesapladım. En düz eğriyi elde etmek istediğimiz için, uygunluk fonksiyonunu tamamlarken eğriliği negatifledim.

# Define the fitness function (objective is to create the flattest U-shape)
def fitness_function(params):
    a, b, c = params
    if a <= 0:
        return -float('inf')  # Penalize downward facing u-shapes heavily
    vertex_x = -b / (2 * a) #x value at vertex
    vertex_y = a * (vertex_x ** 2) + b * vertex_x + c #y value at vertex
    y_left = a * (-1) ** 2 + b * (-1) + c #y-coordinate at x = -1
    y_right = a * (1) ** 2 + b * (1) + c #y-coordinate at x = 1
    curviness = abs(y_left - vertex_y) + abs(y_right - vertex_y)
    return -curviness  # Negate to minimize curviness

Başlangıç popülasyonu oluşturun

Rastgele çözümlerden oluşan bir popülasyon üretmek için ‘random’ kütüphanesini kullanacağız. Bu popülasyondaki her bir birey, a, b ve c için birer değer kümesidir.

# Create the initial population
def create_initial_population(size, lower_bound, upper_bound):
    population = []
    for _ in range(size):
        individual = (random.uniform(lower_bound, upper_bound),
                      random.uniform(lower_bound, upper_bound),
                      random.uniform(lower_bound, upper_bound))
        population.append(individual)
    return population

Bir seçim fonksiyonu oluşturun

Seçim fonksiyonu, bir sonraki popülasyonu oluşturmak için hangi bireylerin üreyeceğini belirleyecektir. Bu örnekte, popülasyondan rastgele bir alt kümenin alındığı ve bu alt küme içindeki en yüksek uygunluğa sahip bireylerin üreme için seçildiği bir turnuva seçimi süreci kullanacağız.

# Selection function using tournament selection
def selection(population, fitnesses, tournament_size=3):
    selected = []
    for _ in range(len(population)):
        tournament = random.sample(list(zip(population, fitnesses)), tournament_size)
        winner = max(tournament, key=lambda x: x[1])[0]
        selected.append(winner)
    return selected

Bir çaprazlama fonksiyonu yazın

Ardından, mevcut çözümlerden yararlanarak çaprazlama ile yeni çözümler oluşturmak için kısa bir fonksiyon yazacağız.

# Crossover function
def crossover(parent1, parent2):
    alpha = random.random()
    child1 = tuple(alpha * p1 + (1 - alpha) * p2 for p1, p2 in zip(parent1, parent2))
    child2 = tuple(alpha * p2 + (1 - alpha) * p1 for p1, p2 in zip(parent1, parent2))
    return child1, child2

Bir mutasyon fonksiyonu tasarlayın

Sonraki nesillere rastgelelik katmak için bir mutasyon fonksiyonuna da ihtiyacımız var. Bu, her nesilde seçilecek yeterli çeşitlilik olmasını sağlamak açısından önemlidir.

# Mutation function
def mutation(individual, mutation_rate, lower_bound, upper_bound):
    individual = list(individual)
    for i in range(len(individual)):
        if random.random() < mutation_rate:
            mutation_amount = random.uniform(-1, 1)
            individual[i] += mutation_amount
            # Ensure the individual stays within bounds
            individual[i] = max(min(individual[i], upper_bound), lower_bound)
    return tuple(individual)

Ana döngü

Sırada, tüm bu parçaları bir araya getirip algoritmamızı çalıştıracak ana döngüyü oluşturmak var. Sonuçları da görselleştirmemiz gerektiğinden, bu bölüme çizim (plot) kodunu ve final tablosunu oluşturmak için gereken kodu da ekleyeceğim.

# Main genetic algorithm function
def genetic_algorithm(population_size, lower_bound, upper_bound, generations, mutation_rate):
    population = create_initial_population(population_size, lower_bound, upper_bound)
    
    # Prepare for plotting
    fig, axs = plt.subplots(3, 1, figsize=(12, 18))  # 3 rows, 1 column for subplots
    best_performers = []
    all_populations = []

    # Prepare for table
    table = PrettyTable()
    table.field_names = ["Generation", "a", "b", "c", "Fitness"]

    for generation in range(generations):
        fitnesses = [fitness_function(ind) for ind in population]

        # Store the best performer of the current generation
        best_individual = max(population, key=fitness_function)
        best_fitness = fitness_function(best_individual)
        best_performers.append((best_individual, best_fitness))
        all_populations.append(population[:])
        table.add_row([generation + 1, best_individual[0], best_individual[1], best_individual[2], best_fitness])

        population = selection(population, fitnesses)

        next_population = []
        for i in range(0, len(population), 2):
            parent1 = population[i]
            parent2 = population[i + 1]

            child1, child2 = crossover(parent1, parent2)

            next_population.append(mutation(child1, mutation_rate, lower_bound, upper_bound))
            next_population.append(mutation(child2, mutation_rate, lower_bound, upper_bound))

        # Replace the old population with the new one, preserving the best individual
        next_population[0] = best_individual
        population = next_population

    # Print the table
    print(table)

    # Plot the population of one generation (last generation)
    final_population = all_populations[-1]
    final_fitnesses = [fitness_function(ind) for ind in final_population]

    axs[0].scatter(range(len(final_population)), [ind[0] for ind in final_population], color='blue', label='a')
    axs[0].scatter([final_population.index(best_individual)], [best_individual[0]], color='cyan', s=100, label='Best Individual a')
    axs[0].set_ylabel('a', color='blue')
    axs[0].legend(loc='upper left')
    
    axs[1].scatter(range(len(final_population)), [ind[1] for ind in final_population], color='green', label='b')
    axs[1].scatter([final_population.index(best_individual)], [best_individual[1]], color='magenta', s=100, label='Best Individual b')
    axs[1].set_ylabel('b', color='green')
    axs[1].legend(loc='upper left')
    
    axs[2].scatter(range(len(final_population)), [ind[2] for ind in final_population], color='red', label='c')
    axs[2].scatter([final_population.index(best_individual)], [best_individual[2]], color='yellow', s=100, label='Best Individual c')
    axs[2].set_ylabel('c', color='red')
    axs[2].set_xlabel('Individual Index')
    axs[2].legend(loc='upper left')
    
    axs[0].set_title(f'Final Generation ({generations}) Population Solutions')

    # Plot the values of a, b, and c over generations
    generations_list = range(1, len(best_performers) + 1)
    a_values = [ind[0][0] for ind in best_performers]
    b_values = [ind[0][1] for ind in best_performers]
    c_values = [ind[0][2] for ind in best_performers]
    fig, ax = plt.subplots()
    ax.plot(generations_list, a_values, label='a', color='blue')
    ax.plot(generations_list, b_values, label='b', color='green')
    ax.plot(generations_list, c_values, label='c', color='red')
    ax.set_xlabel('Generation')
    ax.set_ylabel('Parameter Values')
    ax.set_title('Parameter Values Over Generations')
    ax.legend()

    # Plot the fitness values over generations
    best_fitness_values = [fit[1] for fit in best_performers]
    min_fitness_values = [min([fitness_function(ind) for ind in population]) for population in all_populations]
    max_fitness_values = [max([fitness_function(ind) for ind in population]) for population in all_populations]
    fig, ax = plt.subplots()
    ax.plot(generations_list, best_fitness_values, label='Best Fitness', color='black')
    ax.fill_between(generations_list, min_fitness_values, max_fitness_values, color='gray', alpha=0.5, label='Fitness Range')
    ax.set_xlabel('Generation')
    ax.set_ylabel('Fitness')
    ax.set_title('Fitness Over Generations')
    ax.legend()

    # Plot the quadratic function for each generation
    fig, ax = plt.subplots()
    colors = plt.cm.viridis(np.linspace(0, 1, generations))
    for i, (best_ind, best_fit) in enumerate(best_performers):
        color = colors[i]
        a, b, c = best_ind
        x_range = np.linspace(lower_bound, upper_bound, 400)
        y_values = a * (x_range ** 2) + b * x_range + c
        ax.plot(x_range, y_values, color=color)

    ax.set_xlabel('x')
    ax.set_ylabel('y')
    ax.set_title('Quadratic Function')

    # Create a subplot for the colorbar
    cax = fig.add_axes([0.92, 0.2, 0.02, 0.6])  # [left, bottom, width, height]
    norm = plt.cm.colors.Normalize(vmin=0, vmax=generations)
    sm = plt.cm.ScalarMappable(cmap='viridis', norm=norm)
    sm.set_array([])
    fig.colorbar(sm, ax=ax, cax=cax, orientation='vertical', label='Generation')

    plt.show()

    return max(population, key=fitness_function)

Hepsini çalıştırmak

Son olarak, algoritmamız için parametreleri ayarlayıp çalıştırmamız gerekiyor.

# Parameters for the genetic algorithm
population_size = 100
lower_bound = -50
upper_bound = 50
generations = 20
mutation_rate = 1

# Run the genetic algorithm
best_solution = genetic_algorithm(population_size, lower_bound, upper_bound, generations, mutation_rate)
print(f"Best solution found: a = {best_solution[0]}, b = {best_solution[1]}, c = {best_solution[2]}")

Sonlandırma koşulu (isteğe bağlı)

Bu örnekte, algoritmamızın çalışacağı belirli bir nesil sayısı belirledik. Ancak alternatif olarak, hedef bir uygunluk değerine ulaştığımızda programı durduran bir sonlandırma koşulu da kurabilirdik. Bu da şöyle görünebilir:

def termination_condition(fitnesses, target_fitness):
    return max(fitnesses) >= target_fitness

Hepsini bir araya getirmek

İşte tüm kodun bir arada olduğu hal.

import random
import matplotlib.pyplot as plt
import numpy as np
from prettytable import PrettyTable

# Define the fitness function (objective is to create the flattest U-shape)
def fitness_function(params):
    a, b, c = params
    if a <= 0:
        return -float('inf')  # Penalize downward facing u-shapes heavily
    vertex_x = -b / (2 * a) #x value at vertex
    vertex_y = a * (vertex_x ** 2) + b * vertex_x + c #y value at vertex
    y_left = a * (-1) ** 2 + b * (-1) + c #y-coordinate at x = -1
    y_right = a * (1) ** 2 + b * (1) + c #y-coordinate at x = 1
    curviness = abs(y_left - vertex_y) + abs(y_right - vertex_y)
    return -curviness  # Negate to minimize curviness

# Create the initial population
def create_initial_population(size, lower_bound, upper_bound):
    population = []
    for _ in range(size):
        individual = (random.uniform(lower_bound, upper_bound),
                      random.uniform(lower_bound, upper_bound),
                      random.uniform(lower_bound, upper_bound))
        population.append(individual)
    return population

# Selection function using tournament selection
def selection(population, fitnesses, tournament_size=3):
    selected = []
    for _ in range(len(population)):
        tournament = random.sample(list(zip(population, fitnesses)), tournament_size)
        winner = max(tournament, key=lambda x: x[1])[0]
        selected.append(winner)
    return selected

# Crossover function
def crossover(parent1, parent2):
    alpha = random.random()
    child1 = tuple(alpha * p1 + (1 - alpha) * p2 for p1, p2 in zip(parent1, parent2))
    child2 = tuple(alpha * p2 + (1 - alpha) * p1 for p1, p2 in zip(parent1, parent2))
    return child1, child2

# Mutation function
def mutation(individual, mutation_rate, lower_bound, upper_bound):
    individual = list(individual)
    for i in range(len(individual)):
        if random.random() < mutation_rate:
            mutation_amount = random.uniform(-1, 1)
            individual[i] += mutation_amount
            # Ensure the individual stays within bounds
            individual[i] = max(min(individual[i], upper_bound), lower_bound)
    return tuple(individual)

# Main genetic algorithm function
def genetic_algorithm(population_size, lower_bound, upper_bound, generations, mutation_rate):
    population = create_initial_population(population_size, lower_bound, upper_bound)
    
    # Prepare for plotting
    fig, axs = plt.subplots(3, 1, figsize=(12, 18))  # 3 rows, 1 column for subplots
    best_performers = []
    all_populations = []

    # Prepare for table
    table = PrettyTable()
    table.field_names = ["Generation", "a", "b", "c", "Fitness"]

    for generation in range(generations):
        fitnesses = [fitness_function(ind) for ind in population]

        # Store the best performer of the current generation
        best_individual = max(population, key=fitness_function)
        best_fitness = fitness_function(best_individual)
        best_performers.append((best_individual, best_fitness))
        all_populations.append(population[:])
        table.add_row([generation + 1, best_individual[0], best_individual[1], best_individual[2], best_fitness])

        population = selection(population, fitnesses)

        next_population = []
        for i in range(0, len(population), 2):
            parent1 = population[i]
            parent2 = population[i + 1]

            child1, child2 = crossover(parent1, parent2)

            next_population.append(mutation(child1, mutation_rate, lower_bound, upper_bound))
            next_population.append(mutation(child2, mutation_rate, lower_bound, upper_bound))

        # Replace the old population with the new one, preserving the best individual
        next_population[0] = best_individual
        population = next_population

    # Print the table
    print(table)

    # Plot the population of one generation (last generation)
    final_population = all_populations[-1]
    final_fitnesses = [fitness_function(ind) for ind in final_population]

    axs[0].scatter(range(len(final_population)), [ind[0] for ind in final_population], color='blue', label='a')
    axs[0].scatter([final_population.index(best_individual)], [best_individual[0]], color='cyan', s=100, label='Best Individual a')
    axs[0].set_ylabel('a', color='blue')
    axs[0].legend(loc='upper left')
    
    axs[1].scatter(range(len(final_population)), [ind[1] for ind in final_population], color='green', label='b')
    axs[1].scatter([final_population.index(best_individual)], [best_individual[1]], color='magenta', s=100, label='Best Individual b')
    axs[1].set_ylabel('b', color='green')
    axs[1].legend(loc='upper left')
    
    axs[2].scatter(range(len(final_population)), [ind[2] for ind in final_population], color='red', label='c')
    axs[2].scatter([final_population.index(best_individual)], [best_individual[2]], color='yellow', s=100, label='Best Individual c')
    axs[2].set_ylabel('c', color='red')
    axs[2].set_xlabel('Individual Index')
    axs[2].legend(loc='upper left')
    
    axs[0].set_title(f'Final Generation ({generations}) Population Solutions')

    # Plot the values of a, b, and c over generations
    generations_list = range(1, len(best_performers) + 1)
    a_values = [ind[0][0] for ind in best_performers]
    b_values = [ind[0][1] for ind in best_performers]
    c_values = [ind[0][2] for ind in best_performers]
    fig, ax = plt.subplots()
    ax.plot(generations_list, a_values, label='a', color='blue')
    ax.plot(generations_list, b_values, label='b', color='green')
    ax.plot(generations_list, c_values, label='c', color='red')
    ax.set_xlabel('Generation')
    ax.set_ylabel('Parameter Values')
    ax.set_title('Parameter Values Over Generations')
    ax.legend()

    # Plot the fitness values over generations
    best_fitness_values = [fit[1] for fit in best_performers]
    min_fitness_values = [min([fitness_function(ind) for ind in population]) for population in all_populations]
    max_fitness_values = [max([fitness_function(ind) for ind in population]) for population in all_populations]
    fig, ax = plt.subplots()
    ax.plot(generations_list, best_fitness_values, label='Best Fitness', color='black')
    ax.fill_between(generations_list, min_fitness_values, max_fitness_values, color='gray', alpha=0.5, label='Fitness Range')
    ax.set_xlabel('Generation')
    ax.set_ylabel('Fitness')
    ax.set_title('Fitness Over Generations')
    ax.legend()

    # Plot the quadratic function for each generation
    fig, ax = plt.subplots()
    colors = plt.cm.viridis(np.linspace(0, 1, generations))
    for i, (best_ind, best_fit) in enumerate(best_performers):
        color = colors[i]
        a, b, c = best_ind
        x_range = np.linspace(lower_bound, upper_bound, 400)
        y_values = a * (x_range ** 2) + b * x_range + c
        ax.plot(x_range, y_values, color=color)

    ax.set_xlabel('x')
    ax.set_ylabel('y')
    ax.set_title('Quadratic Function')

    # Create a subplot for the colorbar
    cax = fig.add_axes([0.92, 0.2, 0.02, 0.6])  # [left, bottom, width, height]
    norm = plt.cm.colors.Normalize(vmin=0, vmax=generations)
    sm = plt.cm.ScalarMappable(cmap='viridis', norm=norm)
    sm.set_array([])
    fig.colorbar(sm, ax=ax, cax=cax, orientation='vertical', label='Generation')

    plt.show()

    return max(population, key=fitness_function)

# Parameters for the genetic algorithm
population_size = 100
lower_bound = -50
upper_bound = 50
generations = 20
mutation_rate = 1

# Run the genetic algorithm
best_solution = genetic_algorithm(population_size, lower_bound, upper_bound, generations, mutation_rate)
print(f"Best solution found: a = {best_solution[0]}, b = {best_solution[1]}, c = {best_solution[2]}")

Sonuçların Değerlendirilmesi

Fonksiyonumuzun nasıl performans gösterdiğini anlamak için çıktılarımızı kullanalım.

Kodlu örneğin ham veri çıktısını gösteren tablo.

Öncelikle, tablomuzdan 20 nesli tamamladığımızı ve uygunluğun her nesilde nispeten daha negatiften daha az negatife doğru arttığını görüyoruz. Harika!

Kodlu örnekte nesiller boyunca uygunluk grafiği.

Uygunluk grafiğimizden, uygunluğun zaman içinde iyileştiğini ve sonraki nesillerde biraz yataylaştığını doğrulayabiliyoruz. Ancak erken nesillerin bazılarında modelimizin haklı olarak elediği gerçekten kötü çözümler de olduğunu görüyoruz.

Ayrıca birkaç nesilde uygunluk aralığında çok fazla çeşitlilik olmadığını görüyoruz. Bu her zaman kötü bir şey olmasa da dikkat edilmesi gereken bir durumdur. Bazı durumlarda bu, popülasyonda yeterli çeşitlilik olmadığında ortaya çıkan ve erken yakınsama (premature convergence) olarak adlandırılan bir problemin göstergesi olabilir.

Şimdi de gerçekten düzleşip düzleşmediğini görmek için her nesilden denklemlerimizi çizelim.

Kodlu örnekteki çözümlerle denklemlerin grafiği.

Erken nesil çözümlerimizin oldukça eğri olduğunu görüyoruz ki bu mantıklı. Bu denklem U-şeklinde bir grafik üretir. Ancak sonraki nesillerin giderek daha düz hale geldiğini de görüyoruz. Tam da aradığımız buydu!

Şunu da belirtmek gerekir ki açıkça -50 ile 50 arasındaki sınırlar içinde bakıyoruz. Bu grafiği uzaklaştırırsak, bu görünüşte düz çizgilerin bile aslında daha geniş tabanlı U-şekilleri olduğunu gösterirdi.

Bu örnek, varsayımlarımızın öngörülerimizi desteklediğinden emin olmak için analizlerde dikkatli olmanın önemini vurgular. Modelimiz tam olarak istediğimiz şeyi yaptı ancak yalnızca verdiğimiz sınırlar içinde. Daha uzakta düz bir şekil oluşturacak değişken kombinasyonunu bulmak isteseydik, modeli daha geniş sınırlarla yeniden çalıştırmamız gerekirdi.

Kodlu örnekte her parametrenin nesiller boyunca nasıl değiştiğini gösteren grafik.

Değişkenlerin her birini, nesiller boyunca ne kadar değiştiklerini görmek için ayrı ayrı çizebiliriz. Bu simülasyon çalıştırmasında c’nin başlarda çok dalgalandığını, a ve b’nin ise çok daha az dramatik biçimde değiştiğini görüyoruz. Başlangıç popülasyonu rastgele olduğundan, bu grafiğin görünümü parametreleri değiştirmesek bile her çalıştırmada farklı olacaktır.

Kodlu örneğin son neslindeki tüm bireylerin tamamını gösteren üç alt grafik.

Bakmak istediğim son grafik, tek bir nesildeki tüm popülasyonun anlık görüntüsü: son nesil. Popülasyondaki her bireyin üç parametresi olduğundan, 3 boyutlu grafiklerle uğraşmak yerine bunları üç alt grafiğe ayırmayı tercih ettim. 

Bu grafiklerden gerçekten görmek istediğim şey, bu nesil içinde iyi bir çeşitlilik olup olmadığı. Yeterli çeşitlilik olmazsa, mevcut tek çözümler düşük uygunluk değerlerine sahip olanlar olabilir ve bu da erken yakınsamaya yol açar. Ancak bu grafik, iyi bir çeşitlilik düzeyi olduğunu gösteriyor gibi görünüyor; bu sonuca memnunum.

Dikkat Edilmesi Gerekenler ve İpuçları

Genetik algoritmalar çok çeşitli durumlarda kullanılabilir. Ancak akılda tutulması gereken birkaç husus vardır.

Parametre ayarı

Her modelde olduğu gibi, bir genetik algoritmanın performansı başta popülasyon büyüklüğü, çaprazlama oranı, mutasyon oranı ve sınırlandırma parametreleri olmak üzere çeşitli parametrelere bağlıdır. Bu parametreleri değiştirmek, modelinizin performansını da değiştirecektir.

Genel bir kural olarak, daha büyük popülasyonlar, seçilecek daha fazla seçenek olduğundan optimal çözüme daha hızlı ulaşmanıza yardımcı olur. Ancak daha büyük popülasyonlar, çalıştırmak için daha fazla zaman ve kaynak gerektirir.

Daha yüksek bir çaprazlama oranı, farklı bireylerden yararlı özellikleri daha sık birleştirerek daha hızlı yakınsamaya yol açabilir. Ancak çok yüksek bir çaprazlama oranı, popülasyon yapısını bozarak erken yakınsamaya neden olabilir.

Daha yüksek bir mutasyon oranı, genetik çeşitliliği koruyarak algoritmanın yerel optimumda takılıp kalmasını engeller. Ancak mutasyon oranı çok yüksek olursa, aşırı rastgelelik katarak yakınsama sürecini bozabilir ve algoritmanın çözümleri rafine etmesini zorlaştırabilir.

Sınırlandırma parametreleri, algoritmanın çözümleri aradığı aralığı tanımlar. Bunları, özel iş probleminize göre ayarlamak önemlidir. Çok dar bir sınırlandırma alanı, probleminizin optimal çözümlerini kaçırabilir. Çok geniş bir alan ise daha fazla zaman ve kaynak gerektirir. Ancak başka hususlar da vardır. 

Örneğin, yukarıdaki kodlu örneğimizdeki denklemin teorik olarak sınırı yoktur. Ancak pratikte, bilgisayardan sonsuz büyük bir grafikte en düz yayı bulmasını isteyemeyiz. Bu yüzden sınırlar gereklidir. Fakat bu sınırları değiştirmek, optimal cevabı da değiştirecektir. Bana göre, herhangi bir modelde belirli kullanım durumunuza uygun sınırları belirlemek elzemdir.

Modelinizde ayarlamanız gereken başka önemli parametreler de olabilir. Probleminiz için en iyi ayarları bulmak amacıyla farklı değerlerle deneyler yapın. GA’ların özel olarak ayarlanması hakkında bilgi edinmek için Bilgilendirilmiş Yöntemler: Genetik Algoritmalar bölümüne göz atın.

Kodlama (encoding)

Kodlama, bir genetik algoritmayı kurarken önemli bir adımdır. Olası çözümleri algoritmanın işleyebileceği bir formata dönüştürmeyi içerir. Örneğin, kamyonlarımız için teslimat rotalarını optimize edecek bir genetik algoritma tasarladığımızı varsayalım. Teslimat konumları listesini bir GA’ya girecek kromozoma nasıl dönüştürürsünüz? Kodlama, bu listeyi, olası bir teslimat rotasını temsil eden sıralı konumlar gibi, algoritmanın üzerinde işlem yapabileceği bir sıraya dönüştürür.

Doğru kodlama, genetik algoritmanın çözüm uzayını etkin şekilde keşfetmesini ve çözümleri anlamlı şekillerde birleştirmesini ya da mutasyona uğratmasını sağlar. Olmadan algoritma karmaşık verilerle baş edemezdi. Teslimat konumları, çizelgeleme problemleri veya müşteri hizmeti etkileşimleri gibi veriler, bilgisayarlar için yalnızca kodlama yoluyla erişilebilir hale gelir. 

Seçebileceğiniz çeşitli kodlama şemaları vardır. Sizin durumunuz için hangisinin en uygun olabileceğini öğrenmek üzere Python’da Kategorik Verilerle Çalışma veya Python’da String’leri Byte’lara Dönüştürme içeriklerine göz atabilirsiniz.

Erken yakınsama

Erken yakınsama, popülasyonun çok benzer hale gelmesi durumunda ortaya çıkan bir problemdir. Bu, algoritmanın küresel optimumu bulmak yerine yerel optimumda takılı kalmasına yol açabilir. Özünde, popülasyonda yeterli çeşitlilik yoksa çözümler “akraba evliliği” yapmış gibi olur ve en iyi çözüme ulaşamazsınız.

Erken yakınsamayı önlemek için deneyebileceğiniz birkaç teknik vardır. Daha yüksek bir mutasyon oranı, popülasyona daha fazla çeşitlilik katarak çözüm uzayının yeni bölgelerinin keşfine yardımcı olabilir. Ayrıca, çözüm uzayının başlangıçtan itibaren geniş kapsamda keşfini sağlamak için daha çeşitli bir başlangıç popülasyonuyla başlayabilirsiniz.

Hâlâ erken yakınsama ile karşılaşıyorsanız, algoritmanın ilerlemesine bağlı olarak mutasyon oranı ve çaprazlama oranı gibi parametreleri dinamik olarak ayarlamayı deneyebilirsiniz. Alternatif olarak, zaman zaman birey değişimi yapan birden fazla popülasyon (adanalar gibi düşünebilirsiniz) kullanarak çeşitliliği koruyabilirsiniz.

Erken yakınsama ve diğer makine öğrenimi hataları hakkında daha fazla bilgiyi Makine Öğrenimi Kavramlarını İzleme kursunda bulabilirsiniz.

Elitizm

Öte yandan, güçlü çaprazlama ve mutasyon ya da seçimdeki herhangi bir rastgelelik, harika çözümlerin üreyememesine yol açabilir. İşte bu noktada elitizm devreye girebilir. Elitizm, en iyi bireylerin iyi çözümleri korumak için doğrudan bir sonraki nesle aktarılması tekniğidir. Bu, evrimsel süreç sırasında en iyi çözümlerin kaybolmamasını sağlar.

Ancak elitizmi kullanırken dikkatli olun. Yeterince yüksek popülasyon çeşitliliği olmadan elitizm kullanılırsa yine erken yakınsamaya yol açabilirsiniz. İş probleminiz elitizm tekniklerini gerektiriyorsa, bunu yeterince güçlü çaprazlama ve mutasyon fonksiyonları ile büyük bir popülasyon büyüklüğüyle eşleştirdiğinizden emin olun. Bu yaklaşım, bilinen çözümlerden yararlanma ile yeni olasılıkları keşfetme arasında sağlıklı bir denge kurmaya yardımcı olur.

Sonuç

Genetik algoritmalar, veri biliminin doğal dünyadan ilham almasına harika bir örnektir. Doğal seçilim sürecini taklit ederek karmaşık optimizasyon problemlerini çözmek için güçlü bir yöntem sunarlar.

Biyolojiden ilham alan diğer modellere ilgi duyuyorsanız, sinir ağları ve diğer derin öğrenme tekniklerini Python’da Derin Öğrenmeye Giriş kursunda öğrenebilirsiniz. Ayrıca DataCamp’in Üretken Yapay Zekâ Kavramları kursu ve Yapay Zekâ Etiği içerikleri de ilginizi çekebilir.

Biyolojik verilerle bir proje yapma ilhamı aldıysanız, R ile Genomik Verileri Analiz Etme veya Python’da Biyomedikal Görüntü Analizi içeriklerine göz atın.


Amberle McKee's photo
Author
Amberle McKee
LinkedIn

Biyolojik araştırma ortamında verilerle çalışma konusunda 13 yıllık deneyime sahip bir doktora sahibiyim. Python, MATLAB ve R dahil olmak üzere birkaç programlama dilinde yazılım geliştiriyorum. Öğrenme sevgimi dünyayla paylaşma konusunda tutkuluyum.

Konular

Bu kurslarla yapay zekâyı öğrenin!

Kurs

MLOps Dağıtımı ve Yaşam Döngüsü

4 sa
11.4K
Bu kursta, modern MLOps çerçevesini inceleyerek makine öğrenimi modellerinin yaşam döngüsünü ve dağıtımını keşfedeceksiniz.
Ayrıntıları GörRight Arrow
Kursa Başla
Devamını GörRight Arrow