diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f377c56 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +venv +.idea diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..3754756 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,17 @@ +MIT License +Copyright (c) 2018 YOUR NAME +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/MANIFEST b/MANIFEST new file mode 100644 index 0000000..8425fb4 --- /dev/null +++ b/MANIFEST @@ -0,0 +1,16 @@ +# file GENERATED by distutils, do NOT edit +setup.cfg +setup.py +micromlgen/__init__.py +micromlgen/micromlgen.py +micromlgen/micromlgen_test.py +micromlgen/templates/binary_classification.jinja +micromlgen/templates/classmap.jinja +micromlgen/templates/compute_class.jinja +micromlgen/templates/compute_decisions.jinja +micromlgen/templates/compute_kernels.bck.jinja +micromlgen/templates/compute_kernels.jinja +micromlgen/templates/compute_votes.jinja +micromlgen/templates/kernel_function.jinja +micromlgen/templates/self_test.jinja +micromlgen/templates/svm.jinja diff --git a/README.md b/README.md new file mode 100644 index 0000000..6ad73c6 --- /dev/null +++ b/README.md @@ -0,0 +1,61 @@ +# Introducing MicroML + +MicroML is an attempt to bring Machine Learning algorithms to microcontrollers. +Please refer to [this blog post](https://agrimagsrl.github.io/EloquentArduino/2019/11/introducing-microml/) +to an introduction to the topic. + +## Install + +`pip install micromlgen` + +## Use + +```python +from micromlgen import port +from sklearn.svm import SVC +from sklearn.datasets import load_iris + + +if __name__ == '__main__': + iris = load_iris() + X = iris.data + y = iris.target + clf = SVC(kernel='linear').fit(X, y) + print(port(clf)) +``` + +You may pass a classmap to get readable class names in the ported code + +```python +from micromlgen import port +from sklearn.svm import SVC +from sklearn.datasets import load_iris + + +if __name__ == '__main__': + iris = load_iris() + X = iris.data + y = iris.target + clf = SVC(kernel='linear').fit(X, y) + print(port(clf, classmap={ + 0: 'setosa', + 1: 'virginica', + 2: 'versicolor' + })) +``` + +You can pass a test set to generate self test code + +```python +from micromlgen import port +from sklearn.svm import SVC +from sklearn.datasets import load_iris + + +if __name__ == '__main__': + iris = load_iris() + X_train, X_test = iris.data[:-10, :], iris.data[-10:, :] + y_train, y_test = iris.target[:-10], iris.target[-10:] + clf = SVC(kernel='linear').fit(X_train, y_train) + print(port(clf, test_set=(X_test, y_test))) +``` \ No newline at end of file diff --git a/dist/micromlgen-0.5.tar.gz b/dist/micromlgen-0.5.tar.gz new file mode 100644 index 0000000..fdc33cf Binary files /dev/null and b/dist/micromlgen-0.5.tar.gz differ diff --git a/micromlgen/__init__.py b/micromlgen/__init__.py new file mode 100644 index 0000000..b91295a --- /dev/null +++ b/micromlgen/__init__.py @@ -0,0 +1 @@ +from micromlgen.micromlgen import port \ No newline at end of file diff --git a/micromlgen/micromlgen.py b/micromlgen/micromlgen.py new file mode 100644 index 0000000..010f256 --- /dev/null +++ b/micromlgen/micromlgen.py @@ -0,0 +1,37 @@ +import os +import re +from math import factorial +from jinja2 import FileSystemLoader, Environment + + +def port(clf, test_set=None, classmap=None, **kwargs): + assert type(clf).__name__ == 'SVC', 'Only sklearn.svm.SVC is supported for now' + support_v = clf.support_vectors_ + template_data = { + 'KERNEL_TYPE': clf.kernel, + 'KERNEL_GAMMA': clf.gamma, + 'KERNEL_COEF': clf.coef0, + 'KERNEL_DEGREE': clf.degree, + 'FEATURES_DIM': len(support_v[0]), + 'VECTORS_COUNT': len(support_v), + 'CLASSES_COUNT': len(clf.n_support_), + 'DECISIONS_COUNT': factorial(len(clf.n_support_)), + 'support_v': support_v, + 'n_support': clf.n_support_, + 'intercepts': clf.intercept_, + 'coefs': clf.dual_coef_, + 'X': test_set[0] if test_set else None, + 'y': test_set[1] if test_set else None, + 'classmap': classmap, + 'F': { + 'enumerate': enumerate, + } + } + dir_path = os.path.dirname(os.path.realpath(__file__)) + print(dir_path) + loader = FileSystemLoader(dir_path + '/templates') + template = Environment(loader=loader).get_template('svm.jinja') + code = template.render(template_data) + code = re.sub(r'\n\s*\n', '\n', code) + + return code \ No newline at end of file diff --git a/micromlgen/micromlgen_test.py b/micromlgen/micromlgen_test.py new file mode 100644 index 0000000..60b3692 --- /dev/null +++ b/micromlgen/micromlgen_test.py @@ -0,0 +1,22 @@ +import pickle +from micromlgen import port +from sklearn.svm import SVC +from sklearn.datasets import load_iris + + +if __name__ == '__main__': + test_iris = True + if test_iris: + iris = load_iris() + X = iris.data + y = iris.target + clf = SVC(kernel='linear').fit(X, y) + print(port(clf)) + else: + with open('../svmporter/datasets/svm.clf', 'rb') as file: + payload = pickle.load(file) + clf = payload['clf'] + classmap = payload['classmap'] + # test_set = (payload['X_test'], payload['y_test']) + print(port(clf, classmap=classmap)) + diff --git a/micromlgen/templates/binary_classification.jinja b/micromlgen/templates/binary_classification.jinja new file mode 100644 index 0000000..910149e --- /dev/null +++ b/micromlgen/templates/binary_classification.jinja @@ -0,0 +1,6 @@ +double decision = 0; + +decision = decision - ({% for i in range(0, n_support[0]) %} + kernels[{{ i }}] * {{ coefs[0][i] }} {% endfor %}); +decision = decision - ({% for i in range(n_support[0], n_support[0] + n_support[1]) %} + kernels[{{ i }}] * {{ coefs[0][i] }} {% endfor %}); + +return decision > 0 ? 0 : 1; \ No newline at end of file diff --git a/micromlgen/templates/classmap.jinja b/micromlgen/templates/classmap.jinja new file mode 100644 index 0000000..bb8b3a1 --- /dev/null +++ b/micromlgen/templates/classmap.jinja @@ -0,0 +1,16 @@ +{% if classmap is not none %} + +/** + * Convert class idx to readable name + */ +const char* classIdxToName(uint8_t classIdx) { + switch (classIdx) { + {% for idx, name in classmap.items() %} + case {{ idx }}: + return "{{ name }}"; + {% endfor %} + default: + return "UNKNOWN"; + } +} +{% endif %} \ No newline at end of file diff --git a/micromlgen/templates/compute_class.jinja b/micromlgen/templates/compute_class.jinja new file mode 100644 index 0000000..1b5e1d5 --- /dev/null +++ b/micromlgen/templates/compute_class.jinja @@ -0,0 +1,11 @@ + int classVal = -1; + int classIdx = -1; + + for (int i = 0; i < {{ CLASSES_COUNT }}; i++) { + if (votes[i] > classVal) { + classVal = votes[i]; + classIdx = i; + } + } + + return classIdx; \ No newline at end of file diff --git a/micromlgen/templates/compute_decisions.jinja b/micromlgen/templates/compute_decisions.jinja new file mode 100644 index 0000000..ee84e30 --- /dev/null +++ b/micromlgen/templates/compute_decisions.jinja @@ -0,0 +1,32 @@ +{% set helpers = {'ii': 0} %} + +{% for i in range(0, CLASSES_COUNT) %} + {% for j in range(i + 1, CLASSES_COUNT) %} + {% set start_i = n_support[:i].sum() %} + {% set start_j = n_support[:j].sum() %} + decisions[{{ helpers.ii }}] = {{ intercepts[helpers.ii] }} + {% for k in range(start_i, start_i + n_support[i]) %} + {% with coef=coefs[j-1][k] %} + {% if coef == 1 %} + + kernels[{{ k }}] + {% elif coef == -1 %} + - kernels[{{ k }}] + {% elif coef %} + + kernels[{{ k }}] * {{ coef }} + {% endif %} + {% endwith %} + {% endfor %} + {% for k in range(start_j, start_j + n_support[j]) %} + {% with coef=coefs[i][k] %} + {% if coef == 1 %} + + kernels[{{ k }}] + {% elif coef == -1 %} + - kernels[{{ k }}] + {% elif coef %} + + kernels[{{ k }}] * {{ coef }} + {% endif %} + {% endwith %} + {% endfor %}; + {% if helpers.update({'ii': helpers.ii + 1}) %}{% endif %} + {% endfor %} +{% endfor %} diff --git a/micromlgen/templates/compute_kernels.bck.jinja b/micromlgen/templates/compute_kernels.bck.jinja new file mode 100644 index 0000000..88ea966 --- /dev/null +++ b/micromlgen/templates/compute_kernels.bck.jinja @@ -0,0 +1,10 @@ +{% for i, v in F.enumerate(support_v) %} + {% for j in range(0, FEATURES_DIM) %} + chunkedSupportVectors[{{ i % CHUNK_SIZE }}][{{ j }}] = {{ v[j] }}; + {% endfor %} + + {% if (i + 1) % CHUNK_SIZE == 0 %} + compute_kernels(kernels, {{ i // CHUNK_SIZE * CHUNK_SIZE }}, x, chunkedSupportVectors); + {% endif %} + +{% endfor %} \ No newline at end of file diff --git a/micromlgen/templates/compute_kernels.jinja b/micromlgen/templates/compute_kernels.jinja new file mode 100644 index 0000000..e86bcc1 --- /dev/null +++ b/micromlgen/templates/compute_kernels.jinja @@ -0,0 +1,3 @@ +{% for i, w in F.enumerate(support_v) %} + kernels[{{ i }}] = compute_kernel(x, {% for j, wj in F.enumerate(w) %} {% if j > 0 %},{% endif %} {{ wj }} {% endfor %}); +{% endfor %} \ No newline at end of file diff --git a/micromlgen/templates/compute_votes.jinja b/micromlgen/templates/compute_votes.jinja new file mode 100644 index 0000000..52103a5 --- /dev/null +++ b/micromlgen/templates/compute_votes.jinja @@ -0,0 +1,8 @@ +{% set helpers = {'ii': 0} %} + +{% for i in range(0, CLASSES_COUNT) %} + {% for j in range(i + 1, CLASSES_COUNT) %} + votes[decisions[{{ helpers.ii }}] > 0 ? {{ i }} : {{ j }}] += 1; + {% if helpers.update({'ii': helpers.ii + 1}) %}{% endif %} + {% endfor %} +{% endfor %} \ No newline at end of file diff --git a/micromlgen/templates/kernel_function.jinja b/micromlgen/templates/kernel_function.jinja new file mode 100644 index 0000000..bc51f03 --- /dev/null +++ b/micromlgen/templates/kernel_function.jinja @@ -0,0 +1,29 @@ +/** + * Compute kernel between feature vector and support vector. + * Kernel type: {{ KERNEL_TYPE }} + */ +double compute_kernel(double x[{{ FEATURES_DIM }}], ...) { + va_list w; + double kernel = 0.0; + + va_start(w, {{ FEATURES_DIM }}); + + for (uint16_t i = 0; i < {{ FEATURES_DIM }}; i++) + {% if KERNEL_TYPE in ['linear', 'poly', 'sigmoid'] %} + kernel += x[i] * va_arg(w, double); + {% elif KERNEL_TYPE == 'rbf' %} + kernel += pow(x[i] - va_arg(w, double), 2); + {% else %} + #error "UNKNOWN KERNEL {{ kernel }}"; + {% endif %} + + {% if KERNEL_TYPE == 'poly' %} + kernel = pow((KERNEL_GAMMA * kernel) + KERNEL_COEF, KERNEL_DEGREE); + {% elif KERNEL_TYPE == 'rbf' %} + kernel = exp(-{{ KERNEL_GAMMA }} * kernel); + {% elif KERNEL_TYPE == 'sigmoid' %} + kernel = sigmoid((KERNEL_GAMMA * kernel) + KERNEL_COEF); + {% endif %} + + return kernel; +} \ No newline at end of file diff --git a/micromlgen/templates/self_test.jinja b/micromlgen/templates/self_test.jinja new file mode 100644 index 0000000..6496e4b --- /dev/null +++ b/micromlgen/templates/self_test.jinja @@ -0,0 +1,37 @@ +{% if X is not none %} + +/** + * Test the classifier performances on the test set + */ +void self_test() { + int correct = 0; + double X[{{ X|length }}][{{ FEATURES_DIM }}] = { + {% for x in X %} + {% if loop.index > 1 %},{% endif %} { {% for xi in x %}{% if loop.index > 1 %},{% endif %} {{ xi }} {% endfor %} } + {% endfor %} + }; + + int y[{{ X|length }}] = { {% for yi in y %}{% if loop.index > 1 %},{% endif %} {{ yi }} {% endfor %} }; + + for (int i = 0; i < {{ X|length }}; i++) { + int predicted = predict(X[i]); + + Serial.print('#'); + Serial.print(i); + Serial.print("\t Expected "); + Serial.print(y[i]); + Serial.print("\tGot "); + Serial.print(predicted); + Serial.print('\t'); + Serial.print(predicted == y[i] ? "OK\n" : "ERR\n"); + + correct += (predicted == y[i]) ? 1 : 0; + } + + Serial.print("Run {{ X|length }} predictions. "); + Serial.print(correct); + Serial.print(" were OK ("); + Serial.print(100 * correct / {{ X|length }}); + Serial.print("%)"); +} +{% endif %} \ No newline at end of file diff --git a/micromlgen/templates/svm.jinja b/micromlgen/templates/svm.jinja new file mode 100644 index 0000000..bd27038 --- /dev/null +++ b/micromlgen/templates/svm.jinja @@ -0,0 +1,25 @@ +#pragma once + +{% include 'kernel_function.jinja' %} + +/** + * Predict class for features vector + */ +int predict(double *x) { + double kernels[{{ VECTORS_COUNT }}] = { 0 }; + double decisions[{{ DECISIONS_COUNT }}] = { 0 }; + int votes[{{ CLASSES_COUNT }}] = { 0 }; + + {% include 'compute_kernels.jinja' %} + + {% if CLASSES_COUNT == 2 %} + {% include 'binary_classification.jinja' %} + {% else %} + {% include 'compute_decisions.jinja' %} + {% include 'compute_votes.jinja' %} + {% include 'compute_class.jinja' %} + {% endif %} +} + +{% include 'self_test.jinja' %} +{% include 'classmap.jinja' %} diff --git a/publish b/publish new file mode 100755 index 0000000..b745c14 --- /dev/null +++ b/publish @@ -0,0 +1,6 @@ +#!/bin/bash + +git push origin master -f +rm -rf dist/* +python setup.py sdist +twine upload dist/* \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..224a779 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[metadata] +description-file = README.md \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..ef7267b --- /dev/null +++ b/setup.py @@ -0,0 +1,29 @@ +from distutils.core import setup +setup( + name = 'micromlgen', + packages = ['micromlgen'], + version = '0.5', + license='MIT', + description = 'Generate C code for microcontrollers from Python\'s sklearn classifiers', + author = 'Simone Salerno', + author_email = 'web@agrimag.it', + url = 'https://github.com/agrimagsrl/micromlgen', + download_url = 'https://github.com/agrimagsrl/micromlgen/archive/v_05.tar.gz', + keywords = ['ML', 'microcontrollers', 'sklearn', 'machine learning'], + install_requires=[ + 'jinja2', + ], + package_data= { + 'micromlgen': ['templates/*.jinja'] + }, + classifiers=[ + 'Development Status :: 3 - Alpha', + 'Intended Audience :: Developers', + 'Topic :: Software Development :: Code Generators', + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + ], +) \ No newline at end of file