Werkplaats / De werkplaats
In jouw eigen tempo
← Taalmodellen
MODULE 06 · LES 16

Leer een kleine aanpassing met bevroren basisgewichten

Bouw LoRA voor een lineaire laag, controleer alle adaptergradiënten en train een echte rang-één-aanpassing.

90 min4 checkpoints
DIT GA JE ONTDEKKEN
  • Je implementeert een lage-rangaanpassing zonder de basismatrix te wijzigen.
  • Je bewijst numeriek dat de backward en het samenvoegen van gewichten kloppen.

Je basismodel heeft bruikbare kennis, maar een nieuwe taak vraagt andere uitkomsten. Moet daarvoor iedere grote gewichtsmatrix leren? Low-Rank Adaptation (LoRA) leert een kleine extra route en bewaart de oorspronkelijke basisgewichten.

Denk aan een vaste hoofdweg met een smalle omleiding. De uitvoer bevat beide routes, maar alleen de omleiding krijgt nieuwe instellingen. Hier bouwen we één lineaire laag met rijvectoren; een heel transformer-model vraagt adapters op bewust gekozen lagen.

1. Voeg een kleine route toe

Onze laag gebruikt W van vorm (Din,Dout), A van (Din,r) en B van (r,Dout). r is de adapterrang en α een positieve schaalinstelling; s=α/r schaalt de bijdrage. De oorspronkelijke LoRA-publicatie gebruikt kolomvectoren en schrijft de factoren daarom in een andere volgorde.

Y=XW+s(XA)B

X is de inputbatch en Y de uitvoer. Initialiseer A klein en willekeurig, B met nullen. Dan is de extra route aanvankelijk nul en krijg je exact de basisfunctie; beide factoren nul maken zou ook de eerste gradiënten blokkeren.

x = np.array([[1.,2.]])
w = np.eye(2)
adapter = init_lora(2,2,1)
print(lora_forward(x,w,adapter,1.))
# [[1. 2.]]

Neem daarna A=[1,0]ᵀ en B=[2,−1], met s=1. XA is één, de extra route [2,−1] en de totale uitvoer [3,1].

adapter = {'A':np.array([[1.],[0.]]),
           'B':np.array([[2.,-1.]])}
print(lora_forward(x,w,adapter,1.))
# [[3. 1.]]

Maak init_lora en lora_forward. Er leren r×(Din+Dout) getallen: in dit kleine voorbeeld vier, evenveel als W. Besparing ontstaat dus niet automatisch; bij Din=Dout=128 en r=1 zijn het 256 tegenover 16.384 basisgewichten.

EEN BEVROREN BASIS MET EEN LEERBARE ROUTEEen kleine adapter verandert de uitvoer

Eigen getallenvoorbeeld met X = [1, 2, −1, 0.5]. Je stelt B zelf in; dit diagram traint niet.

Bevroren W4 × 312 basisgewichten
Adapter A en B4 × 1 → 1 × 37 trainbare gewichten
Drie outputkanalen · schaal α/r = 2.00
Route012
XW1.2502.250-0.750
(α/r)XAB0.0000.0000.000
Som1.2502.250-0.750

Uitvoer: 1.2500, 2.2500, -0.7500. Bij B-factor nul draagt de adapter niets bij. De adapter heeft hier minder trainbare gewichten dan de basismatrix.

01
CHECKPOINTBegin met exact dezelfde functie als het basismodel

2. Laat ook de vaste route terugpraten

Een bevroren W krijgt geen update, maar beïnvloedt nog steeds de gradiënt naar X. Die inputgradiënt telt een directe bijdrage dY·Wᵀ en een adapterbijdrage s(dY·Bᵀ)Aᵀ op. Vergeet je de directe route, dan leren eerdere lagen van een andere functie.

De twee adaptergradiënten volgen dezelfde matrixkettingregel. dY is de aangeleverde uitvoergradiënt; superscript T betekent transponeren. Met dezelfde schaal s=α/r krijg je:

dA=sXTdYBTdB=s(XA)TdY

Voor dezelfde handgetallen en dY=[1,1] wordt dY·Bᵀ één. Daardoor is dA=[1,2]ᵀ, dB=[1,1] en dX=[2,1]. De schaalfactor geldt voor beide adaptergradiënten.

dx,g = lora_backward(
    x,w,adapter,1.,np.ones((1,2)))
print(dx)  # [[2. 1.]]
print(g['A'].ravel())  # [1. 2.]
print(g['B'])  # [[1. 1.]]

Maak lora_backward zonder opnieuw over voorbeelden te middelen: dY bevat al de afgeleide van de gekozen loss. Bij B=0 begint dA op nul, terwijl dB al kan leren. Controleer daarnaast niet-nul A én B numeriek tegen een onafhankelijk juiste forward.

02
CHECKPOINTBevroren gewichten laten nog steeds gradiënten door

3. Voeg routes samen voor inferentie

Zonder adapter-dropout kun je één nieuwe matrix Wmerged=W+sAB maken. De uitvoer XWmerged is dan gelijk aan de twee routes, op afronding na. Maak een nieuwe array zodat je W kunt bewaren voor andere adapters.

merged = merge_lora(w,adapter,1.)
print(x@merged)  # [[3. 1.]]
print(w)  # blijft de identiteitsmatrix

Maak merge_lora zonder W, A of B te wijzigen. Een gekwantiseerd basismodel kan bij samenvoegen dequantisatie en nieuwe afrondfouten vragen. Deze proef blijft float en meet geen apparaatversnelling.

03
CHECKPOINTVervang beide routes door één equivalente matrix

4. Controleer dat alleen de adapter leert

We trainen op een haalbare synthetische rang-één-aanpassing. Voor mean squared error is de upstream 2×(prediction−target)/error.size; de noemer omvat voorbeelden én outputkanalen. Bereken eerst alle gradiënten met de huidige factoren, en werk A en B daarna samen bij.

rng = np.random.default_rng(42)
x = rng.normal(size=(5,4))
w = rng.normal(size=(4,3))
a = np.array([.2,-.4,.1,.3])
b = np.array([.5,-.2,.4])
y = x@(w+np.outer(a,b))
start = init_lora(4,3,1)
learned,h = train_lora(
    x,y,w,start,1.,400,.2)
print(h[-1]<h[0]/100)  # True

Maak train_lora met een gekopieerde adapter en steps+1 echte losses: vóór iedere update en eenmaal na de laatste. Bewaar W en de oorspronkelijke adapter onveranderd. De dalende speelgoedloss bewijst dat deze adapter leert, niet dat rang één voor iedere taaltaak genoeg is.

04
CHECKPOINTTrain alleen de adapter en controleer het basismodel

Wat neem je mee?

  • LoRA voegt een geschaalde lage-rangroute toe aan vaste basisgewichten.
  • Bevroren gewichten dragen nog steeds bij aan de inputgradiënt.
  • Echte lossmetingen en invoerbehoud horen bij het trainingsbewijs.

Hierna bepaal je welke tokens tijdens instructietuning een trainingsdoel zijn.

Verder lezen & bronnen

Inhoud gecontroleerd op 2026-10-28.
DE VOLGENDE STAPLeer van antwoorden zonder de vraag als antwoorddoel te tellen
JOUW EXPERIMENT

Probeer het zelf

Python
main.py

Python start zodra jij code uitvoert.

UITVOER

Je uitvoer verschijnt hier.
Alles rekent in je eigen browser.

Jouw checkpoints0 / 4
01
Begin met exact dezelfde functie als het basismodel

Schrijf init_lora(input_dim,output_dim,rank,seed=7) met dictionary A,B; A is normaal verdeeld met standaardafwijking 0.05 en B nul. Controleer positieve exacte ints en rank≤min(Din,Dout). Schrijf lora_forward(x,w,adapter,alpha) voor geldige, compatibele 2D-inputs en positieve alpha.

Een hint

Gebruik np.random.default_rng(seed). Deel alpha door A.shape[1], de rang.

02
Bevroren gewichten laten nog steeds gradiënten door

Schrijf lora_backward(x,w,adapter,alpha,dy) met (dx,grads), grads heeft uitsluitend A en B. Werk met compatibele eindige matrices en verander geen invoer.

Een hint

Reken de directe route via W mee in dx. Deel niet extra door het aantal voorbeelden: dy bevat al de afgeleide van de gekozen loss.

03
Vervang beide routes door één equivalente matrix

Schrijf merge_lora(w,adapter,alpha), retourneer een nieuwe matrix met de samengevoegde gewichten. Verander w, A en B niet.

Een hint

Gebruik gewone optelling om een nieuwe array te maken; geen w += update.

04
Train alleen de adapter en controleer het basismodel

Schrijf train_lora(x,y,w,adapter,alpha,steps=200,rate=0.1) met (nieuwe_adapter,history). Kopieer de adapter, voer precies steps updates uit en houd steps+1 losses bij. Hier zijn inputs geldig, steps een niet-negatieve int en rate positief.

Een hint

Bereken eerst alle gradiënten met de huidige A en B, update ze daarna samen. Verander de oorspronkelijke adapter en W niet.

Vastgelopen? Bekijk de voorbeeldoplossing
import numpy as np

def init_lora(input_dim,output_dim,rank,seed=7):
    if any(type(v) is not int or v<=0 for v in [input_dim,output_dim,rank]) or rank>min(input_dim,output_dim):
        raise ValueError('Ongeldige adaptervorm')
    rng=np.random.default_rng(seed)
    return {'A':rng.normal(0,.05,(input_dim,rank)),'B':np.zeros((rank,output_dim))}

def lora_forward(x,w,adapter,alpha):
    w=np.asarray(w,float);adapter={k:np.asarray(v,float) for k,v in adapter.items()}
    x=np.asarray(x,float)
    scale=alpha/adapter['A'].shape[1]
    return x@w+scale*(x@adapter['A'])@adapter['B']

def lora_backward(x,w,adapter,alpha,dy):
    w=np.asarray(w,float);adapter={k:np.asarray(v,float) for k,v in adapter.items()}
    x=np.asarray(x,float)
    dy=np.asarray(dy,float)
    a=adapter['A'];b=adapter['B'];scale=alpha/a.shape[1]
    return dy@w.T+scale*(dy@b.T)@a.T,{'A':scale*x.T@dy@b.T,'B':scale*(x@a).T@dy}

def merge_lora(w,adapter,alpha):
    w=np.asarray(w,float);adapter={k:np.asarray(v,float) for k,v in adapter.items()}
    return w+(alpha/adapter['A'].shape[1])*(adapter['A']@adapter['B'])

def train_lora(x,y,w,adapter,alpha,steps=200,rate=.1):
    trained={k:v.copy() for k,v in adapter.items()};history=[]
    for _ in range(steps):
        error=lora_forward(x,w,trained,alpha)-y
        history.append(float(np.mean(error**2)))
        _,grad=lora_backward(x,w,trained,alpha,2*error/error.size)
        trained={k:trained[k]-rate*grad[k] for k in trained}
    history.append(float(np.mean((lora_forward(x,w,trained,alpha)-y)**2)))
    return trained,history

Voer de code uit en verklaar voor jezelf waarom elke stap werkt.