Exercise Class 1

Author

Jonas Skjold Raaschou-Pedersen

Published

September 4, 2025

Welcome to the first exercise class of the course. Our course schedule can be found here. For preparations before the first exercise class please read Before the exercise class.

Before the exercise class

Introduction

In the course our main programming language is Python. As our package and project manager for Python we will use uv.

The main goal of the exercise classes is to get hands-on experience with the material covered in the lectures. One of our mantras will be:

if you can’t code it you don’t understand it

One of the subgoals of the exercise classes is to develop a fully fedged Python package consisting of implementations of various methods introduced in the course. This will be done incrementally over the course of the exercise classes. The final package will consist of different modules that contains basic implementations of most of the things that have been taught at the lectures.

A Python package consists of different Python modules (.py files) and to edit those you need a text editor; you can use whichever text editor you like. One option is VSCode.

For version control we will use git which is the software/technology underlying GitHub where e.g. the source code for our course website is hosted. If you aren’t familiar with git then now is the time; I highly recommend reading the first two chapters of the this. Also, make sure you understand the difference between Git and Github, see here.

We will run different commands in our terminal/shell/command line. The shell has a reputation for being complicated and hard to use but ignore this; it is actually quite simple and very powerful. We won’t do anything fancy but you should be comfortable with opening your shell and running basic commands e.g. executing a Python script like:

# Mac & Linux
python scripts/estimate_model.py

# Windows
python scripts\estimate_model.py

cloning a git repository from GitHub:

git clone git@github.com:ai-for-humanity-ucph/2025.git

creating a directory and changing directory into it:

mkdir my-project
cd project

Further commands will be introduced as we progress through the exercise classes.

Installation

If you haven’t already done so, please install the following software on your computer:

  1. git
  2. uv

To check that the installation was successful, run the following commands in your shell:

uv --version
uv 0.8.14
git --version
git version 2.51.0

Installing newest version of Python through uv

We will use Python version 3.13 in the exercise classes. To install this with uv run the following command in your shell:

uv python install 3.13

You can check what Python versions you have installed by running:

uv python list

and verify that 3.13 is listed.

See the docs for more.

Exercise class

With uv and git installed, and Python 3.13 installed through uv, we are ready to start the exercise class.

Exercise 1 - Create a project directory

  1. Create a directory called exercises where you will store all your code for the exercise classes. Try to do this using your shell but you can also point and click if you rather do that. I have for instance created one in my home directory under ai-for-humanity/exercises.

  2. Open your shell and change directory into your new exercises folder. You can check the current directory by typing pwd (if you are on Mac or Linux; Windows users should use cd).

    You should see something like this:

    pwd
     /home/jsr-p/ai-4-humanity/exercises

    Proceed to the next exercise with your shell open.

Exercise 2 - Create a uv project and virtual environment

  1. Initialize a new Python project with a Python package named ai4h using uv by running the following command in your shell:

    uv init --name ai4h --package --python 3.13

    By the way: you can hover over the code and click on the copy to clipboard icon to copy the snippet to your clipboard.

    If successful you should have a folder structure similar to:

    .
    ├── .git
    ├── .gitignore
    ├── pyproject.toml
    ├── .python-version
    ├── README.md
    └── src
        └── ai4h
            └── __init__.py

    Things to note about this is that uv:

    • automatically initialized your exercises folder as a git repository; you can turn this off, see here, but there is no sane reason to do this; using git in your projects is non-negotiable from now on until forever.
    • created a .gitignore file for you; this is a file that tells git which files it should ignore. You can append files that you don’t want git to track to this file; we will return this later
    • created a pyproject.toml file for you; this is a file that contains metadata about your project and its dependencies.
    • created a .python-version file for you; this is a file that tells uv which Python version to use when you create a virtual environment, which we will do in the next exercise.
    • created a src folder for you; this is where your package code should go. The package name is ai4h so the code for this package should go inside the src/ai4h folder. Note the __init__.py file; this file tells Python that this directory should be treated as a package; see also here.
    • created a README.md file for you; this is a markdown file that contains information about your project e.g. how to use it. You can edit this file using markdown syntax; see here for a quick introduction to markdown.
  2. Edit the pyproject.toml file and change the description field to something that describes your project. You can also change the author field to your name.

  3. Create a virtual environment for your project by running the following command in your shell:

    uv venv

    This will create a virtual environment in a folder called .venv in your project directory. Virtual environments are important for reproducability while they isolate your Python dependencies. See also here.

  4. Activate your virtual environment. On Mac and Linux you can do this by running:

    source .venv/bin/activate

    On Windows you can do this by running:

    .venv\Scripts\activate

    To check what Python executable your computer will use when executing python try typing (Mac & Linux)

    which python

    and on Windows:

    where python

    see which and where.

    You should confirm that the Python executable is located inside your .venv folder.

    On my end I get:

    which python
    /home/jsr-p/ai-4-humanity/exercises/.venv/bin/python

    i.e. the python executable is inside the .venv/bin/ folder of my exercises project folder which is what we want.

Exercise 3 - Installing dependencies

  1. Add numpy as a dependency to your project by running the following command in your shell:

    uv add numpy

    This will install numpy in your virtual environment and add it to the pyproject.toml file as a dependency.

  2. Verify that numpy has been added to the pyproject.toml file by opening it and checking that it is listed under the [project.dependencies] section.

  3. Open your Python interpreter by typing python in your shell and try typing the following (ignore the >>> if you copy the lines directly):

    >>> import numpy as np
    >>> import ai4h
    >>> ai4h.main()
    Hello from ai4h!
    • Explain to yourself what you just did.
    • Exit the interpreter by typing quit() or pressing Ctrl+D (Mac & Linux) or Ctrl+Z (Windows).
  4. Remove everything inside the src/ai4h/__init__.py but don’t delete the file entirely.

  5. If you haven’t already, now is the time to stage the relevant files and make a commit.

    You should stage the following files:

    .gitignore
    .python-version
    README.md
    pyproject.toml
    src/ai4h/__init__.py
    uv.lock

    using git add (or your git GUI of choice) and then make a commit (again either with git commit or your git GUI of choice) with a suitable commit message.

  6. Create a new folder called src/ai4h/models. Add a __init__.py file underneath it such that Python knows that the subfolder is a package. Finally, create a file inside the folder called linear_model.py. You can do this however you like; one way would be to run the following:

    mkdir -p src/ai4h/models
    touch src/ai4h/models/__init__.py src/ai4h/models/linear_model.py

    This works on Mac & Linux; Windows users can use the Git BASH shell that comes with git and execute the same commands, see here. See also touch.

    This module will serve as the home for our code related to linear models.

  7. Inside src/ai4h/models/linear_model.py insert the following snippet:

    from pathlib import Path
    
    
    def test():
        project_root = Path(__file__).resolve().parents[3]
        relpath = Path(__file__).resolve().relative_to(project_root)
        print(f"Hello from `{relpath}`!")
  8. Open your Python interpreter again and try the following:

    >>> from ai4h.models import linear_model
    >>> linear_model.test()
    Hello from `src/ai4h/models/linear_model.py`!
  9. Commit your changes to git.

Exercise 4 - Interactive development whichever way you like

  1. Instead of using the classic Python interpreter interactively let’s try something different. Run the following command:

    uv add jupyter --dev 

    Notice the speed of uv when installing jupyter and its dependencies (in particular if it is cached on your machine) 🏎️

    There is another way to install jupyter into your environment but this way is the easiest; if you are interested see here.

  2. Assert whether the first jupyter in your PATH is the one installed into the virtual environment by running which/where as we previously did to assert the same for the Python interpreter.

  3. Inspect your pyproject.toml file and assert that jupyter has been added as a development dependency under the [dependency-groups.dev] section.

  4. Create a folder called notebooks in your project directory.

  5. Start a jupyter lab server by running

    jupyter lab
    # or alternatively the more safe: `uv run jupyter lab` (e.g. if you forgot
    # to activate your environment)

    in your shell.

  6. Open a notebook in the notebooks folder and name it linear_model.ipynb.

  7. Inside the notebook try to import the linear_model module and run the test function i.e. execute:

    from ai4h.models import linear_model
    linear_model.test()

    If you cannot import our ai4h package check whether the kernel you are are using is the one from your virtual environment. One way to debug this is by import the sys module and inspect the sys.executable variable and verifying it uses the Python executable from your virtual environment.

  8. By installing jupyter you also install IPython. Try repeating 7. using IPython instead of jupyter lab. Start IPython by running:

    uv run ipython
  9. Try repeating 7. using jupyter console instead of jupyter lab. Start jupyter console by running:

    uv run jupyter console

    I prefer IPython but you can of course use whatever you like.

  10. Add the line .ipynb_checkpoints/ to your .gitignore file. Add the the changes to uv.lock, pyproject.toml, .gitignore and your notebook notebooks/linear_model.ipynb. Commit the changes.

Exercise 5 - Logistic Regression - Theory

Alright, enough with coding for a moment. One of the most important fundamental models in machine learning and data science is the logistic regression model. In the following exercise we will derive some expressions that will help us to estimate the parameters in a logistic regression model. Grab a pen and paper and give the following exercises a try.

Let \(Y_i \in \{0, 1\}\) be a binary random variable and \(X_i \in \mathbb{R}^{p + 1}\) a vector of covariates, where we assume the first entry is a \(1\) for the constant term. The logistic regression model models the probability:

\[ P(Y_i = 1 \mid X_{i}) = \frac{\exp \{X_i^\top \beta\}} {1 + \exp \{X_i^\top \beta\}} =: \sigma(X_i^\top \beta) \tag{1}\]

where \(\sigma: \mathbb{R} \to [0, 1]\) is the sigmoid function (also known as the expit function). We assume we have an i.i.d sample of \(n\) observations i.e. \(\{(Y_{i}, X_{i})\}_{i=1}^{n}\).

We will estimate \(\beta\) using the iteratively reweighted least squares (IRLS) algorithm and implement it inside of src/ai4h/models/linear_model.py package.


  1. Show that we can go from the probability in Equation 1 to the log odds form: \[ \mathrm{logit} (P(Y_i = 1 \mid X_{i})) := \log \frac{P(Y_i = 1 \mid X_{i})}{1 - P(Y_i = 1 \mid X_{i})} = X_i^\top \beta. \tag{2}\]

    Equation 2 shows that the log odds is linear in the parameters \(\beta\) and that the logit function is the inverse of the sigmoid function.

  2. Write sigmoid function as a function of the variable \(z \in \mathbb{R}\):

    \[ \sigma(z) = \frac{1}{1 + \exp(-z)}. \]

    Show that

    \[ 1 - \sigma(z) = \sigma(-z). \tag{3}\]

  3. Show that

    \[ \frac{\partial}{\partial \beta} \sigma(X_i^\top \beta) = X_{i} \sigma(X_{i}'\beta) [1 - \sigma(X_{i}'\beta)]. \tag{4}\]

    Note that \(\frac{\partial}{\partial \beta} \sigma(X_i^\top \beta) \in \mathbb{R}^{p + 1}\) is a vector.

    Use Equation 3 to help you in the last step.

  4. Show that the log-likelihood function can be written as:

    \[ \ell(\beta) := \sum_{i=1}^{n} \left[ Y_{i} \sigma(X_{i}^{\top}\beta) + (1 - Y_{i}) \{1 - \sigma(X_{i}^{\top}\beta)\} \right] \tag{5}\] In the following we write \(\ell_{i}(\beta)\) for an element of the sum in Equation 5.

  5. Show that:

    \[ \frac{\partial}{\partial \beta} \ell_{i}(\beta) = X_{i}[Y_{i} - \sigma(X_{i}^{\top}\beta)] \tag{6}\]

    and hence: \[ \frac{\partial}{\partial \beta} \ell(\beta) = \sum_{i=1}^{n} X_{i}[Y_{i} - \sigma(X_{i}^{\top}\beta)] \tag{7}\] using linearity of the derivative.

  6. Show that:

    \[ \frac{\partial^{2}}{\partial \beta \partial \beta^{\top}} \ell_{i}(\beta) = -X_{i} \sigma(X_{i}'\beta) [1 - \sigma(X_{i}'\beta)]X_{i}^{\top} \]

    and hence

    \[ \frac{\partial^{2}}{\partial \beta \partial \beta^{\top}} \ell(\beta) = - \sum_{i}^{n} X_{i} \sigma(X_{i}'\beta) [1 - \sigma(X_{i}'\beta)]X_{i}^{\top}. \tag{8}\]

    Hint: take the derivative of Equation 6 wrt. the transpose \(\beta^{\top}\) yielding the requested \((p + 1) \times (p + 1)\) Hessian matrix.

  7. Show that Equation 8 can be written in matrix form as:

    \[ \frac{\partial^{2}}{\partial \beta \partial \beta^{\top}} \ell(\beta) = - \mathbf{X}^{\top} \mathbf{W} \mathbf{X} \tag{9}\]

    where

    \[ \begin{aligned} \mathbf{W} &:= \text{diag}\left( \sigma(X_1^\top \beta) [1 - \sigma(X_1^\top \beta)], \ldots, \sigma(X_n^\top \beta) [1 - \sigma(X_n^\top \beta)] \right) \\ & = \begin{bmatrix} \sigma(X_1^\top \beta) [1 - \sigma(X_1^\top \beta)] & \ldots & 0 \\ \vdots & \ddots & \vdots \\ 0 & \ldots & \sigma(X_n^\top \beta) [1 - \sigma(X_n^\top \beta)] \\ \end{bmatrix} \in \mathbb{R}^{n \times n} \end{aligned} \]

    and \(\mathbf{X}\) is the \(n \times p\) matrix with the \(X_{i}^{\top}\)’s stacked along the rows. Here we used the diag operator.

    Define \(\mathbf{y} \in \mathbb{R}^{n \times 1}\) as the column vector with the \(Y_i\)’s stacked along the rows and \(\mathbf{p} := \sigma(\mathbf{X} \beta) \in \mathbb{R}^{n \times 1}\) as the column vector of probabilities for each \(i\) (where we apply the sigmoid function elementwise).

    Show also that Equation 7 can be written in matrix form as:

    \[ \frac{\partial}{\partial \beta} \ell(\beta) = \mathbf{X}^{\top} (\mathbf{y} - \mathbf{p}) \tag{10}\]

  8. The score equations are given by

    \[ \frac{\partial}{\partial \beta} \ell(\beta) = 0 \]

    which are \(p + 1\) equations nonlinear in \(\beta\).

    We can solve the score equations using the Newton-Rhapson algorithm as written on page 120 in ESLII.

    Let \(\beta_{t}\) be the value of \(\beta\) at iteration \(t\). A single Newton update is:

    \[ \beta_{t} = \beta_{t - 1} - \left( \frac{\partial^{2}}{\partial \beta \partial \beta^{\top}} \ell(\beta) \right)^{-1} \frac{\partial}{\partial \beta} \ell(\beta) \tag{11}\]

    Use Equation 9 and Equation 10 to show that Equation 11 can be written as:

    \[ \beta_{t} = (\mathbf{X}^{\top} \mathbf{W} \mathbf{X})^{-1}\mathbf{X}^{\top}\mathbf{W} \mathbf{Z} \tag{12}\]

    where

    \[ \mathbf{Z} := \mathbf{X} \beta_{t - 1} + \mathbf{W}^{-1}(\mathbf{y} - \mathbf{p}) \tag{13}\] is the adjusted response.

    It is called the iteratively reweighted least squares algorithm while it at each iteration \(t\) solves a weighted least squares problem, which is evident from the expression Equation 12. The reweighting happens while the weights are updated at each iteration because they depedend on the current value of \(\beta\).

Exercise 5 - Logistic Regression - Implementation

All the following code should go into the src/ai4h/models/linear_model.py module. However, when working iteratively on your implementations, you work however and whereever you like. Only the final result should go into your package.

  1. Implement the expit function \(\sigma\) in Equation 1 using numpy.

  2. Simulate from the logistic regression model using Equation 1. Name your function generate_logreg_data. It should return two arrays X, y with the simulated covariates and binary outcomes respectively. It should have the following signature:

    def generate_logreg_data(
     n: int,
     beta: NDArray[np.float64],
     seed: int = 0,
    ) -> tuple[NDArray[np.float64], NDArray[np.int64]]:
        ...

    Note: You don’t have to type hint your function as I do. See here for more on type hinting numpy arrays and here for type hinting in general if you are interested. Type hinting can help e.g. yourself (and LLM’s) to reason about your code.

    Your function should:

    1. Compute the value of p (number of covariates) from the length of the beta vector.

    2. Use np.random.default_rng to create a random number generator (RNG) called rng to subsequently draw random numbers from. See here and here.

    3. Simulate a matrix X of shape (N, p + 1) where the first column is all ones (for the constant term) and the remaining p columns are drawn i.i.d from \(\mathrm{Unif}(-1, 1)\) (random uniform on \([-1, 1]\)).

    4. Draw \(\mathbf{y}\) by using rng.binomial with n=1 and probability parameter p equal to \(\mathbf{p}\).

      Note: The probability parameter p is not to be confused with the number of covariates also denoted by p 🫠

  3. Generate a dataset with n = 10_000 observations using a parameter vector equal to \(\beta = (1, 1, -1)^{\top}\) i.e. \(p + 1 = 3\) (the first parameter is for the constant term).

  4. Implement the IRLS algorithm based on Equation 12 and Equation 13. Do this by writing a function called irls with the following signature:

    def irls(
        X: NDArray[np.float64],
        y: NDArray[np.int64],
        tol: float = 1e-8,
        max_iter: int = 100,
    ):
        """Iteratively reweighted least squares solver."""
        ...

    At each iteration you should check whether the algorithm has converged by using the following condition:

    np.any(np.abs(beta_t_next - beta_t) > tol)

    Your function should return a dictionary with the following keys and values:

    • "estimates": The estimated parameters as a 1d numpy array of shape (p + 1,).
    • "iterations": The number of iterations the algorithm ran.
    • "converged": A boolean indicating whether the algorithm converged (i.e. whether the algorithm stopped because it reached the tolerance level before reaching the maximum number of iterations).

    Note: It is important that you initialize beta to be a(X.shape[0], 1) column vector such that X @ beta also equals a column vector. Likewise, if not already you should make y equal a (n, 1) column vector. This is for the matrix operations of Equation 13 to make sense.

  5. Finish the LogisticRegression class below. Remove the lines raise NotImplementedError() and insert the relevant code instead. Get guidance from the docstrings.

    class LogisticRegression:
        def __init__(self):
            self.coef_ = None
            self.n_iter_ = None
            self.converged_ = None
    
        def fit(self, X: NDArray[np.float64], y: NDArray[np.int64]):
            """Estimate the Logistic Regression model using IRLS"""
            raise NotImplementedError()
            self.coef_ = result["estimates"]
            self.n_iter_ = result["iterations"]
            self.converged_ = result["converged"]
    
        def predict_proba(self, X: NDArray[np.float64]) -> NDArray[np.float64]:
            """Compute predicted probabilities."""
            if self.coef_ is None:
                raise ValueError("Model is not fitted yet.")
            raise NotImplementedError()
    
        def predict(self, X: NDArray[np.float64]) -> NDArray[np.int64]:
            """Predict binary outcomes based on a 0.5 threshold."""
            if self.coef_ is None:
                raise ValueError("Model is not fitted yet.")
            raise NotImplementedError()
  6. Test your implementation by running the following code (however you like):

    n = 10_000
    logreg = LogisticRegression()
    beta = np.array([1, 1.0, -1.0])
    X, y = generate_logreg_data(n, beta, seed=42)
    
    logreg.fit(X, y)
    print("Converged:", logreg.converged_)
    print("Iterations:", logreg.n_iter_)
    print("Estimates:", logreg.coef_)

    You should get something like the following output:

     Converged: True
     Iterations: 6
     Estimates: [ 0.99101948  0.94728109 -0.93985356]
  7. Bonus: implement the negative of the log-likelihood in Equation 5 named neg_loglik with the following signature:

    def neg_loglik(
        X: NDArray[np.float64], y: NDArray[np.float64], beta: NDArray[np.float64]
    ) -> float:
        """negative log likehood for logistic regression"""
        raise NotImplementedError()

    Then install scipy in by running:

    uv pip install scipy

    and run the following snippet:

    from scipy.optimize import minimize
    import numpy as np
    from ai4h.models.linear_model import generate_logreg_data, neg_loglik
    
    n = 10_000
    beta = np.array([1, 1.0, -1.0])
    X, y = generate_logreg_data(n, beta, seed=42)
    res = minimize(
        lambda b: neg_loglik(X, y, b),
        x0=np.zeros(X.shape[1]),
        method="BFGS",
    )
    
    if res.success:
        print("Estimates:", res.x)
    else:
        print("Optimization failed:", res.message)

    Note: we did not add scipy to our pyproject.toml file as a dependency. This is because we only need it for this bonus exercise. uv pip install only temporarily installs the package in your environment but does not add it to your pyproject.toml file.

Further reading

Below are some miscellaneous links that you might find useful. None of them are required to succeed in the course; neither are they required reading; however, reading some of them might change your life (for the better).

Git

Command line

If you want to learn more about the command line see e.g.:

If you really want to become a controller of AI and conjurer of models you should get comfortable with the command line.

Windows users

If you are a Windows users see Grant McDermott’s (Principal Economist at Amazon) slides here for two ways to get the Unix shell (which is what Mac and Linux users have by default) on Windows. Try following Grant McDermott’s advice in his slides and install WSL.

If you feel even more adventurous you can try installing a Linux distribution. This is the proper way to get the full Unix experience. For instance, you can try dual-booting Ubuntu or go directly to this (don’t hesitate to ask me for details if you are interested); you won’t regret it.

Other text editors

If you have already used VsCode and aren’t that impressed you could try out a text editor called NeoVim. I have some introductory slides that introduces it here.

Misc.

More useful things to know about: