@@ -484,33 +484,102 @@ def grad(beta):
484484X , Y = np .meshgrid (x , y )
485485Z = X ** 2 + 0.5 * Y ** 2 + 1
486486
487- fig = plt .figure (figsize = (13 , 6 ))
488-
489- ax_2d = fig .add_subplot (1 , 2 , 1 )
490- ax_2d .contour (X , Y , Z , levels = 20 )
491- ax_2d .plot (trajectory [:, 0 ], trajectory [:, 1 ], marker = "o" , color = "red" )
492- ax_2d .set_title ("Gradient Descent Path on 2D Loss Landscape" )
493- ax_2d .set_xlabel ("x" )
494- ax_2d .set_ylabel ("y" )
495- ax_2d .set_aspect ("equal" )
496-
497- ax_3d = fig .add_subplot (1 , 2 , 2 , projection = "3d" )
498- ax_3d .plot_surface (X , Y , Z , cmap = "viridis" , alpha = 0.6 , linewidth = 0 , antialiased = True )
499- ax_3d .plot (
500- trajectory [:, 0 ],
501- trajectory [:, 1 ],
502- trajectory_loss ,
503- marker = "o" ,
504- color = "red" ,
505- markersize = 4 ,
506- )
507- ax_3d .set_title ("Same Path on the 3D Loss Surface" )
508- ax_3d .set_xlabel ("x" )
509- ax_3d .set_ylabel ("y" )
510- ax_3d .set_zlabel ("loss" )
487+ # %%
488+ #| code-fold: true
489+ def plot_gradient_descent_2d3d_gif (
490+ trajectory , trajectory_loss , X , Y , Z , seconds = 8.0 , hold_seconds = 1.2
491+ ):
492+ """
493+ Animate gradient descent on a 2D loss landscape: reveal one more step of
494+ the trajectory per frame, on both the contour view and the 3D surface.
495+ """
496+ import base64
497+ import io
511498
512- plt .tight_layout ()
513- plt .show ()
499+ from IPython .display import HTML , display
500+ from PIL import Image
501+
502+ n_frames = len (trajectory )
503+
504+ frames = []
505+ for i in range (n_frames ):
506+ fig = plt .figure (figsize = (13 , 6 ))
507+
508+ ax_2d = fig .add_subplot (1 , 2 , 1 )
509+ ax_2d .contour (X , Y , Z , levels = 20 )
510+ ax_2d .plot (
511+ trajectory [: i + 1 , 0 ], trajectory [: i + 1 , 1 ], marker = "o" , color = "red"
512+ )
513+ ax_2d .scatter (
514+ trajectory [i , 0 ], trajectory [i , 1 ], color = "darkorange" , s = 90 , zorder = 5
515+ )
516+ ax_2d .set_title ("Gradient Descent Path on 2D Loss Landscape" )
517+ ax_2d .set_xlabel ("x" )
518+ ax_2d .set_ylabel ("y" )
519+ ax_2d .set_aspect ("equal" )
520+
521+ ax_3d = fig .add_subplot (1 , 2 , 2 , projection = "3d" )
522+ ax_3d .plot_surface (
523+ X , Y , Z , cmap = "viridis" , alpha = 0.6 , linewidth = 0 , antialiased = True
524+ )
525+ ax_3d .plot (
526+ trajectory [: i + 1 , 0 ],
527+ trajectory [: i + 1 , 1 ],
528+ trajectory_loss [: i + 1 ],
529+ marker = "o" ,
530+ color = "red" ,
531+ markersize = 4 ,
532+ )
533+ ax_3d .scatter (
534+ trajectory [i , 0 ],
535+ trajectory [i , 1 ],
536+ trajectory_loss [i ],
537+ color = "darkorange" ,
538+ s = 60 ,
539+ )
540+ ax_3d .set_title ("Same Path on the 3D Loss Surface" )
541+ ax_3d .set_xlabel ("x" )
542+ ax_3d .set_ylabel ("y" )
543+ ax_3d .set_zlabel ("loss" )
544+
545+ fig .suptitle (
546+ f"Step { i } /{ n_frames - 1 } : (x, y)=({ trajectory [i , 0 ]:.2f} , "
547+ f"{ trajectory [i , 1 ]:.2f} ), loss={ trajectory_loss [i ]:.2f} "
548+ )
549+ plt .tight_layout ()
550+
551+ buf = io .BytesIO ()
552+ fig .savefig (buf , format = "png" , dpi = 80 )
553+ plt .close (fig )
554+ buf .seek (0 )
555+ frames .append (Image .open (buf ).convert ("RGB" ))
556+
557+ frame_duration = int (1000 * seconds / n_frames )
558+ hold_duration = int (1000 * hold_seconds )
559+ durations = [frame_duration ] * n_frames
560+ durations [0 ] = hold_duration
561+ durations [- 1 ] = hold_duration
562+
563+ gif_buf = io .BytesIO ()
564+ frames [0 ].save (
565+ gif_buf ,
566+ format = "GIF" ,
567+ save_all = True ,
568+ append_images = frames [1 :],
569+ duration = durations ,
570+ loop = 0 ,
571+ )
572+ gif_buf .seek (0 )
573+
574+ # Quarto's HTML renderer does not pick up embedded image/gif outputs, so we
575+ # emit an <img> tag with the GIF inlined as a base64 data URI instead.
576+ gif_b64 = base64 .b64encode (gif_buf .getvalue ()).decode ("ascii" )
577+ display (
578+ HTML (f'<img src="data:image/gif;base64,{ gif_b64 } " alt="Gradient descent 2D/3D">' )
579+ )
580+
581+
582+ plot_gradient_descent_2d3d_gif (trajectory , trajectory_loss , X , Y , Z )
514583
515584
516585# %% [markdown] id="VpGIH0Oy5H92"
@@ -576,10 +645,20 @@ def mse(y_pred, y):
576645 b -= eta * db
577646 losses .append (mse (y_pred , y ))
578647
579- plt .plot (losses )
580- plt .title ("Training Loss Over Iterations" )
581- plt .xlabel ("Iteration" )
582- plt .ylabel ("MSE" )
648+ fig , axes = plt .subplots (1 , 2 , figsize = (12 , 4 ))
649+
650+ axes [0 ].plot (losses )
651+ axes [0 ].set_title ("Training Loss Over Iterations" )
652+ axes [0 ].set_xlabel ("Iteration" )
653+ axes [0 ].set_ylabel ("MSE" )
654+
655+ axes [1 ].plot (losses )
656+ axes [1 ].set_yscale ("log" )
657+ axes [1 ].set_title ("Same Loss, Log Scale on Y" )
658+ axes [1 ].set_xlabel ("Iteration" )
659+ axes [1 ].set_ylabel ("MSE (log scale)" )
660+
661+ plt .tight_layout ()
583662plt .show ()
584663
585664print ("Learned w:" , w )
0 commit comments