Skip to content

maxhumber/SUIPYER

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

5 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

The [S]wift[UI] + [Py]thon Playbook (SUIPYER)

By @maxhumber

Part 1 - Build the Skeleton

  1. In Xcode create a New Project > iOS > App

  2. Select the following options:

    • Name: ImBoard

    • Interface: SwiftUI

    • Life Cycle: SwiftUI App

  3. Replace ContentView.swift with:

import SwiftUI

struct ContentView: View {
    var body: some View {
        VStack(spacing: 20) {
            Text("I'm Board...")
                .font(.largeTitle)
            Button(action: {}) {
                Text("Predict Fun!")
            }
            Spacer()
        }
        .padding()
    }
}
  1. Create a new file, Game.swift:
import Foundation

struct Game {
    var name: String
    var time: Int
    var age: Int
    var complexity: Double
    var category: Category
    
    enum Category: String, CaseIterable {
        case abstract = "Abstract"
        case childrens = "Childrens"
        case customizable = "Customizable"
        case family = "Family"
        case party = "Party"
        case strategy = "Strategy"
        case thematic = "Thematic"
        case wargames = "Wargames"
    }
}
  1. Create a ViewModel.swift:
import Combine

class ViewModel: ObservableObject {
    var game = Game(
        name: "Pandemic", time: 45, age: 8, complexity: 2.41, category: .strategy
    )
}
  1. Tweak ContentView:
struct ContentView: View {
    @StateObject var viewModel = ViewModel()
    
    var body: some View {
        VStack(spacing: 20) {
            Text("I'm Board...")
                .font(.largeTitle)
            Text("\(viewModel.game.name)")
            Text("\(viewModel.game.time)")
            Text("\(viewModel.game.age)")
            Text("\(viewModel.game.complexity)")
            Text("\(viewModel.game.category.rawValue)")
            Button(action: {}) {
                Text("Predict Fun!")
            }
            Spacer()
        }
        .padding()
    }
}
  1. Swap out Text("\(viewModel.game.name)") for an editable TextField:
		TextField("Name", text: $viewModel.game.name)
  1. Force the game in the ViewModel to be @Published:
    @Published var game = Game(
        name: "Pandemic", time: 45, age: 8, complexity: 2.41, category: .strategy
    )
  1. Add Steppers, Sliders, and Pickers to ContentView:
struct ContentView: View {
    @StateObject var viewModel = ViewModel()
    
    var body: some View {
        VStack(spacing: 20) {
            Text("I'm Board...")
                .font(.largeTitle)
            Stepper(
                "Time: \(viewModel.game.time)",
                value: $viewModel.game.time, in: 5...120, step: 5)
            Stepper(
                "Age: \(viewModel.game.age)",
                value: $viewModel.game.age, in: 4...20, step: 4)
            HStack(spacing: 10) {
                Text("Complexity")
                Slider(value: $viewModel.game.complexity, in: 0...5)
            }
            Picker("Category", selection: $viewModel.game.category) {
                ForEach(Game.Category.allCases, id: \.self) { category in
                    Text(category.rawValue)
                }
            }
            Button(action: {}) {
                Text("Predict Fun!")
            }
            Spacer()
        }
        .padding()
    }
}
  1. Spoof out a predict method (to be replaced) in ViewModel:
import Combine

class ViewModel: ObservableObject {
    @Published var game = Game(
        name: "Pandemic", time: 45, age: 8, complexity: 2.41, category: .strategy
    )
    @Published var prediction: Double?
    
    func predict() {
        prediction = 7.6
    }
}
  1. Connect it to ContentView by replacing the empty Button(action: {}) with:
    Button(action: viewModel.predict) {
        Text("Predict Fun!")
    }
    if let prediction = viewModel.prediction {
        Text("\(prediction)")
    }

Part 2 - Build the Brain

  1. Create a venv at the command line:
python -m venv .venv
  1. Activate it:
source .venv/bin/activate
  1. Install everything:

scikit-learn==0.19.2 is the max supported version for this workflow right now 😭)

pip install coremltools scikit-learn==0.19.2 pandas tensorflow
  1. Create a 01-model.py:
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
import coremltools as ct

# load
df = pd.read_csv('data/games.csv')

# split
target = 'rating'
predictors = [
    'time', 'age', 'complexity', 'abstract',
    'childrens', 'customizable', 'family', 'party',
    'strategy', 'thematic', 'wargames'
]
y = df[target]
X = df[predictors]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.20)

# model
model = LinearRegression()
model.fit(X_train, y_train)
print(model.score(X_train, y_train), model.score(X_test, y_test))

# convert
coreml_model = ct.converters.sklearn.convert(model, predictors, target)
coreml_model.save('models/BoardGameRegressor1.mlmodel')
  1. Train and export the Regressor at the command line:
python 01-model.py

Part 3 - Perform the Surgery

  1. Drag+Drop BoardGameRegressor1.mlmodel into the Xcode Project Folder
    • βœ… Copy items if needed
    • βœ… Add to targets
  2. Add import CoreML to the top of ViewModel.swift
  3. Replace the predict method in the ViewModel with:
func predict() {
    do {
        let model: BoardGameRegressor1 = try BoardGameRegressor1(configuration: .init())
        let pred = try model.prediction(
            time: Double(game.time),
            age: Double(game.age),
            complexity: game.complexity,
            abstract: game.category == .abstract ? 1.0 : 0.0,
            childrens: game.category == .childrens ? 1.0 : 0.0,
            customizable: game.category == .customizable ? 1.0 : 0.0,
            family: game.category == .family ? 1.0 : 0.0,
            party: game.category == .party ? 1.0 : 0.0,
            strategy: game.category == .strategy ? 1.0 : 0.0,
            thematic: game.category == .thematic ? 1.0 : 0.0,
            wargames: game.category == .wargames ? 1.0 : 0.0
        )
        self.prediction = pred.rating
    } catch {
        self.prediction = nil
    }
}
  1. Build and Run to see if it works!

Part 4 - Make it Automagic

  1. Add some defaults to the Game struct:
struct Game {
    var time: Int = 45
    var age: Int = 8
    var complexity: Double = 2.41
    var category: Category = .strategy
    
    // ...
}
  1. Change the first couple of lines of ViewModel to:
    @Published var game = Game() {
        didSet { predict() }
    }
    @Published var prediction: Double?
  1. Get rid of the Button in ContentView and add the following to the end of the VStack:
    .onAppear(perform: viewModel.predict)

Part 5 - Improve the Brain

  1. Create a 02-model.py file that uses Tensorflow:
import pandas as pd
from sklearn.model_selection import train_test_split
import tensorflow as tf
from sklearn.metrics import r2_score
import coremltools as ct

# load
df = pd.read_csv('data/games.csv')

# split
target = 'rating'
predictors = [
    'time', 'age', 'complexity', 'abstract',
    'childrens', 'customizable', 'family', 'party',
    'strategy', 'thematic', 'wargames'
]
y = df[target]
X = df[predictors]
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.20)

# create
model = tf.keras.Sequential([
    tf.keras.layers.Input(shape=(X_train.shape[1],)),
    tf.keras.layers.Dense(8, activation=tf.nn.relu),
    tf.keras.layers.Dense(4, activation=tf.nn.relu),
    tf.keras.layers.Dense(1),
])

# compile
model.compile(
    optimizer=tf.keras.optimizers.RMSprop(),
    loss=tf.keras.losses.mean_squared_error,
    metrics=tf.keras.metrics.mean_absolute_error
)

# train
model.fit(X_train, y_train, epochs=500, batch_size=32, validation_data=(X_test, y_test))

# evaluate
r2_score(y_test, model.predict(X_test).flatten())

# convert
coreml_model = ct.convert(model)
coreml_model.save('models/BoardGameRegressor2.mlmodel')
  1. Run at the command line:
python 02-model.py

Part 6 - Surgery, Again

  1. Drag+Drop BoardGameRegressor2.mlmodel into the Xcode Project Folder
  • βœ… Copy items if needed
  • βœ… Add to targets
  1. Update the predict method in the ViewModel to match:

Note: shape: [1, 11] comes from the number of column/features

    func predict() {
        do {
            let mlArray = try? MLMultiArray(
                shape: [1, 11], dataType: MLMultiArrayDataType.float32
            )
            mlArray![0] = NSNumber(value: game.time)
            mlArray![1] = NSNumber(value: game.age)
            mlArray![2] = NSNumber(value: game.complexity)
            mlArray![3] = NSNumber(value: game.category == .abstract ? 1.0 : 0.0)
            mlArray![4] = NSNumber(value: game.category == .childrens ? 1.0 : 0.0)
            mlArray![5] = NSNumber(value: game.category == .customizable ? 1.0 : 0.0)
            mlArray![6] = NSNumber(value: game.category == .family ? 1.0 : 0.0)
            mlArray![7] = NSNumber(value: game.category == .party ? 1.0 : 0.0)
            mlArray![8] = NSNumber(value: game.category == .strategy ? 1.0 : 0.0)
            mlArray![9] = NSNumber(value: game.category == .thematic ? 1.0 : 0.0)
            mlArray![9] = NSNumber(value: game.category == .wargames ? 1.0 : 0.0)
            let model: BoardGameRegressor2 = try BoardGameRegressor2(configuration: .init())
            let pred = try model.prediction(input:
                BoardGameRegressor2Input(input_1: mlArray!)
            )
            self.prediction = Double(truncating: pred.Identity[0])
        } catch {
            self.prediction = nil
        }
    }
  1. Build and Run! πŸŽ‰

About

The SwiftUI + Python Playbook

Resources

License

Stars

Watchers

Forks