ΑρχικήΑφιέρωμαΜαθηματικά των Transformers: Πως λειτουργούν τα Large Language Models (LLMs)

Μαθηματικά των Transformers: Πως λειτουργούν τα Large Language Models (LLMs)

Σύνοψη
  • Τα επαναληπτικά δίκτυα (RNNs) δυσκολεύονται σε μακρινές εξαρτήσεις και σε παράλληλη εκπαίδευση.
  • Η Attention είναι ένας «διαφορίσιμος μηχανισμός ανάκτησης» που υπολογίζει βάρη και αναμειγνύει πληροφορία από όλη την ακολουθία.
  • Η Scaled Dot‑Product Attention εξηγεί γιατί κλιμακώνουμε με √d για σταθερό Softmax και καλύτερα gradients.

Το 2017 δημοσιεύτηκε το paper “Attention Is All You Need”, το οποίο άλλαξε ριζικά τον τρόπο που χτίζουμε μοντέλα τεχνητής νοημοσύνης για ακολουθίες. Σήμερα, κάθε σύγχρονο Large Language Model (LLM) βασίζεται σε αυτή την αρχιτεκτονική: τον Transformer.

Ίσως έχεις ακούσει ότι οι Transformers «είναι απλώς πολλαπλασιασμοί πινάκων» ή ότι «χρησιμοποιούν μηχανισμούς προσοχής». Αυτά είναι σωστά, αλλά δεν εξηγούν το γιατί η μαθηματική δομή είναι ακριβώς αυτή.

Στο άρθρο θα δούμε τη λογική αλυσίδα: από τα προβλήματα που μας οδήγησαν στους Transformers, μέχρι το μαθηματικό θεμέλιο της Attention.

Ο βασικός στόχος είναι ένας: να καταλάβει το μοντέλο σχέσεις μέσα σε μια ακολουθία, ακόμη κι αν τα σχετικά στοιχεία απέχουν πολύ μεταξύ τους, και να το κάνει με τρόπο που κλιμακώνεται σε τεράστια δεδομένα και υπολογισμό.


Πίνακας περιεχομένων

Το πρόβλημα: γιατί τα RNNs δεν κλιμακώνονται

Πριν τους Transformers, το κυρίαρχο μοντέλο για ακολουθίες ήταν τα Recurrent Neural Networks (RNNs) (και οι παραλλαγές τους όπως LSTM/GRU). Η ιδέα είναι απλή: η ακολουθία επεξεργάζεται βήμα‑βήμα και το δίκτυο διατηρεί μια «κρυφή κατάσταση» που συμπυκνώνει ό,τι έχει δει μέχρι τώρα.

Ένας τυπικός κανόνας ενημέρωσης κρυφής κατάστασης είναι:

h_t = tanh(W_hh · h_{t-1} + W_xh · x_t + b)

Όπου h_t είναι η κρυφή κατάσταση στη χρονική στιγμή t, x_t η είσοδος, και W_hh, W_xh πίνακες βαρών. Το tanh περιορίζει τις τιμές για αριθμητική σταθερότητα.

Δύο κρίσιμα προβλήματα στην πράξη

  1. Εξαφανιζόμενα gradients (vanishing gradients)
    Στην εκπαίδευση με backpropagation, οι κλίσεις πρέπει να διαπεράσουν πολλά χρονικά βήματα. Με πολλαπλασιασμούς παραγώγων μικρότερων του 1, οι κλίσεις τείνουν να μηδενίζονται εκθετικά, άρα η «παλιά» πληροφορία δύσκολα επηρεάζει τα βάρη.
  2. Σειριακή επεξεργασία = χαμηλή απόδοση
    Ακόμη κι αν μειώσουμε το vanishing gradients με LSTM/GRU, το θεμελιώδες bottleneck παραμένει: δεν μπορείς να υπολογίσεις το βήμα t πριν υπολογίσεις το t-1. Αυτό περιορίζει την παραλληλοποίηση σε GPU/TPU.
ΚριτήριοRNN / LSTM / GRUTransformer
ΠαραλληλοποίησηΠεριορισμένη (σειριακή)Ισχυρή (όλες οι θέσεις μαζί)
Μακρινές εξαρτήσειςΔύσκολεςΚαλύτερες μέσω Attention
Κλιμάκωση trainingΔύσκολη σε μεγάλα corporaΠολύ καλύτερη σε σύγχρονο hardware

Ο Transformer προτείνει μια διαφορετική οπτική: κοιτάμε όλες τις θέσεις ταυτόχρονα και αφήνουμε το μοντέλο να μάθει ποιες θέσεις είναι σχετικές μεταξύ τους. Αυτό ακριβώς κάνει η Attention.


Η βασική ιδέα: Attention ως «διαφορίσιμος μηχανισμός ανάκτησης»

Σκέψου την Attention σαν μια lookup δομή (όπως ένας πίνακας/λεξικό): δίνεις ένα “ερώτημα” και παίρνεις “πληροφορία”. Η διαφορά είναι ότι εδώ δεν υπάρχει ακριβής αντιστοίχιση, αλλά σταθμισμένος μέσος όλων των διαθέσιμων πληροφοριών.

Μικρό αριθμητικό παράδειγμα

Έστω τρία “token vectors” (απλοποιημένα σε 2 διαστάσεις). Θέλουμε να ενημερώσουμε την αναπαράσταση του δεύτερου token με βάση το πόσο σχετικό είναι το κάθε token.

# Example token vectors (toy example)
word1 = [1.0, 0.0]
word2 = [0.5, 0.5]
word3 = [0.0, 1.0]

Χρησιμοποιούμε dot product ως μέτρο «ομοιότητας»:

score(word1, word2) = 0.5
score(word2, word2) = 0.5
score(word3, word2) = 0.5

Έπειτα εφαρμόζουμε Softmax για να πάρουμε βάρη που αθροίζουν σε 1:

# Softmax (conceptual)
weights = softmax([0.5, 0.5, 0.5]) ≈ [0.33, 0.33, 0.33]

Και υπολογίζουμε τη νέα αναπαράσταση ως σταθμισμένο άθροισμα:

new_word2 = 0.33*word1 + 0.33*word2 + 0.33*word3
         ≈ [0.495, 0.495]

Αυτό είναι το βασικό μοτίβο: scores → softmax → weighted sum.


Scaled Dot‑Product Attention: το μαθηματικό θεμέλιο των Transformers

Στους Transformers η Attention οργανώνεται γύρω από τρία αντικείμενα:

  • Query (Q): τι “ζητά” η τρέχουσα θέση
  • Key (K): τι “προσφέρει” κάθε θέση για αντιστοίχιση
  • Value (V): ποια πληροφορία τελικά μεταφέρεται

Ξεκινάμε από την είσοδο X (embeddings) και κάνουμε γραμμικές προβολές:

Q = X · W_Q
K = X · W_K
V = X · W_V
ΣύμβολοΤι σημαίνειΤυπικό σχήμα
XΕίσοδος (ακολουθία embeddings)(n, d_model)
Q, KVectors για αντιστοίχιση(n, d_k)
VΠεριεχόμενο που αναμειγνύεται(n, d_v)
Q·K^TΌλα τα pairwise scores(n, n)

Υπολογίζουμε τα scores με dot product:

scores = Q · K^T

Και εδώ εμφανίζεται ένα σημαντικό ζήτημα: όσο μεγαλώνει η διάσταση d_k, τα dot products τείνουν να μεγαλώνουν και το Softmax γίνεται υπερβολικά «αιχμηρό», προκαλώντας φτωχά gradients. Η λύση είναι να κλιμακώσουμε:

scaled_scores = (Q · K^T) / sqrt(d_k)

Μετά:

weights = softmax(scaled_scores)
output  = weights · V

Συνολικά:

Attention(Q, K, V) = softmax((Q · K^T) / sqrt(d_k)) · V

Υλοποίηση (NumPy) με αγγλικά σχόλια/strings

import numpy as np

def softmax(x):
    """
    Numerically stable softmax over the last axis.
    """
    x = x - np.max(x, axis=-1, keepdims=True)
    exp_x = np.exp(x)
    return exp_x / np.sum(exp_x, axis=-1, keepdims=True)

def scaled_dot_product_attention(Q, K, V):
    """
    Scaled Dot-Product Attention.

    Args:
        Q: (n, d_k)
        K: (n, d_k)
        V: (n, d_v)

    Returns:
        output: (n, d_v)
        weights: (n, n)
    """
    d_k = Q.shape[-1]
    scores = np.matmul(Q, K.T)                 # (n, n)
    scaled_scores = scores / np.sqrt(d_k)
    weights = softmax(scaled_scores)           # (n, n)
    output = np.matmul(weights, V)             # (n, d_v)
    return output, weights

Μέχρι εδώ έχουμε τον πυρήνα: Attention που είναι ταυτόχρονα αποτελεσματική και εκπαιδεύσιμη. Στο επόμενο μέρος θα δούμε πώς επεκτείνεται σε Multi‑Head Attention και πώς εισάγουμε τη σειρά με positional encoding.


Multi-Head Attention: πολλές «οπτικές» ταυτόχρονα

Η Scaled Dot-Product Attention λειτουργεί, αλλά έχει έναν περιορισμό: μία μόνο Attention πράξη τείνει να συλλαμβάνει έναν κυρίαρχο τύπο συσχέτισης ανάμεσα στις θέσεις. Στη φυσική γλώσσα και γενικότερα στις ακολουθίες, όμως, χρειαζόμαστε πολλαπλές μορφές σχέσεων την ίδια στιγμή:

  • Συντακτικές σχέσεις (ποια λέξη εξαρτάται από ποια)
  • Σημασιολογικές σχέσεις (παρόμοια έννοια/θέμα)
  • Σχέσεις εγγύτητας (κοντινές θέσεις που επηρεάζουν έντονα)

Το Multi-Head Attention λύνει αυτό το πρόβλημα: υπολογίζει Attention παράλληλα σε πολλούς «heads», με διαφορετικές μαθημένες προβολές. Κάθε head μπορεί να μάθει ένα διαφορετικό μοτίβο συσχέτισης.

Μαθηματική περιγραφή

Για κάθε head i χρησιμοποιούμε διαφορετικούς πίνακες βαρών:

Q_i = X · W_Q^i
K_i = X · W_K^i
V_i = X · W_V^i

head_i = Attention(Q_i, K_i, V_i)

Στη συνέχεια κάνουμε συνένωση (concatenation) των heads και τελική προβολή:

MultiHead(X) = Concat(head_1, ..., head_h) · W_O

Στην πράξη, αν η διάσταση του μοντέλου είναι d_model και έχουμε h heads, επιλέγουμε συνήθως:

d_k = d_model / h

ώστε το συνολικό κόστος να παραμένει διαχειρίσιμο.

Προειδοποίηση:
Πρέπει να ισχύει d_model % num_heads = 0, αλλιώς δεν μπορείς να μοιράσεις ισότιμα τη διάσταση στα heads.

Τυπικές διαστάσεις (πίνακας γρήγορης αναφοράς)

ΠαράμετροςΣυμβολισμόςΣυνήθης επιλογήΤι επηρεάζει
Διάσταση μοντέλουd_model512, 768, 1024+Χωρητικότητα / κόστος
Αριθμός headsh8, 12, 16Ποικιλία σχέσεων
Διάσταση ανά headd_kd_model / hΟξύτητα scores / σταθερότητα

Υλοποίηση Multi-Head Attention (NumPy, αγγλικά σχόλια)

import numpy as np

class MultiHeadAttention:
    """
    Multi-Head Self-Attention (educational NumPy version).
    """

    def __init__(self, d_model, num_heads):
        assert d_model % num_heads == 0, "d_model must be divisible by num_heads"
        self.d_model = d_model
        self.num_heads = num_heads
        self.d_k = d_model // num_heads

        # Projection matrices
        self.W_Q = np.random.randn(d_model, d_model) * 0.01
        self.W_K = np.random.randn(d_model, d_model) * 0.01
        self.W_V = np.random.randn(d_model, d_model) * 0.01
        self.W_O = np.random.randn(d_model, d_model) * 0.01

    def split_heads(self, x):
        # (batch, seq, d_model) -> (batch, heads, seq, d_k)
        b, s, _ = x.shape
        x = x.reshape(b, s, self.num_heads, self.d_k)
        return x.transpose(0, 2, 1, 3)

    def combine_heads(self, x):
        # (batch, heads, seq, d_k) -> (batch, seq, d_model)
        b, h, s, dk = x.shape
        x = x.transpose(0, 2, 1, 3)
        return x.reshape(b, s, h * dk)

    def forward(self, X):
        # Linear projections
        Q = X @ self.W_Q
        K = X @ self.W_K
        V = X @ self.W_V

        # Split into heads
        Qh = self.split_heads(Q)
        Kh = self.split_heads(K)
        Vh = self.split_heads(V)

        # Attention per head
        scores = Qh @ Kh.transpose(0, 1, 3, 2)
        scores = scores / np.sqrt(self.d_k)

        weights = softmax(scores)       # (batch, heads, seq, seq)
        out = weights @ Vh              # (batch, heads, seq, d_k)

        # Combine heads + output projection
        out = self.combine_heads(out)   # (batch, seq, d_model)
        return out @ self.W_O

Το κέρδος είναι ότι διαφορετικά heads μπορούν να «ειδικευτούν» σε διαφορετικές συσχετίσεις, δίνοντας στο μοντέλο μεγαλύτερη εκφραστικότητα χωρίς να πολλαπλασιάζει ανάλογα το κόστος.


Positional Encoding: πως εισάγουμε τη σειρά στην ακολουθία

Η self-attention από μόνη της δεν «βλέπει» τη θέση. Για να καταλάβει το μοντέλο τη σειρά, προσθέτουμε στο embedding κάθε θέσης μια πληροφορία θέσης.

Μια κλασική επιλογή είναι τα σινο-συνημιτονικά positional encodings, τα οποία είναι ντετερμινιστικά και γενικεύουν σε μεγαλύτερα μήκη ακολουθίας.

Ορισμός

PE(pos, 2i)   = sin(pos / 10000^(2i/d_model))
PE(pos, 2i+1) = cos(pos / 10000^(2i/d_model))

Η λογική είναι ότι διαφορετικές διαστάσεις αντιστοιχούν σε διαφορετικές συχνότητες, άρα το μοντέλο μπορεί να μάθει σχετικές αποστάσεις (offsets) πιο εύκολα.

Πληροφορία:
Υπάρχουν και νεότερες προσεγγίσεις, όπως relative positional encodings, που συχνά δουλεύουν καλύτερα σε πολύ μεγάλα context windows.

Υλοποίηση Positional Encoding (NumPy, αγγλικά σχόλια)

def get_positional_encoding(seq_len, d_model):
    """
    Sinusoidal positional encoding.
    Returns: (seq_len, d_model)
    """
    pe = np.zeros((seq_len, d_model))
    position = np.arange(seq_len).reshape(-1, 1)

    div_term = np.exp(np.arange(0, d_model, 2) * -(np.log(10000.0) / d_model))
    pe[:, 0::2] = np.sin(position * div_term)
    pe[:, 1::2] = np.cos(position * div_term)
    return pe

Η χρήση είναι απλή: προσθέτουμε το positional encoding στα token embeddings (ίδιες διαστάσεις), πριν περάσουν από τις στρώσεις του Transformer.


Feed-Forward Networks (FFN): υπολογισμός ανά θέση

Η Attention είναι κυρίως «επικοινωνία» μεταξύ θέσεων. Για να προσθέσουμε υπολογιστική ισχύ και μη-γραμμικότητα, μετά την Attention εφαρμόζουμε ένα μικρό δίκτυο δύο στρωμάτων σε κάθε θέση ανεξάρτητα (position-wise).

Τύπος

FFN(x) = ReLU(x · W1 + b1) · W2 + b2

Συνήθως d_ff ≈ 4×d_model.

Υλοποίηση FFN (NumPy, αγγλικά σχόλια)

class FeedForwardNetwork:
    """
    Position-wise feed-forward network.
    """

    def __init__(self, d_model, d_ff):
        self.W1 = np.random.randn(d_model, d_ff) * np.sqrt(2.0 / d_model)
        self.b1 = np.zeros(d_ff)
        self.W2 = np.random.randn(d_ff, d_model) * np.sqrt(2.0 / d_ff)
        self.b2 = np.zeros(d_model)

    def forward(self, x):
        hidden = x @ self.W1 + self.b1
        hidden = np.maximum(0, hidden)   # ReLU
        return hidden @ self.W2 + self.b2

Σε πολλά σύγχρονα μοντέλα χρησιμοποιείται GELU αντί για ReLU, αλλά η ουσία παραμένει: μη γραμμικότητα + επέκταση/συμπίεση διάστασης.


Layer Normalization: σταθερότητα στην εκπαίδευση

Όταν στοιβάζουμε πολλές στρώσεις, οι κλίμακες των ενεργοποιήσεων μπορεί να αλλάζουν και να δυσκολεύουν τη βελτιστοποίηση. Το Layer Normalization κανονικοποιεί κάθε θέση ως προς τα features της:

LayerNorm(x) = γ · (x - μ) / sqrt(σ^2 + ε) + β

Τα γ και β είναι μαθημένες παράμετροι που επιτρέπουν στο μοντέλο να διατηρεί την κατάλληλη κλίμακα όταν χρειάζεται.

Υλοποίηση LayerNorm (NumPy, αγγλικά σχόλια)

class LayerNorm:
    """
    Layer normalization over the last dimension.
    """

    def __init__(self, d_model, eps=1e-6):
        self.eps = eps
        self.gamma = np.ones(d_model)
        self.beta = np.zeros(d_model)

    def forward(self, x):
        mean = np.mean(x, axis=-1, keepdims=True)
        var = np.var(x, axis=-1, keepdims=True)
        x_hat = (x - mean) / np.sqrt(var + self.eps)
        return self.gamma * x_hat + self.beta
ΔιάταξηΠού μπαίνει το LayerNorm;Σχόλιο
Post-NormΜετά το residual (Add & Norm)Κλασική επιλογή, αλλά σε πολύ βαθιά δίκτυα μπορεί να δυσκολεύει
Pre-NormΠριν την υπο-στρώση (Attention/FFN)Συχνά πιο σταθερό training για μεγάλα βάθη

Residual Connections: γιατί βοηθούν σε βαθιά δίκτυα

Οι residual συνδέσεις επιτρέπουν τη ροή gradients μέσα από πολλές στρώσεις, προσθέτοντας την είσοδο στο αποτέλεσμα μιας υπο-στρώσης:

output = x + F(x)

Στους Transformers, το μοτίβο εμφανίζεται δύο φορές ανά στρώση: μία γύρω από την Attention και μία γύρω από το FFN.


Μία πλήρης Encoder στρώση (Transformer Encoder Layer)

Μια τυπική στρώση Encoder περιλαμβάνει:

  1. Multi-Head Self-Attention (επικοινωνία μεταξύ θέσεων)
  2. Feed-Forward Network (υπολογισμός ανά θέση)

Και τα δύο τυλίγονται με residual + LayerNorm. Παρακάτω είναι μια εκπαιδευτική υλοποίηση Pre-Norm με Dropout.

class TransformerEncoderLayer:
    """
    Encoder layer: (PreNorm -> Attention -> Residual) + (PreNorm -> FFN -> Residual).
    """

    def __init__(self, d_model, num_heads, d_ff, dropout_rate=0.1):
        self.attn = MultiHeadAttention(d_model, num_heads)
        self.ffn = FeedForwardNetwork(d_model, d_ff)
        self.norm1 = LayerNorm(d_model)
        self.norm2 = LayerNorm(d_model)
        self.dropout_rate = dropout_rate

    def dropout(self, x, training=True):
        if not training:
            return x
        mask = np.random.binomial(1, 1 - self.dropout_rate, x.shape)
        return x * mask / (1 - self.dropout_rate)

    def forward(self, x, training=True):
        # Attention block
        attn_out = self.attn.forward(self.norm1.forward(x))
        attn_out = self.dropout(attn_out, training)
        x = x + attn_out

        # FFN block
        ffn_out = self.ffn.forward(self.norm2.forward(x))
        ffn_out = self.dropout(ffn_out, training)
        x = x + ffn_out

        return x

Στο επόμενο μέρος θα δούμε πώς εκπαιδεύουμε ένα Transformer (loss/optimization), πώς επεκτείνεται σε μεγάλες κλίμακες με Mixture of Experts, καθώς και πρακτικές βελτιστοποιήσεις παραγωγής όπως Flash Attention και quantization.


Εκπαίδευση Transformer: Loss και βελτιστοποίηση

Αφού έχουμε την αρχιτεκτονική, πρέπει να την εκπαιδεύσουμε. Η πιο συνηθισμένη ρύθμιση για γλωσσικά μοντέλα είναι η πρόβλεψη του επόμενου token (next-token prediction).

Cross-Entropy Loss για γλωσσική μοντελοποίηση

Αν το μοντέλο προβλέπει πιθανότητες για το επόμενο token, ο στόχος είναι να μεγιστοποιήσουμε την πιθανότητα του σωστού token. Ισοδύναμα, ελαχιστοποιούμε το αρνητικό λογάριθμο:

Loss = -log(P(true_token | context))

Για ένα batch παραδειγμάτων, παίρνουμε τον μέσο όρο:

Loss_batch = -(1/N) * Σ log(P(true_token_i | context_i))

Υλοποίηση loss (NumPy, αγγλικά σχόλια)

import numpy as np

def cross_entropy_next_token_loss(logits, targets):
    """
    Cross-entropy for next-token prediction.

    Args:
        logits: (batch, seq, vocab)
        targets: (batch, seq) integer token ids

    Returns:
        loss: scalar
    """
    b, s, v = logits.shape
    logits_2d = logits.reshape(-1, v)      # (b*s, vocab)
    targets_1d = targets.reshape(-1)       # (b*s,)

    # Stable softmax
    logits_2d = logits_2d - np.max(logits_2d, axis=1, keepdims=True)
    exp_logits = np.exp(logits_2d)
    probs = exp_logits / np.sum(exp_logits, axis=1, keepdims=True)

    idx = np.arange(b * s)
    p_true = probs[idx, targets_1d]
    loss = -np.mean(np.log(p_true + 1e-10))
    return loss

Learning rate schedule με warmup (κλασική επιλογή)

Μια πολύ διαδεδομένη πρακτική είναι η warmup φάση όπου το learning rate ανεβαίνει σταδιακά, και μετά μειώνεται με νόμο inverses sqrt:

lr = d_model^(-0.5) * min(step^(-0.5), step * warmup_steps^(-1.5))

Η warmup φάση μειώνει τον κίνδυνο ασταθών ενημερώσεων όταν τα βάρη είναι ακόμη τυχαία.

ΣτοιχείοΓιατί χρησιμοποιείταιΤυπικό αποτέλεσμα
WarmupΣταθεροποιεί τα πρώτα βήματαΛιγότερα spikes στο loss
Inverse sqrt decayΜικρότερα βήματα κοντά στη σύγκλισηΠιο ομαλή σύγκλιση

Απλοποιημένος “σκελετός” training loop (ψευδοκώδικας)

def train(model, dataloader, warmup_steps):
    step = 0
    for epoch in range(num_epochs):
        for input_ids, target_ids in dataloader:
            step += 1

            lr = (model.d_model ** -0.5) * min(step ** -0.5, step * (warmup_steps ** -1.5))

            logits = model.forward(input_ids, training=True)
            loss = cross_entropy_next_token_loss(logits, target_ids)

            # Backprop would compute gradients here (framework does it)
            # Optimizer update (e.g., Adam) applies gradients with lr

Στην πράξη, χρησιμοποιούμε PyTorch/TensorFlow για αυτόματη παραγώγιση, mixed precision, gradient clipping κ.ά.


Προχωρημένο θέμα: Mixture of Experts (MoE)

Όταν κλιμακώνουμε σε τεράστια μοντέλα, το κόστος υπολογισμού γίνεται κρίσιμο. Το Mixture of Experts (MoE) επιτρέπει να αυξήσουμε την παραμετροποίηση, χωρίς να ενεργοποιούμε όλα τα βάρη για κάθε token.

Η βασική ιδέα είναι να αντικαταστήσουμε το FFN με πολλούς “experts” και ένα gating δίκτυο που επιλέγει τους top‑k experts ανά token:

gate_probs = softmax(x · W_gate)
output = Σ gate_probs[i] * Expert_i(x)  for i in top_k(gate_probs)

Έτσι, με πολλούς experts (π.χ. 64 ή 128) αλλά μικρό k (π.χ. 1 ή 2), κρατάμε το compute σχεδόν σταθερό, ενώ αυξάνουμε σημαντικά την ικανότητα του μοντέλου.

Γιατί χρειάζεται “load balancing”

Αν το gating στέλνει συνεχώς όλα τα tokens στους ίδιους experts, οι υπόλοιποι μένουν “ανεκπαίδευτοι”. Για αυτό προστίθεται ένας βοηθητικός loss που ενθαρρύνει πιο ισορροπημένη χρήση.


Προχωρημένο θέμα: Reasoning και Chain-of-Thought

Σε πολλά προβλήματα, η σωστή απάντηση απαιτεί ενδιάμεσα βήματα. Το Chain-of-Thought prompting ενθαρρύνει το μοντέλο να παράγει τέτοια βήματα, αυξάνοντας αποτελεσματικά την υπολογιστική “αλυσίδα” που εκτελεί.

Η βασική διαισθητική εξήγηση είναι ότι ο Transformer έχει σταθερό βάθος ανά token (π.χ. 24 στρώσεις). Αν παράγει περισσότερα tokens συλλογισμού, “απλώνει” τον υπολογισμό σε περισσότερα βήματα, κάτι που συχνά βελτιώνει την επίδοση σε σύνθετα tasks.

Εκπαίδευση με ενίσχυση (high level)

Μια προσέγγιση είναι να θεωρήσουμε το μοντέλο ως policy που παράγει μια ακολουθία tokens, και να μεγιστοποιήσουμε την αναμενόμενη ανταμοιβή:

J(θ) = E[R(y) | x, θ]
∇J(θ) = E[R(y) * ∇ log P(y | x, θ)]

Στην πράξη, απαιτούνται τεχνικές για πιο “πυκνό” σήμα εκπαίδευσης (baselines, value functions, curriculum, κ.ά.).


Προχωρημένο θέμα: Ποιοτικά δεδομένα εκπαίδευσης

Η ποιότητα ενός LLM περιορίζεται από την ποιότητα των δεδομένων του. Σε μεγάλες συλλογές κειμένων εμφανίζονται spam, διπλότυπα, χαμηλή ποιότητα γραφής, κ.ά.

Αποδιπλοποίηση με MinHash (ιδέα)

Στόχος: να εντοπίσουμε “σχεδόν ίδια” κείμενα. Το MinHash δίνει μια αποδοτική προσέγγιση για εκτίμηση της Jaccard similarity μεταξύ συνόλων shingles.

J(A, B) = |A ∩ B| / |A ∪ B|

Σε παραγωγή, η ακριβής υλοποίηση διαφέρει, αλλά η ιδέα παραμένει: ισχυρή αποδιπλοποίηση = λιγότερο “memorization” και καλύτερη γενίκευση.

Knowledge distillation (βασικό loss)

Στη distillation, ένα “student” μοντέλο μαθαίνει να μιμείται ένα “teacher” μοντέλο μέσω KL-divergence στις πιθανότητες:

KL(P_teacher || P_student)
def distillation_loss(student_logits, teacher_logits, T):
    student_p = softmax(student_logits / T)
    teacher_p = softmax(teacher_logits / T)
    kl = np.sum(teacher_p * np.log(teacher_p / (student_p + 1e-10)))
    return kl * (T ** 2)

“Όλα μαζί”: ένα απλό Transformer LM (σκελετός)

Παρακάτω είναι ένας συμπαγής σκελετός γλωσσικού μοντέλου που:

  • κάνει embeddings + positional encodings,
  • περνά από μια στοίβα encoder layers,
  • προβάλλει σε logits λεξιλογίου (weight tying).

Σημείωση: Η πλήρης “παραγωγική” υλοποίηση γίνεται σε deep learning framework, αλλά το σχήμα είναι αυτό.

class TransformerLanguageModel:
    """
    Minimal Transformer LM skeleton (educational).
    Assumes you already have:
      - TransformerEncoderLayer
      - get_positional_encoding
      - LayerNorm
    """

    def __init__(self, vocab_size, d_model, num_layers, num_heads, d_ff, max_seq_len, dropout_rate=0.1):
        self.vocab_size = vocab_size
        self.d_model = d_model
        self.max_seq_len = max_seq_len
        self.dropout_rate = dropout_rate

        self.token_embedding = np.random.randn(vocab_size, d_model) * 0.02
        self.pos_encoding = get_positional_encoding(max_seq_len, d_model)

        self.layers = [TransformerEncoderLayer(d_model, num_heads, d_ff, dropout_rate) for _ in range(num_layers)]
        self.final_norm = LayerNorm(d_model)

        # Weight tying: output projection shares weights with embedding
        # logits = hidden @ token_embedding.T
        # (We keep token_embedding and use transpose in forward)

    def forward(self, token_ids, training=True):
        # token_ids: (batch, seq)
        batch, seq = token_ids.shape

        x = self.token_embedding[token_ids]              # (batch, seq, d_model)
        x = x * np.sqrt(self.d_model)

        x = x + self.pos_encoding[:seq, :]               # broadcast over batch

        if training:
            mask = np.random.binomial(1, 1 - self.dropout_rate, x.shape)
            x = x * mask / (1 - self.dropout_rate)

        for layer in self.layers:
            x = layer.forward(x, training)

        x = self.final_norm.forward(x)

        logits = x @ self.token_embedding.T              # (batch, seq, vocab)
        return logits

Βελτιστοποιήσεις για παραγωγή (latency & cost)

Σε παραγωγή, τα μεγάλα μοντέλα χρειάζονται τεχνικές που μειώνουν μνήμη, bandwidth και latency.

ΤεχνικήΤι βελτιώνειΚύρια ιδέα
Flash AttentionΜνήμη & ταχύτηταΥπολογισμός attention σε blocks χωρίς πλήρη (n×n) materialization
KV cachingDecoding latencyΑποθήκευση keys/values προηγούμενων βημάτων για autoregressive generation
QuantizationΜνήμη & throughputΜείωση precision (π.χ. 8-bit / 4-bit) με ελεγχόμενο error
Kernel fusionBandwidthΣυνένωση πολλών ops σε έναν kernel για λιγότερα reads/writes

Quantization: βασικός μηχανισμός (κώδικας)

def quantize_tensor(x, num_bits=8):
    """
    Simple affine quantization to integers.
    Returns: q (int), scale, zero_point
    """
    x_min = np.min(x)
    x_max = np.max(x)

    qmin = 0
    qmax = 2 ** num_bits - 1

    scale = (x_max - x_min) / (qmax - qmin) if x_max != x_min else 1.0
    zero_point = qmin - round(x_min / scale)
    zero_point = int(np.clip(zero_point, qmin, qmax))

    q = np.round(x / scale + zero_point)
    q = np.clip(q, qmin, qmax).astype(np.int32)
    return q, scale, zero_point

def dequantize_tensor(q, scale, zero_point):
    return (q.astype(np.float32) - zero_point) * scale
Πληροφορία:
Σε πραγματικά LLM deployments, η quantization γίνεται με πιο προσεκτικές μεθόδους βαθμονόμησης και συχνά ανά υπο-ομάδες βαρών (group-wise).

Συμπέρασμα: γιατί η μαθηματική δομή των Transformers “βγάζει νόημα”

Οι Transformers δεν είναι μια τυχαία συλλογή εξισώσεων. Κάθε κομμάτι υπάρχει για να λύσει ένα συγκεκριμένο πρόβλημα κλιμάκωσης και εκπαίδευσης:

  • Η Attention επιτρέπει ανάκτηση σχετικής πληροφορίας από όλη την ακολουθία.
  • Η κλιμάκωση με √d αποτρέπει υπερβολικά “αιχμηρό” Softmax και βελτιώνει τα gradients.
  • Το Multi-Head επιτρέπει πολλαπλές ταυτόχρονες “προοπτικές” συσχέτισης.
  • Το Positional Encoding εισάγει σειρά σε έναν κατά τα άλλα position-agnostic μηχανισμό.
  • Τα Residual και το LayerNorm κάνουν δυνατή την εκπαίδευση βαθιών δικτύων.
  • Το FFN προσθέτει μη γραμμικότητα και υπολογιστική ισχύ ανά θέση.

Πρακτικές συμβουλές για υλοποίηση και βελτιστοποίηση Transformer

Αν σκοπεύεις να περάσεις από τη θεωρία στην πράξη, υπάρχουν μερικά σημεία που συνήθως “κοστίζουν” χρόνο σε debugging και απόδοση.

Πρώτον, ξεχώρισε καθαρά τα είδη μασκών: padding mask (για να αγνοείς κενά tokens σε batch) και causal mask (για autoregressive παραγωγή ώστε να μη “βλέπει” το μέλλον).

Ένα μεγάλο ποσοστό από περίεργα αποτελέσματα προέρχεται από λάθος broadcasting ή λάθος άξονες στις μάσκες.

Δεύτερον, δώσε προτεραιότητα στη αριθμητική σταθερότητα: α) Softmax πάντα με αφαίρεση του μέγιστου, β) προσοχή στο dtype όταν δουλεύεις με mixed precision, γ) μικρό epsilon σε κανονικοποιήσεις.

Σε μεγάλα βάθη, μια μικρή αριθμητική αστάθεια μπορεί να οδηγήσει σε NaNs μετά από λίγα χιλιάδες βήματα.

Τρίτον, σε training μεγάλης κλίμακας, έλεγξε συστηματικά τα εξής πριν “κάψεις” υπολογιστικό χρόνο:

  • Gradient clipping (π.χ. global norm) για να αποτρέπεις σπάνιες εκρήξεις gradients.
  • Επαλήθευση shapes σε κάθε block (ιδίως στο split/concat των heads).
  • Monitoring μετρικών όπως loss ανά token, perplexity και ρυθμός μάθησης (learning rate) στο warmup.
  • Ablations: ξεκίνα με μικρό μοντέλο/μικρό context για να επιβεβαιώσεις ότι μαθαίνει, μετά αύξησε κλίμακα.

Τέλος, αν σε ενδιαφέρει η ταχύτητα σε παραγωγή, η μεγαλύτερη πρακτική διαφορά συνήθως έρχεται από: (1) KV caching στο decoding, (2) σωστό batching αιτημάτων, (3) επιλογή quantization που ταιριάζει στο hardware σου.

Μην ξεχνάς ότι διαφορετικά workloads (chat, summarization, retrieval-augmented) έχουν διαφορετικά bottlenecks: άλλοτε κυριαρχεί το bandwidth, άλλοτε η μνήμη, άλλοτε το latency ανά token. Η σωστή βελτιστοποίηση ξεκινά με profiling και όχι με υποθέσεις.

Τελική ιδέα: όταν καταλαβαίνεις τη “σχέση αιτίου‑αποτελέσματος” πίσω από κάθε εξίσωση (τι πρόβλημα λύνει), αποκτάς την ικανότητα όχι μόνο να χρησιμοποιείς Transformers, αλλά και να τους προσαρμόζεις με ασφάλεια στις ανάγκες του δικού σου προϊόντος.

Στέλιος Θεοδωρίδης
Στέλιος Θεοδωρίδης
Ο ήρωας μου είναι ο γάτος μου ο Τσάρλι και ακροάζομαι μόνο Psychedelic Trance
RELATED ARTICLES

Πρόσφατα άρθρα

Tηλέφωνα έκτακτης ανάγκης

Δίωξη Ηλεκτρονικού Εγκλήματος: 11188
Ελληνική Αστυνομία: 100
Χαμόγελο του Παιδιού: 210 3306140
Πυροσβεστική Υπηρεσία: 199
ΕΚΑΒ 166