AI Computer Institute
Expert-curated CS & AI curriculum aligned to CBSE standards. A bharath.ai initiative. About Us

Neural Style Transfer: Artistic Image Generation

📚 Computer Vision⏱️ 20 min read🎓 Grade 11
✍️ AI Computer Institute Editorial Team Updated: September 2026 CBSE-aligned · Peer-reviewed · 20 min read
Content curated by subject matter experts with IIT/NIT backgrounds. All chapters are fact-checked against official CBSE/NCERT syllabi.

In 2016, an app called Prisma turned into the fastest-growing photo app in India almost overnight — students at IIT and NIT campuses, cricket fans at stadiums, wedding photographers, all feeding ordinary phone photos into it and getting back paintings that looked like they belonged to Van Gogh or a Cubist sketchbook. The app's founding trick came directly from a 2015 paper, "A Neural Algorithm of Artistic Style" by Leon Gatys, Alexander Ecker, and Matthias Bethge. What Prisma shipped for mobile users was eventually a fast, single-pass network — but the idea it was built on, and the idea this chapter teaches, is the original, slower, mathematically transparent algorithm: given a content photograph and a style painting, synthesize a new image that keeps the content photograph's layout but repaints it in the painting's texture, palette, and brushwork.

The question worth sitting with before any code: how does an algorithm even separate "what is in the photograph" from "what the painting looks like"? Nothing in a JPEG file distinguishes those two things — pixels are just pixels. The answer turns out to live inside a convolutional network you have already studied for classification, repurposed in a way its original designers never intended.

Two Representations Buried Inside One Network

Take a convolutional network trained for image classification — the Gatys paper uses VGG-19, a 19-layer CNN trained on ImageNet. Feed it a photograph. At any convolutional layer l, the network produces a stack of feature maps: if that layer has N_l filters and each filter's output is a grid of H × W values, you get N_l grids of size H × W. Flatten each grid into a vector of length M_l = H × W, and the whole layer's output is a matrix F^l ∈ R^(N_l × M_l), where row i is the flattened output of filter i. Two independent facts about this matrix turn out to be exploitable.

Fact one — content lives in the raw activations, positionally. A deep layer's feature map still has spatial structure: if a face was in the top-left of the photo, filters that respond to face-like patterns activate in the top-left region of their output grid. Reconstructing an image that produces similar activations at a deep layer, position for position, reconstructs the coarse layout of the original — where things are, roughly what shapes they are — while discarding exact pixel values. Gatys et al. pick layer conv4_2 for this: deep enough that low-level noise and color are gone, shallow enough that the arrangement of shapes survives.

Fact two — style lives in the correlations between filters, independent of position. This is the less obvious move. Instead of asking "does filter 12 fire in the top-left," ask "when filter 12 fires strongly, does filter 47 also tend to fire strongly, averaged over the whole image?" If a painting has a signature co-occurrence — thick horizontal brushstrokes always paired with a particular ochre tone — filters sensitive to those two properties will have correlated activation across the entire spatial grid, regardless of where in the painting that stroke-and-tone pair shows up. Measuring these pairwise co-occurrences, summed over every spatial position, throws away exactly the information that fact one kept (position), and keeps exactly the information fact one discarded (texture statistics, recurring patterns). That measurement is called the Gram matrix.

The Gram Matrix: Capturing "Style" Without "What"

For layer l with feature matrix F^l ∈ R^(N_l × M_l), the Gram matrix is

G^l_{ij} = Σ_k F^l_{ik} · F^l_{jk}      for i, j = 1 … N_l

In words: take filter i's flattened output vector and filter j's flattened output vector, and take their dot product — summed over every spatial position k. Do this for every pair (i, j), including i = j. The result is an N_l × N_l matrix, completely independent of M_l (the spatial size). This is the mathematical reason the Gram matrix is position-blind: index k — the only index that carried spatial location — gets summed away entirely. What survives is purely "how much do filter i and filter j agree, on average, across this whole image."

Style loss compares the generated image's Gram matrix against the style image's Gram matrix, at several layers simultaneously — the original paper uses conv1_1, conv2_1, conv3_1, conv4_1, conv5_1, one from each of VGG-19's five convolutional blocks. Shallow layers capture fine texture (brush grain, small color speckling); deep layers capture larger repeating structure (the rhythm of a whole canvas). Matching Gram matrices across all five gives style consistency at every scale, not just one.

Worked Example: Computing Gram Matrices and Losses by Hand

Work through one layer with small, tractable numbers. Suppose at some convolutional layer, the feature representation has N_l = 2 channels, and the spatial grid is 2 × 2 (so M_l = 4 after flattening).

The generated image's feature matrix at this layer:

F = [ [1, 2, 0, 1],     ← channel 1, flattened
      [0, 1, 2, 1] ]    ← channel 2, flattened

Step 1 — Gram matrix of the generated image. Compute every pairwise dot product:

G_11 = (1)(1) + (2)(2) + (0)(0) + (1)(1) = 1 + 4 + 0 + 1 = 6
G_22 = (0)(0) + (1)(1) + (2)(2) + (1)(1) = 0 + 1 + 4 + 1 = 6
G_12 = G_21 = (1)(0) + (2)(1) + (0)(2) + (1)(1) = 0 + 2 + 0 + 1 = 3

G = [ [6, 3],
      [3, 6] ]

Step 2 — compare against the style image's Gram matrix at the same layer. Say the style image (computed once, in advance, and held fixed) gives A = [[8, 2], [2, 8]] at this layer. The per-layer style loss formula from the paper is:

E_l = 1 / (4 · N_l² · M_l²) · Σ_{i,j} (G_{ij} − A_{ij})²

Sum of squared differences: (6−8)² + (3−2)² + (3−2)² + (6−8)² = 4 + 1 + 1 + 4 = 10. With N_l = 2 and M_l = 4, the normalizer is 4 · 2² · 4² = 4 · 4 · 16 = 256. So E_l = 10 / 256 = 0.0390625.

Step 3 — content loss at a separate (deeper) layer. Say the content layer has a single channel for simplicity, with content-image activation P = [1, 2, 0, 1] and generated-image activation F_content = [1.2, 1.9, 0.1, 0.8]. The content loss is:

L_content = ½ · Σ (F_content − P)²

Differences: 0.2, −0.1, 0.1, −0.2. Squares: 0.04, 0.01, 0.01, 0.04, summing to 0.10. Halved: L_content = 0.05.

Step 4 — combine. Total loss is L_total = α · L_content + β · E_l (in practice, style loss sums E_l across five layers with equal weight 1/5 each — here treat this single layer as standing in for that sum). Using the paper's typical order of magnitude, α = 1, β = 1000:

L_total = 1 × 0.05 + 1000 × 0.0390625 = 0.05 + 39.0625 = 39.1125

Notice how badly the two losses are scaled relative to each other before weighting — a single style layer's raw loss (0.039) already dwarfs the content loss (0.05) once multiplied by a β in the thousands. This is not incidental: Gram matrix entries involve sums of products of many activations, so they are naturally larger and more sensitive than direct activation differences. If α and β were both set to 1, the optimizer would essentially ignore content entirely and produce a textured mess with no recognizable photo underneath. Choosing the α : β ratio (Gatys et al. use roughly 1 : 1000 to 1 : 10000 depending on the desired stylization strength) is the single most consequential hyperparameter decision in this algorithm, and this hand-computed example is exactly why.

Now confirm the arithmetic in code, using the same numbers so every value can be checked against the hand computation above:

import torch

# Toy feature maps at one layer: 2 channels, each a flattened 2x2 spatial grid
F = torch.tensor([[1., 2., 0., 1.],
                   [0., 1., 2., 1.]])

def gram_matrix(F):
    # F has shape (N_l, M_l): N_l channels (rows), M_l spatial positions (cols)
    return F @ F.T   # shape (N_l, N_l)

G = gram_matrix(F)
print(G)
# tensor([[6., 3.],
#         [3., 6.]])

A = torch.tensor([[8., 2.],
                   [2., 8.]])         # style image's Gram matrix at this layer (fixed, precomputed)

N_l, M_l = F.shape                    # N_l = 2, M_l = 4
style_loss_l = torch.sum((G - A) ** 2) / (4 * N_l**2 * M_l**2)
print(style_loss_l.item())
# 0.0390625

P = torch.tensor([1., 2., 0., 1.])           # content image activation at a deeper layer
F_content = torch.tensor([1.2, 1.9, 0.1, 0.8])  # generated image activation, same layer

content_loss = 0.5 * torch.sum((F_content - P) ** 2)
print(content_loss.item())
# 0.05

alpha, beta = 1, 1000
total_loss = alpha * content_loss + beta * style_loss_l
print(total_loss.item())
# 39.1125

F @ F.T is exactly the Gram matrix formula written as matrix multiplication instead of an explicit double sum — row i of F dotted with row j of F is precisely Σ_k F_ik F_jk. Every printed value above matches the hand computation, because it is the same computation.

The Optimization Loop: What Actually Gets Trained

This is where neural style transfer breaks a habit every student has built up from ordinary deep learning: normally, you fix the input and train the weights. Here, it is reversed. VGG-19's weights are frozen for the entire process — loaded once, pretrained on ImageNet, never touched again. The thing being optimized is the pixels of the output image itself.

  1. Initialize the generated image x — either as random noise, or (faster to converge) as a copy of the content image.
  2. Forward-pass x through the frozen VGG-19 to get its activations at the content layer and its Gram matrices at the style layers.
  3. Compute L_total = α · L_content(x) + β · L_style(x) against the precomputed content-image activations and style-image Gram matrices.
  4. Backpropagate — but instead of computing gradients with respect to the network's weights, compute ∂L_total / ∂x, the gradient with respect to every pixel of the generated image.
  5. Update x ← x − η · ∂L_total/∂x (the original paper uses L-BFGS; Adam works too) and repeat for hundreds of iterations.

Each iteration nudges the pixel grid slightly toward lower loss, the same gradient-descent machinery used everywhere else in deep learning — just pointed at the input instead of the parameters. This is also precisely why the original algorithm is slow: producing one stylized image means running hundreds of forward-and-backward passes through VGG-19, for that one image alone. A mobile app serving millions of photos a day cannot afford that per-photo optimization loop, which is why production systems (including what apps like Prisma moved to) replaced it with a feed-forward network trained once, per style, to output a stylized image in a single pass — a different architecture built on the same content/Gram-matrix loss ideas taught here.

Diagram: The Neural Style Transfer Pipeline

Content Image (the photograph) Generated Image x pixels ARE the parameters (init: noise or content copy) Style Image (the painting) VGG-19 (frozen — weights fixed) VGG-19 (same frozen weights) VGG-19 (same frozen weights) P = conv4_2(content) F = conv4_2(gen.) G = Gram(gen.), 5 layers A = Gram(style), 5 layers Content Loss ½ · Σ (F − P)² at conv4_2 Style Loss Σₗ wₗ · (G−A)² / (4·Nₗ²·Mₗ²) Total Loss α · L_content + β · L_style ∂L_total / ∂x — gradient reaches ONLY the generated image's pixels repeat for ~hundreds of steps VGG-19's own weights never appear in any update — only x changes, iteration after iteration.

A Common Misconception

The mistake almost every student makes on first exposure: assuming neural style transfer trains the CNN on the style image — as if VGG-19 "learns" Van Gogh's brushwork the way it once learned to recognize cats, and then applies that learned knowledge to a new photo. This is wrong, and it matters, because it misdescribes what gradient descent is even doing here.

VGG-19's weights are loaded once from ImageNet pretraining and never change for the rest of the algorithm — not during "learning the style," not during "applying" it, not at any point. There is no training phase separate from an inference phase, because there is no phase where VGG-19's parameters move at all. The only tensor that receives gradient updates is the generated image x — literally the pixel grid of the output picture. The network's role is fixed and purely instrumental: it is a feature extractor used to measure how far the current pixel grid is from matching a content target and a style target, iteration after iteration. Style transfer of this kind is closer to solving an optimization problem where the "unknown" is an entire image, than it is to training a model in the usual sense of the word.

Active Recall

Attempt each question before reading its answer.

  1. In the original Gatys algorithm, what quantity does gradient descent actually update — the CNN's weights, or the generated image's pixels? Why does this make the algorithm slow to run per image?
  2. A layer produces feature maps for N_l = 2 channels over a flattened spatial size of M_l = 3: channel 1 = [2, 0, 1], channel 2 = [1, 1, 0]. Compute the full 2×2 Gram matrix.
  3. Explain, using the formula G^l_{ij} = Σ_k F^l_{ik} F^l_{jk}, exactly which index being summed away is responsible for the Gram matrix losing spatial information — and why content loss, which does not sum over that index, keeps it.
  4. Why must the content image, the style image, and the generated image all be passed through the same frozen VGG-19 (identical weights), rather than three separately trained networks?
  5. If a student sets β (style weight) to be 100,000 times larger than α (content weight), what will the output image look like, qualitatively, and why does the worked example's arithmetic predict this?
  6. Why does the algorithm take content loss from one deep layer (conv4_2) but take style loss from five layers spanning shallow to deep (conv1_1 through conv5_1)?

Answers

  1. The pixels of the generated image, x. VGG-19's weights stay frozen throughout. This is slow because producing a single output image requires running hundreds of full forward-and-backward passes through the network for that one image alone — there is no reusable "trained model" that stylizes new photos instantly; every new content/style pair restarts the optimization from scratch.
  2. G_11 = 2² + 0² + 1² = 4+0+1 = 5. G_22 = 1² + 1² + 0² = 1+1+0 = 2. G_12 = G_21 = (2)(1) + (0)(1) + (1)(0) = 2+0+0 = 2. So G = [[5, 2], [2, 2]].
  3. Index k ranges over spatial positions (the flattened H×W grid). The Gram matrix formula sums over k, collapsing all spatial positions into one number per channel pair (i,j) — so two images with the same texture statistics arranged completely differently in space produce the same Gram matrix. Content loss, ½Σ(F−P)², sums over the same spatial index but never eliminates it from the underlying comparison — it compares F and P position-by-position, channel-by-channel, before summing the squared differences, so activations must match at the same spatial location to score low loss, which is exactly what "preserving layout" requires.
  4. Because the content loss, style loss, and Gram matrices only make sense as comparable numbers if they were produced by an identical feature-extraction function. If the three images went through differently-weighted networks, a difference in activations could come from the networks themselves differing, not from the images differing — the whole comparison (is this generated image close to this content image at conv4_2?) would be meaningless. A single frozen, shared VGG-19 guarantees the only variable between the three forward passes is the image.
  5. The output will look almost purely textural — heavy stylization with the underlying photograph essentially unrecognizable, because the optimizer barely has to reduce content loss to make it negligible relative to the enormous style term. The worked example showed style loss (0.039) already outweighing content loss (0.05) by three orders of magnitude once multiplied by a β in the thousands; multiplying by 100,000× the α weight instead of roughly 1,000× only worsens that imbalance, so gradient descent will spend almost all its effort lowering style loss and will tolerate large increases in content loss to do it.
  6. Content needs one deep layer because deep layers have discarded low-level pixel noise while still preserving coarse spatial arrangement — exactly the "what is where, roughly" information content loss wants. Style is sampled across five depths because texture exists at multiple scales simultaneously: a shallow layer like conv1_1 captures fine-grained texture (brush grain, small color transitions), while a deep layer like conv5_1 captures large repeating structure (compositional rhythm across the canvas). Matching Gram matrices at only one depth would reproduce style at only one scale; matching across all five reproduces it faithfully at every scale a viewer would notice.

Think About It

Think about this: How would you explain neural style transfer: artistic image generation to a friend who has never seen a computer? What real-world analogy would you use? Imagine you had to build a system using these concepts — what would be your first step? Try this: before moving on, write down three things you learned and one question you still have.

Key Takeaways — Summary and Recap

Let us recap what we covered: the core ideas behind neural style transfer: artistic image generation, how they connect to real-world applications, and why they matter for your journey in computer science. Remember these key points as you move forward. For competitive exam preparation (CBSE, JEE, BITSAT), focus on understanding the WHY behind each concept, not just the WHAT.

← StyleGAN: Style Transfer in GenerationOptical Flow: Estimating Motion →

Found this useful? Share it!

📱 WhatsApp 🐦 Twitter 💼 LinkedIn