4.6 ■ Prototype 3: Training
Sidebar 4.3 Arrays vs collections
Collections were introduced in version 1.2 of Java. The main collection interface is Collection. It is a container of elements that can be added to or removed from the collection. Elements can be iterated on by means of an Iteratorobject. When com- pared with an array, a collection does not exhibit a specific implementation, its elements are not ordered, and it does not have a fixed size.
Another important difference between collections and arrays is that the former treat their elements as Object, while the latter can have different types of elements. The fact that collections deal with Objecthas two main consequences:
■ they cannot contain primitive type (e.g. int);
■ a cast is necessary when accessing their elements.
Since primitive types are not allowed, wrapper classes (e.g. Integer) should be used instead. Using casts brings a minor decrease in readability. We can compare arrays and collections looking at how simple operations can be performed.
Iterator it # items.iterator();
while(it.hasNext()){
String item # (String)it.next(); // do something with the item }
Using an array the same iteration code can be written as follows:
for(int i#0; i < items.length; !!i){ String item # items[i];
// do something with the item }
The two variations of iteration are almost equally complex. The main weakness of the former is the necessity of using casts to adapt the Object, manipulated by the collections, and the effective classes manipulated by the program. Manipulating
Objectinstead of specific classes, gives great flexibility, because it is possible to write algorithms that are independent of details.
Up to now we have looked at the advantages of using the elements of a collec- tion; collections provide several advantages also in populating the set. When using
4.6.3 Implementation
We will have a method (buildDecisionTree()) that implements the main fea- tures of the algorithm, and several auxiliary methods that implement the details. The root node of the decision tree is computed according to Algorithm 4.2. Since the algorithm is generic and does not require accessing any extra information in addition to its arguments, we decide to implement is using static methods. The method buildDecisionTreeis as close an imple- mentation of the original algorithm as possible.
The method receives two parameters; the first is a map that associates each item with its category; the second is a map that associates the names of the features with their types. The collection of items can be obtained getting the key set from the map items(using the method keySet()).
//Input: a training set of items T, a set of attributes A //Output: a decision tree (the root of it)
private static Node buildDecisionTree
(Map items, Map features){ // 0) if the attribute set is empty or the items
// belong to a single class
if(features.size()##0 || information(items)##0.0){
// a) create a leaf node labelled according to the class // of the item
return new Node(findCategory(items)); }
// 1) select the "best" split attribute s in A String splitFeature # selectSplit(items,features); // 2) create a node n with label s.name
an array the number of elements must be known in advance, while the number of elements in a collection is not defined a priori.
Filling a set with two strings:
Collection items # new Vector(); items.add(new String("first")); items.add(new String("second"));
Using an array the same population code can be written as follows:
String [] items # new String[2]; items[0] # new String("first"); items[1] # new String("first");
The use of arrays poses several problems: the number of elements must be known in advance, an index must be used to address elements.
Another important interface is Map. It represents the association of a set of keys to a set of values. The main methods of interface Map are void put(Object key, Object value)and Object get(Object key). The former stores an association between the key and the value, the latter retrieves the value associated with a key (or null if no value is present).
Node n # new Node(splitFeature); // 3) for each possible value vi of s FeatureType splitType #
(FeatureType)features.get(splitFeature); Map partitions#performSplit(items,splitFeature,
splitType.allowedValues());
for (Iterator iter # partitions.keySet().iterator(); iter.hasNext();) { String value # (String) iter.next();
// a) be ni the result of a recursive execution of this // algorithm where:
// the fist input is: Ti # { item in T | item.s ## vi } // the second input is: A - { s }
Map partition#(Map)partitions.get(value); Map remaining # new HashMap(features); remaining.remove(splitFeature);
Node child # buildDecisionTree(partition,remaining); // b) set ni as child node of n and label the // connecting arc vi
n.addChild(value,child); }
// 4) n is the resulting root node of the decision tree return n;
}
In step 0 to determine whether all the items in a set belong to the same category we check if the information content is null (see Equation 4.3).
In step 3, the loop operates on each of the sets of items resulting from the split. It is more efficient to generate all these sets in a single step instead of generating them separately. To keep the link between each value of the split feature and the relative split-set a Map is used. A map stores associations between a key (the possible value) and a value (the corresponding set of items), making it very easy to obtain the set of items given the value.
To keep the method buildDecisionTreeas close as possible to the original algorithm (Algorithm 4.2), several details are missing:
■ how to determine the category of a set of items,
■ how to find the “best” split feature,
■ how to split a set of items,
■ how to calculate information content (in terms of number of bits) of a set of items,
■ how to calculate the information gain deriving from a split.
The findCategory() method determines which is the category of a set of items. This is not as easy as it seems, since the set can be empty or it can contain items from several categories. If the set is empty then the category is unknown, otherwise the frequency of the categories must be computed.
If there is only one category, that is the category of the set, otherwise the category of the set cannot be determined.
private static String findCategory(Map items){ // no category if the set is empty
if(items.size()##0) return "?";
// computes the frequency of each category Map catFreq # new HashMap();
Iterator it # items.keySet().iterator(); String category # "";
while(it.hasNext()){
Item item # (Item)it.next();
category # (String)items.get(item);
Integer count # (Integer)catFreq.get(category); if(count##null)
catFreq.put(category, new Integer(1)); else
catFreq.put(category, new Integer(1!count.intValue())); }
// if only one category is present it is the set’s category if(catFreq.keySet().size() ## 1)
return category;
// otherwise it is not possible to assign a category return "?";
}
The selectSplit()method determines the best split feature for a set of items. The best split feature is the one implying the highest information gain.
private static String selectSplit(Map items,Map features){ Iterator attr#features.keySet().iterator();
String split#null; double maxGain # 0.0; while(attr.hasNext()){
String candidate # (String)attr.next();
FeatureType type # (FeatureType)features.get(candidate); double gain # evaluateSplitGain(items,
candidate,type.allowedValues()); if(gain>maxGain){ maxGain # gain; split # candidate; } } return split; }
The performSplit()method executes the split of a set of items according to a split feature. It returns the resulting sets of items, each of them being labelled with the relative feature value.
private static Map performSplit(Map items, String split, Collection possibleValues){ Map partitions#new HashMap();
for (Iterator iter # possibleValues.iterator();
iter.hasNext();) { String value # (String) iter.next();
partitions.put(value,new HashMap()); }
Iterator it#items.keySet().iterator(); while(it.hasNext()){
Item item#(Item)it.next();
String splitValue # item.value(split);
Map partition#(Map)partitions.get(splitValue); partition.put(item,items.get(item));
}
return partitions; }
The evaluateSplitGain() evaluates the information gain that would derive from splitting a set of items according to a given split feature; it computes Equation 4.4.
private static double evaluateSplitGain(Map items, String split, Collection possibleValues){ double origInfo # information(items);
double splitInfo # 0;
Map partitions#performSplit(items,split,possibleValues); double size#items.size();
for (Iterator iter # possibleValues.iterator();
iter.hasNext();) { String value # (String) iter.next();
Map partition # (Map)partitions.get(value); double partitionSize # partition.size(); double partitionInfo # information(partition); splitInfo !# partitionSize/size*partitionInfo; }
return origInfo - splitInfo; }
The information() method computes the information content of a set of items. The categories represent the symbols of the alphabet. As described in Equation 4.1, the information can be computed from the frequency of each symbol. Thus first of all we need to count the number of occurrences of each symbol, then we can apply the equation to compute the information content.
private static double information(Map items){ Map frequencies # new HashMap();
// compute number of occurrences of classes Iterator it#items.keySet().iterator(); while(it.hasNext()){
Item item#(Item)it.next();
String category#(String)items.get(item);
Long num_occur # (Long)frequencies.get(category); if(num_occur ## null) frequencies.put(category,new Long(1)); else frequencies.put(category, new Long(num_occur.longValue()!1)); } // compute information double info#0;
double numItems # items.size(); it # frequencies.values().iterator(); while(it.hasNext()){
Long num_occurr # (Long)it.next();
double freq# num_occurr.doubleValue() / numItems; info !# freq * Math.log(freq) / Math.log(2.0); }
return -info; }
Finally, we define a constructor for the DecisionTreeclass that invokes the buildDecisionTree()method to build the decision tree; the parameters of the constructor must be the same as those of the method.
public DecisionTree(Map items, Map features) { root # buildDecisionTree(items,features); }
4.6.4 Test
To test the decision tree construction algorithm we need to:
■ define a training set of items;
■ instantiate the decision tree based on this set; and
■ check if it classifies correctly some items.
If the training algorithm works correctly then a decision tree equivalent to the one used in previous testing should be generated, therefore the same test items can be used to check if the tree is correct. We will use the training set of items described in Table 4.2.
The test method testExample()is made up the following steps:
■ create the items, by means of the createItem()method, and add them to the training set mapping them to their categories;
■ create a decision tree classifier based on the training set; and
■ check if the classifier classifies correctly the test set element. public class TestTraining extends TestCase {
public TestTraining(String arg0) { super(arg0);
}
private Item createItem(String ac, String abs){ Feature[] features # new Feature[] {
new Feature("AC",ac,yn), new Feature("ABS",abs,yn) };
return new Item("car",features); }
private FeatureType yn # new FeatureType("YesNo", new String[]{"yes","no"}); public void testExample(){
Map items # new HashMap(); Map features # new HashMap(); features.put("AC",yn);
features.put("ABS",yn);
Item item1 # createItem("yes","yes"); items.put(item1,"high");
Item item2 # createItem("yes","no"); items.put(item2,"medium");
Item item3 # createItem("no","yes"); items.put(item3,"medium");
Item item4 # createItem("no","no"); items.put(item4,"low");
DecisionTree dc # new DecisionTree(items,features); assertEquals("high",dc.assignCategory(item1)); assertEquals("medium",dc.assignCategory(item2)); assertEquals("medium",dc.assignCategory(item3)); assertEquals("low",dc.assignCategory(item4)); } }
Table 4.2 Training set
Item AC ABS Expected category
1 Yes Yes High
2 No No Low
3 Yes No Medium
The test, like those developed for the previous prototypes, is not complete but clearly defines the line that can be followed to make them more complete.
4.7
■
Extensions
The classification criteria adopted in Prototypes 1 and 2 are quite simple. For each item they specify only whether a given feature is present or not. For example, they specify that a car has the airbag option. A straightforward extension could provide the ability to classify items based on the some characteristics of a given feature. For example, a car might have two airbags, four airbags or no airbags.
A more complex and interesting extension requires dealing with the theory of fuzzy sets (Klir and Yuan 1995). A fuzzy set differs from traditional mathematical sets in the form of the membership function. Items are not just said to belong (or not) to a given set. The membership function specifies for each item the percentage membership of that item in a given set. For example, a car with zero airbags belongs 100 per cent to the low risk category; a car with two airbags belongs 60 per cent to the low risk category and 40 per cent to the medium risk category; a car with four airbags belongs 10 per cent to the low risk category, 70 per cent to the medium risk category and 20 per cent to the high risk category. Using the fuzzy set theory, it is more easy to classify items that are not in only one category.
4.8
■
Assessment
Analysis technique. We recognized in the problem statement a well-known domain, whose literature provides us with the main concepts that make up the analysis model.
Modelling technique. The analysis of this problem used, in addition to the class diagrams, the object diagrams to represent static structures and sequence diagrams to describe interactions among objects in typical scenarios. Development approach. We divided the problem into two sub-problems: the representation of the classification information and its processing. The processing deals both with the use of the information to classify an item and the construction of a classification schema starting from a training set.
Attributes of classes may have different features. In particular, read-only attributes are initialized once and never modified; they can be implemented according to the read-only attribute idiom. When there are constraints on the values that an attribute can assume, the constrained-value attribute idiom provides a good implementation solution.
Associations between classes play an important role in object-oriented analysis and design. Associations exist to be navigated; this issue drives the
implementation of n-to-1 (or 1-to-1) associations using the many-to-one association idiom.
Testing the implementation is an essential aspect of the development. There are several strategies and methodologies; two very simple approaches are described by the equals method testing and toString method testing design patterns.
Idiom Read-only attribute
Context A class has a read-only attribute.
Problem It must be ensured that nobody can modify the attribute. Forces or tradeoffs It must be easy to read the attribute.
It must be impossible to write it.
Solution The attribute is implemented as a private member attribute of the class. A gettermethod with the same name as the attribute provides read access to the attribute. If the type of the attribute is a primitive type (e.g. int) or a non-modifiable class (e.g. String) the attribute can be directly returned by the method, other- wise a copy of the attribute should be returned.
Since the attribute must be initialized at some point, the constructor is responsible for this.
Examples class SampleClass {
private String name;
public SampleClass(String name){
this.name # name; }
public String name() { return name; } }
Force resolution Access to the attribute is easily achieved through the method with the same name. Since the attribute is private, it is not possible to modify it.
Design rationale The getter method returns the value of the attribute avoiding direct manipulation of the attribute by the class clients. The simplest form of the getter method is just a return statement having the attribute as argument. If the attribute is a primitive type then it is passed “by value” to the method caller. Otherwise, if it is an object, the caller obtains a reference. In the latter case a problem arises: since the client obtains a reference to the object, nothing can avoid modification. Therefore a simple return statement is suitable only for non-modifiable classes. When a modifiable class is concerned, the getter method must return a copy of the attribute.
Idiom Constrained-value attribute
Context A class has an attribute that has constraints on what values it can assume.
Problem Ensure that only allowed values can be assigned to the attribute.
Forces or tradeoffs It must be possible to easily read and write the attribute. Forbidden values must be avoided.
Solution This idiom is an extension of the “read-only attribute” idiom.
The attribute is implemented as a private member
attribute of the class. A gettermethod with the same name as the attribute provides read access to the attribute. A settermethod with the same name as the attribute provides write access to the attribute. The settermethod checks if the new value is allowed; if it is, the attribute is updated. Otherwise an exception is thrown.
The attribute must be initialized at some point; usually it is the constructor that is responsible for initialization.
Examples private String value;
// getter method
public String value() { return value; } // setter method
public void value(String newValue)
throws IllegalArgumentException {
if(satisfiesConstraints(newValue)){ value # newValue;
return; }
throw new IllegalArgumentException( "value '" ! newValue !
"' not valid for attribute " ! name); }
Force resolution Access to the attribute is easily achieved through the method with the same name.
Since the attribute is private, it is not possible to modify it. Design rationale The “read-only attribute” idiom avoids direct access to
the attribute, thus a new value can be assigned only through the setter method that checks for conformance to constraints.
Idiom Many-to-one association
Context An n : 1 association between two classes navigable from the many role to the one role. Note that a 1 : 1 association is just a special case of an n : 1 association.
Problem Implement the association enabling navigation. Forces or tradeoffs A link to the object must be stored.
It must be navigable.
Solution An attribute contains a reference to the linked object; this attribute is implemented according to the “read-only attribute” idiom.
Examples private Attribute category;
public Attribute category() { return category }
Design rationale Since, in Java, object variables (and attributes) are references to objects and not effective object storage, this kind of variable lends itself to implementing links.
Idiom One-to-many association decomposition
Context A 1 : n association between two classes navigable from the one role to the many role. The exact cardinality of the association is not known in advance.
Problem Implement the association enabling navigation.
Forces or tradeoffs A link to the object must be stored. It must be navigable. Arrays require knowledge of the cardinality.
Solution The transformed association can be easily implemented with a reference in the class Oneplus a reference in the
Manyclass: in total one attribute per class.
Forces resolution Knowledge of cardinality is not required.
We need to implement two one-to-one associations. Design rationale Since we know how to implement one-to-one (and many-
to-one) associations we decompose the original association into simpler and easily implementable associations.
Many One 0 .. n !many 0 .. 1 !first !next 0 .. 1 One Many
4.9
■
References
Cherkassky, V. and Mulier, F. (1998) Learning From Data, John Wiley & Sons. Klir, G. J. and Yuan, B. (1995) Fuzzy Sets and Fuzzy Logic: Theory and
Applications, Pearson Education.
Sun Microsystems (1998) JDC TechTips: January 20, 1998; available at http://java.sun.com/developer/TechTips/1998/tt0120.html
Synthesis
This chapter investigates the use of Java as a hardware description language (HDL). The program simulates a typical computer that adopts the Von Neumann architecture. The basic components are the RAM memory that stores the programs and the data, the keyboard that reads input data from the command-line, the monitor that displays the results, the CPU that processes the elementary operations (load, store, add, jump, etc.), and the bus that interconnects the basic components.
■ Focus: this case study exemplifies the exchange of data between objects and the use of state transition diagrams to describe the behaviour of the CPU.
■ OO techniques: inclusion.
■ Java features: Vectors, Files.
■ Background: the reader is required to know the basic OO concepts and fundamental Java programming techniques.
5.1
■
Specifications
Hardware description languages (HDLs) have been very popular in the last decade. They allow a hardware engineer to describe the system he or she wants to build in terms of the functionalities that it must provide.
The key success factor of HDLs is that they give the opportunity to simu- late and test a high-level description of the system, which can be defined with a relatively little effort when compared with gate-level or even transistor-level descriptions.
HDLs usually incorporate most of the features that are used in general- purpose programming languages: namely structured programming con- structs and OO concepts. Usually, HDLs build on a rich set of libraries that support the automatic generation of low-level system descriptions that can be easily synthesized in hardware components.
The aim of this case study is to use the Java language as HDL. The focus is on the simulation of common hardware components, such as the RAM, the IO devices, the CPU and the bus. These components interact accordingly