Если нельзя, но очень хочется, то нужно обязательно и ничего в мире не стоит того, чтобы делать из этого проблему!


Интересна Java? Кликай по ссылке и изучай!
Если тебе полезно что-то из того, чем я делюсь в своем блоге - можешь поделиться своими деньгами со мной.
с пожеланием
столько времени читатели провели на блоге - 
сейчас онлайн - 

пятница, 24 февраля 2012 г.

Мой первый ИИ: Однослойный персептрон, который с учителем учится решать AND/OR/NOT операции

Этой попыткой я сильно приблизил судный день, ведь одной моей мечтой из детства было создать ИИ, позже оцифровать свой мозг. Шутка. Но немного покодим.

Долго этот таск висел в туду и все же я его решил реализовать. Спасибо Сереге и Костику, ведь именно им я рассказал за ужином, как оцифровали нейрон и что такое нейронная сетка. Знал я это по рассказам в википедии.

Итак я наткнулся на видео. Там все понятно описано.



А вот текст кода


Естественно я его набрал по своему :) Код можно качнуть тут.

Вся соль в синапсах и том, как мы меняем их "сопротивление". Это все математически должно быть доказано, что делай так и будет тебе счастье. Так что пока экспериментирую с тем что есть.

Идем по очереди. И начнем естественно с тестов.

package perceptron;

import org.junit.Test;
import static junit.framework.Assert.assertEquals;

public class AndNeuronTest extends AbstractNeuronTest {

    @Override
    Patterns getPattern() {
        return new Patterns(new double[][]{
            {0, 0, 0},
            {0, 1, 0},
            {1, 0, 0},
            {1, 1, 1},
        });
    }

    @Test
    public void should0when0and0(){
        assertEquals(0d, neuron.process(0, 0));
    }

    @Test
    public void should0when0and1(){
        assertEquals(0d, neuron.process(0, 1));
    }

    @Test
    public void should0when1and0(){
        assertEquals(0d, neuron.process(1, 0));
    }

    @Test
    public void should1when1and1(){
        assertEquals(1d, neuron.process(1, 1));
    }
}

Тест на то, что обученный персептрон выполняет возложенные на него функции.

Родитель инкапсулирует обучение персептрона по шаблону.

package perceptron;

import org.junit.Before;

public abstract class AbstractNeuronTest {

    protected Neuron neuron;
    protected Teacher teacher;

    @Before
    public void teachPerceptron() {
        teacher = new Teacher(getPattern());
        neuron = teacher.teach();
    }

    abstract Patterns getPattern();
}

А делалось это для того, чтобы сделать еще пару тестов на OR и NOT операции

package perceptron;

import org.junit.Test;

import static junit.framework.Assert.assertEquals;

public class OrNeuronTest extends AbstractNeuronTest {

    @Override
    Patterns getPattern() {
        return new Patterns(new double[][]{
            {0, 0, 0},
            {0, 1, 1},
            {1, 0, 1},
            {1, 1, 1},
        });
    }

    @Test
    public void should0when0or0(){
        assertEquals(0d, neuron.process(0, 0));
    }

    @Test
    public void should1when0or1(){
        assertEquals(1d, neuron.process(0, 1));
    }

    @Test
    public void should1when1or0(){
        assertEquals(1d, neuron.process(1, 0));
    }

    @Test
    public void should1when1or1(){
        assertEquals(1d, neuron.process(1, 1));
    }
}

И еще один

package perceptron;

import org.junit.Test;

import static junit.framework.Assert.assertEquals;

public class NotNeuronTest extends AbstractNeuronTest {

    private static int STUB = 1;

    @Override
    Patterns getPattern() {
        return new Patterns(new double[][]{
            {STUB, 0, 1},
            {STUB, 1, 0},
        });
    }

    @Test
    public void should0whenNot1(){
        assertEquals(0d, neuron.process(STUB, 1));
    }

    @Test
    public void should1whenNot0(){
        assertEquals(1d, neuron.process(STUB, 0));
    }
}

Нейрон - это на самом деле интерфейс

package perceptron;

public interface Neuron {

    double process(double... input);

}

Но что дальше?

Разберем самый простой случай (почему самый? а так, просто) - операция AND.

Для него достаточно чтобы учитель вернул класс

package perceptron;

public class Teacher {

    public Neuron teach() {
        return new AndNeuron();
    }
}

А вот и сам класс

package perceptron;

public class AndNeuron implements Neuron {

    public double process(double... input) {
        return (0.3*input[0] + 0.3*input[1] > 0.5)?1:0;
    }
}

Хаха, как смешно. А не смешно! Именно так оно и работает.

А чтобы заработал тест на OR надо подсунуть другую реализацию

package perceptron;

public class OrNeuron implements Neuron {

    public double process(double... input) {
        return (0.6*input[0] + 0.6*input[1] > 0.5)?1:0;
    }
}

Разницу уловили?

А вот для NOT

package perceptron;

public class NotNeuron implements Neuron {

    public double process(double... input) {
        return (0.6*input[0] + -0.09999999999999998*input[1] > 0.5)?1:0;
    }
}

И что получается. Всего две константы говорят нам какая операция будет на выходе? Ага, именно.

Теперь нам осталось сделать такую реализацию, которая в зависимости от входных условий (того самого экземпляра Patterns) могла сама находить необходимые константы.

Самый простой вывод - его величество Random. Попробуем!

package perceptron;

import java.util.Random;

public class RandomNeuron implements Neuron {

    private double synapse1 = 2*new Random().nextDouble() - 1;
    private double synapse2 = 2*new Random().nextDouble() - 1;

    public double process(double... input) {
        return (synapse1*input[0] + synapse2*input[1] > 0.5)?1:0;
    }
}

Кстати учитель немного поменялся - теперь он хоть чем-то (перебором) занимается.

package perceptron;

public class RandomTeacher {

    private Patterns patterns;

    public Teacher(Patterns patterns) {
        this.patterns = patterns;
    }

    public Neuron teach() {
        Neuron neuron;
        do {
            neuron = new RandomNeuron();
        } while (!match(patterns, neuron));
        return neuron;
    }

    private boolean match(Patterns patterns, Neuron neuron) {
        for (InOut inOut : patterns) {
            double expected = inOut.getOut();
            double actual = neuron.process(inOut.getIn()[0], inOut.getIn()[1]);
            if (expected != actual) {
                return false;
            }
        }
        return true;
    }
}

Я назвал класс RandomTeacher потому как планирую его оставить в коде (OCP).

Я бы тут добавил условие на dead loop, но не хотелось в примере усложнять код.

Тут же наверное стоит привести все остальные классы: Pattern, InOut и In. Родились они вокруг массива double - я просто не люблю массивы, у них интерфейс не как у всех объектов.

package perceptron;

import java.util.Arrays;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;

public class Patterns implements Iterable<InOut> {

    private List<InOut> inOuts;

    public Patterns(double[][] data) {
        int countPatterns = data.length;
        int countInput = data[0].length - 1;

        inOuts = new LinkedList<InOut>();
        for (int index = 0; index < countPatterns; index ++) {
            double[] in = Arrays.copyOf(data[index], data[index].length - 1);

            inOuts.add(new InOut(in, data[index][countInput]));
        }
    }

    public InOut get(int index) {
        return inOuts.get(index).copy();
    }

    public Iterator<InOut> iterator() {
        return new LinkedList(inOuts).iterator();
    }

    public int getInCount() {
        return get(0).getIn().length;
    }
}

package perceptron;

public class InOut {

    private In input;
    private double output;

    public InOut(double[] input, double output) {
        this.input = new In(input);
        this.output = output;
    }

    InOut(InOut inOut) {
        this(inOut.getIn(), inOut.getOut());
    }

    public double[] getIn() {
        return input.getAll();
    }

    public double getOut() {
        return this.output;
    }

    public InOut copy() {
        return new InOut(this);
    }
}

package perceptron;

import java.util.Arrays;

public class In {
    private double[] data;

    public In(double[] input) {
        data = Arrays.copyOf(input, input.length);
    }

    public double[] getAll() {
        return Arrays.copyOf(data, data.length);
    }

    public double get(int index) {
        return data[index];
    }
}

Короче, вопрос, почему они такие, а не другие, и почему они вообще есть лучше упустить. Это какая-то 15я версия их, и мы их менять не будем еще долго. Позже я надеюсь от них избавится как-то...

После такой-себе рекламной паузы, направлю нас в сторону решения, которое предложил автор в самом начале.

Синапсы, как мы уже их назвали, можно изменять походу дела (обучения). Делать это будет учитель. И делать он это будет как раньше в школах делали - указкой по пальцам, если что-то не так.

Учителю дадим такую возможность через метод correct у нейрона.

package perceptron;

public interface Neuron {

    double process(double... input);

    void correct(double error);

}

Бить будем с силой error - то есть чем хуже ошибка, тем сильнее бъем.

Нейрон (на этот раз TwoInputNeuron) на это реагирует вполне адекватно (и тут вторая изюминка подхода)

package perceptron;

import java.util.Arrays;

public class TwoInputNeuron implements Neuron {

    private static double DELTA = 0.5;
    private static double INIT_SYNAPSE = 0.5;
    private static double SYNAPSE_CERRECTION = 0.1;
    private static double HOT = 1.0;
    private static double COLD = 0.0;

    private double enter1;
    private double enter2;
    private double outer;
    private double synapse1 = INIT_SYNAPSE;
    private double synapse2 = INIT_SYNAPSE;

    public double process(double... input) {
        enter1 = input[0];
        enter2 = input[1];
        return (enter1*synapse1 + enter2*synapse2 > DELTA)?HOT:COLD;
    }

    public void correct(double error) {
        synapse1 += SYNAPSE_CERRECTION*error*enter1;
        synapse2 += SYNAPSE_CERRECTION*error*enter2;
    }
}

Код все тот же, только вынес константы и добавил метод correct.

Теперь нарисуем этого учителя-злюку!

package perceptron;

public class Teacher {

    private Patterns patterns;
    private Neuron neuron;
    private double allError;

    public Teacher(Patterns patterns) {
        this.patterns = patterns;
        this.neuron = new TwoInputNeuron();
        allError = 0;
    }

    public Neuron teach() {
        do {
            allError = 0;

            for (InOut inOut : patterns) {
                teach(inOut);
            }
        } while (allError != 0);

        return neuron;
    }

    private void teach(InOut inOut) {
        double result = neuron.process(inOut.getIn());
        double error = inOut.getOut() - result;

        neuron.correct(error);

        allError = allError + Math.abs(error);
    }
}

А теперь перейдем к тесту XOR

package perceptron;

import org.junit.Test;

import static junit.framework.Assert.assertEquals;
import static org.junit.Assert.fail;

public class XorNeuronTest {

    private Patterns xorPattern = new Patterns(new double[][]{
            {0, 0, 0},
            {0, 1, 1},
            {1, 0, 1},
            {1, 1, 0},
        });

    @Test
    public void shouldExceptionWhenTeach(){
        Teacher teacher = new Teacher(xorPattern);

        try {
            Neuron neuron = teacher.teach();
            fail("Ожидается исключение");
        } catch(RuntimeException exception) {
            assertEquals("Простите, но прецептрон туп и необучаем!",
                    exception.getMessage());
        }
    }
}

Он такой специально. Потому как его никак не решить такому простому нейрончику аж никак. Нет таких констант-синапсов, которые при суммировании на них входных сигналов дадут соотвествующие выходные сигналы.

Ну и в тесте мы ожидаем исключение, а потому учитель чуть переписался и стал (SecuredTeacher)

package perceptron;

import java.util.Arrays;

public class SingleNeuron implements Neuron {

    private static double DELTA = 0.5;
    private static double INIT_SYNAPSE = 0.5;
    private static double SYNAPSE_CERRECTION = 0.1;
    private static double HOT = 1.0;
    private static double COLD = 0.0;

    private double[] enters;
    private double outer;
    private double[] synapses;

    public SingleNeuron(int countIn) {
        initWeight(countIn);
    }

    public double process(double... input) {
        enters = Arrays.copyOf(input, input.length);

        outer = COLD;
        for (int index = 0; index < enters.length; index ++) {
            outer = outer + enters[index]* synapses[index];
        }
        if (outer > DELTA) {
            outer = HOT;
        } else {
            outer = COLD;
        }

        return outer;
    }

    private void initWeight(int countIn) {
        synapses = new double[countIn];
        for (int index = 0; index < countIn; index ++) {
            synapses[index] = INIT_SYNAPSE;
        }
    }

    public void correct(double error) {
        for (int index = 0; index < enters.length; index ++) {
            synapses[index] += SYNAPSE_CERRECTION*error*enters[index];
        }
    }
}

А нейрон я сделал мультивходовым

package perceptron;

import java.util.Arrays;

public class SingleNeuron implements Neuron {

    private static double DELTA = 0.5;
    private static double INIT_SYNAPSE = 0.5;
    private static double SYNAPSE_CERRECTION = 0.1;
    private static double HOT = 1.0;
    private static double COLD = 0.0;

    private double[] enters;
    private double outer;
    private double[] synapses;

    public SingleNeuron(int countIn) {
        initWeight(countIn);
    }

    public double process(double... input) {
        enters = Arrays.copyOf(input, input.length);

        outer = COLD;
        for (int index = 0; index < enters.length; index ++) {
            outer = outer + enters[index]* synapses[index];
        }
        if (outer > DELTA) {
            outer = HOT;
        } else {
            outer = COLD;
        }

        return outer;
    }

    private void initWeight(int countIn) {
        synapses = new double[countIn];
        for (int index = 0; index < countIn; index ++) {
            synapses[index] = INIT_SYNAPSE;
        }
    }

    public void correct(double error) {
        for (int index = 0; index < enters.length; index ++) {
            synapses[index] += SYNAPSE_CERRECTION*error*enters[index];
        }
    }
}

Но нет ничего неразрешимого. Мы можем построить несколько слоев нейронов и работая с ними подобным образом (синапсы-опыт, учитель, линейка, коррекция опыта) настроить синапсы так, чтобы на любой вход выдавался любой желаемый выход.

Об этом дальше...

Комментариев нет:

Отправить комментарий