<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://emmjab.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://emmjab.github.io/" rel="alternate" type="text/html" /><updated>2026-07-19T04:51:34+00:00</updated><id>https://emmjab.github.io/feed.xml</id><title type="html">emmjab’s projects</title><subtitle>a subset of emmjab&apos;s thoughts live here.</subtitle><entry><title type="html">auction sale price predicting in Lessons 5&amp;amp;6</title><link href="https://emmjab.github.io/2026/07/14/learning-deep-learning-lesson-5.html" rel="alternate" type="text/html" title="auction sale price predicting in Lessons 5&amp;amp;6" /><published>2026-07-14T02:28:00+00:00</published><updated>2026-07-14T02:28:00+00:00</updated><id>https://emmjab.github.io/2026/07/14/learning-deep-learning-lesson-5</id><content type="html" xml:base="https://emmjab.github.io/2026/07/14/learning-deep-learning-lesson-5.html"><![CDATA[<h1 id="intro-to-random-forests-with-tabular-data-on-bulldozer-sales">Intro to Random Forests with Tabular Data on Bulldozer Sales</h1>

<p><strong>This post is part of my Practical Deep Learning journey.</strong></p>

<p>This is my experience with <a href="https://course.fast.ai/Lessons/lesson5.html">Lesson 5</a> and 
<a href="https://course.fast.ai/Lessons/lesson6.html">Lesson 6</a> of the Practical Deep Learning 
course. If you’re just tuning in, I’m working through this material in a study group while 
doing a half-batch at <a href="https://www.recurse.com/">The Recurse Center</a> in May/June 2026. 
See <a href="https://emmjab.github.io/2026/06/02/hello-deep-learning.html">Learning Practical Deep Learning (fastai)</a> for why.</p>

<p>In this lesson we’re exploring tabular data. One of the points of this lesson is to show that deep learning is not the 
best machine learning method for all kinds of problems. For instance, for tabular data, decision trees often perform
pretty well, and you can augment their performance by bagging them into “random forests”. Moreover, decision trees are 
useful because you can visualize them to see which columns are contributing to the performance of the model.</p>

<p>In the first few lessons of the course, we learned how to:</p>
<ul>
  <li>prepare labeled image and unlabeled text datasets</li>
  <li>choose deep learning models architectures and training loss functions, and</li>
  <li>apply metrics to classification or regression tasks to evaluate the performance of the model.</li>
</ul>

<p>In this lesson we’ll:</p>
<ul>
  <li>prepare tabular datasets by labeling “categorical” and “continuous” variables, among other things</li>
  <li>visualize decision trees and make random forests</li>
  <li>explore approaches to investigating the contribution of columns to the prediction</li>
  <li>compare the random forest approach with a neural net</li>
  <li>discuss methods for improving tabular models, e.g. “bagging” (averaging predictions) and “boosting” (summing predictions)</li>
</ul>

<p>In these lessons I decided to do the course using my mac laptop instead of cloud GPUs. So I’ve also been learning
something about parameter fiddling to make the data fit on the machine and make the training loops run in reasonable 
amounts of time. Along those lines, because I followed the course advice to run the notebooks with the latest versions of 
the libraries, I also found a few places where object implementations and parameter names drifted. Nothing crazy! Modern
LLMs seem to be pretty good at suggesting up-to-date library compatibilities and letting you know when they’re not up to
speed. I was sort of shocked to see a: “I’m sorry I can only make suggestions for best practices through 2025.”</p>

<p>Fixing bugs related to library deprecations and transformations is maybe one of the more boring and time-consuming parts 
of running code. But if you don’t spend the time doing it, you’ll never get anywhere. Fixing things blindly (e.g. copy-pasting 
in some fix without reading what it is) is also not great. You might get your code run, but it may silently behave differently 
than you want. So I sucked it up and did it – I can only expect that the world will keep being filled up with more and 
more aging code and messy data that supports our swiftly tilting planet. Tasks in the real world are not carefully prepared 
course notebooks.</p>

<h2 id="bulldozer-data">Bulldozer data?</h2>

<p>In Lessons 5 &amp; 6, we apply deep learning methods to tabular data. The technique the authors use for this type
of data is called “Random Forests”, which is a method for getting better performance on decision trees. In these lessons
there’s an explosion of jupyter notebooks to check out, but ultimately there are two choices of datasets to use for building,
training, and evaluating the random forest models:</p>
<ul>
  <li>the <a href="https://www.kaggle.com/competitions/titanic/data">titanic competition</a>: given a list of passengers on the Titanic and a bunch of data on them (gender, age, ticket cost, etc.), predict whether they survived</li>
  <li>the <a href="https://www.kaggle.com/competitions/bluebook-for-bulldozers/rules">bulldozers competition</a>: given a list of industrial machines sold at auctions over a given period of time and a bunch of data on them (64 fields, including MachineID, ModelID, YearMade, SaleElapsed, SalePrice), predict the auction price for future sales of those machines</li>
</ul>

<p>The Titanic dataset is too gruesome for me, so I went with the bulldozers. The thing is, the bulldozers competition is
from 13-14 years ago (!?). The data is no longer on kaggle, and you can’t click the “accept the rules” button which you
usually need to do to get access to the data. With a little googling around, I found that someone else made an ML course 
using the bulldozers competition and linked <a href="https://github.com/mrdbourke/zero-to-mastery-ml/blob/master/data/bluebook-for-bulldozers.zip">the data in their course repo</a>.
When I went through this lesson originally, I ended up using that data and everything went mostly ok (biggest thing was 
trying to sort out the latest dependencies for a cool tree visualization library, <a href="https://github.com/parrt/dtreeviz"><code class="language-plaintext highlighter-rouge">dtreeviz</code></a>,
but I figured it out and made some cool-looking graphs!).</p>

<p>Predicting “future” auction prices for bulldozers is interesting because in addition to being tabular like the Titanic 
passenger manifest, the data is also a timeseries. The authors pointed out a few things to keep in mind for timeseries 
data:</p>
<ul>
  <li>instead of randomly partitioning training data into train and validation sets, the training data should be a “before” chunk, and the validation set should be “after”</li>
  <li>because things change over time, data from many years earlier might be totally irrelevant to contemporary predictions</li>
  <li>or, because inflation, we should expect to predict higher prices for auction sales of the same machine in later years</li>
</ul>

<p>… Some of these timeseries conditions sound sort of similar to the NLP criteria from the last lesson! In particular, 
the “before” and “after” conditions, and the dependence of the later data points on the earlier ones. There are some
differences though. In the NLP case we wanted to hang on to words from much earlier for inform context - here we’re told 
maybe we should drop earlier data because behavioral patterns may have shifted (but… what about cyclic patterns in the 
market??).</p>

<h2 id="what-are-decision-trees">What are Decision Trees</h2>

<p>For this lesson, we start by learning about how to train a decision tree from a table of data. Next, we move from a single 
decision tree to a “random forest”. Finally, we compare random forest predictions with those from a neural network.</p>

<p>Decision trees and random forests are part of an arsenal of classical (non-neural net) machine learning techniques that 
haven’t disappeared from use. The goal of this lesson seems to be showing students that there are other model architectures,
besides variations on the neural network, that you might reach for to solve a task with a given dataset.</p>

<p>So how do we train a decision tree? According to the authors, “The basic steps to train a decision tree can be written down very easily”:</p>
<ul>
  <li>Loop through each column of the dataset in turn.</li>
  <li>For each column, loop through each possible level of that column in turn.</li>
  <li>Try splitting the data into two groups, based on whether they are greater than or less than that value (or if it is a categorical variable, based on whether they are equal to or not equal to that level of that categorical variable).</li>
  <li>Find the average sale price for each of those two groups, and see how close that is to the actual sale price of each of the items of equipment in that group. That is, treat this as a very simple “model” where our predictions are simply the average sale price of the item’s group.</li>
  <li>After looping through all of the columns and all the possible levels for each, pick the split point that gave the best predictions using that simple model.</li>
  <li>We now have two different groups for our data, based on this selected split. Treat each of these as separate datasets, and find the best split for each by going back to step 1 for each group.</li>
  <li>Continue this process recursively, until you have reached some stopping criterion for each group—for instance, stop splitting a group further when it has only 20 items in it.</li>
</ul>

<p>After reading this I wasn’t totally sure about what “levels” meant, so I spent some time unpacking the details. I turned 
what I learned into the following example.</p>

<p>Let’s say we have a dataset with 100 rows and four columns. The columns are “id” (unique), “age” [1,7], “type” [“bulldozer”, “tractor”, “wheelbarrow”, ‘rake’], 
and “price” (dollars). “price” is the dependent variable. To decide how best to partition the “age” column into two groups, 
we would sort from youngest to oldest. Then, we would create a list of pairs of groups by moving a partition systematically
across the values in the column. If age were [1,7], we would make 6 pairs of row-groups:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>partition A: group 1: 1      | group 2: 234567 
partition B: group 1: 12     | group 2: 34567
partition C: group 1: 123    | group 2: 4567
...
partition F: group 1: 123456 | group 2: 7
</code></pre></div></div>

<p>For a given partition A, we would predict that the sale price for a given row in group 1 is the average sale price for all 
the rows in group 1; similarly the sale price for a given row in group 2 is the average sale price for all the rows in 
group 2. Then we would calculate the RMSE between the row’s actual sale price and the prediction, for all the rows across 
both groups of partition A. The partition that produces the lowest RMSE is selected for that particular column.</p>

<p>Then the next column would be considered. For a categorical column like “type”, we would assign each category an arbitrary 
number, e.g. bulldozer = 1, tractor = 2, wheelbarrow = 3, rake = 4, and proceed with the same partitioning scheme. Then 
this column’s best partition would be compared with the best partition from the other columns. Whichever had the smallest 
sale price RMSE would be chosen as the node’s first split.</p>

<p>To build the next node, the same procedure would be repeated with just the rows on the left side of the chosen partition. 
Simultaneously, the same procedure would be repeated on the right side of the chosen partition.</p>

<p>The last step in the decision tree building algorithm is the stopping criteria. If we don’t impose a stopping criteria, 
we would end up creating a node for each individual row. The RMSE for the final split into individual rows would be 0, 
because the row’s price would be equal to its group’s average price (each side of the partition is a group with just one
element)! This would be pretty unhelpful unless you were trying to make a visualization of overfitting.</p>

<h3 id="partitioning-on-categorical-values">Partitioning on categorical values</h3>
<p>The lesson doesn’t talk about this in detail, but the decision tree’s partitioning algorithm that I just described is 
part of the <code class="language-plaintext highlighter-rouge">scikit.learn</code> library’s implementation. It seems a little strange to consider categorical variables ordered; 
e.g. in the example above there would never be a [“bulldozer”, “rake”] | [“tractor”, “wheelbarrow”] partition, but there’s 
no reason this partition couldn’t produce the smallest RMSE. Some googling and ChatGPT-conversing unearthed “category boosting” 
libraries; these overcome the <code class="language-plaintext highlighter-rouge">scikit.learn</code> ordered categorical partitioning and usually produce better results.</p>

<p>What the lesson <em>does</em> talk about is how we don’t need to make embedding vectors for each of the categorical values. Doing 
the same kind of partitioning of the ordinal values is good enough, meaning that <a href="https://peerj.com/articles/6339/">according to this 2019 paper</a> 
the ordinal partitioning method apparently finds the same best-split as using embeddings.</p>

<h2 id="preparing-tabular-data-for-decision-trees-and-random-forests">Preparing tabular data for decision trees and random forests</h2>
<p>The jupyter notebook for this lesson, <a href="https://github.com/fastai/fastbook/blob/master/09_tabular.ipynb">fastbook chapter 9</a>,
was full of methods for interrogating the model and understanding the predictions by the contributions of each of the columns 
and column values. There were also more than a few places where updates to <code class="language-plaintext highlighter-rouge">scikit-learn</code>, <code class="language-plaintext highlighter-rouge">pandas</code>, and some visualization 
libraries meant I had to figure out ways to reproduce the authors’ lessons with newer or alternative versions of these libraries.</p>

<p>For this reason, I ended up writing a lot of notes and quotes from the original lesson in my local jupyter notebook where I 
executed the cells. So, that’s <a href="/notebooks/bulldozers-tabular-notebook.html">here</a>.</p>

<p>The main things to remember from this lesson are that:</p>
<ul>
  <li>random forests are a great way to interrogate tabular data</li>
  <li>random forests are a great example of an ensemble methods: ways to improve model predictions by combining the results from multiple models with uncorrelated errors</li>
</ul>]]></content><author><name></name></author><summary type="html"><![CDATA[Intro to Random Forests with Tabular Data on Bulldozer Sales]]></summary></entry><entry><title type="html">patent phrase similarity scoring in Lesson 4</title><link href="https://emmjab.github.io/2026/07/09/learning-deep-learning-lesson-4.html" rel="alternate" type="text/html" title="patent phrase similarity scoring in Lesson 4" /><published>2026-07-09T01:53:00+00:00</published><updated>2026-07-09T01:53:00+00:00</updated><id>https://emmjab.github.io/2026/07/09/learning-deep-learning-lesson-4</id><content type="html" xml:base="https://emmjab.github.io/2026/07/09/learning-deep-learning-lesson-4.html"><![CDATA[<h1 id="intro-to-transformers-by-scoring-similarities-in-us-patent-phrases">Intro to Transformers by scoring similarities in US Patent Phrases</h1>

<p><strong>This post is part of my Practical Deep Learning journey.</strong></p>

<p>This is my experience with <a href="https://course.fast.ai/Lessons/lesson4.html">Lesson 4</a> of the 
Practical Deep Learning course. If you’re just tuning in, I’m working through this material 
in a study group while doing a half-batch at <a href="https://www.recurse.com/">The Recurse Center</a> 
in May/June 2026. See <a href="https://emmjab.github.io/2026/06/02/hello-deep-learning.html">Learning Practical Deep Learning (fastai)</a> for why.</p>

<p>Lesson 4 is the Natural Language Processing (NLP) lesson. Here we move from vision models (which learn features of images, 
e.g. for image label prediction) to NLP models (which learn features of text, e.g. for next word prediction). 
Many novel applied computer vision tasks no longer require models trained from scratch. Instead, researchers can start 
from freely available models that were pretrained using huge datasets and lots of compute, and fine-tune them with datasets
specific to the novel task. This is called “transfer learning”; I practiced it in Lesson 2 when I started from the 2015 
ResNet trained on ImageNet (available in the pytorch library), and fine-tuned it on <a href="https://emmjab.github.io/2026/06/03/cloud-classifying.html">different types of clouds</a>)
downloaded from duck duck go.</p>

<p>Text models can take much longer to train than image models (more on this later) – so wouldn’t it make sense to try transfer 
learning for text? “Why yes, that would be a great idea!” say the authors of the course in 2018, as Jeremy Howard and Sebastian 
Ruder proceeded to create what might have remained a state-of-the-art transfer learning method to use on Recurrent Neural Network 
architectures like Long Short-Term Memory (LSTM). But competition was steep! In 2017, the <code class="language-plaintext highlighter-rouge">transformer</code> architecture hit 
the scene with its catchy academic title, <a href="https://arxiv.org/abs/1706.03762">“Attention is all you Need”</a>. Implementations 
swiftly followed in 2018: OpenAI’s <a href="https://cdn.openai.com/research-covers/language-unsupervised/language_understanding_paper.pdf">GPT</a> 
(June) and Google’s <a href="https://arxiv.org/abs/1810.04805">BERT</a> (October).</p>

<p>This lesson is a great example of the authors sticking to their goal of showing students that deep learning methods are still 
in active development. The 2020 version of the NLP lesson (<a href="https://github.com/fastai/fastbook/blob/master/10_nlp.ipynb">fastbook chapter 10</a>) 
was built on the state-of-the-art RNN at the time, a <a href="https://deeplearning.cs.cmu.edu/S23/document/readings/LSTM.pdf">Long Short-Term Memory (LSTM) model</a> 
called <a href="https://arxiv.org/abs/1708.02182">AWD-LSTM</a> released by Salesforce Research in 2017. The fastai authors had been
inspired by the success of transfer learning for image models, and developed a training strategy called <a href="https://arxiv.org/abs/1801.06146">ULMFiT</a> 
(Universal Language Model Fine-Tuning) for transfer learning for language models. The lesson used ULMFiT with the AWD-LSTM
model trained on a Wikipedia dataset (<a href="https://arxiv.org/abs/1609.07843">WikiText-103</a>, 103 million tokens), and involved
fine-tuning with <a href="https://aclanthology.org/P11-1015.pdf">50k IMDb (Internet Movie Database) movie reviews</a>. The goal was 
to classify the sentiment of a subset of IMDb reviews, like we had classified images as dogs, cats, etc. in a prior lesson.
Fine-tuning with the IMDb reviews improved the model’s ability to classify sentiment, because the Wikipedia vocabulary 
would be augmented with IMDb-specific words and contexts, as well as style, syntax, and sentence structure.</p>

<p>By 2022, the transformer architecture was outperforming ULMFiT-assisted AWD-LSTMs. The authors responded by 
<a href="https://www.kaggle.com/code/jhoward/getting-started-with-nlp-for-absolute-beginners">building a new NLP lesson</a> 
using the <a href="https://huggingface.co/docs/transformers/en/index">huggingface transformer library</a>. The lesson’s task was to 
try to score well on a <a href="https://www.kaggle.com/competitions/us-patent-phrase-to-phrase-matching/">US Patent kaggle competition</a> 
(active Mar-June 2022) using a transformer model. The competition task was to compute similarity scores for pairs of phrases 
found in US patents. Why? Patent officers need to deny patent applications if it turns out the invention is already patented, 
but often people use different phrases to describe what is actually the same invention. Ideally, they could use the model
to find patents similar to a new application submission, even if the exact phrase does not match (e.g. “TV set” 
vs “Television set”). The authors chose Microsoft’s <a href="https://arxiv.org/abs/2111.09543">deberta-v3-small</a> (2021). From 
<a href="https://huggingface.co/microsoft/deberta-v3-small">the docs</a>, it has: “6 layers and a hidden size of 768,” and “44M backbone 
parameters with a vocabulary containing 128K tokens”. One of the lessons discusses how to choose a model, but I forgot which one.</p>

<h2 id="preparing-datasets-for-nlp">Preparing datasets for NLP</h2>
<p>The method for preparing a text dataset is largely the same for an LSTM model or a transformer. We need a strategy for 
turning text into numbers, since all deep learning models transform tensors of numbers. It will involve <code class="language-plaintext highlighter-rouge">tokenization</code>, 
<code class="language-plaintext highlighter-rouge">numericalization</code>, and <code class="language-plaintext highlighter-rouge">embedding</code>.</p>
<ul>
  <li><strong>tokenization</strong> is the process of splitting up the text into useful chunks, like words or word-parts, punctuation, starts/ends of sentences, called tokens; the list of unique tokens makes up the “vocabulary” for a given model; words or word-parts that are not yet part of the vocabulary are all mapped to a special token</li>
  <li><strong>numericalization</strong> is the process of mapping each token in the vocabulary to a number, since our model will have to process numbers</li>
  <li><strong>embedding</strong> is the process initializing a matrix where each row is a vector representation for each token without ordinal relationships (e.g. we don’t want the fact that dog:458 &gt; cat:457 to mean that dog &gt; cat)</li>
</ul>

<p>Embedding research has its own historical trajectory. Originally embedding vectors were trained separately (e.g. 
<a href="https://www.jmlr.org/papers/volume3/bengio03a/bengio03a.pdf">origin of embeddings for NLP</a>, 2003; <a href="https://arxiv.org/abs/1310.4546">Word2Vec</a>, 
2013). But, it turned out researchers got great results by initializing embedding vectors randomly and training them as 
part of the first layer of the model, like <code class="language-plaintext highlighter-rouge">w</code> in the image model from the last lesson. This embedding matrix gets updated 
with backpropagation at the end of each training loop.</p>

<p>So the embedding matrix is a parameter in the first layer of the model. What about the last layer of the model?</p>

<p>For the image classification task:</p>
<ul>
  <li>the last layer of the model returned a vector whose length was the number of classes the image could be sorted into, and</li>
  <li>the value of each element was the probability that the image belonged to that particular class</li>
</ul>

<p>A chunk of text could be sorted into classes as well, e.g. “type of document” or “sentiment”. But we can also see a next 
word prediction task as a classification problem. In this case, the next word is the most likely selection (class) from a vector 
of the vocabulary:</p>
<ul>
  <li>the last layer of the model could return a vector whose length is the number of tokens in the vocabulary, and</li>
  <li>the value of each element should be the probability that it is the next word</li>
</ul>

<p>Besides making the last layer perform “classification”, we could also make it do “regression”. More on this in a sec. 
In this case:</p>
<ul>
  <li>the last layer of the model will return just one number</li>
</ul>

<h2 id="choosing-classification-or-regression-for-the-final-layer-of-the-nlp-model">Choosing classification or regression for the final layer of the NLP model</h2>
<p>In the original notebook, the task was to use the RNN AWD-LSTM model to predict sentiment of an IMDb movie review:</p>
<ul>
  <li>positive movie review</li>
  <li>negative movie review</li>
</ul>

<p>In the updated lesson, the task was to use the transformer model to predict a similarity score for phrases (e.g. “TV set” 
and “Television set”) found in US patents, given particular categories:</p>
<ul>
  <li>1.0 - Very close match. This is typically an exact match except possibly for differences in conjugation, quantity (e.g. singular vs. plural), and addition or removal of stopwords (e.g. “the”, “and”, “or”).</li>
  <li>0.75 - Close synonym, e.g. “mobile phone” vs. “cellphone”. This also includes abbreviations, e.g. “TCP” -&gt; “transmission control protocol”.</li>
  <li>0.5 - Synonyms which don’t have the same meaning (same function, same properties). This includes broad-narrow (hyponym) and narrow-broad (hypernym) matches.</li>
  <li>0.25 - Somewhat related, e.g. the two phrases are in the same high level domain but are not synonyms. This also includes antonyms.</li>
  <li>0.0 - Unrelated.</li>
</ul>

<p>Both of these tasks could be either regression or classification tasks. Even though transformer model classes are numbers, 
they’re not continuous – they look a lot like the MNIST handwritten digit classes we predicted in the last lesson. And 
even though the sentiments are words (“pos” and “neg”), we could also see them as values between 0 and 1, pinned to 0 if 
&lt;0.5 and 1 if &gt;0.5.</p>

<p>For the transformer lesson, the authors chose to set num_labels = 1 for the model. Just one label means they chose the 
regression. I think I missed the explanation, but it’s probably because they want the model to learn the ordinal relationships
from 0 to 1. Smaller and larger similarity scores mean something – better and worse similarities; they’re not just 
re-orderable categories.</p>

<h2 id="nlp-fine-tuning-a-transformer-model-patent-phrase-similarities">NLP: fine-tuning a transformer model: Patent phrase similarities</h2>
<p>So, what does this all look like in code? WHat kind of stuff does the transformer library come with to turn datasets into 
something the transformer model can use, and then how do we train the model?</p>

<p>The training data is a csv where each row has the following columns:</p>
<ul>
  <li>id</li>
  <li>anchor phrase: the first phrase to compare</li>
  <li>target phrase: the second phrase to compare</li>
  <li>category code: the category by which the scores will be meaningful</li>
  <li>score: a float of value 0, .25, .5, .75, 1 that corresponds to the phrases’ similarity given the category</li>
</ul>

<p>The basic steps are the following:</p>
<ol>
  <li>Prepare the dataset
    <ol>
      <li>create a <code class="language-plaintext highlighter-rouge">training dataset object</code> from the training df with one string of text per row (concat the phrase &amp; category cols together with separator chars)</li>
      <li>create a <code class="language-plaintext highlighter-rouge">tokenizer object</code> from your choice of pre-trained model, and run it on the training dataset column that has the concatenated string</li>
      <li>rename the training dataset’s ‘score’ column to ‘labels’ because that’s what transformers expect the labels column to be named</li>
      <li>partition the training dataset object into <code class="language-plaintext highlighter-rouge">train</code> and <code class="language-plaintext highlighter-rouge">test</code> (test is validation)</li>
      <li>create the <code class="language-plaintext highlighter-rouge">model object</code> for the task at hand; args include the pretrained model’s name (e.g. <code class="language-plaintext highlighter-rouge">'deberta-v3-small'</code>) and num_labels (1=regression; &gt;1=classification)</li>
      <li>set up the <code class="language-plaintext highlighter-rouge">training args object</code>; args include: learning rate (and learning rate sched args), batch sizes for train and eval, number of epochs, machine spec args, and more</li>
      <li>write a function to <code class="language-plaintext highlighter-rouge">compute metrics</code> to print for each epoch; patent task asks for pearson coefficient (<code class="language-plaintext highlighter-rouge">from scipy.stats import pearsonr</code>)</li>
      <li>set up the <code class="language-plaintext highlighter-rouge">trainer object</code>; args include: <code class="language-plaintext highlighter-rouge">training args object</code>, <code class="language-plaintext highlighter-rouge">model</code>, <code class="language-plaintext highlighter-rouge">training dataset object's 'train'</code> (train), <code class="language-plaintext highlighter-rouge">training dataset object's 'test'</code> (validation), <code class="language-plaintext highlighter-rouge">tokenizer object</code>, <code class="language-plaintext highlighter-rouge">compute metrics func</code></li>
    </ol>
  </li>
  <li>Train the model
    <ol>
      <li>run the <code class="language-plaintext highlighter-rouge">trainer object</code>’s <code class="language-plaintext highlighter-rouge">trainer.train()</code> function, which updates the <code class="language-plaintext highlighter-rouge">model object</code> and the <code class="language-plaintext highlighter-rouge">tokenizer object</code></li>
      <li>on my macbook pro m4 with 24gb ram, this runs for 1 hour; after each epoch, it prints the train loss, validation loss, and metrics -&gt; these numbers tell us if our model is training properly</li>
      <li>optional: (becomes necessary if you want to submit a competition notebook on kaggle): save the trained model &amp; tokz: <code class="language-plaintext highlighter-rouge">trainer.save_model("./patent_model_dir")</code> and then zip it up for upload</li>
    </ol>
  </li>
  <li>Generate predictions (scores) for the test.csv data to submit to the kaggle competition
    <ol>
      <li>create the <code class="language-plaintext highlighter-rouge">test dataset object</code> the same way we made the training dataset object in “prepare the dataset” step 1 above, but using the test df (note: this is not the validation dataset, which was just a split of the training data) create a <code class="language-plaintext highlighter-rouge">training dataset object</code></li>
      <li>use the same <code class="language-plaintext highlighter-rouge">tokenizer object</code> from “prepare the dataset” step 2 above, and run it on the test dataset column that has the concatenated string</li>
      <li>generate the predictions with the <code class="language-plaintext highlighter-rouge">trainer object</code> that we trained before: <code class="language-plaintext highlighter-rouge">preds = trainer.predict(test_ds).predictions.astype(float)</code></li>
      <li>check your predictions – we had some &lt;0 and &gt;1 values but the submission expects <code class="language-plaintext highlighter-rouge">preds = np.clip(preds, 0, 1)</code></li>
      <li>create the <code class="language-plaintext highlighter-rouge">submissions.csv</code> file with the ids and scores (<code class="language-plaintext highlighter-rouge">preds</code>) columns</li>
    </ol>
  </li>
</ol>

<p>Here’s the <a href="/notebooks/patent-nlp-notebook.html">jupyter notebook</a> where I did all of this. It also involved a lot of
figuring out values that worked on my macbook pro instead of a GPU.</p>

<h2 id="submitting-the-similarity-scores-to-kaggle">Submitting the similarity scores to Kaggle</h2>
<p>So… submitting the similarity score csv to Kaggle was actually pretty confusing at first. I thought I could just upload 
the output <code class="language-plaintext highlighter-rouge">submissions.csv</code> file, but this competition makes you upload a kaggle notebook that generates the file. After 
googling around a bunch and having a meandering conversation with ChatGPT, I managed to figure it out and submit successfully 
on my first try.</p>

<p>Basically the steps in mid-July 2026 are the following.</p>

<h3 id="creating-the-submission-notebook-and-getting-the-competition-data-and-your-trained-model-in-there">Creating the Submission Notebook and getting the competition data and your trained model in there</h3>
<ol>
  <li>make sure you’re logged into your kaggle account</li>
  <li>create a notebook from the kaggle competition page, so that the competition datasets are accessible to the notebook</li>
  <li>upload your trained model to the <a href="https://www.kaggle.com/work">Your Work</a> page on Kaggle by +Create-ing a new model</li>
  <li>when the model shows up on the Your Work page, click the three dots and “add to collection” and choose the the contest</li>
  <li>go back to the notebook you just created, and click “add input” on the right; look for your model by filtering by “Your Work”</li>
  <li>now that you have the competition dataset and the model, click on the little “copy path” buttons next to the competition <code class="language-plaintext highlighter-rouge">test.csv</code> and the model folder and paste each into variables in the notebook</li>
</ol>

<h3 id="what-you-need-to-put-in-the-submission-notebook">What you need to put in the submission notebook</h3>
<ol>
  <li>make sure you write the imports you need for the code you’re about to write/run</li>
  <li>create the tokenizer from the model path, and copy-paste in the tokenization function you wrote to initially train your model</li>
  <li>create the same <code class="language-plaintext highlighter-rouge">test_ds</code> from the test.csv in the competition dataset, using the tokenizer you just imported</li>
  <li>create the model from the model path</li>
  <li>set up the trainer from the model and the tokenizer</li>
  <li>run predict with the trainer! <code class="language-plaintext highlighter-rouge">preds = trainer.predict(test_ds).predictions.astype(float).reshape(-1)</code></li>
  <li>put the predictions where they belong: <code class="language-plaintext highlighter-rouge">submission = df_test[["id"]].copy(); submission["score"] = preds</code></li>
  <li>write the <code class="language-plaintext highlighter-rouge">submissions.csv</code> file that Kaggle expects to see in the output <code class="language-plaintext highlighter-rouge">submission.to_csv("/kaggle/working/submission.csv", index=False)</code></li>
</ol>

<p>Then, click the clear and run all cells button to make sure the output file gets created. You can download that file to 
make sure it looks like what the submission expects/matches the output you got when you ran locally.</p>

<p>Last, submit to the competition! There’s a section literally called that in the section on the right. Clicking this might
tell you you’ll need to “turn off the internet” for the notebook. That’s in <code class="language-plaintext highlighter-rouge">settings</code> at the top of the page. I made sure
the notebook still ran after I did this, before I clicked submit. I was a little unsure about whether the libraries I was 
using were already installed, but most deep learning libraries are and the ones I used were there without doing anything
special. The notebook runs in a docker image; you can choose what kind of “accelerators” (read: GPUs) you want to use. I 
didn’t use any for this task; and you can also pick from “environment preferences”: pinned to original, or latest. I haven’t
explored most of these.</p>

<p>Anyway… voila! The submission runs, and you get a “public” and a “private” score – at least for the already-over competitions. 
You can select your best attempts to submit to the private leaderboard if the competition is still active, I think.</p>

<p>Here’s <a href="https://www.kaggle.com/code/emmjab/patent-phrase-similarity-lesson-4">my kaggle notebook</a> where I did all of this.</p>

<h2 id="end-notes">End Notes</h2>
<h3 id="why-transformers-image-models-vs-text-models">Why Transformers? (image models vs text models)</h3>
<p>For image modeling we care about recognizing features across images, even if they’re not always in the same spot. For instance,
whether a dog appears in the top right corner or the bottom left, it is still a dog. This can be handled by processing 
<code class="language-plaintext highlighter-rouge">convolutions</code> (sliding windows), over small patches of each image. We didn’t implement the sliding window part in the 
model-from-scratch lesson, but is baked into ResNet.</p>

<p>Text has different kinds of “spatial” relationships: the order of the words and the sequence of the sentences matter. Because 
these relationships are more “before/after” and “earlier/later” than “up/down” and “left/right”, they’re considered “temporal” 
or “sequential” or “ordered”. To train a model for next word prediction, instead of e.g. inputting a randomly ordered list 
of sentence fragments from our dataset (if sentence fragment ~ image), we send ordered chunks of the document through 
the model, word by word. Unlike dirs full of dog pics and cat pics, the sentences in a document and the words in a sentence 
depend on each other. When LSTMs send tokens through sequentially, the model compares its prediction with the 
actual next token, and this error is backpropagated. It’s like guessing cloud and then baking in “yes that’s a cloud” into 
the parameters.</p>

<p>This was the tension for NLP models: needing to “remember” sequential relationships between words, including grammars that 
control words quite far from each other, and pronouns whose meanings carry between sentences, without forgetting long-range 
context or overfitting. Moreover, LSTM models were slow because sequential relationships are difficult to parallelize over 
GPUs!</p>

<p>These sequence challenges don’t exist for image classification (though they would if you were using snapshots from a video 
recording…). They are what motivated researchers to develop the transformer architecture. The LSTM model uses a recurrent 
hidden state to encode long-distance relationships so information can be carried forward from earlier tokens, since words
are processed one at a time through the document. Instead of a recurrent hidden state, the transformer model includes layers
whose parameters encode long-distance relationships by calculating how much attention a word should pay to many other words 
(e.g. every previous word) in a given context window. That “attention” to other words is called “self-attention” (I guess
because it’s the view from itself?). Perhaps obvious to note, this “self-attention” approach means the transformer model is doing
mannny manyyyy pairwise token calculations, but luckily these can be parallelized.</p>

<h3 id="benchmark-datasets-and-metrics-for-nlp">Benchmark datasets and metrics for NLP</h3>
<p>For the image classification, there were a few benchmark datasets that were developed (e.g. MNIST and ImageNet 
that we discussed in the last post) and used to evaluate model performance. What did NLP use?</p>

<p>NLP models were historically built to accomplish particular tasks: next word prediction, sentiment classification, machine 
translation, “named-entity recognition” (NER), part of speech tagging, question answering, inference, semantic similarity.
Each of these tasks is associated with particular curated benchmark datasets to evaluate model performance. For next-token
prediction, the typical metric is “perplexity”, or how surprised the model is by the correct next token in e.g. the 
<a href="https://huggingface.co/datasets/FALcon6/ptb_text_only">Penn Treebank</a> - a million words of 1989 Wall Street Journal material, 
<a href="https://huggingface.co/datasets/mindchain/wikitext2">WikiText-2</a> - 2 million tokens from good/featured articles 
on wikipedia, and <a href="https://huggingface.co/datasets/Salesforce/wikitext">WikiText-103</a> - 103 million tokens datasets.</p>

<p>In 2018, just before transformer models were being released, researchers developed a benchmark called General Language 
Understanding Evaluation (<a href="https://arxiv.org/abs/1804.07461">GLUE</a>) so models could be evaluated on performance over all 
these independent tasks. Soon after, BERT smashed all the performance records. Researchers responded by developing 
<a href="https://arxiv.org/abs/1905.00537">SuperGLUE</a> to evaluate model performance on more difficult tasks.</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Intro to Transformers by scoring similarities in US Patent Phrases]]></summary></entry><entry><title type="html">digit classifying in Lesson 3</title><link href="https://emmjab.github.io/2026/06/04/learning-deep-learning-lesson-3.html" rel="alternate" type="text/html" title="digit classifying in Lesson 3" /><published>2026-06-04T00:00:00+00:00</published><updated>2026-06-04T00:00:00+00:00</updated><id>https://emmjab.github.io/2026/06/04/learning-deep-learning-lesson-3</id><content type="html" xml:base="https://emmjab.github.io/2026/06/04/learning-deep-learning-lesson-3.html"><![CDATA[<h1 id="intro-to-image-models-by-guessing-handwritten-numbers">Intro to Image Models by Guessing Handwritten Numbers</h1>

<p><strong>This post is part of my Practical Deep Learning journey.</strong></p>

<p>This is my experience with <a href="https://course.fast.ai/Lessons/lesson3.html">Lesson 3</a> of the 
Practical Deep Learning course. If you’re just tuning in, I’m working through this material 
in a study group while doing a half-batch at <a href="https://www.recurse.com/">The Recurse Center</a> 
in May/June 2026. See <a href="https://emmjab.github.io/2026/06/02/hello-deep-learning.html">Learning Practical Deep Learning (fastai)</a> for why.</p>

<p>The lessons for Practical Deep Learning are about learning by doing. <em>Doing</em> means writing code to 
create, run, and analyze the results of models. The authors thought this would be an effective way for 
students to build intuition about how models trained on real-world data perform on particular tasks, and how 
results vary when you change qualities of the model and data (architecture, batch sizes, loss function, learning rate, 
metric, methods for handling missing data, etc.).</p>

<p>The first two lessons use wrapper functions from the <code class="language-plaintext highlighter-rouge">fastai</code> library to abstract away the 
details of creating models like Microsoft Research’s 18- and 34-layer models, <a href="https://github.com/kaiminghe/deep-residual-networks">ResNet-18 and ResNet-34</a>
(published in 2015), which were trained on <a href="https://ieeexplore.ieee.org/document/5206848">ImageNet-1k</a> (both originally and
<a href="https://docs.pytorch.org/vision/main/models/generated/torchvision.models.resnet34.html">currently available in the pytorch library</a>).
The Practical Deep Learning authors wanted to show just how good image classification models had become with ResNet’s 
breakthrough with “residual” or “skip” connections in 2015, and what kind of classification accuracy you could get by fine-tuning 
general models like ResNet on particular labeled image datasets suited to your own purposes. You can try out my 
cloud classifying model (fine-tuned from ResNet-18 using clouds sourced from duck duck go) <a href="https://huggingface.co/spaces/emmjab/dllearning?logs=container">here</a>, 
see the code <a href="https://huggingface.co/spaces/emmjab/dllearning/tree/main">here</a>, and read a stream of 
consciousness blog post of me getting the model into a hugging face repo and website <a href="https://emmjab.github.io/2026/06/03/cloud-classifying.html">here</a>.</p>

<h2 id="handwritten-digits---mnist-dataset">Handwritten digits - MNIST dataset</h2>

<p>For lesson 3, the walkthrough task was to create a binary classification model for images of handwritten 3s and 7s… from 
scratch. That is, instead of starting from the ResNet model and fine-tuning the latest layers to transfer learn parameters 
for a new dataset, we’ll start from scratch with a particular dataset and build a few-layers-deep neural network model 
where we learn what kind of choices we have to make when setting up parameters, the model architecture, and the epoch training 
and validation methods.</p>

<p>The data comes from MNIST, a dataset derived from handwritten forms collected by the US National Institute of Standards (NIST) 
to prepare for digitizing the 1990 US Census forms. The original data was a mixed set of characters (A-Z, a-z, 0-9) written 
by Census Bureau field representatives and high school students from one high school in Bethesda, MD. 
From March to May 1992, the First Census OCR Systems Conference held a competition to read these character sets. 
In 1991, AT&amp;T Bell Labs had bought National Cash Register (NCR); OCR bank check reading was just around the corner.
From 1992 through 1998, Yann LeCun and his team at AT&amp;T Bell Labs remixed and reprocessed the digit data into the MNIST dataset.
In 1998, MNIST was released and it became a benchmark for OCR models. Most of the machine learning python libraries include 
it in a <code class="language-plaintext highlighter-rouge">datasets</code> library.</p>

<p>So, that’s the data. The learning objective was to learn how to assemble and make choices about the parts that make up a 
deep learning model, some of which might be obfuscated by machine learning library defaults, e.g. in <code class="language-plaintext highlighter-rouge">fastai</code> and <code class="language-plaintext highlighter-rouge">pytorch</code>, 
etc. By showing off what’s under the hood, the authors could make sure students don’t overfit on library particulars when 
the goal is to generally learn how to apply deep learning, especially when the state-of-the-art methods are still in rapid
development. To focus on the model, the data is provided, sorted into labeled folders and separated into training, validation, 
and test sets.</p>

<p>To ready the images for the model, we had to load each of the <code class="language-plaintext highlighter-rouge">N</code> images as an <code class="language-plaintext highlighter-rouge">mxn</code> matrix of pixels, convert the pixels
to grayscale and normalize their values (/255), stack them into a tensor, and then convert the <code class="language-plaintext highlighter-rouge">mxn</code> matrix of pixels for each
image into a vector of length <code class="language-plaintext highlighter-rouge">m*n</code>, so we have a resulting stack of images in a tensor of shape <code class="language-plaintext highlighter-rouge">(N, m*n)</code> – <code class="language-plaintext highlighter-rouge">N</code> rows and 
<code class="language-plaintext highlighter-rouge">m*n</code> columns. The labels are <code class="language-plaintext highlighter-rouge">1</code> for a 3 and <code class="language-plaintext highlighter-rouge">0</code> for a 7, stored as a tensor of shape <code class="language-plaintext highlighter-rouge">(N,)</code> (one value for each row).</p>

<p>After walking through the lesson, it’s left to the reader to make modifications so that the model can classify all digits, 
0-9. I ended up explaining how to do both classification tasks, as you’ll see if you keep reading. Notably, only three 
things need to change to accommodate more classes:</p>
<ul>
  <li>the labels (changes from <code class="language-plaintext highlighter-rouge">1</code> (it’s a 3) &amp; <code class="language-plaintext highlighter-rouge">0</code> (it’s a 7) to the actual number represented in the image, <code class="language-plaintext highlighter-rouge">(0-9)</code>)</li>
  <li>the size of the last layer of the model (and the layer’s corresponding parameters) (changes from 1 class to 10 classes)</li>
  <li>the choice of loss function</li>
</ul>

<h3 id="just-a-few-quick-lines-of-code-for-setting-up-the-training-dataset">Just a few quick lines of code for setting up the training dataset</h3>
<p>In the last section I wrote human language for how to turn the folders of images into training data, but let me just do
it in code here because it involves some interesting tensor manipulations.</p>

<p>Here’s a way to make training data &amp; label tensors for the 3s and 7s binary classification case. For each digit folder’s 
files, if it’s a digit we care about (3 or 7), stack its images, then concatenate the two stacks. For the labels, store 
a <code class="language-plaintext highlighter-rouge">1</code> for a handwritten three, and a <code class="language-plaintext highlighter-rouge">0</code> for a handwritten seven.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>x_stacks = []
y_stacks = []
label_dict = {3: 1, 7: 0} # we're labeling 1 if it's a 3, and 0 if it's a 7

for folder in sorted((path/'training').ls(), key=lambda x: int(x.name)):
    num = int(folder.name)
    if num in (3,7):
    
        # images
        tensors = [tensor(Image.open(o)) for o in folder.ls().sorted()]
        x_stacks.append(torch.stack(tensors).float()/255)
        
        # labels
        y_stacks.extend([label_dict[num]] * len(folder.ls()))
        
xs = torch.cat(x_stacks).view(-1, 28*28)    # (12396, 784) - 12396 images, 6131 threes and 6265 sevens each with 784 pixels
ys = tensor(y_stacks)                       # (12396,)
</code></pre></div></div>

<p>Here’s a way to make training data &amp; label tensors for the full dataset. For each digit folder’s files, stack all images, 
then concatenate the two stacks. For the labels, store a <code class="language-plaintext highlighter-rouge">0</code> if it’s a zero, <code class="language-plaintext highlighter-rouge">1</code> if it’s a one, … , <code class="language-plaintext highlighter-rouge">9</code> if it’s a nine.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>x_stacks = []
y_stacks = []

for folder in sorted((path/'training').ls(), key=lambda x: int(x.name)):

    # images
    tensors = [tensor(Image.open(o)) for o in folder.ls().sorted()]
    x_stacks.append(torch.stack(tensors).float()/255)
    
    # labels
    y_stacks.extend([int(folder.name)] * len(folder.ls()))
    
xs = torch.cat(x_stacks).view(-1, 28*28)    # (60000, 784) - 60,000 images, each with 784 pixels
ys = tensor(y_stacks)                       # (60000,)
</code></pre></div></div>

<h2 id="how-do-we-set-up-the-model">How do we set up the model?</h2>

<p>The toy “deep” neural net has two linear layers with a ReLU (“rectified linear unit”) sandwiched between them.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>def model(xb):
    res = xb@w1 + b1
    res = res.max(tensor(0.0))
    res = res@w2 + b2
    return res
</code></pre></div></div>
<p>Unpacking the variables, we have:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">xb</code> =&gt; the stack of a batch of training images as a tensor</li>
  <li><code class="language-plaintext highlighter-rouge">w1</code>, <code class="language-plaintext highlighter-rouge">b1</code>; <code class="language-plaintext highlighter-rouge">w2</code>, <code class="language-plaintext highlighter-rouge">b2</code> =&gt; vectors of parameters that we initialize randomly; in the training loop they will get “trained” into different values</li>
</ul>

<p>Unpacking the functions, we have:</p>
<ul>
  <li><code class="language-plaintext highlighter-rouge">res = xb@w1 + b1</code> =&gt; this is just <code class="language-plaintext highlighter-rouge">y = mx+b</code>, the equation for a line; more on the parameters <code class="language-plaintext highlighter-rouge">w1</code> &amp; <code class="language-plaintext highlighter-rouge">b1</code> later, but the 1st dimension has to match the number of pixels in one image and the 2nd corresponds to our choice of “activations”</li>
  <li><code class="language-plaintext highlighter-rouge">res = res.max(tensor(0.0))</code> =&gt; this is the ReLU - any negative numbers are clipped to 0; this is how we get the output of a series of linear transformations to be something else than a different linear function with some other choice of parameters</li>
  <li><code class="language-plaintext highlighter-rouge">res = res@w2 + b2</code> =&gt; this is just <code class="language-plaintext highlighter-rouge">y = mx+b</code> again, but the 1st dimension of <code class="language-plaintext highlighter-rouge">w2</code> has to match the activation number we chose before, and the 2nd and <code class="language-plaintext highlighter-rouge">b2</code>’s only dim has to match the number of classes we want at the end, e.g. 1 if we’re just guessing 3s or 7s, because <code class="language-plaintext highlighter-rouge">res</code> is now comprised of “logits” which we can convert to a probabilities of belonging to a particular class</li>
</ul>

<p>Just highlighting what’s going on here… we’re kind of using matrix math to smoosh all of our pixels into classes. Pretty weird!!!</p>

<h2 id="how-do-we-train-the-model">How do we train the model?</h2>
<p>Training a model basically involves finding fixed values for the model parameters so that when you pass in an image of a 
handwritten number 8, your model will confidently tell you “that’s an 8”. Finding effective parameters means looping through 
the data and updating the parameters with better values at each the end of each training loop. So what’s in a training loop? 
What’s the procedure for finding better values? One method is “stochastic gradient descent” (SGD). Pytorch (and fastai 
wrappings) include an implementation of this method in their <code class="language-plaintext highlighter-rouge">Optimizer</code> module, but in this lesson we’re learning what’s 
under the hood.</p>

<p>So just to keep in your head as we go through the details, the basic steps for training are, 
after you initialize the parameters:</p>
<ul>
  <li>send the data through the model</li>
  <li>calculate the mean loss on the output</li>
  <li>calculate the gradient of the loss for each of the parameters</li>
  <li>update the parameters based on the gradient</li>
  <li>(and print the loss so the human can take a look)</li>
</ul>

<h3 id="the-loss-function">The loss function</h3>
<p>At the heart of the training loop is the loss function. We will be happy if we calculate a smaller and smaller loss 
at the end of each training loop, because the loss for each image is smaller the closer we are to 100% certain of our 
prediction of <code class="language-plaintext highlighter-rouge">7</code> for an image actually labeled <code class="language-plaintext highlighter-rouge">7</code>.</p>

<p>In each run of the training loop, we execute our model on a batch of training data and parameters, and send the results (the 
tensor of raw output scores (“logits”)) into a “loss function” of our choosing. We can write many different loss functions, 
but any loss function will receive logits computed from the input data from the model and labels from the training data. 
It will turn each image’s computed scalar or vector of logits into probabilities that an image belongs to each class.
It then calculates the “loss” for each image based on the input tensor of labels. It does this by computing how “far” the 
predicted label was from the correct label. We can’t use a function that just says “right!” or “wrong!” for a given labeled 
image because that doesn’t help us figure out what direction to modify the model parameters to make better, or at least 
less wrong, predictions in the next loop.</p>

<p>Small loss means that we were confident and correct in our predictions; large loss means we were either not confident in 
our predictions, or confidently wrong.</p>

<p>The loss for a particular batch is typically one number: the mean loss over all the images.</p>

<h4 id="example-loss-functions">Example loss functions</h4>
<p>For the case where we’re just classifying images as 3 and 7, where 3 is labeled <code class="language-plaintext highlighter-rouge">1</code> and 7 is labeled <code class="language-plaintext highlighter-rouge">0</code>. <code class="language-plaintext highlighter-rouge">yb</code> is a tensor 
that contains the label/target/class for each image. <code class="language-plaintext highlighter-rouge">logits</code> is the model output – each image’s vector or scalar representing
something that can be turned into a probability or confidence in the predicted label as part of the loss function.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>def mnist_loss(model_logits, yb):
    probs = model_logits.sigmoid()
    return torch.where(yb==1, 1-probs, probs).mean()
    
loss = mnist_loss(logits, yb)
</code></pre></div></div>
<p>In this case, loss for all images is calculated in the following steps:</p>
<ul>
  <li>Sigmoid <code class="language-plaintext highlighter-rouge">sigmoid(x) = 1 / (1 + e^(-x))</code> confines the one logit for each image from whatever positive or negative number it is into a value between 0 and 1 corresponding to a probability of being a 3 (closer to 1) or a 7 (closer to 0).</li>
  <li>The torch.where line calculates the loss for each image label: if labeled <code class="language-plaintext highlighter-rouge">1</code>, it calculates how far from 1 the prediction is; if labeled <code class="language-plaintext highlighter-rouge">0</code>, it calculates how far from 0 the prediction is</li>
  <li>Returns the loss as the average loss over all images in the batch</li>
</ul>

<p>If we want to calculate loss when there are many classes (e.g. if we want to do the full dataset of [0,1,2,3,4,5,6,7,8,9]),
we can use the cross-entropy function.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>import torch.nn.functional as F

loss = F.cross_entropy(model_logits, yb)
</code></pre></div></div>
<p>The cross entropy function:</p>
<ul>
  <li>Uses the “softmax” function (<code class="language-plaintext highlighter-rouge">softmax(xᵢ) = e^xᵢ / (e^x₁ + e^x₂ + ... + e^xₙ)</code>) instead of sigmoid to squash each image’s vector of logits into probabilities that all sum to 1.</li>
  <li>For each image, calculates loss as the negative log of the probability calculated for the correct label (<code class="language-plaintext highlighter-rouge">-log(softmax(x))</code>); we only care about that one</li>
  <li>Returns the loss as the average loss over all images in the batch</li>
</ul>

<p>It uses <code class="language-plaintext highlighter-rouge">-log(p)</code> instead of a simple <code class="language-plaintext highlighter-rouge">1-p</code> subtraction for loss so that the optimizer has a larger range of values to work with.
When the probability decreases towards zero the loss can grow very large to indicate that a large change must be made.
When the loss is just <code class="language-plaintext highlighter-rouge">1-p</code>, it’s bound between 0 and 1, which doesn’t distinguish well between “slightly wrong” and “very wrong”.</p>

<h3 id="using-the-loss-to-update-the-model-parameters">Using the loss to update the model parameters</h3>
<p>Calculating the loss just tells us how good or bad we were, on average, at predicting labels. We still need some way to learn
from this loss how to change the parameters (the matrix of weights <code class="language-plaintext highlighter-rouge">w1</code>, <code class="language-plaintext highlighter-rouge">w2</code> and vectors of biases <code class="language-plaintext highlighter-rouge">b1</code>, <code class="language-plaintext highlighter-rouge">b2</code>) for the 
next training loop.</p>

<p>How do we do that? Instead of using the loss value itself, we evaluate the slope of the loss function at each parameter’s 
current value. The slope determines <em>how much</em> and in <em>which direction</em> we should modify the parameters to minimize the loss
for next time. We get the slope at a given point (parameter value) by taking the derivative of the function and evaluating 
it at that point. Because our loss function depends on multiple parameters (four in our case), we take partial derivatives 
with respect to each parameter. All of these partial derivatives taken together are called the gradient. Hence, the “gradient 
descent” part of “stochastic gradient descent”.</p>

<p>The parameters have to be initialized with random values before we run any training loops, and they have to be updated 
at the end of each training loop. How do we do this in practice?</p>

<p>The initialization of the parameters is straightforward:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>def init_params(size, std=1.0): 
    return (torch.randn(size)*std).requires_grad_()
</code></pre></div></div>
<p>The most complicated part is <code class="language-plaintext highlighter-rouge">.requires_grad_()</code>. This is a pytorch method responsible for making the gradient calculation 
from the loss function possible.</p>

<h3 id="how-requires_grad_-and-backward-work-or-how-to-take-partial-derivs-of-the-loss-function-to-get-the-gradient">How .requires_grad_() and .backward() work; or, how to take partial derivs of the loss function to get the gradient</h3>
<p>When a tensor is updated with <code class="language-plaintext highlighter-rouge">.requires_grad_()</code>, it gains the attributes <code class="language-plaintext highlighter-rouge">.is_leaf=True</code> and <code class="language-plaintext highlighter-rouge">.grad=None</code>. When this 
tensor first participates in a calculation, the result is given two things:</p>
<ul>
  <li>a <code class="language-plaintext highlighter-rouge">.grad_fn</code> attribute which is the gradient function of the last operation that produced it (e.g. <code class="language-plaintext highlighter-rouge">AddBackward0</code>, <code class="language-plaintext highlighter-rouge">MulBackward0</code>, <code class="language-plaintext highlighter-rouge">PowBackward0</code>, <code class="language-plaintext highlighter-rouge">SigmoidBackward0</code>, <code class="language-plaintext highlighter-rouge">ReluBackward0</code>, <code class="language-plaintext highlighter-rouge">LogSoftmaxBackward0</code>, <code class="language-plaintext highlighter-rouge">NllLossBackward0</code> (negative log likelihood), etc.),</li>
  <li>and a <code class="language-plaintext highlighter-rouge">.grad_fn.next_functions</code> attribute which points to the leaf node (<code class="language-plaintext highlighter-rouge">AccumulateGrad</code>).</li>
</ul>

<p>When the result participates in another calculation, the next result also gets a <code class="language-plaintext highlighter-rouge">.grad_fn</code> which is the gradient function 
of the last function that produced it, and a <code class="language-plaintext highlighter-rouge">.grad_fn.next_functions</code> which points to the gradient functions from the 
previous result rather than directly to the leaf node. And so on.</p>

<p>When <code class="language-plaintext highlighter-rouge">.backward()</code> is called on a result down the line, the gradients will be calculated and assembled by moving backwards
up the chain of functions, with chain rule, accumulating the results in place into the <code class="language-plaintext highlighter-rouge">.grad</code> attribute of the tensors 
originally set up with <code class="language-plaintext highlighter-rouge">.requires_grad_()</code> (our parameters). As each gradient calculation occurs, the tensor’s <code class="language-plaintext highlighter-rouge">.grad_fn</code> 
stored computation buffers are released from memory. What’s left is the value in <code class="language-plaintext highlighter-rouge">.grad</code> for the parameters, the same shape as the parameter itself, 
which we can use to update the weights. We’ll talk about how in a bit. Once we do that we’ll need to manually clear out 
the gradient with <code class="language-plaintext highlighter-rouge">.grad.zero_()</code> to prepare for the next training loop.</p>

<h3 id="the-shapes-of-the-parameter-tensors">The shapes of the parameter tensors</h3>
<p>Although the parameter values are initialized randomly, the shape of each parameter is dictated by your data. In particular,</p>
<ul>
  <li>in the first layer of the model, <code class="language-plaintext highlighter-rouge">w1</code>’s 1st dimension is the pixels per image in your dataset</li>
  <li>in the last layer of the model, <code class="language-plaintext highlighter-rouge">w2</code>’s 2nd dimension is the number of output classes</li>
  <li>for our simple three layer model, <code class="language-plaintext highlighter-rouge">w1</code>’s 2nd dimension = <code class="language-plaintext highlighter-rouge">w2</code>’s 1st dimension so the matrix math works out. This is also the size for <code class="language-plaintext highlighter-rouge">b1</code>. This number, sometimes called the number of “activations”, is something you choose.</li>
</ul>

<p>It’s easy to see in examples.</p>

<p>This would be param initialization when labels are either 3 or 7 (e.g. just one output class, “is 3?”):</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>w1 = init_params((28*28,30))
b1 = init_params(30)
w2 = init_params((30,1))
b2 = init_params(1)
</code></pre></div></div>
<p>This would be param initialization when images can be labeled any of the 10 digits from 0 to 9:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>w1 = init_params((28*28,30))
b1 = init_params(30)
w2 = init_params((30,10))
b2 = init_params(10)
</code></pre></div></div>

<h3 id="using-the-learning-rate-to-update-the-model-parameters">Using the learning rate to update the model parameters</h3>
<p>The last thing we haven’t discussed for updating the parameters in the training loop is the “learning rate” (<code class="language-plaintext highlighter-rouge">lr</code>). We calculated 
each parameter’s gradient and stored it in <code class="language-plaintext highlighter-rouge">.grad</code>, but this number just tells us a quantity and direction to <em>change</em> 
the old parameter value. That quantity is usually a lot bigger than we actually want to step the size of the parameter.
We’d likely overshoot the correct value, and have to move it all the way back in the other direction, maybe never minimizing
the loss!</p>

<p>Enter: the “learning rate”. This is a number that we can choose, usually pretty small, like 1e-3 (.001) or 1e-4 (.0001).
We multiply the gradient by the learning rate, and subtract it from the parameter value. If we choose a learning rate that
is too small, each training loop change to our parameter values will be so small that it will take forever to train our model.
If we choose a learning rate that is too large, we might step the value of parameters too far and miss the best value.</p>

<p>Here we set the <code class="language-plaintext highlighter-rouge">.data</code> attribute of the parameter to update its value without adding the calculation to a gradient function graph,
and then zero out the gradient to prepare it for the next training loop. This stuff would be written into the optimizer 
class if we used one instead of doing all of this manually.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>w1.data -= w1.grad * lr
w1.grad.zero_()
</code></pre></div></div>

<p>Ultimately we probably want to start our training loops with a higher learning rate and then systematically reduce it 
as we go through more and more training loops, because we’re probably coming up on the optimal values for the parameters.
The machine learning libraries all have standard methods included in their optimizers for doing this. Fastai also includes
a method <code class="language-plaintext highlighter-rouge">.lr_find()</code> to choose an initial learning rate by running training loop with a bunch of different learning 
rates.</p>

<h3 id="putting-everything-together-the-training-loop">Putting everything together: the training loop</h3>
<p>Ok so, we have:</p>
<ul>
  <li>the cleaned data divided into training, validation, and test sets</li>
  <li>we divided the training set into batches, and the training data into the independent (xb - images) and dependent (yb - labels) variables</li>
  <li>for each epoch we pass the all the batches through the training loop:
    <ul>
      <li>the data is sent through the model</li>
      <li>we calculate the loss for the training loop using our chosen loss function (e.g. cross_entropy)</li>
      <li>the parameter values are updated via the chosen learning rate * values calculated from the loss function via our chosen optimizer (e.g. SGD)</li>
    </ul>
  </li>
</ul>

<p>Here’s what we’ve got:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>params = [w1, b1, w2, b2] # initialized parameters

def train_epoch(model, lr, params):

    # loop through each batch of the training data in dl (DataLoaders)
    for xb,yb in dl:
    
        # run the model, calculate the loss
        model_logits = model(xb) # model uses the params we initialized
        loss = F.cross_entropy(model_logits, yb) # or mnist_loss(preds, yb)
        loss.backward() # this sets .grad in our params
        
        # update the parameters
        for p in params:
            p.data -= p.grad*lr
            p.grad.zero_() # this zeros out .grad for the param
            
    print("Training Loss: ", loss)
</code></pre></div></div>

<p>In the example notebooks for the course, the authors always print out the training loss, validation loss, and accuracy for each epoch.
We have calculated the training loss here, but what of the validation loss? And what is the “accuracy”?</p>

<h2 id="how-do-we-validate-the-model">How do we validate the model?</h2>
<p>Reiterating from the last section, we have now written code to train a model. But our only sense of how well it’s performing 
is from calculating the loss on the training data with every loop. We expect that it will perform similarly on the validation 
set, because typically the validation data is just some random subset of the data that we kept aside specifically 
for validation. E.g. if we’re predicting categorical labels, we expect future images of handwritten digits to have a pretty 
similar distribution to the present (e.g. people don’t slowly over time start tilting their 8s until they eventually go 
sideways ∞). That’s not always the case; we might want to manually make a validation set, particularly if things are changing 
over time.</p>

<p>Either way, we should check validation loss because it’s our first proxy for how well the model will perform on real world 
data. We’re only updating the model based on the training loss, so if the validation loss is not also going down, or if it’s
increasing between epochs, that’s a sure sign that we’re overfitting on the training data. That means the model is less 
able to generalize on new data that it hasn’t seen before. Usually the point of training a model is so it will do 
well on data it hasn’t seen before, because we’ve already spent the time labeling the data we’re training it on.</p>

<p>To do this, we can send the validation data through the model whose parameters we’ve just trained, and compute the loss.
This is the same as what we did for the training data, with the caveat that we don’t want to build the computation graph
(save the gradient functions) and we don’t want to call <code class="language-plaintext highlighter-rouge">loss.backward()</code> to compute gradients for the parameters. We can 
do this with <code class="language-plaintext highlighter-rouge">torch.no_grad()</code>. We’re not going to change anything about the model from this loop, we’re just checking on 
the loss.</p>

<p>We also don’t have to bother batching up the images into random groups for this epoch because we’re not looking to make 
improvements to the parameters, we just want the average loss for the total run anyway. We can still use batches though, 
if our validation dataset is too big to send into memory as one big tensor. Here we pretend we made a <code class="language-plaintext highlighter-rouge">valid_dl</code> object,
a DataLoaders for the validation data with a bunch of <code class="language-plaintext highlighter-rouge">xb</code> image tensors and <code class="language-plaintext highlighter-rouge">yb</code> label vectors.</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>def validate_epoch(model):
    # no gradient tracking
    with torch.no_grad():
        val_losses = [F.cross_entropy(model(xb), yb) for xb, yb in valid_dl] # or mnist_loss()
        val_loss_mean = torch.stack(val_losses).mean()
    print("Validation Loss: ", val_loss_mean)
</code></pre></div></div>

<h2 id="what-about-a-metric">What about a metric?</h2>
<p>Even more important than checking the validation loss is the metric that you care to check how your model performs in the
real world. Smaller loss will tell us that we were more confident and correct with our predicted labels, but a metric will 
tell us something more like we correctly labeled 24 out of 30 images (accuracy). It’s possible to have a loss that implies 
that we were not confident with most of our labels, e.g. maybe like 51% confidence, but still find that we actually guessed 
correct labels for like 90% of the validation set.</p>

<p>For <a href="https://www.kaggle.com/competitions/">Kaggle competitions</a>, the metric is provided. It could be accuracy, F1, AUC, 
etc. Your success is based on how well your model performs as evaluated by the metric. For the MNIST problem we’ve been 
working on here, we’ll define a batch accuracy metric for each of the classification tasks (<code class="language-plaintext highlighter-rouge">3</code> vs <code class="language-plaintext highlighter-rouge">7</code> and the full set 
of digits, 0-9).</p>

<p>For the binary case, <code class="language-plaintext highlighter-rouge">3</code> vs <code class="language-plaintext highlighter-rouge">7</code>:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>def batch_accuracy_binary(model_logits, yb):
    preds = model_logits.sigmoid() # the value is clipped between 0 and 1
    correct = (preds&gt;0.5) == yb    # If the value is &gt;.5, it's labeled 1, which "is_3"
    return correct.float().mean()  # fraction correct in this batch
</code></pre></div></div>

<p>For the multiclass case, full set of digits, 0-9:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>def batch_accuracy_multiclass(model_logits, yb):
    return (model_logits.argmax(dim=1) == yb).float().mean() # argmax because we guess the label with the highest probability
</code></pre></div></div>

<p>We can add the <code class="language-plaintext highlighter-rouge">batch_accuracy</code> calculation to the <code class="language-plaintext highlighter-rouge">validate_epoch</code> function:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>def validate_epoch(model):
    with torch.no_grad():
        val_losses = [F.cross_entropy(model(xb), yb) for xb, yb in valid_dl] # or mnist_loss()
        val_loss_mean = torch.stack(val_losses).mean()
        val_accs   = [batch_accuracy_multiclass(model(xb), yb) for xb, yb in valid_dl] # or the _binary() option
        val_accs_mean = torch.stack(val_accs).mean()
    print("Validation Loss: ", val_loss_mean,
          "Validation Accuracy :", val_accs_mean)
</code></pre></div></div>

<p>And then we can wrap all of this up in a loop where we choose how many epochs (full run through all the data) we want to 
train for. The more epochs, the longer the training process will take:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>def train_and_validate_model(model, lr, n_epochs):

    params = init_all_params() # set the w1, b1, w2, b2 with init_params() for the proper sizes (see above)
    
    for epoch in range(n_epochs):
        train_epoch(model, lr, params)  # prints training loss
        validate_epoch(model)           # prints validation loss &amp; validation accuracy
</code></pre></div></div>

<p>Ok that was way longer and more involved than I intended to get in this post. Do you … want to see how
this model performs on some handwritten images now?</p>

<h2 id="saving-and-using-the-model">Saving and using the model!</h2>
<p>Once you train your parameters, you might want to hang onto them! Those parameters, together with the model function, are 
what you use to guess the 0-9 digit in new images of handwritten digits.</p>

<p>If you want to do this one at a time, you can reshape your mxn-sized image (actually we’ve been working with square 28x28 
images this whole time) into a tensor shaped like <code class="language-plaintext highlighter-rouge">(1, m*n)</code>: an <code class="language-plaintext highlighter-rouge">image_tensor</code> with its <code class="language-plaintext highlighter-rouge">m*n</code> pixels all in a row:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>from fastai.vision.all import *

# assuming our image is 28x28 pixels like all of our training data

# image gymnastics  -- we had to do a similar version of this at the beginning to make our huge stack of images
image = Image.open('my_digit.png').convert('L') # L = convert to grayscale
image_tensor = tensor(image).float()/255  # (28, 28); /255 for normalization
image_tensor = image_tensor.flatten().unsqueeze(0)  # (28, 28) → (784,) → (1, 784)

# image gymnastics note -- the "tensor" in the lines above (and in other places in this post) is fastai's implementation
# can also use torch's tensor together with numpy: torch.tensor(np.array(image))

# same method for getting the predicted label as in batch_accuracy, but no comparison with a label because we don't have it!
with torch.no_grad():
    model_logits = model(image_tensor)
    predicted_class = model_logits.argmax(dim=1) # index of the highest probability
    print("The digit is: ", predicted_class.item())
</code></pre></div></div>]]></content><author><name></name></author><summary type="html"><![CDATA[Intro to Image Models by Guessing Handwritten Numbers]]></summary></entry><entry><title type="html">cloud classifying in Lesson 2</title><link href="https://emmjab.github.io/2026/06/03/cloud-classifying.html" rel="alternate" type="text/html" title="cloud classifying in Lesson 2" /><published>2026-06-03T19:35:00+00:00</published><updated>2026-06-03T19:35:00+00:00</updated><id>https://emmjab.github.io/2026/06/03/cloud-classifying</id><content type="html" xml:base="https://emmjab.github.io/2026/06/03/cloud-classifying.html"><![CDATA[<h1 id="classifying-some-clouds-online">Classifying some clouds online</h1>

<p><em>This is a stream of consciousness version of my process for getting the cloud model i trained online.</em></p>

<p>TL;DR try out the model I fine-tuned for classifying clouds at <a href="https://huggingface.co/spaces/emmjab/dllearning">Deep Learning Learning space</a> on huggingface.</p>

<p>According to lesson 2, the latest and greatest way to put models on the web is no longer jupyter with binder,
but now hugging face and gradio.</p>

<p>Making a huggingface space with gradio was very straightforward. I followed the steps in <a href="https://www.tanishq.ai/blog/posts/2021-11-16-gradio-huggingface.html">the blog</a> 
that the lesson2 video suggested.</p>

<ol>
  <li>make a hugging face account</li>
  <li>make a hugging face space, clone it locally (it’s very similar to a github clone/update/push workflow) &amp; follow the rest of the instructions in the page that gets created when you create a space</li>
  <li>including make a token on the hugging face site and use that as your password when you push your local updates</li>
  <li>pushing that code loads the demo app into the space, which lets you write <code class="language-plaintext highlighter-rouge">$something</code> into a box that you can submit, and it appears in a second column as “Hello, <code class="language-plaintext highlighter-rouge">$something</code>!!”</li>
  <li>then you can delete the demo code (or comment it out if you’re me and want to keep the past as notes)</li>
  <li>and add in the code to load your model &amp; run the inference (basically copy-pasting lines from the lesson 2 jupyter notebook where you learn what functions to call to classify one supplied image to the model that we trained, and copy-pasting gradio syntax from the huggingface/gradio blog post)</li>
</ol>

<p>For the sake of time, I will move fast and not break things. Instead of adding the model training code to that repo
and then having to figure out dependencies right now, i’m just going to copy my <code class="language-plaintext highlighter-rouge">export.pkl</code> that I created in
the lesson2 jupyter notebook into the huggingface repo that I just made. If you want some example images to show up, you also have to copy-paste those in.</p>

<p>Um. Well that didn’t work.</p>

<p><code class="language-plaintext highlighter-rouge">remote: -------------------------------------------------------------------------
remote: Your push was rejected because it contains files larger than 10 MiB.
remote: Please use https://git-lfs.github.com/ to store large files.
remote: See also: https://hf.co/docs/hub/repositories-getting-started#terminal
remote: 
remote: Offending files:
remote:   - export.pkl (ref: refs/heads/main)
remote: -------------------------------------------------------------------------
</code></p>

<p>In the past I spent some time with git-lfs and the question of what to do with large files in repos so … I really should have known better. But the tutorial said use the pickle!!!!
If the idea is to train models in this repo though, huggingface probably has something to say about it.</p>

<p>Oh yeah, they do: “Do you have files larger than 10MB? Those files should be tracked with git-xet, which you can initialize with:
<code class="language-plaintext highlighter-rouge">git xet install</code>”. Never heard of it. Hello 2026 and the rock I’ve been living under for the past six years.</p>

<p>But it looks like they’ve also moved on from pickles to something called <a href="https://huggingface.co/docs/safetensors/index"><code class="language-plaintext highlighter-rouge">safetensors</code></a>.
It’s not like huggingface is <a href="https://huggingface.co/docs/hub/en/fastai">incompatible with fastai</a>.</p>

<h2 id="ok-im-not-going-ot-go-nuts-here">Ok I’m not going ot go nuts here</h2>

<p>Just going to try the simplest thing which is to install git-lfs for now and upload the pickle.
I trained it one more time after cleaning the data and the error improved by an order of magnitude (12% to 1%).
But for some reason when I looked at the new losses:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>interp_clean = ClassificationInterpretation.from_learner(learn_clean)
interp_clean.plot_confusion_matrix()
interp_clean.plot_top_losses(5,nrows=5)
</code></pre></div></div>

<p>…there were still images in there that looked like the ones I got rid of with the “cleaner”:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>cleaner = ImageClassifierCleaner(learn)
cleaner
for idx in cleaner.delete(): cleaner.fns[idx].unlink()
for idx,cat in cleaner.change(): shutil.move(str(cleaner.fns[idx]), path/cat)
</code></pre></div></div>

<p>After I had cleaned I made the datablock and path variables again, and results were better…
so why were the wrong images still in there? Did the clean wizard just not find all of the bad images?
I mean I could look through the images to verify this but not right now.</p>

<h2 id="after-adding-lfs-for-the-pickle-is-it-going-to-just-work">After adding lfs for the pickle, is it going to just work?</h2>

<p>No, of course not.</p>
<ol>
  <li>hf mentions adding in <code class="language-plaintext highlighter-rouge">requirements.txt</code> but doesn’t mention what they already have installed? (e.g. they must have already installed <code class="language-plaintext highlighter-rouge">gradio</code>). Got this from the tutorial’s imports (<code class="language-plaintext highlighter-rouge">fastai</code>, and <code class="language-plaintext highlighter-rouge">scikit-image</code>). I put <code class="language-plaintext highlighter-rouge">skimage</code> in the requirements.txt file first and yeah I got an error when I <code class="language-plaintext highlighter-rouge">git push</code>ed.</li>
  <li>the blog code has some issues due to “bitrot”, mostly with the args for gradio’s <code class="language-plaintext highlighter-rouge">gr.Interface()</code>. I just copy-pasted the failure log messages from the hf site gui into chatgpt to figure out what the changes should be. I tried to ask Claude first but it was like, “honestly, I might not be up to date with the latest…” blah blah blah ok Claude thanks for expressing your limitations.</li>
</ol>

<p>I should have probably run this locally before pushing to hf but I didn’t set up a <code class="language-plaintext highlighter-rouge">.venv</code> (and added a <code class="language-plaintext highlighter-rouge">.gitignore</code>) until a few errors in.
Luckily the hf code tree has a great interface for <a href="https://huggingface.co/spaces/emmjab/dllearning/tree/main">checking the status &amp; the logs</a>!</p>

<p>Anyway to run locally you just have to start the venv and then call <code class="language-plaintext highlighter-rouge">python app.py</code> but fyi for the sake of time I didn’t try that yet.</p>

<p>Because now my cloud classifier is live! I tried it and it works! <a href="https://huggingface.co/spaces/emmjab/dllearning">Check it out here!</a></p>]]></content><author><name></name></author><summary type="html"><![CDATA[Classifying some clouds online]]></summary></entry><entry><title type="html">Learning Practical Deep Learning (fastai)</title><link href="https://emmjab.github.io/2026/06/02/hello-deep-learning.html" rel="alternate" type="text/html" title="Learning Practical Deep Learning (fastai)" /><published>2026-06-02T00:00:00+00:00</published><updated>2026-06-02T00:00:00+00:00</updated><id>https://emmjab.github.io/2026/06/02/hello-deep-learning</id><content type="html" xml:base="https://emmjab.github.io/2026/06/02/hello-deep-learning.html"><![CDATA[<h1 id="practically-learning-something-deep">Practically learning something deep</h1>

<p>I’m working through the <a href="https://course.fast.ai/">“fastai” Practical Deep Learning for Coders course</a>
with a study group while doing a half-batch at <a href="https://www.recurse.com/">The Recurse Center</a> in May/June 2026.
<a href="https://course.fast.ai/Lessons/lesson2.html">Lesson 2</a> suggested blogging the learning process.
That’s my cue to set up github pages.</p>

<h2 id="what-is-practical-deep-learning-for-coders">What is Practical Deep Learning for Coders?</h2>

<p>Practical Deep Learning for Coders is a free online course built by <a href="https://en.wikipedia.org/wiki/Jeremy_Howard_(entrepreneur)">Jeremy Howard</a>
and <a href="https://en.wikipedia.org/wiki/Rachel_Thomas_(academic)">Rachel Thomas</a> “with the goal of democratizing deep learning”.
It was first run in 2016 as a certificate course at the University of San Francisco, and then became a MOOC. The initial
course explored the Keras/Theano/TensorFlow libraries, but soon dropped them in favor of pytorch.</p>

<p>When they created the course, democratizing deep learning <a href="https://www.fast.ai/posts/2016-10-07-fastai-launch.html">meant</a>:</p>
<ul>
  <li>fix the shortage of data scientists with deep learning expertise</li>
  <li>create highly automated tools for training deep learning models</li>
  <li>build software to provide deep insight into the training of, and results from, deep learning models</li>
  <li>develop a range of “role model” applications, in areas where deep learning is currently being poorly utilized.</li>
</ul>

<p>In 2018, the authors published a wrapper/utility library around pytorch called “fastai” which handles 
various data pre-processing among other things. The course materials teach the principles of deep learning 
through this library and pytorch. They also peel back the layers of the libraries to build deep learning models from scratch.</p>

<p>In 2019, the University of San Francisco launched the Center for Applied Data Ethics (CADE) - rebranded in 2023 as 
<a href="https://www.usfca.edu/data-institute/centers-initiatives/caide">Center for AI &amp; Data Ethics (CAIDE)</a> initially directed by Rachel Thomas.</p>

<p>In 2020, the authors published a <a href="https://www.mdpi.com/2078-2489/11/2/108">paper on the fastai library</a> 
and released the fastai book,
<a href="https://course.fast.ai/Resources/book.html"><em>Deep Learning for Coders with Fastai and PyTorch: AI Applications Without a PhD</em></a>.</p>

<p>In 2022, the authors responded to transformers hitting the scene by incorporating them into the <a href="https://www.youtube.com/playlist?list=PLfYUBJiXbdtSvpQjSnJJ_PmDQB_VyT5iU">2022 lectures</a>
and <a href="https://github.com/fastai/course22">course materials - course22</a>, recorded by Jeremy Howard at the 
University of Queensland in Brisbane, Australia. See <a href="https://course.fast.ai/Lessons/lesson4.html">lesson 4 on NLP</a> 
with a link to the <a href="https://www.kaggle.com/code/jhoward/getting-started-with-nlp-for-absolute-beginners">patent data kaggle notebook with an intro to hugging face</a>.</p>

<p>In 2023-2024, the authors responded to Stable Diffusion by making a <a href="https://course.fast.ai/Lessons/part2.html">Part 2</a> to the course
with <a href="https://www.youtube.com/playlist?list=PLfYUBJiXbdtRUvTUYpLdfHHp9a58nWVXP">lecture recordings</a>, which goes through building Stable Diffusion from scratch.</p>

<p>It’s 2026 and fastai has had <a href="https://www.fast.ai/">many new blog posts</a> but no new commits to fastbook or the course materials in a bit.</p>

<h2 id="what-material-is-covered">What material is covered?</h2>

<p>The lessons are available at <a href="https://course.fast.ai/">https://course.fast.ai/</a>.</p>

<p>Each lesson link includes:</p>
<ul>
  <li>a 1.5h video lecture recorded by Jeremy Howard
    <ul>
      <li>Part 1, general intro; lectures posted Jul 2022, recorded at the University of Queensland in Brisbane, Australia (<a href="https://www.youtube.com/playlist?list=PLfYUBJiXbdtSvpQjSnJJ_PmDQB_VyT5iU">youtube</a>).</li>
      <li>Part 2, stable diffusion; lectures posted Oct 2022 &amp; Apr 2023 (<a href="https://www.youtube.com/playlist?list=PLfYUBJiXbdtRUvTUYpLdfHHp9a58nWVXP">youtube</a>).</li>
    </ul>
  </li>
  <li>chapter(s) to read/run from one or both course material repos (each a jupyter notebook)
    <ul>
      <li>“fastbook” - course textbook, 2020: <a href="https://github.com/fastai/fastbook">https://github.com/fastai/fastbook</a></li>
      <li>“course22” - course lecture companion, 2022: <a href="https://github.com/fastai/course22">https://github.com/fastai/course22</a></li>
    </ul>
  </li>
  <li>supplementary materials
    <ul>
      <li>links to kaggle competitions from the lecture</li>
      <li>links to hugging face repos from the lecture</li>
      <li>other random relevant links</li>
    </ul>
  </li>
</ul>

<h3 id="table-of-contents-for-the-lessons-at-httpscoursefastai">Table of Contents for the lessons at <a href="https://course.fast.ai/">https://course.fast.ai/</a></h3>
<h4 id="part-1-lessons-1-8">Part 1 (lessons 1-8)</h4>
<ul>
  <li>1: <a href="https://course.fast.ai/Lessons/lesson1.html">Getting started</a></li>
  <li>2: <a href="https://course.fast.ai/Lessons/lesson2.html">Deployment</a></li>
  <li>3: <a href="https://course.fast.ai/Lessons/lesson3.html">Neural net foundations</a></li>
  <li>4: <a href="https://course.fast.ai/Lessons/lesson4.html">Natural Language (NLP)</a></li>
  <li>5: <a href="https://course.fast.ai/Lessons/lesson5.html">From-scratch model</a></li>
  <li>6: <a href="https://course.fast.ai/Lessons/lesson6.html">Random forests</a></li>
  <li>7: <a href="https://course.fast.ai/Lessons/lesson7.html">Collaborative filtering</a></li>
  <li>8: <a href="https://course.fast.ai/Lessons/lesson8.html">Convolutions (CNNs)</a></li>
  <li>Bonus: <a href="https://course.fast.ai/Lessons/lesson8a.html">Data ethics</a></li>
  <li>Summaries (bulleted list of notes for each lesson)</li>
</ul>

<h4 id="part-2-lessons-9-25">Part 2 (lessons 9-25)</h4>
<ul>
  <li>Part 2 <a href="https://course.fast.ai/Lessons/part2.html">overview</a></li>
  <li>9: <a href="https://course.fast.ai/Lessons/lesson9.html">Stable Diffusion</a></li>
  <li>10: <a href="https://course.fast.ai/Lessons/lesson10.html">Diving Deeper</a></li>
  <li>11: <a href="https://course.fast.ai/Lessons/lesson11.html">Matrix multiplication</a></li>
  <li>12: <a href="https://course.fast.ai/Lessons/lesson12.html">Mean shift clustering</a></li>
  <li>13: <a href="https://course.fast.ai/Lessons/lesson13.html">Backpropagation &amp; MLP</a></li>
  <li>14: <a href="https://course.fast.ai/Lessons/lesson14.html">Backpropagation</a></li>
  <li>15: <a href="https://course.fast.ai/Lessons/lesson15.html">Autoencoders</a></li>
  <li>16: <a href="https://course.fast.ai/Lessons/lesson16.html">The Learner framework</a></li>
  <li>17: <a href="https://course.fast.ai/Lessons/lesson17.html">Initialization/normalization</a></li>
  <li>18: <a href="https://course.fast.ai/Lessons/lesson18.html">Accelerated SGD &amp; ResNets</a></li>
  <li>19: <a href="https://course.fast.ai/Lessons/lesson19.html">DDPM and Dropout</a></li>
  <li>20: <a href="https://course.fast.ai/Lessons/lesson20.html">Mixed Precision</a></li>
  <li>21: <a href="https://course.fast.ai/Lessons/lesson21.html">DDIM</a></li>
  <li>22: <a href="https://course.fast.ai/Lessons/lesson22.html">Karras et al (2022)</a></li>
  <li>23: <a href="https://course.fast.ai/Lessons/lesson23.html">Super-resolution</a></li>
  <li>24: <a href="https://course.fast.ai/Lessons/lesson24.html">Attention &amp; transformers</a></li>
  <li>25: <a href="https://course.fast.ai/Lessons/lesson25.html">Latent diffusion</a></li>
  <li>Bonus: Lesson 9a (youtube video)</li>
  <li>Bonus: Lesson 9b (youtube video)</li>
</ul>

<h3 id="repos-for-the-course-materials">Repos for the course materials</h3>
<h4 id="course22---part-1-these-are-the-notebooks-for-each-of-the-2022-lectures-linked-on-each-lesson-of-fastai">Course22 - part 1: these are the notebooks for each of the 2022 lectures linked on each lesson of fast.ai:</h4>
<ul>
  <li>Lesson 1:
    <ul>
      <li>fastbook ch1: <a href="https://github.com/fastai/fastbook/blob/master/01_intro.ipynb">https://github.com/fastai/fastbook/blob/master/01_intro.ipynb</a></li>
      <li><a href="https://github.com/fastai/course22/blob/master/00-is-it-a-bird-creating-a-model-from-your-own-data.ipynb">https://github.com/fastai/course22/blob/master/00-is-it-a-bird-creating-a-model-from-your-own-data.ipynb</a></li>
      <li><a href="https://github.com/fastai/course22/blob/master/01-jupyter-notebook-101.ipynb">https://github.com/fastai/course22/blob/master/01-jupyter-notebook-101.ipynb</a></li>
    </ul>
  </li>
  <li>Lesson 2:
    <ul>
      <li>fastbook ch2: <a href="https://github.com/fastai/fastbook/blob/master/02_production.ipynb">https://github.com/fastai/fastbook/blob/master/02_production.ipynb</a></li>
      <li><a href="https://github.com/fastai/course22/blob/master/02-saving-a-basic-fastai-model.ipynb">https://github.com/fastai/course22/blob/master/02-saving-a-basic-fastai-model.ipynb</a></li>
    </ul>
  </li>
  <li>Lesson 3:
    <ul>
      <li>fastbook ch4: <a href="https://github.com/fastai/fastbook/blob/master/04_mnist_basics.ipynb">https://github.com/fastai/fastbook/blob/master/04_mnist_basics.ipynb</a></li>
      <li><a href="https://github.com/fastai/course22/blob/master/03-which-image-models-are-best.ipynb">https://github.com/fastai/course22/blob/master/03-which-image-models-are-best.ipynb</a></li>
      <li><a href="https://github.com/fastai/course22/blob/master/04-how-does-a-neural-net-really-work.ipynb">https://github.com/fastai/course22/blob/master/04-how-does-a-neural-net-really-work.ipynb</a></li>
      <li>titanic spreadsheet: <a href="https://github.com/fastai/course22/blob/master/xl/titanic.xlsx">https://github.com/fastai/course22/blob/master/xl/titanic.xlsx</a></li>
    </ul>
  </li>
  <li>Lesson 4:
    <ul>
      <li>fastbook ch10: <a href="https://github.com/fastai/fastbook/blob/master/10_nlp.ipynb">https://github.com/fastai/fastbook/blob/master/10_nlp.ipynb</a></li>
      <li><a href="https://www.kaggle.com/code/jhoward/getting-started-with-nlp-for-absolute-beginners">https://www.kaggle.com/code/jhoward/getting-started-with-nlp-for-absolute-beginners</a></li>
    </ul>
  </li>
  <li>Lesson 5:
    <ul>
      <li>fastbook ch4: <a href="https://github.com/fastai/fastbook/blob/master/04_mnist_basics.ipynb">https://github.com/fastai/fastbook/blob/master/04_mnist_basics.ipynb</a></li>
      <li>fastbook ch9: <a href="https://github.com/fastai/fastbook/blob/master/09_tabular.ipynb">https://github.com/fastai/fastbook/blob/master/09_tabular.ipynb</a></li>
      <li><a href="https://github.com/fastai/course22/blob/master/05-linear-model-and-neural-net-from-scratch.ipynb">https://github.com/fastai/course22/blob/master/05-linear-model-and-neural-net-from-scratch.ipynb</a></li>
      <li><a href="https://github.com/fastai/course22/blob/master/06-why-you-should-use-a-framework.ipynb">https://github.com/fastai/course22/blob/master/06-why-you-should-use-a-framework.ipynb</a></li>
      <li><a href="https://github.com/fastai/course22/blob/master/07-how-random-forests-really-work.ipynb">https://github.com/fastai/course22/blob/master/07-how-random-forests-really-work.ipynb</a></li>
    </ul>
  </li>
  <li>Lesson 6:
    <ul>
      <li>fastbook ch9: <a href="https://github.com/fastai/fastbook/blob/master/09_tabular.ipynb">https://github.com/fastai/fastbook/blob/master/09_tabular.ipynb</a></li>
      <li><a href="https://github.com/fastai/course22/blob/master/07-how-random-forests-really-work.ipynb">https://github.com/fastai/course22/blob/master/07-how-random-forests-really-work.ipynb</a></li>
      <li><a href="https://github.com/fastai/course22/blob/master/08-first-steps-road-to-the-top-part-1.ipynb">https://github.com/fastai/course22/blob/master/08-first-steps-road-to-the-top-part-1.ipynb</a></li>
    </ul>
  </li>
  <li>Lesson 7:
    <ul>
      <li>fastbook ch8: <a href="https://github.com/fastai/fastbook/blob/master/08_collab.ipynb">https://github.com/fastai/fastbook/blob/master/08_collab.ipynb</a></li>
      <li><a href="https://github.com/fastai/course22/blob/master/09-small-models-road-to-the-top-part-2.ipynb">https://github.com/fastai/course22/blob/master/09-small-models-road-to-the-top-part-2.ipynb</a></li>
      <li><a href="https://github.com/fastai/course22/blob/master/10-scaling-up-road-to-the-top-part-3.ipynb">https://github.com/fastai/course22/blob/master/10-scaling-up-road-to-the-top-part-3.ipynb</a></li>
    </ul>
  </li>
  <li>Lesson 8:
    <ul>
      <li>fastbook ch13: <a href="https://github.com/fastai/fastbook/blob/master/13_convolutions.ipynb">https://github.com/fastai/fastbook/blob/master/13_convolutions.ipynb</a></li>
      <li><a href="https://github.com/fastai/course22/blob/master/xl/collab_filter.xlsx">https://github.com/fastai/course22/blob/master/xl/collab_filter.xlsx</a></li>
      <li><a href="https://github.com/fastai/course22/blob/master/xl/conv-example.xlsx">https://github.com/fastai/course22/blob/master/xl/conv-example.xlsx</a></li>
    </ul>
  </li>
  <li>Bonus: Data Ethics
    <ul>
      <li>fastbook ch3: <a href="https://github.com/fastai/fastbook/blob/master/03_ethics.ipynb">https://github.com/fastai/fastbook/blob/master/03_ethics.ipynb</a></li>
    </ul>
  </li>
</ul>

<h4 id="course22---part-2-notebook">Course22 - Part 2: notebook</h4>
<ul>
  <li>Notebook repo: <a href="https://github.com/fastai/course22p2">https://github.com/fastai/course22p2</a></li>
  <li>Link directly to the lesson notebooks: <a href="https://github.com/fastai/course22p2/tree/master/nbs">https://github.com/fastai/course22p2/tree/master/nbs</a></li>
</ul>

<h4 id="fastbook-the-book-the-practical-deep-learning-book-chapters-that-accompany-this-course-called-deep-learning-for-coders-with-fastai-and-pytorch-ai-applications-without-a-phd">Fastbook (“the book”): The Practical Deep Learning book chapters that accompany this course, called “Deep Learning for Coders with Fastai and PyTorch: AI Applications Without a PhD”</h4>
<ul>
  <li>Chapter 1, <a href="https://github.com/fastai/fastbook/blob/master/01_intro.ipynb">Intro</a></li>
  <li>Chapter 2, <a href="https://github.com/fastai/fastbook/blob/master/02_production.ipynb">Production</a></li>
  <li>Chapter 3, <a href="https://github.com/fastai/fastbook/blob/master/03_ethics.ipynb">Ethics</a></li>
  <li>Chapter 4, <a href="https://github.com/fastai/fastbook/blob/master/04_mnist_basics.ipynb">MNIST Basics</a></li>
  <li>Chapter 5, <a href="https://github.com/fastai/fastbook/blob/master/05_pet_breeds.ipynb">Pet Breeds</a></li>
  <li>Chapter 6, <a href="https://github.com/fastai/fastbook/blob/master/06_multicat.ipynb">Multi-Category</a></li>
  <li>Chapter 7, <a href="https://github.com/fastai/fastbook/blob/master/07_sizing_and_tta.ipynb">Sizing and TTA</a></li>
  <li>Chapter 8, <a href="https://github.com/fastai/fastbook/blob/master/08_collab.ipynb">Collab</a></li>
  <li>Chapter 9, <a href="https://github.com/fastai/fastbook/blob/master/09_tabular.ipynb">Tabular</a></li>
  <li>Chapter 10, <a href="https://github.com/fastai/fastbook/blob/master/10_nlp.ipynb">NLP</a></li>
  <li>Chapter 11, <a href="https://github.com/fastai/fastbook/blob/master/11_midlevel_data.ipynb">Mid-Level API</a></li>
  <li>Chapter 12, <a href="https://github.com/fastai/fastbook/blob/master/12_nlp_dive.ipynb">NLP Deep-Dive</a></li>
  <li>Chapter 13, <a href="https://github.com/fastai/fastbook/blob/master/13_convolutions.ipynb">Convolutions</a></li>
  <li>Chapter 14, <a href="https://github.com/fastai/fastbook/blob/master/14_resnet.ipynb">Resnet</a></li>
  <li>Chapter 15, <a href="https://github.com/fastai/fastbook/blob/master/15_arch_details.ipynb">Arch Details</a></li>
  <li>Chapter 16, <a href="https://github.com/fastai/fastbook/blob/master/16_accel_sgd.ipynb">Optimizers and Callbacks</a></li>
  <li>Chapter 17, <a href="https://github.com/fastai/fastbook/blob/master/17_foundations.ipynb">Foundations</a></li>
  <li>Chapter 18, <a href="https://github.com/fastai/fastbook/blob/master/18_CAM.ipynb">GradCAM</a></li>
  <li>Chapter 19, <a href="https://github.com/fastai/fastbook/blob/master/19_learner.ipynb">Learner</a></li>
  <li>Chapter 20, <a href="https://github.com/fastai/fastbook/blob/master/20_conclusion.ipynb">Conclusion</a></li>
</ul>

<h2 id="why-am-i-doing-this-course">Why am I doing this course?</h2>

<p>There seem to be two paths for a researcher:</p>
<ul>
  <li>choose a particular field and spend years becoming a domain expert</li>
  <li>choose to become adept in using and developing techniques for processing data, and be domain agnostic</li>
</ul>

<p>On the way:</p>
<ul>
  <li>domain-oriented people end up learning data processing techniques used in their fields</li>
  <li>technique-oriented people end up learning about the data in domains in which they embed</li>
</ul>

<p>What does that have to do with choosing this course? I’m a domain-oriented person too curious to sit still in just one domain.
This has led me to cultivate a techniques orientation so that I can move around and satisfy my curiosity.
The Practical Deep Learning course promises to teach techniques that can be applied by including datasets 
from different domains to process with each lesson. The intended audience is domain experts wanting to try 
out deep learning for their domains, and data techniques people collaborating with domain experts. 
I fall mostly into the latter category.</p>

<p>By following this course, I hope to gain some understanding of the kinds of problems people expect to be able 
to solve with deep learning, and intuition about the limitations of these kinds of models.
Are there domains of research that have been swiftly adopting deep learning to the detriment of other
(slower?) approaches to knowledge production that would aim at answering different but also good questions?</p>

<p>Moreover, the full title of the book is 
“Deep Learning for Coders with Fastai and PyTorch: AI Applications Without a PhD”.
I am a coder, I want to learn how to apply AI, and don’t have a PhD.</p>

<h2 id="why-this-course-out-of-all-the-options-in-2026">Why <em>this</em> course out of all the options in 2026?</h2>

<p>There are mannnny deep learning and other machine learning courses and materials to follow today.
Why did I choose this one?</p>
<ul>
  <li>something I knew - heard about it and started going through the material sometime between 2016 - 2018; gave up after a few lessons because I got conceptually stuck on the ubiquitous graph of layers of nodes and fully connected edges, plus I didn’t have a dataset</li>
  <li>saw that the material was updated over the years so it effectively fills the gap that I have for ML/AI</li>
  <li>someone happened to organize a group to go through this material in particular</li>
  <li>had to start somewhere – will add to this as I go; I’ve already started supplementing my reading <a href="https://karpathy.github.io/2015/05/21/rnn-effectiveness/">with other things</a></li>
</ul>]]></content><author><name></name></author><summary type="html"><![CDATA[Practically learning something deep]]></summary></entry></feed>