Этой попыткой я сильно приблизил судный день, ведь одной моей мечтой из детства было создать ИИ, позже оцифровать свой мозг. Шутка. Но немного покодим.
Долго этот таск висел в туду и все же я его решил реализовать. Спасибо Сереге и Костику, ведь именно им я рассказал за ужином, как оцифровали нейрон и что такое нейронная сетка. Знал я это по рассказам в википедии.
Итак я наткнулся на видео. Там все понятно описано.
А вот текст кода
Естественно я его набрал по своему :) Код можно качнуть тут.
Вся соль в синапсах и том, как мы меняем их "сопротивление". Это все математически должно быть доказано, что делай так и будет тебе счастье. Так что пока экспериментирую с тем что есть.
Идем по очереди. И начнем естественно с тестов.
Тест на то, что обученный персептрон выполняет возложенные на него функции.
Родитель инкапсулирует обучение персептрона по шаблону.
А делалось это для того, чтобы сделать еще пару тестов на OR и NOT операции
И еще один
Нейрон - это на самом деле интерфейс
Но что дальше?
Разберем самый простой случай (почему самый? а так, просто) - операция AND.
Для него достаточно чтобы учитель вернул класс
А вот и сам класс
Хаха, как смешно. А не смешно! Именно так оно и работает.
А чтобы заработал тест на OR надо подсунуть другую реализацию
Разницу уловили?
А вот для NOT
И что получается. Всего две константы говорят нам какая операция будет на выходе? Ага, именно.
Теперь нам осталось сделать такую реализацию, которая в зависимости от входных условий (того самого экземпляра Patterns) могла сама находить необходимые константы.
Самый простой вывод - его величество Random. Попробуем!
Кстати учитель немного поменялся - теперь он хоть чем-то (перебором) занимается.
Я назвал класс RandomTeacher потому как планирую его оставить в коде (OCP).
Я бы тут добавил условие на dead loop, но не хотелось в примере усложнять код.
Тут же наверное стоит привести все остальные классы: Pattern, InOut и In. Родились они вокруг массива double - я просто не люблю массивы, у них интерфейс не как у всех объектов.
Короче, вопрос, почему они такие, а не другие, и почему они вообще есть лучше упустить. Это какая-то 15я версия их, и мы их менять не будем еще долго. Позже я надеюсь от них избавится как-то...
После такой-себе рекламной паузы, направлю нас в сторону решения, которое предложил автор в самом начале.
Синапсы, как мы уже их назвали, можно изменять походу дела (обучения). Делать это будет учитель. И делать он это будет как раньше в школах делали - указкой по пальцам, если что-то не так.
Учителю дадим такую возможность через метод correct у нейрона.
Бить будем с силой error - то есть чем хуже ошибка, тем сильнее бъем.
Нейрон (на этот раз TwoInputNeuron) на это реагирует вполне адекватно (и тут вторая изюминка подхода)
Код все тот же, только вынес константы и добавил метод correct.
Теперь нарисуем этого учителя-злюку!
А теперь перейдем к тесту XOR
Он такой специально. Потому как его никак не решить такому простому нейрончику аж никак. Нет таких констант-синапсов, которые при суммировании на них входных сигналов дадут соотвествующие выходные сигналы.
Ну и в тесте мы ожидаем исключение, а потому учитель чуть переписался и стал (SecuredTeacher)
А нейрон я сделал мультивходовым
Но нет ничего неразрешимого. Мы можем построить несколько слоев нейронов и работая с ними подобным образом (синапсы-опыт, учитель, линейка, коррекция опыта) настроить синапсы так, чтобы на любой вход выдавался любой желаемый выход.
Об этом дальше...
Долго этот таск висел в туду и все же я его решил реализовать. Спасибо Сереге и Костику, ведь именно им я рассказал за ужином, как оцифровали нейрон и что такое нейронная сетка. Знал я это по рассказам в википедии.
Итак я наткнулся на видео. Там все понятно описано.
А вот текст кода
Естественно я его набрал по своему :) Код можно качнуть тут.
Вся соль в синапсах и том, как мы меняем их "сопротивление". Это все математически должно быть доказано, что делай так и будет тебе счастье. Так что пока экспериментирую с тем что есть.
Идем по очереди. И начнем естественно с тестов.
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]; } } }
Но нет ничего неразрешимого. Мы можем построить несколько слоев нейронов и работая с ними подобным образом (синапсы-опыт, учитель, линейка, коррекция опыта) настроить синапсы так, чтобы на любой вход выдавался любой желаемый выход.
Об этом дальше...
Комментариев нет:
Отправить комментарий