
ACTL3143 & ACTL5111 Deep Learning for Actuaries
Lecture Outline
Introduction
Traditional Regression
Stochastic Forecasts
GLMs and Neural Networks
Combined Actuarial Neural Network
Mean-Variance Estimation Network
Mixture Density Network
Distributional Refinement Network
Uncertainty and Regularisation
Dropout and Ensembles
Theyβre not inherently interpretable, so we just have to look at inputs and outputs from the black box.
Neural network models typically just output a prediction without any sense of its confidence.
Therefore we cannot trust the neural network models, which is a dealbreaker.
Now letβs focus on actuarial problems.
Claim size prediction
| π€ Age | π Age | ποΈ Type |
|---|---|---|
| 25 | 3 | π Sedan |
| 40 | 5 | π SUV |
| 19 | 1 | ποΈ Sports Car |
| 60 | 10 | π Hatchback |
\longrightarrow
| Cost |
|---|
| π΅ $1,200 |
| π΅ $2,500 |
| π΅ $3,800 |
| π΅ $800 |
Whatβs wrong? Not enough rows? Not enough columns?
Customer 1 = (25, 3, π)

Customer 2 = (40, 5, π)

Customer 3 = (19, 1, ποΈ)

Customer 4 = (60, 10, π)

All customers

Feed in a batch of customers and the model returns a predicted claim-size distribution for each one.

In practice the network doesnβt emit a whole curve β it emits the parameters of a chosen distribution family, which then define the distribution.

From a single predicted distribution we can read off the mean, the variance, and high quantiles (e.g. Value-at-Risk).

Same inputs, same model β the difference is what comes out.

A gamma GLM with a log link function:
\begin{aligned} Y | \mathbf{X} &\sim \mathrm{Gamma}(\ldots, \ldots) \\ \mathbb{E}[Y | \mathbf{X}] &= \exp\Bigl\{ \beta_0 + \beta_1 \cdot \text{Age} + \beta_2 \cdot \text{Car Age} + \beta_3 \cdot \text{Type} \Bigr\} \end{aligned}
A simple model, easy to train and interpret, butβ¦
GLMs can be

GLMs cannot (easily) do this \longrightarrow Use a neural network


Source: Tomasz WoΕΊniak (2024), LinkedIn Post, accessed on July 15 2024.
Classifiers already give us a probability, which is a big step up compared to regression models.
However, neural networksβ βprobabilitiesβ can be overconfident.
We already saw a case of this.
See Guo et al. (2017).
Lecture Outline
Introduction
Traditional Regression
Stochastic Forecasts
GLMs and Neural Networks
Combined Actuarial Neural Network
Mean-Variance Estimation Network
Mixture Density Network
Distributional Refinement Network
Uncertainty and Regularisation
Dropout and Ensembles
import random
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import keras
from keras.models import Sequential, Model
from keras.layers import Input, Dense, Concatenate, Dropout
from keras.callbacks import EarlyStopping
from keras.initializers import Constant
from keras.regularizers import L1, L2
from sklearn.model_selection import train_test_split
from sklearn.compose import make_column_transformer
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler, OrdinalEncoder
from sklearn.linear_model import LinearRegression
from sklearn.datasets import fetch_california_housing
from sklearn import set_config
set_config(transform_output="pandas")
import scipy.stats as stats
import statsmodels.api as sm
from torch.distributions import Gamma, MixtureSameFamily, CategoricalEach example below follows the same maximum-likelihood recipe:
Choose a distribution and write its density or mass function.
Y_i|\boldsymbol{X}=\boldsymbol{x}_i \sim F(\theta_i), \qquad f(y_i; \theta_i)
Multiply the probabilities to form the likelihood.
L(\boldsymbol{\beta}) = \prod_{i=1}^n f(y_i; \theta_i)
Take logs to get the log-likelihood and negate it to get negative log likelihood.
\ell(\boldsymbol{\beta}) = \sum_{i=1}^n \log f(y_i; \theta_i) \quad \Rightarrow \text{NLL}(\boldsymbol{\beta}) = -\ell(\boldsymbol{\beta})
Drop constants and rescale to get the model loss.
\text{loss}(\boldsymbol{y}, \hat{\boldsymbol{y}})
Multiple linear regression assumes the data-generating process is
Y_i = \beta_0 + \beta_1 x_{i1} + \beta_2 x_{i2} + \ldots + \beta_p x_{ip} + \varepsilon
where \varepsilon \sim \mathcal{N}(0, \sigma^2).
We estimate the coefficients \beta_0, \beta_1, \ldots, \beta_p by minimising the sum of squared residuals or mean squared error
\text{RSS} := \sum_{i=1}^n (y_i - \hat{y}_i)^2 , \quad \text{MSE} := \frac{1}{n} \sum_{i=1}^n (y_i - \hat{y}_i)^2 ,
where \hat{y}_i is the predicted value for the ith observation.
# Generate sample data for linear regression
np.random.seed(0)
X_toy = np.linspace(0, 10, 10)
np.random.shuffle(X_toy)
beta_0 = 2
beta_1 = 3
y_toy = beta_0 + beta_1 * X_toy + np.random.normal(scale=2, size=X_toy.shape)
sigma_toy = 2 # Assuming a standard deviation for the normal distribution
# Fit a simple linear regression model
coefficients = np.polyfit(X_toy, y_toy, 1)
predicted_y = np.polyval(coefficients, X_toy)
# Plot the data points and the fitted line
plt.scatter(X_toy, y_toy, label='Data Points')
plt.plot(X_toy, predicted_y, color='red', label='Fitted Line')
# Draw the normal distribution bell curve sideways at each data point
for i in range(len(X_toy)):
mu = predicted_y[i]
y_values = np.linspace(mu - 4*sigma_toy, mu + 4*sigma_toy, 100)
x_values = stats.norm.pdf(y_values, mu, sigma_toy) + X_toy[i]
plt.plot(x_values, y_values, color='blue', alpha=0.5)
plt.xlabel('$x$')
plt.ylabel('$y$')
plt.legend()
Y_i \sim \mathcal{N}(\mu_i, \sigma^2)
where \mu_i = \beta_0 + \beta_1 x_{i1} + \ldots + \beta_p x_{ip}, and the \sigma^2 is known.
The \mathcal{N}(\mu, \sigma^2) normal distribution has p.d.f.
f(y) = \frac{1}{\sqrt{2\pi\sigma^2}} \exp\left(-\frac{(y - \mu)^2}{2\sigma^2}\right) .
The likelihood function is
L(\boldsymbol{\beta}) = \prod_{i=1}^n \frac{1}{\sqrt{2\pi\sigma^2}} \exp\left(-\frac{(y_i - \mu_i)^2}{2\sigma^2}\right) \Rightarrow \ell(\boldsymbol{\beta}) = -\frac{n}{2}\log(2\pi) - \frac{n}{2}\log(\sigma^2) - \frac{1}{2\sigma^2}\sum_{i=1}^n (y_i - \mu_i)^2 .
Perform maximum likelihood estimation to find \boldsymbol{\beta}.
y_pred = np.polyval(coefficients, X_toy[:4])
fig, axes = plt.subplots(4, 1, figsize=(5.0, 3.0))
x_min = y_pred[:4].min() - 4*sigma_toy
x_max = y_pred[:4].max() + 4*sigma_toy
x_grid = np.linspace(x_min, x_max, 100)
# Plot each normal distribution with different means vertically
for i, ax in enumerate(axes):
mu = y_pred[i]
y_grid = stats.norm.pdf(x_grid, mu, sigma_toy)
ax.plot(x_grid, y_grid)
ax.set_ylabel(f'$f(y ; \\boldsymbol{{x}}_{{{i+1}}})$')
ax.set_xticks([y_pred[i]], labels=[r'$\mu_{' + str(i+1) + r'}$'])
ax.plot(y_toy[i], 0, 'rx', clip_on=False)
plt.tight_layout();
The negative log-likelihood \text{NLL}(\boldsymbol{\beta}) := -\ell(\boldsymbol{\beta}) is to be minimised:
\text{NLL}(\boldsymbol{\beta}) = \frac{n}{2}\log(2\pi) + \frac{n}{2}\log(\sigma^2) + \frac{1}{2\sigma^2}\sum_{i=1}^n (y_i - \mu_i)^2 .
As \sigma^2 is fixed, minimising NLL is equivalent to minimising MSE:
\begin{aligned} \boldsymbol{\beta}^* &= \underset{\boldsymbol{\beta}}{\operatorname{arg\,min}}\,\, \text{NLL}(\boldsymbol{\beta}) \\ &= \underset{\boldsymbol{\beta}}{\operatorname{arg\,min}}\,\, \frac{n}{2}\log(2\pi) + \frac{n}{2}\log(\sigma^2) + \frac{1}{2\sigma^2}\sum_{i=1}^n (y_i - \mu_i)^2 \\ &= \underset{\boldsymbol{\beta}}{\operatorname{arg\,min}}\,\, \frac{1}{n} \sum_{i=1}^n \Bigl( y_i - \hat{y}_i(\boldsymbol{x}_i; \boldsymbol{\beta}) \Bigr)^2 \\ &= \underset{\boldsymbol{\beta}}{\operatorname{arg\,min}}\,\, \text{MSE}\bigl( \boldsymbol{y}, \hat{\boldsymbol{y}}(\mathbf{X}; \boldsymbol{\beta}) \bigr). \end{aligned}
The GLM is often characterised by the mean prediction:
\mu(\boldsymbol{x}; \boldsymbol{\beta}) = g^{-1} \left(\left\langle \boldsymbol{\beta}, \boldsymbol{x} \right\rangle\right)
where g is the link function.
Common GLM distributions for the response variable include:
A Bernoulli distribution with parameter p has p.m.f.
f(y)\ =\ \begin{cases} p & \text{if } y = 1 \\ 1 - p & \text{if } y = 0 \end{cases} \ =\ p^y (1 - p)^{1 - y}.
Our model is Y|\boldsymbol{X}=\boldsymbol{x} follows a Bernoulli distribution with parameter
\mu(\boldsymbol{x}; \boldsymbol{\beta}) = \frac{1}{1 + \exp\left(-\left\langle \boldsymbol{\beta}, \boldsymbol{x} \right\rangle\right)} = \mathbb{P}(Y=1|\boldsymbol{X}=\boldsymbol{x}).
The likelihood function, using \mu_i := \mu(\boldsymbol{x}_i; \boldsymbol{\beta}), is
L(\boldsymbol{\beta}) \ =\ \prod_{i=1}^n \begin{cases} \mu_i & \text{if } y_i = 1 \\ 1 - \mu_i & \text{if } y_i = 0 \end{cases} \ =\ \prod_{i=1}^n \mu_i^{y_i} (1 - \mu_i)^{1 - y_i} .
L(\boldsymbol{\beta}) = \prod_{i=1}^n \mu_i^{y_i} (1 - \mu_i)^{1 - y_i} \Rightarrow \ell(\boldsymbol{\beta}) = \sum_{i=1}^n \Bigl( y_i \log(\mu_i) + (1 - y_i) \log(1 - \mu_i) \Bigr).
The negative log-likelihood is
\text{NLL}(\boldsymbol{\beta}) = -\sum_{i=1}^n \Bigl( y_i \log(\mu_i) + (1 - y_i) \log(1 - \mu_i) \Bigr).
The binary cross-entropy loss is basically identical: \text{BCE}(\boldsymbol{y}, \boldsymbol{\mu}) = - \frac{1}{n} \sum_{i=1}^n \Bigl( y_i \log(\mu_i) + (1 - y_i) \log(1 - \mu_i) \Bigr).
A Poisson distribution with rate \lambda has p.m.f. f(y) = \frac{\lambda^y \exp(-\lambda)}{y!}.
Our model is Y|\boldsymbol{X}=\boldsymbol{x} is Poisson distributed with parameter
\mu(\boldsymbol{x}; \boldsymbol{\beta}) = \exp\left(\left\langle \boldsymbol{\beta}, \boldsymbol{x} \right\rangle\right) .
The likelihood function is
L(\boldsymbol{\beta}) = \prod_{i=1}^n \frac{ \mu_i^{y_i} \exp(-\mu_i) }{y_i!} \Rightarrow \ell(\boldsymbol{\beta}) = \sum_{i=1}^n \Bigl( -\mu_i + y_i \log(\mu_i) - \log(y_i!) \Bigr).
The negative log-likelihood is
\text{NLL}(\boldsymbol{\beta}) = \sum_{i=1}^n \Bigl( \mu_i - y_i \log(\mu_i) + \log(y_i!) \Bigr) .
The Poisson loss is
\text{Poisson}(\boldsymbol{y}, \boldsymbol{\mu}) = \frac{1}{n} \sum_{i=1}^n \Bigl( \mu_i - y_i \log(\mu_i) \Bigr).
A Gamma distribution with mean \mu and dispersion \phi has p.d.f. f(y; \mu, \phi) = \frac{(\mu \phi)^{-\frac{1}{\phi}}}{\Gamma\left(\frac{1}{\phi}\right)} y^{\frac{1}{\phi} - 1} \mathrm{e}^{-\frac{y}{\mu \phi}}
Our model is Y|\boldsymbol{X}=\boldsymbol{x} is Gamma distributed with a dispersion of \phi and a mean of \mu(\boldsymbol{x}; \boldsymbol{\beta}) = \exp\left(\left\langle \boldsymbol{\beta}, \boldsymbol{x} \right\rangle\right).
The likelihood function is L(\boldsymbol{\beta}) = \prod_{i=1}^n \frac{(\mu_i \phi)^{-\frac{1}{\phi}}}{\Gamma\left(\frac{1}{\phi}\right)} y_i^{\frac{1}{\phi} - 1} \exp\left(-\frac{y_i}{\mu_i \phi}\right)
\Rightarrow \ell(\boldsymbol{\beta}) = \sum_{i=1}^n \left[ -\frac{1}{\phi} \log(\mu_i \phi) - \log \Gamma\left(\frac{1}{\phi}\right) + \left(\frac{1}{\phi} - 1\right) \log(y_i) - \frac{y_i}{\mu_i \phi} \right].
The negative log-likelihood is
\text{NLL}(\boldsymbol{\beta}) = \sum_{i=1}^n \left[ \frac{1}{\phi} \log(\mu_i \phi) + \log \Gamma\left(\frac{1}{\phi}\right) - \left(\frac{1}{\phi} - 1\right) \log(y_i) + \frac{y_i}{\mu_i \phi} \right].
Since \phi is a nuisance parameter \text{NLL}(\boldsymbol{\beta}) = \sum_{i=1}^n \left[ \frac{1}{\phi} \log(\mu_i) + \frac{y_i}{\mu_i \phi} \right] + \text{const} \propto \sum_{i=1}^n \left[ \log(\mu_i) + \frac{y_i}{\mu_i} \right].
Note
As \log(\mu_i) = \log(y_i) - \log(y_i / \mu_i), we could write an alternative version \text{NLL}(\boldsymbol{\beta}) \propto \sum_{i=1}^n \left[ \log(y_i) - \log\Bigl(\frac{y_i}{\mu_i}\Bigr) + \frac{y_i}{\mu_i} \right] \propto \sum_{i=1}^n \left[ \frac{y_i}{\mu_i} - \log\Bigl(\frac{y_i}{\mu_i}\Bigr) \right].
This last point is particularly important for analysing worst-case scenarios.
Lecture Outline
Introduction
Traditional Regression
Stochastic Forecasts
GLMs and Neural Networks
Combined Actuarial Neural Network
Mean-Variance Estimation Network
Mixture Density Network
Distributional Refinement Network
Uncertainty and Regularisation
Dropout and Ensembles
def lagged_timeseries(df, target, window):
lagged = pd.DataFrame()
for i in range(window, 0, -1):
lagged[f"T-{i}"] = df[target].shift(i)
lagged["T"] = df[target].values
return lagged
stocks = pd.read_csv("data/interim/aus_fin_stocks.csv")
stocks["Date"] = pd.to_datetime(stocks["Date"])
stocks = stocks.set_index("Date")
_ = stocks.pop("ASX200")
stock = stocks[["CBA"]]
stock = stock.ffill()
# Compute daily log returns
stock_log = np.log(stock / stock.shift(1)).dropna()
# Helper functions for converting log returns to prices
def log_to_price(log_returns, initial_price):
cumulative_log_returns = log_returns.cumsum()
return initial_price * np.exp(cumulative_log_returns)
def get_last_price(stock_df, cutoff_date):
last_known_date = stock_df.loc[:cutoff_date].index[-1]
return stock_df.loc[last_known_date, "CBA"]
# Create lagged features from log returns
df_lags = lagged_timeseries(stock_log, "CBA", 40)
# Split the data in time (same cutoffs as the Time Series lecture)
X_train = df_lags.loc[:"2014"]
X_val = df_lags.loc["2015":"2020"]
X_test = df_lags.loc["2021":]
# Remove any with NAs and split into X and y
X_train = X_train.dropna()
X_val = X_val.dropna()
X_test = X_test.dropna()
y_train = X_train.pop("T")
y_val = X_val.pop("T")
y_test = X_test.pop("T")
lr = LinearRegression()
lr.fit(X_train, y_train);def noisy_autoregressive_forecast(model, X_val, sigma):
"""Roll a one-step model forward, feeding each (noisy) prediction back in."""
window = np.asarray(X_val.iloc[0], dtype="float32").copy()
preds = []
for _ in range(len(X_val)):
X_next = pd.DataFrame(window.reshape(1, -1), columns=X_val.columns)
next_value = model.predict(X_next).flatten()[0]
next_value += np.random.normal(0, sigma)
preds.append(next_value)
window = np.append(window[1:], next_value) # drop oldest, add prediction
return pd.Series(preds, index=X_val.index, name="Multi Step")
# Plot the mean forecast
plt.figure(figsize=(8, 3))
plt.plot(stock.loc["2020-12":].index, stock.loc["2020-12":]["CBA"], label="CBA")
plt.plot(mean_forecast, label="Mean")
# Plot the quantile-based shaded area
plt.fill_between(mean_forecast.index,
lower_quantile,
upper_quantile,
color="grey", alpha=0.2)
# Plot settings
plt.axvline(pd.Timestamp("2021-01-01"), color="black", linestyle="--")
plt.legend(loc="center left", bbox_to_anchor=(1, 0.5))
plt.xlabel("Date")
plt.ylabel("Stock Price")
plt.tight_layout();

Heteroskedasticity!
# Drop the GFC years (2008-2009) from the training set
mask = ~((y_train.index.year >= 2008) & (y_train.index.year <= 2009))
X2 = X_train.loc[mask]
y2 = y_train.loc[mask]
lr2 = LinearRegression()
lr2.fit(X2, y2)
res2 = y2 - lr2.predict(X2)
res2 = (res2 - np.mean(res2)) / np.std(res2)
plt.plot(y2.index, res2)
plt.xlabel("Date")
plt.ylabel("Standardised Residuals")
plt.tight_layout();
Refit the model without the GFC crisis years.
ShapiroResult(statistic=np.float64(0.983029219714092), pvalue=np.float64(3.326265726133293e-20))
Lecture Outline
Introduction
Traditional Regression
Stochastic Forecasts
GLMs and Neural Networks
Combined Actuarial Neural Network
Mean-Variance Estimation Network
Mixture Density Network
Distributional Refinement Network
Uncertainty and Regularisation
Dropout and Ensembles
| ClaimAmount | Exposure | VehPower | VehAge | DrivAge | BonusMalus | VehBrand | VehGas | Area | Density | Region | |
|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 995.20 | 0.59 | 11.0 | 0.0 | 39.0 | 56.0 | B12 | Diesel | D | 778.0 | Picardie |
| 1 | 1128.12 | 0.95 | 4.0 | 1.0 | 49.0 | 50.0 | B12 | Regular | E | 2354.0 | Ile-de-France |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 26637 | 767.55 | 0.43 | 6.0 | 0.0 | 67.0 | 50.0 | B2 | Diesel | C | 142.0 | Languedoc-Roussillon |
| 26638 | 1500.00 | 0.28 | 7.0 | 2.0 | 36.0 | 60.0 | B12 | Diesel | D | 1732.0 | Rhone-Alpes |
26444 rows Γ 11 columns
X_train, X_test, y_train, y_test = train_test_split(
sev.drop("ClaimAmount", axis=1), sev["ClaimAmount"], random_state=2023)
ct = make_column_transformer(
(make_pipeline(OrdinalEncoder(), StandardScaler()), ["Area", "VehGas"]),
("drop", ["VehBrand", "Region"]), remainder=StandardScaler())
X_train = ct.fit_transform(X_train)
X_test = ct.transform(X_test)
plt.hist(y_train[y_train < 5000], bins=30);
# Make some example where the distribution is multimodal because of a binary covariate which separates the means of the two distributions
np.random.seed(1)
fig, axes = plt.subplots(3, 1, figsize=(5.0, 3.0), sharex=True)
x_min = 0
x_max = y_train.max()
x_grid = np.linspace(x_min, x_max, 100)
# Simulate some data from an exponential distribution which has Pr(X < 1000) = 0.9
n = 100
p = 0.1
lambda_ = -np.log(p) / 1000
mu = 1 / lambda_
y_1 = np.random.exponential(scale=mu, size=n)
# Pick a truncated normal distribution with a mean of 1100 and std of 250 (truncated to be positive)
mu = 1100
sigma = 100
y_2 = stats.truncnorm.rvs((0 - mu) / sigma, (np.inf - mu) / sigma, loc=mu, scale=sigma, size=n)
# Combine y_1 and y_2 for the final histogram
y = np.concatenate([y_1, y_2])
# Determine common bins
bins = np.histogram_bin_edges(y, bins=30)
# Plot each normal distribution with different means vertically
for i, ax in enumerate(axes):
if i == 0:
ax.hist(y_1, bins=bins, density=True, color=COLOURS[i+1])
ax.set_ylabel(f'$f(y | x = 1)$')
elif i == 1:
ax.hist(y_2, bins=bins, density=True, color=COLOURS[i+1])
ax.set_ylabel(f'$f(y | x = 2)$')
else:
ax.hist(y, bins=bins, density=True)
ax.set_ylabel(f'$f(y)$')
plt.tight_layout();
Suppose a fitted Gamma GLM model has
Then, it estimates the conditional mean of Y given a new instance \boldsymbol{x}=(1, x_1, x_2, x_3) as follows: \mathbb{E}[Y|\boldsymbol{X}=\boldsymbol{x}] = g^{-1}(\langle \boldsymbol{\beta}, \boldsymbol{x}\rangle) = \exp\big(\beta_0 + \beta_1 x_1 + \beta_2 x_2 + \beta_3 x_3 \big).
A GLM can model any other exponential family distribution using an appropriate link function g.
If Y|\boldsymbol{X}=\boldsymbol{x} is a Gamma r.v. with mean \mu(\boldsymbol{x}; \boldsymbol{\beta}) and dispersion parameter \phi, we can minimise the negative log-likelihood (NLL) \text{NLL} \propto \sum_{i=1}^{n}\left[ \log \mu (\boldsymbol{x}_i; \boldsymbol{\beta})+\frac{y_i}{\mu (\boldsymbol{x}_i; \boldsymbol{\beta})} \right] + \text{const}, i.e., we ignore the dispersion parameter \phi while estimating the regression coefficients.
A GLM predicts the mean for each input; the dispersion is a single shared constant, estimated separately.

Step 1. Use the advanced second derivative iterative method to find the regression coefficients: \boldsymbol{\beta}^* = \underset{\boldsymbol{\beta}}{\text{arg\,min}} \ \sum_{i=1}^{n}\left[ \log \mu (\boldsymbol{x}_i; \boldsymbol{\beta})+\frac{y_i}{\mu (\boldsymbol{x}_i; \boldsymbol{\beta})} \right]
Step 2. Estimate the dispersion parameter: \phi = \frac{1}{n-p}\sum_{i=1}^{n}\frac{\bigl(y_i-\mu(\boldsymbol{x}_i; \boldsymbol{\beta}^*)\bigr)^2}{\mu(\boldsymbol{x}_i; \boldsymbol{\beta}^* )^2}
(Here, p is the number of coefficients in the model. If this p doesnβt include the intercept, then the scaling should be \frac{1}{n-(p+1)}.)
In Python, we can fit a Gamma GLM as follows:
Combining GLM & ANN.
Source: Ronald Richman (2022), Mind the Gap β Safely Incorporating Deep Learning Models into the Actuarial Toolkit, IFoA seminar, Slide 14.
A GLM has a linear predictor. A deep GLM (Tran et al., 2020) swaps that for a neural network β a non-linear mean β but keeps the same Gamma loss, so it still predicts a single Gamma distribution.
def gamma_loss(y_true, y_pred):
return keras.ops.mean(keras.ops.log(y_pred) + y_true / y_pred)
random.seed(1)
inputs = Input(shape=X_train.shape[1:])
x = Dense(64, activation="leaky_relu")(inputs)
x = Dense(64, activation="leaky_relu")(x)
mu = Dense(1, activation="exponential")(x) + 1e-6
deep_glm = Model(inputs, mu)
deep_glm.compile(optimizer="adam", loss=gamma_loss)
deep_glm.fit(X_train, y_train, epochs=100, batch_size=64, verbose=0,
callbacks=[EarlyStopping(patience=10, restore_best_weights=True)],
validation_split=0.2);Lecture Outline
Introduction
Traditional Regression
Stochastic Forecasts
GLMs and Neural Networks
Combined Actuarial Neural Network
Mean-Variance Estimation Network
Mixture Density Network
Distributional Refinement Network
Uncertainty and Regularisation
Dropout and Ensembles
The Combined Actuarial Neural Network is an actuarial neural network architecture proposed by Schelldorfer & WΓΌthrich (2019). We summarise the CANN approach as follows:
\mathbb{E}[Y|\boldsymbol{X}=\boldsymbol{x}] = g^{-1}\Big( \langle\boldsymbol{\beta}, \boldsymbol{x}\rangle + s(\boldsymbol{x};\boldsymbol{w})\Big).
If just a sequential dense network, then
\mathbb{E}[Y|\boldsymbol{X}=\boldsymbol{x}] = g^{-1}\Big( \langle\boldsymbol{\beta}, \boldsymbol{x}\rangle + \Big\langle \boldsymbol{w}^{(K+1)}, \bigl( \boldsymbol{z}^{(K)} \circ \dots \circ \boldsymbol{z}^{(1)}\bigr)(\boldsymbol{x}) \Big\rangle \Big).
# Ensure reproducibility
random.seed(1)
# Make a 4x1 grid of plots
fig, axes = plt.subplots(4, 1, figsize=(5.0, 3.0), sharex=True)
# Define the x-axis
x_min = 0
x_max = 5000
x_grid = np.linspace(x_min, x_max, 100)
# Plot a few Gamma distribution pdfs with different means.
# Then plot Gamma distributions with shifted means and the same dispersion parameter.
glm_means = [1000, 3000, 2000, 4000]
cann_means = [1500, 1400, 3000, 5000]
for i, ax in enumerate(axes):
ax.plot(x_grid, stats.gamma.pdf(x_grid, a=2, scale=glm_means[i]/2), label=f'GLM')
ax.plot(x_grid, stats.gamma.pdf(x_grid, a=2, scale=cann_means[i]/2), label=f'CANN')
ax.set_ylabel(f'$f(y | x_{i+1})$')
if i == 0:
ax.legend(["GLM", "CANN"], loc="upper right", ncol=2)
The CANN architecture.
Source: Figure 8 of Schelldorfer & WΓΌthrich (2019).
random.seed(1)
inputs = Input(shape=X_train.shape[1:])
# GLM part (won't be updated during training)
glm_weights = gamma_glm.params.iloc[1:].values.reshape((-1, 1))
glm_bias = gamma_glm.params.iloc[0]
glm_part = Dense(1, activation='linear', trainable=False,
kernel_initializer=Constant(glm_weights),
bias_initializer=Constant(glm_bias))(inputs)
# Neural network part
x = Dense(64, activation='leaky_relu')(inputs)
nn_part = Dense(1, activation='linear',
kernel_initializer="zeros",
bias_initializer="zeros")(x)
# Combine GLM and CANN estimates
mu = keras.ops.exp(glm_part + nn_part) + 1e-6
cann = Model(inputs, mu)Since this CANN predicts Gamma distributions, we reuse the same gamma_loss from the deep GLM.
Find the dispersion parameter.
Lecture Outline
Introduction
Traditional Regression
Stochastic Forecasts
GLMs and Neural Networks
Combined Actuarial Neural Network
Mean-Variance Estimation Network
Mixture Density Network
Distributional Refinement Network
Uncertainty and Regularisation
Dropout and Ensembles
Our model is
Y \mid \boldsymbol{x} \;\sim\; \mathcal{N}\bigl(\mu(\boldsymbol{x}),\, \sigma^2(\boldsymbol{x})\bigr) .

Make a neural network that models Y \mid \boldsymbol{x} \;\sim\; \mathcal{N}\bigl(\mu(\boldsymbol{x}),\, \sigma^2(\boldsymbol{x})\bigr).
y_pred = np.polyval(coefficients, X_toy[:4])
y_pred[2] *= 1.1
sigma_preds = sigma_toy * np.array([1.0, 3.0, 0.5, 0.5])
fig, axes = plt.subplots(1, 4, figsize=(5.0, 2.0), sharey=True)
x_min = y_pred[:4].min() - 4*sigma_toy
x_max = y_pred[:4].max() + 4*sigma_toy
x_grid = np.linspace(x_min, x_max, 100)
# Plot each normal distribution with different means vertically
for i, ax in enumerate(axes):
y_grid = stats.norm.pdf(x_grid, y_pred[i], sigma_preds[i])
ax.plot(x_grid, y_grid)
ax.plot([y_toy[i], y_toy[i]], [0, stats.norm.pdf(y_toy[i], y_pred[i], sigma_preds[i])], color='red', linestyle='--')
ax.scatter([y_toy[i]], [stats.norm.pdf(y_toy[i], y_pred[i], sigma_preds[i])], color='red', zorder=10)
ax.set_title(f'$f(y ; \\boldsymbol{{x}}_{{{i+1}}})$')
ax.set_xticks([y_pred[i]], labels=[r'$\mu_{' + str(i+1) + r'}$'])
# ax.set_ylim(0, 0.25)
# Turn off the y axes
ax.yaxis.set_visible(False)
plt.tight_layout();
Task: Assume you have X_train and y_train loaded and write the following code, everything up to the model.fit(X_train, y_train) line.
Mean-variance estimation networks: Nix & Weigend (1994).
Lecture Outline
Introduction
Traditional Regression
Stochastic Forecasts
GLMs and Neural Networks
Combined Actuarial Neural Network
Mean-Variance Estimation Network
Mixture Density Network
Distributional Refinement Network
Uncertainty and Regularisation
Dropout and Ensembles
The mean-variance network outputs the parameters of a single normal. But our data may be multimodal, or have some other shape which no single normal or gamma can capture.
A mixture density network (MDN) operates the same way, except now we model Y \mid \boldsymbol{x} as a mixture of K components. The network outputs the mixing weights \boldsymbol{\pi}(\boldsymbol{x}) and each componentβs parameters, giving the density f_{Y|\boldsymbol{X}}(y \mid \boldsymbol{x}) = \sum_{k=1}^{K}\pi_k(\boldsymbol{x})\, f_{k}(y \mid \boldsymbol{x}), \qquad \pi_k(\boldsymbol{x}) \ge 0,\ \ \sum_{k=1}^{K}\pi_k(\boldsymbol{x})=1 . Each component f_k can be any parametric family (normal, gamma, etc.).
Mixture density networks: Bishop (1994).

Suppose there are two types of claims:
(Note the new gamma parametrisation.) So the claim amount Y|\boldsymbol{X}=\boldsymbol{x} has density \begin{align*} f_{Y|\boldsymbol{X}}(y|\boldsymbol{x}) &= \pi_1(\boldsymbol{x})\cdot \frac{\beta_1(\boldsymbol{x})^{\alpha_1(\boldsymbol{x})}}{\Gamma(\alpha_1(\boldsymbol{x}))}\mathrm{e}^{-\beta_1(\boldsymbol{x})y}y^{\alpha_1(\boldsymbol{x})-1} \\ &\quad + \pi_2(\boldsymbol{x})\cdot \frac{\beta_2(\boldsymbol{x})^{\alpha_2(\boldsymbol{x})}}{\Gamma(\alpha_2(\boldsymbol{x}))}\mathrm{e}^{-\beta_2(\boldsymbol{x})y}y^{\alpha_2(\boldsymbol{x})-1}, \end{align*} where \pi_1(\boldsymbol{x}) is the probability of a Type I claim. So here the networkβs six outputs are the mixing weights, shapes and rates: \text{MDN}(\boldsymbol{x}; \boldsymbol{w}^*) = \bigl( \pi_1, \pi_2,\ \alpha_1, \alpha_2,\ \beta_1, \beta_2 \bigr).

The following code implements the gamma MDN from the previous slide.
random.seed(1)
n_components = 2
inputs = Input(shape=X_train.shape[1:])
x = Dense(64, activation="relu")(inputs)
x = Dense(64, activation="relu")(x)
pis = Dense(n_components, activation="softmax")(x)
alphas = Dense(n_components, activation="softplus")(x) + 1e-6
betas = Dense(n_components, activation="softplus")(x) + 1e-6
outputs = Concatenate()([pis, alphas, betas])
gamma_mdn = Model(inputs, outputs)torch.distributions to the rescue.
def gamma_mixture_nll(y_true, y_pred):
pis = y_pred[:, :n_components]
alphas = y_pred[:, n_components:2*n_components]
betas = y_pred[:, 2*n_components:]
mixture = MixtureSameFamily(
mixture_distribution=Categorical(probs=pis),
component_distribution=Gamma(concentration=alphas, rate=betas))
return -keras.ops.mean(mixture.log_prob(keras.ops.squeeze(y_true)))We can now train it like any other Keras model.
Lecture Outline
Introduction
Traditional Regression
Stochastic Forecasts
GLMs and Neural Networks
Combined Actuarial Neural Network
Mean-Variance Estimation Network
Mixture Density Network
Distributional Refinement Network
Uncertainty and Regularisation
Dropout and Ensembles
Start from a trusted parametric baseline (e.g. a GLM), then let a neural network make small adjustments to the whole distribution.
The DRN keeps the baselineβs shape but flexibly refines it using the features.
Source: Avanzi et al. (2026).
Discretise the baseline into bins, then multiply each binβs mass by a learned adjustment factor \hat{a}_k.


A penalty keeps the adjustments faithful to the baseline, so the model stays interpretable and trustworthy.
Source: Avanzi et al. (2026).
drn packageThe drn package fits the DRN architecture along with todayβs earlier models:
GLM: 11.02
CANN: 10.27
MDN: 8.78
DRN: 8.32
Source: Avanzi et al. (2026).
Lecture Outline
Introduction
Traditional Regression
Stochastic Forecasts
GLMs and Neural Networks
Combined Actuarial Neural Network
Mean-Variance Estimation Network
Mixture Density Network
Distributional Refinement Network
Uncertainty and Regularisation
Dropout and Ensembles
There are two major categories of uncertainty in statistical or machine learning:
If you decide to predict the claim amount of an individual using a deep learning model, which source(s) of uncertainty are you dealing with?
Say all the m weights (excluding biases) are in the vector \boldsymbol{\theta}. If we change the loss function to \text{Loss}_{1:n} = \frac{1}{n} \sum_{i=1}^n \text{Loss}_i + \lambda \sum_{j=1}^{m} \left| \theta_j \right|
this would be using L^1 regularisation. A loss like
\text{Loss}_{1:n} = \frac{1}{n} \sum_{i=1}^n \text{Loss}_i + \lambda \sum_{j=1}^{m} \theta_j^2
is called L^2 regularisation.
features, target = fetch_california_housing(as_frame=True, return_X_y=True)
NUM_FEATURES = len(features.columns)
X_main, X_test, y_main, y_test = train_test_split(
features, target, test_size=0.2, random_state=1
)
X_train, X_val, y_train, y_val = train_test_split(
X_main, y_main, test_size=0.25, random_state=1
)
scaler = StandardScaler()
X_train_sc = scaler.fit_transform(X_train)
X_val_sc = scaler.transform(X_val)
X_test_sc = scaler.transform(X_test)def l1_model(regulariser_strength=0.01):
random.seed(123)
model = Sequential([
Dense(30, activation="leaky_relu",
kernel_regularizer=L1(regulariser_strength)),
Dense(1, activation="exponential")
])
model.compile("adam", "mse")
model.fit(X_train_sc, y_train, epochs=4, verbose=0)
return model
def l2_model(regulariser_strength=0.01):
random.seed(123)
model = Sequential([
Dense(30, activation="leaky_relu",
kernel_regularizer=L2(regulariser_strength)),
Dense(1, activation="exponential")
])
model.compile("adam", "mse")
model.fit(X_train_sc, y_train, epochs=10, verbose=0)
return modelA very different way to regularize iterative learning algorithms such as gradient descent is to stop training as soon as the validation error reaches a minimum. This is called early stoppingβ¦ It is such a simple and efficient regularization technique that Geoffrey Hinton called it a βbeautiful free lunchβ.
Alternatively, you can try building a model with slightly more layers and neurons than you actually need, then use early stopping and other regularization techniques to prevent it from overfitting too much. Vincent Vanhoucke, a scientist at Google, has dubbed this the βstretch pantsβ approach: instead of wasting time looking for pants that perfectly match your size, just use large stretch pants that will shrink down to the right size.
Source: GΓ©ron (2019), Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow, 3rd Edition, Chapters 4 and 10.
Lecture Outline
Introduction
Traditional Regression
Stochastic Forecasts
GLMs and Neural Networks
Combined Actuarial Neural Network
Mean-Variance Estimation Network
Mixture Density Network
Distributional Refinement Network
Uncertainty and Regularisation
Dropout and Ensembles
An example of neurons dropped during training.
Sources: Marcus Lautier (2022).
Itβs surprising at first that this destructive technique works at all. Would a company perform better if its employees were told to toss a coin every morning to decide whether or not to go to work? Well, who knows; perhaps it would! The company would be forced to adapt its organization; it could not rely on any single person to work the coffee machine or perform any other critical tasks, so this expertise would have to be spread across several people. Employees would have to learn to cooperate with many of their coworkers, not just a handful of them. The company would become much more resilient. If one person quit, it wouldnβt make much of a difference. Itβs unclear whether this idea would actually work for companies, but it certainly does for neural networks. Neurons trained with dropout cannot co-adapt with their neighboring neurons; they have to be as useful as possible on their own. They also cannot rely excessively on just a few input neurons; they must pay attention to each of their input neurons. They end up being less sensitive to slight changes in the inputs. In the end, you get a more robust network that generalizes better.
Source: AurΓ©lien GΓ©ron (2019), Hands-On Machine Learning with Scikit-Learn, Keras, and TensorFlow, 2nd Edition, p. 366
Dropout is just another layer in Keras.
Making predictions is the same as any other model:
We can make the model think it is still training:
By setting the training=True, we can let dropout happen during prediction stage as well. This will change predictions for the same output differently. This is known as the Monte Carlo dropout and can be used to generate a distribution of predictions.
Combine many models to get better predictions.
Source: Marcus Lautier (2022).
Train M neural networks with different random initial weights independently (even in parallel).
def build_model(seed):
random.seed(seed)
model = Sequential([
Dense(30, activation="leaky_relu"),
Dense(1, activation="exponential")
])
model.compile("adam", "mse")
es = EarlyStopping(restore_best_weights=True, patience=5)
model.fit(X_train_sc, y_train, epochs=1_000,
callbacks=[es], validation_data=(X_val_sc, y_val), verbose=False)
return modelSay the trained weights for the NNs are \boldsymbol{w}^{(1)}, \ldots, \boldsymbol{w}^{(M)}, then we get predictions \bigl\{ \hat{y}(\boldsymbol{x}; \boldsymbol{w}^{(m)}) \bigr\}_{m=1}^{M}
array([[[3.36],
[0.79],
[2.33],
...,
[2.42],
[2.43],
[1.09]],
[[1.66],
[1.13],
[1.72],
...,
[1.91],
[1.94],
[2.34]],
[[2.94],
[0.92],
[2.41],
...,
[2.26],
[2.54],
[1.31]]], shape=(3, 4128, 1), dtype=float32)
Python implementation: CPython
Python version : 3.14.5
IPython version : 9.15.0
keras : 3.15.0
matplotlib: 3.11.0
numpy : 2.5.0
pandas : 3.0.3
seaborn : 0.13.2
scipy : 1.18.0
torch : 2.12.1
drn : 1.0.0
