Ga naar hoofdinhoud

Vision Transformers (ViT) Tutorial: Architectuur en codevoorbeelden

Leer hoe Vision Transformers (ViT’s) patch-embeddings en self-attention gebruiken om CNN’s te verslaan in moderne beeldclassificatie. Deze diepgaande tutorial ontleedt de ViT-architectuur, biedt stapsgewijze Python-code en laat zien wanneer je ViT’s kiest voor realistische computer-visionprojecten.
· 15 min lezen

Verken met AI

We weten allemaal dat Transformers de basis zijn geworden in natural language processing en nu ook terrein winnen in computer vision. Vision Transformers (ViT’s) bieden een nieuwe manier om beelden te begrijpen door dezelfde principes toe te passen als in tekstmodellen; Large Language Models.

ViT’s werden voor het eerst geïntroduceerd in 2020 door Google Research in het artikel “An Image is Worth 16×16 Words”, waarin werd aangetoond dat transformers die op grote afbeeldingsdatasets zijn getraind, kunnen tippen aan of beter presteren dan CNN’s. Sindsdien hebben praktische varianten zoals DeiT en Swin hun gebruik verbreed over veel vision-taken.

In deze tutorial neem ik je mee door de kernconcepten, architectonische componenten en praktische toepassingen van vision transformers (de vanilla/oorspronkelijke variant). Aan het einde begrijpen we niet alleen hoe ViT’s werken, maar ook hoe je ze implementeert in je eigen computer vision-projecten. Ik raad ook aan om mijn andere gids over multi-head attention te bekijken.  

Wat is een Vision Transformer (ViT)?

Laten we beginnen met een zachte introductie van vision transformers. 

Deze modellen behandelen een afbeelding als een sequentie van data, vergelijkbaar met hoe een taalmodel woorden in een zin verwerkt. De afbeelding wordt vervolgens opgedeeld in patches met vaste grootte, die elk worden gevlakt en ingebed in een vector. 

Deze vectors worden daarna door een standaard transformer-encoder gestuurd, die de relaties tussen patches wereldwijd modelleert met behulp van het attention-mechanisme. 

Vergelijking met convolutionele neurale netwerken

Dit is fundamenteel anders dan convolutionele neurale netwerken (CNN’s), die zich richten op lokale kenmerken en vertrouwen op hiërarchische feature-extractie.

Nu we het over CNN’s hebben, laten we ingaan op de verschillen tussen hen en ViT’s:

Kenmerk

Convolutionele neurale netwerken (CNN’s)

Vision Transformers (ViT’s)

Architectuur

Gebruikt Convolutie + Pooling-lagen

Gebruikt Transformer-encoder met Self-Attention

Receptief veld

Lokaal receptief veld maar groeit met diepte

Vanaf het begin globaal

Positioneel bewustzijn

Impliciet via structuur

Expliciet via positionele encoderingen

Data-eisen

Redelijk effectief met kleinere datasets

Vereist grootschalige datasets

Inductieve bias

Sterk

Minimaal maar flexibeler

Potentieel voor parallelisatie

Gemiddeld

Zeer hoog

Opschalen van modellen

Minder schaalbaar naar grote modellen

Zeer schaalbaar

Interpreteerbaarheid

Via pooling-lagen maar nog steeds lastig.

Kan worden verbeterd met attention-visualisatie maar ook best lastig

We hebben vision transformers duidelijk nog niet tot in detail behandeld, maar het is nuttig om enkele punten door te nemen die niet meteen logisch lijken. 

Bijvoorbeeld, inductieve bias en receptieve velden zijn termen die belangrijk zijn, maar die je misschien nog niet eerder hebt gehoord. 

Uitleg inductieve bias

Inductieve bias betekent de aannames die een model maakt om van data te leren. Dat klinkt verwarrend, maar met deze analogie wordt het duidelijker.

Stel je voor dat we samen een legpuzzel maken. We denken dat de stukjes op elkaar aansluiten op basis van vorm en kleur. Die aanname helpt ons de stukjes sneller te leggen. Zo werkt een CNN-model. 

Het maakt slimme gokjes door aan te nemen dat naburige delen in een afbeelding gerelateerd zijn, zoals puzzelstukjes die dicht bij elkaar vaak bij hetzelfde deel van het plaatje horen. Het gaat er ook van uit dat als we een vorm verplaatsen, het nog steeds dezelfde vorm is. Deze behulpzame aannames heten sterke inductieve biases.

Stel je nu voor dat we een grote stapel puzzelstukjes krijgen, maar geen idee hebben hoe het eindplaatje eruitziet. We weten niet eens of we op vorm of op kleur moeten letten, of misschien wel op iets heel anders. Dus we proberen van alles en ontdekken langzaam wat werkt.

Dat is wat een Transformer-model doet. Het maakt geen sterke aannames. Het leert patronen door naar heel veel data te kijken. Dit heet minimale inductieve bias. Dat geeft het model meer vrijheid, maar het heeft meer data en tijd nodig om te leren.

Uitleg receptief veld

De bovenstaande afbeelding laat zien hoe het receptieve veld van een CNN-model verandert. Onthoud dat een CNN meerdere convolutielagen heeft, en naarmate we dieper in het netwerk gaan, wordt elke neuron in een laag door een steeds groter deel van de input beïnvloed. 

Dit uitbreidende receptieve veld stelt diepere lagen in staat om complexere en globalere kenmerken te detecteren, zoals in de figuur hierboven (van oor naar snuit naar hele hond). 

Dit verschilt behoorlijk van het receptieve veld van een Vision Transformer:

Receptief veld van Vision Transformer - Afbeelding door de auteur.

In de bovenstaande illustratie zien we dat het receptieve veld in een (vanilla) ViT globaal is, dus alle patches letten op elkaar met verschillende sterktes (weergegeven door de dikte van de pijlen). 

Voor de eenvoud heb ik laten zien dat de patch (groen gekleurd) geattendeerd wordt door de andere patches, maar in werkelijkheid letten alle patches op elkaar (dat zou te rommelig zijn om te tekenen!).

Wanneer CNN’s vs. vision transformers gebruiken

Begrijpen wanneer je een CNN of een vision transformer gebruikt, is behoorlijk belangrijk om effectieve architectuurkeuzes te maken in onze computer vision-workflows. 

Beide modellen hebben hun sterke punten (en zwakke!), maar ze presteren beter onder verschillende omstandigheden, afhankelijk van datagrootte, rekenbronnen en de complexiteit van de visuele patronen die we willen modelleren.

Ik verwijs graag naar deze punten wanneer ik de keuze tussen beide maak:

Je gebruikt CNN’s wanneer:

  1. Je met beperkte data werkt - We hebben al gesteld dat CNN’s sterke inductieve biases hebben, zoals localiteit, die hen helpen goed te generaliseren, zelfs met kleinere datasets. Dat maakt ze een betrouwbare keuze voor taken waarbij het verzamelen van grote hoeveelheden gelabelde data onpraktisch is.
  2. Realtime prestaties belangrijk zijn - CNN’s hebben meestal lagere inference-latentie en zijn rekenkundig efficiënter op standaardhardware, vooral mobiele en edge-apparaten. Hun architectuur is geoptimaliseerd voor prestaties met minder parameters en minder geheugengebruik.
  3. Je compute-budget beperkt is - Een CNN from scratch trainen of een voorgetrainde fine-tunen vereist aanzienlijk minder middelen dan ViT’s. Je kunt een degelijk CNN trainen met één GPU en gemiddeld geheugen, wat niet altijd het geval is voor transformer-gebaseerde modellen.

Je gebruikt Vision Transformers wanneer:

  1. Je toegang hebt tot grootschalige gelabelde datasets en robuuste compute-infrastructuur - Zoals ik in de vergelijkingstabel aangaf, zijn vision transformers data-intensief en vereisen ze veel traintijd en geheugen, zeker in hun vanilla-vorm. Met genoeg data en compute kunnen ze CNN’s op veel benchmarks overtreffen.
  2. Je langeafstands-ruimtelijke relaties moet vastleggen - In tegenstelling tot CNN’s, die lokaal verwerken (denk aan de sectie over het receptieve veld), gebruiken ViT’s self-attention om relaties tussen alle beeldpatches te modelleren, wat ze bijzonder nuttig maakt voor taken waarbij ruimtelijke context over de hele afbeelding belangrijk is.
  3. Je voorgetrainde modellen en transfer learning wilt gebruiken - We hebben toegang tot voorgetrainde ViT’s (bijv. via HuggingFace of timm) en fine-tunen wordt daarmee veel praktischer. In dat geval kunnen zelfs middelgrote datasets geweldige resultaten opleveren zonder from scratch te trainen.

Kortom: als je je in een low-resource omgeving bevindt of snelle, betrouwbare resultaten nodig hebt met beperkte data, dan zijn CNN’s de veiligere en efficiëntere keuze. 

Als je project daarentegen diep, globaal redeneren over de visuele ruimte vereist en je de rekenkracht hebt om dat te ondersteunen, dan zijn vision transformers de betere optie. 

Componenten van een Vision Transformer

Laten we nu duiken in de daadwerkelijke architectuur van de Vision Transformer. Omdat dit uit meerdere fasen bestaat (vijf om precies te zijn), bekijken we ze één voor één. 

Fase 1: Afbeelding opdelen in patches

Dit is de eerste transformatieslag, waarin we een afbeelding moeten omzetten naar een vorm die de transformer kan begrijpen.

Stel, we hebben een afbeelding van 224×224. In plaats van die in één keer aan te bieden, verdelen we die in kleinere patches met vaste grootte. Als elke patch 16×16 is (zoals voorgesteld in het oorspronkelijke artikel), dan wordt de afbeelding opgesplitst in 14×14 = 196 patches, omdat (224/16 = 14).

Een makkelijke manier om dit te begrijpen is door je voor te stellen dat je een foto in perfect gelijke, vierkante tegeltjes snijdt. Elke patch bevat gelokaliseerde informatie en fungeert als een "woord" in een zin.

In de bovenstaande illustratie zie je dat de afbeelding van een wolf wordt opgesplitst in kleinere, vierkante (en gelijke) patches — 196 om precies te zijn.

Je zou hier echter één vraag kunnen hebben:

“De afbeelding is RGB, dus wat gebeurt er met de 3 kleurkanalen?”

Dit is een belangrijk punt: elke patch is eigenlijk een datablock dat in ons geval de vorm 16×16×3 heeft. 16×16 is de hoogte en breedte, en 3 zijn de kleurkanalen, waardoor elke patch een 3D-tensor is. 

Dit maakt elke patch een compacte representatie van zowel de ruimtelijke als de kleurinformatie uit een lokaal deel van de afbeelding.

Deze opdeling in patches transformeert de 2D-ruimtelijke structuur van de afbeelding in een 1D-sequentie van multidimensionale data, geschikt voor een transformer-model.

Fase 2: Patch-embedding

Vervolgens moeten we elke afbeeldingspatch omzetten in een numerieke representatie die de transformer kan gebruiken.

Elke 16×16 RGB-patch wordt gevlakt tot één vector. In ons voorbeeld: 16×16×3 = 768 waarden. We rollen in feite alle pixel- en kleurwaarden uit tot één reeks getallen.

Deze ruwe vectors zijn informatief, maar nog niet in een ruimte die geschikt is om te leren. Daarom sturen we ze elk door een trainbare lineaire projectielaag. Zie dit als een dense layer die elke input van 768 dimensies in een nieuwe embedding-ruimte van grootte D projecteert (vaak ook 768, maar niet altijd).

Waarom?

Deze projectie is belangrijk omdat hij alle patch-tokens in een consistente, leerbare indeling brengt die de transformer-encoder verwacht. Tot nu toe hadden we een sequentie van vectors met vaste lengte, waarbij elke vector één patch van de afbeelding vertegenwoordigt.

Dit hele proces transformeert ruimtelijke en kleurrijke patches in een uniforme sequentie van “tokens” (vergelijkbaar met woordembeddings in NLP), waardoor de transformer de afbeelding kan behandelen als een gestructureerde reeks leerbare entiteiten.

Fase 3: Positionele encodering

Er is nog één ding voordat we deze tokens naar onze encoder sturen. Je raadt het misschien al als je eerder LLM’s hebt behandeld!

Transformers zijn van nature positie-onafhankelijk.

Dat betekent dat ze reeksen input behandelen als ongeordende verzamelingen: ze hebben geen ingebouwd mechanisme om de volgorde of positie van tokens te begrijpen. En dat is een groot probleem bij beeldanalyse, waar zowel ruimtelijke structuur als lay-out cruciaal zijn!

Laten we dit in detail bespreken. 

In de context van afbeeldingen vertegenwoordigt elke patch een specifiek gebied van de oorspronkelijke afbeelding. Als we het model een reeks patch-embeddings voeren zonder aan te geven waar elke patch vandaan komt, heeft het geen idee of een patch uit de linkerbovenhoek of de rechteronderhoek komt. 

Dit gebrek aan ruimtelijk bewustzijn zou het vermogen van ons model beperken om patronen, vormen, structuren, enz. te begrijpen.

Om dit op te lossen, voegen we iets toe dat positionele encodering heet aan elke patch-embedding. Dit is een vector die fungeert als een ruimtelijk label; een vector die de oorspronkelijke positie van de patch binnen de afbeelding encodet. Deze positionele vectors worden direct bij de patch-embeddings opgeteld voordat de sequentie de transformer ingaat.

Er zijn twee gangbare typen positionele encoderingen:

  • Vaste (sinusvormige) positionele encodering: Deze gebruiken sinus- en cosinusfuncties met verschillende frequenties om positielinformatie te encoderen. Ze zijn deterministisch en hoeven niet geleerd te worden. Dit werd ook gebruikt in het beroemde artikel “Attention is all you need”.
  • Geleerde positionele encodering: Dit zijn trainbare parameters die het model tijdens training leert. Elke positie in de sequentie krijgt een eigen leerbare vector.

Als dit verwarrend is, helpt het om positionele encoderingen te zien als GPS-coördinaten voor de patches. 

Ze vertellen de transformer de oorspronkelijke positie van het token. Door deze informatie op te nemen, kan het self-attention-mechanisme zowel de inhoud van elke patch als zijn relatieve of absolute positie binnen de afbeelding modelleren.

Deze stap is cruciaal — zonder dit zou de Vision Transformer de structuur van een afbeelding niet kunnen reconstrueren, hoe krachtig het attention-mechanisme ook is.

Fase 4: Transformer-encoderblok

Nu zijn we aangekomen bij het punt waar alle deep learning gebeurt — patroonontdekking, contextmodellering en featurefusie.

Er worden meerdere van deze encoderblokken gestapeld om de volledige Vision Transformer-encoder te vormen. Laten we bekijken wat elk encoderblok bevat:

  • Multi-head self-attention (MHSA): Hiermee kan elke patch met elke andere patch interageren. Het helpt het model te bepalen welke delen van de afbeelding het meest relevant voor elkaar zijn.
    • In plaats van een patch geïsoleerd te bekijken, geeft MHSA globale context.
    • Meerdere attention-koppen bekijken verschillende aspecten van de relaties, wat een rijkere interpretatie van de afbeelding geeft. 
  • Feedforward neural network (FFN): Dit is een kleine Multilayer Perceptron die elk token onafhankelijk verwerkt na attention. De reden hiervoor is om het vermogen van het model te verbeteren om complexe, niet-lineaire patronen te leren.
  • Residuverbindingen en layernorm: Deze stabiliseren de training en helpen gradiënten beter door het netwerk te vloeien.

Over het algemeen worden deze encoderblokken in lagen gestapeld (bijv. 12 in ViT-Base), waarbij elke laag diepere beeldrepresentaties leert.

Fase 5: Classificatietoken en MLP-head

We zijn er bijna! Laten we nu naar de laatste fase van de (vanilla) ViT gaan. 

Na alle encoderlagen hebben we een manier nodig om alles samen te vatten en een voorspelling te doen.

Daarom introduceren we een speciaal [CLS]-token aan het begin van de patchsequentie. Dit is een leerbare embedding die tijdens het encoderen met alle andere tokens interageert.

Terwijl het door de transformerlagen gaat, verzamelt het globale informatie. Aan het eind halen we dit token eruit en sturen het door een andere multilayer perceptron (MLP)-head, meestal een paar dense lagen gevolgd door een softmax (voor classificatie).

Deze laatste stap geeft ons het voorspelde label, zoals “deze afbeelding is een hond”, gebaseerd op de informatie die door het [CLS]-token is verzameld. Belangrijk om te onthouden is dat alleen het [CLS]-token naar deze laatste MLP-head wordt gestuurd en dus wordt gebruikt voor classificatie, niet de andere tokens!

Opvallende Vision Transformer-architecturen

Laten we nu enkele revolutionaire Vision Transformer-architecturen bekijken. 

  • ViT (Google Brain) - Dit was de originele vision transformer (en degene die we in dit artikel volgen) die het patch-gebaseerde transformermodel voor beeldclassificatie introduceerde met grote datasets zoals ImageNet-21k. 
  • DeiT (Data-efficient image Transformer) - Geïntroduceerd door Facebook AI, verbetert DeiT de data-efficiëntie door kennisdistillatietechnieken, waardoor trainen op kleinere datasets mogelijk wordt.
  • Swin Transformer - Gebruikt een hiërarchische architectuur en verschoven vensters om de rekentijd te verminderen en toch de nauwkeurigheid te behouden. Vooral effectief bij dichte predictietaken zoals objectdetectie.
  • Andere varianten - Modellen zoals BEiT, CvT en MobileViT bevatten diverse ontwerpverbeteringen om trainingsefficiëntie, modelcompactheid of inference-snelheid te verbeteren.

Voorgetrainde Vision Transformers gebruiken 

Vaak trainen we een ViT niet from scratch! Laten we dus verkennen hoe we voorgetrainde ViT-modellen kunnen gebruiken uit populaire deep learning-bibliotheken.

Hugging Face Transformers-bibliotheek

Een van de makkelijkste en meest betrouwbare manieren om vision transformers te gebruiken is via de Hugging Face Transformers-bibliotheek. Die biedt voorgetrainde ViT-modellen die je met slechts een paar regels code kunt gebruiken. 

Ik heb hier wat code geschreven om het originele ViT Base-model te downloaden en te gebruiken:

# Imports neededfrom transformers import ViTImageProcessor, ViTForImageClassificationfrom PIL import Imageimport torchimport requests# Here I am loading and preprocessing an image - but you can use any image of your choiceimage = Image.open(requests.get(https://huggingface.co/datasets/huggingface/cats-image?image-viewer=ED6CB286C90CF5F1FAF278077DF091477DF05416', stream=True).raw)# Here, we load and process the ViT modelprocessor = ViTImageProcessor.from_pretrained('google/vit-base-patch16-224')model = ViTForImageClassification.from_pretrained('google/vit-base-patch16-224')# Preprocessing the image so it gets it into the necessary formatinputs = processor(images=image, return_tensors='pt')# Forward pass - with no backpropagationwith torch.no_grad():    outputs = model(**inputs)# Here we get the predicted labels (since we are performing classification)logits = outputs.logits	predicted_class = logits.argmax(-1) # Selects the index of the class with the highest logit score (highest predicted probability)print(f"Predicted class index: {predicted_class.item()}")

Laten we deze code iets uitgebreider bespreken. Je hebt misschien een vraag over het downloaden van twee afzonderlijke componenten. Deze zijn gescheiden, maar vullen elkaar aan:

  1. ViTImageProcessor – Deze is verantwoordelijk voor het preprocessen van de afbeelding voordat die aan het model wordt doorgegeven. Hij verzorgt het resizen, normaliseren en het omzetten van de afbeelding naar het formaat dat de vision transformer verwacht. Zie dit als het vision-equivalent van een tokenizer in NLP!
  2. ViTForImageClassification – Dit is het daadwerkelijke model dat inference uitvoert. Het verwacht inputs in een specifiek tensorformaat en geeft voorspellingen terug (bijv. logits of klassekansen).

Een andere vraag die je zou kunnen hebben, gaat over output.logits en specifieker: wat logits zijn. 

In machinelearningmodellen — vooral classificatiemodellen — zijn logits de ruwe outputwaarden die door de laatste laag van het model worden geproduceerd vóór toepassing van een activatiefunctie zoals softmax. Ze vertegenwoordigen ongenormaliseerde scores voor elke klasse.

Je kunt ze zien als de interne zekerheidsniveaus van het model voor elke klasse. Deze waarden kunnen positief of negatief zijn en hoeven niet op te tellen tot één.

Om logits om te zetten in kansen, passen we de softmax-functie toe, die de waarden schaalt naar een waarschijnlijkheidsverdeling.

Dus wanneer we argmax(logits, -1) gebruiken, kiezen we simpelweg de index (klasse) met de hoogste ruwe score, wat meestal overeenkomt met die met de hoogste kans na softmax.

De timm-bibliotheek gebruiken

Een andere geweldige en bekende bibliotheek voor het gebruiken van vision transformers is timm (door Ross Wightman). Deze biedt ook een brede variëteit aan voorgetrainde ViT-varianten en is zeer geschikt voor snelle experimenten of fine-tunen. Zo laad en gebruik je een ViT-model voor inference:

# Relevant importsimport timmimport torchfrom torchvision import transformsfrom PIL import Imageimport requests# Loading an imageurl = 'https://huggingface.co/datasets/huggingface/cats-image?image-viewer=ED6CB286C90CF5F1FAF278077DF091477DF05416'image = Image.open(requests.get(url, stream=True).raw)# Here we have defined the transformation we are going to do on the imagetransform = transforms.Compose([    transforms.Resize((224, 224)),    transforms.ToTensor(),    transforms.Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5])])image_tensor = transform(image).unsqueeze(0)  # Adding batch dimension# Loading the ViT modelmodel = timm.create_model('vit_base_patch16_224', pretrained=True)model.eval() # Setting it to evaluation mode# Inference componentwith torch.no_grad():    outputs = model(image_tensor)    predicted_class = outputs.argmax(dim=1) # Obtaining the class with the highest probabilityprint(f"Predicted class index: {predicted_class.item()}")

Je ziet nu vast de overeenkomst tussen timm en de transformers-bibliotheek. Beide bieden voorgetrainde ViT-modellen, ondersteunen beeldverwerking (al gebruikten we in timm standaard PyTorch-code) en uiteindelijk verkrijgen we de klasse met de hoogste logit.

Conclusie

Al met al hebben vision transformers computer vision veranderd door afbeeldingen om te zetten in “zinnen” van patches en self-attention te gebruiken om vanaf de allereerste laag globale context vast te leggen. In deze tutorial hebben we geleerd over:

  1. Waarom ViT’s verschillen van CNN’s — minimale inductieve bias, globale receptieve velden, enorme schaalbaarheid, enz.
  2. Wanneer je elk model kiest — CNN’s blinken uit met beperkte data en strakke latentie-eisen, terwijl ViT’s beter zijn wanneer we veel data en krachtige hardware hebben.
  3. Onderliggende ViT-architectuur — We hebben alle verschillende fasen behandeld, van het opdelen van afbeeldingen en geleerde positionele encoderingen tot multi-head self-attention, residupaden en het [CLS]-token voor classificatie.
  4. Praktische implementatie — Hoe je voorgetrainde modellen uit verschillende bibliotheken laadt — Hugging Face of timm —, afbeeldingen preprocesset en voorspellingen extraheert.

Om verder te leren raad ik je dit artikel aan over Hoe Transformers werken, dat je de theorie achter Transformers leert. Om dit te consolideren raad ik daarna aan om te verkennen hoe je een transformer from scratch bouwt.  

Wil je beide tegelijk leren en direct hands-on aan de slag? Dan is onze cursus Transformer Models with PyTorch the place to go.

Vision Transformer FAQ's

Waarom hakken Vision Transformers afbeeldingen in 16 × 16 patches in plaats van ruwe pixels te lezen?

Patching zet de 2D-afbeelding om in een 1D-“zin” van tokens, zodat de transformer dezelfde self-attention-trucs kan toepassen die taalmodellen aandrijven.

Hebben ViT’s altijd miljoenen afbeeldingen nodig om goed te werken?

Niet altijd; we kunnen beginnen met een voorgetrainde ViT en fine-tunen. Daarvoor zijn vaak enkele duizenden gelabelde afbeeldingen genoeg voor sterke resultaten.

Hoe “weten” ViT’s waar elke patch vandaan kwam?

Ze voegen positionele encoderingen toe aan elk patch-token, waardoor het model een gevoel van ruimtelijke ordening krijgt.

Wanneer moet ik nog steeds een CNN kiezen boven een ViT?

We kiezen voor CNN’s wanneer data schaars is of we ultrasnelle, geheugenarme inference op edge-apparaten nodig hebben.

Kan ik visualiseren waarop een ViT zich focust?

Ja, we kunnen attention-maps extraheren, opschalen en als heatmap overlappen om te laten zien welke patches de voorspelling sturen.

Vaibhav Mehra

Auteur

Vaibhav Mehra

Onderwerpen

Gerelateerd