diff --git a/cli/package.json b/cli/package.json index 083184dc0..5bd6176f3 100644 --- a/cli/package.json +++ b/cli/package.json @@ -15,8 +15,9 @@ "author": "", "license": "ISC", "dependencies": { - "server": "*", "@epfml/discojs-node": "*", + "csv-parse": "^5.6.0", + "server": "*", "tslib": "2" }, "devDependencies": { diff --git a/cli/src/args.ts b/cli/src/args.ts index ab5e0d92c..ce3537b95 100644 --- a/cli/src/args.ts +++ b/cli/src/args.ts @@ -45,7 +45,7 @@ const unsafeArgs = parse( ) const supportedTasks = Map( - Set.of | TaskProvider<"tabular">>( + Set.of | TaskProvider<"tabular"> | TaskProvider<"text">>( defaultTasks.cifar10, defaultTasks.lusCovid, defaultTasks.simpleFace, diff --git a/cli/src/benchmark_gpt.ts b/cli/src/benchmark_gpt.ts index 6c8bc99ce..c067aba85 100644 --- a/cli/src/benchmark_gpt.ts +++ b/cli/src/benchmark_gpt.ts @@ -134,4 +134,4 @@ async function main(args: Required): Promise { } // You can run this example with "npm start" from this folder -main(args).catch(console.error) +main(args).catch(console.error) \ No newline at end of file diff --git a/cli/src/train_gpt.ts b/cli/src/train_gpt.ts index 531667a0f..bf70b33cf 100644 --- a/cli/src/train_gpt.ts +++ b/cli/src/train_gpt.ts @@ -45,4 +45,4 @@ async function main(): Promise { } // You can run this example with "npm run run_gpt" from this folder -main().catch(console.error) +main().catch(console.error) \ No newline at end of file diff --git a/discojs/src/default_tasks/cifar10.ts b/discojs/src/default_tasks/cifar10.ts index b644b6e93..9d411e022 100644 --- a/discojs/src/default_tasks/cifar10.ts +++ b/discojs/src/default_tasks/cifar10.ts @@ -63,6 +63,6 @@ export const cifar10: TaskProvider<'image'> = { metrics: ['accuracy'] }) - return new models.TFJS('image', model) + return new models.TFJS('image', model, "fedprox") } } diff --git a/discojs/src/default_tasks/lus_covid.ts b/discojs/src/default_tasks/lus_covid.ts index 44dd46ed6..6733df3fd 100644 --- a/discojs/src/default_tasks/lus_covid.ts +++ b/discojs/src/default_tasks/lus_covid.ts @@ -39,7 +39,8 @@ export const lusCovid: TaskProvider<'image'> = { // Model architecture from tensorflow.js docs: // https://codelabs.developers.google.com/codelabs/tfjs-training-classfication/index.html#4 - async getModel (): Promise> { + async getModel(): Promise> { + const seed = 42 const imageHeight = 100 const imageWidth = 100 const imageChannels = 3 @@ -55,7 +56,7 @@ export const lusCovid: TaskProvider<'image'> = { filters: 8, strides: 1, activation: 'relu', - kernelInitializer: 'varianceScaling' + kernelInitializer: tf.initializers.heNormal({ seed }) })) // The MaxPooling layer acts as a sort of downsampling using max values @@ -69,7 +70,7 @@ export const lusCovid: TaskProvider<'image'> = { filters: 16, strides: 1, activation: 'relu', - kernelInitializer: 'varianceScaling' + kernelInitializer: tf.initializers.heNormal({ seed }) })) model.add(tf.layers.maxPooling2d({ poolSize: [2, 2], strides: [2, 2] })) @@ -82,16 +83,16 @@ export const lusCovid: TaskProvider<'image'> = { // output class. model.add(tf.layers.dense({ units: numOutputClasses, - kernelInitializer: 'varianceScaling', - activation: 'softmax' + activation: 'softmax', + kernelInitializer: tf.initializers.heNormal({ seed }) })) - + model.compile({ - optimizer: 'sgd', + optimizer: tf.train.sgd(0.001), loss: 'binaryCrossentropy', metrics: ['accuracy'] }) - return Promise.resolve(new models.TFJS('image', model)) + return Promise.resolve(new models.TFJS('image', model, "fedprox")) } -} +} \ No newline at end of file diff --git a/discojs/src/default_tasks/mnist.ts b/discojs/src/default_tasks/mnist.ts index d73a044d9..6f65aa98b 100644 --- a/discojs/src/default_tasks/mnist.ts +++ b/discojs/src/default_tasks/mnist.ts @@ -66,6 +66,6 @@ export const mnist: TaskProvider<'image'> = { metrics: ['accuracy'] }) - return Promise.resolve(new models.TFJS('image', model)) + return Promise.resolve(new models.TFJS('image', model, "fedprox")) } } diff --git a/discojs/src/default_tasks/simple_face.ts b/discojs/src/default_tasks/simple_face.ts index a87825e5d..f2f60b273 100644 --- a/discojs/src/default_tasks/simple_face.ts +++ b/discojs/src/default_tasks/simple_face.ts @@ -48,6 +48,6 @@ export const simpleFace: TaskProvider<'image'> = { metrics: ['accuracy'] }) - return new models.TFJS('image', model) + return new models.TFJS('image', model, "fedprox") } } diff --git a/discojs/src/default_tasks/tinder_dog.ts b/discojs/src/default_tasks/tinder_dog.ts index a19bf5f8b..7884c36cf 100644 --- a/discojs/src/default_tasks/tinder_dog.ts +++ b/discojs/src/default_tasks/tinder_dog.ts @@ -79,6 +79,6 @@ export const tinderDog: TaskProvider<'image'> = { metrics: ['accuracy'] }) - return Promise.resolve(new models.TFJS('image', model)) + return Promise.resolve(new models.TFJS('image', model, "fedprox")) } } \ No newline at end of file diff --git a/discojs/src/default_tasks/titanic.ts b/discojs/src/default_tasks/titanic.ts index b9462ee50..9efbe4b86 100644 --- a/discojs/src/default_tasks/titanic.ts +++ b/discojs/src/default_tasks/titanic.ts @@ -90,6 +90,6 @@ export const titanic: TaskProvider<'tabular'> = { metrics: ['accuracy'] }) - return Promise.resolve(new models.TFJS('tabular', model)) + return Promise.resolve(new models.TFJS('tabular', model, "fedprox")) } } diff --git a/discojs/src/models/gpt/index.ts b/discojs/src/models/gpt/index.ts index 2eb02d4fe..6fdf4b6c1 100644 --- a/discojs/src/models/gpt/index.ts +++ b/discojs/src/models/gpt/index.ts @@ -76,30 +76,10 @@ export class GPT extends Model<"text"> { async #runBatch( batch: Batched, ): Promise { - const tfBatch = this.#batchToTF(batch); - - let logs: tf.Logs | undefined; - await this.model.fitDataset(tf.data.array([tfBatch]), { - epochs: 1, - verbose: 0, // don't pollute - callbacks: { - onEpochEnd: (_, cur) => { - logs = cur; - }, - }, - }); - tf.dispose(tfBatch); - if (logs === undefined) throw new Error("batch didn't gave any logs"); - - const { loss, acc: accuracy } = logs; - if (loss === undefined || isNaN(loss)) - throw new Error("training loss is undefined or NaN"); - - return { - accuracy, - loss, - memoryUsage: tf.memory().numBytes / 1024 / 1024 / 1024, - }; + const {xs, ys} = this.#batchToTF(batch); + const logs = await this.model.trainOnBatch(xs, ys); + tf.dispose([xs, ys]) + return this.getBatchLogs(logs) } async #evaluate( diff --git a/discojs/src/models/gpt/model.ts b/discojs/src/models/gpt/model.ts index 01ee51e92..8c646c6d4 100644 --- a/discojs/src/models/gpt/model.ts +++ b/discojs/src/models/gpt/model.ts @@ -4,7 +4,6 @@ import * as tf from '@tensorflow/tfjs' import type { GPTConfig } from './config.js' import { getModelSizes, DefaultGPTConfig } from './config.js' import { getCustomAdam, clipByGlobalNormObj } from './optimizers.js' -import evaluate from './evaluate.js' import { GPTArchitecture } from './layers.js' const debug = createDebug("discojs:models:gpt:model"); @@ -55,101 +54,52 @@ export class GPTModel extends tf.LayersModel { : tf.train.adam(this.config.lr) } - override async fitDataset(dataset: Dataset, trainingArgs: tf.ModelFitDatasetArgs): Promise { - const callbacks = trainingArgs.callbacks as tf.CustomCallbackArgs - const evalDataset = trainingArgs.validationData as tf.data.Dataset<{ xs: tf.Tensor2D, ys: tf.Tensor3D }> - await callbacks.onTrainBegin?.() + override async trainOnBatch(x: tf.Tensor, y: tf.Tensor): Promise { + let weightUpdateTime = performance.now() - for (let epoch = 1; epoch <= trainingArgs.epochs; epoch++) { - let accuracyFraction: [number, number] = [0, 0]; - let averageLoss = 0 - let iteration = 1 - const iterator = await dataset.iterator() - let next = await iterator.next() + let preprocessingTime = performance.now() + await Promise.all([x.data(), y.data()]) + preprocessingTime = performance.now() - preprocessingTime - while (next.done !== true && iteration <= this.config.maxIter) { - let weightUpdateTime = performance.now() - await callbacks.onEpochBegin?.(epoch) - const { xs, ys } = next.value as { xs: tf.Tensor2D, ys: tf.Tensor3D } + let logitsTensor: tf.Tensor; + const lossTensor = tf.tidy(() => { + const { grads, value: lossTensor } = this.optimizer.computeGradients(() => { + const logits = this.apply(x) + if (Array.isArray(logits)) + throw new Error('model outputs too many tensor') + if (logits instanceof tf.SymbolicTensor) + throw new Error('model outputs symbolic tensor') + logitsTensor = tf.keep(logits) + return tf.losses.softmaxCrossEntropy(y, logits) + }) + const gradsClipped = clipByGlobalNormObj(grads, 1) + this.optimizer.applyGradients(gradsClipped) + return lossTensor + }) - let preprocessingTime = performance.now() - await Promise.all([xs.data(), ys.data()]) - preprocessingTime = performance.now() - preprocessingTime - - // TODO include as a tensor inside the model - const accTensor = tf.tidy(() => { - const logits = this.apply(xs) - if (Array.isArray(logits)) - throw new Error('model outputs too many tensor') - if (logits instanceof tf.SymbolicTensor) - throw new Error('model outputs symbolic tensor') - return tf.metrics.categoricalAccuracy(ys, logits) - }) - const accSize = accTensor.shape.reduce((l, r) => l * r, 1) - const accSumTensor = accTensor.sum() - const accSum = await accSumTensor.array() - tf.dispose(accSumTensor) - if (typeof accSum !== 'number') - throw new Error('got multiple accuracy sum') - accuracyFraction = [accuracyFraction[0] + accSum, accuracyFraction[1] + accSize]; - tf.dispose([accTensor]) + // @ts-expect-error Variable 'logitsTensor' is used before being assigned + const accTensor = tf.metrics.categoricalAccuracy(y, logitsTensor) + const accSize = accTensor.shape.reduce((l, r) => l * r, 1) + const accSumTensor = accTensor.sum() + const accSum = await accSumTensor.array() + if (typeof accSum !== 'number') + throw new Error('got multiple accuracy sum') + // @ts-expect-error Variable 'logitsTensor' is used before being assigned + tf.dispose([accTensor, accSumTensor, logitsTensor]) + + const loss = await lossTensor.array() + weightUpdateTime = performance.now() - weightUpdateTime - const lossTensor = tf.tidy(() => { - const { grads, value: lossTensor } = this.optimizer.computeGradients(() => { - const logits = this.apply(xs) - if (Array.isArray(logits)) - throw new Error('model outputs too many tensor') - if (logits instanceof tf.SymbolicTensor) - throw new Error('model outputs symbolic tensor') - return tf.losses.softmaxCrossEntropy(ys, logits) - }) - const gradsClipped = clipByGlobalNormObj(grads, 1) - this.optimizer.applyGradients(gradsClipped) - return lossTensor - }) - - const loss = await lossTensor.array() - averageLoss += loss - weightUpdateTime = performance.now() - weightUpdateTime - - tf.dispose([xs, ys, lossTensor]) - - if ( - evalDataset !== undefined && - this.config.evaluateEvery !== undefined && - iteration % this.config.evaluateEvery == 0 - ){ - const iterationLogs = await evaluate(this, evalDataset, this.config.maxEvalBatches) - debug('evaluation metrics: %O', iterationLogs); - } - const memory = tf.memory().numBytes / 1024 / 1024 / 1024 - debug("training metrics: %O", { - epoch, - iteration, - loss, - memory, - allocated: tf.memory().numTensors, - preprocessingTime, - weightUpdateTime, - }); - iteration++ - next = await iterator.next() - } - // Memory leak: If we reached the last iteration rather than the end of the dataset, cleanup the tensors - if (next.done !== true && iteration > this.config.maxIter) { - const { xs, ys } = next.value as { xs: tf.Tensor2D, ys: tf.Tensor3D } - tf.dispose([xs, ys]) - } - let logs: tf.Logs = { - 'loss': averageLoss / (iteration - 1), // -1 because iteration got incremented at the end of the loop - 'acc': accuracyFraction[0] / accuracyFraction[1], - } - if (evalDataset !== undefined) { - logs = { ...logs, ...await evaluate(this, evalDataset, this.config.maxEvalBatches) } - } - await callbacks.onEpochEnd?.(epoch, logs) - } - await callbacks.onTrainEnd?.() - return new tf.History() + tf.dispose([x, y, lossTensor]) + + const memory = tf.memory().numBytes / 1024 / 1024 / 1024 + debug("training metrics: %O", { + loss, + memory, + allocated: tf.memory().numTensors, + preprocessingTime, + weightUpdateTime, + }); + return [loss, accSum / accSize] } } diff --git a/discojs/src/models/model.ts b/discojs/src/models/model.ts index dd7c0477c..b7e51eab7 100644 --- a/discojs/src/models/model.ts +++ b/discojs/src/models/model.ts @@ -6,6 +6,8 @@ import type { WeightsContainer, } from "../index.js"; +import * as tf from "@tensorflow/tfjs"; + import type { BatchLogs, EpochLogs } from "./logs.js"; /** @@ -15,12 +17,16 @@ import type { BatchLogs, EpochLogs } from "./logs.js"; **/ // TODO make it typesafe: same shape of data/input/weights export abstract class Model implements Disposable { + protected prevRoundWeights: WeightsContainer | undefined; // TODO don't allow external access but upgrade train to return weights on every epoch /** Return training state */ abstract get weights(): WeightsContainer; /** Set training state */ abstract set weights(ws: WeightsContainer); + set previousRoundWeights(ws: WeightsContainer | undefined) { + this.prevRoundWeights = ws + } /** * Improve predictor * @@ -39,6 +45,26 @@ export abstract class Model implements Disposable { batch: Batched, ): Promise>; + protected getBatchLogs( + logs: number | number[], + ): BatchLogs { + if (!Array.isArray(logs) || logs.length != 2) + throw new Error("training output has unexpected shape") + + const [loss, accuracy] = logs + + if ( + typeof loss !== "number" || isNaN(loss) || + typeof accuracy !== "number" || isNaN(accuracy) + ) + throw new Error("training loss or accuracy is undefined or NaN"); + + return { + accuracy, + loss, + memoryUsage: tf.memory().numBytes / 1024 / 1024 / 1024, + }; + } /** * This method is automatically called to cleanup the memory occupied by the model * when leaving the definition scope if the instance has been defined with the `using` keyword. diff --git a/discojs/src/models/tfjs.ts b/discojs/src/models/tfjs.ts index b60060f49..a21b71583 100644 --- a/discojs/src/models/tfjs.ts +++ b/discojs/src/models/tfjs.ts @@ -1,3 +1,4 @@ +import createDebug from "debug"; import { List, Map, Range } from "immutable"; import * as tf from '@tensorflow/tfjs' @@ -13,14 +14,19 @@ import { BatchLogs } from './index.js' import { Model } from './index.js' import { EpochLogs } from './logs.js' +const debug = createDebug("discojs:models:tfjs"); + type Serialized = [D, tf.io.ModelArtifacts]; +type FrameWorkAlgorithm = "fedaverage" | "fedprox"; + /** TensorFlow JavaScript model with standard training */ export class TFJS extends Model { /** Wrap the given trainable model */ constructor ( public readonly datatype: D, - private readonly model: tf.LayersModel + private readonly model: tf.LayersModel, + public readonly framework: FrameWorkAlgorithm = "fedprox", ) { super() @@ -62,30 +68,124 @@ export class TFJS extends Model { async #runBatch( batch: Batched, - ): Promise> { + ): Promise { const { xs, ys } = this.#batchToTF(batch); + let logs: [number, number]; + if (this.framework === "fedaverage") { + logs = await this.trainFedAverage(xs, ys); + } else if (this.framework === "fedprox") { + logs = await this.trainFedProx(xs, ys); + } else { + throw new Error("unknown framework"); + } + tf.dispose([xs, ys]) + return this.getBatchLogs(logs) + } - const { history } = await this.model.fit(xs, ys, { - epochs: 1, - verbose: 0, // don't pollute - }); - - const { loss: losses, acc: accuracies } = history; - if ( - losses === undefined || - accuracies === undefined || - typeof losses[0] !== "number" || - typeof accuracies[0] !== "number" || - isNaN(losses[0]) || - isNaN(accuracies[0]) - ) - throw new Error("training loss or accuracy is undefined or NaN"); - - return { - accuracy: accuracies[0], - loss: losses[0], - memoryUsage: tf.memory().numBytes / 1024 / 1024 / 1024, + async trainFedAverage( + xs: tf.Tensor, ys: tf.Tensor, + ) : Promise<[number, number]> { + let logitsTensor: tf.Tensor; + + const optimizer = tf.train.sgd(0.01); // adjust the learning rate here + const lossFunction: () => tf.Scalar = () => { + // Apply the model to get logits + const logits = this.model.apply(xs) as tf.Tensor; + logitsTensor = tf.keep(logits); + + // Calculate binary cross-entropy loss + const loss = tf.losses.sigmoidCrossEntropy(ys, logits); + + // Add regularization term (L2 norm of weights) + const regularizationTerm = tf.addN( + this.model.getWeights().map(w => w.square().sum()) + ); + + return tf.add(loss, regularizationTerm); }; + const lossTensor = optimizer.minimize(lossFunction, true); + if (lossTensor === null) throw new Error("loss should not be null") + + // @ts-expect-error Variable 'logitsTensor' is used before being assigned + const accTensor = tf.metrics.categoricalAccuracy(ys, logitsTensor) + const accSize = accTensor.shape.reduce((l, r) => l * r, 1) + const accSumTensor = accTensor.sum() + const accSum = await accSumTensor.array() + if (typeof accSum !== 'number') + throw new Error('got multiple accuracy sum') + // @ts-expect-error Variable 'logitsTensor' is used before being assigned + tf.dispose([accTensor, accSumTensor, logitsTensor]) + + const loss = await lossTensor.array() + tf.dispose([xs, ys, lossTensor]) + + const memory = tf.memory().numBytes / 1024 / 1024 / 1024 + debug("training metrics: %O", { + loss, + memory, + allocated: tf.memory().numTensors, + }); + return [loss, accSum / accSize] + } + + + // First iteration: replace trainOnBatch with custom loss computation + async trainFedProx( + xs: tf.Tensor, ys: tf.Tensor, + ): Promise<[number, number]> { + let logitsTensor: tf.Tensor; + const lossFunction: () => tf.Scalar = () => { + // Proximal term + let proximalTerm = tf.tensor(0) + if (this.prevRoundWeights !== undefined) { + // squared norm + const norm = new WeightsContainer(this.model.getWeights()) + .sub(this.prevRoundWeights) + .map(t => t.square().sum()) + .reduce((t, acc) => tf.add(t, acc)).asScalar() + const mu = 1 + proximalTerm = tf.mul(mu / 2, norm) + } + + this.model.apply(xs) + const logits = this.model.apply(xs) + if (Array.isArray(logits)) + throw new Error('model outputs too many tensor') + if (logits instanceof tf.SymbolicTensor) + throw new Error('model outputs symbolic tensor') + logitsTensor = tf.keep(logits) + // binaryCrossentropy as implemented by tensorflow.js + // https://github.com/tensorflow/tfjs/blob/2644bd0d6cea677f80e44ed4a44bea5e04aabeb3/tfjs-layers/src/losses.ts#L193 + let y: tf.Tensor; + y = tf.clipByValue(logits, 0.00001, 1 - 0.00001); + y = tf.log(tf.div(y, tf.sub(1, y))); + const loss = tf.losses.sigmoidCrossEntropy(ys, y); + console.log(loss.dataSync(), proximalTerm.dataSync()) + return tf.add(loss, proximalTerm) + } + const lossTensor = this.model.optimizer.minimize(lossFunction, true) + if (lossTensor === null) throw new Error("loss should not be null") + + // @ts-expect-error Variable 'logitsTensor' is used before being assigned + const accTensor = tf.metrics.categoricalAccuracy(ys, logitsTensor) + const accSize = accTensor.shape.reduce((l, r) => l * r, 1) + const accSumTensor = accTensor.sum() + const accSum = await accSumTensor.array() + if (typeof accSum !== 'number') + throw new Error('got multiple accuracy sum') + // @ts-expect-error Variable 'logitsTensor' is used before being assigned + tf.dispose([accTensor, accSumTensor, logitsTensor]) + + const loss = await lossTensor.array() + tf.dispose([xs, ys, lossTensor]) + + const memory = tf.memory().numBytes / 1024 / 1024 / 1024 + debug("training metrics: %O", { + loss, + memory, + allocated: tf.memory().numTensors, + }); + return [loss, accSum / accSize] } async #evaluate( @@ -180,7 +280,10 @@ export class TFJS extends Model { return new this( datatype, await tf.loadLayersModel({ - load: () => Promise.resolve(artifacts), + load: () => { + console.log("deserialize called") + return Promise.resolve(artifacts) + }, }), ); } @@ -207,7 +310,7 @@ export class TFJS extends Model { return [this.datatype, await ret] } - [Symbol.dispose](): void{ + [Symbol.dispose](): void { this.model.dispose() } diff --git a/discojs/src/serialization/model.spec.ts b/discojs/src/serialization/model.spec.ts index a966d39db..135406e78 100644 --- a/discojs/src/serialization/model.spec.ts +++ b/discojs/src/serialization/model.spec.ts @@ -28,7 +28,7 @@ describe('serialization', () => { ] }) rawModel.compile({ optimizer: 'sgd', loss: 'hinge' }) - const model = new models.TFJS("image", rawModel) + const model = new models.TFJS("image", rawModel, "fedprox") const encoded = await serialization.model.encode(model) assert.isTrue(serialization.isEncoded(encoded)) diff --git a/discojs/src/training/trainer.ts b/discojs/src/training/trainer.ts index 1124137be..db6877323 100644 --- a/discojs/src/training/trainer.ts +++ b/discojs/src/training/trainer.ts @@ -90,7 +90,8 @@ export class Trainer { let previousRoundWeights: WeightsContainer | undefined; for (let round = 0; round < totalRound; round++) { await this.#client.onRoundBeginCommunication(); - + + this.model.previousRoundWeights = previousRoundWeights yield this.#runRound(dataset, validationDataset); let localWeights = this.model.weights; diff --git a/docs/examples/custom_task.ts b/docs/examples/custom_task.ts index 609b748ca..4b4548d65 100644 --- a/docs/examples/custom_task.ts +++ b/docs/examples/custom_task.ts @@ -55,7 +55,7 @@ const customTask: TaskProvider<"tabular"> = { metrics: ['accuracy'] }) - return Promise.resolve(new models.TFJS('tabular', model)) + return Promise.resolve(new models.TFJS('tabular', model, "fedprox")) } } diff --git a/package-lock.json b/package-lock.json index dab9961e5..3608f60dc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -29,6 +29,7 @@ "license": "ISC", "dependencies": { "@epfml/discojs-node": "*", + "csv-parse": "^5.6.0", "server": "*", "tslib": "2" }, diff --git a/webapp/src/components/task_creation_form/TaskForm.vue b/webapp/src/components/task_creation_form/TaskForm.vue index 29648ea05..90bc917e5 100644 --- a/webapp/src/components/task_creation_form/TaskForm.vue +++ b/webapp/src/components/task_creation_form/TaskForm.vue @@ -76,6 +76,11 @@ v-model="scheme" :field="field" /> + ()) @@ -270,6 +276,7 @@ const onSubmit = async (rawTask: any): Promise => { await tf.loadLayersModel( tf.io.browserFiles(modelFiles.value.toArray()), ), + rawTask.framework ); break; case "text": diff --git a/webapp/src/components/testing/TestSteps.vue b/webapp/src/components/testing/TestSteps.vue index 751e76c14..2727d8082 100644 --- a/webapp/src/components/testing/TestSteps.vue +++ b/webapp/src/components/testing/TestSteps.vue @@ -124,7 +124,7 @@ class="font-bold uppercase" :class="result.output.correct ? 'text-green-500' : 'text-red-700'" > - {{ result.output.truth }} + {{ result.output.truth.toUpperCase() }}

@@ -515,4 +515,4 @@ function saveCsv(): void { } } } - + \ No newline at end of file diff --git a/webapp/src/components/training/TrainingInformation.vue b/webapp/src/components/training/TrainingInformation.vue index 9d7b95b3c..1aae86b60 100644 --- a/webapp/src/components/training/TrainingInformation.vue +++ b/webapp/src/components/training/TrainingInformation.vue @@ -354,4 +354,4 @@ function toggleAdvancedInfo(): void { localStorage.setItem("initiallyOpen", newOpen + ""); initiallyOpen.value = newOpen; } - + \ No newline at end of file diff --git a/webapp/src/task_creation_form.ts b/webapp/src/task_creation_form.ts index f33420364..78864ee1c 100644 --- a/webapp/src/task_creation_form.ts +++ b/webapp/src/task_creation_form.ts @@ -66,6 +66,15 @@ const generalInformation: FormSection = { type: 'select', options: ['Decentralized', 'Federated'], default: 'Decentralized' + }, + { + id: "framework", + "name" : "Training Framework", + "yup" : yup.string().required(), + "as" : "input", + "type" : "select", + "options" : ["fedaverage", "fedprox"], + "default" : "fedprox" } ] }