Search code examples
javagenetic-algorithmevolutionary-algorithmbitarray

Java: Mixing together two double bitstrings for Genetic Algorithm crossover


I am implementing evolutionary neural network. I ran into problem when it comes to the crossover of two double values. I am evolving the weights of the links in the Neural Network.

    //Get the weights that I want to crossover
    double weightA = a.getWeight();
    double weightB = b.getWeight();
    //Round to 6 decimal numbers.
    weightA = (double)Math.round(weightA * 1000000) / 1000000;
    weightB = (double)Math.round(weightB * 1000000) / 1000000;
    //Convert the doubles to binary strings
    String binaryA = Long.toBinaryString(Double.doubleToRawLongBits(weightA));
    String binaryB = Long.toBinaryString(Double.doubleToRawLongBits(weightB));
    //Define random crossover point.
    int crossOverPoint = randInt(0, binaryA.length());
    //Put the strings together based on the crossover point.
    String newBinary = binaryA.substring(0,crossOverPoint) + binaryB.substring(crossOverPoint+1,binaryB.length());
    double newWeight = Double.longBitsToDouble(new BigInteger(newBinary, 2).longValue());

The problem I am encountering is that I am getting very large or very small weights after crossover which is probably the result of something about how many bits are used in each string for decimal places. How should I do this to get values after crossover that are similar to the two parents?

I had a workaround for this problem that gave me decent results but I am fairly sure that is not the correct approach, which basically finds the average between the two values and adds some Gaussian noise with standard deviation based on the interval of the original two values.

    double interval = Math.abs(weightA-weightB);
    double newWeight = (weightA+weightB)*0.5 + r.nextGaussian()*interval*2;

Solution

  • I'm not that familiar with genetic algorithms, but from what I know your treatment of doubles doesn't seem to be a good way of approaching it:

    I assume here that you want to use the first crossOverPoint bits of the binary representation of the first double and the last (64-crossOverPoint) bits of the second double (correct me if I'm wrong). If you use Strings you'll have to make sure to include leading 0s. The simpler approach would be to combine the binary representations of the longs using bit operations:

    long weightALong = Double.doubleToRawLongBits(weightA);
    long weightBLong = Double.doubleToRawLongBits(weightB);
    long mask = -1L; // all bits set to 1
    int crossOverPoint = randInt(0, Long.SIZE);
    long combined;
    // treat special cases because of modulo Long.SIZE of second parameter of shifting operations
    if (crossOverPoint == 0) {
        combined = weightBLong;
    } else if (combined == Long.SIZE) {
        combined = weightALong;
    } else {
        combined = (weightALong & (mask << (Long.SIZE - crossOverPoint))) |
                   (weightBLong & (mask >>> crossOverPoint));
    }
    double newWeight = Double.longBitsToDouble(combined);
    

    However from the binary representation of doubles I guess that combining the binary representations that way may not be the best way to combine doubles:

    • If the first bits are different, the right choice of crossOverPoint (1) can just change the sign.
    • the exponent comes completely from weightA in (52 / 64) of all cases.
    • NaN, POSITIVE_INFINITY, and NEGATIVE_INFINITY can be produced from values different from all of these three if you get a unlucky combination in the mantissa.

    I guess your workaround seems to be the better choice. (Maybe you should ask that question on https://cs.stackexchange.com/)