userguide/activity_classifier/export_coreml.md
In this section, we describe how to export the model created by the activity classifier toolkit to Core ML, and use it in Xcode to get real-time predictions in a Swift app.
Exporting an activity classifier model in Core ML format can be performed using the export_coreml function.
model.export_coreml('ActivityClassification.mlmodel')
Starting with Turi Create 6.0, the API for integrating an Activity Classifier into your Swift app has become more explicit. There are now individual features for the accelerometer and gyroscope on x, y, and z axes. Dragging the saved model to Xcode will automatically generate relevant code for calling the model in your app:
For more information about the generated programmatic API, please refer to Core ML documentation.
prediction_window containing the accelerometer's X-axis readings.prediction_window containing the accelerometer's Y-axis readings.prediction_window containing the accelerometer's Z-axis readings.prediction_window containing the gyroscope's X-axis readings.prediction_window containing the gyroscope's Y-axis readings.prediction_window containing the gyroscope's Z-axis readings.stateOut output of the previous prediction.activityProbability.stateIn input at the next prediction call.For more information on the model architecture, please see the how does it work? section.
Deploying an activity classification model in an iOS/watchOS app involves 3 basic steps:
MLMultiArrays.prediction() method to get a predicted activity.The ActivityClassifier model expects to receive MLMultiArray arrays containing readings of the sensors' values.
The app will need to aggregate the sensors' readings into a MLMultiArray with a dimension of prediction_window.
Finally, the app needs to save the last stateOut outputs, to be fed to the model in the next prediction.
struct ModelConstants {
static let predictionWindowSize = 50
static let sensorsUpdateInterval = 1.0 / 50.0
static let stateInLength = 400
}
let activityClassificationModel = ActivityClassification()
var currentIndexInPredictionWindow = 0
let accelDataX = try! MLMultiArray(shape: [ModelConstants.predictionWindowSize] as [NSNumber], dataType: MLMultiArrayDataType.double)
let accelDataY = try! MLMultiArray(shape: [ModelConstants.predictionWindowSize] as [NSNumber], dataType: MLMultiArrayDataType.double)
let accelDataZ = try! MLMultiArray(shape: [ModelConstants.predictionWindowSize] as [NSNumber], dataType: MLMultiArrayDataType.double)
let gyroDataX = try! MLMultiArray(shape: [ModelConstants.predictionWindowSize] as [NSNumber], dataType: MLMultiArrayDataType.double)
let gyroDataY = try! MLMultiArray(shape: [ModelConstants.predictionWindowSize] as [NSNumber], dataType: MLMultiArrayDataType.double)
let gyroDataZ = try! MLMultiArray(shape: [ModelConstants.predictionWindowSize] as [NSNumber], dataType: MLMultiArrayDataType.double)
var stateOutput = try! MLMultiArray(shape:[ModelConstants.stateInLength as NSNumber], dataType: MLMultiArrayDataType.double)
let motionManager = CMMotionManager()
We need to enable the accelerometer and gyroscope sensors, set them to the required update interval, and set our handler block.
For more info on using CoreMotion sensors, please see CoreMotion documentation
guard motionManager.isAccelerometerAvailable, motionManager.isGyroAvailable else { return }
motionManager.accelerometerUpdateInterval = TimeInterval(ModelConstants.sensorsUpdateInterval)
motionManager.gyroUpdateInterval = TimeInterval(ModelConstants.sensorsUpdateInterval)
motionManager.startAccelerometerUpdates(to: .main) { accelerometerData, error in
guard let accelerometerData = accelerometerData else { return }
// Add the current data sample to the data array
self.addAccelSampleToDataArray(accelSample: accelerometerData)
}
For simplicity, we only show here setting the accelerometer’s block handler.
Whenever a new reading has been received from the sensor, we will add it to our prediction_window long data array.
When the array is full, the application is ready to call the model and get a new activity prediction.
func addAccelSampleToDataArray (accelSample: CMAccelerometerData) {
// Add the current accelerometer reading to the data array
accelDataX[[currentIndexInPredictionWindow] as [NSNumber]] = accelSample.acceleration.x as NSNumber
accelDataY[[currentIndexInPredictionWindow] as [NSNumber]] = accelSample.acceleration.y as NSNumber
accelDataZ[[currentIndexInPredictionWindow] as [NSNumber]] = accelSample.acceleration.z as NSNumber
// Update the index in the prediction window data array
currentIndexInPredictionWindow += 1
// If the data array is full, call the prediction method to get a new model prediction.
// We assume here for simplicity that the Gyro data was added to the data arrays as well.
if (currentIndexInPredictionWindow == ModelConstants.predictionWindowSize) {
if let predictedActivity = performModelPrediction() {
// Use the predicted activity here
// ...
// Start a new prediction window
currentIndexInPredictionWindow = 0
}
}
}
After prediction_window readings are aggregated, we call the model to get a prediction of the current user's activity.
func performModelPrediction () -> String? {
// Perform model prediction
let modelPrediction = try! activityClassificationModel.prediction(acc_x: accelDataX, acc_y: accelDataY, acc_z: accelDataZ, gyro_x: gyroDataX, gyro_y: gyroDataY, gyro_z: gyroDataZ, stateIn: stateOutput)
// Update the state vector
stateOutput = modelPrediction.stateOut
// Return the predicted activity - the activity with the highest probability
return modelPrediction.activity
}
Dragging the saved model to Xcode will automatically generate relevant code for calling the model in your app:
For more information about the generated programmatic API please refer to Core ML documentation.
prediction_window and width equal to the number of features. The array should contain the sensors' readings, that have been aggregated for the past prediction_window samples.hiddenOut output of the previous prediction.cellOut output of the previous prediction.activity_idProbability.hiddenIn input at the next prediction call.cellIn input at the next prediction call.For more information on the model architecture, please see the how does it work? section.
Deploying an activity classification model in an iOS/watchOS app involves 3 basic steps:
prediction_window long array.prediction() method to get a predicted activity.The ActivityClassifier model expects to receive an array containing prediction_window readings of the sensors' values.
The app will need to aggregate the sensors' readings into a MLMultiArray with dimensions 1 x prediction_window x number_of_features.
In addition, the app needs to save the last hiddenOut and cellOut outputs, to be fed to the model in the next prediction.
struct ModelConstants {
static let numOfFeatures = 6
static let predictionWindowSize = 50
static let sensorsUpdateInterval = 1.0 / 50.0
static let hiddenInLength = 200
static let hiddenCellInLength = 200
}
let activityClassificationModel = ActivityClassification()
var currentIndexInPredictionWindow = 0
let predictionWindowDataArray = try? MLMultiArray(shape: [1 , ModelConstants.predictionWindowSize , ModelConstants.numOfFeatures] as [NSNumber], dataType: MLMultiArrayDataType.double)
var lastHiddenOutput = try? MLMultiArray(shape:[ModelConstants.hiddenInLength as NSNumber], dataType: MLMultiArrayDataType.double)
var lastHiddenCellOutput = try? MLMultiArray(shape:[ModelConstants.hiddenCellInLength as NSNumber], dataType: MLMultiArrayDataType.double)
We need to enable the accelerometer and gyroscope sensors, set them to the required update interval, and set our handler block.
For more info on using CoreMotion sensors please see CoreMotion documentation
guard let motionManager = motionManager, motionManager.isAccelerometerAvailable && motionManager.isGyroAvailable else { return }
motionManager.accelerometerUpdateInterval = TimeInterval(ModelConstants.sensorsUpdateInterval)
motionManager.gyroUpdateInterval = TimeInterval(ModelConstants.sensorsUpdateInterval)
motionManager.startAccelerometerUpdates(to: .main) { accelerometerData, error in
guard let accelerometerData = accelerometerData else { return }
// Add the current data sample to the data array
self.addAccelSampleToDataArray(accelSample: accelerometerData)
}
For simplicity, we only show here setting the accelerometer’s block handler.
Whenever a new reading has been received from the sensor, we will add it to our prediction_window long data array.
When the array is full, the application is ready to call the model and get a new activity prediction.
func addAccelSampleToDataArray (accelSample: CMAccelerometerData) {
// Add the current accelerometer reading to the data array
guard let dataArray = predictionWindowDataArray else { return }
dataArray[[0, currentIndexInPredictionWindow, 0] as [NSNumber]] = accelSample.acceleration.x as NSNumber
dataArray[[0, currentIndexInPredictionWindow, 1] as [NSNumber]] = accelSample.acceleration.y as NSNumber
dataArray[[0, currentIndexInPredictionWindow, 2] as [NSNumber]] = accelSample.acceleration.z as NSNumber
// Update the index in the prediction window data array
currentIndexInPredictionWindow += 1
// If the data array is full, call the prediction method to get a new model prediction.
// We assume here for simplicity that the Gyro data was added to the data array as well.
if (currentIndexInPredictionWindow == ModelConstants.predictionWindowSize) {
let predictedActivity = performModelPrediction() ?? "N/A"
// Use the predicted activity here
// ...
// Start a new prediction window
currentIndexInPredictionWindow = 0
}
}
After prediction_window readings are aggregated, we call the model to get a prediction of the current user's activity.
func performModelPrediction () -> String? {
guard let dataArray = predictionWindowDataArray else { return "Error!"}
// Perform model prediction
let modelPrediction = try? activityClassificationModel.prediction(features: dataArray, hiddenIn: lastHiddenOutput, cellIn: lastHiddenCellOutput)
// Update the state vectors
lastHiddenOutput = modelPrediction?.hiddenOut
lastHiddenCellOutput = modelPrediction?.cellOut
// Return the predicted activity - the activity with the highest probability
return modelPrediction?.activity_id
}