Кластеризация и снижение размерности
Как найти структуру в данных без меток — k-means с нуля, метод главных компонент через SVD и визуализация эмбеддингов с t-SNE и UMAP.
Зачем это нужно
Заголовок раздела «Зачем это нужно»Размеченных данных всегда мало, неразмеченных — океан. обучение без учителяобучение без учителяEN unsupervised learningМеток нет; ищем структуру в данных — кластеры, главные направления, плотность. ищет в данных структуру без подсказок: какие объекты похожи, вдоль каких направлений данные на самом деле меняются. В мире LLM эти инструменты встречаются на каждом шагу:
- Смотреть на эмбеддинги. Модель выдала векторы размерности 1024 для тысячи документов. Разделились ли темы? Нет ли среди них мусора? Ответ даёт карта на плоскости, построенная методами снижения размерности.
- Чистить данные. Похожие документы в корпусе предобучения находят через кластеры эмбеддингов: это помогает удалять смысловые дубликаты и балансировать темы (о дедупликации — в уроке 5.1.2).
- Быстрый поиск. Векторные базы данных для RAG разбивают миллионы векторов на кластеры k-means, чтобы не сравнивать запрос с каждым (ступень 8 и «Китайский угол» ниже).
- Сжатие. Метод главных компонент — это SVD центрированных данных. На той же математике низкого ранга стоят LoRA и MLA в DeepSeek (урок 1.1.3).
Кластеризация и k-means
Заголовок раздела «Кластеризация и k-means»кластеризациякластеризацияEN clustering — разбить объекты на группы так, чтобы внутри группы они были похожи, а между группами различались. Меток нет: сколько групп и какие они, алгоритм решает сам или с подсказкой — числом .
Целевая функция
Заголовок раздела «Целевая функция»метод k-среднихметод k-средних (k-means)EN k-means описывает каждый кластер одной точкой — центроидомцентроидEN centroid («центром масс» кластера: среднее всех его точек; у точек 1, 2 и 3 центроид — 2) и минимизирует сумму квадратов расстояний от точек до центроидов их кластеров. Эту сумму называют inertia:
где — номер кластера точки , а — центроид этого кластера. Словами: для каждой точки берём квадрат расстояния до «её» центра и всё складываем. Чем меньше , тем теснее точки прижаты к своим центрам. Точный минимум искать NP-трудно (как построение лучшего дерева в прошлом уроке), поэтому используют алгоритм Ллойда.
Алгоритм Ллойда
Заголовок раздела «Алгоритм Ллойда»- Назначение. Каждую точку относим к ближайшему центроиду.
- Обновление. Каждый центроид переносим в среднее арифметическое своих точек.
- Повторяем, пока назначения не перестанут меняться.
Почему это работает? Оба шага не увеличивают . На шаге назначения каждое слагаемое либо уменьшается, либо остаётся прежним: мы выбрали ближайший центроид. На шаге обновления среднее — именно та точка, которая минимизирует сумму квадратов расстояний до точек кластера (производная по обнуляется при ). Если на шаге назначения хоть одна точка сменила кластер, строго уменьшился: точку переносят, только когда другой центроид строго ближе (при равенстве расстояний она остаётся на месте). Значит, ни одно разбиение не может повториться. Способов разбить точек на групп конечное число, поэтому алгоритм обязательно остановится.
Пример. Точки на прямой: 1, 2, 3, 10, 11, 12, . Неудачный старт: центроиды 1 и 2. Пройдём алгоритм руками.
- Старт. Точка 1 ближе к центроиду 1, все остальные — к центроиду 2. .
- Итерация 1. Обновление: центроид первого кластера — среднее , то есть 1; второго — среднее . Назначение: 1, 2, 3 ближе к 1, а 10, 11, 12 — к 7.6. .
- Итерация 2. Обновление: центроиды 2 и 11. Назначение не изменилось — алгоритм сошёлся, .
То же в таблице:
| Шаг | Центроиды | Кластеры | |
|---|---|---|---|
| старт | 1, 2 | {1} и {2, 3, 10, 11, 12} | 246 |
| итерация 1 | 1, 7.6 | {1, 2, 3} и {10, 11, 12} | 41.68 |
| итерация 2 | 2, 11 | {1, 2, 3} и {10, 11, 12} | 4 |
На второй итерации назначения не изменились — сошлись. Ровно этот пример проверяет задание ниже: ваша функция
вернёт историю inertia [41.68, 4.0] — по числу на итерацию, без стартовой строки.
Локальные минимумы и k-means++
Заголовок раздела «Локальные минимумы и k-means++»Алгоритм гарантирует остановку, но не глобальный минимум. Пусть облаков точек три и . Если два стартовых центроида попали в одно облако, а третий — между двумя другими, k-means может так и остаться: первое облако разрезано пополам, два других слиты в один кластер. Два средства:
- k-means++ (Артур и Васильвицкий, 2007). Первый центроид — случайная точка. Каждый следующий выбирается с вероятностью, пропорциональной квадрату расстояния до ближайшего уже выбранного центроида. Далёкие точки выбираются чаще, центроиды разбегаются по данным.
- Несколько запусков с разными стартами, оставляем решение с наименьшим . В scikit-learn за это отвечает
параметр
n_init.
Как выбрать k
Заголовок раздела «Как выбрать k»Нельзя выбирать по минимуму : inertia монотонно падает с ростом и при равна нулю, каждая точка — свой кластер. Это та же ловушка, что ошибка на train в уроке 2.1.2. Обычно смотрят на «локоть»: после него добавление кластеров почти не уменьшает . Ещё используют коэффициент силуэта: насколько точка ближе к своему кластеру, чем к соседнему. Часто же диктует задача: «нужно 5 сегментов клиентов для 5 рассылок».
Ограничения
Заголовок раздела «Ограничения»k-means неявно считает кластеры «круглыми» и примерно одного размера, ведь он использует евклидово расстояние до центра. Вытянутые облака, полумесяцы из прошлого урока, кольца он режет неправильно. Он чувствителен к масштабу признаков (стандартизируйте!) и к выбросам: одна далёкая точка тянет за собой центроид. Альтернативы: DBSCAN ищет плотные области и умеет объявлять точки шумом; иерархическая кластеризация строит дерево вложенных групп; гауссовы смеси — «мягкий» k-means, где каждая точка принадлежит кластерам с вероятностями. Одна итерация k-means стоит . Для миллионов точек есть mini-batch вариант.
Снижение размерности и PCA
Заголовок раздела «Снижение размерности и PCA»снижение размерностиснижение размерностиEN dimensionality reduction нужно, чтобы увидеть данные (из 1024 измерений в 2), сжать их, убрать шум и ускорить другие алгоритмы. Самый важный метод — метод главных компонентметод главных компонент (PCA)EN principal component analysis (PCA).
Интуиция на пяти точках
Заголовок раздела «Интуиция на пяти точках»Возьмём точки . Они почти лежат на прямой . Двумерные данные фактически одномерны: всё, что происходит, — движение вдоль этой прямой, остальное — шум. PCA находит это направление автоматически.
Сначала одно новое понятие. Ковариация двух признаков (она уже встречалась в уроке 2.2.1) — среднее произведение их отклонений от средних. Она положительна, если признаки растут вместе, и отрицательна, если один растёт, когда другой падает. Ковариационная матрица — таблица ковариаций всех пар признаков. На её диагонали стоят ковариации признака с самим собой, то есть просто дисперсии (разброс каждого признака), а вне диагонали — ковариации разных признаков.
- Центрируем: среднее вычитаем из всех точек. Получаем .
- Считаем ковариационную матрицу . Например, левый верхний элемент — дисперсия первой координаты: . (Делим на , а не на 5, — так принято для оценки по выборке; на результат PCA это не влияет.)
- Её собственные числа (урок 1.1.3) — 11.87 и 0.015, собственный вектор первого — . Это и есть направление прямой: . Собственное число здесь — дисперсия данных вдоль своего направления: вдоль прямой точки разбросаны сильно (11.87), поперёк — почти никак (0.015).
- Доля объяснённой дисперсии — какая часть всего разброса данных приходится на это направление. У первой компоненты это .
- Проекции точек на (скалярное произведение центрированной точки на ): для первой точки , для всех пяти: . Пять пар чисел превратились в пять чисел почти без потери информации.
Для любознательных: почему собственные векторы
Заголовок раздела «Для любознательных: почему собственные векторы»Этот раздел объясняет, почему ответ — именно собственный вектор. При первом чтении его можно пропустить: достаточно запомнить вывод «первая главная компонента — собственный вектор с наибольшим собственным числом».
Для центрированных данных дисперсия проекции на единичное направление равна
Нужно найти единичный вектор , при котором максимально. Матрица симметрична, а у симметричной матрицы собственные векторы можно выбрать единичными и попарно перпендикулярными (вспомните матрицу из урока 1.1.3 с собственными направлениями и ). Обозначим их , а собственные числа — . Разложим , где . Тогда
Это взвешенное среднее собственных чисел с весами , которые в сумме дают 1. Оно максимально, когда весь вес отдан наибольшему: , и дисперсия вдоль него равна . В нашем примере это 11.87. Первая главная компонента — собственный вектор с наибольшим , вторая — со следующим (она ортогональна первой), и так далее. Тот же ответ, , даёт метод множителей Лагранжа, если вы с ним знакомы.
PCA через SVD
Заголовок раздела «PCA через SVD»На практике ковариационную матрицу не строят, а делают SVD центрированных данных из урока 1.1.3. Напомним: SVD раскладывает любую матрицу в произведение , где на диагонали стоят сингулярные числа — коэффициенты растяжения, упорядоченные по важности. Связь с ковариационной матрицей:
Отсюда весь рецепт:
| Что нужно | Как получить из SVD |
|---|---|
| главные направления | строки (столбцы ) |
| дисперсия вдоль -й компоненты | |
| доля объяснённой дисперсии | |
| координаты в компонентах | |
| восстановление | + среднее |
SVD численно устойчивее, чем явное перемножение .
Связь с низким рангом (для любознательных). По теореме Эккарта — Янга — лучшее приближение центрированных данных матрицей ранга . Ошибка восстановления (сумма квадратов всех отклонений ) равна сумме отброшенных . В примере с пятью точками это — против у оставленной компоненты. Значит, PCA отвечает на вопрос «в каком -мерном подпространстве живут данные?». Та же идея работает в LoRA (обновление весов низкого ранга) и в MLA DeepSeek-V2, где ключи и значения сжимаются в латентный вектор малой размерности. Отличие одно: там проекцию не вычисляют по SVD, а обучают вместе с моделью.
Практические замечания. Сколько компонент оставить — по накопленной доле дисперсии (например, 95%) или 2–3 для картинки. Признаки в разных единицах (рубли и годы) сначала стандартизируют, иначе первая компонента просто укажет на признак с самым большим разбросом. Знак компоненты произволен: и — одно направление. Не удивляйтесь, если на другом компьютере картинка отразится зеркально.
Визуализация эмбеддингов: t-SNE и UMAP
Заголовок раздела «Визуализация эмбеддингов: t-SNE и UMAP»PCA — линейный метод. Он сохраняет глобальную геометрию, но если кластеры лежат на изогнутых поверхностях, в проекции они могут наложиться друг на друга. Нелинейные методы жертвуют глобальной геометрией ради соседства: близкие в исходном пространстве точки должны остаться близкими на картинке.
t-SNEt-SNE (стохастическое вложение соседей)EN t-distributed stochastic neighbor embedding (t-SNE)Метод визуализации, сохраняющий соседство точек. Размеры кластеров и расстояния между далёкими кластерами на картинке t-SNE почти ничего не значат. (ван дер Маатен и Хинтон, 2008). Главное, что нужно знать: t-SNE раскладывает точки на плоскости так, чтобы соседи остались соседями, и подбирает раскладку градиентным спуском. Как именно он это делает, описано ниже — для любознательных:
- Для каждой точки превращаем расстояния до остальных в вероятности «быть соседом» с помощью гауссова ядра (формулы «колокола» из урока 1.3.1: чем дальше точка, тем меньше вероятность). Ширину ядра подбирают так, чтобы perplexity распределения равнялась заданной (обычно 5–50). Это то же понятие perplexity, что в уроке 1.3: эффективное число соседей.
- На плоскости считаем похожие вероятности, но с распределением Стьюдента. Его тяжёлые хвосты дают далёким точкам разойтись и спасают от «скучивания» в центре.
- Двигаем точки на плоскости градиентным спуском, минимизируя KL-дивергенцию между двумя распределениями.
UMAP (Макиннес, Хили, Мелвилл, 2018) строит граф ближайших соседей (каждую точку соединяет линиями с её
ближайшими соседями, как в kNN из прошлого урока) и ищет плоскую раскладку, сохраняющую этот
граф. Работает заметно быстрее t-SNE, масштабируется до миллионов точек и, по мнению авторов, лучше сохраняет
глобальную структуру. Главные параметры — n_neighbors и min_dist.
flowchart LR A["Тексты<br/>на разных языках"] --> B["Модель эмбеддингов<br/>например, BGE"] B --> C["Векторы<br/>1024 измерения"] C --> D["PCA<br/>до 50 измерений"] D --> E["t-SNE / UMAP<br/>до 2 измерений"] E --> F["Точечный график,<br/>цвет — известная тема"]
| PCA | t-SNE | UMAP | |
|---|---|---|---|
| Тип | линейный | нелинейный | нелинейный |
| Что сохраняет | глобальную дисперсию | локальное соседство | соседство, частично глобальную структуру |
| Скорость | очень быстро (SVD) | медленно на больших данных | быстро |
| Новые точки | проекция одной формулой | нужно пересчитывать | есть приближённое преобразование |
| Детерминизм | да (с точностью до знака) | нет | нет |
| Для чего | сжатие, предобработка, первый взгляд | красивые карты кластеров | большие карты эмбеддингов |
Практика
Заголовок раздела «Практика»Задание: k-means с нуля
Заголовок раздела «Задание: k-means с нуля»Реализуйте назначение, обновление центроидов, inertia и полный алгоритм Ллойда на NumPy. Особое внимание —
пустому кластеру: среднее пустого массива даёт nan («не число»), и он отравит все следующие итерации.
Кнопка «Запустить» прогоняет пример с шестью точками из урока: сверяйте вывод с таблицей выше.
Формы массивов: точки X — таблица , по строке на точку; центроиды C — ; метки labels — целых
чисел от 0 до , номер кластера каждой точки. В примере из урока , поэтому точки записаны как
[[1.0], [2.0], ...].
import numpy as np
def assign(X, C):
"""Номер ближайшего центроида для каждой точки.
X: (n, d) — точки, C: (k, d) — центроиды. Вернуть массив формы (n,) с числами 0..k−1.
"""
# TODO: ваш код (подсказка: X[:, None, :] - C[None, :, :] даёт все попарные разности)
...
def update(X, labels, C_old):
"""Новые центроиды: для каждого кластера j — среднее точек с labels == j.
Если в кластер не попало ни одной точки, оставьте старый центроид C_old[j].
"""
# TODO: ваш код
...
def inertia(X, C, labels):
"""Сумма квадратов расстояний от каждой точки до центроида её кластера (float)."""
# TODO: ваш код
...
def kmeans(X, C_init, max_iter=100):
"""Алгоритм Ллойда.
1) labels = assign(X, C_init);
2) в цикле: C = update(...), new_labels = assign(...), добавить inertia в history;
если new_labels совпали с labels — выйти (сошлись), иначе labels = new_labels.
Вернуть (C, labels, history).
"""
# TODO: ваш код
...
# «Запустить»: шесть точек на прямой и неудачный старт
X = np.array([[1.0], [2.0], [3.0], [10.0], [11.0], [12.0]])
result = kmeans(X, C_init=np.array([[1.0], [2.0]]))
if result is None:
print("Допишите функции выше — kmeans пока ничего не возвращает.")
else:
C, labels, history = result
print("центроиды:", C.ravel(), "метки:", labels, "inertia по итерациям:", [round(h, 2) for h in history])
PCA и k-means++ на «эмбеддингах»
Заголовок раздела «PCA и k-means++ на «эмбеддингах»»Запустите локально. Мы сгенерируем синтетические 64-мерные «эмбеддинги» двенадцати слов из трёх тем (центр темы плюс шум), спроецируем их PCA на плоскость и найдём темы k-means с инициализацией k-means++.
import numpy as np
rng = np.random.default_rng(0)topics = { "фрукты": ["яблоко/apple", "банан/banana", "груша/pear", "апельсин/orange"], "животные": ["кошка/cat", "собака/dog", "панда/panda", "тигр/tiger"], "города": ["Москва/Moscow", "Пекин/Beijing", "Шанхай/Shanghai", "Казань/Kazan"],}# Синтетические «эмбеддинги» размерности 64: центр темы + личный шум словаwords, vecs = [], []for topic, ws in topics.items(): center = rng.normal(0, 1, 64) for w in ws: words.append(w) vecs.append(center + rng.normal(0, 0.5, 64))X = np.array(vecs) # (12, 64)
# ── PCA через SVD ─────────────────────────────────────────────Xc = X - X.mean(axis=0) # 1) центрируем: вычитаем среднее каждого столбцаU, S, Vt = np.linalg.svd(Xc, full_matrices=False) # 2) Xc = U · diag(S) · Vt (S — сингулярные числа)explained = S**2 / (S**2).sum() # 3) доля дисперсии компонент: σᵢ² / Σσⱼ²Z = Xc @ Vt[:2].T # 4) проекция на PC1, PC2: каждое слово → 2 числаprint("доля дисперсии PC1, PC2:", np.round(explained[:2], 3))for (a, b), w in zip(Z, words): print(f"{a:6.2f} {b:6.2f} {w}")
# ── k-means++ и алгоритм Ллойда в 64-мерном пространстве ─────def kmeans_pp_init(X, k, rng): """Стартовые центроиды k-means++: каждый следующий — скорее подальше от уже выбранных.""" C = [X[rng.integers(len(X))]] # первый центр — случайная точка for _ in range(k - 1): # для каждой точки — квадрат расстояния до БЛИЖАЙШЕГО из уже выбранных центров d2 = np.min([((X - c) ** 2).sum(1) for c in C], axis=0) # rng.choice с вероятностями p: точка с бо́льшим d2 выбирается чаще C.append(X[rng.choice(len(X), p=d2 / d2.sum())]) # далёкие точки вероятнее return np.array(C)
def kmeans(X, C, n_iter=100): """Алгоритм Ллойда (то же, что в задании, только компактнее).""" for _ in range(n_iter): labels = ((X[:, None] - C[None]) ** 2).sum(-1).argmin(1) # назначение: ближайший центроид # обновление: среднее точек кластера; пустой кластер сохраняет старый центроид C_new = np.array([X[labels == j].mean(0) if (labels == j).any() else C[j] for j in range(len(C))]) if np.allclose(C_new, C): # центроиды почти не сдвинулись — сошлись break C = C_new return C, labels
C, labels = kmeans(X, kmeans_pp_init(X, 3, rng))for j in range(3): print(f"кластер {j}:", ", ".join(w for w, l in zip(words, labels) if l == j))доля дисперсии PC1, PC2: [0.447 0.329] -3.73 4.14 яблоко/apple -3.80 4.42 банан/banana -5.26 4.52 груша/pear -4.24 4.49 апельсин/orange -1.97 -5.26 кошка/cat -2.91 -5.21 собака/dog -1.92 -5.87 панда/panda -2.42 -5.21 тигр/tiger 6.61 0.87 Москва/Moscow 6.02 1.30 Пекин/Beijing 6.96 0.43 Шанхай/Shanghai 6.68 1.37 Казань/Kazanкластер 0: Москва/Moscow, Пекин/Beijing, Шанхай/Shanghai, Казань/Kazanкластер 1: яблоко/apple, банан/banana, груша/pear, апельсин/orangeкластер 2: кошка/cat, собака/dog, панда/panda, тигр/tigerЗнаки координат и номера кластеров на вашей машине могут отличаться. Это нормально: направление компоненты определено с точностью до знака, а номера кластеров — просто имена. Заметьте: три темы уместились в две компоненты с 78% дисперсии. Три центра в любом пространстве лежат в одной плоскости. Настоящие эмбеддинги (ступень 4) устроены сложнее, но этот конвейер вы будете применять к ним без изменений.
Проверь себя
Заголовок раздела «Проверь себя»- k-means минимизирует сумму квадратов расстояний до центроидов. Алгоритм Ллойда чередует назначение и обновление, всегда сходится, но к локальному минимуму. Помогают k-means++ и несколько запусков.
- не выбирают по минимуму inertia. Признаки стандартизируют, а для некруглых кластеров берут DBSCAN или гауссовы смеси.
- PCA — проекция на собственные векторы ковариационной матрицы. На практике это SVD центрированных данных, доля дисперсии компоненты — . PCA даёт лучшее приближение низкого ранга, ту же идею, что в LoRA и MLA.
- t-SNE и UMAP сохраняют соседство и строят красивые карты эмбеддингов. Размеры и расстояния между кластерами на них не читаются.
- k-means работает внутри векторных баз (IVF в Milvus), а кластеризация — одна из задач оценки эмбеддингов (C-MTEB).
Дальше: модуль 2.4 — практический ML на табличных данных. Начнём с pandas, затем разведка данных, градиентный бустинг изнутри и проект с честной валидацией.