# Übungsblatt 3 - Evolutionary Algorithms (Paul Helstab & Yannik Bretschneider)
## Loading Data
```{r setwd}
# Get the current working directory
current_directory <- getwd()
# Define the folder name to check
folder_name <- "w3"
# Construct the full path to the folder
folder_path <- file.path(current_directory, folder_name)
# Check if the folder exists (means most likely in the repo root folder)
if (dir.exists(folder_path)) {
setwd(folder_path)
cat("Changed working directory to:", getwd(), "\n")
} else {
cat("Folder 'w3' does not exist in the current working directory.
Staying where we are\n")
}
```
```{r read_data}
file_path <- "./data1.txt"
print(paste0("read data from '", file_path, "'"))
data <- read.table(
file_path,
header = FALSE,
sep = " ",
# colClasses = c("numeric", "numeric", "integer", "integer")
)
colnames(data) <- c("x1", "x2", "x_bias", "label")
evaluation_set_idx <- c(90:109)
```
```r
# possible, but unnecessary here:
# grab first 10 as evaluation set
evaluation_set <- data[evaluation_set_idx, ]
# grab the rest as training set
training_set <- data[-evaluation_set_idx, ]
```
## Aufgabe 1: Perzeptron
```{r peceptron-impl}
# randomized order
random_order <- sample(nrow(data))
vecs <- data[random_order, 1:3]
classes <- data[random_order, 4]
weights <- c(0, 0, 0)
# matrix for logging weights
wlog <- matrix(weights, ncol = 3)
infer <- function(x, wghts) {
return(sign(x %*% wghts))
}
perceptron_infer <- function(x) {
return(infer(x, weights))
}
while (TRUE) {
mispredictions <- 0
for (i in seq_len(nrow(vecs))) {
vec <- as.numeric(vecs[i, ])
class <- classes[i]
# print(vec %*% weights)
error <- as.numeric(class - perceptron_infer(vec))
if (error != 0) {
mispredictions <- mispredictions + 1
print("adjusting")
weights <- weights + error * vec
wlog <- rbind(wlog, weights)
}
}
if (mispredictions == 0) {
print("No more mispredictions")
break
} else {
print(paste0("Misclassifications: ", mispredictions))
}
}
print("NOTE: for more interesting plots, run this chunk multiple times until multiple \"adjusting\" can be seen in the output")
```
```{r apply-to-data}
perceptron_reinfer <- t(sapply(seq_len(nrow(data)), function(i) {
return(c(as.numeric(data[i, 1:3]), perceptron_infer((as.numeric(data[i, 1:3])))))
}))
# perceptron_reinfer
```
### Plots
The plots show the datapoints on a 2D plot, with the colors showing the classes (`green := 1` and `red := -1`)
The separation plane defined by the normal of the weight vector is shown in increasing opacities, until the final
weights are reached.
```{r plot-weights}
perceptron_plot <- function() {
plot(data$x1, data$x2,
col = ifelse(data$label == 1, "green", "red"),
pch = 16, xlab = "x1", ylab = "x2", xlim = c(-5, 5), ylim = c(-5, 5)
)
legend("topright",
legend = c("Class 1", "Class -1"),
col = c("green", "red"), pch = 16
)
# Plot the separation planes
num_steps <- nrow(wlog)
alpha_step <- 1 / num_steps
for (i in 1:num_steps) {
a <- wlog[i, 1]
b <- wlog[i, 2]
c <- wlog[i, 3]
# Calculate the line coordinates
x <- seq(-5, 5, length.out = 100)
y <- -(a * x + c) / b
# Determine the line color based on the iteration
alpha <- i * alpha_step
line_color <- rgb(0, 0, 1, alpha)
# Plot the line
lines(x, y, col = line_color)
}
# Plot the final separation plane
a <- weights[1]
b <- weights[2]
c <- weights[3]
x <- seq(-5, 5, length.out = 100)
y <- -(a * x + c) / b
lines(x, y, col = "blue", lwd = 2)
}
perceptron_plot()
```
```{r plot-weights-pdf}
pdf("perceptron-plot.pdf", width = 8, height = 6)
perceptron_plot()
dev.off()
```
## Aufgabe 2: Perzeptron Evolutionary Algorithm (EA)
```{r peceptron-ea-impl}
# randomized order
ea_random_order <- sample(nrow(data))
ea_vecs <- data[ea_random_order, 1:3]
ea_classes <- data[ea_random_order, 4]
NUM_INDIVIDUALS <- 3
MUTATE_ALL_GENES <- FALSE
MUTATE_STEP <- function() {
return(0.5)
}
# MUTATE_STEP <- function() {
# return(runif(1, min = -1, max = 1)[1])
# }
weights <- matrix(0, ncol = 3, nrow = NUM_INDIVIDUALS)
# matrix for logging weights
ea_wlog <- matrix(0, ncol = 3)
fittest <- c(0, 0, 0)
mutate <- function(genes, step_size_fn, mutate_all) {
if (mutate_all == TRUE) {
for (i in seq_along(genes)) {
step <- step_size_fn() * sample(c(1, -1), size = 1)[1] # mutation in step_size, -step_size or not at all
genes[i] <- genes[i] + step
}
} else {
idx_to_mutate <- sample(seq_along(genes), size = 1)[1]
genes[idx_to_mutate] <- genes[idx_to_mutate] + step_size_fn() * sample(c(1, -1), size = 1)[1] # mutation in step_size or not AT all
}
return(genes)
}
infer <- function(x, wghts) {
return(sign(x %*% wghts))
}
fitness <- function(wghts) {
mispredictions <- 0
for (i in seq_len(nrow(ea_vecs))) {
vec <- as.numeric(ea_vecs[i, ])
class <- ea_classes[i]
# print(vec %*% weights)
error <- as.numeric(class - infer(vec, wghts))
if (error != 0) {
mispredictions <- mispredictions + 1
}
}
return(mispredictions)
}
while (TRUE) {
i_fittest <- weights[1, ]
i_fit <- fitness(weights[1, ])
for (individual in 2:NUM_INDIVIDUALS) {
this_fitness <- fitness(weights[individual, ])
if (this_fitness < i_fit) {
i_fittest <- weights[individual, ]
i_fit <- this_fitness
}
}
# add fittest to weights
ea_wlog <- rbind(ea_wlog, i_fittest)
if (i_fit == 0) {
print("No more mispredictions")
fittest <- i_fittest
break
} else {
print(paste0("Misclassifications: ", i_fit))
for (individual in 1:NUM_INDIVIDUALS) {
weights[individual, ] <- mutate(i_fittest, step_size_fn = MUTATE_STEP, mutate_all = MUTATE_ALL_GENES)
}
}
}
```
```{r ea-apply-to-data}
ea_reinfer <- t(sapply(seq_len(nrow(data)), function(i) {
return(c(as.numeric(data[i, 1:3]), infer((as.numeric(data[i, 1:3])), fittest)))
}))
# ea_reinfer
```
### Plots
The plots show the datapoints on a 2D plot, with the colors showing the classes (`green := 1` and `red := -1`)
The separation plane defined by the normal of the weight vector is shown in increasing opacities, until the final
weights are reached.
```{r ea-plot-weights}
ea_plot <- function() {
plot(data$x1, data$x2,
col = ifelse(data$label == 1, "green", "red"),
pch = 16, xlab = "x1", ylab = "x2", xlim = c(-5, 5), ylim = c(-5, 5)
)
legend("topright",
legend = c("Class 1", "Class -1"),
col = c("green", "red"), pch = 16
)
# Plot the separation planes
num_steps <- nrow(ea_wlog)
alpha_step <- 1 / num_steps
for (i in 1:num_steps) {
a <- ea_wlog[i, 1]
b <- ea_wlog[i, 2]
c <- ea_wlog[i, 3]
# Calculate the line coordinates
x <- seq(-5, 5, length.out = 100)
y <- -(a * x + c) / b
# Determine the line color based on the iteration
alpha <- i * alpha_step
line_color <- rgb(0, 0, 1, alpha)
# Plot the line
lines(x, y, col = line_color)
}
# Plot the final separation plane
a <- fittest[1]
b <- fittest[2]
c <- fittest[3]
x <- seq(-5, 5, length.out = 100)
y <- -(a * x + c) / b
lines(x, y, col = "blue", lwd = 2)
}
ea_plot()
```
```{r plot-weights-pdf}
pdf("ea-plot_static-delta_one.pdf", width = 8, height = 6)
ea_plot()
dev.off()
```
### Evaluation
Gaussian (Mutate All) with a high number of individuals generally acheives a correct result first-try. In our very constrained individuum pool tests with 3 individuals, this also seems to be the best choice for parameters for the EA. Its efficacy seems to increase even more with a larger gene pool.
## Aufgabe 3: Exploration vs Exploitation
```{r setup}
# generate a random binary vector of length 10
generate_individual <- function() {
sample(c(0, 1), 10, replace = TRUE)
}
# 1-point crossover
one_point_crossover <- function(parent1, parent2) {
point <- sample(1:9, 1)
child1 <- c(parent1[1:point], parent2[(point + 1):10])
child2 <- c(parent2[1:point], parent1[(point + 1):10])
return(list(child1, child2))
}
# 2-point crossover
two_point_crossover <- function(parent1, parent2) {
points <- sort(sample(1:9, 2))
child1 <- c(parent1[1:points[1]], parent2[(points[1] + 1):points[2]], parent1[(points[2] + 1):10])
child2 <- c(parent2[1:points[1]], parent1[(points[1] + 1):points[2]], parent2[(points[2] + 1):10])
return(list(child1, child2))
}
# uniform crossover
uniform_crossover <- function(parent1, parent2) {
mask <- sample(c(0, 1), 10, replace = TRUE)
child1 <- ifelse(mask == 1, parent1, parent2)
child2 <- ifelse(mask == 1, parent2, parent1)
return(list(child1, child2))
}
# Function to convert binary vector to decimal
binary_to_decimal <- function(binary_vector) {
sum(binary_vector * 2^(rev(seq_along(binary_vector) - 1)))
}
```
```{r initialize_parents}
# init parents
parent1 <- generate_individual()
parent2 <- generate_individual()
print(parent1)
print(parent2)
# Set num gens
num_generations <- 10000
# for tracking unique solutions
unique_solutions <- list()
unique_solutions[[binary_to_decimal(parent1)]] <- TRUE
unique_solutions[[binary_to_decimal(parent2)]] <- TRUE
```
```{r run_genetic_algorithm}
for (generation in 1:num_generations) {
# Choose crossover method
crossover_method <- sample(c("one_point", "two_point", "uniform"), 1)
if (crossover_method == "one_point") {
children <- one_point_crossover(parent1, parent2)
} else if (crossover_method == "two_point") {
children <- two_point_crossover(parent1, parent2)
} else {
children <- uniform_crossover(parent1, parent2)
}
# Replace
parent1 <- children[[1]]
parent2 <- children[[2]]
# Track sols
unique_solutions[[binary_to_decimal(parent1)]] <- TRUE
unique_solutions[[binary_to_decimal(parent2)]] <- TRUE
}
# Calculate number of unique solutions observed
num_unique_solutions <- length(unique_solutions)
# Calculate number of possible solutions covered
num_possible_solutions <- 2^10
num_solutions_covered <- length(unique_solutions)
# Print results
cat("Number of unique solutions observed:", num_unique_solutions, "\n")
cat("Number of possible solutions covered:", num_solutions_covered, "\n")
cat("Percentage of 10-bit numbers covered:", round(num_solutions_covered / num_possible_solutions * 100, 2), "%\n")
```