-
Notifications
You must be signed in to change notification settings - Fork 8
Add randomesque #46
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Add randomesque #46
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,7 +1,7 @@ | ||||||||||||||||||||||||||||||||||||
| /* eslint-disable @typescript-eslint/no-non-null-assertion */ | ||||||||||||||||||||||||||||||||||||
| import { minimize_Powell } from 'optimization-js'; | ||||||||||||||||||||||||||||||||||||
| import { Stimulus, Zeta } from './type'; | ||||||||||||||||||||||||||||||||||||
| import { itemResponseFunction, fisherInformation, normal, uniform, findClosest } from './utils'; | ||||||||||||||||||||||||||||||||||||
| import { itemResponseFunction, fisherInformation, normal, uniform } from './utils'; | ||||||||||||||||||||||||||||||||||||
| import { validateZetaParams, fillZetaDefaults, ensureZetaNumericValues } from './corpus'; | ||||||||||||||||||||||||||||||||||||
| import seedrandom from 'seedrandom'; | ||||||||||||||||||||||||||||||||||||
| import _clamp from 'lodash/clamp'; | ||||||||||||||||||||||||||||||||||||
|
|
@@ -18,6 +18,7 @@ export interface CatInput { | |||||||||||||||||||||||||||||||||||
| priorDist?: string; | ||||||||||||||||||||||||||||||||||||
| priorPar?: number[]; | ||||||||||||||||||||||||||||||||||||
| randomSeed?: string | null; | ||||||||||||||||||||||||||||||||||||
| randomesque?: number; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| export class Cat { | ||||||||||||||||||||||||||||||||||||
|
|
@@ -27,6 +28,7 @@ export class Cat { | |||||||||||||||||||||||||||||||||||
| public maxTheta: number; | ||||||||||||||||||||||||||||||||||||
| public priorDist: string; | ||||||||||||||||||||||||||||||||||||
| public priorPar: number[]; | ||||||||||||||||||||||||||||||||||||
| public randomesque: number; | ||||||||||||||||||||||||||||||||||||
| private readonly _zetas: Zeta[]; | ||||||||||||||||||||||||||||||||||||
| private readonly _resps: (0 | 1)[]; | ||||||||||||||||||||||||||||||||||||
| private _theta: number; | ||||||||||||||||||||||||||||||||||||
|
|
@@ -38,7 +40,7 @@ export class Cat { | |||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||||
| * Create a Cat object. This expects an single object parameter with the following keys | ||||||||||||||||||||||||||||||||||||
| * @param {{method: string, itemSelect: string, nStartItems: number, startSelect:string, theta: number, minTheta: number, maxTheta: number, priorDist: string, priorPar: number[]}=} destructuredParam | ||||||||||||||||||||||||||||||||||||
| * @param {{method: string, itemSelect: string, nStartItems: number, startSelect:string, theta: number, minTheta: number, maxTheta: number, priorDist: string, priorPar: number[], randomSeed: string|null, randomesque: number}=} destructuredParam | ||||||||||||||||||||||||||||||||||||
| * method: ability estimator, e.g. MLE or EAP, default = 'MLE' | ||||||||||||||||||||||||||||||||||||
| * itemSelect: the method of item selection, e.g. "MFI", "random", "closest", default method = 'MFI' | ||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||||||
| * nStartItems: first n trials to keep non-adaptive selection | ||||||||||||||||||||||||||||||||||||
|
|
@@ -49,6 +51,7 @@ export class Cat { | |||||||||||||||||||||||||||||||||||
| * priorDist: the prior distribution type (only applies to EAP estimator) | ||||||||||||||||||||||||||||||||||||
| * priorPar: the prior distribution parameters (only applies to EAP estimator) | ||||||||||||||||||||||||||||||||||||
| * randomSeed: set a random seed to trace the simulation | ||||||||||||||||||||||||||||||||||||
| * randomesque: number of top items to randomly select from in MFI/closest (default = 1, i.e. always pick the best) | ||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| constructor({ | ||||||||||||||||||||||||||||||||||||
|
|
@@ -62,6 +65,7 @@ export class Cat { | |||||||||||||||||||||||||||||||||||
| priorDist = 'norm', // only applies to EAP estimator | ||||||||||||||||||||||||||||||||||||
| priorPar = priorDist === 'unif' ? [-4, 4] : [0, 1], // only applies to EAP estimator | ||||||||||||||||||||||||||||||||||||
| randomSeed = null, | ||||||||||||||||||||||||||||||||||||
| randomesque = 1, | ||||||||||||||||||||||||||||||||||||
| }: CatInput = {}) { | ||||||||||||||||||||||||||||||||||||
| this.method = Cat.validateMethod(method); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
|
@@ -78,6 +82,7 @@ export class Cat { | |||||||||||||||||||||||||||||||||||
| this._theta = theta; | ||||||||||||||||||||||||||||||||||||
| this._seMeasurement = Number.MAX_VALUE; | ||||||||||||||||||||||||||||||||||||
| this.nStartItems = nStartItems; | ||||||||||||||||||||||||||||||||||||
| this.randomesque = Math.max(1, Math.round(randomesque)); | ||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This quietly turns private static validateRandomesque(randomesque: number): number {
if (!Number.isInteger(randomesque) || randomesque < 1) {
throw new Error(
`randomesque must be a positive integer. Received ${randomesque}.`,
);
}
return randomesque;
} |
||||||||||||||||||||||||||||||||||||
| this._rng = randomSeed === null ? seedrandom() : seedrandom(randomSeed); | ||||||||||||||||||||||||||||||||||||
| this._prior = this.method === 'eap' ? Cat.validatePrior(priorDist, priorPar, minTheta, maxTheta) : []; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
@@ -291,13 +296,28 @@ export class Cat { | |||||||||||||||||||||||||||||||||||
| ...element, | ||||||||||||||||||||||||||||||||||||
| })); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| if (stimuliAddFisher.length === 0) { | ||||||||||||||||||||||||||||||||||||
| return { nextStimulus: undefined as unknown as Stimulus, remainingStimuli: [] as Stimulus[] }; | ||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The double-cast hides a real type contract.
export interface SelectorResult {
nextStimulus: Stimulus | undefined;
remainingStimuli: Stimulus[];
}
public findNextItem(stimuli: Stimulus[], itemSelect: string = this.itemSelect, deepCopy = true) {to: public findNextItem(
stimuli: Stimulus[],
itemSelect: string = this.itemSelect,
deepCopy = true,
): SelectorResult {
if (stimuliAddFisher.length === 0) {
return { nextStimulus: undefined as unknown as Stimulus, remainingStimuli: [] as Stimulus[] };
}with: if (stimuliAddFisher.length === 0) {
return { nextStimulus: undefined, remainingStimuli: [] };
}5.. Drop the casts in if (arr.length === 0) {
return { nextStimulus: undefined, remainingStimuli: [] };
}
nextStimulus: picked as Stimulus,to: nextStimulus: picked,The
to: After these changes, |
||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| stimuliAddFisher.sort((a, b) => b.fisherInformation - a.fisherInformation); | ||||||||||||||||||||||||||||||||||||
| stimuliAddFisher.forEach((stimulus: Stimulus) => { | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| // Randomesque: pick randomly from the top-K items by Fisher information | ||||||||||||||||||||||||||||||||||||
| const nrIt = Math.min(this.randomesque, stimuliAddFisher.length); | ||||||||||||||||||||||||||||||||||||
| const threshold = stimuliAddFisher[nrIt - 1].fisherInformation; | ||||||||||||||||||||||||||||||||||||
| const topK = stimuliAddFisher.filter((s) => s.fisherInformation >= threshold); | ||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. See comment in Also see the comment there about extracting this logic to a shared utility. |
||||||||||||||||||||||||||||||||||||
| const pickIndex = this.randomInteger(0, topK.length - 1); | ||||||||||||||||||||||||||||||||||||
| const picked = topK[pickIndex]; | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const remaining = stimuliAddFisher.filter((s) => s !== picked); | ||||||||||||||||||||||||||||||||||||
| remaining.forEach((stimulus: Stimulus) => { | ||||||||||||||||||||||||||||||||||||
| delete stimulus['fisherInformation']; | ||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||
| delete (picked as Stimulus)['fisherInformation']; | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+312
to
+316
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The original code deleted stimuliAddFisher.forEach((s) => delete (s as Stimulus)['fisherInformation']);
const remaining = stimuliAddFisher.filter((s) => s !== picked); |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||
| nextStimulus: stimuliAddFisher[0], | ||||||||||||||||||||||||||||||||||||
| remainingStimuli: stimuliAddFisher.slice(1).sort((a: Stimulus, b: Stimulus) => a.difficulty! - b.difficulty!), | ||||||||||||||||||||||||||||||||||||
| nextStimulus: picked as Stimulus, | ||||||||||||||||||||||||||||||||||||
| remainingStimuli: remaining.sort((a: Stimulus, b: Stimulus) => a.difficulty! - b.difficulty!), | ||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
|
|
@@ -319,9 +339,21 @@ export class Cat { | |||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| private selectorClosest(arr: Stimulus[]) { | ||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same here
Suggested change
|
||||||||||||||||||||||||||||||||||||
| //findClosest requires arr is sorted by difficulty | ||||||||||||||||||||||||||||||||||||
| const index = findClosest(arr, this._theta + 0.481); | ||||||||||||||||||||||||||||||||||||
| const nextItem = arr[index]; | ||||||||||||||||||||||||||||||||||||
| arr.splice(index, 1); | ||||||||||||||||||||||||||||||||||||
| if (arr.length === 0) { | ||||||||||||||||||||||||||||||||||||
| return { nextStimulus: undefined as unknown as Stimulus, remainingStimuli: [] as Stimulus[] }; | ||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same comment here about the double-cast. |
||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||
| // Compute distances from target for randomesque support | ||||||||||||||||||||||||||||||||||||
| const target = this._theta + 0.481; | ||||||||||||||||||||||||||||||||||||
| const distances = arr.map((s, i) => ({ index: i, dist: Math.abs(s.difficulty! - target) })); | ||||||||||||||||||||||||||||||||||||
| distances.sort((a, b) => a.dist - b.dist); | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const nrIt = Math.min(this.randomesque, distances.length); | ||||||||||||||||||||||||||||||||||||
| const threshold = distances[nrIt - 1].dist; | ||||||||||||||||||||||||||||||||||||
| const topK = distances.filter((d) => d.dist <= threshold); | ||||||||||||||||||||||||||||||||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||||||||||||||||||||||||||||||||||||
| const pick = topK[this.randomInteger(0, topK.length - 1)]; | ||||||||||||||||||||||||||||||||||||
|
Comment on lines
+350
to
+353
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
private pickFromTopK<T>(sorted: T[], score: (item: T) => number): T {
const nrIt = Math.min(this.randomesque, sorted.length);
const threshold = score(sorted[nrIt - 1]);
const topK = sorted.filter((s) => score(s) >= threshold);
// For closest, score returns *distance*, so the comparator flips.
// pass an `order: "desc" | "asc"` flag, or have the caller pre-negate.
return topK[this.randomInteger(0, topK.length - 1)];
}If we ever revisit the tie semantics it'll only need to change in one place. |
||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
| const nextItem = arr[pick.index]; | ||||||||||||||||||||||||||||||||||||
| arr.splice(pick.index, 1); | ||||||||||||||||||||||||||||||||||||
| return { | ||||||||||||||||||||||||||||||||||||
| nextStimulus: nextItem, | ||||||||||||||||||||||||||||||||||||
| remainingStimuli: arr, | ||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The biggest blocker for me right now is that none of the changes are tested. Could you add test coverage for:
randomesque = 1(default) returns the deterministic top-1 pick for bothmfiandclosest.randomesque = 3with a fixedrandomSeed: the picked item is always one of the top 3 by Fisher information; over many seeds, all 3 candidates appear.closest.randomesque = 2, assert the random pool is 3 items.stimulireturns{ nextStimulus: undefined, remainingStimuli: [] }for both selectors.Catinstances with the samerandomSeedandrandomesque > 1produce the same pick sequence.new Cat({ randomesque: 0 }),-1,2.5, andNaNall throw errors. (Requires the suggestion I made about validating the input.)