<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Rlhf on lo0 — Blog Técnico</title><link>https://blog.lo0.es/tags/rlhf/</link><description>Recent content in Rlhf on lo0 — Blog Técnico</description><generator>Hugo -- gohugo.io</generator><language>es</language><lastBuildDate>Wed, 27 May 2026 08:00:00 +0200</lastBuildDate><atom:link href="https://blog.lo0.es/tags/rlhf/index.xml" rel="self" type="application/rss+xml"/><item><title>Alignment moderno: DPO, KTO, ORPO y SimPO — el sumiller que aprende sin recibir reward model</title><link>https://blog.lo0.es/posts/alignment-moderno-dpo-kto-orpo-simpo/</link><pubDate>Wed, 27 May 2026 08:00:00 +0200</pubDate><guid>https://blog.lo0.es/posts/alignment-moderno-dpo-kto-orpo-simpo/</guid><description>&lt;blockquote>
&lt;p>Este post es la &lt;strong>continuación natural&lt;/strong> del &lt;a href="https://blog.lo0.es/posts/fine-tuning-continuo-produccion/">fine-tuning continuo en producción&lt;/a>, que cubre el ciclo operativo (Postgres + queries SQL + hot-swap) que alimenta a estos métodos con datos reales. Aquí entramos al &lt;strong>dentro&lt;/strong> de cada uno: qué optimiza cada loss, qué hipótesis hace, y por qué eligen una u otra.&lt;/p>
&lt;/blockquote>
&lt;h2 id="tldr">TL;DR&lt;/h2>
&lt;p>RLHF clásico —el de los papers de InstructGPT— está prácticamente extinto en producción. El motivo no es ideológico: es que en 2023 Rafailov y otros demostraron que el reward model &lt;strong>no tiene por qué existir&lt;/strong> como objeto separado. Con un cambio de variable elegante, la política óptima se puede entrenar directamente desde pares de preferencia, sin pasar por reward y sin RL. Eso es &lt;strong>DPO&lt;/strong>. Desde ahí la familia se ha ramificado en cuatro métodos que conviven en 2026: &lt;strong>DPO&lt;/strong> cuando tienes pares &lt;code>(chosen, rejected)&lt;/code>, &lt;strong>KTO&lt;/strong> cuando sólo tienes señal binaria 👍/👎, &lt;strong>ORPO&lt;/strong> cuando quieres SFT y preferencias en la misma pasada para ahorrar memoria, y &lt;strong>SimPO&lt;/strong> cuando quieres eliminar también el modelo de referencia y normalizar por longitud. Este post explica qué hace exactamente cada loss, lo prueba con un ejemplo numérico end-to-end, da la tabla de decisión real entre los cuatro y desmonta los tres sesgos que matan el método en producción cuando nadie los vigila.&lt;/p>
&lt;h2 id="estás-aquí-tune">Estás aquí: TUNE&lt;/h2>
&lt;div class="diagram" style="max-width:780px;margin:1rem auto;">
&lt;svg viewBox="0 0 780 90" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="estás aquí: Tune">
&lt;style>.box{stroke:#444;stroke-width:1.4;rx:6}.active{fill:#ff8a4c;stroke-width:3}.idle{fill:#f4f4f4}.lbl{font:600 12px sans-serif;fill:#222}.arr{stroke:#666;stroke-width:1.4;fill:none;marker-end:url(#alm)}.cyc{stroke:#888;stroke-width:1.2;fill:none;stroke-dasharray:4 2;marker-end:url(#alm)}&lt;/style>
&lt;defs>&lt;marker id="alm" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>&lt;/defs>
&lt;text x="390" y="20" text-anchor="middle" class="lbl">Estás aquí: TUNE · preference optimization sin reward model&lt;/text>
&lt;rect x="30" y="35" width="110" height="35" class="box idle"/>&lt;text x="85" y="58" text-anchor="middle" class="lbl">1 · Data&lt;/text>
&lt;rect x="155" y="35" width="110" height="35" class="box active"/>&lt;text x="210" y="58" text-anchor="middle" class="lbl">2 · Tune&lt;/text>
&lt;rect x="280" y="35" width="110" height="35" class="box idle"/>&lt;text x="335" y="58" text-anchor="middle" class="lbl">3 · Eval&lt;/text>
&lt;rect x="405" y="35" width="110" height="35" class="box idle"/>&lt;text x="460" y="58" text-anchor="middle" class="lbl">4 · Deploy&lt;/text>
&lt;rect x="530" y="35" width="110" height="35" class="box idle"/>&lt;text x="585" y="58" text-anchor="middle" class="lbl">5 · Observe&lt;/text>
&lt;rect x="655" y="35" width="110" height="35" class="box idle"/>&lt;text x="710" y="58" text-anchor="middle" class="lbl">6 · Retrain&lt;/text>
&lt;path class="arr" d="M140,52 L155,52"/>&lt;path class="arr" d="M265,52 L280,52"/>&lt;path class="arr" d="M390,52 L405,52"/>&lt;path class="arr" d="M515,52 L530,52"/>&lt;path class="arr" d="M640,52 L655,52"/>
&lt;path class="cyc" d="M710,72 L710,82 L85,82 L85,72"/>
&lt;/svg>
&lt;/div>
&lt;p>El &lt;a href="https://blog.lo0.es/posts/pipeline-llmops-seis-etapas/">pipeline LLMOps de seis etapas&lt;/a> sitúa Tune entre Data y Eval. Dentro de Tune conviven tres modalidades: SFT (supervised fine-tuning), preference optimization (lo que cubre este post) y agent training / RFT. Lo que sigue es el &lt;strong>dentro&lt;/strong> de la segunda modalidad.&lt;/p>
&lt;h2 id="la-analogía-el-sumiller-que-entrena-el-paladar-sin-libro-de-teoría">La analogía: el sumiller que entrena el paladar sin libro de teoría&lt;/h2>
&lt;p>Imagina que quieres formar a un sumiller. Tienes dos caminos. El primero es &lt;strong>enseñarle teoría enológica&lt;/strong>: variedades de uva, terruños, métodos de vinificación, tipos de barrica. Le das tarjetas con descripciones canónicas de vinos y le pides que las memorice. Eso es &lt;strong>SFT&lt;/strong>: pares &lt;code>(prompt, respuesta ideal)&lt;/code>. Funciona como educación general, pero el sumiller que sale de ahí no distingue dos riojas excelentes entre sí.&lt;/p>
&lt;p>El segundo camino es &lt;strong>el comparador ciego&lt;/strong>. Le pones dos copas idénticas opacas. Le dices &amp;ldquo;este es mejor que este&amp;rdquo;. No le explicas por qué. Repites el ejercicio mil veces, con mil pares distintos. Al cabo de un tiempo, el sumiller no necesita que se lo digas: tiene &lt;strong>el paladar formado&lt;/strong>. No ha aprendido teoría enológica, ha aprendido a &lt;strong>discriminar&lt;/strong>.&lt;/p>
&lt;p>DPO, KTO, ORPO y SimPO son cuatro variantes del segundo camino. Las cuatro entrenan al modelo a discriminar, no a memorizar. Las diferencias entre ellas son:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>DPO&lt;/strong>: dos copas en cada catación, el sumiller siempre sabe cuál es la &amp;ldquo;buena&amp;rdquo; y cuál es la &amp;ldquo;mala&amp;rdquo;.&lt;/li>
&lt;li>&lt;strong>KTO&lt;/strong>: una sola copa cada vez, sólo se le dice &amp;ldquo;esto te gustaría&amp;rdquo; o &amp;ldquo;esto no te gustaría&amp;rdquo;. Sin pares.&lt;/li>
&lt;li>&lt;strong>ORPO&lt;/strong>: la catación incluye también una pequeña clase teórica embebida (SFT en paralelo) para no olvidar el catálogo.&lt;/li>
&lt;li>&lt;strong>SimPO&lt;/strong>: como DPO pero el sumiller no compara contra &amp;ldquo;lo que tu maestro habría dicho&amp;rdquo; (modelo de referencia); compara directamente las dos copas, normalizando por la cantidad de líquido en cada una.&lt;/li>
&lt;/ul>
&lt;p>La analogía no es decorativa: el resto del post es esa misma idea expresada matemáticamente.&lt;/p>
&lt;h2 id="por-qué-existe-dpo-el-truco-de-rafailov">Por qué existe DPO: el truco de Rafailov&lt;/h2>
&lt;p>Para entender DPO conviene &lt;strong>recorrer en treinta segundos&lt;/strong> lo que reemplaza. RLHF clásico, el de InstructGPT, tiene tres fases:&lt;/p>
&lt;div class="diagram" style="max-width:760px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 760 280" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="RLHF clásico vs DPO">
&lt;style>
.bx{fill:#f8f8f8;stroke:#444;stroke-width:1.4;rx:8}
.bxa{fill:#ffe6d6;stroke:#444;stroke-width:1.4;rx:8}
.bxb{fill:#d6eaff;stroke:#444;stroke-width:1.4;rx:8}
.bxc{fill:#d9f5d6;stroke:#444;stroke-width:1.4;rx:8}
.bxx{fill:#fff5b0;stroke:#444;stroke-width:1.4;rx:8}
.lt{font:600 13px sans-serif;fill:#222}
.ls{font:400 11px sans-serif;fill:#555}
.h{font:700 13px sans-serif;fill:#222}
.ar{stroke:#666;stroke-width:1.5;fill:none;marker-end:url(#mrl)}
.aw{stroke:#c0392b;stroke-width:1.8;fill:none;marker-end:url(#mrlx)}
&lt;/style>
&lt;defs>
&lt;marker id="mrl" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#666"/>&lt;/marker>
&lt;marker id="mrlx" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="6" markerHeight="6" orient="auto">&lt;path d="M0,0 L10,5 L0,10 z" fill="#c0392b"/>&lt;/marker>
&lt;/defs>
&lt;text x="200" y="20" text-anchor="middle" class="h">RLHF clásico (3 fases, 3 modelos)&lt;/text>
&lt;rect x="40" y="35" width="160" height="55" class="bxa"/>
&lt;text x="120" y="56" text-anchor="middle" class="lt">SFT&lt;/text>
&lt;text x="120" y="74" text-anchor="middle" class="ls">π_ref ← (prompt, respuesta)&lt;/text>
&lt;rect x="40" y="110" width="160" height="55" class="bxb"/>
&lt;text x="120" y="131" text-anchor="middle" class="lt">Reward Model&lt;/text>
&lt;text x="120" y="149" text-anchor="middle" class="ls">r_φ ← (prompt, chosen, rejected)&lt;/text>
&lt;rect x="40" y="185" width="160" height="55" class="bxc"/>
&lt;text x="120" y="206" text-anchor="middle" class="lt">PPO (RL)&lt;/text>
&lt;text x="120" y="224" text-anchor="middle" class="ls">π_θ maximiza r_φ con KL a π_ref&lt;/text>
&lt;path class="ar" d="M120,90 L120,110"/>
&lt;path class="ar" d="M120,165 L120,185"/>
&lt;text x="220" y="135" class="ls">3 modelos en memoria&lt;/text>
&lt;text x="220" y="150" class="ls">+ rollouts on-policy&lt;/text>
&lt;text x="220" y="165" class="ls">+ inestable, hard to debug&lt;/text>
&lt;text x="560" y="20" text-anchor="middle" class="h">DPO (1 fase, 2 modelos)&lt;/text>
&lt;rect x="400" y="35" width="160" height="55" class="bxa"/>
&lt;text x="480" y="56" text-anchor="middle" class="lt">SFT (igual)&lt;/text>
&lt;text x="480" y="74" text-anchor="middle" class="ls">π_ref ← (prompt, respuesta)&lt;/text>
&lt;rect x="400" y="125" width="160" height="80" class="bxx"/>
&lt;text x="480" y="146" text-anchor="middle" class="lt">DPO&lt;/text>
&lt;text x="480" y="164" text-anchor="middle" class="ls">π_θ ← (chosen, rejected)&lt;/text>
&lt;text x="480" y="180" text-anchor="middle" class="ls">loss cerrada en log-probs&lt;/text>
&lt;text x="480" y="196" text-anchor="middle" class="ls">sin RL, sin reward explícito&lt;/text>
&lt;path class="ar" d="M480,90 L480,125"/>
&lt;text x="580" y="220" class="ls" font-style="italic">"el reward está implícito&lt;/text>
&lt;text x="580" y="236" class="ls" font-style="italic">en log π_θ - log π_ref"&lt;/text>
&lt;path class="aw" d="M260,140 L390,140"/>
&lt;text x="325" y="130" text-anchor="middle" class="ls" fill="#c0392b">Rafailov 2023:&lt;/text>
&lt;text x="325" y="158" text-anchor="middle" class="ls" fill="#c0392b">"sólo necesitas π_θ y π_ref"&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>&lt;strong>Fase 1 – SFT.&lt;/strong> Entrenas el modelo sobre &lt;code>(prompt, respuesta ideal)&lt;/code>. Sale &lt;code>π_ref&lt;/code>: la política de referencia. Es el modelo &amp;ldquo;educado&amp;rdquo;.&lt;/p>
&lt;p>&lt;strong>Fase 2 – Reward model.&lt;/strong> Sobre el mismo modelo (otra cabeza), entrenas un &lt;strong>regresor&lt;/strong>: dado &lt;code>(prompt, chosen, rejected)&lt;/code>, aprende a dar score más alto al &lt;code>chosen&lt;/code>. Sale &lt;code>r_φ(x, y)&lt;/code>.&lt;/p>
&lt;p>&lt;strong>Fase 3 – PPO.&lt;/strong> Tomas &lt;code>π_ref&lt;/code> como punto de partida y entrenas otra copia &lt;code>π_θ&lt;/code> para que &lt;strong>maximice &lt;code>r_φ(x, π_θ(x))&lt;/code>&lt;/strong> con una penalización KL para no alejarse demasiado de &lt;code>π_ref&lt;/code>. Eso requiere generar rollouts (decode on-policy a cada paso), tener los tres modelos en memoria (&lt;code>π_θ&lt;/code>, &lt;code>π_ref&lt;/code>, &lt;code>r_φ&lt;/code>), y un setup de RL clásico —inestable, sensible a hiperparámetros y famoso por sus problemas de reproducibilidad—.&lt;/p>
&lt;p>La observación de Rafailov fue: &lt;strong>la fase 3 tiene solución cerrada&lt;/strong>. Si planteas el problema de optimización exacto que resuelve PPO&lt;/p>
&lt;p>$$\max_{\pi_\theta} ; \mathbb{E}&lt;em>{x \sim D, , y \sim \pi&lt;/em>\theta(\cdot|x)} \big[ r_\phi(x,y) \big] - \beta , \mathrm{KL}\big(\pi_\theta(\cdot|x) ,|, \pi_\mathrm{ref}(\cdot|x)\big)$$&lt;/p>
&lt;p>resulta que la política óptima tiene la forma&lt;/p>
&lt;p>$$\pi^{*}(y|x) = \frac{1}{Z(x)} , \pi_\mathrm{ref}(y|x) , \exp!\left( \tfrac{1}{\beta} r_\phi(x,y) \right)$$&lt;/p>
&lt;p>y de ahí se despeja el reward implícito:&lt;/p>
&lt;p>$$r_\phi(x,y) = \beta \log \frac{\pi^{*}(y|x)}{\pi_\mathrm{ref}(y|x)} + \beta \log Z(x).$$&lt;/p>
&lt;p>El segundo término es una función sólo de &lt;code>x&lt;/code> y se cancela cuando comparas dos respuestas al mismo prompt. &lt;strong>El reward no necesita aprenderse&lt;/strong>: está implícito en la ratio de log-probs entre el modelo entrenado y el de referencia. Si plugueas eso en el modelo de Bradley-Terry para preferencias (la fórmula que dice &amp;ldquo;la probabilidad de que &lt;code>yw&lt;/code> sea preferido a &lt;code>yl&lt;/code> es &lt;code>σ(r(x,yw) - r(x,yl))&lt;/code>&amp;rdquo;), sale la &lt;strong>loss de DPO&lt;/strong>:&lt;/p>
&lt;p>$$\mathcal{L}&lt;em>\mathrm{DPO}(\pi&lt;/em>\theta;\pi_\mathrm{ref}) = -,\mathbb{E}&lt;em>{(x, y_w, y_l) \sim D} \log \sigma!\Big( \beta \big[ \log \tfrac{\pi&lt;/em>\theta(y_w|x)}{\pi_\mathrm{ref}(y_w|x)} - \log \tfrac{\pi_\theta(y_l|x)}{\pi_\mathrm{ref}(y_l|x)} \big] \Big).$$&lt;/p>
&lt;p>Eso es DPO entero. &lt;strong>No hay reward model, no hay RL, no hay rollouts.&lt;/strong> Sólo log-probs sobre datos estáticos.&lt;/p>
&lt;h2 id="dpo-con-números-reales">DPO con números reales&lt;/h2>
&lt;p>La fórmula intimida menos cuando se evalúa con un ejemplo. Imagina un par del dataset:&lt;/p>
&lt;ul>
&lt;li>&lt;code>x&lt;/code> = &amp;ldquo;Explica qué es un KVM switch&amp;rdquo;.&lt;/li>
&lt;li>&lt;code>y_w&lt;/code> = respuesta correcta (chosen).&lt;/li>
&lt;li>&lt;code>y_l&lt;/code> = respuesta confusa (rejected).&lt;/li>
&lt;/ul>
&lt;p>Después de un forward pass tenemos cuatro números (sumas de log-probs por token, signo negativo porque cada &lt;code>log p_token ≤ 0&lt;/code>):&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Cantidad&lt;/th>
&lt;th>Valor&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>&lt;code>log π_θ(y_w | x)&lt;/code>&lt;/td>
&lt;td>−45.2&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>log π_θ(y_l | x)&lt;/code>&lt;/td>
&lt;td>−52.1&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>log π_ref(y_w | x)&lt;/code>&lt;/td>
&lt;td>−47.3&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>&lt;code>log π_ref(y_l | x)&lt;/code>&lt;/td>
&lt;td>−50.8&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>Las &amp;ldquo;ratios&amp;rdquo; son la mejora del modelo entrenado respecto al de referencia para cada respuesta:&lt;/p>
&lt;ul>
&lt;li>Para el &lt;code>y_w&lt;/code>: &lt;code>−45.2 − (−47.3) = +2.1&lt;/code> → el modelo entrenado le da &lt;strong>más&lt;/strong> probabilidad que el de referencia. Bien.&lt;/li>
&lt;li>Para el &lt;code>y_l&lt;/code>: &lt;code>−52.1 − (−50.8) = −1.3&lt;/code> → el modelo entrenado le da &lt;strong>menos&lt;/strong> probabilidad. También bien.&lt;/li>
&lt;/ul>
&lt;p>Con &lt;code>β = 0.1&lt;/code> (valor típico) el &amp;ldquo;margen&amp;rdquo; interior del logaritmo es:&lt;/p>
&lt;p>$$m = \beta \cdot (2.1 - (-1.3)) = 0.1 \cdot 3.4 = 0.34$$&lt;/p>
&lt;p>Y la loss:&lt;/p>
&lt;p>$$\mathcal{L}_\mathrm{DPO} = -\log \sigma(0.34) = -\log(0.584) \approx 0.538.$$&lt;/p>
&lt;p>La intuición se ve directamente: &lt;strong>cuanto más sube la log-prob del chosen y más baja la del rejected (en relación a &lt;code>π_ref&lt;/code>), más positivo es &lt;code>m&lt;/code>, más alto el sigmoide y más baja la loss&lt;/strong>. Si el margen es negativo (el modelo se equivoca), &lt;code>σ(m) &amp;lt; 0.5&lt;/code> y la loss explota hacia arriba. El gradiente empuja al modelo a aumentar &lt;code>π_θ(y_w|x)&lt;/code> y bajar &lt;code>π_θ(y_l|x)&lt;/code>.&lt;/p>
&lt;p>El papel de &lt;code>β&lt;/code>: es la &lt;strong>temperatura del alineamiento&lt;/strong>. Si &lt;code>β&lt;/code> es pequeño, el modelo se permite alejarse mucho de &lt;code>π_ref&lt;/code>; si es grande, el KL pesa mucho y el modelo casi no se mueve. Valor típico en 2026: &lt;strong>0.05–0.3&lt;/strong>, con &lt;code>0.1&lt;/code> como punto de partida.&lt;/p>
&lt;h2 id="kto-cuando-sólo-tienes-">KTO: cuando sólo tienes 👍/👎&lt;/h2>
&lt;p>DPO necesita &lt;strong>pares&lt;/strong>. En la práctica, eso casi nunca lo da el producto: lo que da el producto es feedback binario (un thumbs up o un thumbs down, una conversión o un abandono). KTO —Kahneman-Tversky Optimization, Ethayarajh et al. 2024— resuelve exactamente ese caso.&lt;/p>
&lt;p>La intuición viene de la &lt;strong>teoría de las perspectivas&lt;/strong> de Kahneman y Tversky: los humanos somos &lt;strong>más sensibles a las pérdidas que a las ganancias&lt;/strong> de la misma magnitud (loss aversion: una pérdida de 100 € duele más que ganar 100 €). KTO traslada eso al loss:&lt;/p>
&lt;p>$$\mathcal{L}&lt;em>\mathrm{KTO}(x, y) =
\begin{cases}
\lambda_d \big[ 1 - \sigma!\big(\beta \cdot ( h&lt;/em>\theta(x,y) - z_0 ) \big) \big] &amp;amp; \text{si } y \text{ es deseable},\
\lambda_u \big[ 1 - \sigma!\big(\beta \cdot ( z_0 - h_\theta(x,y) ) \big) \big] &amp;amp; \text{si } y \text{ es indeseable},
\end{cases}$$&lt;/p>
&lt;p>donde &lt;code>h_θ(x,y) = log(π_θ(y|x) / π_ref(y|x))&lt;/code> es exactamente la misma ratio que aparecía en DPO, &lt;code>z_0&lt;/code> es una estimación de la divergencia KL del batch (sirve como &amp;ldquo;punto neutro&amp;rdquo;) y &lt;code>λ_d&lt;/code>, &lt;code>λ_u&lt;/code> son los pesos de deseable / indeseable.&lt;/p>
&lt;p>El punto crítico: &lt;strong>KTO no necesita pares&lt;/strong>. Cada ejemplo es &lt;code>(prompt, respuesta, label binario)&lt;/code>. Esto encaja con la telemetría real de producto. La regla práctica habitual es &lt;code>λ_u &amp;gt; λ_d&lt;/code> (ej. 1.0 vs 0.33) cuando la base de datos tiene más 👍 que 👎, para que la señal negativa no se diluya.&lt;/p>
&lt;p>KTO funciona especialmente bien en dos escenarios:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>Productos con UX de feedback explícito&lt;/strong> (chatbots con 👍/👎): cada thumbs es un ejemplo KTO directo, no hace falta sintetizar pares.&lt;/li>
&lt;li>&lt;strong>Datasets desbalanceados&lt;/strong> (mucho más 👍 que 👎, o al revés): los pesos &lt;code>λ_d&lt;/code>, &lt;code>λ_u&lt;/code> lo gestionan explícitamente.&lt;/li>
&lt;/ol>
&lt;h2 id="orpo-sft-y-preferencias-en-una-sola-pasada">ORPO: SFT y preferencias en una sola pasada&lt;/h2>
&lt;p>DPO asume que ya has hecho SFT. Entrenas dos fases: primero SFT para conseguir &lt;code>π_ref&lt;/code>, luego DPO sobre &lt;code>π_ref&lt;/code>. Dos pases por los datos, dos optimizaciones, dos modelos en memoria.&lt;/p>
&lt;p>ORPO —Odds Ratio Preference Optimization, Hong et al. 2024— &lt;strong>fusiona ambas fases&lt;/strong>. La loss combina dos términos:&lt;/p>
&lt;p>$$\mathcal{L}&lt;em>\mathrm{ORPO} = \mathcal{L}&lt;/em>\mathrm{SFT}(y_w) + \lambda \cdot \mathcal{L}_\mathrm{OR}(y_w, y_l)$$&lt;/p>
&lt;p>El primer término es el SFT clásico sobre la respuesta chosen (cross-entropy negativa). El segundo es el &lt;strong>odds ratio&lt;/strong> entre chosen y rejected:&lt;/p>
&lt;p>$$\mathcal{L}&lt;em>\mathrm{OR} = -\log \sigma!\Big( \log \tfrac{\mathrm{odds}&lt;/em>\theta(y_w|x)}{\mathrm{odds}&lt;em>\theta(y_l|x)} \Big), \quad \text{con } \mathrm{odds}&lt;/em>\theta(y|x) = \tfrac{P_\theta(y|x)}{1 - P_\theta(y|x)}.$$&lt;/p>
&lt;p>Lo que importa: &lt;strong>no hay &lt;code>π_ref&lt;/code>&lt;/strong>. ORPO entrena un solo modelo, en una sola pasada, sin cargar la política de referencia en memoria. Sobre el papel suena bien y en la práctica funciona: un Llama 3 8B alineado con ORPO sobre 5k pares tarda ~3 horas en 4×H100 y queda en VRAM con QLoRA agresivo en una sola RTX 4090.&lt;/p>
&lt;p>El parámetro &lt;code>λ&lt;/code> es el peso del término preference. Típico: &lt;code>0.1–0.3&lt;/code>. Si &lt;code>λ&lt;/code> es muy alto, el modelo aprende a discriminar pero olvida el SFT (catastrophic forgetting); si es muy bajo, el alignment apenas se nota.&lt;/p>
&lt;h2 id="simpo-de-verdad-necesitas-modelo-de-referencia">SimPO: ¿de verdad necesitas modelo de referencia?&lt;/h2>
&lt;p>SimPO —Simple Preference Optimization, Meng et al. 2024— lleva la pregunta de ORPO un paso más lejos: si ORPO se libera de &lt;code>π_ref&lt;/code> para el SFT+preference combinado, &lt;strong>¿por qué no liberarse de &lt;code>π_ref&lt;/code> también en el caso DPO puro?&lt;/strong>&lt;/p>
&lt;p>La loss de SimPO:&lt;/p>
&lt;p>$$\mathcal{L}&lt;em>\mathrm{SimPO} = -\log \sigma!\Big( \tfrac{\beta}{|y_w|} \log \pi&lt;/em>\theta(y_w|x) - \tfrac{\beta}{|y_l|} \log \pi_\theta(y_l|x) - \gamma \Big).$$&lt;/p>
&lt;p>Dos cambios respecto a DPO:&lt;/p>
&lt;ol>
&lt;li>&lt;strong>No hay &lt;code>π_ref&lt;/code>&lt;/strong>: directamente se comparan log-probs absolutas del modelo entrenado.&lt;/li>
&lt;li>&lt;strong>Length-normalization&lt;/strong>: cada log-prob se divide por la longitud de su respuesta &lt;code>|y|&lt;/code>. Esto es clave porque sin normalizar, las respuestas largas tienden a tener log-prob total más baja (cada token aporta su &lt;code>log p &amp;lt; 0&lt;/code>), creando un sesgo artificial.&lt;/li>
&lt;li>&lt;strong>Margen explícito &lt;code>γ&lt;/code>&lt;/strong>: la loss es baja si la diferencia de log-probs normalizadas supera &lt;code>γ&lt;/code>. Típico: &lt;code>γ = 0.5–1.5&lt;/code>.&lt;/li>
&lt;/ol>
&lt;p>Con &lt;code>β = 2.0&lt;/code>, &lt;code>γ = 1.0&lt;/code>, &lt;code>|y_w| = 120&lt;/code> tokens, &lt;code>|y_l| = 100&lt;/code> tokens y las log-probs anteriores:&lt;/p>
&lt;p>$$m = \tfrac{2.0}{120} \cdot (-45.2) - \tfrac{2.0}{100} \cdot (-52.1) - 1.0 = -0.753 + 1.042 - 1.0 = -0.711.$$&lt;/p>
&lt;p>$$\mathcal{L}_\mathrm{SimPO} = -\log \sigma(-0.711) \approx 1.11.$$&lt;/p>
&lt;p>Loss más alta que DPO en el mismo punto: SimPO tarda más en converger, pero usa &lt;strong>la mitad de memoria&lt;/strong> (un solo modelo en VRAM) y elimina la dependencia de &lt;code>π_ref&lt;/code>. Es la opción dominante cuando la memoria es el cuello de botella.&lt;/p>
&lt;h2 id="tabla-de-decisión-cuál-usar-y-cuándo">Tabla de decisión: cuál usar y cuándo&lt;/h2>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Señal disponible&lt;/th>
&lt;th>Memoria disponible&lt;/th>
&lt;th>SFT previo&lt;/th>
&lt;th>Método recomendado&lt;/th>
&lt;th>Justificación&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>Pares &lt;code>(chosen, rejected)&lt;/code>&lt;/td>
&lt;td>Alta (≥ 80 GB GPU)&lt;/td>
&lt;td>Sí&lt;/td>
&lt;td>&lt;strong>DPO&lt;/strong>&lt;/td>
&lt;td>Baseline más establecido, mejor reproducibilidad&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Pares &lt;code>(chosen, rejected)&lt;/code>&lt;/td>
&lt;td>Baja (24–48 GB GPU)&lt;/td>
&lt;td>Sí&lt;/td>
&lt;td>&lt;strong>SimPO&lt;/strong>&lt;/td>
&lt;td>Elimina &lt;code>π_ref&lt;/code> → ~50 % menos VRAM&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Pares &lt;code>(chosen, rejected)&lt;/code>&lt;/td>
&lt;td>Cualquiera&lt;/td>
&lt;td>No&lt;/td>
&lt;td>&lt;strong>ORPO&lt;/strong>&lt;/td>
&lt;td>SFT y preferencias en una pasada&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Señal binaria 👍/👎 sin pares&lt;/td>
&lt;td>Alta&lt;/td>
&lt;td>Sí&lt;/td>
&lt;td>&lt;strong>KTO&lt;/strong>&lt;/td>
&lt;td>El único método nativo para datos no-emparejados&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Señal binaria + pocos pares&lt;/td>
&lt;td>Alta&lt;/td>
&lt;td>Sí&lt;/td>
&lt;td>&lt;strong>KTO&lt;/strong> con sub-batch DPO&lt;/td>
&lt;td>Combinación documentada en TRL 0.13&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>Trayectorias multistep (tool use)&lt;/td>
&lt;td>Muy alta&lt;/td>
&lt;td>Sí&lt;/td>
&lt;td>&lt;strong>RLHF/RFT puro&lt;/strong>&lt;/td>
&lt;td>Los métodos preference-pair no capturan la dinámica&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>Magnitudes típicas de dataset:&lt;/strong>&lt;/p>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Método&lt;/th>
&lt;th>Mínimo viable&lt;/th>
&lt;th>Sweet spot&lt;/th>
&lt;th>Plateau&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>DPO&lt;/td>
&lt;td>1 000 pares&lt;/td>
&lt;td>5 000–20 000&lt;/td>
&lt;td>&amp;gt; 50 000&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>SimPO&lt;/td>
&lt;td>2 000 pares&lt;/td>
&lt;td>5 000–20 000&lt;/td>
&lt;td>&amp;gt; 50 000&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>ORPO&lt;/td>
&lt;td>3 000 pares (incluye SFT)&lt;/td>
&lt;td>10 000–30 000&lt;/td>
&lt;td>&amp;gt; 80 000&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>KTO&lt;/td>
&lt;td>5 000 ejemplos binarios&lt;/td>
&lt;td>20 000–80 000&lt;/td>
&lt;td>&amp;gt; 200 000&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>KTO necesita típicamente &lt;strong>3-4× más volumen&lt;/strong> que DPO porque la señal binaria es más débil que la comparativa. La compensación es que la señal binaria es la que naturalmente produce un producto en producción.&lt;/p>
&lt;h2 id="los-tres-sesgos-que-rompen-el-método">Los tres sesgos que rompen el método&lt;/h2>
&lt;p>Los cuatro métodos comparten un problema: están entrenando sobre &lt;strong>proxies&lt;/strong> de calidad, no sobre calidad. Esos proxies tienen sesgos sistemáticos que el modelo puede explotar trivialmente.&lt;/p>
&lt;h3 id="length-bias">Length bias&lt;/h3>
&lt;p>Documentado en el paper original de DPO y en literatura posterior. Las respuestas largas tienden a ser preferidas por humanos —probablemente porque parecen &amp;ldquo;más completas&amp;rdquo;—. Si el dataset de pares hereda ese sesgo, el modelo aprende que &lt;strong>alargar la respuesta es lo que se premia&lt;/strong>, no que mejor contenido es lo que se premia. Resultado: tras 2–3 epochs el modelo escupe verborrea.&lt;/p>
&lt;p>Mitigaciones:&lt;/p>
&lt;ul>
&lt;li>&lt;strong>DPO&lt;/strong>: filtrado del dataset eliminando pares donde &lt;code>|y_w| &amp;gt; 1.3 · |y_l|&lt;/code> (regla del 30 %).&lt;/li>
&lt;li>&lt;strong>SimPO&lt;/strong>: la length-normalization en la loss lo arregla por construcción.&lt;/li>
&lt;li>&lt;strong>ORPO / KTO&lt;/strong>: filtrado del dataset o regularización auxiliar de longitud (DPOP, R-DPO).&lt;/li>
&lt;/ul>
&lt;div class="diagram" style="max-width:740px;margin:1.5rem auto;">
&lt;svg viewBox="0 0 740 230" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Length bias durante entrenamiento">
&lt;style>
.ax{stroke:#444;stroke-width:1.4;fill:none}
.gr{stroke:#888;stroke-width:0.5;fill:none;stroke-dasharray:2 2}
.cv1{stroke:#c0392b;stroke-width:2.5;fill:none}
.cv2{stroke:#2980b9;stroke-width:2.5;fill:none}
.lb{font:600 11px sans-serif;fill:#222}
.sm{font:400 10px sans-serif;fill:#555}
.h{font:700 13px sans-serif;fill:#222}
&lt;/style>
&lt;text x="370" y="20" text-anchor="middle" class="h">Length bias: longitud media de respuesta durante el entrenamiento&lt;/text>
&lt;line x1="60" y1="180" x2="700" y2="180" class="ax"/>
&lt;line x1="60" y1="180" x2="60" y2="50" class="ax"/>
&lt;line x1="60" y1="150" x2="700" y2="150" class="gr"/>
&lt;line x1="60" y1="120" x2="700" y2="120" class="gr"/>
&lt;line x1="60" y1="90" x2="700" y2="90" class="gr"/>
&lt;line x1="60" y1="60" x2="700" y2="60" class="gr"/>
&lt;text x="55" y="184" text-anchor="end" class="sm">100&lt;/text>
&lt;text x="55" y="154" text-anchor="end" class="sm">150&lt;/text>
&lt;text x="55" y="124" text-anchor="end" class="sm">200&lt;/text>
&lt;text x="55" y="94" text-anchor="end" class="sm">250&lt;/text>
&lt;text x="55" y="64" text-anchor="end" class="sm">300&lt;/text>
&lt;text x="20" y="115" class="sm" transform="rotate(-90,20,115)">tokens / respuesta&lt;/text>
&lt;text x="370" y="210" text-anchor="middle" class="sm">epoch&lt;/text>
&lt;text x="100" y="200" text-anchor="middle" class="sm">0&lt;/text>
&lt;text x="220" y="200" text-anchor="middle" class="sm">1&lt;/text>
&lt;text x="340" y="200" text-anchor="middle" class="sm">2&lt;/text>
&lt;text x="460" y="200" text-anchor="middle" class="sm">3&lt;/text>
&lt;text x="580" y="200" text-anchor="middle" class="sm">4&lt;/text>
&lt;text x="680" y="200" text-anchor="middle" class="sm">5&lt;/text>
&lt;path class="cv1" d="M100,150 C160,140 200,120 220,110 C280,80 340,65 460,55 C540,52 620,51 680,50"/>
&lt;path class="cv2" d="M100,150 C160,148 220,146 340,144 C460,143 580,142 680,142"/>
&lt;rect x="430" y="60" width="170" height="40" fill="white" stroke="#bbb"/>
&lt;line x1="438" y1="72" x2="468" y2="72" class="cv1"/>
&lt;text x="475" y="76" class="lb">DPO sin filtro&lt;/text>
&lt;line x1="438" y1="90" x2="468" y2="90" class="cv2"/>
&lt;text x="475" y="94" class="lb">SimPO (length-norm)&lt;/text>
&lt;/svg>
&lt;/div>
&lt;p>La gráfica muestra el patrón habitual: sin mitigación, en 3 epochs un Llama 3 8B con DPO puede pasar de respuestas de ~150 tokens a respuestas de ~280 tokens, &lt;strong>sin que mejore la calidad evaluada por humanos&lt;/strong>. SimPO mantiene la longitud aproximadamente estable.&lt;/p>
&lt;h3 id="position-bias-en-la-curación-del-dataset">Position bias (en la curación del dataset)&lt;/h3>
&lt;p>Si los pares se generan automáticamente con un LLM judge (cubierto en detalle en el siguiente post de esta serie), hay un sesgo conocido: &lt;strong>los jueces prefieren la primera respuesta&lt;/strong> que ven cuando las dos son comparables. Si todos los pares del dataset tienen el chosen siempre en posición A y el rejected en posición B, el modelo no aprende preferencia: aprende un artefacto del proceso de curación.&lt;/p>
&lt;p>Mitigación: shuffle aleatorio de orden en la query al judge &lt;strong>y promediar dos pasadas con orden invertido&lt;/strong> (vista en herramientas como Promptfoo y Inspect AI por defecto).&lt;/p>
&lt;h3 id="distribution-shift-entre-datos-y-π_ref">Distribution shift entre datos y &lt;code>π_ref&lt;/code>&lt;/h3>
&lt;p>DPO, KTO y SimPO comparan implícitamente el modelo entrenado contra una distribución. Si los datos de preferencia provienen de un modelo (otro LLM generando candidatos) muy distinto de &lt;code>π_ref&lt;/code>, el modelo entrenado puede explorar regiones donde &lt;code>π_ref&lt;/code> tiene probabilidad casi cero, dando ratios numéricamente inestables (log de cantidades muy pequeñas). En la práctica esto se traduce en explosiones de loss, gradientes NaN o regresión silenciosa.&lt;/p>
&lt;p>Mitigación: &lt;strong>generar los candidatos del dataset con el propio &lt;code>π_ref&lt;/code>&lt;/strong> siempre que sea posible (rejection sampling sobre &lt;code>π_ref&lt;/code>, con un judge externo eligiendo el chosen). Esa es la prescripción canónica de RLHF on-policy aplicada al setting offline.&lt;/p>
&lt;h2 id="implicaciones-en-hardware-on-premise">Implicaciones en hardware on-premise&lt;/h2>
&lt;p>Las cifras siguientes son indicativas para un escenario típico de mayo 2026: Llama 3.1 8B Instruct como &lt;code>π_ref&lt;/code>, dataset de 5 000–20 000 pares, QLoRA (NF4) con LoRA rank 16 sobre todos los proyectores del bloque transformer (&lt;code>q,k,v,o,gate,up,down&lt;/code>), batch size efectivo 16, 1–3 epochs.&lt;/p>
&lt;h3 id="en-una-rtx-4090-24-gb">En una RTX 4090 (24 GB)&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Método&lt;/th>
&lt;th>VRAM pico&lt;/th>
&lt;th>Tiempo / epoch (5k pares)&lt;/th>
&lt;th>Notas&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>DPO&lt;/td>
&lt;td>~22 GB&lt;/td>
&lt;td>50–80 min&lt;/td>
&lt;td>Necesita &lt;code>π_ref&lt;/code> en VRAM aunque cuantizada FP8/INT8&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>SimPO&lt;/td>
&lt;td>~14 GB&lt;/td>
&lt;td>45–70 min&lt;/td>
&lt;td>Sin &lt;code>π_ref&lt;/code> — la opción natural en 4090&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>ORPO&lt;/td>
&lt;td>~16 GB&lt;/td>
&lt;td>60–90 min&lt;/td>
&lt;td>Sin &lt;code>π_ref&lt;/code> — viable holgadamente&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>KTO&lt;/td>
&lt;td>~22 GB&lt;/td>
&lt;td>90–150 min (10k binarios)&lt;/td>
&lt;td>Misma VRAM que DPO, más datos por epoch&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>La 4090 (24 GB, Ada Lovelace, sin NVLink) es &lt;strong>perfectamente viable&lt;/strong> para Llama 8B con QLoRA si se elige el método con cabeza. Para 13B la elección entre SimPO/ORPO ya no es preferencia, es requisito.&lt;/p>
&lt;h3 id="en-un-cluster-genérico-4h100-sxm-320-gb-nvlink">En un cluster genérico 4×H100 SXM (320 GB, NVLink)&lt;/h3>
&lt;table>
&lt;thead>
&lt;tr>
&lt;th>Método&lt;/th>
&lt;th>Modelo viable&lt;/th>
&lt;th>Tiempo / epoch (10k pares)&lt;/th>
&lt;th>Notas&lt;/th>
&lt;/tr>
&lt;/thead>
&lt;tbody>
&lt;tr>
&lt;td>DPO&lt;/td>
&lt;td>Llama 3 70B (4-bit)&lt;/td>
&lt;td>60–90 min&lt;/td>
&lt;td>Tensor parallel = 4, sigue holgado&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>SimPO&lt;/td>
&lt;td>Llama 3 70B (BF16)&lt;/td>
&lt;td>50–75 min&lt;/td>
&lt;td>Cabe BF16 entero gracias a no tener &lt;code>π_ref&lt;/code>&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>ORPO&lt;/td>
&lt;td>Llama 3 70B (BF16)&lt;/td>
&lt;td>70–100 min&lt;/td>
&lt;td>Similar a SimPO en consumo&lt;/td>
&lt;/tr>
&lt;tr>
&lt;td>KTO&lt;/td>
&lt;td>Llama 3 70B (4-bit)&lt;/td>
&lt;td>100–140 min&lt;/td>
&lt;td>Datasets mayores compensados por paralelismo&lt;/td>
&lt;/tr>
&lt;/tbody>
&lt;/table>
&lt;p>En un cluster NVLink la diferencia operativa entre métodos se difumina: todos caben. La elección vuelve a ser sobre &lt;strong>qué tipo de señal tienes&lt;/strong>, no sobre presupuesto.&lt;/p>
&lt;h2 id="lo-que-no-hemos-cubierto-próximos-artículos">Lo que no hemos cubierto (próximos artículos)&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>LoRA y QLoRA fundamentos&lt;/strong>: la matemática del adapter de bajo rango que sostiene todo lo anterior — por qué cabe un 70B en 24 GB.&lt;/li>
&lt;li>&lt;strong>LLM-as-judge fundamentos&lt;/strong>: cómo se construye el judge que genera los pares chosen/rejected sin sesgo de posición ni de verbosidad. El siguiente post de esta tanda lo cubre.&lt;/li>
&lt;li>&lt;strong>Online DPO y iterative on-policy&lt;/strong>: estado del arte en investigación 2026 (Fast-Slow Chasing, RLOO, iterative preference learning) y por qué todavía no es producción.&lt;/li>
&lt;li>&lt;strong>Distillation y synthetic preference data&lt;/strong>: cuándo merece la pena generar pares con un modelo grande para entrenar uno pequeño.&lt;/li>
&lt;/ul>
&lt;h2 id="ver-también">Ver también&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.lo0.es/posts/fine-tuning-continuo-produccion/">Fine-tuning continuo en producción&lt;/a> — el ciclo operativo (Postgres, queries SQL, hot-swap multi-LoRA) que alimenta de pares a los métodos de este post.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/retrain-cerrar-el-bucle-feedback-dataset-adapter/">Retrain: cerrar el bucle&lt;/a> — cómo las señales de producción se convierten en el dataset de preferencia.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/evals-llm-la-capa-despues-de-tracing/">Evals para LLMs: la capa después del tracing&lt;/a> — la batería de evaluadores que decide si el adapter alineado se promociona a producción.&lt;/li>
&lt;li>&lt;a href="https://blog.lo0.es/posts/llm-as-judge-fundamentos/">LLM-as-judge: el corrector de oposiciones&lt;/a> — el mecanismo que genera los pares &lt;code>(chosen, rejected)&lt;/code> consumidos aquí. Calibración del judge con Cohen&amp;rsquo;s kappa y los cuatro sesgos que invalidan los pares si no se vigilan.&lt;/li>
&lt;/ul>
&lt;h2 id="referencias">Referencias&lt;/h2>
&lt;ul>
&lt;li>Rafailov, R., Sharma, A., Mitchell, E., Ermon, S., Manning, C. D., Finn, C. &lt;em>Direct Preference Optimization: Your Language Model is Secretly a Reward Model&lt;/em> (NeurIPS 2023).&lt;/li>
&lt;li>Ethayarajh, K., Xu, W., Muennighoff, N., Jurafsky, D., Kiela, D. &lt;em>KTO: Model Alignment as Prospect Theoretic Optimization&lt;/em> (ICML 2024).&lt;/li>
&lt;li>Hong, J., Lee, N., Thorne, J. &lt;em>ORPO: Monolithic Preference Optimization without Reference Model&lt;/em> (EMNLP 2024).&lt;/li>
&lt;li>Meng, Y., Xia, M., Chen, D. &lt;em>SimPO: Simple Preference Optimization with a Reference-Free Reward&lt;/em> (NeurIPS 2024).&lt;/li>
&lt;li>HuggingFace TRL 0.13 — implementaciones de referencia: &lt;a href="https://huggingface.co/docs/trl">https://huggingface.co/docs/trl&lt;/a>.&lt;/li>
&lt;li>Tunstall, L. et al. &lt;em>The Alignment Handbook&lt;/em> — recetas reproducibles: &lt;a href="https://github.com/huggingface/alignment-handbook">https://github.com/huggingface/alignment-handbook&lt;/a>.&lt;/li>
&lt;li>Park, R. et al. &lt;em>Disentangling Length from Quality in Direct Preference Optimization&lt;/em> (R-DPO, ACL 2024).&lt;/li>
&lt;/ul></description></item></channel></rss>