Sensuator: A Hybrid Sensor-Actuator Approach to Soft Robotic Proprioception Using Recurrent Neural Networks
This is simple tutorial for loading our data, preprocessing the data, training the model and using the model to predict outputs.
Setting our options
Firstly, we set our options. These will be used later in the script.
% Which network do we want to build/train?
opt_training = TrainingOptions.BiLSTM;
% For our GRU, LSTM and BiLSTM networks, how many hidden units do we want
% For our MLP network, what size do we want for each layer?
% How long do we train for in epochs?
We have a pretrained model, set this to true to re-train from scratch.
Plot colors (for later use)
plot_tools.color(1,:) = [227,26,28]./255; % Red
plot_tools.color(2,:) = [51,160,44]./255; % Green
plot_tools.color(3,:) = [31,120,180]./255; % Blue
plot_tools.color(4,:) = [152,78,163]./255; % Purple
plot_tools.color(5,:) = [255,127,0]./255; % Orange
Loading the data
Now we can load our data into memory.
train_data = csvread('train_data.csv');
test_data = csvread('test_data.csv');
As a sanity check, we will check the size of each dataset.
The first column is the pressure sensor's reading. The next 6 columns are each ConTact sensor's voltage. And the 8th-14th & 15th - 21st column are the X and Y coordinates in pixels for each LED, respectively.
We can plot what the raw data looks like:
plot(train_data(:,1:7), 'Color', [51,160,44]./255); hold on; % Green
plot(test_data(:,1:7), 'Color', [227,26,28]./255); % Red
ylabel('Raw Sensor Reading');
plot(train_data(:,14), train_data(:,21)); hold on;
plot(test_data(:,14), test_data(:,21));
legend('Training Data: Tip Position', 'Test Data: Tip Position');
Preprocessing the data
We first split our training data to set aside a validation set. Here we'll do a simple 80/20 split.
len_train_data = length(train_data);
split_idx = floor(ratio*len_train_data);
val_idx = split_idx+1:len_train_data;
Let's specify our columns so we can use them later too. X and Y are basically our inputs and outputs to our model (not to be confused with the coordinates)
We also want the size of these for our network parameters
Now we can put our data into their respective variables
x_train = train_data(train_idx, x_col);
y_train = train_data(train_idx, y_col);
x_val = train_data(val_idx, x_col);
y_val = train_data(val_idx, y_col);
x_test = test_data(:, x_col);
y_test = test_data(:, y_col);
Normalization
Let's use simple z-score normalization to normalize our data to zero mean and unit variance. Note that we use the values from the training data to normalize our test data.
x_train = (x_train - x_bar) ./ x_sd;
x_val = (x_val - x_bar) ./ x_sd;
y_train = (y_train - y_bar) ./ y_sd;
y_val = (y_val - y_bar) ./ y_sd;
x_test = (x_test - x_bar) ./ x_sd;
y_test = (y_test - y_bar) ./ y_sd;
Smoothing
For the training data only, let's smooth out the raw data so our network learns better. We use 3rd order Butterworth low pass filter with a cutoff at 5 Hz. We do not do the same with the test data.
fs = 50; % Sampling frequency (Hz)
fc = 5; % Low pass cut-off frequency (Hz)
[b,a] = butter(3, Wn, 'low'); % 3rd order Butterworth low pass filter
% Filtfilt is used to negate any phase shift caused by the filter
x_train = filtfilt(b,a,x_train);
y_train = filtfilt(b,a,y_train);
x_val = filtfilt(b,a,x_val);
y_val = filtfilt(b,a,y_val);
Lastly, we have to transpose the data for the Deep Learning Toolbox.
Constructing the network
Now let's construct our network based on the options we chose above. We can also construct these networks using the Deep Network Designer tool.
case TrainingOptions.LSTM
sequenceInputLayer(inputs)
lstmLayer(opt_hidden_units)
fullyConnectedLayer(outputs)
sequenceInputLayer(inputs)
gruLayer(opt_hidden_units)
fullyConnectedLayer(outputs)
case TrainingOptions.BiLSTM
sequenceInputLayer(inputs)
bilstmLayer(opt_hidden_units)
fullyConnectedLayer(outputs)
sequenceInputLayer(inputs)
fullyConnectedLayer(inputs)
fullyConnectedLayer(opt_mlp_size)
fullyConnectedLayer(opt_mlp_size)
fullyConnectedLayer(opt_mlp_size)
fullyConnectedLayer(opt_mlp_size)
fullyConnectedLayer(outputs)
Then we set our hyperparameters. We've set these parameters to the values presented in the paper (Table 1).
opts = trainingOptions('adam', ...
'MaxEpochs',opt_max_epoch, ...
'GradientThreshold',10, ...
'ValidationData',{x_val,y_val}, ...
'ValidationFrequency',5, ...
'InitialLearnRate',0.01, ...
'LearnRateSchedule','piecewise', ...
'LearnRateDropPeriod',5, ...
'LearnRateDropFactor',0.5, ...
'Plots','training-progress', 'ExecutionEnvironment', 'cpu');
Training
It's time to train! This will take a while if you have retrain set to true.
if isfile('sensuator_model.mat') && ~opt_retrain
load('sensuator_model.mat');
[net, net_info] = trainNetwork(x_train, y_train, layers, opts);
% Save the model for later use. (Plotting, experiments, degradation, etc.)
save('sensuator_model', 'net', 'net_info');
Using the model
Let's now use the model to predict pixels from sensor data!
Firstly, let's fit our training data, to see how it performs with our training data.
[net_train_fit, y_pred_train] = predictAndUpdateState(net, x_train);
Then, let's give our test data a try
[net_test_fit, y_pred_test] = predictAndUpdateState(net, x_test);
De-normalization of the outputs
Let's de-normalize our data so we can plot it.
y_pred_train_plot = y_pred_train';
y_pred_train_plot = (y_pred_train_plot .* y_sd) + y_bar;
y_train_plot = (y_train_plot .* y_sd) + y_bar;
y_test_plot = (y_test_plot .* y_sd) + y_bar;
y_pred_test_plot = y_pred_test';
y_pred_test_plot = (y_pred_test_plot .* y_sd) + y_bar;
% Save the data for later use. So we don't have to run everything again
% just to generate new plots.
save('model_plot_data','y_pred_train_plot','y_train_plot','y_test_plot','y_pred_test_plot');
Sanity check: Plotting the training data
Now let's take a peek at how well our model performs with the training data
figure(3); hold on; grid on; box on;
plot(y_train_plot(plot_time,1),'Color', plot_tools.color(2,:), 'LineWidth', 0.8);
plot(y_pred_train_plot(plot_time,1),'Color', plot_tools.color(1,:), 'LineWidth', 0.8);
plot(y_train_plot(plot_time,2:14),'Color', plot_tools.color(2,:), 'LineWidth', 0.8);
plot(y_pred_train_plot(plot_time,2:14),'Color', plot_tools.color(1,:), 'LineWidth', 0.8);
ylabel('LED Positions (X and Y in pixels)');
legend('Training Data', 'Training Predictions');
Not bad!
Trying the model on the test data
Now let's try the test data:
figure(4); clf; hold on; grid on; box on;
plot(y_test_plot(plot_time,1),'Color', plot_tools.color(2,:), 'LineWidth', 0.8);
plot(y_pred_test_plot(plot_time,1),'Color', plot_tools.color(1,:), 'LineWidth', 0.8);
plot(y_test_plot(plot_time,2:14),'Color', plot_tools.color(2,:), 'LineWidth', 0.8);
plot(y_pred_test_plot(plot_time,2:14),'Color', plot_tools.color(1,:), 'LineWidth', 0.8);
ylabel('LED Positions (X and Y in pixels)');
legend({'Actual', 'Predictions'},'location','NorthEast');