Ladvien's Lab

Latest Posts

Boss

A human sends machine learning job to the Boss. A Job is JSON object containing the the desired machine learning script and the parameters needed for successful execution. The Boss stores the Job and Creates an Order. The Order is another JSON object representing the state of a requested Job.

Worker

The Worker uses node-scheduler to fire an HTTP request to the Boss letting it know the Worker is “bored.” The Boss will then search through the Orders for the oldest unassigned Order, if it finds one, it will return this Order to the Worker as a JSON object. At this point, the Boss updates the Order’s status to “assigned.”

The Worker sends another HTTP request, this time requesting the Job information associated with the Order the Boss had assigned.

The worker passes the Job information into the appropriate machine learning Python script via stdout. The script is executed and whether successful or not, an Outcome object is passed back to the Worker Node through stdout.

Utilizing all Machines in the House

Machine learning is a new world for me. But, it’s pretty dern cool. I like making machines do the hard stuff while I’m off doing other work. It makes me feel extra productive–like, “I created that machine, so any work it does I get credit for. And! The work I did while it as doing its work.” This is the reason I own two 3D-printers.

I’m noticing there is a possibility of utilizing old computers I’ve lying around the house for the same effect. The plan is to abstract a neural network script, install it on all the computers lying about, and create a HQ Computer where I can create a sets of hyperparameters passed to the Worker Nodes throughout the house.

Why? Glad I asked for you. I feel guilty there are computers used. There’s an old AMD desktop with a GFX1060 in it, a 2013 MacBook Pro (my son’s), and my 2015 MacBook Pro. These don’t see much use anymore, since my employer has provided an iMac to work on. They need to earn their keep.

How? Again, glad to ask for you. I’ll create a system to make deep-learning jobs from hyperparameter sets and send them to these idle machines, thus, trying to get them to solve problems while I’m working on paying the bills. This comes from the power of neural networks. They need little manual tweaking. You simply provide them with hyperparameters and let them run.

Worker Nodes

The Worker Nodes code is pretty straightforward. It uses Node, Express, and python-shell to create a bastardized REST interface to create simple interactions between the HQ Node controlling the job queue.

The above code is a dead simple NodeJS server using Express. It is using body-parser middleware to shape JSON objects. The pythonJob object looks something like this (real paths names have been changed to help protect their anonymity).

Python Side

Here’s the Python script in the above example. It is meant to detect what type of data is in a table. If it’s is continuous it leaves it alone (I’ll probably add normalization option as some point), if it is categorical, it converts it to a dummy variable. It then saves this encoded data on the Worker Node side (right now). Lastly, it returns a JSON string back to the node side.

"""
Created on Mon Jun 11 21:12:10 2018
@author: cthomasbrittain
"""importsysimportjson#filename=sys.argv[1]filepath=sys.argv[2]pathToWriteProcessedFile=sys.argv[3]request=sys.argv[4]request=json.loads(request)try:cols_to_remove=request['columnsToRemove']unreasonable_increase=request['unreasonableIncreaseThreshold']except:# If columns aren't contained or no columns, exit nicelyresult={'status':400,'message':'Expected script parameters not found.'}print(str(json.dumps(result)))quit()pathToData=filepath+filename# Clean Data --------------------------------------------------------------------# -------------------------------------------------------------------------------# Importing data transformation librariesimportpandasaspd# The following method will do the following:a# 1. Add a prefix to columns based upon datatypes (cat and con)# 2. Convert all continuous variables to numeric (float64)# 3. Convert all categorical variables to objects# 4. Rename all columns with prefixes, convert to lower-case, and replace# spaces with underscores.# 5. Continuous blanks are replaced with 0 and categorical 'not collected'# This method will also detect manually assigned prefixes and adjust the # columns and data appropriately. # Prefix key:# a) con = continuous# b) cat = categorical# c) rem = removal (discards entire column)defadd_datatype_prefix(df,date_to_cont=True):importpandasaspd# Get a list of current column names.column_names=list(df.columns.values)# Encode each column based with a three letter prefix based upon assigned datatype.# 1. con = continuous# 2. cat = categoricalfornameincolumn_names:ifdf[name].dtype=='object':try:df[name]=pd.to_datetime(df[name])if(date_to_cont):new_col_names="con_"+name.lower().replace(" ","_").replace("/","_")df=df.rename(columns={name:new_col_names})else:new_col_names="date_"+name.lower().replace(" ","_").replace("/","_")df=df.rename(columns={name:new_col_names})exceptValueError:passcolumn_names=list(df.columns.values)fornameincolumn_names:ifname[0:3]=="rem"or"con"or"cat"or"date":passifdf[name].dtype=='object':new_col_names="cat_"+name.lower().replace(" ","_").replace("/","_")df=df.rename(columns={name:new_col_names})elifdf[name].dtype=='float64'ordf[name].dtype=='int64'ordf[name].dtype=='datetime64[ns]':new_col_names="con_"+name.lower().replace(" ","_").replace("/","_")df=df.rename(columns={name:new_col_names})column_names=list(df.columns.values)# Get lists of coolumns for conversioncon_column_names=[]cat_column_names=[]rem_column_names=[]date_column_names=[]fornameincolumn_names:ifname[0:3]=="cat":cat_column_names.append(name)elifname[0:3]=="con":con_column_names.append(name)elifname[0:3]=="rem":rem_column_names.append(name)elifname[0:4]=="date":date_column_names.append(name)# Make sure continuous variables are correct datatype. (Otherwise, they'll be dummied).fornameincon_column_names:df[name]=pd.to_numeric(df[name],errors='coerce')df[name]=df[name].fillna(value=0)fornameincat_column_names:df[name]=df[name].apply(str)df[name]=df[name].fillna(value='not_collected')# Remove unwanted columns df=df.drop(columns=rem_column_names,axis=1)returndf# ------------------------------------------------------# Encoding Categorical variables# ------------------------------------------------------# The method below creates dummy variables from columns with# the prefix "cat". There is the argument to drop the first column# to avoid the Dummy Variable Trap.defdummy_categorical(df,drop_first=True):# Get categorical data columns.columns=list(df.columns.values)columnsToEncode=columns.copy()fornameincolumns:ifname[0:3]!='cat':columnsToEncode.remove(name)# if there are no columns to encode, return unmutated.ifnotcolumnsToEncode:returndf# Encode categoriesfornameincolumnsToEncode:ifname[0:3]!='cat':continuetmp=pd.get_dummies(df[name],drop_first=drop_first)names={}# Get a clean column name.clean_name=name.replace(" ","_").replace("/","_").lower()# Get a dictionary for renaming the dummay variables in the scheme of old_col_name + response_stringifclean_name[0:3]=="cat":fortmp_nameintmp:tmp_name=str(tmp_name)new_tmp_name=tmp_name.replace(" ","_").replace("/","_").lower()new_tmp_name=clean_name+"_"+new_tmp_namenames[tmp_name]=new_tmp_name# Rename the dummy variable dataframetmp=tmp.rename(columns=names)# join the dummy variable back to original dataframe.df=df.join(tmp)# Drop all old categorical columnsdf=df.drop(columns=columnsToEncode,axis=1)returndf# Read the filedf=pd.read_csv(pathToData)# Drop columns such as unique IDstry:df=df.drop(cols_to_remove,axis=1)except:# If columns aren't contained or no columns, exit nicelyresult={'status':404,'message':'Problem with columns to remove.'}print(str(json.dumps(result)))quit()# Get the number of columns before hot encodingnum_cols_before=df.shape[1]# Encode the data.df=add_datatype_prefix(df)df=dummy_categorical(df)# Get the new dataframe shape.num_cols_after=df.shape[1]percentage_increase=num_cols_after/num_cols_beforeresult=""ifpercentage_increase>unreasonable_increase:message="\"error\": \"Feature increase is greater than unreasonableIncreaseThreshold, most likely a unique id was included."result={'status':400,'message':message}else:filename=filename.replace(".csv","")importosifnotos.path.exists(pathToWriteProcessedFile):os.makedirs(pathToWriteProcessedFile)writeFile=pathToWriteProcessedFile+filename+"_encoded.csv"df.to_csv(path_or_buf=writeFile,sep=',')# Process the results and return JSON results objectresult={'status':200,'message':'encoded data','path':writeFile}print(str(json.dumps(result)))

That’s the premise. I’ll be adding more services to as a series of articles.

Saving Brain Waves to Remote MongoDB by way of Node REST API

In this section I’m going to focus getting a remote Linux server setup with MongoDB and NodeJS. This will allow us to make POST requests to our Linux server, saving the EEG data.

I’m going to assume you are able to SSH into your Ubuntu 16 LTS server for this guide. You don’t have a server? No sweat. I wrote a guide on setting up a blog post which explains how to get a cheap Linux server setup.

E: The method driver /usr/lib/apt/methods/https could not be found.
N: Is the package apt-transport-https installed?
E: Failed to fetch https://repo.mongodb.org/apt/ubuntu/dists/xenial/mongodb-org/4.0/InRelease
E: Some index files failed to download. They have been ignored, or old ones used instead.

Then install apt-transport-https

sudo apt-get install apt-transport-https

Now, let’s install MongoDB.

sudo apt-get install -y mongodb-org

Voila!

2. Setup MongoDB

We still need to do a bit of setup. First, let’s check and make sure Mongo is fully installed.

sudo service mongod start

This starts the MongoDB daemon, the program which runs in the background and waits for someone to make connection with the database.

Speaking of which, let’s try to connect to the database

mongo

You should get the following:

root@localhost:~# mongo
MongoDB shell version v4.0.2
connecting to: mongodb://127.0.0.1:27017
MongoDB server version: 4.0.2
Welcome to the MongoDB shell.
For interactive help, type"help".
For more comprehensive documentation, see
http://docs.mongodb.org/
Questions? Try the support group
http://groups.google.com/group/mongodb-user
Server has startup warnings:
2018-09-02T03:52:18.996+0000 I STORAGE [initandlisten]
2018-09-02T03:52:18.996+0000 I STORAGE [initandlisten] ** WARNING: Using the XFS filesystem is strongly recommended with the WiredTiger storage engine
2018-09-02T03:52:18.996+0000 I STORAGE [initandlisten] ** See http://dochub.mongodb.org/core/prodnotes-filesystem
2018-09-02T03:52:19.820+0000 I CONTROL [initandlisten]
2018-09-02T03:52:19.820+0000 I CONTROL [initandlisten] ** WARNING: Access control is not enabled for the database.
2018-09-02T03:52:19.820+0000 I CONTROL [initandlisten] ** Read and write access to data and configuration is unrestricted.
2018-09-02T03:52:19.820+0000 I CONTROL [initandlisten]
---
Enable MongoDB's free cloud-based monitoring service, which will then receive and display
metrics about your deployment (disk utilization, CPU, operation statistics, etc).
The monitoring data will be available on a MongoDB website with a unique URL accessible to you
and anyone you share the URL with. MongoDB may use this information to make product
improvements and to suggest MongoDB products and deployment options to you.
To enable free monitoring, run the following command: db.enableFreeMonitoring()
To permanently disable this reminder, run the following command: db.disableFreeMonitoring()
---
>

This is good. It means Mongo is up and running. Notice, it is listening on 127.0.0.1:27017. If you try to access the database from any network, other than locally, it will refuse. The plan, to have NodeJS connect to the MongoDB database locally. Then, will send all of our data to Node and let it handle security.

In the Mongo command line type:

quit()

And hit enter. This should bring you back to the Linux command prompt.

A few notes on MongoDB on Ubuntu.

The congfiguration file is located at /etc/mongod.conf

Log file is at /var/log/mongodb/mongod.log

The database is stored at /var/lib/mongodb, but this can be changed in the config file.

Oh, and one last bit. Still at the Linux command prompt type:

sudo systemctl enable mongod

You should get back

Created symlink from /etc/systemd/system/multi-user.target.wants/mongod.service to /lib/systemd/system/mongod.service.

This setup a symlink which will cause Linux to load mongod every time it boots–you won’t need to manually start it.

Next, NodeJS.

3. Install NodeJS and npm

Type

sudo apt-get install nodejs -y

This should install NodeJS, but we also need the Node Package Managers npm.

sudo apt-get install npm -y

Let’s upgrade npm. This is important, as the mind-wave-journal-server depends on recent versions of several packages that are not accessible to earlier versions of npm.

The following commands should prepare npm for upgrading, then upgrade.

This should download all the packages needed to run the little server program I wrote to store the EEG data into the Mongo database.

Let’s run the mind-wave-journal-server.

node server/server.js

This should be followed with:

root@localhost:~/mind-wave-journal-server# node server/server.js
(node:1443) DeprecationWarning: current URL string parser is deprecated, and will be removed in a future version. To use the new parser, pass option { useNewUrlParser: true} to MongoClient.connect.
Started on port 8080

5. Testing mind-wave-journal-server with Postman

Now, we are going to use Postman to test our new API.

For this next part you’ll need either a Mac or Chrome, as Postman has a native Mac app or a Chrome app.

After you add the Postman app it should redirect you to your Chrome applications. Click on the Postman icon.

Your choice, but I skipped the sign-up option for now.

Select Create a Request

The purpose of Postman, in a nutshell, we are going to use it to create POST requests and send them to the mind-wave-journal-server to make sure it’s ready for the iOS app to start making POST requests, saving the EEG data to our Mongo server.

Let’s create our first test POST request. Start by naming the request Test eegsamples. Create a folder to put the new request in, I named it mind-wave-journal-server. Then click

You will need to set the type as POST. The url will be

http://your_ip_address:8080/eegsamples

No select the Headers section and add the Content Type: application/json

Lastly, select Body, then raw and enter the following JSON into the text area:

If all goes well, then you should get a similar response in the Postman response section

Notice, the response is similar to what we sent. However, there is the additional _id. This is great. It is the id assigned to the by MongoDB when the data is entered. In short, it means it successfully saved to the database.

6. Now What?

Several caveats.

First, each time you restart your server you will manually need to start your mind-waver-journal-server. You can turn it into a Linux service and enable it. If this interests anyone, let me know in the comments and I’ll add it.

Second, notice I don’t currently have a way to retrieve data from the MongDB. The easiest way will probably be using Robot 3T. Like the first caveat, if anyone is interested let me know and I’ll add instructions. Otherwise, this series will stay on track to setup a Mongo BI connection to the database for viewing in Tableau (eh, gross).

Your Node server is ready to be called by the iOS app. In the next article I’ll return to building the MindWaveJournal app in iOS.

Step 1: iOS App

I’m going to assume you have Xcode installed.

Step 1.1: Install CocoaPods

CocoaPods is a package handler for Xcode. We will be using it to install Alamofire, which a Swift library for making HTTP requests. We will need HTTP call support as we will call our server to store the EEG samples.

sudo gem install cocoapods

After you hit Return it will prompt for your password

Step 1.2: Setup Xcode Project

Now, let’s setup a project folder. This is main folder where all the iOS app code will live. It’s a bad habit, but I usually put mine on the Desktop.

Open Xcode and select “Create a new Xcode proejct”

Then select “Single View App” and click “Next”

Let’s call the project MindWaveJournaler and click “Next”

Choose your Desktop as location for the project and click “Create”

Step 1.3: Development Environment Setup

You’ve created a Project Folder, but we have to setup the project folder to be used with CocoaPods. After, we will use CocoaPods to install Alamofire.

Back in the terminal, type:

cd ~/Desktop/MindWaveJournaler
pod init

This creates a Podfile in the root folder of our project. We can list CocoaPod packages in the Podfile and run pod install in the same directory, this will cause CocoaPods to install all the packages we listed.

Sadly, we are really only doing this for Alamofire right now. But, later, when we start building on to this app it will allow us to quickly access third-party frameworks.

Ok, back to typing:

open -a Xcode Podfile

This will open the Podfile for editing in Xcode. Now let’s insert the our desired pod information.

Copy information below and paste it into your file:

# Uncomment the next line to define a global platform for your project
platform :ios, '11.4'
target 'MindWaveJournaler' do
# Comment the next line if you're not using Swift and don't want to use dynamic frameworks
use_frameworks!
# Pods for MindWaveJournaler
pod 'Alamofire', '~> 4.7'
target 'MindWaveJournalerTests' do
inherit! :search_paths
# Pods for testing
end
target 'MindWaveJournalerUITests' do
inherit! :search_paths
# Pods for testing
end
end

You may notice the only changes we made were

platform :ios, '11.4'
...
pod 'Alamofire', '~> 4.7'

These lines tell CocoaPods which version of iOS we are targetting with our app (this will silence a warning, but shouldn’t be required). The other, is telling CocoaPods which version of Alamofire we’d like to use on this project.

Ok, now let’s run this Podfile.

Back in the same directory as the Podfile type:

pod install

You should see CocoaPods do its thing with output much like below.

Step 1.4: Install NeuroSky iOS SDK

NeuroSky has a “Swift SDK.” Really, it’s an Objective-C SDK which is “bridged” into Swift. Essentialy, this means we won’t be able to see what’s going on the SDK, but we can use functions from the pre-compiled binaries.

I’ve not been impressed with NeuroSky’s website. Or the SDK. It does the job, but not much more.

Lastly, you have to enter your “Billing Information.” Really, this is only your email address, last name, street address, city, and zip.

(Really NeuroSky? This is very 1990.)

Eh, I made mine up.

Anyway, after your enter information click, then click “Continue to PayPal” (What? I just provided my information…) You should be rewarded with a download link. Click it and download the files.

Unzip the files and navigate lib folder

iOS Developer Tools 4.8 -> MWM_Comm_SDK_for_iOS_V0.2.9 -> lib

Copy all files from the lib folder into the main directory of the MindWaveJournaler project folders.

Step 1.5: Workspace Setup

CocoaPods works by creating a .xcworkspace file. It contains all the information needed to compile your project with all of the CocoaPod packages installed. In our case the file will be called MindWaveJournaler.xcworkspace. And every time you want to work on your project, you must open it with this specific file.

It can be a bit confusing because Xcode created a .xcodeproj file which is tempting to click on.

Go ahead and open the MindWaveJournaler.xcworkspace file. The workspace should open with one warning, which we will resolve shortly.

But first, another caveat. CoreBluetooth, Apple’s Bluetooth LE Framework, only works when compiled for and run on an actual device. It does *not work in the iOS Simulator.* Once upon a time it did, if your Mac had the hardware, however, my version of the story is Apple didn’t like having to support the confusion and dropped it.

Moving on. Click on the yellow warning. Then click on the warning in the sidebar. This should create a prompt asking if you’d like to make some changes. This should automatically make some tweaks to the build settings which should make our project mo’ betta.

Click Perform Changes.

This should silence the warning and make your project error free. Go ahead and hit Play button and let it compile to the simulator (we aren’t testing the Bluetooth, so it’s ok). Everything should compile correctly, if not, just let me know the specifics of your problems in the comments.

Step 1.5: Enable Secure HTTP Request

There are still a few tweaks we need to make to the Xcode workspace to get everything working.

First, open the ViewController.swift file and add import Alamofire right below import UIKit. If auto-complete lists Alamofire as an option you know the workspace is detecting its presence. Good deal.

Now, for Alamofire to be able to securely make HTTP request an option needs to be added to the Info.plist file. I scratched my head as to why the HTTP calls were not being made successfully until Manab Kumar Mal’s StackOverflow post:

Ok, following his instructions open up the Info.plist file in your MindWaveJournaler folder. Now add an entry by right-clicking and selecting Add Row. Change the Application Category to NSAppTransportSecurity and make sure it’s set as dictionary. Now, click the plus sign by the new dictionary and set this attribute as NSAllowsArbitraryLoads, setting the type bool, and the value as YES.

Step 1.5: Setup Objective-C Bridge Header for MindWave SDK

There’s a few other bits of housekeeping, though. As I mentioned earlier, the MindwAve SDK is in an Objective-C precompiled binary. It is usable in a Swift project, but requires setting up a “bridge header” file.

Let’s tell the Swift compile we have a header file. In Xcode go to Project File -> Build Settings -> All then in the search box type Swift Compiler - General (if you don’t include the hyphen and spaces it wont find it).

Double-click on the line Objective-C Bridging Header directly underneath the name of your project (see red box in image). Copy and paste the following into the box and click off to save the change.

$(PROJECT_DIR)/$(PROJECT_NAME)-Bridging-Header.h

This creates a relative path to your Bridging-Header file. In a little bit we are going to try to compile, if you get errors around this file not being found, then it’s probably not named per our naming scheme (YourProjectName-Bridging-Header) or it wasn’t saved in the same folder as the .xworkspace file. No worries, if you have troubles just leave me a comment below.

One last thing to do before we’re ready to code. We still need to import the MindWave SDK into our project.

Right click on your project file and select New Group. Name the group MindWave SDK. Now right click on the folder you created and select Add Files to "MindWave SDK".... Navigate to the lib folder containing the MindWave SDK and select all files inside it.

When you add the SDK, Xcode should automatically detect the binary file (libMWMSDK.a) and create a link to it. But, let’s make sure, just in case. Click on your project file, then go to the General tab.

It needs to be linked under the Build Phases tab as well, under Linked Frameworks and Libraries.

Open the ViewController file and under viewDidLoad() after the existing code, type:

let mwDevice = MWMDevice()
mwDevice.scanDevice()

Watch for autocomplete detecting the existince of the MindWave SDK

Now for the true test, Compile and Run. But, before we do, please be aware–this will only work on an actual iOS device. If you try to run it in the iOS simulator it will fail. It actually fails on two accounts, first, CoreBluetooth will not work in the iOS simulator, second, the MindWave SDK binaries were compiled specifically ARM architecture.

Ok! Enough preamble. Connect and select your iOS device and hit Run.

If all goes well you should see two things. A blank white screen appear on your phone and concerning message in the Xcode console.

The CoreBluetooth error has to do with firing up the iOS Bluetooth services without checking to make sure the iOS BLE is turned on and ready to go. This is a good thing, it probably means the MindWave SDK has been foudn and is functioning properly.

If you get any other errors, let’s chat. I’ll help if I can.

This is part of a series, which I’m writing with care as I’ve time. I’ll get the next part out ASAP.

Description

This project takes brain wave readings from a MindWave Mobile 2+, transmits them to an iOS app via Bluetooth LE. The iOS app makes calls to a remote Node server, which is a minimal REST API, passing off the brain wave sample. The Node server stores the data on a MongoDB server. The MongoDB server is then exposed to business intelligence applications use with MongoDB BI Connector. Lastly, using Tableau Professional Desktop, the data is accessed and visualizations created.

The end result is a system which could allow a remote EEG analyst to examine samples nearly in real time.

Below, I’m going to show how I was able to setup the system. But, before that a few words of warning.

Gotchas

Hacker Haters

This isn’t a hacker friendly project. It relies on several paid licenses, an Apple Developer License ($99) and Tableau Desktop Professional ($10,000,000,000 or something). Of course, the central piece of hardware, the MindWave Mobile, is also $99, but I think that one is fair. Oh! Let’s not forget, even though you bought an Apple Developer license, you still need a Mac (or Hackintosh) to compile the app.

However, as a proof-of-concept, I think it’s solid. Hopefully a good hacker will be able to see how several tweaks in the system could make it dirt cheap to deploy.

Mimimum Viable Hack..er, Product

The source code provided here is a minimally viable. Fancy words meaning, only base functionality was implemented. There many other things which could be done to improve each piece of the system.

Not to be a douche, but please don’t point them out. That’s the only thing I ask for providing this free information.

There are many improvements I know can be made. The reason they were not made had nothing to do with my ignorance (well, at least a majority of them), but rather my time constraints.

Regarding the business intelligence platform–if anyone has a free suggestions, please leave them in the comments below. The first improvement I’d like to the entire system is to get away from Tableau. Have I mentioned I hate it?