Files
8.-samopisnyy-poiskovyy-age…/src/index.py
T
kuzakhmetovartur 04e3b78a9c
CI / build (3.1) (push) Has been cancelled
CI / build (3.11) (push) Has been cancelled
CI / build (3.8) (push) Has been cancelled
CI / build (3.9) (push) Has been cancelled
feat: solution for '8. Самописный поисковый агент на основе deep agents from scratch'
2026-06-30 16:40:46 +03:00

261 lines
9.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""
Simple Deep Search Agent
========================
This module implements a minimal deep learning based search agent.
It uses a TFIDF vectorizer to transform documents and queries into
feature vectors and a single linear layer (logistic regression)
to predict relevance scores. The agent can be trained on a small
synthetic dataset and used to retrieve the topk most relevant
documents for a given query.
Author: Artur Kuzakhmetov
Date: 30.06.2026
"""
import sys
from typing import List, Tuple
import numpy as np
import torch
import torch.nn as nn
import torch.optim as optim
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.model_selection import train_test_split
class SearchAgent(nn.Module):
"""
A simple search agent based on a linear classifier.
"""
def __init__(self, documents: List[str], device: torch.device = None):
"""
Parameters
----------
documents : List[str]
List of document texts.
device : torch.device, optional
Device to run the model on. Defaults to CUDA if available.
"""
super().__init__()
self.documents = documents
self.device = device or torch.device("cuda" if torch.cuda.is_available() else "cpu")
# Fit TFIDF vectorizer on documents
self.vectorizer = TfidfVectorizer()
self.doc_vectors = self.vectorizer.fit_transform(self.documents).toarray()
self.doc_vectors = torch.tensor(self.doc_vectors, dtype=torch.float32, device=self.device)
# Linear layer: input_dim -> 1 (relevance score)
self.linear = nn.Linear(self.doc_vectors.shape[1], 1).to(self.device)
def forward(self, query_vec: torch.Tensor) -> torch.Tensor:
"""
Forward pass: compute relevance scores for all documents given a query vector.
Parameters
----------
query_vec : torch.Tensor
Tensor of shape (1, feature_dim).
Returns
-------
torch.Tensor
Tensor of shape (num_documents,) with relevance scores.
"""
# Compute dot product between query and each document vector
scores = torch.matmul(self.doc_vectors, query_vec.t()).squeeze(1)
return scores
def train_agent(
self,
queries: List[str],
labels: List[List[int]],
epochs: int = 10,
lr: float = 0.01,
batch_size: int = 4,
verbose: bool = True,
) -> None:
"""
Train the agent on querydocument relevance pairs.
Parameters
----------
queries : List[str]
List of query texts.
labels : List[List[int]]
List of relevance labels for each query. Each inner list contains
indices of relevant documents (0based).
epochs : int, default 10
Number of training epochs.
lr : float, default 0.01
Learning rate.
batch_size : int, default 4
Batch size.
verbose : bool, default True
Whether to print training progress.
"""
# Vectorize queries
query_vectors = self.vectorizer.transform(queries).toarray()
query_vectors = torch.tensor(query_vectors, dtype=torch.float32, device=self.device)
# Prepare training data
# For each query, create a target vector of relevance scores (1 for relevant, 0 otherwise)
targets = []
for rel_indices in labels:
target = torch.zeros(self.doc_vectors.shape[0], device=self.device)
target[rel_indices] = 1.0
targets.append(target)
targets = torch.stack(targets) # shape: (num_queries, num_documents)
# Loss and optimizer
criterion = nn.BCEWithLogitsLoss()
optimizer = optim.Adam(self.parameters(), lr=lr)
dataset = torch.utils.data.TensorDataset(query_vectors, targets)
loader = torch.utils.data.DataLoader(dataset, batch_size=batch_size, shuffle=True)
self.train()
for epoch in range(1, epochs + 1):
epoch_loss = 0.0
for batch_q, batch_t in loader:
optimizer.zero_grad()
outputs = self.forward(batch_q) # shape: (batch_size, num_documents)
loss = criterion(outputs, batch_t)
loss.backward()
optimizer.step()
epoch_loss += loss.item() * batch_q.size(0)
epoch_loss /= len(dataset)
if verbose:
print(f"Epoch {epoch}/{epochs} - Loss: {epoch_loss:.4f}")
def evaluate(
self,
queries: List[str],
labels: List[List[int]],
threshold: float = 0.5,
) -> Tuple[float, float]:
"""
Evaluate the agent on a test set.
Parameters
----------
queries : List[str]
List of query texts.
labels : List[List[int]]
List of relevance labels for each query.
threshold : float, default 0.5
Threshold to convert scores to binary predictions.
Returns
-------
Tuple[float, float]
(precision, recall)
"""
self.eval()
with torch.no_grad():
query_vectors = self.vectorizer.transform(queries).toarray()
query_vectors = torch.tensor(query_vectors, dtype=torch.float32, device=self.device)
outputs = self.forward(query_vectors) # shape: (num_queries, num_documents)
preds = (outputs > threshold).int()
# Compute precision and recall
total_relevant = 0
total_predicted = 0
total_correct = 0
for i, rel_indices in enumerate(labels):
pred_indices = preds[i].nonzero(as_tuple=True)[0].cpu().numpy().tolist()
total_relevant += len(rel_indices)
total_predicted += len(pred_indices)
total_correct += len(set(pred_indices) & set(rel_indices))
precision = total_correct / total_predicted if total_predicted > 0 else 0.0
recall = total_correct / total_relevant if total_relevant > 0 else 0.0
return precision, recall
def search(self, query: str, top_k: int = 3) -> List[Tuple[str, float]]:
"""
Retrieve topk documents for a given query.
Parameters
----------
query : str
Query text.
top_k : int, default 3
Number of documents to return.
Returns
-------
List[Tuple[str, float]]
List of (document_text, score) tuples sorted by descending score.
"""
self.eval()
with torch.no_grad():
q_vec = self.vectorizer.transform([query]).toarray()
q_vec = torch.tensor(q_vec, dtype=torch.float32, device=self.device)
scores = self.forward(q_vec).cpu().numpy().flatten()
top_indices = np.argsort(scores)[::-1][:top_k]
return [(self.documents[i], float(scores[i])) for i in top_indices]
def demo():
"""
Demo usage of the SearchAgent with a tiny synthetic dataset.
"""
# Sample documents
docs = [
"Deep learning models can learn complex patterns from data.",
"Search engines index web pages to provide relevant results.",
"Natural language processing enables computers to understand text.",
"Python is a popular programming language for data science.",
"Machine learning algorithms improve over time with more data.",
"Artificial intelligence encompasses machine learning and deep learning.",
"Information retrieval is a key component of search systems.",
"Neural networks consist of layers of interconnected nodes.",
"Data preprocessing is essential before training models.",
"Evaluation metrics help assess model performance.",
]
# Sample queries and relevance labels (indices of relevant docs)
queries = [
"What is deep learning?",
"How do search engines work?",
"Explain natural language processing.",
"Why use Python for data science?",
"What is machine learning?",
]
relevance = [
[0, 5], # Relevant docs for query 0
[1, 6], # Relevant docs for query 1
[2, 7], # Relevant docs for query 2
[3, 9], # Relevant docs for query 3
[4, 5], # Relevant docs for query 4
]
# Split into train/test
train_q, test_q, train_rel, test_rel = train_test_split(
queries, relevance, test_size=0.4, random_state=42
)
agent = SearchAgent(docs)
print("Training agent...")
agent.train_agent(train_q, train_rel, epochs=20, lr=0.01, verbose=True)
print("\nEvaluating agent...")
precision, recall = agent.evaluate(test_q, test_rel)
print(f"Precision: {precision:.2f}, Recall: {recall:.2f}")
# Search example
query = "Tell me about deep learning and AI."
print(f"\nSearching for: '{query}'")
results = agent.search(query, top_k=5)
for doc, score in results:
print(f"Score: {score:.4f} | Doc: {doc}")
if __name__ == "__main__":
demo()