-
Notifications
You must be signed in to change notification settings - Fork 108
Soletta Machine Learning: Light Sensor Tutorial
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 and its concepts.
If need to install Soletta Machine Learning please head to Soletta Machine Learning Quickstart.
For instructions on how to use machine learning on flows written with Soletta, see How to add machine learning to flows
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". If she forgets lights on and leaves, machine learning should suggest to turn lights off.
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.
In Coding Part,
- First we'll be choosing an appropriate engine for our machine learning problem.
- Then,we'll create input and output variables for our experiment.
- After that ,we'll register a callback which will be called every time new inputs are read and changes the corresponding output values.
- For Prediction Phase We have to register a change callback which will be called every time SML predicts a value.
- Now Everything is Set-up,we just have trigger the processing of SML Experiment via call_process().
The SML main flow consist in the following steps:
- Choose an engine (ANN or Fuzzy)
- Create the inputs and outputs variables
- Register the read callback
- Register the change callback
- Read the variable value, set it in SML and call process()
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) {
struct sml_object *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.
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:
struct 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. Although we have the possibility to define each term to describe your problem, for this example we will let the fuzzy engine do this for us. To improve the quality of the automatically created terms, we need to give fuzzy engine some hints about the variable being used. As both variables are boolean values, lets set the default term width to 0.5 and the range from 0 to 1. So we will have 2 terms, one for on and another for off.
To learn more about terms, see https://github.com/solettaproject/soletta/wiki/Soletta-Machine-Learning#fuzzy-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_set_default_term_width(sml, sensor, 0.5);
light = sml_new_output(sml, "Light");
sml_variable_set_range(sml, light, 0, 1);
sml_fuzzy_variable_set_default_term_width(sml, light, 0.5);
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_state_callback(sml, read_state_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_state_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_state_cb(struct sml_object *sml, void *data)
{
struct sml_variables_list *inputs, *outputs;
struct 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) {
printf("Oops! User forgot to turn lights off.\n");
} else {
switch_state = 0;
}
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;
}
To fetch predicted output values, it's required to set a callback function using sml_set_output_state_changed_callback
This callback will be called when SML makes a prediction for at least one output variable.
So in main() function:
sml_set_output_state_changed_callback(sml, output_state_changed_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
output_state_changed_cb(struct sml_object *sml,
struct sml_variables_list *changed, void *data)
{
struct 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");
}
}
Everything is set up, it's time to trigger the processing. It will be done in a simple loop, making 150 sml_process() calls, but more elaborated ways to call process() can be implemented, using mainloops.
for (int i = 0; i < 150; i++) {
if (sml_process(sml) < 0) {
printf("Failed to process\n");
}
}
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_state_cb(struct sml_object *sml, void *data)
{
struct sml_variables_list *inputs, *outputs;
struct 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) {
printf("Oops! User forgot to turn lights off.\n");
} else {
switch_state = 0;
}
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
output_state_changed_cb(struct sml_object *sml,
struct sml_variables_list *changed, void *data)
{
struct 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[])
{
struct sml_object *sml;
struct 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_set_default_term_width(sml, sensor, 0.5);
light = sml_new_output(sml, "Light");
sml_variable_set_range(sml, light, 0, 1);
sml_fuzzy_variable_set_default_term_width(sml, light, 0.5);
sml_set_read_state_callback(sml, read_state_cb, NULL);
sml_set_output_state_changed_callback(sml, output_state_changed_cb, NULL);
for (int i = 0; i < 150; i++) {
if (sml_process(sml) < 0) {
printf("Failed to process\n");
}
}
sml_free(sml);
return 0;
}
Running this sample, some messages will be written on terminal.
User got in the room.
and
User left the room.
just display the simulation state.
Message:
Oops! User forgot to turn lights off.
Indicates user failed to act. Since the program was trained to learn to keep lights off when user isn't in the room (indicated by presence sensor), the following message is expected to be displayed:
Light should be turned OFF.
In a real product it could be used to turn lights off, send user a message or any other desired behavior.
At the first times you try to retrieve SML may return NaN as result while it doesn't have enough information about current scenario. In this example, you may see a message indicating such situation in the beginning of the simulation:
Sorry, can't predict light state.
There is a directory inside soletta-machine-learning repository with a few code samples:
$ cd machine-learning/examples/
List: