Publish Docker image (Multi-arch) / Build & push (amd64) (push) Failing after 1m40s
Release (Linux, macOS, Windows) / Linux Release (push) Failing after 42m16s
Publish Docker image (Multi-arch) / Build & push (arm64) (push) Canceled after 0s
Release (Linux, macOS, Windows) / macOS Release (push) Canceled after 0s
Release (Linux, macOS, Windows) / Windows Release (push) Canceled after 0s
Publish Docker image (Multi-arch) / Create multi-arch manifests (push) Canceled after 0s
Sync production source including routing, billing, Astra detection and public uptime. Update custom build instructions and license packaging.
213 lines
8.8 KiB
JavaScript
213 lines
8.8 KiB
JavaScript
export const VALUE_MIN = 1;
|
|
export const VALUE_MAX = 355;
|
|
export const DIMENSION = VALUE_MAX - VALUE_MIN + 1;
|
|
export const ALPHA = 0.5;
|
|
export const ORDERED_BLOCK_WEIGHT = 0.25;
|
|
|
|
export function parseNumbers(text) {
|
|
const runs = [];
|
|
let current = [];
|
|
let previousEnd = 0;
|
|
for (const match of String(text).matchAll(/\d+/g)) {
|
|
const separator = String(text).slice(previousEnd, match.index);
|
|
const value = Number(match[0]);
|
|
if (current.length && /\p{L}/u.test(separator)) {
|
|
runs.push(current);
|
|
current = [];
|
|
}
|
|
if (value >= VALUE_MIN && value <= VALUE_MAX) current.push(value);
|
|
previousEnd = match.index + match[0].length;
|
|
}
|
|
if (current.length) runs.push(current);
|
|
return runs.reduce((best, run) => run.length > best.length ? run : best, []);
|
|
}
|
|
|
|
export function countNumbers(numbers) {
|
|
const counts = Array(DIMENSION).fill(0);
|
|
numbers.forEach((number) => { counts[number - VALUE_MIN] += 1; });
|
|
return counts;
|
|
}
|
|
|
|
function mean(values) {
|
|
return values.reduce((total, value) => total + value, 0) / values.length;
|
|
}
|
|
|
|
function standardize(values) {
|
|
const center = mean(values);
|
|
const variance = mean(values.map((value) => (value - center) ** 2));
|
|
const scale = Math.max(Math.sqrt(variance), 1e-12);
|
|
return values.map((value) => (value - center) / scale);
|
|
}
|
|
|
|
function dot(left, right) {
|
|
let value = 0;
|
|
for (let index = 0; index < left.length; index += 1) value += left[index] * right[index];
|
|
return value;
|
|
}
|
|
|
|
function norm(values) {
|
|
return Math.sqrt(dot(values, values));
|
|
}
|
|
|
|
function normalized(values) {
|
|
const scale = Math.max(norm(values), 1e-12);
|
|
return values.map((value) => value / scale);
|
|
}
|
|
|
|
function subtractBasis(values, basis) {
|
|
const output = values.slice();
|
|
for (const vector of basis || []) {
|
|
const projection = dot(output, vector);
|
|
for (let index = 0; index < output.length; index += 1) output[index] -= projection * vector[index];
|
|
}
|
|
return output;
|
|
}
|
|
|
|
function hellingerFeature(counts) {
|
|
const total = counts.reduce((sum, value) => sum + value, 0) + ALPHA * DIMENSION;
|
|
return counts.map((value) => Math.sqrt((value + ALPHA) / total));
|
|
}
|
|
|
|
function splitIntoFour(values) {
|
|
const base = Math.floor(values.length / 4);
|
|
const remainder = values.length % 4;
|
|
const chunks = [];
|
|
let start = 0;
|
|
for (let index = 0; index < 4; index += 1) {
|
|
const size = base + (index < remainder ? 1 : 0);
|
|
chunks.push(values.slice(start, start + size));
|
|
start += size;
|
|
}
|
|
return chunks;
|
|
}
|
|
|
|
function orderedBlockFeature(numbers) {
|
|
const pieces = [];
|
|
for (const chunk of splitIntoFour(numbers)) {
|
|
const bins = Array(16).fill(0.5);
|
|
for (const value of chunk) {
|
|
const index = Math.min(15, Math.floor(((value - 1) / 355) * 16));
|
|
bins[index] += 1;
|
|
}
|
|
const total = bins.reduce((sum, value) => sum + value, 0);
|
|
pieces.push(...bins.map((value) => Math.sqrt(value / total)));
|
|
}
|
|
const lastDigits = Array(10).fill(0.5);
|
|
numbers.forEach((value) => { lastDigits[value % 10] += 1; });
|
|
const lastTotal = lastDigits.reduce((sum, value) => sum + value, 0);
|
|
pieces.push(...lastDigits.map((value) => Math.sqrt(value / lastTotal)));
|
|
return pieces;
|
|
}
|
|
|
|
function robustScoreCounts(counts, bank) {
|
|
const artifact = bank.robust.hellinger;
|
|
const feature = hellingerFeature(counts);
|
|
let projected = feature.map((value, index) => (value - artifact.feature_mean[index]) / artifact.feature_scale[index]);
|
|
projected = subtractBasis(projected, artifact.nuisance_basis);
|
|
projected = normalized(projected);
|
|
const scores = artifact.centroids.map((centroid) => dot(projected, centroid));
|
|
return standardize(scores);
|
|
}
|
|
|
|
function orderedBlockScores(numbers, bank) {
|
|
const artifact = bank.robust.ordered_blocks;
|
|
const feature = orderedBlockFeature(numbers);
|
|
const standardizedFeature = feature.map((value, index) => (value - artifact.feature_mean[index]) / artifact.feature_scale[index]);
|
|
const unit = normalized(standardizedFeature);
|
|
const environmentScores = artifact.environment_centroids.map(
|
|
(centroids) => centroids.map((centroid) => dot(unit, centroid)),
|
|
);
|
|
const template = standardize(artifact.centroids.map((_, modelIndex) => Math.max(
|
|
...environmentScores.map((scores) => scores[modelIndex]),
|
|
)));
|
|
const projected = normalized(subtractBasis(standardizedFeature, artifact.nuisance_basis));
|
|
const nuisance = standardize(artifact.centroids.map((centroid) => dot(projected, centroid)));
|
|
return standardize(template.map((value, index) => 0.5 * value + 0.5 * nuisance[index]));
|
|
}
|
|
|
|
function robustScoreNumbers(numbers, bank) {
|
|
const marginal = robustScoreCounts(countNumbers(numbers), bank);
|
|
const artifact = bank.robust.ordered_blocks;
|
|
const weight = artifact ? Number(artifact.weight || 0) : 0;
|
|
if (!artifact || weight === 0) return marginal;
|
|
const ordered = orderedBlockScores(numbers, bank);
|
|
return marginal.map((value, index) => (1 - weight) * value + weight * ordered[index]);
|
|
}
|
|
|
|
function softmax(values) {
|
|
const maximum = Math.max(...values);
|
|
const weights = values.map((value) => Math.exp(value - maximum));
|
|
const total = weights.reduce((sum, value) => sum + value, 0);
|
|
return weights.map((value) => value / total);
|
|
}
|
|
|
|
function jsSimilarity(left, right) {
|
|
const leftTotal = left.reduce((sum, value) => sum + value, 0);
|
|
const rightTotal = right.reduce((sum, value) => sum + value, 0) + ALPHA * DIMENSION;
|
|
const p = left.map((value) => value / leftTotal);
|
|
const q = right.map((value) => (value + ALPHA) / rightTotal);
|
|
const midpoint = p.map((value, index) => (value + q[index]) / 2);
|
|
const divergence = (values) => values.reduce(
|
|
(total, value, index) => total + (value ? value * Math.log(value / midpoint[index]) : 0),
|
|
0,
|
|
);
|
|
const js = (divergence(p) + divergence(q)) / 2;
|
|
return 1 - Math.sqrt(js / Math.log(2));
|
|
}
|
|
|
|
export function analyzeGlobalOutputs(outputs, bank) {
|
|
const modelIds = bank.models.map((model) => model.id);
|
|
const valid = [];
|
|
const diagnostics = [];
|
|
outputs.forEach((item, index) => {
|
|
const expected = Number(item.expected_count || 0);
|
|
const numbers = parseNumbers(item.text || "");
|
|
const minimum = expected ? Math.max(80, Math.ceil(expected * 0.55)) : 80;
|
|
const accepted = numbers.length >= minimum;
|
|
diagnostics.push({ index, parsed_numbers: numbers.length, minimum_numbers: minimum, accepted });
|
|
if (accepted) valid.push({ numbers, counts: countNumbers(numbers), scores: robustScoreNumbers(numbers, bank) });
|
|
});
|
|
if (!valid.length) throw new Error("没有可用回答:请粘贴完整数字序列;拒答或严重截断的回答不会计入。");
|
|
|
|
const combinedScores = modelIds.map((_, modelIndex) => mean(valid.map((item) => item.scores[modelIndex])));
|
|
const calibrationKey = String(Math.min(valid.length, 3));
|
|
const beta = Number(bank.calibration[calibrationKey].beta);
|
|
const probabilities = softmax(combinedScores.map((value) => beta * value));
|
|
const pooledCounts = Array.from({ length: DIMENSION }, (_, index) => valid.reduce((sum, item) => sum + item.counts[index], 0));
|
|
const modelEntries = Object.fromEntries(bank.models.map((model) => [model.id, model]));
|
|
const familyOrder = [...new Set(bank.models.map((model) => model.family || "models"))];
|
|
const familyNames = Object.fromEntries(familyOrder.map((family) => [
|
|
family,
|
|
bank.models.find((model) => (model.family || "models") === family)?.family_name || family,
|
|
]));
|
|
const results = modelIds.map((model, index) => ({
|
|
model,
|
|
display_name: modelEntries[model].display_name,
|
|
probability: probabilities[index],
|
|
profile_similarity: jsSimilarity(pooledCounts, modelEntries[model].counts),
|
|
score: combinedScores[index],
|
|
family: modelEntries[model].family || "models",
|
|
family_name: familyNames[modelEntries[model].family || "models"],
|
|
})).sort((left, right) => right.probability - left.probability);
|
|
const familyProbabilities = Object.fromEntries(familyOrder.map((family) => [
|
|
family,
|
|
results.filter((item) => item.family === family).reduce((sum, item) => sum + item.probability, 0),
|
|
]));
|
|
results.forEach((item) => { item.conditional_probability = item.probability / familyProbabilities[item.family]; });
|
|
const winningFamily = familyOrder.reduce((best, family) => familyProbabilities[family] > familyProbabilities[best] ? family : best);
|
|
return {
|
|
prediction: results[0].model,
|
|
prediction_name: results[0].display_name,
|
|
probability: results[0].probability,
|
|
used_outputs: valid.length,
|
|
results,
|
|
diagnostics,
|
|
calibration: { queries: calibrationKey, beta, cv_accuracy: bank.calibration[calibrationKey].cv_accuracy },
|
|
family_prediction: winningFamily,
|
|
family_prediction_name: familyNames[winningFamily],
|
|
family_probability: familyProbabilities[winningFamily],
|
|
family_probabilities: familyOrder.map((family) => ({ family, display_name: familyNames[family], probability: familyProbabilities[family] })),
|
|
method: "统一全局稳健数字指纹",
|
|
};
|
|
}
|