{
  "schemaVersion": 1,
  "slug": "gan",
  "title": "Train a generator against a discriminator",
  "className": "TinyGAN",
  "question": "Which network is being updated, and what is it trying to change?",
  "description": "Follow real and generated points through one discriminator, then replay separate optimizer steps and compare generator-loss gradients.",
  "category": "Generative models",
  "tags": [
    "GAN",
    "Adversarial learning",
    "Generator",
    "Discriminator"
  ],
  "curator": "TensorViz",
  "attribution": "Original TensorViz teaching example. PyTorch provides the underlying operators.",
  "license": "No separate redistribution license has been declared for these project examples.",
  "limitations": "A toy two-dimensional training process, not a trained image model. One optimizer step does not demonstrate distribution matching, stable convergence or protection against mode collapse. The forward graph shares D across both inputs; separate update functions in the downloaded source implement detach/freeze behavior.",
  "openingStep": "judge",
  "provenance": {
    "capturedAt": "2026-09-17T06:39:54.901653+00:00",
    "productRevision": "cf0ab1c9c58c1dc640235b742fa032cade00b43c",
    "sourceSha256": "83dea846127a7455d89d2d0faaec5105d412b5b4eb434b34dce826aca371956d",
    "python": "3.13.13",
    "torch": "2.7.1",
    "execution": {
      "inputs": {
        "args": [
          {
            "shape": [
              4,
              4
            ],
            "dtype": "float32",
            "fill": "randn"
          },
          {
            "shape": [
              4,
              2
            ],
            "dtype": "float32",
            "fill": "randn"
          }
        ]
      },
      "device": "cpu",
      "mode": "eval",
      "seed": 0,
      "deterministic": true
    },
    "outcome": "passed",
    "outputShape": [
      2,
      4,
      1
    ],
    "method": "TensorViz multifile projection and isolated execution worker",
    "scope": "Two-dimensional GAN forward, separate SGD updates, frozen/detached parameter checks and minimax/non-saturating gradient comparisons. No image dataset or trained generator.",
    "numericalChecks": [
      "A discriminator update changes only D; detached samples give the generator no gradient",
      "A generator update changes only G while retaining a gradient path through frozen D",
      "Stable logit losses match minimax and non-saturating analytic gradients",
      "The non-saturating loss gives a stronger gradient in the confident-fake example"
    ]
  },
  "layers": [
    {
      "id": "input",
      "label": "input",
      "op": "Input",
      "kind": "input",
      "parent": null,
      "params": {
        "shape": "4,4"
      }
    },
    {
      "id": "generator",
      "label": "generator",
      "op": "Module",
      "kind": "module",
      "parent": null,
      "params": {},
      "source": {
        "line": 37,
        "endLine": 37
      }
    },
    {
      "id": "discriminator",
      "label": "discriminator",
      "op": "Module",
      "kind": "module",
      "parent": null,
      "params": {},
      "source": {
        "line": 38,
        "endLine": 38
      }
    },
    {
      "id": "stack@TinyGAN#0",
      "label": "stack@TinyGAN#0",
      "op": "torch.stack",
      "kind": "layer",
      "parent": null,
      "params": {},
      "source": {
        "line": 44,
        "endLine": 44
      }
    },
    {
      "id": "output",
      "label": "output",
      "op": "Output",
      "kind": "output",
      "parent": null,
      "params": {}
    },
    {
      "id": "generator.hidden",
      "label": "generator.hidden",
      "op": "Linear",
      "kind": "layer",
      "parent": "generator",
      "params": {
        "in_features": 4,
        "out_features": 8
      },
      "source": {
        "line": 15,
        "endLine": 15
      }
    },
    {
      "id": "generator.relu",
      "label": "generator.relu",
      "op": "ReLU",
      "kind": "layer",
      "parent": "generator",
      "params": {},
      "source": {
        "line": 16,
        "endLine": 16
      }
    },
    {
      "id": "generator.project",
      "label": "generator.project",
      "op": "Linear",
      "kind": "layer",
      "parent": "generator",
      "params": {
        "in_features": 8,
        "out_features": 2
      },
      "source": {
        "line": 17,
        "endLine": 17
      }
    },
    {
      "id": "discriminator.hidden",
      "label": "discriminator.hidden",
      "op": "Linear",
      "kind": "layer",
      "parent": "discriminator",
      "params": {
        "in_features": 2,
        "out_features": 8
      },
      "source": {
        "line": 26,
        "endLine": 26
      }
    },
    {
      "id": "discriminator.relu",
      "label": "discriminator.relu",
      "op": "ReLU",
      "kind": "layer",
      "parent": "discriminator",
      "params": {},
      "source": {
        "line": 27,
        "endLine": 27
      }
    },
    {
      "id": "discriminator.logit",
      "label": "discriminator.logit",
      "op": "Linear",
      "kind": "layer",
      "parent": "discriminator",
      "params": {
        "in_features": 8,
        "out_features": 1
      },
      "source": {
        "line": 28,
        "endLine": 28
      }
    }
  ],
  "connections": [
    {
      "id": "edge-1",
      "source": "generator.hidden",
      "target": "generator.relu",
      "sourcePort": null,
      "targetPort": null
    },
    {
      "id": "edge-2",
      "source": "generator.relu",
      "target": "generator.project",
      "sourcePort": null,
      "targetPort": null
    },
    {
      "id": "edge-3",
      "source": "discriminator.hidden",
      "target": "discriminator.relu",
      "sourcePort": null,
      "targetPort": null
    },
    {
      "id": "edge-4",
      "source": "discriminator.relu",
      "target": "discriminator.logit",
      "sourcePort": null,
      "targetPort": null
    },
    {
      "id": "edge-5",
      "source": "input",
      "target": "generator.hidden",
      "sourcePort": null,
      "targetPort": null
    },
    {
      "id": "edge-6",
      "source": "input",
      "target": "generator",
      "sourcePort": null,
      "targetPort": null
    },
    {
      "id": "edge-7",
      "source": "input",
      "target": "discriminator.hidden",
      "sourcePort": null,
      "targetPort": null
    },
    {
      "id": "edge-8",
      "source": "input",
      "target": "discriminator",
      "sourcePort": null,
      "targetPort": null
    },
    {
      "id": "edge-9",
      "source": "generator.project",
      "target": "discriminator.hidden",
      "sourcePort": null,
      "targetPort": null
    },
    {
      "id": "edge-10",
      "source": "generator",
      "target": "discriminator",
      "sourcePort": null,
      "targetPort": null
    },
    {
      "id": "edge-11",
      "source": "discriminator.logit",
      "target": "stack@TinyGAN#0",
      "sourcePort": null,
      "targetPort": null
    },
    {
      "id": "edge-12",
      "source": "discriminator",
      "target": "stack@TinyGAN#0",
      "sourcePort": null,
      "targetPort": null
    },
    {
      "id": "edge-13",
      "source": "stack@TinyGAN#0",
      "target": "output",
      "sourcePort": null,
      "targetPort": null
    }
  ],
  "steps": [
    {
      "id": "generate",
      "target": "generator",
      "title": "Map noise into candidate samples",
      "note": "Each four-feature noise vector becomes a two-dimensional point. The generated point is differentiable with respect to G's parameters, allowing the later adversarial loss to update the generator."
    },
    {
      "id": "judge",
      "target": "discriminator",
      "title": "Use the same discriminator for real and generated points",
      "note": "D returns one logit for every point, using shared weights for both populations. Sigmoid converts a logit into the real-data probability used in the comparison panel."
    },
    {
      "id": "train-d",
      "target": "discriminator.logit",
      "title": "Update D with generated samples detached",
      "note": "The discriminator step minimizes −log D(real) − log(1−D(fake)). Detaching fake samples prevents this update from reaching G. The recorded stage verifies that generated coordinates stay unchanged."
    },
    {
      "id": "train-g",
      "target": "generator.project",
      "title": "Freeze D's weights and update G through its signal",
      "note": "The generator step minimizes −log D(G(z)), the original paper's practical non-saturating alternative. Gradients pass through D into G while D's weights stay fixed. Compare the gradient case to the original minimax objective."
    }
  ],
  "milestone": {
    "name": "Generative adversarial networks",
    "year": 2014,
    "kind": "Training method",
    "fidelity": "MLP generator 4 → 8 → 2 and shared discriminator 2 → 8 → 1. Stable logit losses reproduce the binary objectives; recorded training uses one D step and one non-saturating G step on four synthetic points.",
    "papers": [
      {
        "title": "Generative Adversarial Nets",
        "url": "https://arxiv.org/abs/1406.2661"
      }
    ]
  },
  "experiment": {
    "kind": "scatter",
    "title": "Update one network at a time.",
    "description": "Recorded two-dimensional GAN updates on four toy points. Choose the discriminator and generator stages to inspect which parameters and generated samples change. One alternating update does not establish a learned distribution or stable GAN training.",
    "controlLabel": "Training stage",
    "cases": [
      {
        "id": "initial",
        "label": "Before either update",
        "target": "discriminator",
        "note": "Start from the same random initialization and fixed noise samples. The discriminator assigns probabilities to real and generated points. Two separate optimizers will update one network at a time.",
        "scatter": [
          {
            "label": "Real training points",
            "points": [
              [
                0.8,
                0.9
              ],
              [
                1.1,
                1
              ],
              [
                0.9,
                1.2
              ],
              [
                1.2,
                0.8
              ]
            ]
          },
          {
            "label": "Generated points · fixed noise",
            "points": [
              [
                -0.22813317282577703,
                -0.3276989391422434
              ],
              [
                -0.2688216890906561,
                -0.15840660986347985
              ],
              [
                -0.16171488677014473,
                -0.18551719824129884
              ],
              [
                -0.02167181371945623,
                -0.00892497458031128
              ]
            ]
          }
        ],
        "vectors": [
          {
            "label": "D(real) probabilities",
            "values": [
              0.45041952807182845,
              0.4381473904738692,
              0.44725035194362145,
              0.43326496414354243
            ]
          },
          {
            "label": "D(G(z)) probabilities",
            "values": [
              0.45943446700705226,
              0.466044105135477,
              0.46431699252134984,
              0.4635737206849328
            ]
          }
        ],
        "metrics": [
          {
            "label": "Generator parameter change · norm since initialization",
            "value": 0
          },
          {
            "label": "Discriminator parameter change · norm since initialization",
            "value": 0
          },
          {
            "label": "Discriminator loss",
            "value": 1.4383596549033577
          },
          {
            "label": "Non-saturating generator loss",
            "value": 0.7693029023617073
          }
        ]
      },
      {
        "id": "discriminator",
        "label": "After one discriminator update",
        "target": "discriminator",
        "note": "Update D to raise scores on real points and lower scores on detached generated points. G receives no gradient, so its generated coordinates are unchanged. This is one discriminator step, not convergence.",
        "scatter": [
          {
            "label": "Real training points",
            "points": [
              [
                0.8,
                0.9
              ],
              [
                1.1,
                1
              ],
              [
                0.9,
                1.2
              ],
              [
                1.2,
                0.8
              ]
            ]
          },
          {
            "label": "Generated points · fixed noise",
            "points": [
              [
                -0.22813317282577703,
                -0.3276989391422434
              ],
              [
                -0.2688216890906561,
                -0.15840660986347985
              ],
              [
                -0.16171488677014473,
                -0.18551719824129884
              ],
              [
                -0.02167181371945623,
                -0.00892497458031128
              ]
            ]
          }
        ],
        "vectors": [
          {
            "label": "D(real) probabilities",
            "values": [
              0.4618374407951994,
              0.45213569813854126,
              0.4606652206196403,
              0.4471101759267283
            ]
          },
          {
            "label": "D(G(z)) probabilities",
            "values": [
              0.45917411617156073,
              0.4663089124784695,
              0.4648303083623374,
              0.46574247256100143
            ]
          }
        ],
        "metrics": [
          {
            "label": "Generator parameter change · norm since initialization",
            "value": 0
          },
          {
            "label": "Discriminator parameter change · norm since initialization",
            "value": 0.05382850019897555
          },
          {
            "label": "Discriminator loss",
            "value": 1.4102484651383072
          },
          {
            "label": "Non-saturating generator loss",
            "value": 0.7678595162017722
          }
        ]
      },
      {
        "id": "generator",
        "label": "After one generator update",
        "target": "generator",
        "note": "Keep D fixed and update G using the non-saturating loss −log D(G(z)). Gradients pass through D to G, while D's parameters remain unchanged from the preceding step. Reported parameter deltas are cumulative from initialization.",
        "scatter": [
          {
            "label": "Real training points",
            "points": [
              [
                0.8,
                0.9
              ],
              [
                1.1,
                1
              ],
              [
                0.9,
                1.2
              ],
              [
                1.2,
                0.8
              ]
            ]
          },
          {
            "label": "Generated points · fixed noise",
            "points": [
              [
                -0.2255131292992903,
                -0.30430324884347076
              ],
              [
                -0.26882809848948697,
                -0.1470698170203419
              ],
              [
                -0.15973096713245571,
                -0.16620098304208236
              ],
              [
                -0.022516537696330524,
                -0.0027605584126047106
              ]
            ]
          }
        ],
        "vectors": [
          {
            "label": "D(real) probabilities",
            "values": [
              0.4618374407951994,
              0.45213569813854126,
              0.4606652206196403,
              0.4471101759267283
            ]
          },
          {
            "label": "D(G(z)) probabilities",
            "values": [
              0.4602067625508986,
              0.46673990003112903,
              0.4655560307130478,
              0.4657285165633527
            ]
          }
        ],
        "metrics": [
          {
            "label": "Generator parameter change · norm since initialization",
            "value": 0.010857495950403676
          },
          {
            "label": "Discriminator parameter change · norm since initialization",
            "value": 0.05382850019897555
          },
          {
            "label": "Discriminator loss",
            "value": 1.4112609546215316
          },
          {
            "label": "Non-saturating generator loss",
            "value": 0.7666844405711858
          }
        ]
      },
      {
        "id": "gradients",
        "label": "Why use the non-saturating generator loss?",
        "target": "generator",
        "scatterAxes": {
          "xLabel": "Discriminator logit",
          "yLabel": "Loss derivative",
          "equalScale": false
        },
        "note": "The original minimax game minimizes log(1−D(G(z))) for G. When D confidently rejects a fake, this gradient is small. The original paper also recommends maximizing log D(G(z)), the non-saturating alternative used in these updates. These analytic derivatives are with respect to the discriminator logit, not a measured training outcome.",
        "scatter": [
          {
            "label": "Minimax gradient · x = logit",
            "points": [
              [
                -6,
                -0.0024726231566347743
              ],
              [
                -2,
                -0.11920292202211755
              ],
              [
                2,
                -0.8807970779778823
              ]
            ]
          },
          {
            "label": "Non-saturating gradient · x = logit",
            "points": [
              [
                -6,
                -0.9975273768433652
              ],
              [
                -2,
                -0.8807970779778824
              ],
              [
                2,
                -0.11920292202211769
              ]
            ]
          }
        ],
        "vectors": [
          {
            "label": "Discriminator logits",
            "values": [
              -6,
              -2,
              2
            ]
          },
          {
            "label": "Minimax derivative",
            "values": [
              -0.0024726231566347743,
              -0.11920292202211755,
              -0.8807970779778823
            ]
          },
          {
            "label": "Non-saturating derivative",
            "values": [
              -0.9975273768433652,
              -0.8807970779778824,
              -0.11920292202211769
            ]
          }
        ],
        "metrics": [
          {
            "label": "Gradient magnitude ratio at logit −6",
            "value": 403.4287934927351
          }
        ]
      }
    ]
  },
  "revision": "c8dd1ae1b30e8df7",
  "files": {
    "graph.tensorviz.json": {
      "url": "/models/gan/c8dd1ae1b30e8df7/graph.tensorviz.json",
      "sha256": "b2145796846cc3d6bd16e61f23ff1681aebed037f825cf234e9496d78054467a",
      "bytes": 36178
    },
    "model.py": {
      "url": "/models/gan/c8dd1ae1b30e8df7/model.py",
      "sha256": "83dea846127a7455d89d2d0faaec5105d412b5b4eb434b34dce826aca371956d",
      "bytes": 9784
    },
    "run.py": {
      "url": "/models/gan/c8dd1ae1b30e8df7/run.py",
      "sha256": "b92a9f66360e7709f1977d2f2add6fe5e8c4be40635e21aaa0747453b9fbce31",
      "bytes": 707
    },
    "requirements.txt": {
      "url": "/models/gan/c8dd1ae1b30e8df7/requirements.txt",
      "sha256": "edb86016b42cb3cc6cdc0c490aab929d3bb098ced4515394f0b55766033e0daf",
      "bytes": 91
    },
    "preview.svg": {
      "url": "/models/gan/c8dd1ae1b30e8df7/preview.svg",
      "sha256": "43f5155a16913a1fd53fc8ffdd68c1048141b2a8e40f9237ac6ab9e35ac18577",
      "bytes": 1947
    }
  }
}
