New blogpost, ES meal planning
This commit is contained in:
parent
cdc3485d71
commit
a7324b8445
133
content/post/evolutionary-strategy-meal-planning.md
Normal file
133
content/post/evolutionary-strategy-meal-planning.md
Normal file
@ -0,0 +1,133 @@
|
||||
---
|
||||
title: "Evolutionary Strategy Meal Planning"
|
||||
date: 2019-09-24T15:23:59-04:00
|
||||
draft: false
|
||||
---
|
||||
|
||||
While sitting in my house just day dreaming, I was looking for ideas for my next project. Thoughts flew across my mind relating things that
|
||||
I was currently doing, challenges I was currently facing, and technology. Thinking back to my days at GMU, I remembered how much I really liked my
|
||||
Artificial Intelligence class, specifically evolutionary algorithms and genetic programming. I also thought of how hard it was to track macronutrients
|
||||
(something I'm currently doing) whenever I went out to eat at my favorite restaurants. Thinking about different ways to automatically meal plan, I had an idea.
|
||||
What if, given a database of nutrition information from a specific restaurant, I could "evolve" the best meal for myself based on evolutionary strategy
|
||||
algorithms I had learned about in class? With this question in mind, I set out to see whether or not my question was feasible.
|
||||
|
||||
## Candidate Solutions, and Evaluating Fitness
|
||||
|
||||
What would my candidates even look like that I would feed into the algorithm. Can I model meals in a way that I can "breed" or "mutate" them in order to
|
||||
achieve better fitness scores over time? What would my fitness evaluator function look like?
|
||||
|
||||
I started on the question of candidates. I downloaded the nutritional spreadsheet from my favorite local sports bar, Beef O'Brady's, and got to work. My
|
||||
favorite programming language, Julia, has a complex type system. It allows for very complex data to be modeled and represented in a native type.
|
||||
In the end though, my choice of data structure was influenced by the structure of my data. The nutritional spreadsheet that I downloaded from the sports bar had
|
||||
each menu item represented in a row. To read the data, I used the Julia library ExcelReaders.jl, which was able to stuff my data into a Julia DataFrame. I
|
||||
decided that candidate solutions could just be n-row DataFrame objects, a randomly selected subset of size n taken from the entire dataset.
|
||||
|
||||
```julia
|
||||
"""
|
||||
Helper function to generate a random candidate from the dataset.
|
||||
|
||||
n is the size of the candidate.
|
||||
"""
|
||||
function randomCandidate(n::Integer)
|
||||
# Select n random rows from the dataset.
|
||||
rows = [randRow() for i = 1:n]
|
||||
df[rows, :]
|
||||
end
|
||||
|
||||
|
||||
"""
|
||||
generateInitialPopulation(lambda::Integer, candidateSize::Integer)
|
||||
|
||||
From our dataset, generate an array of initial candidates to begin the search.
|
||||
"""
|
||||
function generateInitialPopulation(lambda::Integer, candidateSize::Integer)
|
||||
[randomCandidate(candidateSize) for i = 1:lambda]
|
||||
end
|
||||
|
||||
```
|
||||
|
||||
With the design of the candidate solutions in hand, I thought that the easiest method for evaluating the fitness of these solutions would be a simple subtraction.
|
||||
I built my fitness evaluation function to take the absolute value of the difference of my target calories with the total calories of a candidate meal.
|
||||
|
||||
```julia
|
||||
"""
|
||||
fitness(candidate::DataFrames.DataFrame)
|
||||
|
||||
Calculate the fitness of the candidate, which is the
|
||||
absolute value of the difference of TARGETCALORIES and the sum
|
||||
of all calories in the meal.
|
||||
"""
|
||||
function fitness(candidate::DataFrames.DataFrame)
|
||||
abs(TARGETCALORIES - sum(+, candidate[:Calories]))
|
||||
end
|
||||
```
|
||||
|
||||
With the candidate solutions designed, and the fitness evaluation function now complete, I had to decide on an actual algorithm to grow generations
|
||||
of meals with better and better fitness scores. To find the right algorithm I turned to an old textbook of mine, The Essentials of Metaheuristics. Within
|
||||
this book, Algorithm 18, the (mu, lambda) strategy, seemed perfect [1].
|
||||
|
||||
## The (mu/lambda) Evolution Strategy.
|
||||
|
||||
Boiled down, this algorithm consists of a few, discrete steps. The first step is to generate an initial population. I do this using the functions I pasted
|
||||
above to build a population of size lambda. The next step, is to begin evolving new solutions to our existing problem. The evolution process can be broken
|
||||
down into a few steps as well. The first step is to evaluate the fitness of all members of the population, saving the most fit candidate we saw in the
|
||||
evaluation process. The next step is to save the `mu` candidates who had the highest fitness scores (this is called truncation selection). The final step is
|
||||
to iterate over each of our `mu` parents we selected, and to `lambda/mu` times generate new children by mutating copies of the parent. We only exit the
|
||||
evolution process if the best candidate from the generation is the ideal solution, or we have ran out of time.
|
||||
|
||||
## My Mutation Algorithm
|
||||
|
||||
The mutation algorithm that I designed is very simple, it merely walks each of the items within a meal (rows in a small dataframe), and flips a coin on each one.
|
||||
If the result is heads, it will replace that food item with a randomly selected row from the dataset. If the result is tails, it will move on to the next food
|
||||
item and flip another coin. It repeats this process until it has iterated through all rows in a candidate solution.
|
||||
|
||||
```julia
|
||||
"""
|
||||
Our mutator function
|
||||
|
||||
steps through the parent, and randomly selects the
|
||||
allelles to delete. Will replace the alleles with new
|
||||
alleles (meals).
|
||||
"""
|
||||
function mutate(parent)
|
||||
|
||||
# Copy the parent so we can do some work.
|
||||
child = deepcopy(parent)
|
||||
toDelete = []
|
||||
|
||||
for i in 1:size(parent, 1)
|
||||
if rand(Float64) > 0.5 # NOTE: make this tunable
|
||||
push!(toDelete, i)
|
||||
end
|
||||
# If we get tails, delete the row and push a new one to it.
|
||||
end
|
||||
# Delete all rows we don't want at once.
|
||||
DataFrames.deleterows!(child, toDelete)
|
||||
|
||||
# Add new random rows from the ones we deleted
|
||||
for i in 1:length(toDelete)
|
||||
push!(child, df[randRow(), :])
|
||||
end
|
||||
|
||||
child
|
||||
end
|
||||
```
|
||||
|
||||
## Results
|
||||
|
||||
Using Algorithm 18 [1], and all the above functions, I implemented the (mu, lambda) evolutionary meal planning strategy. My candidate solutions were subset DataFrames
|
||||
drawn from the DataFrame of the entire dataset. My mutator function was based on a simple "coin flip" probability, replacing random meal items with other random
|
||||
meal items in hopes of mutating into a higher fitness meal. The results from a few runs are shown below.
|
||||
|
||||
![Image](/img/post/run1.png)
|
||||
|
||||
![Image](/img/post/run2.png)
|
||||
|
||||
I definitely think that even from my small experiment, the results do show that evolutionary strategies are adequate search algorithms in the search space of meal
|
||||
planning. If you can model meals in such a way that they are made up of food items which can be modified or replaced (like alleles), then applying the algorithm
|
||||
is straightforward after that.
|
||||
|
||||
## References
|
||||
|
||||
[1]: [The Essentials of Metaheuristics, Sean Luke, Pg. 33](https://cs.gmu.edu/~sean/book/metaheuristics/Essentials.pdf)
|
||||
|
BIN
static/img/post/run1.png
Normal file
BIN
static/img/post/run1.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
BIN
static/img/post/run2.png
Normal file
BIN
static/img/post/run2.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
Loading…
Reference in New Issue
Block a user