Замена RuBERT на sentence-transformers/all-MiniLM-L12-v2, упрощение формулы расчета, поддержка загрузки из отдельных .npy файлов
This commit is contained in:
@@ -25,7 +25,7 @@ COPY app/ ./app/
|
|||||||
RUN mkdir -p data/models data/vectors
|
RUN mkdir -p data/models data/vectors
|
||||||
|
|
||||||
# Переменные окружения по умолчанию
|
# Переменные окружения по умолчанию
|
||||||
ENV RAG_MODEL=DeepPavlov/rubert-base-cased
|
ENV RAG_MODEL=sentence-transformers/all-MiniLM-L12-v2
|
||||||
ENV RAG_CACHE_DIR=/app/data/models
|
ENV RAG_CACHE_DIR=/app/data/models
|
||||||
ENV RAG_VECTORS_PATH=/app/data/vectors/vectors.npz
|
ENV RAG_VECTORS_PATH=/app/data/vectors/vectors.npz
|
||||||
ENV RAG_API_HOST=0.0.0.0
|
ENV RAG_API_HOST=0.0.0.0
|
||||||
|
|||||||
@@ -416,9 +416,7 @@ async def update_scoring_params(
|
|||||||
try:
|
try:
|
||||||
params = service.vector_store.update_scoring_params(
|
params = service.vector_store.update_scoring_params(
|
||||||
score_multiplier=request.score_multiplier,
|
score_multiplier=request.score_multiplier,
|
||||||
k_min=request.k_min,
|
k=request.k,
|
||||||
k_max=request.k_max,
|
|
||||||
base_multiplier_factor=request.base_multiplier_factor,
|
|
||||||
)
|
)
|
||||||
return ScoringParamsResponse(**params)
|
return ScoringParamsResponse(**params)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class Settings:
|
|||||||
|
|
||||||
# Модель
|
# Модель
|
||||||
model_name: str = field(
|
model_name: str = field(
|
||||||
default_factory=lambda: os.getenv("RAG_MODEL", "DeepPavlov/rubert-base-cased")
|
default_factory=lambda: os.getenv("RAG_MODEL", "sentence-transformers/all-MiniLM-L12-v2")
|
||||||
)
|
)
|
||||||
cache_dir: str = field(
|
cache_dir: str = field(
|
||||||
default_factory=lambda: os.getenv("RAG_CACHE_DIR", "data/models")
|
default_factory=lambda: os.getenv("RAG_CACHE_DIR", "data/models")
|
||||||
@@ -73,8 +73,8 @@ class Settings:
|
|||||||
default_factory=lambda: int(os.getenv("RAG_AUTOSAVE_INTERVAL", "600")) # 10 минут
|
default_factory=lambda: int(os.getenv("RAG_AUTOSAVE_INTERVAL", "600")) # 10 минут
|
||||||
)
|
)
|
||||||
|
|
||||||
# Размерность векторов (768 для ruBERT)
|
# Размерность векторов (384 для all-MiniLM-L12-v2)
|
||||||
vector_dim: int = 768
|
vector_dim: int = 384
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def is_auth_required(self) -> bool:
|
def is_auth_required(self) -> bool:
|
||||||
|
|||||||
24
app/main.py
24
app/main.py
@@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
FastAPI приложение RAG сервиса.
|
FastAPI приложение Embedding сервиса.
|
||||||
|
|
||||||
Сервис для векторного скоринга текстов с использованием ruBERT.
|
Сервис для векторного скоринга текстов с использованием sentence-transformers.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
@@ -90,7 +90,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|||||||
global _autosave_task
|
global _autosave_task
|
||||||
|
|
||||||
setup_logging()
|
setup_logging()
|
||||||
logger.info(f"RAG Service v{__version__} запускается...")
|
logger.info(f"Embedding Service v{__version__} запускается...")
|
||||||
|
|
||||||
settings = get_settings()
|
settings = get_settings()
|
||||||
logger.info(f"Настройки: model={settings.model_name}, vectors_path={settings.vectors_path}")
|
logger.info(f"Настройки: model={settings.model_name}, vectors_path={settings.vectors_path}")
|
||||||
@@ -112,7 +112,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|||||||
# logger.info("Прогрев модели при запуске...")
|
# logger.info("Прогрев модели при запуске...")
|
||||||
# await service.warmup()
|
# await service.warmup()
|
||||||
|
|
||||||
logger.info("RAG Service готов к работе")
|
logger.info("Embedding Service готов к работе")
|
||||||
|
|
||||||
yield
|
yield
|
||||||
|
|
||||||
@@ -125,21 +125,21 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
# При остановке сохраняем векторы
|
# При остановке сохраняем векторы
|
||||||
logger.info("RAG Service останавливается, финальное сохранение векторов...")
|
logger.info("Embedding Service останавливается, финальное сохранение векторов...")
|
||||||
try:
|
try:
|
||||||
service.save_vectors()
|
service.save_vectors()
|
||||||
logger.info("Векторы сохранены")
|
logger.info("Векторы сохранены")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Ошибка сохранения векторов: {e}")
|
logger.error(f"Ошибка сохранения векторов: {e}")
|
||||||
|
|
||||||
logger.info("RAG Service остановлен")
|
logger.info("Embedding Service остановлен")
|
||||||
|
|
||||||
|
|
||||||
# Создание приложения
|
# Создание приложения
|
||||||
app = FastAPI(
|
app = FastAPI(
|
||||||
title="RAG Service",
|
title="Embedding Service",
|
||||||
description="""
|
description="""
|
||||||
Сервис векторного скоринга текстов с использованием ruBERT.
|
Сервис векторного скоринга текстов с использованием sentence-transformers.
|
||||||
|
|
||||||
## Возможности
|
## Возможности
|
||||||
|
|
||||||
@@ -150,10 +150,10 @@ app = FastAPI(
|
|||||||
|
|
||||||
## Алгоритм скоринга
|
## Алгоритм скоринга
|
||||||
|
|
||||||
1. Текст преобразуется в вектор через ruBERT (768 измерений)
|
1. Текст преобразуется в вектор через sentence-transformers/all-MiniLM-L12-v2 (384 измерения)
|
||||||
2. Вычисляется косинусное сходство с положительными примерами
|
2. Вычисляется косинусное сходство с положительными примерами (топ-k ближайших)
|
||||||
3. Вычисляется косинусное сходство с отрицательными примерами
|
3. Вычисляется косинусное сходство с отрицательными примерами (топ-k ближайших)
|
||||||
4. Финальный скор = разница между сходствами, нормализованная в [0, 1]
|
4. Финальный скор = (diff * multiplier + 1) / 2, где diff = avg_pos - avg_neg, нормализованный в [0, 1]
|
||||||
""",
|
""",
|
||||||
version=__version__,
|
version=__version__,
|
||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
Pydantic схемы для API RAG сервиса.
|
Pydantic схемы для API Embedding сервиса.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
@@ -65,7 +65,7 @@ class ScoreResponse(BaseModel):
|
|||||||
"meta": {
|
"meta": {
|
||||||
"positive_examples": 500,
|
"positive_examples": 500,
|
||||||
"negative_examples": 350,
|
"negative_examples": 350,
|
||||||
"model": "DeepPavlov/rubert-base-cased",
|
"model": "sentence-transformers/all-MiniLM-L12-v2",
|
||||||
"timestamp": 1706270000
|
"timestamp": 1706270000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,14 +111,14 @@ class StatsResponse(BaseModel):
|
|||||||
model_config = {
|
model_config = {
|
||||||
"json_schema_extra": {
|
"json_schema_extra": {
|
||||||
"example": {
|
"example": {
|
||||||
"model_name": "DeepPavlov/rubert-base-cased",
|
"model_name": "sentence-transformers/all-MiniLM-L12-v2",
|
||||||
"model_loaded": True,
|
"model_loaded": True,
|
||||||
"device": "cpu",
|
"device": "cpu",
|
||||||
"vector_store": {
|
"vector_store": {
|
||||||
"positive_count": 500,
|
"positive_count": 500,
|
||||||
"negative_count": 350,
|
"negative_count": 350,
|
||||||
"total_count": 850,
|
"total_count": 850,
|
||||||
"vector_dim": 768,
|
"vector_dim": 384,
|
||||||
"max_examples": 10000
|
"max_examples": 10000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,39 +180,21 @@ class ScoringParamsResponse(BaseModel):
|
|||||||
score_multiplier: float = Field(
|
score_multiplier: float = Field(
|
||||||
...,
|
...,
|
||||||
description=(
|
description=(
|
||||||
"Базовый множитель для усиления разницы в скорах. "
|
"Множитель для масштабирования разницы в скорах. "
|
||||||
"Используется как основа для расчета финального множителя. "
|
"Используется в формуле: score = (diff * score_multiplier + 1) / 2, "
|
||||||
|
"где diff = avg_pos - avg_neg (разница средних сходств топ-k примеров). "
|
||||||
"Чем больше значение, тем сильнее влияние разницы между положительными и отрицательными примерами на итоговый score. "
|
"Чем больше значение, тем сильнее влияние разницы между положительными и отрицательными примерами на итоговый score. "
|
||||||
"Рекомендуемое значение: 5.0"
|
"Рекомендуемое значение: 5.0"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
k_min: int = Field(
|
k: int = Field(
|
||||||
...,
|
...,
|
||||||
description=(
|
description=(
|
||||||
"Минимальное количество ближайших примеров для расчета среднего сходства. "
|
"Количество ближайших примеров для расчета среднего сходства. "
|
||||||
"Алгоритм берет топ-k самых похожих примеров из каждого типа (положительные/отрицательные) "
|
"Алгоритм берет топ-k самых похожих примеров из каждого типа (положительные/отрицательные) "
|
||||||
"и вычисляет среднее косинусное сходство. "
|
"и вычисляет среднее косинусное сходство. "
|
||||||
"Меньшее значение k делает алгоритм более чувствительным к различиям, но может быть менее стабильным. "
|
"Меньшее значение k делает алгоритм более чувствительным к различиям, но может быть менее стабильным. "
|
||||||
"Рекомендуемое значение: 5"
|
"Рекомендуемое значение: 3"
|
||||||
)
|
|
||||||
)
|
|
||||||
k_max: int = Field(
|
|
||||||
...,
|
|
||||||
description=(
|
|
||||||
"Максимальное количество ближайших примеров для расчета среднего сходства. "
|
|
||||||
"Алгоритм выбирает k в диапазоне [k_min, k_max] в зависимости от количества доступных примеров. "
|
|
||||||
"Большее значение k делает алгоритм более стабильным, но менее чувствительным к различиям. "
|
|
||||||
"Должно быть >= k_min. Рекомендуемое значение: 10"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
base_multiplier_factor: float = Field(
|
|
||||||
...,
|
|
||||||
description=(
|
|
||||||
"Множитель для базового score_multiplier. "
|
|
||||||
"Финальный множитель рассчитывается как: score_multiplier * base_multiplier_factor * адаптивный_коэффициент. "
|
|
||||||
"Этот параметр усиливает влияние разницы между положительными и отрицательными примерами. "
|
|
||||||
"Чем больше значение, тем больше диапазон итогового score (от 0 до 1). "
|
|
||||||
"Рекомендуемое значение: 15.0"
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -220,9 +202,7 @@ class ScoringParamsResponse(BaseModel):
|
|||||||
"json_schema_extra": {
|
"json_schema_extra": {
|
||||||
"example": {
|
"example": {
|
||||||
"score_multiplier": 5.0,
|
"score_multiplier": 5.0,
|
||||||
"k_min": 5,
|
"k": 3
|
||||||
"k_max": 10,
|
|
||||||
"base_multiplier_factor": 15.0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -234,42 +214,22 @@ class UpdateScoringParamsRequest(BaseModel):
|
|||||||
None,
|
None,
|
||||||
gt=0,
|
gt=0,
|
||||||
description=(
|
description=(
|
||||||
"Базовый множитель для усиления разницы в скорах. "
|
"Множитель для масштабирования разницы в скорах. "
|
||||||
"Используется как основа для расчета финального множителя. "
|
"Используется в формуле: score = (diff * score_multiplier + 1) / 2, "
|
||||||
|
"где diff = avg_pos - avg_neg (разница средних сходств топ-k примеров). "
|
||||||
"Чем больше значение, тем сильнее влияние разницы между положительными и отрицательными примерами на итоговый score. "
|
"Чем больше значение, тем сильнее влияние разницы между положительными и отрицательными примерами на итоговый score. "
|
||||||
"Должен быть > 0. Рекомендуемое значение: 5.0"
|
"Должен быть > 0. Рекомендуемое значение: 5.0"
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
k_min: Optional[int] = Field(
|
k: Optional[int] = Field(
|
||||||
None,
|
None,
|
||||||
ge=1,
|
ge=1,
|
||||||
description=(
|
description=(
|
||||||
"Минимальное количество ближайших примеров для расчета среднего сходства. "
|
"Количество ближайших примеров для расчета среднего сходства. "
|
||||||
"Алгоритм берет топ-k самых похожих примеров из каждого типа (положительные/отрицательные) "
|
"Алгоритм берет топ-k самых похожих примеров из каждого типа (положительные/отрицательные) "
|
||||||
"и вычисляет среднее косинусное сходство. "
|
"и вычисляет среднее косинусное сходство. "
|
||||||
"Меньшее значение k делает алгоритм более чувствительным к различиям, но может быть менее стабильным. "
|
"Меньшее значение k делает алгоритм более чувствительным к различиям, но может быть менее стабильным. "
|
||||||
"Должно быть >= 1. Рекомендуемое значение: 5"
|
"Должно быть >= 1. Рекомендуемое значение: 3"
|
||||||
)
|
|
||||||
)
|
|
||||||
k_max: Optional[int] = Field(
|
|
||||||
None,
|
|
||||||
ge=1,
|
|
||||||
description=(
|
|
||||||
"Максимальное количество ближайших примеров для расчета среднего сходства. "
|
|
||||||
"Алгоритм выбирает k в диапазоне [k_min, k_max] в зависимости от количества доступных примеров. "
|
|
||||||
"Большее значение k делает алгоритм более стабильным, но менее чувствительным к различиям. "
|
|
||||||
"Должно быть >= 1 и >= k_min. Рекомендуемое значение: 10"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
base_multiplier_factor: Optional[float] = Field(
|
|
||||||
None,
|
|
||||||
gt=0,
|
|
||||||
description=(
|
|
||||||
"Множитель для базового score_multiplier. "
|
|
||||||
"Финальный множитель рассчитывается как: score_multiplier * base_multiplier_factor * адаптивный_коэффициент. "
|
|
||||||
"Этот параметр усиливает влияние разницы между положительными и отрицательными примерами. "
|
|
||||||
"Чем больше значение, тем больше диапазон итогового score (от 0 до 1). "
|
|
||||||
"Должен быть > 0. Рекомендуемое значение: 15.0"
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -277,9 +237,7 @@ class UpdateScoringParamsRequest(BaseModel):
|
|||||||
"json_schema_extra": {
|
"json_schema_extra": {
|
||||||
"example": {
|
"example": {
|
||||||
"score_multiplier": 5.0,
|
"score_multiplier": 5.0,
|
||||||
"k_min": 5,
|
"k": 3
|
||||||
"k_max": 10,
|
|
||||||
"base_multiplier_factor": 15.0
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"""
|
"""
|
||||||
RAG сервис для скоринга постов с использованием ruBERT.
|
RAG сервис для скоринга постов с использованием sentence-transformers.
|
||||||
|
|
||||||
Использует модель DeepPavlov/rubert-base-cased для создания эмбеддингов
|
Использует модель sentence-transformers/all-MiniLM-L12-v2 для создания эмбеддингов
|
||||||
и сравнивает их с эталонными примерами через VectorStore.
|
и сравнивает их с эталонными примерами через VectorStore.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@@ -66,7 +66,7 @@ class RAGService:
|
|||||||
"""
|
"""
|
||||||
RAG сервис для оценки постов на основе векторного сходства.
|
RAG сервис для оценки постов на основе векторного сходства.
|
||||||
|
|
||||||
Использует ruBERT для создания эмбеддингов текста и сравнивает
|
Использует sentence-transformers для создания эмбеддингов текста и сравнивает
|
||||||
их с эталонными примерами (опубликованные vs отклоненные посты).
|
их с эталонными примерами (опубликованные vs отклоненные посты).
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
@@ -92,9 +92,8 @@ class RAGService:
|
|||||||
self.cache_dir = self._settings.cache_dir
|
self.cache_dir = self._settings.cache_dir
|
||||||
self.min_text_length = self._settings.min_text_length
|
self.min_text_length = self._settings.min_text_length
|
||||||
|
|
||||||
# Модель и токенизатор загружаются лениво
|
# Модель загружается лениво
|
||||||
self._model = None
|
self._model = None
|
||||||
self._tokenizer = None
|
|
||||||
self._device = None
|
self._device = None
|
||||||
self._model_loaded = False
|
self._model_loaded = False
|
||||||
|
|
||||||
@@ -104,6 +103,7 @@ class RAGService:
|
|||||||
max_examples=self._settings.max_examples,
|
max_examples=self._settings.max_examples,
|
||||||
storage_path=self._settings.vectors_path,
|
storage_path=self._settings.vectors_path,
|
||||||
score_multiplier=self._settings.score_multiplier,
|
score_multiplier=self._settings.score_multiplier,
|
||||||
|
k=3, # Фиксированное значение k для топ-k ближайших примеров
|
||||||
)
|
)
|
||||||
|
|
||||||
logger.info(f"RAGService инициализирован (model={self.model_name})")
|
logger.info(f"RAGService инициализирован (model={self.model_name})")
|
||||||
@@ -138,64 +138,37 @@ class RAGService:
|
|||||||
|
|
||||||
def _load_model_sync(self) -> None:
|
def _load_model_sync(self) -> None:
|
||||||
"""Синхронная загрузка модели (вызывается в executor)."""
|
"""Синхронная загрузка модели (вызывается в executor)."""
|
||||||
logger.info("RAGService: Начало _load_model_sync, импорт transformers...")
|
logger.info("RAGService: Начало _load_model_sync, импорт sentence_transformers...")
|
||||||
from transformers import AutoModel, AutoTokenizer
|
from sentence_transformers import SentenceTransformer
|
||||||
import torch
|
import torch
|
||||||
|
|
||||||
# Определяем устройство
|
# Определяем устройство
|
||||||
self._device = "cuda" if torch.cuda.is_available() else "cpu"
|
self._device = "cuda" if torch.cuda.is_available() else "cpu"
|
||||||
logger.info(f"RAGService: Устройство определено: {self._device}")
|
logger.info(f"RAGService: Устройство определено: {self._device}")
|
||||||
|
|
||||||
# Загружаем токенизатор
|
# Загружаем модель SentenceTransformer
|
||||||
logger.info(f"RAGService: Загрузка токенизатора из {self.model_name}...")
|
|
||||||
self._tokenizer = AutoTokenizer.from_pretrained(
|
|
||||||
self.model_name,
|
|
||||||
cache_dir=self.cache_dir,
|
|
||||||
)
|
|
||||||
logger.info("RAGService: Токенизатор загружен")
|
|
||||||
|
|
||||||
# Загружаем модель
|
|
||||||
logger.info(f"RAGService: Загрузка модели из {self.model_name} (это может занять несколько минут)...")
|
logger.info(f"RAGService: Загрузка модели из {self.model_name} (это может занять несколько минут)...")
|
||||||
self._model = AutoModel.from_pretrained(
|
self._model = SentenceTransformer(
|
||||||
self.model_name,
|
self.model_name,
|
||||||
cache_dir=self.cache_dir,
|
cache_folder=self.cache_dir,
|
||||||
|
device=self._device,
|
||||||
)
|
)
|
||||||
logger.info("RAGService: Модель загружена, перенос на устройство...")
|
|
||||||
self._model.to(self._device)
|
|
||||||
self._model.eval() # Режим инференса
|
|
||||||
|
|
||||||
logger.info(f"RAGService: Модель готова на устройстве: {self._device}")
|
logger.info(f"RAGService: Модель готова на устройстве: {self._device}")
|
||||||
|
|
||||||
def _get_embedding_sync(self, text: str) -> np.ndarray:
|
def _get_embedding_sync(self, text: str) -> np.ndarray:
|
||||||
"""
|
"""
|
||||||
Получает эмбеддинг текста (синхронно).
|
Получает эмбеддинг текста (синхронно).
|
||||||
|
|
||||||
Использует [CLS] токен как представление всего текста.
|
Использует SentenceTransformer для получения нормализованного эмбеддинга.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
text: Текст для векторизации
|
text: Текст для векторизации
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
Numpy массив с эмбеддингом (768 измерений для ruBERT)
|
Numpy массив с эмбеддингом (384 измерений для all-MiniLM-L12-v2)
|
||||||
"""
|
"""
|
||||||
import torch
|
# SentenceTransformer автоматически нормализует эмбеддинги
|
||||||
|
embedding = self._model.encode(text, convert_to_numpy=True, normalize_embeddings=True)
|
||||||
# Токенизация с ограничением длины
|
|
||||||
inputs = self._tokenizer(
|
|
||||||
text,
|
|
||||||
return_tensors="pt",
|
|
||||||
truncation=True,
|
|
||||||
max_length=512,
|
|
||||||
padding=True,
|
|
||||||
)
|
|
||||||
inputs = {k: v.to(self._device) for k, v in inputs.items()}
|
|
||||||
|
|
||||||
# Получаем эмбеддинг
|
|
||||||
with torch.no_grad():
|
|
||||||
outputs = self._model(**inputs)
|
|
||||||
# Используем [CLS] токен (первый токен)
|
|
||||||
embedding = outputs.last_hidden_state[:, 0, :].cpu().numpy()
|
|
||||||
|
|
||||||
return embedding.flatten()
|
return embedding.flatten()
|
||||||
|
|
||||||
def _get_embeddings_batch_sync(self, texts: List[str], batch_size: int = 16) -> List[np.ndarray]:
|
def _get_embeddings_batch_sync(self, texts: List[str], batch_size: int = 16) -> List[np.ndarray]:
|
||||||
@@ -211,37 +184,17 @@ class RAGService:
|
|||||||
Returns:
|
Returns:
|
||||||
Список numpy массивов с эмбеддингами
|
Список numpy массивов с эмбеддингами
|
||||||
"""
|
"""
|
||||||
import torch
|
# SentenceTransformer автоматически обрабатывает батчи
|
||||||
|
embeddings = self._model.encode(
|
||||||
|
texts,
|
||||||
|
batch_size=batch_size,
|
||||||
|
convert_to_numpy=True,
|
||||||
|
normalize_embeddings=True,
|
||||||
|
show_progress_bar=False,
|
||||||
|
)
|
||||||
|
|
||||||
all_embeddings = []
|
# Преобразуем в список отдельных массивов
|
||||||
|
return [emb.flatten() for emb in embeddings]
|
||||||
for i in range(0, len(texts), batch_size):
|
|
||||||
batch_texts = texts[i:i + batch_size]
|
|
||||||
|
|
||||||
# Токенизация батча
|
|
||||||
inputs = self._tokenizer(
|
|
||||||
batch_texts,
|
|
||||||
return_tensors="pt",
|
|
||||||
truncation=True,
|
|
||||||
max_length=512,
|
|
||||||
padding=True,
|
|
||||||
)
|
|
||||||
inputs = {k: v.to(self._device) for k, v in inputs.items()}
|
|
||||||
|
|
||||||
# Получаем эмбеддинги
|
|
||||||
with torch.no_grad():
|
|
||||||
outputs = self._model(**inputs)
|
|
||||||
# [CLS] токен для каждого текста в батче
|
|
||||||
batch_embeddings = outputs.last_hidden_state[:, 0, :].cpu().numpy()
|
|
||||||
|
|
||||||
# Разбиваем на отдельные эмбеддинги
|
|
||||||
for j in range(len(batch_texts)):
|
|
||||||
all_embeddings.append(batch_embeddings[j])
|
|
||||||
|
|
||||||
if i > 0 and i % (batch_size * 10) == 0:
|
|
||||||
logger.info(f"RAGService: Обработано {i}/{len(texts)} текстов")
|
|
||||||
|
|
||||||
return all_embeddings
|
|
||||||
|
|
||||||
async def get_embeddings_batch(self, texts: List[str], batch_size: Optional[int] = None) -> List[np.ndarray]:
|
async def get_embeddings_batch(self, texts: List[str], batch_size: Optional[int] = None) -> List[np.ndarray]:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -27,19 +27,17 @@ class VectorStore:
|
|||||||
примеры. Использует косинусное сходство для расчета скора.
|
примеры. Использует косинусное сходство для расчета скора.
|
||||||
|
|
||||||
Attributes:
|
Attributes:
|
||||||
vector_dim: Размерность векторов (768 для ruBERT)
|
vector_dim: Размерность векторов (384 для all-MiniLM-L12-v2)
|
||||||
max_examples: Максимальное количество примеров каждого типа
|
max_examples: Максимальное количество примеров каждого типа
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
vector_dim: int = 768,
|
vector_dim: int = 384,
|
||||||
max_examples: int = 10000,
|
max_examples: int = 10000,
|
||||||
storage_path: Optional[str] = None,
|
storage_path: Optional[str] = None,
|
||||||
score_multiplier: float = 5.0,
|
score_multiplier: float = 5.0,
|
||||||
k_min: int = 5,
|
k: int = 3,
|
||||||
k_max: int = 10,
|
|
||||||
base_multiplier_factor: float = 15.0,
|
|
||||||
):
|
):
|
||||||
"""
|
"""
|
||||||
Инициализация хранилища.
|
Инициализация хранилища.
|
||||||
@@ -48,18 +46,14 @@ class VectorStore:
|
|||||||
vector_dim: Размерность векторов
|
vector_dim: Размерность векторов
|
||||||
max_examples: Максимальное количество примеров каждого типа
|
max_examples: Максимальное количество примеров каждого типа
|
||||||
storage_path: Путь для сохранения/загрузки векторов (опционально)
|
storage_path: Путь для сохранения/загрузки векторов (опционально)
|
||||||
score_multiplier: Базовый множитель для усиления разницы в скорах
|
score_multiplier: Множитель для масштабирования разницы в скорах
|
||||||
k_min: Минимальное значение k для топ-k ближайших примеров
|
k: Количество ближайших примеров для расчета среднего сходства
|
||||||
k_max: Максимальное значение k для топ-k ближайших примеров
|
|
||||||
base_multiplier_factor: Множитель для базового score_multiplier
|
|
||||||
"""
|
"""
|
||||||
self.vector_dim = vector_dim
|
self.vector_dim = vector_dim
|
||||||
self.max_examples = max_examples
|
self.max_examples = max_examples
|
||||||
self.storage_path = storage_path
|
self.storage_path = storage_path
|
||||||
self.score_multiplier = score_multiplier
|
self.score_multiplier = score_multiplier
|
||||||
self.k_min = k_min
|
self.k = k
|
||||||
self.k_max = k_max
|
|
||||||
self.base_multiplier_factor = base_multiplier_factor
|
|
||||||
|
|
||||||
# Инициализируем пустые массивы
|
# Инициализируем пустые массивы
|
||||||
# Используем список для динамического добавления, потом конвертируем в numpy
|
# Используем список для динамического добавления, потом конвертируем в numpy
|
||||||
@@ -72,8 +66,15 @@ class VectorStore:
|
|||||||
self._lock = threading.Lock()
|
self._lock = threading.Lock()
|
||||||
|
|
||||||
# Пытаемся загрузить сохраненные векторы
|
# Пытаемся загрузить сохраненные векторы
|
||||||
if storage_path and os.path.exists(storage_path):
|
# Проверяем наличие storage_path или отдельных .npy файлов
|
||||||
self._load_from_disk()
|
if storage_path:
|
||||||
|
storage_dir = Path(storage_path).parent
|
||||||
|
positive_npy = storage_dir / "positive_embeddings.npy"
|
||||||
|
negative_npy = storage_dir / "negative_embeddings.npy"
|
||||||
|
|
||||||
|
# Загружаем если есть .npz файл или отдельные .npy файлы
|
||||||
|
if os.path.exists(storage_path) or positive_npy.exists() or negative_npy.exists():
|
||||||
|
self._load_from_disk()
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def positive_count(self) -> int:
|
def positive_count(self) -> int:
|
||||||
@@ -292,35 +293,23 @@ class VectorStore:
|
|||||||
else:
|
else:
|
||||||
neg_similarities = np.array([])
|
neg_similarities = np.array([])
|
||||||
|
|
||||||
# Используем топ-k ближайших примеров для более чувствительной оценки
|
# Используем топ-k ближайших примеров для расчета среднего сходства
|
||||||
# Берем k в диапазоне [k_min, k_max] для большей чувствительности к различиям
|
k_pos = min(self.k, len(pos_similarities))
|
||||||
k_pos = min(self.k_max, max(self.k_min, len(pos_similarities)))
|
top_k_pos = np.sort(pos_similarities)[-k_pos:]
|
||||||
|
avg_pos = float(np.mean(top_k_pos))
|
||||||
# Топ-k положительных примеров (самые близкие)
|
|
||||||
top_k_pos_sim = float(np.mean(np.sort(pos_similarities)[-k_pos:]))
|
|
||||||
|
|
||||||
# Для отрицательных: если их меньше k, берем все, иначе топ-k
|
# Для отрицательных: если их меньше k, берем все, иначе топ-k
|
||||||
if len(neg_similarities) > 0:
|
if len(neg_similarities) > 0:
|
||||||
k_neg = min(self.k_max, max(self.k_min, len(neg_similarities)))
|
k_neg = min(self.k, len(neg_similarities))
|
||||||
top_k_neg_sim = float(np.mean(np.sort(neg_similarities)[-k_neg:]))
|
top_k_neg = np.sort(neg_similarities)[-k_neg:]
|
||||||
|
avg_neg = float(np.mean(top_k_neg))
|
||||||
else:
|
else:
|
||||||
# Если нет отрицательных примеров, используем нейтральное значение
|
# Если нет отрицательных примеров, используем нейтральное значение
|
||||||
top_k_neg_sim = top_k_pos_sim # Нейтральный скор = 0.5
|
avg_neg = avg_pos # Нейтральный скор = 0.5
|
||||||
|
|
||||||
# === Вариант 1: neg/pos (разница между топ-k положительными и отрицательными) ===
|
# Формула расчета score: (diff * scale + 1) / 2, переводим из [-1, 1] в [0, 1]
|
||||||
# Используем более агрессивную нормализацию для малых различий
|
diff = avg_pos - avg_neg
|
||||||
diff = top_k_pos_sim - top_k_neg_sim
|
score_neg_pos = np.clip((diff * self.score_multiplier + 1) / 2, 0.0, 1.0)
|
||||||
|
|
||||||
# Увеличиваем множитель для большей чувствительности к малым различиям
|
|
||||||
# Базовый множитель умножаем на base_multiplier_factor для работы с топ-k
|
|
||||||
base_multiplier = self.score_multiplier * self.base_multiplier_factor
|
|
||||||
|
|
||||||
# Адаптивный множитель: чем больше примеров, тем выше чувствительность
|
|
||||||
# При 500 примерах: 1.25, при 1000+: 1.5
|
|
||||||
adaptive_multiplier = base_multiplier * (1.0 + min(0.5, (self.positive_count + self.negative_count) / 2000))
|
|
||||||
|
|
||||||
score_neg_pos = 0.5 + (diff * adaptive_multiplier)
|
|
||||||
score_neg_pos = max(0.0, min(1.0, score_neg_pos))
|
|
||||||
|
|
||||||
# === Вариант 2: pos only (только положительные, топ-k ближайших) ===
|
# === Вариант 2: pos only (только положительные, топ-k ближайших) ===
|
||||||
# Берём топ-5 ближайших положительных примеров
|
# Берём топ-5 ближайших положительных примеров
|
||||||
@@ -352,9 +341,9 @@ class VectorStore:
|
|||||||
neg_mean = neg_std = neg_min = neg_max = 0.0
|
neg_mean = neg_std = neg_min = neg_max = 0.0
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"VectorStore: top_k_pos={k_pos}, top_k_neg={k_neg if len(neg_similarities) > 0 else 0}, "
|
f"VectorStore: k={self.k}, k_pos={k_pos}, k_neg={k_neg if len(neg_similarities) > 0 else 0}, "
|
||||||
f"top_k_pos_sim={top_k_pos_sim:.4f}, top_k_neg_sim={top_k_neg_sim:.4f}, "
|
f"avg_pos={avg_pos:.4f}, avg_neg={avg_neg:.4f}, "
|
||||||
f"diff={diff:.4f}, adaptive_mult={adaptive_multiplier:.2f}, "
|
f"diff={diff:.4f}, score_multiplier={self.score_multiplier}, "
|
||||||
f"score_neg_pos={score_neg_pos:.4f}, score_pos_only={score_pos_only:.4f}, "
|
f"score_neg_pos={score_neg_pos:.4f}, score_pos_only={score_pos_only:.4f}, "
|
||||||
f"pos_mean={pos_mean:.4f}±{pos_std:.4f}[{pos_min:.4f}-{pos_max:.4f}], "
|
f"pos_mean={pos_mean:.4f}±{pos_std:.4f}[{pos_min:.4f}-{pos_max:.4f}], "
|
||||||
f"neg_mean={neg_mean:.4f}±{neg_std:.4f}[{neg_min:.4f}-{neg_max:.4f}]"
|
f"neg_mean={neg_mean:.4f}±{neg_std:.4f}[{neg_min:.4f}-{neg_max:.4f}]"
|
||||||
@@ -395,35 +384,88 @@ class VectorStore:
|
|||||||
|
|
||||||
def _load_from_disk(self) -> None:
|
def _load_from_disk(self) -> None:
|
||||||
"""Загружает векторы с диска."""
|
"""Загружает векторы с диска."""
|
||||||
if not self.storage_path or not os.path.exists(self.storage_path):
|
if not self.storage_path:
|
||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with self._lock:
|
with self._lock:
|
||||||
data = np.load(self.storage_path, allow_pickle=True)
|
storage_dir = Path(self.storage_path).parent
|
||||||
|
positive_npy = storage_dir / "positive_embeddings.npy"
|
||||||
|
negative_npy = storage_dir / "negative_embeddings.npy"
|
||||||
|
|
||||||
# Загружаем векторы
|
# Проверяем наличие отдельных .npy файлов
|
||||||
pos_vectors = data.get('positive_vectors', np.array([]))
|
if positive_npy.exists() or negative_npy.exists():
|
||||||
neg_vectors = data.get('negative_vectors', np.array([]))
|
logger.info("VectorStore: Обнаружены отдельные .npy файлы, загружаем их...")
|
||||||
|
|
||||||
|
# Загружаем положительные векторы
|
||||||
|
if positive_npy.exists():
|
||||||
|
pos_vectors = np.load(positive_npy, allow_pickle=False)
|
||||||
|
if pos_vectors.size > 0:
|
||||||
|
# Проверяем размерность
|
||||||
|
if len(pos_vectors.shape) == 2:
|
||||||
|
# Массив векторов [N, dim]
|
||||||
|
self._positive_vectors = [vec for vec in pos_vectors]
|
||||||
|
elif len(pos_vectors.shape) == 1:
|
||||||
|
# Один вектор [dim]
|
||||||
|
self._positive_vectors = [pos_vectors]
|
||||||
|
else:
|
||||||
|
logger.warning(f"VectorStore: Неожиданная размерность positive_embeddings.npy: {pos_vectors.shape}")
|
||||||
|
self._positive_vectors = []
|
||||||
|
logger.info(f"VectorStore: Загружено {len(self._positive_vectors)} положительных векторов из {positive_npy}")
|
||||||
|
|
||||||
|
# Загружаем отрицательные векторы
|
||||||
|
if negative_npy.exists():
|
||||||
|
neg_vectors = np.load(negative_npy, allow_pickle=False)
|
||||||
|
if neg_vectors.size > 0:
|
||||||
|
# Проверяем размерность
|
||||||
|
if len(neg_vectors.shape) == 2:
|
||||||
|
# Массив векторов [N, dim]
|
||||||
|
self._negative_vectors = [vec for vec in neg_vectors]
|
||||||
|
elif len(neg_vectors.shape) == 1:
|
||||||
|
# Один вектор [dim]
|
||||||
|
self._negative_vectors = [neg_vectors]
|
||||||
|
else:
|
||||||
|
logger.warning(f"VectorStore: Неожиданная размерность negative_embeddings.npy: {neg_vectors.shape}")
|
||||||
|
self._negative_vectors = []
|
||||||
|
logger.info(f"VectorStore: Загружено {len(self._negative_vectors)} отрицательных векторов из {negative_npy}")
|
||||||
|
|
||||||
|
# Нормализуем загруженные векторы
|
||||||
|
self._positive_vectors = [self._normalize_vector(np.array(v)) for v in self._positive_vectors]
|
||||||
|
self._negative_vectors = [self._normalize_vector(np.array(v)) for v in self._negative_vectors]
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"VectorStore: Загружено с диска из .npy файлов ({self.positive_count} pos, "
|
||||||
|
f"{self.negative_count} neg)"
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
if pos_vectors.size > 0:
|
# Если отдельных .npy файлов нет, пытаемся загрузить из старого формата .npz
|
||||||
self._positive_vectors = list(pos_vectors)
|
if os.path.exists(self.storage_path):
|
||||||
if neg_vectors.size > 0:
|
logger.info(f"VectorStore: Загружаем из старого формата .npz: {self.storage_path}")
|
||||||
self._negative_vectors = list(neg_vectors)
|
data = np.load(self.storage_path, allow_pickle=True)
|
||||||
|
|
||||||
# Загружаем хеши
|
# Загружаем векторы
|
||||||
pos_hashes = data.get('positive_hashes', np.array([]))
|
pos_vectors = data.get('positive_vectors', np.array([]))
|
||||||
neg_hashes = data.get('negative_hashes', np.array([]))
|
neg_vectors = data.get('negative_vectors', np.array([]))
|
||||||
|
|
||||||
if pos_hashes.size > 0:
|
if pos_vectors.size > 0:
|
||||||
self._positive_hashes = list(pos_hashes)
|
self._positive_vectors = list(pos_vectors)
|
||||||
if neg_hashes.size > 0:
|
if neg_vectors.size > 0:
|
||||||
self._negative_hashes = list(neg_hashes)
|
self._negative_vectors = list(neg_vectors)
|
||||||
|
|
||||||
logger.info(
|
# Загружаем хеши
|
||||||
f"VectorStore: Загружено с диска ({self.positive_count} pos, "
|
pos_hashes = data.get('positive_hashes', np.array([]))
|
||||||
f"{self.negative_count} neg): {self.storage_path}"
|
neg_hashes = data.get('negative_hashes', np.array([]))
|
||||||
)
|
|
||||||
|
if pos_hashes.size > 0:
|
||||||
|
self._positive_hashes = list(pos_hashes)
|
||||||
|
if neg_hashes.size > 0:
|
||||||
|
self._negative_hashes = list(neg_hashes)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"VectorStore: Загружено с диска ({self.positive_count} pos, "
|
||||||
|
f"{self.negative_count} neg): {self.storage_path}"
|
||||||
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"VectorStore: Ошибка загрузки с диска: {e}")
|
logger.error(f"VectorStore: Ошибка загрузки с диска: {e}")
|
||||||
@@ -452,26 +494,20 @@ class VectorStore:
|
|||||||
"""Возвращает текущие параметры формулы расчета score."""
|
"""Возвращает текущие параметры формулы расчета score."""
|
||||||
return {
|
return {
|
||||||
"score_multiplier": self.score_multiplier,
|
"score_multiplier": self.score_multiplier,
|
||||||
"k_min": self.k_min,
|
"k": self.k,
|
||||||
"k_max": self.k_max,
|
|
||||||
"base_multiplier_factor": self.base_multiplier_factor,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def update_scoring_params(
|
def update_scoring_params(
|
||||||
self,
|
self,
|
||||||
score_multiplier: Optional[float] = None,
|
score_multiplier: Optional[float] = None,
|
||||||
k_min: Optional[int] = None,
|
k: Optional[int] = None,
|
||||||
k_max: Optional[int] = None,
|
|
||||||
base_multiplier_factor: Optional[float] = None,
|
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""
|
"""
|
||||||
Обновляет параметры формулы расчета score.
|
Обновляет параметры формулы расчета score.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
score_multiplier: Базовый множитель (должен быть > 0)
|
score_multiplier: Множитель для масштабирования разницы (должен быть > 0)
|
||||||
k_min: Минимальное значение k (должно быть >= 1)
|
k: Количество ближайших примеров для расчета среднего (должно быть >= 1)
|
||||||
k_max: Максимальное значение k (должно быть >= k_min)
|
|
||||||
base_multiplier_factor: Множитель для базового score_multiplier (должен быть > 0)
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
dict: Обновленные параметры
|
dict: Обновленные параметры
|
||||||
@@ -485,30 +521,14 @@ class VectorStore:
|
|||||||
raise ValueError("score_multiplier должен быть > 0")
|
raise ValueError("score_multiplier должен быть > 0")
|
||||||
self.score_multiplier = score_multiplier
|
self.score_multiplier = score_multiplier
|
||||||
|
|
||||||
if k_min is not None:
|
if k is not None:
|
||||||
if k_min < 1:
|
if k < 1:
|
||||||
raise ValueError("k_min должен быть >= 1")
|
raise ValueError("k должен быть >= 1")
|
||||||
if self.k_max < k_min:
|
self.k = k
|
||||||
raise ValueError("k_min не может быть больше k_max")
|
|
||||||
self.k_min = k_min
|
|
||||||
|
|
||||||
if k_max is not None:
|
|
||||||
if k_max < 1:
|
|
||||||
raise ValueError("k_max должен быть >= 1")
|
|
||||||
if k_max < self.k_min:
|
|
||||||
raise ValueError("k_max не может быть меньше k_min")
|
|
||||||
self.k_max = k_max
|
|
||||||
|
|
||||||
if base_multiplier_factor is not None:
|
|
||||||
if base_multiplier_factor <= 0:
|
|
||||||
raise ValueError("base_multiplier_factor должен быть > 0")
|
|
||||||
self.base_multiplier_factor = base_multiplier_factor
|
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
f"VectorStore: Параметры формулы обновлены: "
|
f"VectorStore: Параметры формулы обновлены: "
|
||||||
f"score_multiplier={self.score_multiplier}, "
|
f"score_multiplier={self.score_multiplier}, k={self.k}"
|
||||||
f"k_min={self.k_min}, k_max={self.k_max}, "
|
|
||||||
f"base_multiplier_factor={self.base_multiplier_factor}"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return self.get_scoring_params()
|
return self.get_scoring_params()
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ services:
|
|||||||
- ./data/models:/app/data/models
|
- ./data/models:/app/data/models
|
||||||
- ./data/vectors:/app/data/vectors
|
- ./data/vectors:/app/data/vectors
|
||||||
environment:
|
environment:
|
||||||
- RAG_MODEL=${RAG_MODEL:-DeepPavlov/rubert-base-cased}
|
- RAG_MODEL=${RAG_MODEL:-sentence-transformers/all-MiniLM-L12-v2}
|
||||||
- RAG_CACHE_DIR=/app/data/models
|
- RAG_CACHE_DIR=/app/data/models
|
||||||
- RAG_VECTORS_PATH=/app/data/vectors/vectors.npz
|
- RAG_VECTORS_PATH=/app/data/vectors/vectors.npz
|
||||||
- RAG_MAX_EXAMPLES=${RAG_MAX_EXAMPLES:-10000}
|
- RAG_MAX_EXAMPLES=${RAG_MAX_EXAMPLES:-10000}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# RAG Service Configuration
|
# RAG Service Configuration
|
||||||
|
|
||||||
# Модель
|
# Модель
|
||||||
RAG_MODEL=DeepPavlov/rubert-base-cased
|
RAG_MODEL=sentence-transformers/all-MiniLM-L12-v2
|
||||||
RAG_CACHE_DIR=data/models
|
RAG_CACHE_DIR=data/models
|
||||||
|
|
||||||
# VectorStore
|
# VectorStore
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "rag-service"
|
name = "rag-service"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "RAG Service - сервис векторного скоринга на FastAPI с ruBERT"
|
description = "Embedding Service - сервис векторного скоринга на FastAPI с sentence-transformers"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.11"
|
requires-python = ">=3.11"
|
||||||
license = {text = "MIT"}
|
license = {text = "MIT"}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ pydantic>=2.5.0
|
|||||||
# ML / NLP
|
# ML / NLP
|
||||||
torch>=2.1.0
|
torch>=2.1.0
|
||||||
transformers>=4.36.0
|
transformers>=4.36.0
|
||||||
|
sentence-transformers>=2.2.0
|
||||||
numpy>=1.24.0
|
numpy>=1.24.0
|
||||||
|
|
||||||
# Утилиты
|
# Утилиты
|
||||||
|
|||||||
Reference in New Issue
Block a user