Published on 08/2022

Iris Plants Classification

The Iris Plants Classification project is like the "Hello World" of Machine Learning - it consists of training a model to recognise different species of iris plants based on their characteristics, using the data of 150 plants.

Now, decomposing the problem:

  1. Importing modules
  2. Importing the dataset
  3. Splitting the dataset
  4. Training the model
  5. Testing the model

Importing Modules

Using Python, we'll import TensorFlow (to download the dataset and build the model), pandas (for dataframe manipulation) and NumPy (to pick out the most likely species from each prediction).


python
import tensorflow as tf
import pandas as pd
import numpy as np
python
import tensorflow as tf
import pandas as pd
import numpy as np

Importing the Dataset

The dataset itself comes from the UCI Machine Learning Repository. We'll create a function that uses TensorFlow's get_file to download it from that URL (caching it locally so it only downloads once) and returns the path to the downloaded file.


python
def retrieve_iris_data():
    iris_url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data'
    cache_dir = '.'
    cache_subdir = 'data'
    iris_file = tf.keras.utils.get_file('iris.data', iris_url, cache_dir=cache_dir, cache_subdir=cache_subdir)
    return iris_file
python
def retrieve_iris_data():
    iris_url = 'https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data'
    cache_dir = '.'
    cache_subdir = 'data'
    iris_file = tf.keras.utils.get_file('iris.data', iris_url, cache_dir=cache_dir, cache_subdir=cache_subdir)
    return iris_file

We need to create a pandas dataframe that the ML model can process, so we will make column names to facilitate making the dataframe. Since machine learning models need numerical values to process, we'll also create a dictionary that maps each of the plant names to a number.


python
iris_features = ['sepal_length', 'sepal_width', 'petal_length','petal_width', 'species']
label_map = {'Iris-setosa':0, 'Iris-versicolor':1, 'Iris-virginica':2}
python
iris_features = ['sepal_length', 'sepal_width', 'petal_length','petal_width', 'species']
label_map = {'Iris-setosa':0, 'Iris-versicolor':1, 'Iris-virginica':2}

Now we can create a function that processes and returns a dataframe version of the dataset.


python
def parse_iris_data(iris_path):
    iris_df = pd.read_csv(iris_path, names=iris_features)
    iris_df['species'].replace(label_map, inplace=True)
    return iris_df
python
def parse_iris_data(iris_path):
    iris_df = pd.read_csv(iris_path, names=iris_features)
    iris_df['species'].replace(label_map, inplace=True)
    return iris_df

Then we can assign variables to each of the functions to ultimately create our dataframe:


python
iris_filepath = retrieve_iris_data()
iris_df = parse_iris_data(iris_filepath)
python
iris_filepath = retrieve_iris_data()
iris_df = parse_iris_data(iris_filepath)

Splitting the Dataset

To split our dataset into the training dataset and the testing dataset, we'll take the first 10 values for each species to create the testing dataset and the other 40 of each species to create the training dataset - that way 20% of our original dataset is the testing dataset and the other 80% is the training dataset, with every species represented equally in both.

This isn't a random split, though. It works here because the rows for each species aren't in any meaningful order, but the standard approach is a shuffled, stratified split (for example scikit-learn's train_test_split with stratify), which avoids accidentally testing on an unrepresentative slice of the data.


python
# Split up test data and training data
test_data = iris_df.groupby('species').head(10)   # test data (20% of the total data)
iris_df = iris_df.groupby('species').tail(40)  # training data
python
# Split up test data and training data
test_data = iris_df.groupby('species').head(10)   # test data (20% of the total data)
iris_df = iris_df.groupby('species').tail(40)  # training data

Then we split the training and testing datasets into features (the measurements) and labels (the species): during training, the model guesses the species from the features, compares its guess with the label, and adjusts itself based on how wrong it was.


python
# Split up test data and training data into their features and labels
test_features = test_data.drop('species', axis=1)
test_labels = test_data.drop(['sepal_length', 'sepal_width', 'petal_length','petal_width'], axis=1)
training_features = iris_df.drop('species', axis=1)
training_labels = iris_df.drop(['sepal_length', 'sepal_width', 'petal_length','petal_width'], axis=1)
python
# Split up test data and training data into their features and labels
test_features = test_data.drop('species', axis=1)
test_labels = test_data.drop(['sepal_length', 'sepal_width', 'petal_length','petal_width'], axis=1)
training_features = iris_df.drop('species', axis=1)
training_labels = iris_df.drop(['sepal_length', 'sepal_width', 'petal_length','petal_width'], axis=1)

Training the Model

First we will create a neural network with 4 input nodes (one for each measurement), 2 hidden layers of 10 nodes (where the processing happens) and an output layer of 3 nodes (one for each species).


python
iris_model = tf.keras.Sequential([
    tf.keras.layers.Dense(10, activation=tf.nn.relu, input_shape=(4,)),
    tf.keras.layers.Dense(10, activation=tf.nn.relu),
    tf.keras.layers.Dense(3)
  ])
  
iris_model.summary()
Output
Model: "sequential_2"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 dense_6 (Dense)             (None, 10)                50        
                                                                 
 dense_7 (Dense)             (None, 10)                110       
                                                                 
 dense_8 (Dense)             (None, 3)                 33        
                                                                 
=================================================================
Total params: 193 (772.00 Byte)
Trainable params: 193 (772.00 Byte)
Non-trainable params: 0 (0.00 Byte)
_________________________________________________________________
python
iris_model = tf.keras.Sequential([
    tf.keras.layers.Dense(10, activation=tf.nn.relu, input_shape=(4,)),
    tf.keras.layers.Dense(10, activation=tf.nn.relu),
    tf.keras.layers.Dense(3)
  ])
  
iris_model.summary()
Output
Model: "sequential_2"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 dense_6 (Dense)             (None, 10)                50        
                                                                 
 dense_7 (Dense)             (None, 10)                110       
                                                                 
 dense_8 (Dense)             (None, 3)                 33        
                                                                 
=================================================================
Total params: 193 (772.00 Byte)
Trainable params: 193 (772.00 Byte)
Non-trainable params: 0 (0.00 Byte)
_________________________________________________________________

Then we compile the network, which configures how it will learn: the optimiser (Adam) that adjusts the weights, the loss function that measures how wrong its predictions are, and the metric (accuracy) that we want to track.


python
iris_model.compile(optimizer='adam', loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True), metrics=[tf.keras.metrics.SparseCategoricalAccuracy()])
python
iris_model.compile(optimizer='adam', loss=tf.keras.losses.SparseCategoricalCrossentropy(from_logits=True), metrics=[tf.keras.metrics.SparseCategoricalAccuracy()])

Now we can train the model. (The output below is from a second run on a model that had already been trained, which is why the accuracy is already high in the first epoch - on a fresh model it starts much lower and climbs over the 100 epochs.)


python
iris_model.fit(training_features, training_labels, verbose=2, batch_size=16, epochs=100)
Output
Epoch 1/100
8/8 - 0s - loss: 0.0826 - sparse_categorical_accuracy: 0.9750 - 23ms/epoch - 3ms/step
Epoch 2/100
8/8 - 0s - loss: 0.0816 - sparse_categorical_accuracy: 0.9833 - 44ms/epoch - 5ms/step
Epoch 3/100
8/8 - 0s - loss: 0.0811 - sparse_categorical_accuracy: 0.9833 - 23ms/epoch - 3ms/step
Epoch 4/100
8/8 - 0s - loss: 0.0822 - sparse_categorical_accuracy: 0.9750 - 39ms/epoch - 5ms/step
Epoch 5/100
8/8 - 0s - loss: 0.0820 - sparse_categorical_accuracy: 0.9833 - 30ms/epoch - 4ms/step
...
Epoch 96/100
8/8 - 0s - loss: 0.0721 - sparse_categorical_accuracy: 0.9750 - 34ms/epoch - 4ms/step
Epoch 97/100
8/8 - 0s - loss: 0.0726 - sparse_categorical_accuracy: 0.9833 - 49ms/epoch - 6ms/step
Epoch 98/100
8/8 - 0s - loss: 0.0717 - sparse_categorical_accuracy: 0.9833 - 34ms/epoch - 4ms/step
Epoch 99/100
8/8 - 0s - loss: 0.0727 - sparse_categorical_accuracy: 0.9833 - 26ms/epoch - 3ms/step
Epoch 100/100
8/8 - 0s - loss: 0.0715 - sparse_categorical_accuracy: 0.9833 - 23ms/epoch - 3ms/step
python
iris_model.fit(training_features, training_labels, verbose=2, batch_size=16, epochs=100)
Output
Epoch 1/100
8/8 - 0s - loss: 0.0826 - sparse_categorical_accuracy: 0.9750 - 23ms/epoch - 3ms/step
Epoch 2/100
8/8 - 0s - loss: 0.0816 - sparse_categorical_accuracy: 0.9833 - 44ms/epoch - 5ms/step
Epoch 3/100
8/8 - 0s - loss: 0.0811 - sparse_categorical_accuracy: 0.9833 - 23ms/epoch - 3ms/step
Epoch 4/100
8/8 - 0s - loss: 0.0822 - sparse_categorical_accuracy: 0.9750 - 39ms/epoch - 5ms/step
Epoch 5/100
8/8 - 0s - loss: 0.0820 - sparse_categorical_accuracy: 0.9833 - 30ms/epoch - 4ms/step
...
Epoch 96/100
8/8 - 0s - loss: 0.0721 - sparse_categorical_accuracy: 0.9750 - 34ms/epoch - 4ms/step
Epoch 97/100
8/8 - 0s - loss: 0.0726 - sparse_categorical_accuracy: 0.9833 - 49ms/epoch - 6ms/step
Epoch 98/100
8/8 - 0s - loss: 0.0717 - sparse_categorical_accuracy: 0.9833 - 34ms/epoch - 4ms/step
Epoch 99/100
8/8 - 0s - loss: 0.0727 - sparse_categorical_accuracy: 0.9833 - 26ms/epoch - 3ms/step
Epoch 100/100
8/8 - 0s - loss: 0.0715 - sparse_categorical_accuracy: 0.9833 - 23ms/epoch - 3ms/step

Testing the Model

After having trained itself on 80% of the original dataset, we now need to test it on data it hasn't seen before, to check that all of the weights it configured for each node are actually accurate. First we can create a predicting function that will help us to evaluate the model:


python
iris_labels = ['Iris-setosa', 'Iris-versicolor', 'Iris-virginica']
def predict(features, labels):
    predictions = iris_model.predict(features)
    pred_df = pd.DataFrame(columns=['Actual','Prediction','Correct'])
    pred_df['Actual'] = [iris_labels[x] for x in labels]
    pred_df['Prediction'] = [iris_labels[x] for x in [np.argmax(p) for p in predictions]]
    pred_df['Correct'] = pred_df['Actual'] == pred_df['Prediction']
    return pred_df
python
iris_labels = ['Iris-setosa', 'Iris-versicolor', 'Iris-virginica']
def predict(features, labels):
    predictions = iris_model.predict(features)
    pred_df = pd.DataFrame(columns=['Actual','Prediction','Correct'])
    pred_df['Actual'] = [iris_labels[x] for x in labels]
    pred_df['Prediction'] = [iris_labels[x] for x in [np.argmax(p) for p in predictions]]
    pred_df['Correct'] = pred_df['Actual'] == pred_df['Prediction']
    return pred_df

Then we evaluate the model's performance on the test dataset:


python
iris_model.evaluate(test_features, test_labels)
test_predictions = predict(test_features, test_labels['species'].to_list())
print(test_predictions)
Output
1/1 [==============================] - 0s 137ms/step - loss: 0.1743 - sparse_categorical_accuracy: 1.0000
1/1 [==============================] - 0s 18ms/step
             Actual       Prediction  Correct
0       Iris-setosa      Iris-setosa     True
1       Iris-setosa      Iris-setosa     True
2       Iris-setosa      Iris-setosa     True
3       Iris-setosa      Iris-setosa     True
4       Iris-setosa      Iris-setosa     True
5       Iris-setosa      Iris-setosa     True
6       Iris-setosa      Iris-setosa     True
7       Iris-setosa      Iris-setosa     True
8       Iris-setosa      Iris-setosa     True
9       Iris-setosa      Iris-setosa     True
10  Iris-versicolor  Iris-versicolor     True
11  Iris-versicolor  Iris-versicolor     True
12  Iris-versicolor  Iris-versicolor     True
13  Iris-versicolor  Iris-versicolor     True
14  Iris-versicolor  Iris-versicolor     True
15  Iris-versicolor  Iris-versicolor     True
16  Iris-versicolor  Iris-versicolor     True
17  Iris-versicolor  Iris-versicolor     True
18  Iris-versicolor  Iris-versicolor     True
19  Iris-versicolor  Iris-versicolor     True
20   Iris-virginica   Iris-virginica     True
21   Iris-virginica   Iris-virginica     True
22   Iris-virginica   Iris-virginica     True
23   Iris-virginica   Iris-virginica     True
24   Iris-virginica   Iris-virginica     True
25   Iris-virginica   Iris-virginica     True
26   Iris-virginica   Iris-virginica     True
27   Iris-virginica   Iris-virginica     True
28   Iris-virginica   Iris-virginica     True
29   Iris-virginica   Iris-virginica     True
python
iris_model.evaluate(test_features, test_labels)
test_predictions = predict(test_features, test_labels['species'].to_list())
print(test_predictions)
Output
1/1 [==============================] - 0s 137ms/step - loss: 0.1743 - sparse_categorical_accuracy: 1.0000
1/1 [==============================] - 0s 18ms/step
             Actual       Prediction  Correct
0       Iris-setosa      Iris-setosa     True
1       Iris-setosa      Iris-setosa     True
2       Iris-setosa      Iris-setosa     True
3       Iris-setosa      Iris-setosa     True
4       Iris-setosa      Iris-setosa     True
5       Iris-setosa      Iris-setosa     True
6       Iris-setosa      Iris-setosa     True
7       Iris-setosa      Iris-setosa     True
8       Iris-setosa      Iris-setosa     True
9       Iris-setosa      Iris-setosa     True
10  Iris-versicolor  Iris-versicolor     True
11  Iris-versicolor  Iris-versicolor     True
12  Iris-versicolor  Iris-versicolor     True
13  Iris-versicolor  Iris-versicolor     True
14  Iris-versicolor  Iris-versicolor     True
15  Iris-versicolor  Iris-versicolor     True
16  Iris-versicolor  Iris-versicolor     True
17  Iris-versicolor  Iris-versicolor     True
18  Iris-versicolor  Iris-versicolor     True
19  Iris-versicolor  Iris-versicolor     True
20   Iris-virginica   Iris-virginica     True
21   Iris-virginica   Iris-virginica     True
22   Iris-virginica   Iris-virginica     True
23   Iris-virginica   Iris-virginica     True
24   Iris-virginica   Iris-virginica     True
25   Iris-virginica   Iris-virginica     True
26   Iris-virginica   Iris-virginica     True
27   Iris-virginica   Iris-virginica     True
28   Iris-virginica   Iris-virginica     True
29   Iris-virginica   Iris-virginica     True

It classified all 30 test plants correctly! That's encouraging, although 30 samples is a small test set, so one or two mistakes would change the accuracy a lot.

Thanks for reading - I hope this helps, and don't forget to check out the full source code on GitHub too!