blob: 4cced631736993a4c6c482ad62438afd50740280 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
* smart-service-implementation includes this application's specific
* implementations of the smart-service.
*/
var addAttributes = require('../../lib/add-attributes');
var hashInterface = require('../namespace/interface-util').hashInterface;
var log = require('../../lib/log')('services:smart-service');
var perceptron = require('../../lib/learning/perceptron');
var rank = require('../../lib/learning/rank');
var _ = require('lodash');
var LEARNER_SHORTCUT = 1;
var LEARNER_AUTORPC = 2;
var LEARNER_METHOD_INPUT = 3;
var LEARNER_METHOD_INVOCATION = 4;
// Associate the learner types with the constructor
var LEARNER_MAP = {};
LEARNER_MAP[LEARNER_SHORTCUT] = shortcutLearner;
LEARNER_MAP[LEARNER_AUTORPC] = autoRPCLearner;
LEARNER_MAP[LEARNER_METHOD_INPUT] = methodInputLearner;
LEARNER_MAP[LEARNER_METHOD_INVOCATION] = methodInvocationLearner;
// Associate the learner types with additional functions.
// Note: update and predict are required.
var LEARNER_METHODS = {};
LEARNER_METHODS[LEARNER_SHORTCUT] = {
featureExtractor: shortcutLearnerFeatureExtractor,
update: shortcutLearnerUpdate,
predict: shortcutLearnerPredict
};
LEARNER_METHODS[LEARNER_AUTORPC] = {
featureExtractor: autoRPCLearnerFeatureExtractor,
update: autoRPCLearnerUpdate,
predict: autoRPCLearnerPredict
};
LEARNER_METHODS[LEARNER_METHOD_INPUT] = {
computeKey: methodInputLearnerComputeKey,
update: topKLearnerUpdate,
predict: topKLearnerPredict
};
LEARNER_METHODS[LEARNER_METHOD_INVOCATION] = {
computeKey: methodInvocationLearnerComputeKey,
update: topKLearnerUpdate,
predict: topKLearnerPredict
};
// Export the implementation constants
module.exports = {
LEARNER_SHORTCUT: LEARNER_SHORTCUT,
LEARNER_AUTORPC: LEARNER_AUTORPC,
LEARNER_METHOD_INPUT: LEARNER_METHOD_INPUT,
LEARNER_METHOD_INVOCATION: LEARNER_METHOD_INVOCATION,
LEARNER_MAP: LEARNER_MAP,
LEARNER_METHODS: LEARNER_METHODS
};
/*
* Create a shortcut learner that analyzes directory paths visited and predicts
* the most useful shortcuts.
* The expected attributes in params include:
* - k, the max # of shortcuts to return
*/
function shortcutLearner(type, params) {
this.directoryCount = {};
this.type = type;
this.params = params;
addAttributes(this, LEARNER_METHODS[type]);
}
/*
* Given an input name, return relevant features for the shortcut learner.
*/
function shortcutLearnerFeatureExtractor(name) {
return pathFeatureExtractor(name);
}
/*
* Given an input, extract the relevant feature vector and update the weights
* of the learner.
* input contains name and weight (default 1). Note: weight can be negative.
*/
function shortcutLearnerUpdate(input) {
var features = this.featureExtractor(input.name);
input.weight = input.weight || 1;
_.forOwn(features, function(value, key) {
if (this.directoryCount[key] === undefined) {
this.directoryCount[key] = 0;
}
this.directoryCount[key] += features[key] * input.weight;
}, this);
}
/*
* Given an input, determine which children are most popular.
* The input should have "name" (string) and "exclude" (Array<string>).
*/
function shortcutLearnerPredict(input) {
// Make sure to set proper defaults for bad input.
var defaults = {
name: '',
exclude: [],
penalize: true
};
input = _.assign({}, defaults, input);
// Also ensure that k, the number of children to return, is defined.
var k = this.params.k || 1;
var penalize = input.penalize;
log.debug('Predict top', k, 'children under', input.name, 'excluding',
input.exclude);
// First score the items that are prefixed by the input name.
// Separate the scored items from the excluded items.
var scoredItems = [];
var excludedItems = [];
_.forOwn(this.directoryCount, function(score, item) {
if (item.indexOf(input.name) === 0) {
var scoredItem = {
item: item,
score: score
};
if (input.exclude.indexOf(item) === -1) {
scoredItems.push(scoredItem);
} else {
excludedItems.push(scoredItem);
}
}
});
// Next, penalize all scoredItems by the excludedItems.
if (penalize) {
excludedItems.forEach(function(excludedItem) {
rank.applyDiversityPenalty(
scoredItems,
excludedItem,
shortcutLearnerFeatureExtractor,
excludedItem.score
);
});
}
// Then determine the top k items including diversity.
// TODO(alexfandrianto): This step forces us to take O(kn) runtime. Is there a
// faster way to find the 'best' shortcuts?
var topK = [];
for (var i = 0; i < k; i++) {
var bestItemIndex = rank.getBestItemIndex(scoredItems);
if (bestItemIndex >= 0) {
topK.push(scoredItems[bestItemIndex]);
} else {
return topK; // return early since there are no more top items.
}
// If we haven't yet found all topK, penalize similar items.
if (i < k - 1) {
// Remove the most recent top item, and return early if we can.
scoredItems.splice(bestItemIndex, 1);
if (scoredItems.length === 0) {
return topK;
}
// Otherwise, penalize all remaining items.
if (penalize) {
rank.applyDiversityPenalty(
scoredItems,
topK[i],
shortcutLearnerFeatureExtractor,
topK[i].score
);
}
}
}
return topK;
}
/*
* TODO(alexfandrianto): Don't ignore 'params'. Improve this algorithm.
* Create an autorpc learner that learns which RPCs should be performed
* automatically.
*/
function autoRPCLearner(type, params) {
this.weights = {};
this.type = type;
this.learningRate = 0.05;
addAttributes(this, LEARNER_METHODS[type]);
}
/*
* Given input data, return an appropriate feature vector for RPCs.
* Input must have: methodName, interface, and name.
*/
function autoRPCLearnerFeatureExtractor(input) {
var features = {};
// The user may have an innate bias for making RPCs.
features['_biasTerm'] = 1;
// Same-named methods may act similarly and might want to be queried too.
features[input.methodName] = 1;
// Same-named methods that share service interfaces are likely similar.
features[input.methodName + '|' + hashInterface(input.interface)] = 1;
// Services in the same namespace subtree may be queried similarly.
var pathFeatures = pathFeatureExtractor(input.name);
addAttributes(features, pathFeatures);
// Services in the same namespace subtree with this method name are also
// likely to be queried similarly.
for (var key in pathFeatures) {
if (pathFeatures.hasOwnProperty(key)) {
features[input.methodName + '|' + key] = pathFeatures[key];
}
}
return features;
}
/*
* Given input data, update the learner's weights.
* Input must have: methodName, interface, name, and reward.
* TODO(alexfandrianto): Remove the weights printout.
*/
function autoRPCLearnerUpdate(input) {
perceptron.update(
this.weights,
this.featureExtractor(input),
input.reward,
this.learningRate
);
log.debug('Final weights: ', this.weights);
}
/*
* Given input data, return the predicted reward.
*/
function autoRPCLearnerPredict(input) {
return perceptron.predict(this.weights, this.featureExtractor(input));
}
/*
* Create a method input learner that suggests the most likely inputs to a
* given argument of a method.
* Params can optionally include:
* - minThreshold, the minimum score of a suggestable value
* - maxValues, the largest number of suggestable values that may be returned
* - penalty, a constant for the rate to penalize incorrect suggestions
* - reward, a constant for the rate to reward chosen values
*
* Uses a simple topK Update and Prediction function.
* This learner's input needs to have argName, methodName, and interface.
* Update also needs an argument value.
*/
function methodInputLearner(type, params) {
this.type = type;
this.inputMap = {}; // map[string]map[string]number
// Override the default params with relevant fields from params.
this.params = {
penalty: 0.1,
reward: 0.4
};
_.assign(this.params, params);
addAttributes(this, LEARNER_METHODS[type]);
}
/*
* Given input data, compute the appropriate lookup key.
*/
function methodInputLearnerComputeKey(input) {
var keyArr = [
hashInterface(input.interface),
input.methodName,
input.argName
];
return keyArr.join('|');
}
/*
* Create a method invocation learner that suggests the most likely invocations
* for a given method.
* Params can optionally include:
* - minThreshold, the minimum score of a suggestable value
* - maxValues, the largest number of suggestable values that may be returned
* - penalty, a constant for the rate to penalize incorrect suggestions
* - reward, a constant for the rate to reward chosen values
*
* Uses a simple topK Update and Prediction function.
* This learner's input needs to have a methodName and interface.
* Update also needs a JSON-encoded arguments string.
*/
function methodInvocationLearner(type, params) {
this.type = type;
this.inputMap = {}; // map[string]map[string]number
// Override the default params with relevant fields from params.
this.params = {
penalty: 0.1,
reward: 0.4
};
_.assign(this.params, params);
addAttributes(this, LEARNER_METHODS[type]);
}
/*
* Given input data, compute the appropriate lookup key
* Input must have: methodName and interface.
*/
function methodInvocationLearnerComputeKey(input) {
var keyArr = [
hashInterface(input.interface),
input.methodName
];
return keyArr.join('|');
}
/*
* Given input data, boost the rank of the given value and penalize others.
* Note: Learners using this predict function need to be similar structurally.
* input can contain:
* - fields used to compute the key
* - value to update for that key
* - (optional) reset: a flag that resets the value
*/
function topKLearnerUpdate(input) {
var key = this.computeKey(input);
var predValues = this.predict(input);
var value = input.value;
// Setup the inputMap and values if not yet defined.
if (this.inputMap[key] === undefined) {
this.inputMap[key] = {};
}
var values = this.inputMap[key];
if (values[value] === undefined) {
values[value] = 0;
}
// Reset the value (for negative feedback).
if (input.reset) {
delete values[value];
return;
}
// Give a reward to the chosen value.
values[value] += this.params.reward * (1 - values[value]);
// Induce a penalty on failed predictions.
for (var i = 0; i < predValues.length; i++) {
var pred = predValues[i];
if (pred !== value) {
values[pred] += this.params.penalty * (0 - values[pred]);
}
}
}
/*
* Given input data, predict the most likely values.
* Note: Learners using this predict function need to be similar structurally.
*/
function topKLearnerPredict(input) {
var key = this.computeKey(input);
var values = this.inputMap[key];
// Immediately return nothing if there are no values to suggest.
if (values === undefined) {
return [];
}
// Convert the values to scored items for ranking.
var scoredItems = Object.getOwnPropertyNames(values).map(
function getScoredItem(value) {
return {
item: value,
score: values[value]
};
}
);
// Filter the scored items by minThreshold
if (this.params.minThreshold !== undefined) {
scoredItems = scoredItems.filter(function applyThreshold(scoredItem) {
return scoredItem.score >= this.params.minThreshold;
}, this);
}
// Rank the scored items and return the top values (limit to maxValues)
var maxValues = this.params.maxValues;
if (maxValues === undefined || maxValues < 0) {
maxValues = scoredItems.length;
}
var bestK = rank.getBestKItems(scoredItems, maxValues);
return bestK === null ? [] : bestK.map(function(goodItem) {
return goodItem.item;
});
}
/*
* Given a path string, this feature extractor assigns diminishing returns
* credit to each ancestor along the path.
*/
function pathFeatureExtractor(path) {
var vector = {};
var split = path.split('/');
var growingPath = '';
for (var i = 0; i < split.length; i++) {
if (split[i] === '') {
continue;
}
if (i === 0) {
growingPath += split[i];
} else {
growingPath += '/' + split[i];
}
// give 1, 1/2, 1/4, 1/8, ... credit assignment
vector[growingPath] = Math.pow(2, i+1-split.length);
}
return vector;
}