The problem in brief
You are handed a PyTorch implementation of a convolutional sentence classifier "based on" Kim (2014), Convolutional Neural Networks for Sentence Classification, trained on MR: 10 662 one-sentence movie-review snippets labelled positive or negative. It reaches less than 60 % accuracy, while the paper reports 76.1 % for its simplest variant. Task 1: read the paper and fix four deviations (dropout, optimiser, pooling, filter sizes and batch size), explaining the optimiser change. Task 2: improve further with a component or architecture change of your choice (pretrained embeddings, recurrent networks or a Transformer), justify it and run it.
How you are scored
A selection test graded by the coaches: each step must be implemented and run, with the logs kept, and the written answers must be correct. The quantitative yardstick is test accuracy on the fixed split of the starter notebook (the first 80 % of each class for training, the next 10 % for validation, the last 10 % for test: 8 530 / 1 066 / 1 066 sentences), with the model chosen on validation accuracy:
Targets from the paper (which uses 10-fold cross-validation, so numbers are comparable but not identical): 76.1 % for CNN-rand (random embeddings), 81.0 to 81.5 % with pretrained word2vec vectors.
First look at the data
- Perfectly balanced classes (5 331 each): accuracy is a fair metric and 50 % is chance.
- Sentences are short, 21 tokens on average, at most 59: a whole review fits in one window of a few dozen tokens.
- Sentiment is carried by a few phrases ("a gorgeous, witty, seductive movie", "not worth the price"), wherever they appear in the sentence. That observation decides the pooling question below.
- The training set is small (8 530 sentences) for learning 300-dimensional embeddings of 18 000 words from scratch: pretrained knowledge should matter a lot.
A first baseline
The starter, run as given (mean pooling, filters of width 2/3/4 with 50 maps, no dropout, plain SGD with the default learning rate , batch 25, 50 epochs, words seen fewer than 5 times replaced by <unk>): 59.8 % test accuracy, 60.4 % on validation. Its training accuracy is still only 63 % after 50 epochs (figure below): the model is not overfitting, it is barely learning. That diagnosis already points at the optimiser.
The key idea
Task 1 is about matching the paper detail by detail, and each detail has a reason.
Step 1, dropout. Kim applies dropout with on the penultimate layer: the vector of pooled filter outputs, just before the final linear layer. During training each of the 300 features is zeroed with probability 0.5, so the classifier cannot rely on one co-adapted detector. The paper also rescales and clips the output weights to an L2 norm of at most (max-norm); we implement it too.
Step 2, the optimiser: Adadelta instead of plain SGD. SGD applies the same global step to every parameter, , and with that step is far too small here. Adadelta (Zeiler, 2012) keeps two running averages per parameter, of squared gradients and of squared updates, with decay :
The differences that matter: (i) the step size is per parameter and adapts to the scale of its gradients, which is ideal for embeddings, where a rare word's row receives a gradient only a few times per epoch and still gets a meaningful update; (ii) the ratio of the two RMS terms has the units of the parameter itself, so there is no learning rate to tune (PyTorch's lr=1.0 is the paper's setting); (iii) it is robust to badly scaled gradients across layers. With it, training converges in a few epochs instead of crawling.
Step 3, pooling: max over time instead of mean. Each filter is a detector of one n-gram pattern; its activation along the sentence is high where the pattern occurs. Max pooling keeps the strongest occurrence, wherever it is, and ignores the length of the sentence. Mean pooling dilutes one decisive phrase among all the neutral positions, so "boring" in a 40-token sentence weighs four times less than in a 10-token one. For "is this pattern present?", max is the right summary.
Step 4, filters and batch. Widths 3, 4, 5 with 100 feature maps each (a 300-dimensional sentence vector) and mini-batches of 50.
Two further paper details turned out to matter as much as the four steps: the vocabulary keeps every training word (min_freq=1, the starter used 5), and random embeddings are drawn from . With PyTorch's default initialisation the same model reached only 68.5 %: Adadelta's first updates are tiny (), so vectors that start 7 times too large barely move. Matching the paper's initial scale brought us to 76 %.
Task 2: give the model knowledge it cannot learn from 8 530 sentences.
- Pretrained word vectors (our first choice, the paper's CNN-non-static): initialise the embedding layer with GloVe 6B 300-d vectors, trained on 6 billion tokens of Wikipedia and news, and keep fine-tuning them. "Superb" and "terrific" then start close together even if only one appears in training. Only the embedding initialisation changes; 16 159 of our 18 064 words have a GloVe vector, the others stay random in .
- A pretrained Transformer (second choice): replace the whole CNN by DistilBERT, a 6-layer Transformer pretrained by masked language modelling, with a linear classifier on its
[CLS]representation, fine-tuned end to end (AdamW, learning rate , linear warm-up and decay, 2 epochs). Self-attention reads the sentence in context, so it can handle negation and contrast ("not bad at all", "pretty but empty"), which a bag of local n-gram detectors misses.
The full solution
solution.py re-implements the notebook without the deprecated torchtext (the basic_english tokenizer is re-written in 10 lines) and runs every variant with the same data split:
class CNN(nn.Module):
def forward(s, ids):
x = s.emb(ids).permute(0, 2, 1) # (batch, 300, length)
h = [torch.relu(conv(x)) for conv in s.convs] # widths 3, 4, 5, 100 maps each
h = [t.max(dim=-1).values for t in h] # Step 3: max over time
return s.fc(s.drop(torch.cat(h, -1))) # Step 1: dropout 0.5 before fc
opt = torch.optim.Adadelta(model.parameters(), lr=1.0, rho=0.95, eps=1e-6) # Step 2
# after each step: rescale rows of fc.weight to L2 norm <= 3 (paper's max-norm)
Sentences are padded to at least 5 tokens so that the widest filter always has a window. Every model keeps the epoch with the best validation accuracy, and the test set is evaluated once at the end. CNN runs use 2 seeds and report both.
Results
| model | validation | test |
|---|---|---|
| starter as given (mean pooling, SGD , no dropout) | 0.604 | 0.598 |
| Task 1 with PyTorch's default embedding init (failed attempt) | 0.687 | 0.685 |
| Task 1: Kim CNN-rand (seeds 0 / 1) | 0.751 / 0.745 | 0.760 / 0.755 |
| Task 2a: + GloVe 300-d, fine-tuned (seeds 0 / 1) | 0.787 / 0.781 | 0.794 / 0.783 |
| Task 2b: DistilBERT fine-tuned, 2 epochs | 0.853 | 0.844 |
Task 1 reproduces the paper's 76.1 % almost exactly (mean 75.8 %). Pretrained word vectors add 3 points (paper: about 5 with word2vec); a pretrained Transformer adds another 5.6, for a total gain of 24.5 points over the starter. Fine-tuning DistilBERT took 9 minutes on a laptop GPU (Apple MPS).

The curves tell the story of the two tasks. The starter is under-fitting: wrong optimiser and step size, and mean pooling, which washes out the signal. The Kim CNN fits the training set perfectly in under 10 epochs and then overfits (validation stops improving while training reaches 100 %, despite dropout): it has learnt all it can from 8 530 sentences. Beyond that point the only lever is outside knowledge, which is exactly what pretrained embeddings and pretrained Transformers bring.
What to remember
- Read the training curve before touching the model: low training accuracy means under-fitting (optimiser, learning rate, capacity, pooling); training at 100 % with validation stuck means over-fitting (regularisation, more data, pretraining).
- Max pooling detects "is the pattern anywhere?"; mean pooling measures "how much of the sentence is the pattern?". Choose the summary that matches the question.
- Adaptive optimisers (Adadelta, Adam) give each parameter its own step size, which matters most for sparse embedding gradients.
- Reproducing a paper means reproducing all of it, including "minor" details such as initialisation scale and vocabulary thresholds; here the initialisation alone was worth 7 points.
- Small datasets are won by transfer: pretrained word vectors help, a pretrained Transformer fine-tuned for two epochs helps much more.