- Startseite
- Allgemeine Informationen zur Lehrveranstaltung
- Einfaches Python Setup und Wichtige Informationen
- 0. Python Einführung und Checkup
- 1. Einführung und einfache Algorithmen
- 2. Differenzieren und Integrieren
- 3. Vektoren, Matrizen und Vektorisierung in Python
- 4. Datenanalyse bzw. Datenauswertung
- 5. Grundlagen der Optimierung und Gradient Descent
- 6. Stochastische Optimierung und Genetische Algorithmen
- 7. Monte-Carlo-Methoden – Simulation und Integration
- 8. Monte-Carlo-Methoden, Teil 2 – Monte-Carlo-Integration, Teil 2 und Random Walk
- 9. Unsupervised Machine Learning: Clustering von Daten
- 10. Supervised Machine Learning: Grundlagen
- 11. Einführung in künstliche neuronale Netzwerke
Die Jupyter-Notebooks zur Lehrveranstaltung finden Sie im zugehörigen GitHub-Repository.
6 Stochastische Optimierung und Genetische Algorithmen¶
In diesem Notebook wollen wir uns letztlich mit stochastischer Optimierung und genetischen Algorithmen befassen. Wir tun das zwar hauptsächlich auf einem pragmatischen Niveau, aber auch sodass wir in der kurzen Zeit bereits etwas damit anfangen können.
Zu diesem Zweck brauchen wir kurz ein paar Vorüberlegungen zu den Begriffen Zufallszahlen und Sampling (Stichproben), und zwar hauptsächlich, wie wir diese in Python bekommen und woher.
6.1 Zufallszahlen¶
Echte Zufallszahlen sind schwer zu erzeugen. Daher bedienen sich Computer-Nutzer sogenannter Pseudo-Zufallszahlen. Damit ist gemeint, dass man einen (Pseudo-Zufallszahlen-)Generator hat, der eine bestimmte Methode verfolgt, um die gewünschte Anzahl von Zahlen so zu erzeugen, dass sie möglichst gut der gewünschten Wahrscheinlichkeitsverteilung entsprechen. Sehen wir uns das gleich einmal anhand einiger Beispiele an.
Zunächst die Imports von heute:
%matplotlib inline
import numpy as np
# Importiere Statistische Package aus SciPy
import scipy.stats as scs
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from tqdm import tqdm
NumPy hat ein Modul namens random, in dem man die wichtigsten Dinge finden kann, z.B.:
# Zufallszahlen zwischen 0 und 1, gleichverteilt
# Fixiere den Seed, um reproduzierbare Ergebnisse zu erhalten:
#np.random.seed(1234)
# Generiere ein Feld von Zufallszahlen
np.random.random(size=(4, 5))
array([[0.79197118, 0.68952016, 0.24804623, 0.54430981, 0.78626382], [0.55648307, 0.76230378, 0.1462054 , 0.62987766, 0.37530907], [0.15204072, 0.68521923, 0.45125418, 0.70348015, 0.62994591], [0.70240103, 0.97066536, 0.63898829, 0.5710686 , 0.6405659 ]])
# Zufallszahlen zwischen 1 und 2
a = 1
b = 2
(b - a) * np.random.random(size=(4, 5)) + a
array([[1.30387473, 1.73281576, 1.99759377, 1.96305614, 1.130397 ], [1.0255525 , 1.22743875, 1.81184428, 1.10726127, 1.62034331], [1.90063649, 1.61034101, 1.86227192, 1.31539797, 1.07224203], [1.79628723, 1.98071287, 1.01562668, 1.81603579, 1.98357505]])
# Zufallszahlen aus der Normalverteilung
normal_values_x = np.random.normal(0., 1., size=(1000))
normal_values_y = np.random.normal(0., 1., size=(1000))
fig = plt.figure()
plt.scatter(normal_values_x, normal_values_y, s=0.1)
plt.show()
Zur Erinnerung: Wir können jederzeit die Eigenschaften einer so erzeugten Verteilung überprüfen, z.B. den Mittelwert und die Standardabweichung, aber auch höhere Momente wie Skewness oder NumPyKurtosis:
# checke Mittelwert
print("Mittelert:", np.mean(normal_values_x))
# checke Standardabweichung
print("Standardabweichung:", np.std(normal_values_x))
# checke Skewness (Asymmetrie)
print("Skewness:", scs.skew(normal_values_x))
# checke Kurtosis ("Dicke der Extrembereiche im Vergleich zu Normal")
print("Kurtosis:", scs.kurtosis(normal_values_x))
Mittelert: -0.043983512854805236 Standardabweichung: 1.0099340409290625 Skewness: 0.1242659577410384 Kurtosis: -0.12900228333213315
6.2 Sampling¶
Mit diesen ersten Schritten haben wir ein Gefühl für das Arbeiten mit Zufallszahlen in Python bekommen. Der nächste Schritt ist, aus einer Vorhandenen Menge von Daten eine Stichprobe zu ziehen. Man nennt diesen Schritt auch einfach Sampling (vom englischen Begriff her).
Zunächst sollte ich erwähnen, dass wir bereits im vorigen Abschnitt “gesampelt” haben, denn die entsprechenden NumPy-Funktionen ziehen ja auch eine Stichprobe aus der gewünschten Verteilung. Wenn es also nur darum geht, Werte aus einer der Standard-Verteilungen zu bekommen, dann benutzt man einfach den entsprechenden Befehl.
Wenn wir allerdings Daten vorgegeben haben und aus diesen unsere Stichprobe ziehen wollen, dann müssen wir das den Computer aus einer bestimmten Menge (einem Array) zufällig die gewünschte Anzahl von Werten ziehen lassen. In NumPy gibt es auch dafür einen Befehl.
# Erzeuge ein einfaches Array von Daten
all_data = np.arange(1, 20)
# hole mir eine zufällige 3x5 Matrix von Werten aus diesem Array von 1 bis 20
np.random.choice(all_data, size=(3, 5))
array([[18, 8, 1, 12, 4], [ 1, 15, 6, 16, 1], [10, 1, 10, 6, 2]])
# hier kommen einige Zahlen mehrfach vor. Wenn man das nicht möchte:
np.random.choice(all_data, size=(17), replace=False)
array([10, 3, 13, 1, 18, 19, 8, 17, 16, 11, 5, 12, 15, 4, 9, 2, 6])
# und schließlich kann man noch ein Array mit Wahrscheinlichkeiten
# übergeben, nach der die Elemente bestimmt werden sollen
# Beispiel: Ein Würfel mit den Augenzahlen 1, 1, 1, 2, 2, 3
# zunächst die Möglichkeiten für die Augen
die_faces = np.array([1, 2, 3])
# Dann die relativen Häufigkeiten
die_probs_raw = np.array([3, 2, 1])
# das Argument p muss jedoch auf 1 normiert sein
die_probs = die_probs_raw/np.sum(die_probs_raw)
print("Wahrscheinlichkeiten für 1, 2, 3:", die_probs, "\n")
# Erzeuge nun einige Würfe mit diesem Würfel
n_rolls = 300
die_rolls = np.random.choice(die_faces, size=n_rolls, p=die_probs)
print("Ein paar Würfe:", die_rolls)
Wahrscheinlichkeiten für 1, 2, 3: [0.5 0.33333333 0.16666667] Ein paar Würfe: [1 1 1 1 2 3 2 1 2 2 2 2 3 1 2 1 3 1 2 1 2 2 2 2 2 3 3 2 1 1 3 2 3 1 1 1 1 1 3 1 1 2 2 1 1 1 1 3 1 2 2 1 2 1 3 1 1 1 2 3 2 1 2 1 1 3 1 2 2 1 1 1 2 3 3 1 2 3 2 2 1 1 2 1 2 2 2 2 1 1 1 3 2 1 1 1 1 1 1 1 2 1 1 2 3 1 3 2 1 3 2 2 2 1 1 1 3 2 1 1 1 2 3 3 1 2 1 1 2 2 2 1 2 1 2 2 2 1 2 2 3 3 3 1 1 2 1 2 3 1 3 1 1 2 1 1 2 3 1 2 1 2 1 1 3 2 1 2 3 2 3 1 3 1 1 2 1 2 3 1 3 2 2 1 1 1 3 3 3 3 1 3 1 1 2 1 2 2 1 1 1 1 1 3 3 2 3 3 3 1 2 1 2 1 1 1 1 1 2 3 3 2 2 2 1 2 1 2 1 1 2 1 2 3 3 2 2 1 1 1 1 1 1 1 3 1 1 2 1 1 3 1 1 2 2 1 1 2 2 2 2 1 3 1 2 1 1 2 1 1 2 1 3 3 1 3 1 1 1 1 1 1 3 2 1 2 1 2 2 2 2 1 1 1 1 1 1 2 2 2]
# Passt das zusammen?
# Was sollten die Ergebnisse sein?
print("Theoretische Anzahl:", n_rolls * die_probs)
fig = plt.figure()
plt.hist(die_rolls, bins=[1,2,3,4])
plt.show()
Theoretische Anzahl: [150. 100. 50.]
6.3 Stochastische Optimierung¶
Mit diesen Werkzeugen können wir uns an die stochastische Optimierung wagen. Was bedeutet das eigentlich? Wir hatten in der vorigen Einheit mit Optimierung zu tun, ganz im Allgemeinen. Dabei sucht man grundsätzlich das Optimum einer Funktion von meist mehreren Variablen.
Die Methoden, die wir dabei besprochen haben waren zunächst einmal “brute force”, also alle Möglichkeiten durchzugehen und die beste über einen globalen Vergleich zu identifizieren. Danach haben wir aber auch noch mit “Gradient Descent” experimentiert, bei dem man die Ableitung der Funktion nutzt, um schrittweise an den tiefsten Punkt zu kommen.
Ein konkretes Problem bei Gradient Descent z.B. ist, dass man in einem lokalen Minimum “steckenbleiben” kann, wenn der Weg dort hinein führt und die Schrittweite zu klein ist, um wieder herauszukommen, obwohl das globale Minimum ein anderes ist. Man kann sich das so vorstellen wie eine riesige Berg- und Tal-Landschaft mit vielen Tälern, Mulden, Gipfeln, Bergrücken, Löchern, etc., und irgendeins davon ist der niedrigste Punkt. Insbesondere wenn diese Landschaft in hochdimensionalen Räumen betrachtet wird, leuchtet ein, dass man es mit Gradient Descent vielleicht schwer haben könnte.
Das hat vor allem auch damit zu tun, dass man diese Funktions-“Landschaft” gar nicht wirklich kennt, weil man entweder keine Ahnung hat, wie die Funktion überhaupt aussieht, oder weil es sehr teuer ist, die Funktion zu berechnen. Vor allem in so einem Fall hilft es, wenn man sich nicht die gesamte Landschaft ansehen muss, sondern Schritt für Schritt mit sehr wenigen Werten zu einer Lösung kommen kann.
Eine mögliche Alternative hier ist, einfach viele zufällige Werte aus dem Wertebereich zu nehmen (aber längst nicht alle, also ein Sample), und für diese Werte den Wert der Funktion zu bestimmen. Der kleinste davon ist dann eine Näherung für das Minimum. Im folgenden Beispiel ist aus Gründen der Anschaulichkeit die Funktion bekannt, sodass wir sie auch plotten können und uns den Algorithmus etwas ansehen können. Behalten Sie aber bitte im Kopf, dass wir das im Normalfall nicht hätten, sondern einfach nur die Samples, für die wir die Funktion berechnen. Sehen wir uns gleich einmal an, ob sowas funktioniert:
# definiere eine Funktion für die Suche nach deren Minimum
def a_landscape(x, y):
# mehrere Täler und Berge, mit einer globalen Neigung
our_function = 4*np.sin(x) + 6*np.sin(y) - 0.5 * x - 0.2 * y
return our_function
# plotten wir das mal
# definiere x-Werte
x_vals = np.linspace(-10, 10, 500)
# definiere y-Werte
y_vals = np.linspace(-10, 10, 500)
# erzeuge x-y-Grid für 3D Plots
X, Y = np.meshgrid(x_vals, y_vals)
Z = a_landscape(X, Y)
# neue Grafik
fig = plt.figure()
# 3D Achsen erzeugen
ax = fig.add_subplot(1,1,1, projection='3d')
# erzeuge 3D-Oberflächenplot
ax.plot_surface(X, Y, Z, cmap="magma")
plt.show()
# für Reproduzierbarkeit die nächste Zeile verwenden
# np.random.seed(12345)
sample_size = 100
# nun wählen wir einen Satz Werte zufällig aus unserem Bereich:
# Wertebereich für x und y
a = -10
b = 10
def evaluate_a_sample(size=sample_size, verbose=False):
# ziehe eine Stichprobe von 2xSamplesize (für x und y)
test_sample = (b - a) * np.random.random(size=(2, sample_size)) + a
# Werte die Funktion an diesen Punkten aus
sample_values = a_landscape(*test_sample)
if verbose: print("Alle Funktionswerte des Samples:\n", sample_values)
the_minimum = np.min(sample_values)
if verbose: print("Der kleinste Wert im Sample:", the_minimum)
return the_minimum
evaluate_a_sample(100, verbose=True)
Alle Funktionswerte des Samples: [ 1.17142875 0.97573562 -3.88491455 2.78113873 10.03976207 3.26409715 1.29010883 -1.15698163 -0.81935754 4.29662903 -2.12142552 -4.50508201 -1.78235587 -1.17330563 4.33399325 -7.9429288 8.26670808 -6.77115772 -5.60014475 4.4569142 0.34175233 -0.7787587 3.12033423 -1.3133641 -1.72820894 -2.87851437 2.47129906 -0.36734833 0.83624414 -2.3955801 -4.89375516 -2.77561799 4.3778461 -1.6969477 -8.67149128 -5.05554994 -1.96724822 -2.1786499 3.60664283 1.22486584 7.41380034 -2.3077217 -2.80336404 2.60211965 -1.35013164 -5.30006766 3.91344113 3.46325515 8.23429133 -3.22076531 -6.77810866 4.03788049 -0.26031826 3.03814719 1.25850898 3.27579486 -0.74325716 -1.87642393 -3.00947318 4.6310794 -8.88558398 -4.84110639 -1.29279623 9.40325394 -4.22371007 13.29426299 -1.92711722 3.26627624 -0.63672832 -4.32156533 -11.90296007 -3.66432569 -10.37227186 5.5145162 -5.42790445 0.66389439 7.41867903 -0.2858999 -1.00233719 7.03982306 5.09687712 7.67575945 10.58376319 -0.9602119 -9.41629795 -6.47610497 -5.80803747 -3.37737569 8.03517916 1.65976662 5.61108428 -2.07240621 5.779313 -4.1636313 -5.62779617 -6.95856956 3.32749184 -1.51709816 1.46712751 13.22907335] Der kleinste Wert im Sample: -11.902960073154448
-11.902960073154448
# Anzahl der Samples
n_samples = 200
# Eine Liste mit Resultaten anlegen
min_list = []
# Mache die Auswertung für einige Samples
for i in range(n_samples):
min_list.append(evaluate_a_sample(100))
print("Liste der Minima:", min_list, "\n")
print("Minimum der Minima:", np.min(min_list))
fig = plt.figure()
plt.hist(min_list)
plt.show()
Liste der Minima: [-11.64478035499988, -11.632923326168536, -12.03483233279757, -11.694570337481183, -10.797256628606783, -11.057610276532678, -10.767234812207075, -13.239854794940975, -12.13968826353819, -13.238118833968828, -13.223021786640718, -10.78030295716005, -12.739959347247558, -12.628887912071688, -8.419735759110473, -13.243088458187328, -11.92871001436789, -10.325902506402471, -11.782331596862367, -12.15880175426219, -11.158539455859302, -11.14757635716059, -12.859272859978859, -12.147087305610164, -13.03120189030213, -11.514678656858244, -12.529324264602648, -9.76117268489881, -13.152047551321584, -13.125506039748862, -10.720219243823813, -10.562433315303311, -13.0711975122816, -12.07025776642195, -9.253504873207989, -12.610791643180676, -12.638198715556449, -12.26817080654964, -10.323230596378991, -12.663676617368196, -12.704187698652062, -13.197403325019218, -13.519597628402899, -10.794784124927357, -11.60822626585363, -13.684819352483318, -12.01919138656807, -9.351741045449296, -11.983382960225073, -10.493174031549657, -13.034472626015885, -11.537872760664333, -8.443780084996716, -10.176831755632517, -11.640725657423067, -11.928137668010026, -12.290323097731196, -10.85826827683184, -12.180059772356236, -11.900275789327788, -9.631930859729154, -12.701852612996415, -10.122477043508624, -11.693826235054924, -10.916835857817773, -13.440726946169052, -11.419017879327711, -12.233725258340373, -11.871437320808072, -12.892083045586547, -10.525104742028237, -12.45364105439871, -10.32886348719294, -12.816988906597686, -11.56625201575246, -10.199382716362035, -12.178819074756241, -13.396311477232857, -10.31535218707413, -13.162457761182282, -10.520831424741095, -11.77149615854568, -13.924964177506912, -10.824462441690045, -12.396013297728091, -11.784046099426641, -13.267915158585861, -12.602514087361806, -12.281922980472851, -13.548733116585833, -11.401254851664767, -11.819471972724006, -13.182762655665968, -12.878643702848894, -13.058411992199494, -12.403348132784682, -11.241860624167145, -11.657916054305671, -10.291750255155646, -11.779266890747882, -10.660393539100788, -11.711926326276565, -9.608327229691138, -12.401985873530709, -11.064214251131727, -11.948065689084277, -11.590177384457412, -12.922798523530924, -12.224135677670441, -12.005999228122095, -10.277047980629135, -10.342150554736273, -8.469642404899506, -11.332570745040004, -12.929787789086394, -12.997263827358434, -10.91110639210329, -9.88193403129701, -11.773502466016295, -12.976214645314288, -10.48416700409964, -12.426582227990485, -12.622928970122341, -11.618850614031183, -10.382504537211284, -11.212992274468066, -11.572396254423944, -13.283896713387485, -11.08016357160019, -12.128746900915605, -12.945394349087678, -13.241782812827434, -12.161016758515327, -12.555524326581306, -12.690519982520337, -12.667184013307365, -12.30501572694944, -13.157361006470849, -9.820065018613219, -11.21183799245604, -11.387273723539579, -11.792597529058312, -13.192160294909387, -12.73935995541893, -11.651576387715195, -13.16923574597592, -12.00795948266077, -13.158554821362122, -10.89818234825325, -13.141129993617751, -13.151597622374046, -12.414747034535988, -10.720731783225089, -12.503317532840562, -9.834112746360875, -12.325210720875353, -12.270495581852229, -12.91734538076934, -12.855031100474827, -13.247557501805469, -11.813684878913662, -12.155164966956765, -10.594581476160924, -11.665224379951619, -12.000847782907709, -11.519773819136665, -13.369778674554993, -10.499452842543109, -10.294484166920222, -12.828197225700231, -10.194923842325727, -11.634594284289665, -11.150113938855581, -10.137359443566954, -12.94867293839079, -13.44954672252019, -13.062207555604088, -10.458073996987133, -10.61006346851499, -13.309109066022955, -11.56738841282575, -13.504263888781608, -11.884538372643911, -13.527952117473344, -11.414016769023823, -9.801423485166206, -12.191631772819012, -12.115792586515663, -11.920351291901804, -10.773523495391617, -11.397480492414843, -12.048711088595294, -12.588774131916795, -11.645729224995215, -14.097563568947432, -9.78177000829253, -11.320192005085268, -12.261253420981973, -13.552678861231636, -11.581742869958646] Minimum der Minima: -14.097563568947432
Letzten Endes wird man die Sache aber etwas cleverer angehen als wir hier in diesem Beispiel und z.B. bei jedem neuen Sample die Position des Minimums (oder der 10 kleinsten Werte) aus dem vergangenen Sample als Basis dafür hernehmen, wo die Punkte des neuen Samples konzentriert werden sollten. Dadurch konzentriert man die Suche auf interessante Bereiche.
Diese Taktik geht bereits in Richtung des nächsten Themas und ist daher eine gute Überleitung, nämlich:
6.4 Genetische Algorithmen¶
Ein genetischer Algorithmus ist eine Taktik zur stochastischen Optimierung, d.h., zum Finden der optimalen Parameter für eine Funktion, sodass diese minimal oder maximal wird. Die Basis für die “Genetik” bei der Optimierung ist eine Art Codierung des Inputs, z.B. ein Vektor von Zahlen. Aus diesem Vektor wird dann der Wert der zu optimierenden Funktion berechnet, die in diesem Zusammenhang “Fitnessfunktion” heißt.
Bei der Optimierung selbst geht man über mehere Schritte, die mit “Generationen” verglichen werden, weil sie mehrere “Individuen” dieser Vektoren enthalten. Von einer Generation zur nächsten gibt eine Auswahl der “fittesten” Individuen, genannt “Eltern”, ihre “Erbinformation” weiter. Dadurch entstehen im Laufe der Generationen immer fittere Individuen, d.h. wir nähern uns dem Optimum der Fitnessfunktion.
6.5 Beispiel: Das binäre Rucksackproblem¶
Warum ist das interessant? Nehmen wir z.B. ein kombinatorisches Optimierungsproblem etwas genauer unter die Lupe, das als “binäres Rucksackproblem” bezeichnet wird. Denken Sie dabei etwa an einen Fahrradboten. Dieser hat einen Rucksack für den Transport von Gütern, die er ausliefert. Jedes Ding, das er liefern kann, hat dabei ein Gewicht und einen Preis. Der Bote packt für eine Fahrt den Rucksack voll mit Dingen, sodass der gesamte Preis möglichst hoch wird, das gesamte Gewicht jedoch die Kapazität des Rucksacks nicht überschreitet. Dabei darf er jedes Ding entweder nicht mitnehmen oder genau einmal mitnehmen (daher binär).
Dieses Problem ist, wie gesagt, kombinatorisch. Man kann, um es zu lösen, im Prinzip auch per “brute force” alle Möglichkeiten durchgehen und dafür das Maximum der aufsummierten Preise finden. Das wird allerdings sehr schnell sehr langwierig. Bei der Abkürzung der Zeit für die Lösung hilft der genetische Algorithmus.
Sehen wir uns das nun im Detail an.
# Tabelle der Anzahl der Möglichkeiten, Dinge
# in den Rucksack zu packen, wenn es _N_ zur Auswahl gibt
for n in np.arange(1, 62, 5):
# für jedes Ding gibt es 2 Möglichkeiten (mit, nicht mit)
# daher insgesamt 2**n
print(n, "->", 2**n)
1 -> 2 6 -> 64 11 -> 2048 16 -> 65536 21 -> 2097152 26 -> 67108864 31 -> 2147483648 36 -> 68719476736 41 -> 2199023255552 46 -> 70368744177664 51 -> 2251799813685248 56 -> 72057594037927936 61 -> 2305843009213693952
# wie lange dauert sowas ca.?
# nehmen wir eine Mikrosekunde für eine Kombination an
2**61 * 1.e-6 / 3600 / 24 / 365 # hier sollten Jahre herauskommen
73117.802169384
Nun ist soweit klar, dass es mit der Zeit relativ schnell knapp wird. Wie löst man so ein Problem aber jetzt konkret mit einem genetischen Algorithmus? Wir brauchen folgendes.
- Ein Encoding der “genetischen Information” für jedes Individuum einer Generation/Population (also hier für jede Kombination von Dingen, die eingepackt werden)
- Einen Mechanismus, der uns die erste Generation von Individuen erzeugt
- Einen Mechanismus (oder mehrere), der den genetischen Code von Individuen von einer zur nächsten Generation verändern kann
- Eine Vorschrift, mit der die Fitness jedes Individuums berechnet werden kann
- Eine Vorschrift dafür, welche und wie viele der fittesten Individuen einer Generation zu Eltern für die nächste Generation werden
Wir brauchen natürlich auch die Basis für dieses Problem, nämlich die Liste mit den Dingen, ihren Gewichten und Preisen, sowie das Gewichtslimit für den Rucksack. Aber dann kann es schon losgehen. Also legen wir los.
# Setze das Limit für den Rucksack
weight_limit = 15
# Setze N
num_items = 20
# Erzeugen der Liste der Dinge mit Gewichten und Preisen
item_weights = np.round((3.5 - 0.2) * np.random.random(size=num_items) + 0.2, 2)
item_prices = np.round((30. - 0.1) * np.random.random(size=num_items) + 0.1, 2)
# was ist zum Beispiel mit Ding nummer 5?
(item_weights[4], item_prices[4])
(0.96, 27.42)
# Das Encoding für eine Variante des Einpackens
# ist ein Vektor mit _N_ Zahlen, die entweder 1 oder 0 sein können
test_encoding = np.random.choice([0, 1], size=num_items, replace=True)
test_encoding
array([0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 0, 1, 0, 1, 0, 0, 1])
# Damit kann man leicht das Gewicht für eine Variante ausrechnen
the_weight = np.sum(item_weights * test_encoding)
the_weight
18.110000000000003
# Ebenso funktioniert der Gesamtpreis
the_price = np.sum(item_prices * test_encoding)
the_price
151.51999999999998
# Testen wir nun einmal kurz die brute-force-Lösung
# D.h. wir gehen durch alle Möglichkeiten und suchen die beste raus
# Zunächst definieren wir aber die Fitness-Funktion:
# wir schreiben diese Funktion so, dass sie mehrere Encodings
# gleichzeitig abarbeiten kann
def fitness(encodings):
# berechne Gewicht und Preis
the_weights = np.sum(item_weights * encodings, axis=1)
the_prices = np.sum(item_prices * encodings, axis=1)
# testen, ob das Gewicht in den Rucksack passt
the_prices = np.where(the_weights <= weight_limit * np.ones(len(encodings)),
# ja, passt, die Fitness ist der Preis
the_prices,
# nein, passt nicht, die Fitness wird auf 0 gesetzt
np.zeros(len(encodings))
)
return the_prices, the_weights
# Nun zum Ausprobieren aller Möglichkeiten:
# setze Wert für besten Preis an
best_price = 0.
# setze Wert für bestes Encoding an
best_encoding = np.zeros(num_items)
# setze Wert für Gewicht zum besten Preis an
best_weight = 0.
# loop über alle natürlichen Zahlen bis 2**N
for counter in tqdm(np.arange(0, 2**num_items, 1)):
# generiere binäre Darstellung des Zählers (als String) und ergänze führende Nullen
counter_binary = np.binary_repr(counter).zfill(num_items)
# verwandle den String in ein numpy-Array mit 0en und 1en
current_encoding = np.array([int(a_letter) for a_letter in counter_binary])
# berechne den Preis (inklusive Gewichtscheck) über die Fitness-Funktion
the_price, the_weight = fitness([current_encoding])
# Abfrage, ob wir eine bessere Lösung gefunden haben als bisher
if the_price > best_price:
# ersetze Bestwerte für Preis, Encoding und Gewicht
best_price = the_price
best_encoding = current_encoding
best_weight = the_weight
print("The best price is", best_price)
print("at a weight of", best_weight)
print("The best encoding is\n", best_encoding)
100%|██████████| 1048576/1048576 [00:26<00:00, 39137.20it/s]
The best price is [242.98] at a weight of [14.95] The best encoding is [1 1 1 1 1 0 0 0 0 1 1 0 1 1 0 0 1 0 0 0]
# Nun zum genetischen Algorithmus
# definiere Anzahl der Eltern
num_parents = 20
# definiere Anzahl Kinder pro Elternpaar
num_c_p_p = 3
# definiere Anzahl der Individuen in einer Generation
# entspricht dem Quadrat der Anzahl der Eltern
# (wegen der Möglichkeiten von Crossovers)
generation_size = num_c_p_p * num_parents**2
# definiere maximale Anzahl der Generationen
max_generations = 10
# definiere Crossover von zwei Encodings, d.h. rekombiniere die beiden
# an einer zufällig gewählten Stelle
def crossover(encoding_1, encoding_2):
# wähle eine zufällige Stelle aus der Länge der Encodings
cut_position = np.random.choice(range(num_items))
# rekombiniere die beiden Arrays zu einem mit Schnitt an dieser Stelle
new_encoding = np.hstack((encoding_1[:cut_position], encoding_2[cut_position:]))
return new_encoding
# erzeuge Generation 0
next_generation = np.random.choice([0, 1], size=(generation_size, num_items), replace=True)
# Loop über die Generationen
for count_generations in range(max_generations):
# print(next_generation)
# bestimme die Fitness aller Individuen in dieser Generation
the_prices, the_weights = fitness(next_generation)
# print(the_prices)
# bestimme Reihenfolge der Indizes nach Fitness der Individuen
the_ranking = np.argsort(the_prices)[::-1]
# print(the_ranking)
# suche die nächsten Eltern aus den besten aus
the_parents = (next_generation[the_ranking])[:num_parents]
# baue die nachfolgende Generation aus den Eltern plus
# num_parents x num_parents "Kindern" zusammen
the_children = []
# erzeuge Kinder durch Crossover, d.h. Kombination der genetischen
# Codes der beiden Eltern an einer bestimmten Stelle
# Erster Loop über alle Eltern
for parent_1 in the_parents:
# Zweiter Loop über alle Eltern
for parent_2 in the_parents:
# Zusätzlicher Loop über die Anzahl der Kinder pro Elternpaar
for child_counter in range(num_c_p_p):
# hänge alle möglichen Crossovers in eine Liste zusammen
the_children.append(crossover(parent_1, parent_2))
# mache aus der Liste ein Array
next_generation = np.array(the_children)
# hier sind auch die Eltern wieder dabei, weil ein Crossover eines Encodings
# mit sich selbst das gleiche Encoding nochmals erzeugt
# print("parents", the_parents)
# print("children", the_children)
# print("new gen.", next_generation)
# Output für diese Generation
print("Gen.:", count_generations, "Beste Fitness:", np.round(np.max(the_prices), 2),
"mit Gewicht ", np.round(the_weights[the_ranking[0]], 2), "bestes Encoding:", the_parents[0])
Gen.: 0 Beste Fitness: 215.22 mit Gewicht 14.69 bestes Encoding: [1 1 1 1 1 0 0 0 1 1 0 0 0 1 1 0 1 0 0 0] Gen.: 1 Beste Fitness: 241.22 mit Gewicht 14.94 bestes Encoding: [1 0 1 1 1 0 0 0 1 1 1 1 1 1 1 0 1 0 0 0] Gen.: 2 Beste Fitness: 241.22 mit Gewicht 14.94 bestes Encoding: [1 0 1 1 1 0 0 0 1 1 1 1 1 1 1 0 1 0 0 0] Gen.: 3 Beste Fitness: 241.22 mit Gewicht 14.94 bestes Encoding: [1 0 1 1 1 0 0 0 1 1 1 1 1 1 1 0 1 0 0 0] Gen.: 4 Beste Fitness: 241.22 mit Gewicht 14.94 bestes Encoding: [1 0 1 1 1 0 0 0 1 1 1 1 1 1 1 0 1 0 0 0] Gen.: 5 Beste Fitness: 241.22 mit Gewicht 14.94 bestes Encoding: [1 0 1 1 1 0 0 0 1 1 1 1 1 1 1 0 1 0 0 0] Gen.: 6 Beste Fitness: 241.22 mit Gewicht 14.94 bestes Encoding: [1 0 1 1 1 0 0 0 1 1 1 1 1 1 1 0 1 0 0 0] Gen.: 7 Beste Fitness: 241.22 mit Gewicht 14.94 bestes Encoding: [1 0 1 1 1 0 0 0 1 1 1 1 1 1 1 0 1 0 0 0] Gen.: 8 Beste Fitness: 241.22 mit Gewicht 14.94 bestes Encoding: [1 0 1 1 1 0 0 0 1 1 1 1 1 1 1 0 1 0 0 0] Gen.: 9 Beste Fitness: 241.22 mit Gewicht 14.94 bestes Encoding: [1 0 1 1 1 0 0 0 1 1 1 1 1 1 1 0 1 0 0 0]
6.6 Übungsaufgabe: Pushen Sie das Rucksackproblem an die Grenzen¶
Nehmen Sie den Code von eben und experimentieren Sie damit. Sie könnten z.B. damit folgendes tun:
- Verändern Sie die Anzahl der Eltern (und damit auch die Generationsgröße) und prüfen Sie, ob die Lösung dadurch näher an die brute-force-Lösung herankommt
- Verändern Sie die Anzahl der Kinder pro Elternpaar und überprüfen Sie den Effekt
- Verändern Sie das Limit für den Rucksack
- Verändern Sie die Limits für die Preise und Gewichte
- Finden Sie heraus, wie weit Sie $N$ auf Ihrem Rechner ohne Probleme nach oben schrauben können
- Implementieren Sie ein Timing für Teile eines Runs und den gesamten Run
- Visualisieren Sie die Entwicklung des Timings mit $N$