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

import com.syvys.jaRBM.IO.BatchDatasourceReaderImpl;
import com.syvys.jaRBM.RBMNetLearn.BackpropLearner;
import com.syvys.jaRBM.RBMNetLearn.GreedyLearner;
import com.syvys.jaRBM.RBMNetLearn.StochasticMetaDescentLearner;
import dbn.convert.Converter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import mulan.classifier.InvalidDataException;
import mulan.classifier.MultiLabelOutput;
import mulan.data.MultiLabelInstances;
import weka.core.Instance;
import weka.core.TechnicalInformation;

/**
 * An implementation of {@link Learner} that trains a {@link DeepBeliefNetwork}.
 * This control class implements all the abstract methods of {@link Learner} and 
 * {@link mulan.classifier.MultiLabelLearnerBase}. It has all the important tasks, like building the
 * neural network and predicting multi-label output from a dataset.
 * 
 * @author Evripidis Gkanias
 * @see Learner
 * @see DeepBeliefNetwork
 */
public class DBLearner extends Learner {
    private boolean isBuilt = false;
    
    /**
     * This method builds the neural network internal. It is used by the method
     * <code>build({@link mulan.data.MultiLabelInstances} mli)</code> for building the
     * classifier. It trains the {@link DeepBeliefNetwork} to predict data like
     * these on the parameter <code>mli</code>.
     * 
     * @param mli a {@link mulan.data.MultiLabelInstances} dataset that is used for the 
     * training task
     * @throws Exception 
     */
    @Override
    protected void buildInternal(MultiLabelInstances mli) throws Exception {
        if (!super.isSetNN()) throw new Exception("Deep Belief Network has not set!");
        //if (isBuilt) return;
        
        double[][] dataset = Converter.InstancesToDoubleMatrix(
                mli.getDataSet(),
                super.featureIndices = mli.getFeatureIndices());
        double[][] targets = Converter.InstancesToDoubleMatrix(
                mli.getDataSet(),
                super.labelIndices = mli.getLabelIndices());
        
        // Start training unsupervised (upward) the DBN
        this.buildUnsupervised(dataset, this.getLearningRate());
        
        // Start trainig supervised the (downward) DBN
        this.buildSupervised(dataset, targets, this.getLearningRate());
        
        isBuilt = true;
    }

    /**
     * This method builds the neural network in un-supervised. It is used by the method
     * <code>buildInternal({@link mulan.data.MultiLabelInstances} mli)</code> for building the
     * classifier. It trains the {@link DeepBeliefNetwork} to reduce the dimensionality
     * of the input pattern.
     * 
     * @param dataset the dataset with which the NN will be trained.
     * @param lr the un-supervised learners learning rate
     */
    protected void buildUnsupervised(double[][] dataset, double lr) {
        
        this.setLearningRate(lr);
        
        debug("");
        debug("Greedy learning:");
        GreedyLearner glearner = new GreedyLearner(
                super._nn,
                new BatchDatasourceReaderImpl(dataset),
                1);
        
        // Number of epochs per layer
        int epochsPerLayer = super._greedyEpochs;
        // Last RBM index to train
        int lastRBMIndex = super._nn.getNumRBMs() - 2;
        // Number of gibbs sampling iterations for CD
        int gibbsIterAmount = 0;
        // Every this number of epochs increases the gibbs sampling for each layer
        int gibbsEpochs = 100;
        // Maximum increase times of gibbs iterations
        int maxEpochsGibbs = 0;
        
        glearner.Learn(epochsPerLayer, lastRBMIndex,
                gibbsIterAmount, gibbsEpochs, maxEpochsGibbs);
        
        this._nn = (DeepBeliefNetwork) glearner.getLearnedRBMNet().clone();
        
    }
    
    /**
     * This method builds the neural network in supervised. It is used by the method
     * <code>buildInternal({@link MultiLabelInstances} mli)</code> for building the
     * classifier. It trains the {@link DeepBeliefNetwork} to predict multi-label
     * instances using the datasets labels.
     * 
     * @param dataset the dataset with which the NN will be trained.
     * @param targets the labels of the dataset
     * @param lr the supervised learners learning rate
     */
    protected void buildSupervised(double[][] dataset, double[][] targets, double lr) {
        
        this.setLearningRate(lr);
        
        BackpropLearner mylearner = new StochasticMetaDescentLearner(
                super._nn,
                new BatchDatasourceReaderImpl(dataset), 
                new BatchDatasourceReaderImpl(targets),
                1);
        
        debug("");
        debug("Back propagation learning:");
        for (int i = 0; i < super._bpEpochs; i++) {
            double error = mylearner.Learn();
            debug("Epoch " + (i+1) + ": Backprop training error (" + mylearner.getClass().getSimpleName() + ") = " + error);
            if (error < 0.0001) break;
        }
        
        this._nn = (DeepBeliefNetwork) mylearner.getLearnedRBMNet();
    }
    
    /**
     * This method is used to predict a {@link mulan.classifier.MultiLabelOutput} using the trained
     * {@link DeepBeliefNetwork}. It takes as input the data <code>instnc</code>
     * and give as output the {@link mulan.classifier.MultiLabelOutput} output.<br />
     * It causes two exceptions:<br />
     * {@link InvalidDataException} when the data are not the appropriate format
     * or null.<br />
     * {@link Exception} when another error is occurred.
     * 
     * @param inst the input vector
     * @return the labels in a {@link mulan.classifier.MultiLabelOutput} form
     * @throws Exception
     * @throws InvalidDataException 
     */
    @Override
    protected MultiLabelOutput makePredictionInternal(Instance inst) throws Exception, InvalidDataException {
        
        int numAttributes = inst.numAttributes();
        if (numAttributes < this._nn.getNetInputSize()) {
            throw new InvalidDataException("Input instance do not have enough "
                    + "attributes to be processed by the model. Instance is not "
                    + "consistent with the data the model was built for.");
        }
        
        // if instance has more attributes than model input, we assume that true outputs
        // are there, so we remove them
        List<Integer> someLabelIndices = new ArrayList();
        boolean labelsAreThere = false;
        if (numAttributes > this._nn.getNetInputSize()) {
            for (int index : this.labelIndices) {
                someLabelIndices.add(index);
            }

            labelsAreThere = true;
        }
        
        double[] inputPattern = new double[this._nn.getNetInputSize()];
        int indexCounter = 0;
        for (int attrIndex = 0; attrIndex < numAttributes; attrIndex++) {
            if (labelsAreThere && someLabelIndices.contains(attrIndex)) {
                continue;
            }
            inputPattern[indexCounter] = inst.value(attrIndex);
            indexCounter++;
        }
        
        boolean[] labelPredictions = new boolean[numLabels];
        Arrays.fill(labelPredictions, false);
        
        
        double[] labelConfidences = this._nn.feedForward(inputPattern);
        
        //*
        if (this._threshold > 0) {
            double max = 0;
            double min = labelConfidences[0];

            for (int i = 0; i < numLabels; i++) {
                if (labelConfidences[i] > max) max = labelConfidences[i];
                if (labelConfidences[i] < min) min = labelConfidences[i];
            }

            for (int i = 0; i < numLabels; i++) {

                // translate to probabilities
                // labelConfidences[i] = (labelConfidences[i] - min) / (max - min);

                if (this._threshold > 0) {
                    labelPredictions[i] = labelConfidences[i] >= this._threshold;
                }
            }
        }
        //*/
        
        MultiLabelOutput mlo;
        if (this._threshold > 0)
            mlo = new MultiLabelOutput(labelPredictions, labelConfidences);
        else
            mlo = new MultiLabelOutput(labelConfidences);
        
        return mlo;
    }
    
    @Override
    public TechnicalInformation getTechnicalInformation() {
        TechnicalInformation technicalInfo = new TechnicalInformation(TechnicalInformation.Type.UNPUBLISHED);
        technicalInfo.setValue(TechnicalInformation.Field.AUTHOR, "Gkanias E.");
        technicalInfo.setValue(TechnicalInformation.Field.YEAR, "2013");
        technicalInfo.setValue(TechnicalInformation.Field.COPYRIGHT, "2012-2013 Aristotle University of Thessaloniki, Greece");
        technicalInfo.setValue(TechnicalInformation.Field.SCHOOL, "Aristotle University of Thessaloniki");
        technicalInfo.setValue(TechnicalInformation.Field.TITLE, "Deep Belief Networks for Multi-Label Learning");
        technicalInfo.setValue(TechnicalInformation.Field.PAGES, "30-40");
        return technicalInfo;
    }

    @Override
    public String globalInfo() {
        return "The implementation of Deep Learning for Deep Belief Networks. "
                + "The learned model is stored in {@link DeepBeliefNetwork} "
                + "neural network.";
    }

}
