Werkplaats / De werkplaats
In jouw eigen tempo
← Taalmodellen
MODULE 05 · LES 14

Benader gewichten met een kleiner getallenrooster

Implementeer symmetrische en per-kolom kwantisatie, meet reconstructie- en uitvoerfouten en scheid simulatie van echte opslagwinst.

60 min3 checkpoints
DIT GA JE ONTDEKKEN
  • Je berekent schaal, gehele codes en reconstructiefouten voor een gewichtsmatrix.
  • Je maakt onderscheid tussen minder precisie, fysieke opslag en gemeten modelkwaliteit.

Je taalmodel past nét niet in het geheugen. Kun je ieder gewicht met minder bits benaderen zonder te veel voorspelling te verliezen? Kwantisatie (quantization) vervangt waarden door codes op een kleiner rooster.

Denk aan een meetlat met minder streepjes. Waarden tussen twee streepjes moeten afronden; één grote uitschieter kan de afstand tussen alle streepjes vergroten. Of de benadering opslag of rekentijd bespaart, hangt daarnaast van het bestandsformaat en de uitvoerengine af.

1. Kies een schaal en rond af

Onze symmetrische onderwijsvariant gebruikt qmax=2^(bits−1)−1. Bij vier bits zijn de codes −7 tot en met 7 en nemen we schaal s=max(abs(W))/7. Code nul betekent precies nul; de reconstructie vermenigvuldigt de gehele code weer met s.

q=rint(Ws),W^=sq

W is het oorspronkelijke gewicht, s de roosterafstand en q zijn gehele code; Ŵ is de benadering. Clip de code op het afgesproken bereik. np.rint rondt exacte halve waarden naar de dichtstbijzijnde even integer.

Neem gewichten [−1.5,−.5,.5,1.5,7]. Het maximum zeven geeft schaal één. De codes worden [−2,0,0,2,7]: ook negatieve halve waarden volgen de even-regel.

w = np.array([-1.5,-.5,.5,1.5,7.])
q,scale = quantize_symmetric(w,4)
print(q)  # [-2 0 0 2 7]
print(scale)  # 1.0
print(q.astype(float)*scale)
# [-2. 0. 0. 2. 7.]

Maak quantize_symmetric zonder de invoer te veranderen. Voor nulgewichten kiezen we schaal één; bij extreem kleine getallen voorkomen we schaalnul met de kleinste positieve float. Alleen eindige, niet-lege gewichten en exacte integerbits van twee tot acht zijn geldig.

PRECISIE HEEFT EEN PRIJSEen uitschieter maakt het rooster grover

Symmetrisch bereik −7 tot +7, 15 niveaus. Exacte halve codes ronden naar even.

Zes voorbeeldgewichten, één gedeelde schaal.
GewichtCode qq × schaalFout
-2.00-2-2.0000.000
-0.5000.0000.500
0.0000.0000.000
0.5000.000-0.500
1.2011.000-0.200
7.0077.0000.000
Roosterafstand1.00000Gewicht-MSE0.090000

Ideaal gepakt: 3 bytes aan codes. In onze NumPy-oefening: altijd 6 bytes (int8). Beide hebben daarnaast schaalmetadata nodig; onze float64-schaal gebruikt 8 bytes. De uitvoerkwaliteit wordt apart gemeten.

Verhoog het laatste gewicht in de visualisatie en bekijk hoe de andere reconstructies verschuiven. Meer bits geeft meer mogelijke codes, maar een groter maximum bepaalt nog steeds het rooster. De getoonde gewicht-MSE is een meting van dit voorbeeld, geen volledige modelbeoordeling.

01
CHECKPOINTEen schaal bepaalt de afstand tussen roosterpunten

2. Laat kolommen hun eigen rooster kiezen

In Y=XW+b hoort iedere kolom van W bij één outputkanaal. Met een aparte schaal per kolom kan een maximum tien in de ene kolom de kleine gewichten in een andere kolom met maximum .2 minder verstoren. Een nulkolom krijgt schaal één.

w = np.array([[.1,10.,0.],
              [.2,-8.,0.],
              [-.15,6.,0.]])
q,s = quantize_columns(w,4)
print(np.round(s,4))
# [.0286 1.4286 1.]
print(q[:,2])  # [0 0 0]

Maak quantize_columns met een schaalvector van vorm (Cout,). Die vector broadcast over de inputdimensie. De extra schalen kosten metadata; meet daarom zowel de reconstructie als de totale opslag.

Een fijnere kolomschaal garandeert niet dat ieder afzonderlijk gewicht of iedere voorspelling beter wordt. Afrondpunten kunnen verschuiven en fouten werken samen in een matrixproduct. Vergelijk de concrete uitkomsten in plaats van alleen het aantal roosterpunten.

02
CHECKPOINTGeef iedere outputkolom een eigen schaal

3. Meet wat de laag daadwerkelijk uitvoert

Neem W=[.1,1] als één outputkolom en een tweebitrooster. Met schaal één wordt .1 nul, terwijl één exact blijft. Voor input [10,0] versterkt het matrixproduct de kleine gewichtsfout .1 tot uitvoerfout één.

w = np.array([[.1],[1.]])
x = np.array([[10.,0.]])
bias = np.array([.3])
q,s = quantize_columns(w,2)
reference = x@w+bias
approx = quantized_linear(x,q,s,bias)
print(reference,approx)
# [[1.3]] [[.3]]
print(output_errors(reference,approx))
# (1.0,1.0)

Maak quantized_linear door eerst naar float te dequantiseren en daarna XŴ+b te berekenen. output_errors retourneert de gemiddelde absolute fout en de grootste absolute fout over gelijke, eindige, niet-lege arrays. Activaties en bias blijven in deze oefening float.

Onze codes staan altijd in int8. Vijf vierbitcodes gebruiken daarom hier vijf bytes, plus acht bytes voor één float64-schaal; ideaal gepakte codes zouden drie bytes gebruiken. Deze simulatie levert geen snellere integerkernel en bewijst geen onveranderde taalmodelkwaliteit.

03
CHECKPOINTMeet de afwijking na de matrixvermenigvuldiging

Wat neem je mee?

  • Schaal en afrondregel bepalen de numerieke benadering.
  • Per-kolomschalen beperken de invloed van andere outputkanalen.
  • Meet uitvoerfout, echte opslag en modelkwaliteit afzonderlijk.

Hierna kijk je hoe pretraining en aanpassen de modelgewichten zelf veranderen.

Verder lezen & bronnen

Inhoud gecontroleerd op 2026-10-28.
DE VOLGENDE STAPKies wat een basismodel tijdens pretraining ziet
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 / 3
01
Een schaal bepaalt de afstand tussen roosterpunten

Schrijf quantize_symmetric(weights,bits=8) met (q,scale). q is np.int8 met dezelfde vorm. Gewichten zijn eindig en niet leeg; bits is een exacte int van 2 tot 8. Anders ValueError. Wijzig de input niet.

Een hint

np.nextafter(0.,1.) geeft de kleinste positieve representabele float. Rond met np.rint en cast pas na clipping.

02
Geef iedere outputkolom een eigen schaal

Schrijf quantize_columns(weights,bits=8) met (q,scales). Gebruik dezelfde validatie en afronding als eerder, maar nu voor een niet-lege 2D-matrix en een aparte schaal per outputkolom. Een nulkolom krijgt schaal 1.

Een hint

max(abs(weights),axis=0) geeft kolommaxima; de schaalvector broadcast over de inputdimensie.

03
Meet de afwijking na de matrixvermenigvuldiging

Schrijf quantized_linear(x,q,scales,bias), die eerst de int8-gewichten naar float dequantiseert en dan de lineaire uitvoer berekent. Schrijf output_errors(reference,approximation) met (mae,max_abs); valideer gelijke, eindige, niet-lege vormen met ValueError.

Een hint

Voer q.astype(float)*scales uit vóór het matrixproduct. De fouten worden over alle uitvoerelementen berekend.

Vastgelopen? Bekijk de voorbeeldoplossing
import numpy as np

def quantize_symmetric(weights,bits=8):
    weights=np.asarray(weights,float)
    if weights.size==0 or not np.isfinite(weights).all() or type(bits) is not int or not 2<=bits<=8:
        raise ValueError('Ongeldige gewichten of bitbreedte')
    qmax=2**(bits-1)-1
    maximum=float(np.max(np.abs(weights)))
    scale=max(maximum/qmax,float(np.nextafter(0.,1.))) if maximum else 1.
    q=np.clip(np.rint(weights/scale),-qmax,qmax).astype(np.int8)
    return q,scale

def quantize_columns(weights,bits=8):
    weights=np.asarray(weights,float)
    if weights.ndim!=2 or weights.size==0 or not np.isfinite(weights).all() or type(bits) is not int or not 2<=bits<=8:
        raise ValueError('Gebruik een eindige matrix en 2 tot 8 bits')
    qmax=2**(bits-1)-1
    maximum=np.max(np.abs(weights),axis=0)
    scales=np.where(maximum>0,np.maximum(maximum/qmax,np.nextafter(0.,1.)),1.)
    q=np.clip(np.rint(weights/scales),-qmax,qmax).astype(np.int8)
    return q,scales

def quantized_linear(x,q,scales,bias):
    restored=q.astype(float)*scales
    return x@restored+bias

def output_errors(reference,approximation):
    reference=np.asarray(reference,float);approximation=np.asarray(approximation,float)
    if reference.shape!=approximation.shape or reference.size==0 or not np.isfinite(reference).all() or not np.isfinite(approximation).all():
        raise ValueError('Ongeldige uitvoerarrays')
    with np.errstate(over='ignore',invalid='ignore'):
        difference=np.abs(reference-approximation)
    maximum=float(difference.max())
    if np.isfinite(maximum):
        mean=float((difference/maximum).mean())*maximum if maximum else 0.
        return mean,maximum
    # Alleen werkelijk overgelopen verschillen herschalen: behoud nauwkeurige kleine verschillen elders.
    scale=max(float(np.abs(reference).max()),float(np.abs(approximation).max()))
    overflow=np.isinf(difference)
    scaled=np.where(overflow,np.abs(reference/scale-approximation/scale),difference/scale)
    return float(scaled.mean())*scale,float(scaled.max())*scale

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