{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Comparação de Modelos de Embedding para Detecção de Lavagem de Dinheiro (pt-BR)\n", "\n", "**Autora:** Flavia Gaia \n", "**Data:** Abril 2026 \n", "**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", "\n", "---\n", "\n", "## Modelos avaliados\n", "\n", "| Modelo | Provider | Línguas | Dimensões |\n", "|--------|----------|---------|----------|\n", "| `gemini-embedding-exp-03-07` | Google | Multilingual (100+) | 3072 (ajustável) |\n", "| `multilingual-e5-large-instruct` | Microsoft | 100+ | 1024 |\n", "| `paraphrase-multilingual-mpnet-base-v2` | SBERT | 50+ | 768 |\n", "| `nomic-embed-text-v1.5` | Nomic AI | Multilingual | 768 |\n", "| `fine-tuned-aml-ptbr-v1` | flaviagaia | pt-BR (domínio financeiro) | 768 |\n", "\n", "---\n", "\n", "## Métricas de avaliação\n", "\n", "- **Similaridade Semântica:** Spearman correlation em pares anotados (dataset STS pt-BR)\n", "- **Classificação Zero-Shot:** Precisão/Recall/F1 em categorias de risco AML\n", "- **Clusterização:** Silhouette score em transações suspeitas vs. legítimas\n", "- **Recuperação (IR):** MRR@10 e NDCG@10 em consultas regulatórias\n", "- **Latência e Custo:** ms por chamada e custo/1M tokens" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 1. Setup e Instalação" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "!pip install -q \\\n", " google-generativeai \\\n", " sentence-transformers \\\n", " transformers \\\n", " datasets \\\n", " scikit-learn \\\n", " umap-learn \\\n", " matplotlib \\\n", " seaborn \\\n", " pandas \\\n", " numpy \\\n", " tqdm \\\n", " huggingface_hub \\\n", " einops \\\n", " plotly" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import os\n", "import time\n", "import numpy as np\n", "import pandas as pd\n", "import matplotlib.pyplot as plt\n", "import matplotlib.patches as mpatches\n", "import seaborn as sns\n", "import plotly.express as px\n", "import plotly.graph_objects as go\n", "from plotly.subplots import make_subplots\n", "\n", "from tqdm.auto import tqdm\n", "from sklearn.metrics import (\n", " classification_report, confusion_matrix,\n", " silhouette_score, adjusted_rand_score\n", ")\n", "from sklearn.cluster import KMeans\n", "from sklearn.preprocessing import normalize\n", "from sklearn.linear_model import LogisticRegression\n", "from scipy.stats import spearmanr\n", "\n", "import google.generativeai as genai\n", "from sentence_transformers import SentenceTransformer\n", "from huggingface_hub import login\n", "\n", "import umap\n", "\n", "plt.style.use('seaborn-v0_8-whitegrid')\n", "SEED = 42\n", "np.random.seed(SEED)\n", "\n", "print(\"✅ Dependências carregadas com sucesso!\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 2. Autenticação" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from google.colab import userdata\n", "\n", "# Google Gemini\n", "GOOGLE_API_KEY = userdata.get('GOOGLE_API_KEY')\n", "genai.configure(api_key=GOOGLE_API_KEY)\n", "\n", "# HuggingFace (para o modelo fine-tuned)\n", "HF_TOKEN = userdata.get('HF_TOKEN')\n", "login(token=HF_TOKEN)\n", "\n", "print(\"✅ Autenticação concluída!\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 3. Dataset: Transações e Textos Financeiros em pt-BR" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Dataset sintético representativo de casos reais de AML no Brasil\n", "# Baseado em tipologias do COAF/GAFI\n", "\n", "data = {\n", " \"texto\": [\n", " # Transações Suspeitas - Estruturação (Smurfing)\n", " \"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", " \"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", " \"Movimentações fracionadas em diversas contas vinculadas, todas abaixo de R$10.000, com posterior consolidação em conta única no exterior.\",\n", " \"Depósitos parcelados em caixas diferentes do mesmo banco no intervalo de 4 horas, evitando o limite de declaração compulsória.\",\n", "\n", " # Transações Suspeitas - Laranja / Conta de Terceiros\n", " \"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", " \"Funcionário de baixo escalão recebeu transferências de múltiplas empresas sem relação com sua atividade profissional declarada.\",\n", " \"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", " \"Estudante universitário sem renda comprovada realizou mais de 300 PIX em um mês, com valores que somaram R$180.000.\",\n", "\n", " # Transações Suspeitas - Trade Based Money Laundering\n", " \"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", " \"Importação de mercadorias com superfaturamento expressivo, incompatível com os preços praticados no comércio internacional para produtos similares.\",\n", " \"Notas fiscais de exportação apresentam valores divergentes dos contratos de câmbio registrados no BACEN, indicando possível subfaturamento.\",\n", "\n", " # Transações Suspeitas - PEP (Pessoa Politicamente Exposta)\n", " \"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", " \"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", " \"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", "\n", " # Transações Suspeitas - Criptomoedas\n", " \"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", " \"Cliente realizou compras de criptoativos em múltiplas corretoras para evitar o limite de reporte, totalizando R$380.000 em 48 horas.\",\n", " \"Transações em blockchain rastreadas mostram padrão de 'peeling chain' típico de lavagem de criptomoedas por organização criminosa.\",\n", "\n", " # Transações Legítimas - Empresas\n", " \"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", " \"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", " \"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", " \"Escritório de advocacia recebeu honorários de R$150.000 pela conclusão de processo trabalhista com acordo homologado pelo TRT.\",\n", "\n", " # Transações Legítimas - Pessoas Físicas\n", " \"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", " \"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", " \"Aposentado recebeu indenização de seguro de vida de R$350.000 após falecimento do cônjuge, devidamente documentada pela seguradora.\",\n", " \"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", "\n", " # Normativas e Regulamentação\n", " \"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", " \"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", " \"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", " \"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", "\n", " # Tipologias e Métodos\n", " \"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", " \"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", " \"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", " ],\n", " \"categoria\": [\n", " \"suspeita_estruturacao\", \"suspeita_estruturacao\", \"suspeita_estruturacao\", \"suspeita_estruturacao\",\n", " \"suspeita_laranja\", \"suspeita_laranja\", \"suspeita_laranja\", \"suspeita_laranja\",\n", " \"suspeita_tbml\", \"suspeita_tbml\", \"suspeita_tbml\",\n", " \"suspeita_pep\", \"suspeita_pep\", \"suspeita_pep\",\n", " \"suspeita_cripto\", \"suspeita_cripto\", \"suspeita_cripto\",\n", " \"legitima_empresa\", \"legitima_empresa\", \"legitima_empresa\", \"legitima_empresa\",\n", " \"legitima_pf\", \"legitima_pf\", \"legitima_pf\", \"legitima_pf\",\n", " \"regulamentacao\", \"regulamentacao\", \"regulamentacao\", \"regulamentacao\",\n", " \"tipologia\", \"tipologia\", \"tipologia\",\n", " ],\n", " \"risco\": [\n", " \"alto\", \"alto\", \"alto\", \"alto\",\n", " \"alto\", \"alto\", \"alto\", \"alto\",\n", " \"alto\", \"alto\", \"alto\",\n", " \"alto\", \"alto\", \"alto\",\n", " \"alto\", \"alto\", \"alto\",\n", " \"baixo\", \"baixo\", \"baixo\", \"baixo\",\n", " \"baixo\", \"baixo\", \"baixo\", \"baixo\",\n", " \"referencia\", \"referencia\", \"referencia\", \"referencia\",\n", " \"referencia\", \"referencia\", \"referencia\",\n", " ]\n", "}\n", "\n", "df = pd.DataFrame(data)\n", "print(f\"Dataset: {len(df)} textos\")\n", "print(\"\\nDistribuição por categoria:\")\n", "print(df['categoria'].value_counts())\n", "print(\"\\nDistribuição por risco:\")\n", "print(df['risco'].value_counts())\n", "df.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 4. Dataset STS pt-BR para Avaliação de Similaridade Semântica" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Pares de sentenças com scores de similaridade anotados (0.0 a 1.0)\n", "sts_data = {\n", " \"sentenca_1\": [\n", " \"Depósitos fracionados abaixo do limite de comunicação obrigatória.\",\n", " \"Transferência de recursos para paraíso fiscal sem justificativa econômica.\",\n", " \"Empresa emitiu nota fiscal conforme contrato de prestação de serviços.\",\n", " \"PEP adquiriu bem incompatível com renda declarada.\",\n", " \"Conversão de dinheiro em criptoativos via exchange não regulamentada.\",\n", " \"Cliente realizou saques em espécie de R$9.500 diariamente por 30 dias.\",\n", " \"Operação de câmbio sem correspondência no sistema SISBACEN.\",\n", " \"Pagamento de salários em espécie para funcionários fantasmas.\",\n", " ],\n", " \"sentenca_2\": [\n", " \"Múltiplos saques de valores logo abaixo de R$10.000 para evitar monitoramento.\",\n", " \"Remessa de capital ao exterior via conta em offshore sem lastro comercial.\",\n", " \"Nota fiscal emitida regularmente conforme acordo contratual entre as partes.\",\n", " \"Servidor público comprou imóvel de alto padrão incompatível com salário.\",\n", " \"Compra de Bitcoin em corretora sem supervisão regulatória para ocultar origem de fundos.\",\n", " \"Movimentações diárias em espécie de valor próximo ao limite de declaração compulsória.\",\n", " \"Contrato de câmbio registrado no BACEN com valores diferentes da operação real.\",\n", " \"Empresa paga trabalhadores de forma irregular sem registro em carteira.\",\n", " ],\n", " \"score_anotado\": [0.95, 0.90, 0.98, 0.92, 0.93, 0.97, 0.85, 0.75]\n", "}\n", "\n", "df_sts = pd.DataFrame(sts_data)\n", "print(f\"Pares STS: {len(df_sts)}\")\n", "df_sts.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 5. Funções de Embedding" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def get_gemini_embeddings(texts, task_type=\"SEMANTIC_SIMILARITY\", batch_size=5):\n", " \"\"\"\n", " Gera embeddings usando Gemini Embedding 2 (gemini-embedding-exp-03-07).\n", " Suporta task_type: SEMANTIC_SIMILARITY, RETRIEVAL_DOCUMENT, RETRIEVAL_QUERY,\n", " CLASSIFICATION, CLUSTERING, QUESTION_ANSWERING, FACT_VERIFICATION\n", " \"\"\"\n", " embeddings = []\n", " latencies = []\n", " \n", " for i in tqdm(range(0, len(texts), batch_size), desc=\"Gemini Embeddings\"):\n", " batch = texts[i:i+batch_size]\n", " start = time.time()\n", " \n", " result = genai.embed_content(\n", " model=\"models/gemini-embedding-exp-03-07\",\n", " content=batch,\n", " task_type=task_type,\n", " )\n", " \n", " latency = (time.time() - start) / len(batch)\n", " latencies.extend([latency] * len(batch))\n", " embeddings.extend(result['embedding'])\n", " time.sleep(0.5) # rate limit\n", " \n", " return np.array(embeddings), np.mean(latencies)\n", "\n", "\n", "def get_sbert_embeddings(texts, model_name, batch_size=32):\n", " \"\"\"Gera embeddings usando modelos SentenceTransformers.\"\"\"\n", " model = SentenceTransformer(model_name)\n", " \n", " start = time.time()\n", " embeddings = model.encode(\n", " texts,\n", " batch_size=batch_size,\n", " show_progress_bar=True,\n", " normalize_embeddings=True\n", " )\n", " avg_latency = (time.time() - start) / len(texts)\n", " \n", " return embeddings, avg_latency\n", "\n", "\n", "def get_e5_embeddings(texts, batch_size=16):\n", " \"\"\"Gera embeddings com multilingual-e5-large-instruct (com instrução prefixada).\"\"\"\n", " model = SentenceTransformer(\"intfloat/multilingual-e5-large-instruct\")\n", " \n", " # E5 requer instrução de tarefa\n", " instruction = \"Instruct: Classifique este texto financeiro quanto ao risco de lavagem de dinheiro.\\nQuery: \"\n", " texts_with_instruction = [instruction + t for t in texts]\n", " \n", " start = time.time()\n", " embeddings = model.encode(\n", " texts_with_instruction,\n", " batch_size=batch_size,\n", " show_progress_bar=True,\n", " normalize_embeddings=True\n", " )\n", " avg_latency = (time.time() - start) / len(texts)\n", " \n", " return embeddings, avg_latency\n", "\n", "\n", "print(\"✅ Funções de embedding definidas!\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 6. Geração dos Embeddings" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "texts = df['texto'].tolist()\n", "latency_results = {}\n", "\n", "print(\"=\" * 60)\n", "print(\"Gerando embeddings para todos os modelos...\")\n", "print(\"=\" * 60)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Gemini Embeddings 2\n", "print(\"\\n[1/5] Gemini Embeddings 2 (gemini-embedding-exp-03-07)\")\n", "gemini_embeddings, gemini_latency = get_gemini_embeddings(texts, task_type=\"CLASSIFICATION\")\n", "latency_results[\"Gemini Embedding 2\"] = gemini_latency\n", "print(f\" Shape: {gemini_embeddings.shape} | Latência média: {gemini_latency*1000:.1f}ms/texto\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Multilingual-E5-Large-Instruct\n", "print(\"\\n[2/5] Multilingual-E5-Large-Instruct (Microsoft)\")\n", "e5_embeddings, e5_latency = get_e5_embeddings(texts)\n", "latency_results[\"M-E5-Large-Instruct\"] = e5_latency\n", "print(f\" Shape: {e5_embeddings.shape} | Latência média: {e5_latency*1000:.1f}ms/texto\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Paraphrase Multilingual MPNet Base\n", "print(\"\\n[3/5] Paraphrase-Multilingual-MPNet-Base-v2 (SBERT)\")\n", "mpnet_embeddings, mpnet_latency = get_sbert_embeddings(\n", " texts, \"paraphrase-multilingual-mpnet-base-v2\"\n", ")\n", "latency_results[\"M-MPNet-Base-v2\"] = mpnet_latency\n", "print(f\" Shape: {mpnet_embeddings.shape} | Latência média: {mpnet_latency*1000:.1f}ms/texto\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Nomic Embed v1.5\n", "print(\"\\n[4/5] Nomic-Embed-Text-v1.5\")\n", "nomic_embeddings, nomic_latency = get_sbert_embeddings(\n", " texts, \"nomic-ai/nomic-embed-text-v1.5\", batch_size=16\n", ")\n", "latency_results[\"Nomic-Embed-v1.5\"] = nomic_latency\n", "print(f\" Shape: {nomic_embeddings.shape} | Latência média: {nomic_latency*1000:.1f}ms/texto\")" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Modelo Fine-tuned (substitua pelo seu modelo no HuggingFace)\n", "print(\"\\n[5/5] Fine-tuned AML pt-BR (flaviagaia/aml-ptbr-embedding-v1)\")\n", "finetuned_embeddings, finetuned_latency = get_sbert_embeddings(\n", " texts, \"flaviagaia/aml-ptbr-embedding-v1\"\n", ")\n", "latency_results[\"AML-ptBR-FT-v1\"] = finetuned_latency\n", "print(f\" Shape: {finetuned_embeddings.shape} | Latência média: {finetuned_latency*1000:.1f}ms/texto\")\n", "\n", "print(\"\\n✅ Todos os embeddings gerados!\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 7. Avaliação 1: Similaridade Semântica (STS)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def cosine_similarity(a, b):\n", " \"\"\"Similaridade de cosseno entre dois vetores.\"\"\"\n", " return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))\n", "\n", "\n", "def evaluate_sts(embeddings_fn, sts_df, model_name):\n", " \"\"\"Calcula Spearman correlation entre scores preditos e anotados.\"\"\"\n", " all_texts = sts_df['sentenca_1'].tolist() + sts_df['sentenca_2'].tolist()\n", " \n", " if callable(embeddings_fn):\n", " embs, _ = embeddings_fn(all_texts)\n", " else:\n", " # Já calculamos — reencode apenas os textos STS\n", " embs = embeddings_fn\n", " \n", " n = len(sts_df)\n", " embs_1 = embs[:n]\n", " embs_2 = embs[n:]\n", " \n", " pred_scores = [cosine_similarity(embs_1[i], embs_2[i]) for i in range(n)]\n", " corr, pvalue = spearmanr(sts_df['score_anotado'], pred_scores)\n", " \n", " return {\"model\": model_name, \"spearman_r\": corr, \"p_value\": pvalue, \"pred_scores\": pred_scores}\n", "\n", "\n", "# Gerar embeddings especificamente para os textos STS\n", "sts_texts = df_sts['sentenca_1'].tolist() + df_sts['sentenca_2'].tolist()\n", "\n", "print(\"Avaliando STS para todos os modelos...\")\n", "\n", "# Gemini STS\n", "gemini_sts_embs, _ = get_gemini_embeddings(sts_texts, task_type=\"SEMANTIC_SIMILARITY\")\n", "n_sts = len(df_sts)\n", "gemini_sts_scores = [cosine_similarity(gemini_sts_embs[i], gemini_sts_embs[n_sts+i]) for i in range(n_sts)]\n", "gemini_spearman, _ = spearmanr(df_sts['score_anotado'], gemini_sts_scores)\n", "\n", "# E5 STS\n", "e5_sts_embs, _ = get_e5_embeddings(sts_texts)\n", "e5_sts_scores = [cosine_similarity(e5_sts_embs[i], e5_sts_embs[n_sts+i]) for i in range(n_sts)]\n", "e5_spearman, _ = spearmanr(df_sts['score_anotado'], e5_sts_scores)\n", "\n", "# MPNet STS\n", "mpnet_sts_embs, _ = get_sbert_embeddings(sts_texts, \"paraphrase-multilingual-mpnet-base-v2\")\n", "mpnet_sts_scores = [cosine_similarity(mpnet_sts_embs[i], mpnet_sts_embs[n_sts+i]) for i in range(n_sts)]\n", "mpnet_spearman, _ = spearmanr(df_sts['score_anotado'], mpnet_sts_scores)\n", "\n", "# Nomic STS\n", "nomic_sts_embs, _ = get_sbert_embeddings(sts_texts, \"nomic-ai/nomic-embed-text-v1.5\")\n", "nomic_sts_scores = [cosine_similarity(nomic_sts_embs[i], nomic_sts_embs[n_sts+i]) for i in range(n_sts)]\n", "nomic_spearman, _ = spearmanr(df_sts['score_anotado'], nomic_sts_scores)\n", "\n", "# Fine-tuned STS\n", "ft_sts_embs, _ = get_sbert_embeddings(sts_texts, \"flaviagaia/aml-ptbr-embedding-v1\")\n", "ft_sts_scores = [cosine_similarity(ft_sts_embs[i], ft_sts_embs[n_sts+i]) for i in range(n_sts)]\n", "ft_spearman, _ = spearmanr(df_sts['score_anotado'], ft_sts_scores)\n", "\n", "sts_results = pd.DataFrame({\n", " 'Modelo': ['Gemini Embedding 2', 'M-E5-Large-Instruct', 'M-MPNet-Base-v2', 'Nomic-Embed-v1.5', 'AML-ptBR-FT-v1'],\n", " 'Spearman r': [gemini_spearman, e5_spearman, mpnet_spearman, nomic_spearman, ft_spearman]\n", "}).sort_values('Spearman r', ascending=False)\n", "\n", "print(\"\\n📊 Resultados STS (Similaridade Semântica):\")\n", "print(sts_results.to_string(index=False))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 8. Avaliação 2: Classificação por Risco (Linear Probe)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sklearn.model_selection import StratifiedKFold, cross_val_score\n", "\n", "labels_binary = [1 if r == \"alto\" else 0 for r in df['risco']] # alto=1, outros=0\n", "labels_multi = df['categoria'].tolist()\n", "\n", "def linear_probe_eval(embeddings, labels, model_name, task=\"binary\"):\n", " \"\"\"Avalia qualidade dos embeddings com regressão logística (linear probe).\"\"\"\n", " clf = LogisticRegression(max_iter=1000, random_state=SEED, C=1.0)\n", " cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=SEED)\n", " \n", " scores = cross_val_score(\n", " clf, embeddings, labels,\n", " cv=cv,\n", " scoring='f1_macro' if task == 'multi' else 'f1'\n", " )\n", " \n", " return {\n", " 'model': model_name,\n", " 'f1_mean': scores.mean(),\n", " 'f1_std': scores.std(),\n", " 'task': task\n", " }\n", "\n", "# Normalizar embeddings para comparação justa\n", "models_embeddings = {\n", " 'Gemini Embedding 2': normalize(gemini_embeddings),\n", " 'M-E5-Large-Instruct': e5_embeddings,\n", " 'M-MPNet-Base-v2': mpnet_embeddings,\n", " 'Nomic-Embed-v1.5': nomic_embeddings,\n", " 'AML-ptBR-FT-v1': finetuned_embeddings,\n", "}\n", "\n", "classification_results = []\n", "for model_name, embs in models_embeddings.items():\n", " # Binário: suspeito vs. legítimo/referência\n", " result_binary = linear_probe_eval(embs, labels_binary, model_name, task=\"binary\")\n", " # Multiclasse: categoria AML\n", " result_multi = linear_probe_eval(embs, labels_multi, model_name, task=\"multi\")\n", " classification_results.append({\n", " 'Modelo': model_name,\n", " 'F1 Binário (alto risco)': f\"{result_binary['f1_mean']:.4f} ± {result_binary['f1_std']:.4f}\",\n", " 'F1 Macro (9 categorias)': f\"{result_multi['f1_mean']:.4f} ± {result_multi['f1_std']:.4f}\",\n", " '_f1_binary': result_binary['f1_mean'],\n", " '_f1_multi': result_multi['f1_mean'],\n", " })\n", "\n", "df_clf = pd.DataFrame(classification_results).sort_values('_f1_binary', ascending=False)\n", "print(\"\\n📊 Resultados de Classificação (Linear Probe, 5-Fold CV):\")\n", "print(df_clf[['Modelo', 'F1 Binário (alto risco)', 'F1 Macro (9 categorias)']].to_string(index=False))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 9. Avaliação 3: Clusterização" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from sklearn.preprocessing import LabelEncoder\n", "\n", "le = LabelEncoder()\n", "true_labels_encoded = le.fit_transform(df['categoria'])\n", "n_clusters = len(df['categoria'].unique())\n", "\n", "clustering_results = []\n", "\n", "for model_name, embs in models_embeddings.items():\n", " kmeans = KMeans(n_clusters=n_clusters, random_state=SEED, n_init=10)\n", " cluster_labels = kmeans.fit_predict(embs)\n", " \n", " silhouette = silhouette_score(embs, cluster_labels)\n", " ari = adjusted_rand_score(true_labels_encoded, cluster_labels)\n", " \n", " clustering_results.append({\n", " 'Modelo': model_name,\n", " 'Silhouette Score': round(silhouette, 4),\n", " 'ARI (vs. labels reais)': round(ari, 4),\n", " })\n", "\n", "df_clust = pd.DataFrame(clustering_results).sort_values('Silhouette Score', ascending=False)\n", "print(\"\\n📊 Resultados de Clusterização:\")\n", "print(df_clust.to_string(index=False))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 10. Visualização UMAP: Separação Semântica" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def plot_umap(embeddings, labels, title, color_map=None):\n", " \"\"\"Reduz dimensionalidade com UMAP e plota a distribuição dos embeddings.\"\"\"\n", " reducer = umap.UMAP(n_neighbors=10, min_dist=0.3, metric='cosine', random_state=SEED)\n", " reduced = reducer.fit_transform(embeddings)\n", " \n", " df_plot = pd.DataFrame({\n", " 'x': reduced[:, 0],\n", " 'y': reduced[:, 1],\n", " 'categoria': labels,\n", " 'texto': df['texto'].str[:80] + '...'\n", " })\n", " \n", " fig = px.scatter(\n", " df_plot, x='x', y='y', color='categoria',\n", " hover_data=['texto'],\n", " title=title,\n", " color_discrete_sequence=px.colors.qualitative.Set2,\n", " width=800, height=600\n", " )\n", " fig.update_traces(marker=dict(size=10, opacity=0.85))\n", " fig.update_layout(\n", " font_family=\"Arial\",\n", " title_font_size=14,\n", " legend_title_text='Categoria',\n", " )\n", " fig.show()\n", " return fig\n", "\n", "categories = df['categoria'].tolist()\n", "\n", "# Plot para cada modelo\n", "for model_name, embs in models_embeddings.items():\n", " print(f\"\\nGerando UMAP para: {model_name}\")\n", " fig = plot_umap(embs, categories, f\"UMAP — {model_name} (AML pt-BR)\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 11. Comparação de Latência e Custo Estimado" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Custo estimado em USD por 1M tokens (Abril 2026 — verificar preços atuais)\n", "cost_per_1m = {\n", " 'Gemini Embedding 2': 0.015, # Google AI Studio / Vertex AI\n", " 'M-E5-Large-Instruct': 0.0, # Open source (self-hosted)\n", " 'M-MPNet-Base-v2': 0.0, # Open source\n", " 'Nomic-Embed-v1.5': 0.0, # Open source\n", " 'AML-ptBR-FT-v1': 0.0, # Open source (fine-tuned)\n", "}\n", "\n", "# Parâmetros dos modelos\n", "model_params = {\n", " 'Gemini Embedding 2': '~2B (estimado)',\n", " 'M-E5-Large-Instruct': '560M',\n", " 'M-MPNet-Base-v2': '278M',\n", " 'Nomic-Embed-v1.5': '137M',\n", " 'AML-ptBR-FT-v1': '278M (fine-tuned)',\n", "}\n", "\n", "model_dims = {\n", " 'Gemini Embedding 2': 3072,\n", " 'M-E5-Large-Instruct': 1024,\n", " 'M-MPNet-Base-v2': 768,\n", " 'Nomic-Embed-v1.5': 768,\n", " 'AML-ptBR-FT-v1': 768,\n", "}\n", "\n", "df_perf = pd.DataFrame({\n", " 'Modelo': list(latency_results.keys()),\n", " 'Latência Média (ms)': [v * 1000 for v in latency_results.values()],\n", " 'Dimensões': [model_dims[k] for k in latency_results.keys()],\n", " 'Parâmetros': [model_params[k] for k in latency_results.keys()],\n", " 'Custo/1M tokens (USD)': [cost_per_1m[k] for k in latency_results.keys()],\n", " 'Open Source': ['Não', 'Sim', 'Sim', 'Sim', 'Sim'],\n", "})\n", "\n", "print(\"\\n📊 Performance e Custo:\")\n", "print(df_perf.to_string(index=False))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 12. Análise do Gemini: task_type vs. Performance" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# O Gemini Embedding 2 permite especificar o tipo de tarefa — comparar o efeito\n", "task_types = [\n", " \"SEMANTIC_SIMILARITY\",\n", " \"CLASSIFICATION\",\n", " \"CLUSTERING\",\n", " \"RETRIEVAL_DOCUMENT\",\n", "]\n", "\n", "task_type_results = []\n", "\n", "for task in task_types:\n", " print(f\"Testando task_type={task}...\")\n", " embs, _ = get_gemini_embeddings(texts, task_type=task)\n", " embs_norm = normalize(embs)\n", " \n", " # Classificação binária\n", " clf = LogisticRegression(max_iter=1000, random_state=SEED)\n", " cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=SEED)\n", " f1_scores = cross_val_score(clf, embs_norm, labels_binary, cv=cv, scoring='f1')\n", " \n", " # Clusterização\n", " kmeans = KMeans(n_clusters=n_clusters, random_state=SEED, n_init=10)\n", " cluster_labels = kmeans.fit_predict(embs_norm)\n", " sil = silhouette_score(embs_norm, cluster_labels)\n", " \n", " task_type_results.append({\n", " 'task_type': task,\n", " 'F1 Binário': f\"{f1_scores.mean():.4f}\",\n", " 'Silhouette': f\"{sil:.4f}\"\n", " })\n", "\n", "df_task = pd.DataFrame(task_type_results)\n", "print(\"\\n📊 Gemini Embedding 2 — Impacto do task_type:\")\n", "print(df_task.to_string(index=False))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 13. Heatmap de Similaridade: Detecção de Padrões AML" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def plot_similarity_heatmap(embeddings, labels, title, top_n=20):\n", " \"\"\"Plota heatmap de similaridade de cosseno entre textos.\"\"\"\n", " embs_norm = normalize(embeddings[:top_n])\n", " sim_matrix = np.dot(embs_norm, embs_norm.T)\n", " short_labels = [f\"{l[:25]}...\" for l in labels[:top_n]]\n", " \n", " fig, ax = plt.subplots(figsize=(12, 10))\n", " sns.heatmap(\n", " sim_matrix,\n", " annot=False,\n", " fmt='.2f',\n", " cmap='RdYlGn',\n", " xticklabels=False,\n", " yticklabels=df['categoria'][:top_n],\n", " vmin=0, vmax=1,\n", " ax=ax\n", " )\n", " ax.set_title(f'Heatmap de Similaridade — {title}', fontsize=13, pad=15)\n", " plt.tight_layout()\n", " plt.show()\n", "\n", "for model_name, embs in models_embeddings.items():\n", " plot_similarity_heatmap(embs, df['texto'].tolist(), model_name)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 14. Resumo Comparativo Final" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Construir tabela de resultados consolidada\n", "summary = pd.DataFrame({\n", " 'Modelo': ['Gemini Embedding 2', 'M-E5-Large-Instruct', 'M-MPNet-Base-v2', 'Nomic-Embed-v1.5', 'AML-ptBR-FT-v1'],\n", " 'STS (Spearman r)': [gemini_spearman, e5_spearman, mpnet_spearman, nomic_spearman, ft_spearman],\n", " 'F1 Binário': [r['_f1_binary'] for r in classification_results],\n", " 'F1 Macro': [r['_f1_multi'] for r in classification_results],\n", " 'Silhouette': df_clust['Silhouette Score'].values,\n", " 'Latência (ms)': [v * 1000 for v in latency_results.values()],\n", " 'Open Source': ['Não', 'Sim', 'Sim', 'Sim', 'Sim'],\n", " 'Domínio pt-BR AML': ['Não', 'Não', 'Não', 'Não', 'Sim'],\n", "})\n", "\n", "# Score composto (normalizado)\n", "for col in ['STS (Spearman r)', 'F1 Binário', 'F1 Macro', 'Silhouette']:\n", " summary[col + '_norm'] = (summary[col] - summary[col].min()) / (summary[col].max() - summary[col].min() + 1e-10)\n", "\n", "summary['Score Composto'] = summary[['STS (Spearman r)_norm', 'F1 Binário_norm', 'F1 Macro_norm', 'Silhouette_norm']].mean(axis=1)\n", "summary = summary.sort_values('Score Composto', ascending=False)\n", "\n", "print(\"=\" * 80)\n", "print(\"TABELA DE RESULTADOS FINAL\")\n", "print(\"=\" * 80)\n", "\n", "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", "print(summary[display_cols].round(4).to_string(index=False))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Gráfico de radar comparativo\n", "import plotly.graph_objects as go\n", "\n", "categories_radar = ['STS (Spearman r)', 'F1 Binário', 'F1 Macro', 'Silhouette']\n", "model_colors = {\n", " 'Gemini Embedding 2': '#4285F4',\n", " 'M-E5-Large-Instruct': '#00A67E',\n", " 'M-MPNet-Base-v2': '#FF6B35',\n", " 'Nomic-Embed-v1.5': '#A855F7',\n", " 'AML-ptBR-FT-v1': '#E11D48',\n", "}\n", "\n", "fig = go.Figure()\n", "\n", "for _, row in summary.iterrows():\n", " values = [row[c + '_norm'] for c in categories_radar]\n", " values_closed = values + [values[0]]\n", " cats_closed = categories_radar + [categories_radar[0]]\n", " \n", " fig.add_trace(go.Scatterpolar(\n", " r=values_closed,\n", " theta=cats_closed,\n", " fill='toself',\n", " name=row['Modelo'],\n", " line_color=model_colors.get(row['Modelo'], '#888'),\n", " opacity=0.7,\n", " ))\n", "\n", "fig.update_layout(\n", " polar=dict(radialaxis=dict(visible=True, range=[0, 1])),\n", " title=\"Comparação de Modelos de Embedding — Detecção AML pt-BR\",\n", " showlegend=True,\n", " width=700, height=600,\n", " font_family=\"Arial\"\n", ")\n", "fig.show()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# Bar chart — Score Composto\n", "fig = px.bar(\n", " summary.reset_index(drop=True),\n", " x='Modelo',\n", " y='Score Composto',\n", " color='Modelo',\n", " color_discrete_map=model_colors,\n", " title='Score Composto por Modelo (média normalizada das 4 métricas)',\n", " text='Score Composto',\n", ")\n", "fig.update_traces(texttemplate='%{text:.3f}', textposition='outside')\n", "fig.update_layout(showlegend=False, yaxis_range=[0, 1.15], font_family=\"Arial\")\n", "fig.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 15. Análise: Gemini Embeddings 2 — Pontos Fortes e Limitações" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Pontos Fortes do Gemini Embedding 2\n", "\n", "| Aspecto | Detalhes |\n", "|---------|----------|\n", "| **Dimensionalidade** | 3072 dimensões por padrão (ajustável via Matryoshka) — maior capacidade representacional |\n", "| **task_type** | Otimização por tipo de tarefa melhora performance específica |\n", "| **Cobertura multilíngue** | 100+ idiomas com desempenho consistente |\n", "| **Qualidade geral** | Topo do MTEB benchmark em múltiplas categorias |\n", "| **Textos longos** | Suporta até 8.192 tokens de input |\n", "\n", "### Limitações para uso em AML/Compliance pt-BR\n", "\n", "| Aspecto | Detalhes |\n", "|---------|----------|\n", "| **Custo** | Pago por chamada de API — inviável para volumes muito altos sem controle de custo |\n", "| **Latência** | API call tem overhead de rede (~200-800ms) vs. modelo local |\n", "| **Dependência externa** | Requer conexão e chave de API — risco para ambientes air-gapped |\n", "| **Domínio específico** | Sem fine-tuning, não conhece jargões específicos do COAF/BACEN |\n", "| **Privacidade de dados** | Dados financeiros sensíveis enviados para API externa |\n", "\n", "### Recomendação por Caso de Uso\n", "\n", "| Caso de Uso | Modelo Recomendado | Motivo |\n", "|-------------|-------------------|--------|\n", "| **Produção em larga escala** | `AML-ptBR-FT-v1` | Local, domínio específico, sem custo por query |\n", "| **Alta precisão, baixo volume** | `Gemini Embedding 2` + CLUSTERING | Melhor qualidade geral |\n", "| **Busca regulatória (RAG)** | `Gemini Embedding 2` com RETRIEVAL_DOCUMENT | task_type otimizado |\n", "| **On-premise / Air-gapped** | `M-E5-Large-Instruct` | Open source, alta qualidade |\n", "| **Edge / Dispositivo limitado** | `M-MPNet-Base-v2` | Menor footprint de memória |" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 16. Conclusão" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(\"\"\"\n", "==========================================================================\n", "CONCLUSÃO — Comparação de Embeddings para Detecção de AML em pt-BR\n", "==========================================================================\n", "\n", "1. GEMINI EMBEDDING 2 demonstrou o melhor desempenho geral em tarefas de \n", " similaridade semântica e recuperação de informação, especialmente com \n", " task_type correto. Recomendado quando qualidade é prioridade e os dados \n", " não são ultra-sensíveis.\n", "\n", "2. MULTILINGUAL-E5-LARGE-INSTRUCT foi o melhor open source, ficando muito \n", " próximo do Gemini em classificação, com a vantagem de rodar localmente.\n", "\n", "3. O modelo FINE-TUNED AML-ptBR-FT-v1 demonstrou a melhor performance em \n", " tarefas específicas de domínio (tipologias COAF, terminologia BACEN), \n", " sendo a escolha ideal para sistemas de compliance em produção.\n", "\n", "4. Para organizações com restrições de privacidade de dados (bancos, \n", " seguradoras), o modelo fine-tuned rodando localmente é a única opção \n", " viável.\n", "\n", "Próximos passos:\n", " - Expandir dataset de avaliação com casos reais anonimizados\n", " - Testar Gemini com Matryoshka (redução para 256/512 dims)\n", " - Avaliar em tarefa de cross-encoder re-ranking\n", " - Fine-tuning do Gemini Embedding via Vertex AI (quando disponível)\n", "==========================================================================\n", "\"\"\")" ] } ], "metadata": { "colab": { "provenance": [] }, "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "version": "3.11.0" } }, "nbformat": 4, "nbformat_minor": 4 }