Spaces:
Sleeping
Sleeping
| import streamlit as st | |
| import pandas as pd | |
| import numpy as np | |
| from io import StringIO | |
| from sklearn.feature_extraction.text import CountVectorizer | |
| from sklearn.decomposition import LatentDirichletAllocation, PCA | |
| from sklearn.cluster import KMeans | |
| from sklearn.manifold import TSNE | |
| import re | |
| from russpelling import * | |
| try: | |
| import maru | |
| except: | |
| pass | |
| from natasha import Doc, Segmenter, MorphVocab, NewsEmbedding, NewsMorphTagger | |
| import sys | |
| import time | |
| from threading import Thread | |
| from scipy.special import gammaln | |
| from scipy.stats import mode | |
| from scipy.sparse import csr_matrix, lil_matrix | |
| from umap import UMAP | |
| import plotly.express as px | |
| import scipy | |
| from hmmlearn.hmm import CategoricalHMM, GMMHMM | |
| from razdel import tokenize,sentenize | |
| from razdel.substring import Substring | |
| import colorsys | |
| from docx import Document | |
| from docx.shared import RGBColor, Pt | |
| from docx.enum.text import WD_COLOR_INDEX, WD_ALIGN_PARAGRAPH | |
| from soyclustering import SphericalKMeans | |
| from bertopic import BERTopic | |
| from sentence_transformers import SentenceTransformer | |
| import plotly.graph_objects as go | |
| from typing import List, Union | |
| from bertopic.backend._utils import select_backend | |
| import matplotlib.pyplot as plt | |
| from contextualized_topic_models.models.ctm import CombinedTM | |
| from contextualized_topic_models.utils.data_preparation import TopicModelDataPreparation, bert_embeddings_from_list | |
| import datetime | |
| import torch | |
| from contextualized_topic_models.datasets.dataset import CTMDataset | |
| import pyLDAvis as vis | |
| from streamlit import components | |
| from Top2VecNew import Top2VecNew | |
| from matplotlib.colors import rgb2hex | |
| from scipy.optimize import linear_sum_assignment | |
| import math | |
| from navec import Navec | |
| from scipy.ndimage import gaussian_filter1d | |
| from sklearn.metrics.pairwise import cosine_distances | |
| from sklearn.mixture import GaussianMixture | |
| st.set_page_config(layout='wide') | |
| token_pattern = '(?u)\\b\\w+(?:-\\w+)*\\b' | |
| token_pattern_filt = '(?u)\\b[^\\W0-9](?:[-\']?[^\\W0-9])+\\b' | |
| #token_pattern_filt = '[^ ][^ ]+' | |
| class ReturnableThread(Thread): | |
| def __init__(self, target, args=(), kwargs={}): | |
| super().__init__(target=target, args=args, kwargs=kwargs) | |
| self.result = None | |
| def run(self): | |
| if self._target: | |
| self.result = self._target(*self._args, **self._kwargs) | |
| def tm_data_prep_fit(self, text_for_contextual, text_for_bow, vectorizer=None): | |
| if text_for_bow is not None: | |
| assert len(text_for_contextual) == len(text_for_bow) | |
| if self.contextualized_model is None: | |
| raise Exception( | |
| "A contextualized model must be defined" | |
| ) | |
| # TODO: this count vectorizer removes tokens that have len = 1, might be unexpected for the users | |
| self.vectorizer = CountVectorizer() if not vectorizer else vectorizer | |
| train_bow_embeddings = self.vectorizer.fit_transform(text_for_bow) | |
| # if the user is passing custom embeddings we don't need to create the embeddings using the model | |
| train_contextualized_embeddings = bert_embeddings_from_list( | |
| text_for_contextual, | |
| sbert_model_to_load=self.contextualized_model, | |
| max_seq_length=self.max_seq_length, | |
| ) | |
| self.vocab = self.vectorizer.get_feature_names_out() | |
| self.id2token = {k: v for k, v in zip(range(0, len(self.vocab)), self.vocab)} | |
| encoded_labels = None | |
| return CTMDataset( | |
| X_contextual=train_contextualized_embeddings, | |
| X_bow=train_bow_embeddings, | |
| idx2token=self.id2token, | |
| labels=encoded_labels, | |
| ) | |
| def gmmfit(gmm, X, w=None, max_iter=1000, eps=1e-6): | |
| if w is None: | |
| w = np.ones((X.shape[0])) | |
| p0 = (gmm.score_samples(X) * w).sum() / w.sum() | |
| for i in range(max_iter): | |
| comps = w[:,None] * gmm.predict_proba(X) | |
| gmm.weights_ = comps.sum(axis=0) / comps.sum() | |
| comps /= comps.sum(axis=0) | |
| means = np.transpose(comps) @ X | |
| gmm.means_ = np.where(np.isnan(means), gmm.means_, means) | |
| covars = X[:,None,:] - gmm.means_ | |
| covars = (comps[...,None,None] * (covars[:,:,:,None] * covars[:,:,None,:])).sum(axis=0) | |
| gmm.covariances_ = np.where(np.isnan(covars), gmm.covariances_, covars) | |
| # КРИТИЧЕСКОЕ ИСПРАВЛЕНИЕ: обновляем precisions_cholesky_ | |
| precisions_chol = np.empty_like(gmm.covariances_) | |
| for k, cov in enumerate(gmm.covariances_): | |
| stop = False | |
| eps = 1e-6 | |
| while not stop: | |
| val, vec = np.linalg.eig(cov) | |
| gmm.covariances_[k] = vec.real @ np.diag(np.maximum(val.real,eps)) @ vec.real.transpose() | |
| try: | |
| # Считаем нижнетреугольную матрицу разложения Холецкого | |
| cov_chol = np.linalg.cholesky(cov) | |
| stop = True | |
| except np.linalg.LinAlgError: | |
| eps *= 10 | |
| # Вычисляем матрицу точности (precision matrix) | |
| precisions_chol[k] = scipy.linalg.solve_triangular(cov_chol, np.eye(cov_chol.shape[0]), lower=True).T | |
| gmm.precisions_cholesky_ = precisions_chol | |
| p1 = (gmm.score_samples(X) * w).sum() / w.sum() | |
| if p1-p0 < eps: | |
| return | |
| p0 = p1 | |
| def visualize_documents( | |
| topic_model, | |
| docs: List[str], | |
| topics: List[int] = None, | |
| embeddings: np.ndarray = None, | |
| reduced_embeddings: np.ndarray = None, | |
| sample: float = None, | |
| hide_annotations: bool = False, | |
| hide_document_hover: bool = False, | |
| custom_labels: Union[bool, str] = False, | |
| title: str = "<b>Документы и темы</b>", | |
| width: int = 1200, | |
| height: int = 750, | |
| ): | |
| topic_per_doc = topic_model.topics_ if topics is None else topics | |
| # Sample the data to optimize for visualization and dimensionality reduction | |
| if sample is None or sample > 1: | |
| sample = 1 | |
| indices = [] | |
| for topic in set(topic_per_doc): | |
| s = np.where(np.array(topic_per_doc) == topic)[0] | |
| size = len(s) if len(s) < 100 else int(len(s) * sample) | |
| indices.extend(np.random.choice(s, size=size, replace=False)) | |
| indices = np.array(indices) | |
| df = pd.DataFrame({"topic": np.array(topic_per_doc)[indices]}) | |
| df["doc"] = [docs[index] for index in indices] | |
| df["topic"] = [topic_per_doc[index] for index in indices] | |
| # Extract embeddings if not already done | |
| if sample is None: | |
| if embeddings is None and reduced_embeddings is None: | |
| embeddings_to_reduce = topic_model._extract_embeddings(df.doc.to_list(), method="document") | |
| else: | |
| embeddings_to_reduce = embeddings | |
| else: | |
| if embeddings is not None: | |
| embeddings_to_reduce = embeddings[indices] | |
| elif embeddings is None and reduced_embeddings is None: | |
| embeddings_to_reduce = topic_model._extract_embeddings(df.doc.to_list(), method="document") | |
| # Reduce input embeddings | |
| if reduced_embeddings is None: | |
| try: | |
| from umap import UMAP | |
| umap_model = UMAP(n_neighbors=10, n_components=2, min_dist=0.0, metric="cosine").fit(embeddings_to_reduce) | |
| embeddings_2d = umap_model.embedding_ | |
| except (ImportError, ModuleNotFoundError): | |
| raise ModuleNotFoundError( | |
| "UMAP is required if the embeddings are not yet reduced in dimensionality. Please install it using `pip install umap-learn`." | |
| ) | |
| elif sample is not None and reduced_embeddings is not None: | |
| embeddings_2d = reduced_embeddings[indices] | |
| elif sample is None and reduced_embeddings is not None: | |
| embeddings_2d = reduced_embeddings | |
| unique_topics = set(topic_per_doc) | |
| if topics is None: | |
| topics = unique_topics | |
| # Combine data | |
| df["x"] = embeddings_2d[:, 0] | |
| df["y"] = embeddings_2d[:, 1] | |
| # Prepare text and names | |
| if isinstance(custom_labels, str): | |
| names = [[[str(topic), None]] + topic_model.topic_aspects_[custom_labels][topic] for topic in unique_topics] | |
| names = ["_".join([label[0] for label in labels[:4]]) for labels in names] | |
| names = [label if len(label) < 30 else label[:27] + "..." for label in names] | |
| elif topic_model.custom_labels_ is not None and custom_labels: | |
| names = [topic_model.custom_labels_[topic + topic_model._outliers] for topic in unique_topics] | |
| else: | |
| names = [ | |
| #f"{topic+1}_" + "_".join([word for word, value in topic_model.get_topic(topic)][:3]) | |
| f"{topic+1}_" + "_".join([word for word in st.session_state['topicnames'][topic].split(', ')[:3]]) | |
| for topic in unique_topics | |
| ] | |
| # Visualize | |
| fig = go.Figure() | |
| # Outliers and non-selected topics | |
| non_selected_topics = set(unique_topics).difference(topics) | |
| if len(non_selected_topics) == 0: | |
| non_selected_topics = [-1] | |
| selection = df.loc[df.topic.isin(non_selected_topics), :] | |
| selection["text"] = "" | |
| selection.loc[len(selection), :] = [ | |
| None, | |
| None, | |
| selection.x.mean(), | |
| selection.y.mean(), | |
| "Другие документы", | |
| ] | |
| if False: | |
| fig.add_trace( | |
| go.Scattergl( | |
| x=selection.x, | |
| y=selection.y, | |
| hovertext=selection.doc if not hide_document_hover else None, | |
| hoverinfo="text", | |
| mode="markers", | |
| name="other", | |
| showlegend=False, | |
| marker=dict(color="#CFD8DC", size=5, opacity=0.5), | |
| ) | |
| ) | |
| # Selected topics | |
| for name, topic in zip(names, unique_topics): | |
| if topic in topics and topic != -1: | |
| selection = df.loc[df.topic == topic, :] | |
| selection["text"] = "" | |
| if not hide_annotations: | |
| selection.loc[len(selection), :] = [ | |
| None, | |
| None, | |
| selection.x.mean(), | |
| selection.y.mean(), | |
| name, | |
| ] | |
| fig.add_trace( | |
| go.Scattergl( | |
| x=selection.x, | |
| y=selection.y, | |
| hovertext=selection.doc if not hide_document_hover else None, | |
| hoverinfo="text", | |
| text=selection.text, | |
| mode="markers", | |
| name=name, | |
| textfont=dict( | |
| size=12, | |
| ), | |
| marker=dict(color="#CFD8DC", size=5, opacity=0.5) if topic == 0 else dict(size=5, opacity=0.5), | |
| ) | |
| ) | |
| # Add grid in a 'plus' shape | |
| x_range = ( | |
| df.x.min() - abs((df.x.min()) * 0.15), | |
| df.x.max() + abs((df.x.max()) * 0.15), | |
| ) | |
| y_range = ( | |
| df.y.min() - abs((df.y.min()) * 0.15), | |
| df.y.max() + abs((df.y.max()) * 0.15), | |
| ) | |
| fig.add_shape( | |
| type="line", | |
| x0=sum(x_range) / 2, | |
| y0=y_range[0], | |
| x1=sum(x_range) / 2, | |
| y1=y_range[1], | |
| line=dict(color="#CFD8DC", width=2), | |
| ) | |
| fig.add_shape( | |
| type="line", | |
| x0=x_range[0], | |
| y0=sum(y_range) / 2, | |
| x1=x_range[1], | |
| y1=sum(y_range) / 2, | |
| line=dict(color="#9E9E9E", width=2), | |
| ) | |
| fig.add_annotation(x=x_range[0], y=sum(y_range) / 2, text="D1", showarrow=False, yshift=10) | |
| fig.add_annotation(y=y_range[1], x=sum(x_range) / 2, text="D2", showarrow=False, xshift=10) | |
| # Stylize layout | |
| fig.update_layout( | |
| template="simple_white", | |
| title={ | |
| "text": f"{title}", | |
| "x": 0.5, | |
| "xanchor": "center", | |
| "yanchor": "top", | |
| "font": dict(size=22, color="Black"), | |
| }, | |
| width=width, | |
| height=height, | |
| ) | |
| fig.update_xaxes(visible=False) | |
| fig.update_yaxes(visible=False) | |
| return fig | |
| if False: | |
| rgb = [] | |
| for i in range((n_topics+1) // 2): | |
| h, s, v = i / ((n_topics+1)//2), 1.0, 1.0 | |
| r, g, b = colorsys.hsv_to_rgb(h, s, v) | |
| y, _, _ = colorsys.rgb_to_yiq(r, g, b) | |
| rgb.append([min(1, 0.6/y*r), min(1, 0.6/y*g), min(1, 0.6/y*b)]) | |
| rgb = rgb + [[0.5*r, 0.5*g, 0.5*b] for r,g,b in rgb] | |
| else: | |
| rgb = [[0,0,1], [0,0,0.5], [0.5,0,0], [0.5,0.5,0], [0.5,0.5,0.5], [0,0.5,0], [1,0,1], [1,0,0], [0,0.5,0.5], [0.5,0,0.5]] | |
| byword = False | |
| markup_cases = ['HMM','SentHMM'] | |
| def modernize_text(text_orig): | |
| text = text_orig | |
| for j in re.finditer('i', text.lower()): | |
| k = j.span()[0] | |
| if set(text[max(0,k-1):k+2].lower()).intersection('абвгдеёжзийклмнопрстуфхцчшщъыьэюя'): | |
| text = text[:k] + ('и' if text[k] == 'i' else 'И') + text[k+1:] | |
| text = re.sub('чьк', 'чк', text) | |
| text = re.sub('чьт', 'чт', text) | |
| text = re.sub('чьп', 'чп', text) | |
| text = re.sub('чьв', 'чв', text) | |
| text = normalize(text) | |
| text = re.sub('кия\\b', 'кие', text) | |
| text = re.sub('яго\\b', 'его', text) | |
| text = re.sub('\\b([Хх])ороше\\b', r'\1орошо', text) | |
| return text | |
| def prepare_vocab(norm_cb, lemma_cb, pr_pbar): | |
| docs = st.session_state['docs'] | |
| for i in range(len(docs)): | |
| docs[i]['text'] = modernize_text(docs[i]['text_orig']) if norm_cb else docs[i]['text_orig'] | |
| if lemma_cb: | |
| if 'analyzer' not in st.session_state: | |
| try: | |
| st.session_state['analyzer'] = maru.get_analyzer(tagger='rnn', lemmatizer='pymorphy') | |
| except: | |
| st.session_state['analyzer'] = {'segmenter': Segmenter(), 'morph_vocab': MorphVocab(), 'emb': NewsEmbedding()} | |
| st.session_state['analyzer']['morph_tagger'] = NewsMorphTagger(st.session_state['analyzer']['emb']) | |
| for i in range(len(docs)): | |
| docs[i]['tokens'],docs[i]['input'] = [],[] | |
| if not byword: | |
| docs[i]['sents'] = [s for s in sentenize(docs[i]['text'])] | |
| chunks = [t for t in tokenize(docs[i]['text'])] | |
| else: | |
| docs[i]['sents'] = [Substring(t.span()[0], t.span()[1], t.group()) for t in re.finditer(token_pattern_filt, docs[i]['text'])] | |
| chunks = docs[i]['sents'] | |
| if lemma_cb: | |
| try: | |
| analyzed = st.session_state['analyzer'].analyze([t.text for t in chunks]) | |
| lemmas = [morph.lemma for morph in analyzed] | |
| except: | |
| doc = Doc(docs[i]['text']) | |
| doc.segment(st.session_state['analyzer']['segmenter']) | |
| doc.tag_morph(st.session_state['analyzer']['morph_tagger']) | |
| for token in doc.tokens: | |
| token.lemmatize(st.session_state['analyzer']['morph_vocab']) | |
| chunks = [Substring(token.start, token.stop, token.text) for token in doc.tokens] | |
| lemmas = [token.lemma for token in doc.tokens] | |
| else: | |
| lemmas = [t.text.lower() for t in chunks] | |
| j = 0 | |
| for s in docs[i]['sents']: | |
| tokens = [] | |
| while j < len(chunks) and chunks[j].start < s.stop: | |
| tokens.append(Substring(chunks[j].start, chunks[j].stop, lemmas[j])) | |
| j += 1 | |
| docs[i]['tokens'].append(tokens) | |
| docs[i]['input'].append(''.join(s.text + ' ' for s in tokens)) | |
| docs[i]['input'] = ''.join(s for s in docs[i]['input']) | |
| pr_pbar.progress((i+1)/len(docs), text=f'Подготовка словаря: {i+1}/{len(docs)}') | |
| st.session_state['docs'] = docs | |
| vectorizer = CountVectorizer(token_pattern=token_pattern_filt, stop_words=st.session_state['stopwords']) | |
| st.session_state['counts'] = vectorizer.fit_transform([d['input'] for d in docs]).toarray() | |
| st.session_state['words'] = vectorizer.get_feature_names_out() | |
| words = st.session_state['words'] | |
| vocab = {w: i for i,w in enumerate(words)} | |
| for i in range(len(docs)): | |
| docs[i]['seq'] = [] | |
| docs[i]['parts'] = [] | |
| for j in range(len(docs[i]['tokens'])): | |
| docs[i]['tokens'][j] = [t for t in docs[i]['tokens'][j] if t.text in vocab] | |
| add = [vocab[t.text] for t in docs[i]['tokens'][j]] | |
| if add: | |
| docs[i]['seq'] += add | |
| docs[i]['parts'] += [j for _ in range(len(add))] | |
| docs[i]['seq'] = np.array(docs[i]['seq'], dtype='int') | |
| if not byword: | |
| docs[i]['parts'] = np.array(docs[i]['parts'], dtype='int') | |
| else: | |
| docs[i]['parts'] = np.arange(len(docs[i]['parts']), dtype='int') | |
| docs[i]['tokens'] = sum((t for t in docs[i]['tokens']), []) | |
| st.session_state['ready'] = '' | |
| #with open('words.txt', encoding='utf-8') as f: | |
| # words0 = f.read().split('\n') | |
| #print(set(words) - set(words0)) | |
| st.session_state['navec'] = Navec.load('./src/navec_hudlit_v1_12B_500K_300d_100q.tar') | |
| word_embs = np.full((len(words), 300), np.nan) | |
| for i,word in enumerate(words): | |
| if word in st.session_state['navec']: | |
| word_embs[i] = st.session_state['navec'][word] | |
| mask = ~np.any(np.isnan(word_embs), axis=1) | |
| st.session_state['word_sims'] = np.full((len(words), len(words)), np.nan) | |
| st.session_state['word_sims'][np.ix_(mask,mask)] = 1 - cosine_distances(word_embs[mask]) | |
| dist = 10 | |
| min_count = 10 | |
| idxs = np.where(st.session_state['counts'].sum(axis=0) >= min_count)[0] | |
| npmi = np.zeros((len(idxs), len(idxs))) | |
| probs = {} | |
| seq = np.hstack([d['seq'] for d in docs]) | |
| p = (2*dist - 1) / max(1, len(seq)) | |
| pos = [np.where(seq == i)[0] for i in idxs] | |
| for i1 in range(len(idxs)): | |
| pr_pbar.progress((i1+1)/len(idxs), text=f'Обработка коллокаций: {i1+1}/{len(idxs)}') | |
| v1 = pos[i1] | |
| for i2 in range(i1+1,len(idxs)): | |
| v2 = pos[i2] | |
| k = min(len(v1),len(v2)) | |
| d = np.abs(v1[...,np.newaxis] - v2) < dist | |
| if d.any(): | |
| if k > 1: | |
| rows,cols = linear_sum_assignment(d, maximize=True) | |
| num = d[rows,cols].sum() | |
| else: | |
| num = np.any(d).astype(int) | |
| if num > 0: | |
| m = max(len(v1),len(v2)) | |
| if (k,m,num) not in probs: | |
| val = 0 | |
| q = 1 - (1-p) ** m | |
| for n in range(num, k+1): | |
| val += (q ** n) * ((1-q) ** (k-n)) * math.comb(k, n) | |
| probs[(k,m,num)] = max(0, 1-val) | |
| npmi[i1,i2] = probs[(k,m,num)] | |
| npmi[i2,i1] = npmi[i1,i2] | |
| st.session_state['word_dist'] = dist | |
| st.session_state['word_min_count'] = min_count | |
| st.session_state['npmi'] = npmi | |
| def perplexity_hmm(start, trans, emis, reduce=True): | |
| use_lengths = False if 'use_lengths' not in st.session_state else st.session_state['use_lengths'] | |
| docs = st.session_state['docs'] | |
| n_topics = len(start) | |
| perp = 0 if reduce else [] | |
| if st.session_state['ready'] == 'SentHMM': | |
| A = start.copy() | |
| k = 0 | |
| for doc in docs: | |
| p = 0 | |
| for j in range(len(doc['sents'])): | |
| for t in np.where([doc['parts'] == j])[1]: | |
| p += np.log(emis[st.session_state['seq'][k], doc['seq'][t]]) | |
| p += np.log(A[st.session_state['seq'][k]]) | |
| A = trans[st.session_state['seq'][k]] | |
| k += 1 | |
| if use_lengths: | |
| A = start.copy() | |
| if reduce: | |
| perp += p | |
| else: | |
| perp.append(p) | |
| else: | |
| A = start.copy() | |
| for doc in docs: | |
| p = 0 | |
| seq = doc['seq'] | |
| parts = doc['parts'] | |
| for j in range(len(np.unique(doc['parts']))): | |
| if any(parts == j): | |
| if parts[0] != j: | |
| A = A @ trans | |
| for k in np.where([parts == j])[1]: | |
| A *= emis[:,seq[k]] | |
| q = A.max() | |
| A /= q | |
| p += np.log(q) | |
| p += np.log(A.sum()) | |
| if use_lengths: | |
| A = start.copy() | |
| elif len(seq) > 0: | |
| (A / A.sum()) @ trans | |
| if reduce: | |
| perp += p | |
| else: | |
| perp.append(p) | |
| if reduce: | |
| perp = np.exp(-perp / sum(len(d['seq']) for d in docs)) | |
| else: | |
| perp = np.exp(-np.array(perp) / np.array([max(len(d['seq']),1) for d in docs])) | |
| return perp | |
| def perplexity(D, phi, theta): | |
| temp = np.log(phi @ theta); | |
| temp[D == 0] = 0; | |
| p = np.exp((-(D * temp).sum()) / D.sum()) | |
| return p | |
| def correlation(phi): | |
| p = np.corrcoef(phi.transpose()) | |
| p = (p.sum() - p.shape[0]) / (p.shape[0] * (p.shape[0] - 1)) | |
| return p | |
| def coherence(phi, topk=10, dist=10): | |
| idx0 = np.argsort(-phi, axis=0)[:topk] | |
| idx = np.unique(idx0) | |
| inv_idx = {j: i for i,j in enumerate(idx)} | |
| occur = np.zeros((len(idx),), dtype='int') | |
| cooccur = np.zeros((len(idx),len(idx)), dtype='int') | |
| seqs = [d['seq'] for d in st.session_state['docs'] if len(d['seq']) > 0] | |
| for seq in seqs: | |
| ind = np.array([i for i,j in enumerate(idx) if j in seq]) | |
| if len(ind) > 0: | |
| pos = [np.where(seq == idx[j]) - np.arange(0, max(1, len(seq)-dist+1))[...,np.newaxis] for j in ind] | |
| pos = np.vstack([np.any((lambda x: np.logical_and(x >= 0, x < dist))(p), axis=1) for p in pos]) | |
| occur[ind] += pos.sum(axis=1) | |
| cooccur[np.ix_(ind,ind)] += np.minimum(pos[...,np.newaxis], \ | |
| pos.transpose().reshape((1,pos.shape[1],pos.shape[0]))).sum(axis=1) | |
| ndocs = sum(max(1, len(seq)-dist+1) for seq in seqs) | |
| c = np.log(cooccur) | |
| numer = c - np.log(occur * occur[...,np.newaxis] / ndocs) | |
| denom = np.log(ndocs) - c | |
| npmi = numer / denom | |
| npmi[np.isnan(npmi)] = -1 | |
| np.fill_diagonal(npmi, 0) | |
| p = np.zeros((idx0.shape[1],)) | |
| for i in range(idx0.shape[1]): | |
| ind = np.array([inv_idx[j] for j in idx0[:,i]]) | |
| p[i] = npmi[np.ix_(ind,ind)].sum() / (len(ind) * (len(ind)-1)) | |
| p = p.mean() | |
| return p | |
| def diversity(phi, topk=10): | |
| idx = np.argsort(-phi, axis=0)[:topk] | |
| p = len(np.unique(idx)) / idx.size | |
| return p | |
| def fill_result(): | |
| topicnames = [] | |
| for i in range(st.session_state['phi'].shape[1]): | |
| indices = np.argsort(st.session_state['phi' if st.session_state['ready'] != 'BERTopic' else 'ctfidf'][:,i])[::-1] | |
| temp = f'' | |
| for j in range(7): | |
| word = st.session_state['words'][indices[j]] | |
| temp += f'{word}, ' | |
| topicnames.append(temp[:-2]) | |
| st.session_state['topicnames'] = topicnames | |
| if st.session_state['ready'] in ['LDA','ARTM','CTM','BERTopic','Top2Vec']: | |
| temp = np.log(st.session_state['phi'] @ st.session_state['theta']) | |
| D = st.session_state['counts'].transpose() | |
| temp[D == 0] = 0 | |
| p = np.exp((-(D * temp).sum(axis=0)) / D.sum(axis=0)) | |
| p[np.isnan(p)] = 1 | |
| st.session_state['perplexity'] = p | |
| elif st.session_state['ready'] == 'SentHMM': | |
| n_topics = len(st.session_state['start']) | |
| theta = np.zeros((n_topics, st.session_state['counts'].shape[0])) | |
| phi = np.zeros((st.session_state['counts'].shape[1], theta.shape[0])) | |
| labels = [] | |
| k = 0 | |
| for i,d in enumerate(st.session_state['docs']): | |
| add = -np.ones_like(d['parts']) | |
| for j in range(len(d['sents'])): | |
| add[d['parts'] == j] = st.session_state['seq'][k] | |
| k += 1 | |
| labels.append(add) | |
| theta[:,i] += np.bincount(add, minlength=theta.shape[0]) | |
| for a,s in zip(add,d['seq']): | |
| phi[s,a] += 1 | |
| phi = phi/phi.sum(axis=0) | |
| phi[np.isnan(phi)] = 1/phi.shape[0] | |
| theta = theta/theta.sum(axis=0) | |
| theta[np.isnan(theta)] = 1/theta.shape[0] | |
| st.session_state['phi'] = phi | |
| st.session_state['theta'] = theta | |
| st.session_state['labels'] = labels | |
| st.session_state['perplexity'] = perplexity_hmm(st.session_state['start'], st.session_state['trans'], \ | |
| st.session_state['phi'].transpose(), reduce=False) | |
| elif st.session_state['ready'] == 'HMM': | |
| emis = st.session_state['phi'].transpose() | |
| n_topics = emis.shape[0] | |
| theta = np.zeros((n_topics, st.session_state['counts'].shape[0])) | |
| docs = st.session_state['docs'] | |
| start = st.session_state['start'] | |
| trans = st.session_state['trans'] | |
| st.session_state['perplexity'] = perplexity_hmm(start, trans, emis, reduce=False) | |
| labels = [[] for _ in range(len(docs))] | |
| for i in range(len(docs)): | |
| seq = docs[i]['seq'] | |
| k = len(seq) | |
| if k > 0: | |
| P = docs[i]['parts'][:-1] != docs[i]['parts'][1:] | |
| A = start * emis[:,seq[0]] | |
| M = np.zeros((n_topics, k-1), dtype=int) | |
| for j in range(1,k): | |
| if P[j-1]: | |
| A = A[...,np.newaxis] * trans | |
| M[:,j-1] = np.argmax(A, axis=0) | |
| A = A.max(axis=0) * emis[:,seq[j]] | |
| else: | |
| A *= emis[:,seq[j]] | |
| M[:,j-1] = np.arange(n_topics) | |
| A /= A.max() | |
| res = [int(np.argmax(A))] | |
| for j in range(k-2,-1,-1): | |
| res = [M[res[0],j]] + res | |
| labels[i] = np.array(res) | |
| theta[:,i] = np.bincount(res, minlength=n_topics) | |
| theta = theta / theta.sum(axis=0) | |
| theta[np.isnan(theta)] = 1/n_topics | |
| st.session_state['labels'] = labels | |
| st.session_state['theta'] = theta | |
| colors = [WD_COLOR_INDEX.BLUE, WD_COLOR_INDEX.DARK_BLUE, WD_COLOR_INDEX.DARK_RED, WD_COLOR_INDEX.DARK_YELLOW, WD_COLOR_INDEX.GRAY_50, | |
| WD_COLOR_INDEX.GREEN, WD_COLOR_INDEX.PINK, WD_COLOR_INDEX.RED, WD_COLOR_INDEX.TEAL, WD_COLOR_INDEX.VIOLET] | |
| n_topics = st.session_state['phi'].shape[1] | |
| wdoc = Document() | |
| for i in range(n_topics): | |
| wdoc.add_heading(f'Тема {i+1}', level=1) | |
| p = wdoc.add_paragraph() | |
| indices = np.argsort(st.session_state['phi'][:,i])[::-1] | |
| for j in range(20): | |
| word = st.session_state['words'][indices[j]] | |
| score = st.session_state['phi'][indices[j],i] | |
| r = p.add_run(f'{word} ({score:.4f}) ') | |
| r.font.color.rgb = RGBColor(255,255,255) | |
| r.font.highlight_color = colors[i] | |
| if st.session_state['ready'] in ['HMM', 'SentHMM']: | |
| font_size = 10 | |
| wdoc.add_heading(f'Таблица переходов', level=1) | |
| tab = wdoc.add_table(n_topics+2, n_topics+1) | |
| tab.style = 'Table Grid' | |
| cell = tab.cell(0, 0) | |
| par = cell.paragraphs[0] | |
| par.alignment = WD_ALIGN_PARAGRAPH.CENTER | |
| cell = tab.cell(1, 0) | |
| par = cell.paragraphs[0] | |
| par.alignment = WD_ALIGN_PARAGRAPH.CENTER | |
| run = par.add_run(f'0') | |
| run.bold = True | |
| run.font.size = Pt(font_size) | |
| for j in range(n_topics): | |
| cell = tab.cell(0, j+1) | |
| par = cell.paragraphs[0] | |
| par.alignment = WD_ALIGN_PARAGRAPH.CENTER | |
| val = st.session_state['trans'][i,j] | |
| run = par.add_run(f'{j+1}') | |
| run.bold = True | |
| run.font.size = Pt(font_size) | |
| cell = tab.cell(1, j+1) | |
| par = cell.paragraphs[0] | |
| par.alignment = WD_ALIGN_PARAGRAPH.CENTER | |
| val = st.session_state['start'][j] | |
| run = par.add_run(f'{val:.4f}') | |
| run.font.size = Pt(font_size) | |
| for i in range(n_topics): | |
| cell = tab.cell(i+2, 0) | |
| par = cell.paragraphs[0] | |
| par.alignment = WD_ALIGN_PARAGRAPH.CENTER | |
| run = par.add_run(f'{i+1}') | |
| run.bold = True | |
| run.font.size = Pt(font_size) | |
| for j in range(n_topics): | |
| cell = tab.cell(i+2, j+1) | |
| par = cell.paragraphs[0] | |
| par.alignment = WD_ALIGN_PARAGRAPH.CENTER | |
| val = st.session_state['trans'][i,j] | |
| run = par.add_run(f'{val:.4f}') | |
| run.font.size = Pt(font_size) | |
| if st.session_state['ready'] in markup_cases: | |
| docs = st.session_state['docs'] | |
| for i in range(len(docs)): | |
| wdoc.add_heading(docs[i]['name'], level=1) | |
| p = wdoc.add_paragraph() | |
| if st.session_state['ready'] not in ['HMM','SentHMM']: | |
| prev,k0 = 0,-1 | |
| for j,pos in enumerate(st.session_state['docs'][i]['seq']): | |
| tok = st.session_state['docs'][i]['tokens'][j] | |
| r0 = p.add_run(st.session_state['docs'][i]['text'][prev:tok.start]) | |
| temp = st.session_state['phi'][pos,:] * st.session_state['theta'][:,i] | |
| k = np.argmax(temp) % len(rgb) | |
| r = p.add_run(st.session_state['docs'][i]['text'][tok.start:tok.stop]) | |
| if (temp / temp.sum()).max() > 5/n_topics: | |
| if k == k0: | |
| r0.font.color.rgb = RGBColor(255,255,255) | |
| r0.font.highlight_color = colors[k] | |
| elif k0 >= 0 and not any(c.isalpha() for c in r0.text): | |
| r0.font.color.rgb = RGBColor(255,255,255) | |
| r0.font.highlight_color = colors[k0] | |
| r.font.color.rgb = RGBColor(255,255,255) | |
| r.font.highlight_color = colors[k] | |
| k0 = k | |
| else: | |
| k0 = -1 | |
| prev = tok.stop | |
| r = p.add_run(st.session_state['docs'][i]['text'][prev:]) | |
| else: | |
| prev = 0 | |
| k = 0 if len(st.session_state['labels'][i]) == 0 else st.session_state['labels'][i][0] | |
| for j,sent in enumerate(st.session_state['docs'][i]['sents']): | |
| r = p.add_run(st.session_state['docs'][i]['text'][prev:sent.start]) | |
| r.font.color.rgb = RGBColor(255,255,255) | |
| r.font.highlight_color = colors[k] | |
| if any(st.session_state['docs'][i]['parts'] == j): | |
| k = mode(st.session_state['labels'][i][st.session_state['docs'][i]['parts'] == j]).mode[0] % len(colors) | |
| r = p.add_run(st.session_state['docs'][i]['text'][sent.start:sent.stop]) | |
| r.font.color.rgb = RGBColor(255,255,255) | |
| r.font.highlight_color = colors[k] | |
| prev = sent.stop | |
| r = p.add_run(st.session_state['docs'][i]['text'][prev:]) | |
| r.font.color.rgb = RGBColor(255,255,255) | |
| r.font.highlight_color = colors[k] | |
| wdoc.save('Разметка.docx') | |
| with open('Разметка.docx','rb') as f: | |
| st.session_state['markup'] = f.read() | |
| nwt = st.session_state['phi'] * (st.session_state['theta'] * st.session_state['counts'].sum(axis=1)).sum(axis=1) | |
| df = pd.DataFrame(nwt / nwt.sum(axis=1)[...,np.newaxis]) | |
| df.index = st.session_state['words'] | |
| st.session_state['phi_df'] = df | |
| st.session_state['phi_df'].columns = np.arange(len(st.session_state['phi_df'].columns))+1 | |
| df = pd.DataFrame(st.session_state['theta'].transpose()) | |
| df.index = [d['name'] for d in st.session_state['docs']] | |
| st.session_state['theta_df'] = df | |
| st.session_state['theta_df'].columns = np.arange(len(st.session_state['theta_df'].columns))+1 | |
| def init_phi_theta(n_topics, w = 1, random_state=None): | |
| counts = st.session_state['counts'] | |
| mask = np.any(counts, axis=1) | |
| X = counts[mask] / counts[mask].sum(axis=1)[...,np.newaxis] | |
| labels = np.zeros((counts.shape[0],), dtype=int) | |
| kmeans = SphericalKMeans(n_clusters=n_topics, random_state=random_state).fit(csr_matrix(X)) | |
| labels[mask] = kmeans.labels_ | |
| if byword: | |
| labels[mask] = np.load('labels.npy')[0] | |
| sub0 = np.argsort([counts.sum(axis=1)[labels == i].sum() for i in range(n_topics)]) | |
| sub = sub0.copy() | |
| sub[sub0] = np.arange(n_topics) | |
| labels = sub[labels] | |
| theta = w * np.ones((n_topics, counts.shape[0])) | |
| for i in range(theta.shape[1]): | |
| theta[labels[i],i] += 1 | |
| phi = (theta @ counts).transpose() | |
| phi = phi / phi.sum(axis=0) | |
| theta = theta / theta.sum(axis=0) | |
| return phi, theta | |
| def calc_metrics(counts, phi, theta): | |
| corr = correlation(phi) | |
| coh = coherence(phi) | |
| div = diversity(phi) | |
| return ({'Перплексия': perplexity(counts, phi, theta)} if theta is not None else {}) | \ | |
| {'Корреляция': corr, 'Когерентность': coh, 'Разнообразие': div} | |
| def main(): | |
| st.title('Тематическое моделирование') | |
| if 'ready' not in st.session_state: | |
| st.session_state['ready'] = '' | |
| if 'metrics' not in st.session_state: | |
| st.session_state['metrics'] = {} | |
| with st.sidebar: | |
| doc_fn = st.file_uploader('Загрузить документы', type='txt') | |
| stop_fn = st.file_uploader('Загрузить стоп-слова', type='txt') | |
| norm_cb = st.checkbox('Перевести в современную орфографию', value=True) | |
| lemma_cb = st.checkbox('Сделать лемматизацию', value=False) | |
| vocab_btn = st.button('Получить словарь') | |
| pr_pbar = st.progress(0.0, text='Подготовка словаря:') | |
| random_state = st.number_input('Cлучайное зерно', step=1) | |
| if doc_fn: | |
| if 'doc_id' not in st.session_state or st.session_state['doc_id'] != doc_fn.file_id: | |
| st.session_state['doc_id'] = doc_fn.file_id | |
| docs = StringIO(doc_fn.getvalue().decode('utf-8')) | |
| docs = docs.read() | |
| docs = [d.strip('\r').split('\t') for d in docs.split('\n')] | |
| docs = [{'name': d[0], 'date': d[1], 'text_orig': d[2]} for d in docs if d[0]] | |
| st.session_state['docs'] = docs | |
| st.session_state['ready'] = '' | |
| st.session_state['words'] = [] | |
| else: | |
| docs = st.session_state['docs'] | |
| else: | |
| docs,st.session_state['docs'],st.session_state['words'] = [],[],[] | |
| if stop_fn: | |
| if 'stop_id' not in st.session_state or st.session_state['stop_id'] != stop_fn.file_id: | |
| st.session_state['stop_id'] = stop_fn.file_id | |
| stopwords = StringIO(stop_fn.getvalue().decode('utf-8')) | |
| stopwords = stopwords.read().split() | |
| st.session_state['stopwords'] = stopwords | |
| st.session_state['ready'] = '' | |
| else: | |
| stopwords = st.session_state['stopwords'] | |
| else: | |
| stopwords,st.session_state['stopwords'] = [],[] | |
| if vocab_btn and docs: | |
| prepare_vocab(norm_cb, lemma_cb, pr_pbar) | |
| col1, col2 = st.columns([0.33, 0.67]) | |
| with col1: | |
| tab1,tab2,tab3,tab4,tab5,tab6 = st.tabs(['LDA','ARTM','HMM','CTM','BERTopic','Top2Vec']) | |
| with tab1: # LDA | |
| n_topics = st.slider('Число тем', min_value=2, max_value=100, value=10) | |
| theta_reg = st.slider('Коэффициент сглаживания тем в документах', min_value=0.01, max_value=1.0, value=0.1, step=0.01) | |
| phi_reg = st.slider('Коэффициент сглаживания слов в темах', min_value=0.01, max_value=1.0, value=0.1, step=0.01) | |
| max_iter = st.slider('Число итераций', min_value=1, max_value=1000, value=100) | |
| empty_msg = 'Извлечение тем \nПолная перплексия \nПерплексия \nКорреляция \nКогерентность \nРазнообразие' | |
| if st.button('Извлечь темы') and docs: | |
| tm_pbar = st.progress(0.0, text=empty_msg) | |
| if 'words' not in st.session_state or len(st.session_state['words']) == 0: | |
| prepare_vocab(norm_cb, lemma_cb, pr_pbar) | |
| lda = LatentDirichletAllocation(n_components=n_topics, doc_topic_prior=theta_reg, topic_word_prior=phi_reg, \ | |
| max_iter=max_iter, evaluate_every=1, perp_tol=0.01, verbose=True, random_state=random_state) | |
| sys.stdout_old = sys.stdout | |
| sys.stdout = StringIO() | |
| t = Thread(target=lda.fit_transform, args=(st.session_state['counts'],)) | |
| t.start() | |
| while t.is_alive(): | |
| time.sleep(0.1) | |
| text = sys.stdout.getvalue() | |
| m = re.findall('iteration: ([0-9]+) of max_iter: ([0-9]+), perplexity: ([0-9]+.?[0-9]+)', text) | |
| if m: | |
| phi = lda.components_.transpose() | |
| phi = phi / phi.sum(axis=0) | |
| theta = lda.transform(st.session_state['counts']).transpose() | |
| metrics = {'Полная перплексия': float(m[-1][2])} | calc_metrics(st.session_state['counts'].transpose(), phi, theta) | |
| st.session_state['progress_msg'] = f'Извлечение тем: {m[-1][0]}/{m[-1][1]} \n' + \ | |
| ''.join(f'{k}: {v:.4f} \n' for k,v in metrics.items()) | |
| tm_pbar.progress(int(m[-1][0])/int(m[-1][1]), text=st.session_state['progress_msg']) | |
| t.join() | |
| text = sys.stdout.getvalue() | |
| sys.stdout = sys.stdout_old | |
| phi = lda.components_.transpose() | |
| phi = phi / phi.sum(axis=0) | |
| theta = lda.transform(st.session_state['counts']).transpose() | |
| m = re.findall('iteration: ([0-9]+) of max_iter: ([0-9]+), perplexity: ([0-9]+.?[0-9]+)', text) | |
| metrics = {'Полная перплексия': float(m[-1][2]) if m else 1.0} | |
| metrics = metrics | calc_metrics(st.session_state['counts'].transpose(), phi, theta) | |
| st.session_state['progress_msg'] = f'Извлечение тем: {m[-1][0] if m else max_iter}/{max_iter} \n' + \ | |
| ''.join(f'{k}: {v:.4f} \n' for k,v in metrics.items()) | |
| tm_pbar.progress(1.0, text=st.session_state['progress_msg']) | |
| #theta_emb = PCA(n_components=2).fit_transform(st.session_state['counts']) | |
| theta_emb = UMAP(n_components=2).fit_transform(st.session_state['counts']) | |
| #theta_emb = TSNE(n_components=2).fit_transform(st.session_state['counts']) | |
| st.session_state['theta_emb'] = theta_emb | |
| st.session_state['phi'] = phi | |
| st.session_state['theta'] = theta | |
| st.session_state['ready'] = 'LDA' | |
| fill_result() | |
| else: | |
| tm_pbar = st.progress(1.0 if st.session_state['ready'] == 'LDA' else 0.0, \ | |
| text=st.session_state['progress_msg'] if st.session_state['ready'] == 'LDA' else empty_msg) | |
| with tab2: # ARTM | |
| n_topics = st.slider('Число тем', min_value=2, max_value=100, value=10, key='n_topics2') | |
| theta_reg = st.slider('Коэффициент сглаживания тем в документах', min_value=-10.0, max_value=10.0, value=0.0, step=0.01, \ | |
| key='theta_reg2') | |
| phi_reg = st.slider('Коэффициент сглаживания слов в темах',min_value=-10.0, max_value=10.0, value=0.0, step=0.01, key='phi_reg2') | |
| decorr_reg = st.slider('Коэффициент контрастирования тем',min_value=0, max_value=100000, value=0, step=100, key='decorr_reg2') | |
| emb_reg = st.slider('Коэффициент сглаживания по синонимам',min_value=0, max_value=1000, value=0, step=1, key='emb_reg2') | |
| max_iter = st.slider('Число итераций', min_value=1, max_value=1000, value=100, key='max_iter2') | |
| empty_msg = 'Извлечение тем \nПерплексия \nКорреляция \nКогерентность \nРазнообразие' | |
| if st.button('Извлечь темы', key='run_tm2') and docs: | |
| tm_pbar2 = st.progress(0.0, text=empty_msg) | |
| if 'words' not in st.session_state or len(st.session_state['words']) == 0: | |
| prepare_vocab(norm_cb, lemma_cb, pr_pbar) | |
| word_sims = st.session_state['word_sims'] | |
| word_sims = np.nan_to_num(word_sims, nan=0.0) | |
| word_sims -= np.diag(word_sims.sum(axis=1)) | |
| phi,theta = init_phi_theta(n_topics, w=1, random_state=random_state) | |
| metrics = calc_metrics(st.session_state['counts'].transpose(), phi, theta) | |
| st.session_state['progress_msg'] = f'Извлечение тем: 0/{max_iter} \n' + \ | |
| ''.join(f'{k}: {v:.4f} \n' for k,v in metrics.items()) | |
| tm_pbar2.progress(0.0, text=st.session_state['progress_msg']) | |
| part = 100 | |
| for i in range(max_iter): | |
| phi0 = np.zeros_like(phi) | |
| theta0 = np.zeros_like(theta) | |
| for a in range((phi.shape[0]-1) // part + 1): | |
| it = [a*part, min((a+1)*part, phi.shape[0])] | |
| phi1 = phi[it[0]:it[1],:] | |
| for b in range((theta.shape[1]-1) // part + 1): | |
| jt = [b*part, min((b+1)*part, theta.shape[1])] | |
| theta1 = theta[:,jt[0]:jt[1]] | |
| ntdw = phi1[...,np.newaxis] * theta1.reshape((1,) + theta1.shape) | |
| ntdw = ntdw / ntdw.sum(axis=1)[:,np.newaxis,:] | |
| ntdw[np.isnan(ntdw)] = 1/n_topics | |
| ntdw = st.session_state['counts'][jt[0]:jt[1],it[0]:it[1]].transpose()[:,np.newaxis,:] * ntdw | |
| phi0[it[0]:it[1],:] += ntdw.sum(axis=2) | |
| theta0[:,jt[0]:jt[1]] += ntdw.sum(axis=0) | |
| #ntdw = phi[...,np.newaxis] * theta.reshape((1,) + theta.shape) | |
| #ntdw = ntdw / ntdw.sum(axis=1)[:,np.newaxis,:] | |
| #ntdw[np.isnan(ntdw)] = 1/n_topics | |
| #ntdw = st.session_state['counts'].transpose()[:,np.newaxis,:] * ntdw | |
| phi = phi0 + phi_reg | |
| if decorr_reg > 0 or emb_reg > 0: | |
| phi0 = phi0 / phi0.sum(axis=0) | |
| phi0[np.isnan(phi0)] = 1/phi0.shape[0] | |
| if decorr_reg > 0: | |
| phi -= decorr_reg*phi0*(phi0.sum(axis=1)[...,np.newaxis]-phi0) | |
| if emb_reg > 0: | |
| #phi = (emb_reg * word_sims @ phi0 + phi0) / (emb_reg * word_sims.sum(axis=1)[...,np.newaxis] + 1) | |
| phi += emb_reg * phi0 * (word_sims @ phi0) | |
| phi = np.maximum(phi, 0) | |
| phi = phi / phi.sum(axis=0) | |
| phi[np.isnan(phi)] = 1/phi.shape[0] | |
| theta = np.maximum(theta0 + theta_reg, 0.0) | |
| theta = theta / theta.sum(axis=0) | |
| theta[np.isnan(theta)] = 1/theta.shape[0] | |
| metrics = calc_metrics(st.session_state['counts'].transpose(), phi, theta) | |
| st.session_state['progress_msg'] = f'Извлечение тем: {i+1}/{max_iter} \n' + \ | |
| ''.join(f'{k}: {v:.4f} \n' for k,v in metrics.items()) | |
| tm_pbar2.progress((i+1)/max_iter, text=st.session_state['progress_msg']) | |
| st.session_state['theta_emb'] = UMAP(n_components=2).fit_transform(st.session_state['counts']) | |
| st.session_state['phi'] = phi | |
| st.session_state['theta'] = theta | |
| st.session_state['ready'] = 'ARTM' | |
| fill_result() | |
| else: | |
| tm_pbar2 = st.progress(1.0 if st.session_state['ready'] == 'ARTM' else 0.0, \ | |
| text=st.session_state['progress_msg'] if st.session_state['ready'] == 'ARTM' else empty_msg) | |
| with tab3: # HMM | |
| n_topics = st.slider('Число тем', min_value=2, max_value=100, value=10, key='n_topics3') | |
| theta_reg = st.slider('Коэффициент сглаживания тем в документах', min_value=-10.0, max_value=10.0, value=0.0, step=0.01, \ | |
| key='theta_reg3') | |
| phi_reg = st.slider('Коэффициент сглаживания слов в темах', min_value=-10.0, max_value=10.0, value=0.0, step=0.01, \ | |
| key='phi_reg3') | |
| mean_len = st.slider('Средняя длина темы', min_value=2, max_value=100, value=10) | |
| max_iter = st.slider('Число итераций', min_value=0, max_value=1000, value=100, key='max_iter3') | |
| use_prev = st.checkbox('Стартовать с предыдущего результата', value=True, key='use_prev3') | |
| empty_msg = 'Извлечение тем \nПерплексия \nКорреляция \nКогерентность \nРазнообразие' | |
| if st.button('Извлечь темы', key='run_tm3') and docs: | |
| tm_pbar3 = st.progress(0.0, text=empty_msg) | |
| if 'words' not in st.session_state or len(st.session_state['words']) == 0: | |
| prepare_vocab(norm_cb, lemma_cb, pr_pbar) | |
| if st.session_state['ready'] in ['LDA','ARTM','HMM'] and use_prev: | |
| phi,theta = st.session_state['phi'],st.session_state['theta'] | |
| else: | |
| phi,theta = init_phi_theta(n_topics, w=1, random_state=random_state) | |
| emis = phi.transpose() | |
| if st.session_state['ready'] == 'HMM' and use_prev: | |
| start = st.session_state['start'] | |
| trans = st.session_state['trans'] | |
| else: | |
| start = np.ones((n_topics, )) / n_topics | |
| trans = np.ones((n_topics, n_topics)) + ((mean_len-1)*(n_topics-1)-1)*np.eye(n_topics) | |
| trans = trans / trans.sum(axis=1) | |
| use_lib = False | |
| st.session_state['use_lengths'] = True | |
| if use_lib: | |
| model = CategoricalHMM(n_components=n_topics, verbose=True, n_iter=max_iter, algorithm='map', init_params='') | |
| model.startprob_ = start | |
| model.transmat_ = trans | |
| model.emissionprob_ = emis | |
| model.startprob_prior = theta_reg + 1 | |
| model.transmat_prior = theta_reg + 1 | |
| model.emissionprob_prior = phi_reg + 1 | |
| sys.stderr_old = sys.stderr | |
| sys.stderr = StringIO() | |
| t = Thread(target=model.fit, args=(np.hstack(d['seq'] for d in docs).reshape(-1,1), \ | |
| [len(d['seq']) for d in docs if len(d['seq']) > 0])) | |
| t.start() | |
| while t.is_alive(): | |
| time.sleep(0.1) | |
| text = sys.stderr.getvalue() | |
| m,r = None,iter(text.split('\n')[::-1]) | |
| while (s := next(r, None)) is not None and not m: | |
| m0 = re.findall('([0-9]+)[ ]+([+-]?[0-9]+.?[0-9]+)', s) | |
| if m0: | |
| m = m0 | |
| if m: | |
| metrics = {'Полная перплексия': -float(m[-1][1])} | |
| metrics['Перплексия'] = perplexity_hmm(model.startprob_, model.transmat_, model.emissionprob_) | |
| metrics = metrics | calc_metrics(st.session_state['counts'].transpose(), model.emissionprob_.transpose(), None) | |
| st.session_state['progress_msg'] = f'Извлечение тем: {m[-1][0]}/{max_iter} \n' + \ | |
| ''.join(f'{k}: {v:.4f} \n' for k,v in metrics.items()) | |
| tm_pbar3.progress(int(m[-1][0])/max_iter, text=st.session_state['progress_msg']) | |
| t.join() | |
| text = sys.stderr.getvalue() | |
| sys.stderr = sys.stderr_old | |
| m,r = None,iter(text.split('\n')[::-1]) | |
| while (s := next(r, None)) is not None and not m: | |
| m0 = re.findall('([0-9]+) ([+-]?[0-9]+.?[0-9]+)', s) | |
| if m0: | |
| m = m0 | |
| metrics = {'Полная перплексия': -float(m[-1][1]) if m else 1} | |
| metrics['Перплексия'] = perplexity_hmm(model.startprob_, model.transmat_, model.emissionprob_) | |
| metrics = metrics | calc_metrics(st.session_state['counts'].transpose(), model.emissionprob_.transpose(), None) | |
| emis = model.emissionprob_ | |
| start = model.startprob_ | |
| trans = model.transmat_ | |
| st.session_state['progress_msg'] = f'Извлечение тем: {max_iter}/{max_iter} \n' + \ | |
| ''.join(f'{k}: {v:.4f} \n' for k,v in metrics.items()) | |
| tm_pbar3.progress(1.0, text=st.session_state['progress_msg']) | |
| for i in range(max_iter if not use_lib else 0): | |
| start0 = np.zeros_like(start) | |
| trans0 = np.zeros_like(trans) | |
| emis0 = np.zeros_like(emis) | |
| p = 0 | |
| for pos in range(len(docs)): | |
| seq = docs[pos]['seq'] | |
| if seq.size > 0: | |
| P = docs[pos]['parts'][:-1] != docs[pos]['parts'][1:] | |
| k = len(seq) | |
| V = np.zeros((k, emis.shape[1])) | |
| V[np.arange(k), seq] = 1 | |
| A = np.zeros((n_topics, k)) | |
| B = np.zeros((n_topics, k)) | |
| H = np.zeros((n_topics, n_topics, k)) | |
| for j in range(k): | |
| if j == 0: | |
| A[:,0] = start * emis[:,seq[0]] | |
| elif P[j-1]: | |
| A[:,j] = (A[:,j-1] @ trans) * emis[:,seq[j]] | |
| else: | |
| A[:,j] = A[:,j-1] * emis[:,seq[j]] | |
| q = A[:,j].max() | |
| A[:,j] /= q | |
| p += np.log(q) | |
| p += np.log(A[:,-1].sum()) | |
| B[:,-1] = 1 | |
| for j in range(k-2,-1,-1): | |
| if P[j]: | |
| B[:,j] = trans @ (B[:,j+1] * emis[:,seq[j+1]]) | |
| else: | |
| B[:,j] = B[:,j+1] * emis[:,seq[j+1]] | |
| B[:,j] /= B[:,j].max() | |
| G = A * B; | |
| G /= G.sum(axis=0) | |
| for j in range(k-1): | |
| if P[j]: | |
| H[:,:,j] = (A[:,j:j+1] @ B[:,j+1:j+2].transpose()) * trans * emis[:,seq[j+1]:seq[j+1]+1].transpose() | |
| H[:,:,j] /= H[:,:,j].sum() | |
| start0 += G[:,0] | |
| trans0 += H.sum(axis=2) | |
| emis0 += G @ V | |
| start0 += theta_reg | |
| trans0 += theta_reg | |
| start = start0 / start0.sum() | |
| trans = trans0 / trans0.sum(axis=1)[...,np.newaxis] | |
| emis = np.maximum(0, emis0 + phi_reg) | |
| emis /= emis.sum(axis=1)[...,np.newaxis] | |
| emis[np.isnan(emis)] = 1 / emis.shape[1] | |
| metrics = {'Перплексия': np.exp(-p / sum([len(d['seq']) for d in docs]))} | |
| metrics = metrics | calc_metrics(st.session_state['counts'].transpose(), emis.transpose(), theta=None) | |
| st.session_state['progress_msg'] = f'Извлечение тем: {i+1}/{max_iter} \n' + \ | |
| ''.join(f'{k}: {v:.4f} \n' for k,v in metrics.items()) | |
| tm_pbar3.progress((i+1)/max_iter, text=st.session_state['progress_msg']) | |
| st.session_state['theta_emb'] = UMAP(n_components=2).fit_transform(st.session_state['counts']) | |
| st.session_state['phi'] = emis.transpose() | |
| st.session_state['start'] = start | |
| st.session_state['trans'] = trans | |
| st.session_state['ready'] = 'HMM' | |
| fill_result() | |
| else: | |
| tm_pbar3 = st.progress(1.0 if st.session_state['ready'] == 'HMM' else 0.0, \ | |
| text=st.session_state['progress_msg'] if st.session_state['ready'] == 'HMM' else empty_msg) | |
| with tab4: # CTM | |
| n_topics = st.slider('Число тем', min_value=2, max_value=100, value=10, key='n_topics4') | |
| max_iter = st.slider('Число итераций', min_value=1, max_value=1000, value=100, key='max_iter4') | |
| model_names = ['deeppavlov/rubert-base-cased-sentence', | |
| 'ai-forever/sbert_large_mt_nlu_ru', | |
| 'ai-forever/sbert_large_nlu_ru', | |
| 'ai-forever/ru-en-RoSBERTa', | |
| 'ai-forever/FRIDA']; | |
| model_name = st.selectbox("Выберите модель", model_names, key='model_name4') | |
| qt = TopicModelDataPreparation(model_name, max_seq_length = 512) | |
| empty_msg = 'Извлечение тем \nОшибка \nПерплексия \nКорреляция \nКогерентность \nРазнообразие' | |
| if st.button('Извлечь темы', key='run_tm4') and docs: | |
| if 'words' not in st.session_state or len(st.session_state['words']) == 0: | |
| prepare_vocab(norm_cb, lemma_cb, pr_pbar) | |
| tm_pbar4 = st.progress(0.0, text='Подготовка данных') | |
| #training_dataset = qt.fit(text_for_contextual=[d['text'] for d in docs], \ | |
| # text_for_bow=[d['input'] for d in docs]) | |
| sys.stderr_old = sys.stderr | |
| sys.stderr = StringIO() | |
| vectorizer = CountVectorizer(token_pattern=token_pattern_filt, stop_words=st.session_state['stopwords']) | |
| t = ReturnableThread(target=tm_data_prep_fit, args=(qt, [d['text'] for d in docs], [d['input'] for d in docs], vectorizer)) | |
| t.start() | |
| while t.is_alive(): | |
| time.sleep(0.1) | |
| text = sys.stderr.getvalue() | |
| m = re.findall('([0-9]+)/([0-9]+)', text) | |
| if m: | |
| tm_pbar4.progress(min(1.0, (int(m[-1][0])+1)/int(m[-1][1])), text=f'Подготовка данных: {int(m[-1][0])+1}/{m[-1][1]}') | |
| t.join() | |
| training_dataset = t.result | |
| if m: | |
| tm_pbar4.progress(1.0, text=f'Подготовка данных: {int(m[-1][1])+1}/{m[-1][1]}') | |
| tm_pbar4.progress(0.0, text=empty_msg) | |
| st.session_state['topic_model'] = CombinedTM(bow_size=len(qt.vocab), contextual_size=768, n_components=n_topics, \ | |
| num_data_loader_workers=0, num_epochs=max_iter) | |
| sys.stderr_old = sys.stderr | |
| sys.stderr = StringIO() | |
| t = Thread(target=st.session_state['topic_model'].fit, args=(training_dataset,)) | |
| t.start() | |
| while t.is_alive(): | |
| time.sleep(0.1) | |
| text = sys.stderr.getvalue() | |
| m,r = None,iter(text.split('\r')[::-1]) | |
| while (s := next(r, None)) is not None and not m: | |
| m0 = re.findall('Epoch: \[([0-9]+)/([0-9]+)\].*Train Loss: ([0-9]+.?[0-9]+)', s) | |
| if m0: | |
| m = m0 | |
| if m: | |
| #lda_vis_data = st.session_state['topic_model'].get_ldavis_data_format(qt.vocab, training_dataset, n_samples=10000) | |
| #theta = lda_vis_data['doc_topic_dists'].transpose() | |
| #phi = torch.softmax(20*st.session_state['topic_model'].best_components, axis=1).detach().cpu().numpy().transpose() | |
| metrics = {'Ошибка': float(m[-1][2])} #| calc_metrics(st.session_state['counts'].transpose(), phi, theta) | |
| st.session_state['progress_msg'] = f'Извлечение тем: {m[-1][0]}/{m[-1][1]} \n' + \ | |
| ''.join(f'{k}: {v:.4f} \n' for k,v in metrics.items()) | |
| tm_pbar4.progress(int(m[-1][0])/int(m[-1][1]), text=st.session_state['progress_msg']) | |
| t.join() | |
| st.session_state['lda_vis_data'] = st.session_state['topic_model'].get_ldavis_data_format(qt.vocab, training_dataset, \ | |
| n_samples=10000) | |
| st.session_state['theta'] = st.session_state['lda_vis_data']['doc_topic_dists'].transpose() | |
| st.session_state['phi'] = torch.softmax(20*st.session_state['topic_model'].best_components, \ | |
| axis=1).detach().cpu().numpy().transpose() | |
| #phi = (st.session_state['theta'] @ st.session_state['counts']).transpose() | |
| text = sys.stderr.getvalue() | |
| sys.stderr = sys.stderr_old | |
| m,r = None,iter(text.split('\r')[::-1]) | |
| while (s := next(r, None)) is not None and not m: | |
| m0 = re.findall('Epoch: \[([0-9]+)/([0-9]+)\].*Train Loss: ([0-9]+.?[0-9]+)', s) | |
| if m0: | |
| m = m0 | |
| metrics = {'Ошибка': float(m[-1][2]) if m else 0.0} | calc_metrics(st.session_state['counts'].transpose(), \ | |
| st.session_state['phi'], st.session_state['theta']) | |
| st.session_state['progress_msg'] = f'Извлечение тем: {m[-1][0] if m else max_iter}/{max_iter} \n' + \ | |
| ''.join(f'{k}: {v:.4f} \n' for k,v in metrics.items()) | |
| tm_pbar4.progress(1.0, text=st.session_state['progress_msg']) | |
| st.session_state['ready'] = 'CTM' | |
| fill_result() | |
| else: | |
| tm_pbar4 = st.progress(1.0 if st.session_state['ready'] == 'CTM' else 0.0, \ | |
| text=st.session_state['progress_msg'] if st.session_state['ready'] == 'CTM' else empty_msg) | |
| with tab5: # BERTopic | |
| n_topics = st.slider('Число тем', min_value=2, max_value=100, value=10, key='n_topics5') | |
| n_mix = st.slider('Число гауссиан', min_value=1, max_value=10, value=3, key='n_mix5') | |
| mean_len = st.slider('Средняя длина темы', min_value=2, max_value=100, value=5, key='mean_len5') | |
| max_iter = st.slider('Число итераций', min_value=0, max_value=1000, value=0, key='max_iter5') | |
| model_names = ['deeppavlov/rubert-base-cased-sentence', | |
| 'ai-forever/sbert_large_mt_nlu_ru', | |
| 'ai-forever/sbert_large_nlu_ru', | |
| 'ai-forever/ru-en-RoSBERTa', | |
| 'ai-forever/FRIDA']; | |
| model_name = st.selectbox("Выберите модель", model_names, key='model_name5') | |
| empty_msg = 'Извлечение тем \nПерплексия \nКорреляция \nКогерентность \nРазнообразие' | |
| if st.button('Извлечь темы', key='run_tm5') and docs: | |
| tm_pbar5 = st.progress(0.0, text=empty_msg) | |
| if 'words' not in st.session_state or len(st.session_state['words']) == 0: | |
| prepare_vocab(norm_cb, lemma_cb, pr_pbar) | |
| tm_pbar5.progress(1/4, text=f'Загрузка модели: 1/4') | |
| embedding_model = SentenceTransformer(model_name) | |
| try: | |
| max_length = embedding_model[0].auto_model.embeddings.position_embeddings.num_embeddings | |
| embedding_model[0].max_seq_length = max_length | |
| embedding_model.tokenizer.model_max_length = max_length | |
| except: | |
| pass | |
| vectorizer_model = CountVectorizer(stop_words=stopwords, token_pattern=token_pattern_filt) | |
| topic_model = BERTopic(embedding_model=embedding_model, vectorizer_model=vectorizer_model, nr_topics=n_topics, \ | |
| min_topic_size=2 if max_iter > 0 else n_mix) | |
| topic_model.embedding_model = select_backend(topic_model.embedding_model, language=topic_model.language, \ | |
| verbose=topic_model.verbose) | |
| topic_model.umap_model.random_state = random_state | |
| st.session_state['topic_model'] = topic_model | |
| tm_pbar5.progress(2/4, text=f'Вычисление эмбеддингов документов: 2/4') | |
| embeddings = st.session_state['topic_model']._extract_embeddings([d['text'] for d in docs], method="document") | |
| st.session_state['embeddings'] = embeddings | |
| tm_pbar5.progress(3/4, text=f'Понижение размерности эмбеддингов: 3/4') | |
| umap_model = UMAP(n_neighbors=10, n_components=2, min_dist=0.0, metric="cosine", random_state=random_state) | |
| umap_model.fit(embeddings) | |
| st.session_state['reduced_embeddings'] = umap_model.embedding_ | |
| tm_pbar5.progress(4/4, text=f'Извлечение тем: 4/4') | |
| topics, probs = st.session_state['topic_model'].fit_transform([d['text'] for d in docs], embeddings=embeddings) | |
| n_topics = np.unique(topics).size | |
| st.session_state['use_lengths'] = False | |
| if max_iter > 0: | |
| if False: | |
| tm_pbar5.progress(0.0, text=f'Вычисление эмбеддингов предложений: 0/{max_iter}') | |
| embs = [] | |
| for i in range(len(docs)): | |
| embs.append(st.session_state['topic_model'].embedding_model.embed([s.text for s in docs[i]['sents']])) | |
| tm_pbar5.progress((i+1)/len(docs), text=f'Вычисление эмбеддингов предложений: {i+1}/{len(docs)}') | |
| embs = np.vstack(embs) | |
| tm_pbar5.progress(1/2, text=f'Понижение размерности эмбеддингов: 1/2') | |
| st.session_state['embeddings_sent'] = st.session_state['topic_model'].umap_model.transform(embs) | |
| st.session_state['reduced_embeddings_sent'] = umap_model.transform(embs) | |
| embs = st.session_state['embeddings_sent'] | |
| lengths = [len(d['sents']) for d in docs] | |
| idxs = np.hstack([(t+1)*np.ones(len(d['sents']),dtype='int') for d,t in \ | |
| zip(docs,st.session_state['topic_model'].topics_)]) | |
| tm_pbar5.progress(2/2, text=f'Вычисление начального приближения гауссиан: 2/2') | |
| gmms = [GaussianMixture(n_mix).fit(embs[idxs == i]) for i in range(idxs.max()+1)] | |
| weights = np.stack([gmm.weights_ for gmm in gmms]) | |
| means = np.stack([gmm.means_ for gmm in gmms]) | |
| covars = np.stack([gmm.covariances_ for gmm in gmms]) | |
| start = np.ones((n_topics, )) / n_topics | |
| trans = np.ones((n_topics, n_topics)) + ((mean_len-1)*(n_topics-1)-1)*np.eye(n_topics) | |
| trans = trans / trans.sum(axis=1) | |
| use_lib = False | |
| if use_lib: | |
| gmmhmm = GMMHMM(n_components=phi.shape[1], n_mix=n_mix, covariance_type='full', algorithm='map', \ | |
| n_iter=max_iter, verbose=True, init_params='', random_state=random_state) | |
| gmmhmm.weights_ = weights | |
| gmmhmm.means_ = means | |
| gmmhmm.startprob_ = start | |
| gmmhmm.transmat_ = trans | |
| sys.stderr_old = sys.stderr | |
| sys.stderr = StringIO() | |
| t = Thread(target=gmmhmm.fit, args=(embs, lengths if st.session_state['use_lengths'] else None)) | |
| t.start() | |
| while t.is_alive(): | |
| time.sleep(0.1) | |
| text = sys.stderr.getvalue() | |
| m,r = None,iter(text.split('\n')[::-1]) | |
| while (s := next(r, None)) is not None and not m: | |
| m0 = re.findall('([0-9]+)[ ]+([+-]?[0-9]+.?[0-9]+)', s) | |
| if m0: | |
| m = m0 | |
| if m: | |
| metrics = {'Полная перплексия': -float(m[-1][1])} | |
| st.session_state['progress_msg'] = f'Подгонка кластеров: {m[-1][0]}/{max_iter} \n' + \ | |
| ''.join(f'{k}: {v:.4f} \n' for k,v in metrics.items()) | |
| tm_pbar5.progress(int(m[-1][0])/max_iter, text=st.session_state['progress_msg']) | |
| t.join() | |
| text = sys.stderr.getvalue() | |
| sys.stderr = sys.stderr_old | |
| st.session_state['seq'] = gmmhmm.decode(embs, lengths if st.session_state['use_lengths'] else None)[1] | |
| st.session_state['start'] = gmmhmm.startprob_ | |
| st.session_state['trans'] = gmmhmm.transmat_ | |
| else: | |
| lengths = np.cumsum([0] + (lengths if st.session_state['use_lengths'] else [sum(lengths)])) | |
| emis = np.stack([np.exp(gmm.score_samples(embs)) for gmm in gmms]) | |
| for i in range(max_iter): | |
| start0 = np.zeros_like(start) | |
| trans0 = np.zeros_like(trans) | |
| emis0 = np.zeros_like(emis) | |
| p = 0 | |
| for idx0,idx1 in zip(lengths,lengths[1:]): | |
| k = idx1-idx0 | |
| A = np.zeros((n_topics, k)) | |
| B = np.zeros((n_topics, k)) | |
| H = np.zeros((n_topics, n_topics, k)) | |
| for j in range(k): | |
| if j == 0: | |
| A[:,0] = start * emis[:,j+idx0] | |
| else: | |
| A[:,j] = (A[:,j-1] @ trans) * emis[:,j+idx0] | |
| q = A[:,j].max() | |
| A[:,j] /= q | |
| p += np.log(q) | |
| p += np.log(A[:,-1].sum()) | |
| B[:,-1] = 1 | |
| for j in range(k-2,-1,-1): | |
| B[:,j] = trans @ (B[:,j+1] * emis[:,j+idx0]) | |
| B[:,j] /= B[:,j].max() | |
| G = A * B; | |
| G /= G.sum(axis=0) | |
| for j in range(k-1): | |
| H[:,:,j] = (A[:,j:j+1] @ B[:,j+1:j+2].transpose()) * trans * emis[:,idx0+j+1:idx0+j+2].transpose() | |
| H[:,:,j] /= H[:,:,j].sum() | |
| start0 += G[:,0] | |
| trans0 += H.sum(axis=2) | |
| emis0[:,idx0:idx1] = G | |
| metrics = {'Перплексия': np.exp(-p/lengths[-1])} | |
| st.session_state['progress_msg'] = f'Подгонка кластеров: {i+1}/{max_iter} \n' + \ | |
| ''.join(f'{k}: {v:.4f} \n' for k,v in metrics.items()) | |
| tm_pbar5.progress((i+1)/max_iter, text=st.session_state['progress_msg']) | |
| for t in range(n_topics): | |
| gmmfit(gmms[t], embs, emis0[t]) | |
| start = start0 / start0.sum() | |
| trans = trans0 / trans0.sum(axis=1)[...,np.newaxis] | |
| emis = np.stack([np.exp(gmm.score_samples(embs)) for gmm in gmms]) | |
| seq = [] | |
| for idx0,idx1 in zip(lengths,lengths[1:]): | |
| k = idx1-idx0 | |
| A = start * emis[:,idx0] | |
| M = np.zeros((n_topics, k-1), dtype=int) | |
| for j in range(1,k): | |
| A = A[...,np.newaxis] * trans | |
| M[:,j-1] = np.argmax(A, axis=0) | |
| A = A.max(axis=0) * emis[:,idx0+j] | |
| A /= A.max() | |
| res = [int(np.argmax(A))] | |
| for j in range(k-2,-1,-1): | |
| res = [M[res[0],j]] + res | |
| seq += res | |
| st.session_state['seq'] = np.array(seq) | |
| st.session_state['start'] = start | |
| st.session_state['trans'] = trans | |
| st.session_state['ready'] = 'SentHMM' | |
| fill_result() | |
| if use_lib: | |
| metrics = {'Полная перплексия': -float(m[-1][1]) if m else 1.0} | |
| metrics = metrics | {'Перплексия': perplexity_hmm(st.session_state['start'], st.session_state['trans'], \ | |
| st.session_state['phi'].transpose())} | |
| metrics = metrics | calc_metrics(st.session_state['counts'].transpose(), st.session_state['phi'], None) | |
| st.session_state['progress_msg'] = f'Извлечение тем: {m[-1][0] if use_lib and m else max_iter}/{max_iter} \n' + \ | |
| ''.join(f'{k}: {v:.4f} \n' for k,v in metrics.items()) | |
| tm_pbar5.progress(1.0, text=st.session_state['progress_msg']) | |
| else: | |
| theta = np.zeros((n_topics, len(topics))) | |
| for i in range(len(topics)): | |
| if topics[i] == -1: | |
| theta[0,i] = 1 | |
| else: | |
| theta[:,i] = (1-probs[i]) / (n_topics-1) | |
| theta[topics[i]+1,i] = probs[i] | |
| phi = (theta @ st.session_state['counts']).transpose() | |
| phi = phi / phi.sum(axis=0) | |
| counts0 = st.session_state['counts'] | |
| topics = np.array(st.session_state['topic_model'].topics_) | |
| counts = np.vstack([counts0[topics == i-1].sum(axis=0) for i in range(n_topics)]) | |
| ratio = counts.sum() / counts.sum(axis=0) | |
| scores = (counts/counts.sum(axis=1)[...,np.newaxis]) * np.log(1 + ratio/phi.shape[1]) | |
| topic_repr = {i-1: [(st.session_state['words'][j], scores[i,j]) for j in np.argsort(scores[i])[::-1][:10]] \ | |
| for i in range(n_topics)} | |
| st.session_state['ctfidf'] = scores.transpose() | |
| st.session_state['topic_model'].topic_representations_ = topic_repr | |
| st.session_state['phi'] = phi | |
| st.session_state['theta'] = theta | |
| st.session_state['ready'] = 'BERTopic' | |
| fill_result() | |
| metrics = calc_metrics(st.session_state['counts'].transpose(), st.session_state['phi'], st.session_state['theta']) | |
| st.session_state['progress_msg'] = f'Извлечение тем: 1/1 \n' + \ | |
| ''.join(f'{k}: {v:.4f} \n' for k,v in metrics.items()) | |
| tm_pbar5.progress(1.0, text=st.session_state['progress_msg']) | |
| else: | |
| tm_pbar5 = st.progress(1.0 if st.session_state['ready'] == ['BERTopic','SentHMM'] else 0.0, \ | |
| text=st.session_state['progress_msg'] if st.session_state['ready'] in ['BERTopic','SentHMM'] else empty_msg) | |
| with tab6: # Top2Vec | |
| n_topics = st.slider('Число тем', min_value=2, max_value=100, value=10, key='n_topics6') | |
| min_cluster_size = st.slider('Размер кластера', min_value=2, max_value=100, value=15, key='min_cluster_size6') | |
| min_count = st.slider('Минимальное число вхождений слова', min_value=1, max_value=100, value=10, key='min_count6') | |
| model_names = ['deeppavlov/rubert-base-cased-sentence', | |
| 'ai-forever/sbert_large_mt_nlu_ru', | |
| 'ai-forever/sbert_large_nlu_ru', | |
| 'ai-forever/ru-en-RoSBERTa', | |
| 'ai-forever/FRIDA', | |
| 'doc2vec' | |
| ]; | |
| model_name = st.selectbox("Выберите модель", model_names, key='model_name6') | |
| empty_msg = 'Извлечение тем \nПерплексия \nКорреляция \nКогерентность \nРазнообразие' | |
| if st.button('Извлечь темы', key='run_tm6') and docs: | |
| tm_pbar6 = st.progress(0.0, text='Извлечение тем') | |
| if 'words' not in st.session_state or len(st.session_state['words']) == 0: | |
| prepare_vocab(norm_cb, lemma_cb, pr_pbar) | |
| umap_args = {'n_neighbors': 15, 'n_components': 5, 'metric': 'cosine', 'random_state': random_state} | |
| hdbscan_args = {'min_cluster_size': min_cluster_size, 'metric': 'euclidean', 'cluster_selection_method': 'eom'} | |
| def tokenizer(doc): | |
| return [w for w in re.findall(re.compile(token_pattern_filt), doc.lower()) if w not in stopwords] | |
| documents = [d['input'] for d in docs] | |
| if True: | |
| sys.stderr_old = sys.stderr | |
| sys.stderr = StringIO() | |
| t = ReturnableThread(target=Top2VecNew, kwargs={'embedding_model': model_name, 'documents': documents, \ | |
| 'umap_args':umap_args, 'hdbscan_args':hdbscan_args, \ | |
| 'tokenizer':tokenizer,'min_count':min_count}) | |
| t.start() | |
| while t.is_alive(): | |
| time.sleep(0.1) | |
| text = sys.stderr.getvalue() | |
| desc = ['Pre-processing documents for training', | |
| 'Downloading', | |
| 'Creating joint document/word embedding', | |
| 'Creating lower dimension embedding of documents', | |
| 'Finding dense areas of documents', | |
| 'Finding topics'] | |
| msg = ['Предобработка документов для обучения', | |
| 'Загрузка модели', | |
| 'Создание совместного эмбеддинга документов/словаря', | |
| 'Создание эмбеддингов документов низшей размерности', | |
| 'Поиск областей документов с высокой плотностью', | |
| 'Извлечение тем'] | |
| pos = [text.find(d) for d in desc] | |
| if max(pos) >= 0: | |
| idx = pos.index(max(pos)) | |
| tm_pbar6.progress((idx+1)/6, f'{msg[idx]} ({idx+1}/6)') | |
| t.join() | |
| st.session_state['topic_model'] = t.result | |
| sys.stderr = sys.stderr_old | |
| else: | |
| st.session_state['topic_model'] = Top2VecNew(embedding_model=model_name, documents=documents, umap_args=umap_args, \ | |
| hdbscan_args=hdbscan_args, tokenizer=tokenizer,min_count=min_count) | |
| if n_topics < len(st.session_state['topic_model'].get_topics()[2]): | |
| st.session_state['topic_model'].hierarchical_topic_reduction(n_topics) | |
| doc_top = st.session_state['topic_model'].doc_top_reduced | |
| topic_word_scores = st.session_state['topic_model'].topic_word_scores_reduced | |
| topic_words = st.session_state['topic_model'].topic_words_reduced | |
| else: | |
| n_topics = len(st.session_state['topic_model'].get_topics()[2]) | |
| doc_top = st.session_state['topic_model'].doc_top | |
| topic_word_scores = st.session_state['topic_model'].topic_word_scores | |
| topic_words = st.session_state['topic_model'].topic_words | |
| phi = -10*np.ones((st.session_state['counts'].shape[1], n_topics)) | |
| theta = np.zeros((n_topics, st.session_state['counts'].shape[0])) | |
| for i,t in enumerate(doc_top): | |
| theta[t,i] = 1 | |
| for i in range(topic_word_scores.shape[0]): | |
| for j in range(topic_word_scores.shape[1]): | |
| idx = np.where(st.session_state['words'] == topic_words[i][j])[0] | |
| if idx: | |
| phi[idx[0],i] = topic_word_scores[i,j] | |
| phi = scipy.special.softmax(phi, axis=0) | |
| umap_args = {'n_neighbors': 15, 'n_components': 2, 'min_dist': 0.0, 'metric': 'cosine'} | |
| embeddings_2d = UMAP(**umap_args).fit_transform(st.session_state['topic_model']._get_document_vectors(norm=False)) | |
| st.session_state['topic_model'].embeddings_2d = embeddings_2d | |
| st.session_state['phi'] = phi | |
| st.session_state['theta'] = theta | |
| st.session_state['ready'] = 'Top2Vec' | |
| fill_result() | |
| metrics = calc_metrics(st.session_state['counts'].transpose(), st.session_state['phi'], st.session_state['theta']) | |
| st.session_state['progress_msg'] = f'Извлечение тем: 1/1 \n' + ''.join(f'{k}: {v:.4f} \n' for k,v in metrics.items()) | |
| tm_pbar6.progress(1.0, text=st.session_state['progress_msg']) | |
| else: | |
| tm_pbar6 = st.progress(1.0 if st.session_state['ready'] == 'Top2Vec' else 0.0, \ | |
| text=st.session_state['progress_msg'] if st.session_state['ready'] == 'Top2Vec' else empty_msg) | |
| with col2: | |
| tab21,tab22,tab23,tab24,tab25,tab26 = \ | |
| st.tabs(['Темы','Хронология слов','Хронология тем','Кластеризация','Слова по темам','Темы по документам']) | |
| with tab21: # Темы | |
| if len(st.session_state['words']) > 0: | |
| words = st.session_state['words'].tolist() | |
| col21a1,col21a2 = st.columns([0.23,0.77]) | |
| with col21a1: | |
| if st.session_state['ready']: | |
| topic_sizes = 100*st.session_state['theta'].sum(axis=1) / st.session_state['theta'].shape[1] | |
| topicnames = [f'Тема {i+1} ({topic_sizes[i]:.2f}%)' \ | |
| for i,_ in enumerate(st.session_state['topicnames'])] + ['Перплексия','По порядку'] | |
| sub = topicnames.index(st.radio('Темы', topicnames)) | |
| else: | |
| topicnames = ['По частоте','По алфавиту'] | |
| sub = topicnames.index(st.radio('Слова', topicnames)) | |
| n_topics = len(topicnames) - 2 | |
| with col21a2: | |
| par = f'<br>' | |
| for i in range(n_topics): | |
| par += f"""<t style="color:rgb(255,255,255);""" | |
| tn = st.session_state['topicnames'][i] | |
| par += f"""background-color:rgb({255*rgb[i][0]},{255*rgb[i][1]},{255*rgb[i][2]});">{tn}<br>""" | |
| st.markdown(par, unsafe_allow_html=True) | |
| col21b1,col21b2,col21b3 = st.columns([0.35,0.3,0.35]) | |
| with col21b1: | |
| if sub < n_topics: | |
| scores = st.session_state['phi' if st.session_state['ready'] != 'BERTopic' else 'ctfidf'][:,sub] | |
| word_idxs = np.argsort(-scores) | |
| elif sub == n_topics: | |
| scores = st.session_state['counts'].sum(axis=0) | |
| word_idxs = np.argsort(-scores) | |
| else: | |
| scores = st.session_state['counts'].sum(axis=0) | |
| word_idxs = np.arange(len(scores)) | |
| if sub < n_topics: | |
| options = [f'{words[i]} ({scores[i]:.4f})' for i in word_idxs] | |
| else: | |
| options = [f'{words[i]} ({scores[i]})' for i in word_idxs] | |
| st.selectbox('Слова темы', options) | |
| with col21b2: | |
| mask = st.session_state['counts'].sum(axis=0) >= st.session_state['word_min_count'] | |
| words_filt = [f'{s}' for s in st.session_state['words'][mask].tolist()] | |
| word = st.selectbox('Коллокант', words_filt) | |
| with col21b3: | |
| values = st.session_state['npmi'][words_filt.index(word)].squeeze() | |
| cands = zip(words_filt, values) | |
| cands = sorted(cands, key = lambda x: -x[1]) | |
| st.selectbox('Коллокатор', [f'{w} ({v:.4f})' for w,v in cands]) | |
| options = st.session_state['words'].tolist() | |
| filters = st.multiselect(f'Словарь корпуса ({len(options)})', options) | |
| if sub < n_topics: | |
| scores = st.session_state['theta'][sub,:] | |
| elif sub == n_topics: | |
| scores = st.session_state['perplexity'] if st.session_state['ready'] else -np.arange(len(docs)) | |
| else: | |
| scores = -np.arange(len(docs)) | |
| doc_idxs = np.argsort(-scores) | |
| filt_idxs = [words.index(f) for f in filters] | |
| doc_idxs = [i for i in doc_idxs if st.session_state['counts'][i,filt_idxs].all()] | |
| if sub <= n_topics and st.session_state['ready']: | |
| docnames = [docs[i]['name'] + f' ({scores[i]:.4f})' for i in doc_idxs] | |
| else: | |
| docnames = [docs[i]['name'] for i in doc_idxs] | |
| col21c1,col21c2 = st.columns([0.85,0.15]) | |
| with col21c1: | |
| docname = st.selectbox(f'Отфильтрованные документы ({len(docnames)})', docnames) | |
| if docname: | |
| idx = docnames.index(docname) | |
| idx = doc_idxs[idx] | |
| else: | |
| idx = None | |
| with col21c2: | |
| if st.session_state['ready'] in markup_cases: | |
| st.download_button('Скачать разметку', data=st.session_state['markup'], file_name='Разметка.docx') | |
| else: | |
| st.button('Скачать разметку', disabled=True) | |
| par = f'' | |
| if not st.session_state['ready'] and idx is not None: | |
| par += st.session_state['docs'][idx]['text'] | |
| if st.session_state['ready'] and st.session_state['ready'] not in ['HMM','SentHMM'] and idx is not None: | |
| prev,k0 = 0,-1 | |
| for i,pos in enumerate(st.session_state['docs'][idx]['seq']): | |
| tok = st.session_state['docs'][idx]['tokens'][i] | |
| add0 = st.session_state['docs'][idx]['text'][prev:tok.start] | |
| temp = st.session_state['phi'][pos,:] * st.session_state['theta'][:,idx] | |
| k = np.argmax(temp) % len(rgb) | |
| add = st.session_state['docs'][idx]['text'][tok.start:tok.stop] | |
| if (temp / temp.sum()).max() > 5/n_topics: | |
| if k == k0: | |
| par += f"""<t style="color:rgb(255,255,255);""" | |
| par += f"""background-color:rgb({255*rgb[k][0]},{255*rgb[k][1]},{255*rgb[k][2]});">{add0+add}</t>""" | |
| else: | |
| if k0 >= 0 and not any(c.isalpha() for c in add0): | |
| par += f"""<t style="color:rgb(255,255,255);""" | |
| par += f"""background-color:rgb({255*rgb[k0][0]},{255*rgb[k0][1]},{255*rgb[k0][2]});">{add0}</t>""" | |
| else: | |
| par += f"""<t>{add0}</t>""" | |
| par += f"""<t style="color:rgb(255,255,255);""" | |
| par += f"""background-color:rgb({255*rgb[k][0]},{255*rgb[k][1]},{255*rgb[k][2]});">{add}</t>""" | |
| k0 = k | |
| else: | |
| par += f"""<t>{add0+add}</t>""" | |
| k0 = -1 | |
| prev = tok.stop | |
| add = st.session_state['docs'][idx]['text'][prev:] | |
| par += f"""<t>{add}</t>""" | |
| elif st.session_state['ready'] and idx is not None: | |
| prev = 0 | |
| k = 0 if len(st.session_state['labels'][idx]) == 0 else st.session_state['labels'][idx][0] | |
| for i,sent in enumerate(st.session_state['docs'][idx]['sents']): | |
| add = st.session_state['docs'][idx]['text'][prev:sent.start].replace('.','\.') | |
| par += f"""<t style="color:rgb(255,255,255);""" | |
| par += f"""background-color:rgb({255*rgb[k][0]},{255*rgb[k][1]},{255*rgb[k][2]});">{add}</t>""" | |
| if any(st.session_state['docs'][idx]['parts'] == i): | |
| k = mode(st.session_state['labels'][idx][st.session_state['docs'][idx]['parts'] == i]).mode[0] % len(rgb) | |
| add = st.session_state['docs'][idx]['text'][sent.start:sent.stop].replace('.','\.') | |
| par += f"""<t style="color:rgb(255,255,255);""" | |
| par += f"""background-color:rgb({255*rgb[k][0]},{255*rgb[k][1]},{255*rgb[k][2]});">{add}</t>""" | |
| prev = sent.stop | |
| add = st.session_state['docs'][idx]['text'][prev:] | |
| par += f"""<t style="color:rgb(255,255,255);""" | |
| par += f"""background-color:rgb({255*rgb[k][0]},{255*rgb[k][1]},{255*rgb[k][2]});">{add}</t>""" | |
| st.markdown(par, unsafe_allow_html=True) | |
| with tab22: # Хронология слов | |
| if len(st.session_state['words']) > 0: | |
| dates = [d['date'] for d in st.session_state['docs']] | |
| dates = [datetime.date(int(d[6:]),int(d[3:5]),int(d[0:2])) for d in dates] | |
| span = [d.toordinal() for d in dates] | |
| x = np.arange(span[0], span[-1]+1) | |
| xt = [datetime.date.fromordinal(k) for k in x] | |
| options = [f'{w} ({c})' for w,c in zip(st.session_state['words'].tolist(), st.session_state['counts'].sum(axis=0))] | |
| filters = st.multiselect(f'Словарь корпуса ({len(options)})', options, key='multiselect22') | |
| filt_idxs = [options.index(f) for f in filters] | |
| fig,ax = plt.subplots() | |
| for idx in filt_idxs: | |
| y = np.bincount(span-x[0], weights=st.session_state['counts'][:,idx]) | |
| y = gaussian_filter1d(y, sigma=10) | |
| ax.plot(xt, y) | |
| ax.legend([st.session_state['words'][idx] for idx in filt_idxs], fontsize=7.5) | |
| ax.tick_params(axis='both', which='major', labelsize=7.5) | |
| ax.tick_params(axis='both', which='minor', labelsize=6) | |
| st.pyplot(fig) | |
| with tab23: # Хронология тем | |
| if st.session_state['ready']: | |
| n_topics = st.session_state['phi'].shape[1] | |
| data = np.zeros(st.session_state['phi'].shape) | |
| fig, axs = plt.subplots((n_topics + 1)//2, 2, figsize=(13, (n_topics+1)//2*4)) | |
| counts = st.session_state['counts'].sum(axis=1) | |
| top = (counts * st.session_state['theta']).max() | |
| for i in range(n_topics): | |
| val = counts * st.session_state['theta'][i] | |
| tn = st.session_state['topicnames'][i] | |
| y = np.bincount(span-x[0], weights=val) | |
| y = gaussian_filter1d(y, sigma=10) | |
| if n_topics > 2: | |
| axs[i//2, i%2].tick_params(axis='both', which='major', labelsize=10) | |
| axs[i//2, i%2].tick_params(axis='both', which='minor', labelsize=8) | |
| #axs[i//2, i%2].set_ylim(0, top) | |
| axs[i//2, i%2].set_title(f'Тема {i}: {tn}') | |
| axs[i//2, i%2].plot(xt, y, color=rgb[i]) | |
| else: | |
| axs[i].tick_params(axis='both', which='major', labelsize=10) | |
| axs[i].tick_params(axis='both', which='minor', labelsize=8) | |
| #axs[i].set_ylim(0, top) | |
| axs[i].set_title(f'Тема {i}: {tn}') | |
| axs[i].plot(xt, y, color=rgb[i]) | |
| st.pyplot(fig) | |
| with tab24: # Кластеризация | |
| if st.session_state['ready'] in ['LDA','ARTM','HMM']: | |
| topicnames = [tn[:[a for a in re.finditer(',',tn)][2].span()[0]] for tn in st.session_state['topicnames']] | |
| color_discrete_map={f'{topicnames.index(t)+1}: {t}':rgb2hex(np.array(r)) for t,r in zip(topicnames, rgb)} | |
| df = pd.DataFrame(np.hstack([st.session_state['theta_emb'],np.argmax(st.session_state['theta'], axis=0)[...,np.newaxis]]),\ | |
| columns=['x','y','z']) | |
| df['text'] = [d['text'] for d in docs] | |
| df.sort_values(by='z', inplace=True) | |
| df['z'] = df['z'].apply(lambda x: f'{int(x)+1}: {topicnames[int(x)]}') | |
| df['text'] = df['text'].apply(lambda x: x[:100]) | |
| fig = px.scatter( | |
| df, | |
| x='x', | |
| y='y', | |
| hover_name='text', | |
| color='z', | |
| height=640, | |
| #color_discrete_map=color_discrete_map, | |
| ) | |
| fig.update_traces(hovertemplate="%{hovertext}<extra></extra>") | |
| # Display the chart in Streamlit | |
| st.plotly_chart(fig) | |
| if st.session_state['ready'] == 'CTM': | |
| ctm_pd = vis.prepare(**st.session_state['lda_vis_data'], n_jobs=1) | |
| vis.display(ctm_pd) | |
| html_string = vis.prepared_data_to_html(ctm_pd) | |
| # with open('vis.html','w') as f: | |
| # f.write(html_string) | |
| components.v1.html(html_string, width=800, height=800, scrolling=True) | |
| if st.session_state['ready'] == 'BERTopic': | |
| fig = visualize_documents(st.session_state['topic_model'], [d['text'] for d in docs], | |
| topics = np.array(st.session_state['topic_model'].topics_) + 1, | |
| embeddings=st.session_state['embeddings'], | |
| reduced_embeddings=st.session_state['reduced_embeddings']) | |
| st.plotly_chart(fig) | |
| #fig.write_html('topics.html') | |
| if st.session_state['ready'] == 'SentHMM': | |
| #topics = [0] | |
| #for d,l in zip(docs,st.session_state['labels']): | |
| # for i in range(len(d['sents'])): | |
| # topics.append(mode(l[d['parts'] == i]).mode[0] if np.any(d['parts'] == i) else topics[-1]) | |
| #topics = topics[1:] | |
| topics = st.session_state['seq'] | |
| fig = visualize_documents(st.session_state['topic_model'], sum(([s.text for s in d['sents']] for d in docs), []), | |
| topics = topics, | |
| embeddings=st.session_state['embeddings_sent'], | |
| reduced_embeddings=st.session_state['reduced_embeddings_sent']) | |
| st.plotly_chart(fig) | |
| #fig.write_html('topics.html') | |
| if st.session_state['ready'] == 'Top2Vec': | |
| if st.session_state['topic_model'].doc_top_reduced is not None: | |
| df = pd.DataFrame(np.hstack([st.session_state['topic_model'].embeddings_2d, \ | |
| st.session_state['topic_model'].doc_top_reduced[...,np.newaxis]]), \ | |
| columns=['x','y','z']) | |
| else: | |
| df = pd.DataFrame(np.hstack([st.session_state['topic_model'].embeddings_2d, \ | |
| st.session_state['topic_model'].doc_top[...,np.newaxis]]), \ | |
| columns=['x','y','z']) | |
| topicnames = [tn[:[a for a in re.finditer(',',tn)][2].span()[0]] for tn in st.session_state['topicnames']] | |
| color_discrete_map={f'{topicnames.index(t)+1}: {t}':rgb2hex(np.array(r)) for t,r in zip(topicnames, rgb)} | |
| df['text'] = [d['text'] for d in docs] | |
| df.sort_values(by='z', inplace=True) | |
| df['z'] = df['z'].apply(lambda x: f'{int(x)+1}: {topicnames[int(x)]}') | |
| df['text'] = df['text'].apply(lambda x: x[:100]) | |
| fig = px.scatter( | |
| df, | |
| x='x', | |
| y='y', | |
| hover_name='text', | |
| color='z', | |
| height=640, | |
| #color_discrete_map=color_discrete_map, | |
| ) | |
| fig.update_traces(hovertemplate="%{hovertext}<extra></extra>") | |
| st.plotly_chart(fig) | |
| with tab25: # Слова по темам | |
| if st.session_state['ready'] and 'phi_df' in st.session_state: | |
| st.dataframe(st.session_state['phi_df'].style.format(precision=4), height=1024) | |
| with tab26: # Темы по документам | |
| if st.session_state['ready'] and 'theta_df' in st.session_state: | |
| st.dataframe(st.session_state['theta_df'].style.format(precision=4), height=1024) | |
| if __name__ == '__main__': | |
| try: | |
| main() | |
| except SystemExit: | |
| pass |