The problem in brief
StyleGAN2, trained on the FFHQ dataset of faces, turns a 512-dimensional vector (its "W" latent space) into a realistic 1024x1024 portrait. You receive 2,000 such vectors, each labelled 1 if the generated face smiles and 0 otherwise. The task (50 points) is to train a single linear layer that predicts the label from (20 points), take its weight vector as a "smile direction", and edit a new face by moving its latent code along that direction (25 points), then show the strip of edited faces (5 points). The notebook comes from a third-party repository (github.com/teodora-dimitrova/Singapore-Team-Selection), which also contains a solution notebook that we did not use. The deliverable is code plus pictures, so there is no official number; we added an automatic evaluation with CLIP to compare directions objectively.
How you are scored
Points are given per question for working code: 10 for the classifier, 10 for the training loop, 10 for extracting the direction, 15 for the editing function and 5 for a strip that runs. There is no accuracy threshold. To go beyond "it runs", we measured two things on 24 random faces, for each direction and edit strength:
- Smile: CLIP's probability for "a photo of a smiling person." against "a photo of a person with a neutral, serious face.".
- Identity kept: the cosine similarity between the CLIP image embeddings of the edited face and the original face (1 means unchanged).
A good edit raises the first number a lot while keeping the second close to 1.
First look at the data
- The latents are real W vectors of this generator: their mean has a cosine of 0.996 with the generator's own
w_avg. - The labels are imbalanced: 80.9 % of the faces are labelled as smiling. A classifier that always says "smile" is right 80.9 % of the time.
- The signal is weak. The best L2-regularised logistic regression reaches a 5-fold cross-validated accuracy of 82.2 % and an AUC of 0.78, barely above the majority rule in accuracy. The notebook's recipe (Adam, 40 epochs, no regularisation) reaches 86.5 % on the training set, which is mostly memorisation: with 512 weights and 2,000 points, an unregularised linear model fits noise.
This matters for the edit, because the edit does not use the classifier's predictions, it uses its weight vector. A noisy weight vector is a noisy direction.
The notebook's answers
The four blanks are one or two lines each:
self.fc = nn.Linear(latent_dim, output_dim) # Q1.1
optimizer.zero_grad() # Q1.2
loss = criterion(model(latents_batch), labels_batch)
loss.backward()
optimizer.step()
direction = model.fc.weight.detach() # Q2.1, then divide by its norm
w_edited = w_start + strength * direction # Q2.2
img = g.synthesis(w_edited, noise_mode="const") # w_start is (1, 18, 512)
Two details. generator.mapping already returns w repeated for the 18 synthesis layers, shape , so the direction broadcasts over the 18 layers and w_edited can go straight into synthesis. And noise_mode="const" freezes StyleGAN's per-pixel noise, so that the frames of the strip differ only by the edit.
The key idea
Why the weight vector is a direction. A linear classifier predicts smile when . Its decision boundary is the hyperplane , and is the normal to that hyperplane: it is the direction in which the smile logit increases fastest, by per unit step. Moving with changes the logit by exactly and, if W space is well organised, changes the image mostly in the attribute the classifier was trained on. This is the InterFaceGAN idea.
Which estimate of the direction? Three natural candidates:
- the notebook's unregularised classifier;
- an L2-regularised logistic regression (, the best cross-validated AUC);
- the plain difference of class means, .
The difference of means looks simplest, but it is the worst. For Gaussian classes with a shared covariance , the optimal linear boundary has normal
The mean difference is : it is the true direction stretched along the high-variance directions of W. Any attribute that varies a lot and happens to correlate with smiling in these 2,000 faces (age, hair, glasses, pose) leaks into it. A classifier estimates itself, which "explains away" those correlations. The figure shows it: the mean-difference row changes the man's age, hair and glasses, while the classifier rows mostly change the mouth and cheeks.
Stability. Refitting each estimator on bootstrap resamples of the 2,000 latents, the average cosine between two refits is 0.68 for the notebook's classifier, 0.72 for the regularised one () and only 0.55 at . Regularisation is what makes the direction reproducible. The mean difference is very stable (0.88), but stably biased: stability is not correctness.
Editing only some layers. StyleGAN feeds separately to each of its 18 synthesis layers: the first layers set coarse structure (pose, face shape), the middle ones features such as the mouth and eyes, the last ones colour and fine texture. Adding the direction only to layers 4 to 7 targets the expression and leaves the rest of the face alone.

The full solution
- Convert the third-party files safely.
latent_labels.ptandffhq.pklare pickles, which can execute code when loaded. We loaded them once inside a sandbox without network access and re-saved them as.npy,.safetensorsand a JSON of the generator's constructor arguments; the rest of the pipeline rebuilds the generator from the official NVlabs code and these plain tensors. - Fill in the notebook (classifier, training loop, normalised weight, edit), which reproduces the Q3 strip.
- Improve the direction: L2-regularised logistic regression with chosen by cross-validated AUC.
- Restrict the edit to synthesis layers 4 to 7.
- Evaluate on 24 random faces with CLIP, at strengths .
Generating one 1024x1024 face takes about 0.4 s on an Apple laptop GPU (StyleGAN2-ADA's custom CUDA kernels fall back to their reference PyTorch implementations).

Results
Mean over 24 random faces (unedited: CLIP P(smile) = 0.56).
| direction | layers | P(smile) at -6 | P(smile) at +6 | identity at +6 | identity at +3 |
|---|---|---|---|---|---|
| mean difference of classes | all 18 | 0.23 | 0.77 | 0.75 | 0.87 |
| notebook classifier (Adam, 40 epochs) | all 18 | 0.22 | 0.82 | 0.89 | 0.94 |
| logistic regression, | all 18 | 0.13 | 0.87 | 0.82 | 0.92 |
| logistic regression, | 0-5 | 0.14 | 0.83 | 0.88 | 0.94 |
| logistic regression, | 4-7 | 0.12 | 0.89 | 0.90 | 0.96 |
The regularised direction on layers 4 to 7 gives the strongest change of expression in both directions while keeping the face closest to the original. The notebook's own direction works (it earns the points) but changes less smile per unit of identity lost, and the mean difference is clearly the most entangled. These are proxy measures: CLIP's notion of "smiling" and of "same person" is not a human judgement, and on some faces (like the notebook's seed 123) the layer-restricted edit is visibly subtle.
What to remember
- The weight of a linear classifier is the normal to its boundary: it points along the attribute, which is why it can serve as an editing direction in a well-structured latent space.
- The class-mean difference is not the discriminative direction. It equals and is pulled towards high-variance, correlated attributes; a classifier (or , i.e. LDA) disentangles them.
- Regularise when you will use the weights themselves. With 512 dimensions and 2,000 noisy labels, an unregularised fit is unstable; choose the penalty by cross-validation, and check stability with bootstrap refits.
- Use the structure of the generator. StyleGAN's per-layer inputs let you edit only the layers that control the attribute.
- Turn "looks good" into numbers. A frozen judge like CLIP, scoring both the attribute and the preservation of the rest, makes generative edits comparable; and load third-party pickles only in a sandbox, converting them to plain tensors once.