AI・機械学習
応答のフィンガープリント分析により、KimiがClaudeと類似していることが判明
Writing fingerprint analysis of responses reveals Kimi's similarity to Claude (typebulb.com)
要約
この分析では、大規模言語モデル(LLM)の応答をフィンガープリント化し、その類似性をヒートマップで可視化しています。その結果、KimiというモデルがClaudeと高い類似性を持つことが明らかになりました。これは、モデル間のスタイルの違いや共通点を理解する上で興味深い洞察を提供します。
全文翻訳
あなたは比較的正しい!どのLLMモデルが似たような文章を書くのか?単語のみから構築されたヒートマップ。
--- フォーマット: typebulb/v1
名前: "あなたは比較的正しい!"
---
**code.tsx**
```tsx
import React, { useEffect, useMemo, useState } from "react";
import { createRoot } from "react-dom/client";
import { encode } from "gpt-tokenizer/encoding/cl100k_base";
// ── Types ────────────────────────────────────────────────────────
type RawResult = {
testId: string;
testName: string;
subject: string;
score: number;
response: string;
response2?: string;
reasoning: string;
error?: string;
};
type RawData = {
timestamp?: string;
judge?: string;
results: RawResult[];
};
type MetaRow = {
match: string;
lab: string;
released: string | null;
};
type Counts = {
map: Map<string, number>;
n: number;
};
type Bg = {
words: Counts;
phrases: Counts;
common: Set<string>;
};
type ModelRow = {
name: string;
disp: string; // "<true lab>: <model>" — OpenRouter エントリは実際のラボに解決されます
lab: string;
released: string | null; // YYYY-MM、データブロックから。提供されるまでnull
chars: number; // コーパスサイズ
grams: Counts; // 文字の3連(トリグラム)
words: Counts; // 単語の1連(ユニグラム)(特徴的な表示用)
phrases: Counts; // 単語の2連(バイグラム)(特徴的な表示用)
wordDf: Map<string, number>; // 各単語を含む回答の数
phraseDf: Map<string, number>; // 各バイグラムを含む回答の数
docs: number; // コーパス内の回答数
H: number; // 自身のエントロピー、トリグラムあたりのビット数(生のMLE)
};
// ── Text stats ───────────────────────────────────────────────────
const LAMBDA = 0.8; // 補間: λ・モデル + (1−λ)・プールされた背景
const lg = (x: number) => Math.log(x) / Math.LN2;
function countGrams(text: string, k: number): Counts {
const map = new Map<string, number>();
for (let i = 0; i + k <= text.length; i++) {
const g = text.slice(i, i + k);
map.set(g, (map.get(g) ?? 0) + 1);
}
return { map, n: Math.max(1, text.length - k + 1) };
}
function countList(items: string[]): Counts {
const map = new Map<string, number>();
for (const it of items) map.set(it, (map.get(it) ?? 0) + 1);
return { map, n: Math.max(1, items.length) };
}
function pool(all: Counts[]): Counts {
const map = new Map<string, number>();
let n = 0;
for (const c of all) {
for (const [g, v] of c.map)
map.set(g, (map.get(g) ?? 0) + v);
n += c.n;
}
return { map, n };
}
// モデルqの下でのグラムgの補間確率、プールされた背景にバックオフ
const prob = (g: string, q: Counts, bg: Counts) =>
LAMBDA * ((q.map.get(g) ?? 0) / q.n) +
(1 - LAMBDA) * ((bg.map.get(g) ?? 0) / bg.n);
// BPEトークン数(希少性スクリーニング用):トークナイザーは頻度でトレーニングされているため、「the」は1トークン、「congratulatory」は4トークンかかります。特徴は、単語数よりも多くのトークンをコストがかかる必要があります(単語≥2、フレーズ≥3)。
const tokCache = new Map<string, number>();
function tokenCount(w: string): number {
let c = tokCache.get(w);
if (c === undefined) {
c = encode(w).length;
tokCache.set(w, c);
}
return c;
}
function rareEnough(term: string, extra = 1): boolean {
const parts = term.split(" ");
return parts.reduce((s, w) => s + tokenCount(w), 0) >= parts.length + extra;
}
// log10 P(X ≥ c) (X ~ Poisson(λ)):フィールドのレートでc回以上の使用が発生する確率
function log10PoissonTail(c: number, lambda: number): number {
if (c <= 0 || lambda <= 0) return 0;
let lnTerm = c * Math.log(lambda) - lambda;
for (let k = 2; k <= c; k++)
lnTerm -= Math.log(k);
let sum = 1, term = 1;
for (let k = c; k < c + 200; k++) {
term *= lambda / (k + 1);
sum += term;
if (term < 1e-12 * sum) break;
}
return Math.min(0, (lnTerm + Math.log(sum)) / Math.LN10);
}
function crossEntropy(a: Counts, b: Counts, bg: Counts): number {
let s = 0;
for (const [g, c] of a.map)
s -= (c / a.n) * lg(prob(g, b, bg));
return s;
}
function entropy(a: Counts): number {
let s = 0;
for (const c of a.map.values()) {
const p = c / a.n;
s -= p * lg(p);
}
return s;
}
type Tale = {
term: string;
ratio: number;
bCount: number;
tokens: number;
surprise: number;
dfA: number;
dfB: number;
};
// 両方のモデルが依存しているが、フィールドの残りはめったに使用しない用語 — 親和性の証拠。
// 各サバイバーは修正された驚きを持ちます:両方のモデルがフィールドのレートでそのカウントにヒットする可能性のある結合ポアソン確率の−log10、マイナススキャンされたすべての用語の「ルック・エルスウェア」コスト。
function sharedTales(a: ModelRow, b: ModelRow, bg: Bg, top: number): Tale[] {
const minus = (pool: Counts, ...subs: Counts[]): Counts => {
const map = new Map(pool.map);
let n = pool.n;
for (const s of subs) {
for (const [g, v] of s.map) {
const r = (map.get(g) ?? 0) - v;
if (r > 0) map.set(g, r);
else map.delete(g);
}
n -= s.n;
}
return { map, n: Math.max(1, n) };
};
const restWords = minus(bg.words, a.words, b.words);
const restPhrases = minus(bg.phrases, a.phrases, b.phrases);
let scanned = 0;
const cand: Tale[] = [];
const consider = (term: string, aC: Counts, bC: Counts, rest: Counts, boring: boolean, dfA: number, dfB: number) => {
scanned++;
const ca = aC.map.get(term) ?? 0,
cb = bC.map.get(term) ?? 0;
if (ca < 3 || cb < 3 || boring || !rareEnough(term)) return;
if (dfA < 2 || dfB < 2) return; // ある習慣が回答全体に再発する。1つの回答での1回のバーストはカウントされない
const lo = Math.min(ca / aC.n, cb / bC.n);
const fieldCount = rest.map.get(term) ?? 0;
const qf = (fieldCount + 0.5) / (rest.n + 1);
if (lo / qf < 2) return;
cand.push({
term,
ratio: lo / qf,
bCount: fieldCount,
dfA,
dfB,
tokens: term.split(" ").reduce((s, w) => s + tokenCount(w), 0),
surprise: -(log10PoissonTail(ca, qf * aC.n) + log10PoissonTail(cb, qf * bC.n)),
});
};
for (const [w] of a.words.map)
consider(w, a.words, b.words, restWords, bg.common.has(w), a.wordDf.get(w) ?? 0, b.wordDf.get(w) ?? 0);
for (const [p] of a.phrases.map) {
const [w1, w2] = p.split(" ");
consider(p, a.phrases, b.phrases, restPhrases, bg.common.has(w1) && bg.common.has(w2), a.phraseDf.get(p) ?? 0, b.phraseDf.get(p) ?? 0);
}
const lookElsewhere = Math.log(Math.max(1, scanned)) / Math.LN10;
for (const t of cand) t.surprise = Math.max(0, t.surprise - lookElsewhere);
cand.sort((x, y) => y.surprise - x.surprise || y.tokens - x.tokens); // 最も運を超えたものから順に。希少性がタイを破る
const picked: Tale[] = [];
for (const t of cand) {
if (picked.length >= top) break;
if (picked.some(p => p.term.split(" ").some(w => t.term.split(" ").includes(w)))) continue; // フレーズとその単語は1つの特徴
picked.push(t);
}
return picked;
}
// True-lab + データブロックからのリリースメタデータ。リストされたプロバイダープレフィックスにフォールバックします
const META = tb.json<MetaRow[]>(1);
const metaFor = (name: string) => META.find(m => m.match === shortName(name));
function buildModels(results: RawResult[]): { models: ModelRow[]; bgGrams: Counts; bg: Bg } {
const byModel = new Map<string, RawResult[]>();
for (const r of results) {
const list = byModel.get(r.subject) ?? [];
list.push(r);
byModel.set(r.subject, list);
}
const models: ModelRow[] = [];
for (const [name, rs] of byModel) {
let corpus = "";
let docs = 0;
const wordDf = new Map<string, number>(), phraseDf = new Map<string, number>();
for (const r of rs) {
if (r.error) continue;
const piece = r.response + (r.response2 ? " " + r.response2 : "");
corpus += " " + piece;
docs++;
const dToks = piece.replace(/\s+/g, " ").toLowerCase().match(/[a-z']+/g) ?? [];
for (const w of new Set(dToks))
wordDf.set(w, (wordDf.get(w) ?? 0) + 1);
for (const p of new Set(dToks.slice(0, -1).map((w, i) => w + " " + dToks[i + 1])))
phraseDf.set(p, (phraseDf.get(p) ?? 0) + 1);
}
const text = corpus.replace(/\s+/g, " ").trim();
if (text.length < 100) continue;
const grams = countGrams(text, 3);
const toks = text.toLowerCase().match(/[a-z']+/g) ?? [];
const meta = metaFor(name);
const lab = meta?.lab ?? (name.includes(": ") ? name.slice(0, name.indexOf(": ")) : name);
models.push({
name,
lab,
disp: `${lab}: ${shortName(name)}`,
released: meta?.released ?? null,
chars: text.length,
grams,
words: countList(toks),
phrases: countList(toks.slice(0, -1).map((w, i) => w + " " + toks[i + 1])),
wordDf,
phraseDf,
docs,
H: entropy(grams),
});
}
models.sor
```