fit() is a happy path. The moment your training has two optimizers (GAN), needs gradient accumulation, or wants dynamic loss weighting, you override train_step() — or write a manual loop with tf.GradientTape / torch.autograd / jax.grad depending on backend. We do all four in this track.
fit() is a contract, not a cage
Most of the time, model.fit() is all you need — it owns the epoch loop, the batching, the metric bookkeeping, the callbacks, and the distribution strategy. The point of this track isn't to abandon that machinery; it's to learn exactly where the contract ends so you can step in at the smallest possible level. Reach for custom logic only when the default forward-loss-backward cycle genuinely can't express what you want:
- GANs — alternating generator and discriminator steps, two losses, two optimizers
- Multi-task learning — custom loss weighting or per-task gradient surgery
- Research experiments — gradient manipulation, custom regularization, meta-learning
- Gradient accumulation — simulating larger batch sizes on limited GPU memory
Three levels, in order of cost
Keras gives you a ladder, and you should climb it from the bottom: (1) override train_step() for custom logic that still runs inside fit() — you keep the progress bar, callbacks, and distribution; (2) write a full manual loop when you need to control the loop structure itself; (3) drop to backend-specific APIs for the rare case that needs raw tape access. Each rung up trades infrastructure you got for free for control you have to maintain by hand. The whole rest of this track is teaching you to stop at the lowest rung that solves your problem.