A-Z Julia Endpoint With Genie. An introduction to using the Genie... | by Emmett Boudreau | Towards Data Science

notebook
Github repo
One of the most important tasks that any aspiring Data-Scientist should be able to accomplish is creating and deploying an end-point. End-points are the back-bone of modern machine-learning, and are the easiest way to translate models into a real production environment. In order to deploy a model at an endpoint, you will first need to select a web-framework to use to create that endpoint. For Python and R, the choice is pretty obvious, and it can be rather easy to find a proper web-framework that will meet your needs.
But what about Julia?
Although Julia’s ecosystem certainly isn’t as mature as Python or R’s, Julia does have some interesting options for deploying your models. Firstly, you could always fall back onto PyCall. This will allow you to use Python libraries like Flask and Django to do your deployment instead of having to worry about using a Julia project that is still relatively young compared to those two options. But for those of us with a desire for higher performance, I introduce
Genie.jl.
Of course, Genie is rather young compared to other web frameworks, but in my opinion, it brings a lot more interesting ideas to the table that make deploying a model an activity of leisure. Genie is still in version 0.31.1, but you might be surprised just how capable the technology already is. Today I am going to be walking you through everything with Genie from setting up a virtual environment to serializing models and deploying a Genie-based API on Ubuntu.
For the sake of an example, nothing about a model is really going to matter. Regardless, we are going to be using Lathe to create our model, because unlike many other options, a Lathe model is incredibly easy to serialize and deploy. For today’s example we are going to be using Lathe 0.1.2, but the syntax should remain the same until at least the release of Lathe 1.0. First thing is first, let’s find some data!
www.kaggle.com
The data-set I decided on was a “ Vehicle Dataset” from Kaggle. I chose this one mostly because I am interested in cars. Also, I’ve been especially excited about them because I am about to buy a Del Sol next week. For the car people out there I will just say one thing:
Twincharged B20 Swap.
Back in the world of Data Science, I downloaded our new data-set, and now we can read it in. We will do this using CSV.jl:
using CSVdf = CSV.read("car data.csv")
Funny thing about Julia; although DataFrames.jl is a dependency of CSV.jl, it will not be precompiled whenever we import CSV. So in other words, df is of the type DataFrame, which comes from DataFrames.jl, but we can’t use any of the methods associated with this type until we also import DataFrames.
using DataFrames
Though there are certainly other ways to remove your missing values, this is one that works:
df = collect(skipmissing(eachrow(df)))
An advantage to using this method is that collect() and skipmissing() are both base methods in the Julia language. That means that they can apply to any type, and even your own types if explicitly imported. Eachrow() is from data-frames, and is just used to turn our df into something that can be iterated across. A disadvantage to this is that we will get an Array of DataFrame rows as a return.
We can mitigate this by asserting the DataFrame type to this data.
df = DataFrame(df)
Skipping ahead, we’re not going to bother about statistical testing, as we don’t really care about accuracy for this example, so next we will go ahead and split our data. To do this we will use TrainTestSplit from Lathe.preprocess. In the unstable version of Lathe we will need to have the at parameter set (the float set to 75 percent), but in any stable version this is not the case.
Next we will need to decide on a target and a feature. I thought a cool idea for this would be to compare Kms_Driven to Selling_Price. Then I thought maybe the year would be cool, too, so why not just do both of them?
target = :Present_Pricefeature1 = :Yearfeature2 = :Kms_Driven
Now let’s plug those in to get the actual arrays out of the DataFrame:
trainy = train[!,target]testy = test[!,target]trainX1 = train[!,feature1]trainX2 = train[!,feature2]testX1 = test[!,feature1]testX2 = test[!,feature2]
Now we will fit two separate Lathe models:
using Lathe.modelsmodel1 = LinearRegression(trainX1,trainy)model2 = LinearRegression(trainX2,trainy)
And get two predictions:
yhat1 = model1.predict(testX1)yhat2 = model2.predict(testX2)
Now we will test their accuracy both together and separate. For our validation metric, we will be using mean absolute error:
Not bad!
Let’s see what happens if we combine the two. We will combine the two by averaging all of the predictions in a zip loop:
yhati = [mean([pred1,pred2]) for (pred1,pred2) in zip(yhat1,yhat2)]
Now we’ll compare all of the accuracies:
As you might have expected, our new feature is bringing down our accuracy ever so slightly because this model doesn’t make any logical decisions on which value to use or prioritize. As a result, we’ll just use the first model, as it has the best accuracy. In order to serialize this model, we are going to use JLD2. Using JLD2, we can use the @save macro to save our model the serialized JLD2 format.
using JLD2@save "mdl.jld2" model1
Now we’ll take that JLD2 file and we will move it to a new directory. I’ll do this in Bash, but of course, you can do this however you want:
mkdir CarPricemv mdl.jld2 ./CarPrice
Now we’re going to make a directory for our endpoint.
mkdir endpcd endp
Now we will initialize a Genie project. To do this, we will enter the Julia REPL, import Genie, and then finally newapp_webservice() method:
juliausing GenieGenie.newapp_webservice("CarPricePredictor")
This will instantiate a new Pkg environment and create a new directory system for our Genie endpoint. It will also start a server, so you can go ahead and visit your localhost (http://127.0.0.1:8000/ by default) and check out your server! These files will not be able to be written to by default, so you will need to adjust the permissions in order to edit them. After that, I moved our model file to our endpoint’s directory, then put it in its own folder called models. Now we just load it back up using JLD2 inside of the routes.jl file generated by Genie:
using Genie.Routerroute("/") do  mdl = @load "models/mdl.jld2"end
Next, we will request a parameter argument from the request, we can do this like so:
using Genie.Routerroute("/") do  mdl = @load "models/mdl.jld2"  year = haskey(@params, :year)end
Now we will just predict off of that data, and return the result.
using Genie.Routerroute("/") do  mdl = @load "models/mdl.jld2"  year = haskey(@params, :year)  return(mdl.predict(year))end
In order to deploy the model, we will first have to get our files onto our server. To do this you can either use FTP, HTTP (via Git or Wget), or STP. Normally in this scenario, you would use STP, but since I already committed my files to Github, i’ll just git clone it into my /var/www directory on my server.
ssh emmett@(this_is_my_ip)cd /var/wwwgit clone https://github.com/emmettgb/CarPricePredictor
Now we have two things to setup:
A webserver
A supervisor
For my web-server, I am going to be using NGINX. I use NGINX for basically everything because it has a few advantages over Apache, primarily being that it typically has a better response time. If you need to make a decision, I would suggest going with whichever you are familiar with. If you aren’t familiar with either, go with NGINX because it has easier configurations and in my subjective opinion (as well as by many metrics) is just better. There are certainly advantages to using Apache instead, but I don’t think that you will be missing much by at least starting by trying NGINX.
What we are going to have to do with our NGINX configuration is create a proxy pass onto the port that our server will be running on, which by default will be 8000. If you are running multiple web-servers on your VPS, then you’ll probably need to use a higher port like 8001 or 8002. The configuration files are stored in /etc/nginx/conf.d, so let’s create a new configuration:
sudo dnf/apt/pacman/man install nginxsudo nano /etc/nginx/conf.d/carpredictor.conf
Here is an example of a configuration for a proxy pass from port 80 (the default HTTP protocol port) to port 8000:
server {    listen 80;    server_name your domain;    location / {        proxy_pass http://127.0.0.1:8000;        proxy_set_header Host $host;        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;    }}
If this is your first NGINX server, you’ll have to unlink the NGINX default site:
sudo unlink /etc/nginx/sites-enabled/default
Next, we will need to setup a supervisor. Like NGINX, the configuration files for supervisor. My server is on Ubuntu, despite my RHEL obsession, so installing supervisor is as easy as
sudo apt-get install supervisor
Now we will create a new configuration.
sudo nano /etc/supervisor/conf.d/flask_app.conf
Here is my supervisor configuration:
[program:genieapp]directory=/var/www/CarPricePredictorcommand=. bin/serverautostart=trueautorestart=truestopasgroup=truekillasgroup=truestderr_logfile=/var/log/appname/lognameerr.logstdout_logfile=/var/log/appname/lognamestdout.log
The command and directory are the two parameters you should look at here. We are going to source the SH file in bin/server that comes with our Genie endpoint, which will automatically startup a Genie server we can use. We also need the directory to run said command, and that is pretty much it. Now we just need to reload our services:
sudo nginx -s reloadsudo service supervisor reload
And now our endpoint is deployed!
Julia deployment isn’t too difficult, and has been made easier recently. Whenever I was first getting into Genie, the documentation was absolutely abysmal. That has fortunately changed with time, and now the tool is far easier to use than it ever has before. The Lathe model is also an aspect of this that I think is really cool. Alternatively, you could also deploy a pipeline using Lathe. Here is a tutorial for deploying a pipeline for older versions of Lathe:
https://towardsdatascience.com/building-pipelines-with-lathe-0-0-7-unstable-in-julia-f7ba1850bde3
The difference between the old version and the new version is that now pipelines take one parameter:
steps.
So in order to make a pipeline, you would just do:
using Lathe.preprocessscaler = Normalizer(xt)using Lathe.modelsmdl = LinearRegression(trainX,trainy)pipl = Pipeline([scaler, mdl])yhat = pipl.predict(xt)
Pretty cool, right?
So pipeline deployment is definitely viable, and just as well is pretty cool using Lathe in Julia. I’m excited to see just how far this methodology will be able to take machine-learning using Lathe in this aspect, as pipelines are definitely an important part of the DS ecosystem.