Train your own TensorFlow Models - AI for the web. | Towards Data Science

Training your own TensorFlow Lite models provides you with an opportunity to create your own custom AI applications. Further, using a runtime such as WasmEdge provides you with an opportunity to run your custom TensorFlow applications on many different platforms. Including shared hardware. More on shared hardware in a minute ...
WasmEdge is a lightweight and high-performance WebAssembly (WASM) Virtual Machine (VM) which is optimized for edge computing. It is widely used in scenarios such as cloud serverless functions (FaaS), Software as a Service (SaaS), Blockchain smart contracts & IoT, as well as, real-time automotive applications.
Hosted by CNCF, WasmEdge aims to become an open source “reference implementation” of Wasm and its edge-related extensions.
This article focussed mainly on WasmEdge’s TensorFlow extensions. You can learn more about WasmEdge via GitHub; if you would like to contribute to WasmEdge, please see this contributor document.
The Wasm ecosystem is still in its early days. However Wasm has some very important and unique advantage over traditional execution environments.
Aside from its performance ...
“In its Ahead Of Time (AOT) compiled mode, WasmEdge is the fastest Wasm VM on the market today.” [1]
... one particular unique proposition is that Wasm executables run in a stand-alone, stack-based Wasm VM. This means that any untrusted Wasm programs can run alongside other Wasm programs on shared hardware; with no side effects.
This is a huge deal in terms of the future of manufacturing. For example Toyota has recently announced that they will “cut car production by 40 per cent over global chip shortage” [2].
All fields of consumer electronics, no matter what the genre, have a constant drive to satisfy consumer demands. These demands include different varieties of entertainment, infotainment, safety, anti-spam, voice recognition, object detection and much more.
Being able to train and run sophisticated AI applications (which can assist in satisfying these consumer demands) on shared hardware is paramount to production. Here are a few quotes from global manufacturers which drive home this point.
Volkswagen
“Germany’s Volkswagen said it also may need to cut production further and that it expected the supply of chips in the third quarter to be ‘very volatile and tight’.”
Ford
“Ford Motors said would temporarily shut its Kansas City assembly plant that builds its best-selling F-150 pickup truck due to a semiconductor-related part shortage”
The best way to start learning about how to train TensorFlow is to look at some ready made TensorFlow training data.
Our first example is specifically aimed at natural language sentiment analysis. We are going to inspect some training data. Once you have an understanding of this specific dataset, you can then go ahead and create your own training data and then train and run the natural language model for a specific use case (whatever that may be).
In our second example, we are also going to start by looking at some ready made data; diving into the details about bounding box coordinates and so forth.
Once we have an understanding about how Single Shot Detector (SSD) bounding boxes work, we can create our own training data and then train and run the SSD model for specific use cases.
Let’s get started with some hands-on demonstrations; please keep reading even if you are not coding. There are many diagrams and explanations which will make this easy to understand.
Before we start, let’s run a few commands to get our system ready. We install TensorFlow, TensorFlow Model Maker, Numpy and also Pandas.
pip3 install tensorflowpip3 install tflite-model-makerpip3 install numpy~=1.19.2pip3 install pandas
Then we open a python3 interpreter and run the following code.
import osimport numpy as npimport pandas as pdimport tensorflow as tffrom tflite_model_maker import model_specfrom tflite_model_maker import text_classifierfrom tflite_model_maker.config import ExportFormatfrom tflite_model_maker.text_classifier import AverageWordVecSpecfrom tflite_model_maker.text_classifier import DataLoader# Get the datadata_dir = tf.keras.utils.get_file(      fname='SST-2.zip',      origin='https://dl.fbaipublicfiles.com/glue/data/SST-2.zip',      extract=True)data_dir = os.path.join(os.path.dirname(data_dir), 'SST-2')
You can see here, as part of this setup, that we are downloading a public dataset from fbaipublicfiles.com. We need to go and look at that public data because it is the key to us understanding how to create our own data.
If we print the data_dir we can see the data has been saved to our home directory (inside a hidden .keras folder).
print(data_dir)/Users/tpmccallum/.keras/datasets/SST-2
If we look at this data we can see that the dataset is in a format which is not suitable for TensorFlow Lite ... yet!
We can fix that :)
To make this data TensorFlow Lite compatible we perform the following tasks.
First we convert the tab separated data file dev.tsv to a comma separated file and also replace the labels (turning 0 into the word negative, and 1 into the word positive as shown in the output spreadsheet image below).
df = pd.read_csv('/Users/tpmccallum/.keras/datasets/SST-2/dev.tsv', sep='\t')label_map = {0: 'negative', 1: 'positive'}df.replace({'label': label_map}, inplace=True)df.to_csv('/Users/tpmccallum/.keras/datasets/SST-2/dev.csv')replace_label(os.path.join(os.path.join(data_dir, 'dev.tsv')), 'dev.csv')
We also do this for the train.tsv file
df = pd.read_csv('/Users/tpmccallum/.keras/datasets/SST-2/train.tsv', sep='\t')label_map = {0: 'negative', 1: 'positive'}df.replace({'label': label_map}, inplace=True)df.to_csv('/Users/tpmccallum/.keras/datasets/SST-2/train.csv')replace_label(os.path.join(os.path.join(data_dir, 'train.tsv')), 'train.csv')
Then finally we do this again for the test.tsv file.
df = pd.read_csv('/Users/tpmccallum/.keras/datasets/SST-2/test.tsv', sep='\t')label_map = {0: 'negative', 1: 'positive'}df.replace({'label': label_map}, inplace=True)df.to_csv('/Users/tpmccallum/.keras/datasets/SST-2/test.csv')replace_label(os.path.join(os.path.join(data_dir, 'test.tsv')), 'test.csv')
Ok, great! Now we have some meaningful TensorFlow Lite compatible data that we can use and replicate (using our own sentences and labels).
Each of the 3 spreadsheets ( dev.csv, train.csv and test.csv) have two columns (sentence and label).
The sentence is either positive or negative. For example the sentence “unflinchingly bleak and desperate” is correctly labelled negative.
Understanding this outline enables you to now go and create your own training and testing data. You would simply add sentences and then correctly/accurately label them (and then store your custom files in place of these original ones).
We can now load these csv files and then perform the training.
spec = model_spec.get('average_word_vec')train_data = DataLoader.from_csv(filename='train.csv', text_column='sentence', label_column='label', model_spec=spec, is_training=True)test_data = DataLoader.from_csv(filename='dev.csv', text_column='sentence', label_column='label', model_spec=spec, is_training=False)# Trainmodel = text_classifier.create(train_data, model_spec=spec, epochs=10)
The training process will create output like the following.
Epoch 1/101/2104 [..............................]2104/2104 [==============================] - 3s 1ms/step - loss: 0.6841 - accuracy: 0.5570// snip //Epoch 10/102104/2104 [==============================] - 2s 1ms/step - loss: 0.3340 - accuracy: 0.8647
We can now perform a test on the trained model
# Testloss, acc = model.evaluate(test_data)
The output from the training is as follows
28/28 [==============================] - 0s 1ms/step - loss: 0.5160 - accuracy: 0.8303
If we are happy with these results, we can then export the trained model as a .tflite file; which we can then use in our application (more on creating the application soon).
model.export(export_dir='average_word_vec')
If we open the newly created TensorFlow Lite compatible file (model.tflite) in netron app, we can see the input and output specification. The specifications will help us write our application.
As we can see the input is called input_1 and the output is called Identity .
The description in the model properties describes the data format for the input. Specifically ...
Embedding vectors representing the input text to be classified. The input needs to be converted from raw text to embedding vectors using the attached dictionary file.
Essentially this input wants the text (which the user provides) to be converted to one single array of individual i32 integers i.e. [5043, 201023, ... , 29361, 3499]. So we need to learn how the model’s creators want us to map words to numbers.
By investigating the data, we learned that the creators of this model have used a dictionary file which maps words/phrases to numbers. The dictionary file has one unique integer per line (which corresponds to text).
For example on line 85, 155 we have the word “awesome” and the integer 29361 (as shown below).
awesome|29361
So in our application, if a user types the word “awesome”, we need to send the integer of 29361 to the model (as one of the many elements in the i32 array which the model is expecting).
It is up to you whether you do that on the client side or the server side. The programming language is your own personal preference; I have created some Rust code below, which shows how you would read the dictionary from your local file system and then create a HashMap out of the data. However, we can also read this file over the web instead; allow me to explain in the next section.
    // Create HashMap which stores String and Integer pairs    let mut map_name: HashMap<String, i32> = HashMap::new();    // Read the dictionary file     let filename = "src/dictionary.txt";    let file = File::open(filename).unwrap();    let reader = BufReader::new(file);    // Process each line    for (index, line) in reader.lines().enumerate() {        let line = line.unwrap();        // Create vector to store each line as split data        let v: Vec<&str> = line.split(|c| c == '|').collect();        // Place the split data into the HashMap        map_name.insert(v[0].to_string(), v[1].parse::<i32>().unwrap());    }    // Create another vector to hold the input data    let size = 256;    let mut vecForModel: Vec<i32> = Vec::with_capacity(size);    // Split this functions input by space    let vInputString: Vec<&str> = input_string.split(|c| c == ' ').collect();    // See if any words are in the HashMap    for word in vInputString {        if map_name.contains_key(word){            // If so, add the appropriate integer            vecForModel.push(*map_name.get(word).unwrap());        }
In the interests of showing you how to create AI applications for the web, let’s parse this dictionary file remotely.
WasmEdge’s RESTful API can actually remotely fetch this dictionary file for us; analogous to using Content Deliver Network (CDN), as opposed to storing files locally. This has a couple of advantages.
Firstly, the dictionary file can be updated (at its remote location) by the application developer without the user needing to do anything.
Secondly, the fetching and reading of the file is now done 100% on the server side (the client does not need to do any of this high bandwidth work). The user of our AI application (on the client side) only uploads their sentence and the URL pointer to the dictionary file. We must remember that this dictionary file is over two hundred thousand lines long (239, 232) and can potentially grow as new words/language are added.
The above screen capture show that this is just a text file.
It is much quicker to fetch this sort of large text file remotely and the heavy lifting is better suited to the server side. The diagram below illustrates how the remote fetch works.
The best way to make this AI application available is to use form data (i.e. a web form). Let’s see what the web form would look like.
Aside from a couple of input boxes (in the HTML), we would have a Javascript function which would take the inputs and then call WasmEdge’s RESTful API.
Here is the Javascript; essentially just an AJAX request.
function callService() {          setTimeout(function() {            $('#process').prop('disabled', true);          }, 0);          var formData = new FormData();          formData.append('input_1', $('#input_1').val());          formData.append('fetch_input_2', $('#input_2').val());          console.log("Running ...");          $.ajax({            url: "https://rpc.ssvm.secondstate.io:8081/api/multipart/run/392/classify",            type: "post",            cache: false,            data: formData,            contentType: false,            processData: false,            xhrFields: {              responseType: "" // defaults to text            },            success: function(r_data) {              console.log("Successfully ran the ajax");              document.getElementById("result_box").innerHTML = r_data;              $('#process').prop('disabled', false);            },            error: function(r_error) {              console.log("Error running the ajax: " + r_error);              alert("Rate limit exceeded");              $('#process').prop('disabled', false);            }          });          console.log("END");          return false;        }
Just a reminder that WasmEdge can be run on almost any device. For the sake of this article we are going to use the web so that you can see it in action.
As you can see from the Javascript code above, WasmEdge has an API endpoint which can be called by any device which is able to make a secure HTTP request. Obviously for automotive use the programs would be run on local hardware and not over the web.
However, just to demonstrate WasmEdge’s versatility, let’s take a closer look at WasmEdge’s API endpoint structure.
You will notice that this URL ends with a function’s name, in this case classify. This is actually the name of the Rust function which performs all of the logic (executes the TensorFlow model). For demonstration purposes, the Rust function has already been written, compiled to Wasm and deployed at wasm_id 392 (as shown in the URL above). Don’t worry, we will take a deep dive into the Rust/Wasm very soon. For now let’s just focus on the application’s frontend (client side).
The following simple curl command can be used to execute our pre-written Wasm binary. Notice how we add two --form parameters (which let curl emulate a filled-in form). Notice that the second input is actually a remote fetch of the large dictionary file which we spoke about earlier.
Hint: When performing a remote fetch, use a URL which does not undergo redirects i.e. if storing data in GitHub then use raw.githubusercontent style of URL as shown below.
curl --location --request POST 'https://rpc.ssvm.secondstate.io:8081/api/multipart/run/392/classify' --form 'input_1="Awesome movie"' --form 'fetch_input_2="https://raw.githubusercontent.com/tpmccallum/Tim2/main/dictionary_testing.txt"'
Once the curl works we can go ahead and create a HTML form for our application. The one we prepared earlier looks like the following image, you can access the HTML source code here.
This same page is also available as a live demo via GitHub pages.
Now that we have covered the HTML and Javascript, let’s look at the executable code which we are actually calling via the WasmEdge API.
This whole application is powered by a Rust function, here is a link to the Rust source code we prepared earlier. Notice the function is called classify which matches the API URL above.
We compile the Rust source code to WebAssembly (Wasm) using a single command.
rustwasmc build
As mentioned above, WasmEdge is able to run this in many modes i.e. via NodeJS, on embedded devices and so forth. However, we are going to continue to use WasmEdge’s API for this demonstration so you can try it out.
Let’s see how to interpret the output.
As we can see in the model’s properties, the output is called “Identity”. Identity is a single array with two elements (both of data type float 32). The first is the index 0-1 relating to negative sentiment. For example, 0.25 means it has 1/4 of negative connotations and 1 means it is a totally negative comment.
The second element represents positive sentiment.
The 2 elements in the array will always both add up to a total of 1. For example neutral would be 0.5 negative and 0.5 positive.
Let’s try some sentences and see what we get.
The sentence of ...
“Terminally bland , painfully slow.”
resulted in:
a high negative index of 1
a low positive index of 0
In contrast the sentence of ...
“This film was excellent. Great actors, good plot and entertaining.”
resulted in:
a low negative index of 0.09839419
a higher positive index of 0.9016058
Just to dive a little deeper, before we wrap up the natural language example, I have taken the liberty of pasting in the output from the logs on the WasmEdge server side. The output below shows all of the println! statements in the Rust source code; essentially showing how we remotely fetched the dictionary, took the user’s input and then created a 256 element array for the model which contains the numeric representation for any word/integer matches (during the cross referencing of the input with the dictionary file).
Input string: "This film was excellent. Great actors, good plot and entertaining"Processing word: "this"Processing word: "film"Processing word: "was"Processing word: "excellent."Processing word: "great"Processing word: "actors,"Processing word: "good"Processing word: "plot"Processing word: "and"Processing word: "entertaining"Processing loading the modelFinal Vec For Model: [2838, 976, 3070, 6874, 1092, 9754, 408, 6224, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]Adding outputRunning sessionThe words you provided have a negative connotation index of 0.09839419, and a positive connotation index of 0.9016058.
That wraps up the first example, now let’s move onto another example; object detection. You may have seen this sort of AI in driverless car demonstrations. Object detection has a wide range of uses, especially in engineering and manufacturing industries.
As with the natural language example above, we will first install some dependencies
pip3 install testresourcespip3 install pycocotoolspip install grpcio==1.32pip3 install tflite-model-maker
Then enter a Python interpreter by typing python3.
import numpy as npimport osfrom tflite_model_maker.config import ExportFormatfrom tflite_model_maker import model_specfrom tflite_model_maker import object_detectorimport tensorflow as tfassert tf.__version__.startswith('2')tf.get_logger().setLevel('ERROR')from absl import logginglogging.set_verbosity(logging.ERROR)
For this particular object detection, we will be using the Salads dataset from gs://cloud-ml-data/img/openimage/csv/salads_ml_use.csv
The aforementioned salads_ml_use.csv follows a specific convention. Here are the conditions for creating your own csv file for training purposes.
The goal is to understand the model, so that you can go ahead and train it on any images/objects which you like; not just salad 😊
The csv file:
Must be UTF-8 encoded
Must end with a .csv extension
Has one row for each bounding box in the set
Contains one image per line; an image with multiple bounding boxes will be repeated on as many rows as there are bounding boxes
Each label must start with a letter and only contain letters, numbers, and underscores.
Each bounding box for the specific object in the image (on that row of the csv file) can be expressed in one of two ways.
With only 2 vertices (consisting of a set of x,y coordinates) if they are diagonally opposite points of the rectangle.
With only 2 vertices (consisting of a set of x,y coordinates) if they are diagonally opposite points of the rectangle.
For example:
(x_relative_min,    y_relative_min,    ,    ,    x_relative_max,    y_relative_max,    ,)
2. With all 4 vertices
For example
(x_relative_min,    y_relative_min,    x_relative_max,    y_relative_min,    x_relative_max,    y_relative_max,    x_relative_min,y_relative_max)
As you can see from the examples, These coordinates must be a float in the 0 to 1 range, where 0 represents the minimum x or y value, and 1 represents the greatest x or y value.
As we mentioned at the start of this article, it is really important that we understand the data, so that we can create our own training data. These coordinates can be a little confusing so let’s take a look at a diagram which will make this clearer. Firstly, as you can see min & max is ordered from top left to bottom right. So don’t be confused or try and relate this to a traditional data plotting exercise (this is different).
The following diagram adds a little more detail; demonstrates where the coordinates of bounding boxes are situated.
For example, x_min = 0, x_max = 1, y_min = 0 and y_max = 1
We are going to use a ready made dataset in a minute, but now that we understand how this data works, let’s take a look at how we would create our own dataset from scratch.
There are many applications which allow you to manually create bounding boxes with labels on your own images. One such application is called labelImg.
Another application, which we are using in this article, is called MakeML.
The image below shows how we took a picture of a desk and then identified the laptop (by drawing a yellow bounding box and creating a label called “laptop”).
We then identify the laptop’s keyboard (by drawing a purple box and creating a label called “keyboard”).
After drawing one bounding box on each of your images (the more the better), you then go ahead and click the “Export dataset” link in the lower left corner.
There are a few different data formats to choose from. Let’s compare them.
The Turicreate compatible data is as follows.
[{'label':'keyboard','type':'rectangle','coordinates':{'x':298,'y':301,'width':397,'height':69}},{'label':'laptop','type':'rectangle','coordinates':{'x':294,'y':225,'width':508,'height':407}}]
This Common Objects in Context (COCO) compatible dataset is a JSON file (as shown below).
{  "categories" : [    {      "id" : 1629415338,      "name" : "laptop"    },    {      "name" : "keyboard",      "id" : 1629415413    }  ],  "images" : [    {      "file_name" : "ssd10.png",      "height" : 450,      "id" : 0,      "width" : 600    }  ],  "annotations" : [    {      "id" : 0,      "category_id" : 1629415413,      "iscrowd" : 0,      "bbox" : [        101,        268,        498,        114      ],      "segmentation" : [],      "image_id" : 0,      "area" : 27456    },    {      "iscrowd" : 0,      "id" : 1,      "bbox" : [        40,        22,        549,        21      ],      "segmentation" : [],      "area" : 207401,      "image_id" : 0,      "category_id" : 1629415338    }  ]}
The Pascal VOC dataset format is an XML file (as shown below)
<annotation>    <folder>images</folder>    <filename>ssd10.png</filename>    <size>        <width>600</width>        <height>450</height>        <depth>3</depth>    </size>    <segmented>0</segmented>    <object>        <name>keyboard</name>        <pose>Unspecified</pose>        <truncated>0</truncated>        <occluded>0</occluded>        <difficult>0</difficult>        <bndbox>            <xmin>101</xmin>            <ymin>268</ymin>            <xmax>498</xmax>            <ymax>337</ymax>        </bndbox>    </object>    <object>        <name>laptop</name>        <pose>Unspecified</pose>        <truncated>0</truncated>        <occluded>0</occluded>        <difficult>0</difficult>        <bndbox>            <xmin>40</xmin>            <ymin>22</ymin>            <xmax>549</xmax>            <ymax>430</ymax>        </bndbox>    </object></annotation>
You may recall from above that we need to have our data formatted in a particular way i.e. each row of our spreadsheet needs to be as follows:
link_to_image, label, bounding_box_coordinates i.e.
gs://folder/ssd10.png,laptop,x_min,y_min,x_max,y_min,x_max,y_max,x_min,y_max
gs://folder/ssd10.png,keyboard,x_min,y_min,x_max,y_min,x_max,y_max,x_min,y_max
In order to comply with the format discussed above we will need to convert the image shape and box coordinates to 0–1 values (as apposed pixel coordinates in the 600px by 450px format).
If the width (x axis) is 600px and the height (y axis) is 450px then we would perform the following conversion for the laptop’s bounding box coordinates.
40px (xmin) / 600px (width) = 0.06
549px (xmax) / 600px (width) = 0.91
22px (ymin) / 450px (height) = 0.04
430 (ymax) / 450px (height) = 0.95
Therefore the single row entry for the laptop bounding box would be as follows
gs://folder/ssd10.png,laptop,0.06,0.04,0.91,0.04,0.91,0.95,0.06,0.95
Once you have your spreadsheet rows and images ready you can go ahead and run the following commands.
spec = model_spec.get('efficientdet_lite0')train_data, validation_data, test_data = object_detector.DataLoader.from_csv('gs://cloud-ml-data/img/openimage/csv/salads_ml_use.csv')model = object_detector.create(train_data, model_spec=spec, batch_size=8, train_whole_model=True, validation_data=validation_data)model.evaluate(test_data)model.export(export_dir='.')
The application which uses the trained model to perform object detection needs to provide the correct input (data types) and also know how to interpret the output of the model.
If we inspect the model.tflite file (using an application like netron . app), we see the following
This model has well described inputs and outputs (we can see that the input expects 320 x 320 pixels in RGB)
We make sure that our Rust source code provides the image in that 320x320 array format; a flat array.
let mut resized = image::imageops::thumbnail(&img, 320, 320);let mut flat_img: Vec<u8> = Vec::new();for rgb in resized.pixels() {    flat_img.push(rgb[0]);    flat_img.push(rgb[1]);    flat_img.push(rgb[2]);}
The outputs are detailed in the model’s specifications, as per the image below.
Let’s look at an example using the aforementioned salad dataset.
The model is essentially taking on new images, which it has never seen before, and then returning bounding box coordinates in much the same way was we created bounding box coordinates manually when generating our training data.
Interestingly though, the model gives us back the bounding boxes in the same0–1 format and in relation to the scaled down 320x320 image size.
When we return the original image to our users we will need to make sure that we convert the object bounding box based on the original image’s size.
Here is an example of the bounding box coordinates, let’s call this datares0.
[0.8125, 0.72265625, 0.5859375, 0.45703125, ...0.4296875, 0.36328125, 0.36328125, 0.3203125]
The logic for processing the boxes is as follows (based on the fact that we are working in sets of 4 coordinates per box)
let mut iter = 0;while (iter * 4) < res0.len() {    // bounding box logic    iter += 1;}
A concrete implementation of the bounding box logic (given that the original image was 512px by 512px) would be as follows.
let image_height: f32 = img.height() as f32; //512let image_width: f32 = img.width() as f32; //512let x1 = res0[4 * iter + 1] * image_width;let y1 = res0[4 * iter] * image_height;let x2 = res0[4 * iter + 3] * image_width;let y2 = res0[4 * iter + 2]  * image_height;
I would like to end this article with an additional tool which may help make this data training process quicker and a little more robust.
We mentioned the labelImg image labelling application above. This is an open source product which builds training sets as XML files in PASCAL VOC format. This is the format use by ImageNet.
ImageNet is an ongoing research effort to provide researchers around the world with image data for training large-scale object recognition models.
Thankfully, labelImg appears to have a free transformation tool, available at this GitHub repository, which can convert your XML files, from PASCAL VOC format, to the TensorFlow compatible format which we have unpacked in this article; formally described here in Google’s official documentation. labelImg also supports YOLO and CreateML formats.
Using labelImg will allow you to create your own custom images and custom labels. The additional conversion tool will save you from having to write your own conversion tool.
Once you have your dataset trained and exported as tflite file you can go ahead and create your own AI application using WasmEdge.
What will you create?
If you have any questions, please leave a comment below.
Also if you would like to contribute to WasmEdge please visit the official WasmEdge GitHub page.
The SecondState team are the original creators, and current maintainers of WasmEdge, so please also feel free to check out SecondState’s web site, blog and GitHub repositories.
Thanks for reading!