Bladeren bron

Add ability to always include cluster scale

Kirk Trombley 3 jaren geleden
bovenliggende
commit
91cae31d77
2 gewijzigde bestanden met toevoegingen van 58 en 35 verwijderingen
  1. 3 3
      nearest.html
  2. 55 32
      nearest.js

+ 3 - 3
nearest.html

@@ -41,7 +41,7 @@
                     </div>
                     <div class="panel dropdown">
                         <label for="image-mean">Cluster:</label>
-                        <select type="checkbox" onchange="onMeanArgumentChanged()" id="image-summary">
+                        <select type="checkbox" onchange="onClusterChoiceChanged()" id="image-summary">
                             <option selected>All Pixels</option>
                             <option>Biggest (M)</option>
                             <option>Smallest (m)</option>
@@ -59,11 +59,11 @@
                 </div>
 
                 <div id="cluster-mean-warning" class="container center-justified hide">
-                    Warning: Cluster means work best with mean-focused metrics
+                    Warning: RMS has little meaning when clusters are used
                 </div>
 
                 <div class="container control hide">
-                    <label for="scale-by-cluster-size">Cluster Scale in Distance:</label>
+                    <label for="scale-by-cluster-size">Scale measure by cluster size:</label>
                     <input type="checkbox" checked oninput="onScaleByClusterChanged()" id="scale-by-cluster-size">
                 </div>
 

+ 55 - 32
nearest.js

@@ -1,7 +1,7 @@
 // Selectors + DOM Manipulation
 const getColorInputNode = () => document.getElementById("color-input");
 const getMetricDropdownNode = () => document.getElementById("metric");
-const getMeanArgumentDropdownNode = () => document.getElementById("image-summary");
+const getClusterChoiceDropdownNode = () => document.getElementById("image-summary");
 const getClusterScaleToggleNode = () => document.getElementById("scale-by-cluster-size");
 const getClusterMeanWarning = () => document.getElementById("cluster-mean-warning");
 const getIncludeXToggleNode = () => document.getElementById("include-x");
@@ -107,8 +107,8 @@ const readColorInput = () => {
 // State
 const state = {
   metric: null,
-  meanArgument: null,
-  includeScaleInDist: null,
+  clusterChoice: null,
+  includeScale: null,
   includeX: null,
   normQY: null,
   closeCoeff: null,
@@ -124,47 +124,68 @@ const state = {
 const getBestKMean = (stats, q) => argMin(stats.kMeans.map((z, i) => vectorSqDist(z.vector, q.vector) / stats.kWeights[i]));
 const getWorstKMean = (stats, q) => argMax(stats.kMeans.map((z, i) => vectorSqDist(z.vector, q.vector) / stats.kWeights[i]));
 
+const getScale = weight => state.includeScale ? (1 / weight) : 1;
+
 const summarySelectors = [
   // true mean
   stats => [stats.trueMean, 1],
   // largest cluster
-  stats => [stats.kMeans[stats.largestCluster], stats.kWeights[stats.largestCluster]],
+  stats => [stats.kMeans[stats.largestCluster], getScale(stats.kWeights[stats.largestCluster])],
   // smallest cluster
-  stats => [stats.kMeans[stats.smallestCluster], stats.kWeights[stats.smallestCluster]],
+  stats => [stats.kMeans[stats.smallestCluster], getScale(stats.kWeights[stats.smallestCluster])],
   // best fit cluster
   (stats, q) => {
     const best = getBestKMean(stats, q);
-    return [stats.kMeans[best], stats.kWeights[best]];
+    return [stats.kMeans[best], getScale(stats.kWeights[best])];
   },
   // worst fit cluster
   (stats, q) => {
     const worst = getWorstKMean(stats, q);
-    return [stats.kMeans[worst], stats.kWeights[worst]];
+    return [stats.kMeans[worst], getScale(stats.kWeights[worst])];
   },
 ];
 
-const selectedSummary = (stats, q) => summarySelectors[state.meanArgument](stats, q);
+const selectedSummary = (stats, q) => summarySelectors[state.clusterChoice](stats, q);
 
 const metrics = [
   // RMS
-  (stats, q) => stats.inertia - 2 * vectorDot(selectedSummary(stats, q)[0].vector, q.vector),
+  (stats, q) => { 
+    const [ mean, scale ] = selectedSummary(stats, q);
+    return (stats.inertia - 2 * vectorDot(mean.vector, q.vector)) * scale; 
+  },
   // mean angle
-  (stats, q) => -vectorDot(selectedSummary(stats, q)[0].unit, q.unit),
+  (stats, q) => {
+    const [ mean, scale ] = selectedSummary(stats, q);
+    return -vectorDot(mean.unit, q.unit) * scale
+  },
   // mean dist
   (stats, q) => {
+    const [ mean, scale ] = selectedSummary(stats, q);
     // TODO I know there's some way to avoid recalculation here but I'm just too lazy right now
-    const [data, scale] = selectedSummary(stats, q);
-    return vectorSqDist(data.vector, q.vector) / (state.includeScaleInDist ? scale : 1);
+    return vectorSqDist(mean.vector, q.vector) * scale;
   },
   // hue angle
-  (stats, q) => angleDiff(selectedSummary(stats, q)[0].hue, q.hue),
+  (stats, q) => {
+    const [ mean, scale ] = selectedSummary(stats, q);
+    return angleDiff(mean.hue, q.hue) * scale;
+  },
   // max inertia
-  (stats) => -stats.inertia,
+  (stats, q) => {
+    const [ , scale ] = selectedSummary(stats, q);
+    return -stats.inertia * scale;
+  },
   // custom
-  (stats, q) => (state.includeX ? stats.inertia : 0) - state.closeCoeff * vectorDot(
-    selectedSummary(stats, q)[0][state.normQY ? "unit" : "vector"],
-    state.normQY ? q.unit : q.vector,
-  ),
+  (stats, q) => {
+    const [ mean, scale ] = selectedSummary(stats, q);
+    return (
+      (state.includeX ? stats.inertia : 0) 
+      - 
+      state.closeCoeff * vectorDot(
+        mean[state.normQY ? "unit" : "vector"],
+        state.normQY ? q.unit : q.vector,
+      )
+    ) * scale;
+  },
 ];
 
 const scorePokemon = pkmn => ({
@@ -215,12 +236,14 @@ const mathDefinitions = {
   `,
 };
 
+const clusterScaleText = muArg => (state.clusterChoice > 0 && state.includeScale ? String.raw`\frac{\left|P\right|}{\left|${muArg}\right|}` : "")
+
 const metricText = [
-  muArg => String.raw`${mathArgBest("min", "P")}\left[I\left(P\right) - 2\vec{q}\cdot \vec{\mu}\left(${muArg}\right)\right]`,
-  muArg => String.raw`${mathArgBest("min", "P")}\left[-\cos\left(\angle \left(\vec{q}, \vec{\mu}\left(${muArg}\right)\right)\right)\right]`,
-  muArg => String.raw`${mathArgBest("min", "P")}\left[${state.meanArgument > 0 && state.includeScaleInDist ? String.raw`\frac{\left|P\right|}{\left|${muArg}\right|}` : ""} \left|\left| \vec{q} - \vec{\mu}\left(${muArg}\right) \right|\right|^2\right]`,
-  muArg => String.raw`${mathArgBest("min", "P")}\left[\angle \left(\vec{q}_{\perp}, \vec{\mu}\left(${muArg}\right)_{\perp} \right)\right]`,
-  muArg => String.raw`${mathArgBest("min", "P")}\left[-I\left(P\right)\right]`,
+  muArg => String.raw`${mathArgBest("min", "P")}\left[${clusterScaleText(muArg)}I\left(P\right) - 2\vec{q}\cdot ${clusterScaleText(muArg)}\vec{\mu}\left(${muArg}\right)\right]`,
+  muArg => String.raw`${mathArgBest("min", "P")}\left[-${clusterScaleText(muArg)}\cos\left(\angle \left(\vec{q}, \vec{\mu}\left(${muArg}\right)\right)\right)\right]`,
+  muArg => String.raw`${mathArgBest("min", "P")}\left[${clusterScaleText(muArg)}\left|\left| \vec{q} - \vec{\mu}\left(${muArg}\right) \right|\right|^2\right]`,
+  muArg => String.raw`${mathArgBest("min", "P")}\left[${clusterScaleText(muArg)}\angle \left(\vec{q}_{\perp}, \vec{\mu}\left(${muArg}\right)_{\perp} \right)\right]`,
+  muArg => String.raw`${mathArgBest("min", "P")}\left[-${clusterScaleText(muArg)}I\left(P\right)\right]`,
 ].map(s => muArg => TeXZilla.toMathML(s(muArg)));
 
 const muArgs = [
@@ -234,7 +257,7 @@ const muArgs = [
 const renderVec = math => String.raw`\vec{${math.charAt(0)}}${math.substr(1)}`;
 const renderNorm = vec => String.raw`\frac{${vec}}{\left|\left|${vec}\right|\right|}`;
 const updateObjective = () => {
-  const muArg = muArgs[state.meanArgument];
+  const muArg = muArgs[state.clusterChoice];
   let tex = metricText?.[state.metric]?.(muArg);
   if (!tex) {
     const { includeX, normQY, closeCoeff } = state;
@@ -475,7 +498,7 @@ const onCustomControlsChanged = skipScore => {
 const checkClusterMeanWarning = () => {
   const warning = getClusterMeanWarning();
   const unhidden = warning.getAttribute("class").replaceAll("hide", "");
-  if (state.meanArgument !== 0 && state.metric === 0) {
+  if (state.clusterChoice !== 0 && state.metric === 0) {
     warning.setAttribute("class", unhidden);
   } else {
     warning.setAttribute("class", unhidden + " hide");
@@ -485,7 +508,7 @@ const checkClusterMeanWarning = () => {
 const checkScaleByClusterToggle = () => {
   const toggle = getClusterScaleToggleNode()?.parentNode;
   const unhidden = toggle.getAttribute("class").replaceAll("hide", "");
-  if (state.meanArgument !== 0 && state.metric === 2) {
+  if (state.clusterChoice !== 0) {
     toggle.setAttribute("class", unhidden);
   } else {
     toggle.setAttribute("class", unhidden + " hide");
@@ -493,7 +516,7 @@ const checkScaleByClusterToggle = () => {
 }
 
 const onScaleByClusterChanged = skipScore => {
-  state.includeScaleInDist = getClusterScaleToggleNode()?.checked ?? true;
+  state.includeScale = getClusterScaleToggleNode()?.checked ?? true;
 
   updateObjective();
 
@@ -502,12 +525,12 @@ const onScaleByClusterChanged = skipScore => {
   }
 }
 
-const onMeanArgumentChanged = skipScore => {
-  const meanArgument = getMeanArgumentDropdownNode()?.selectedIndex ?? 0;
-  if (meanArgument === state.meanArgument) {
+const onClusterChoiceChanged = skipScore => {
+  const clusterChoice = getClusterChoiceDropdownNode()?.selectedIndex ?? 0;
+  if (clusterChoice === state.clusterChoice) {
     return;
   }
-  state.meanArgument = meanArgument;
+  state.clusterChoice = clusterChoice;
   checkClusterMeanWarning();
   checkScaleByClusterToggle();
   updateObjective();
@@ -581,7 +604,7 @@ const onPageLoad = () => {
   // fake some events but don't do any scoring
   onColorChanged(true);
   onMetricChanged(true);
-  onMeanArgumentChanged(true);
+  onClusterChoiceChanged(true);
   onScaleByClusterChanged(true);
   onLimitChanged(true);
   // then do a rescore directly, which will do nothing unless old data was loaded