/*
 *    Learner.java
 *    Copyright (C) 2012-2013 Aristotle University of Thessaloniki, Greece
 */
package dbn.classifier.neural;

import dbn.convert.Converter;
import mulan.classifier.MultiLabelLearnerBase;
import mulan.classifier.MultiLabelOutput;
import weka.core.Instances;

/**
 * An abstract control class for {@link DeepBeliefNetwork}. Trains the neural 
 * network and can make prediction. it has only the necessary fields and 
 * some getter and setter methods. The implementation of this class is 
 * the class {@link DBLearner}.
 * 
 * @author Evripidis Gkanias
 * @see DeepBeliefNetwork
 * @see mulan.classifier.MultiLabelLearnerBase
 */
public abstract class Learner extends MultiLabelLearnerBase {
    /**
     * Informs whether the DBN is set.<br />
     * <code>true</code>: the neural network has been set, so you can train or get it<br />
     * <code>false</code>: the neural network has not been set and if you
     * try to train or get it you will receive a {@link NullPointerException}
     */
    private boolean _isDBNSet;
    
    /** 
     * The number of back-propagation epochs. This is the number of the training iterations. The
     * training set will pass through the classifier <code>_bpEpochs</code> times
     * for better performance during the supervised training task.
     */
    protected int _bpEpochs;
    
    /** 
     * The number of epochs per RBM. This field specifies how many iterations will
     * the {@link com.syvys.jaRBM.RBMImpl} do during the un-supervised training task.
     */
    protected int _greedyEpochs;
    
    /**
     * This field is used to convert the output (double array) of the neural network 
     * in a bipartition array. <br />
     * If the value of a label is more than the value threshold, the corresponding
     * bipartition layer will have positive value<br />
     * On the other hand, if the value of the label is less than the value of the
     * threshold, the corresponding bipartition layer will have negative value
     */
    protected double _threshold;
    
    /**
     * The {@link DeepBeliefNetwork} that the Learner will train. When the neural
     * network will be trained, user can get it with the <code>getLearnedDBN()</code>
     * function.
     */
    protected DeepBeliefNetwork _nn;

    /**
     * The constructor. Initializes the default values for some necessary fields.
     */
    public Learner() {
        super();
        this._bpEpochs = 10;
        this._greedyEpochs = 100;
        this._threshold = -1;
        this._isDBNSet = false;
    }
    
    // <editor-fold defaultstate="collapsed" desc="Setters and Getters">
    /**
     * Sets the {@link DeepBeliefNetwork}. If the networks is null, it throws an
     * exception.
     * @throws {@link NullPointerException} when the dbn is null
     * @param dbn 
     */
    public void setDBN(DeepBeliefNetwork dbn) {
        if (dbn == null) throw new NullPointerException();
        this._nn = dbn;
        this._isDBNSet = true;
    }

    /**
     * Sets the number of training epochs. Must be greater than 0.<br />
     * Default value is 10.
     *
     * @param epochs the number of training epochs
     * @throws IllegalArgumentException if passed value is invalid
     */
    public void setBPEpochs(int epochs) {
        if (epochs <= 0) {
            throw new IllegalArgumentException("The number of training epochs "
                    + "must be greater than zero. " +
                    "Entered value is : " + epochs);
        }
        this._bpEpochs = epochs;
    }

    /**
     * Sets the number of training epochs for every {@link com.syvys.jaRBM.RBM} during the 
     * un-supervised learning. Must be greater than 0.<br />
     * Default value is 100.
     *
     * @param epochsPerLayer the number of training epochs per {@link com.syvys.jaRBM.RBM}
     * @throws IllegalArgumentException if passed value is invalid
     */
    public void setPreTrainingEpochs(int epochsPerLayer) {
        if (epochsPerLayer <= 0) {
            throw new IllegalArgumentException("The number of training epochs "
                    + " for every RBM must be greater than zero. " +
                    "Entered value is : " + epochsPerLayer);
        }
        this._greedyEpochs = epochsPerLayer;
    }

    /**
     * Sets the momentum value that specifies the training urge. Must be greater 
     * than 0 and no more than 1.<br />
     * Default value is 0.6.
     * 
     * @param momentum the momentum
     * @throws IllegalArgumentException if passed value is invalid
     */
    public void setMomentum(double momentum) {
        if (momentum <= 0 || momentum > 1) {
            throw new IllegalArgumentException("The momentum must be greater than 0 and no more than 1. " +
                    "Entered value is : " + momentum);
        }
        for (int i = 0; i < _nn.getNumRBMs(); i++) {
            this._nn.getRBM(i).setMomentum(momentum);
        }
    }

    /**
     * Sets the learning rate. Must be greater than 0 and no more than 1.<br />
     * Default value is 0.05.
     *
     * @param learningRate the learning rate
     * @throws IllegalArgumentException if passed value is invalid
     */
    public void setLearningRate(double learningRate) {
        if (learningRate <= 0 || learningRate > 1) {
            throw new IllegalArgumentException("The learning rate must be greater than 0 and no more than 1. " +
                    "Entered value is : " + learningRate);
        }
        for (int i = 0; i < _nn.getNumRBMs(); i++) {
            this._nn.getRBM(i).setLearningRate(learningRate);
        }
    }
    
    /**
     * Sets the regularization cost term for weights decay.
     * Must be greater than 0 and no more than 1.<br />
     * Default value is 0.00001.
     *
     * @param weightCost the weights decay cost term
     * @throws IllegalArgumentException if passed value is invalid
     */
    public void setWeightCost(double weightCost) {
        if (weightCost <= 0 || weightCost > 1) {
            throw new IllegalArgumentException("The weights decay regularization cost " +
                    "term must be greater than 0 and no more than 1. " +
                    "The passed  value is : " + weightCost);
        }
        for (int i = 0; i < _nn.getNumRBMs(); i++) {
            this._nn.getRBM(i).setWeightCost(weightCost);
        }
    }
    
    /**
     * Sets the threshold that is used for the bipartition. Must be greater than
     * 0 and no more than 1.<br />
     * Default value is -1.
     * 
     * @param threshold the threshold
     * @throws IllegalArgumentException if passed value is invalid
     */
    public void setThreshold(double threshold) {
        if (threshold <= 0 || threshold > 1) {
            throw new IllegalArgumentException("The threshold " +
                    "must be greater than 0 and no more than 1. " +
                    "The passed  value is : " + threshold);
        }
        this._threshold = threshold;
        Converter.SetThreshold(threshold);
    }

    /**
     * Gets number of training epochs.
     * Default value is 20.
     * @return training epochs
     */
    public int getEpochs() {
        return this._bpEpochs;
    }

    /**
     * Gets the momentum. The default value is 0.6.
     * @return momentum
     */
    public double getMomentum() {
        return this._nn.getRBM(0).getMomentum();
    }

    /**
     * Gets the learning rate. The default value is 0.05.
     * @return learning rate
     */
    public double getLearningRate() {
        return this._nn.getRBM(0).getLearningRate();
    }
    
    /**
     * Gets a value of the regularization cost term for weights decay.
     * @return regularization cost
     */
    public double getWeightCost() {
        return this._nn.getRBM(0).getWeightCost();
    }
    
    /**
     * Gets the threshold. The default value is -1.
     * 
     * @return threshold
     * @throws NullPointerException if threshold has not set
     */
    public double getThreshold() {
        if (this._threshold < 0) throw new NullPointerException("Threshold has not set!");
        
        return this._threshold;
    }
    
    /**
     * Informs if the neural network has been set.
     * @return <code>true</code> if the DBN has been set <br />
     *         <code>false</code> if the DBN has not been set
     */
    public boolean isSetNN() {
        return this._isDBNSet;
    }
    
    /**
     * Returns the {@link DeepBeliefNetwork}. It throws a 
     * {@link NullPointerException} when the neural network has not been set.
     * 
     * @return the learned {@link DeepBeliefNetwork}
     * @throws NullPointerException
     */
    public DeepBeliefNetwork getLeanedDBN() throws NullPointerException {
        if (!this.isSetNN()) throw new NullPointerException("The Deep Belief Network has not set!");
        
        return this._nn;
    }
    // </editor-fold>
    
    /**
     * This method uses the <code>makePrediction(Instance instnc)</code> method
     * to predict multiple {@link Instances} returning an array of 
     * {@link MultiLabelOutput}. <br />
     * It throws an {@link Exception} when a data format problem is occurred
     * 
     * @param instances the testing dataset
     * @return an array of {@link mulan.classifier.MultiLabelOutput} objects, one for each instance
     * @throws Exception 
     */
    public MultiLabelOutput[] makePrediction(Instances instances) throws Exception {
        MultiLabelOutput[] mlo = new MultiLabelOutput[instances.numInstances()];
        
        for (int i = 0; i < instances.numInstances(); i++) {
            mlo[i] = makePrediction(instances.instance(i));
        }
        
        return mlo;
    }
}
