Transformers
Transformers
With Early Release ebooks, you get books in their earliest form—the
author’s raw and unedited content as they write—so you can take advantage
of these technologies long before the official release of these titles.
Nicole Koenigstein
Transformers: The Definitive Guide
by Nicole Koenigstein
Copyeditor: TO COME
Proofreader: TO COME
Indexer: TO COME
The views expressed in this work are those of the author and do not
represent the publisher’s views. While the publisher and the author have
used good faith efforts to ensure that the information and instructions
contained in this work are accurate, the publisher and the author disclaim all
responsibility for errors or omissions, including without limitation
responsibility for damages resulting from the use of or reliance on this
work. Use of the information and instructions contained in this work is at
your own risk. If any code samples or other technology this work contains
or describes is subject to open source licenses or the intellectual property
rights of others, it is your responsibility to ensure that your use thereof
complies with such licenses and/or rights.
978-1-098-16695-3
[LSI]
Brief Table of Contents (Not Yet Final)
With Early Release ebooks, you get books in their earliest form—the author’s raw
and unedited content as they write—so you can take advantage of these
technologies long before the official release of these titles.
This will be the 1st chapter of the final book. Please note that the GitHub repo is
avaialble at [Link]
If you have comments about how we might improve the content and/or examples
in this book, or if you notice missing material within this chapter, please reach out
to the editor at sgrey@[Link].
1
Since its introduction in 2017, the transformer architecture has revolutionized the
field of natural language processing (NLP), marking a paradigm shift towards
models capable of natural language understanding (NLU). This shift was possible
because transformers process sequential data in parallel, enabling a deeper and
more contextual understanding of language than was achievable with previous
sequential models, like long short-term memory (LSTM) networks.
Among the most exciting developments are multimodal models. These models can
process and interpret multiple data types simultaneously, such as text and images,
demonstrating the architecture’s capability to integrate and learn from various data
types and opening the doors to innovative applications that could transform our
interaction with technology.
I assume in this book that you have at least some familiarity with the transformer
architecture. Perhaps you’re read the book Natural Language Processing with
Transformers, or a similar work. This chapter aims to provide you with a
comprehensive understanding of the transformers architecture to set the stage for
the more advanced and complex models, beyond NLP, that I will cover in later
chapters. This chapter briefly reviews the main parts of the model, to refresh and
solidify your knowledge. I will start off by explaining the basic transformer
architecture. Then I’ll cover how longer context is possible, and finish with a tour
through the various types of attention mechanisms.
Transformer basics
This section explains the main architectural components of the original transformer
model, such as encoder and decoder, positional embeddings, and attention
mechanism.
A tokenizer is used to tokenize the text. This is the first step to make natural
language digestible for the model, before applying token embeddings and finally
positional embeddings. The different types of tokenization are:
Character-level tokenization
it would yield:
Word-level tokenization
that is, the sequence will be split into its words, plus punctuation. The downside is
that this requires a large vocabulary, and if the language changes, this tokenization
will not be able to understand new words.
Subword tokenization
Most modern LLMs use subword tokenization, in which the word is split
into smaller parts. For instance, a subword tokenizer would split the word
hiking, the tokenizer into:
[cook, ing]
[ing]
Single-characters words are also included.
Now that you understand the basics of tokenization, let’s move on to token and
positional embeddings.
e : N → R
de
that maps each token ID to a d -dimensional vector. This is achieved
e
tokenizer = AutoTokenizer.from_pretrained('bert-base-u
model = AutoModel.from_pretrained('bert-base-uncased'
print(input_ids)
outputs = model(input_ids)
embeddings = outputs.last_hidden_state
print(embeddings)
Get the input IDs and pass them through the model to get the embeddings
Get the last hidden state, to access the embeddings of the tokens
This will result in the following output for the input IDs of the sentence:
This representation lacks the position of the word in the sequence. And since the
transformer does not have recurrence, meaning that it doesn’t need to process the
data sequentially as it was originally represented, you need a function to represent
the position. This is why you need to add positional embeddings: without them the
model treats sequences as unordered collections of words.
The positional-embedding function learns to encode a token’s location within a
sequence into a vector in the space R . The original Transformer uses for position
de
pi :
2t/d
p i,2t =sin (k/10000 )
2t/d
p i,2t+1 =cos (k/10000 )
position of the first token is captured by a vector, p[1] , while the position of the
second token is captured by a different learned vector p[2], and so on.
This technique enables transformer models to understand the order of words. In the
next section you’ll see how the transformer uses this vector representation to
understand and learn from the text.
Attention mechanism
In that context, you will often hear the term attribution matrix, which is computed
from the input embeddings. Here the term attribution refers to the significance
between different parts of the input. The attribution matrix is computed with the Q
(query) and the K (key) matrices. The resulting scores form the Q and K
interaction to determine the attention weights, which are then applied to the V
(Value) matrix to produce the output of the attention mechanism:
T
QK
Attention (Q, K, V ) =sof tmax ( )V
√d k
This attribution matrix is crucial for understanding how the model interprets and
processes the corresponding input sequences. For instance, by analyzing these
scores, you can gain insights into the model’s decision-making process, such as
which tokens it sees as more relevant than others when generating the output token.
Libraries such as Captum help make this decision-making process visible.
However, despite the specific roles of Q, K and V , the initial computation for each
of these matrices follows a similar process: a linear projection of the input
embeddings. This means that for each of these matrices, the input embeddings are
multiplied by a weight matrix. This process can be mathematically described as
follows:
Query matrix Q: Q = W qE
Key matrix K: K = Wk E
Value matrix V: V = Wv E
Here E represents the input embeddings, and W , W and W are the weight
q k v
matrices for the query, key and value projections, respectively. Take the dot
product of the query and key matrices, followed by the softmax function and the
scaling factor (for scaled dot attention). The result will be a matrix of scores
representing self-attention, or how much focus each token should put on each other
token by considering its relationship with every other element in the sequence.
These scores are then used to weight the values in the V matrix, producing the
final weighted-sum output of the attention mechanism:
Output = AttentionScores × V
This dynamic process allows the model to focus on different parts of the input
sequences for each input token, making it possible to understand each token’s
contextual relevance and information.
Multi-head attention
The attention mechanism you’ve seen so far represents the computation performed
by a single attention head, which is the component responsible for calculating
attention in the transformer. However, the original transformer, as well as state-of-
the-art (SOTA) models apply multiple attention heads simultaneously. Each
individual attention head has its own learnable parameters, which are then
combined into a single output. This allows the model to integrate information from
the same sequence and capture a variety of relationships between its words or
elements. This approach enhances the model’s ability to understand and represent
complex dependencies in the data.
As I mentioned, the first transformer model was used for machine translation.
That’s why it uses two distinct types of attention mechanism within the
architecture: one for the encoder, and another for the decoder.
The decoder’s attention is masked (also called causal attention) to prevent the
model from attending to future tokens (subsequent positions). In practice, this
means that for the prediction i, the model can only attend to the position < i. With
that method in place, the model generates each token based only on the tokens
previously created, from left to right, thus preventing it from using future tokens in
the sequence. This is important for all task where the model must generate one
token at a time as, for instance, for translation.
Now that you understand the two distinct variations of attention used with the first
transformer, let’s look at the encoder and decoder.
The first transformer model’s architecture (Figure 1-1) was characterized by its
encoder-decoder structure. Some subsequent models leverage a decoder-only
framework, such as GPT, LLaMA, Mistral, and Falcon.
Figure 1-1. Encoder and decoder part of the Transformer architecture.
The encoder itself is composed of six identical layers, each containing two
principal components: a multi-head self-attention mechanism and a point-wise
fully connected feed-forward network. The term point-wise refers to applying the
same linear transformation to each sequence element. These components are
further refined with residual connections and layer normalization.
The decoder interprets the encoded information, mirroring the encoder’s layered
structure but introduces an essential feature: masked multi-head self-attention. This
added feature in the decoder prevents the model from accessing subsequent
positions in the sequence.
The model maintains a consistent output dimension of 512 across all sub-layers,
including the embedding layers, meaning its maximum sequence length is 512
tokens. This limitation comes mostly from the specific architectural setup of the
first transformer model, which made it hard to process longer sequences on the
available hardware efficiently.
Enhancements in transformer design: Longer context
and attention variations
Now it’s time to look into methods by which modern transformer models, like
GPT-4, achieve higher levels of performance and flexibility. In particular the
ability to process more information at once, through longer context windows.
Attention-mechanism variations such as multi-query and flash attention also
increase the efficiency and accuracy of SOTA transformer models.
A model’s context window refers to the portion of text it can process when making
predictions or generating text. A longer context window allows the model to
understand more complex narratives and capture nuances better than it could using
a chunked version of a text with a small context window.
However, simply extending the context length results in quadratic increases in time
complexity and memory usage, which can constrain improvements. Therefore,
2
recent enhancements, such as rotary positional embedding (RoPE) , position
3 4
interpolation (PI) and Yet another RoPE extensioN method (YaRN) , are
designed to more effectively manage longer contexts during inference.
RoPE brings absolute and relative PEs together. But before I dive deeper into how
RoPE works, let’s first look at the key differences between absolute and relative
PEs.
With absolute PEs, for each token embedding, the model adds information
about the absolute position of the token. Absolute PEs are simpler and faster
to compute.
Relative PEs consider distances between sequence elements and can be shared
across sequences, which helps the model to understand and interpret the
relationships and distances between different tokens within a sequence.
Relative PEs result in an increase in performance, but are computationally
more complex.
cosmθ 1 − sin mθ 1 0 0 0 0
sin mθ 1 cos mθ 1 0 0 0 0
0 0 cos mθ 2 − sin mθ 2 0 0
6
R =
Θ,m
0 0 sin mθ 2 cos mθ 2 0 0
0 0 0 0 cos mθ 3 − sin mθ 3
0 0 0 0 sin mθ 3 cos mθ 3
Higher dimensions are divided into d/2 subspaces, so the dimension number has to
be even. Let’s put the math into code to make the theoretical concept more clear:
def simple_rotary_matrix(d, m, max_len):
assert d % 2 == 0, "Embedding dimension must be ev
cos_theta = [Link](theta)
sin_theta = [Link](theta)
R = [Link]((d, d))
return R
Compute thetas
d = 6
max_len = 10
R_matrix = simple_rotary_matrix(d, m=1, max_len=max_le
print(R_matrix)
Embedding dimension d
Sequence length
Figure 1-2. Illustration of Rotary Position Embedding(RoPE). Image adapted from: Jianlin Su et al.
To apply RoPE in the context of self-attention, define the relationship between the
qm in position m and key k in position n as:
n
T
T d d T d
q m k n = (R Wq xm ) (R Wk xn ) = x Wq R Wk xn
Θ,m Θ,m Θ,n−m
Here R d
Θ,n−m
= (R
d
Θ,m
) R
d
Θ,n
represents the rotary matrix adapting the relative
positions.
RoPE enhances efficiency and accuracy, so it’s used in SOTA models like LLaMA
and, LLaMA 2. Even SOTA LLMs have a maximum number of tokens they can
process at once. For instance, the LLaMA models can handle up to 2048 tokens in
a single input.
This limitation becomes a problem in use cases that involve long prompts or
extensive document summaries, where LLMs capable of managing more extensive
contexts are desirable. However, it would take substantial computational resources
to create a new LLM with an expanded context capability from the ground up. This
raises an important question: Is it possible to increase the context window size of
an already pre-trained LLM? The good news is: yes! PI and YaRN can extend
these RoPE-based pre-trained LLMs with minimal fine-tuning. Figure 1-3
demonstrates the PI technique for a LLaMA model with a 2048 context window.
Figure 1-3. How the position interpolation (PI) method works for a LLaMA model with a 2048 context window.
The blue dots stand for the training limit of LLMs; the red squares illustrate how models adapt to new
positions. The blue dots and the green triangles demonstrate how PI scales down from [0, 4096] to [0, 2048]
to keep them within the trained range.
Normally, LLM models use input positions (blue dots) within their trained range.
For length extrapolation, models handle new positions (red squares) up to 4096.
Position interpolation downscales these indices (blue dots and the green triangles)
from [0, 4096] to [0, 2048], ensuring they stay within the pretrained range.
To extend the context window, PI interpolates the position indices within the pre-
trained limit, with a small set of fine-tuning applied.
′
mL
f (x, m) = f (x, )
′
L
Here L ′
> L is a new context window beyond the pre-trained one.
Let me take a short step back and explain an important way to evaluate the
performance of a model - perplexity (PPL). This is a measure how “surprised” or
“perplexed” a model is about context. That is, perplexity measures on how well a
probability model predicts a sample, with lower values indicating better predictive
accuracy. Let me illustrate this with a concrete coding example:
Compute loss
The wiki_text
yields 121.19. This significantly higher perplexity score indicates that the model
finds this sentence quite surprising or unlikely. This is because the model was most
likely just trained on data indicating that a falcon is a bird known for its remarkable
flying abilities, not a transformer model.
For evaluating LLM performance with longer context windows, you will use
sliding window perplexity. This metric calculates perplexity over a fixed-size
window of tokens, moving across the text, to better handle and evaluate large texts
and datasets.
5
To address this, practitioners of neural tangent kernel (NTK) theory developed,
NTK-aware interpolation, adjusting the scaling of frequencies differently across
dimensions to preserve high-frequency information. One of the applications of
NTK theory is identifying and mitigating issues related to training neural networks,
such as difficulties in learning high-frequency components or patterns in data with
low intrinsic dimensionality, as is the case with RoPE. Intrinsic dimensionality
refers to the minimum number of parameters needed to accurately describe a
dataset without losing significant information, representing the dataset’s inherent
complexity.
However, NTK-aware interpolation can stretch some dimensions beyond their
bounds, potentially degrading the model’s performance. Additionally, NTK-by-
parts interpolation and dynamic NTK interpolation were introduced as refined
strategies, focusing on preserving relative local distances and adapting scale factors
dynamically for varying sequence lengths, respectively.
and k by a constant factor, enhancing the attention mechanism without altering its
n
Figure 1-4. How the context window can affect the perplexity.
As you have seen the lower the perplexity score, the better the model performs. For
instance, LLaMA 7b with YaRN and 128k extrapolation performs well in
comparison to LLaMA 7b without YaRN.
The 7B, 13B and 70B LLaMA 2 models with improved context length are
available under the LLaMA 2 license on Hugging Face. The links to each model
can be found in this repository.
Next, let’s move to different attention variations and how they improve the
performance.
Today’s transformers are more efficient than previous models, like LSTMs. That is,
the first transformer model achieved a similar high BLEU score as LSTMs, which
needed to be trained for months, after only 3.5 days of training. However,
transformers can still be considered memory-hungry, since the time and memory
complexity of self-attention grows quadratically with the sequence length. This
section explores various improvements on the attention mechanisms used in high-
performing SOTA LLMs including:
6
Cross attention
7
Multi-query attention (MQA)
8
Grouped-query attention (GQA)
9
FlashAttention
10
FlashAttention-2
It is common for models to combine different attention variations: for instance,
Falcon uses multi-query attention and FlashAttention.
Cross-Attention
In cross-attention the inputs from two different sequences are combined. Usually
this means that the queries come from the decoder and the keys and the values
come from the encoder. So, in essence, cross-attention enables the interaction
between a set of embeddings. This is important for applications where you want to
attend to a source sequence while generating a target sequence, such as translation
or question-answering tasks. Let me explain the concept further with code.
scaling_factor = W_query.shape[1]**0.5
attn_scores = [Link]('bk,mk->bm', Q, K)
attn_weights = [Link](attn_scores / scaling_fac
Y = [Link]('bm,mv->bv', attn_weights, V)
return Y
In this code, you can see that the input for Q comes from x and for K and V from
1
Multi-query attention (MQA) uses only a single key-value head, whereas multi-
head attention (MHA) uses h-number of heads for query, key and value heads,
respectively. Thus, MQA significantly speeds up the decoder’s inference time.
Figure 1-5 compares the two.
Figure 1-5. Comparison of multi-head attention (left) and multi-query attention (right). Where multi-head
attention has h number of query, key and value heads, multi-query shares a single key and value head across
all query heads.
To make this difference more tangible, read the following code which computes
MHA. Note that there is a letter h for each Q, K and V to represent the head’s
dimension.
import torch
import [Link] as F
scaling_factor = W_key.shape[1]**0.5
Q = [Link]('d,hdk->hk', x, W_query)
K = [Link]('md,hdk->hmk', M, W_key)
V = [Link]('md,hdv->hmv', M, W_value)
attn_scores = [Link]('hk,hmk->hm', Q, K) / s
o = [Link]('hm,hmv->hv', attn_weights, V)
y = [Link]('hv,hdv->d', o, P_o)
return y
Weight matrices
Compute attribution matrices using the scaling factor for scaled dot-product
attention
scaling_factor = W_key.shape[1]**0.5
Q = [Link]('bnd,hdk->bhnk', X, W_query)
K = [Link]('bmd,dk->bmk', M, W_key)
V = [Link]('bmd,dv->bmv', M, W_value)
attn_scores = [Link]('bhnk,bmk->bhnm', Q, K
attn_weights = [Link](attn_scores + mask, dim=
O = [Link]('bhnm,bmv->bhnv', attn_weights, V
Y = [Link]('bhnv,hdv->bnd', O, P_o)
return Y
These two examples make it clear that MQA is identical to MHA, except that in
MQA the different Q heads share a single set of keys and values. This modification
speeds up computation in the decoder, but can lead to loss of quality, though still
more performant than MHA. GQA was developed to address this.
Grouped-Query Attention
Figure 1-6. Comparison of multi-head attention (left) and grouped-query attention (right). Where multi-head
attention has h number of query, key and value heads grouped-query attention instead shares one key and
value head for each group of query heads, interpolating between multi-head and multi-query attention.
Comparing MHA to GQA, you can see that GQA consolidates multiple key and
value heads into a single key and value head, effectively reducing the key-value
size. This means significantly less data to load into memory during computation,
decreasing the required bandwidth and capacity by a factor of h. The following
code illustrates this setup:
def GroupedQueryAttention(Q, K, V, num_heads, group_si
attn_scores = [Link]('bid,bjd->bij', Q, K) /
attn_weights = [Link](attn_scores, dim=-1)
attn_output = [Link]('bij,bjd->bid', attn_we
return Y
GQA is specifically beneficial for larger models as they usually expand the number
of heads. That said, employing GQA substantially reduces both memory
bandwidth and capacity, while maintaining performance as models scale up.
Thus, memory bandwidth overhead from attention has less impact in larger
models. This is because the key-value cache size increases linearly with the model
dimension, whereas the model’s floating-point operations per second (FLOPs) and
parameters increase quadratically with the model dimension.
Even given these improvements, there is still room to optimize how attention
leverages the GPU memory. This is where FlashAttention and FlashAttention-2
come in.
FlashAttention
FlashAttention uses tiling to rearrange how attention calculations are performed.
By doing so, it avoids creating a N x N attention matrix. Tiling involves
transferring chunks of input data from GPU high bandwidth memory (HBM) and
GPU on-chip SRAM (speedy cache). FlashAttention iterates over sections of the K
and V matrices, transferring them to the “speedy cache”. Within each section, it
cycles through portions of the Q matrix, moving them to SRAM, then saves the
results of the attention process back to the HBM (illustrated in Figure 1-7).
Figure 1-7. FlashAttention uses tiling to eliminate the large N x N attention matrix. It works by cycling
through segments of the K and V matrices in its outer loop (indicated with red arrows), loading these
segments into the fast on-chip SRAM. For each segment, FlashAttention also processes chunks of the Q matrix
(denoted by blue arrows), loading them into SRAM, then saving the attention output back to HBM.
This is impressive, but there is still room for more improvement. The number of
non-matmul FLOPs operations can be further reduced, as you will see in the next
section.
FlashAttention-2
This approach involves inverting the loop hierarchy, focusing first on row
segments in the outer loop and column segments in the inner loop. This reverses
the original method presented in the FlashAttention and introduces parallel
processing along the sequence length dimension. Figure 1-8 illustrates this.
Figure 1-8. In the forward pass (left), the tasks (thread blocks) are distributed in parallel, with each task
handling a segment of rows from the attention matrix. In the backward pass (right), each task is responsible for
a segment of columns within the attention matrix.
Figure 1-9 compares the work partitioning between different warps in the forward
pass in FlashAttention and FlashAttention-2. Efficiently dividing work among
warps can significantly impact the performance of parallel computing tasks,
including those in deep learning models like transformers.
Figure 1-9. Comparison of work partitioning between different warps in the forward pass in FlashAttention
(left) and FlashAttention-2 (right).
Conclusion
Let me conclude this chapter with good news. I’m sure you’re keen to try these
amazing improvements on the original transformer architecture, especially the last
one. FlashAttention-2 is already supported in Hugging Face for many
architectures, including Falcon, LLaMA, and Mistral. Even better, I will cover
some common tasks for which you can use these models in the next chapter.
1
Ashish Vaswani et al. “Attention Is All You Need.”, [Link] (2017).
2
Jianlin Su et al. “RoFormer: Enhanced Transformer with Rotary Position Embedding”,
[Link] (2021).
3
Shouyuan Chen et al. “Extending Context Window of Large Language Models via Positional
Interpolation”, [Link] (2023).
4
Bowen Peng et al. “YaRN: Efficient Context Window Extension of Large Language Models”,
[Link] (2023).
5
Arthur Jacot et al. “Neural Tangent Kernel: Convergence and Generalization in Neural
Networks”, [Link] (2018).
6
Mozhdeh Gheini et al. “Cross-Attention is All You Need: Adapting Pretrained Transformers
for Machine Translation”, [Link] (2021).
7
Noam Shazeer “Fast Transformer Decoding: One Write-Head is All You Need”,
[Link] (2019).
8
Joshua Ainslie et al. “GQA: Training Generalized Multi-Query Transformer Models from
Multi-Head Checkpoints”, [Link] (2023).
9
Tri Dao et al. “FlashAttention: Fast and Memory-Efficient Exact Attention with IO-
Awareness”, [Link] (2022).
0
Tri Dao “FlashAttention-2: Faster Attention with Better Parallelism and Work Partitioning”,
[Link] (2023).
Chapter 2. Leveraging and Refining LLMs
With Early Release ebooks, you get books in their earliest form—the author’s raw and
unedited content as they write—so you can take advantage of these technologies long
before the official release of these titles.
This will be the 2nd chapter of the final book. Please note that the GitHub repo is
avaialble at [Link]
If you have comments about how we might improve the content and/or examples in
this book, or if you notice missing material within this chapter, please reach out to the
editor at sgrey@[Link].
In the previous chapter, you learned about breakthroughs like RoPE and
FlashAttention. These innovations are at your fingertips to use with models such as
LLaMA 3, and Mistral. This is exactly what you do in this chapter. You will use these
models to perform common NLP tasks such as named entity recognition (NER), text
classification, summarization and text generation. To spice things up, you will use also
LlamaIndex to perform some of these tasks. LlamaIndex is a framework that not only
simplifies but also accelerates the development of LLM applications, bringing a new
level of innovation to our projects.
As you progress through the chapter, I’ll explain various prompting strategies to
enhance the response of your LLM model, such as chain-of-thought and three-of-
thought prompting. Lastly, I show you how to refine your language model to better
align with your preferences. I’ll do this using techniques such as reinforcement
learning from human feedback (RLHF), direct preference optimization (DPO), and
Odds Ratio Preference Optimization (ORPO).
In this section, you will use instruction models to perform common NLP tasks. These
billion-parameter models show already impressive results without further training.
That said, I will not dive into fine-tuning LLMs on a dataset for specific downstream
tasks. If you’re interested in learning how to fine-tune models for these tasks, I
recommend the following books: Natural Language Processing with Transformers or
Transformers in Action, which have dedicated chapters for each task. As for specific,
niche tasks or when working with highly domain-specific data, fine-tuning a model on
a targeted dataset might yield better results.
The terms LLMs, instruction models (often referred to as chat models), and foundation models are
frequently used interchangeably, yet they have distinct meanings. Instruction models are a
specialized type of LLM designed to follow prompts for specific tasks. These models are
particularly trained to process prompts that are structured either as direct questions or actionable
tasks. On the other hand, the term foundation model denotes a broader class of generative artificial
intelligence models trained on vast datasets to handle a wide array of tasks to generate outputs.
These models are not limited to language alone but also include capabilities to process visual and
auditory data. Therefore, while all instruction models are LLMs and fit within the broader category
of foundation models, not all foundation models are restricted to language tasks.
Next, go to the settings page of your Hugging Face account and click on “Access
Tokens” as shown in Figure 2-2:
Figure 2-2. Create access token on Hugging Face.
Here you can create a “read” token for accessing the model. To load the model in your
notebook follow the code in Example 2-1:
hf_token = "your_access_token"
HfFolder.save_token(hf_token)
model_id = "meta-llama/Meta-Llama-3-8B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(model_id, toke
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
load_in_8bit=False,
bnb_4bit_use_double_quant=False,
bnb_4bit_quant_type="fp4",
bnb_4bit_compute_dtype=torch.bfloat16
)
model = AutoModelForCausalLM.from_pretrained(
model_id,
device_map='auto',
quantization_config = bnb_config,
token=True,
)
BitsAndBytes configuration
Bfloat16 is a 16-bit floating point format designed to offer near 32-bit precision
in the most significant bits.
NOTE
I will only show important steps, the accompanying notebooks in the books repo will cover all
details.
Example 2-3.
The code from listing Example 2-2 is okay if you simply want to try out the model.
However, if you want to use a chat model for your application you have to specify
prompt templates.
Text Generation
To use LLaMA 3 for text generation, you have to set up the LLaMA 3 template, as
shown in Example 2-4:
Example 2-4. Chat template for LLaMA 3 using Hugging Face chat template
messages = [
{
"role": "system",
"content": "Tell me five facts about the statue
},
{"role": "user", "content": "You are an expert in Hi
]
chat_template = tokenizer.apply_chat_template(messages,
Example 2-5.
<|begin_of_text|><|start_header_id|>system<|end_header_i
liberty.<|eot_id|><|start_header_id|>user<|end_header_id
<|eot_id|>
To now generate text with your messages, you have to apply your chat template, and
generate the output as shown in Example 2-6
prompt = tokenizer.apply_chat_template(
messages,
add_generation_prompt=True,
return_tensors="pt"
).to([Link])
terminators = [
tokenizer.eos_token_id,
tokenizer.convert_tokens_to_ids("<|eot_id|>")
]
outputs = [Link](
prompt,
max_new_tokens=512,
eos_token_id=terminators,
do_sample=True,
temperature=0.2,
top_p=0.8,
)
response = outputs[0][[Link][-1]:]
print([Link](response, skip_special_tokens=Tru
Top-p sampling, chooses the smallest set of top words such that their total
probability exceeds a certain threshold (p), and then samples the next word
from this set.
Example 2-7.
Text Summarization
In this section, you will use Mistral and LLaMA 3 together with LamaIndex to
perform text summarization. To use LLaMA 3, for text summarization, you now have
to use the LlamaIndex specific prompt template for chat models:
Example 2-8.
system_prompt = """<|begin_of_text|><|start_header_id|>s
You are a helpful, respectful, and honest assistant.
<|eot_id|><|start_header_id|>user<|end_header_id|>
"""
Additionally, you need to change, how you load and create your model:
Example 2-9.
model_id = "meta-llama/Meta-Llama-3-8B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(model_id, toke
stopping_ids = [
tokenizer.eos_token_id,
tokenizer.convert_tokens_to_ids("<|eot_id|>"),
]
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
load_in_8bit=False, # You can optionally load it in 8bit
bnb_4bit_use_double_quant=False,
bnb_4bit_quant_type="fp4",
bnb_4bit_compute_dtype=torch.bfloat16
)
llm = HuggingFaceLLM(
model_name=model_id,
max_new_tokens=512,
model_kwargs={
"token": hf_token,
"quantization_config": bnb_config
},
generate_kwargs={
"do_sample": True,
"temperature": 0.6,
"top_p": 0.9,
},
system_prompt=system_prompt,
query_wrapper_prompt=query_wrapper_prompt,
tokenizer_name=model_id,
tokenizer_kwargs={"token": hf_token},
stopping_ids=stopping_ids,
device_map="auto",
)
Now, to summarize text from a document, you can use the simple directory reader
from LlamaIndex as shown here:
Example 2-10. Access documents with LlamaIndex
documents = SimpleDirectoryReader('./data').load_data()
This will load your documents from the data directory. To query the model with the
summarization task, you need to create embeddings first:
embed_model = HuggingFaceEmbedding(
model_name="hkunlp/instructor-large"
)
[Link] = llm
Settings.embed_model = embed_model
Settings.num_output = 256
Settings.context_window = 4096
Settings.chunk_size = 512
Settings.chunk_overlap = 64
vector_index = VectorStoreIndex.from_documents(documents
With the embeddings in place, you can ask LLaMA 3 to provide you a summary of the
text. How you can do this, is shown in Example 2-12
print(
vector_index.as_query_engine(
llm=llm,
).query("Provide a short summary of the patient record o
)
I instructed the model to summarize a patient record, this is the resulting model output:
Example 2-13.
However, the patient record is not that long and LLaMA 3 models only have a context
window of 8k tokens. So, if you want to summarize longer documents, you need a
model with a larger context window, or you would have to extend the context window
of LLaMA 3. However, to simplify things here, change the model to be Mistral 7B
version 0.2, which has a context window of 32k tokens. For that, you only have to
change the model id:
Example 2-14.
model_id = "mistralai/Mistral-7B-Instruct-v0.2"
Now you use Mistral 7B version 0.2 for your summarization task. In addition, to use it
correctly, you have to modify the prompt template as follows:
Example 2-15.
system_prompt = """<|prompter|>\n"""
Now you can acces a shared txt file via my GoogleDrive account to summarize the
first two chapters from another book I have written. This document has 16917 tokens,
which are clearly more tokens than the 8k tokens the LLaMA 3 model can handle.
Accessing the file and getting the embeddings stays all the same as shown in
Example 2-10 and Example 2-11, you just have to adjust the query:
Example 2-16.
query_engine = vector_index.as_query_engine()
print(query_engine.query(
"""Could you summarize the given context? Return your re
the key points of the text and does not miss anything im
Example 2-17.
In this section, you’ll use LLaMA 3 and LlamaINdex to perform Named Entity
Recognition (NER), as shown in Example 2-18:
input_text = """
Tim Cook is CEO of Apple. Apple is an American multinati
corporation and technology company headquartered in Cupe
California, in Silicon Valley.
"""
messages = [
ChatMessage(role="system", content="You are and entities
ChatMessage(role="user", content=text),
]
response = [Link](messages)
Example 2-19.
Here are the entities found in the text:
Text Classification
In this section, you use LangChain’s news loader (which helps you download web
articles) to perform sentiment classification. LangChain is a similar framework like
LlamaIndex. All you have to do, is to provide the links you want to analyze as shows
in Example 2-20:
news = NewsURLLoader(urls=[
'[Link]
[Link]
'[Link]
sp-500-hits-fresh-record-as-all-eyes-turn-to-fed-de
'[Link]
'[Link]
a81a-0e82c7c9fd0b/[Link]'
]).load()
You could, of course, also use a scraper to provide the links to the articles you want to
analyze. To organize the gathered data for further processing, you can store the article
headline and content in a DataFrame:
Example 2-21.
news_content = [c.page_content for c in news]
news_headline = [[Link]["title"] for c in news]
Now you can use the prompt template and utility function from Example 2-22 to
perform sentiment classification on the news data.
def get_sentiment(text):
# Formatted instruction string with dynamic text input
instruction = f"""Classify the text into neutral, negati
Answer only with the word. Text: {text}
"""
messages = [
ChatMessage(role="system", content="You are an e
ChatMessage(role="user", content=instruction),
]
response = [Link](messages)
sentiment = extract_sentiment(response)
return sentiment
news_df['Sentiment'] = news_df['News_Headline'].apply(ge
The great thing about using a chat model to perform sentiment classification, is, that
you can ask the model to reason. So, if you want the model to explain why it classified
the text as neutral, negative or positive you simply alter your instruction:
Example 2-23.
messages = [
ChatMessage(role="system", content="You are an expert in
ChatMessage(role="user", content=instruction),
]
response = [Link](messages)
For the first news headline about Nvidia’s CTC event it responds as follows:
Example 2-24.
Getting the model’s explanations for its classifications offers valuable insights beyond
the mere classification labels. This approach, applicable across various domains,
enables a deeper understanding of the model’s reasoning processes. Whether for
enhancing content moderation, improving customer feedback analysis, or refining
market sentiment evaluations.
Now that you’ve seen how to use instruction models with basic prompting techniques,
the next section will dive deeper into various enhanced prompting strategies. These
refined approaches optimize how you instruct your model and are designed to yield
more precise outputs.
In all previous examples you prompted the chat model to answer your question
without providing an example or guidance, this is known as zero-shot prompting. The
next stage is to provide the model with some examples; this is called few-shot
prompting. For instance, for the sentiment classification task from the previous section
you would alter the prompt as follows:
messages = [
ChatMessage(role="system", content="You are an expert in
ChatMessage(role="user", content=instruction),
]
In this example of few-shot prompting, you provide the model two examples for
classifying sentiment. This approach works well for a wide range of tasks, but if you
want to empower the LLM to undertake reasoning tasks, you have to employ a more
advanced prompting technique.
In contrast to zero-shot and few-shot prompting, where you ask the models to directly
produce the final answer, you can encourage the LLM to generate intermediate
reasoning steps before providing the final answer to a problem. Doing so gives you the
ability to break down complex problems into manageable, intermediate steps. This
section will cover various such techniques, such as chain-of-thought and three-of-
thought prompting.
Chain-of-thought prompting
1
Chain-of-thought prompting (CoT) enhances the reasoning capabilities of LLMs,
especially in multi-step reasoning tasks. Inspired by the human thought process, this
technique first solves intermediate steps before getting to the final answer. Figure 2-3
shows examples of how you alter the prompt for CoT.
Figure 2-3. Examples for arithmetic and commonsense reasoning benchmarks. The highlighted statements show
how you can phrase an explicit CoT example for the model for each shown task.
To use this with the template you applied in the previous section, you could do the
following:
Example 2-25.
messages = [
ChatMessage(role="system", content="You are a mathematic
ChatMessage(role="user", content=question),
]
response = [Link](messages)
print(response)
Example 2-26.
Let's solve the problem!
Essentially, this technique involves presenting the same prompt to the model multiple
times and then determining the final result based on the majority response.
to the end of a question. This straightforward prompt enables the LLM to produce a
chain of thought, allowing it to generate a more precise response. Example 2-27 shows
how you would question the model.
messages = [
ChatMessage(role="system", content="You are a mathematic
ChatMessage(role="user", content=question),
]
response = [Link](messages)
Example 2-28.
23 - 20 = 3 apples left
Then, they bought 6 more apples. So, the new total is:
However, sometimes even that might not be enough to get the correct answer.
If you have more complex tasks where initial decisions play a pivotal role or a more
exploratory and strategic approach is needed, you can use tree of thoughts prompting
2
(ToT) . An overview how this technique works is shown in Figure 2-5.
ToT uses the LLMs ability to evaluate coherent language sequences in combination
with search algorithms for data structures such as depth-first search (DFS) or breadth-
first search (BFS).
DFS VS. BFS
DFS and BFS are widely used algorithms in computer science for searching within tree or graph
data structures. Starting from the root, BFS examines all nodes present at the current level of the
tree prior to progressing to the next level, whereas DFS dives as deep as possible down each
branch before moving to another.
ToT expands upon the CoT prompting method by allowing the LLM to investigate
coherent text segments that act as an intermediary step in problem-solving. Figure 2-6
shows an example for a math problem.
Figure 2-6. Thought generation (a) and evaluation of the thought (b).
Example 2-29 demonstrates how you can use ToT solving Game of 24 tasks with
ChatGPT. Game of 24 is a math reasoning contest where participants aim to
manipulate four integer numbers using basic arithmetic operations (addition,
subtraction, multiplication, division) to achieve a result of 24.
[Link]['OPENAI_API_KEY'] = 'your-api-key'
task = Game24Task()
Example 2-30.
ToT is a good choice for tasks (such as solving mathematical problems) that involve
analytical reasoning, or for coding.
Figure 2-7. Thread of thought prompting helps LLMs to navigate chaotic information.
After this prompt, you let the model refine its conclusion by combining the initial
prompted text with the model’s response and a conclusion marker such as:
Supervised Fine-Tuning
The goal of SFT is to refine the general-purpose model, that understands language, but
isn’t yet specialized for specific tasks, like engaging in dialogue as a chatbot or
instruct model. SFT adjusts the model’s parameters (weights) to reduce errors
specifically for the target task, making it more effective at handling types of input it
will encounter in its designated role. This is achieved by adjusting the internal
parameters to reduce the loss function, effectively making the predictions closely align
with the ground truth provided by the training data.
During SFT, adjustments to the model’s weights are governed by gradient descent
algorithms, focusing on minimizing the divergence between the predicted outputs and
the actual data:
θnew = θold − η ⋅ 𝛻
θLSF T
Here, η represents the learning rate, a hyperparameter that determines the step size
during the weight update phase. The gradient of the loss function with respect to the
model parameters 𝛻 θLSF T , guids the update direction.
Using SFT for your LLM is extremely easy, you can just leverage Hugging Face’s trl
library for that. This library is specifically tailored to support SFT and alignment tasks.
This class uses system instructions, questions, and answers, forming the basis of the
prompt structure. You can use the class as shown here:
Example 2-31.
training_args = TrainingArguments(
per_device_train_batch_size = 2,
gradient_accumulation_steps = 4,
warmup_steps = 100,
max_steps = 600,
learning_rate = 2e-4,
fp16 = not [Link].is_bf16_supported(
bf16 = [Link].is_bf16_supported(),
optim = "adamw_8bit",
weight_decay = 0.01,
)
sft_trainer = SFTTrainer(
model = model,
train_dataset = formatted_dataset,
dataset_text_field = "text",
max_seq_length = 4096,
args = training_args
)
By refining the model in this manner, SFT aims to produce a model that not only
performs well on general tasks but excels at domain-specific tasks.
Reinforcement learning from human feedback usually starts with fine-tuning a pre-
trained language model with SFT on a high-quality dataset tailored to specific tasks,
such as dialogues. In the second phase, the SFT model, π SFT
, is prompted with
prompts x to produce answer pairs (y 1,
SFT
y2) ∼ π (y ∣ x) . Human labelers review the
generated answers and label them according to their preferences, represented as
yw ≻ yl ∣ x out of (y
1, y2) . Here y and y represents the preferred and dispreferred
w l
prompt, respectively.
The reward function integrates several components into a single RLHF process, by
receiving a prompt x from the dataset D of human comparisons. The fine-tuned policy
generates text y. This output, concatenated with the original prompt, is evaluated by
the preference model, yielding a scalar preferability score r . Figure 2-8 shows this
φ
Here, rφ (x, y) represents the scalar output of the reward model for prompt x and
generated text y. D is the dataset of human comparisons used to train the reward
model. y is the preferred text of pair (y_1,y_2), wheras y is the less preferred text by
w l
7
Proximal policy optimization (PPO) maximizes the reward within the current batch.
PPO, an on-policy algorithm, restricts updates to the current batch of prompt-
generation pairs. An on-policy algorithm directly learns from and optimizes the
decision-making policy it is currently using to make actions. This restriction helps to
control that gradient updates do not happen to quickly, and therefore, stabilizing the
learning process.
RLHF can be complex to implement due to its need for extensive hyperparameter
tuning. This complexity mainly stems from the instability of the PPO algorithm used
within RLHF. PPO’s instability arises from its reliance on successive small updates,
which must balance between making sufficient progress and maintaining policy
performance. This delicate balance can lead to variance in training outcomes, as even
minor changes in hyperparameters or training data can disproportionately affect the
learning trajectory.
DPO presents a more streamlined approach by optimizing the LLM’s policy directly,
without the requirement for a reward model. DPO streamlines this process by
integrating human preferences directly into the optimization mechanism, bypassing the
step of modeling preferences as an independent reward function. Figure 2-9 compares
RLHF with DPO.
At the heart of DPO is its strategy of directly modifying the language model’s
parameters to prioritize responses that are more favorable based on direct input. This is
accomplished through an optimization process constrained by the KL divergence. The
KL-constrained reward maximization quantifies the disparity between the LLM’s
response probability distribution and a target distribution reflecting human
preferences. The reward function can be express in terms of the optimal policy π , the r
πr (y ∣ x)
r (x, y) = β log + β log Z (x)
πref (y ∣ x)
This equation can be applied to your r and optimal model π , taking the difference of
* *
8
the rewards between the two, considering the Bradley-Terry model , the RLHF policy
π
*
can be expressed as:
1
*
p (y1 ≻ y2 ∣ x) =
* *
π (y2∣x) π (y1∣x)
1+ exp (β log − β log )
πref (y2∣x) πref (y1∣x)
Here p * *
(y1 ≻ y2 ∣ x) = σ(r (x, y1) − r (x, y2))
*
is the derived difference from the
Bradley-Terry model.
Considering this reparameterization you can express the probability of human
preference as maximum likelihood objective for the now parameterized policy π : θ
πθ(yw ∣ x) πθ(yl ∣ x)
LDPO(πθ; πref ) = −E(x,y [log σ(β log − β log )]
w,yl)∼D
πref (yw ∣ x) πref (yl ∣ x)
Minimizing this divergence allows DPO to closely align the model’s outputs with
preferred outcomes, effectively converting the optimization task into a classification
task where responses are classified as preferred or not.
To use DPO for your LLM, you can again use the trl library from Hugging Face. Here,
you load your data and model as you would do with every other fine-tuning task and
create training arguments as you would do with fine-tuning. The difference is that you
now use the DPO Trainer class:
Example 2-32.
training_args = TrainingArguments(
per_device_train_batch_size = 2,
gradient_accumulation_steps = 4,
warmup_ratio = 0.1,
num_train_epochs = 2,
learning_rate = 5e-6,
fp16 = not [Link].is_bf16_supported(
bf16 = [Link].is_bf16_supported(),
optim = "adamw_8bit",
weight_decay = 0.0,
output_dir = "outputs",
)
dpo_trainer = DPOTrainer(
model=model,
ref_model=None,
args=training_args,
beta=0.1,
train_dataset=transformed_datasets["train_prefs"],
eval_dataset=transformed_datasets["test_prefs"],
tokenizer=tokenizer,
max_length=1024,
max_prompt_length=512,
)
And in addition to using the Trainer class, you need to have a dataset with chosen and
rejected columns:
Example 2-33.
DatasetDict({
train_prefs: Dataset({
features: ['prompt', 'chosen', 'rejected'],
num_rows: 30
})
test_prefs: Dataset({
features: ['prompt', 'chosen', 'rejected'],
num_rows: 1
})
})
The DPO Trainer class additionally allows you to use two enhanced methods for more
9
robust DPO training. The first method, referred to as Ψ PO , acknowledges that
traditional DPO is prone to overfitting coming from its specific use of KL divergence
in the loss function, particularly when dealing with deterministic preferences or
limited datasets. The overfitting comes from an inadequate regularization, which can
cause the policy to become overly deterministic and overlook other potentially useful
actions. To address this, Ψ PO adds a regularization term to the DPO loss function,
which results in a more robust KL-regularization against deterministic preferences
from annotators. To leverage this enhanced method, you simply integrate the loss into
the DPO Trainer class:
dpo_trainer = DPOTrainer(
model=model,
ref_model=None,
args=training_args,
beta=0.1,
train_dataset=transformed_datasets["train_prefs"],
eval_dataset=transformed_datasets["test_prefs"],
tokenizer=tokenizer,
max_length=1024,
max_prompt_length=512,
loss_type="ipo"
)
10
The other method, Kahneman-Tversky Optimization (KTO) reduces your DPO to a
singleton feedback, rather than an explicit feedback. That is, you define the loss
function based on individual examples such as good or bad. To use this method, just
add the following loss function to your DPO Trainer:
Example 2-35.
loss_type="kto_pair"
However, RLHF and DPO still depend on a preference-aligned phase, after the SFT
phase. In the next section, I will introduce a technique, which eliminates this
additional preference alignment phase.
Odds Ration Performance Optimization
Figure 2-10. Log probabilities during fine-tuning indicate that both chosen and rejected responses are likely, even
though only chosen ones guide the training.
To address this issue, a monolithic preference alignment method named Odd Ration
Performance Optimization (ORPO) can be used. ORPO dynamically penalizes the
non-preferred response for each query without the need for creating sets of rejected
tokens. The objective function is as follows:
oddsθ (yw ∣ x)
LOR = − log σ(log )
oddsθ (yl ∣ x)
training_args = ORPOConfig(
output_dir='./results',
per_device_train_batch_size = 2,
gradient_accumulation_steps = 4,
num_train_epochs = 2,
warmup_ratio = 0.1,
remove_unused_columns=False,
learning_rate=9e-1,
evaluation_strategy="steps",
beta=0.1,
)
orpo_trainer = ORPOTrainer(
model=model,
args=training_args,
tokenizer=tokenizer,
train_dataset=transformed_datasets["trai
eval_dataset=transformed_datasets["test_
)
Figure 2-11 shows the AlpacaEval 2.0 score comparison of ORPO, DPO and RLHF.
AlpacaEval evaluates models based on their responses to single-turn prompts.
While ORPO is computationally efficient, RLHF or DPO might still yield better
results, therefore, I recommend you evaluate the different alignment methods based on
your use case and available dataset.
Conclusion
In this chapter you learned how straightforward it is to use an instruct model for tasks
such as text summarization using LangChain. This establishes the base for you, to use
these LLMs for your applications.
In the last section you gained an understanding, how you can align your LLM with
human preferences. There are different methods to chose from such as RLHF, DPO
and, ORPO. Each comes with its own strengths and weaknesses, while RLHF is still
the most chosen method in the industry. In the next chapter, you will take a closer look
into reinforcement learning by using it with reinforcement learning transformers.
1
Jason Wei et al. “Chain-of-Thought Prompting Elicits Reasoning in Large Language Models”,
[Link] (2023).
2
Shunyu Yao et al. “Tree of Thoughts: Deliberate Problem Solving with Large Language Models.”,
[Link] (2023).
3
Yucheng Zhou et al. “Thread of Thought Unraveling Chaotic Contexts”,
[Link] (2023).
4
Long Ouyang et al. “Training language models to follow instructions with human feedback”,
[Link] (2022).
5
Rafael Rafailov et al. “Direct Preference Optimization: Your Language Model is Secretly a
Reward Model.”, [Link] (2023).
6
Jiwoo Hong et al. “ORPO: Monolithic Preference Optimization without Reference Model”,
[Link] (2024).
7
John Schulman et al. “Proximal Policy Optimization Algorithms”,
[Link] (2017).
8
Bradley, Ralph Allan, and Milton E. Terry. “Rank Analysis of Incomplete Block Designs: I. The
Method of Paired Comparisons.” Biometrika 39, no. 3/4 (1952): 324–45.
[Link]
9
Mohammad Gheshlaghi Azar et al. “A General Theoretical Paradigm to Understand Learning
from Human Preferences”, [Link] (2023).
0
Kawin Ethayarajh et al. “KTO: Model Alignment as Prospect Theoretic Optimization”,
[Link] (2024).
About the Author