Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/i18n/ailab/en_us.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"selectDataset": "Select the data set you would like to use.",
"uploadedDataset": "You just uploaded a dataset.",
"selectedDataset": "You just selected a dataset.",
"dataDisplayDataset": "Explore the dataset columns.",
"dataDisplayLabel": "Choose one column to predict.",
"dataDisplayFeatures": "Choose one or more columns as inputs to help make the prediction.",
"selectedFeatureNumerical": "You just selected a numerical feature.",
Expand Down
73 changes: 55 additions & 18 deletions apps/src/MLTrainers.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import KNN from 'ml-knn';
import {stripSpaceAndSpecial} from '@cdo/apps/aiUtils';

const KNNTrainers = ['knnClassify', 'knnRegress'];
const ID3Trainers = ['id3Classify', 'id3Regress'];

function getKeyByValue(object, value) {
return Object.keys(object).find(key => object[key] === value);
Expand Down Expand Up @@ -58,7 +59,46 @@ function convertTestValue(featureNumberKey, feature, value) {
const convertedValue = Object.keys(featureNumberKey).includes(feature)
? featureNumberKey[feature][value]
: value;
return parseInt(convertedValue);
return parseFloat(convertedValue);
}

function predictDecisionTreeNode(node, testValues) {
if (node.type === 'leaf') {
return node.prediction;
}

const value = testValues[node.featureIndex];
if (node.splitType === 'numerical') {
const child = value <= node.threshold ? node.left : node.right;
return child
? predictDecisionTreeNode(child, testValues)
: node.defaultLabel;
}

const child = node.children[String(value)];
return child ? predictDecisionTreeNode(child, testValues) : node.defaultLabel;
}

function convertPrediction(featureNumberKey, label, rawPrediction) {
return Object.keys(featureNumberKey).includes(label)
? getKeyByValue(featureNumberKey[label], rawPrediction)
: parseFloat(rawPrediction);
}

function getModelFeatures(modelData) {
return modelData.features
? modelData.features.map(feature => feature.id)
: modelData.selectedFeatures;
}

function getTestValues(modelData, features) {
return features.map(feature =>
convertTestValue(
modelData.featureNumberKey,
feature,
modelData.testData[stripSpaceAndSpecial(feature)]
)
);
}

export function predict(modelData) {
Expand All @@ -67,28 +107,25 @@ export function predict(modelData) {
// Re-instantiate the trained model.
const model = KNN.load(modelData.trainedModel);
// Prepare test data.
const features = modelData.features
? modelData.features.map(feature => feature.id)
: modelData.selectedFeatures;

const testValues = features.map(feature =>
convertTestValue(
modelData.featureNumberKey,
feature,
modelData.testData[stripSpaceAndSpecial(feature)]
)
);
const features = getModelFeatures(modelData);
const testValues = getTestValues(modelData, features);
// Make a prediction.
const rawPrediction = model.predict(testValues);
// Convert prediction to human readable (if needed)

const label = modelData.label ? modelData.label.id : model.labelColumn;

const prediction = Object.keys(modelData.featureNumberKey).includes(label)
? getKeyByValue(modelData.featureNumberKey[label], rawPrediction)
: parseFloat(rawPrediction);
return prediction;
} else {
return 'Error: unknown trainer';
return convertPrediction(modelData.featureNumberKey, label, rawPrediction);
}

if (ID3Trainers.includes(modelData.selectedTrainer)) {
const features = getModelFeatures(modelData);
const testValues = getTestValues(modelData, features);
const label = modelData.label ? modelData.label.id : modelData.labelColumn;
const root = modelData.trainedModel.root || modelData.trainedModel;
const rawPrediction = predictDecisionTreeNode(root, testValues);
return convertPrediction(modelData.featureNumberKey, label, rawPrediction);
}

return 'Error: unknown trainer';
}
3 changes: 3 additions & 0 deletions apps/src/ailab/Ailab.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ function getInstructionsDefaults(aiMsg) {
selectDataset: aiMsg.selectDataset(),
uploadedDataset: aiMsg.uploadedDataset(),
selectedDataset: aiMsg.selectedDataset(),
dataDisplayDataset: aiMsg.dataDisplayDataset
? aiMsg.dataDisplayDataset()
: aiMsg.selectedDataset(),
dataDisplayLabel: aiMsg.dataDisplayLabel(),
dataDisplayFeatures: aiMsg.dataDisplayFeatures(),
selectedFeatureNumerical: aiMsg.selectedFeatureNumerical(),
Expand Down
53 changes: 53 additions & 0 deletions apps/test/unit/MLTrainersTest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {predict} from '@cdo/apps/MLTrainers';

describe('MLTrainers', () => {
it('predicts with a saved ID3 classification tree', () => {
const prediction = predict({
selectedTrainer: 'id3Classify',
trainedModel: {
root: {
type: 'decision',
featureIndex: 0,
splitType: 'categorical',
defaultLabel: 0,
children: {
0: {type: 'leaf', prediction: 0},
1: {type: 'leaf', prediction: 1},
},
},
},
featureNumberKey: {
color: {blue: 0, green: 1},
label: {no: 0, yes: 1},
},
label: {id: 'label', values: ['no', 'yes']},
features: [{id: 'color', values: ['blue', 'green']}],
testData: {color: 'green'},
});

expect(prediction).toBe('yes');
});

it('predicts with a saved ID3 numerical threshold tree', () => {
const prediction = predict({
selectedTrainer: 'id3Regress',
trainedModel: {
root: {
type: 'decision',
featureIndex: 0,
splitType: 'numerical',
threshold: 50,
defaultLabel: 10,
left: {type: 'leaf', prediction: 10},
right: {type: 'leaf', prediction: 20},
},
},
featureNumberKey: {},
label: {id: 'cost'},
features: [{id: 'temperature'}],
testData: {temperature: '75'},
});

expect(prediction).toBe(20);
});
});
19 changes: 13 additions & 6 deletions frontend/packages/labs/ailab/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,11 @@ AI Lab is a highly configurable environment providing a student experience for d
- viewing per-column graphs and metadata
- viewing custom [CrossTab](https://github.com/code-dot-org/ml-playground/pull/62) tables for comparing categorical columns
- viewing scatter plot graphs for comparing continuous columns
- using KNN classification or KNN regression to train a model
- choosing KNN or ID3 decision tree training for a model
- choosing the subset of data to reserve for validation
- measuring accuracy of the resulting trained model using reserved data
- using the trained model by doing predictions
- saving the trained model to the server for use in App Lab
- exporting the trained model for use in programming labs

It can be run standalone for more rapid development, though its ultimate destination is to appear in [Code Studio](https://studio.code.org/). When run standalone, it does not have all of the styling of Code Studio. It also calls stub functions for completion, saving a trained model, and indicating which [Dynamic](https://github.com/code-dot-org/code-dot-org/pull/39384) [Instruction](https://github.com/code-dot-org/ml-playground/pull/97) should be shown. The standalone runtime does have a dropdown for selecting which set of level parameters are used, in lieu of a level providing these parameters, and it also shows the current Dynamic Instruction identifier.

Expand Down Expand Up @@ -51,9 +51,15 @@ Take care that each column is correctly listed in the `.json` file, and test tha

### Scenes:

#### Select algorithm

This is the first scene. Students choose which machine learning algorithm will
train the model. The same dataset, label, and feature selection steps are used
after choosing either KNN or ID3.

#### Select dataset

This is usually the first scene, and can offer selection of both "pre-canned" datasets, which have accompanying metadata, or user-uploaded CSV. The tiles use an art style somewhat consistent with that used elsewhere in our product. There is a fun "grow" animation on tile hover, just to feel a little more interactive, which is reused for the A.I. bot head elsewhere in the app.
This scene can offer selection of both "pre-canned" datasets, which have accompanying metadata, or user-uploaded CSV. The tiles use an art style somewhat consistent with that used elsewhere in our product. There is a fun "grow" animation on tile hover, just to feel a little more interactive, which is reused for the A.I. bot head elsewhere in the app.

#### Data display (label & features)

Expand Down Expand Up @@ -91,14 +97,15 @@ This screen does a few things:
- It shows the results of previous sets of predictions, with their respective statements. This way the student can compare statements to see which have the highest "predictive power".
- It lets the student view details of the most recent set of predictions, which shows in a pop-up. This view has a toggle between showing correct and incorrect predictions. In the table, the student can examine each row of reserved data to see how the label's actual value compares to what was predicted.
- The student can "Try it out!" and run their own predictions. The interface here is very similar to what they can see in App Lab once they import the saved model there. The A.I. bot makes a reappearance here, because it's popular!
- For decision tree models, it shows the trained tree and lets the student step through the path taken by the current "Try it out!" feature values.

#### Save Model
#### Export Model

The student can fill out the "model card" and then save the model, along with the model card information, to the server. This model can then be imported in App Lab. The model card information is seen in App Lab when previewing the model prior to importing it. [Model cards](https://modelcards.withgoogle.com/about) serve as an accessible reference for an AI model. They allow the student to document decisions. And analyzing model cards in the curriculum helps students to [explore](https://codeorg.medium.com/code-org-curriculum-now-teaches-ai-to-every-student-f4d09895be15) issues of bias and ethics.
The student can fill out the "model card" fields and then export the model, along with the model card information, for use in programming labs. This is also where the app previews the language-agnostic prediction API: `getPrediction(data)`. The model card information is seen when previewing the model prior to importing it. [Model cards](https://modelcards.withgoogle.com/about) serve as an accessible reference for an AI model. They allow the student to document decisions. And analyzing model cards in the curriculum helps students to [explore](https://codeorg.medium.com/code-org-curriculum-now-teaches-ai-to-every-student-f4d09895be15) issues of bias and ethics.

#### Model Summary

The student can see a summary of the model that they have just saved. Proceeding from here will go to the next level in the progression.
The student can see a summary of the model that they have just exported. Proceeding from here will go to the next level in the progression.

## Common operations

Expand Down
62 changes: 61 additions & 1 deletion frontend/packages/labs/ailab/i18n/mlPlayground.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,24 @@
"tryAgain": "Try again",
"navigateDone": "Finish",
"saveProgress": "Save",
"exportProgress": "Export",
"navigationTabsAriaLabel": "AI Lab sections",
"navigationTabAlgorithm": "Algorithm",
"navigationTabDataset": "Dataset",
"navigationTabTrain": "Train",
"navigationTabTest": "Test",
"navigationTabExport": "Export",
"navigationAlgorithmLabel": "Algorithm",
"navigationAlgorithmRestartAriaLabel": "Start over from algorithm selection. Current algorithm: {algorithm}.",
"algorithmResetDialogTitle": "Start over?",
"algorithmResetDialogMessage": "Are you sure you want to start over? Any training you've done on the model will be reset.",
"algorithmResetDialogCancel": "Cancel",
"algorithmResetDialogConfirm": "Start over",
"selectAlgorithmHeading": "Choose an algorithm",
"algorithmKnnName": "K-Nearest Neighbors",
"algorithmKnnDescription": "Uses similar rows in the dataset to make a prediction.",
"algorithmDecisionTreeName": "Decision Tree",
"algorithmDecisionTreeDescription": "Builds a tree of questions about the selected features.",
"correctAnswer": "Correct",
"incorrectAnswer": "Incorrect",
"potentialUsesLabel": "Intended Use",
Expand All @@ -17,6 +35,18 @@
"potentialMisusesDescriptionSituations": "Are there situations where this model definitely shouldn't be used?",
"potentialMisusesPlaceholder": "Write a brief description.",
"modelNameLabel": "Model name",
"exportModelHeading": "Export your model",
"exportModelDescription": "Publish this model for use in programming labs.",
"exportModelApiPreviewHeading": "Prediction block",
"exportModelApiPreviewDescription": "Use this block from a program to ask the model for a prediction.",
"exportModelApiResultLabel": "Returns",
"exportModelApiResult": "prediction",
"exportModelBlockAriaLabel": "Blockly block preview for getPrediction. Data fields: {fields}. Returns prediction.",
"exportModelBlockDataLabel": "data",
"exportModelBlockNoFields": "feature values",
"exportModelBlockNumberValue": "number",
"exportModelBlockTextValue": "text",
"exportModelBlockUsingLabel": "using",
"datasetDescriptionLabel": "About the Data",
"datasetDescriptionPlaceholder": "How was the data collected? Who collected it? When was it collected?",
"saveModelColumnCountLabel": "Column Descriptions",
Expand All @@ -37,6 +67,14 @@
"modelCardFeatures": "Features",
"dataDisplayRowCount": "There are {rowCount} rows of data.",
"dataDisplayRowCountTruncated": "There are {rowCount} rows of data. (Showing first {rowLimit} rows)",
"dataDisplayViewToggleAriaLabel": "Dataset view",
"dataDisplayTableView": "Table",
"dataDisplayCardsView": "Cards",
"dataCardsDeckPrevious": "Previous",
"dataCardsDeckNext": "Next",
"dataCardsDeckProgress": "Row {current} of {total}",
"dataCardsDeckCardTitle": "Row {rowNumber}",
"dataCardsDeckCardAriaLabel": "Dataset row {rowNumber}",
"columnInspectorDataType": "Data Type",
"columnInspectorDescription": "Description",
"columnDetailsInformation": "Column information",
Expand All @@ -45,8 +83,16 @@
"columnDetailsMaximumValue": "max",
"columnDetailsValueRange": "range",
"columnDetailsTooManyLabels": "{labelCount} values were found in this column. A graph is only shown when there are {maxLabelCount} or fewer.",
"columnDetailsOtherValues": "Other",
"columnDetailsTopValuesShown": "Showing the {shownCount} most common values, with {otherCount} more grouped as Other.",
"columnDetailsCategoricalChartAriaLabel": "Bar chart for {column}. Values shown: {values}.",
"columnDetailsHistogramLabel": "Value distribution",
"columnDetailsHistogramAriaLabel": "Histogram for {column}.",
"columnDetailsBoxPlotLabel": "Spread",
"columnDetailsBoxPlotAriaLabel": "Box plot. Minimum {min}, first quartile {q1}, median {median}, third quartile {q3}, maximum {max}.",
"selectLabelButton": "Select label",
"scatterPlotLabel": "Relationship information",
"mixedRelationshipPlotAriaLabel": "Relationship plot comparing {feature} and {label}.",
"addFeatureButton": "Add feature",
"columnType_numerical": "numerical",
"columnType_categorical": "categorical",
Expand All @@ -61,7 +107,7 @@
"dataCardLastUpdated": "Last updated",
"dataCardPotentialUses": "Potential uses",
"dataCardPotentialMisuses": "Potential misuses",
"saveStatus_success": "Your model was saved!",
"saveStatus_success": "Your model was exported!",
"saveStatus_failure": "There was an error. Your model did not save. Please try again.",
"saveStatus_piiProfanity": "Your model could not be saved because it contains profanity or personally identifying information (e.g. email, address, phone number).",
"trainModelHeading": "Training",
Expand All @@ -81,6 +127,20 @@
"resultsTableActualValueHeader": "Actual",
"resultsTablePredictedValueHeader": "A.I. Prediction",
"resultsTablePredictionRange": "Predictions are +/- {percentage}% of range",
"resultsTablePopulateTestData": "Use prediction row {rowNumber} in Try it out",
"decisionTreeVisualizationHeading": "Decision Tree",
"decisionTreeSvgLabel": "Decision tree diagram",
"decisionTreeLeafLabel": "Prediction",
"decisionTreeTraceHeading": "Path Trace",
"decisionTreeTraceEmpty": "No feature values selected.",
"decisionTreeTracePrevious": "Previous",
"decisionTreeTraceNext": "Next",
"decisionTreeTraceProgress": "Step {current} of {total}",
"decisionTreeTraceCurrentStep": "current",
"decisionTreeTraceDecision": "{feature}: {value} -> {branch}",
"decisionTreeTracePrediction": "Prediction: {prediction}",
"decisionTreeTraceDefault": "No matching branch; using the default prediction.",
"decisionTreeDefaultBranch": "default",
"predictHeader": "Try it out!",
"predictButton": "Predict",
"predictAIBotPredicts": "A.I. predicts",
Expand Down
Loading
Loading