1
\$\begingroup\$

recently I've been working on machine learning with Java and Weka, and while I found the .ARFF format easy to understand (you just need an .ARFF file and Weka will do the rest :P), sometimes the process can be really annoying: You have some data, and after some processing you want to feed it to a Weka classifier. The thing is, the Weka classifiers only accept an Instances object as an input, and an Instances generally uses an ARFF file as an input.

But I really don't want to write the processed data onto an ARFF file just for that, so I've decided to find a way to create an Instances object without using the .ARFF format. However, here is what I found on the documentation: http://weka.sourceforge.net/doc.stable/weka/core/Instances.html enter image description here As you can see from the doc we have no easy way to initialize an Instances object without using the .ARFF format. But the strange thing is there is a file, CreateInstances.java, which is distributed along with the Weka package (go into the Weka folder, inside of which there is a wekaexamples.zip, you can find it in there). Here is its code:

import weka.core.Attribute;
import weka.core.DenseInstance;
import weka.core.Instances;

import java.util.ArrayList;

/**
 * Generates an weka.core.Instances object with different attribute types.
 *
 * @author FracPete (fracpete at waikato dot ac dot nz)
 * @version $Revision: 6054 $
 */
public class CreateInstances {

  /**
   * Generates the Instances object and outputs it in ARFF format to stdout.
   *
   * @param args    ignored
   * @throws Exception  if generation of instances fails
   */
  public static void main(String[] args) throws Exception {
    ArrayList<Attribute>    atts;
    ArrayList<Attribute>    attsRel;
    ArrayList<String>       attVals;
    ArrayList<String>       attValsRel;
    Instances           data;
    Instances           dataRel;
    double[]            vals;
    double[]            valsRel;
    int             i;

    // 1. set up attributes
    atts = new ArrayList<Attribute>();
    // - numeric
    atts.add(new Attribute("att1"));
    // - nominal
    attVals = new ArrayList<String>();
    for (i = 0; i < 5; i++)
      attVals.add("val" + (i+1));
    atts.add(new Attribute("att2", attVals));
    // - string
    atts.add(new Attribute("att3", (ArrayList<String>) null));
    // - date
    atts.add(new Attribute("att4", "yyyy-MM-dd"));
    // - relational
    attsRel = new ArrayList<Attribute>();
    // -- numeric
    attsRel.add(new Attribute("att5.1"));
    // -- nominal
    attValsRel = new ArrayList<String>();
    for (i = 0; i < 5; i++)
      attValsRel.add("val5." + (i+1));
    attsRel.add(new Attribute("att5.2", attValsRel));
    dataRel = new Instances("att5", attsRel, 0);
    atts.add(new Attribute("att5", dataRel, 0));

    // 2. create Instances object
    data = new Instances("MyRelation", atts, 0);

    // 3. fill with data
    // first instance
    vals = new double[data.numAttributes()];
    // - numeric
    vals[0] = Math.PI;
    // - nominal
    vals[1] = attVals.indexOf("val3");
    // - string
    vals[2] = data.attribute(2).addStringValue("This is a string!");
    // - date
    vals[3] = data.attribute(3).parseDate("2001-11-09");
    // - relational
    dataRel = new Instances(data.attribute(4).relation(), 0);
    // -- first instance
    valsRel = new double[2];
    valsRel[0] = Math.PI + 1;
    valsRel[1] = attValsRel.indexOf("val5.3");
    dataRel.add(new DenseInstance(1.0, valsRel));
    // -- second instance
    valsRel = new double[2];
    valsRel[0] = Math.PI + 2;
    valsRel[1] = attValsRel.indexOf("val5.2");
    dataRel.add(new DenseInstance(1.0, valsRel));
    vals[4] = data.attribute(4).addRelation(dataRel);
    // add
    data.add(new DenseInstance(1.0, vals));

    // second instance
    vals = new double[data.numAttributes()];  // important: needs NEW array!
    // - numeric
    vals[0] = Math.E;
    // - nominal
    vals[1] = attVals.indexOf("val1");
    // - string
    vals[2] = data.attribute(2).addStringValue("And another one!");
    // - date
    vals[3] = data.attribute(3).parseDate("2000-12-01");
    // - relational
    dataRel = new Instances(data.attribute(4).relation(), 0);
    // -- first instance
    valsRel = new double[2];
    valsRel[0] = Math.E + 1;
    valsRel[1] = attValsRel.indexOf("val5.4");
    dataRel.add(new DenseInstance(1.0, valsRel));
    // -- second instance
    valsRel = new double[2];
    valsRel[0] = Math.E + 2;
    valsRel[1] = attValsRel.indexOf("val5.1");
    dataRel.add(new DenseInstance(1.0, valsRel));
    vals[4] = data.attribute(4).addRelation(dataRel);
    // add
    data.add(new DenseInstance(1.0, vals));

    // 4. output data
    System.out.println("Cu lac gion tan! ");
    System.out.println(data);
  }
}

Obviously an Instances object can be constructed from an Attribute object, yet this is not mentioned in the doc at all! So I've decided to write some code to convert the data (in double[][] format) to a Weka-readable one (which is an Instances object). Then I fed it to a simple classifier. Here is my code:

import java.util.ArrayList;
import java.util.Arrays;
import matlabcontrol.MatlabProxy;
import matlabcontrol.MatlabProxyFactory;
import matlabcontrol.MatlabProxyFactoryOptions;
import weka.classifiers.Classifier;
import weka.classifiers.Evaluation;
import weka.classifiers.evaluation.NominalPrediction;
import weka.core.Attribute;
import weka.core.DenseInstance;
import weka.core.Instances;
import weka.classifiers.trees.J48;
import weka.core.FastVector;

/**
 * A simple program to convert custom data into a Weka-readable format (which is an Instances object)
 * without using the ArffLoader class (otherwise you would need to write your data to an .arff file, then read it again) 
 * @author Dang Manh Truong (dangmanhtruong@gmail.com)
 */
public class WekaClassificationWithoutArff {
    public static int[] makeUnique(int[] values){
        int[] new_arr = Arrays.stream(values).distinct().toArray();
        Arrays.sort(new_arr);
        return new_arr;
    }

    /**
     * Load data file using MATLAB's functionalities (the right tool for the right job :P)  
     * via a MatlabProxy (which must be connected beforehand)
     * Inspired by https://code.google.com/archive/p/matlabcontrol/wikis/Walkthrough.wiki
     * @param proxy
     * @param file_name    
     * @return data_mat
     */        
    public static double[][] load_data_using_matlab(MatlabProxy proxy, String file_name){
        try{           
            if (proxy.isConnected() == false){
                throw new java.lang.Exception("In load_data_using_matlab: No connection with MATLAB found ");
            }           

            Object[] out = proxy.returningFeval("load_data_in_matlab",4,file_name); 
            int is_error = (int) (((double[])out[3])[0]);
            if (is_error == 1){
                throw new java.lang.Exception("In load_data_using_matlab: Could not complete operation ");
            }
            int row_num = (int) (((double[])out[0])[0]); 
            int col_num = (int) (((double[])out[1])[0]);
            double[] one_dim_mat = (double[]) out[2];            
            double[][] data_mat = new double[row_num][col_num];
            for (int j = 0; j < col_num; j++){
                for (int i = 0; i < row_num; i++){
                    data_mat[i][j] = one_dim_mat[j * row_num + i];
                }                
            }
            return data_mat;                    
        }
        catch (Exception e){
            System.err.println(e.getMessage());
            return null;
        }
    }   

    public static double calculateAccuracy(FastVector predictions) {
        double correct = 0;

        for (int i = 0; i < predictions.size(); i++) {
            NominalPrediction np = (NominalPrediction) predictions.elementAt(i);
            if (np.predicted() == np.actual()) {
                correct++;
            }
        }

        return 100 * correct / predictions.size();
    }

    public static Instances[][] crossValidationSplit(Instances data, int numberOfFolds) {
        Instances[][] split = new Instances[2][numberOfFolds];

        for (int i = 0; i < numberOfFolds; i++) {
            split[0][i] = data.trainCV(numberOfFolds, i);
            split[1][i] = data.testCV(numberOfFolds, i);
        }

        return split;
    }

    public static Evaluation simpleClassify(Classifier model, Instances trainingSet, Instances testingSet) throws Exception {
        Evaluation validation = new Evaluation(trainingSet);

        model.buildClassifier(trainingSet);
        validation.evaluateModel(model, testingSet);

        return validation;
    }    

    public static void main(String[] args) throws Exception{
        String file_name = "iris";
                // Connect to MATLAB
        MatlabProxyFactoryOptions options = new MatlabProxyFactoryOptions.Builder().setUsePreviouslyControlledSession(true).build();
        MatlabProxyFactory factory = new MatlabProxyFactory(options);
        MatlabProxy proxy = factory.getProxy();        
        double[][] data = load_data_using_matlab(proxy,file_name);
        // Separate into attributes and class identifiers
        // Here for simplicity we assume that the classes are integers from 1 to N, where N is the number of classes
        int row_num = data.length;
        int col_num = data[0].length;
        double[][] X = new double[row_num][col_num - 1];
        int[] Y = new int[row_num];
        for (int row_idx = 0; row_idx < row_num; row_idx++){
            System.arraycopy(data[row_idx], 0, X[row_idx], 0, col_num - 1);
            Y[row_idx] = (int) data[row_idx][col_num - 1];
        }        
        int[] classes = makeUnique(Y);
        int num_of_classes = classes.length; 
        int num_of_observations = X.length;
        int num_of_attributes = X[0].length;

        // Now convert into weka-friendly instances
        // First we must create a list of attributes
        ArrayList<Attribute> atts = new ArrayList<>();
        for (int j = 1; j <= num_of_attributes; j++){
            atts.add(new Attribute("Numeric attribute " + Integer.toString(j)));
        }
        ArrayList<String> attVals = new ArrayList<>();
        for (int count = 1; count <= num_of_classes; count++){
            attVals.add("Class " + Integer.toString(count));
        }
        atts.add(new Attribute("Class attribute", attVals));

        // Create an instances object
        Instances data_in_weka_format = new Instances("Data", atts, 0);
        data_in_weka_format.setClassIndex(data_in_weka_format.numAttributes() - 1);

        // Now add data
        for (int i = 0; i < num_of_observations; i++){
            double[] data_row = new double[data_in_weka_format.numAttributes()];
            System.arraycopy(X[i], 0, data_row, 0, num_of_attributes);
            data_row[num_of_attributes] = attVals.indexOf("Class " + Integer.toString(Y[i]));
            data_in_weka_format.add(new DenseInstance(1.0, data_row));
        }

        // Let's perform a simple classification
        Classifier model = new J48();
        int num_of_folds = 10; // 10-folds cross-validation
        Instances[][] split = crossValidationSplit(data_in_weka_format,num_of_folds);// Choose a type of validation split
        Instances[] trainingSplits = split[0];
        Instances[] testingSplits  = split[1];

        // Collect every group of predictions for current model in a FastVector
        FastVector predictions = new FastVector();

        // For each training-testing split pair, train and test the classifier
        for(int i = 0; i < trainingSplits.length; i++) {
            Evaluation validation = simpleClassify(model, trainingSplits[i], testingSplits[i]);
            predictions.appendElements(validation.predictions());     

        }

        // Calculate overall accuracy of current classifier on all splits
        double accuracy = calculateAccuracy(predictions);

        // Print current classifier's name and accuracy in a complicated, but nice-looking way.
        System.out.println(model.getClass().getSimpleName() + ": " + String.format("%.2f%%", accuracy) + "\n=====================");
    }
}

I'd like to explain the main parts of the code - First, I read the data using Matlab using the matlabcontrol API: https://code.google.com/archive/p/matlabcontrol/wikis/Walkthrough.wiki . It seems inconvenient to read data from matlab, I know, but I've decided to do this because you need the right tool for the right job :), and Matlab is good at these kinds of things I read the iris dataset which can be downloaded online, but I've converted the classes into integers:

5.1,3.5,1.4,0.2,1
4.9,3.0,1.4,0.2,1
4.7,3.2,1.3,0.2,1
4.6,3.1,1.5,0.2,1
5.0,3.6,1.4,0.2,1
5.4,3.9,1.7,0.4,1
4.6,3.4,1.4,0.3,1
5.0,3.4,1.5,0.2,1
4.4,2.9,1.4,0.2,1
4.9,3.1,1.5,0.1,1
5.4,3.7,1.5,0.2,1
4.8,3.4,1.6,0.2,1
4.8,3.0,1.4,0.1,1
4.3,3.0,1.1,0.1,1
5.8,4.0,1.2,0.2,1
5.7,4.4,1.5,0.4,1
5.4,3.9,1.3,0.4,1
5.1,3.5,1.4,0.3,1
5.7,3.8,1.7,0.3,1
5.1,3.8,1.5,0.3,1
5.4,3.4,1.7,0.2,1
5.1,3.7,1.5,0.4,1
4.6,3.6,1.0,0.2,1
5.1,3.3,1.7,0.5,1
4.8,3.4,1.9,0.2,1
5.0,3.0,1.6,0.2,1
5.0,3.4,1.6,0.4,1
5.2,3.5,1.5,0.2,1
5.2,3.4,1.4,0.2,1
4.7,3.2,1.6,0.2,1
4.8,3.1,1.6,0.2,1
5.4,3.4,1.5,0.4,1
5.2,4.1,1.5,0.1,1
5.5,4.2,1.4,0.2,1
4.9,3.1,1.5,0.1,1
5.0,3.2,1.2,0.2,1
5.5,3.5,1.3,0.2,1
4.9,3.1,1.5,0.1,1
4.4,3.0,1.3,0.2,1
5.1,3.4,1.5,0.2,1
5.0,3.5,1.3,0.3,1
4.5,2.3,1.3,0.3,1
4.4,3.2,1.3,0.2,1
5.0,3.5,1.6,0.6,1
5.1,3.8,1.9,0.4,1
4.8,3.0,1.4,0.3,1
5.1,3.8,1.6,0.2,1
4.6,3.2,1.4,0.2,1
5.3,3.7,1.5,0.2,1
5.0,3.3,1.4,0.2,1
7.0,3.2,4.7,1.4,2
6.4,3.2,4.5,1.5,2
6.9,3.1,4.9,1.5,2
5.5,2.3,4.0,1.3,2
6.5,2.8,4.6,1.5,2
5.7,2.8,4.5,1.3,2
6.3,3.3,4.7,1.6,2
4.9,2.4,3.3,1.0,2
6.6,2.9,4.6,1.3,2
5.2,2.7,3.9,1.4,2
5.0,2.0,3.5,1.0,2
5.9,3.0,4.2,1.5,2
6.0,2.2,4.0,1.0,2
6.1,2.9,4.7,1.4,2
5.6,2.9,3.6,1.3,2
6.7,3.1,4.4,1.4,2
5.6,3.0,4.5,1.5,2
5.8,2.7,4.1,1.0,2
6.2,2.2,4.5,1.5,2
5.6,2.5,3.9,1.1,2
5.9,3.2,4.8,1.8,2
6.1,2.8,4.0,1.3,2
6.3,2.5,4.9,1.5,2
6.1,2.8,4.7,1.2,2
6.4,2.9,4.3,1.3,2
6.6,3.0,4.4,1.4,2
6.8,2.8,4.8,1.4,2
6.7,3.0,5.0,1.7,2
6.0,2.9,4.5,1.5,2
5.7,2.6,3.5,1.0,2
5.5,2.4,3.8,1.1,2
5.5,2.4,3.7,1.0,2
5.8,2.7,3.9,1.2,2
6.0,2.7,5.1,1.6,2
5.4,3.0,4.5,1.5,2
6.0,3.4,4.5,1.6,2
6.7,3.1,4.7,1.5,2
6.3,2.3,4.4,1.3,2
5.6,3.0,4.1,1.3,2
5.5,2.5,4.0,1.3,2
5.5,2.6,4.4,1.2,2
6.1,3.0,4.6,1.4,2
5.8,2.6,4.0,1.2,2
5.0,2.3,3.3,1.0,2
5.6,2.7,4.2,1.3,2
5.7,3.0,4.2,1.2,2
5.7,2.9,4.2,1.3,2
6.2,2.9,4.3,1.3,2
5.1,2.5,3.0,1.1,2
5.7,2.8,4.1,1.3,2
6.3,3.3,6.0,2.5,3
5.8,2.7,5.1,1.9,3
7.1,3.0,5.9,2.1,3
6.3,2.9,5.6,1.8,3
6.5,3.0,5.8,2.2,3
7.6,3.0,6.6,2.1,3
4.9,2.5,4.5,1.7,3
7.3,2.9,6.3,1.8,3
6.7,2.5,5.8,1.8,3
7.2,3.6,6.1,2.5,3
6.5,3.2,5.1,2.0,3
6.4,2.7,5.3,1.9,3
6.8,3.0,5.5,2.1,3
5.7,2.5,5.0,2.0,3
5.8,2.8,5.1,2.4,3
6.4,3.2,5.3,2.3,3
6.5,3.0,5.5,1.8,3
7.7,3.8,6.7,2.2,3
7.7,2.6,6.9,2.3,3
6.0,2.2,5.0,1.5,3
6.9,3.2,5.7,2.3,3
5.6,2.8,4.9,2.0,3
7.7,2.8,6.7,2.0,3
6.3,2.7,4.9,1.8,3
6.7,3.3,5.7,2.1,3
7.2,3.2,6.0,1.8,3
6.2,2.8,4.8,1.8,3
6.1,3.0,4.9,1.8,3
6.4,2.8,5.6,2.1,3
7.2,3.0,5.8,1.6,3
7.4,2.8,6.1,1.9,3
7.9,3.8,6.4,2.0,3
6.4,2.8,5.6,2.2,3
6.3,2.8,5.1,1.5,3
6.1,2.6,5.6,1.4,3
7.7,3.0,6.1,2.3,3
6.3,3.4,5.6,2.4,3
6.4,3.1,5.5,1.8,3
6.0,3.0,4.8,1.8,3
6.9,3.1,5.4,2.1,3
6.7,3.1,5.6,2.4,3
6.9,3.1,5.1,2.3,3
5.8,2.7,5.1,1.9,3
6.8,3.2,5.9,2.3,3
6.7,3.3,5.7,2.5,3
6.7,3.0,5.2,2.3,3
6.3,2.5,5.0,1.9,3
6.5,3.0,5.2,2.0,3
6.2,3.4,5.4,2.3,3
5.9,3.0,5.1,1.8,3

And here is the code you need to setup in matlab:

function [row_num,col_num,D,is_error] = load_data_in_matlab(file_name)
is_error = 0;
try
    fullname = [file_name '.dat'];
    D = importdata(fullname);   
    [row_num, col_num] = size(D);    
catch ME
    row_num = 0;
    col_num = 0;
    D = 0;
    is_error = 1;
    fprintf('Error loading data file "%s": %s \n',file_name,ME.message);
end

end
  • Then I proceeded to convert it to a Weka-readable format (which is an Instances object) by first creating an Attribute object, then add the numeric attributes and the class attributes. Afterwards, create an Instances object, then add the data:

    // Now convert into weka-friendly instances
    // First we must create a list of attributes
    ArrayList<Attribute> atts = new ArrayList<>();
    for (int j = 1; j <= num_of_attributes; j++){
        atts.add(new Attribute("Numeric attribute " + Integer.toString(j)));
    }
    ArrayList<String> attVals = new ArrayList<>();
    for (int count = 1; count <= num_of_classes; count++){
        attVals.add("Class " + Integer.toString(count));
    }
    atts.add(new Attribute("Class attribute", attVals));
    
    // Create an instances object
    Instances data_in_weka_format = new Instances("Data", atts, 0);
    data_in_weka_format.setClassIndex(data_in_weka_format.numAttributes() - 1);
    
    // Now add data
    for (int i = 0; i < num_of_observations; i++){
        double[] data_row = new double[data_in_weka_format.numAttributes()];
        System.arraycopy(X[i], 0, data_row, 0, num_of_attributes);
        data_row[num_of_attributes] = attVals.indexOf("Class " + Integer.toString(Y[i]));
        data_in_weka_format.add(new DenseInstance(1.0, data_row));
    }
    
  • Finally perform a simple classification :) The code seems to work, but I'm not sure if there is any bug leftover in it. And I'd also appreciate any review on code aesthetics,optimization,... . Thank you very much :)

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.