mirror of
https://github.com/keymanapp/keyman.git
synced 2026-09-24 08:37:42 +00:00
Merge pull request #16600 from keymanapp/change/web/lexically-weight-prediction-search
change(web): add lexical weighting to prediction search
This commit is contained in:
commit
bf98a2f03b
14 changed files with 142 additions and 180 deletions
|
|
@ -27,10 +27,16 @@ export interface CorrectionResultMapping<ResultType> {
|
|||
readonly matchedResult: Readonly<ResultType>;
|
||||
|
||||
/**
|
||||
* Gets the "total cost" of the edge, which should be considered as the
|
||||
* Gets the "correction cost" of the edge, which should be considered as the
|
||||
* negative log-likelihood of the input path taken to reach the node
|
||||
* multiplied by the 'probability' induced by needed Damerau-Levenshtein edits
|
||||
* to the resulting output.
|
||||
*/
|
||||
readonly totalCost: number;
|
||||
readonly correctionCost: number;
|
||||
|
||||
/**
|
||||
* The "total cost" of the edge - comprised of both the correction cost and the
|
||||
* prediction cost based on the model's frequency data for the word.
|
||||
*/
|
||||
readonly currentCost: number;
|
||||
}
|
||||
|
|
@ -29,10 +29,14 @@ type CompleteSearchPath<MappingType> = {
|
|||
|
||||
export type PathResult<MappingType> = NullPath | IntermediateSearchPath | CompleteSearchPath<MappingType>;
|
||||
|
||||
export function CORRECTION_QUEUE_COMPARATOR<T extends {currentCost: number}>(a: T, b: T) {
|
||||
export function PREDICTION_QUEUE_COMPARATOR<T extends {currentCost: number}>(a: T, b: T) {
|
||||
return a.currentCost - b.currentCost;
|
||||
}
|
||||
|
||||
export function CORRECTION_QUEUE_COMPARATOR<T extends {correctionCost: number}>(a: T, b: T) {
|
||||
return a.correctionCost - b.correctionCost;
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents objects that support correction search via the `getBestMatches`
|
||||
* method, providing metadata relative to optimizing the search process for
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import { PriorityQueue } from 'keyman/common/web-utils';
|
|||
import { LexicalModelTypes } from '@keymanapp/common-types';
|
||||
|
||||
import { ClassicalDistanceCalculation } from './classical-calculation.js';
|
||||
import { CORRECTION_QUEUE_COMPARATOR, CorrectionSearchable } from './correction-searchable.js';
|
||||
import { PREDICTION_QUEUE_COMPARATOR, CorrectionSearchable } from './correction-searchable.js';
|
||||
import { CorrectionResultMapping } from './correction-result-mapping.js';
|
||||
import { ExecutionTimer, STANDARD_TIME_BETWEEN_DEFERS } from './execution-timer.js';
|
||||
import { SearchQuotientNode } from './search-quotient-node.js';
|
||||
|
|
@ -262,7 +262,7 @@ export class SearchNode {
|
|||
* The correction search evaluates Nodes in cost-ascending order based on this property's
|
||||
* return value.
|
||||
*/
|
||||
get currentCost(): number {
|
||||
get correctionCost(): number {
|
||||
// - We reintrepret 'known cost' as a psuedo-probability.
|
||||
// - Noting that 1/e = 0.367879441, an edit-distance cost of 1 may be intepreted as -ln(1/e) - a log-space 'likelihood'.
|
||||
// - Not exactly normalized, though.
|
||||
|
|
@ -278,6 +278,14 @@ export class SearchNode {
|
|||
return EDIT_DISTANCE_COST_SCALE * this.editCount + this.inputSamplingCost;
|
||||
}
|
||||
|
||||
get predictionCost(): number {
|
||||
return -Math.log(this.currentTraversal.p);
|
||||
}
|
||||
|
||||
get currentCost(): number {
|
||||
return this.correctionCost + this.predictionCost;
|
||||
}
|
||||
|
||||
addEdit() {
|
||||
this.addedEditCost++;
|
||||
}
|
||||
|
|
@ -625,7 +633,7 @@ export async function *getBestMatches<
|
|||
// If no filter function is provided, default to one that always returns true.
|
||||
filter ??= () => true;
|
||||
|
||||
let spaceQueue = new PriorityQueue<Correctable>(CORRECTION_QUEUE_COMPARATOR);
|
||||
let spaceQueue = new PriorityQueue<Correctable>(PREDICTION_QUEUE_COMPARATOR);
|
||||
|
||||
// Stage 1 - if we already have extracted results, build a queue just for them
|
||||
// and iterate over it first.
|
||||
|
|
@ -633,7 +641,7 @@ export async function *getBestMatches<
|
|||
// Does not get any results that another iterator pulls up after this is
|
||||
// created - and those results won't come up later in stage 2, either. Only
|
||||
// intended for restarting a search, not searching twice in parallel.
|
||||
const priorResultsQueue = new PriorityQueue<ResultMapping>((a, b) => a.totalCost - b.totalCost);
|
||||
const priorResultsQueue = new PriorityQueue<ResultMapping>(PREDICTION_QUEUE_COMPARATOR);
|
||||
priorResultsQueue.enqueueAll(searchModules.map((space) => space.previousResults).flat());
|
||||
|
||||
// With potential prior results re-queued, NOW enqueue. (Not before - the heap may reheapify!)
|
||||
|
|
@ -642,7 +650,7 @@ export async function *getBestMatches<
|
|||
// Stage 2: the fun part; actually searching!
|
||||
do {
|
||||
const entry: ResultMapping = timer.time(() => {
|
||||
if((priorResultsQueue.peek()?.totalCost ?? Number.POSITIVE_INFINITY) <= spaceQueue.peek().currentCost) {
|
||||
if((priorResultsQueue.peek()?.currentCost ?? Number.POSITIVE_INFINITY) <= spaceQueue.peek().currentCost) {
|
||||
const result = priorResultsQueue.dequeue();
|
||||
|
||||
// There's no guarantee that the filter closure is the same instance as
|
||||
|
|
@ -669,7 +677,7 @@ export async function *getBestMatches<
|
|||
let lowestCostSource = spaceQueue.dequeue();
|
||||
const newResult = lowestCostSource.handleNextNode();
|
||||
spaceQueue.enqueue(lowestCostSource);
|
||||
spaceQueue = new PriorityQueue(CORRECTION_QUEUE_COMPARATOR, spaceQueue.toArray());
|
||||
spaceQueue = new PriorityQueue(PREDICTION_QUEUE_COMPARATOR, spaceQueue.toArray());
|
||||
|
||||
if(newResult.type == 'none') {
|
||||
return null;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { PriorityQueue } from 'keyman/common/web-utils';
|
||||
import { LexicalModelTypes } from '@keymanapp/common-types';
|
||||
|
||||
import { CORRECTION_QUEUE_COMPARATOR, PathResult } from './correction-searchable.js';
|
||||
import { PREDICTION_QUEUE_COMPARATOR, PathResult } from './correction-searchable.js';
|
||||
import { SearchQuotientNode } from './search-quotient-node.js';
|
||||
import { SearchQuotientRoot } from './search-quotient-root.js';
|
||||
import { SearchNode } from './distance-modeler.js';
|
||||
|
|
@ -10,7 +10,7 @@ import LexicalModel = LexicalModelTypes.LexicalModel;
|
|||
import { TokenResultMapping } from './token-result-mapping.js';
|
||||
|
||||
export class LegacyQuotientRoot extends SearchQuotientRoot {
|
||||
private selectionQueue: PriorityQueue<SearchNode> = new PriorityQueue(CORRECTION_QUEUE_COMPARATOR);
|
||||
private selectionQueue: PriorityQueue<SearchNode> = new PriorityQueue(PREDICTION_QUEUE_COMPARATOR);
|
||||
private processed: SearchNode[] = [];
|
||||
|
||||
constructor(model: LexicalModel) {
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
import { LexicalModelTypes } from '@keymanapp/common-types';
|
||||
import { KMWString, PriorityQueue } from 'keyman/common/web-utils';
|
||||
|
||||
import { CORRECTION_QUEUE_COMPARATOR, PathResult } from './correction-searchable.js';
|
||||
import { PREDICTION_QUEUE_COMPARATOR, PathResult } from './correction-searchable.js';
|
||||
import { SearchNode } from './distance-modeler.js';
|
||||
import { SearchQuotientNode, PathInputProperties } from './search-quotient-node.js';
|
||||
import { SearchQuotientSpur } from './search-quotient-spur.js';
|
||||
|
|
@ -24,7 +24,7 @@ import Transform = LexicalModelTypes.Transform;
|
|||
// The set of search spaces corresponding to the same 'context' for search.
|
||||
// Whenever a wordbreak boundary is crossed, a new instance should be made.
|
||||
export class LegacyQuotientSpur extends SearchQuotientSpur {
|
||||
private transposeQueue: PriorityQueue<SearchNode> = new PriorityQueue(CORRECTION_QUEUE_COMPARATOR);
|
||||
private transposeQueue: PriorityQueue<SearchNode> = new PriorityQueue(PREDICTION_QUEUE_COMPARATOR);
|
||||
private incomingTransposeRootNodes: TokenResultMapping[] = [];
|
||||
|
||||
public readonly insertLength: number;
|
||||
|
|
|
|||
|
|
@ -11,7 +11,7 @@
|
|||
import { PriorityQueue } from 'keyman/common/web-utils';
|
||||
import { LexicalModelTypes } from '@keymanapp/common-types';
|
||||
|
||||
import { CORRECTION_QUEUE_COMPARATOR, PathResult } from './correction-searchable.js';
|
||||
import { PREDICTION_QUEUE_COMPARATOR, PathResult } from './correction-searchable.js';
|
||||
import { LegacyQuotientRoot } from './legacy-quotient-root.js';
|
||||
import { generateSpaceSeed, InputSegment, SearchQuotientNode } from './search-quotient-node.js';
|
||||
import { SearchQuotientSpur } from './search-quotient-spur.js';
|
||||
|
|
@ -20,7 +20,7 @@ import { TokenResultMapping } from './token-result-mapping.js';
|
|||
// The set of search spaces corresponding to the same 'context' for search.
|
||||
// Whenever a wordbreak boundary is crossed, a new instance should be made.
|
||||
export class SearchQuotientCluster extends SearchQuotientNode {
|
||||
private selectionQueue: PriorityQueue<SearchQuotientNode> = new PriorityQueue(CORRECTION_QUEUE_COMPARATOR);
|
||||
private selectionQueue: PriorityQueue<SearchQuotientNode> = new PriorityQueue(PREDICTION_QUEUE_COMPARATOR);
|
||||
readonly spaceId: number;
|
||||
|
||||
// We use an array and not a PriorityQueue b/c batch-heapifying at a single
|
||||
|
|
@ -102,7 +102,7 @@ export class SearchQuotientCluster extends SearchQuotientNode {
|
|||
entries.forEach((path) => path.increaseMaxEditDistance());
|
||||
|
||||
// Since we just modified the stored instances, and the costs may have shifted, we need to re-heapify.
|
||||
this.selectionQueue = new PriorityQueue<SearchQuotientNode>(CORRECTION_QUEUE_COMPARATOR, entries.slice());
|
||||
this.selectionQueue = new PriorityQueue<SearchQuotientNode>(PREDICTION_QUEUE_COMPARATOR, entries.slice());
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -131,7 +131,7 @@ export class SearchQuotientCluster extends SearchQuotientNode {
|
|||
const bestPath = this.selectionQueue.dequeue();
|
||||
const baseResult = bestPath.handleNextNode();
|
||||
this.selectionQueue.enqueue(bestPath);
|
||||
this.selectionQueue = new PriorityQueue(CORRECTION_QUEUE_COMPARATOR, this.selectionQueue.toArray());
|
||||
this.selectionQueue = new PriorityQueue(PREDICTION_QUEUE_COMPARATOR, this.selectionQueue.toArray());
|
||||
|
||||
let finalResult = baseResult;
|
||||
if(baseResult.type == 'complete') {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { KMWString, PriorityQueue } from 'keyman/common/web-utils';
|
|||
import { LexicalModelTypes } from '@keymanapp/common-types';
|
||||
import { buildMergedTransform } from '@keymanapp/models-templates';
|
||||
|
||||
import { CORRECTION_QUEUE_COMPARATOR, PathResult } from './correction-searchable.js';
|
||||
import { PREDICTION_QUEUE_COMPARATOR, PathResult } from './correction-searchable.js';
|
||||
import { EDIT_DISTANCE_COST_SCALE, SearchNode } from './distance-modeler.js';
|
||||
import { generateSpaceSeed, InputSegment, PathInputProperties, SearchQuotientNode } from './search-quotient-node.js';
|
||||
import { generateSubsetId } from './tokenization-subsets.js';
|
||||
|
|
@ -34,7 +34,7 @@ export const MAX_EDIT_THRESHOLD_FACTOR = 2.5;
|
|||
// The set of search spaces corresponding to the same 'context' for search.
|
||||
// Whenever a wordbreak boundary is crossed, a new instance should be made.
|
||||
export abstract class SearchQuotientSpur extends SearchQuotientNode {
|
||||
private selectionQueue: PriorityQueue<SearchNode> = new PriorityQueue(CORRECTION_QUEUE_COMPARATOR);
|
||||
private selectionQueue: PriorityQueue<SearchNode> = new PriorityQueue(PREDICTION_QUEUE_COMPARATOR);
|
||||
|
||||
/**
|
||||
* Holds all incoming Nodes generated from a parent `SearchSpace` that have not yet been
|
||||
|
|
@ -152,7 +152,7 @@ export abstract class SearchQuotientSpur extends SearchQuotientNode {
|
|||
entries.forEach(function(edge) { edge.calculation = edge.calculation.increaseMaxDistance(); });
|
||||
|
||||
// Since we just modified the stored instances, and the costs may have shifted, we need to re-heapify.
|
||||
this.selectionQueue = new PriorityQueue<SearchNode>(CORRECTION_QUEUE_COMPARATOR, entries);
|
||||
this.selectionQueue = new PriorityQueue<SearchNode>(PREDICTION_QUEUE_COMPARATOR, entries);
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -419,7 +419,7 @@ export abstract class SearchQuotientSpur extends SearchQuotientNode {
|
|||
// Allows a little 'wiggle room' + 2 "hard" edits.
|
||||
// Can be important if needed characters don't actually exist on the keyboard
|
||||
// ... or even just not the then-current layer of the keyboard.
|
||||
if(currentNode.currentCost > this.lowestPossibleSingleCost + MAX_EDIT_THRESHOLD_FACTOR * EDIT_DISTANCE_COST_SCALE) {
|
||||
if(currentNode.correctionCost > this.lowestPossibleSingleCost + MAX_EDIT_THRESHOLD_FACTOR * EDIT_DISTANCE_COST_SCALE) {
|
||||
return unmatchedResult;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,8 +33,8 @@ export function initTokenResultFilterer() {
|
|||
return false;
|
||||
}
|
||||
|
||||
if((priorReturnCosts.get(searchResult.matchString) ?? Number.MAX_VALUE) > searchResult.totalCost) {
|
||||
priorReturnCosts.set(searchResult.matchString, searchResult.totalCost);
|
||||
if((priorReturnCosts.get(searchResult.matchString) ?? Number.MAX_VALUE) > searchResult.correctionCost) {
|
||||
priorReturnCosts.set(searchResult.matchString, searchResult.correctionCost);
|
||||
|
||||
return true;
|
||||
} else {
|
||||
|
|
@ -118,6 +118,14 @@ export class TokenResultMapping implements CorrectionResultMapping<SearchNode> {
|
|||
* multiplied by the 'probability' induced by needed Damerau-Levenshtein edits
|
||||
* to the resulting output.
|
||||
*/
|
||||
get correctionCost(): number {
|
||||
return this.node.correctionCost;
|
||||
}
|
||||
|
||||
get currentCost(): number {
|
||||
return this.node.currentCost;
|
||||
}
|
||||
|
||||
get totalCost(): number {
|
||||
return this.node.currentCost;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -532,7 +532,7 @@ export function buildAndMapPredictions(
|
|||
transition: ContextTransition,
|
||||
tokenization: ContextTokenization,
|
||||
// Originally, Readonly<TokenResultMapping> - but we only need these three components here.
|
||||
match: Readonly<{matchString: string, totalCost: number, editCount: number}>,
|
||||
match: Readonly<{matchString: string, correctionCost: number, editCount: number}>,
|
||||
costFactor: number
|
||||
): CorrectionPredictionTuple[] {
|
||||
const model = transition.final.model;
|
||||
|
|
@ -545,7 +545,7 @@ export function buildAndMapPredictions(
|
|||
|
||||
// --- to move into predictFromCorrections ---
|
||||
let correction = match.matchString;
|
||||
let rootCost = match.totalCost;
|
||||
let rootCost = match.correctionCost;
|
||||
|
||||
// Replace the existing context with the correction.
|
||||
const correctionTransform: Transform = {
|
||||
|
|
@ -656,7 +656,7 @@ export async function correctAndEnumerate(
|
|||
|
||||
// Only run the correction search when corrections are enabled.
|
||||
let rawPredictions: CorrectionPredictionTuple[] = [];
|
||||
let bestCorrectionCost: number;
|
||||
let bestTotalCost: number;
|
||||
const correctionPredictionMap: Record<string, Distribution<Suggestion>> = {};
|
||||
for await(const match of getBestTokenMatches(searchModules, timer)) {
|
||||
// Corrections obtained: now to predict from them!
|
||||
|
|
@ -700,8 +700,8 @@ export async function correctAndEnumerate(
|
|||
const predictions = buildAndMapPredictions(transition, tokenization, match, costFactor);
|
||||
|
||||
// Only set 'best correction' cost when a correction ACTUALLY YIELDS predictions.
|
||||
if(predictions.length > 0 && bestCorrectionCost === undefined) {
|
||||
bestCorrectionCost = match.totalCost * costFactor;
|
||||
if(predictions.length > 0 && bestTotalCost === undefined) {
|
||||
bestTotalCost = match.totalCost * costFactor;
|
||||
}
|
||||
|
||||
// If we're getting the same prediction again, it's lower-cost. Update!
|
||||
|
|
@ -714,7 +714,7 @@ export async function correctAndEnumerate(
|
|||
|
||||
rawPredictions = rawPredictions.concat(predictions);
|
||||
|
||||
if(shouldStopSearchingEarly(bestCorrectionCost, match.totalCost, rawPredictions)) {
|
||||
if(shouldStopSearchingEarly(bestTotalCost, match.totalCost, rawPredictions)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
@ -738,20 +738,15 @@ export function shouldStopSearchingEarly(
|
|||
return true;
|
||||
// If enough have been found, we're safe to terminate earlier.
|
||||
} else if(rawPredictions.length >= ModelCompositor.MAX_SUGGESTIONS) {
|
||||
if(currentCorrectionCost >= bestCorrectionCost + CORRECTION_SEARCH_THRESHOLDS.REPLACEMENT_SEARCH_THRESHOLD) {
|
||||
// Very useful for stopping 'sooner' when words reach a sufficient length.
|
||||
return true;
|
||||
} else {
|
||||
// Sort the prediction list; we need them in descending probability order
|
||||
// for the next check.
|
||||
rawPredictions.sort((a, b) => b.totalProb - a.totalProb);
|
||||
// Sort the prediction list; we need them in descending probability order
|
||||
// for the next check.
|
||||
rawPredictions.sort((a, b) => b.totalProb - a.totalProb);
|
||||
|
||||
// If the best result at the current state of the search fails to beat the worst
|
||||
// pending suggestion from previous tiers, assume all further corrections will
|
||||
// similarly fail to win; terminate the search-loop.
|
||||
if(rawPredictions[ModelCompositor.MAX_SUGGESTIONS-1].totalProb > Math.exp(-currentCorrectionCost)) {
|
||||
return true;
|
||||
}
|
||||
// If the best result at the current state of the search fails to beat the worst
|
||||
// pending suggestion from previous tiers, assume all further corrections will
|
||||
// similarly fail to win; terminate the search-loop.
|
||||
if(rawPredictions[ModelCompositor.MAX_SUGGESTIONS-1].totalProb > Math.exp(-currentCorrectionCost)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { PriorityQueue } from 'keyman/common/web-utils';
|
|||
import { jsonFixture } from '@keymanapp/common-test-resources/model-helpers.mjs';
|
||||
import { LexicalModelTypes } from '@keymanapp/common-types';
|
||||
|
||||
import { CORRECTION_QUEUE_COMPARATOR, models, SearchNode } from '@keymanapp/lm-worker/test-index';
|
||||
import { CORRECTION_QUEUE_COMPARATOR, models, PREDICTION_QUEUE_COMPARATOR, SearchNode } from '@keymanapp/lm-worker/test-index';
|
||||
|
||||
import SENTINEL_CODE_UNIT = models.SENTINEL_CODE_UNIT;
|
||||
import Distribution = LexicalModelTypes.Distribution;
|
||||
|
|
@ -50,15 +50,6 @@ function edgeHasChars(edge: SearchNode, input: string, match: string) {
|
|||
return lastEntry(edge.calculation.matchSequence) == match;
|
||||
}
|
||||
|
||||
function findEdgesWithChars(edgeArray: SearchNode[], match: string) {
|
||||
let results = edgeArray.filter(function(value) {
|
||||
return lastEntry(value.calculation.matchSequence) == match;
|
||||
});
|
||||
|
||||
assert.isAtLeast(results.length, 1);
|
||||
return results;
|
||||
}
|
||||
|
||||
function fetchCommonTENode() {
|
||||
const rootSeed = SEARCH_EDGE_SEED++;
|
||||
const rootNode = new SearchNode(testModel.traverseFromRoot(), rootSeed, toKey);
|
||||
|
|
@ -130,7 +121,9 @@ describe('Correction Distance Modeler', () => {
|
|||
|
||||
assert.equal(rootNode.editCount, 0);
|
||||
assert.equal(rootNode.inputSamplingCost, 0);
|
||||
assert.equal(rootNode.currentCost, 0);
|
||||
assert.equal(rootNode.correctionCost, 0);
|
||||
assert.isAbove(rootNode.predictionCost, 0);
|
||||
assert.isAbove(rootNode.currentCost, 0);
|
||||
|
||||
assert.equal((rootNode.currentTraversal as TrieTraversal).prefix, '');
|
||||
assert.isFalse(rootNode.hasPartialInput);
|
||||
|
|
@ -151,7 +144,7 @@ describe('Correction Distance Modeler', () => {
|
|||
|
||||
assert.equal(clonedNode.editCount, 0);
|
||||
assert.equal(clonedNode.inputSamplingCost, 0);
|
||||
assert.equal(clonedNode.currentCost, 0);
|
||||
assert.equal(clonedNode.correctionCost, 0);
|
||||
|
||||
assert.equal((clonedNode.currentTraversal as TrieTraversal).prefix, '');
|
||||
assert.isFalse(clonedNode.hasPartialInput);
|
||||
|
|
@ -167,6 +160,9 @@ describe('Correction Distance Modeler', () => {
|
|||
// Verify aliasing for properties holding immutable objects
|
||||
assert.equal(clonedNode.calculation, originalNode.calculation);
|
||||
assert.equal(clonedNode.currentTraversal, originalNode.currentTraversal);
|
||||
|
||||
// Verify local values are properly copied.
|
||||
assert.equal(clonedNode.currentCost, originalNode.currentCost);
|
||||
});
|
||||
|
||||
it('properly deep-copies fully-processed nodes later in the search path', () => {
|
||||
|
|
@ -222,12 +218,12 @@ describe('Correction Distance Modeler', () => {
|
|||
|
||||
// *****
|
||||
|
||||
function assertSourceNodeProps(node: SearchNode) {
|
||||
function assertExpectedNodeProps(node: SearchNode) {
|
||||
assert.equal(node.resultKey, 'te');
|
||||
|
||||
assert.equal(node.editCount, 0);
|
||||
assert.equal(node.inputSamplingCost, -Math.log(firstLayerTransforms[0].p) - Math.log(secondLayerTransforms[0].p));
|
||||
assert.equal(node.currentCost, node.inputSamplingCost);
|
||||
assert.equal(node.correctionCost, node.inputSamplingCost);
|
||||
|
||||
assert.isFalse(node.hasPartialInput);
|
||||
assert.isFalse(node.isFullReplacement)
|
||||
|
|
@ -238,12 +234,12 @@ describe('Correction Distance Modeler', () => {
|
|||
assert.equal(node.spaceId, secondSpaceId);
|
||||
}
|
||||
|
||||
assertSourceNodeProps(teNode);
|
||||
assertExpectedNodeProps(teNode);
|
||||
|
||||
const clonedNode = new SearchNode(teNode);
|
||||
|
||||
// Root node properties; may as well re-assert 'em.
|
||||
assertSourceNodeProps(clonedNode);
|
||||
assertExpectedNodeProps(clonedNode);
|
||||
|
||||
// Avoid aliasing for properties holding mutable objects
|
||||
assert.notEqual(clonedNode.priorInput, teNode.priorInput);
|
||||
|
|
@ -308,12 +304,12 @@ describe('Correction Distance Modeler', () => {
|
|||
|
||||
// *****
|
||||
|
||||
function assertSourceNodeProps(node: SearchNode) {
|
||||
function assertExpectedNodeProbs(node: SearchNode) {
|
||||
assert.equal(node.resultKey, 'te');
|
||||
|
||||
assert.equal(node.editCount, 0);
|
||||
assert.equal(node.inputSamplingCost, -Math.log(firstLayerTransforms[0].p) - Math.log(secondLayerTransforms[0].p));
|
||||
assert.equal(node.currentCost, node.inputSamplingCost);
|
||||
assert.equal(node.correctionCost, node.inputSamplingCost);
|
||||
|
||||
assert.isTrue(node.hasPartialInput);
|
||||
assert.isFalse(node.isFullReplacement)
|
||||
|
|
@ -324,12 +320,12 @@ describe('Correction Distance Modeler', () => {
|
|||
assert.equal(node.spaceId, secondLayerId);
|
||||
}
|
||||
|
||||
assertSourceNodeProps(teNode);
|
||||
assertExpectedNodeProbs(teNode);
|
||||
|
||||
const clonedNode = new SearchNode(teNode);
|
||||
|
||||
// Root node properties; may as well re-assert 'em.
|
||||
assertSourceNodeProps(clonedNode);
|
||||
assertExpectedNodeProbs(clonedNode);
|
||||
|
||||
// Avoid aliasing for properties holding mutable objects
|
||||
assert.notEqual(clonedNode.priorInput, teNode.priorInput);
|
||||
|
|
@ -577,7 +573,7 @@ describe('Correction Distance Modeler', () => {
|
|||
// Allow a little value wiggle due to double-precision limitations.
|
||||
assert.approximately(subsetNodes[i].inputSamplingCost, expectedCosts[i], 1e-8);
|
||||
// No actual edit-tracking is done yet, so these should also match.
|
||||
assert.approximately(subsetNodes[i].currentCost, expectedCosts[i], 1e-8);
|
||||
assert.approximately(subsetNodes[i].correctionCost, expectedCosts[i], 1e-8);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -605,7 +601,7 @@ describe('Correction Distance Modeler', () => {
|
|||
assert.equal(lastEntry(ins1_dl0[1].calculation.matchSequence), 'h');
|
||||
assert.equal(ins1_dl0[1].editCount, 0);
|
||||
assert.isBelow(ins1_dl0[0].inputSamplingCost, ins1_dl0[1].inputSamplingCost);
|
||||
assert.isBelow(ins1_dl0[0].currentCost, ins1_dl0[1].currentCost);
|
||||
assert.isBelow(ins1_dl0[0].correctionCost, ins1_dl0[1].correctionCost);
|
||||
|
||||
// Correction of _other_ input characters to the 't' and the 'h' come
|
||||
// after ALL other corrections - these don't get both 't' and 'h' input
|
||||
|
|
@ -616,13 +612,13 @@ describe('Correction Distance Modeler', () => {
|
|||
assert.equal(lastEntry(ins1_dl0[FIRST_CHAR_VARIANTS].calculation.matchSequence), 'h');
|
||||
assert.equal(ins1_dl0[FIRST_CHAR_VARIANTS].editCount, 1);
|
||||
assert.isBelow(ins1_dl0[FIRST_CHAR_VARIANTS-1].inputSamplingCost, ins1_dl0[FIRST_CHAR_VARIANTS].inputSamplingCost);
|
||||
assert.isBelow(ins1_dl0[FIRST_CHAR_VARIANTS-1].currentCost, ins1_dl0[FIRST_CHAR_VARIANTS].currentCost);
|
||||
assert.isBelow(ins1_dl0[FIRST_CHAR_VARIANTS-1].correctionCost, ins1_dl0[FIRST_CHAR_VARIANTS].correctionCost);
|
||||
|
||||
assert.equal(lastEntry(ins1_dl0[FIRST_CHAR_VARIANTS+1].calculation.inputSequence), SENTINEL_CODE_UNIT);
|
||||
assert.equal(lastEntry(ins1_dl0[FIRST_CHAR_VARIANTS+1].calculation.matchSequence), 't');
|
||||
assert.equal(ins1_dl0[FIRST_CHAR_VARIANTS+1].editCount, 1);
|
||||
assert.isBelow(ins1_dl0[FIRST_CHAR_VARIANTS].inputSamplingCost, ins1_dl0[FIRST_CHAR_VARIANTS+1].inputSamplingCost);
|
||||
assert.isBelow(ins1_dl0[FIRST_CHAR_VARIANTS].currentCost, ins1_dl0[FIRST_CHAR_VARIANTS+1].currentCost);
|
||||
assert.isBelow(ins1_dl0[FIRST_CHAR_VARIANTS].correctionCost, ins1_dl0[FIRST_CHAR_VARIANTS+1].correctionCost);
|
||||
|
||||
// For everything in between... well, the input-sampling weight is uniform, and
|
||||
// all require a full edit.
|
||||
|
|
@ -649,7 +645,7 @@ describe('Correction Distance Modeler', () => {
|
|||
assert.equal(ins0_dl1[0].editCount, 0);
|
||||
assert.isUndefined(lastEntry(ins0_dl1[0].calculation.inputSequence));
|
||||
assert.equal(ins0_dl1[0].inputSamplingCost, subsetNodes[3].inputSamplingCost);
|
||||
assert.equal(ins0_dl1[0].currentCost, subsetNodes[3].currentCost);
|
||||
assert.equal(ins0_dl1[0].correctionCost, subsetNodes[3].correctionCost);
|
||||
|
||||
// ************
|
||||
// Set 1: set for ins 2, dl 1 - 'tr' + 'th'.
|
||||
|
|
@ -667,8 +663,8 @@ describe('Correction Distance Modeler', () => {
|
|||
assert.equal(lastEntry(ins2_dl1[0].calculation.matchSequence), 't');
|
||||
assert.equal(ins2_dl1[0].editCount, 0);
|
||||
// The subset hasn't yet split!
|
||||
assert.equal(ins2_dl1[0].currentCost, subsetNodes[1].currentCost);
|
||||
assert.isBelow(ins2_dl1[0].currentCost, ins2_dl1[1].currentCost);
|
||||
assert.equal(ins2_dl1[0].correctionCost, subsetNodes[1].correctionCost);
|
||||
assert.isBelow(ins2_dl1[0].correctionCost, ins2_dl1[1].correctionCost);
|
||||
|
||||
// All other (non-'t') entries get full subset probability with edit count 1;
|
||||
// they're all substitutions, as they fail to match against a non-'t' path.
|
||||
|
|
@ -699,8 +695,8 @@ describe('Correction Distance Modeler', () => {
|
|||
assert.equal(lastEntry(ins2_dl0[0].calculation.matchSequence), 'c');
|
||||
assert.equal(ins2_dl0[0].editCount, 0);
|
||||
// The subset won't split.
|
||||
assert.equal(ins2_dl0[0].currentCost, subsetNodes[2].currentCost);
|
||||
assert.isBelow(ins2_dl0[0].currentCost, ins2_dl0[1].currentCost);
|
||||
assert.equal(ins2_dl0[0].correctionCost, subsetNodes[2].correctionCost);
|
||||
assert.isBelow(ins2_dl0[0].correctionCost, ins2_dl0[1].correctionCost);
|
||||
|
||||
// All other (non-'c') entries get full subset probability with edit count 1;
|
||||
// they're all substitutions, as they fail to match against a non-'t' path.
|
||||
|
|
@ -816,7 +812,7 @@ describe('Correction Distance Modeler', () => {
|
|||
const subsetNodes = teNode.buildSubstitutionEdges(synthDistribution, SEARCH_EDGE_SEED++);
|
||||
assert.equal(subsetNodes.length, 4);
|
||||
subsetNodes.sort(CORRECTION_QUEUE_COMPARATOR);
|
||||
const expectedCosts = [0.5, .25, 0.15, 0.1].map(x => -Math.log(x) + teNode.currentCost);
|
||||
const expectedCosts = [0.5, .25, 0.15, 0.1].map(x => -Math.log(x) + teNode.correctionCost);
|
||||
// The known subs for the subsets defined above.
|
||||
for(let i=0; i < expectedCosts.length; i++) {
|
||||
assert.isTrue(subsetNodes[i].hasPartialInput);
|
||||
|
|
@ -826,7 +822,7 @@ describe('Correction Distance Modeler', () => {
|
|||
// Allow a little value wiggle due to double-precision limitations.
|
||||
assert.approximately(subsetNodes[i].inputSamplingCost, expectedCosts[i], 1e-8);
|
||||
// No actual edit-tracking is done yet, so these should also match.
|
||||
assert.approximately(subsetNodes[i].currentCost, expectedCosts[i], 1e-8);
|
||||
assert.approximately(subsetNodes[i].correctionCost, expectedCosts[i], 1e-8);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -862,13 +858,13 @@ describe('Correction Distance Modeler', () => {
|
|||
assert.equal(lastEntry(ins1_dl0[TE_CHILD_PATH_COUNT].calculation.matchSequence), 'l');
|
||||
assert.equal(ins1_dl0[TE_CHILD_PATH_COUNT].editCount, 1);
|
||||
assert.isBelow(ins1_dl0[TE_CHILD_PATH_COUNT-1].inputSamplingCost, ins1_dl0[TE_CHILD_PATH_COUNT].inputSamplingCost);
|
||||
assert.isBelow(ins1_dl0[TE_CHILD_PATH_COUNT-1].currentCost, ins1_dl0[TE_CHILD_PATH_COUNT].currentCost);
|
||||
assert.isBelow(ins1_dl0[TE_CHILD_PATH_COUNT-1].correctionCost, ins1_dl0[TE_CHILD_PATH_COUNT].correctionCost);
|
||||
|
||||
assert.equal(lastEntry(ins1_dl0[TE_CHILD_PATH_COUNT+1].calculation.inputSequence), SENTINEL_CODE_UNIT);
|
||||
assert.equal(lastEntry(ins1_dl0[TE_CHILD_PATH_COUNT+1].calculation.matchSequence), 'r');
|
||||
assert.equal(ins1_dl0[TE_CHILD_PATH_COUNT+1].editCount, 1);
|
||||
assert.isBelow(ins1_dl0[TE_CHILD_PATH_COUNT].inputSamplingCost, ins1_dl0[TE_CHILD_PATH_COUNT+1].inputSamplingCost);
|
||||
assert.isBelow(ins1_dl0[TE_CHILD_PATH_COUNT].currentCost, ins1_dl0[TE_CHILD_PATH_COUNT+1].currentCost);
|
||||
assert.isBelow(ins1_dl0[TE_CHILD_PATH_COUNT].correctionCost, ins1_dl0[TE_CHILD_PATH_COUNT+1].correctionCost);
|
||||
|
||||
// For everything in between... well, the input-sampling weight is uniform, and
|
||||
// all require a full edit.
|
||||
|
|
@ -912,8 +908,8 @@ describe('Correction Distance Modeler', () => {
|
|||
assert.equal(lastEntry(ins2_dl1[0].calculation.matchSequence), 'a');
|
||||
assert.equal(ins2_dl1[0].editCount, 0);
|
||||
// The subset hasn't yet split!
|
||||
assert.equal(ins2_dl1[0].currentCost, subsetNodes[1].currentCost);
|
||||
assert.isBelow(ins2_dl1[0].currentCost, ins2_dl1[1].currentCost);
|
||||
assert.equal(ins2_dl1[0].correctionCost, subsetNodes[1].correctionCost);
|
||||
assert.isBelow(ins2_dl1[0].correctionCost, ins2_dl1[1].correctionCost);
|
||||
|
||||
// All other (non-'t') entries get full subset probability with edit count 1;
|
||||
// they're all substitutions, as they fail to match against a non-'t' path.
|
||||
|
|
@ -943,8 +939,8 @@ describe('Correction Distance Modeler', () => {
|
|||
assert.equal(lastEntry(ins2_dl0[0].calculation.matchSequence), 'c');
|
||||
assert.equal(ins2_dl0[0].editCount, 0);
|
||||
// The subset won't split.
|
||||
assert.equal(ins2_dl0[0].currentCost, subsetNodes[2].currentCost);
|
||||
assert.isBelow(ins2_dl0[0].currentCost, ins2_dl0[1].currentCost);
|
||||
assert.equal(ins2_dl0[0].correctionCost, subsetNodes[2].correctionCost);
|
||||
assert.isBelow(ins2_dl0[0].correctionCost, ins2_dl0[1].correctionCost);
|
||||
|
||||
// All other (non-'c') entries get full subset probability with edit count 1;
|
||||
// they're all substitutions, as they fail to match against a non-'t' path.
|
||||
|
|
@ -1058,7 +1054,7 @@ describe('Correction Distance Modeler', () => {
|
|||
const layer1Edges = rootNode.buildSubstitutionEdges(synthDistribution1, layer1Id)
|
||||
// No 2+ inserts here; we're fine with just one call.
|
||||
.flatMap(e => e.processSubsetEdge());
|
||||
const layer1Queue = new PriorityQueue(CORRECTION_QUEUE_COMPARATOR, layer1Edges);
|
||||
const layer1Queue = new PriorityQueue(PREDICTION_QUEUE_COMPARATOR, layer1Edges);
|
||||
|
||||
const tEdge = layer1Queue.dequeue();
|
||||
assertEdgeChars(tEdge, 't', 't');
|
||||
|
|
@ -1068,7 +1064,7 @@ describe('Correction Distance Modeler', () => {
|
|||
const layer2Edges = tEdge.buildSubstitutionEdges(synthDistribution2, layer2Id)
|
||||
// No 2+ inserts here; we're fine with just one call.
|
||||
.flatMap(e => e.processSubsetEdge());
|
||||
const layer2Queue = new PriorityQueue(CORRECTION_QUEUE_COMPARATOR, layer2Edges);
|
||||
const layer2Queue = new PriorityQueue(PREDICTION_QUEUE_COMPARATOR, layer2Edges);
|
||||
|
||||
const eEdge = layer2Queue.dequeue();
|
||||
assertEdgeChars(eEdge, 'e', 'e');
|
||||
|
|
@ -1078,21 +1074,16 @@ describe('Correction Distance Modeler', () => {
|
|||
assertEdgeChars(hEdge, 'h', 'h');
|
||||
assert.equal(hEdge.spaceId, layer2Id);
|
||||
|
||||
// Needed for a proper e <-> h transposition.
|
||||
const ehEdge = findEdgesWithChars(layer2Edges, 'h')[0];
|
||||
|
||||
assert.isOk(ehEdge);
|
||||
|
||||
// Final round: we'll use three nodes and throw all of their results into the same priority queue.
|
||||
// Note: as we're constructing these directly, we're not modeling transpositions.
|
||||
const layer3Id = SEARCH_EDGE_SEED++;
|
||||
const layer3eEdges = eEdge.buildSubstitutionEdges(synthDistribution3, layer3Id)
|
||||
// No 2+ inserts here; we're fine with just one call.
|
||||
.flatMap(e => e.processSubsetEdge());
|
||||
const layer3hEdges = hEdge.buildSubstitutionEdges(synthDistribution3, layer3Id)
|
||||
.flatMap(e => e.processSubsetEdge());
|
||||
const layer3ehEdges = ehEdge.buildSubstitutionEdges(synthDistribution3, layer3Id)
|
||||
.flatMap(e => e.processSubsetEdge());
|
||||
const layer3Queue = new PriorityQueue(CORRECTION_QUEUE_COMPARATOR, layer3eEdges.concat(layer3hEdges).concat(layer3ehEdges));
|
||||
const layer3Queue = new PriorityQueue(PREDICTION_QUEUE_COMPARATOR, layer3eEdges.concat(layer3hEdges));
|
||||
|
||||
// Find the first result with an actual word directly represented.
|
||||
let bestEdge;
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@ import {
|
|||
models,
|
||||
LegacyQuotientRoot,
|
||||
SearchQuotientCluster,
|
||||
TokenResultMapping
|
||||
TokenResultMapping,
|
||||
CORRECTION_QUEUE_COMPARATOR
|
||||
} from '@keymanapp/lm-worker/test-index';
|
||||
|
||||
import TrieModel = models.TrieModel;
|
||||
|
|
@ -32,74 +33,31 @@ function buildTestTimer() {
|
|||
describe('Correction Searching', () => {
|
||||
describe('without multi-tokenization; using a single SearchPath sequence', () => {
|
||||
const checkRepeatableResults_teh = async (iter: AsyncGenerator<Readonly<TokenResultMapping>, any, any>) => {
|
||||
const firstIterResult = await iter.next(); // {value: <actual value>, done: <iteration complete?>}
|
||||
assert.isFalse(firstIterResult.done);
|
||||
|
||||
const firstResult: TokenResultMapping = firstIterResult.value; // Retrieves <actual value>
|
||||
// No checks on the first set's cost.
|
||||
assert.equal(firstResult.matchString, "ten");
|
||||
|
||||
// All start with 'te' but one, and invoke one edit of the same cost.
|
||||
// 'th' has an 'h' at the same cost (input 3) of the 'e' (input 2).
|
||||
const secondBatch = [
|
||||
'tec', 'tel', 'tem',
|
||||
'ter', 'tes', 'th',
|
||||
'te'
|
||||
const expectedFirstTwenty = [
|
||||
'ten', // no edits required whatsoever
|
||||
'th', // one edit (deletion), but gets to ignore the cost of a keystroke and still prefixes 'the'
|
||||
'the', // one edit (transposition), incurs the cost of all three keystrokes
|
||||
'te', // one edit (deletion), but gets to ignore the cost of a keystroke
|
||||
'tel', 'beh', // both cost one edit (hard character replacement: n/h -> l vs t -> b)
|
||||
// Other edits, generally of one edit cost, predicting words of varying frequency.
|
||||
'ter', 'tha', 'thi', 'thr', 'tho', 'tem', 'thu', 'then', 'men', 'wen', 'gen', 'en', 'sen', 'tec'
|
||||
];
|
||||
|
||||
async function checkBatch(batch: string[], prevCost: number) {
|
||||
let cost;
|
||||
while(batch.length > 0) {
|
||||
const iter_result = await iter.next();
|
||||
assert.isFalse(iter_result.done);
|
||||
|
||||
const result = iter_result.value;
|
||||
assert.isAbove(result.totalCost, prevCost);
|
||||
if(cost !== undefined) {
|
||||
assert.equal(result.totalCost, cost);
|
||||
} else {
|
||||
cost = result.totalCost;
|
||||
}
|
||||
|
||||
const matchIndex = batch.findIndex((entry) => entry == result.matchString);
|
||||
assert.notEqual(matchIndex, -1, `'${result.matchString}' received as prediction too early`);
|
||||
batch.splice(matchIndex, 1);
|
||||
}
|
||||
|
||||
return cost;
|
||||
let results: TokenResultMapping[] = [];
|
||||
for(let i = 0; i < expectedFirstTwenty.length; i++) {
|
||||
const iterResult = await iter.next();
|
||||
results.push(iterResult.value as TokenResultMapping);
|
||||
}
|
||||
|
||||
const secondCost = await checkBatch(secondBatch, firstResult.totalCost);
|
||||
assert.sameOrderedMembers(results.map((r) => r.matchString), expectedFirstTwenty);
|
||||
for(let i=0; i < expectedFirstTwenty.length - 1; i++) {
|
||||
assert.isAtLeast(results[i+1].totalCost, results[i].totalCost);
|
||||
}
|
||||
|
||||
// Single hard edit, all other input probability aspects are equal
|
||||
const thirdBatch = [
|
||||
// 't' -> 'b' (sub)
|
||||
'beh',
|
||||
// '' -> 'c' (insertion)
|
||||
'tech',
|
||||
// 'eh' -> 'he' (transposition)
|
||||
'the'
|
||||
];
|
||||
|
||||
await checkBatch(thirdBatch, secondCost);
|
||||
|
||||
// All replace the low-likelihood case for the third input.
|
||||
const fourthBatch = [
|
||||
'thi', 'tho', 'thr',
|
||||
'thu', 'tha'
|
||||
];
|
||||
|
||||
await checkBatch(fourthBatch, secondCost);
|
||||
|
||||
// Replace the _first_ input's char OR insert an extra char,
|
||||
// also matching the low-likelihood third-char option.
|
||||
const fifthBatch = [
|
||||
'cen', 'en', 'gen',
|
||||
'ken', 'len', 'men',
|
||||
'sen', 'then', 'wen'
|
||||
];
|
||||
|
||||
await checkBatch(fifthBatch, secondCost);
|
||||
// The results will not be in the order as raw correction likelihood because some words
|
||||
// are more frequent than others.
|
||||
results.sort(CORRECTION_QUEUE_COMPARATOR);
|
||||
assert.notSameOrderedMembers(results.map((r) => r.matchString), expectedFirstTwenty);
|
||||
}
|
||||
|
||||
it('Empty search root, loaded model', async () => {
|
||||
|
|
@ -113,12 +71,13 @@ describe('Correction Searching', () => {
|
|||
|
||||
// While there's no input, insertion operations can produce suggestions.
|
||||
const resultState = await iter.next();
|
||||
const result = resultState.value;
|
||||
const result = resultState.value as TokenResultMapping;
|
||||
|
||||
// Just one suggestion root should be returned as the first result.
|
||||
assert.equal(result.totalCost, 0); // Gives a perfect match
|
||||
assert.equal(result.correctionCost, 0); // Gives a perfect match
|
||||
assert.equal(result.matchString, ''); // an empty match string.
|
||||
assert.isFalse(resultState.done);
|
||||
assert.isAbove(result.totalCost, 0);
|
||||
});
|
||||
|
||||
// Hmm... how best to update this...
|
||||
|
|
@ -423,9 +382,9 @@ describe('Correction Searching', () => {
|
|||
// paths of lower total cost.
|
||||
pathsResults.push(nextFromPaths);
|
||||
|
||||
assert.isAtLeast(nextFromCluster.totalCost, baseCost);
|
||||
assert.isAtLeast(nextFromPaths.totalCost, baseCost);
|
||||
baseCost = Math.max(baseCost, nextFromCluster.totalCost, nextFromPaths.totalCost);
|
||||
assert.isAtLeast(nextFromCluster.correctionCost, baseCost);
|
||||
assert.isAtLeast(nextFromPaths.correctionCost, baseCost);
|
||||
baseCost = Math.max(baseCost, nextFromCluster.correctionCost, nextFromPaths.correctionCost);
|
||||
}
|
||||
|
||||
assert.deepEqual(genResults.map(r => r.matchString), pathsResults.map(r => r.matchString));
|
||||
|
|
@ -434,22 +393,13 @@ describe('Correction Searching', () => {
|
|||
assert.sameDeepMembers(pathsResults.slice(0, 3).map(r => r.matchString), ['th', 'to', 'tr']);
|
||||
// These involve likely-enough corrections that should show, given the model fixture.
|
||||
assert.includeDeepMembers(pathsResults.map(r => r.matchString), [
|
||||
'ty', // 'type' is quite frequent according to the text fixture.
|
||||
't', // Deleting the second keystroke outright lands here.
|
||||
'oth', // What if we insert an 'o' early on? 'other' is a very common English word
|
||||
'ti' // 'time' is pretty common too.
|
||||
'ti', // 'time' is pretty common too.
|
||||
'thi', // 'this' is common enough to show up early despite inserting the 'i'.
|
||||
'wh', // "which" is a super-frequent English word, worthy of being a forced correction
|
||||
'sh' // "she" is also strong enough to force an early appearance.
|
||||
]);
|
||||
|
||||
// NOTE: this level of corrections does not yet consider the word likelihood - only
|
||||
// the raw correction cost. No ordering of "likely word" to "unlikely word" should
|
||||
// occur yet.
|
||||
|
||||
// 'time': weight 934
|
||||
// 'type': weight 540
|
||||
const timeResult = pathsResults.find(r => r.matchString == 'ti');
|
||||
const typeResult = pathsResults.find(r => r.matchString == 'ty');
|
||||
// Correction to either should be equally likely.
|
||||
assert.equal(timeResult.totalCost, typeResult.totalCost);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -427,7 +427,7 @@ describe('LegacyQuotientSpur', () => {
|
|||
const entry_the = thirdResults.find((entry) => entry.matchString == 'the' && entry.editCount == 1);
|
||||
assert.isOk(entry_the);
|
||||
|
||||
thirdResults.sort((a, b) => a.totalCost - b.totalCost);
|
||||
thirdResults.sort((a, b) => a.correctionCost - b.correctionCost);
|
||||
const the_index = thirdResults.findIndex((entry) => entry.matchString == 'the' && entry.editCount == 1);
|
||||
// `teh` should appear fairly early as a viable correction.
|
||||
assert.isBelow(the_index, 10);
|
||||
|
|
@ -438,7 +438,7 @@ describe('LegacyQuotientSpur', () => {
|
|||
// We want to make sure we don't auto-ignore transposition cases by
|
||||
// accident by failing that conditional.
|
||||
const the_entry = thirdResults[the_index];
|
||||
assert.isBelow(the_entry.totalCost - thirdResults[0].totalCost, CORRECTION_SEARCH_THRESHOLDS.REPLACEMENT_SEARCH_THRESHOLD);
|
||||
assert.isBelow(the_entry.correctionCost - thirdResults[0].correctionCost, CORRECTION_SEARCH_THRESHOLDS.REPLACEMENT_SEARCH_THRESHOLD);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -99,9 +99,9 @@ describe('QuotientNodeFinalizer', () => {
|
|||
|
||||
assert.equal(searchResult.type, 'complete');
|
||||
if(searchResult.type == 'complete') {
|
||||
assert.equal(searchResult.mapping.totalCost, -Math.log(therefo.bestExample.p));
|
||||
assert.equal(searchResult.mapping.correctionCost, -Math.log(therefo.bestExample.p));
|
||||
assert.isNotNaN(searchResult.cost);
|
||||
assert.equal(searchResult.cost, searchResult.mapping.totalCost);
|
||||
assert.isAtLeast(searchResult.cost, searchResult.mapping.totalCost);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
|
@ -129,9 +129,9 @@ describe('QuotientNodeFinalizer', () => {
|
|||
|
||||
assert.equal(searchResult.type, 'complete');
|
||||
if(searchResult.type == 'complete') {
|
||||
assert.isAbove(searchResult.mapping.totalCost, -Math.log(therefo.bestExample.p));
|
||||
assert.isAbove(searchResult.mapping.correctionCost, -Math.log(therefo.bestExample.p));
|
||||
assert.isNotNaN(searchResult.cost);
|
||||
assert.equal(searchResult.cost, searchResult.mapping.totalCost);
|
||||
assert.isAtLeast(searchResult.cost, searchResult.mapping.totalCost);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ describe('buildAndMapPredictions', () => {
|
|||
const mappedPredictions = buildAndMapPredictions(
|
||||
transition,
|
||||
transition.base.displayTokenization,
|
||||
{matchString: 'the', totalCost: 0, editCount: 0},
|
||||
{matchString: 'the', correctionCost: 0, editCount: 0},
|
||||
1
|
||||
);
|
||||
|
||||
|
|
@ -104,7 +104,7 @@ describe('buildAndMapPredictions', () => {
|
|||
const mappedPredictions = buildAndMapPredictions(
|
||||
transition,
|
||||
transition.base.displayTokenization,
|
||||
{matchString: '', totalCost: 0, editCount: 0},
|
||||
{matchString: '', correctionCost: 0, editCount: 0},
|
||||
1
|
||||
);
|
||||
|
||||
|
|
@ -148,7 +148,7 @@ describe('buildAndMapPredictions', () => {
|
|||
const mappedPredictions = buildAndMapPredictions(
|
||||
transition,
|
||||
transition.base.displayTokenization,
|
||||
{matchString: '', totalCost: 0, editCount: 0},
|
||||
{matchString: '', correctionCost: 0, editCount: 0},
|
||||
1
|
||||
);
|
||||
|
||||
|
|
@ -210,7 +210,7 @@ describe('buildAndMapPredictions', () => {
|
|||
const mappedPredictions = buildAndMapPredictions(
|
||||
transition,
|
||||
transition.final.displayTokenization,
|
||||
{matchString: '', totalCost: 0, editCount: 0},
|
||||
{matchString: '', correctionCost: 0, editCount: 0},
|
||||
1
|
||||
);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue