Before introducing the main text, I will add some basic knowledge and examples of Go language to you.
Go Language Tutorial
Go is an open source programming language that makes it easy to construct simple, reliable and efficient software.
Go was developed in late 2007 by Robert Griesemer, Rob Pike, Ken Thompson, and later joined Ian Lance Taylor, Russ Cox and others, and finally opened source in November 2009, with a stable version of Go 1 released earlier in 2012. Now Go development is fully open and has an active community.
Go language features
Simple, fast and safe
Parallel, interesting, open source
Memory management, v array security, fast compilation
Go language usage
Go is designed as a system programming language for giant central servers with web servers, storage clusters or similar purposes.
For the field of high-performance distributed systems, Go is undoubtedly more efficient than most other languages. It provides massive parallel support, which is better for game server development.
The first Go program
Next, let's write the first Go program (the extension of the Go language source file is .go), the code is as follows:
Example
package main import "fmt" func main() { ("Hello, World!") }
Execute the above code to output
$ go run Hello, World!
OK, the text begins.
Out of a fun mentality, I decided to learn Go. I think the best way to learn a new language is to learn in depth and make as many mistakes as possible. Although this may be slow, it ensures that there will be no compilation errors in the later process.
Go is different from other languages I am used to. Go prefers to implement it alone, while other languages like Java prefer inheritance. In fact, there is no concept of inheritance in Go language because there is no such thing as an object at all. For example, in C language, it has structures but no classes. But in this way it can still have common ideas and design patterns like "constructors" (a way to produce structures in an orderly manner in this case).
Go language firmly supports composition and opposes inheritance, which has caused strong discussion on the Internet and has also made people rethink where the language should develop. So, from this perspective, the difference between Go and other languages may not be that big.
This article will focus on how to implement genetic algorithms in Go language. If you haven't participated in GoLang Tour, I suggest you take a quick look at the introduction to this language.
Without further ado, let's start with the code! The first example is very similar to what I have done before: find a quadratic minimum.
type GeneticAlgorithmSettings struct { PopulationSize int MutationRate int CrossoverRate int NumGenerations int KeepBestAcrossPopulation bool } type GeneticAlgorithmRunner interface { GenerateInitialPopulation(populationSize int) []interface{} PerformCrossover(individual1, individual2 interface{}, mutationRate int) interface{} PerformMutation(individual interface{}) interface{} Sort([]interface{}) }
I immediately defined a set of settings for use in the algorithm that was started later.
The GeneticAlgorithmRunner for the second part looks a bit strange. GeneticAlgorithmRunner is an interface that asks how to generate initial populations, perform corssovers and mutataions, and sorts the answers so that the best individuals in Population will be maintained so that the next generation will be better. I think this looks weird because "interfaces" are often used in object-oriented languages that usually require objects to implement certain features and methods. There is no difference here. This small piece of code is actually saying that it is requesting something to define the details of these methods. This is what I did:
type QuadraticGA struct {} func (l QuadraticGA) GenerateInitialPopulation(populationSize int) []interface{}{ initialPopulation := make([]interface{}, 0, populationSize) for i:= 0; i < populationSize; i++ { initialPopulation = append(initialPopulation, makeNewEntry()) } return initialPopulation } func (l QuadraticGA) PerformCrossover(result1, result2 interface{}, _ int) interface{}{ return (result1.(float64) + result2.(float64)) / 2 } func (l QuadraticGA) PerformMutation(_ interface{}, _ int) interface{}{ return makeNewEntry() } func (l QuadraticGA) Sort(population []interface{}){ (population, func(i, j int) bool { return calculate(population[i].(float64)) > calculate(population[j].(float64)) }) }
What's even more strange is that I've never mentioned the interfaces of these methods. Remember, because there is no object, and there is no inheritance. The QuadraticGA structure is a blank object implicitly as a GeneticAlgorithmRunner. Each required method is bound to the structure in parentheses, like "@override" in Java. Now, the structure and settings need to be passed to the module running the algorithm.
settings := { PopulationSize: 5, MutationRate: 10, CrossoverRate: 100, NumGenerations: 20, KeepBestAcrossPopulation: true, } best, err := (QuadraticGA{}, settings) if err != nil { println(err) }else{ ("Best: x: %f y: %f\n", best, calculate(best.(float64))) }
It's simple, right? "QuadraticGA {}" simply creates a new instance of the structure, and the rest is done by the Run() method. This method returns search results and any errors that occur, as Go does not believe in try/catch – another war author takes a strict design stance.
Now let’s calculate the performance of each term to find the quadratic function to find a new X value:
func makeNewEntry() float64 { return highRange * rand.Float64() } func calculate(x float64) float64 { return (x, 2) - 6*x + 2 // minimum should be at x=3 }
Since the interface has been created for the secondary implementation, the GA itself needs to complete:
func Run(geneticAlgoRunner GeneticAlgorithmRunner, settings GeneticAlgorithmSettings) (interface{}, error){ population := () (population) bestSoFar := population[len(population) - 1] for i:= 0; i < ; i++ { newPopulation := make([]interface{}, 0, ) if { newPopulation = append(newPopulation, bestSoFar) } // perform crossovers with random selection probabilisticListOfPerformers := createStochasticProbableListOfIndividuals(population) newPopIndex := 0 if { newPopIndex = 1 } for ; newPopIndex < ; newPopIndex++ { indexSelection1 := () % len(probabilisticListOfPerformers) indexSelection2 := () % len(probabilisticListOfPerformers) // crossover newIndividual := ( probabilisticListOfPerformers[indexSelection1], probabilisticListOfPerformers[indexSelection2], ) // mutate if (101) < { newIndividual = (newIndividual) } newPopulation = append(newPopulation, newIndividual) } population = newPopulation // sort by performance (population) // keep the best so far bestSoFar = population[len(population) - 1] } return bestSoFar, nil } func createStochasticProbableListOfIndividuals(population []interface{}) []interface{} { totalCount, populationLength:= 0, len(population) for j:= 0; j < populationLength; j++ { totalCount += j } probableIndividuals := make([]interface{}, 0, totalCount) for index, individual := range population { for i:= 0; i < index; i++{ probableIndividuals = append(probableIndividuals, individual) } } return probableIndividuals }
Much like before, a new population was created, and members of the population would mate for generations, while their offspring might carry mutations. The better a person performs, the more likely he is to mate. Over time, the algorithm converges to the best answer, or at least a pretty good one.
So when it runs, what does it return?
Best: x: 3.072833 y: -6.994695
Not bad! Since the population size is only 5 or 20 generations, and the input range is limited to [0 100], this search is nailed to the vertex.
Now, you may be wondering why I defined all interface methods to return "interface {}". It's like Go and generics. There are no objects, so no object type returns, but data with no described size can still be passed on the stack. This is essentially what this return type means: it passes some known and similar types of objects. With this "generic", I can move the GA to its own package and move the same code to multiple different types of data.
We have two inputs of 3D quadratic equations, rather than a single input of a two-dimensional quadratic equation. The interface method only needs to be changed slightly:
type Quad3D struct { x, y float64 } func makeNewQuadEntry(newX, newY float64) Quad3D { return Quad3D{ x: newX, y: newY, } } func calculate3D(entry Quad3D) float64 { return (, 2)- 6 * + (, 2)- 6 * + 2 } type Quadratic3dGA struct { } func (l Quadratic3dGA) GenerateInitialPopulation(populationSize int)[]interface{}{ initialPopulation := make([]interface{}, 0, populationSize) for i:= 0; i < populationSize; i++ { initialPopulation = append(initialPopulation, makeNewQuadEntry(makeNewEntry(), makeNewEntry())) } return initialPopulation } func (l Quadratic3dGA) PerformCrossover(result1, result2 interface{}, mutationRate int) interface{}{ r1Entry, r2Entry := result1.(Quad3D), result2.(Quad3D) return makeNewQuadEntry(( + ) / 2, ( + ) / 2,) } func (l Quadratic3dGA) PerformMutation(_ interface{}) interface{}{ return makeNewQuadEntry(makeNewEntry(), makeNewEntry()) } func (l Quadratic3dGA) Sort(population []interface{}){ (population, func(i, j int) bool { return calculate3D(population[i].(Quad3D)) > calculate3D(population[j].(Quad3D)) }) } func quadratic3dMain(){ settings := { PopulationSize: 25, MutationRate: 10, CrossoverRate: 100, NumGenerations: 20, KeepBestAcrossPopulation: true, } best, err := (Quadratic3dGA{}, settings) entry := best.(Quad3D) if err != nil { println(err) }else{ ("Best: x: %f y: %f z: %f\n", , , calculate3D(entry)) } }
Instead of float64s everywhere, you can pass Quad3D entries anywhere; each has an X and a Y value. For each entry created, it is created using the constructor makeNewQuadEntry. None of the code in the Run() method has changed.
When it runs, we get this output:
Best: x: 3.891671 y: 4.554884 z: -12.787259
Very close!
Oh, I forgot to say it's going fast! When doing this in Java, there is a noticeable wait time even with the same settings. Solving quadratic equations in a relatively small range is not very complicated, but it is worth noting for one person.
Go is compiled locally, such as C. When binary is executed, it seems to spit out an answer right away. Here is an easy way to measure the execution time of each run:
func main() { beforeQuadTime := () quadraticMain() afterQuadTime := (beforeQuadTime) ("%d\n", afterQuadTime) before3dQuadTime := () quadratic3dMain() after3dQuatTime := (before3dQuadTime) ("%d\n", after3dQuatTime) }
Side note: Can I say I'm glad we're a community of developers to get them out of past mistakes and build comprehensive time modules and packages into a language? Java 8+ owns them, Python owns them, and owns them. This made me happy.
Now output:
Best: x: 3.072833 y: -6.994695 136,876 Best: x: 3.891671 y: 4.554884 z: -12.787259 4,142,778
That "nearly momentary" feeling is what I want to convey, and now we have very difficult numbers. 136,876 looks big, but time is reported in nanoseconds.
Reiterate: nanoseconds. Not a few milliseconds, we are all used to the Internet age or other common languages like Python and Java; nanoseconds. 1/1,000,000 milliseconds.
This means that we found an answer to a quadratic equation that uses a genetic algorithm to search for answers in less than a millisecond. This sentence, "Damn Moment" seems to fit well, doesn't it? This includes printing to the terminal.
So, what about computing something more intensive? I used it on Fanduel before I showed a way to find good fantasy football lineups. This includes reading data from spreadsheets, crafting and filtering lineups, and performing more complex crossovers and mutations. Forced searching for the best solution may take more than 75,000 years (at least with Python I was using at the time).
I don't need to check all the details anymore, you can go to the code yourself, but I'll show the output here:
Best: 121.409960:, $58100 QB: Aaron Rodgers - 23.777778 RB: Latavius Murray - 15.228571 RB: DeMarco Murray - 19.980000 WR: Kelvin Benjamin - 11.800000 WR: Stefon Diggs - 14.312500 WR: Alshon Jeffery - 9.888889 TE: Connor Hamlett - 8.200000 D: Philadelphia Eagles - 10.777778 K: Phil Dawson - 7.444444 16,010,182
Oh, yes! It seems like this is a great lineup now! It only takes 16 milliseconds to find.
Now, this genetic algorithm can be improved. Like C, when an object is passed to a method, the object is copied (reading the data) on the stack. As the object size grows, it is best not to copy them repeatedly, but to create them in the heap and pass pointers around. Currently, I will take it as a future work.
Go is also written with native support for coroutines and channels. It uses multiple cores to solve a problem, which is much simpler than in the past. This is a huge advantage compared to other languages in the single-core era. I want to enhance this algorithm to use these tools, but this must also be left to future work.
I enjoyed the learning process. It is difficult for me to consider engineering solutions using combination rather than inheritance, because I have been used to it for over 8 years and it is also the way I learn how to program. But each language and method has its own advantages and disadvantages; each language is a different tool in my tools. For anyone who is worried about trying, don't. There is a hump (more like a speed bump), but you will soon overcome it and embark on the road to success.
There are some things I like, I like other languages, mainly a set of basic function methods to manipulate data. I need a lambda function and method to map, reduce and filter arrays or parts of data. The reason designers object to functional implementation is that code should always be simple, easy to read and write, and that this is achievable with for loops. I think mapping, filtering and reducing is usually easier to read and write, but it is a debate that is already raging in a war.
Although I'm disagreeing with some developers' opinions and different ways I have to think about solving the problem, Go is really a great language. I encourage everyone to try it after learning one or two languages. It quickly became one of the most popular languages, and there are many reasons why. I look forward to using it more in the future.
Summarize
The above is the example code for implementing genetic algorithms in Go language introduced to you. I hope it will be helpful to you. If you have any questions, please leave me a message and the editor will reply to you in time. Thank you very much for your support for my website!