手把手教你使用Java實現一個神經網路
- 2019 年 10 月 3 日
- 筆記
首先看一下運行效果:
下面是項目整體目錄:
0.實現神經網路總覽
神經網路由層、神經元、權重、激活函數和偏置組成。每層都有一個或者多個神經元,每一個神經元都和神經輸入/輸出連接,這些連接就是權重。
需要重點強調一下,一個神經網路可能有很多隱含層,也可能一個沒有,因為每層的神經元數目也可能不同。然而,輸入輸出層的神經元個數分別等於神經輸入/輸出的個數。
我們為了實現,需要定義以下的類:
- Neuron: 定義人工神經元
- NeuralLayer: 抽象類,定義一個神經元層。
- InputLayer: 定義神經輸入層
- HiddenLayer:定義輸入層和輸出層之間的層
- OutputLayer: 定義神經輸出層。
- InputNeuron: 定義神經網路輸入中出現的神經元。
- NeuralNet:將前面定義的所有類組成一個ANN結構。
除了以上的類,我們還需要為激活函數定義一個IActivationFunction介面。這是必要的,因為激活函數與方法類似,需要作為神經元的一個屬性進行分配。所以要為激活函數定義類,這些類需要實現IActivationFunction介面:
- Linear
- Sigmoid
- Step
- HyperTan
第一章的編碼基本完成。除此之外,還需要定義倆個類。一個用於異常處理(NeuralException),另一個用於產生隨機數(RandomNumberGenerator)。最後,將這些類分別放到倆個包。
1.神經元Neuron類
神經元類是本章程式碼的基礎類。根據理論,人工神經元有如下屬性
- 輸入
- 權重
- 偏置
- 激活函數
- 輸出
首先定義神經元的各種屬性:
public class Neuron { //神經元相關的權重 protected ArrayList<Double> weight; //神經元的輸入 private ArrayList<Double> input; //這個神經元的輸出,由激活函數產生 private Double output; //傳遞給激活函數的值 private Double outputBeforeActivation; //輸入的數量。如果為0,則表示神經元尚未初始化。 private int numberOfInputs = 0; //神經元的偏差。除了第一層,其他都應該是1.0。 protected Double bias = 1.0; //神經元的激活函數 private IActivationFunction activationFunction; }
當實例化神經元時,需要指定輸入數據的個數以及激活函數。構造函數如下:
public Neuron(int numberofinputs,IActivationFunction iaf){ numberOfInputs=numberofinputs; weight=new ArrayList<>(numberofinputs+1); input=new ArrayList<>(numberofinputs); activationFunction=iaf; }
注意,為偏置定義另一個權重。一個重要的步驟是初始化神經元,也就是為權重賦初始值。這主要在init()方法中完成,通過隨機數生成器靜態類RandomNumberGenerator生成隨機數,賦值權重。注意:設置權重值時需要防止權重數組越界。
public void init(){ if(numberOfInputs>0){ for(int i=0;i<=numberOfInputs;i++){ double newWeight = RandomNumberGenerator.GenerateNext(); try{ this.weight.set(i, newWeight); } catch(IndexOutOfBoundsException iobe){ this.weight.add(newWeight); } } } }
最後,在calc()方法中計算輸出值:
public void calc(){ outputBeforeActivation=0.0; if(numberOfInputs>0){ if(input!=null && weight!=null){ for(int i=0;i<=numberOfInputs;i++){ outputBeforeActivation+=(i==numberOfInputs?bias:input.get(i))*weight.get(i); } } } output=activationFunction.calc(outputBeforeActivation); }
首先需要對所有輸入和權重的成績進行求和(偏置乘最後一個權重,i==Number-OfInputs),然後將得出的結果保存在屬性outputBeforeActivation中。激活函數用這個值計算神經元的輸出。
總程式碼如下:
package neuralnet; import java.util.ArrayList; public class Neuron { //神經元相關的權重 protected ArrayList<Double> weight; //神經元的輸入 private ArrayList<Double> input; //這個神經元的輸出,由激活函數產生 private Double output; //傳遞給激活函數的值 private Double outputBeforeActivation; //輸入的數量。如果為0,則表示神經元尚未初始化。 private int numberOfInputs = 0; //神經元的偏差。除了第一層,其他都應該是1.0。 protected Double bias = 1.0; //神經元的激活函數 private IActivationFunction activationFunction; public Neuron(){ } public Neuron(int numberofinputs){ numberOfInputs=numberofinputs; weight=new ArrayList<>(numberofinputs+1); input=new ArrayList<>(numberofinputs); } public Neuron(int numberofinputs,IActivationFunction iaf){ numberOfInputs=numberofinputs; weight=new ArrayList<>(numberofinputs+1); input=new ArrayList<>(numberofinputs); activationFunction=iaf; } public void init(){ if(numberOfInputs>0){ for(int i=0;i<=numberOfInputs;i++){ double newWeight = RandomNumberGenerator.GenerateNext(); try{ this.weight.set(i, newWeight); } catch(IndexOutOfBoundsException iobe){ this.weight.add(newWeight); } } } } public void setInputs(double [] values){ if(values.length==numberOfInputs){ for(int i=0;i<numberOfInputs;i++){ try{ input.set(i, values[i]); } catch(IndexOutOfBoundsException iobe){ input.add(values[i]); } } } } public void setInputs(ArrayList<Double> values){ if(values.size()==numberOfInputs){ input=values; } } public ArrayList<Double> getArrayInputs(){ return input; } public double[] getInputs(){ double[] inputs = new double[numberOfInputs]; for (int i=0;i<numberOfInputs;i++){ inputs[i]=this.input.get(i); } return inputs; } public void setInput(int i,double value){ if(i>=0 && i<numberOfInputs){ try{ input.set(i, value); } catch(IndexOutOfBoundsException iobe){ input.add(value); } } } public double getInput(int i){ return input.get(i); } public double[] getWeights(){ double[] weights = new double[numberOfInputs+1]; for(int i=0;i<=numberOfInputs;i++){ weights[i]=weight.get(i); } return weights; } public ArrayList<Double> getArrayWeights(){ return weight; } public void updateWeight(int i, double value){ if(i>=0 && i<=numberOfInputs){ weight.set(i, value); } } public int getNumberOfInputs(){ return this.numberOfInputs; } public void setWeight(int i,double value) throws NeuralException{ if(i>=0 && i<numberOfInputs){ this.weight.set(i, value); } else{ throw new NeuralException("Invalid weight index"); } } public double getOutput(){ return output; } public void calc(){ outputBeforeActivation=0.0; if(numberOfInputs>0){ if(input!=null && weight!=null){ for(int i=0;i<=numberOfInputs;i++){ outputBeforeActivation+=(i==numberOfInputs?bias:input.get(i))*weight.get(i); } } } output=activationFunction.calc(outputBeforeActivation); } public void setActivationFunction(IActivationFunction iaf){ this.activationFunction=iaf; } public double getOutputBeforeActivation(){ return outputBeforeActivation; } }
2.NeuralLayer類
在這個類中,將把在同一層中對齊的神經元分成一組。因為一層需要將值傳遞給另一層,也需要定義層與層之間的連接。類的屬性定義如下:
//這一層的神經元數量 protected int numberOfNeuronsInLayer; //這一層的神經元 private ArrayList<Neuron> neuron; //激勵函數 protected IActivationFunction activationFnc; //將值提供給此層的前一層 protected NeuralLayer previousLayer; protected NeuralLayer nextLayer; protected ArrayList<Double> input; protected ArrayList<Double> output; protected int numberOfInputs;
這個類是抽象的,整整可實例化的層類是InputLayer、HiddenLayer和Outp-utLayer。創建一個類是,必須使用另一個類的構造函數,這幾個類具有相似的構造函數。
而層的初始化和計算都和神經元一樣,他們也實現了init()方法和calc() 方法。生命欸protected類型,確保了只有子類可以調用或覆蓋這些方法。
全部的程式碼如下:
package neuralnet; import java.util.ArrayList; public abstract class NeuralLayer { //這一層的神經元數量 protected int numberOfNeuronsInLayer; //這一層的神經元 private ArrayList<Neuron> neuron; //激勵函數 protected IActivationFunction activationFnc; //將值提供給此層的前一層 protected NeuralLayer previousLayer; protected NeuralLayer nextLayer; protected ArrayList<Double> input; protected ArrayList<Double> output; protected int numberOfInputs; public NeuralLayer(int numberofneurons){ this.numberOfNeuronsInLayer=numberofneurons; neuron = new ArrayList<>(numberofneurons); output = new ArrayList<>(numberofneurons); } public NeuralLayer(int numberofneurons,IActivationFunction iaf){ this.numberOfNeuronsInLayer=numberofneurons; this.activationFnc=iaf; neuron = new ArrayList<>(numberofneurons); output = new ArrayList<>(numberofneurons); } public int getNumberOfNeuronsInLayer(){ return numberOfNeuronsInLayer; } public ArrayList<Neuron> getListOfNeurons(){ return neuron; } protected NeuralLayer getPreviousLayer(){ return previousLayer; } protected NeuralLayer getNextLayer(){ return nextLayer; } protected void setPreviousLayer(NeuralLayer layer){ previousLayer=layer; } protected void setNextLayer(NeuralLayer layer){ nextLayer=layer; } protected void init(){ if(numberOfNeuronsInLayer>=0){ for(int i=0;i<numberOfNeuronsInLayer;i++){ try{ neuron.get(i).setActivationFunction(activationFnc); neuron.get(i).init(); } catch(IndexOutOfBoundsException iobe){ neuron.add(new Neuron(numberOfInputs,activationFnc)); neuron.get(i).init(); } } } } protected void setInputs(ArrayList<Double> inputs){ this.numberOfInputs=inputs.size(); this.input=inputs; } protected void calc(){ if(input!=null && neuron!=null){ for(int i=0;i<numberOfNeuronsInLayer;i++){ neuron.get(i).setInputs(this.input); neuron.get(i).calc(); try{ output.set(i,neuron.get(i).getOutput()); } catch(IndexOutOfBoundsException iobe){ output.add(neuron.get(i).getOutput()); } } } } protected ArrayList<Double> getOutputs(){ return output; } protected Neuron getNeuron(int i){ return neuron.get(i); } protected void setNeuron(int i, Neuron _neuron){ try{ this.neuron.set(i, _neuron); } catch(IndexOutOfBoundsException iobe){ this.neuron.add(_neuron); } } }
3.ActivationFunction介面
在定義NeerualNetwork類之前,先看介面的Java程式碼示例:
public interface IActivationFunction { double calc(double x); public enum ActivationFunctionENUM { STEP, LINEAR, SIGMOID, HYPERTAN } }
其中calc()方法屬於實現IActivationFunction介面的特定的激活函數類,例如Sigmoid函數。
public class Sigmoid implements IActivationFunction { private double a=1.0; public Sigmoid(){ } public Sigmoid(double value){ this.setA(value); } public void setA(double value){ this.a=value; } @Override public double calc(double x){ return 1.0/(1.0+Math.exp(-a*x)); } }
這也是多態性的一個示例,即在相同的函數名下,類和方法呈現不同的行為,產生靈活的應用。
4.神經網路(NeuralNet)類
最後,定義神經網路類。到目前為止,我們已經知道,神經網路在神經層中組織神經元,且每個神經網路至少有倆層,一個用來接收收入,一個用來處理輸出,還有一個數量可變的隱含層。因此,除了具有和神經元以及NeuralLary類相似的屬性之外,Neural還將擁有這幾個屬性,如numberOfInputs,numberOfOutputs等。
private InputLayer inputLayer; private ArrayList<HiddenLayer> hiddenLayer; private OutputLayer outputLayer; private int numberOfHiddenLayers; private int numberOfInputs; private int numberOfOutputs; private ArrayList<Double> input; private ArrayList<Double> output;
這個類的構造函數比前面類的參數更多:
public NeuralNet(int numberofinputs,int numberofoutputs, int [] numberofhiddenneurons,IActivationFunction[] hiddenAcFnc, IActivationFunction outputAcFnc)
如果隱含層的數量是可變的,我們還應該考慮到可能有多個隱含層或0個隱含層,且對每個隱含層來說,隱藏神經元的數量也是可變的。處理這種可變性的最好方法就是把每個隱含層中的神經元數量表示為一個整數向量(參數 numberofhiddenlayers)。此外,需要為每個隱含層定義激活函數,包括輸出層,完成這個目標所需要的參數分別為hiddenActivationFnc和outputAcFnc。
完整實現如下:
public NeuralNet(int numberofinputs,int numberofoutputs, int [] numberofhiddenneurons,IActivationFunction[] hiddenAcFnc, IActivationFunction outputAcFnc){ numberOfHiddenLayers=numberofhiddenneurons.length; numberOfInputs=numberofinputs; numberOfOutputs=numberofoutputs; if(numberOfHiddenLayers==hiddenAcFnc.length){ input=new ArrayList<>(numberofinputs); inputLayer=new InputLayer(numberofinputs); if(numberOfHiddenLayers>0){ hiddenLayer=new ArrayList<>(numberOfHiddenLayers); } for(int i=0;i<numberOfHiddenLayers;i++){ if(i==0){ try{ hiddenLayer.set(i,new HiddenLayer(numberofhiddenneurons[i], hiddenAcFnc[i], inputLayer.getNumberOfNeuronsInLayer())); } catch(IndexOutOfBoundsException iobe){ hiddenLayer.add(new HiddenLayer(numberofhiddenneurons[i], hiddenAcFnc[i], inputLayer.getNumberOfNeuronsInLayer())); } inputLayer.setNextLayer(hiddenLayer.get(i)); } else{ try{ hiddenLayer.set(i, new HiddenLayer(numberofhiddenneurons[i], hiddenAcFnc[i],hiddenLayer.get(i-1) .getNumberOfNeuronsInLayer() )); } catch(IndexOutOfBoundsException iobe){ hiddenLayer.add(new HiddenLayer(numberofhiddenneurons[i], hiddenAcFnc[i],hiddenLayer.get(i-1) .getNumberOfNeuronsInLayer() )); } hiddenLayer.get(i-1).setNextLayer(hiddenLayer.get(i)); } } if(numberOfHiddenLayers>0){ outputLayer=new OutputLayer(numberofoutputs,outputAcFnc, hiddenLayer.get(numberOfHiddenLayers-1) .getNumberOfNeuronsInLayer() ); hiddenLayer.get(numberOfHiddenLayers-1).setNextLayer(outputLayer); } else{ outputLayer=new OutputLayer(numberofinputs, outputAcFnc, numberofoutputs); inputLayer.setNextLayer(outputLayer); } } }
5.運行程式
程式碼如下:
package neuralnet; import neuralnet.math.IActivationFunction; import neuralnet.math.Linear; import neuralnet.math.RandomNumberGenerator; import neuralnet.math.Sigmoid; public class NeuralNetConsoleTest { public static void main(String[] args){ RandomNumberGenerator.seed=0; int numberOfInputs=2; int numberOfOutputs=1; int[] numberOfHiddenNeurons= { 3 }; IActivationFunction[] hiddenAcFnc = { new Sigmoid(1.0) } ; Linear outputAcFnc = new Linear(1.0); System.out.println("Creating Neural Netword..."); NeuralNet nn = new NeuralNet(numberOfInputs,numberOfOutputs, numberOfHiddenNeurons,hiddenAcFnc,outputAcFnc); System.out.println("Neural Network Network..."); double [] neuralInput = { 1.5 , 0.5 }; System.out.println("Feeding the values {1.5;0.5} to the neural network"); double [] neuralOutput; nn.setInputs(neuralInput); nn.calc(); neuralOutput=nn.getOutputs(); System.out.println("OutPut 1:" + neuralOutput[0]); neuralInput[0] = 1.0; neuralInput[1] = 2.1; System.out.println("Feeding the values {1.0;2.1} to the neural network"); nn.setInputs(neuralInput); nn.calc(); neuralOutput=nn.getOutputs(); System.out.println("OutPut 2:" + neuralOutput[0]); } }
到此就完成了我們神經網路的全部程式碼:下面是源程式碼壓縮包。有需要的同學可以下載運行。