LogoData Structures & Algorithms

k-NN классификация – распределение по категориям

Алгоритм k-ближайших соседей (k-Nearest Neighbors).

Цель: предсказать, какие фильмы (товары/треки) понравятся пользователю.

Идея k-NN: находим k ближайших по вкусу пользователей (по общим оценкам), затем для неоценённых фильмов усредняем рейтинги соседей с весами по схожести.

Сложность:

Наивно: время – O(U log U + k·I) (сортируем пользователей и агрегируем по фильмам), память – O(U+I).

knnRecommender.ts
1type Ratings = Record<string, Record<string, number>>;
2
3function euclidOnOverlap(a: Record<string, number>, b: Record<string, number>): number {
4 const common = Object.keys(a).filter(k => k in b);
5 // common – это массив общих ключей двух объектов a и b, то есть пересечение их ключей.
6 if (common.length === 0) return Infinity;
7 let sumSq = 0; // sumSq – это накопитель суммы квадратов разностей оценок на пересечении ключей.
8 for (const k of common) sumSq += (a[k] - b[k]) ** 2;
9 return Math.sqrt(sumSq);
10}
11
12/**
13 * Рекомендательная система k-NN.
14 * 1) Ищем k ближайших пользователей (схожесть = 1 / (1 + distance)).
15 * 2) Для фильмов, которых у user нет, считаем взвешенное среднее рейтингов соседей.
16 */
17function knnRecommend(data: Ratings, user: string, k: number): {
18 neighbors: string[];
19 recommendations: Array<{ item: string; score: number }>;
20} {
21 const dists: Array<{ name: string; dist: number; sim: number }> = [];
22 for (const [name, r] of Object.entries(data)) {
23 if (name === user) continue;
24 const d = euclidOnOverlap(data[user], r); // d – евклидова дистанция
25 if (!Number.isFinite(d)) continue;
26 dists.push({ name, dist: d, sim: 1 / (1 + d) });
27 }
28 dists.sort((a, b) => a.dist - b.dist);
29 const neighbors = dists.slice(0, k);
30
31 const seen = new Set(Object.keys(data[user]));
32 const score: Record<string, number> = {};
33 const weight: Record<string, number> = {};
34
35 for (const n of neighbors) {
36 for (const [item, rating] of Object.entries(data[n.name])) {
37 if (seen.has(item)) continue;
38 score[item] = (score[item] ?? 0) + n.sim * rating;
39 weight[item] = (weight[item] ?? 0) + n.sim;
40 }
41 }
42
43 const recommendations = Object.keys(score)
44 .map(item => ({ item, score: +(score[item] / weight[item]).toFixed(2) }))
45 .sort((a, b) => b.score - a.score);
46
47 return { neighbors: neighbors.map(n => n.name), recommendations };
48}
49
50// Данные
51const ratings: Ratings = {
52 you: { Inception: 5, Matrix: 4 },
53 alice: { Inception: 5, Matrix: 5, Titanic: 1 },
54 bob: { Inception: 4, Matrix: 4, Avatar: 3, Titanic: 1 },
55 carol: { Matrix: 5, Interstellar: 5, Avatar: 2 },
56 dave: { Inception: 2, Titanic: 5, Avatar: 4 },
57 eve: { Matrix: 4, Interstellar: 4, Inception: 5 },
58};
59
60// Примеры
61knnRecommend(ratings, 'you', 2);
62 // neighbors -> ["eve","alice"];
63 // top -> [["Interstellar",4],["Titanic",1]]
64knnRecommend(ratings, 'you', 3);
65 // neighbors -> ["eve","alice","bob"];
66 // top -> [["Interstellar",4],["Avatar",3],["Titanic",1]]
67knnRecommend(ratings, 'you', 4);
68 // neighbors -> ["eve","alice","bob","carol"];
69 // top -> [["Interstellar",4.33],["Avatar",2.5],["Titanic",1]]