talisman
Version:
Straightforward fuzzy matching, information retrieval and NLP building blocks for JavaScript.
321 lines (229 loc) • 9.75 kB
JavaScript
'use strict';
Object.defineProperty(exports, "__esModule", {
value: false
});
exports.NNDescentClusterer = undefined;
exports.default = nnDescent;
var _abstract = require('./abstract');
var _abstract2 = _interopRequireDefault(_abstract);
var _choice = require('pandemonium/choice');
var _geometricReservoirSample = require('pandemonium/geometric-reservoir-sample');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } /* eslint no-constant-condition: 0 */
/**
* Talisman clustering/nn-descent
* ===============================
*
* JavaScript implementation of the NN-Descent algorithm designed to generate
* k-NN graphs approximations in a performant fashion.
*
* [Reference]:
* http://www.cs.princeton.edu/cass/papers/www11.pdf
*
* [Article]:
* "Efficient K-Nearest Neighbor Graph Construction for Generic Similarity
* Measures" Wei Dong, Moses Charikar, Kai Li.
*/
// TODO: JSDoc
/**
* Defaults.
*/
var DEFAULTS = {
// Sampling coefficient
rho: 0.5,
// Early termination coefficient
delta: 0.001,
// Maximum number of iterations to perform
maxIterations: Infinity,
// RNG to use
rng: Math.random
};
/**
* NN-Descent Clusterer class.
*
* @constructor
*/
var NNDescentClusterer = exports.NNDescentClusterer = function (_RecordLinkageCluster) {
_inherits(NNDescentClusterer, _RecordLinkageCluster);
function NNDescentClusterer(params, items) {
_classCallCheck(this, NNDescentClusterer);
// Checking rho
var _this = _possibleConstructorReturn(this, _RecordLinkageCluster.call(this, params, items));
_this.rho = params.rho || DEFAULTS.rho;
if (typeof _this.rho !== 'number' || _this.rho > 1 || _this.rho <= 0) throw new Error('talisman/clustering/record-linkage/nn-descent: rho should be a number greater than 0 and less or equal than 1.');
// Checking delta
_this.delta = params.delta || DEFAULTS.delta;
if (typeof _this.delta !== 'number' || _this.delta >= 1 || _this.delta <= 0) throw new Error('talisman/clustering/record-linkage/nn-descent: delta should be a number greater than 0 and less than 1.');
// Checking maxIterations
_this.maxIterations = params.maxIterations || DEFAULTS.maxIterations;
if (_this.maxIterations <= 0) throw new Error('talisman/clustering/record-linkage/nn-descent: maxIterations should be > 0');
// Checking similarity
_this.similarity = params.similarity;
if (typeof _this.similarity !== 'function') throw new Error('talisman/clustering/record-linkage/nn-descent: similarity should be a function.');
// Checking RNG
_this.rng = params.rng || DEFAULTS.rng;
if (typeof _this.rng !== 'function') throw new Error('talisman/clustering/record-linkage/nn-descent: rng should be a function.');
_this.sampleFunction = (0, _geometricReservoirSample.createGeometricReservoirSample)(_this.rng);
_this.choiceFunction = (0, _choice.createChoice)(_this.rng);
// Checking k
_this.k = params.k;
if (typeof _this.k !== 'number' || _this.k <= 0) throw new Error('talisman/clustering/record-linkage/nn-descent: k should be > 0');
// Properties
_this.iterations = 0;
_this.computations = 0;
_this.c = 0;
return _this;
}
NNDescentClusterer.prototype.sampleItems = function sampleItems(forItem) {
var _this2 = this;
var items = new Set(this.sampleFunction(this.k, this.items));
// The original item should obviously not be in the sample
if (items.has(forItem)) {
items.delete(forItem);
while (items.size < this.k) {
items.add(this.choiceFunction(this.items));
}
}
return Array.from(items).map(function (item) {
return {
item: item,
similarity: _this2.similarity(item, forItem),
processed: false
};
});
};
NNDescentClusterer.prototype.sample = function sample(items, n) {
// NOTE: Probably possible to mutate here, but not sure.
if (items.length <= n) return items.slice();
return this.sampleFunction(n, items);
};
NNDescentClusterer.prototype.pickFalses = function pickFalses(elements) {
var list = [];
for (var i = 0, l = elements.length; i < l; i++) {
var element = elements[i];
if (element.processed) list.push(element.item);
}
return list;
};
NNDescentClusterer.prototype.pickTruesAndMarkFalses = function pickTruesAndMarkFalses(elements) {
var list = [];
for (var i = 0, l = elements.length; i < l; i++) {
var element = elements[i];
if (!element.processed && this.rng() < this.rho) {
element.processed = true;
list.push(element.item);
}
}
return list;
};
NNDescentClusterer.prototype.reverse = function reverse(lists) {
var R = new Map();
for (var i = 0, l = this.items.length; i < l; i++) {
R.set(this.items[i], []);
}for (var _i = 0, _l = this.items.length; _i < _l; _i++) {
var item = this.items[_i],
list = lists.get(item);
for (var j = 0, m = list.length; j < m; j++) {
R.get(list[j]).push(item);
}
}
return R;
};
NNDescentClusterer.prototype.union = function union(a, b) {
var set = new Set(a);
for (var i = 0, l = b.length; i < l; i++) {
set.add(b[i]);
}return Array.from(set);
};
NNDescentClusterer.prototype.updateNN = function updateNN(K, item, similarity) {
// NOTE: this is a naive approach that could be bested by a priority queue
// or by caching the min similarity + holding elements in a Set
var minSimilarity = Infinity,
minSimilarityIndex = -1;
for (var i = 0, l = K.length; i < l; i++) {
var element = K[i];
if (item === element.item) return;
if (element.similarity < minSimilarity) {
minSimilarity = element.similarity;
minSimilarityIndex = i;
}
}
if (minSimilarity < similarity) {
// Replacing the item
K[minSimilarityIndex] = {
item: item,
similarity: similarity,
processed: false
};
// NOTE: we could avoid to store c in instance state by making this
// function return something meaningful.
this.c++;
}
};
NNDescentClusterer.prototype.run = function run() {
var B = new Map(),
rhok = Math.ceil(this.rho * this.k);
for (var i = 0, l = this.items.length; i < l; i++) {
var item = this.items[i];
B.set(item, this.sampleItems(item));
}
var before = new Map(),
current = new Map();
// Performing the iterations
while (true) {
this.iterations++;
this.c = 0;
for (var _i2 = 0, _l2 = this.items.length; _i2 < _l2; _i2++) {
var _item = this.items[_i2];
before.set(_item, this.pickFalses(B.get(_item)));
current.set(_item, this.pickTruesAndMarkFalses(B.get(_item)));
}
var before2 = this.reverse(before),
current2 = this.reverse(current);
for (var _i3 = 0, _l3 = this.items.length; _i3 < _l3; _i3++) {
var _item2 = this.items[_i3];
before.set(_item2, this.union(before.get(_item2), this.sample(before2.get(_item2), rhok)));
current.set(_item2, this.union(current.get(_item2), this.sample(current2.get(_item2), rhok)));
var currentTargets = current.get(_item2),
beforeTargets = before.get(_item2);
for (var j = 0, m = currentTargets.length; j < m; j++) {
var u1 = currentTargets[j];
for (var k = j + 1; k < m; k++) {
var u2 = currentTargets[k],
similarity = this.similarity(u1, u2);
this.computations++;
this.updateNN(B.get(u1), u2, similarity);
this.updateNN(B.get(u2), u1, similarity);
}
for (var _k = 0, n = beforeTargets.length; _k < n; _k++) {
var _u = beforeTargets[_k];
if (u1 === _u) continue;
var _similarity = this.similarity(u1, _u);
this.computations++;
this.updateNN(B.get(u1), _u, _similarity);
this.updateNN(B.get(_u), u1, _similarity);
}
}
}
// Termination?
// console.log('iteration', this.c, this.delta * this.items.length * this.k, this.computations);
if (this.iterations >= this.maxIterations || this.c <= this.delta * this.items.length * this.k) break;
}
return B;
};
return NNDescentClusterer;
}(_abstract2.default);
/**
* Shortcut function for the NN-Descent clusterer.
*
* @param {object} params - Clusterer parameters.
* @param {array} items - Items to cluster.
*/
function nnDescent(params, items) {
var clusterer = new NNDescentClusterer(params, items);
return clusterer.run();
}
module.exports = exports['default'];
exports['default'].NNDescentClusterer = exports.NNDescentClusterer;