Этой попыткой я сильно приблизил судный день, ведь одной моей мечтой из детства было создать ИИ, позже оцифровать свой мозг. Шутка. Но немного покодим.
Долго этот таск висел в туду и все же я его решил реализовать. Спасибо Сереге и Костику, ведь именно им я рассказал за ужином, как оцифровали нейрон и что такое нейронная сетка. Знал я это по рассказам в википедии.
Итак я наткнулся на видео. Там все понятно описано.
А вот текст кода
Естественно я его набрал по своему :) Код можно качнуть тут.
Вся соль в синапсах и том, как мы меняем их "сопротивление". Это все математически должно быть доказано, что делай так и будет тебе счастье. Так что пока экспериментирую с тем что есть.
Идем по очереди. И начнем естественно с тестов.
Тест на то, что обученный персептрон выполняет возложенные на него функции.
Родитель инкапсулирует обучение персептрона по шаблону.
А делалось это для того, чтобы сделать еще пару тестов на 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];
}
}
}Но нет ничего неразрешимого. Мы можем построить несколько слоев нейронов и работая с ними подобным образом (синапсы-опыт, учитель, линейка, коррекция опыта) настроить синапсы так, чтобы на любой вход выдавался любой желаемый выход.
Об этом дальше...

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