uv --version
uv 0.8.14
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.
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.
If you haven’t already done so, please install the following software on your computer:
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
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.
With uv
and git
installed, and Python 3.13
installed through uv
, we are ready to start the exercise class.
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
.
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.
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
:
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..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 laterpyproject.toml
file for you; this is a file that contains metadata about your project and its dependencies..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.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.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.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.
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.
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
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.
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.
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.
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()
from ai4h! Hello
quit()
or pressing Ctrl+D
(Mac & Linux) or Ctrl+Z
(Windows).Remove everything inside the src/ai4h/__init__.py
but don’t delete the file entirely.
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.
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.
Inside src/ai4h/models/linear_model.py
insert the following snippet:
from pathlib import Path
def test():
= Path(__file__).resolve().parents[3]
project_root = Path(__file__).resolve().relative_to(project_root)
relpath print(f"Hello from `{relpath}`!")
Open your Python interpreter again and try the following:
>>> from ai4h.models import linear_model
>>> linear_model.test()
from `src/ai4h/models/linear_model.py`! Hello
Commit your changes to git.
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.
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.
Inspect your pyproject.toml
file and assert that jupyter
has been added as a development dependency under the [dependency-groups.dev]
section.
Create a folder called notebooks
in your project directory.
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.
Open a notebook in the notebooks
folder and name it linear_model.ipynb
.
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 thesys
module and inspect thesys.executable
variable and verifying it uses the Python executable from your virtual environment.
By installing jupyter
you also install IPython
. Try repeating 7.
using IPython
instead of jupyter lab
. Start IPython by running:
uv run ipython
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.
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.
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.
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.
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}\]
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.
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.
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.
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.
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}\]
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\).
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.
Implement the expit function \(\sigma\) in Equation 1 using numpy
.
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(
int,
n:
beta: NDArray[np.float64],int = 0,
seed: -> 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:
Compute the value of p
(number of covariates) from the length of the beta
vector.
Use np.random.default_rng
to create a random number generator (RNG) called rng
to subsequently draw random numbers from. See here and here.
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]\)).
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 byp
🫠
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).
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],float = 1e-8,
tol: int = 100,
max_iter:
):"""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.
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()
Test your implementation by running the following code (however you like):
= 10_000
n = LogisticRegression()
logreg = np.array([1, 1.0, -1.0])
beta = generate_logreg_data(n, beta, seed=42)
X, y
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]
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
= 10_000
n = np.array([1, 1.0, -1.0])
beta = generate_logreg_data(n, beta, seed=42)
X, y = minimize(
res lambda b: neg_loglik(X, y, b),
=np.zeros(X.shape[1]),
x0="BFGS",
method
)
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 yourpyproject.toml
file.
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
)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.
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.
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.
More useful things to know about: