Tapster iOS Tutorial
January 14, 2018 ยท View on GitHub
Requirements
- PredictionIO 0.12.0+
- Xcode 9.0+ and iOS 9.0+
PredictionIO Setup
Create a new app
- After installing PredictionIO, we need to start HBase and Elasticsearch.
$ pio-start-all
Note: If your computer went into sleep mode, you might need to restart HBase and Elasticsearch.
- Then create a new app.
$ pio app new tapster
Take note of the app name and the access key.
Note:
- You can always view all your application's credentials using
$ pio app list
Setup a Similar Product engine
Step 1: Clone the Similar Product Template. This guide uses the template version v0.12.0.
$ git clone https://github.com/apache/incubator-predictionio-template-similar-product.git tapster-similar-product
$ cd tapster-similar-product
Step 2: Add our app name tapster to the appName field in the engine.json file.
Step 3: Modify our Engine!
- By the default, the template accepts
viewevents. So we need to change it tolikeevent.
In DataSource.scala, modify viewEventsRDD in the readTraining method.
override
def readTraining(sc: SparkContext): TrainingData = {
...
// get all "user" "like" "item" events
val viewEventsRDD: RDD[ViewEvent] = PEventStore.find(
appName = dsp.appName,
entityType = Some("user"),
eventNames = Some(List("like")), // MODIFIED
// targetEntityType is optional field of an event.
targetEntityType = Some(Some("item")))(sc)
// eventsDb.find() returns RDD[Event]
.map { event =>
val viewEvent = try {
event.event match {
case "like" => ViewEvent( // MODIFIED
user = event.entityId,
item = event.targetEntityId.get,
t = event.eventTime.getMillis)
case _ => throw new Exception(s"Unexpected event ${event} is read.")
}
} catch {
...
}
}
viewEvent
}.cache()
...
}
- By default, the Event Server only accepts the comic's categories. We also need to send in the comic's title and image URLs so that we can return them in the recommendation response.
In DataSource.scala, modify itemsRDD in the readTraining method and also the Item class.
class DataSource(val dsp: DataSourceParams) extends PDataSource[TrainingData,
EmptyEvaluationInfo, Query, EmptyActualResult] {
...
override
def readTraining(sc: SparkContext): TrainingData = {
...
// create a RDD of (entityID, Item)
val itemsRDD: RDD[(String, Item)] = PEventStore.aggregateProperties(
appName = dsp.appName,
entityType = "item"
)(sc).map { case (entityId, properties) =>
val item = try {
// Assume categories is optional property of item.
Item(
title = properties.get[String]("title"), // ADDED
categories = properties.getOpt[List[String]]("categories"),
imageURLs = properties.get[List[String]]("imageURLs")) // ADDED
} catch {
...
}
(entityId, item)
}.cache()
...
}
...
}
...
case class Item(
title: String, // ADDED
categories: Option[List[String]],
imageURLs: List[String] // ADDED
)
...
- Initially, the recommendation response only returns the comic ID. We need to also include the comic's other properties: title and imageURLs in the recommendation result.
In Engine.scala, modify the ItemScore class.
case class ItemScore(
itemID: String, // MODIFIED
title: String, // ADDED
imageURLs: List[String], // ADDED
score: Double
) extends Serializable
In ALSAlgorithm.scala, modify itemScores in the predict method.
class ALSAlgorithm(val ap: ALSAlgorithmParams)
...
def predict(model: ALSModel, query: Query): PredictedResult = {
...
val itemScores = topScores.map { case (i, s) =>
new ItemScore(
itemID = model.itemIntStringMap(i), // MODIFIED
title = model.items(i).title, // ADDED
imageURLs = model.items(i).imageURLs, // ADDED
score = s
)
}
new PredictedResult(itemScores)
}
...
}
In CooccurrenceAlgorithm.scala, modify itemScores in the predict method.
class CooccurrenceAlgorithm(val ap: CooccurrenceAlgorithmParams)
extends P2LAlgorithm[PreparedData, CooccurrenceModel, Query, PredictedResult] {
...
def predict(model: CooccurrenceModel, query: Query): PredictedResult = {
...
val itemScores = counts
.filter { case (i, v) =>
isCandidateItem(
i = i,
items = model.items,
categories = query.categories,
queryList = queryList,
whiteList = whiteList,
blackList = blackList
)
}
.sortBy(_._2)(Ordering.Int.reverse)
.take(query.num)
.map { case (index, count) =>
ItemScore(
itemID = model.itemIntStringMap(index), // MODIFIED
title = model.items(index).title, // ADDED
imageURLs = model.items(index).imageURLs, // ADDED
score = count
)
}
...
}
}
Step 4: Build the engine. Simply run,
$ cd tapster-similar-product
$ pio build
If you modified the code correctly, you should see the message that your engine is ready for training.
Note: The final code for the engine can be found at this repository. You can check the step-by-step changes in its commit history.
Setting up the iOS app
To install dependencies for the iOS project, make sure you have installed CocoaPods. Then at the Xcode project root directory, run
$ pod install
Open the project workspace Tapster iOS Demo.xcworkspace (created by CocoaPods) and you should be able to run the app by selecting Start Reading in the home screen. You can swipe right or left to like or dislike a comic just like in Tinder! However, there is no recommendation for now. New comics are randomly generated.
Import data
Before we can do any prediction, we need some data! First, we need to start the event server.
# Switch back to the engine directory
$ cd ../tapster-similar-product
$ pio eventserver
The import process has been included in DataViewController.swift. However, you need to add the app ID of the PredictionIO app that you created earlier so that the eventClient knows where to send the data to.
let eventClient = EventClient(accessKey: "<Your App's access key here>")
The import process consists of 3 steps:
- Send comic data using the
setItemmethod. - Send user data using the
setUsermethod. - Send likes data using the
recordActionmethod.
Now, run the application again. In the home screen, tap on Import Data and then Run Import button. The whole import will take a while. Check Xcode's debug console to see the progress.
Train and deploy the engine
First, you need to switch back to the engine root directory if you're currently at the iOS app directory. Then to train and deploy the engine, run
# At the engine directory
$ pio train
$ pio deploy
In your production server, you might want to set up a cron job to retrain the engine with the latest dataset.
Connect the iOS app with PredictionIO
Here is the exciting part: adding the recommendation to your iOS app!
In ComicViewController.swift, ComicViewController is the controller that is responsible for managing and displaying the comics. We will add the recommendation logic there.
- Import the PredictionIO Swift SDK at the top of the
ComicViewController.swiftfile.
import PredictionIO
- Create a
engineClientas a stored property ofComicViewController.
...
let engineClient = EngineClient()
var directionComicDeleted: Direction = .right
...
- Then query the engine for recommended comics in the
updateComicsmethod.
let query: [String: Any] = [
"num": 1,
"items": likedComicIDs,
"blackList": displayedComicIDs
]
engineClient.sendQuery(query, responseType: RecommendationResponse.self) { result in
guard let response = result.value, !response.comics.isEmpty else { return }
DispatchQueue.main.async {
self.addAndAnimateNewComic(response.comics[0])
}
}
That's it! Rerun the app, swipe right on a comic you like and you will notice that similar comics will be displayed.
Conclusion
Congratulation! I hope the tutorial has helped you understand how to integrate a Prediction IO engine to an iOS application via the PredictionIO Swift SDK. You should now be able to utilize the power of machine learning and make your mobile app more interesting.