Słów kilka dla początkujących o sieciach neuronowych…
Każdy z nas choć raz w życiu spotkałem się z pojęciem “sieć neuronowa” - czy to w szkole podstawowej na biologii, czy to na studiach na informatyce. Jeśli pojęcie to jest Ci zupełnie obce, to dziś postaram Ci się je przybliżyć. Sieć, którą będziemy tworzyć i omawiać napiszemy w Javie (czysta Java, bez frameworków) w taki sposób, aby można ją było wpiąć w dowolne miejsce - mobile, web, desktop, itd.
Jak nasza sieć neuronowa się ma do lekcji biologii w szkole?
Tworzona przez nas sieć będzie składać się z neuronów (u nas będzie to jeden neuron), które posiadają taką samą budowę jak ludzki neuron. Poniżej przedstawiam zestawienie nazw używanych przez biologów z nazwami używanymi w informatyce.
Dendryty – Wejścia (1)
Synapsy – Wagi (1)
Jądro – Blok sumujący (2)
Wzgórek aksonu – Blok aktywacji (3)
Akson – Wyjście (3)
Podana wyżej kolejność nie jest przypadkowa, w takiej właśnie kolejności dane przepływają przez neuron. Możemy wyróżnić 3 role elementów: wejściowe(1), obliczeniowe(2), wyjściowe(3).
Wejściowe - przyjmują dane oraz określają ich ważność (wagę)
Obliczeniowe - wykonują właściwe operacje obliczeniowe
Wyjściowe - przygotowują dane (ostateczna obróbka) oraz zwracają wynik obliczeń
Na nasz neuron działa wiele różnych czynników. Aby określić, które są istotne, a które nie musimy pomnożyć wartość danej informacji z jej wagą (“dendryt * synapsa”). W praktyce będzie wyglądać to tak, że utworzymy sobie 2 tablice (lub listy) i w nich będziemy trzymać wspomniane wcześniej dane.
protected NeuralCell() {
dendrites = new ArrayList<>();
synapses = new ArrayList<>();
}
Wyniki otrzymane z wejścia trafiają do jądra (bloku sumującego), w którym są sumowane (czyli “dendryt[1] * synapsa[1]” + … + “dendryt[n] * synapsa[n]”) - tworzą tzw. potencjał membranowy. Dalej dane (potencjał membranowy) trafiają na wzgórek aksonu (blok aktywacji), gdzie poddane zostają funkcji (najczęściej liniowej lub bipolarnej funkcji skoku jednostkowego, ale można także zastosować własną funkcję). Następnie na wyjście zostaje zwrócona wartość powstała w bloku aktywacji.
Skoro znamy już zasadę działa przejdziemy teraz do implementacji neuronu w Javie. Wcześniej pokazałem Wam już konstruktor, teraz czas na całą resztę kodu.
Aby nasz neuron był łatwo i fajnie modyfikowalny będziemy go tworzyć z pomocą klasy abstrakcyjnej. Klasa ta będzie posiadać tylko jedną abstrakcyjną metodą, w której zdefiniujemy nasz blok aktywacji.
public abstract class NeuralCell {
private List<Double> dendrites;
private List<Double> synapses;
protected NeuralCell() {
dendrites = new ArrayList<>();
synapses = new ArrayList<>();
}
protected abstract double finalizeData(double membranePotential);
}
Metoda ta wymaga dostarczenia potencjału membranowego. Czas napisać funkcję, która będzie go wyliczać.
public abstract class NeuralCell {
private List<Double> dendrites;
private List<Double> synapses;
protected NeuralCell() {
dendrites = new ArrayList<>();
synapses = new ArrayList<>();
}
protected abstract double finalizeData(double membranePotential);
public int getInputSize() {
return dendrites.size();
}
private double processCellNode(int index) {
return (dendrites.get(index) * synapses.get(index));
}
private double getMembranePotential() {
final int size = getInputSize();
if (size == 0)
return -1;
double sum = 0;
for (int i = 0; i < size; i++)
sum += processCellNode(i);
return sum;
}
}
Skoro mamy już metody obliczające, to teraz czas na metodę publikującą.
public abstract class NeuralCell {
private List<Double> dendrites;
private List<Double> synapses;
protected NeuralCell() {
dendrites = new ArrayList<>();
synapses = new ArrayList<>();
}
protected abstract double finalizeData(double membranePotential);
public int getInputSize() {
return dendrites.size();
}
private double processCellNode(int index) {
return (dendrites.get(index) * synapses.get(index));
}
private double getMembranePotential() {
final int size = getInputSize();
if (size == 0)
return -1;
double sum = 0;
for (int i = 0; i < size; i++)
sum += processCellNode(i);
return sum;
}
public double getOutput() {
if (getInputSize() == 0)
return -1;
return finalizeData(getMembranePotential());
}
}
Kolejnym etapem będzie dostarczenie naszemu neutronowi danych.
public abstract class NeuralCell {
private List<Double> dendrites;
private List<Double> synapses;
protected NeuralCell() {
dendrites = new ArrayList<>();
synapses = new ArrayList<>();
}
protected abstract double finalizeData(double membranePotential);
public int getInputSize() {
return dendrites.size();
}
private void addInput() {
dendrites.add(0.0);
synapses.add(0.0);
}
public void addInput(int count) {
for (int i = 1; i <= count; i++)
addInput();
}
public double getInputWeight(int index) {
return synapses.get(index);
}
public void setInputWeight(int index, double weight) {
synapses.set(index, weight);
}
public double getInputData(int index) {
return dendrites.get(index);
}
public void setInputData(int index, double value) {
dendrites.set(index, value);
}
private double processCellNode(int index) {
return (dendrites.get(index) * synapses.get(index));
}
private double getMembranePotential() {
final int size = getInputSize();
if (size == 0)
return -1;
double sum = 0;
for (int i = 0; i < size; i++)
sum += processCellNode(i);
return sum;
}
public double getOutput() {
if (getInputSize() == 0)
return -1;
return finalizeData(getMembranePotential());
}
}
I to w zasadzie wszystko. Stworzyliśmy pełnoprawny neuron. Dorzucam jeszcze przykład użycia neuronu. Opis tego co dzieje się w przykładzie znajdziecie pod kodem.
public class Main {
private static final int X = 0;
private static final int Y = 1;
public static void main(String[] args) {
ChartView chartView = new ChartView();
ChartDrawer chartDrawer = chartView.getChartDrawer();
RandomPair rand = new RandomPair();
NeuralCell neuralCell = new NeuralCell() {
@Override
public double finalizeData(double membranePotential) {
if (membranePotential > 0)
return 1;
else if (membranePotential < 0)
return -1;
else
return 0;
}
};
neuralCell.addInput(2); // x,y
double[] weight = rand.getRandomDoublePair();
neuralCell.setInputWeight(X, weight[X]);
neuralCell.setInputWeight(Y, weight[Y]);
int counter = 0;
while (counter != 99999) {
addRandomPoint(rand, neuralCell, chartDrawer);
counter++;
}
}
private static void addRandomPoint(RandomPair rand, NeuralCell neuralCell, ChartDrawer chartDrawer) {
final int[] values = rand.getRandomIntPair(200);
neuralCell.setInputData(X, values[X]);
neuralCell.setInputData(Y, values[Y]);
final int result = (int) neuralCell.getOutput();
if (result == 1)
chartDrawer.drawPoint(values, Color.BLUE);
else if (result == -1)
chartDrawer.drawPoint(values, Color.RED);
}
}
W przykładzie tym tworzymy neuron, którego celem będzie określenie, po której stronie prostej malejącej znajduje się punkt. Punkt ten następnie malowany jest na czerwony lub niebieski kolor.
Oto efekt pracy neutronu:
Za każdym razem uzyskamy ciut inny wynik, ponieważ wszystko jest losowane w trakcie działania programu. Oto efekt kolejnego uruchomienia programu:
To co dzieje się w tym przykładzie to:
1. Utworzenie neuronu i określenie jego bloku aktywacji
2. Dodanie 2 wejść (ponieważ na nasz punkt składa się wartość X oraz Y)
3. Przypisujemy losowe wagi danym jakie znajdą się pod wspomnianympunktem
4. Zaczynam generować losową parę punktów (w zakresie 200 ponieważ tak duży był mój wykres)
5. Wylosowane wartości wprowadzamy do neuronu jako dane wejścia (wagę tych danych ustaliliśmy na początku i nie zmieniamy jej)
6. Na podstawie rezultatu zwróconego przez neuron określamy jaki kolor przyjmie nasz punk na wykresie.
Przedstawiony tu przykład to najprostsza sieć neuronowa jaką można wykonać. W sieciach z jakimi się spotykamy (czy to świadomie, czy też nie) występuje dużo więcej neuronów połączonych ze sobą. Mówię Ci o tym, żebyś nie pomyślał, że temat jest tak prosty jak wygląd w tym przykładzie - nie, temat ten może taki być,ale najczęściej wymaga od nas zaplanowania bardzo złożonej sieci,czasem składającej się nawet z setek neutronów.Jeśli nie zraziłeś się tą informacją, to spróbuj wykonać sieć neutronową działającą na zasadzie XOR (ta wymaga połączenia 3 neutronów).
Mam nadzieję, że artykuł Wam się spodobał i dacie o tym znać w komentarzach.
Specjalnie dla Android.com.pl
Łukasz Bednarczyk