flaviaggp commited on
Commit
8aadc08
·
verified ·
1 Parent(s): f34255d

Add embedding comparison notebook (Gemini Embedding 2 vs others, AML pt-BR)

Browse files
Files changed (1) hide show
  1. embedding_comparison.ipynb +1031 -0
embedding_comparison.ipynb ADDED
@@ -0,0 +1,1031 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "markdown",
5
+ "metadata": {},
6
+ "source": [
7
+ "# Comparação de Modelos de Embedding para Detecção de Lavagem de Dinheiro (pt-BR)\n",
8
+ "\n",
9
+ "**Autora:** Flavia Gaia \n",
10
+ "**Data:** Abril 2026 \n",
11
+ "**Objetivo:** Comparar Gemini Embedding 2 com modelos alternativos para classificação de textos financeiros em português, no contexto de detecção de lavagem de dinheiro (AML/CFT).\n",
12
+ "\n",
13
+ "---\n",
14
+ "\n",
15
+ "## Modelos avaliados\n",
16
+ "\n",
17
+ "| Modelo | Provider | Línguas | Dimensões |\n",
18
+ "|--------|----------|---------|----------|\n",
19
+ "| `gemini-embedding-exp-03-07` | Google | Multilingual (100+) | 3072 (ajustável) |\n",
20
+ "| `multilingual-e5-large-instruct` | Microsoft | 100+ | 1024 |\n",
21
+ "| `paraphrase-multilingual-mpnet-base-v2` | SBERT | 50+ | 768 |\n",
22
+ "| `nomic-embed-text-v1.5` | Nomic AI | Multilingual | 768 |\n",
23
+ "| `fine-tuned-aml-ptbr-v1` | flaviagaia | pt-BR (domínio financeiro) | 768 |\n",
24
+ "\n",
25
+ "---\n",
26
+ "\n",
27
+ "## Métricas de avaliação\n",
28
+ "\n",
29
+ "- **Similaridade Semântica:** Spearman correlation em pares anotados (dataset STS pt-BR)\n",
30
+ "- **Classificação Zero-Shot:** Precisão/Recall/F1 em categorias de risco AML\n",
31
+ "- **Clusterização:** Silhouette score em transações suspeitas vs. legítimas\n",
32
+ "- **Recuperação (IR):** MRR@10 e NDCG@10 em consultas regulatórias\n",
33
+ "- **Latência e Custo:** ms por chamada e custo/1M tokens"
34
+ ]
35
+ },
36
+ {
37
+ "cell_type": "markdown",
38
+ "metadata": {},
39
+ "source": [
40
+ "## 1. Setup e Instalação"
41
+ ]
42
+ },
43
+ {
44
+ "cell_type": "code",
45
+ "execution_count": null,
46
+ "metadata": {},
47
+ "outputs": [],
48
+ "source": [
49
+ "!pip install -q \\\n",
50
+ " google-generativeai \\\n",
51
+ " sentence-transformers \\\n",
52
+ " transformers \\\n",
53
+ " datasets \\\n",
54
+ " scikit-learn \\\n",
55
+ " umap-learn \\\n",
56
+ " matplotlib \\\n",
57
+ " seaborn \\\n",
58
+ " pandas \\\n",
59
+ " numpy \\\n",
60
+ " tqdm \\\n",
61
+ " huggingface_hub \\\n",
62
+ " einops \\\n",
63
+ " plotly"
64
+ ]
65
+ },
66
+ {
67
+ "cell_type": "code",
68
+ "execution_count": null,
69
+ "metadata": {},
70
+ "outputs": [],
71
+ "source": [
72
+ "import os\n",
73
+ "import time\n",
74
+ "import numpy as np\n",
75
+ "import pandas as pd\n",
76
+ "import matplotlib.pyplot as plt\n",
77
+ "import matplotlib.patches as mpatches\n",
78
+ "import seaborn as sns\n",
79
+ "import plotly.express as px\n",
80
+ "import plotly.graph_objects as go\n",
81
+ "from plotly.subplots import make_subplots\n",
82
+ "\n",
83
+ "from tqdm.auto import tqdm\n",
84
+ "from sklearn.metrics import (\n",
85
+ " classification_report, confusion_matrix,\n",
86
+ " silhouette_score, adjusted_rand_score\n",
87
+ ")\n",
88
+ "from sklearn.cluster import KMeans\n",
89
+ "from sklearn.preprocessing import normalize\n",
90
+ "from sklearn.linear_model import LogisticRegression\n",
91
+ "from scipy.stats import spearmanr\n",
92
+ "\n",
93
+ "import google.generativeai as genai\n",
94
+ "from sentence_transformers import SentenceTransformer\n",
95
+ "from huggingface_hub import login\n",
96
+ "\n",
97
+ "import umap\n",
98
+ "\n",
99
+ "plt.style.use('seaborn-v0_8-whitegrid')\n",
100
+ "SEED = 42\n",
101
+ "np.random.seed(SEED)\n",
102
+ "\n",
103
+ "print(\"✅ Dependências carregadas com sucesso!\")"
104
+ ]
105
+ },
106
+ {
107
+ "cell_type": "markdown",
108
+ "metadata": {},
109
+ "source": [
110
+ "## 2. Autenticação"
111
+ ]
112
+ },
113
+ {
114
+ "cell_type": "code",
115
+ "execution_count": null,
116
+ "metadata": {},
117
+ "outputs": [],
118
+ "source": [
119
+ "from google.colab import userdata\n",
120
+ "\n",
121
+ "# Google Gemini\n",
122
+ "GOOGLE_API_KEY = userdata.get('GOOGLE_API_KEY')\n",
123
+ "genai.configure(api_key=GOOGLE_API_KEY)\n",
124
+ "\n",
125
+ "# HuggingFace (para o modelo fine-tuned)\n",
126
+ "HF_TOKEN = userdata.get('HF_TOKEN')\n",
127
+ "login(token=HF_TOKEN)\n",
128
+ "\n",
129
+ "print(\"✅ Autenticação concluída!\")"
130
+ ]
131
+ },
132
+ {
133
+ "cell_type": "markdown",
134
+ "metadata": {},
135
+ "source": [
136
+ "## 3. Dataset: Transações e Textos Financeiros em pt-BR"
137
+ ]
138
+ },
139
+ {
140
+ "cell_type": "code",
141
+ "execution_count": null,
142
+ "metadata": {},
143
+ "outputs": [],
144
+ "source": [
145
+ "# Dataset sintético representativo de casos reais de AML no Brasil\n",
146
+ "# Baseado em tipologias do COAF/GAFI\n",
147
+ "\n",
148
+ "data = {\n",
149
+ " \"texto\": [\n",
150
+ " # Transações Suspeitas - Estruturação (Smurfing)\n",
151
+ " \"Múltiplos depósitos em dinheiro de R$9.800 realizados em agências diferentes no mesmo dia, logo abaixo do limite de comunicação obrigatória ao COAF.\",\n",
152
+ " \"Cliente realizou 15 depósitos em espécie de valores entre R$9.000 e R$9.900 em um período de 30 dias, totalizando R$145.000.\",\n",
153
+ " \"Movimentações fracionadas em diversas contas vinculadas, todas abaixo de R$10.000, com posterior consolidação em conta única no exterior.\",\n",
154
+ " \"Depósitos parcelados em caixas diferentes do mesmo banco no intervalo de 4 horas, evitando o limite de declaração compulsória.\",\n",
155
+ "\n",
156
+ " # Transações Suspeitas - Laranja / Conta de Terceiros\n",
157
+ " \"Conta de pessoa física com renda declarada de R$1.500/mês apresentou movimentação de R$2,3 milhões nos últimos 6 meses, sem justificativa econômica.\",\n",
158
+ " \"Funcionário de baixo escalão recebeu transferências de múltiplas empresas sem relação com sua atividade profissional declarada.\",\n",
159
+ " \"Idoso aposentado com benefício de R$1.200 passou a movimentar mais de R$500.000 mensais após abrir conta em banco digital.\",\n",
160
+ " \"Estudante universitário sem renda comprovada realizou mais de 300 PIX em um mês, com valores que somaram R$180.000.\",\n",
161
+ "\n",
162
+ " # Transações Suspeitas - Trade Based Money Laundering\n",
163
+ " \"Empresa exportou commodity agrícola com subfaturamento de 40% em relação ao preço de mercado internacional, com pagamento via offshore nas Ilhas Cayman.\",\n",
164
+ " \"Importação de mercadorias com superfaturamento expressivo, incompatível com os preços praticados no comércio internacional para produtos similares.\",\n",
165
+ " \"Notas fiscais de exportação apresentam valores divergentes dos contratos de câmbio registrados no BACEN, indicando possível subfaturamento.\",\n",
166
+ "\n",
167
+ " # Transações Suspeitas - PEP (Pessoa Politicamente Exposta)\n",
168
+ " \"Secretário municipal adquiriu imóvel de R$3,5 milhões em nome de familiar, incompatível com sua renda pública declarada de R$12.000/mês.\",\n",
169
+ " \"Servidor público federal realizou investimentos em fundos de renda variável em nome de cônjuge, utilizando recursos de origem não declarada.\",\n",
170
+ " \"Ex-governador transferiu R$8 milhões para conta em paraíso fiscal dois dias antes de ser indiciado por desvio de verbas públicas.\",\n",
171
+ "\n",
172
+ " # Transações Suspeitas - Criptomoedas\n",
173
+ " \"Conversão de R$1,2 milhão em Bitcoin através de exchanges não regulamentadas, seguida de mistura via serviço de tumbling e saque em stablecoin.\",\n",
174
+ " \"Cliente realizou compras de criptoativos em múltiplas corretoras para evitar o limite de reporte, totalizando R$380.000 em 48 horas.\",\n",
175
+ " \"Transações em blockchain rastreadas mostram padrão de 'peeling chain' típico de lavagem de criptomoedas por organização criminosa.\",\n",
176
+ "\n",
177
+ " # Transações Legítimas - Empresas\n",
178
+ " \"Empresa de construção civil recebeu pagamento de R$4,2 milhões referente à conclusão da 3ª fase de obra de edificação residencial conforme contrato.\",\n",
179
+ " \"Distribuidora de alimentos processou faturamento mensal de R$8 milhões, compatível com histórico de 5 anos e crescimento de 12% no setor.\",\n",
180
+ " \"Clínica médica recebeu repasse do convênio do plano de saúde no valor de R$320.000, referente a procedimentos realizados em março/2025.\",\n",
181
+ " \"Escritório de advocacia recebeu honorários de R$150.000 pela conclusão de processo trabalhista com acordo homologado pelo TRT.\",\n",
182
+ "\n",
183
+ " # Transações Legítimas - Pessoas Físicas\n",
184
+ " \"Profissional liberal emitiu notas fiscais de serviços de consultoria no valor de R$45.000 em março, compatível com declaração de IR anterior.\",\n",
185
+ " \"Vendedor autônomo de veículos usados realizou 8 transações entre R$20.000 e R$80.000, todas com documentação de transferência de propriedade registrada.\",\n",
186
+ " \"Aposentado recebeu indenização de seguro de vida de R$350.000 após falecimento do cônjuge, devidamente documentada pela seguradora.\",\n",
187
+ " \"Agricultor familiar recebeu crédito rural do PRONAF no valor de R$80.000 para custeio da safra, conforme contrato com banco público.\",\n",
188
+ "\n",
189
+ " # Normativas e Regulamentação\n",
190
+ " \"A Circular BACEN 3.978/2020 estabelece procedimentos para implementação de política de prevenção à lavagem de dinheiro e ao financiamento do terrorismo pelas instituições financeiras.\",\n",
191
+ " \"O COAF (Conselho de Controle de Atividades Financeiras) é a unidade de inteligência financeira do Brasil, responsável por receber e analisar comunicações de operações suspeitas.\",\n",
192
+ " \"A Resolução CVM 50/2021 determina que fundos de investimento implementem controles KYC e monitorem continuamente operações atípicas de seus cotistas.\",\n",
193
+ " \"O Decreto 9.663/2019 consolida as disposições sobre prevenção à lavagem de dinheiro, atualizando a regulamentação da Lei 9.613/1998.\",\n",
194
+ "\n",
195
+ " # Tipologias e Métodos\n",
196
+ " \"O método 'smurfing' consiste no fracionamento de grandes volumes de dinheiro ilícito em pequenas quantias para burlar sistemas de monitoramento automático.\",\n",
197
+ " \"Casas de câmbio são frequentemente utilizadas para conversão de moeda e integração de recursos ilícitos no sistema financeiro formal.\",\n",
198
+ " \"O uso de empresas de fachada (shell companies) em jurisdições com baixa transparência facilita o ocultamento da origem e titularidade de ativos.\",\n",
199
+ " ],\n",
200
+ " \"categoria\": [\n",
201
+ " \"suspeita_estruturacao\", \"suspeita_estruturacao\", \"suspeita_estruturacao\", \"suspeita_estruturacao\",\n",
202
+ " \"suspeita_laranja\", \"suspeita_laranja\", \"suspeita_laranja\", \"suspeita_laranja\",\n",
203
+ " \"suspeita_tbml\", \"suspeita_tbml\", \"suspeita_tbml\",\n",
204
+ " \"suspeita_pep\", \"suspeita_pep\", \"suspeita_pep\",\n",
205
+ " \"suspeita_cripto\", \"suspeita_cripto\", \"suspeita_cripto\",\n",
206
+ " \"legitima_empresa\", \"legitima_empresa\", \"legitima_empresa\", \"legitima_empresa\",\n",
207
+ " \"legitima_pf\", \"legitima_pf\", \"legitima_pf\", \"legitima_pf\",\n",
208
+ " \"regulamentacao\", \"regulamentacao\", \"regulamentacao\", \"regulamentacao\",\n",
209
+ " \"tipologia\", \"tipologia\", \"tipologia\",\n",
210
+ " ],\n",
211
+ " \"risco\": [\n",
212
+ " \"alto\", \"alto\", \"alto\", \"alto\",\n",
213
+ " \"alto\", \"alto\", \"alto\", \"alto\",\n",
214
+ " \"alto\", \"alto\", \"alto\",\n",
215
+ " \"alto\", \"alto\", \"alto\",\n",
216
+ " \"alto\", \"alto\", \"alto\",\n",
217
+ " \"baixo\", \"baixo\", \"baixo\", \"baixo\",\n",
218
+ " \"baixo\", \"baixo\", \"baixo\", \"baixo\",\n",
219
+ " \"referencia\", \"referencia\", \"referencia\", \"referencia\",\n",
220
+ " \"referencia\", \"referencia\", \"referencia\",\n",
221
+ " ]\n",
222
+ "}\n",
223
+ "\n",
224
+ "df = pd.DataFrame(data)\n",
225
+ "print(f\"Dataset: {len(df)} textos\")\n",
226
+ "print(\"\\nDistribuição por categoria:\")\n",
227
+ "print(df['categoria'].value_counts())\n",
228
+ "print(\"\\nDistribuição por risco:\")\n",
229
+ "print(df['risco'].value_counts())\n",
230
+ "df.head()"
231
+ ]
232
+ },
233
+ {
234
+ "cell_type": "markdown",
235
+ "metadata": {},
236
+ "source": [
237
+ "## 4. Dataset STS pt-BR para Avaliação de Similaridade Semântica"
238
+ ]
239
+ },
240
+ {
241
+ "cell_type": "code",
242
+ "execution_count": null,
243
+ "metadata": {},
244
+ "outputs": [],
245
+ "source": [
246
+ "# Pares de sentenças com scores de similaridade anotados (0.0 a 1.0)\n",
247
+ "sts_data = {\n",
248
+ " \"sentenca_1\": [\n",
249
+ " \"Depósitos fracionados abaixo do limite de comunicação obrigatória.\",\n",
250
+ " \"Transferência de recursos para paraíso fiscal sem justificativa econômica.\",\n",
251
+ " \"Empresa emitiu nota fiscal conforme contrato de prestação de serviços.\",\n",
252
+ " \"PEP adquiriu bem incompatível com renda declarada.\",\n",
253
+ " \"Conversão de dinheiro em criptoativos via exchange não regulamentada.\",\n",
254
+ " \"Cliente realizou saques em espécie de R$9.500 diariamente por 30 dias.\",\n",
255
+ " \"Operação de câmbio sem correspondência no sistema SISBACEN.\",\n",
256
+ " \"Pagamento de salários em espécie para funcionários fantasmas.\",\n",
257
+ " ],\n",
258
+ " \"sentenca_2\": [\n",
259
+ " \"Múltiplos saques de valores logo abaixo de R$10.000 para evitar monitoramento.\",\n",
260
+ " \"Remessa de capital ao exterior via conta em offshore sem lastro comercial.\",\n",
261
+ " \"Nota fiscal emitida regularmente conforme acordo contratual entre as partes.\",\n",
262
+ " \"Servidor público comprou imóvel de alto padrão incompatível com salário.\",\n",
263
+ " \"Compra de Bitcoin em corretora sem supervisão regulatória para ocultar origem de fundos.\",\n",
264
+ " \"Movimentações diárias em espécie de valor próximo ao limite de declaração compulsória.\",\n",
265
+ " \"Contrato de câmbio registrado no BACEN com valores diferentes da operação real.\",\n",
266
+ " \"Empresa paga trabalhadores de forma irregular sem registro em carteira.\",\n",
267
+ " ],\n",
268
+ " \"score_anotado\": [0.95, 0.90, 0.98, 0.92, 0.93, 0.97, 0.85, 0.75]\n",
269
+ "}\n",
270
+ "\n",
271
+ "df_sts = pd.DataFrame(sts_data)\n",
272
+ "print(f\"Pares STS: {len(df_sts)}\")\n",
273
+ "df_sts.head()"
274
+ ]
275
+ },
276
+ {
277
+ "cell_type": "markdown",
278
+ "metadata": {},
279
+ "source": [
280
+ "## 5. Funções de Embedding"
281
+ ]
282
+ },
283
+ {
284
+ "cell_type": "code",
285
+ "execution_count": null,
286
+ "metadata": {},
287
+ "outputs": [],
288
+ "source": [
289
+ "def get_gemini_embeddings(texts, task_type=\"SEMANTIC_SIMILARITY\", batch_size=5):\n",
290
+ " \"\"\"\n",
291
+ " Gera embeddings usando Gemini Embedding 2 (gemini-embedding-exp-03-07).\n",
292
+ " Suporta task_type: SEMANTIC_SIMILARITY, RETRIEVAL_DOCUMENT, RETRIEVAL_QUERY,\n",
293
+ " CLASSIFICATION, CLUSTERING, QUESTION_ANSWERING, FACT_VERIFICATION\n",
294
+ " \"\"\"\n",
295
+ " embeddings = []\n",
296
+ " latencies = []\n",
297
+ " \n",
298
+ " for i in tqdm(range(0, len(texts), batch_size), desc=\"Gemini Embeddings\"):\n",
299
+ " batch = texts[i:i+batch_size]\n",
300
+ " start = time.time()\n",
301
+ " \n",
302
+ " result = genai.embed_content(\n",
303
+ " model=\"models/gemini-embedding-exp-03-07\",\n",
304
+ " content=batch,\n",
305
+ " task_type=task_type,\n",
306
+ " )\n",
307
+ " \n",
308
+ " latency = (time.time() - start) / len(batch)\n",
309
+ " latencies.extend([latency] * len(batch))\n",
310
+ " embeddings.extend(result['embedding'])\n",
311
+ " time.sleep(0.5) # rate limit\n",
312
+ " \n",
313
+ " return np.array(embeddings), np.mean(latencies)\n",
314
+ "\n",
315
+ "\n",
316
+ "def get_sbert_embeddings(texts, model_name, batch_size=32):\n",
317
+ " \"\"\"Gera embeddings usando modelos SentenceTransformers.\"\"\"\n",
318
+ " model = SentenceTransformer(model_name)\n",
319
+ " \n",
320
+ " start = time.time()\n",
321
+ " embeddings = model.encode(\n",
322
+ " texts,\n",
323
+ " batch_size=batch_size,\n",
324
+ " show_progress_bar=True,\n",
325
+ " normalize_embeddings=True\n",
326
+ " )\n",
327
+ " avg_latency = (time.time() - start) / len(texts)\n",
328
+ " \n",
329
+ " return embeddings, avg_latency\n",
330
+ "\n",
331
+ "\n",
332
+ "def get_e5_embeddings(texts, batch_size=16):\n",
333
+ " \"\"\"Gera embeddings com multilingual-e5-large-instruct (com instrução prefixada).\"\"\"\n",
334
+ " model = SentenceTransformer(\"intfloat/multilingual-e5-large-instruct\")\n",
335
+ " \n",
336
+ " # E5 requer instrução de tarefa\n",
337
+ " instruction = \"Instruct: Classifique este texto financeiro quanto ao risco de lavagem de dinheiro.\\nQuery: \"\n",
338
+ " texts_with_instruction = [instruction + t for t in texts]\n",
339
+ " \n",
340
+ " start = time.time()\n",
341
+ " embeddings = model.encode(\n",
342
+ " texts_with_instruction,\n",
343
+ " batch_size=batch_size,\n",
344
+ " show_progress_bar=True,\n",
345
+ " normalize_embeddings=True\n",
346
+ " )\n",
347
+ " avg_latency = (time.time() - start) / len(texts)\n",
348
+ " \n",
349
+ " return embeddings, avg_latency\n",
350
+ "\n",
351
+ "\n",
352
+ "print(\"✅ Funções de embedding definidas!\")"
353
+ ]
354
+ },
355
+ {
356
+ "cell_type": "markdown",
357
+ "metadata": {},
358
+ "source": [
359
+ "## 6. Geração dos Embeddings"
360
+ ]
361
+ },
362
+ {
363
+ "cell_type": "code",
364
+ "execution_count": null,
365
+ "metadata": {},
366
+ "outputs": [],
367
+ "source": [
368
+ "texts = df['texto'].tolist()\n",
369
+ "latency_results = {}\n",
370
+ "\n",
371
+ "print(\"=\" * 60)\n",
372
+ "print(\"Gerando embeddings para todos os modelos...\")\n",
373
+ "print(\"=\" * 60)"
374
+ ]
375
+ },
376
+ {
377
+ "cell_type": "code",
378
+ "execution_count": null,
379
+ "metadata": {},
380
+ "outputs": [],
381
+ "source": [
382
+ "# Gemini Embeddings 2\n",
383
+ "print(\"\\n[1/5] Gemini Embeddings 2 (gemini-embedding-exp-03-07)\")\n",
384
+ "gemini_embeddings, gemini_latency = get_gemini_embeddings(texts, task_type=\"CLASSIFICATION\")\n",
385
+ "latency_results[\"Gemini Embedding 2\"] = gemini_latency\n",
386
+ "print(f\" Shape: {gemini_embeddings.shape} | Latência média: {gemini_latency*1000:.1f}ms/texto\")"
387
+ ]
388
+ },
389
+ {
390
+ "cell_type": "code",
391
+ "execution_count": null,
392
+ "metadata": {},
393
+ "outputs": [],
394
+ "source": [
395
+ "# Multilingual-E5-Large-Instruct\n",
396
+ "print(\"\\n[2/5] Multilingual-E5-Large-Instruct (Microsoft)\")\n",
397
+ "e5_embeddings, e5_latency = get_e5_embeddings(texts)\n",
398
+ "latency_results[\"M-E5-Large-Instruct\"] = e5_latency\n",
399
+ "print(f\" Shape: {e5_embeddings.shape} | Latência média: {e5_latency*1000:.1f}ms/texto\")"
400
+ ]
401
+ },
402
+ {
403
+ "cell_type": "code",
404
+ "execution_count": null,
405
+ "metadata": {},
406
+ "outputs": [],
407
+ "source": [
408
+ "# Paraphrase Multilingual MPNet Base\n",
409
+ "print(\"\\n[3/5] Paraphrase-Multilingual-MPNet-Base-v2 (SBERT)\")\n",
410
+ "mpnet_embeddings, mpnet_latency = get_sbert_embeddings(\n",
411
+ " texts, \"paraphrase-multilingual-mpnet-base-v2\"\n",
412
+ ")\n",
413
+ "latency_results[\"M-MPNet-Base-v2\"] = mpnet_latency\n",
414
+ "print(f\" Shape: {mpnet_embeddings.shape} | Latência média: {mpnet_latency*1000:.1f}ms/texto\")"
415
+ ]
416
+ },
417
+ {
418
+ "cell_type": "code",
419
+ "execution_count": null,
420
+ "metadata": {},
421
+ "outputs": [],
422
+ "source": [
423
+ "# Nomic Embed v1.5\n",
424
+ "print(\"\\n[4/5] Nomic-Embed-Text-v1.5\")\n",
425
+ "nomic_embeddings, nomic_latency = get_sbert_embeddings(\n",
426
+ " texts, \"nomic-ai/nomic-embed-text-v1.5\", batch_size=16\n",
427
+ ")\n",
428
+ "latency_results[\"Nomic-Embed-v1.5\"] = nomic_latency\n",
429
+ "print(f\" Shape: {nomic_embeddings.shape} | Latência média: {nomic_latency*1000:.1f}ms/texto\")"
430
+ ]
431
+ },
432
+ {
433
+ "cell_type": "code",
434
+ "execution_count": null,
435
+ "metadata": {},
436
+ "outputs": [],
437
+ "source": [
438
+ "# Modelo Fine-tuned (substitua pelo seu modelo no HuggingFace)\n",
439
+ "print(\"\\n[5/5] Fine-tuned AML pt-BR (flaviagaia/aml-ptbr-embedding-v1)\")\n",
440
+ "finetuned_embeddings, finetuned_latency = get_sbert_embeddings(\n",
441
+ " texts, \"flaviagaia/aml-ptbr-embedding-v1\"\n",
442
+ ")\n",
443
+ "latency_results[\"AML-ptBR-FT-v1\"] = finetuned_latency\n",
444
+ "print(f\" Shape: {finetuned_embeddings.shape} | Latência média: {finetuned_latency*1000:.1f}ms/texto\")\n",
445
+ "\n",
446
+ "print(\"\\n✅ Todos os embeddings gerados!\")"
447
+ ]
448
+ },
449
+ {
450
+ "cell_type": "markdown",
451
+ "metadata": {},
452
+ "source": [
453
+ "## 7. Avaliação 1: Similaridade Semântica (STS)"
454
+ ]
455
+ },
456
+ {
457
+ "cell_type": "code",
458
+ "execution_count": null,
459
+ "metadata": {},
460
+ "outputs": [],
461
+ "source": [
462
+ "def cosine_similarity(a, b):\n",
463
+ " \"\"\"Similaridade de cosseno entre dois vetores.\"\"\"\n",
464
+ " return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))\n",
465
+ "\n",
466
+ "\n",
467
+ "def evaluate_sts(embeddings_fn, sts_df, model_name):\n",
468
+ " \"\"\"Calcula Spearman correlation entre scores preditos e anotados.\"\"\"\n",
469
+ " all_texts = sts_df['sentenca_1'].tolist() + sts_df['sentenca_2'].tolist()\n",
470
+ " \n",
471
+ " if callable(embeddings_fn):\n",
472
+ " embs, _ = embeddings_fn(all_texts)\n",
473
+ " else:\n",
474
+ " # Já calculamos — reencode apenas os textos STS\n",
475
+ " embs = embeddings_fn\n",
476
+ " \n",
477
+ " n = len(sts_df)\n",
478
+ " embs_1 = embs[:n]\n",
479
+ " embs_2 = embs[n:]\n",
480
+ " \n",
481
+ " pred_scores = [cosine_similarity(embs_1[i], embs_2[i]) for i in range(n)]\n",
482
+ " corr, pvalue = spearmanr(sts_df['score_anotado'], pred_scores)\n",
483
+ " \n",
484
+ " return {\"model\": model_name, \"spearman_r\": corr, \"p_value\": pvalue, \"pred_scores\": pred_scores}\n",
485
+ "\n",
486
+ "\n",
487
+ "# Gerar embeddings especificamente para os textos STS\n",
488
+ "sts_texts = df_sts['sentenca_1'].tolist() + df_sts['sentenca_2'].tolist()\n",
489
+ "\n",
490
+ "print(\"Avaliando STS para todos os modelos...\")\n",
491
+ "\n",
492
+ "# Gemini STS\n",
493
+ "gemini_sts_embs, _ = get_gemini_embeddings(sts_texts, task_type=\"SEMANTIC_SIMILARITY\")\n",
494
+ "n_sts = len(df_sts)\n",
495
+ "gemini_sts_scores = [cosine_similarity(gemini_sts_embs[i], gemini_sts_embs[n_sts+i]) for i in range(n_sts)]\n",
496
+ "gemini_spearman, _ = spearmanr(df_sts['score_anotado'], gemini_sts_scores)\n",
497
+ "\n",
498
+ "# E5 STS\n",
499
+ "e5_sts_embs, _ = get_e5_embeddings(sts_texts)\n",
500
+ "e5_sts_scores = [cosine_similarity(e5_sts_embs[i], e5_sts_embs[n_sts+i]) for i in range(n_sts)]\n",
501
+ "e5_spearman, _ = spearmanr(df_sts['score_anotado'], e5_sts_scores)\n",
502
+ "\n",
503
+ "# MPNet STS\n",
504
+ "mpnet_sts_embs, _ = get_sbert_embeddings(sts_texts, \"paraphrase-multilingual-mpnet-base-v2\")\n",
505
+ "mpnet_sts_scores = [cosine_similarity(mpnet_sts_embs[i], mpnet_sts_embs[n_sts+i]) for i in range(n_sts)]\n",
506
+ "mpnet_spearman, _ = spearmanr(df_sts['score_anotado'], mpnet_sts_scores)\n",
507
+ "\n",
508
+ "# Nomic STS\n",
509
+ "nomic_sts_embs, _ = get_sbert_embeddings(sts_texts, \"nomic-ai/nomic-embed-text-v1.5\")\n",
510
+ "nomic_sts_scores = [cosine_similarity(nomic_sts_embs[i], nomic_sts_embs[n_sts+i]) for i in range(n_sts)]\n",
511
+ "nomic_spearman, _ = spearmanr(df_sts['score_anotado'], nomic_sts_scores)\n",
512
+ "\n",
513
+ "# Fine-tuned STS\n",
514
+ "ft_sts_embs, _ = get_sbert_embeddings(sts_texts, \"flaviagaia/aml-ptbr-embedding-v1\")\n",
515
+ "ft_sts_scores = [cosine_similarity(ft_sts_embs[i], ft_sts_embs[n_sts+i]) for i in range(n_sts)]\n",
516
+ "ft_spearman, _ = spearmanr(df_sts['score_anotado'], ft_sts_scores)\n",
517
+ "\n",
518
+ "sts_results = pd.DataFrame({\n",
519
+ " 'Modelo': ['Gemini Embedding 2', 'M-E5-Large-Instruct', 'M-MPNet-Base-v2', 'Nomic-Embed-v1.5', 'AML-ptBR-FT-v1'],\n",
520
+ " 'Spearman r': [gemini_spearman, e5_spearman, mpnet_spearman, nomic_spearman, ft_spearman]\n",
521
+ "}).sort_values('Spearman r', ascending=False)\n",
522
+ "\n",
523
+ "print(\"\\n📊 Resultados STS (Similaridade Semântica):\")\n",
524
+ "print(sts_results.to_string(index=False))"
525
+ ]
526
+ },
527
+ {
528
+ "cell_type": "markdown",
529
+ "metadata": {},
530
+ "source": [
531
+ "## 8. Avaliação 2: Classificação por Risco (Linear Probe)"
532
+ ]
533
+ },
534
+ {
535
+ "cell_type": "code",
536
+ "execution_count": null,
537
+ "metadata": {},
538
+ "outputs": [],
539
+ "source": [
540
+ "from sklearn.model_selection import StratifiedKFold, cross_val_score\n",
541
+ "\n",
542
+ "labels_binary = [1 if r == \"alto\" else 0 for r in df['risco']] # alto=1, outros=0\n",
543
+ "labels_multi = df['categoria'].tolist()\n",
544
+ "\n",
545
+ "def linear_probe_eval(embeddings, labels, model_name, task=\"binary\"):\n",
546
+ " \"\"\"Avalia qualidade dos embeddings com regressão logística (linear probe).\"\"\"\n",
547
+ " clf = LogisticRegression(max_iter=1000, random_state=SEED, C=1.0)\n",
548
+ " cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=SEED)\n",
549
+ " \n",
550
+ " scores = cross_val_score(\n",
551
+ " clf, embeddings, labels,\n",
552
+ " cv=cv,\n",
553
+ " scoring='f1_macro' if task == 'multi' else 'f1'\n",
554
+ " )\n",
555
+ " \n",
556
+ " return {\n",
557
+ " 'model': model_name,\n",
558
+ " 'f1_mean': scores.mean(),\n",
559
+ " 'f1_std': scores.std(),\n",
560
+ " 'task': task\n",
561
+ " }\n",
562
+ "\n",
563
+ "# Normalizar embeddings para comparação justa\n",
564
+ "models_embeddings = {\n",
565
+ " 'Gemini Embedding 2': normalize(gemini_embeddings),\n",
566
+ " 'M-E5-Large-Instruct': e5_embeddings,\n",
567
+ " 'M-MPNet-Base-v2': mpnet_embeddings,\n",
568
+ " 'Nomic-Embed-v1.5': nomic_embeddings,\n",
569
+ " 'AML-ptBR-FT-v1': finetuned_embeddings,\n",
570
+ "}\n",
571
+ "\n",
572
+ "classification_results = []\n",
573
+ "for model_name, embs in models_embeddings.items():\n",
574
+ " # Binário: suspeito vs. legítimo/referência\n",
575
+ " result_binary = linear_probe_eval(embs, labels_binary, model_name, task=\"binary\")\n",
576
+ " # Multiclasse: categoria AML\n",
577
+ " result_multi = linear_probe_eval(embs, labels_multi, model_name, task=\"multi\")\n",
578
+ " classification_results.append({\n",
579
+ " 'Modelo': model_name,\n",
580
+ " 'F1 Binário (alto risco)': f\"{result_binary['f1_mean']:.4f} ± {result_binary['f1_std']:.4f}\",\n",
581
+ " 'F1 Macro (9 categorias)': f\"{result_multi['f1_mean']:.4f} ± {result_multi['f1_std']:.4f}\",\n",
582
+ " '_f1_binary': result_binary['f1_mean'],\n",
583
+ " '_f1_multi': result_multi['f1_mean'],\n",
584
+ " })\n",
585
+ "\n",
586
+ "df_clf = pd.DataFrame(classification_results).sort_values('_f1_binary', ascending=False)\n",
587
+ "print(\"\\n📊 Resultados de Classificação (Linear Probe, 5-Fold CV):\")\n",
588
+ "print(df_clf[['Modelo', 'F1 Binário (alto risco)', 'F1 Macro (9 categorias)']].to_string(index=False))"
589
+ ]
590
+ },
591
+ {
592
+ "cell_type": "markdown",
593
+ "metadata": {},
594
+ "source": [
595
+ "## 9. Avaliação 3: Clusterização"
596
+ ]
597
+ },
598
+ {
599
+ "cell_type": "code",
600
+ "execution_count": null,
601
+ "metadata": {},
602
+ "outputs": [],
603
+ "source": [
604
+ "from sklearn.preprocessing import LabelEncoder\n",
605
+ "\n",
606
+ "le = LabelEncoder()\n",
607
+ "true_labels_encoded = le.fit_transform(df['categoria'])\n",
608
+ "n_clusters = len(df['categoria'].unique())\n",
609
+ "\n",
610
+ "clustering_results = []\n",
611
+ "\n",
612
+ "for model_name, embs in models_embeddings.items():\n",
613
+ " kmeans = KMeans(n_clusters=n_clusters, random_state=SEED, n_init=10)\n",
614
+ " cluster_labels = kmeans.fit_predict(embs)\n",
615
+ " \n",
616
+ " silhouette = silhouette_score(embs, cluster_labels)\n",
617
+ " ari = adjusted_rand_score(true_labels_encoded, cluster_labels)\n",
618
+ " \n",
619
+ " clustering_results.append({\n",
620
+ " 'Modelo': model_name,\n",
621
+ " 'Silhouette Score': round(silhouette, 4),\n",
622
+ " 'ARI (vs. labels reais)': round(ari, 4),\n",
623
+ " })\n",
624
+ "\n",
625
+ "df_clust = pd.DataFrame(clustering_results).sort_values('Silhouette Score', ascending=False)\n",
626
+ "print(\"\\n📊 Resultados de Clusterização:\")\n",
627
+ "print(df_clust.to_string(index=False))"
628
+ ]
629
+ },
630
+ {
631
+ "cell_type": "markdown",
632
+ "metadata": {},
633
+ "source": [
634
+ "## 10. Visualização UMAP: Separação Semântica"
635
+ ]
636
+ },
637
+ {
638
+ "cell_type": "code",
639
+ "execution_count": null,
640
+ "metadata": {},
641
+ "outputs": [],
642
+ "source": [
643
+ "def plot_umap(embeddings, labels, title, color_map=None):\n",
644
+ " \"\"\"Reduz dimensionalidade com UMAP e plota a distribuição dos embeddings.\"\"\"\n",
645
+ " reducer = umap.UMAP(n_neighbors=10, min_dist=0.3, metric='cosine', random_state=SEED)\n",
646
+ " reduced = reducer.fit_transform(embeddings)\n",
647
+ " \n",
648
+ " df_plot = pd.DataFrame({\n",
649
+ " 'x': reduced[:, 0],\n",
650
+ " 'y': reduced[:, 1],\n",
651
+ " 'categoria': labels,\n",
652
+ " 'texto': df['texto'].str[:80] + '...'\n",
653
+ " })\n",
654
+ " \n",
655
+ " fig = px.scatter(\n",
656
+ " df_plot, x='x', y='y', color='categoria',\n",
657
+ " hover_data=['texto'],\n",
658
+ " title=title,\n",
659
+ " color_discrete_sequence=px.colors.qualitative.Set2,\n",
660
+ " width=800, height=600\n",
661
+ " )\n",
662
+ " fig.update_traces(marker=dict(size=10, opacity=0.85))\n",
663
+ " fig.update_layout(\n",
664
+ " font_family=\"Arial\",\n",
665
+ " title_font_size=14,\n",
666
+ " legend_title_text='Categoria',\n",
667
+ " )\n",
668
+ " fig.show()\n",
669
+ " return fig\n",
670
+ "\n",
671
+ "categories = df['categoria'].tolist()\n",
672
+ "\n",
673
+ "# Plot para cada modelo\n",
674
+ "for model_name, embs in models_embeddings.items():\n",
675
+ " print(f\"\\nGerando UMAP para: {model_name}\")\n",
676
+ " fig = plot_umap(embs, categories, f\"UMAP — {model_name} (AML pt-BR)\")"
677
+ ]
678
+ },
679
+ {
680
+ "cell_type": "markdown",
681
+ "metadata": {},
682
+ "source": [
683
+ "## 11. Comparação de Latência e Custo Estimado"
684
+ ]
685
+ },
686
+ {
687
+ "cell_type": "code",
688
+ "execution_count": null,
689
+ "metadata": {},
690
+ "outputs": [],
691
+ "source": [
692
+ "# Custo estimado em USD por 1M tokens (Abril 2026 — verificar preços atuais)\n",
693
+ "cost_per_1m = {\n",
694
+ " 'Gemini Embedding 2': 0.015, # Google AI Studio / Vertex AI\n",
695
+ " 'M-E5-Large-Instruct': 0.0, # Open source (self-hosted)\n",
696
+ " 'M-MPNet-Base-v2': 0.0, # Open source\n",
697
+ " 'Nomic-Embed-v1.5': 0.0, # Open source\n",
698
+ " 'AML-ptBR-FT-v1': 0.0, # Open source (fine-tuned)\n",
699
+ "}\n",
700
+ "\n",
701
+ "# Parâmetros dos modelos\n",
702
+ "model_params = {\n",
703
+ " 'Gemini Embedding 2': '~2B (estimado)',\n",
704
+ " 'M-E5-Large-Instruct': '560M',\n",
705
+ " 'M-MPNet-Base-v2': '278M',\n",
706
+ " 'Nomic-Embed-v1.5': '137M',\n",
707
+ " 'AML-ptBR-FT-v1': '278M (fine-tuned)',\n",
708
+ "}\n",
709
+ "\n",
710
+ "model_dims = {\n",
711
+ " 'Gemini Embedding 2': 3072,\n",
712
+ " 'M-E5-Large-Instruct': 1024,\n",
713
+ " 'M-MPNet-Base-v2': 768,\n",
714
+ " 'Nomic-Embed-v1.5': 768,\n",
715
+ " 'AML-ptBR-FT-v1': 768,\n",
716
+ "}\n",
717
+ "\n",
718
+ "df_perf = pd.DataFrame({\n",
719
+ " 'Modelo': list(latency_results.keys()),\n",
720
+ " 'Latência Média (ms)': [v * 1000 for v in latency_results.values()],\n",
721
+ " 'Dimensões': [model_dims[k] for k in latency_results.keys()],\n",
722
+ " 'Parâmetros': [model_params[k] for k in latency_results.keys()],\n",
723
+ " 'Custo/1M tokens (USD)': [cost_per_1m[k] for k in latency_results.keys()],\n",
724
+ " 'Open Source': ['Não', 'Sim', 'Sim', 'Sim', 'Sim'],\n",
725
+ "})\n",
726
+ "\n",
727
+ "print(\"\\n📊 Performance e Custo:\")\n",
728
+ "print(df_perf.to_string(index=False))"
729
+ ]
730
+ },
731
+ {
732
+ "cell_type": "markdown",
733
+ "metadata": {},
734
+ "source": [
735
+ "## 12. Análise do Gemini: task_type vs. Performance"
736
+ ]
737
+ },
738
+ {
739
+ "cell_type": "code",
740
+ "execution_count": null,
741
+ "metadata": {},
742
+ "outputs": [],
743
+ "source": [
744
+ "# O Gemini Embedding 2 permite especificar o tipo de tarefa — comparar o efeito\n",
745
+ "task_types = [\n",
746
+ " \"SEMANTIC_SIMILARITY\",\n",
747
+ " \"CLASSIFICATION\",\n",
748
+ " \"CLUSTERING\",\n",
749
+ " \"RETRIEVAL_DOCUMENT\",\n",
750
+ "]\n",
751
+ "\n",
752
+ "task_type_results = []\n",
753
+ "\n",
754
+ "for task in task_types:\n",
755
+ " print(f\"Testando task_type={task}...\")\n",
756
+ " embs, _ = get_gemini_embeddings(texts, task_type=task)\n",
757
+ " embs_norm = normalize(embs)\n",
758
+ " \n",
759
+ " # Classificação binária\n",
760
+ " clf = LogisticRegression(max_iter=1000, random_state=SEED)\n",
761
+ " cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=SEED)\n",
762
+ " f1_scores = cross_val_score(clf, embs_norm, labels_binary, cv=cv, scoring='f1')\n",
763
+ " \n",
764
+ " # Clusterização\n",
765
+ " kmeans = KMeans(n_clusters=n_clusters, random_state=SEED, n_init=10)\n",
766
+ " cluster_labels = kmeans.fit_predict(embs_norm)\n",
767
+ " sil = silhouette_score(embs_norm, cluster_labels)\n",
768
+ " \n",
769
+ " task_type_results.append({\n",
770
+ " 'task_type': task,\n",
771
+ " 'F1 Binário': f\"{f1_scores.mean():.4f}\",\n",
772
+ " 'Silhouette': f\"{sil:.4f}\"\n",
773
+ " })\n",
774
+ "\n",
775
+ "df_task = pd.DataFrame(task_type_results)\n",
776
+ "print(\"\\n📊 Gemini Embedding 2 — Impacto do task_type:\")\n",
777
+ "print(df_task.to_string(index=False))"
778
+ ]
779
+ },
780
+ {
781
+ "cell_type": "markdown",
782
+ "metadata": {},
783
+ "source": [
784
+ "## 13. Heatmap de Similaridade: Detecção de Padrões AML"
785
+ ]
786
+ },
787
+ {
788
+ "cell_type": "code",
789
+ "execution_count": null,
790
+ "metadata": {},
791
+ "outputs": [],
792
+ "source": [
793
+ "def plot_similarity_heatmap(embeddings, labels, title, top_n=20):\n",
794
+ " \"\"\"Plota heatmap de similaridade de cosseno entre textos.\"\"\"\n",
795
+ " embs_norm = normalize(embeddings[:top_n])\n",
796
+ " sim_matrix = np.dot(embs_norm, embs_norm.T)\n",
797
+ " short_labels = [f\"{l[:25]}...\" for l in labels[:top_n]]\n",
798
+ " \n",
799
+ " fig, ax = plt.subplots(figsize=(12, 10))\n",
800
+ " sns.heatmap(\n",
801
+ " sim_matrix,\n",
802
+ " annot=False,\n",
803
+ " fmt='.2f',\n",
804
+ " cmap='RdYlGn',\n",
805
+ " xticklabels=False,\n",
806
+ " yticklabels=df['categoria'][:top_n],\n",
807
+ " vmin=0, vmax=1,\n",
808
+ " ax=ax\n",
809
+ " )\n",
810
+ " ax.set_title(f'Heatmap de Similaridade — {title}', fontsize=13, pad=15)\n",
811
+ " plt.tight_layout()\n",
812
+ " plt.show()\n",
813
+ "\n",
814
+ "for model_name, embs in models_embeddings.items():\n",
815
+ " plot_similarity_heatmap(embs, df['texto'].tolist(), model_name)"
816
+ ]
817
+ },
818
+ {
819
+ "cell_type": "markdown",
820
+ "metadata": {},
821
+ "source": [
822
+ "## 14. Resumo Comparativo Final"
823
+ ]
824
+ },
825
+ {
826
+ "cell_type": "code",
827
+ "execution_count": null,
828
+ "metadata": {},
829
+ "outputs": [],
830
+ "source": [
831
+ "# Construir tabela de resultados consolidada\n",
832
+ "summary = pd.DataFrame({\n",
833
+ " 'Modelo': ['Gemini Embedding 2', 'M-E5-Large-Instruct', 'M-MPNet-Base-v2', 'Nomic-Embed-v1.5', 'AML-ptBR-FT-v1'],\n",
834
+ " 'STS (Spearman r)': [gemini_spearman, e5_spearman, mpnet_spearman, nomic_spearman, ft_spearman],\n",
835
+ " 'F1 Binário': [r['_f1_binary'] for r in classification_results],\n",
836
+ " 'F1 Macro': [r['_f1_multi'] for r in classification_results],\n",
837
+ " 'Silhouette': df_clust['Silhouette Score'].values,\n",
838
+ " 'Latência (ms)': [v * 1000 for v in latency_results.values()],\n",
839
+ " 'Open Source': ['Não', 'Sim', 'Sim', 'Sim', 'Sim'],\n",
840
+ " 'Domínio pt-BR AML': ['Não', 'Não', 'Não', 'Não', 'Sim'],\n",
841
+ "})\n",
842
+ "\n",
843
+ "# Score composto (normalizado)\n",
844
+ "for col in ['STS (Spearman r)', 'F1 Binário', 'F1 Macro', 'Silhouette']:\n",
845
+ " summary[col + '_norm'] = (summary[col] - summary[col].min()) / (summary[col].max() - summary[col].min() + 1e-10)\n",
846
+ "\n",
847
+ "summary['Score Composto'] = summary[['STS (Spearman r)_norm', 'F1 Binário_norm', 'F1 Macro_norm', 'Silhouette_norm']].mean(axis=1)\n",
848
+ "summary = summary.sort_values('Score Composto', ascending=False)\n",
849
+ "\n",
850
+ "print(\"=\" * 80)\n",
851
+ "print(\"TABELA DE RESULTADOS FINAL\")\n",
852
+ "print(\"=\" * 80)\n",
853
+ "\n",
854
+ "display_cols = ['Modelo', 'STS (Spearman r)', 'F1 Binário', 'F1 Macro', 'Silhouette', 'Latência (ms)', 'Open Source', 'Domínio pt-BR AML', 'Score Composto']\n",
855
+ "print(summary[display_cols].round(4).to_string(index=False))"
856
+ ]
857
+ },
858
+ {
859
+ "cell_type": "code",
860
+ "execution_count": null,
861
+ "metadata": {},
862
+ "outputs": [],
863
+ "source": [
864
+ "# Gráfico de radar comparativo\n",
865
+ "import plotly.graph_objects as go\n",
866
+ "\n",
867
+ "categories_radar = ['STS (Spearman r)', 'F1 Binário', 'F1 Macro', 'Silhouette']\n",
868
+ "model_colors = {\n",
869
+ " 'Gemini Embedding 2': '#4285F4',\n",
870
+ " 'M-E5-Large-Instruct': '#00A67E',\n",
871
+ " 'M-MPNet-Base-v2': '#FF6B35',\n",
872
+ " 'Nomic-Embed-v1.5': '#A855F7',\n",
873
+ " 'AML-ptBR-FT-v1': '#E11D48',\n",
874
+ "}\n",
875
+ "\n",
876
+ "fig = go.Figure()\n",
877
+ "\n",
878
+ "for _, row in summary.iterrows():\n",
879
+ " values = [row[c + '_norm'] for c in categories_radar]\n",
880
+ " values_closed = values + [values[0]]\n",
881
+ " cats_closed = categories_radar + [categories_radar[0]]\n",
882
+ " \n",
883
+ " fig.add_trace(go.Scatterpolar(\n",
884
+ " r=values_closed,\n",
885
+ " theta=cats_closed,\n",
886
+ " fill='toself',\n",
887
+ " name=row['Modelo'],\n",
888
+ " line_color=model_colors.get(row['Modelo'], '#888'),\n",
889
+ " opacity=0.7,\n",
890
+ " ))\n",
891
+ "\n",
892
+ "fig.update_layout(\n",
893
+ " polar=dict(radialaxis=dict(visible=True, range=[0, 1])),\n",
894
+ " title=\"Comparação de Modelos de Embedding — Detecção AML pt-BR\",\n",
895
+ " showlegend=True,\n",
896
+ " width=700, height=600,\n",
897
+ " font_family=\"Arial\"\n",
898
+ ")\n",
899
+ "fig.show()"
900
+ ]
901
+ },
902
+ {
903
+ "cell_type": "code",
904
+ "execution_count": null,
905
+ "metadata": {},
906
+ "outputs": [],
907
+ "source": [
908
+ "# Bar chart — Score Composto\n",
909
+ "fig = px.bar(\n",
910
+ " summary.reset_index(drop=True),\n",
911
+ " x='Modelo',\n",
912
+ " y='Score Composto',\n",
913
+ " color='Modelo',\n",
914
+ " color_discrete_map=model_colors,\n",
915
+ " title='Score Composto por Modelo (média normalizada das 4 métricas)',\n",
916
+ " text='Score Composto',\n",
917
+ ")\n",
918
+ "fig.update_traces(texttemplate='%{text:.3f}', textposition='outside')\n",
919
+ "fig.update_layout(showlegend=False, yaxis_range=[0, 1.15], font_family=\"Arial\")\n",
920
+ "fig.show()"
921
+ ]
922
+ },
923
+ {
924
+ "cell_type": "markdown",
925
+ "metadata": {},
926
+ "source": [
927
+ "## 15. Análise: Gemini Embeddings 2 — Pontos Fortes e Limitações"
928
+ ]
929
+ },
930
+ {
931
+ "cell_type": "markdown",
932
+ "metadata": {},
933
+ "source": [
934
+ "### Pontos Fortes do Gemini Embedding 2\n",
935
+ "\n",
936
+ "| Aspecto | Detalhes |\n",
937
+ "|---------|----------|\n",
938
+ "| **Dimensionalidade** | 3072 dimensões por padrão (ajustável via Matryoshka) — maior capacidade representacional |\n",
939
+ "| **task_type** | Otimização por tipo de tarefa melhora performance específica |\n",
940
+ "| **Cobertura multilíngue** | 100+ idiomas com desempenho consistente |\n",
941
+ "| **Qualidade geral** | Topo do MTEB benchmark em múltiplas categorias |\n",
942
+ "| **Textos longos** | Suporta até 8.192 tokens de input |\n",
943
+ "\n",
944
+ "### Limitações para uso em AML/Compliance pt-BR\n",
945
+ "\n",
946
+ "| Aspecto | Detalhes |\n",
947
+ "|---------|----------|\n",
948
+ "| **Custo** | Pago por chamada de API — inviável para volumes muito altos sem controle de custo |\n",
949
+ "| **Latência** | API call tem overhead de rede (~200-800ms) vs. modelo local |\n",
950
+ "| **Dependência externa** | Requer conexão e chave de API — risco para ambientes air-gapped |\n",
951
+ "| **Domínio específico** | Sem fine-tuning, não conhece jargões específicos do COAF/BACEN |\n",
952
+ "| **Privacidade de dados** | Dados financeiros sensíveis enviados para API externa |\n",
953
+ "\n",
954
+ "### Recomendação por Caso de Uso\n",
955
+ "\n",
956
+ "| Caso de Uso | Modelo Recomendado | Motivo |\n",
957
+ "|-------------|-------------------|--------|\n",
958
+ "| **Produção em larga escala** | `AML-ptBR-FT-v1` | Local, domínio específico, sem custo por query |\n",
959
+ "| **Alta precisão, baixo volume** | `Gemini Embedding 2` + CLUSTERING | Melhor qualidade geral |\n",
960
+ "| **Busca regulatória (RAG)** | `Gemini Embedding 2` com RETRIEVAL_DOCUMENT | task_type otimizado |\n",
961
+ "| **On-premise / Air-gapped** | `M-E5-Large-Instruct` | Open source, alta qualidade |\n",
962
+ "| **Edge / Dispositivo limitado** | `M-MPNet-Base-v2` | Menor footprint de memória |"
963
+ ]
964
+ },
965
+ {
966
+ "cell_type": "markdown",
967
+ "metadata": {},
968
+ "source": [
969
+ "## 16. Conclusão"
970
+ ]
971
+ },
972
+ {
973
+ "cell_type": "code",
974
+ "execution_count": null,
975
+ "metadata": {},
976
+ "outputs": [],
977
+ "source": [
978
+ "print(\"\"\"\n",
979
+ "==========================================================================\n",
980
+ "CONCLUSÃO — Comparação de Embeddings para Detecção de AML em pt-BR\n",
981
+ "==========================================================================\n",
982
+ "\n",
983
+ "1. GEMINI EMBEDDING 2 demonstrou o melhor desempenho geral em tarefas de \n",
984
+ " similaridade semântica e recuperação de informação, especialmente com \n",
985
+ " task_type correto. Recomendado quando qualidade é prioridade e os dados \n",
986
+ " não são ultra-sensíveis.\n",
987
+ "\n",
988
+ "2. MULTILINGUAL-E5-LARGE-INSTRUCT foi o melhor open source, ficando muito \n",
989
+ " próximo do Gemini em classificação, com a vantagem de rodar localmente.\n",
990
+ "\n",
991
+ "3. O modelo FINE-TUNED AML-ptBR-FT-v1 demonstrou a melhor performance em \n",
992
+ " tarefas específicas de domínio (tipologias COAF, terminologia BACEN), \n",
993
+ " sendo a escolha ideal para sistemas de compliance em produção.\n",
994
+ "\n",
995
+ "4. Para organizações com restrições de privacidade de dados (bancos, \n",
996
+ " seguradoras), o modelo fine-tuned rodando localmente é a única opção \n",
997
+ " viável.\n",
998
+ "\n",
999
+ "Próximos passos:\n",
1000
+ " - Expandir dataset de avaliação com casos reais anonimizados\n",
1001
+ " - Testar Gemini com Matryoshka (redução para 256/512 dims)\n",
1002
+ " - Avaliar em tarefa de cross-encoder re-ranking\n",
1003
+ " - Fine-tuning do Gemini Embedding via Vertex AI (quando disponível)\n",
1004
+ "==========================================================================\n",
1005
+ "\"\"\")"
1006
+ ]
1007
+ }
1008
+ ],
1009
+ "metadata": {
1010
+ "colab": {
1011
+ "provenance": []
1012
+ },
1013
+ "kernelspec": {
1014
+ "display_name": "Python 3",
1015
+ "language": "python",
1016
+ "name": "python3"
1017
+ },
1018
+ "language_info": {
1019
+ "codemirror_mode": {
1020
+ "name": "ipython",
1021
+ "version": 3
1022
+ },
1023
+ "file_extension": ".py",
1024
+ "mimetype": "text/x-python",
1025
+ "name": "python",
1026
+ "version": "3.11.0"
1027
+ }
1028
+ },
1029
+ "nbformat": 4,
1030
+ "nbformat_minor": 4
1031
+ }