21.1. Model Formulation
Alpha Version: Work in progress.
This section presents the model formulation and loss function for GPT-1 fine-tuning, using Natural Language Inference (NLI) as an example.
21.1.1. Model Formulation
NLI tasks involve predicting the relationship label between a pair of input sentences. Consequently, its architecture builds directly upon the Sequence Classification model introduced in Chapter 12.
The fine-tuning architecture takes the final transformer output representation $h_{L}$ and passes it through a linear (dense) layer followed by a softmax function to generate class predictions.
During fine-tuning, the premise and hypothesis are concatenated into a single input sequence $X$ and fed into the pre-trained GPT-1 model. The hidden representation corresponding to the final token, denoted as $h_{L}^{m}$, is projected via a linear layer (with weight matrix $W_{y}$) and transformed by softmax to compute the class probability distribution.
Figure 21.2 illustrates the overall fine-tuning architecture.
Fig.21-2: GPT-1 Fine-Tuning Model.
Let the input sequence $X$ be represented as
$$ X = [x^{0}, \ldots, x^{m}], $$
and the target class label $y$ as
$$y \in \lbrace 0, \ldots, \vert{}\text{Category}\vert{}-1 \rbrace.$$
Let $h_{L}^{m}$ denote the hidden representation of the final input token (i.e., the last row of $h_{L}$). The predicted output probability vector $P \in \mathbb{R}^{\vert{}\text{Category}\vert{}}$ is computed as follows:
$$ P = \operatorname{softmax}(h_{L}^{m}W_{y}) $$
where:
- $P \in \mathbb{R}^{\vert{}\text{Category}\vert{}}$ is the predicted class probability vector.
- $h_{L}^{m} \in \mathbb{R}^{1 \times d_{\text{model}}}$ is the hidden representation of the final input token.
- $W_{y} \in \mathbb{R}^{d_{\text{model}} \times \vert{}\text{Category}\vert{}}$ is the weight matrix of the linear classification layer.
21.1.2. Loss Function
With the classifier defined, the model is trained by minimizing the categorical cross-entropy loss $L_{c}$.
The categorical cross-entropy loss $L_{c}$ is formulated as:
$$L_{c} = - \sum_{k=1}^{\vert{}\text{Category}\vert{}} Y_{k} \log(P(y_{k}\mid x^{0}, \ldots, x^{m}))$$
where:
- $Y \in \mathbb{R}^{\vert{}\text{Category}\vert{}}$ is the one-hot encoded ground-truth vector for label $y$.
- $Y_k \in \lbrace 0,1 \rbrace$ denotes the $k$-th element of $Y$, where $Y_k = 1$ for the target class and $Y_k = 0$ otherwise.
If the true class index is $c$, the one-hot vector $Y$ can be expressed as:
$$ Y = \underbrace{ [0,\ldots,0,\overset{c\text{-th}}{1},0,\ldots,0] }_{|\text{Category}|} $$Consequently, the classification loss $L_{c}$ simplifies to:
$$L_{c} = - \log(P(y_{c}\mid x^{0}, \ldots, x^{m}))$$
However, optimizing solely for the task-specific classification loss risks degrading the general language modeling capabilities acquired during pre-training—a phenomenon known as catastrophic forgetting.
To mitigate this issue, GPT-1 fine-tuning incorporates the auxiliary language-model loss $L_{p}$ alongside the categorical cross-entropy loss $L_{c}$.
Specifically, the overall fine-tuning objective function $L_{f}$ is defined as a weighted sum of both losses:
$$L_{f} = L_{c} + \lambda L_{p}$$
where $\lambda \in [0,1]$ is a hyperparameter balancing the two loss components. The original GPT-1 implementation adopts $\lambda = 0.5$.
The auxiliary loss $L_{p}$ is computed over the same input sequence by predicting the next token at each position, following the exact procedure outlined in Chapter 20.
Including this auxiliary objective helps retain pre-trained language knowledge while adapting the model to downstream target tasks.
21.1.3. Implementation
The following function computes the categorical cross-entropy loss $L_{c}$:
def clf_loss_fn(labels, logits):
"""
Classification cross-entropy
Parameters
----------
labels : (B,) integer class indices {0=entailment, 1=neutral, 2=contradiction}
logits : (B, 3) raw classification logits
Returns
-------
Scalar mean cross-entropy over the batch.
"""
loss = tf.keras.losses.sparse_categorical_crossentropy(
labels, logits, from_logits=True
) # (B,)
return tf.reduce_mean(loss)The function below constructs the fine-tuning step, computing the joint loss $L_{f}$:
def make_finetune_step(model: GPT1ForNLI, optimizer, lm_coef: float, grad_clip: float):
"""
Return a @tf.function-compiled fine-tuning step for *model*.
The returned function implements the joint objective from Eq. 5:
Lf = Lc(clf) + \lambda \dot Lp(lm)
It updates:
train_loss <- Lf (joint)
train_clf_loss <- Lc (classification only)
train_lm_loss <- Lp (auxiliary LM only)
train_accuracy <- NLI classification accuracy
"""
@tf.function(input_signature=_finetune_step_signature)
def finetune_step(input_ids, labels):
"""
One gradient-update step on a (premise, hypothesis, label) batch.
input_ids : (B, T) NLI-format sequences <s> premise $ hypothesis </s>
labels : (B,) integer class {0=entailment, 1=neutral, 2=contradiction}
Forward (GPT-1 Section 3.2 & 3.3):
clf_logits = Wy \dot h_n^{</s>} -> Lc (classification)
lm_logits = softmax(h_n \dot We^T) at all steps -> Lp (auxiliary LM)
Lf = Lc + \lambda \dot Lp
"""
with tf.device(DEVICE):
with tf.GradientTape() as tape:
clf_logits, lm_logits, _ = model(input_ids, training=True)
# clf_logits : (B, 3)
# lm_logits : (B, T, V)
# Lc: NLI classification loss
c_loss = clf_loss_fn(labels, clf_logits)
# Lp: auxiliary LM loss on the NLI sequence
# Position j predicts token j+1 within the full sequence.
lm_targets = input_ids[:, 1:] # (B, T-1) next tokens
lm_pred = lm_logits[:, :-1, :] # (B, T-1, V) shifted logits
l_loss = lm_loss_fn(lm_targets, lm_pred)
# Lf: joint objective (Eq. 5)
total = c_loss + lm_coef * l_loss
gradients = tape.gradient(total, model.trainable_variables)
gradients, _ = tf.clip_by_global_norm(gradients, grad_clip)
optimizer.apply_gradients(zip(gradients, model.trainable_variables))
train_loss(total)
train_clf_loss(c_loss)
train_lm_loss(l_loss)
train_accuracy.update_state(labels, clf_logits)
return finetune_step