@torch.no_grad()
def generate_samples(G, n_samples=16, latent_dim=100):
    z = torch.randn(n_samples, latent_dim)
    images = G(z)
    return (images + 1) / 2  # rescale from [-1,1] back to [0,1]


if __name__ == "__main__":
    torch.manual_seed(0)
    G, D = train_dcgan(epochs=5)
    samples = generate_samples(G, n_samples=16)
    print("Generated sample batch shape:", samples.shape)

    # sanity check: discriminator accuracy should hover near 50%
    # if training has approached the theoretical equilibrium of
    # Theorem 29.1, rather than staying near 100% (D too strong,
    # generator stuck) or near 0% (G has "won" too easily, often
    # a sign of mode collapse rather than genuine convergence).
    with torch.no_grad():
        real_batch = next(iter(
            DataLoader(
                datasets.FashionMNIST(
                    root="./data", train=True,
                    transform=transforms.Compose([
                        transforms.ToTensor(),
                        transforms.Normalize((0.5,), (0.5,)),
                    ]),
                ),
                batch_size=256, shuffle=True,
            )
        ))[0]
        fake_batch = G(torch.randn(256, 100))
        d_acc_real = (torch.sigmoid(D(real_batch)) > 0.5).float().mean()
        d_acc_fake = (torch.sigmoid(D(fake_batch)) < 0.5).float().mean()
        print(f"D accuracy on real: {d_acc_real:.3f}, "
              f"on fake: {d_acc_fake:.3f}")
