aidoc.uz

1-qism · Matndan til modeligacha·02-bob

Transformer — barchasini o'zgartirgan g'oya

Attention mexanizmi qadam-baqadam, raqamlar bilan. Q/K/V nima, pozitsion kodlash nega kerak, dekodlash strategiyalari va BERT'ni o'z ma'lumotingizda moslash.

O'tgan bobda RNN oilasi bilan tanishdik. Ular ishlaydi — lekin uch muammosi bor edi va ular shunchalik jiddiy ediki, oxir-oqibat butun yondashuvdan voz kechishga to'g'ri keldi.

2017-yilda Google tadqiqotchilari maqola chiqardi. Sarlavhasi kesin edi: "Attention Is All You Need" — sizga faqat e'tibor kerak.

Ular RNN'ni yaxshilashga urinmadi. Ular uni butunlay olib tashladi.

Nima muammo edi#

Uchta to'siq bor edi va ularning har biri jiddiy.

1. Moslashuv muammosi#

Tarjimada kirish va chiqish uzunligi har xil bo'ladi:

ingliz:   she doesn't like potatoes              →  4 so'z
fransuz:  elle n'aime pas les pommes de terre    →  7 so'z

Qaysi so'z qaysi so'zga to'g'ri keladi? pommes de terre — bu uchta so'z bitta potatoes ga to'g'ri keladi.

RNN buni bilmaydi. U butun gapni bitta qisqa vektorga siqadi, keyin undan tarjimani ochishga urinadi:

she doesn't like potatoes
kirish gap
encoder (RNN)
so'zma-so'z o'qiydi
[ 0.2, -0.7, ... ]
BUTUN gap — bitta vektor
decoder (RNN)
shu vektordan tarjimani ochadi
seq2seq: butun gap bitta vektorga siqiladi — uzun gaplarda bu vektor to'lib ketadi

2. Gradient so'nishi#

O'tgan bobda hisoblab ko'rgan edik: 20 qadamdan keyin gradient 0.0000009 ga tushadi. Model uzoq bog'lanishlarni o'rgana olmaydi.

3. Parallellashmaydi#

Bu eng qimmat cheklov. Raqamlar bilan ko'ramiz:

# RNN: 100 so'zlik gap, har qadam 1 ms
rnn_vaqt = 100 * 1        # 100 ms — qadamlar navbat bilan
 
# Transformer: hamma so'z bir vaqtda
tr_vaqt = 1               # ~1 ms — GPU hammasini parallel hisoblaydi
 
print(f"Tezlik farqi: {rnn_vaqt / tr_vaqt:.0f}x")   # 100x

GPU'da 10 000 yadro bor. RNN ularning bittasini ishlatadi, qolgani bo'sh turadi.


Attention: kerakli joyga qarash#

G'oya tarjimondan olingan. Tarjimon butun gapni yodlab, keyin qaytadan yozmaydi. U har bir so'zni yozayotganda manba matnning aynan kerakli joyiga qaraydi.

pommes de terre yozayotganda tarjimon potatoes ga qaraydi — she yoki doesn't ga emas.

Uch qadam#

1
Ball berish (score)
Har bir kirish so'zi hozirgi vazifaga qanchalik mos ekanini hisoblaymiz. Odatda skalyar ko'paytma bilan — birinchi bobdagi o'sha amal.
2
Softmax
Ballarni 0 dan 1 gacha qiymatlarga aylantiramiz va yig'indisi 1 bo'ladi. Endi bu «e'tibor ulushi»: diqqatimning 70% i shu so'zda, 20% i qo'shnisida.
3
Vaznli yig'indi
Har bir so'zning vektorini o'z ulushiga ko'paytirib qo'shamiz. Natijada har qadam uchun YANGI kontekst vektori chiqadi.

Softmax nima — raqamlar bilan#

Softmax butun kitobda uchraydi, shuning uchun uni bir marta puxta tushunib olamiz.

U ixtiyoriy sonlar ro'yxatini ehtimolliklar ro'yxatiga aylantiradi:

import numpy as np
 
 
def softmax(x):
    """Sonlar ro'yxatini yig'indisi 1 bo'lgan ehtimolliklarga aylantiradi."""
    e = np.exp(x - np.max(x))     # -max: katta sonlarda toshib ketmaslik uchun
    return e / e.sum()
 
 
ballar = np.array([2.0, 1.0, 0.1])
print(softmax(ballar))            # [0.659 0.242 0.099]
print(softmax(ballar).sum())      # 1.0

Softmax kuchaytiradi: kichik farqni katta farqqa aylantiradi.

print(softmax(np.array([1.0, 1.1])))      # [0.475 0.525]  ← kichik farq
print(softmax(np.array([1.0, 3.0])))      # [0.119 0.881]  ← katta farq

Self-attention: kutubxonadagi qidiruv#

Endi asosiy g'oyaga o'tamiz. Self-attention — matnning o'zi ichida, boshqa hech narsa bilan solishtirmasdan qidirish.

Eng yaxshi tashbeh — kutubxona:

Kutubxonaga kirdingiz, Fransuz inqilobi haqida kitob kerak. Siz hamma kitobni o'qib chiqmaysiz. Siz javondagi kitoblarning muqovasidagi nomlarga qaraysiz va keraklisini tanlab olasiz.

Q
Query — so'rov
«Menga Fransuz inqilobi haqida kitob kerak». Bu — hozir e'tibor qaratilayotgan so'zning nima izlayotgani.
K
Key — kalit
Kitob muqovasidagi nom. Har bir so'zning «men bu haqdaman» degan yorlig'i. So'rov aynan shular bilan solishtiriladi.
V
Value — qiymat
Kitobning ichidagi matn. So'z mos kelsa, undan olinadigan haqiqiy ma'lumot.

Formula va uning ma'nosi#

Attention(Q, K, V) = softmax( Q · Kᵀ / √d ) · V

Bo'lib tushuntiramiz:

QismNima qiladi
Q · KᵀHar bir so'rovni har bir kalit bilan solishtiradi → ballar matritsasi
/ √dBallarni jilovlaydi (pastda batafsil)
softmax(...)Ballarni e'tibor ulushlariga aylantiradi
· VUlushlarga ko'ra ma'lumotni yig'adi

Nima uchun √d ga bo'linadi#

Bu detal ko'pincha tushuntirilmaydi, lekin u muhim. Tajriba qilamiz:

import numpy as np
 
np.random.seed(0)
 
for d in [4, 64, 512]:
    q = np.random.randn(d)
    k = np.random.randn(d)
    ball = q @ k
    print(f"d={d:>3}:  ball = {ball:>7.2f}   √d ga bo'lgach = {ball / np.sqrt(d):>6.2f}")
d=  4:  ball =    1.83   √d ga bo'lgach =   0.92
d= 64:  ball =   -8.94   √d ga bo'lgach =  -1.12
d=512:  ball =   26.13   √d ga bo'lgach =   1.15

O'lcham oshgan sari skalyar ko'paytma kattalashadi. Va softmax katta sonlarni ko'rsa nima bo'ladi:

kichik = np.array([1.0, 2.0, 0.5])
katta = kichik * 20
 
print(softmax(kichik))    # [0.245 0.665 0.090]  ← taqsimlangan
print(softmax(katta))     # [0.000 1.000 0.000]  ← bittasiga yopishib qoldi

Softmax deyarli butun ehtimollikni bitta elementga beradi. Bu ikki muammo tug'diradi:

  1. Model faqat bitta so'zga qaraydi, kontekstni ko'rmaydi
  2. Gradient nolga tushadi — softmax to'yingan zonada hosila juda kichik, model o'rganmaydi

√d ga bo'lish ballarni taxminan -2 … 2 oralig'ida ushlab turadi. Oddiy, lekin butun o'qitishni mumkin qiladigan detal.

Attention'ni noldan yozamiz#

Nazariyani kodga aylantiramiz. To'liq ishlaydigan misol:

import numpy as np
 
 
def softmax(x, axis=-1):
    e = np.exp(x - np.max(x, axis=axis, keepdims=True))
    return e / e.sum(axis=axis, keepdims=True)
 
 
def self_attention(X, W_q, W_k, W_v):
    """
    X    — kirish: (n_token, d_model)
    W_*  — o'rganiladigan matritsalar: (d_model, d_k)
    """
    Q = X @ W_q                      # (n, d_k)
    K = X @ W_k                      # (n, d_k)
    V = X @ W_v                      # (n, d_k)
 
    d_k = Q.shape[-1]
    ballar = Q @ K.T / np.sqrt(d_k)  # (n, n) — har so'z har so'zga
 
    ulushlar = softmax(ballar)       # (n, n) — qatorlar yig'indisi 1
    return ulushlar @ V, ulushlar    # (n, d_k), (n, n)
 
 
# 4 ta token, har biri 8 o'lchamli
np.random.seed(42)
X = np.random.randn(4, 8)
W_q, W_k, W_v = (np.random.randn(8, 8) * 0.1 for _ in range(3))
 
chiqish, ulushlar = self_attention(X, W_q, W_k, W_v)
 
print("chiqish shakli:", chiqish.shape)          # (4, 8)
print("\ne'tibor matritsasi (har qator yig'indisi = 1):")
print(np.round(ulushlar, 3))
print("\nqatorlar yig'indisi:", np.round(ulushlar.sum(axis=1), 6))
chiqish shakli: (4, 8)

e'tibor matritsasi (har qator yig'indisi = 1):
[[0.254 0.245 0.253 0.248]
 [0.248 0.259 0.244 0.249]
 [0.251 0.243 0.256 0.250]
 [0.247 0.252 0.248 0.253]]

qatorlar yig'indisi: [1. 1. 1. 1.]
Amaliy topshiriq

E'tibor matritsasini qo'lda hisoblang

Quyidagi kichik misolni qog'ozda hisoblang, keyin kod bilan tekshiring:

Q = np.array([[1, 0],
              [0, 1],
              [1, 1]], dtype=float)
 
K = np.array([[1, 0],
              [0, 1],
              [1, 1]], dtype=float)
 
V = np.array([[10, 0],
              [0, 10],
              [5,  5]], dtype=float)
  1. Q @ K.T ni hisoblang
  2. √2 ga bo'ling
  3. Har qatorga softmax qo'llang
  4. V ga ko'paytiring

Uchinchi qator ([1,1]) natijasi nima uchun boshqalardan farq qiladi?

Maslahatni ko'rsatish

Yechim: Q @ K.T ni hisoblang → 3x3 matritsa Har bir elementni √2 ga bo'ling Har qatorga alohida softmax qo'llang Birinchi qator uchun: ballar = [1*1+0*0, 1*0+0*1, 1*1+0*1] = [1, 0, 1] /√2 → [0.707, 0, 0.707] softmax → [0.422, 0.208, 0.370] (taxminan) Diqqat: softmax har QATORGA alohida qo'llanadi, butun matritsaga emas. Agar butun matritsaga qo'llasangiz, qatorlar yig'indisi 1 bo'lmaydi.

Multi-head: bir vaqtda bir necha savol#

Bitta gapga bir nechta savol berish mumkin:

"Anvar kecha Toshkentda ukasiga kitob berdi"

kim harakat qildi?     → Anvar
qachon?                → kecha
qayerda?               → Toshkentda
kimga?                 → ukasiga
nimani?                → kitob

Bitta attention bularning hammasini bir vaqtda kuzata olmaydi. Shuning uchun bir nechta parallel attention ishlatiladi — har biri head (bosh) deb ataladi.

head 1
grammatik bog'lanish — ega va kesim
o'z Wq, Wk, Wv
head 2
olmosh qaysi otga ishora qiladi
o'z Wq, Wk, Wv
head 3
mavzuviy yaqinlik
o'z Wq, Wk, Wv
BERT-base'da 12 ta head, 12 ta qatlam = 144 nuqtai nazar
birlashtirish + chiziqli proyeksiya
natijalar qo'shilib, asl o'lchamga qaytariladi
Multi-head attention: har bir head o'z Q/K/V matritsalariga ega va turli munosabatga ixtisoslashadi
def multi_head_attention(X, n_head=4):
    """Soddalashtirilgan multi-head: har bir head alohida hisoblanadi."""
    d_model = X.shape[-1]
    d_head = d_model // n_head          # har bir head ulushi
 
    natijalar = []
    for _ in range(n_head):
        W_q, W_k, W_v = (np.random.randn(d_model, d_head) * 0.1 for _ in range(3))
        chiqish, _ = self_attention(X, W_q, W_k, W_v)
        natijalar.append(chiqish)
 
    birlashgan = np.concatenate(natijalar, axis=-1)     # (n, d_model)
    W_o = np.random.randn(d_model, d_model) * 0.1
    return birlashgan @ W_o
 
 
X = np.random.randn(4, 8)
print(multi_head_attention(X, n_head=4).shape)          # (4, 8)

Diqqat qiling: d_head = d_model // n_head. Head soni ko'paysa, har birining o'lchami kichrayadi — umumiy hisob hajmi o'zgarmaydi. 4 ta katta head yoki 8 ta kichik head — narx bir xil.

Self-attention nima berdi
  • Bir kirishdan bir nechta turli tasavvur — har head o'z munosabatini kuzatadi
  • Hamma head parallel hisoblanadi — GPU to'liq band bo'ladi
  • Uzoq so'zlar orasidagi bog'liqlik BITTA qadamda ko'rinadi, RNN'dagidek navbat kutmaydi
  • E'tibor matritsasini ko'rish mumkin — model qaysi so'zga qarayotgani ma'lum
Nima evaziga
  • Narx tokenlar soniga KVADRATIK: 2 barobar uzun matn = 4 barobar hisob
  • Xotira ham kvadratik — kontekst oynasi shu sabab cheklangan
  • Tartibni umuman bilmaydi — so'zlar to'plamini ko'radi, ketma-ketlikni emas
  • Kichik ma'lumotda RNN'dan yomonroq ishlaydi — ko'p misol talab qiladi

Kvadratik narx — bugungi eng katta muammo. 100 ming tokenli kontekst uchun e'tibor matritsasi 10 milliard katakdan iborat bo'ladi. Shuning uchun FlashAttention, sliding window, linear attention kabi o'nlab optimizatsiya usullari o'ylab topilgan.


Pozitsion kodlash: tartibni qaytarish#

Self-attention tartibni bilmaydi. Buni isbotlash oson:

X1 = np.array([[1., 0.], [0., 1.]])      # ikki token
X2 = X1[::-1]                             # o'rinlari almashtirilgan
 
W = np.eye(2)
c1, _ = self_attention(X1, W, W, W)
c2, _ = self_attention(X2, W, W, W)
 
print(np.allclose(c1, c2[::-1]))          # True — natija bir xil!

Model uchun "It odamni qopdi" va "Odamni it qopdi" bir xil.

Yechim: o'rinni vektorga qo'shish#

Asl maqolada sinus va kosinus funksiyalari ishlatilgan:

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

Kodda:

def positional_encoding(max_uzunlik, d_model):
    """Har bir o'rin uchun takrorlanmas 'imzo' vektori."""
    pos = np.arange(max_uzunlik)[:, None]              # (uzunlik, 1)
    i = np.arange(d_model)[None, :]                    # (1, d_model)
 
    burchak = pos / np.power(10000, (2 * (i // 2)) / d_model)
 
    pe = np.zeros((max_uzunlik, d_model))
    pe[:, 0::2] = np.sin(burchak[:, 0::2])             # juft o'lchamlar
    pe[:, 1::2] = np.cos(burchak[:, 1::2])             # toq o'lchamlar
    return pe
 
 
pe = positional_encoding(50, 128)
print(pe.shape)                                        # (50, 128)
 
# Embedding'ga QO'SHILADI, ulanmaydi
X_with_pos = X_embedding + pe[:len(X_embedding)]

Uni ko'rish mumkin:

import matplotlib.pyplot as plt
 
pe = positional_encoding(100, 64)
 
plt.figure(figsize=(10, 4))
plt.imshow(pe.T, cmap="RdBu", aspect="auto")
plt.xlabel("pozitsiya")
plt.ylabel("o'lcham")
plt.colorbar()
plt.title("Pozitsion kodlash")
plt.show()

Grafikda to'lqinsimon naqsh ko'rinadi: pastki o'lchamlar tez o'zgaradi (yaqin pozitsiyalarni ajratadi), yuqorigilari sekin (uzoq masofani ifodalaydi).


Transformer bloki#

Endi qismlarni yig'amiz. Bitta blok to'rt komponentdan iborat.

kirish X
(n_token, d_model)
Multi-head self-attention
so'zlar bir-biriga qaraydi
+ qoldiq bog'lanish
X ni chiqishga QO'SHAMIZ
Layer normalization
qiymatlarni bir oraliqqa keltiradi
Feedforward (2 chiziqli qatlam)
chiziqsizlik qo'shadi — parametrlarning 66% i shu yerda
+ qoldiq bog'lanish
Layer normalization
chiqish
(n_token, d_model) — kirish bilan BIR XIL
Transformer bloki: kirish va chiqish o'lchami bir xil — shuning uchun bloklarni ustma-ust qo'yish mumkin

Qoldiq bog'lanish — eng muhim detal#

H = LayerNorm(X + Attention(X)) — bu + X qismi ko'rinishidan arzimas, aslida esa chuqur tarmoqlarni umuman mumkin qilgan narsa.

Nima uchun? Gradient hisoblang:

# Qoldiq bog'lanishsiz: gradient har qatlamda ko'payadi
g = 1.0
for qatlam in range(50):
    g *= 0.9                      # har qatlam gradientni 0.9 ga ko'paytiradi
print(f"Qoldiqsiz, 50 qatlam: {g:.8f}")      # 0.00515378
 
# Qoldiq bilan: qisqa yo'l bor, gradient 1 dan boshlanadi
g = 1.0
for qatlam in range(50):
    g = g * 0.9 + g               # + g — qoldiq bog'lanish orqali o'tadi
print(f"Qoldiq bilan, 50 qatlam: {g:.2e}")   # juda katta, ya'ni signal yo'qolmaydi

Qoldiq bog'lanish gradientga qisqa yo'l beradi — u qatlamlar orqali o'tmasdan to'g'ridan-to'g'ri pastga tushadi.

Layer normalization#

Vektor qiymatlarini o'rtacha 0, standart chetlanish 1 bo'ladigan qilib keltiradi:

def layer_norm(x, eps=1e-5):
    """Har bir vektorni alohida normallashtiradi."""
    ortacha = x.mean(axis=-1, keepdims=True)
    std = x.std(axis=-1, keepdims=True)
    return (x - ortacha) / (std + eps)
 
 
x = np.array([10.0, 20.0, 30.0, 40.0])
print(layer_norm(x))            # [-1.342 -0.447  0.447  1.342]
print(layer_norm(x).mean())     # ~0
print(layer_norm(x).std())      # ~1

Nima uchun kerak: o'qitish davomida qiymatlar sudralib ketadi — ba'zi qatlamlarda juda katta, ba'zilarida juda kichik bo'lib qoladi. Bu o'qitishni beqaror qiladi. Normallashtirish ularni bir oraliqda ushlab turadi.

Feedforward qatlam#

Ikkita chiziqli qatlam, orasida ReLU:

FFN(x) = max(0, x·W₁ + b₁)·W₂ + b₂
def feedforward(x, d_model, d_ff=None):
    """Odatda d_ff = 4 × d_model."""
    d_ff = d_ff or d_model * 4
    W1 = np.random.randn(d_model, d_ff) * 0.02
    W2 = np.random.randn(d_ff, d_model) * 0.02
 
    yashirin = np.maximum(0, x @ W1)       # ReLU
    return yashirin @ W2

To'liq blok#

class TransformerBlock:
    """Soddalashtirilgan transformer bloki (o'qitish qismisiz)."""
 
    def __init__(self, d_model, n_head):
        self.d_model = d_model
        self.n_head = n_head
 
    def forward(self, x):
        # 1. Attention + qoldiq + norm
        attn = multi_head_attention(x, self.n_head)
        x = layer_norm(x + attn)
 
        # 2. FFN + qoldiq + norm
        ff = feedforward(x, self.d_model)
        x = layer_norm(x + ff)
 
        return x
 
 
blok = TransformerBlock(d_model=8, n_head=2)
X = np.random.randn(4, 8)
 
for i in range(3):                   # 3 ta blokni ketma-ket qo'llaymiz
    X = blok.forward(X)
 
print(X.shape)                       # (4, 8) — o'lcham o'zgarmadi

Kirish va chiqish o'lchami bir xil — shuning uchun bloklarni xohlagancha ustma-ust qo'yish mumkin. GPT-3'da 96 ta blok bor va ularning tuzilishi aynan bir xil.


Encoder, decoder va niqob#

Asl transformer tarjima uchun ikki qismdan iborat edi.

Encoder-only
BERT — matnni TUSHUNADI, ikki tomondan o'qiydi
tasniflash, qidiruv
Decoder-only
GPT, Claude, Llama — matn YOZADI, chapdan o'ngga
bugun deyarli hammasi
Encoder-decoder
T5, asl transformer — bir matndan boshqasini yasaydi
tarjima, xulosalash
Uch xil arxitektura va ularning qo'llanishi

Niqob (mask) — nima uchun kerak#

Decoder keyingi so'zni bashorat qilishi kerak. Agar u javobni oldindan ko'rsa, o'rganish ma'nosiz bo'ladi — bu ma'lumot sizib chiqishi.

Yechim: kelajakdagi tokenlarni -∞ bilan to'sish.

def causal_mask(n):
    """Yuqori uchburchakni -inf qiladi: har token faqat o'zidan oldingilarni ko'radi."""
    mask = np.triu(np.ones((n, n)), k=1)        # yuqori uchburchak = 1
    return np.where(mask == 1, -np.inf, 0.0)
 
 
n = 4
mask = causal_mask(n)
print(mask)
[[  0. -inf -inf -inf]
 [  0.   0. -inf -inf]
 [  0.   0.   0. -inf]
 [  0.   0.   0.   0.]]

Ballar matritsasiga qo'shiladi, keyin softmax qo'llanadi:

ballar = np.random.randn(4, 4)
ulushlar = softmax(ballar + mask)
 
print(np.round(ulushlar, 3))
[[1.    0.    0.    0.   ]
 [0.412 0.588 0.    0.   ]
 [0.244 0.377 0.379 0.   ]
 [0.301 0.198 0.259 0.242]]

exp(-inf) = 0 bo'lgani uchun kelajakdagi tokenlarning ulushi aniq nol. Birinchi token faqat o'zini ko'radi, ikkinchisi birinchi ikkitasini va hokazo.


Model qanday o'qitiladi#

Bu murakkab modelni o'qitish kutilganidan ancha sodda.

Vazifa bitta: matnning bir qismini ber, keyingi so'zni bashorat qil.

kirish:  "Bo'lish yoki bo'lmaslik — mana"
javob:   "masala"

To'g'ri javob matnning o'zida turibdi. Hech kim qo'lda yorliq yozmaydi. Shuning uchun bunga self-supervised — o'z-o'zini nazorat qiluvchi o'qitish deyiladi.

Internetdagi har qanday matn tayyor o'quv materiali.

Cross-entropy — xatoni o'lchash#

def cross_entropy(bashorat, togri_indeks):
    """
    bashorat     — lug'atdagi har so'z uchun ehtimollik
    togri_indeks — haqiqiy keyingi so'zning raqami
    """
    return -np.log(bashorat[togri_indeks] + 1e-12)
 
 
lugat = ["masala", "savol", "traktor"]
bashorat = np.array([0.7, 0.2, 0.1])
 
for i, soz in enumerate(lugat):
    print(f"agar to'g'ri javob '{soz}' bo'lsa → xato = {cross_entropy(bashorat, i):.3f}")
agar to'g'ri javob 'masala' bo'lsa → xato = 0.357
agar to'g'ri javob 'savol' bo'lsa → xato = 1.609
agar to'g'ri javob 'traktor' bo'lsa → xato = 2.303

Mantiq aniq: model to'g'ri javobga qanchalik yuqori ehtimollik bergan bo'lsa, xato shunchalik kichik. Agar u to'g'ri javobni deyarli nol deb baholasa, xato juda katta bo'ladi va model kuchli tuzatiladi.


Dekodlash: javobni tanlash#

Modelda ehtimollik vektori bor. Undan qanday so'z tanlanadi? Bu tanlov natijaga hayratlanarli darajada ta'sir qiladi.

def greedy(ehtimolliklar):
    """Eng ehtimolli so'zni oladi."""
    return int(np.argmax(ehtimolliklar))
 
 
def temperature_sample(logitlar, t=1.0):
    """t kichik → ishonchli va zerikarli; t katta → tavakkalchi va ijodiy."""
    return softmax(logitlar / t)
 
 
def top_k(ehtimolliklar, k=5):
    """Faqat eng ehtimolli k ta so'zdan tanlaydi."""
    indekslar = np.argsort(ehtimolliklar)[::-1][:k]
    p = ehtimolliklar[indekslar]
    p = p / p.sum()                                  # qayta normallashtirish
    return int(np.random.choice(indekslar, p=p))
 
 
def top_p(ehtimolliklar, p=0.9):
    """Ehtimolliklar yig'indisi p ga yetguncha so'z oladi (nucleus sampling)."""
    indekslar = np.argsort(ehtimolliklar)[::-1]
    yigindi = np.cumsum(ehtimolliklar[indekslar])
    kesim = np.searchsorted(yigindi, p) + 1
 
    tanlov = indekslar[:kesim]
    q = ehtimolliklar[tanlov]
    q = q / q.sum()
    return int(np.random.choice(tanlov, p=q))

Temperature qanday ishlashini ko'ramiz:

logitlar = np.array([3.0, 2.0, 1.0, 0.5, 0.1])
 
for t in [0.1, 0.5, 1.0, 2.0]:
    p = temperature_sample(logitlar, t)
    print(f"t={t:>4}:  {np.round(p, 3)}")
t= 0.1:  [1.    0.    0.    0.    0.   ]     ← greedy bilan bir xil
t= 0.5:  [0.84  0.114 0.015 0.006 0.003]     ← ishonchli
t= 1.0:  [0.578 0.213 0.078 0.047 0.032]     ← muvozanatli
t= 2.0:  [0.363 0.22  0.134 0.104 0.081]     ← tavakkalchi

BPE: notanish so'z muammosi#

Lug'at cheklangan. Notanish so'z kelsa <UNK> bo'lib qoladi va ma'no yo'qoladi.

Yechim: so'zni butun deb hisoblamaslik.

BPE'ni noldan yozamiz#

Byte-Pair Encoding algoritmi hayratlanarli darajada oddiy. Uni to'liq yozamiz:

from collections import Counter
 
 
def bpe_orgatish(korpus, birlashmalar=10):
    """Eng ko'p uchraydigan harf juftliklarini ketma-ket birlashtiradi."""
    # Har so'zni harflarga ajratamiz, oxiriga so'z tugashi belgisi
    lugat = Counter()
    for soz in korpus.split():
        lugat[" ".join(soz) + " </w>"] += 1
 
    qoidalar = []
 
    for qadam in range(birlashmalar):
        # Barcha qo'shni juftliklarni sanaymiz
        juftlar = Counter()
        for soz, son in lugat.items():
            belgilar = soz.split()
            for i in range(len(belgilar) - 1):
                juftlar[(belgilar[i], belgilar[i + 1])] += son
 
        if not juftlar:
            break
 
        eng_kop = juftlar.most_common(1)[0][0]
        qoidalar.append(eng_kop)
 
        # Shu juftlikni butun lug'atda birlashtiramiz
        yangi = {}
        eski = " ".join(eng_kop)
        birlashgan = "".join(eng_kop)
        for soz, son in lugat.items():
            yangi[soz.replace(eski, birlashgan)] = son
        lugat = yangi
 
        print(f"{qadam + 1:>2}. birlashtirildi: {eng_kop[0]!r} + {eng_kop[1]!r}{birlashgan!r}")
 
    return qoidalar, lugat
 
 
korpus = "kitob kitoblar kitobxon kitobcha maktab maktablar maktabda"
qoidalar, lugat = bpe_orgatish(korpus, birlashmalar=8)
 
print("\nYakuniy bo'linish:")
for soz in lugat:
    print(" ", soz)

Natija:

 1. birlashtirildi: 'k' + 'i' → 'ki'
 2. birlashtirildi: 'ki' + 't' → 'kit'
 3. birlashtirildi: 'kit' + 'o' → 'kito'
 4. birlashtirildi: 'kito' + 'b' → 'kitob'
 5. birlashtirildi: 'm' + 'a' → 'ma'
 6. birlashtirildi: 'ma' + 'k' → 'mak'
 7. birlashtirildi: 'mak' + 't' → 'makt'
 8. birlashtirildi: 'makt' + 'a' → 'makta'

Yakuniy bo'linish:
  kitob </w>
  kitob l a r </w>
  kitob x o n </w>
  kitob c h a </w>
  makta b </w>
  makta b l a r </w>
  makta b d a </w>

Algoritm hech kim aytmasdan turib kitob va makta o'zaklarini o'zi topdi.


BERT va niqoblangan o'qitish#

Asl transformer matnni faqat chapdan o'ngga o'qiydi. Lekin ba'zi vazifalarda ikki tomondan kontekst kerak:

"U bankka bordi va pul yechdi."
     ↑ qanday bank? Javob O'NGDA turibdi.

BERT shuni hal qildi. Lekin ikki tomonlama model uchun "keyingi so'zni bashorat qil" vazifasi ishlamaydi — javob ko'rinib turibdi.

Shuning uchun boshqa vazifa: Masked Language Model.

kirish:  "Bugun havo juda [MASK] edi"
vazifa:  [MASK] o'rnida nima bo'lishi kerak?

Tokenlarning 15% i tasodifiy yashiriladi.

BERT (encoder-only, MLM)
  • Ikki tomondan kontekst — ma'no noaniqligini yaxshi hal qiladi
  • Tasniflash, NER, qidiruvda kuchli
  • Kichik va tez — CPU'da ham ishlaydi
  • Fine-tuning uchun kam misol yetarli
Kamchiliklari
  • Matn generatsiya qila olmaydi (2024 gacha shunday deb hisoblangan)
  • O'qitish isrofgar — har o'tishda tokenlarning faqat 15% i o'rganishga hissa qo'shadi
  • Kontekst oynasi kichik — odatda 512 token
  • Ko'rsatmaga amal qilmaydi — har vazifa uchun alohida fine-tuning kerak

Aynan shu isrofgarlik sabab bugungi katta modellar decoder-only: u yerda HAR BIR token o'quv misoli bo'ladi, ya'ni bir xil hisob quvvatidan 6-7 barobar ko'p foyda olinadi.


Amaliyot: BERT'ni o'z ma'lumotingizda moslash#

Katta modelni noldan o'qitish imkonsiz — BERT 64 ta TPU'da 4 kun o'qitilgan. Lekin kerak ham emas.

Ilmiy maqola yozishni o'rganish uchun tilni qaytadan o'rganmaysiz. Bilgan tilingizni yangi vazifaga moslaysiz, xolos.

  1. Kutubxonalarni o'rnating

    terminal
    pip install transformers datasets accelerate evaluate
    Successfully installed transformers-4.48.0 datasets-3.2.0 ...
  2. Ma'lumotni tayyorlang

    from datasets import Dataset
    import pandas as pd
     
    df = pd.read_csv("sharhlar.csv")          # ustunlar: matn, belgi (0/1)
     
    ds = Dataset.from_pandas(df)
    ds = ds.train_test_split(test_size=0.2, seed=42)
     
    print(ds)
    DatasetDict({
        train: Dataset({ features: ['matn', 'belgi'], num_rows: 8000 })
        test:  Dataset({ features: ['matn', 'belgi'], num_rows: 2000 })
    })
    
  3. Tokenizatsiya qiling

    from transformers import AutoTokenizer
     
    MODEL = "distilbert-base-multilingual-cased"      # o'zbekchani ham tushunadi
    tokenizer = AutoTokenizer.from_pretrained(MODEL)
     
    def tokenlash(namunalar):
        return tokenizer(
            namunalar["matn"],
            truncation=True,          # 512 tokendan uzunini kesadi
            max_length=256,
            padding="max_length",
        )
     
    ds = ds.map(tokenlash, batched=True)
    ds = ds.rename_column("belgi", "labels")          # Trainer shu nomni kutadi
    ds.set_format("torch", columns=["input_ids", "attention_mask", "labels"])
  4. Modelni yuklang va o'qiting

    import numpy as np
    import evaluate
    from transformers import (
        AutoModelForSequenceClassification,
        TrainingArguments,
        Trainer,
    )
     
    model = AutoModelForSequenceClassification.from_pretrained(MODEL, num_labels=2)
     
    metrika = evaluate.load("accuracy")
     
    def hisobla(eval_pred):
        logitlar, yorliqlar = eval_pred
        bashorat = np.argmax(logitlar, axis=-1)
        return metrika.compute(predictions=bashorat, references=yorliqlar)
     
     
    args = TrainingArguments(
        output_dir="./natijalar",
        num_train_epochs=3,
        per_device_train_batch_size=16,
        per_device_eval_batch_size=32,
        learning_rate=2e-5,                # fine-tuning uchun KICHIK bo'lishi shart
        warmup_steps=500,
        weight_decay=0.01,
        eval_strategy="epoch",
        save_strategy="epoch",
        load_best_model_at_end=True,
        logging_steps=50,
    )
     
    trainer = Trainer(
        model=model,
        args=args,
        train_dataset=ds["train"],
        eval_dataset=ds["test"],
        compute_metrics=hisobla,
    )
     
    trainer.train()
  5. Ishlatib ko'ring

    from transformers import pipeline
     
    klassifikator = pipeline(
        "text-classification",
        model=model,
        tokenizer=tokenizer,
        device=0 if torch.cuda.is_available() else -1,
    )
     
    for matn in ["Xizmat ajoyib edi, rahmat!",
                 "Juda uzoq kutdik, boshqa kelmayman"]:
        natija = klassifikator(matn)[0]
        print(f"{natija['label']}  {natija['score']:.0%}{matn}")

E'tiborni ko'rish#

Attention'ning ajoyib tomoni — uni ko'rish mumkin:

# pip install bertviz
from bertviz import head_view
from transformers import AutoTokenizer, AutoModel
 
model = AutoModel.from_pretrained("bert-base-uncased", output_attentions=True)
tokenizer = AutoTokenizer.from_pretrained("bert-base-uncased")
 
matn = "The cat sat on the mat because it was tired"
kirish = tokenizer(matn, return_tensors="pt")
chiqish = model(**kirish)
 
tokenlar = tokenizer.convert_ids_to_tokens(kirish["input_ids"][0])
head_view(chiqish.attentions, tokenlar)

Bu interaktiv vizualizatsiya ochadi. Unda ko'rasizki:

  • Ba'zi head'lar olmoshni otga bog'laydiitcat
  • Ba'zilari artikllarni otga bog'laydi
  • Ba'zilari faqat qo'shni so'zga qaraydi
  • Ba'zilari [CLS] yoki [SEP] ga qaraydi (bu "hech narsa qilmayapman" degani)
Amaliy topshiriq

O'z modelingizni o'qiting va e'tiborni tahlil qiling

  1. O'zbekcha yoki inglizcha kayfiyat to'plamini toping (kamida 2000 ta misol)
  2. distilbert-base-multilingual-cased ni fine-tuning qiling
  3. Aniqlikni o'lchang va 1-bobdagi GRU modeli bilan solishtiring
  4. bertviz bilan bir nechta gapning e'tibor matritsasini ko'ring

Savollar:

  • Fine-tuning natijasi noldan o'qitilgan GRU'dan qanchalik yaxshi?
  • Qaysi head'lar mantiqiy naqsh ko'rsatyapti, qaysilari shovqin?
  • Model xato qilgan misollarni ko'ring — ularda umumiy narsa bormi?
Maslahatni ko'rsatish

Ma'lumot topish: - huggingface.co/datasets da o'zbekcha to'plamlar bor - Telegram kanal izohlarini yig'ing - Play Market ilova sharhlarini oling Agar aniqlik 60% dan past bo'lsa, tekshiring: 1. Yorliqlar to'g'ri joylashganmi? (0 va 1, matn emas) 2. max_length yetarlimi? Uzun sharhlar kesilmayaptimi? 3. Sinflar muvozanatlimi? 90/10 bo'lsa model doim ko'pchilikni tanlaydi 4. learning_rate 2e-5 mi? Katta bo'lsa model buziladi E'tiborni tahlil qilishda [CLS] va [SEP] ga qaragan head'lar ko'p bo'ladi — bu normal, ular 'bo'sh' head'lar deb ataladi.


Distillation: katta modeldan kichigiga#

Oxirgi texnika. Katta model juda ko'p narsa biladi, lekin bitta vazifa uchun bu ortiqcha va qimmat.

Knowledge distillation — bilimni "o'qituvchi" modeldan "o'quvchi" modelga o'tkazish.

o'qituvchi model (katta)
javob: 90% ijobiy, 10% salbiy
o'quvchi model (kichik)
shu TAQSIMOTNI takrorlashga o'rganadi
natija
40% kichik, 60% tez, sifatning 97% i saqlanadi
Distillation: o'quvchi model o'qituvchining EHTIMOLLIKLARINI takrorlashga o'rganadi

Nima uchun bu shunchaki yorliqlarda o'qitishdan yaxshiroq?

Chunki o'qituvchining ehtimolliklari ko'proq ma'lumot beradi:

Quruq yorliq:              "ijobiy"
O'qituvchi ehtimolligi:    ijobiy 0.55, salbiy 0.45

Ikkinchisi qo'shimcha ma'lumot beradi: bu misol chegaraviy. O'quvchi model shundan "bu turdagi gaplar noaniq" degan bilimni oladi.

Bu soft labels yoki "dark knowledge" deb ataladi.


Xulosa#

RNN
so'zma-so'z o'qiydi
sekin, unutuvchan
seq2seq
encoder + decoder
axborot bo'yni
+ attention
kerakli joyga qaraydi
moslashuv hal bo'ldi
self-attention
matn o'z ichida qidiradi (Q, K, V)
parallel
transformer
faqat self-attention, RNN yo'q
96 blokgacha
RNN'dan transformer'gacha bo'lgan yo'l
Yodda saqlang
  • 01Q, K, V — kutubxonadagi qidiruv. So'rov, kalit, qiymat. Uchtaligi bejiz emas: so'zning qidirayotgani bilan taklif qilayotgani har xil narsa.
  • 02√d ga bo'lish — arzimas ko'rinadigan, lekin butun o'qitishni mumkin qiladigan detal. Busiz softmax to'yinadi va gradient nolga tushadi.
  • 03Qoldiq bog'lanish (+ X) — 96 qatlamli modelni o'qitish imkonini bergan narsa. Gradientga qisqa yo'l beradi.
  • 04Parallellik — asosiy yutuq. Aynan shu modellarni milliardlab parametrgacha o'stirish imkonini berdi.
  • 05Temperature va top-p — javob sifatiga eng ko'p ta'sir qiladigan sozlamalar. JSON kerak bo'lsa temperature=0 qo'ying.
  • 06O'zbekcha matn ingliz tiliga qaraganda 2-3 barobar ko'p token yeydi. Byudjet hisoblashda buni unutmang.

Keyingi bobda bu bloklardan katta til modellari qanday yig'ilishini ko'ramiz: miqyos qonuni, kutilmagan qobiliyatlar, LoRA bilan arzon moslash va RLHF.