Created
August 9, 2017 12:17
-
-
Save dev001hajipro/bacad84846d08b50baa3d1cab74ac753 to your computer and use it in GitHub Desktop.
KotlinとJavaFXで遺伝的アルゴリズムで、特定の文字列に進化させる
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import javafx.animation.AnimationTimer | |
import javafx.application.Application | |
import javafx.scene.Group | |
import javafx.scene.Scene | |
import javafx.scene.canvas.Canvas | |
import javafx.scene.canvas.GraphicsContext | |
import javafx.scene.paint.Color | |
import javafx.scene.text.Font | |
import javafx.stage.Stage | |
fun random(min: Double = 0.0, max: Double = 1.1) = (Math.random() * (max - min + 1)) + min | |
fun newChar(): Char { | |
var c = Math.floor(random(64.0, 122.0)).toInt() | |
c = if (c == 64) 32 else c // @ mark to space. | |
return c.toChar() | |
} | |
fun main(args: Array<String>) { | |
Application.launch(App::class.java, *args) | |
} | |
/** | |
* JavaFXのCanvasを用意して、AnimationTimerでゲームループを実行します。 | |
* ProcessingやP5.jsと同じように、アプリ起動時に、start()関数が呼ばれ、 | |
* ゲームループ内で、draw(ctx:GraphicsContext)が呼ばれます。 | |
* start関数とdraw関数はユーザーが実装する必要があります。 | |
*/ | |
class App : Application() { | |
lateinit var canvas: Canvas | |
lateinit var ctx: GraphicsContext | |
override fun start(primaryStage: Stage) { | |
canvas = Canvas(1280.0, 720.0) | |
ctx = canvas.graphicsContext2D | |
val root = Group() | |
root.children.add(canvas) | |
object : AnimationTimer() { | |
override fun handle(now: Long) { | |
draw(ctx) | |
} | |
}.start() | |
with(primaryStage) { | |
title = "遺伝的アルゴリズムでランダム文字列を'$TARGET'に進化させる" | |
scene = Scene(root, canvas.width, canvas.height) | |
show() | |
} | |
} | |
} | |
////////////////////////////////////////////// | |
fun <T> randIndex(src: List<T>): Int = Math.floor(Math.random() * src.size).toInt() | |
class DNA(targetLength: Int = 10, var fitness: Double = 0.0) { | |
var genes: List<Char> = List(targetLength, { newChar() }) | |
fun calcFitness(target: String) { | |
fitness = calcScore() / target.length | |
} | |
fun calcScore(): Double = genes.foldIndexed(0.0) { i, acc, c -> if (TARGET[i] == c) acc + 1.0 else acc } | |
fun crossover(partnerDNA: DNA): DNA { | |
val childDNA = DNA(genes.size) | |
val midpoint = Math.floor(Math.random() * genes.size) | |
childDNA.genes = genes.mapIndexed { i, c -> if (i > midpoint) c else partnerDNA.genes[i] } | |
return childDNA | |
} | |
fun mutate(rate: Double): DNA { | |
genes = genes.map { if (Math.random() < rate) newChar() else it } | |
return this | |
} | |
} | |
data class Point(val x: Int, val y: Int) | |
fun xy(x: Int): Point = Point(x % 4, x / 4) | |
const val POPULATION_COUNT = 150 | |
const val MUTATION_RATE = 0.01 | |
const val TARGET = "to be or not to be" | |
var foundFlag = false | |
// ステップ1. 集団の作成 | |
var population: List<DNA> = List(POPULATION_COUNT) { DNA(TARGET.length) } | |
var generationCount = 0 | |
fun draw(ctx: GraphicsContext) { | |
ctx.fill = Color(1.0, 1.0, 1.0, 1.0) | |
ctx.fillRect(0.0, 0.0, ctx.canvas.width, ctx.canvas.height) | |
if (!foundFlag) { | |
// ステップ2.選択 | |
// 集団の各要素の適応度を計算 | |
population.forEach { it.calcFitness(TARGET) } | |
// 交配プールを作成 | |
val matingPool = population.map { dna -> | |
val n = Math.floor(dna.fitness * 100.0).toInt() | |
List(n) { dna } | |
}.flatten() | |
// ステップ3.生殖(reproduction) | |
population = population.map { | |
val dad = matingPool[randIndex(matingPool)] | |
val mam = matingPool[randIndex(matingPool)] | |
val child = dad.crossover(mam) | |
child.mutate(MUTATION_RATE) | |
} | |
population.forEach { it.calcFitness(TARGET) } | |
foundFlag = (population.indexOfFirst { dna -> dna.genes.joinToString("") == TARGET } != -1) | |
++generationCount | |
} | |
// 表示 ///// | |
ctx.font = Font("Monospaced", 14.0) | |
population.map { it.genes.joinToString("") } | |
.forEachIndexed { i, s -> | |
val (x, y) = xy(i) | |
ctx.fill = if (s == TARGET) Color.RED else Color.BLACK | |
ctx.fillText(s, 10.0 + (x * 150), 30.0 + (y * 15)) | |
} | |
ctx.fill = Color.BLACK | |
val maxDNA = population.maxBy { dna -> dna.fitness } ?: population[0] | |
ctx.font = Font("Monospaced", 32.0) | |
ctx.fillText("Best phrase : ${maxDNA.genes.joinToString("")}", 1280.0 / 2, 50.0) | |
ctx.font = Font("Monospaced", 20.0) | |
ctx.fillText("total generations: $generationCount", 1280.0 / 2, 50.0 + 20.0 * 1) | |
// average | |
//SUM_DNA_LENGTH | |
val averageFitness = population.map { it.calcScore() }.average() | |
ctx.fillText("average fitness : ${Math.floor(averageFitness * 100.0) / 100.0}", 1280.0 / 2, 50.0 + 20.0 * 2) | |
ctx.fillText("mutation rate : $MUTATION_RATE", 1280.0 / 2, 50.0 + 20.0 * 3) | |
} | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment