|
|
import streamlit as st |
|
|
import pandas as pd |
|
|
import numpy as np |
|
|
import matplotlib.pyplot as plt |
|
|
import seaborn as sns |
|
|
from sklearn.metrics import classification_report, confusion_matrix, roc_curve, auc, precision_recall_curve, precision_score, recall_score, f1_score |
|
|
import json |
|
|
|
|
|
|
|
|
try: |
|
|
from use_ml import predict_sentiment, predict_category, predict_categorys |
|
|
except ImportError: |
|
|
st.error("Модуль use_ml не найден") |
|
|
try: |
|
|
from use_nn import predict_sentiment as nn_predict_sentiment |
|
|
from use_nn import predict_category as nn_predict_category |
|
|
from use_nn import predict_categorys as nn_predict_categorys |
|
|
except ImportError: |
|
|
st.error("Модуль use_nn не найден") |
|
|
try: |
|
|
from use_transformer import predict_sentiment as tf_predict_sentiment |
|
|
from use_transformer import predict_category as tf_predict_category |
|
|
from use_transformer import predict_categorys as tf_predict_categorys |
|
|
except ImportError: |
|
|
st.error("Модуль use_transformer не найден") |
|
|
|
|
|
|
|
|
st.set_page_config( |
|
|
page_title="Анализ классификаторов текста", |
|
|
page_icon="📊", |
|
|
layout="wide" |
|
|
) |
|
|
|
|
|
|
|
|
def load_models(task_type): |
|
|
"""Загрузка моделей в зависимости от типа задачи""" |
|
|
models = {} |
|
|
|
|
|
if task_type == "Бинарная": |
|
|
try: |
|
|
models["Классическая ML"] = predict_sentiment() |
|
|
except: |
|
|
pass |
|
|
try: |
|
|
models["Нейросеть"] = nn_predict_sentiment() |
|
|
except: |
|
|
pass |
|
|
try: |
|
|
models["Трансформер"] = tf_predict_sentiment() |
|
|
except: |
|
|
pass |
|
|
|
|
|
elif task_type == "Многоклассовая": |
|
|
try: |
|
|
models["Классическая ML"] = predict_category() |
|
|
except: |
|
|
pass |
|
|
try: |
|
|
models["Нейросеть"] = nn_predict_category() |
|
|
except: |
|
|
pass |
|
|
try: |
|
|
models["Трансформер"] = tf_predict_category() |
|
|
except: |
|
|
pass |
|
|
|
|
|
elif task_type == "Многометочная": |
|
|
try: |
|
|
models["Классическая ML"] = predict_categorys() |
|
|
except: |
|
|
pass |
|
|
try: |
|
|
models["Нейросеть"] = nn_predict_categorys() |
|
|
except: |
|
|
pass |
|
|
try: |
|
|
models["Трансформер"] = tf_predict_categorys() |
|
|
except: |
|
|
pass |
|
|
|
|
|
return models |
|
|
|
|
|
|
|
|
def plot_probabilities(probs, labels, model_name): |
|
|
"""Визуализация вероятностей""" |
|
|
fig, ax = plt.subplots(figsize=(10, 6)) |
|
|
y_pos = np.arange(len(labels)) |
|
|
|
|
|
if isinstance(probs, (np.ndarray, list)) and len(probs) > 1: |
|
|
|
|
|
ax.barh(y_pos, probs, align='center') |
|
|
ax.set_yticks(y_pos) |
|
|
ax.set_yticklabels(labels) |
|
|
ax.set_xlabel('Вероятность') |
|
|
ax.set_title(f'Вероятности классов - {model_name}') |
|
|
else: |
|
|
|
|
|
binary_probs = [1 - probs, probs] if isinstance(probs, (int, float)) else [1 - probs[0], probs[0]] |
|
|
binary_labels = ['Negative', 'Positive'] |
|
|
ax.barh([0, 1], binary_probs, align='center') |
|
|
ax.set_yticks([0, 1]) |
|
|
ax.set_yticklabels(binary_labels) |
|
|
ax.set_xlabel('Вероятность') |
|
|
ax.set_title(f'Вероятности классов - {model_name}') |
|
|
|
|
|
plt.tight_layout() |
|
|
return fig |
|
|
|
|
|
|
|
|
def calculate_and_display_binary_metrics(true_labels, predictions): |
|
|
"""Расчет и отображение метрик для бинарной классификации""" |
|
|
|
|
|
y_true = [1 if label == 'positive' else 0 for label in true_labels] |
|
|
y_pred = [1 if pred['probs'] >= 0.5 else 0 for pred in predictions] |
|
|
y_scores = [pred['probs'] for pred in predictions] |
|
|
|
|
|
|
|
|
fpr, tpr, _ = roc_curve(y_true, y_scores) |
|
|
roc_auc = auc(fpr, tpr) |
|
|
|
|
|
|
|
|
precision, recall, _ = precision_recall_curve(y_true, y_scores) |
|
|
|
|
|
|
|
|
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6)) |
|
|
|
|
|
|
|
|
ax1.plot(fpr, tpr, color='darkorange', lw=2, label=f'ROC curve (AUC = {roc_auc:.2f})') |
|
|
ax1.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--') |
|
|
ax1.set_xlim([0.0, 1.0]) |
|
|
ax1.set_ylim([0.0, 1.05]) |
|
|
ax1.set_xlabel('False Positive Rate') |
|
|
ax1.set_ylabel('True Positive Rate') |
|
|
ax1.set_title('ROC Curve') |
|
|
ax1.legend(loc="lower right") |
|
|
|
|
|
|
|
|
ax2.plot(recall, precision, color='blue', lw=2) |
|
|
ax2.set_xlim([0.0, 1.0]) |
|
|
ax2.set_ylim([0.0, 1.05]) |
|
|
ax2.set_xlabel('Recall') |
|
|
ax2.set_ylabel('Precision') |
|
|
ax2.set_title('Precision-Recall Curve') |
|
|
|
|
|
st.pyplot(fig) |
|
|
|
|
|
|
|
|
cm = confusion_matrix(y_true, y_pred) |
|
|
fig, ax = plt.subplots(figsize=(8, 6)) |
|
|
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', ax=ax, |
|
|
xticklabels=['Negative', 'Positive'], |
|
|
yticklabels=['Negative', 'Positive']) |
|
|
ax.set_title('Confusion Matrix') |
|
|
ax.set_xlabel('Predicted') |
|
|
ax.set_ylabel('Actual') |
|
|
st.pyplot(fig) |
|
|
|
|
|
|
|
|
st.subheader("Отчет классификации") |
|
|
report = classification_report(y_true, y_pred, output_dict=True) |
|
|
report_df = pd.DataFrame(report).transpose() |
|
|
st.dataframe(report_df, use_container_width=True) |
|
|
|
|
|
|
|
|
def calculate_and_display_multiclass_metrics(true_labels, predictions): |
|
|
"""Расчет и отображение метрик для многоклассовой классификации""" |
|
|
|
|
|
all_classes = list(set(true_labels)) |
|
|
|
|
|
|
|
|
y_pred = [pred['labels'][np.argmax(pred['probs'])] for pred in predictions] |
|
|
y_true = true_labels |
|
|
|
|
|
|
|
|
cm = confusion_matrix(y_true, y_pred, labels=all_classes) |
|
|
fig, ax = plt.subplots(figsize=(10, 8)) |
|
|
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', ax=ax, |
|
|
xticklabels=all_classes, yticklabels=all_classes) |
|
|
ax.set_title('Confusion Matrix') |
|
|
ax.set_xlabel('Predicted') |
|
|
ax.set_ylabel('Actual') |
|
|
plt.xticks(rotation=45) |
|
|
plt.yticks(rotation=0) |
|
|
st.pyplot(fig) |
|
|
|
|
|
|
|
|
st.subheader("Отчет классификации") |
|
|
report = classification_report(y_true, y_pred, output_dict=True) |
|
|
report_df = pd.DataFrame(report).transpose() |
|
|
st.dataframe(report_df, use_container_width=True) |
|
|
|
|
|
|
|
|
class_report = classification_report(y_true, y_pred, output_dict=True) |
|
|
classes_metrics = {} |
|
|
for class_name in all_classes: |
|
|
if class_name in class_report: |
|
|
classes_metrics[class_name] = { |
|
|
'Precision': class_report[class_name]['precision'], |
|
|
'Recall': class_report[class_name]['recall'], |
|
|
'F1-Score': class_report[class_name]['f1-score'] |
|
|
} |
|
|
|
|
|
metrics_df = pd.DataFrame(classes_metrics).T |
|
|
fig, ax = plt.subplots(figsize=(12, 6)) |
|
|
metrics_df.plot(kind='bar', ax=ax) |
|
|
ax.set_title('Метрики по классам') |
|
|
ax.set_ylabel('Score') |
|
|
ax.legend(bbox_to_anchor=(1.05, 1), loc='upper left') |
|
|
plt.xticks(rotation=45) |
|
|
st.pyplot(fig) |
|
|
|
|
|
|
|
|
def calculate_and_display_multilabel_metrics(true_labels, predictions): |
|
|
"""Расчет и отображение метрик для многометочной классификации""" |
|
|
|
|
|
all_labels = predictions[0]['labels'] |
|
|
|
|
|
|
|
|
y_true_binary = np.zeros((len(true_labels), len(all_labels))) |
|
|
y_pred_binary = np.zeros((len(predictions), len(all_labels))) |
|
|
|
|
|
for i, (true_label_list, pred) in enumerate(zip(true_labels, predictions)): |
|
|
for j, label in enumerate(all_labels): |
|
|
|
|
|
if label in true_label_list: |
|
|
y_true_binary[i, j] = 1 |
|
|
|
|
|
|
|
|
if pred['probs'][j] >= 0.5: |
|
|
y_pred_binary[i, j] = 1 |
|
|
|
|
|
|
|
|
metrics_per_label = {} |
|
|
for j, label in enumerate(all_labels): |
|
|
metrics_per_label[label] = { |
|
|
'Precision': precision_score(y_true_binary[:, j], y_pred_binary[:, j]), |
|
|
'Recall': recall_score(y_true_binary[:, j], y_pred_binary[:, j]), |
|
|
'F1-Score': f1_score(y_true_binary[:, j], y_pred_binary[:, j]), |
|
|
'Support': np.sum(y_true_binary[:, j]) |
|
|
} |
|
|
|
|
|
|
|
|
st.subheader("Метрики по меткам") |
|
|
metrics_df = pd.DataFrame(metrics_per_label).T |
|
|
st.dataframe(metrics_df, use_container_width=True) |
|
|
|
|
|
|
|
|
fig, axes = plt.subplots(2, 2, figsize=(15, 10)) |
|
|
|
|
|
|
|
|
axes[0, 0].barh(range(len(all_labels)), [metrics_per_label[label]['Precision'] for label in all_labels]) |
|
|
axes[0, 0].set_yticks(range(len(all_labels))) |
|
|
axes[0, 0].set_yticklabels(all_labels) |
|
|
axes[0, 0].set_title('Precision по меткам') |
|
|
axes[0, 0].set_xlim(0, 1) |
|
|
|
|
|
|
|
|
axes[0, 1].barh(range(len(all_labels)), [metrics_per_label[label]['Recall'] for label in all_labels]) |
|
|
axes[0, 1].set_yticks(range(len(all_labels))) |
|
|
axes[0, 1].set_yticklabels(all_labels) |
|
|
axes[0, 1].set_title('Recall по меткам') |
|
|
axes[0, 1].set_xlim(0, 1) |
|
|
|
|
|
|
|
|
axes[1, 0].barh(range(len(all_labels)), [metrics_per_label[label]['F1-Score'] for label in all_labels]) |
|
|
axes[1, 0].set_yticks(range(len(all_labels))) |
|
|
axes[1, 0].set_yticklabels(all_labels) |
|
|
axes[1, 0].set_title('F1-Score по меткам') |
|
|
axes[1, 0].set_xlim(0, 1) |
|
|
|
|
|
|
|
|
axes[1, 1].barh(range(len(all_labels)), [metrics_per_label[label]['Support'] for label in all_labels]) |
|
|
axes[1, 1].set_yticks(range(len(all_labels))) |
|
|
axes[1, 1].set_yticklabels(all_labels) |
|
|
axes[1, 1].set_title('Support (количество примеров) по меткам') |
|
|
|
|
|
plt.tight_layout() |
|
|
st.pyplot(fig) |
|
|
|
|
|
|
|
|
st.subheader("Примеры предсказаний") |
|
|
sample_indices = np.random.choice(len(predictions), min(5, len(predictions)), replace=False) |
|
|
|
|
|
for idx in sample_indices: |
|
|
with st.expander(f"Пример {idx + 1}"): |
|
|
col1, col2 = st.columns(2) |
|
|
|
|
|
with col1: |
|
|
st.write("**Истинные метки:**") |
|
|
st.write(true_labels[idx]) |
|
|
|
|
|
with col2: |
|
|
st.write("**Предсказанные метки:**") |
|
|
predicted_labels = [all_labels[i] for i, prob in enumerate(predictions[idx]['probs']) if prob >= 0.5] |
|
|
st.write(predicted_labels) |
|
|
|
|
|
st.write("**Вероятности:**") |
|
|
prob_df = pd.DataFrame({ |
|
|
'Метка': all_labels, |
|
|
'Вероятность': predictions[idx]['probs'] |
|
|
}).sort_values('Вероятность', ascending=False) |
|
|
st.dataframe(prob_df, use_container_width=True) |
|
|
|
|
|
|
|
|
def process_test_file(uploaded_file, task_type): |
|
|
"""Обработка загруженного JSONL файла""" |
|
|
data = [] |
|
|
for line in uploaded_file: |
|
|
data.append(json.loads(line.decode('utf-8'))) |
|
|
|
|
|
df = pd.DataFrame(data) |
|
|
return df |
|
|
|
|
|
|
|
|
def calculate_metrics(df, predictions, task_type): |
|
|
"""Расчет метрик качества""" |
|
|
if task_type == "Бинарная": |
|
|
y_true = df['label'].apply(lambda x: 1 if x == 'positive' else 0) |
|
|
y_pred = [1 if pred['probs'] >= 0.5 else 0 for pred in predictions] |
|
|
y_scores = [pred['probs'] for pred in predictions] |
|
|
|
|
|
|
|
|
fpr, tpr, _ = roc_curve(y_true, y_scores) |
|
|
roc_auc = auc(fpr, tpr) |
|
|
|
|
|
|
|
|
precision, recall, _ = precision_recall_curve(y_true, y_scores) |
|
|
|
|
|
return { |
|
|
'fpr': fpr, |
|
|
'tpr': tpr, |
|
|
'roc_auc': roc_auc, |
|
|
'precision': precision, |
|
|
'recall': recall, |
|
|
'y_true': y_true, |
|
|
'y_pred': y_pred, |
|
|
'y_scores': y_scores |
|
|
} |
|
|
|
|
|
elif task_type == "Многоклассовая": |
|
|
|
|
|
return {"message": "Многоклассовые метрики требуют дополнительной реализации"} |
|
|
|
|
|
else: |
|
|
return {"message": "Многометочные метрики требуют дополнительной реализации"} |
|
|
|
|
|
|
|
|
|
|
|
st.title("📊 Анализ классификаторов текста") |
|
|
|
|
|
|
|
|
st.sidebar.title("Навигация") |
|
|
app_mode = st.sidebar.selectbox( |
|
|
"Выберите режим", |
|
|
["Интерактивная классификация", "Анализ тестовой выборки"], |
|
|
key="main_navigation" |
|
|
) |
|
|
|
|
|
|
|
|
if app_mode == "Интерактивная классификация": |
|
|
st.header("🔍 Интерактивная классификация") |
|
|
|
|
|
col1, col2 = st.columns([1, 1]) |
|
|
|
|
|
with col1: |
|
|
task_type = st.selectbox( |
|
|
"Тип задачи", |
|
|
["Бинарная", "Многоклассовая", "Многометочная"], |
|
|
key="interactive_task_type" |
|
|
) |
|
|
|
|
|
available_models = list(load_models(task_type).keys()) |
|
|
if not available_models: |
|
|
st.error("Нет доступных моделей для выбранного типа задачи") |
|
|
st.stop() |
|
|
|
|
|
selected_models = st.multiselect( |
|
|
"Выберите модели для сравнения", |
|
|
available_models, |
|
|
default=available_models[0] if available_models else None, |
|
|
key="interactive_models" |
|
|
) |
|
|
|
|
|
text_input = st.text_area( |
|
|
"Введите текст для классификации", |
|
|
height=150, |
|
|
placeholder="Введите текст здесь...", |
|
|
key="interactive_text_input" |
|
|
) |
|
|
|
|
|
with col2: |
|
|
if text_input and selected_models: |
|
|
models = load_models(task_type) |
|
|
|
|
|
for model_name in selected_models: |
|
|
st.subheader(f"Модель: {model_name}") |
|
|
|
|
|
try: |
|
|
result = models[model_name](text_input) |
|
|
|
|
|
|
|
|
if task_type == "Бинарная": |
|
|
sentiment = "Positive" if result['probs'] >= 0.5 else "Negative" |
|
|
confidence = result['probs'] if result['probs'] >= 0.5 else 1 - result['probs'] |
|
|
|
|
|
st.write(f"**Результат**: {sentiment}") |
|
|
st.write(f"**Уверенность**: {confidence:.3f}") |
|
|
|
|
|
|
|
|
fig = plot_probabilities(result['probs'], result.get('labels', ['Negative', 'Positive']), |
|
|
model_name) |
|
|
st.pyplot(fig) |
|
|
|
|
|
else: |
|
|
if task_type == "Многоклассовая": |
|
|
predicted_idx = np.argmax(result['probs']) |
|
|
predicted_label = result['labels'][predicted_idx] |
|
|
confidence = result['probs'][predicted_idx] |
|
|
|
|
|
st.write(f"**Предсказанный класс**: {predicted_label}") |
|
|
st.write(f"**Уверенность**: {confidence:.3f}") |
|
|
|
|
|
else: |
|
|
predicted_labels = [result['labels'][i] for i, prob in enumerate(result['probs']) if |
|
|
prob >= 0.5] |
|
|
st.write(f"**Предсказанные классы**: {', '.join(predicted_labels)}") |
|
|
|
|
|
|
|
|
fig = plot_probabilities(result['probs'], result['labels'], model_name) |
|
|
st.pyplot(fig) |
|
|
|
|
|
|
|
|
prob_df = pd.DataFrame({ |
|
|
'Класс': result['labels'], |
|
|
'Вероятность': result['probs'] |
|
|
}).sort_values('Вероятность', ascending=False) |
|
|
|
|
|
st.dataframe(prob_df, use_container_width=True) |
|
|
|
|
|
except Exception as e: |
|
|
st.write("График не поддерживается у данной модели") |
|
|
|
|
|
|
|
|
elif app_mode == "Анализ тестовой выборки": |
|
|
st.header("📈 Анализ тестовой выборки") |
|
|
|
|
|
uploaded_file = st.file_uploader( |
|
|
"Загрузите JSONL файл с тестовой выборкой", |
|
|
type=['jsonl'], |
|
|
help="Файл должен содержать поля 'text' и 'label' (для бинарной/многоклассовой) или 'labels' (для многометочной)", |
|
|
key="file_uploader" |
|
|
) |
|
|
|
|
|
if uploaded_file: |
|
|
task_type = st.selectbox( |
|
|
"Тип задачи для анализа", |
|
|
["Бинарная", "Многоклассовая", "Многометочная"], |
|
|
key="analysis_task_type" |
|
|
) |
|
|
|
|
|
available_models = list(load_models(task_type).keys()) |
|
|
if not available_models: |
|
|
st.error("Нет доступных моделей для выбранного типа задачи") |
|
|
st.stop() |
|
|
|
|
|
selected_model = st.selectbox( |
|
|
"Выберите модель для анализа", |
|
|
available_models, |
|
|
key="analysis_model" |
|
|
) |
|
|
|
|
|
if st.button("Запустить анализ", key="analyze_button"): |
|
|
with st.spinner("Обработка данных..."): |
|
|
|
|
|
df = process_test_file(uploaded_file, task_type) |
|
|
st.write(f"Загружено {len(df)} примеров") |
|
|
|
|
|
|
|
|
st.subheader("Структура данных") |
|
|
st.dataframe(df.head(), use_container_width=True) |
|
|
|
|
|
if task_type == "Многометочная" and 'labels' not in df.columns: |
|
|
st.error("Для многометочной классификации в файле должно быть поле 'labels'") |
|
|
st.stop() |
|
|
elif task_type != "Многометочная" and 'label' not in df.columns: |
|
|
st.error("Для бинарной и многоклассовой классификации в файле должно быть поле 'label'") |
|
|
st.stop() |
|
|
|
|
|
|
|
|
model = load_models(task_type)[selected_model] |
|
|
predictions = [] |
|
|
true_labels = [] |
|
|
|
|
|
progress_bar = st.progress(0) |
|
|
for i, row in df.iterrows(): |
|
|
try: |
|
|
result = model(row['text']) |
|
|
predictions.append(result) |
|
|
|
|
|
|
|
|
if task_type == "Многометочная": |
|
|
true_labels.append(row['labels']) |
|
|
else: |
|
|
true_labels.append(row['label']) |
|
|
|
|
|
except Exception as e: |
|
|
st.error(f"Ошибка при обработке примера {i}: {str(e)}") |
|
|
predictions.append(None) |
|
|
true_labels.append(None) |
|
|
|
|
|
progress_bar.progress((i + 1) / len(df)) |
|
|
|
|
|
|
|
|
valid_indices = [i for i, pred in enumerate(predictions) if pred is not None] |
|
|
predictions = [predictions[i] for i in valid_indices] |
|
|
true_labels = [true_labels[i] for i in valid_indices] |
|
|
|
|
|
st.write(f"Успешно обработано {len(predictions)} из {len(df)} примеров") |
|
|
|
|
|
|
|
|
if task_type == "Бинарная": |
|
|
calculate_and_display_binary_metrics(true_labels, predictions) |
|
|
|
|
|
elif task_type == "Многоклассовая": |
|
|
calculate_and_display_multiclass_metrics(true_labels, predictions) |
|
|
|
|
|
elif task_type == "Многометочная": |
|
|
calculate_and_display_multilabel_metrics(true_labels, predictions) |
|
|
|
|
|
|
|
|
|
|
|
st.sidebar.markdown("---") |
|
|
st.sidebar.info(""" |
|
|
**Инструкция:** |
|
|
1. **Интерактивная классификация**: Тестируйте модели на произвольном тексте |
|
|
2. **Анализ тестовой выборки**: Загрузите JSONL файл для оценки качества |
|
|
""") |
|
|
|
|
|
|
|
|
st.markdown(""" |
|
|
<style> |
|
|
.main-header { |
|
|
font-size: 2.5rem; |
|
|
color: #1f77b4; |
|
|
text-align: center; |
|
|
margin-bottom: 2rem; |
|
|
} |
|
|
.metric-card { |
|
|
background-color: #f0f2f6; |
|
|
padding: 1rem; |
|
|
border-radius: 0.5rem; |
|
|
margin: 0.5rem 0; |
|
|
} |
|
|
</style> |
|
|
""", unsafe_allow_html=True) |