UNPKG

talisman

Version:

Straightforward fuzzy matching, information retrieval and NLP building blocks for JavaScript.

321 lines (229 loc) 9.75 kB
'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;