
com.fixedorgo.neuron.NeoFuzzyNeuron Maven / Gradle / Ivy
/*
* Copyright (C) 2015 Timur Zagorsky
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.fixedorgo.neuron;
import com.fixedorgo.neuron.Synapse.SynapseBuilder;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import static java.lang.Math.pow;
/**
* A main Neo-Fuzzy-Neuron class that allow to create and use Neuron with
* unlimited number of Synapses. But please make sure you have applied {@link Input}s
* with correct dimension that should correspond to {@link Synapse} number. It's
* highly recommended to use {@link NeuronBuilder} and
* {@link com.fixedorgo.neuron.NeoFuzzyNeuron.Input.InputBuilder}
*
* @author Timur Zagorsky
* @since 0.1
*/
public class NeoFuzzyNeuron {
/**
* Map of {@link Synapse}s that represent input variables.
*/
protected final Map synapses;
protected NeoFuzzyNeuron(Map synapses) {
this.synapses = synapses;
}
/**
* Main static factory method to get new {@link NeoFuzzyNeuron} instance
* @param synapses that represents input variables, must not be null
* @return new {@link NeoFuzzyNeuron} instance with provided {@link Synapse}s
* @throws NullPointerException if synapses are absent
*/
public static NeoFuzzyNeuron neuron(Synapse... synapses) {
if (synapses == null) {
throw new NullPointerException("Synapses must not be null");
}
Map synapseMap = new HashMap<>();
for (Synapse synapse : synapses) {
synapseMap.put(synapse.name.toLowerCase(), synapse);
}
return new NeoFuzzyNeuron(synapseMap);
}
/**
* Calculation of Neo-Fuzzy-Neuron output. Neuron has a nonlinear synaptic transfer
* characteristic so the input signals through the nonlinear synapses are applied
* and summed up. Order of the Inputs is not important.
* @param inputs values of Neo-Fuzzy-Neuron input signals, must not be null
* @return Neo-Fuzzy-Neuron output value
* @throws NullPointerException in case of null {@link Input}[] inputs
* @throws NeuronInputDimensionException if input dimension is not corresponded
*/
public double calculate(Input[] inputs) {
checkInputDimension(inputs);
double output = 0;
for (Input input : inputs) {
output += synapseFor(input).apply(input.value);
}
return output;
}
/**
* Use Stepwise Learning Algorithm in the "passive mode" when only input/output data
* are available. Actual Neuron output is calculated with standard
* {@link #calculate(com.fixedorgo.neuron.NeoFuzzyNeuron.Input[])}
* @param inputs values of Neo-Fuzzy-Neuron input signals, must not be null
* @param trainingData that represents desired output value
*/
public void learn(Input[] inputs, double trainingData) {
learn(inputs, calculate(inputs), trainingData);
}
/**
* Use Stepwise Learning Algorithm with special case of learning rate calculation.
* The {@link #optimalLearningRate(com.fixedorgo.neuron.NeoFuzzyNeuron.Input[])} to
* calculate optimal learning rate
* @param inputs values of Neo-Fuzzy-Neuron input signals, must not be null
* @param output actual output of the Neo-Fuzzy-Neuron
* @param trainingData that represents desired output value
*/
public void learn(Input[] inputs, double output, double trainingData) {
learn(inputs, output, trainingData, optimalLearningRate(inputs));
}
/**
* Stepwise Learning Algorithm. The gradient descent scheme is employed here
* to reduce the error E through the adjustment of weight wij. Then
* the renewal of weight is given by:
*
* dwij = - Learning Rate * Error * Membership Degree
*
* @param inputs values of Neo-Fuzzy-Neuron input signals, must not be null
* @param output actual output of the Neo-Fuzzy-Neuron
* @param trainingData that represents desired output value
* @param learningRate that is given by some ratio and always greater than 0
* @throws NullPointerException in case of null {@link Input}[] inputs
* @throws NeuronInputDimensionException if input dimension is not corresponded
*/
public void learn(Input[] inputs, final double output, final double trainingData, final double learningRate) {
checkInputDimension(inputs);
for (final Input input : inputs) {
synapseFor(input).learnWith(new LearningFunction() {
@Override
public double apply(MembershipFunction membershipFunction) {
return -learningRate * (output - trainingData) * membershipFunction.apply(input.value);
}
});
}
}
/**
* Calculates optimal Neo-Fuzzy-Neuron learning rate according to [Walmir Caminhas,
* "A Fast Learning Algorithm for Neofuzzy Networks", 1998]. For internal use, at least now.
* @param inputs array of values to Neo-Fuzzy-Neuron, must not be null
* @return learning rate calculated value
* @throws NullPointerException in case of null {@link Input}[] inputs
* @throws NeuronInputDimensionException if input dimension is not corresponded
*/
protected double optimalLearningRate(Input[] inputs) {
checkInputDimension(inputs);
double sumOfSegments = 0;
for (Input input : inputs) {
for (double membershipDegree : synapseFor(input).fuzzySegment(input.value)) {
sumOfSegments += pow(membershipDegree, 2);
}
}
return 1 / sumOfSegments;
}
/**
* Input data for a given Synapse of the Neo-Fuzzy-Neuron. Should be used with
* default {@link NeoFuzzyNeuron} implementation due to ability of dynamic
* {@link NeoFuzzyNeuron} creation.
*/
public static class Input {
protected final String synapseName;
protected final double value;
/**
* To prevent direct Input instantiation
*/
protected Input(String synapseName, double value) {
this.synapseName = synapseName;
this.value = value;
}
/**
* Static factory method to get new {@link Input} instance
* @param synapseName to which Input should be applied, must not be null
* @param value of the input signal
* @return new {@link Input} instance
* @throws NullPointerException if synapseName is null
*/
public static Input input(String synapseName, double value) {
if (synapseName == null) {
throw new NullPointerException("Synapse name must not be null");
}
return new Input(synapseName, value);
}
/**
* Static access to {@link InputBuilder} instance
* @param synapseName for first created {@link Synapse}
* @return new instance of {@link InputBuilder}
*/
public static InputBuilder input(String synapseName) {
return new InputBuilder(synapseName);
}
/**
* A builder that provides fluent interface to create new {@link Input} instance.
*/
public static class InputBuilder {
protected Set inputs = new HashSet<>();
protected String synapseName;
/**
* To prevent direct Builder instantiation
*/
protected InputBuilder(String synapseName) {
input(synapseName);
}
/**
* Set name of the target {@link Synapse}
* @param synapseName name of the target Synapse, must not be null
* @return same {@link InputBuilder} instance
* @throws NullPointerException if synapseName is null
*/
public InputBuilder input(String synapseName) {
if (synapseName == null) {
throw new NullPointerException("Synapse name must not be null");
}
this.synapseName = synapseName;
return this;
}
/**
* Adds specified value for a new {@link Input} instance and sets it
* to Inputs collection
* @param value input signal value
* @return same {@link InputBuilder} instance
* @throws IllegalStateException if a synapse name was not specified first
*/
public InputBuilder as(double value) {
if (synapseName == null) {
throw new IllegalStateException("You must specify Synapse name first via InputBuilder.input() method");
}
inputs.add(new Input(synapseName, value));
synapseName = null; // to prevent call of this method twice
return this;
}
/**
* Alternative way to use {@link #input(String)} method
* @param synapseName name of the target Synapse, must not be null
* @return same {@link InputBuilder} instance
*/
public InputBuilder and(String synapseName) {
return input(synapseName);
}
/**
* Just for fluent needs
* @return same {@link InputBuilder} instance
*/
public InputBuilder and() {
return this;
}
/**
* Builds array of {@link Input} instances according to specified data
* @return array of new {@link Input} instances
* @throws IllegalStateException if Input values were not set at all
*/
public Input[] build() {
if (inputs.isEmpty()) {
throw new IllegalStateException("You have to specify at least one Input value. " +
"Use InputBuilder.as() method");
}
return inputs.toArray(new Input[inputs.size()]);
}
}
@Override
public boolean equals(Object obj) {
return obj instanceof Input &&
synapseName.equalsIgnoreCase(Input.class.cast(obj).synapseName) &&
value == Input.class.cast(obj).value;
}
@Override
public int hashCode() {
int result = 17;
result = 37 * result + synapseName.hashCode();
long l = Double.doubleToLongBits(value);
result = 37 * result + (int) (l ^ (l >>> 32));
return result;
}
@Override
public String toString() {
return String.format("(%s: %s)", synapseName, value);
}
}
/**
* Static access to {@link NeuronBuilder} instance
* @return new instance of {@link NeuronBuilder}
*/
public static NeuronBuilder neuron() {
return new NeuronBuilder();
}
/**
* A builder for creating {@link NeoFuzzyNeuron} instances. There are two ways to use
* Bulder API:
*
*
1. Use Builder with internal {@link SynapseBuilder} out of the box. Example:
*
{@code
*
* NeoFuzzyNeuron catsDinner = neuron().withVariable("cats")
* .hasRange(0, 20)
* .hasRulesCount(10)
* .and().withVariable("fishes")
* .hasRange(0, 40)
* .hasRulesCount(15)
* .build();}
*
* 2. Or another approach is to use {@link SynapseBuilder} separately. Example:
*
{@code
*
* NeoFuzzyNeuron passengers = neuron().withVariable(synapse("passengers")
* .withRange(0, 40)
* .withRulesCount(20)
* .build()).build();}
*
* Both approaches are equivalent. Please see {@link SynapseBuilder} for details.
*/
public static class NeuronBuilder {
protected Map synapses = new HashMap<>();
/**
* To prevent direct Builder instantiation
*/
protected NeuronBuilder() {
}
/**
* Uses {@link SynapseBuilder} to construct new {@link Synapse} via {@link SynapseBuilderWrapper}
* @param synapseName for new {@link Synapse}, must not be null
* @return new {@link SynapseBuilderWrapper} instance
* @throws NullPointerException if synapseName is null
* @throws IllegalArgumentException if such Synapse with same name already created
*/
public SynapseBuilderWrapper withVariable(String synapseName) {
if (synapseName == null) {
throw new NullPointerException("Synapse name must not be null");
}
if (synapses.containsKey(synapseName.toLowerCase())) {
throw new IllegalArgumentException(String.format("Synapse with name [%s] is already defined", synapseName));
}
return new SynapseBuilderWrapper(new SynapseBuilder(synapseName));
}
/**
* Uses {@link Synapse} instance directly to construct new {@link Synapse}
* @param synapse instance that is fully constructed and ready to go, must not be null
* @return same {@link NeuronBuilder} instance
* @throws NullPointerException if synapse is null
*/
public NeuronBuilder withVariable(Synapse synapse) {
if (synapse == null) {
throw new NullPointerException("Synapse instance must not be null");
}
synapses.put(synapse.name.toLowerCase(), synapse);
return this;
}
/**
* Just for fluent needs
* @return same {@link NeuronBuilder} instance
*/
public NeuronBuilder and() {
return this;
}
/**
* Builds new {@link NeoFuzzyNeuron} instances according to specified data
* @return new {@link NeoFuzzyNeuron} instance
* @throws IllegalStateException if there is no Synapses at all
*/
public NeoFuzzyNeuron build() {
if (synapses.isEmpty()) {
throw new IllegalStateException("You have to specify at least one Synapse. " +
"See NeuronBuilder.withVariable() method");
}
return new NeoFuzzyNeuron(synapses);
}
/**
* Simple wrapper for {@link SynapseBuilder} instance. Nothing special
*/
public class SynapseBuilderWrapper {
private SynapseBuilder synapseBuilder;
protected SynapseBuilderWrapper(SynapseBuilder synapseBuilder) {
this.synapseBuilder = synapseBuilder;
}
public SynapseBuilderWrapper hasRange(double lower, double upper) {
synapseBuilder.withRange(lower, upper);
return this;
}
public NeuronBuilder hasRulesCount(int count) {
withVariable(synapseBuilder.withRulesCount(count).build());
return NeuronBuilder.this;
}
}
}
/**
* Quick access to {@link Synapse} for a given {@link Input}. For internal use only
* @param input for Neo-Fuzzy-Neuron, must not be null
* @return {@link Synapse} that corresponds to the {@link Input}
* @throws NullPointerException in case of null {@link Input}
* @throws SynapseNameNotFoundException if no such Synapse is defined
*/
protected Synapse synapseFor(Input input) {
if (input == null) {
throw new NullPointerException("Input must not be null");
}
String synapseName = input.synapseName.toLowerCase();
if (!synapses.containsKey(synapseName)) {
throw new SynapseNameNotFoundException(synapseName);
}
return synapses.get(synapseName);
}
/**
* Checks that {@link Input}[] has correct dimension according to number of {@link Synapse}s.
* For internal use only
* @param inputs of teh Neo-Fuzzy-Neuron, must not be null
* @return the same {@link Input}[] array instance after checking
* @throws NullPointerException in case of null {@link Input}[]
* @throws NeuronInputDimensionException if input dimension is not corresponded
*/
protected Input[] checkInputDimension(Input[] inputs) {
if (inputs == null) {
throw new NullPointerException("Input[] must not be null");
}
if (inputs.length != synapses.size()) {
throw new NeuronInputDimensionException(inputs.length, synapses.size());
}
return inputs;
}
protected static class SynapseNameNotFoundException extends RuntimeException {
public SynapseNameNotFoundException(String synapseName) {
super(String.format("Synapse with name [%s] was not found", synapseName));
}
}
protected static class NeuronInputDimensionException extends RuntimeException {
public NeuronInputDimensionException(int inputDimension, int numberOfSynapses) {
super(String.format("Current Input dimension [%s] does not correspond " +
"to synapse number [%s]", inputDimension, numberOfSynapses));
}
}
@Override
public boolean equals(Object obj) {
return obj instanceof NeoFuzzyNeuron &&
synapses.equals(NeoFuzzyNeuron.class.cast(obj).synapses);
}
@Override
public int hashCode() {
int result = 17;
for (String key : synapses.keySet())
result = 37 * result + key.hashCode();
for (Synapse synapse : synapses.values())
result = 37 * result + synapse.hashCode();
return result;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder("Neo-Fuzzy-Neuron:\n");
for (Synapse synapse : synapses.values()) {
sb.append(synapse);
}
return sb.toString();
}
}