<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="/feed.xml" rel="self" type="application/atom+xml" /><link href="/" rel="alternate" type="text/html" /><updated>2026-06-01T18:19:09+00:00</updated><id>/feed.xml</id><title type="html">guelfoweb</title><subtitle>random notes</subtitle><author><name>guelfoweb</name></author><entry><title type="html">Intent Routing per agenti locali</title><link href="/ai/ita/2026/06/01/intent-routing-per-agenti-locali.html" rel="alternate" type="text/html" title="Intent Routing per agenti locali" /><published>2026-06-01T00:00:00+00:00</published><updated>2026-06-01T00:00:00+00:00</updated><id>/ai/ita/2026/06/01/intent-routing-per-agenti-locali</id><content type="html" xml:base="/ai/ita/2026/06/01/intent-routing-per-agenti-locali.html"><![CDATA[<p>Quando si progetta una CLI agentica per modelli piccoli si tende a pensare che più strumenti significhino più capacità. È un po’ come mettere una persona davanti a una console piena di pulsanti e aspettarsi che trovi immediatamente quello giusto. In teoria può farlo. In pratica aumenta solo il numero di scelte possibili e quindi la probabilità di errore.</p>

<p>Con le <code class="language-plaintext highlighter-rouge">tool-call</code> succede qualcosa di molto simile. La tentazione è esporre tutto al modello: filesystem, shell, web, editing, vision, audio.</p>

<p>In teoria il modello dovrebbe scegliere lo strumento corretto, ma nella pratica, soprattutto con modelli piccoli, succede spesso il contrario.</p>

<p>Un prompt come:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>show me how grep works
</code></pre></div></div>

<p>non dovrebbe eseguire <code class="language-plaintext highlighter-rouge">grep</code>: l’utente sta chiedendo una spiegazione. Ma un runtime troppo permissivo può interpretarlo come una richiesta operativa e aprire inutilmente l’accesso alla shell.</p>

<p>È un po’ come chiedere a un meccanico come funziona un motore e vederlo aprire il cofano per iniziare a smontare la tua auto. La domanda riguarda una spiegazione, non un intervento.</p>

<p>Allo stesso modo:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>tell me about file systems
</code></pre></div></div>

<p>non significa “<em>ispeziona la directory corrente</em>”. È una domanda concettuale.</p>

<p>Oppure:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>tell me about malware analysis, C2 and IoC
</code></pre></div></div>

<p>non significa “<em>analizza i file presenti nella workdir come malware sample</em>”.</p>

<p>In questi esempi il sistema non ha sbagliato strumento. Ha sbagliato intenzione.</p>

<p>All’inizio pensavo che il problema fosse solo il modello. In realtà, <strong>una parte importante del problema è il runtime</strong>.</p>

<h2 id="introdurre-un-livello-di-routing-prima-della-tool-call">Introdurre un livello di routing prima della tool-call</h2>

<p>La soluzione richiede un compromesso: introdurre una piccola componente deterministica prima di lasciare spazio al modello aggiungendo un livello di routing prima della tool-call.</p>

<p>Il flusso non è più:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>prompt -&gt; modello -&gt; tutti i tool disponibili
</code></pre></div></div>

<p>ma qualcosa di più controllato:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>prompt -&gt; intent routing -&gt; eventuale intent gate -&gt; subset minimo di tool -&gt; modello
</code></pre></div></div>

<p>Il primo passaggio classifica la richiesta. Il runtime prova a capire se l’utente sta facendo una domanda generale, oppure sta chiedendo accesso al filesystem, una ricerca web, una modifica file, un’analisi immagine/audio o un’operazione più rischiosa.</p>

<p>Se l’intento è chiaro, il sistema espone solo i tool necessari.</p>

<p>Per esempio:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>read README.md
</code></pre></div></div>

<p>porta solo agli strumenti <code class="language-plaintext highlighter-rouge">filesystem</code>.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>search online for information about Dante Alighieri
</code></pre></div></div>

<p>porta solo agli strumenti <code class="language-plaintext highlighter-rouge">web</code>.</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>compare image1.png and image2.jpg
</code></pre></div></div>

<p>porta solo al percorso <code class="language-plaintext highlighter-rouge">vision</code>.</p>

<h3 id="gestione-dei-casi-ambigui">Gestione dei casi ambigui</h3>

<p>Nei casi ambigui entra in gioco un secondo livello: l’<strong>intent gate</strong>.</p>

<p>È una piccola richiesta al modello, ma molto vincolata. Non gli viene chiesto di risolvere il task, viene chiesto solo se abbia senso procedere con tool locali oppure trattare il prompt come conversazione.</p>

<p>Esempio concettuale:</p>

<p>User prompt:</p>
<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>tell me about malware analysis
</code></pre></div></div>

<p>Question to model:</p>
<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Should local filesystem/shell tools be used to perform malware analysis for this user request?

Answer only YES or NO.
</code></pre></div></div>
<p>Se la risposta è <strong>NO</strong>, il runtime <strong>non espone i tool</strong> e risponde normalmente.</p>

<p>Se la risposta è <strong>YES</strong>, <strong>espone solo il subset</strong> coerente con quell’intento.</p>

<p>Questa distinzione risolve diversi problemi pratici:</p>

<ul>
  <li>Domande concettuali che prima attivavano shell o filesystem restano conversazione.</li>
  <li>Richieste operative reali continuano a usare i tool.</li>
  <li>I casi ambigui non vengono decisi da una regex rigida, ma da un controllo semantico molto stretto.</li>
  <li>Il modello vede meno strumenti, quindi ha meno possibilità di scegliere male.</li>
  <li>Si riducono loop inutili, consumo di token e comportamenti laterali.</li>
</ul>

<p>La parte importante è che il gate non rende l’agente deterministico nel senso classico. Non decide il contenuto della risposta, decide solo se sia appropriato aprire una certa classe di capacità operative. Per un piccolo modello locale questa differenza è enorme.</p>

<p>In pratica, il modello non viene lasciato davanti a una cassetta degli attrezzi completa a ogni turno, ma gli viene dato solo quello che serve per la richiesta corrente.</p>

<p>Se a un muratore viene chiesto di misurare un muro, per quel compito bastano un metro, una matita e un foglio di carta. Mettergli davanti anche trapano, betoniera, martello demolitore e sega circolare non lo aiuta a lavorare meglio. Gli offre semplicemente più modi per fare qualcosa che non gli è stato chiesto.</p>

<p>Meno strumenti significa meno ambiguità, dunque meno errori, meno loop e meno token spesi per decidere cosa fare.</p>

<p>Uno dei modi migliori per rendere un agente più affidabile non è aggiungere capacità ma togliere quelle che non servono in quel momento.</p>]]></content><author><name>guelfoweb</name></author><category term="ai" /><category term="ita" /><category term="tool-call" /><category term="intent" /><category term="cli" /><summary type="html"><![CDATA[Quando si progetta una CLI agentica per modelli piccoli si tende a pensare che più strumenti significhino più capacità. È un po’ come mettere una persona davanti a una console piena di pulsanti e aspettarsi che trovi immediatamente quello giusto. In teoria può farlo. In pratica aumenta solo il numero di scelte possibili e quindi la probabilità di errore.]]></summary></entry><entry><title type="html">AGENTS.md su progetti complessi</title><link href="/ai/agents/2026/03/25/agents-md-su-progetti-complessi.html" rel="alternate" type="text/html" title="AGENTS.md su progetti complessi" /><published>2026-03-25T00:00:00+00:00</published><updated>2026-03-25T00:00:00+00:00</updated><id>/ai/agents/2026/03/25/agents-md-su-progetti-complessi</id><content type="html" xml:base="/ai/agents/2026/03/25/agents-md-su-progetti-complessi.html"><![CDATA[<p>Questa non è una guida. Voglio solo condividere la mia esperienza e qualche osservazione personale, soprattutto su progetti complessi dove uso l’AI in modo continuo e dove iniziano a pesare gli aspetti progettuali e di sicurezza.</p>

<p>Non ha molto senso cercare di scrivere un <code class="language-plaintext highlighter-rouge">AGENTS.md</code> “perfetto” fin dall’inizio. Non funziona così. Più in generale, quando lavoro a qualcosa di strutturato lo affronto come in edilizia: si parte da un progetto di massima, poi si entra nei dettagli e inevitabilmente qualcosa cambia in corso d’opera. Non perché il piano iniziale fosse sbagliato, ma perché alcune cose diventano chiare solo mentre si costruisce.</p>

<p>Leggendo in giro trovo spesso riferimenti al file <code class="language-plaintext highlighter-rouge">AGENTS.md</code> presentato come soluzione per rendere gli agenti più affidabili. In parte è vero, ma il problema non è avere il file, è <strong>come lo si scrive</strong>.</p>

<h2 id="dove-ho-sbagliato-allinizio">Dove ho sbagliato all’inizio</h2>

<p>All’inizio ho fatto quello che fanno in molti: ho trattato <code class="language-plaintext highlighter-rouge">AGENTS.md</code> come documentazione. Ho scritto file lunghi, pieni di contesto, architettura, regole generiche, note varie.</p>

<p>Il risultato era poco utile. L’AI ignorava parti del file o si comportava comunque in modo incoerente. Più aggiungevo contenuto, meno sembrava avere effetto.</p>

<p>Col tempo ho iniziato a vederla in modo diverso. Non come un README per l’AI, che resta utile ma ha un altro scopo, ma come un insieme di vincoli operativi. Qualcosa che serve a limitare il comportamento del modello, non a spiegargli tutto.</p>

<h2 id="lunghezza-e-contesto">Lunghezza e contesto</h2>

<p>Qui secondo me c’è un punto spesso sottovalutato. La lunghezza non è solo una questione di leggibilità ma è un limite tecnico.</p>

<p>Tutto quello che mettiamo in <code class="language-plaintext highlighter-rouge">AGENTS.md</code> finisce nel contesto del modello, e il contesto è limitato.</p>

<p>Senza entrare nel merito delle preferenze o delle solite discussioni tra strumenti diversi, in generale i modelli più recenti lavorano con contesti molto ampi, anche nell’ordine delle centinaia di migliaia di token o più, ma comunque finiti e condivisi tra prompt, codice e output.</p>

<p>Ogni riga in più compete con codice e istruzioni, quindi a un certo punto perde peso o viene ignorata. Per questo non mi convince molto l’idea di file troppo completi.</p>

<h2 id="il-modo-in-cui-lo-sto-usando-adesso">Il modo in cui lo sto usando adesso</h2>

<p>Quello che faccio ora è più semplice, ma molto più iterativo. C’è sempre un confronto continuo con il modello prima di arrivare al file.</p>

<p>Uso l’AI per ragionare sul progetto e sugli aspetti di sicurezza, per chiarire i punti critici e mettere a fuoco le scelte. Solo dopo chiedo di estrarre regole operative, poche, brevi e verificabili.</p>

<p>Poi faccio una revisione manuale, tolgo duplicati e frasi vaghe, e spesso torno di nuovo sul modello per riformulare o migliorare alcune regole. Solo alla fine costruisco <code class="language-plaintext highlighter-rouge">AGENTS.md</code>.</p>

<p>Il risultato è più piccolo, ma mi sembra più efficace. In particolare, quello che sembra fare davvero la differenza è la specificità. Regole generiche non aiutano, mentre quelle concrete fanno la differenza.</p>

<h2 id="un-approccio-in-evoluzione">Un approccio in evoluzione</h2>

<p>Come detto all’inizio, <code class="language-plaintext highlighter-rouge">AGENTS.md</code> non è qualcosa che si scrive una volta e basta. Un progetto è un cantiere, cambia continuamente, e il file cambia insieme a lui.</p>

<p>Anche qui il confronto con il modello continua. Quando emergono nuovi problemi o voglio introdurre modifiche, torno a discuterne con l’AI, valuto alternative e poi aggiorno le regole. A volte si aggiungono vincoli, altre volte si semplifica o si rimuove ciò che non serve più.</p>

<p>Non credo esista ancora un metodo stabile o valido per tutti. Gli LLM non sono deterministici e anche il modo in cui interpretano queste regole può cambiare.</p>

<p>Per ora quello che mi sembra funzionare è abbastanza semplice: file corto, regole concrete, e niente delega completa al modello.</p>

<p>Il resto, almeno per me, è ancora sperimentazione.</p>]]></content><author><name>guelfoweb</name></author><category term="ai" /><category term="agents" /><category term="agents.md" /><category term="security" /><summary type="html"><![CDATA[Questa non è una guida. Voglio solo condividere la mia esperienza e qualche osservazione personale, soprattutto su progetti complessi dove uso l’AI in modo continuo e dove iniziano a pesare gli aspetti progettuali e di sicurezza.]]></summary></entry><entry><title type="html">Encrypting small pieces of text in Obsidian, my way</title><link href="/projects/2026/01/16/encrypting-small-text-in-obsidian.html" rel="alternate" type="text/html" title="Encrypting small pieces of text in Obsidian, my way" /><published>2026-01-16T00:00:00+00:00</published><updated>2026-01-16T00:00:00+00:00</updated><id>/projects/2026/01/16/encrypting-small-text-in-obsidian</id><content type="html" xml:base="/projects/2026/01/16/encrypting-small-text-in-obsidian.html"><![CDATA[<p>I recently published a small project called <strong>obsidian-text-lock</strong>.</p>

<p>I want to say this immediately: I did not start this project to build “the next Obsidian plugin”. I wrote it to solve a very specific problem I have in my daily notes.</p>

<p>I do not need to encrypt my entire vault. I do not even need to encrypt whole notes. What I often need is much simpler. Inside my notes there are small portions of text that I would prefer not to leave in plain view. Things like temporary passwords, access tokens, or short private annotations mixed with normal content.</p>

<p>There are already plugins that can do this, and they work well. The problem is not quality. The problem, for me, is accumulation. Every plugin is another thing to install, configure, update, and trust over time. For something as simple as encrypting selected text, that felt excessive.</p>

<p>I already use <strong>Templater</strong> extensively in Obsidian. I use it for automation, note generation, and small scripting tasks. At some point I realized that everything I needed was already there. I could select text, run JavaScript, and access the <em>Web Crypto API</em>. So I asked myself a simple question: why add another plugin when the tool I already use can do the job?</p>

<p>That is how obsidian-text-lock was born.</p>

<p>While working on this project, I used ChatGPT as a support tool to better understand some Obsidian and Web Crypto API details, and to get feedback during code review.</p>

<p>Technically, <strong>it is not really a plugin</strong>. It is just two Templater templates. One encrypts the selected text, the other decrypts it. There is no background process, no interface, and no vault-wide behavior. You select text, run the template, and the selection is replaced. When you need the text back, you do the opposite.</p>

<p>When a piece of text is encrypted, the note shows a small lock marker and a short message. The encrypted data itself is stored inside an Obsidian comment, so it stays hidden in Preview mode but remains part of the Markdown file. This means the note stays readable, and the encrypted block does not visually pollute the content.</p>

<p><img src="https://github.com/guelfoweb/obsidian-text-lock/raw/main/screenshots/encrypted-selection.png" alt="Encrypted selection" /></p>

<p>From a security point of view, I deliberately kept things boring and standard. Encryption is done using <code class="language-plaintext highlighter-rouge">AES-256-GCM</code>, and the key is derived from a password using <code class="language-plaintext highlighter-rouge">PBKDF2</code> with <code class="language-plaintext highlighter-rouge">SHA-256</code>. There is no custom cryptography and no home-made tricks. Each encrypted block contains everything it needs to be decrypted later: <code class="language-plaintext highlighter-rouge">salt</code>, <code class="language-plaintext highlighter-rouge">IV</code> (nonce), and ciphertext. <strong>The password is the only secret</strong>. If you lose it, the data is gone. That is not a bug, it is the expected behavior.</p>

<p>There are also clear limitations, and I think it is important to be honest about them.</p>

<ul>
  <li>This works reliably only in <em>Source mode</em>, because of how Templater accesses text selections.</li>
  <li>There is no key management, no recovery, and no protection against someone simply deleting the encrypted block.</li>
</ul>

<p>This tool is meant for personal notes and low-risk scenarios, not for highly sensitive or regulated data.</p>

<p>I decided to publish <em>obsidian-text-lock</em> because it is small, transparent, and easy to understand. It does not try to replace existing plugins, and it does not aim to be feature-rich. It simply solves a narrow problem in a way that fits my workflow.</p>

<p>If you already use Templater and want a minimal way to encrypt small parts of your notes, this might be useful to you.</p>

<p>The project is available on GitHub:
<a href="https://github.com/guelfoweb/obsidian-text-lock">https://github.com/guelfoweb/obsidian-text-lock</a></p>]]></content><author><name>guelfoweb</name></author><category term="projects" /><category term="obsidian" /><category term="templater" /><category term="encryption" /><category term="security" /><summary type="html"><![CDATA[I recently published a small project called obsidian-text-lock.]]></summary></entry><entry><title type="html">Ten years of Knockpy: version 8 released</title><link href="/projects/2025/10/26/ten-years-knockpy-version-8-release.html" rel="alternate" type="text/html" title="Ten years of Knockpy: version 8 released" /><published>2025-10-26T00:00:00+00:00</published><updated>2025-10-26T00:00:00+00:00</updated><id>/projects/2025/10/26/ten-years-knockpy-version-8-release</id><content type="html" xml:base="/projects/2025/10/26/ten-years-knockpy-version-8-release.html"><![CDATA[<p>Knockpy is a small project I have been working on for about ten years. It started from curiosity and need, and over time it became a stable tool, used in many penetration testing distributions.
Through the years, it has proved useful in an important area of security and OSINT: subdomain reconnaissance, a key step for those who map the exposed assets of an organization.</p>

<p>The goal has always been to keep the tool simple, portable, and easy to adapt to different situations. I never wanted to make it something huge or very complex, but rather something that works well, clearly, and helps people who work with these tasks every day.</p>

<h4 id="recognitions-that-are-good-for-the-code">Recognitions that are good for the code</h4>

<p>One of the things that makes me happiest, even after many years, is when a researcher writes to me privately to say thanks, or mentions Knockpy in a report or public post. Knowing that the tool really helped to find vulnerabilities or to achieve results in bug bounty programs is a quiet but deep satisfaction. Not because the credit is mine, but because the project, in its small way, has been useful to someone. And that, for me, gives meaning to the time spent keeping it alive.</p>

<h3 id="what-has-changed">What has changed</h3>

<p>With version 8, I decided to deeply review the internal architecture. This need came with time, from some limitations that appeared as technologies evolved, and also from the wish to make Knockpy easier to integrate into modern environments. Many parts of the original code had become hard to extend, and some choices made years ago no longer made sense today.</p>

<h4 id="asynchronous-dns-engine">Asynchronous DNS engine</h4>

<p>The first step was to clearly separate the different modules: DNS resolution first, then validation, and result saving. Each component is now isolated and can be used independently. This makes it possible, for example, to use the asynchronous DNS engine without running the full scanning pipeline, or to build a custom analysis with dynamic wordlists and specific parameters.</p>

<p>The DNS resolution part was completely rewritten using <code class="language-plaintext highlighter-rouge">asyncio</code>, to get better performance without losing stability. I tried to keep compatibility with existing tools and at the same time make the output cleaner, more consistent, and easier to use. Results are saved in JSON format, organized by domain, so they can be easily used in other analyses or automations.</p>

<h4 id="http-response-content-in-bytes-and-supported-tls-protocol-version">HTTP response content in bytes and supported TLS protocol version</h4>

<p>Alongside these structural changes, I added two new features designed to improve the analysis of active subdomains. The first shows the size in bytes of the HTTP response content; the second checks the TLS protocol, allowing the detection of supported versions and possible vulnerabilities. These data are also included in the JSON output, making them easy to extract and combine.</p>

<h4 id="optimized-python-module">Optimized Python module</h4>

<p>Since the previous version, Knockpy can be used both from the command line and as a Python module. This gives more flexibility for those who want to integrate it into their own scripts.</p>

<h3 id="ai-helps-but-human-judgment-remains-essential">AI helps, but human judgment remains essential</h3>

<p>During the development of this version, I decided to experiment a bit. For some functions, especially the more structural ones, I used help from artificial intelligence tools. The experience was instructive. In some cases, I received good suggestions, useful for seeing the code from new angles or rethinking certain choices. In other cases, the code made by AI did not match the style and logic of the project.
The time spent reviewing, fixing, and adapting was still valuable. I learned that AI can be a good assistant, but it cannot replace the thinking and responsibility that every development choice needs. In any case, it was an interesting collaboration, and I think it could have a role again in the future if used carefully.</p>

<h2 id="main-features">Main features</h2>

<h3 id="simple-resolution-of-a-domain">Simple resolution of a domain</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>knockpy <span class="nt">-d</span> guelfoweb.com

guelfoweb.com <span class="o">[</span><span class="s1">'185.199.110.153'</span>, <span class="s1">'185.199.108.153'</span>, <span class="s1">'185.199.109.153'</span>, <span class="s1">'185.199.111.153'</span><span class="o">]</span>
http   <span class="o">[</span>301, <span class="s1">'https://guelfoweb.com/'</span>, <span class="s1">'GitHub.com'</span>, 162]
https  <span class="o">[</span>200, None, <span class="s1">'GitHub.com'</span>, 6681]
cert   <span class="o">[</span>True, <span class="s1">'2025-12-25'</span>, <span class="s1">'guelfoweb.com'</span>, <span class="o">[</span><span class="s1">'TLS 1.2'</span>, <span class="s1">'TLS 1.3'</span><span class="o">]]</span>
<span class="nt">------------------------------------------------------------</span>
1 domains <span class="k">in </span>00:00:00
</code></pre></div></div>

<p>With the <code class="language-plaintext highlighter-rouge">-d</code> parameter you set the domain. The first line shows the domain name and the list of IP addresses that resolve it.</p>

<p>The answers for the <code class="language-plaintext highlighter-rouge">http</code> and <code class="language-plaintext highlighter-rouge">https</code> protocols are shown in this order:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">status_code</code>, <code class="language-plaintext highlighter-rouge">redirect</code>, <code class="language-plaintext highlighter-rouge">webserver</code>, <code class="language-plaintext highlighter-rouge">content_byte</code></li>
</ul>

<p><code class="language-plaintext highlighter-rouge">cert</code> gives information about the certificate and the TLS protocol. It returns <code class="language-plaintext highlighter-rouge">True</code> if no problems are found; otherwise it returns <code class="language-plaintext highlighter-rouge">False</code>. The items shown in the list are:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">True</code>, <code class="language-plaintext highlighter-rouge">expiration_date</code>, <code class="language-plaintext highlighter-rouge">subjectAltName</code>, <code class="language-plaintext highlighter-rouge">TLS_supported</code></li>
</ul>

<h3 id="subdomain-reconnaissance">Subdomain reconnaissance</h3>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">$ </span>knockpy <span class="nt">-d</span> guelfoweb.com <span class="nt">--recon</span>
Reconnaissance...
- VirusTotal: ✔️
- Shodan:     ✔️
Scanned 3/3 domains...

www.guelfoweb.com <span class="o">[</span><span class="s1">'185.199.109.153'</span>, <span class="s1">'185.199.110.153'</span>, <span class="s1">'185.199.111.153'</span>, <span class="s1">'185.199.108.153'</span><span class="o">]</span>
http   <span class="o">[</span>301, <span class="s1">'https://guelfoweb.com/'</span>, <span class="s1">'GitHub.com'</span>, 162]
https  <span class="o">[</span>301, <span class="s1">'https://guelfoweb.com/'</span>, <span class="s1">'GitHub.com'</span>, 162]
cert   <span class="o">[</span>True, <span class="s1">'2025-12-25'</span>, <span class="s1">'guelfoweb.com'</span>, <span class="o">[</span><span class="s1">'TLS 1.2'</span>, <span class="s1">'TLS 1.3'</span><span class="o">]]</span>
<span class="nt">------------------------------------------------------------</span>
guelfoweb.com <span class="o">[</span><span class="s1">'185.199.109.153'</span>, <span class="s1">'185.199.110.153'</span>, <span class="s1">'185.199.108.153'</span>, <span class="s1">'185.199.111.153'</span><span class="o">]</span>
http   <span class="o">[</span>301, <span class="s1">'https://guelfoweb.com/'</span>, <span class="s1">'GitHub.com'</span>, 162]
https  <span class="o">[</span>200, None, <span class="s1">'GitHub.com'</span>, 6681]
cert   <span class="o">[</span>True, <span class="s1">'2025-12-25'</span>, <span class="s1">'guelfoweb.com'</span>, <span class="o">[</span><span class="s1">'TLS 1.2'</span>, <span class="s1">'TLS 1.3'</span><span class="o">]]</span>
<span class="nt">------------------------------------------------------------</span>
2 domains <span class="k">in </span>00:00:07
</code></pre></div></div>

<p>Adding the <code class="language-plaintext highlighter-rouge">--recon</code> option runs an online scan for subdomains. For each subdomain, the tool resolves it and shows the results in the order they are found.</p>

<h4 id="api-key">API Key</h4>

<p>For deeper checks, it is strongly recommended to set up <code class="language-plaintext highlighter-rouge">VirusTotal</code> and <code class="language-plaintext highlighter-rouge">Shodan</code> APIs. You can set the environment variables in two ways:</p>

<h5 id="1-using-a-file-named-env-recommended">1. Using a file named <code class="language-plaintext highlighter-rouge">.env</code> (recommended):</h5>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nv">API_KEY_VIRUSTOTAL</span><span class="o">=</span>your-virustotal-api-key
<span class="nv">API_KEY_SHODAN</span><span class="o">=</span>your-shodan-api-key
</code></pre></div></div>

<h5 id="2-using-a-unixlinux-shell-command">2. Using a Unix/Linux shell command:</h5>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">export </span><span class="nv">API_KEY_VIRUSTOTAL</span><span class="o">=</span>your-virustotal-api-key
<span class="nb">export </span><span class="nv">API_KEY_SHODAN</span><span class="o">=</span>your-shodan-api-key
</code></pre></div></div>

<h4 id="bruteforcing">Bruteforcing</h4>

<p>This is not a real brute-force attack, but a wordlist-based attack. To enable it, add the <code class="language-plaintext highlighter-rouge">--bruteforce</code> (or <code class="language-plaintext highlighter-rouge">--brute</code>) option. Knockpy will load the default list automatically.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>knockpy <span class="nt">-d</span> guelfoweb.com <span class="nt">--recon</span> <span class="nt">--brute</span>
</code></pre></div></div>

<h5 id="wordlist">Wordlist</h5>

<p>If you want to use your own wordlist, give its path with <code class="language-plaintext highlighter-rouge">--wordlist</code> followed by the file path.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>knockpy <span class="nt">-d</span> guelfoweb.com <span class="nt">--recon</span> <span class="nt">--brute</span> <span class="nt">--wordlist</span> path/to/wordlist.txt
</code></pre></div></div>

<h3 id="wildcard-test">Wildcard test</h3>

<p>Testing for wildcard DNS is important before scanning. It avoids invalid results because the server could answer the same way for every subdomain. Run the test like this:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>knockpy <span class="nt">-d</span> guelfoweb.com <span class="nt">--wildcard</span>
</code></pre></div></div>

<p>If the test is positive (wildcard is enabled), you do not need to continue the scan.</p>

<h3 id="output-of-results">Output of results</h3>

<p>Each scan is saved automatically to a file named <code class="language-plaintext highlighter-rouge">domain.com_YYYY_MM_DD_HH_mm_ss.json</code>. In the example above, the file was saved as <code class="language-plaintext highlighter-rouge">guelfoweb.com_2025_10_25_21_46_40.json</code>.</p>

<h4 id="specific-directory">Specific directory</h4>

<p>To save the file in a specific folder, use <code class="language-plaintext highlighter-rouge">--save</code> and give the folder path. For example:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>knockpy <span class="nt">-d</span> guelfoweb.com <span class="nt">--recon</span> <span class="nt">--save</span> path/to/results
</code></pre></div></div>

<h4 id="json-structure">JSON structure</h4>

<p>Results are saved in JSON format, grouped by domain. This makes it easy to use them in other analyses or automations. The file <code class="language-plaintext highlighter-rouge">guelfoweb.com_2025_10_25_21_46_40.json</code> has this structure:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[
  {
    "domain": "www.guelfoweb.com",
    "ip": [
      "185.199.109.153",
      "185.199.110.153",
      "185.199.111.153",
      "185.199.108.153"
    ],
    "http": [
      301,
      "https://guelfoweb.com/",
      "GitHub.com",
      162
    ],
    "https": [
      301,
      "https://guelfoweb.com/",
      "GitHub.com",
      162
    ],
    "cert": [
      true,
      "2025-12-25",
      "guelfoweb.com",
      [
        "TLS 1.2",
        "TLS 1.3"
      ]
    ]
  },
  {
    "domain": "guelfoweb.com",
    "ip": [
      "185.199.109.153",
      "185.199.110.153",
      "185.199.108.153",
      "185.199.111.153"
    ],
    "http": [
      301,
      "https://guelfoweb.com/",
      "GitHub.com",
      162
    ],
    "https": [
      200,
      null,
      "GitHub.com",
      6681
    ],
    "cert": [
      true,
      "2025-12-25",
      "guelfoweb.com",
      [
        "TLS 1.2",
        "TLS 1.3"
      ]
    ]
  }
]
</code></pre></div></div>

<h4 id="view-a-report">View a report</h4>

<p>To show the results in a more readable way, use <code class="language-plaintext highlighter-rouge">--report</code> with the JSON file path. In the example:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>knockpy <span class="nt">--report</span> guelfoweb.com_2025_10_25_21_46_40.json
</code></pre></div></div>

<h2 id="python-api">Python API</h2>

<p>If installed, Knockpy can be imported as a Python module, which makes it easy to use.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">knock</span> <span class="kn">import</span> <span class="n">KNOCKPY</span>

<span class="n">domain</span> <span class="o">=</span> <span class="s">'example.com'</span>

<span class="n">results</span> <span class="o">=</span> <span class="n">KNOCKPY</span><span class="p">(</span>
    <span class="n">domain</span><span class="p">,</span>
    <span class="n">dns</span><span class="o">=</span><span class="s">"8.8.8.8"</span><span class="p">,</span>
    <span class="n">useragent</span><span class="o">=</span><span class="s">"Mozilla/5.0"</span><span class="p">,</span>
    <span class="n">timeout</span><span class="o">=</span><span class="mi">2</span><span class="p">,</span>
    <span class="n">threads</span><span class="o">=</span><span class="mi">10</span><span class="p">,</span>
    <span class="n">recon</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
    <span class="n">bruteforce</span><span class="o">=</span><span class="bp">True</span><span class="p">,</span>
    <span class="n">wordlist</span><span class="o">=</span><span class="bp">None</span><span class="p">,</span>
    <span class="n">silent</span><span class="o">=</span><span class="bp">False</span>
<span class="p">)</span>

<span class="k">for</span> <span class="n">entry</span> <span class="ow">in</span> <span class="n">results</span><span class="p">:</span>
    <span class="k">print</span><span class="p">(</span><span class="n">entry</span><span class="p">[</span><span class="s">'domain'</span><span class="p">],</span> <span class="n">entry</span><span class="p">[</span><span class="s">'ip'</span><span class="p">],</span> <span class="n">entry</span><span class="p">[</span><span class="s">'http'</span><span class="p">],</span> <span class="n">entry</span><span class="p">[</span><span class="s">'cert'</span><span class="p">])</span>
</code></pre></div></div>

<p>For production, set <code class="language-plaintext highlighter-rouge">silent=True</code> to avoid printing scan details during runs.</p>

<p><strong>Project link:</strong> <a href="https://github.com/guelfoweb/knock">https://github.com/guelfoweb/knock</a></p>]]></content><author><name>guelfoweb</name></author><category term="projects" /><category term="knockpy" /><category term="subdomains" /><summary type="html"><![CDATA[Knockpy is a small project I have been working on for about ten years. It started from curiosity and need, and over time it became a stable tool, used in many penetration testing distributions. Through the years, it has proved useful in an important area of security and OSINT: subdomain reconnaissance, a key step for those who map the exposed assets of an organization.]]></summary></entry><entry><title type="html">Capire gli embedding con EmbeddingGemma</title><link href="/ai/ita/2025/09/23/capire-gli-embedding-con-embeddinggemma.html" rel="alternate" type="text/html" title="Capire gli embedding con EmbeddingGemma" /><published>2025-09-23T00:00:00+00:00</published><updated>2025-09-23T00:00:00+00:00</updated><id>/ai/ita/2025/09/23/capire-gli-embedding-con-embeddinggemma</id><content type="html" xml:base="/ai/ita/2025/09/23/capire-gli-embedding-con-embeddinggemma.html"><![CDATA[<p>Si parla molto di LLM, i cosiddetti <em>Large Language Models</em> come ChatGPT, Gemini o Llama, modelli che sanno scrivere testi, rispondere a domande, riassumere documenti. Insomma addestrati per generare linguaggio.</p>

<p>Accanto a questa famiglia esiste un altro tipo di modello, meno conosciuto ma non per questo meno importante. Sono <strong>i modelli di embedding</strong>. A differenza degli LLM, questi non producono frasi, il loro scopo è quello di prendere un testo e trasformarlo in una sequenza di numeri, un vettore che ne rappresenta il significato.</p>

<p>Anche un LLM, per funzionare, utilizza internamente un sistema di embedding. Ogni parola, ogni pezzo di parola, viene trasformato in numeri prima di poter essere elaborato. La differenza è che negli LLM questo passaggio rimane nascosto, serve solo come base per arrivare alla generazione del linguaggio. Nei modelli di embedding, invece, questa trasformazione è l’obiettivo stesso.</p>

<p>Per intenderci, se chiediamo ad un LLM “<strong><em>cos’è una firma digitale?</em></strong>” ci risponderà con una spiegazione articolata. Un modello di embedding, alla stessa domanda, non scrive nessuna risposta testuale, restituisce invece un insieme di numeri che rappresentano quella frase nello <strong>spazio semantico</strong>, una sorta di mappa in cui ogni frase trova una posizione in base al suo significato.</p>

<p>Facciamo un esempio per chiarire meglio il concetto. Chiediamo al modello di embedding “<em>Cos’è una firma digitale?</em>”. Non gli forniamo nessun documento da confrontare, vogliamo solo vedere cosa produce in uscita.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">sentence_transformers</span> <span class="kn">import</span> <span class="n">SentenceTransformer</span>

<span class="n">model</span> <span class="o">=</span> <span class="n">SentenceTransformer</span><span class="p">(</span><span class="s">"google/embeddinggemma-300m"</span><span class="p">)</span>

<span class="n">query</span> <span class="o">=</span> <span class="s">"Cos'è una firma digitale?"</span>

<span class="c1"># Query embedding
</span><span class="n">query_emb</span> <span class="o">=</span> <span class="n">model</span><span class="p">.</span><span class="n">encode</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="n">normalize_embeddings</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

<span class="k">print</span><span class="p">(</span><span class="s">"Embedding size:"</span><span class="p">,</span> <span class="n">query_emb</span><span class="p">.</span><span class="n">shape</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">"Values:"</span><span class="p">,</span> <span class="n">query_emb</span><span class="p">)</span>
</code></pre></div></div>

<p>Il risultato sarà il seguente:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Embedding size: (768,)
Values: [-1.29152596e-01 -2.52732392e-02 -1.64837558e-02  4.21979874e-02
  1.01330150e-02  4.22639251e-02 -9.70820710e-03  3.03274114e-02
 -2.69848965e-02  3.98598611e-03 -3.41513008e-02  2.13023946e-02
 -1.75719075e-02  7.05240667e-02  1.03565618e-01 -4.28020488e-03
  .......
  .......
 -1.63111847e-03  4.68430854e-02 -5.01399534e-03  1.51540190e-02
 -5.29640205e-02 -1.45818135e-02 -3.43373977e-02  2.02104747e-02
 -1.22715998e-02  6.40184134e-02 -4.49002022e-03  3.19903553e-03
 -4.00503315e-02 -8.90033245e-02  1.74124353e-02  1.15116490e-02]
</code></pre></div></div>

<p>Questi numeri costituiscono un <strong>vettore rappresentativo</strong> dell’intera frase. Nel caso di <strong>EmbeddingGemma</strong> (il modello di embedding che stiamo utilizzando) il vettore ha sempre una <strong>dimensione fissa</strong> di 768 valori, indipendentemente dalla lunghezza del testo. Possiamo pensare questi numeri come coordinate che permettono di confrontare una domanda (query) con altri testi e stabilire, tramite algoritmi che misurano la similarità o la distanza euclidea, se due frasi esprimono concetti simili oppure trattano argomenti molto diversi.</p>

<p>In generale possiamo dire che dati due o più vettori numerici, più le loro coordinate sono vicine, più le frasi che rappresentano condividono lo stesso significato; al contrario, se i vettori risultano distanti, significa che i testi corrispondenti parlano di argomenti molto diversi.</p>

<p>Per rendere il concetto più semplice, possiamo immaginare che ogni frase sia come una città su una mappa. L’embedding è come la <strong>coppia di coordinate</strong> (latitudine e longitudine) che ci dice dove si trova quella città. Se due città sono vicine, vuol dire che hanno molto in comune (regione, clima, cultura, economia, storia, lingua…), se invece sono lontane, vuol dire che appartengono a contesti diversi.</p>

<p>Se ora forniamo un documento dove dice che <em>“La firma digitale è un sistema informatico che assicura autenticità e integrità di un documento elettronico.”</em>, i due embedding, quello della domanda e quello del documento, finiranno in punti vicini della mappa semantica.</p>

<p>Se invece confrontiamo la stessa domanda con un testo che parla del Colosseo, ad esempio <em>“Il Colosseo è un antico anfiteatro romano situato nel centro di Roma.”</em>, i due punti saranno molto lontani.</p>

<p>Vediamolo con un esempio.</p>

<h4 id="codice_1">Codice_1</h4>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">sentence_transformers</span> <span class="kn">import</span> <span class="n">SentenceTransformer</span><span class="p">,</span> <span class="n">util</span>

<span class="n">model</span> <span class="o">=</span> <span class="n">SentenceTransformer</span><span class="p">(</span><span class="s">"./embeddinggemma-300m"</span><span class="p">)</span>

<span class="c1"># Documenti da confrontare
</span><span class="n">documents</span> <span class="o">=</span> <span class="p">[</span>
    <span class="s">"La firma digitale è un sistema informatico che assicura autenticità e integrità di un documento elettronico."</span><span class="p">,</span>
    <span class="s">"Il Colosseo è un antico anfiteatro romano situato nel centro di Roma."</span>
<span class="p">]</span>

<span class="c1"># Query dell'utente
</span><span class="n">query</span> <span class="o">=</span> <span class="s">"Cos'è una firma digitale?"</span>

<span class="c1"># Calcolo degli embedding
</span><span class="n">doc_embeddings</span> <span class="o">=</span> <span class="n">model</span><span class="p">.</span><span class="n">encode</span><span class="p">(</span><span class="n">documents</span><span class="p">,</span> <span class="n">convert_to_tensor</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
<span class="n">query_embedding</span> <span class="o">=</span> <span class="n">model</span><span class="p">.</span><span class="n">encode</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="n">convert_to_tensor</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

<span class="c1"># Calcolo la similarità coseno tra la query e i documenti
</span><span class="n">cosine_scores</span> <span class="o">=</span> <span class="n">util</span><span class="p">.</span><span class="n">cos_sim</span><span class="p">(</span><span class="n">query_embedding</span><span class="p">,</span> <span class="n">doc_embeddings</span><span class="p">)</span>

<span class="c1"># Trovo il documento più simile
</span><span class="n">best_idx</span> <span class="o">=</span> <span class="n">cosine_scores</span><span class="p">.</span><span class="n">argmax</span><span class="p">()</span>
<span class="k">print</span><span class="p">(</span><span class="s">"Documento più rilevante:"</span><span class="p">,</span> <span class="n">documents</span><span class="p">[</span><span class="n">best_idx</span><span class="p">])</span>
<span class="k">print</span><span class="p">(</span><span class="s">"Punteggio di similarità:"</span><span class="p">,</span> <span class="n">cosine_scores</span><span class="p">[</span><span class="mi">0</span><span class="p">][</span><span class="n">best_idx</span><span class="p">].</span><span class="n">item</span><span class="p">())</span>

<span class="k">print</span><span class="p">()</span>

<span class="c1"># Stampa di tutti i punteggi per confronto
</span><span class="k">for</span> <span class="n">doc</span><span class="p">,</span> <span class="n">score</span> <span class="ow">in</span> <span class="nb">zip</span><span class="p">(</span><span class="n">documents</span><span class="p">,</span> <span class="n">cosine_scores</span><span class="p">[</span><span class="mi">0</span><span class="p">]):</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">doc</span><span class="si">}</span><span class="s"> → </span><span class="si">{</span><span class="n">score</span><span class="p">.</span><span class="n">item</span><span class="p">()</span><span class="si">:</span><span class="p">.</span><span class="mi">3</span><span class="n">f</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
</code></pre></div></div>

<p>Il risultato sarà il seguente:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Documento più rilevante: La firma digitale è un sistema informatico che assicura autenticità e integrità di un documento elettronico.
Punteggio di similarità: 0.7245991230010986

La firma digitale è un sistema informatico che assicura autenticità e integrità di un documento elettronico. → 0.725
Il Colosseo è un antico anfiteatro romano situato nel centro di Roma. → 0.244
</code></pre></div></div>

<p>In questo esempio il modello ha restituito come più rilevante la frase sulla firma digitale, con un punteggio di similarità (0.72) nettamente superiore rispetto a quella sul Colosseo (0.24).</p>

<p>In realtà questo risultato non dovrebbe sorprenderci. La query <code class="language-plaintext highlighter-rouge">Cos’è una firma digitale?</code> e il documento <code class="language-plaintext highlighter-rouge">La firma digitale è un sistema informatico che assicura autenticità e integrità di un documento elettronico.</code> condividono esplicitamente le stesse parole chiave, in particolare l’espressione <code class="language-plaintext highlighter-rouge">firma digitale</code>. Questo facilita il compito del modello che può appoggiarsi anche alla corrispondenza lessicale.</p>

<p><strong>Proviamo quindi con un esempio più difficile</strong></p>

<p>Per rendere il test più interessante proviamo con un esempio più difficile, con documenti che non contengano le stesse parole della query, in modo da verificare la capacità del modello di cogliere davvero la similarità semantica e non solo la somiglianza superficiale delle stringhe.</p>

<p>Modifichiamo il primo documento in <strong>Codice_1</strong>.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Documenti da confrontare
</span><span class="n">documents</span> <span class="o">=</span> <span class="p">[</span>
    <span class="s">"È un sistema che permette di verificare l’identità dell’autore di un file e di controllare che il contenuto non sia stato modificato."</span><span class="p">,</span>
    <span class="s">"Il Colosseo è un antico anfiteatro romano situato nel centro di Roma."</span>
<span class="p">]</span>

<span class="c1"># Query dell'utente
</span><span class="n">query</span> <span class="o">=</span> <span class="s">"Cos'è una firma digitale?"</span>
</code></pre></div></div>

<p>Risultato:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Documento più rilevante: È un sistema che permette di verificare l’identità dell’autore di un file e di controllare che il contenuto non sia stato modificato.
Punteggio di similarità: 0.3943994641304016

È un sistema che permette di verificare l’identità dell’autore di un file e di controllare che il contenuto non sia stato modificato. → 0.394
Il Colosseo è un antico anfiteatro romano situato nel centro di Roma. → 0.244
</code></pre></div></div>

<p>La query è rimasta sempre la stessa: <code class="language-plaintext highlighter-rouge">Cos’è una firma digitale?</code>, ma il primo documento non contiene più la stessa espressione. Al posto di <code class="language-plaintext highlighter-rouge">firma digitale</code> viene usata una descrizione del concetto, parlando di un sistema che consente di verificare l’identità dell’autore di un file e di controllare che non sia stato modificato.</p>

<p><code class="language-plaintext highlighter-rouge">È un sistema che permette di verificare l’identità dell’autore di un file e di controllare che il contenuto non sia stato modificato.</code></p>

<p>Nonostante questa differenza lessicale, il modello è riuscito a capire che quella descrizione si riferiva allo stesso concetto e l’ha collegata correttamente alla domanda. Il risultato è stato un punteggio più alto rispetto a un documento del tutto diverso, come quello sul Colosseo. In altre parole, non si è fermato alle singole parole, ma ha saputo cogliere il senso complessivo della frase.</p>

<h2 id="come-avviene-il-confronto-con-cosine-similarity">Come avviene il confronto con cosine similarity?</h2>

<p>Supponiamo di avere la nostra domanda (query)  e le due frasi (doc1 e doc2) trasformati, per semplicità, in embedding a 5 dimensioni invece che 768.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>query = "Cos’è una firma digitale?" 
E(query) = [0.010,  0.042, -0.009,  0.030, -0.015]

doc1 = "La firma digitale garantisce autenticità dei documenti"
E(doc1) = [0.012,  0.039, -0.011,  0.028, -0.017]

doc2 = "Il Colosseo si trova a Roma"
E(doc2) = [-0.045,  0.002,  0.037, -0.020,  0.041]
</code></pre></div></div>

<p>Indichiamo per comodità E(query) = <code class="language-plaintext highlighter-rouge">A</code>, E(doc1) = <code class="language-plaintext highlighter-rouge">B</code> e E(doc2) = <code class="language-plaintext highlighter-rouge">C</code></p>

<p>Il confronto tramite <strong><a href="https://en.wikipedia.org/wiki/Cosine_similarity">cosine similarity</a></strong> è molto semplice dal punto di vista matematico (e <a href="https://www.youtube.com/watch?v=e9U0QAFbfLI">questo video</a> lo spiega abbastanza bene) in quanto si basa sul prodotto scalare e sulla norma dei vettori.</p>

<p>\(\mathrm{sim}_{\cos}(A,B)=\frac{A\cdot B}{\|A\|\;\|B\|}\)
ovvero:
\(\mathrm{sim}_{\cos}(A,B)=
\frac{\sum_{i=1}^{n} A_i B_i}{
\sqrt{\sum_{i=1}^{n} A_i^{2}}\;\sqrt{\sum_{i=1}^{n} B_i^{2}}
}\)</p>

<p>risparmiandoci i calcoli, visto che Claude Sonnet è bravo e veloce a fare i conti, otteniamo:</p>

<ul>
  <li><strong>Cosine similarity (A,B) ≈</strong><code class="language-plaintext highlighter-rouge">1.028</code> (molto simili, coseno vicino a 1, quasi identici)</li>
  <li><strong>Cosine similarity (A,C) ≈</strong><code class="language-plaintext highlighter-rouge">-0.536</code> (frasi molto diverse, quasi in direzione opposta)</li>
</ul>

<p>La cosine similarity guarda l’<strong>angolo</strong> tra le due frecce (vettori) nello spazio. Se l’angolo è piccolo, i vettori sono quasi paralleli, dunque sono frasi con lo stesso significato.</p>

<h2 id="come-fa-embeddinggemma-a-lavorare-con-100-lingue">Come fa EmbeddingGemma a lavorare con 100 lingue?</h2>

<p>EmbeddingGemma <strong>non ha cento vocabolari diversi</strong> al suo interno, uno per ciascuna delle lingue su cui è stato addestrato. Usa invece un unico tokenizer multilingue, basato su subword, cioè pezzi di parola che vengono combinati per rappresentare testi in lingue diverse.</p>

<p>Verifichiamo la parola italiana <code class="language-plaintext highlighter-rouge">digitale</code> e poi il termine inglese <code class="language-plaintext highlighter-rouge">digital</code>, per capire se vengono rappresentati come token unici o spezzati in sub-token e, soprattutto, per osservare come vengono convertiti dal modello.</p>
<h4 id="codice_2">Codice_2</h4>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">transformers</span> <span class="kn">import</span> <span class="n">AutoTokenizer</span>

<span class="c1"># Tokenizer EmbeddingGemma
</span><span class="n">model_id</span> <span class="o">=</span> <span class="s">"google/embeddinggemma-300m"</span>
<span class="n">tokenizer</span> <span class="o">=</span> <span class="n">AutoTokenizer</span><span class="p">.</span><span class="n">from_pretrained</span><span class="p">(</span><span class="n">model_id</span><span class="p">)</span>

<span class="c1"># Word to test
</span><span class="n">word</span> <span class="o">=</span> <span class="s">"digitale"</span>

<span class="c1"># Get ID
</span><span class="n">ids</span> <span class="o">=</span> <span class="n">tokenizer</span><span class="p">.</span><span class="n">encode</span><span class="p">(</span><span class="n">word</span><span class="p">,</span> <span class="n">add_special_tokens</span><span class="o">=</span><span class="bp">False</span><span class="p">)</span>
<span class="n">tokens</span> <span class="o">=</span> <span class="n">tokenizer</span><span class="p">.</span><span class="n">convert_ids_to_tokens</span><span class="p">(</span><span class="n">ids</span><span class="p">)</span>

<span class="k">print</span><span class="p">(</span><span class="s">"Word:"</span><span class="p">,</span> <span class="n">word</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">"Token IDs:"</span><span class="p">,</span> <span class="n">ids</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">"Tokens:"</span><span class="p">,</span> <span class="n">tokens</span><span class="p">)</span>

<span class="c1"># Check if the word matches a unique token
</span><span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">tokens</span><span class="p">)</span> <span class="o">==</span> <span class="mi">1</span><span class="p">:</span>
    <span class="k">print</span><span class="p">(</span><span class="s">"unique token"</span><span class="p">)</span>
<span class="k">else</span><span class="p">:</span>
    <span class="k">print</span><span class="p">(</span><span class="s">"sub-token"</span><span class="p">)</span>

</code></pre></div></div>

<p>Risposta per <code class="language-plaintext highlighter-rouge">digitale</code> in italiano</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Word: digitale
Token IDs: <span class="o">[</span>29345, 1203]
Tokens: <span class="o">[</span><span class="s1">'digit'</span>, <span class="s1">'ale'</span><span class="o">]</span>
sub-token
</code></pre></div></div>

<p>Risposta per <code class="language-plaintext highlighter-rouge">digital</code> in inglese</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Word: digital
Token IDs: <span class="o">[</span>36661]
Tokens: <span class="o">[</span><span class="s1">'digital'</span><span class="o">]</span>
unique token
</code></pre></div></div>

<p>Dai risultati possiamo osservare che <code class="language-plaintext highlighter-rouge">digital</code> in inglese è talmente frequente da essere un <strong>token unico,</strong> mentre l’italiano <code class="language-plaintext highlighter-rouge">digitale</code> viene spezzato (sub-token) in due pezzi: <code class="language-plaintext highlighter-rouge">digit</code> e <code class="language-plaintext highlighter-rouge">ale</code>. Nonostante la differenza, entrambi contengono il frammento <code class="language-plaintext highlighter-rouge">digit</code> che riduce la distanza tra le due rappresentazioni, ma è grazie all’addestramento multilingue che il modello impara ad allineare i significati e a considerare i due termini semanticamente vicini.</p>

<p>Il modello, dunque, non ha un token per ogni parola di ogni lingua, sarebbe impraticabile avere un vocabolario separato per 100 lingue. <strong>EmbeddingGemma</strong> lavora con un <strong>set di mattoncini linguistici</strong> (sub-token) che possono essere combinati per ricostruire le parole delle lingue su cui è stato addestrato.</p>

<p>Ad esempio <code class="language-plaintext highlighter-rouge">digitalization</code> diventa:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Word: digitalization
Token IDs: [36661, 1854]
Tokens: ['digital', 'ization']
sub-token
</code></pre></div></div>

<p>Il termine viene spezzato in due parti:</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">digital</code>: già presente come token unico e molto frequente (ID 36661).</li>
  <li><code class="language-plaintext highlighter-rouge">ization</code>: suffisso comune in inglese, riusato in molte parole (<em>organization, realization, optimization…</em>).</li>
</ul>

<h2 id="cosa-possiamo-fare-con-embeddinggemma">Cosa possiamo fare con EmbeddingGemma?</h2>

<p>Come abbiamo già detto, EmbeddingGemma è stato ottimizzato esclusivamente per catturare la similarità semantica. Non scrive frasi, ma crea rappresentazioni numeriche che permettono di fare <strong>ricerca semantica</strong>, <strong>classificazione</strong>, <strong>clustering</strong> o di supportare applicazioni di <strong>Retrieval-Augmented Generation</strong> (RAG).</p>

<p>Vediamoli in ordine uno per uno.</p>
<h3 id="ricerca-semantica">Ricerca semantica</h3>

<p>Abbiamo visto che la query e i documenti vengono trasformati in vettori e poi confrontati tramite algoritmi di similarità. Prendiamo ad esempio un mini dataset in ambito cybersecurity e poniamo alcune domande utilizzando lo script di <strong>Codice_1</strong>.</p>

<p><strong>Dataset di documenti (frasi per il nostro esempio)</strong></p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">Il phishing è una tecnica fraudolenta che cerca di rubare credenziali fingendosi un ente affidabile.</code></li>
  <li><code class="language-plaintext highlighter-rouge">Un ransomware è un malware che cripta i file di un computer e chiede un riscatto per sbloccarli.</code></li>
  <li><code class="language-plaintext highlighter-rouge">Un attacco DDoS consiste nell'inviare un numero enorme di richieste a un server per renderlo inaccessibile.</code></li>
  <li><code class="language-plaintext highlighter-rouge">L'autenticazione a due fattori (2FA) aumenta la sicurezza richiedendo un codice aggiuntivo oltre alla password.</code></li>
  <li><code class="language-plaintext highlighter-rouge">Un firewall controlla il traffico di rete in ingresso e in uscita per proteggere i sistemi informatici.</code></li>
</ol>

<p>Se proviamo con le seguenti <strong>query</strong>, vediamo che il modello riesce a rispondere in modo corretto e senza difficoltà.</p>

<ol>
  <li><code class="language-plaintext highlighter-rouge">Quale evento informatico blocca un server sommergendolo di richieste?</code></li>
  <li><code class="language-plaintext highlighter-rouge">Quale inganno online induce una persona a consegnare informazioni private pensando di parlare con un ente affidabile?</code></li>
  <li><code class="language-plaintext highlighter-rouge">Quale malware blocca l’uso dei documenti sul PC finché non viene versato denaro?</code></li>
  <li><code class="language-plaintext highlighter-rouge">Quale procedura di accesso richiede la conferma tramite smartphone oltre all’inserimento tradizionale?</code></li>
  <li><code class="language-plaintext highlighter-rouge">Quale tecnologia agisce come barriera tra un computer e Internet, impedendo intrusioni non autorizzate?</code></li>
</ol>

<p>Di seguito un esempio (n.5) di risposta:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Query: Quale tecnologia agisce come barriera tra un computer e Internet, impedendo intrusioni non autorizzate?
Documento più rilevante: Un firewall controlla il traffico di rete in ingresso e in uscita per proteggere i sistemi informatici.
Punteggio di similarità: 0.578133225440979
</code></pre></div></div>

<h4 id="migliorare-il-prompt-per-retrieval">Migliorare il prompt per retrieval</h4>

<p>Come suggerito in una discussione su <a href="https://huggingface.co/BAAI/bge-large-en-v1.5/discussions/11">Hugging Face</a> e come documentato nella guida di <a href="https://sbert.net/examples/sentence_transformer/training/prompts/README.html">Sentence-Transformers</a>, l’uso dell’istruzione <code class="language-plaintext highlighter-rouge">Represent this sentence for searching relevant...</code> prima della query può aiutare il modello a interpretare meglio il compito e, di conseguenza, migliorare le prestazioni nei task di retrieval.</p>

<p>La query (in <em>Codice_1</em>) verrà quindi preceduta dall’istruzione per migliorare la qualità degli embedding nelle attività di retrieval semantico.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">prompt</span> <span class="o">=</span> <span class="s">"Represent this sentence for searching relevant documents: "</span>
<span class="n">query</span> <span class="o">=</span> <span class="s">"Cos'è una firma digitale?"</span>
<span class="n">query_prompted</span> <span class="o">=</span> <span class="n">prompt</span> <span class="o">+</span> <span class="n">query</span>
</code></pre></div></div>

<h3 id="classificazione">Classificazione</h3>

<p>Un modello di embedding non decide da solo se ad esempio un’email è phishing o no, si limita a trasformare il testo in numeri che ne rappresentano il significato. Quei numeri diventano la materia prima per un classificatore. In questo esempio verrà usato <code class="language-plaintext highlighter-rouge">LogisticRegression</code>, un semplice classificatore della libreria scikit-learn.</p>

<p>Immaginiamo di avere solo 12 email, metà legittime e metà di phishing. Useremo alcune di esse per insegnare al classificatore a riconoscere la differenza e le altre per metterlo alla prova. Alla fine confronteremo le risposte con quelle corrette e calcoliamo quante ne ha indovinate.</p>
<h4 id="codice_3">Codice_3</h4>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">sentence_transformers</span> <span class="kn">import</span> <span class="n">SentenceTransformer</span>
<span class="kn">from</span> <span class="nn">sklearn.linear_model</span> <span class="kn">import</span> <span class="n">LogisticRegression</span>
<span class="kn">from</span> <span class="nn">sklearn.model_selection</span> <span class="kn">import</span> <span class="n">train_test_split</span>
<span class="kn">from</span> <span class="nn">sklearn.metrics</span> <span class="kn">import</span> <span class="n">accuracy_score</span>

<span class="n">emails</span> <span class="o">=</span> <span class="p">[</span>
    <span class="c1"># Phishing (1)
</span>    <span class="s">"Aggiorna subito la tua password cliccando su questo link"</span><span class="p">,</span>
    <span class="s">"Hai vinto un premio! Inserisci i tuoi dati bancari per riceverlo"</span><span class="p">,</span>
    <span class="s">"Il tuo conto è stato bloccato, verifica immediatamente le tue credenziali"</span><span class="p">,</span>
    <span class="s">"Gentile cliente, la tua carta di credito è sospesa. Accedi qui per sbloccarla"</span><span class="p">,</span>
    <span class="s">"Riceverai un rimborso, basta compilare il modulo online con i tuoi dati"</span><span class="p">,</span>
    <span class="s">"Il tuo account verrà chiuso se non confermi subito l’accesso"</span><span class="p">,</span>

    <span class="c1"># Legittime (0)
</span>    <span class="s">"La riunione del team è fissata per domani alle 10"</span><span class="p">,</span>
    <span class="s">"Grazie per aver acquistato sul nostro sito, trovi la fattura in allegato"</span><span class="p">,</span>
    <span class="s">"Il corso di formazione inizierà la prossima settimana"</span><span class="p">,</span>
    <span class="s">"Ecco il verbale della riunione di ieri"</span><span class="p">,</span>
    <span class="s">"La consegna del tuo pacco è prevista per giovedì"</span><span class="p">,</span>
    <span class="s">"La biblioteca comunale resterà chiusa per lavori fino a fine mese"</span>
<span class="p">]</span>

<span class="n">labels</span> <span class="o">=</span> <span class="p">[</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span><span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">,</span><span class="mi">0</span><span class="p">]</span>

<span class="c1"># Carica EmbeddingGemma
</span><span class="n">model</span> <span class="o">=</span> <span class="n">SentenceTransformer</span><span class="p">(</span><span class="s">"google/embeddinggemma-300m"</span><span class="p">)</span>

<span class="c1"># Calcola gli embedding delle email
</span><span class="n">X</span> <span class="o">=</span> <span class="n">model</span><span class="p">.</span><span class="n">encode</span><span class="p">(</span><span class="n">emails</span><span class="p">,</span> <span class="n">normalize_embeddings</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
<span class="n">y</span> <span class="o">=</span> <span class="n">labels</span>

<span class="c1"># Split train/test
</span><span class="n">test_size</span> <span class="o">=</span> <span class="mf">0.5</span>
<span class="n">X_train</span><span class="p">,</span> <span class="n">X_test</span><span class="p">,</span> <span class="n">y_train</span><span class="p">,</span> <span class="n">y_test</span> <span class="o">=</span> <span class="n">train_test_split</span><span class="p">(</span><span class="n">X</span><span class="p">,</span> <span class="n">y</span><span class="p">,</span> <span class="n">test_size</span><span class="o">=</span><span class="n">test_size</span><span class="p">,</span> <span class="n">random_state</span><span class="o">=</span><span class="mi">42</span><span class="p">)</span>

<span class="c1"># Stampa numeri e percentuali
</span><span class="n">n_total</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">emails</span><span class="p">)</span>
<span class="n">n_train</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">X_train</span><span class="p">)</span>
<span class="n">n_test</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">X_test</span><span class="p">)</span>

<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Totale esempi: </span><span class="si">{</span><span class="n">n_total</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Training set: </span><span class="si">{</span><span class="n">n_train</span><span class="si">}</span><span class="s"> esempi (</span><span class="si">{</span><span class="n">n_train</span><span class="o">/</span><span class="n">n_total</span><span class="si">:</span><span class="p">.</span><span class="mi">0</span><span class="o">%</span><span class="si">}</span><span class="s">)"</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Test set: </span><span class="si">{</span><span class="n">n_test</span><span class="si">}</span><span class="s"> esempi (</span><span class="si">{</span><span class="n">n_test</span><span class="o">/</span><span class="n">n_total</span><span class="si">:</span><span class="p">.</span><span class="mi">0</span><span class="o">%</span><span class="si">}</span><span class="s">)</span><span class="se">\n</span><span class="s">"</span><span class="p">)</span>

<span class="c1"># Allena classificatore
</span><span class="n">clf</span> <span class="o">=</span> <span class="n">LogisticRegression</span><span class="p">()</span>
<span class="n">clf</span><span class="p">.</span><span class="n">fit</span><span class="p">(</span><span class="n">X_train</span><span class="p">,</span> <span class="n">y_train</span><span class="p">)</span>

<span class="c1"># Predizione
</span><span class="n">pred</span> <span class="o">=</span> <span class="n">clf</span><span class="p">.</span><span class="n">predict</span><span class="p">(</span><span class="n">X_test</span><span class="p">)</span>

<span class="k">print</span><span class="p">(</span><span class="s">"Predizioni:"</span><span class="p">,</span> <span class="n">pred</span><span class="p">.</span><span class="n">tolist</span><span class="p">())</span>
<span class="k">print</span><span class="p">(</span><span class="s">"Valori reali:"</span><span class="p">,</span> <span class="n">y_test</span><span class="p">)</span>
<span class="k">print</span><span class="p">(</span><span class="s">"Accuratezza:"</span><span class="p">,</span> <span class="n">accuracy_score</span><span class="p">(</span><span class="n">y_test</span><span class="p">,</span> <span class="n">pred</span><span class="p">))</span>
</code></pre></div></div>

<p>Con <code class="language-plaintext highlighter-rouge">train_test_split</code> il dataset viene diviso in due parti, esattamente al 50% visto che <code class="language-plaintext highlighter-rouge">test_size=0.5</code>:</p>

<ol>
  <li><strong>Training test</strong>: usato per addestrare il classificatore, il modello impara da questa porzione.</li>
  <li><strong>Test set</strong>: messo da parte e usato solo per valutare, sono le email che userà in <code class="language-plaintext highlighter-rouge">.predict(X_test)</code></li>
</ol>

<p>Vediamo alcuni risultati cambiando i valori di <code class="language-plaintext highlighter-rouge">test_size</code>:</p>

<p>Risultati per <code class="language-plaintext highlighter-rouge">test_size=0.2</code>: indovinate <strong>2 su 3</strong> con una accuratezza del <strong>66,7%</strong>.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Totale esempi: 12
Training set: 9 esempi (75%)
Test set: 3 esempi (25%)

Predizioni: [1, 0, 1]
Valori reali: [0, 0, 1]
Accuratezza: 0.6666666666666666
</code></pre></div></div>

<p>Risultati per <code class="language-plaintext highlighter-rouge">test_size=0.5</code>: indovinate <strong>5 su 6</strong> con una accuratezza del <strong>83,3%</strong>.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Totale esempi: 12
Training set: 6 esempi (50%)
Test set: 6 esempi (50%)

Predizioni: [0, 0, 1, 0, 0, 1]
Valori reali: [0, 0, 1, 0, 1, 1]
Accuratezza: 0.8333333333333334
</code></pre></div></div>

<p>Risultati per <code class="language-plaintext highlighter-rouge">test_size=0.8</code>: indovinate <strong>8 su 10</strong> con una accuratezza del <strong>80%</strong>.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Totale esempi: 12
Training set: 2 esempi (17%)
Test set: 10 esempi (83%)

Predizioni: [0, 0, 1, 0, 1, 1, 1, 1, 1, 1]
Valori reali: [0, 0, 1, 0, 1, 1, 1, 0, 1, 0]
Accuratezza: 0.8
</code></pre></div></div>

<p>I risultati cambiano molto a seconda di come dividiamo i dati tra training e test. Con il 75% dei dati usati per l’addestramento l’accuratezza è stata del 66,7%, con una divisione a metà è salita all’83,3%, mentre con appena 2 esempi usati per allenare il modello (e ben 10 per testarlo) si è comunque mantenuta intorno all’80%.</p>

<p>È bene ricordare che si tratta di un test fatto con pochissimi esempi, non possiamo aspettarci stabilità nei numeri. La cosa importante, però, è che la pipeline funziona. EmbeddingGemma riesce a trasformare il testo in numeri che catturano il significato e, a partire da questi, persino un classificatore semplicissimo come la Logistic Regression riesce a distinguere tra phishing ed email legittime. Con un numero maggiore di dati reali i risultati diventerebbero molto più solidi e affidabili.</p>

<p>È un pò come insegnare a un bambino a distinguere tra frutta e verdura: se gli mostriamo solo pochi esempi all’inizio farà confusione, ma man mano che gli facciamo vedere altri casi imparerà a riconoscerle sempre meglio.</p>

<h3 id="clustering">Clustering</h3>

<p>Mentre nella <strong>ricerca semantica</strong> c’è sempre una query, nel <strong>clustering</strong>, invece, non c’è nessuna query. Lo scopo è quello di scoprire gruppi di testi simili.</p>

<p>Prendiamo un insieme di testi, li trasformiamo tutti in embedding e lasciamo che un algoritmo di clustering (come <code class="language-plaintext highlighter-rouge">k-means</code>) scopra automaticamente i gruppi. L’idea è che testi simili finiranno nello stesso cluster, anche senza etichette. È un approccio esplorativo che serve a capire come si organizzano i dati da soli.</p>

<p>Il clustering è un ottimo metodo per scoprire strutture nascoste nei dati.</p>

<p>Proviamo a fare un esempio concreto con 6 frasi e applichiamo il <code class="language-plaintext highlighter-rouge">k-means</code> con due cluster. Ad occhio (sono pochi documenti) è facile distinguere che un cluster riguarda i <strong>luoghi</strong> e l’altro i <strong>servizi digitali</strong>, ma è interessante verificare come vengono suddivisi.</p>

<h4 id="codice_4">Codice_4</h4>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">sentence_transformers</span> <span class="kn">import</span> <span class="n">SentenceTransformer</span>
<span class="kn">from</span> <span class="nn">sklearn.cluster</span> <span class="kn">import</span> <span class="n">KMeans</span>

<span class="c1"># Carica EmbeddingGemma
</span><span class="n">model</span> <span class="o">=</span> <span class="n">SentenceTransformer</span><span class="p">(</span><span class="s">"google/embeddinggemma-300m"</span><span class="p">)</span>

<span class="c1"># Dataset di frasi di esempio
</span><span class="n">sentences</span> <span class="o">=</span> <span class="p">[</span>
    <span class="s">"Il Colosseo si trova a Roma"</span><span class="p">,</span>
    <span class="s">"La Torre Eiffel è a Parigi"</span><span class="p">,</span>
    <span class="s">"Il Vesuvio è un vulcano vicino a Napoli"</span><span class="p">,</span>
    <span class="s">"Lo SPID permette di accedere ai servizi online della Pubblica Amministrazione"</span><span class="p">,</span>
    <span class="s">"La Carta d’Identità Elettronica può essere usata per l’accesso ai servizi digitali"</span><span class="p">,</span>
    <span class="s">"Ho dimenticato la password del mio account SPID"</span>
<span class="p">]</span>

<span class="c1"># Calcola gli embedding
</span><span class="n">embeddings</span> <span class="o">=</span> <span class="n">model</span><span class="p">.</span><span class="n">encode</span><span class="p">(</span><span class="n">sentences</span><span class="p">,</span> <span class="n">normalize_embeddings</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

<span class="c1"># Applica k-means con 2 cluster (luoghi e servizi digitali)
</span><span class="n">num_clusters</span> <span class="o">=</span> <span class="mi">2</span>
<span class="n">clustering_model</span> <span class="o">=</span> <span class="n">KMeans</span><span class="p">(</span><span class="n">n_clusters</span><span class="o">=</span><span class="n">num_clusters</span><span class="p">,</span> <span class="n">random_state</span><span class="o">=</span><span class="mi">42</span><span class="p">)</span>
<span class="n">clustering_model</span><span class="p">.</span><span class="n">fit</span><span class="p">(</span><span class="n">embeddings</span><span class="p">)</span>
<span class="n">cluster_assignment</span> <span class="o">=</span> <span class="n">clustering_model</span><span class="p">.</span><span class="n">labels_</span>

<span class="c1"># Stampa i risultati
</span><span class="n">clusters</span> <span class="o">=</span> <span class="p">[[]</span> <span class="k">for</span> <span class="n">i</span> <span class="ow">in</span> <span class="nb">range</span><span class="p">(</span><span class="n">num_clusters</span><span class="p">)]</span>
<span class="k">for</span> <span class="n">sentence_id</span><span class="p">,</span> <span class="n">cluster_id</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">cluster_assignment</span><span class="p">):</span>
    <span class="n">clusters</span><span class="p">[</span><span class="n">cluster_id</span><span class="p">].</span><span class="n">append</span><span class="p">(</span><span class="n">sentences</span><span class="p">[</span><span class="n">sentence_id</span><span class="p">])</span>

<span class="k">for</span> <span class="n">i</span><span class="p">,</span> <span class="n">cluster</span> <span class="ow">in</span> <span class="nb">enumerate</span><span class="p">(</span><span class="n">clusters</span><span class="p">):</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="se">\n</span><span class="s">Cluster </span><span class="si">{</span><span class="n">i</span><span class="o">+</span><span class="mi">1</span><span class="si">}</span><span class="s">:"</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">sentence</span> <span class="ow">in</span> <span class="n">cluster</span><span class="p">:</span>
        <span class="k">print</span><span class="p">(</span><span class="s">" -"</span><span class="p">,</span> <span class="n">sentence</span><span class="p">)</span>
</code></pre></div></div>

<p>Il raggruppamento per somiglianza ha prodotto i due gruppi di testi vicini nello spazio semantico, ma senza applicare etichette.</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Cluster 1:
 - Il Colosseo si trova a Roma
 - La Torre Eiffel è a Parigi
 - Il Vesuvio è un vulcano vicino a Napoli

Cluster 2:
 - Lo SPID permette di accedere ai servizi online della Pubblica Amministrazione
 - La Carta d’Identità Elettronica può essere usata per l’accesso ai servizi digitali
 - Ho dimenticato la password del mio account SPID
</code></pre></div></div>

<p>Possiamo pensare al clustering con <code class="language-plaintext highlighter-rouge">k-means</code> come a una forma di <strong>auto-classificazione</strong>. È un pò come prendere una scatola piena di documenti e chiedere al modello di dividerli in pile in base a quello che gli sembra più simile, senza dirgli prima quali etichette usare. Magari una pila conterrà documenti che parlano di riunioni, un’altra quelli che parlano di conti bancari. Non è detto che i gruppi coincidano sempre con le categorie che avevamo in mente, per questo spesso rivelano strutture interessanti nei dati.</p>

<h3 id="rag-retrieval-augmented-generation">RAG (Retrieval-Augmented Generation)</h3>

<p>Il RAG è il punto d’incontro tra due mondi: da un lato gli <strong>embedding</strong>, che servono a cercare e recuperare i testi più rilevanti, e dall’altro gli <strong>LLM</strong>, che hanno la capacità di generare risposte articolate in linguaggio naturale.</p>

<p>Se prima abbiamo visto la <strong>ricerca semantica</strong>, utile per trovare il documento più vicino a una query, la <strong>classificazione</strong>, dove insegniamo al modello a distinguere testi etichettati, e il <strong>clustering</strong>, che invece raggruppa automaticamente testi simili, con il <strong>RAG</strong> facciamo un passo in più. Qui gli embedding ci aiutano a recuperare le informazioni giuste e poi lasciamo che sia l’LLM a costruire una risposta finale chiara e leggibile per l’utente.</p>

<p>Vediamo un esempio concreto con <code class="language-plaintext highlighter-rouge">transformers</code> e <code class="language-plaintext highlighter-rouge">Gemma-2b-it</code> come LLM open source.</p>
<h4 id="codice_5">Codice_5</h4>
<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">from</span> <span class="nn">sentence_transformers</span> <span class="kn">import</span> <span class="n">SentenceTransformer</span><span class="p">,</span> <span class="n">util</span>
<span class="kn">from</span> <span class="nn">transformers</span> <span class="kn">import</span> <span class="n">pipeline</span>

<span class="c1"># 1. Embedding model per retrieval
</span><span class="n">embedder</span> <span class="o">=</span> <span class="n">SentenceTransformer</span><span class="p">(</span><span class="s">"google/embeddinggemma-300m"</span><span class="p">)</span>

<span class="c1"># Documenti
</span><span class="n">documents</span> <span class="o">=</span> <span class="p">[</span>
    <span class="s">"Lo SPID è un sistema di autenticazione che consente ai cittadini italiani di accedere ai servizi online della Pubblica Amministrazione."</span><span class="p">,</span>
    <span class="s">"La Carta d’Identità Elettronica (CIE) può essere usata per l’accesso ai servizi digitali."</span><span class="p">,</span>
    <span class="s">"Il Colosseo è un antico anfiteatro romano situato a Roma."</span>
<span class="p">]</span>

<span class="c1"># Query
</span><span class="n">query</span> <span class="o">=</span> <span class="s">"Qual è il sistema che permette di accedere ai servizi online della Pubblica Amministrazione?"</span>

<span class="c1"># 2. Retrieval
</span><span class="n">doc_embeddings</span> <span class="o">=</span> <span class="n">embedder</span><span class="p">.</span><span class="n">encode</span><span class="p">(</span><span class="n">documents</span><span class="p">,</span> <span class="n">convert_to_tensor</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>
<span class="n">query_embedding</span> <span class="o">=</span> <span class="n">embedder</span><span class="p">.</span><span class="n">encode</span><span class="p">(</span><span class="n">query</span><span class="p">,</span> <span class="n">convert_to_tensor</span><span class="o">=</span><span class="bp">True</span><span class="p">)</span>

<span class="n">cosine_scores</span> <span class="o">=</span> <span class="n">util</span><span class="p">.</span><span class="n">cos_sim</span><span class="p">(</span><span class="n">query_embedding</span><span class="p">,</span> <span class="n">doc_embeddings</span><span class="p">)</span>
<span class="n">best_idx</span> <span class="o">=</span> <span class="n">cosine_scores</span><span class="p">.</span><span class="n">argmax</span><span class="p">().</span><span class="n">item</span><span class="p">()</span>
<span class="n">retrieved_doc</span> <span class="o">=</span> <span class="n">documents</span><span class="p">[</span><span class="n">best_idx</span><span class="p">]</span>

<span class="k">print</span><span class="p">(</span><span class="s">"Documento più rilevante:"</span><span class="p">,</span> <span class="n">retrieved_doc</span><span class="p">)</span>

<span class="c1"># 3. Passiamo la query + documento a un LLM
</span><span class="n">generator</span> <span class="o">=</span> <span class="n">pipeline</span><span class="p">(</span><span class="s">"text-generation"</span><span class="p">,</span> <span class="n">model</span><span class="o">=</span><span class="s">"google/gemma-2b-it"</span><span class="p">)</span>

<span class="n">prompt</span> <span class="o">=</span> <span class="sa">f</span><span class="s">"Domanda: </span><span class="si">{</span><span class="n">query</span><span class="si">}</span><span class="se">\n\n</span><span class="s">Contesto: </span><span class="si">{</span><span class="n">retrieved_doc</span><span class="si">}</span><span class="se">\n\n</span><span class="s">Risposta:"</span>
<span class="n">output</span> <span class="o">=</span> <span class="n">generator</span><span class="p">(</span><span class="n">prompt</span><span class="p">,</span> <span class="n">max_new_tokens</span><span class="o">=</span><span class="mi">100</span><span class="p">)[</span><span class="mi">0</span><span class="p">][</span><span class="s">"generated_text"</span><span class="p">]</span>

<span class="k">print</span><span class="p">(</span><span class="s">"</span><span class="se">\n</span><span class="s">Risposta generata:</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">output</span><span class="p">)</span>
</code></pre></div></div>

<p>Vediamo il risultato ottenuto:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Documento più rilevante: Lo SPID è un sistema di autenticazione che consente ai cittadini italiani di accedere ai servizi online della Pubblica Amministrazione.

Risposta generata:
 Domanda: Qual è il sistema che permette di accedere ai servizi online della Pubblica Amministrazione?

Contesto: Lo SPID è un sistema di autenticazione che consente ai cittadini italiani di accedere ai servizi online della Pubblica Amministrazione.

Risposta: Il sistema SPID è il sistema di autenticazione per i servizi online della Pubblica Amministrazione.
</code></pre></div></div>

<p>In questo caso, EmbeddingGemma ha fatto bene il suo lavoro di <strong>retrieval</strong>, ma ha riportato <strong>solo il documento più vicino</strong>, quello sullo SPID. L’LLM ha quindi costruito la risposta basandosi su quel contesto, <strong>ignorando la CIE</strong> che era comunque presente negli altri documenti. Questo succede perchè nel nostro esempio abbiamo utilizzato solo tre frasi come documenti. Nei sistemi RAG reali non si passa solo <strong>1 documento</strong>, ma un piccolo set, potrebbero essere i primi 3 più rilevanti, così l’LLM ha più materiale per generare una risposta completa.</p>

<h2 id="conclusione">Conclusione</h2>

<p>Dal punto di vista pratico, EmbeddingGemma è molto leggero: ha circa 300 milioni di parametri, occupa meno di 200 MB di RAM e accetta input fino a 2048 token. L’output è un embedding di 768 dimensioni, che può essere ridotto nel caso si voglia ottimizzare memoria e velocità.</p>

<p>Grazie alle sue dimensioni contenute, EmbeddingGemma può essere usato anche su dispositivi modesti, senza bisogno di GPU dedicate. Questo lo rende ideale per sperimentazioni locali, per applicazioni leggere o per scenari edge, dove non è pratico affidarsi al cloud. In altre parole, è un modello accessibile a chiunque voglia lavorare con gli embedding senza infrastrutture costose.</p>

<hr />
<h2 id="note-su-embeddinggemma">Note su EmbeddingGemma</h2>

<p><a href="https://deepmind.google/models/gemma/embeddinggemma/"><strong>EmbeddingGemma</strong></a> è un modello di embedding sviluppato da Google DeepMind ed è open-source. Nasce sulla base dell’architettura Gemma 3, la stessa famiglia di modelli LLM rilasciata da Google, ma è stato adattato appositamente per fare embedding e non generazione. Prima di essere addestrato sugli embedding, i suoi pesi sono stati inizializzati con quelli di <a href="https://deepmind.google/models/gemma/t5gemma/"><em>T5Gemma</em></a>, così da partire già con una solida comprensione linguistica (grammatica, semantica, multilinguismo) e specializzarsi poi nell’unico compito di rappresentare i testi come vettori numerici.</p>]]></content><author><name>guelfoweb</name></author><category term="ai" /><category term="ita" /><category term="embedding" /><category term="semantic" /><category term="classification" /><category term="clustering" /><category term="rag" /><summary type="html"><![CDATA[Si parla molto di LLM, i cosiddetti Large Language Models come ChatGPT, Gemini o Llama, modelli che sanno scrivere testi, rispondere a domande, riassumere documenti. Insomma addestrati per generare linguaggio.]]></summary></entry><entry><title type="html">Weights manipulation</title><link href="/ai/projects/2025/08/24/weights-manipulation.html" rel="alternate" type="text/html" title="Weights manipulation" /><published>2025-08-24T00:00:00+00:00</published><updated>2025-08-24T00:00:00+00:00</updated><id>/ai/projects/2025/08/24/weights-manipulation</id><content type="html" xml:base="/ai/projects/2025/08/24/weights-manipulation.html"><![CDATA[<p>Some time ago I asked myself: do we really need many days of calculation and powerful GPUs to understand how an open weights language model manages its safety mechanisms? More important, is there a fast and reversible way that does not need the creation of abliterated models to make the model more obedient to specific requests?</p>

<p>From that question I started a research that was not easy. Making the code work with different models took time, because of many adjustments to fix library problems and memory limits.</p>

<p>After I got a working version (not completely stable), I tried a different approach: change in real time the embedding weights of specific tokens, reducing step by step the ones linked to refusal (sorry, cannot, dangerous) and increasing the ones linked to compliance (sure, help, explain).</p>

<p>With some models this worked well: small changes to refusal tokens slowly weakened the safety mechanisms, with the changes applied directly in RAM while the model was running. This way, the language style of the original model is kept, because the weights on disk stay the same, and the changes are fully reversible by reloading the model. This method takes minutes of tests instead of the many hours needed for abliteration.</p>

<p>But with some newer models the challenge is different. It is not enough to change only the embeddings, because the safety strategies are deep in the architecture, making the system much more resistant.</p>

<p>The study showed an important change in architecture: older models often put safety in the token embeddings, while modern ones spread it across the full neural network.</p>

<p>I documented on GitHub a real use case and the steps I used to change the weights of an older open model, with more details about both the advantages and the limits.</p>

<p>Link: <a href="https://github.com/guelfoweb/weights-manipulation">weights-manipulation</a></p>]]></content><author><name>guelfoweb</name></author><category term="ai" /><category term="projects" /><category term="weights" /><category term="abliteration" /><summary type="html"><![CDATA[Some time ago I asked myself: do we really need many days of calculation and powerful GPUs to understand how an open weights language model manages its safety mechanisms? More important, is there a fast and reversible way that does not need the creation of abliterated models to make the model more obedient to specific requests?]]></summary></entry><entry><title type="html">Sentenza</title><link href="/ai/projects/2025/04/23/sentenza.html" rel="alternate" type="text/html" title="Sentenza" /><published>2025-04-23T00:00:00+00:00</published><updated>2025-04-23T00:00:00+00:00</updated><id>/ai/projects/2025/04/23/sentenza</id><content type="html" xml:base="/ai/projects/2025/04/23/sentenza.html"><![CDATA[<p>The division of texts into chunks is an important step to build a good vector database. When embeddings are created for RAG systems, the size and meaning of the segments directly affect the accuracy and relevance of search results. Chunks that are too short break the content too much, while chunks that are too long risk joining unrelated information, making queries less effective.</p>

<p>The whole process depends on the tokenizer used. Finding the right sentence boundaries is important to apply good chunking strategies.</p>

<p><img src="https://github.com/user-attachments/assets/69535092-dc03-4bfe-aac1-a53f07b94aea" alt="Image" /></p>

<p>The histogram in the figure, made from a literary text, shows the distribution of 1011 sentences, with an average length of 118.48 characters and a standard deviation of 94.49. This shows that the sentence lengths in the corpus are very different.</p>

<p>A limit of the current method comes from the asymmetric distribution of sentence lengths, with a long tail on the right (see graph). This means that one single chunk_size value may not be good for the whole corpus, and very long sentences may need special treatment.</p>

<p>The graph also shows that most sentences are under 200 characters, with many between 50 and 150.</p>

<p>The purple line shows the average, while the dotted lines show the standard deviation (+212.97 and -23.99), giving a quick view of the variability in the corpus.</p>

<p>Finally, two segmentation parameters were calculated: a chunk size of 401 characters (red line) and an overlap of 141 (green line), for a total of about 461 chunks. The colored areas – green for overlap and pink for chunk size – make it easy to see how these values fit with the real sentences.</p>

<p>The library <code class="language-plaintext highlighter-rouge">sentenza</code>, written during these holidays to help me with text analysis and used to make this graph, is still experimental, but it can give useful support to improve the text chunking process.</p>

<p>Link: <a href="https://github.com/guelfoweb/sentenza">sentenza</a></p>]]></content><author><name>guelfoweb</name></author><category term="ai" /><category term="projects" /><category term="rag" /><category term="chunks" /><category term="vector db" /><summary type="html"><![CDATA[The division of texts into chunks is an important step to build a good vector database. When embeddings are created for RAG systems, the size and meaning of the segments directly affect the accuracy and relevance of search results. Chunks that are too short break the content too much, while chunks that are too long risk joining unrelated information, making queries less effective.]]></summary></entry><entry><title type="html">Hello World</title><link href="/blog/2025/03/20/hello-world.html" rel="alternate" type="text/html" title="Hello World" /><published>2025-03-20T00:00:00+00:00</published><updated>2025-03-20T00:00:00+00:00</updated><id>/blog/2025/03/20/hello-world</id><content type="html" xml:base="/blog/2025/03/20/hello-world.html"><![CDATA[<p>This is my first post on <strong>GitHub Pages</strong>.</p>]]></content><author><name>guelfoweb</name></author><category term="blog" /><category term="personal" /><summary type="html"><![CDATA[This is my first post on GitHub Pages.]]></summary></entry></feed>