apple / apple/coremltools

Used coremltools to make mlmodel updatable with MSE loss but the model gives negative MSE loss when training

Open
#2,070 0 comments 0 reactions 0 assignees View on GitHub
on-device update PyTorch (traced) question
Dominant language
Python
Stars
5.4k
Forks
850
Avg merge
4d 5h
Merged PRs (30d)
10

Description

## ❓Question

I converted a toy Pytorch regression model to CoreML mlmodel using coremltools and set it to be updatable with mean_squared_error_loss. But when testing the training, the `context.metrics[.lossValue]` in `MLUpdateProgressHandlers` can give negative value which is impossible for MSE loss. Further more, the `context.metrics[.lossValue]` is very different from my own computed loss as shown in the screenshot below. I was wondering if I used a wrong way to extract the training loss or I used `set_mean_squared_error_loss` in a wrong way? Does `context.metrics[.lossValue]` really give MSE if I used coremltools function `set_mean_squared_error_loss` to set the loss? Any suggestion is appreciated.**

I am using `coremltools==7.0`, `torch==1.13.1`, `xcode==15.0.1`

Screenshot 2023-11-26 at 12 29 41 PM
Screenshot 2023-11-26 at 12 30 47 PM
Screenshot 2023-11-26 at 12 31 29 PM

Here is my full code to convert Pytorch model to updatable CoreML model:

```python
import torch
import torch.optim as optim
import torch.nn as nn
import coremltools as ct

# Define a simple neural network with two layers
class SimpleRegressionModel(nn.Module):
def __init__(self):
super(SimpleRegressionModel, self).__init__()
self.layer1 = nn.Linear(2, 5) # 2 inputs, 5 outputs
self.layer2 = nn.Linear(5, 1) # 5 inputs, 1 output

def forward(self, x):
x = torch.relu(self.layer1(x))
x = self.layer2(x)
return x

# Create the model
model = SimpleRegressionModel()

# Create a sample input tensor
sample_input = torch.rand(1, 2) # Adjust the shape according to your model's input

# Trace the model with a sample input
traced_model = torch.jit.trace(model, sample_input)

# Convert the traced model to Core ML format
input_features = [ct.TensorType(shape=(1, 2))]
output_features = ["output"]
mlmodel = ct.convert(
traced_model,
inputs=input_features,
convert_to="neuralnetwork"
)

mlmodel.save("regression.mlmodel")

import coremltools
from coremltools.models.neural_network import NeuralNetworkBuilder, SgdParams, AdamParams
from coremltools.models import datatypes

# Load the model specification
spec = coremltools.utils.load_spec('regression.mlmodel')
builder = NeuralNetworkBuilder(spec=spec)

builder.inspect_output_features() # Name: linear_1

# Make layers updatable
builder.make_updatable(['linear_0', 'linear_1'])

# Manually add a mean squared error loss layer
feature = ('linear_1', datatypes.Array(1))
builder.set_mean_squared_error_loss(name='lossLayer', input_feature=feature)

# Define the optimizer (SGD in this example)
# sgd_params = SgdParams(lr=0.001, batch=16) # Adjust learning rate and batch size as needed
# builder.set_sgd_optimizer(sgd_params)

# define the optimizer (Adam in this example)
adam_params = AdamParams(lr=0.01, beta1=0.9, beta2=0.999, eps=1e-8, batch=16)
builder.set_adam_optimizer(adam_params)

# Set the number of epochs
builder.set_epochs(100)

# Optionally, set descriptions for your training inputs
spec.description.trainingInput[0].shortDescription = 'Input data'
spec.description.trainingInput[1].shortDescription = 'Target output data'

# Save the updated model
updated_model = coremltools.models.MLModel(spec)
updated_model.save('updatable_regression30.mlmodel')

```

Here is the full code I use to try to update the saved `updatable_regression30.mlmodel`:

```swift
import CoreML

import GameKit

func generateSampleData(numSamples: Int, seed: UInt64) -> ([MLMultiArray], [MLMultiArray]) {
var inputArray = [MLMultiArray]()
var outputArray = [MLMultiArray]()

// Create a random number generator with a fixed seed
let randomSource = GKLinearCongruentialRandomSource(seed: seed)
let randomDistribution = GKRandomDistribution(randomSource: randomSource, lowestValue: 0, highestValue: 1000)

for _ in 0.. Double {
// my own function to compute loss in order to compare with context.metrics[.lossValue]
let (inputData, outputData) = data
var totalLoss: Double = 0

for (index, input) in inputData.enumerated() {
let output = outputData[index]

// Using optional binding to safely unwrap the prediction
if let prediction = try? model.prediction(from: MLDictionaryFeatureProvider(dictionary: ["x": MLFeatureValue(multiArray: input)])),
let predictedOutput = prediction.featureValue(for: "linear_1")?.multiArrayValue {

// Now you can safely use predictedOutput
let loss = (output[0].doubleValue - predictedOutput[0].doubleValue)
totalLoss += loss * loss // Mean squared error
}
}
return totalLoss / Double(inputData.count) // Calculating the mean of squared errors
}

func trainModel() {
// Load the updatable model
guard let updatableModelURL = Bundle.main.url(forResource: "updatable_regression30", withExtension: "mlmodelc") else {
print("Failed to load the updatable model")
return
}

// Generate sample data
let (inputData, outputData) = generateSampleData(numSamples: 200, seed: 8)

let validationData = generateSampleData(numSamples: 100, seed:18)

// Create an MLArrayBatchProvider from the sample data
var featureProviders = [MLFeatureProvider]()
for (index, input) in inputData.enumerated() {
let output = outputData[index]
let dataPointFeatures: [String: MLFeatureValue] = [
"x": MLFeatureValue(multiArray: input),
"linear_1_true": MLFeatureValue(multiArray: output)
]
if let provider = try? MLDictionaryFeatureProvider(dictionary: dataPointFeatures) {
featureProviders.append(provider)
}
}
let batchProvider = MLArrayBatchProvider(array: featureProviders)


// Define progress handlers
var lossValues: [Double] = []
var computedTrainingLossValues: [Double] = []
var validationLossValues: [Double] = []

let progressHandlers = MLUpdateProgressHandlers(forEvents: [.trainingBegin, .epochEnd],
progressHandler: { context in
switch context.event {
case .trainingBegin:
print("Training began.")
case .epochEnd:
let loss = context.metrics[.lossValue] as! Double
lossValues.append(loss)
let validationLoss = computeLoss(model: context.model, data: validationData)
validationLossValues.append(validationLoss)
let computedTrainLoss = computeLoss(model: context.model, data: (inputData, outputData))
computedTrainingLossValues.append(validationLoss)
print("Epoch \(context.metrics[.epochIndex]!) ended. Training Loss: \(loss), Computed Training Loss: \(computedTrainLoss), Validation Loss: \(validationLoss)")
print("\(context.metrics[.lossValue]!)")
default:
break
}
},
completionHandler: { context in
if let error = context.task.error {
print("Update task failed with error: \(error)")
} else {
let updatedModel = context.model
do {
let fileManager = FileManager.default
let documentDirectory = try fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor:nil, create:true)
let fileURL = documentDirectory.appendingPathComponent("CatDog5.mlmodelc")
try updatedModel.write(to: fileURL)
print("Model updated and saved successfully to \(fileURL)")
} catch {
print("Failed to save the updated model: \(error)")
}
}
}
)

// Create an update task with progress handlers
let updateTask = try! MLUpdateTask(forModelAt: updatableModelURL,
trainingData: batchProvider,
configuration: nil,
progressHandlers: progressHandlers)

// Start the update task
updateTask.resume()
}

trainModel()
```

Contributor guide

Open the contributing guide

Research direction

Start with the conversion code around builder.set_mean_squared_error_loss and the Swift MLUpdateProgressHandlers callback, then run the supplied training example. Compare context.metrics[.lossValue] with computeLoss and verify the training-input and output feature names; done means explaining the negative or differing metric and identifying the correct loss interpretation or usage.

Written by the indexing model from the issue text.

Assessment

Tech stack
python, pytorch, swift
Domain
machine-learning, mobile-dev
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Needs clarification
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.