如何在AForge / Accord.Net中训练一个简单的遗传算法?

时间:2018-01-29 12:25:18

标签: c# .net genetic-algorithm aforge accord.net

我有10个数字W,而且每个数字都在-1f1f的范围内。我有一个函数:f(W),它可以重新计算浮点值。我需要最小化f W的变化值。我确实需要一个超级的预设,我希望学会成为0.01的最小步骤。 f需要时间,我不需要从多个线程执行它。

如何为这样的系统实现一个简单的GA,其基数W为浮点数组,GA的最小步长为0.01,每个W值的限制为[-1, 1]范围,以及一个函数f(W),以最小化在AForge / Accord.Net中返回浮点数?

1 个答案:

答案 0 :(得分:1)

遗传算法伪代码:

// While the population is not optimized
while (population.isNotOptimized()) {
    // Update each member of the population (i.e. do physics, calculations etc.)
    population.updateMembers()
    // Calculate the new fitnesses of each member of the population
    population.calculateMemberFitnesses()

    // Create a new, empty population
    newPopulation = empty

    // While new population size is not the same as current pop size
    while (newPopulation.size < population.size) {
        // Select 2 members from current pop using roullette selection
        mum = roullette(population)
        dad = roullette(population)

        // Crossover to create child
        val child = crossover(mum, dad)

        // Mutate child
        mutate(child)

        // Add child to new pop
        newPopulation.add(child)
    }
}

根据您的问题,您希望优化神经网络权重。因此,在这种情况下,您的人口将是一组网络权重。以下是我对简单多层网络和相关遗传算法的实现:

package Learning

import Simulation.Configuration
import Utilities.NonDeterminism

private const val LAYER_COUNT = 3
private const val OUTPUT_COUNT = 3

// A simple ANN
// No back prop as we'll train it with a GA
class Network(private val configuration: Configuration) {

    private var inputCount = 0
    private var hiddenCount = 0

    // The networks layers
    private val layers = mutableListOf<Layer>()

    init {
        inputCount = configuration.asteroidsToConsider * 2 + 4
        hiddenCount = (inputCount + OUTPUT_COUNT) / 2
        layers.clear()
        for (layer in 0 until LAYER_COUNT) {
            when (layer) {
                0 -> { layers.add(Layer(configuration, inputCount, hiddenCount)) }
                LAYER_COUNT - 1 -> { layers.add(Layer(configuration, if(LAYER_COUNT == 2) { inputCount } else { hiddenCount }, OUTPUT_COUNT)) }
                else -> { layers.add(Layer(configuration, hiddenCount, hiddenCount)) }
            }
        }
    }

    fun setWeights(weights: Array<Double>) {
        layers.clear()
        for (layer in 0 until LAYER_COUNT) {
            when (layer) {
                0 -> {
                    layers.add(Layer(configuration, inputCount, hiddenCount, weights.sliceArray(IntRange(0, inputCount * hiddenCount))))
                }
                LAYER_COUNT - 1 -> {
                    val startIndex = layers.sumBy { it.weights.size - 1 }
                    val endIndex = when (LAYER_COUNT) {
                        2 -> {
                            startIndex + inputCount * OUTPUT_COUNT
                        } else -> {
                            startIndex + hiddenCount * OUTPUT_COUNT
                        }
                    }
                    when (LAYER_COUNT) {
                        2 -> {
                            layers.add(Layer(configuration, inputCount, OUTPUT_COUNT, weights.sliceArray(IntRange(startIndex, endIndex))))
                        } else -> {
                            layers.add(Layer(configuration, hiddenCount, OUTPUT_COUNT, weights.sliceArray(IntRange(startIndex, endIndex))))
                        }
                    }
                }
                else -> {
                    val startIndex = layers.sumBy { it.weights.size - 1 }
                    val endIndex = startIndex + hiddenCount * hiddenCount
                    layers.add(Layer(configuration, hiddenCount, hiddenCount, weights.sliceArray(IntRange(startIndex, endIndex))))
                }
            }
        }
    }

    // Run the network against the inputs and return the outputs
    fun update(inputs: Array<Double>) : Array<Double> {
        var outputs = inputs
        layers.forEach { layer ->
            outputs = layer.activate(outputs)
        }
        return outputs
    }

    // Gets the weights of all the layers as a single array
    fun getCopyOfWeights() : Array<Double> {
        val weights = mutableListOf<Double>()
        layers.forEach { layer ->
            weights.addAll(layer.weights)
        }
        return weights.toTypedArray()
    }

    // Represents a layer in the artificial neural network
    private class Layer(private val configuration: Configuration,
                        private val inputSize: Int,
                        private val outputSize: Int,
                        var weights: Array<Double> = Array((inputSize * outputSize), { _ ->
                            NonDeterminism.randomNetworkWeight()
                        })) {

        // Activate the layer and return the set of outputs
        fun activate(inputs: Array<Double>) : Array<Double> {
            // Validate the inputs are the correct size
            if (inputs.size >= weights.size)
                throw IllegalStateException("There are an incorrect number of inputs")

            // Create the output array
            val outputs = Array(outputSize, { _ -> 0.0 })

            // Foreach output, work out the activation
            for (output in 0 until outputs.size) {
                // Foreach input, multiply by the corresponding weight
                var netInput = 0.0
                for (input in 0 until inputs.size) {
                    netInput += weights[input * output + 1] * inputs[input]
                }

                // Set the output
                outputs[output] = tanh(netInput)
            }

            return outputs
        }

        // Calculate the sigmoid derivative of the passed input
        private fun sigmoid(input: Double) : Double = 1 / (1 + Math.exp(-input / configuration.activationResponse))
        // Calculate the tanh derivative of the passed input
        private fun tanh(input: Double) : Double = 2 * sigmoid(input * 2) - 1
    }
}

package Learning

import Simulation.Configuration
import Utilities.NonDeterminism

private const val CROSSOVER_RATE = 1.75
private const val MUTATION_CHANCE = 0.01
private const val MAX_PERTURBATION = 0.1
private const val ELITES = 3

// Functions for performing selection, crossover and mutation of a networks weights
class Genetics(private val configuration: Configuration = Configuration()) {

    // The population of network members to perform genetic selection on
    private val population = mutableListOf<NetworkPopulationMember>()

    // Reset the genetics
    fun reset() {
        population.clear()
    }

    // Creates a population member with the passed weights
    fun addPopulationMember(weights: DoubleArray) {
        population.add(NetworkPopulationMember(configuration, weights.toTypedArray()))
    }

    // Sets the population fitnesses to the passed set of doubles
    fun setPopulationFitnesses(fitnesses: Collection<Double>) {
        fitnesses.forEachIndexed { index, fitness ->
            population.elementAt(index).fitness = fitness
        }
    }

    // Starts a new epoch
    fun epoch() : Collection<DoubleArray> {
        // Sort the current population by fitness
        population.sortByDescending { it.fitness }

        // Create a new population
        val newPopulation = mutableListOf<NetworkPopulationMember>()

        // Add elites (the best one from the current population gets copied into the new population N times)
        for (elite in 0 until ELITES) {
            val eliteMember = NetworkPopulationMember(configuration, population.first().network.getCopyOfWeights())
            newPopulation.add(eliteMember)
        }

        // While the new population still needs filling
        while (newPopulation.size < population.size) {
            // Select parents
            val dad = rouletteSelection()
            val mum = rouletteSelection()

            // Create child
            val children = dad.crossover(mum)

            // Add child to new population
            newPopulation.addAll(listOf(children.first, children.second))
        }

        // Clear the current pop and set to the new one
        population.clear()
        population.addAll(newPopulation)

        // Return a copy of the new population
        return newPopulation.map { it.network.getCopyOfWeights().toDoubleArray() }
    }

    // Selects a member from the population using roullette method
    // This means the chance of being selected is directly proportional
    // To the fitness of the member in respect to the rest of the population
    private fun rouletteSelection() : NetworkPopulationMember {
        // Work out the total fitness of the population
        var totalPopulationFitness = 0.0
        population.forEach { member ->
            totalPopulationFitness += member.fitness
        }

        // Select a random slice point
        val slice = NonDeterminism.randomDouble() * totalPopulationFitness

        // Keep looping the population until the trackFitness exceeds the slice point
        var trackedFitness = 0.0
        population.forEach { member ->
            trackedFitness += member.fitness
            if (trackedFitness > slice) {
                // We found the first member after the slice point
                // Return it
                return member
            }
        }

        // For some reason the slice was greater than than total fitness
        // So just return the last member from the population
        return population[population.size - 1]
    }
}

// Represents a network as a member of an evolving population set
class NetworkPopulationMember(private val configuration: Configuration, val network: Network = Network(configuration)) {

    // Constructor for creating a new member with specified set of weights
    constructor(configuration: Configuration, weights: Array<Double>) : this(configuration) {
        network.setWeights(weights)
    }

    // Tracks the fitness of this member with respect to the rest of the population
    var fitness = 0.0

    // Performs crossover with the passed member and returns a new member
    fun crossover(with: NetworkPopulationMember) : Pair<NetworkPopulationMember, NetworkPopulationMember> {
        // If the crossover rate is not met, or the parents are the same just return a copy of one of them
        if (NonDeterminism.randomDouble(2.0) < CROSSOVER_RATE || this == with) {
            return Pair(NetworkPopulationMember(configuration, this.network.getCopyOfWeights()),
                    NetworkPopulationMember(configuration, with.network.getCopyOfWeights()))
        }

        // Get the weights of the parents
        val mumWeights = with.network.getCopyOfWeights()
        val dadWeights = this.network.getCopyOfWeights()

        // Determine the random crossover point
        val crossoverPoint = NonDeterminism.randomCrossoverPoint(mumWeights.size - 1)

        // Create the child A weights array
        val childWeightsA = mutableListOf<Double>()
        childWeightsA.addAll(mumWeights.sliceArray(IntRange(0, crossoverPoint)))
        childWeightsA.addAll(dadWeights.sliceArray(IntRange(crossoverPoint, mumWeights.size - 1)))

        // Create child B weights array
        val childWeightsB = mutableListOf<Double>()
        childWeightsB.addAll(dadWeights.sliceArray(IntRange(0, crossoverPoint)))
        childWeightsB.addAll(mumWeights.sliceArray(IntRange(crossoverPoint, mumWeights.size - 1)))

        for (index in 0 until childWeightsA.size) {
            if (NonDeterminism.randomDouble() < MUTATION_CHANCE) {
                childWeightsA[index] += if(NonDeterminism.randomBoolean()) MAX_PERTURBATION else - MAX_PERTURBATION
            }
            if (NonDeterminism.randomDouble() < MUTATION_CHANCE) {
                childWeightsB[index] += if(NonDeterminism.randomBoolean()) MAX_PERTURBATION else - MAX_PERTURBATION
            }
        }

        // Create and return the child network
        return Pair(NetworkPopulationMember(configuration, childWeightsA.toTypedArray()), NetworkPopulationMember(configuration, childWeightsB.toTypedArray()))
    }
}