# Запуск: streamlit run streamlit_app.py
import streamlit as st
from gensim.models import Word2Vec, FastText, Doc2Vec
from gensim.utils import simple_preprocess
from sklearn.metrics.pairwise import cosine_similarity
from sklearn.decomposition import PCA
import numba
numba.config.CACHE_DIR = '/tmp/numba_cache'
numba.config.DISABLE_CACHING = False
import os
import umap
import pandas as pd
import numpy as np
import networkx as nx
import plotly.graph_objs as go
import plotly.express as px
#Загрузка обученной модели
st.set_page_config(layout="wide", page_title="Исследование векторов")
st.title("Интерактивное изучение векторных представлений")
#sidebar: загрузка модели
st.sidebar.header("Выберите модель и затем загрузите обученную модель")
model_type = st.sidebar.selectbox("Тип модели", ["Word2Vec", "FastText", "Doc2Vec"])
model_file = st.sidebar.file_uploader("Загрузить обученную модель")
#инициализация/загрузка модели
model_w2v = None
model_fasttext = None
model_doc2vec = None
df_steps = None
if "df_steps" in st.session_state and st.session_state["df_steps"] is not None:
df_steps = st.session_state["df_steps"]
df_proj = None
if "df_proj" in st.session_state and st.session_state["df_proj"] is not None:
df_proj = st.session_state["df_proj"]
df = None
if "df" in st.session_state and st.session_state["df"] is not None:
df = st.session_state["df"]
if model_type == "Word2Vec":
if model_file and st.session_state.get("model_w2v") is None:
with open("temp_model.model", "wb") as f:
f.write(model_file.getbuffer())
model_w2v = Word2Vec.load("temp_model.model")
try:
os.remove("temp_model.model")
except OSError:
pass
st.session_state["model_w2v"] = model_w2v
else:
model_w2v = st.session_state.get("model_w2v")
elif model_type == "FastText":
if model_file and st.session_state.get("model_fasttext") is None:
with open("temp_model.model", "wb") as f:
f.write(model_file.getbuffer())
model_fasttext = FastText.load("temp_model.model")
try:
os.remove("temp_model.model")
except OSError:
pass
st.session_state["model_fasttext"] = model_fasttext
else:
model_fasttext = st.session_state.get("model_fasttext")
else:#Doc2Vec
if model_file and st.session_state.get("model_doc2vec") is None:
with open("temp_model.model", "wb") as f:
f.write(model_file.getbuffer())
model_fasttext = Doc2Vec.load("temp_model.model")
try:
os.remove("temp_model.model")
except OSError:
pass
st.session_state["model_doc2vec"] = model_doc2vec
else:
model_doc2vec = st.session_state.get("model_doc2vec")
#вспомогательные функции
def in_vocab(model, word):
"""
проверка слова на наличие в словаре
"""
if model is None:
return False
try:
return word in model.wv
except Exception:
return False
def most_similar(model, positive=None, negative=None, topn=10):
"""
возвращает результат из выражения вида король - мужчина + женщина (= королева)
"""
try:
return model.wv.most_similar(positive=positive or [], negative=negative or [], topn=topn)
except Exception as e:
return []
def build_html_report(title: str,
df_steps: pd.DataFrame | None = None,
df_proj: pd.DataFrame | None = None,
df_matrix: pd.DataFrame | None = None,
figs: list = None) -> str:
"""
Формирует HTML отчёт: таблицы и графики.
"""
figs = figs or []
html_parts = [f"
{title}
",
"Отчёт сформирован автоматически из последних доступных данных.
"]
if df_steps is not None and not df_steps.empty:
html_parts.append("Промежуточные шаги выражения
")
html_parts.append(df_steps.to_html(index=False))
else:
html_parts.append("Нет данных о промежуточных шагах
")
if df_proj is not None and not df_proj.empty:
html_parts.append("Проекции слов на ось
")
html_parts.append(df_proj.to_html(index=True))
else:
html_parts.append("Нет данных о проекциях
")
if df_matrix is not None and not df_matrix.empty:
html_parts.append("Матрица сходств
")
html_parts.append(df_matrix.to_html(index=True))
else:
html_parts.append("Нет матрицы сходств
")
# вставляем графики Plotly: первый с include_plotlyjs="cdn"
for i, f in enumerate(figs):
html_parts.append(f"График {i+1}
")
html_parts.append(f.to_html(full_html=False, include_plotlyjs=("cdn" if i == 0 else False)))
return "\n".join(html_parts)
def cosine_between_vecs(a, b):
"""
угол косинуса между векторами
"""
if a is None or b is None:
return None
val = cosine_similarity([a], [b])[0][0]
return float(val)
def infer_docvec(model, text):
"""
возвращает вектор документа
"""
if model is None:
return None
try:
return model.infer_vector(simple_preprocess(text))
except Exception:
return None
def word_vector(model, word):
"""
возвращает вектор слова
"""
try:
return model.wv[word]
except Exception:
return None
#UI: векторная арифметика
st.header("Интерактивная векторная арифметика")
col1, col2 = st.columns([2,1])
with col1:
expr = st.text_input("Введите выражение (пример: сша - трамп + путин)", value="сша - трамп + путин")
topn = st.number_input("Количество ближайших соседей (topn)", min_value=1, max_value=15, value=3)
run_expr = st.button("Вычислить выражение")
with col2:
st.write(f"Тип модели: {model_type}")
def parse_expression(expr_str):
"""
парсинг выражений вида: w1 - w2 + w3 - w4
"""
# Простая лексическая парсировка: слова и +/-
tokens = expr_str.replace("+", " + ").replace("-", " - ").split()
ops = []
current = None
# схема: первый токен может быть +/- или словом
sign = 1
vec_ops = []
for t in tokens:
if t == "+":
sign = 1
elif t == "-":
sign = -1
else:
vec_ops.append((t, sign))
sign = 1
return vec_ops
def compute_intermediate_vectors(model, expr_ops):
#статистика
intermediate = []
#результирующий вектор со всеми вычислениями, здесь будет храниться вычисления вида сша-трамп+путин
result = np.zeros(model.wv.vector_size)
for word, sign in expr_ops:
if not in_vocab(model, word):
intermediate.append({"word": word, "present": False, "vec": None, "result_after": None})
continue
vec = word_vector(model, word) * sign
result = result + vec
intermediate.append({"word": word, "present": True, "vec": vec.copy(), "result_after": result.copy()})
return intermediate, result
#подсчёт векторной арифметики
if run_expr:
#выбрать активную модель
active_model = model_w2v if model_type=="Word2Vec" else (model_fasttext if model_type=="FastText" else model_doc2vec)
if active_model is None:
st.error("Модель не загружена")
else:
ops = parse_expression(expr)
intermediate, final_vec = compute_intermediate_vectors(active_model, ops)
# показываем таблицу промежуточных шагов
rows = []
for i, s in enumerate(intermediate):
if not s["present"]:
rows.append({"шаг": i+1, "слово": s["word"], "в словаре": False, "наиболее похожие": None})
else:
ms = most_similar(active_model, positive=[s["vec"]], topn=topn)
rows.append({
"шаг": i+1,
"слово": s["word"],
"в словаре": True,
"наиболее похожие": ", ".join([f"{w} ({float(sim):.3f})" for w, sim in ms])
})
df_steps = pd.DataFrame(rows)
st.session_state["df_steps"] = df_steps
st.subheader("Промежуточные шаги")
st.dataframe(df_steps)
#ближайшие соседи для финального вектора
st.subheader("Результат выражения — ближайшие слова")
try:
final_neighbors = active_model.wv.similar_by_vector(final_vec, topn=topn)
except Exception:
final_neighbors = []
st.write(final_neighbors)
#визуализация финального вектора в 2D
st.subheader("2D проекция: промежуточные и итоговый векторы")
#соберём векторы для рисования: все оригинальные слов-векторов и результат
vis_vectors = []
vis_labels = []
for s in intermediate:
if s["present"]:
vis_vectors.append(s["vec"])
vis_labels.append(f"{s['word']} (шаг)")
vis_vectors.append(final_vec)
vis_labels.append("финальный вектор")
vis_vectors_np = np.array(vis_vectors)
reducer = UMAP_OR_PCA = None
try:
reducer = umap.UMAP(n_components=2, random_state=42)
proj = reducer.fit_transform(vis_vectors_np)
except Exception:
reducer = PCA(n_components=2)
proj = reducer.fit_transform(vis_vectors_np)
fig = px.scatter(x=proj[:,0], y=proj[:,1], text=vis_labels, title="2D проекция")
st.plotly_chart(fig, use_container_width=True)
#UI: косинусное расстояние и матрица сходств
st.header("Калькулятор косинусного сходства и матрица близостей")
col1, col2 = st.columns(2)
with col1:
word_a = st.text_input("Слово A", value="путин", key="cos_a")
word_b = st.text_input("Слово B", value="президент", key="cos_b")
calc_cos = st.button("Посчитать косинусное сходство")
with col2:
words_for_matrix = st.text_area("Список слов для матрицы (через запятую)", value="россия,трамп,китай,спорт")
calc_matrix = st.button("Построить матрицу сходств")
if calc_cos:
active_model = model_w2v if model_type=="Word2Vec" else (model_fasttext if model_type=="FastText" else model_doc2vec)
if active_model is None:
st.error("Модель не загружена")
else:
if in_vocab(active_model, word_a) and in_vocab(active_model, word_b):
va = word_vector(active_model, word_a)
vb = word_vector(active_model, word_b)
cosv = cosine_between_vecs(va, vb)
st.metric("Косинусное сходство", f"{cosv:.4f}")
else:
st.error("Одно из слов отсутствует в словаре модели")
if calc_matrix:
active_model = model_w2v if model_type=="Word2Vec" else (model_fasttext if model_type=="FastText" else model_doc2vec)
words = [w.strip() for w in words_for_matrix.split(",") if w.strip()]
present = [w for w in words if in_vocab(active_model, w)]
if not present:
st.error("Нет слов из списка в словаре модели")
else:
mat = np.array([word_vector(active_model, w) for w in present])
simm = cosine_similarity(mat)
df = pd.DataFrame(simm, index=present, columns=present)
st.session_state["df"] = df
st.subheader("Heatmap семантической близости")
fig = px.imshow(df.values, x=present, y=present, color_continuous_scale='RdBu_r', zmin=-1, zmax=1)
st.plotly_chart(fig, use_container_width=True)
st.dataframe(df.style.background_gradient(cmap='RdBu_r', axis=None))
#UI: семантическая ось и проекция
st.header("Семантические оси и проекция")
axis_left = st.text_input("Слово A (лево оси)", value="мужчина", key="axis_a")
axis_right = st.text_input("Слово B (право оси)", value="женщина", key="axis_b")
words_for_proj = st.text_area("Слова для проекции (через запятую)", value="король,королева,президент,работник,няня")
do_proj = st.button("Произвести проекцию на ось")
def project_on_axis(model, left, right, targets):
axis = word_vector(model, left) - word_vector(model, right)
scores = {}
for w in targets:
if in_vocab(model, w):
vec = word_vector(model, w)
#если score > 0 то относится к левому, иначе к правому
score = cosine_similarity([vec], [axis])[0][0]
scores[w] = float(score)
else:
scores[w] = None
return scores, axis
if do_proj:
active_model = model_w2v if model_type=="Word2Vec" else (model_fasttext if model_type=="FastText" else model_doc2vec)
targets = [w.strip() for w in words_for_proj.split(",") if w.strip()]
if not in_vocab(active_model, axis_left) or not in_vocab(active_model, axis_right):
st.error("Одна из опорных слов отсутствует в модели")
else:
scores, axis_vec = project_on_axis(active_model, axis_left, axis_right, targets)
df_proj = pd.DataFrame.from_dict(scores, orient='index', columns=['projection']).sort_values('projection', ascending=False)
st.session_state["df_proj"] = df_proj
st.dataframe(df_proj)
st.subheader("График проекций")
fig = px.bar(df_proj.reset_index().rename(columns={'index':'word'}), x='word', y='projection', color='projection', color_continuous_scale='RdBu')
st.plotly_chart(fig, use_container_width=True)
#UI: граф семантических связей
st.header("Граф семантических связей")
graph_seed = st.text_input("Слово (центр графа)", value="россия", key="graph_seed")
graph_depth = st.slider("Глубина (уровней соседей)", 1, 3, 2)
graph_topn = st.slider("TopN соседей на уровень", 1, 8, 5)
def build_similarity_graph(model, seed, depth=2, topn=5):
G = nx.Graph()
visited = set()
def expand(node, d):
if d>depth:
return
visited.add(node)
if not in_vocab(model, node):
return
try:
neighbors = model.wv.most_similar(node, topn=topn)
except Exception:
neighbors = []
for nb, sim in neighbors:
G.add_node(node)
G.add_node(nb)
G.add_edge(node, nb, weight=float(sim))
if nb not in visited:
expand(nb, d+1)
expand(seed, 1)
return G
if st.button("Построить граф"):
active_model = model_w2v if model_type=="Word2Vec" else (model_fasttext if model_type=="FastText" else model_doc2vec)
if not in_vocab(active_model, graph_seed):
st.error("Корневое слово отсутствует в модели")
else:
G = build_similarity_graph(active_model, graph_seed, depth=graph_depth, topn=graph_topn)
st.write(f"Узлы: {len(G.nodes())}, Рёбра: {len(G.edges())}")
#визуализация через plotly
pos = nx.spring_layout(G, seed=42)
edge_x = []
edge_y = []
for e in G.edges():
x0, y0 = pos[e[0]]
x1, y1 = pos[e[1]]
edge_x += [x0, x1, None]
edge_y += [y0, y1, None]
node_x = []
node_y = []
texts = []
for n in G.nodes():
x, y = pos[n]
node_x.append(x)
node_y.append(y)
texts.append(n)
edge_trace = go.Scatter(x=edge_x, y=edge_y, mode='lines', line=dict(width=0.5, color='#888'), hoverinfo='none')
node_trace = go.Scatter(
x=node_x, y=node_y, mode='markers+text', text=texts, textposition="top center",
hoverinfo='text', marker=dict(showscale=False, size=10, color='skyblue', line_width=2)
)
fig = go.Figure(data=[edge_trace, node_trace])
fig.update_layout(showlegend=False, margin=dict(b=20,l=5,r=5,t=40))
st.plotly_chart(fig, use_container_width=True)
#UI: генерация отчёта
st.header("Генерация отчёта")
report_title = st.text_input("Заголовок отчёта", value="Отчёт")
report_btn = st.button("Сгенерировать отчёт")
if report_btn:
try:
last_steps = df_steps
except Exception:
last_steps = pd.DataFrame()
try:
last_proj = df_proj
except Exception:
last_proj = pd.DataFrame()
try:
last_mat = df
except Exception:
last_mat = pd.DataFrame()
# добавляем последние графики, если есть
figs_to_add = []
if "fig" in globals() and fig is not None:
figs_to_add.append(fig)
html_report = build_html_report(report_title, last_steps, last_proj, last_mat, figs_to_add)
st.download_button(
label="Скачать HTML отчёт",
data=html_report.encode("utf-8"),
file_name="report.html",
mime="text/html",
)
st.sidebar.header("Для doc2vec только схожести предложений")