ouroboros / src /DualLearning.tsx
REXWind's picture
Publish dual-learning showcase with live inference and sourced annotation cases
a74f603 verified
Raw History Blame Contribute Delete
8.06 kB
import {useEffect,useRef,useState} from 'react';
import {Typewriter} from './Typewriter';
import type {Example} from './types';
type Mode='joint'|'t2p'|'p2t';
const details={
joint:{title:'Two directions. A shared learning process.',body:'Each protein–text pair supervises both directions in the same training step. Shared self-attention parameters link the encoder and decoder within each modality, coupling the two tasks.',tag:'JOINT TRAINING',loss:'L = ½ L(text → protein) + ½ L(protein → text)'},
t2p:{title:'Start with a function. Generate a sequence.',body:'The text encoder reads a functional description. A cross-modal projection conditions the protein decoder, which generates amino acids one token at a time.',tag:'TEXT → PROTEIN',loss:'Functional description → protein sequence'},
p2t:{title:'Start with a sequence. Describe its function.',body:'The protein encoder reads an amino-acid sequence. The reverse projection conditions the text decoder, which generates a functional description.',tag:'PROTEIN → TEXT',loss:'Protein sequence → functional description'},
};
export function DualLearning({example}:{example:Example}){
const [mode,setMode]=useState<Mode>('joint');
const [technical,setTechnical]=useState(false),[playing,setPlaying]=useState(()=>!window.matchMedia('(prefers-reduced-motion: reduce)').matches);
const section=useRef<HTMLElement>(null);
useEffect(()=>{
if(!playing)return;
let visible=true;const io=new IntersectionObserver(entries=>{visible=entries[0].isIntersecting;});if(section.current)io.observe(section.current);
const timer=setInterval(()=>{if(visible&&!document.hidden)setMode(current=>current==='joint'?'t2p':current==='t2p'?'p2t':'joint');},4500);
return()=>{clearInterval(timer);io.disconnect();};
},[playing]);
const item=details[mode];
const direction=mode==='p2t'?'p2t':'t2p';
return <section ref={section} id="dual-learning" className="section dual-section">
<div className="section-heading"><div><p className="eyebrow"><span>01</span> THE CORE IDEA</p><h2>Read. Write.<br/><em>Learn together.</em></h2></div><p className="section-deck">One protein–text pair.<br/>Two prediction tasks.<br/><strong>Connected through shared parameters.</strong></p></div>
<div className="dual-experience">
<div className="dual-toolbar"><div className="dual-tour"><span className="micro">ONE FRAMEWORK · TWO GENERATION TASKS</span><button className="text-button" aria-pressed={playing} onClick={()=>setPlaying(!playing)}>{playing?'Ⅱ Pause tour':'▷ Guided tour'}</button></div><div className="segmented" role="group" aria-label="Learning direction">{([['joint','Joint learning'],['t2p','Text → Protein'],['p2t','Protein → Text']] as [Mode,string][]).map(([v,l])=><button key={v} onClick={()=>{setPlaying(false);setMode(v);}} aria-pressed={mode===v}>{l}</button>)}</div></div>
<div className={'paired-example core-demo pair-mode-'+direction}>
<div className="pair-language"><div><span className="micro">FUNCTIONAL LANGUAGE</span><span className="pair-role">{direction==='t2p'?'INPUT':'MODEL OUTPUT'}</span></div>{direction==='t2p'?<p className="core-input-text">{example.text}</p>:<Typewriter key={mode} text={example.generatedText} direction="p2t"/>}</div>
<div className="pair-bridge"><span aria-hidden="true">{direction==='t2p'?'→':'←'}</span><small>{direction==='t2p'?'DESIGN':'DESCRIBE'}<br/>{' '}TOKEN BY TOKEN</small></div>
<div className="pair-protein"><div><span className="micro">PROTEIN SEQUENCE</span><span className="pair-role">{direction==='t2p'?'MODEL OUTPUT':'INPUT'}</span></div>{direction==='t2p'?<Typewriter key={mode} text={example.generatedProtein} direction="t2p"/>:<div className="core-input-sequence">{example.protein}</div>}</div>
</div>
<div className="pair-source"><span className="saved-badge">RECORDED DEMO</span><span>large-v5 · {example.name} · {example.id}. Each direction starts from its reference input; this is not a round trip.</span><a href="/data/core-example.json" download>Source ↗</a></div>
<div className={'dual-diagram mode-'+mode}>
<div className="modality text-modality"><div className="modality-title"><span className="modality-icon">Aa</span><div><span className="micro">MODALITY 01</span><h3>Functional language</h3></div></div><div className="model-node node-text-encoder"><span className="node-number">01</span><div><small>READ</small><strong>Text encoder</strong></div><span className="node-dots">•••</span></div><div className="sharing-link"><i/><span><b>Shared</b> self-attention</span><i/></div><div className="model-node node-text-decoder"><span className="node-number">04</span><div><small>WRITE</small><strong>Text decoder</strong></div><span className="node-dots">•••</span></div></div>
<div className="cross-modal"><div className="flow flow-t2p"><span>DESIGN</span><svg viewBox="0 0 250 55" aria-hidden="true"><path d="M0 27H238"/><path d="m227 19 12 8-12 8"/><circle className="flow-particle" r="4" cy="27" cx="0"/></svg><small>Cross-modal projection</small></div><div className="dual-symbol" aria-hidden="true">∞</div><div className="flow flow-p2t"><span>DESCRIBE</span><svg viewBox="0 0 250 55" aria-hidden="true"><path d="M250 27H12"/><path d="m23 19-12 8 12 8"/><circle className="flow-particle" r="4" cy="27" cx="250"/></svg><small>Cross-modal projection</small></div></div>
<div className="modality protein-modality"><div className="modality-title"><span className="modality-icon">⌁</span><div><span className="micro">MODALITY 02</span><h3>Protein sequence</h3></div></div><div className="model-node node-protein-decoder"><span className="node-number">02</span><div><small>WRITE</small><strong>Protein decoder</strong></div><span className="node-dots">•••</span></div><div className="sharing-link"><i/><span><b>Shared</b> self-attention</span><i/></div><div className="model-node node-protein-encoder"><span className="node-number">03</span><div><small>READ</small><strong>Protein encoder</strong></div><span className="node-dots">•••</span></div></div>
</div>
<div className="dual-explanation" aria-live="polite"><div key={mode}><span className="eyebrow">{item.tag}</span><h3>{item.title}</h3><p>{item.body}</p></div><div className="loss-block"><span className="micro">{mode==='joint'?'ONE JOINT OBJECTIVE':'INFERENCE PATH'}</span><code>{item.loss}</code><span className="coupling-label"><i/> Parameter-coupled bidirectional generation</span></div></div>
</div>
<div className="dual-takeaways"><div><span>01</span><p><strong>The same paired data</strong>Each sequence and description supports learning in both directions.</p></div><div><span>02</span><p><strong>Two supervised predictions</strong>Generate a sequence from text, and text from a sequence.</p></div><div><span>03</span><p><strong>Parameters connect the tasks</strong>Shared weights receive learning signals from the joint objective.</p></div></div>
<button className="text-button mechanism-toggle" onClick={()=>setTechnical(!technical)} aria-expanded={technical}>{technical?'−':'+'} A closer look at the mechanism</button>
{technical&&<div className="technical-note"><p>The animation replays two archived model outputs from a held-out HSP20 reference example. Each direction uses the corresponding reference input, not the output of the other direction. Playback timing illustrates autoregressive decoding and is not measured inference speed. The live playground generates new outputs on demand.</p><p>The original large-v5 model jointly optimizes two autoregressive, teacher-forced cross-entropy losses. Corresponding encoder/decoder self-attention weights are shared within the text stack and within the protein stack. The projections are initialized as transposes but optimized independently.</p><p>Running a generated sequence back through the annotation model is an optional inference experiment. The training objective does not include a separate cycle-reconstruction loss. Model size: 1,226,512,384 independent parameters.</p></div>}
</section>;
}