Subscribe:

Pages

The Weka API: Matrix class

Weka includes a matrix library in weka.core.matrix.Matrix. This library is adapted from the JAMA package, originally developed by NIST and the University of Maryland. The aim of the library is to support functions for numerical linear algebra.

Creating a
Matrix Instance

To create a Matrix instance from jRuby, I use the following construction method. It takes a two-dimensional Ruby array as argument, and returns an instance of the Matrix class.

def make_matrix array_2d
matrix = Matrix.new(array_2d.length, array_2d[0].length)
array_2d.length.times do |i|
array_2d[0].length.times do |j|
matrix.set(i, j, array_2d[i][j])
end
end
return matrix
end


For example: puts make_matrix([[1,2,3], [4,5,6], [7,8,10]])
displays

1 2 3

4 5 6
7 8 10

Solving a Linear System

The example within the Weka documentation solves the following problem:

Solving using jRuby is equally trivial:

A = make_matrix [[1,2,3], [4,5,6], [7,8,10]]
b = make_matrix [[0.1],[0.2],[0.3]]
x = A.solve b

Warning: although the matrices can be converted to strings using to_s, the result is limited in decimal places, which can be misleading. Better to display the solution using explict 'get' statements, such as:

puts "Solution: "
puts x.get(0,0), x.get(1,0), x.get(2,0)

Solution:
-0.0333333333333334
0.0666666666666667
-3.46944695195361e-17

Arithmetic on Matrices

The Matrix library supports several standard operations on matrices. It is easy to add, subtract, or multiply two matrices which are of the right 'shape'. For example, to find the 'residual' from the above solution: ||Ax - b||

r = A.times(x).minus(b)
puts r.normInf

(Here, 'normInf' is the infinity norm: the maximum value in the rows of the matrix 'r'. The library supports other norms. The popular "square root of sum of squares" is represented by 'normF', the Frobenius norm.)

The Weka API: Overview

The Weka data mining library makes available an extensive range of functionality, some of it hidden unless you delve into the API documentation. So, over the next few weeks, I intend to work through some of this documentation, and get a clearer picture of what is available and how to use it from jRuby. I am currently using the developer version 3.7, which you can find on the Weka website.

I start by opening up the Weka documentation in a browser, and this reveals a number of packages, some with many subpackages. In alphabetical order, the main packages are:

weka.associations:

The items in this package are for learning association rules: such rules are relations between variables in the data. There are two interfaces and several classes. The classes include standard algorithms such as APriori.

weka.classifiers:

The items in this package are for constructing classification models: these are models which predict the value of a specified attribute from the remaining attributes. This package is quite complex, and has several layers to it. There are four interfaces and a number of classes. The more familiar classifiers, such as J48 or LibSVM, are subclasses of the abstract class Classifier, and are grouped within subpackages. Some of the other classes, such as EnsembleLibrary, are for handling groups of classifiers.

The subpackages bring together the main groups of classificaion algorithms, such as bayes, functions, trees etc. These subpackages contain classes with concrete implementations of classification models, such as NaiveBayes and J48.

weka.clusterers:

This package contains concrete classes for a number of standard clustering algorithms, including Cobweb and SimpleKMeans: a clustering algorithm groups instances of data based on their similarity. Also, some interfaces and abstract classes are present for organising the clustering algorithms into groups, such as those which can estimate density for an instance.

weka.core:

This package contains many 'day-to-day' classes such as Instance and Instances. There are also classes such as EuclideanDistance and EditDistance which compute distances between instances or strings, respectively. MathematicalExpression is a class for evaluating strings containing expressions adhering to a simple grammar. There are over 70 classes in total, so plenty to explore.

weka.datagenerators and weka.estimators:

These packages provide ways to generate and estimate data based on distributions of values.

weka.experiment:

This package contains some interesting 'organisational' interfaces and classes. For example, the 'ResultsProducer' is an interface for objects which work with different randomisations of a dataset. The CrossValidationResultProducer is an implementation of this interface which organises a run of n-fold cross validation.

A collection of classes starting 'Result' provide ways of outputting data in standard formats, such as for GnuPlot, HTML etc.

This is the kind of material I tend to start coding myself, and perhaps I could save some time by using this package instead!

weka.filters:

Filters are techniques for modifying the dataset, either by modifying the number or balance of instances, or by altering the values of attributes themselves. Some useful examples include PrincipalComponents, which performs a transformation of the data, and RandomProjection, which reduces the dimensionality of data.

weka.gui:

Many of these classes are for the Weka graphical environment, but some components can be usefully reused in our own GUIs. These include visualisations of trees and other datatypes, which are under subpackages such as treeviewer and visualize.

Using libsvm from jruby

The latest libsvm distribution contains a Java version of the libsvm system. I want to use this from jruby, but there is a problem. The java version of libsvm has been written based directly on the C code, and this means class names do not have capital letters, which upsets the import process.

Correcting this is fairly simple. All the important classes need to have capital letter names. Having done this and recompiled the code, I am making it available here. You can unpack the code (using 'jar xf libsvm.jar') to find the source files. Or, you can just 'require "libsvm"' to use the contents within a jruby program.

Using this from jruby is not as smooth as it might be: some convenience functions to convert ruby data into the SVM format would be helpful, but for now here is a simple example of learning and then categorising some vectors. (The example is adapted from that in a great introduction to SVMs.)

require "java"
require "libsvm"

import "libsvm.Parameter"
import "libsvm.Model"
import "libsvm.Problem"
import "libsvm.Node"
import "libsvm.Svm"

puts "Classification with LIBSVM"
puts "--------------------------"

# Sample dataset: the 'Play Tennis' dataset
# from T. Mitchell, Machine Learning (1997)
# --------------------------------------------
# Labels for each instance in the training set
# 1 = Play, 0 = Not
@@labels = [0, 0, 1, 1, 1, 0, 1, 0, 1, 1, 1, 1, 1, 0]

# Recoding the attribute values into range [0, 1]
@@instances = [
[0.0,1.0,1.0,0.0],
[0.0,1.0,1.0,1.0],
[0.5,1.0,1.0,0.0],
[1.0,0.5,1.0,0.0],
[1.0,0.0,0.0,0.0],
[1.0,0.0,0.0,1.0],
[0.5,0.0,0.0,1.0],
[0.0,0.5,1.0,0.0],
[0.0,0.0,0.0,0.0],
[1.0,0.5,0.0,0.0],
[0.0,0.5,0.0,1.0],
[0.5,0.5,1.0,1.0],
[0.5,1.0,0.0,0.0],
[1.0,0.5,1.0,1.0]
]

# create some arbitrary train/test split
@@training_labels = @@labels.slice(0,10)
@@training_instances = @@instances.slice(0,10)
@@test_labels = @@labels.slice(10,14)
@@test_instances = @@instances.slice(10,14)

# convert vector of values into a vector of Nodes
def convert values
ns = Node[values.size].new
values.each_with_index do |v, i|
n = Node.new
n.index = i
n.value = v
ns[i] = n
end
return ns
end

# Define kernel parameters
# -- changing these makes the difference between something working or not
@@pa = Parameter.new
@@pa.C = 10
@@pa.svm_type = Parameter::NU_SVC
@@pa.degree = 1
@@pa.coef0 = 0
@@pa.eps= 0.001
@@pa.probability = 0
@@pa.nu = 0.5
@@pa.gamma = 100

@@sp = Problem.new

# Add documents to the training set
@@sp.l = @@training_labels.size
@@sp.x = Node[@@training_instances.size][@@training_instances[0].size].new
@@training_instances.each_with_index do |instance, i|
instance.each_with_index do |v, j|
n = Node.new
n.index = j
n.value = v
@@sp.x[i][j] = n
end
end
@@sp.y = Java::double[@@training_labels.size].new
@@training_labels.each_with_index {|v, i| @@sp.y[i] = v}

# Try four different Kernels
@@kernels = [ Parameter::LINEAR, Parameter::POLY, Parameter::RBF, Parameter::SIGMOID ]
@@kernel_names = [ 'Linear', 'Polynomial', 'Radial basis function', 'Sigmoid' ]

@@kernels.each_index do |j|

# Iterate and over each kernel type
@@pa.kernel_type = @@kernels[j]
m = Svm.svm_train(@@sp, @@pa)
errors = 0

# Test kernel performance on the training set
@@training_labels.each_index do |i|
pred = Svm.svm_predict(m, @@sp.x[i])
puts "Prediction: #{pred}, True label: #{@@training_labels[i]}, Kernel: #{@@kernel_names[j]}"
errors += 1 if @@labels[i] != pred
end
puts "Kernel #{@@kernel_names[j]} made #{errors} errors on the training set"

# Test kernel performance on the test set
errors = 0
@@test_labels.each_index do |i|
pred = Svm.svm_predict(m, convert(@@test_instances[i]))
puts "\t Prediction: #{pred}, True label: #{@@test_labels[i]}"
errors += 1 if @@test_labels[i] != pred
end

puts "Kernel #{@@kernel_names[j]} made #{errors} errors on the test set \n\n"
end

In terms of performance, you don't appear to lose anything by switching to jruby and the java version of libsvm: I reran my experiment from an earlier post (with suitable changes to the code) and it ran in the same amount of time, and, of course, got pretty much the same result.


Ruby and LibSVM - update

In a previous post I explained how to get LibSVM working with Ruby 1.9. There is now an updated version of RubySVM available at Sourceforge. Installation is fairly straightforward. If you use Ubuntu or Debian though, you will also need to make sure 'mkmf' is installed: make sure you install 'ruby1.9-dev' first.

Generating Graphs with JFreeChart

jRuby gives us access to all the libraries available for the Java platform; I have recently started looking at JFreeChart, which seems capable of generating all kinds of fancy graphs, with very little work. To write programs using JFreeChart you need to download the software, and extract the jar files: jcommon-X.jar and jfreechart-X.jar (where the -X stands for the version numbers). I tend to rename the jar files to jcommon.jar and jfreechart.jar.

Examples of using JFreeChart are available in different places: there are examples within the source code (in the folder source/org/jfree/chart/demos/), and an
article at InformIT from 2004 to help guide you into the APIs. Some examples are coming up for other scripting languages on the java platform, such as this example using Groovy to plot a simple piechart. Here's my own take, of a simple chart, for jRuby.

And the source code:
# jRuby implementation of simple PieChart example for JFreeChart

require "java"
require "jcommon.jar"
require "jfreechart.jar"

# -- Code to create a pie chart
include_class "org.jfree.data.general.DefaultPieDataset"
include_class "org.jfree.chart.ChartPanel"
include_class "org.jfree.chart.ChartFactory"

def create_chart
dataset = DefaultPieDataset.new
dataset.set_value("Comet Nuclei", 1.26)
dataset.set_value("Mars Asteroids", 7.0)
dataset.set_value("Apollo Objects", 0.55)

chart = ChartFactory.create_pie_chart(
"Impact Craters greater than 20km radius per million square kilometers on Mars",
dataset,
true,
true,
false
)

ChartPanel.new chart
end

# -- Code to build the Swing Frame
include_class "javax.swing.JFrame"
include_class "java.awt.BorderLayout"

def create_frame chart
frame = JFrame.new("jRuby using JFreeChart")
frame.content_pane.add(chart, BorderLayout::CENTER)
frame.setSize(600, 400)
frame.visible = true
frame.default_close_operation = JFrame::EXIT_ON_CLOSE
end

create_frame create_chart

(If you are interested, the numbers are from an article on Mariner IV and the Martian craters, by E.J. Opik.)

Finally... The fun of using JFreeChart does not end with displaying the images on the screen. A right-click on the image brings up a menu which lets you customise the image's colours, rescale, save to a file and even print. This is an enormous amount of functionality rapidly available, and the images look great.