llm-pig 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- llm_pig-0.1.0/PKG-INFO +38 -0
- llm_pig-0.1.0/README.md +21 -0
- llm_pig-0.1.0/pyproject.toml +21 -0
- llm_pig-0.1.0/src/llm_pig/__init__.py +4 -0
- llm_pig-0.1.0/src/llm_pig/parser.py +13 -0
- llm_pig-0.1.0/src/llm_pig/plots - Copia.py +558 -0
- llm_pig-0.1.0/src/llm_pig/plots.py +631 -0
llm_pig-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: llm-pig
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python library to visualize LLM perplexity metrics in interactive graphs
|
|
5
|
+
Author: Juan Morysson Viana Marciano
|
|
6
|
+
Author-email: juanmorysson@gmail.com
|
|
7
|
+
Requires-Python: >=3.10,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Requires-Dist: pandas (>=2.0.0)
|
|
15
|
+
Requires-Dist: plotly (>=5.0.0)
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# llm-pig 🐷
|
|
19
|
+
|
|
20
|
+
`llm-pig` is a comprehensive Python library designed for advanced LLM perplexity analysis, covering both token-level micro inspection and macro-level dataset evaluation. It provides clean, interactive HTML visualizations powered by Pandas and Plotly.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
|
|
26
|
+
* **Token-Level Curve Analysis**: Interactive line charts tracking token uncertainty across a prompt or document.
|
|
27
|
+
* **Cascading Bar Charts**: Sorted bar representations highlighting peak perplexity tokens.
|
|
28
|
+
* **SHAP-Style Text Highlights**: Individual and comparative multi-model text highlights using dynamic color gradient scales.
|
|
29
|
+
* **Macro Statistical Distribution**: Comprehensive Boxplots and Violin Plots summarizing model performance across large test datasets (including Min, Q1, Median, Q3, and Max metrics).
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## Installation
|
|
34
|
+
|
|
35
|
+
Install the package via pip:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
pip install llm-pig
|
llm_pig-0.1.0/README.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# llm-pig 🐷
|
|
2
|
+
|
|
3
|
+
`llm-pig` is a comprehensive Python library designed for advanced LLM perplexity analysis, covering both token-level micro inspection and macro-level dataset evaluation. It provides clean, interactive HTML visualizations powered by Pandas and Plotly.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
* **Token-Level Curve Analysis**: Interactive line charts tracking token uncertainty across a prompt or document.
|
|
10
|
+
* **Cascading Bar Charts**: Sorted bar representations highlighting peak perplexity tokens.
|
|
11
|
+
* **SHAP-Style Text Highlights**: Individual and comparative multi-model text highlights using dynamic color gradient scales.
|
|
12
|
+
* **Macro Statistical Distribution**: Comprehensive Boxplots and Violin Plots summarizing model performance across large test datasets (including Min, Q1, Median, Q3, and Max metrics).
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
Install the package via pip:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install llm-pig
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "llm-pig"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "A Python library to visualize LLM perplexity metrics in interactive graphs"
|
|
5
|
+
authors = ["Juan Morysson Viana Marciano <juanmorysson@gmail.com>"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
packages = [{include = "llm_pig", from = "src"}]
|
|
8
|
+
|
|
9
|
+
[tool.poetry.dependencies]
|
|
10
|
+
python = "^3.10"
|
|
11
|
+
plotly = ">=5.0.0"
|
|
12
|
+
pandas = ">=2.0.0"
|
|
13
|
+
|
|
14
|
+
[build-system]
|
|
15
|
+
requires = ["poetry-core"]
|
|
16
|
+
build-backend = "poetry.core.masonry.api"
|
|
17
|
+
|
|
18
|
+
[dependency-groups]
|
|
19
|
+
dev = [
|
|
20
|
+
"pytest (>=9.1.1,<10.0.0)"
|
|
21
|
+
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import pandas as pd
|
|
2
|
+
|
|
3
|
+
class PerplexityParser:
|
|
4
|
+
def __init__(self, tokens: list, perplexities: list):
|
|
5
|
+
if len(tokens) != len(perplexities):
|
|
6
|
+
raise ValueError("A lista de tokens e a lista de perplexidades devem ter o mesmo tamanho.")
|
|
7
|
+
self.df = pd.DataFrame({
|
|
8
|
+
"token": tokens,
|
|
9
|
+
"perplexity": perplexities
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
def get_dataframe(self) -> pd.DataFrame:
|
|
13
|
+
return self.df
|
|
@@ -0,0 +1,558 @@
|
|
|
1
|
+
import plotly.express as px
|
|
2
|
+
from .parser import PerplexityParser
|
|
3
|
+
|
|
4
|
+
class PerplexityVisualizer:
|
|
5
|
+
def __init__(self, parser: PerplexityParser):
|
|
6
|
+
self.df = parser.get_dataframe()
|
|
7
|
+
|
|
8
|
+
def plot_token_curve(self, save_path: str = "perplexity_curve.html", x_axis: str = "index", tick_angle: int = 0):
|
|
9
|
+
"""
|
|
10
|
+
Gera um gráfico de linha interativo da perplexidade por token.
|
|
11
|
+
|
|
12
|
+
Parâmetros:
|
|
13
|
+
- save_path (str): Caminho para salvar o arquivo HTML gerado.
|
|
14
|
+
- x_axis (str): Define o que aparece no eixo X ('index' ou 'token').
|
|
15
|
+
- tick_angle (int): Ângulo de rotação dos rótulos do eixo X (ex: 0 para normal, 90 para vertical).
|
|
16
|
+
"""
|
|
17
|
+
if x_axis not in ["index", "token"]:
|
|
18
|
+
raise ValueError("O parâmetro 'x_axis' deve ser 'index' ou 'token'.")
|
|
19
|
+
|
|
20
|
+
x_data = self.df.index if x_axis == "index" else self.df["token"]
|
|
21
|
+
|
|
22
|
+
fig = px.line(
|
|
23
|
+
self.df,
|
|
24
|
+
x=x_data,
|
|
25
|
+
y="perplexity",
|
|
26
|
+
hover_data=["token"],
|
|
27
|
+
markers=True,
|
|
28
|
+
title="LLM Perplexity Curve by Token",
|
|
29
|
+
labels={"x": "Token Position" if x_axis == "index" else "Token", "perplexity": "Perplexity"}
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
fig.update_traces(
|
|
33
|
+
hovertemplate="<b>Token:</b> %{customdata[0]}<br><b>Perplexity:</b> %{y:.2f}<extra></extra>"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
fig.update_layout(xaxis=dict(tickangle=tick_angle))
|
|
37
|
+
|
|
38
|
+
fig.write_html(save_path)
|
|
39
|
+
print(f"Interactive chart successfully saved to: {save_path}")
|
|
40
|
+
return fig
|
|
41
|
+
|
|
42
|
+
def plot_token_bars(self, save_path: str = "perplexity_bars.html", top_n: int = None, cascade: bool = True):
|
|
43
|
+
"""
|
|
44
|
+
Gera um gráfico de barras horizontais com os tokens no eixo Y e valores exibidos nas barras.
|
|
45
|
+
|
|
46
|
+
Parâmetros:
|
|
47
|
+
- save_path (str): Caminho para salvar o arquivo HTML gerado.
|
|
48
|
+
- top_n (int, opcional): Limita a quantidade de tokens exibidos.
|
|
49
|
+
- cascade (bool): Se True (padrão), ordena do maior para o menor (maior perplexidade no topo).
|
|
50
|
+
"""
|
|
51
|
+
df_plot = self.df.copy()
|
|
52
|
+
|
|
53
|
+
if cascade:
|
|
54
|
+
df_plot = df_plot.sort_values(by="perplexity", ascending=True)
|
|
55
|
+
|
|
56
|
+
if top_n is not None:
|
|
57
|
+
if cascade:
|
|
58
|
+
df_plot = df_plot.tail(top_n)
|
|
59
|
+
else:
|
|
60
|
+
df_plot = df_plot.head(top_n)
|
|
61
|
+
|
|
62
|
+
fig = px.bar(
|
|
63
|
+
df_plot,
|
|
64
|
+
x="perplexity",
|
|
65
|
+
y="token",
|
|
66
|
+
orientation="h",
|
|
67
|
+
text="perplexity", # Define a coluna que terá o valor exibido na barra
|
|
68
|
+
title="LLM Perplexity by Token (Bar Chart)",
|
|
69
|
+
labels={"perplexity": "Perplexity", "token": "Token"}
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
# Formata o texto para exibir duas casas decimais e posiciona o texto dentro/ao lado da barra
|
|
73
|
+
fig.update_traces(
|
|
74
|
+
texttemplate='%{text:.2f}',
|
|
75
|
+
textposition='inside',
|
|
76
|
+
hovertemplate="<b>Token:</b> %{y}<br><b>Perplexity:</b> %{x:.2f}<extra></extra>"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
fig.write_html(save_path)
|
|
80
|
+
print(f"Interactive bar chart successfully saved to: {save_path}")
|
|
81
|
+
return fig
|
|
82
|
+
|
|
83
|
+
def perplexity_text(self, save_path: str = "perplexity_text.html", top_n: int = None):
|
|
84
|
+
"""
|
|
85
|
+
Gera uma visualização em texto destacado (tipo SHAP) onde o degradê
|
|
86
|
+
e a legenda são recalculados com base apenas nos tokens destacados pelo filtro.
|
|
87
|
+
"""
|
|
88
|
+
df = self.df.copy()
|
|
89
|
+
|
|
90
|
+
# Identifica os tokens que entram no top_n
|
|
91
|
+
if top_n is not None and top_n < len(df):
|
|
92
|
+
df_filtered = df.nlargest(top_n, "perplexity")
|
|
93
|
+
top_indices = df_filtered.index
|
|
94
|
+
else:
|
|
95
|
+
df_filtered = df
|
|
96
|
+
top_indices = df.index
|
|
97
|
+
|
|
98
|
+
# Recalcula min e max baseados estritamente nos itens que receberão o destaque
|
|
99
|
+
min_p = df_filtered["perplexity"].min()
|
|
100
|
+
max_p = df_filtered["perplexity"].max()
|
|
101
|
+
|
|
102
|
+
def get_color_and_text_color(val, is_highlighted):
|
|
103
|
+
if not is_highlighted:
|
|
104
|
+
return "transparent", "#333333", False
|
|
105
|
+
|
|
106
|
+
if max_p == min_p:
|
|
107
|
+
norm_val = 0.5
|
|
108
|
+
else:
|
|
109
|
+
# Normaliza usando o novo limite inferior do filtro
|
|
110
|
+
norm_val = (val - min_p) / (max_p - min_p)
|
|
111
|
+
|
|
112
|
+
r = int(255 - norm_val * (255 - 128))
|
|
113
|
+
g = int(192 - norm_val * 192)
|
|
114
|
+
b = int(203 - norm_val * (203 - 32))
|
|
115
|
+
alpha = 0.3 + (norm_val * 0.7)
|
|
116
|
+
bg_color = f"rgba({r}, {g}, {b}, {alpha})"
|
|
117
|
+
text_color = "#ffffff" if norm_val > 0.5 else "#333333"
|
|
118
|
+
|
|
119
|
+
return bg_color, text_color, True
|
|
120
|
+
|
|
121
|
+
spans = []
|
|
122
|
+
for idx, row in df.iterrows():
|
|
123
|
+
token = row["token"]
|
|
124
|
+
p_val = row["perplexity"]
|
|
125
|
+
is_hl = idx in top_indices
|
|
126
|
+
|
|
127
|
+
bg_color, text_color, highlighted = get_color_and_text_color(p_val, is_hl)
|
|
128
|
+
safe_token = token.replace("\n", "<br>")
|
|
129
|
+
|
|
130
|
+
if highlighted:
|
|
131
|
+
span_html = (
|
|
132
|
+
f'<span style="background-color: {bg_color}; color: {text_color}; '
|
|
133
|
+
f'padding: 3px 6px; margin: 2px; border-radius: 4px; display: inline-block; '
|
|
134
|
+
f'font-weight: 500;" title="Perplexity: {p_val:.2f}">{safe_token}</span>'
|
|
135
|
+
)
|
|
136
|
+
else:
|
|
137
|
+
span_html = (
|
|
138
|
+
f'<span style="background-color: transparent; color: {text_color}; '
|
|
139
|
+
f'padding: 3px 6px; margin: 2px; display: inline-block;" '
|
|
140
|
+
f'title="Perplexity: {p_val:.2f} (Not in Top {top_n})">{safe_token}</span>'
|
|
141
|
+
)
|
|
142
|
+
spans.append(span_html)
|
|
143
|
+
|
|
144
|
+
legend_start = "rgba(255, 192, 203, 0.3)"
|
|
145
|
+
legend_end = "rgba(128, 0, 32, 1.0)"
|
|
146
|
+
|
|
147
|
+
html_content = f"""
|
|
148
|
+
<!DOCTYPE html>
|
|
149
|
+
<html>
|
|
150
|
+
<head>
|
|
151
|
+
<meta charset="utf-8">
|
|
152
|
+
<title>LLM Perplexity Text Highlight</title>
|
|
153
|
+
<style>
|
|
154
|
+
body {{
|
|
155
|
+
font-family: Arial, sans-serif;
|
|
156
|
+
margin: 40px;
|
|
157
|
+
background-color: #f9f9f9;
|
|
158
|
+
color: #333;
|
|
159
|
+
}}
|
|
160
|
+
.container {{
|
|
161
|
+
background: #fff;
|
|
162
|
+
padding: 25px;
|
|
163
|
+
border-radius: 8px;
|
|
164
|
+
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
|
165
|
+
line-height: 2.5;
|
|
166
|
+
font-size: 18px;
|
|
167
|
+
}}
|
|
168
|
+
h2 {{
|
|
169
|
+
margin-bottom: 20px;
|
|
170
|
+
}}
|
|
171
|
+
.legend-container {{
|
|
172
|
+
margin-top: 25px;
|
|
173
|
+
display: flex;
|
|
174
|
+
align-items: center;
|
|
175
|
+
gap: 15px;
|
|
176
|
+
font-size: 14px;
|
|
177
|
+
color: #555;
|
|
178
|
+
}}
|
|
179
|
+
.legend-bar {{
|
|
180
|
+
width: 200px;
|
|
181
|
+
height: 14px;
|
|
182
|
+
border-radius: 4px;
|
|
183
|
+
background: linear-gradient(to right, {legend_start}, {legend_end});
|
|
184
|
+
border: 1px solid #ddd;
|
|
185
|
+
}}
|
|
186
|
+
</style>
|
|
187
|
+
</head>
|
|
188
|
+
<body>
|
|
189
|
+
<h2>LLM Perplexity Text Highlight (Top {top_n if top_n else 'All'} Tokens)</h2>
|
|
190
|
+
<div class="container">
|
|
191
|
+
{"".join(spans)}
|
|
192
|
+
|
|
193
|
+
<div class="legend-container">
|
|
194
|
+
<span>Low ({min_p:.2f})</span>
|
|
195
|
+
<div class="legend-bar"></div>
|
|
196
|
+
<span>High ({max_p:.2f})</span>
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
</body>
|
|
200
|
+
</html>
|
|
201
|
+
"""
|
|
202
|
+
|
|
203
|
+
with open(save_path, "w", encoding="utf-8") as f:
|
|
204
|
+
f.write(html_content)
|
|
205
|
+
|
|
206
|
+
print(f"Text highlight visualization with recalculated legend successfully saved to: {save_path}")
|
|
207
|
+
|
|
208
|
+
def compare_perplexity_texts(
|
|
209
|
+
self,
|
|
210
|
+
save_path: str = "perplexity_comparison.html",
|
|
211
|
+
tokens: list[list[str]] = None,
|
|
212
|
+
perplexities: list[list[float]] = None,
|
|
213
|
+
labels: list[str] = None,
|
|
214
|
+
top_n: int = None
|
|
215
|
+
):
|
|
216
|
+
"""
|
|
217
|
+
Gera uma visualização comparativa em texto destacado utilizando escala de cores
|
|
218
|
+
global e uma única legenda unificada no rodapé.
|
|
219
|
+
"""
|
|
220
|
+
if not tokens or not perplexities or len(tokens) != len(perplexities):
|
|
221
|
+
raise ValueError("As listas 'tokens' e 'perplexities' devem ser fornecidas e ter o mesmo comprimento.")
|
|
222
|
+
|
|
223
|
+
if labels is None:
|
|
224
|
+
labels = [f"Model {i+1}" for i in range(len(tokens))]
|
|
225
|
+
|
|
226
|
+
if len(tokens) != len(labels):
|
|
227
|
+
raise ValueError("A quantidade de listas de tokens deve ser igual à quantidade de 'labels'.")
|
|
228
|
+
|
|
229
|
+
import pandas as pd
|
|
230
|
+
|
|
231
|
+
models_dfs = []
|
|
232
|
+
all_highlighted_perplexities = []
|
|
233
|
+
|
|
234
|
+
# Passo 1: Processa cada modelo e coleta os valores destacados globalmente
|
|
235
|
+
for model_tokens, model_perps, label in zip(tokens, perplexities, labels):
|
|
236
|
+
if len(model_tokens) != len(model_perps):
|
|
237
|
+
raise ValueError(f"As listas de tokens e perplexidades do modelo '{label}' devem ter o mesmo tamanho.")
|
|
238
|
+
|
|
239
|
+
df = pd.DataFrame({"token": model_tokens, "perplexity": model_perps})
|
|
240
|
+
|
|
241
|
+
if top_n is not None and top_n < len(df):
|
|
242
|
+
df_filtered = df.nlargest(top_n, "perplexity")
|
|
243
|
+
top_indices = set(df_filtered.index)
|
|
244
|
+
else:
|
|
245
|
+
top_indices = set(df.index)
|
|
246
|
+
df_filtered = df
|
|
247
|
+
|
|
248
|
+
models_dfs.append({"df": df, "top_indices": top_indices, "label": label})
|
|
249
|
+
|
|
250
|
+
if not df_filtered.empty:
|
|
251
|
+
all_highlighted_perplexities.extend(df_filtered["perplexity"].tolist())
|
|
252
|
+
|
|
253
|
+
# Passo 2: Determina o Mínimo e Máximo global entre todos os tokens destacados
|
|
254
|
+
if all_highlighted_perplexities:
|
|
255
|
+
global_min_p = min(all_highlighted_perplexities)
|
|
256
|
+
global_max_p = max(all_highlighted_perplexities)
|
|
257
|
+
else:
|
|
258
|
+
global_min_p = 0.0
|
|
259
|
+
global_max_p = 1.0
|
|
260
|
+
|
|
261
|
+
def get_color_and_text_color(val, is_highlighted):
|
|
262
|
+
if not is_highlighted:
|
|
263
|
+
return "transparent", "#333333", False
|
|
264
|
+
|
|
265
|
+
if global_max_p == global_min_p:
|
|
266
|
+
norm_val = 0.5
|
|
267
|
+
else:
|
|
268
|
+
norm_val = (val - global_min_p) / (global_max_p - global_min_p)
|
|
269
|
+
norm_val = max(0.0, min(1.0, norm_val))
|
|
270
|
+
|
|
271
|
+
r = int(255 - norm_val * (255 - 128))
|
|
272
|
+
g = int(192 - norm_val * 192)
|
|
273
|
+
b = int(203 - norm_val * (203 - 32))
|
|
274
|
+
alpha = 0.3 + (norm_val * 0.7)
|
|
275
|
+
bg_color = f"rgba({r}, {g}, {b}, {alpha})"
|
|
276
|
+
text_color = "#ffffff" if norm_val > 0.5 else "#333333"
|
|
277
|
+
|
|
278
|
+
return bg_color, text_color, True
|
|
279
|
+
|
|
280
|
+
sections_html = []
|
|
281
|
+
|
|
282
|
+
# Passo 3: Constrói o HTML de cada seção de texto
|
|
283
|
+
for item in models_dfs:
|
|
284
|
+
df = item["df"]
|
|
285
|
+
top_indices = item["top_indices"]
|
|
286
|
+
label = item["label"]
|
|
287
|
+
|
|
288
|
+
spans = []
|
|
289
|
+
for idx, row in df.iterrows():
|
|
290
|
+
token = row["token"]
|
|
291
|
+
p_val = row["perplexity"]
|
|
292
|
+
is_hl = idx in top_indices
|
|
293
|
+
|
|
294
|
+
bg_color, text_color, highlighted = get_color_and_text_color(p_val, is_hl)
|
|
295
|
+
safe_token = token.replace("\n", "<br>")
|
|
296
|
+
|
|
297
|
+
if highlighted:
|
|
298
|
+
span_html = (
|
|
299
|
+
f'<span style="background-color: {bg_color}; color: {text_color}; '
|
|
300
|
+
f'padding: 3px 6px; margin: 2px; border-radius: 4px; display: inline-block; '
|
|
301
|
+
f'font-weight: 500;" title="Perplexity: {p_val:.2f}">{safe_token}</span>'
|
|
302
|
+
)
|
|
303
|
+
else:
|
|
304
|
+
span_html = (
|
|
305
|
+
f'<span style="background-color: transparent; color: {text_color}; '
|
|
306
|
+
f'padding: 3px 6px; margin: 2px; display: inline-block;" '
|
|
307
|
+
f'title="Perplexity: {p_val:.2f} (Not in Top {top_n})">{safe_token}</span>'
|
|
308
|
+
)
|
|
309
|
+
spans.append(span_html)
|
|
310
|
+
|
|
311
|
+
section_content = f"""
|
|
312
|
+
<div class="model-section">
|
|
313
|
+
<h3>{label}</h3>
|
|
314
|
+
<div class="text-container">
|
|
315
|
+
{"".join(spans)}
|
|
316
|
+
</div>
|
|
317
|
+
</div>
|
|
318
|
+
"""
|
|
319
|
+
sections_html.append(section_content)
|
|
320
|
+
|
|
321
|
+
legend_start = "rgba(255, 192, 203, 0.3)"
|
|
322
|
+
legend_end = "rgba(128, 0, 32, 1.0)"
|
|
323
|
+
|
|
324
|
+
html_content = f"""
|
|
325
|
+
<!DOCTYPE html>
|
|
326
|
+
<html>
|
|
327
|
+
<head>
|
|
328
|
+
<meta charset="utf-8">
|
|
329
|
+
<title>LLM Perplexity Comparison</title>
|
|
330
|
+
<style>
|
|
331
|
+
body {{
|
|
332
|
+
font-family: Arial, sans-serif;
|
|
333
|
+
margin: 40px;
|
|
334
|
+
background-color: #f9f9f9;
|
|
335
|
+
color: #333;
|
|
336
|
+
}}
|
|
337
|
+
.model-section {{
|
|
338
|
+
margin-bottom: 25px;
|
|
339
|
+
}}
|
|
340
|
+
h3 {{
|
|
341
|
+
margin-bottom: 8px;
|
|
342
|
+
color: #444;
|
|
343
|
+
}}
|
|
344
|
+
.text-container {{
|
|
345
|
+
background: #fff;
|
|
346
|
+
padding: 20px;
|
|
347
|
+
border-radius: 8px;
|
|
348
|
+
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
|
349
|
+
line-height: 2.5;
|
|
350
|
+
font-size: 18px;
|
|
351
|
+
}}
|
|
352
|
+
h2 {{
|
|
353
|
+
margin-bottom: 30px;
|
|
354
|
+
}}
|
|
355
|
+
.global-legend-container {{
|
|
356
|
+
margin-top: 35px;
|
|
357
|
+
background: #fff;
|
|
358
|
+
padding: 20px;
|
|
359
|
+
border-radius: 8px;
|
|
360
|
+
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
|
361
|
+
display: flex;
|
|
362
|
+
align-items: center;
|
|
363
|
+
justify-content: center;
|
|
364
|
+
gap: 20px;
|
|
365
|
+
font-size: 15px;
|
|
366
|
+
color: #555;
|
|
367
|
+
}}
|
|
368
|
+
.legend-bar {{
|
|
369
|
+
width: 300px;
|
|
370
|
+
height: 16px;
|
|
371
|
+
border-radius: 4px;
|
|
372
|
+
background: linear-gradient(to right, {legend_start}, {legend_end});
|
|
373
|
+
border: 1px solid #ddd;
|
|
374
|
+
}}
|
|
375
|
+
</style>
|
|
376
|
+
</head>
|
|
377
|
+
<body>
|
|
378
|
+
<h2>LLM Perplexity Text Comparison (Top {top_n if top_n else 'All'} Tokens)</h2>
|
|
379
|
+
{"".join(sections_html)}
|
|
380
|
+
|
|
381
|
+
<div class="global-legend-container">
|
|
382
|
+
<span>Low ({global_min_p:.2f})</span>
|
|
383
|
+
<div class="legend-bar"></div>
|
|
384
|
+
<span>High ({global_max_p:.2f})</span>
|
|
385
|
+
</div>
|
|
386
|
+
</body>
|
|
387
|
+
</html>
|
|
388
|
+
"""
|
|
389
|
+
|
|
390
|
+
with open(save_path, "w", encoding="utf-8") as f:
|
|
391
|
+
f.write(html_content)
|
|
392
|
+
|
|
393
|
+
print(f"Global comparison visualization successfully saved to: {save_path}")
|
|
394
|
+
|
|
395
|
+
def plot_macro_comparison(
|
|
396
|
+
self,
|
|
397
|
+
macro_data: dict[str, list[float]],
|
|
398
|
+
save_path: str = "macro_perplexity_comparison.html",
|
|
399
|
+
show_values: bool = False
|
|
400
|
+
):
|
|
401
|
+
"""
|
|
402
|
+
Gera um gráfico de caixa (Boxplot) interativo para comparação macro,
|
|
403
|
+
exibindo as 5 estatísticas principais com cores alinhadas aos modelos.
|
|
404
|
+
"""
|
|
405
|
+
import pandas as pd
|
|
406
|
+
import numpy as np
|
|
407
|
+
import plotly.graph_objects as go
|
|
408
|
+
|
|
409
|
+
rows = []
|
|
410
|
+
for model_name, values in macro_data.items():
|
|
411
|
+
for val in values:
|
|
412
|
+
rows.append({"Model": model_name, "Perplexity": val})
|
|
413
|
+
|
|
414
|
+
df_long = pd.DataFrame(rows)
|
|
415
|
+
|
|
416
|
+
fig = go.Figure()
|
|
417
|
+
colors = px.colors.qualitative.Plotly[:len(macro_data)]
|
|
418
|
+
models = list(macro_data.items())
|
|
419
|
+
|
|
420
|
+
for i, (model_name, values) in enumerate(models):
|
|
421
|
+
df_model = df_long[df_long["Model"] == model_name]
|
|
422
|
+
color = colors[i % len(colors)]
|
|
423
|
+
|
|
424
|
+
fig.add_trace(go.Box(
|
|
425
|
+
y=df_model["Perplexity"],
|
|
426
|
+
name=model_name,
|
|
427
|
+
boxpoints=False,
|
|
428
|
+
marker_color=color
|
|
429
|
+
))
|
|
430
|
+
|
|
431
|
+
if show_values:
|
|
432
|
+
arr = np.array(values)
|
|
433
|
+
q1 = np.percentile(arr, 25)
|
|
434
|
+
median = np.median(arr)
|
|
435
|
+
q3 = np.percentile(arr, 75)
|
|
436
|
+
minimum = np.min(arr)
|
|
437
|
+
maximum = np.max(arr)
|
|
438
|
+
|
|
439
|
+
stat_y = [minimum, q1, median, q3, maximum]
|
|
440
|
+
stat_labels = [
|
|
441
|
+
f"Min: {minimum:.2f}",
|
|
442
|
+
f"Q1: {q1:.2f}",
|
|
443
|
+
f"Median: {median:.2f}",
|
|
444
|
+
f"Q3: {q3:.2f}",
|
|
445
|
+
f"Max: {maximum:.2f}"
|
|
446
|
+
]
|
|
447
|
+
|
|
448
|
+
fig.add_trace(go.Scatter(
|
|
449
|
+
x=[model_name] * len(stat_y),
|
|
450
|
+
y=stat_y,
|
|
451
|
+
mode="markers+text",
|
|
452
|
+
text=stat_labels,
|
|
453
|
+
textposition="top right",
|
|
454
|
+
textfont=dict(size=11, color=color, family="Arial Black"),
|
|
455
|
+
marker=dict(color=color, size=6, symbol="circle"),
|
|
456
|
+
showlegend=False,
|
|
457
|
+
hovertemplate="Model: " + model_name + "<br>%{text}<extra></extra>"
|
|
458
|
+
))
|
|
459
|
+
|
|
460
|
+
fig.update_layout(
|
|
461
|
+
title="Macro Comparison: Statistical Distribution Summary",
|
|
462
|
+
xaxis_title="Models",
|
|
463
|
+
yaxis_title="Perplexity",
|
|
464
|
+
showlegend=False,
|
|
465
|
+
margin=dict(r=150) # Corrigido de 'right' para 'r'
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
fig.write_html(save_path)
|
|
469
|
+
print(f"Macro comparison statistical chart successfully saved to: {save_path}")
|
|
470
|
+
return fig
|
|
471
|
+
|
|
472
|
+
def plot_macro_violin(
|
|
473
|
+
self,
|
|
474
|
+
macro_data: dict[str, list[float]],
|
|
475
|
+
save_path: str = "macro_violin_comparison.html",
|
|
476
|
+
show_values: bool = False
|
|
477
|
+
):
|
|
478
|
+
"""
|
|
479
|
+
Gera um Violin Plot interativo para comparação macro da distribuição de perplexidade,
|
|
480
|
+
exibindo opcionalmente as 5 principais estatísticas (Min, Q1, Median, Q3, Max)
|
|
481
|
+
com cores sincronizadas ao modelo.
|
|
482
|
+
|
|
483
|
+
Parâmetros:
|
|
484
|
+
- macro_data (dict): Dicionário de listas com as perplexidades dos modelos.
|
|
485
|
+
- save_path (str): Caminho para salvar o arquivo HTML gerado.
|
|
486
|
+
- show_values (bool): Se True, exibe as estatísticas numéricas ao lado direito de cada violino.
|
|
487
|
+
"""
|
|
488
|
+
import pandas as pd
|
|
489
|
+
import numpy as np
|
|
490
|
+
import plotly.graph_objects as go
|
|
491
|
+
|
|
492
|
+
rows = []
|
|
493
|
+
for model_name, values in macro_data.items():
|
|
494
|
+
for val in values:
|
|
495
|
+
rows.append({"Model": model_name, "Perplexity": val})
|
|
496
|
+
|
|
497
|
+
df_long = pd.DataFrame(rows)
|
|
498
|
+
|
|
499
|
+
fig = go.Figure()
|
|
500
|
+
colors = px.colors.qualitative.Plotly[:len(macro_data)]
|
|
501
|
+
models = list(macro_data.items())
|
|
502
|
+
|
|
503
|
+
for i, (model_name, values) in enumerate(models):
|
|
504
|
+
df_model = df_long[df_long["Model"] == model_name]
|
|
505
|
+
color = colors[i % len(colors)]
|
|
506
|
+
|
|
507
|
+
# Adiciona o Violin Plot com caixa interna (box) embutida
|
|
508
|
+
fig.add_trace(go.Violin(
|
|
509
|
+
y=df_model["Perplexity"],
|
|
510
|
+
name=model_name,
|
|
511
|
+
box_visible=True, # Mostra o boxplot interno embutido no violino
|
|
512
|
+
meanline_visible=True, # Mostra a linha da média
|
|
513
|
+
fillcolor=color,
|
|
514
|
+
opacity=0.6,
|
|
515
|
+
line_color=color
|
|
516
|
+
))
|
|
517
|
+
|
|
518
|
+
if show_values:
|
|
519
|
+
arr = np.array(values)
|
|
520
|
+
q1 = np.percentile(arr, 25)
|
|
521
|
+
median = np.median(arr)
|
|
522
|
+
q3 = np.percentile(arr, 75)
|
|
523
|
+
minimum = np.min(arr)
|
|
524
|
+
maximum = np.max(arr)
|
|
525
|
+
|
|
526
|
+
stat_y = [minimum, q1, median, q3, maximum]
|
|
527
|
+
stat_labels = [
|
|
528
|
+
f"Min: {minimum:.2f}",
|
|
529
|
+
f"Q1: {q1:.2f}",
|
|
530
|
+
f"Median: {median:.2f}",
|
|
531
|
+
f"Q3: {q3:.2f}",
|
|
532
|
+
f"Max: {maximum:.2f}"
|
|
533
|
+
]
|
|
534
|
+
|
|
535
|
+
fig.add_trace(go.Scatter(
|
|
536
|
+
x=[model_name] * len(stat_y),
|
|
537
|
+
y=stat_y,
|
|
538
|
+
mode="markers+text",
|
|
539
|
+
text=stat_labels,
|
|
540
|
+
textposition="top right",
|
|
541
|
+
textfont=dict(size=11, color=color, family="Arial Black"),
|
|
542
|
+
marker=dict(color=color, size=6, symbol="circle"),
|
|
543
|
+
showlegend=False,
|
|
544
|
+
hovertemplate="Model: " + model_name + "<br>%{text}<extra></extra>"
|
|
545
|
+
))
|
|
546
|
+
|
|
547
|
+
fig.update_layout(
|
|
548
|
+
title="Macro Comparison: Violin Distribution Summary",
|
|
549
|
+
xaxis_title="Models",
|
|
550
|
+
yaxis_title="Perplexity",
|
|
551
|
+
showlegend=False,
|
|
552
|
+
margin=dict(r=150) # Margem lateral para acomodar os rótulos estatísticos
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
fig.write_html(save_path)
|
|
556
|
+
print(f"Macro violin comparison chart successfully saved to: {save_path}")
|
|
557
|
+
return fig
|
|
558
|
+
|
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
import plotly.express as px
|
|
2
|
+
from .parser import PerplexityParser
|
|
3
|
+
|
|
4
|
+
class PerplexityVisualizer:
|
|
5
|
+
def __init__(self, parser: PerplexityParser):
|
|
6
|
+
self.df = parser.get_dataframe()
|
|
7
|
+
|
|
8
|
+
def plot_token_curve(self, save_path: str = "perplexity_curve.html", x_axis: str = "index", tick_angle: int = 0):
|
|
9
|
+
"""
|
|
10
|
+
Generates an interactive line chart displaying token-by-token perplexity along a prompt or document,
|
|
11
|
+
facilitating the identification of uncertainty spikes.
|
|
12
|
+
|
|
13
|
+
Parameters:
|
|
14
|
+
- save_path (str, optional): File path to save the generated HTML file. Default: "perplexity_curve.html".
|
|
15
|
+
- x_axis (str, optional): Defines the X-axis representation ('index' or 'token'). Default: "index".
|
|
16
|
+
- tick_angle (int, optional): Rotation angle for X-axis labels (e.g., 0 for normal, 90 for vertical). Default: 0.
|
|
17
|
+
|
|
18
|
+
Example Usage:
|
|
19
|
+
--------------
|
|
20
|
+
visualizer.plot_token_curve(
|
|
21
|
+
save_path="perplexity_curve.html",
|
|
22
|
+
x_axis="token",
|
|
23
|
+
tick_angle=45
|
|
24
|
+
)
|
|
25
|
+
"""
|
|
26
|
+
if x_axis not in ["index", "token"]:
|
|
27
|
+
raise ValueError("O parâmetro 'x_axis' deve ser 'index' ou 'token'.")
|
|
28
|
+
|
|
29
|
+
x_data = self.df.index if x_axis == "index" else self.df["token"]
|
|
30
|
+
|
|
31
|
+
fig = px.line(
|
|
32
|
+
self.df,
|
|
33
|
+
x=x_data,
|
|
34
|
+
y="perplexity",
|
|
35
|
+
hover_data=["token"],
|
|
36
|
+
markers=True,
|
|
37
|
+
title="LLM Perplexity Curve by Token",
|
|
38
|
+
labels={"x": "Token Position" if x_axis == "index" else "Token", "perplexity": "Perplexity"}
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
fig.update_traces(
|
|
42
|
+
hovertemplate="<b>Token:</b> %{customdata[0]}<br><b>Perplexity:</b> %{y:.2f}<extra></extra>"
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
fig.update_layout(xaxis=dict(tickangle=tick_angle))
|
|
46
|
+
|
|
47
|
+
fig.write_html(save_path)
|
|
48
|
+
print(f"Interactive chart successfully saved to: {save_path}")
|
|
49
|
+
return fig
|
|
50
|
+
|
|
51
|
+
def plot_token_bars(self, save_path: str = "perplexity_bars.html", top_n: int = None, cascade: bool = True):
|
|
52
|
+
"""
|
|
53
|
+
Generates a horizontal bar chart displaying tokens on the Y-axis and perplexity values on the X-axis,
|
|
54
|
+
allowing clear visualization and ranking of token uncertainties.
|
|
55
|
+
|
|
56
|
+
Parameters:
|
|
57
|
+
- save_path (str, optional): File path to save the generated HTML file. Default: "perplexity_bars.html".
|
|
58
|
+
- top_n (int, optional): Limits the number of displayed tokens. Default: None (displays all tokens).
|
|
59
|
+
- cascade (bool, optional): If True, sorts tokens by perplexity value (highest on top). Default: True.
|
|
60
|
+
|
|
61
|
+
Example Usage:
|
|
62
|
+
--------------
|
|
63
|
+
visualizer.plot_token_bars(
|
|
64
|
+
save_path="perplexity_bars.html",
|
|
65
|
+
top_n=10,
|
|
66
|
+
cascade=True
|
|
67
|
+
)
|
|
68
|
+
"""
|
|
69
|
+
df_plot = self.df.copy()
|
|
70
|
+
|
|
71
|
+
if cascade:
|
|
72
|
+
df_plot = df_plot.sort_values(by="perplexity", ascending=True)
|
|
73
|
+
|
|
74
|
+
if top_n is not None:
|
|
75
|
+
if cascade:
|
|
76
|
+
df_plot = df_plot.tail(top_n)
|
|
77
|
+
else:
|
|
78
|
+
df_plot = df_plot.head(top_n)
|
|
79
|
+
|
|
80
|
+
fig = px.bar(
|
|
81
|
+
df_plot,
|
|
82
|
+
x="perplexity",
|
|
83
|
+
y="token",
|
|
84
|
+
orientation="h",
|
|
85
|
+
text="perplexity", # Define a coluna que terá o valor exibido na barra
|
|
86
|
+
title="LLM Perplexity by Token (Bar Chart)",
|
|
87
|
+
labels={"perplexity": "Perplexity", "token": "Token"}
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
# Formata o texto para exibir duas casas decimais e posiciona o texto dentro/ao lado da barra
|
|
91
|
+
fig.update_traces(
|
|
92
|
+
texttemplate='%{text:.2f}',
|
|
93
|
+
textposition='inside',
|
|
94
|
+
hovertemplate="<b>Token:</b> %{y}<br><b>Perplexity:</b> %{x:.2f}<extra></extra>"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
fig.write_html(save_path)
|
|
98
|
+
print(f"Interactive bar chart successfully saved to: {save_path}")
|
|
99
|
+
return fig
|
|
100
|
+
|
|
101
|
+
def perplexity_text(self, save_path: str = "perplexity_text.html", top_n: int = None):
|
|
102
|
+
"""
|
|
103
|
+
Generates an individual SHAP-style highlighted text visualization where token background colors
|
|
104
|
+
and legend gradients are dynamically calculated based on token perplexity scores.
|
|
105
|
+
|
|
106
|
+
Parameters:
|
|
107
|
+
- save_path (str, optional): File path to save the generated HTML file. Default: "perplexity_text.html".
|
|
108
|
+
- top_n (int, optional): Limits highlighting to the top N tokens by perplexity score. Default: None (highlights all).
|
|
109
|
+
|
|
110
|
+
Example Usage:
|
|
111
|
+
--------------
|
|
112
|
+
visualizer.perplexity_text(
|
|
113
|
+
save_path="perplexity_text.html",
|
|
114
|
+
top_n=5
|
|
115
|
+
)
|
|
116
|
+
"""
|
|
117
|
+
df = self.df.copy()
|
|
118
|
+
|
|
119
|
+
# Identifica os tokens que entram no top_n
|
|
120
|
+
if top_n is not None and top_n < len(df):
|
|
121
|
+
df_filtered = df.nlargest(top_n, "perplexity")
|
|
122
|
+
top_indices = df_filtered.index
|
|
123
|
+
else:
|
|
124
|
+
df_filtered = df
|
|
125
|
+
top_indices = df.index
|
|
126
|
+
|
|
127
|
+
# Recalcula min e max baseados estritamente nos itens que receberão o destaque
|
|
128
|
+
min_p = df_filtered["perplexity"].min()
|
|
129
|
+
max_p = df_filtered["perplexity"].max()
|
|
130
|
+
|
|
131
|
+
def get_color_and_text_color(val, is_highlighted):
|
|
132
|
+
if not is_highlighted:
|
|
133
|
+
return "transparent", "#333333", False
|
|
134
|
+
|
|
135
|
+
if max_p == min_p:
|
|
136
|
+
norm_val = 0.5
|
|
137
|
+
else:
|
|
138
|
+
# Normaliza usando o novo limite inferior do filtro
|
|
139
|
+
norm_val = (val - min_p) / (max_p - min_p)
|
|
140
|
+
|
|
141
|
+
r = int(255 - norm_val * (255 - 128))
|
|
142
|
+
g = int(192 - norm_val * 192)
|
|
143
|
+
b = int(203 - norm_val * (203 - 32))
|
|
144
|
+
alpha = 0.3 + (norm_val * 0.7)
|
|
145
|
+
bg_color = f"rgba({r}, {g}, {b}, {alpha})"
|
|
146
|
+
text_color = "#ffffff" if norm_val > 0.5 else "#333333"
|
|
147
|
+
|
|
148
|
+
return bg_color, text_color, True
|
|
149
|
+
|
|
150
|
+
spans = []
|
|
151
|
+
for idx, row in df.iterrows():
|
|
152
|
+
token = row["token"]
|
|
153
|
+
p_val = row["perplexity"]
|
|
154
|
+
is_hl = idx in top_indices
|
|
155
|
+
|
|
156
|
+
bg_color, text_color, highlighted = get_color_and_text_color(p_val, is_hl)
|
|
157
|
+
safe_token = token.replace("\n", "<br>")
|
|
158
|
+
|
|
159
|
+
if highlighted:
|
|
160
|
+
span_html = (
|
|
161
|
+
f'<span style="background-color: {bg_color}; color: {text_color}; '
|
|
162
|
+
f'padding: 3px 6px; margin: 2px; border-radius: 4px; display: inline-block; '
|
|
163
|
+
f'font-weight: 500;" title="Perplexity: {p_val:.2f}">{safe_token}</span>'
|
|
164
|
+
)
|
|
165
|
+
else:
|
|
166
|
+
span_html = (
|
|
167
|
+
f'<span style="background-color: transparent; color: {text_color}; '
|
|
168
|
+
f'padding: 3px 6px; margin: 2px; display: inline-block;" '
|
|
169
|
+
f'title="Perplexity: {p_val:.2f} (Not in Top {top_n})">{safe_token}</span>'
|
|
170
|
+
)
|
|
171
|
+
spans.append(span_html)
|
|
172
|
+
|
|
173
|
+
legend_start = "rgba(255, 192, 203, 0.3)"
|
|
174
|
+
legend_end = "rgba(128, 0, 32, 1.0)"
|
|
175
|
+
|
|
176
|
+
html_content = f"""
|
|
177
|
+
<!DOCTYPE html>
|
|
178
|
+
<html>
|
|
179
|
+
<head>
|
|
180
|
+
<meta charset="utf-8">
|
|
181
|
+
<title>LLM Perplexity Text Highlight</title>
|
|
182
|
+
<style>
|
|
183
|
+
body {{
|
|
184
|
+
font-family: Arial, sans-serif;
|
|
185
|
+
margin: 40px;
|
|
186
|
+
background-color: #f9f9f9;
|
|
187
|
+
color: #333;
|
|
188
|
+
}}
|
|
189
|
+
.container {{
|
|
190
|
+
background: #fff;
|
|
191
|
+
padding: 25px;
|
|
192
|
+
border-radius: 8px;
|
|
193
|
+
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
|
194
|
+
line-height: 2.5;
|
|
195
|
+
font-size: 18px;
|
|
196
|
+
}}
|
|
197
|
+
h2 {{
|
|
198
|
+
margin-bottom: 20px;
|
|
199
|
+
}}
|
|
200
|
+
.legend-container {{
|
|
201
|
+
margin-top: 25px;
|
|
202
|
+
display: flex;
|
|
203
|
+
align-items: center;
|
|
204
|
+
gap: 15px;
|
|
205
|
+
font-size: 14px;
|
|
206
|
+
color: #555;
|
|
207
|
+
}}
|
|
208
|
+
.legend-bar {{
|
|
209
|
+
width: 200px;
|
|
210
|
+
height: 14px;
|
|
211
|
+
border-radius: 4px;
|
|
212
|
+
background: linear-gradient(to right, {legend_start}, {legend_end});
|
|
213
|
+
border: 1px solid #ddd;
|
|
214
|
+
}}
|
|
215
|
+
</style>
|
|
216
|
+
</head>
|
|
217
|
+
<body>
|
|
218
|
+
<h2>LLM Perplexity Text Highlight (Top {top_n if top_n else 'All'} Tokens)</h2>
|
|
219
|
+
<div class="container">
|
|
220
|
+
{"".join(spans)}
|
|
221
|
+
|
|
222
|
+
<div class="legend-container">
|
|
223
|
+
<span>Low ({min_p:.2f})</span>
|
|
224
|
+
<div class="legend-bar"></div>
|
|
225
|
+
<span>High ({max_p:.2f})</span>
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
</body>
|
|
229
|
+
</html>
|
|
230
|
+
"""
|
|
231
|
+
|
|
232
|
+
with open(save_path, "w", encoding="utf-8") as f:
|
|
233
|
+
f.write(html_content)
|
|
234
|
+
|
|
235
|
+
print(f"Text highlight visualization with recalculated legend successfully saved to: {save_path}")
|
|
236
|
+
|
|
237
|
+
def compare_perplexity_texts(
|
|
238
|
+
self,
|
|
239
|
+
save_path: str = "perplexity_comparison.html",
|
|
240
|
+
tokens: list[list[str]] = None,
|
|
241
|
+
perplexities: list[list[float]] = None,
|
|
242
|
+
labels: list[str] = None,
|
|
243
|
+
top_n: int = None
|
|
244
|
+
):
|
|
245
|
+
"""
|
|
246
|
+
Generates a comparative multi-model SHAP-style text visualization using a unified global
|
|
247
|
+
color scale gradient and a single consolidated legend in the footer.
|
|
248
|
+
|
|
249
|
+
Parameters:
|
|
250
|
+
- save_path (str, optional): File path to save the generated HTML file. Default: "perplexity_comparison.html".
|
|
251
|
+
- tokens (list of list of str, required): List containing token lists for each model. Default: None.
|
|
252
|
+
- perplexities (list of list of float, required): List containing perplexity score lists for each model. Default: None.
|
|
253
|
+
- labels (list of str, optional): Model identifiers or aliases. Default: None (auto-generated).
|
|
254
|
+
- top_n (int, optional): Limits highlighting to the top N tokens per model. Default: None.
|
|
255
|
+
|
|
256
|
+
Example Usage:
|
|
257
|
+
--------------
|
|
258
|
+
visualizer.compare_perplexity_texts(
|
|
259
|
+
save_path="perplexity_comparison.html",
|
|
260
|
+
tokens=[["O", " modelo"], ["A", " resposta"]],
|
|
261
|
+
perplexities=[[1.1, 2.3], [2.0, 3.5]],
|
|
262
|
+
labels=["Gemini", "GPT-4"],
|
|
263
|
+
top_n=2
|
|
264
|
+
)
|
|
265
|
+
"""
|
|
266
|
+
if not tokens or not perplexities or len(tokens) != len(perplexities):
|
|
267
|
+
raise ValueError("As listas 'tokens' e 'perplexities' devem ser fornecidas e ter o mesmo comprimento.")
|
|
268
|
+
|
|
269
|
+
if labels is None:
|
|
270
|
+
labels = [f"Model {i+1}" for i in range(len(tokens))]
|
|
271
|
+
|
|
272
|
+
if len(tokens) != len(labels):
|
|
273
|
+
raise ValueError("A quantidade de listas de tokens deve ser igual à quantidade de 'labels'.")
|
|
274
|
+
|
|
275
|
+
import pandas as pd
|
|
276
|
+
|
|
277
|
+
models_dfs = []
|
|
278
|
+
all_highlighted_perplexities = []
|
|
279
|
+
|
|
280
|
+
# Passo 1: Processa cada modelo e coleta os valores destacados globalmente
|
|
281
|
+
for model_tokens, model_perps, label in zip(tokens, perplexities, labels):
|
|
282
|
+
if len(model_tokens) != len(model_perps):
|
|
283
|
+
raise ValueError(f"As listas de tokens e perplexidades do modelo '{label}' devem ter o mesmo tamanho.")
|
|
284
|
+
|
|
285
|
+
df = pd.DataFrame({"token": model_tokens, "perplexity": model_perps})
|
|
286
|
+
|
|
287
|
+
if top_n is not None and top_n < len(df):
|
|
288
|
+
df_filtered = df.nlargest(top_n, "perplexity")
|
|
289
|
+
top_indices = set(df_filtered.index)
|
|
290
|
+
else:
|
|
291
|
+
top_indices = set(df.index)
|
|
292
|
+
df_filtered = df
|
|
293
|
+
|
|
294
|
+
models_dfs.append({"df": df, "top_indices": top_indices, "label": label})
|
|
295
|
+
|
|
296
|
+
if not df_filtered.empty:
|
|
297
|
+
all_highlighted_perplexities.extend(df_filtered["perplexity"].tolist())
|
|
298
|
+
|
|
299
|
+
# Passo 2: Determina o Mínimo e Máximo global entre todos os tokens destacados
|
|
300
|
+
if all_highlighted_perplexities:
|
|
301
|
+
global_min_p = min(all_highlighted_perplexities)
|
|
302
|
+
global_max_p = max(all_highlighted_perplexities)
|
|
303
|
+
else:
|
|
304
|
+
global_min_p = 0.0
|
|
305
|
+
global_max_p = 1.0
|
|
306
|
+
|
|
307
|
+
def get_color_and_text_color(val, is_highlighted):
|
|
308
|
+
if not is_highlighted:
|
|
309
|
+
return "transparent", "#333333", False
|
|
310
|
+
|
|
311
|
+
if global_max_p == global_min_p:
|
|
312
|
+
norm_val = 0.5
|
|
313
|
+
else:
|
|
314
|
+
norm_val = (val - global_min_p) / (global_max_p - global_min_p)
|
|
315
|
+
norm_val = max(0.0, min(1.0, norm_val))
|
|
316
|
+
|
|
317
|
+
r = int(255 - norm_val * (255 - 128))
|
|
318
|
+
g = int(192 - norm_val * 192)
|
|
319
|
+
b = int(203 - norm_val * (203 - 32))
|
|
320
|
+
alpha = 0.3 + (norm_val * 0.7)
|
|
321
|
+
bg_color = f"rgba({r}, {g}, {b}, {alpha})"
|
|
322
|
+
text_color = "#ffffff" if norm_val > 0.5 else "#333333"
|
|
323
|
+
|
|
324
|
+
return bg_color, text_color, True
|
|
325
|
+
|
|
326
|
+
sections_html = []
|
|
327
|
+
|
|
328
|
+
# Passo 3: Constrói o HTML de cada seção de texto
|
|
329
|
+
for item in models_dfs:
|
|
330
|
+
df = item["df"]
|
|
331
|
+
top_indices = item["top_indices"]
|
|
332
|
+
label = item["label"]
|
|
333
|
+
|
|
334
|
+
spans = []
|
|
335
|
+
for idx, row in df.iterrows():
|
|
336
|
+
token = row["token"]
|
|
337
|
+
p_val = row["perplexity"]
|
|
338
|
+
is_hl = idx in top_indices
|
|
339
|
+
|
|
340
|
+
bg_color, text_color, highlighted = get_color_and_text_color(p_val, is_hl)
|
|
341
|
+
safe_token = token.replace("\n", "<br>")
|
|
342
|
+
|
|
343
|
+
if highlighted:
|
|
344
|
+
span_html = (
|
|
345
|
+
f'<span style="background-color: {bg_color}; color: {text_color}; '
|
|
346
|
+
f'padding: 3px 6px; margin: 2px; border-radius: 4px; display: inline-block; '
|
|
347
|
+
f'font-weight: 500;" title="Perplexity: {p_val:.2f}">{safe_token}</span>'
|
|
348
|
+
)
|
|
349
|
+
else:
|
|
350
|
+
span_html = (
|
|
351
|
+
f'<span style="background-color: transparent; color: {text_color}; '
|
|
352
|
+
f'padding: 3px 6px; margin: 2px; display: inline-block;" '
|
|
353
|
+
f'title="Perplexity: {p_val:.2f} (Not in Top {top_n})">{safe_token}</span>'
|
|
354
|
+
)
|
|
355
|
+
spans.append(span_html)
|
|
356
|
+
|
|
357
|
+
section_content = f"""
|
|
358
|
+
<div class="model-section">
|
|
359
|
+
<h3>{label}</h3>
|
|
360
|
+
<div class="text-container">
|
|
361
|
+
{"".join(spans)}
|
|
362
|
+
</div>
|
|
363
|
+
</div>
|
|
364
|
+
"""
|
|
365
|
+
sections_html.append(section_content)
|
|
366
|
+
|
|
367
|
+
legend_start = "rgba(255, 192, 203, 0.3)"
|
|
368
|
+
legend_end = "rgba(128, 0, 32, 1.0)"
|
|
369
|
+
|
|
370
|
+
html_content = f"""
|
|
371
|
+
<!DOCTYPE html>
|
|
372
|
+
<html>
|
|
373
|
+
<head>
|
|
374
|
+
<meta charset="utf-8">
|
|
375
|
+
<title>LLM Perplexity Comparison</title>
|
|
376
|
+
<style>
|
|
377
|
+
body {{
|
|
378
|
+
font-family: Arial, sans-serif;
|
|
379
|
+
margin: 40px;
|
|
380
|
+
background-color: #f9f9f9;
|
|
381
|
+
color: #333;
|
|
382
|
+
}}
|
|
383
|
+
.model-section {{
|
|
384
|
+
margin-bottom: 25px;
|
|
385
|
+
}}
|
|
386
|
+
h3 {{
|
|
387
|
+
margin-bottom: 8px;
|
|
388
|
+
color: #444;
|
|
389
|
+
}}
|
|
390
|
+
.text-container {{
|
|
391
|
+
background: #fff;
|
|
392
|
+
padding: 20px;
|
|
393
|
+
border-radius: 8px;
|
|
394
|
+
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
|
395
|
+
line-height: 2.5;
|
|
396
|
+
font-size: 18px;
|
|
397
|
+
}}
|
|
398
|
+
h2 {{
|
|
399
|
+
margin-bottom: 30px;
|
|
400
|
+
}}
|
|
401
|
+
.global-legend-container {{
|
|
402
|
+
margin-top: 35px;
|
|
403
|
+
background: #fff;
|
|
404
|
+
padding: 20px;
|
|
405
|
+
border-radius: 8px;
|
|
406
|
+
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
|
|
407
|
+
display: flex;
|
|
408
|
+
align-items: center;
|
|
409
|
+
justify-content: center;
|
|
410
|
+
gap: 20px;
|
|
411
|
+
font-size: 15px;
|
|
412
|
+
color: #555;
|
|
413
|
+
}}
|
|
414
|
+
.legend-bar {{
|
|
415
|
+
width: 300px;
|
|
416
|
+
height: 16px;
|
|
417
|
+
border-radius: 4px;
|
|
418
|
+
background: linear-gradient(to right, {legend_start}, {legend_end});
|
|
419
|
+
border: 1px solid #ddd;
|
|
420
|
+
}}
|
|
421
|
+
</style>
|
|
422
|
+
</head>
|
|
423
|
+
<body>
|
|
424
|
+
<h2>LLM Perplexity Text Comparison (Top {top_n if top_n else 'All'} Tokens)</h2>
|
|
425
|
+
{"".join(sections_html)}
|
|
426
|
+
|
|
427
|
+
<div class="global-legend-container">
|
|
428
|
+
<span>Low ({global_min_p:.2f})</span>
|
|
429
|
+
<div class="legend-bar"></div>
|
|
430
|
+
<span>High ({global_max_p:.2f})</span>
|
|
431
|
+
</div>
|
|
432
|
+
</body>
|
|
433
|
+
</html>
|
|
434
|
+
"""
|
|
435
|
+
|
|
436
|
+
with open(save_path, "w", encoding="utf-8") as f:
|
|
437
|
+
f.write(html_content)
|
|
438
|
+
|
|
439
|
+
print(f"Global comparison visualization successfully saved to: {save_path}")
|
|
440
|
+
|
|
441
|
+
def plot_macro_comparison(
|
|
442
|
+
self,
|
|
443
|
+
macro_data: dict[str, list[float]],
|
|
444
|
+
save_path: str = "macro_perplexity_comparison.html",
|
|
445
|
+
show_values: bool = False
|
|
446
|
+
):
|
|
447
|
+
"""
|
|
448
|
+
Generates an interactive Boxplot for macro-level comparison of perplexity distributions across multiple models,
|
|
449
|
+
showing statistical metrics (Min, Q1, Median, Q3, Max) with model-aligned colors.
|
|
450
|
+
|
|
451
|
+
Parameters:
|
|
452
|
+
- macro_data (dict, required): Dictionary mapping model names to lists of perplexity floats. Default: None (positional).
|
|
453
|
+
- save_path (str, optional): File path to save the generated HTML file. Default: "macro_perplexity_comparison.html".
|
|
454
|
+
- show_values (bool, optional): If True, displays key statistical values on the right side. Default: False.
|
|
455
|
+
|
|
456
|
+
Example Usage:
|
|
457
|
+
--------------
|
|
458
|
+
macro_data = {
|
|
459
|
+
"Gemini": [1.25, 1.30, 1.45],
|
|
460
|
+
"GPT-4": [1.10, 1.15, 1.25]
|
|
461
|
+
}
|
|
462
|
+
visualizer.plot_macro_comparison(
|
|
463
|
+
macro_data=macro_data,
|
|
464
|
+
save_path="macro_boxplot.html",
|
|
465
|
+
show_values=True
|
|
466
|
+
)
|
|
467
|
+
"""
|
|
468
|
+
import pandas as pd
|
|
469
|
+
import numpy as np
|
|
470
|
+
import plotly.graph_objects as go
|
|
471
|
+
|
|
472
|
+
rows = []
|
|
473
|
+
for model_name, values in macro_data.items():
|
|
474
|
+
for val in values:
|
|
475
|
+
rows.append({"Model": model_name, "Perplexity": val})
|
|
476
|
+
|
|
477
|
+
df_long = pd.DataFrame(rows)
|
|
478
|
+
|
|
479
|
+
fig = go.Figure()
|
|
480
|
+
colors = px.colors.qualitative.Plotly[:len(macro_data)]
|
|
481
|
+
models = list(macro_data.items())
|
|
482
|
+
|
|
483
|
+
for i, (model_name, values) in enumerate(models):
|
|
484
|
+
df_model = df_long[df_long["Model"] == model_name]
|
|
485
|
+
color = colors[i % len(colors)]
|
|
486
|
+
|
|
487
|
+
fig.add_trace(go.Box(
|
|
488
|
+
y=df_model["Perplexity"],
|
|
489
|
+
name=model_name,
|
|
490
|
+
boxpoints=False,
|
|
491
|
+
marker_color=color
|
|
492
|
+
))
|
|
493
|
+
|
|
494
|
+
if show_values:
|
|
495
|
+
arr = np.array(values)
|
|
496
|
+
q1 = np.percentile(arr, 25)
|
|
497
|
+
median = np.median(arr)
|
|
498
|
+
q3 = np.percentile(arr, 75)
|
|
499
|
+
minimum = np.min(arr)
|
|
500
|
+
maximum = np.max(arr)
|
|
501
|
+
|
|
502
|
+
stat_y = [minimum, q1, median, q3, maximum]
|
|
503
|
+
stat_labels = [
|
|
504
|
+
f"Min: {minimum:.2f}",
|
|
505
|
+
f"Q1: {q1:.2f}",
|
|
506
|
+
f"Median: {median:.2f}",
|
|
507
|
+
f"Q3: {q3:.2f}",
|
|
508
|
+
f"Max: {maximum:.2f}"
|
|
509
|
+
]
|
|
510
|
+
|
|
511
|
+
fig.add_trace(go.Scatter(
|
|
512
|
+
x=[model_name] * len(stat_y),
|
|
513
|
+
y=stat_y,
|
|
514
|
+
mode="markers+text",
|
|
515
|
+
text=stat_labels,
|
|
516
|
+
textposition="top right",
|
|
517
|
+
textfont=dict(size=11, color=color, family="Arial Black"),
|
|
518
|
+
marker=dict(color=color, size=6, symbol="circle"),
|
|
519
|
+
showlegend=False,
|
|
520
|
+
hovertemplate="Model: " + model_name + "<br>%{text}<extra></extra>"
|
|
521
|
+
))
|
|
522
|
+
|
|
523
|
+
fig.update_layout(
|
|
524
|
+
title="Macro Comparison: Statistical Distribution Summary",
|
|
525
|
+
xaxis_title="Models",
|
|
526
|
+
yaxis_title="Perplexity",
|
|
527
|
+
showlegend=False,
|
|
528
|
+
margin=dict(r=150)
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
fig.write_html(save_path)
|
|
532
|
+
print(f"Macro comparison statistical chart successfully saved to: {save_path}")
|
|
533
|
+
return fig
|
|
534
|
+
|
|
535
|
+
def plot_macro_violin(
|
|
536
|
+
self,
|
|
537
|
+
macro_data: dict[str, list[float]],
|
|
538
|
+
save_path: str = "macro_violin_comparison.html",
|
|
539
|
+
show_values: bool = False
|
|
540
|
+
):
|
|
541
|
+
"""
|
|
542
|
+
Generates an interactive Violin Plot for macro-level analysis of perplexity distributions,
|
|
543
|
+
displaying probability density, internal box stats, and optional key statistical markers.
|
|
544
|
+
|
|
545
|
+
Parameters:
|
|
546
|
+
- macro_data (dict, required): Dictionary mapping model names to lists of perplexity floats. Default: None (positional).
|
|
547
|
+
- save_path (str, optional): File path to save the generated HTML file. Default: "macro_violin_comparison.html".
|
|
548
|
+
- show_values (bool, optional): If True, displays statistical values (Min, Q1, Median, Q3, Max). Default: False.
|
|
549
|
+
|
|
550
|
+
Example Usage:
|
|
551
|
+
--------------
|
|
552
|
+
macro_data = {
|
|
553
|
+
"Gemini": [1.25, 1.30, 1.45],
|
|
554
|
+
"GPT-4": [1.10, 1.15, 1.25]
|
|
555
|
+
}
|
|
556
|
+
visualizer.plot_macro_violin(
|
|
557
|
+
macro_data=macro_data,
|
|
558
|
+
save_path="macro_violin.html",
|
|
559
|
+
show_values=True
|
|
560
|
+
)
|
|
561
|
+
"""
|
|
562
|
+
import pandas as pd
|
|
563
|
+
import numpy as np
|
|
564
|
+
import plotly.graph_objects as go
|
|
565
|
+
|
|
566
|
+
rows = []
|
|
567
|
+
for model_name, values in macro_data.items():
|
|
568
|
+
for val in values:
|
|
569
|
+
rows.append({"Model": model_name, "Perplexity": val})
|
|
570
|
+
|
|
571
|
+
df_long = pd.DataFrame(rows)
|
|
572
|
+
|
|
573
|
+
fig = go.Figure()
|
|
574
|
+
colors = px.colors.qualitative.Plotly[:len(macro_data)]
|
|
575
|
+
models = list(macro_data.items())
|
|
576
|
+
|
|
577
|
+
for i, (model_name, values) in enumerate(models):
|
|
578
|
+
df_model = df_long[df_long["Model"] == model_name]
|
|
579
|
+
color = colors[i % len(colors)]
|
|
580
|
+
|
|
581
|
+
# Adiciona o Violin Plot com caixa interna (box) embutida
|
|
582
|
+
fig.add_trace(go.Violin(
|
|
583
|
+
y=df_model["Perplexity"],
|
|
584
|
+
name=model_name,
|
|
585
|
+
box_visible=True, # Mostra o boxplot interno embutido no violino
|
|
586
|
+
meanline_visible=True, # Mostra a linha da média
|
|
587
|
+
fillcolor=color,
|
|
588
|
+
opacity=0.6,
|
|
589
|
+
line_color=color
|
|
590
|
+
))
|
|
591
|
+
|
|
592
|
+
if show_values:
|
|
593
|
+
arr = np.array(values)
|
|
594
|
+
q1 = np.percentile(arr, 25)
|
|
595
|
+
median = np.median(arr)
|
|
596
|
+
q3 = np.percentile(arr, 75)
|
|
597
|
+
minimum = np.min(arr)
|
|
598
|
+
maximum = np.max(arr)
|
|
599
|
+
|
|
600
|
+
stat_y = [minimum, q1, median, q3, maximum]
|
|
601
|
+
stat_labels = [
|
|
602
|
+
f"Min: {minimum:.2f}",
|
|
603
|
+
f"Q1: {q1:.2f}",
|
|
604
|
+
f"Median: {median:.2f}",
|
|
605
|
+
f"Q3: {q3:.2f}",
|
|
606
|
+
f"Max: {maximum:.2f}"
|
|
607
|
+
]
|
|
608
|
+
|
|
609
|
+
fig.add_trace(go.Scatter(
|
|
610
|
+
x=[model_name] * len(stat_y),
|
|
611
|
+
y=stat_y,
|
|
612
|
+
mode="markers+text",
|
|
613
|
+
text=stat_labels,
|
|
614
|
+
textposition="top right",
|
|
615
|
+
textfont=dict(size=11, color=color, family="Arial Black"),
|
|
616
|
+
marker=dict(color=color, size=6, symbol="circle"),
|
|
617
|
+
showlegend=False,
|
|
618
|
+
hovertemplate="Model: " + model_name + "<br>%{text}<extra></extra>"
|
|
619
|
+
))
|
|
620
|
+
|
|
621
|
+
fig.update_layout(
|
|
622
|
+
title="Macro Comparison: Violin Distribution Summary",
|
|
623
|
+
xaxis_title="Models",
|
|
624
|
+
yaxis_title="Perplexity",
|
|
625
|
+
showlegend=False,
|
|
626
|
+
margin=dict(r=150) # Margem lateral para acomodar os rótulos estatísticos
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
fig.write_html(save_path)
|
|
630
|
+
print(f"Macro violin comparison chart successfully saved to: {save_path}")
|
|
631
|
+
return fig
|