Skip to content

Getting Started on SML

Bruno Dilly edited this page Jul 20, 2015 · 19 revisions

Overview

If you're new to Soletta Machine Learning (SML), here are a few steps to create a simple software using its C API.

See Soletta Machine Learning for a introduction to the project and learn a bit about machine learning.

For instructions on how to use machine learning on flows written with Soletta, see How to add machine learning to flows

Building

Start by cloning Soletta Machine Learning git repository:

$ git clone [email protected]:solettaproject/soletta-machine-learning.git

Install the dependencies: fuzzylite, fann and soletta

fuzzylite is fuzzy logic control library written in C++:

FANN is a neural network library.

Soletta is a framework for making IoT devices. Some Soletta's data types are used by SML. Also SML provides a module for Soletta that depends on it.

CMake building system is used to build SML, so it's required to install it.

After dependencies are properly installed, it's time to build SML:

$ cd soletta-machine-learning/
$ mkdir build/
$ cd build/
$ cmake ..
$ make

Modeling the solution

Before start coding, it's a good idea to think about the problem and try to answer a few questions:

  • What is it trying to learn?
  • What would be the input and output variables?
  • What engine is better to this specific problem? See this section to learn about their differences.
  • (For fuzzy only) How can these variables change? Take on consideration the type of variables, ranges, how should they be separated on terms.

Coding

Let's consider a very simple scenario, a product composed by a presence sensor, a switch and a light bulb. So when the user gets in the her room presence sensor reads "on", the user press the switch turning the light on. When the user is leaving the room she disable the switch, turning the lights out, and the presence sensor reads "off".

The SML main flow consist in the following steps:

Choose an engine

Let's use fuzzy engine for demonstration purposes, but both engines should be do fine for such scenario.

Start creating a file hello_world.c including general purpose header sml.h and sml_fuzzy specific for the chosen engine. sml_ann.h would be included if neural network would be used.

Also, create the main() function. For now, just create a sml instance and delete it later. This instance will be used to add variables, receive values, and process this data making it possible to predict output.

#include <sml.h>
#include <sml_fuzzy.h>
#include <stdio.h>

int 
main(int argc, char **argv) {
    Sml *sml;

    sml = sml_fuzzy_new();

    // TODO: everything else

    sml_free(sml);

    return 0;
}

So after compiling it:

$ gcc hello_world.c -o hello_world `pkg-config --cflags --libs sml`

You should be able to run it:

$ ./hello_world

but it won't do anything useful yet.

Add variables

In this scenario, there are only two variables:

  • one input variable, the presence sensor, that may be 0 or 1
  • one output variable, the light state, that also can be 0 or 1 (representing on / off)

So let's declare these variables:

Sml_Variable *sensor, *light;

And add them to our example, after the sml instance creation and before its deletion - where was marked with TODO. The range (from 0 to 1), and terms must to be defined.

Terms are a way to split the values in the range in meaningful parts. Fuzzy supports some functions to describe them, as ramps, triangles and others. Since they're binary variables, using two ramps, one starting on 0 and other ending on 1, with a small overlap between them, seems good enough. This way, it will try to predict the output for any possible value being more certain about it on extremities.

To learn more about terms, see https://github.com/solettaproject/soletta/wiki/Soletta-Machine-Learning#terms.

We don't need to explicitly delete this variables or terms later, since it'll be done when sml_free() is called.

sensor = sml_new_input(sml, "PresenceSensor");
sml_variable_set_range(sml, sensor, 0, 1);
sml_fuzzy_variable_add_term_ramp(sml, sensor, "Off", 0, 0.6, 1);
sml_fuzzy_variable_add_term_ramp(sml, sensor, "On", 0.4, 1.0, 1);

light = sml_new_output(sml, "Light");
sml_variable_set_range(sml, light, 0, 1);
sml_fuzzy_variable_add_term_ramp(sml, light, "Off", 0, 0.6, 1);
sml_fuzzy_variable_add_term_ramp(sml, light, "On", 0.4, 1.0, 1);

Register read callback

It's required to set a read callback. This function will be called at each processing cycle to update variables values. Inside main(), after creating sml instance.

sml_set_read_callback(sml, read_cb, NULL);

Third argument is a pointer for any data that may be needed by this function.

So in this function the values of presence sensor and light must to be fetched. It could be done via GPIO, OIC, or any other way, depending in your product. It may be synchronous or asynchronous. But it's out of scope for this tutorial.

To keep it simple, yet illustrative, we're going to simulate the values in a function read_cb() Variable sensor_state represents presense sensor reading, switch_state represents light state. This variable will be global since its going to be used by the callback of state changes (you could use a struct with this variable and pass them to callbacks).

static int switch_state = 0;

To simulate a daily based used, a day would be a cycle of 15 readings, user will be present on last 5 readings of each "day". When she leaves she remembers to turn lights off... most of the time. This is simulated by off_count . When she forgets lights on and leaves, machine learning should suggest to turn lights off.

After states are "fetch", they're set on sml variables (input and output).

static bool
read_cb(Sml *sml, void *data)
{
    Sml_Variables_List *inputs, *outputs;
    Sml_Variable *sensor, *light;

    static int sensor_state = 0;
    static int off_count = 0;
    static unsigned int count = 0;

    // user is present after 10 reads
    if (count == 10) {
        printf("User got in the room.\n");
        sensor_state = 1;
        switch_state = 1;
        count++;
    }

    // and stay there for 5 more reads
    else if (count == 15) {
        printf("User left the room.\n");
        off_count++;
        sensor_state = 0;
        // most of times she remembers to swith off lights when she leaves
        if (off_count % 4 == 0) {
            switch_state = 0;
            printf("Oops! User forgot to turn lights off.\n");
        }
        count = 0;
    }

    // ... forever on this cycle
    else {
        count++;
    }

    inputs = sml_get_input_list(sml);
    sensor = sml_variables_list_index(sml, inputs, 0);
    sml_variable_set_value(sml, sensor, sensor_state);

    outputs = sml_get_output_list(sml);
    light = sml_variables_list_index(sml, outputs, 0);
    sml_variable_set_value(sml, light, switch_state);

    return true;
}

Register change callback

To fetch predicted output values, it's required to set a callback function using sml_set_change_state_callback()

This callback will be called when SML makes a prediction for at least one output variable.

So in main() function:

sml_set_change_state_callback(sml, change_state_cb, NULL);

When SML fails to predict the value of an output variable, it sets this value as NaN (not a number). To handle that, we should use math library.

#include <math.h>

Change state callback only will be called on output value changes. But most of the time, this prediction should matches current state of output variable. On this case no action must to be taken. Also, sometimes SML doesn't have enough information to make a prediction, setting output variable value to NaN.

So in change state callback we're going to check first if it was able to predict a value, then check if we need to act, in case prediction and current light state diverges.

In this example we'll just print a message informing the light state should be changed.

static void
change_state_cb(Sml *sml, Sml_Variables_List *changed, void *data)
{
    Sml_Variable *light;
    double prediction;

    light = sml_variables_list_index(sml, changed, 0);
    prediction = sml_variable_get_value(sml, light);

    /* When SML can't predict a value, it'll be set to NaN */
    if (isnan(prediction)) {
        printf("Sorry, can't predict light state.\n");
        return;
    }

    /* prediction is equal to current state */
    if ((prediction > 0.5) == switch_state) {
        return;
    }

    if (prediction > 0.5) {
        printf("Light should be turned ON.\n");
    } else {
        printf("Light should be turned OFF.\n");
    }
}

Call process()

Everything is set up, it's time to trigger the processing. It will be done in a simple loop, making 100 process() calls, but more elaborated ways to call process() can be implemented, using mainloops.

for (int i = 0; i < 100; i++)
{
    sml_process(sml);
}

Final code

After all these steps, code should look like this:

#include <math.h>
#include <sml.h>
#include <sml_fuzzy.h>
#include <stdio.h>


static int switch_state = 0;

static bool
read_cb(Sml *sml, void *data)
{
    Sml_Variables_List *inputs, *outputs;
    Sml_Variable *sensor, *light;

    static int sensor_state = 0;
    static int off_count = 0;
    static unsigned int count = 0;

    // user is present after 10 reads
    if (count == 10) {
        printf("User got in the room.\n");
        sensor_state = 1;
        switch_state = 1;
        count++;
    }

    // and stay there for 5 more reads
    else if (count == 15) {
        printf("User left the room.\n");
        off_count++;
        sensor_state = 0;
        // most of times she remembers to swith off lights when she leaves
        if (off_count % 4 == 0) {
            switch_state = 0;
            printf("Oops! User forgot to turn lights off.\n");
        }
        count = 0;
    }

    // ... forever on this cycle
    else {
        count++;
    }

    inputs = sml_get_input_list(sml);
    sensor = sml_variables_list_index(sml, inputs, 0);
    sml_variable_set_value(sml, sensor, sensor_state);

    outputs = sml_get_output_list(sml);
    light = sml_variables_list_index(sml, outputs, 0);
    sml_variable_set_value(sml, light, switch_state);

    return true;
}

static void
change_state_cb(Sml *sml, Sml_Variables_List *changed, void *data)
{
    Sml_Variable *light;
    double prediction;

    light = sml_variables_list_index(sml, changed, 0);
    prediction = sml_variable_get_value(sml, light);

    /* When SML can't predict a value, it'll be set to NaN */
    if (isnan(prediction)) {
        printf("Sorry, can't predict light state.\n");
        return;
    }

    /* prediction is equal to current state */
    if ((prediction > 0.5) == switch_state) {
        return;
    }

    if (prediction > 0.5) {
        printf("Light should be turned ON.\n");
    } else {
        printf("Light should be turned OFF.\n");
    }
}

int
main(int argc, char *argv[])
{
    Sml *sml;
    Sml_Variable *sensor, *light;

    sml = sml_fuzzy_new();

    sensor = sml_new_input(sml, "PresenceSensor");
    sml_variable_set_range(sml, sensor, 0, 1);
    sml_fuzzy_variable_add_term_ramp(sml, sensor, "Off", 0, 0.6, 1);
    sml_fuzzy_variable_add_term_ramp(sml, sensor, "On", 0.4, 1.0, 1);

    light = sml_new_output(sml, "Light");
    sml_variable_set_range(sml, light, 0, 1);
    sml_fuzzy_variable_add_term_ramp(sml, light, "Off", 0, 0.6, 1);
    sml_fuzzy_variable_add_term_ramp(sml, light, "On", 0.4, 1.0, 1);

    sml_set_read_callback(sml, read_cb, NULL);
    sml_set_change_state_callback(sml, change_state_cb, NULL);

    for (int i = 0; i < 150; i++)
    {
        sml_process(sml);
    }

    sml_free(sml);

    return 0;
}

Output

Fine tuning

SML supports a lot of customization, even some options specific for each engine.

To learn more about it, check the project's online API

If you prefer, it's possible to build it online

$ cd machine-learning/build/ 
$ make doc

It depends on Doxygen

Code samples

There is a directory inside soletta-machine-learning repository with a few code samples:

$ cd machine-learning/examples/

List: