diff --git a/SimoxUtility/CMakeLists.txt b/SimoxUtility/CMakeLists.txt index b39459da876abe4caebf4d4c65af2b4b22101683..dd2a7c433bd57f01791724dba4458e9fb94f9e03 100644 --- a/SimoxUtility/CMakeLists.txt +++ b/SimoxUtility/CMakeLists.txt @@ -40,6 +40,8 @@ SET(SOURCES math/pose/orthogonalize.cpp math/pose/interpolate.cpp + math/regression/linear3d.cpp + math/statistics/Histogram1D.cpp meta/type_name.cpp @@ -187,6 +189,8 @@ SET(INCLUDES math/pose/transform.h math/pose/interpolate.h + math/regression/linear3d.h + math/similarity/cosine_similarity.h math/similarity/angular_similarity.h diff --git a/SimoxUtility/math.h b/SimoxUtility/math.h index 831dc4ca47cab6ec3d30ed744ca82479c25e5b6a..b57cef0caa0dce7c2474a110f47e0d200eec07a6 100644 --- a/SimoxUtility/math.h +++ b/SimoxUtility/math.h @@ -14,6 +14,7 @@ #include "math/periodic_clamp.h" #include "math/pose.h" #include "math/project_to_plane.h" +#include "math/regression.h" #include "math/rescale.h" #include "math/scale_value.h" #include "math/similarity.h" diff --git a/SimoxUtility/math/regression.h b/SimoxUtility/math/regression.h new file mode 100644 index 0000000000000000000000000000000000000000..1e6131d854a53b1695f052cb1de44b7f91765313 --- /dev/null +++ b/SimoxUtility/math/regression.h @@ -0,0 +1,5 @@ +#pragma once + +// This file is generated! + +#include "regression/linear3d.h" diff --git a/SimoxUtility/math/regression/linear3d.cpp b/SimoxUtility/math/regression/linear3d.cpp new file mode 100644 index 0000000000000000000000000000000000000000..ce4fa0eb132307fe674cb533f162e6a44d9539ef --- /dev/null +++ b/SimoxUtility/math/regression/linear3d.cpp @@ -0,0 +1,86 @@ +#include "linear3d.h" + +#include <Eigen/Dense> + + +namespace simox::math +{ + + LinearRegression3D + LinearRegression3D::Fit( + const std::vector<double>& xs, + const std::vector<Eigen::Vector3d>& ys, + bool offsetInput) + { + if (offsetInput and xs.at(0) != 0) + { + double offset = - xs.at(0); // Move x_0 to 0. + std::vector<double> virtualXs = xs; + for (double& x : virtualXs) + { + x = x + offset; + } + LinearRegression3D r = LinearRegression3D::Fit(virtualXs, ys, false); + r.inputOffset = offset; + return r; + } + + Eigen::Matrix3Xd ysMatrix(3, ys.size()); + for (long col = 0; col < ysMatrix.cols(); ++col) + { + ysMatrix.col(col) = ys.at(col); + } + + // The matrix of the predictor functions evaluated at the corresponding xs. + // Since this is a linear regression, the constant function a(t) = 1 and identity + // b(t) = t are used. + Eigen::MatrixX2d linFuncMatrix(xs.size(), 2); + linFuncMatrix.col(0) = Eigen::RowVectorXd::Ones(xs.size()); + linFuncMatrix.col(1) = Eigen::Map<const Eigen::VectorXd>(xs.data(), xs.size()); + + // `linFuncMatrix` is poorly conditioned for xs that are close together + // (e.g. time stamps), so the normal equation would loose a lot of precision. + auto qrDecomp = linFuncMatrix.colPivHouseholderQr(); + + // Each coordinate can be treated individually (general multivariate regression). + // `coeffs` contains a_i and b_i in a_0 + b_0 * t = x, a_1 + b_1 * t = y, etc. + Eigen::Matrix<double, 3, 2> coeffs; + for (int dim = 0; dim < 3; ++dim) + { + Eigen::VectorXd coords = ysMatrix.row(dim).transpose(); + coeffs.row(dim) = qrDecomp.solve(coords); + } + + return LinearRegression3D { .coefficients = coeffs }; + } + + + Eigen::Vector3d + LinearRegression3D::predict(double x) const + { + Eigen::Vector2d input; + input << 1.0, x + inputOffset; + return coefficients * input; + } + +} + + +std::ostream& +simox::math::operator<<(std::ostream& os, const LinearRegression3D& r) +{ + os << "<LinearRegression3D y = a + b * x with [ "; + for (Eigen::Index row = 0; row < r.coefficients.rows(); ++row) + { + if (row != 0) + { + os << " | "; + } + os << "y_" << row + << " = " << r.coefficients(row, 0) + << " + " << r.coefficients(row, 1) << " * x" + ; + } + os << " ] and input offset " << r.inputOffset << ">"; + return os; +} diff --git a/SimoxUtility/math/regression/linear3d.h b/SimoxUtility/math/regression/linear3d.h new file mode 100644 index 0000000000000000000000000000000000000000..6807c0bc50a813585cec9f92f18856443b03119b --- /dev/null +++ b/SimoxUtility/math/regression/linear3d.h @@ -0,0 +1,76 @@ +#pragma once + +#include <Eigen/Core> + + +namespace simox::math +{ + + /** + * @brief A linear regression model of the form y = a + b * x, + * or per dimension, y_i = a_i + b_i * x. + * + * - x is the scalar input variable (e.g. time) + * - y is 3D vector output variable (e.g. position) + * - a is a 3D bias vector + * - b is a 3D slope vector + * + * In matrix notation, the equation system represented by the model is: + * + * [[ a_0 b_0 ] [ 1 ] [ y_0 ] + * [ a_1 b_1 ] * [ x ] = [ y_1 ] + * [ a_2 b_2 ]] [ y_2 ] + * + * Given data x[j] in R and y[j] \in R^3 (j = 0 .. n-1), + * the regression solves the following equation system(s) + * for [ a_i, b_i ] (i = 0 .. (3-1)): + * + * [[ 1 x[0] ] [ y[0]_i ] + * [ ... ] [ a_i ] [ ... ] + * [ 1 x[j] ] * [ b_i ] = [ y[j]_i ] + * [ ... ] [ ... ] + * [ 1 x[n-1] ]] [ y[n-1]_i ] + */ + class LinearRegression3D + { + public: + + using CoefficientsMatrix = Eigen::Matrix<double, 3, 2>; + + /** + * The coefficients of the bias term and input variable x + * [[ a_0 b_0 ] + * [ a_1 b_1 ] + * [ a_2 b_2 ]] + */ + CoefficientsMatrix coefficients = CoefficientsMatrix::Zero(); + + /// The input offset, so the virtual input x' = x + offset. + double inputOffset = 0; + + + /** + * @brief Fit a linear regression model to the given data. + * @param xs The input variables. + * @param ys The output variables. + * @param offsetInput If true, the inputs are offset to x' = x - x_0. + * @return The regression model. + */ + static LinearRegression3D + Fit(const std::vector<double>& xs, + const std::vector<Eigen::Vector3d>& ys, + bool offsetInput = false); + + /** + * @brief Predict the output variable of the given input variable. + * @param x The input variable. + * @return The predicted output variable. + */ + Eigen::Vector3d predict(double x) const; + + }; + + + std::ostream& operator<<(std::ostream& os, const LinearRegression3D& r); + +} diff --git a/SimoxUtility/tests/math/CMakeLists.txt b/SimoxUtility/tests/math/CMakeLists.txt index 0e9e6d7f60b91ca657eb84535062f981e080e27b..a3558c2192c3a02fd5dc2762b2929d833546f476 100644 --- a/SimoxUtility/tests/math/CMakeLists.txt +++ b/SimoxUtility/tests/math/CMakeLists.txt @@ -8,4 +8,4 @@ ADD_SU_TEST( SoftMinMax ) ADD_SU_TEST( statistics ) ADD_SUBDIRECTORY(pose) - +ADD_SUBDIRECTORY(regression) diff --git a/SimoxUtility/tests/math/regression/CMakeLists.txt b/SimoxUtility/tests/math/regression/CMakeLists.txt new file mode 100644 index 0000000000000000000000000000000000000000..fab80e952c987a730acc2bd292e52736646cd80c --- /dev/null +++ b/SimoxUtility/tests/math/regression/CMakeLists.txt @@ -0,0 +1 @@ +ADD_SU_TEST( linear3d ) diff --git a/SimoxUtility/tests/math/regression/linear3d.cpp b/SimoxUtility/tests/math/regression/linear3d.cpp new file mode 100644 index 0000000000000000000000000000000000000000..a235d0a6d5d1a6cd11532df66a52f889ff652dc9 --- /dev/null +++ b/SimoxUtility/tests/math/regression/linear3d.cpp @@ -0,0 +1,95 @@ +/** +* @package SimoxUtility +* @author Raphael Grimm +* @copyright 2019 Raphael Grimm +*/ + +#define BOOST_TEST_MODULE SimoxUtility_RegressionTest + +#include <random> +#include <iostream> + +#include <boost/test/included/unit_test.hpp> + +#include <SimoxUtility/math/regression/linear3d.h> + + + +namespace SimoxMathRegressionTest +{ + +struct Fixture +{ + const double prec = 1e-10; + + std::function<Eigen::Vector3d(double)> f = [](double x) + { + Eigen::Vector3d y; + for (Eigen::Index i = 0; i < y.rows(); ++i) + { + y(i) = - (1 + i) + (2 * i) * x; + } + return y; + }; + const std::vector<double> xs + { + -1, 0, 2 + }; + const std::vector<Eigen::Vector3d> ys + { + f(xs[0]), f(xs[1]), f(xs[2]) + }; + +}; +} + + +BOOST_FIXTURE_TEST_SUITE(SimoxMathRegressionTest, Fixture) + + +BOOST_AUTO_TEST_CASE(test_linear_regression_3d_fit_and_predict) +{ + using simox::math::LinearRegression3D; + + // Fit + + const LinearRegression3D regression = LinearRegression3D::Fit(xs, ys); + + BOOST_TEST_MESSAGE("Regression: " << regression); + + BOOST_CHECK_CLOSE(regression.coefficients(0, 0), - (1 + 0), prec); + BOOST_CHECK_CLOSE(regression.coefficients(1, 0), - (1 + 1), prec); + BOOST_CHECK_CLOSE(regression.coefficients(2, 0), - (1 + 2), prec); + + BOOST_CHECK_CLOSE(regression.coefficients(0, 1), (2 * 0), prec); + BOOST_CHECK_CLOSE(regression.coefficients(1, 1), (2 * 1), prec); + BOOST_CHECK_CLOSE(regression.coefficients(2, 1), (2 * 2), prec); + + + // Predict + + BOOST_CHECK_LE((regression.predict(xs[0]) - ys[0]).norm(), prec); + BOOST_CHECK_LE((regression.predict(xs[1]) - ys[1]).norm(), prec); + BOOST_CHECK_LE((regression.predict(xs[2]) - ys[2]).norm(), prec); +} + + +BOOST_AUTO_TEST_CASE(test_linear_regression_3d_fit_and_predict_with_input_offset) +{ + using simox::math::LinearRegression3D; + + const bool inputOffset = true; + const LinearRegression3D regression = LinearRegression3D::Fit(xs, ys, inputOffset); + + BOOST_TEST_MESSAGE("Regression: " << regression); + BOOST_CHECK_EQUAL(regression.inputOffset, - xs[0]); + + // Coefficients are different now, but prediction should be the same. + + BOOST_CHECK_LE((regression.predict(xs[0]) - ys[0]).norm(), prec); + BOOST_CHECK_LE((regression.predict(xs[1]) - ys[1]).norm(), prec); + BOOST_CHECK_LE((regression.predict(xs[2]) - ys[2]).norm(), prec); +} + + +BOOST_AUTO_TEST_SUITE_END()