Subscribe:

Pages

Accessing WEKA from jRuby

WEKA (http://www.cs.waikato.ac.nz/ml/weka/) is a data mining platform and library, written in Java. The platform contains implementations of numerous machine-learning algorithms and data-processing filters. The best way of getting hold of the system is to download the zip file, described as 'for Linux'. Unpack the zipped folder, and copy the 'weka.jar' file to your working directory; everything else in the folder is documentation or examples, and not necessary to work with WEKA.

You will also need jruby (http://jruby.codehaus.org/); I have renamed the jruby jar to simply jruby.jar. When I wrote this, WEKA was at version 3.6.1 and jruby was at version 1.3.0.

The graphical interface for WEKA can be called up by entering 'java -jar weka.jar' on the command prompt. However, if you add weka.jar to the CLASSPATH, the classes in weka.jar become accessible as an API. The API is documented in javadoc format: point your browser at the file 'documentation.html' within the weka folder, and click on 'Package Documentation'.

Assuming weka.jar, jruby.jar, and your program are in the same folder, the calling format is:
  1. Write a ruby program, and store in a file, say 'wekascript.rb'
  2. java -jar jruby.jar wekascript.rb
Further command line arguments can be provided, and picked up in the Ruby program through ARGV, as we shall see shortly. (You can streamline this process by adding weka.jar permanently to your CLASSPATH environment variable if you wish.)

Clustering Data using WEKA from jRuby

jRuby provides easy access to Java classes and methods, and WEKA is no exception. The following program builds a simple kmeans clusterer on a supplied input file, and then prints out the assigned cluster for each data instance. The 'include_class' statements are there to simplify references to classes in the API. When classifying each instance, we must watch for the exception thrown in case a classification cannot be made. Finally, notice that the filename is passed as a command-line parameter: the parameters after the name of the jruby program are packaged up into ARGV in the usual ruby style.
# Weka scripting from jruby
# Written by Peter Lane, 2009.
# Run with: java -jar jruby.jar trial-kmeans.rb FILENAME.arff

require "java"
require "weka"

include_class "java.io.FileReader"
include_class "weka.clusterers.SimpleKMeans"
include_class "weka.core.Instances"

# load data file
file = FileReader.new ARGV[0]
data = Instances.new file

# create the model
kmeans = SimpleKMeans.new
kmeans.buildClusterer data

# print out the built model
print kmeans

# Display the cluster for each instance
data.numInstances.times do |i|
cluster = "UNKNOWN"
begin
cluster = kmeans.clusterInstance(data.instance(i))
rescue java.lang.Exception
end
puts "#{data.instance(i)},#{cluster}"
end

We can see that the WEKA api makes it easy to pass in a data file. Data can be in a number of formats, including ARFF and CSV.

When run on the weather.arff example (in WEKA's 'data' folder), the output looks like the following:

kMeans
======

Number of iterations: 3
Within cluster sum of squared errors: 16.237456311387238
Missing values globally replaced with mean/mode

Cluster centroids:
Cluster#
Attribute Full Data 0 1
(14) (9) (5)
==============================================
outlook sunny sunny overcast
temperature 73.5714 75.8889 69.4
humidity 81.6429 84.1111 77.2
windy FALSE FALSE TRUE
play yes yes yes


sunny,85,85,FALSE,no,0
sunny,80,90,TRUE,no,0
overcast,83,86,FALSE,yes,0
rainy,70,96,FALSE,yes,0
rainy,68,80,FALSE,yes,0
rainy,65,70,TRUE,no,1
overcast,64,65,TRUE,yes,1
sunny,72,95,FALSE,no,0
sunny,69,70,FALSE,yes,0
rainy,75,80,FALSE,yes,0
sunny,75,70,TRUE,yes,1
overcast,72,90,TRUE,yes,1
overcast,81,75,FALSE,yes,0
rainy,71,91,TRUE,no,1


Using jRuby to Create Instances

One of the advantages of using a language like jruby to talk to WEKA is that we should have more control on how our data is constructed and passed to the machine-learning algorithms. A good start is how to construct our own set of instances, rather than reading them directly in from file. There are some quirks to WEKA's construction of a set of Instances. In particular, each attribute must be defined through an instance of the Attribute class. This class gives a string name to the attribute and, if the attribute is a nominal attribute, the class also holds a vector of the nominal values. Each instance can then be constructed and added to the growing set of Instances. The code below shows how to 'by-hand' construct a dataset which can then be passed to one of WEKA's learning algorithms.
require "java"
require "weka"

include_class "weka.clusterers.SimpleKMeans"
include_class "weka.core.Instances"
include_class "weka.core.Instance"
include_class "weka.core.Attribute"
include_class "weka.core.FastVector"

# Convert provided arguments into a FastVector
# -- use mainly in making nominal attributes
def make_vector *args
atts = FastVector.new(args.length)
args.each {|arg| atts.addElement arg}
return atts
end

# Create the five attributes
outlook = Attribute.new("outlook", make_vector("sunny", "overcast", "rainy"))
temperature = Attribute.new("temperature")
humidity = Attribute.new("humidity")
windy = Attribute.new("windy", make_vector("FALSE", "TRUE"))
play = Attribute.new("class", make_vector("yes", "no"))

# Instances class will hold the complete dataset
dataset = Instances.new(
"weather dataset",
make_vector(outlook, temperature, humidity, windy, play),
14)

# Dataset
DATA = [
["sunny", 85, 85, "FALSE", "no"],
["sunny", 80, 90, "TRUE", "no"],
["overcast", 83, 86, "FALSE", "yes"],
["rainy", 70, 96, "FALSE", "yes"],
["rainy", 68, 80, "FALSE", "yes"],
["rainy", 65, 70, "TRUE", "no"],
["overcast", 64, 65, "TRUE", "yes"],
["sunny", 72, 95, "FALSE", "no"],
["sunny", 69, 70, "FALSE", "yes"],
["rainy", 75, 80, "FALSE", "yes"],
["sunny", 75, 70, "TRUE", "yes"],
["overcast", 72, 90, "TRUE", "yes"],
["overcast", 81, 75, "FALSE", "yes"],
["rainy", 71, 91, "TRUE", "no"]
]

# Convert raw data into Instances
DATA.each do |defn|
instance = Instance.new 5
instance.setValue(outlook, defn[0])
instance.setValue(temperature, defn[1])
instance.setValue(humidity, defn[2])
instance.setValue(windy, defn[3])
instance.setValue(play, defn[4])
instance.setDataset dataset
dataset.add instance
end

puts "Created dataset:"
puts dataset # outputs ARFF format of data

puts "\nClusters from k-means"
# create the model
kmeans = SimpleKMeans.new
kmeans.buildClusterer dataset

# Display the cluster for each instance
dataset.numInstances.times do |i|
cluster = "UNKNOWN"
begin
cluster = kmeans.clusterInstance(dataset.instance(i))
rescue java.lang.Exception
end
puts "#{dataset.instance(i)}, #{cluster}"
end

Writing Ruby code in gvim

This post is about putting together a set of tools to improve the experience of writing Ruby code. Software development, even of small programs, can be made easier and quicker if your editor supports the writing process and you have access to key information. I look at some options for customising the gvim editor to support Ruby development and how standard Ruby tools, such as ri and the debugger, can be used from within gvim. There are many very powerful ways of extending the gvim environment; this post only gives a pointer to the basics.

Note: only tested on Ubuntu Linux (jaunty), with ruby 1.9.2 and gvim 7.1.

gvim is can be customised whilst you are running the application, or the customisations can be preserved and used everytime your start gvim by editing the file ~/.gvimrc. You should create this file if it does not exist. Add the options as described below, or see the end for my complete .gvimrc

When Writing Ruby Code

To make gvim aware of ruby code when you open a ruby program, you need to tell it to be aware of the language it is editing, and it can tell that from the file extension, or filetype. The following options make sure gvim uses the appropriate plugin when we open a program:

filetype on
filetype plugin on

There are three code-specific functions I need from my text editor: syntax colouring, auto-indentation and auto-completion. The first two improve the visual appearance of the code, and the last speeds up the typing process.

Writing code in black-and-white is to be avoided at all costs. The extra visual cues you get from having def-end pairs one colour, do-end blocks a second, etc is invaluable. The colours soon become intuitive, and help in identifying syntax errors. Also, I find the colours make it easier to recognise blocks and recurring patterns when refactoring code. To turn on syntax highlighting, you need the option:

syntax on

Explore the options under the 'Edit/Colour Scheme' menu to find a colour scheme you like, then add the option (replace 'slate' with your favourite scheme name):

colors slate

The structure of a Ruby program is naturally captured with indentation. For me, the 'right' level of indent is two spaces per level. I also do not like tab characters, and prefer them expanded into spaces. The following options turn on the automatic indentation, set the size of indentation to two, and convert all tab commands:

filetype indent on
set tabstop=2
set shiftwidth=2
set expandtab

Not an option as such, but something worth knowing. When you are entering text, use CTRL-P to auto-complete your text. gvim will complete the word you are entering, or present a list of available choices. You can move through the list either using CTRL-P repeatedly, or using the cursor keys to move up and down and 'enter' to select the completion. For example, if I were writing this text in gvim I could type 'comp<ctrl-p>' and expect to press 'enter' to complete the word 'completion'. After a little while, this becomes second nature, and saves on typing. In addition, it saves a lot of mistakes, as variables or method names completed through auto-completion should agree with each other!

Helpful Editor Functions

Other functions that are helpful include the ability to 'fold' long definitions. I don't use this too much, but it can be helpful in large classes whose code is mostly static. To fold a long definition:

  1. Put the cursor on the first line of the definition

  2. Type 'zf'

  3. Click with the mouse on the last line of the definition


To expand a folded definition, type 'za'.

Another useful function is the ability to highlight and search for a particular name or variable. The command 'gd' will highlight all occurrences of the name under the cursor, and 'n' 'p' will move you forwards and back through the list, as with the usual gvim search.

Ruby Tools

Documentation through ri

The 'ri' command-line tool allows access to the Ruby documentation, and can be a quick and easy way to check Class definitions or argument lists for methods in the standard library. The following mapping connects the Function key 'F3' to a call to the 'ri' command-line tool. After pressing <f3> the user must enter details of the Class or method name to be shown.

map <f3> :!ri



Executing Code from gVim

You can execute the code in the current file you are editing from within gVim. To do this, you would normally go into command mode (press ':') then enter '!ruby %' which executes ruby as a shell command. The '%' sign means to pass ruby the current filename. The output is then shown in the window.

You can make this a single key press by adding a key mapping terminated with <cr> to your .gvimrc file:

map <f1> :!ruby %<cr>


Final .gvimrc

My complete .gvimrc file is below. It contains some extra settings: incsearch so that searching is incremental, stepping to intermediate matches as you type; hlsearch to show all search results with a yellow highlight (use :noh to remove the highlighting); and scrolloff to encourage gvim to keep at least the given number of lines around the cursor when scrolling. nowrap and the call to guioptions stops gvim wrapping long lines of source code, and provides a handy horizontal scrollbar where needed. And yes, that is a key mapping to start up an instance of irb within the editor!

set nocompatible

syntax on
filetype on
filetype indent on
filetype plugin on
set nowrap
set guioptions+=b

set tabstop=2
set shiftwidth=2
set expandtab
set hlsearch
set scrolloff=2
set incsearch
map <f1> :!ruby %<cr>
map <f2> :!ruby -r debug %<cr>
map <f3> :!irb<cr>
map <f4> :!ri