Учимся генерировать эмбеддинги для текста: выбор модели, пакетная обработка, кэширование. Много кода, минимум теории.
Научитесь генерировать эмбеддинги для текста: выбор модели, пакетная обработка, кэширование. Много кода, минимум теории.
За 45 минут вы:
Эмбеддинг — это вектор чисел, представляющий текст в многомерном пространстве.
"кот сидит на коврике" → [0.12, -0.45, 0.78, 0.33, ..., 0.21] # 384 числа
Ключевое свойство: семантически близкие тексты имеют близкие векторы.
"кот на коврике" → [0.12, -0.45, 0.78, ...]
"кошка на подстилке" → [0.15, -0.42, 0.75, ...] # Близко к первому!
"автомобиль едет" → [0.89, 0.23, -0.12, ...] # Далеко от первогоРасстояние между векторами показывает семантическую близость текстов.
Начнём с простого примера:
from sentence_transformers import SentenceTransformer
# Загружаем модель
model = SentenceTransformer('all-MiniLM-L6-v2')
# Один текст
text = "Машинное обучение — подраздел искусственного интеллекта"
embedding = model.encode(text)
print(f"Размерность: {embedding.shape}") # (384,)
print(f"Первые 10 чисел: {embedding[:10]}")
# Несколько текстов
texts = [
"Машинное обучение",
"Глубокое обучение",
"Искусственный интеллект",
"Анализ данных"
]
embeddings = model.encode(texts)
print(f"\nМатрица эмбеддингов: {embeddings.shape}") # (4, 384)Вывод:
Размерность: (384,)
Первые 10 чисел: [0.023 -0.045 0.078 ...]
Матрица эмбеддингов: (4, 384)
Разные модели — разное качество, скорость, размерность.
| Модель | Размерность | Скорость | Качество | Когда использовать |
|---|---|---|---|---|
| all-MiniLM-L6-v2 | 384 | ⚡⚡⚡ Очень быстро | 👍 Хорошее | Прототипы, демо, небольшие проекты |
| all-mpnet-base-v2 | 768 | ⚡⚡ Средне | 👍👍 Отличное | Продакшен, высокое качество |
| all-distilroberta-v1 | 768 | ⚡⚡⚡ Быстро | 👍👍 Хорошее | Баланс скорости и качества |
| Модель | Размерность | Языки | Когда использовать |
|---|---|---|---|
| paraphrase-multilingual-mpnet-base-v2 | 768 | 50+ | Многоязычные проекты |
| rubert-tiny2 | 312 | RU, EN | Быстрые решения для русского |
Напишите скрипт для сравнения:
import time
from sentence_transformers import SentenceTransformer, util
models_to_test = {
"MiniLM": "all-MiniLM-L6-v2",
"MPNet": "all-mpnet-base-v2",
"DistilRoBERTa": "all-distilroberta-v1",
}
test_pairs = [
("кот сидит на коврике", "кошка лежит на подстилке"),
("Python программирование", "веб-разработка"),
("машинное обучение", "искусственный интеллект"),
("как дела?", "привет, как настроение?"),
]
print("Сравнение моделей:\n")
for name, model_name in models_to_test.items():
print(f"{'='*50}")
print(f"{name} ({model_name})")
print(f"{'='*50}")
model = SentenceTransformer(model_name)
# Тест скорости
start = time.time()
for _ in range(50):
model.encode("тестовый текст для замера скорости")
elapsed = time.time() - start
speed = 50 / elapsed
print(f" Скорость: {speed:.1f} текстов/сек")
print(f" Размерность: {model.get_sentence_embedding_dimension()}")
# Тест качества (косинусное сходство для пар)
print(f"\n Косинусное сходство для пар:")
for text1, text2 in test_pairs:
emb1 = model.encode(text1, convert_to_tensor=True)
emb2 = model.encode(text2, convert_to_tensor=True)
sim = util.cos_sim(emb1, emb2).item()
# Обрезаем текст для вывода
t1 = text1[:25] + "..." if len(text1) > 25 else text1
t2 = text2[:25] + "..." if len(text2) > 25 else text2
print(f" '{t1}' ↔ '{t2}': {sim:.3f}")
print()Пример вывода:
==================================================
MiniLM (all-MiniLM-L6-v2)
==================================================
Скорость: 142.3 текстов/сек
Размерность: 384
Косинусное сходство для пар:
'кот сидит на коврике' ↔ 'кошка лежит на подсти...': 0.812
'Python программирование' ↔ 'веб-разработка': 0.345
'машинное обучение' ↔ 'искусственный интеллект': 0.723
'как дела?' ↔ 'привет, как настроение?': 0.654
Как интерпретировать:
Генерация эмбеддингов для тысяч документов по одному — медленно. Используйте батчи!
texts = ["документ"] * 10000 # 10 тысяч документов
embeddings = []
for text in texts:
embedding = model.encode(text) # 10000 отдельных вызовов!
embeddings.append(embedding)
# Время: ~70 секунд для 10K документовdef batch_encode(model, texts, batch_size=32):
"""
Генерация эмбеддингов батчами для экономии памяти и ускорения.
Args:
model: Модель sentence-transformers
texts: Список текстов
batch_size: Размер батча
"""
all_embeddings = []
for i in range(0, len(texts), batch_size):
batch = texts[i:i + batch_size]
batch_embeddings = model.encode(
batch,
batch_size=batch_size,
show_progress_bar=True # Прогресс-бар
)
all_embeddings.append(batch_embeddings)
return np.vstack(all_embeddings)
# Использование
import numpy as np
texts = ["документ"] * 10000 # 10 тысяч документов
embeddings = batch_encode(model, texts, batch_size=64)
print(f"Матрица: {embeddings.shape}") # (10000, 384)
# Время: ~15 секунд для 10K документов (в 4-5 раз быстрее!)# Маленький батч (16) — меньше памяти, медленнее
embeddings = batch_encode(model, texts, batch_size=16)
# Средний батч (64) — баланс (рекомендуется)
embeddings = batch_encode(model, texts, batch_size=64)
# Большой батч (256) — быстрее, но больше памяти
embeddings = batch_encode(model, texts, batch_size=256)Рекомендации:
10000: batch_size=128–256 (если хватает памяти)
Генерация эмбеддингов — дорогая операция. Кэшируйте результаты!
import hashlib
import json
import numpy as np
from pathlib import Path
class EmbeddingCache:
"""Кэширование эмбеддингов на диске."""
def __init__(self, cache_dir=".cache/embeddings"):
self.cache_dir = Path(cache_dir)
self.cache_dir.mkdir(parents=True, exist_ok=True)
def _get_cache_key(self, text: str) -> str:
"""Хэш текста как ключ кэша."""
return hashlib.md5(text.encode('utf-8')).hexdigest()
def get(self, text: str) -> np.ndarray | None:
"""Получить из кэша или None."""
key = self._get_cache_key(text)
cache_file = self.cache_dir / f"{key}.npy"
if cache_file.exists():
return np.load(cache_file)
return None
def set(self, text: str, embedding: np.ndarray):
"""Сохранить в кэш."""
key = self._get_cache_key(text)
cache_file = self.cache_dir / f"{key}.npy"
np.save(cache_file, embedding)
def get_or_compute(self, model, text: str) -> np.ndarray:
"""Получить из кэша или вычислить."""
embedding = self.get(text)
if embedding is None:
embedding = model.encode(text)
self.set(text, embedding)
return embedding
# Использование
cache = EmbeddingCache()
# Первый вызов — генерация (медленно)
embedding1 = cache.get_or_compute(model, "важный документ")
# Второй вызов — из кэша (мгновенно!)
embedding2 = cache.get_or_compute(model, "важный документ")def batch_encode_with_cache(model, texts, cache, batch_size=64):
"""Пакетная генерация с кэшированием."""
all_embeddings = []
cache_hits = 0
cache_misses = 0
for i in range(0, len(texts), batch_size):
batch = texts[i:i + batch_size]
batch_embeddings = []
for text in batch:
emb = cache.get(text)
if emb is not None:
batch_embeddings.append(emb)
cache_hits += 1
else:
cache_misses += 1
emb = model.encode(text)
cache.set(text, emb)
batch_embeddings.append(emb)
all_embeddings.append(np.array(batch_embeddings))
print(f"Кэш: {cache_hits} попаданий, {cache_misses} промахов")
return np.vstack(all_embeddings)Модели имеют ограничение на длину текста (обычно 512 токенов).
long_text = "Очень длинный документ..." * 1000 # 10000 символов
# ❌ Модель обрежет текст или выдаст ошибку
embedding = model.encode(long_text)def chunk_text(text, max_length=500):
"""
Разбиение длинного текста на чанки.
Args:
text: Исходный текст
max_length: Максимальная длина чанка в символах
"""
chunks = []
# Разбиваем по предложениям
sentences = text.replace('!', '.').replace('?', '.').split('.')
current_chunk = ""
for sentence in sentences:
sentence = sentence.strip()
if not sentence:
continue
if len(current_chunk) + len(sentence) < max_length:
current_chunk += " " + sentence
else:
if current_chunk:
chunks.append(current_chunk.strip())
current_chunk = sentence
if current_chunk:
chunks.append(current_chunk.strip())
return chunks
# Использование
long_text = "Длинный документ..." * 1000
chunks = chunk_text(long_text, max_length=500)
# Генерируем эмбеддинги для каждого чанка
chunk_embeddings = model.encode(chunks)
# Усредняем для получения одного вектора документа
document_embedding = np.mean(chunk_embeddings, axis=0)# Модель с контекстом 1024 токена
model = SentenceTransformer('msmarco-MiniLM-L-6-v3')
# Модель с контекстом 512 токенов (стандарт)
model = SentenceTransformer('all-MiniLM-L6-v2')Для косинусного сходства удобно нормализовать векторы.
from sklearn.preprocessing import normalize
# Генерация
embeddings = model.encode(texts)
# L2-нормализация
normalized_embeddings = normalize(embeddings)
# Теперь скалярное произведение = косинусное сходство
from numpy import dot
similarity = dot(normalized_embeddings[0], normalized_embeddings[1])
print(f"Сходство: {similarity:.3f}")Посмотрите, как векторы группируются в пространстве.
import matplotlib.pyplot as plt
from sklearn.manifold import TSNE
def visualize_embeddings(embeddings, labels=None, title="Векторное пространство"):
"""
Визуализация эмбеддингов в 2D с помощью t-SNE.
Args:
embeddings: Матрица эмбеддингов (n_samples, n_dimensions)
labels: Метки для раскраски точек (опционально)
title: Заголовок графика
"""
# Снижение размерности до 2D
tsne = TSNE(n_components=2, random_state=42, perplexity=5)
embeddings_2d = tsne.fit_transform(embeddings)
plt.figure(figsize=(10, 8))
if labels is not None:
scatter = plt.scatter(
embeddings_2d[:, 0],
embeddings_2d[:, 1],
c=labels,
cmap='viridis',
alpha=0.6,
s=100
)
plt.colorbar(scatter, label='Категория')
else:
plt.scatter(embeddings_2d[:, 0], embeddings_2d[:, 1], alpha=0.6, s=100)
plt.title(title)
plt.xlabel("Компонента 1")
plt.ylabel("Компонента 2")
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# Использование
texts = [
"кот кошка питомец",
"собака пёс питомец",
"автомобиль машина транспорт",
"мотоцикл байк транспорт",
"Python программирование код",
"JavaScript веб программирование"
]
embeddings = model.encode(texts)
labels = [0, 0, 1, 1, 2, 2] # Категории: животные, транспорт, код
visualize_embeddings(embeddings, labels, "Группировка текстов")Создайте утилиту для генерации эмбеддингов с кэшированием:
# embedding_generator.py
import numpy as np
from sentence_transformers import SentenceTransformer
from pathlib import Path
import hashlib
import json
class EmbeddingGenerator:
"""Генератор эмбеддингов с кэшированием и пакетной обработкой."""
def __init__(
self,
model_name: str = 'all-MiniLM-L6-v2',
cache_dir: str = ".cache/embeddings",
batch_size: int = 64
):
self.model = SentenceTransformer(model_name)
self.batch_size = batch_size
self.cache_dir = Path(cache_dir)
self.cache_dir.mkdir(parents=True, exist_ok=True)
# Статистика
self.cache_hits = 0
self.cache_misses = 0
def _get_cache_key(self, text: str) -> str:
return hashlib.md5(text.encode('utf-8')).hexdigest()
def _get_cache_path(self, key: str) -> Path:
return self.cache_dir / f"{key}.npy"
def get_cached(self, text: str) -> np.ndarray | None:
key = self._get_cache_key(text)
cache_path = self._get_cache_path(key)
if cache_path.exists():
self.cache_hits += 1
return np.load(cache_path)
self.cache_misses += 1
return None
def save_cache(self, text: str, embedding: np.ndarray):
key = self._get_cache_key(text)
cache_path = self._get_cache_path(key)
np.save(cache_path, embedding)
def encode_single(self, text: str) -> np.ndarray:
"""Эмбеддинг для одного текста."""
cached = self.get_cached(text)
if cached is not None:
return cached
embedding = self.model.encode(text)
self.save_cache(text, embedding)
return embedding
def encode_batch(self, texts: list[str]) -> np.ndarray:
"""Пакетная генерация с кэшированием."""
all_embeddings = []
for i in range(0, len(texts), self.batch_size):
batch = texts[i:i + self.batch_size]
batch_embeddings = []
for text in batch:
emb = self.get_cached(text)
if emb is not None:
batch_embeddings.append(emb)
else:
emb = self.model.encode(text)
self.save_cache(text, emb)
batch_embeddings.append(emb)
all_embeddings.append(np.array(batch_embeddings))
return np.vstack(all_embeddings)
def get_stats(self) -> dict:
"""Статистика кэша."""
total = self.cache_hits + self.cache_misses
hit_rate = (self.cache_hits / total * 100) if total > 0 else 0
return {
"cache_hits": self.cache_hits,
"cache_misses": self.cache_misses,
"hit_rate": f"{hit_rate:.1f}%"
}
# Использование
if __name__ == "__main__":
generator = EmbeddingGenerator(
model_name='all-MiniLM-L6-v2',
batch_size=64
)
documents = [
"Python — язык программирования",
"Django — веб-фреймворк",
"Flask — микрофреймворк",
"FastAPI — современный фреймворк",
]
embeddings = generator.encode_batch(documents)
print(f"Сгенерировано эмбеддингов: {embeddings.shape}")
print(f"Статистика: {generator.get_stats()}")# ❌ НЕЛЬЗЯ
model1 = SentenceTransformer('all-MiniLM-L6-v2')
model2 = SentenceTransformer('all-mpnet-base-v2')
emb1 = model1.encode("текст")
emb2 = model2.encode("текст")
# Эти векторы в разных пространствах — сравнение бессмысленно!Правило: Одна модель на весь проект.
# ❌ Генерация каждый раз
for query in queries:
embedding = model.encode(query) # 1000 вызовов!
# ✅ Кэширование
for query in queries:
embedding = cache.get_or_compute(model, query) # Повторные — из кэша# ❌ Модель обрежет текст
long_text = "..." * 10000
embedding = model.encode(long_text)
# ✅ Разбиение на чанки
chunks = chunk_text(long_text)
chunk_embeddings = model.encode(chunks)
embedding = np.mean(chunk_embeddings, axis=0)# ❌
embeddings = model.encode(texts)
similarity = np.dot(embeddings[0], embeddings[1])
# ✅
from sklearn.preprocessing import normalize
normalized = normalize(embeddings)
similarity = np.dot(normalized[0], normalized[1])Теперь вы умеете генерировать эмбеддинги. Следующий шаг — Метрики сходства:
my_embeddings.py с генератором эмбеддинговГотовы? Открывайте similarity_metrics!
Вопросы ещё не добавлены
Вопросы для этой подтемы ещё не добавлены.