1-qism · Matndan til modeligacha·01-bob
Matnni raqamga aylantirish
Kompyuter harfni tushunmaydi. Matn qanday qilib songa aylanadi, nega sodda usullar ishlamaydi va embedding nima uchun hamma narsani o'zgartirdi — ishlaydigan kod bilan.
Sizda 10 000 ta mijoz sharhi bor. Ularning qaysi biri norozilik, qaysi biri maqtov — bilishingiz kerak. Qo'lda o'qib chiqsangiz bir hafta ketadi.
Kompyuterga topshirmoqchisiz. Lekin muammo shundaki:
Kompyuter
"xizmat yomon edi"degan matnni ko'rmaydi. U faqat sonlar bilan ishlaydi.
Demak birinchi vazifa aniq: matnni songa aylantirish. Va buni shunday qilish kerakki, sonlarda so'zning ma'nosi saqlansin.
Bu bob — butun kitobning poydevori. Bu yerda o'rganadigan uch narsa (vektor, o'xshashlik, ketma-ketlik) keyingi o'n bobda qayta-qayta uchraydi.
Muhitni tayyorlash#
Bu bobdagi hamma kod ishlaydi. Uni ishga tushirish uchun muhitni tayyorlaymiz.
Python muhitini yarating
Loyiha uchun alohida papka va virtual muhit:
~/loyihamkdir aidoc-1bob && cd aidoc-1bobpython3 -m venv venvsource venv/bin/activate(venv) $ ← qator boshida shu paydo bo'lsa, muhit faolWindows'da oxirgi qator o'rniga:
venv\Scripts\activateKutubxonalarni o'rnating
terminalpip install numpy scikit-learn gensim torch matplotlib nltkSuccessfully installed numpy-2.1.3 scikit-learn-1.6.0 gensim-4.3.3 torch-2.6.0 ...Nima uchun kerak:
- numpy — vektor va matritsalar bilan ishlash
- scikit-learn — tayyor TF-IDF va yordamchi funksiyalar
- gensim — word2vec o'qitish
- torch — neyron tarmoqlar
- nltk — matnni tokenlarga ajratish
Ishlayotganini tekshiring
tekshir.pyfayl yarating:import numpy as np import torch print("numpy:", np.__version__) print("torch:", torch.__version__) print("GPU bormi:", torch.cuda.is_available())terminalpython tekshir.pynumpy: 2.1.3 torch: 2.6.0 GPU bormi: FalseGPU bormi: Falsechiqsa xavotir olmang — bu bobdagi hamma narsa protsessorda ishlaydi.
1-bosqich: matnni bo'laklarga ajratish#
Har qanday ishdan oldin matnni tokenlarga ajratish kerak. Token — bu matnning eng kichik mustaqil bo'lagi.
Uchta qaror qabul qilinadi va har biri natijaga ta'sir qiladi:
- 1
- Registr — katta harflarni tushirish«Havo» va «havo» — bu bitta so'z. Kompyuter buni o'zi bilmaydi. Lekin ehtiyot bo'ling: «Olma» (shahar) va «olma» (meva) ham birlashib ketadi.
- 2
- Tinish belgilari — saqlashmi yoki tashlashmiNuqta, so'roq va undov ma'no tashiydi: savolmi, his-hayajonmi, gap tugadimi. Kayfiyat tahlilida «!» juda foydali. Mavzu tasnifida esa ortiqcha.
- 3
- Normallashtirish — so'z o'zagiga keltirish«keldi», «kelaman», «kelgan» — bularning hammasi «kel» dan. Bu lemmatizatsiya deyiladi. Lug'atni kichraytiradi, lekin grammatik ma'lumot yo'qoladi.
Kodda eng sodda ko'rinishi:
def tokenlash(matn):
"""Eng sodda tokenizator: kichik harf + bo'shliq bo'yicha ajratish."""
return matn.lower().split()
print(tokenlash("Bugun havo juda issiq edi"))
# ['bugun', 'havo', 'juda', 'issiq', 'edi']To'g'riroq variant — muntazam ifoda bilan:
import re
def tokenlash(matn):
"""Tinish belgilarini alohida token qilib ajratadi."""
matn = matn.lower()
# \w+ — harf va raqamlar ketma-ketligi
# [^\w\s] — harf ham, bo'shliq ham bo'lmagan belgi (tinish belgisi)
return re.findall(r"\w+|[^\w\s]", matn)
print(tokenlash("Salom, dunyo!"))
# ['salom', ',', 'dunyo', '!']2-bosqich: one-hot — eng sodda, eng ahmoq#
Tokenlarimiz bor. Endi ularni songa aylantiramiz.
Eng sodda g'oya: lug'atdagi har bir so'zga o'z raqami beriladi. Va so'z shu raqamli
xonasida 1, qolgan hamma joyda 0 bo'lgan uzun qator bilan ifodalanadi.
To'liq ishlaydigan kod:
import numpy as np
def one_hot(gap):
"""Gapdagi har bir so'zni one-hot vektorga aylantiradi."""
sozlar = tokenlash(gap)
lugat = sorted(set(sozlar)) # takrorsiz so'zlar, alifbo tartibida
indeks = {s: i for i, s in enumerate(lugat)} # so'z → raqam
matritsa = np.zeros((len(sozlar), len(lugat)), dtype=int)
for qator, soz in enumerate(sozlar):
matritsa[qator, indeks[soz]] = 1 # o'z joyiga 1 qo'yamiz
return matritsa, lugat
matritsa, lugat = one_hot("choyxonaga boramizmi yoki oshxonaga")
print("Lug'at:", lugat)
print(matritsa)Natija:
Lug'at: ['boramizmi', 'choyxonaga', 'oshxonaga', 'yoki']
[[0 1 0 0] ← choyxonaga
[1 0 0 0] ← boramizmi
[0 0 0 1] ← yoki
[0 0 1 0]] ← oshxonaga
Ishladi. Matn songa aylandi. Lekin ikki jiddiy muammo bor va ikkalasi ham halokatli.
Muammo 1: ma'no butunlay yo'qoladi#
choyxona va oshxona — ikkalasi ham ovqat yeyiladigan joy. Bu ikki so'z bir-biriga
yaqin bo'lishi kerak, to'g'rimi?
Keling, o'lchab ko'ramiz. Ikki vektorning yaqinligini skalyar ko'paytma bilan o'lchaymiz — juft-juft ko'paytirib qo'shamiz:
choyxona = np.array([0, 0, 0, 0, 0, 1, 0, 0, 0])
oshxona = np.array([0, 0, 1, 0, 0, 0, 0, 0, 0])
traktor = np.array([0, 1, 0, 0, 0, 0, 0, 0, 0])
print("choyxona · oshxona =", choyxona @ oshxona) # 0
print("choyxona · traktor =", choyxona @ traktor) # 0Ikkalasi ham nol.
Kompyuter uchun choyxona bilan oshxona orasidagi masofa choyxona bilan traktor
orasidagi masofa bilan bir xil. Hech qanday yaqinlik yo'q.
Muammo 2: hajm — hisoblab ko'ramiz#
Bu muammo yanada aniqroq. Haqiqiy raqamlar bilan hisoblaymiz.
lugat_hajmi = 200_000 # o'rtacha til lug'ati
bayt = 4 # bitta butun son (int32)
bitta_soz = lugat_hajmi * bayt
print(f"Bitta so'z: {bitta_soz / 1024:.0f} KB")
# 100 so'zlik hujjat
hujjat = bitta_soz * 100
print(f"100 so'zlik hujjat: {hujjat / 1024**2:.0f} MB")
# 10 000 ta hujjat
korpus = hujjat * 10_000
print(f"10 000 hujjat: {korpus / 1024**4:.1f} TB")Bitta so'z: 781 KB
100 so'zlik hujjat: 76 MB
10 000 hujjat: 0.7 TB
Bir necha sahifalik matn uchun 700 gigabayt. Va bu sonlarning 99.9995% i — nol.
- Tushunish uchun eng sodda: bitta 1, qolgani 0
- Kichik lug'atlarda (masalan 20 ta kategoriya) hamon ishlatiladi
- Boshqa hamma usullar uni yaxshilash urinishi sifatida paydo bo'lgan
- Zamonaviy modellarda ham chiqishda ishlatiladi — keyingi bobda ko'ramiz
- Ma'no butunlay yo'qoladi — har qanday ikki so'z bir xil darajada uzoq
- Hajm lug'at bilan chiziqli o'sadi — 200 ming so'z = 200 ming o'lcham
- Vektorlarning 99.99% i nol — xotira behuda ketadi
- Lug'atda yo'q so'z bilan nima qilishni bilmaydi
Xulosa: one-hot — bu birinchi qadam, oxirgi emas. U muammoni ko'rsatib beradi: bizga ma'noni saqlaydigan va ixcham vektorlar kerak.
3-bosqich: bag-of-words — chastotani saqlash#
Endi bitta gap emas, butun hujjat bilan ishlaymiz.
G'oyasi nomida: hujjatdagi so'zlarni xaltaga solib silkitamiz. Tartib yo'qoladi, lekin har bir so'z necha marta uchragani saqlanib qoladi.
Nega tartibni tashlaymiz? Chunki ko'p vazifalar uchun u kerak emas. Agar sharhda "ajoyib", "zo'r", "tavsiya qilaman" so'zlari ko'p bo'lsa — bu ijobiy sharh, so'zlar qaysi tartibda turgani muhim emas.
def bag_of_words(hujjatlar):
"""Hujjatlar ro'yxatidan chastota matritsasini quradi."""
tokenlangan = [tokenlash(h) for h in hujjatlar]
# Butun korpus bo'ylab yagona lug'at
hamma_sozlar = [s for hujjat in tokenlangan for s in hujjat]
lugat = sorted(set(hamma_sozlar))
indeks = {s: i for i, s in enumerate(lugat)}
matritsa = np.zeros((len(hujjatlar), len(lugat)), dtype=int)
for qator, hujjat in enumerate(tokenlangan):
for soz in hujjat:
matritsa[qator, indeks[soz]] += 1 # 1 qo'ymaymiz — SANAYMIZ
return lugat, matritsa
korpus = [
"bu film ajoyib ajoyib",
"yomon demayman lekin ajoyib ham emas",
"ajoyib faqat nodon odam shunday deydi",
]
lugat, matritsa = bag_of_words(korpus)
# Chiroyli chiqarish
print(f"{'':>12}", " ".join(f"{s[:6]:>7}" for s in lugat))
for i, qator in enumerate(matritsa):
print(f"{i + 1}-hujjat: ", " ".join(f"{v:>7}" for v in qator)) ajoyib bu deydi demayman emas faqat film ham
1-hujjat: 2 1 0 0 0 0 1 0
2-hujjat: 1 0 0 1 1 0 0 1
3-hujjat: 1 0 1 0 0 1 0 0
One-hot'dan farqi aniq: endi vektor butun hujjatni ifodalaydi va chastota saqlanadi.
4-bosqich: TF-IDF — qaysi so'z hujjatni ajratib turadi#
Bag-of-words'da yana bir muammo bor va u juda amaliy.
Tasavvur qiling, 1000 ta hujjatda qidiryapsiz. va so'zi hamma hujjatda bor — demak u
hujjatlarni ajratib turmaydi. Aksincha, mitoxondriya so'zi faqat uchtasida bor — va
aynan shu uchtasi sizga kerak.
TF-IDF ana shu mantiqni formulaga soladi.
- TF
- Term Frequency — chastotaSo'z shu hujjatda necha marta uchradi. Ko'p uchrasa — hujjat uchun muhimroq. Odatda hujjat uzunligiga bo'linadi, aks holda uzun hujjatlar sun'iy ustunlik oladi.
- IDF
- Inverse Document Frequency — noyoblikSo'z NECHTA hujjatda umuman uchraydi. KAM hujjatda uchrasa — u ajratuvchi so'z, ballari yuqori. Hamma joyda uchrasa — ballari nolga yaqin.
- ×
- Ikkalasini ko'paytiramizYuqori ball olish uchun so'z shu hujjatda ko'p, boshqa hujjatlarda esa kam uchrashi kerak. Aynan shunday so'z hujjatning mavzusini bildiradi.
Raqamlar bilan ko'ramiz#
Formulani yodlashdan ko'ra bir marta qo'lda hisoblab ko'rish foydaliroq. Uchta hujjat:
1-hujjat: "mushuk uyda uxlaydi"
2-hujjat: "it uyda uxlaydi"
3-hujjat: "mitoxondriya hujayrada energiya ishlab chiqaradi"
uyda so'zi uchun 1-hujjatda:
TF = 1 / 3 = 0.333 (3 so'zdan 1 tasi)
DF = 2 (2 ta hujjatda uchraydi)
IDF = ln(3 / (1 + 2)) + 1 = ln(1.0) + 1 = 1.0
TF-IDF = 0.333 × 1.0 = 0.333
mitoxondriya so'zi uchun 3-hujjatda:
TF = 1 / 5 = 0.200
DF = 1 (faqat 1 ta hujjatda)
IDF = ln(3 / (1 + 1)) + 1 = ln(1.5) + 1 = 1.405
TF-IDF = 0.200 × 1.405 = 0.281
Endi solishtiring: uyda ko'proq uchrasa ham (TF yuqori), mitoxondriya noyobligi
uchun ballini deyarli ushlab qoldi. Agar hujjatlar soni ko'paysa, farq keskin ochiladi.
To'liq kod#
import numpy as np
def compute_tf(hujjatlar, lugat, indeks):
"""Har bir hujjatda har bir so'zning nisbiy chastotasi."""
tf = np.zeros((len(hujjatlar), len(lugat)), dtype=np.float32)
for i, hujjat in enumerate(hujjatlar):
sozlar = tokenlash(hujjat)
for soz in sozlar:
tf[i, indeks[soz]] += 1 / len(sozlar) # hujjat uzunligiga bo'lamiz
return tf
def compute_idf(hujjatlar, lugat, indeks):
"""Har bir so'zning noyoblik koeffitsienti."""
n = len(hujjatlar)
idf = np.zeros(len(lugat), dtype=np.float32)
for soz in lugat:
df = sum(1 for h in hujjatlar if soz in tokenlash(h))
idf[indeks[soz]] = np.log(n / (1 + df)) + 1
return idf
def tf_idf(hujjatlar):
tokenlangan = [tokenlash(h) for h in hujjatlar]
lugat = sorted(set(s for h in tokenlangan for s in h))
indeks = {s: i for i, s in enumerate(lugat)}
tf = compute_tf(hujjatlar, lugat, indeks)
idf = compute_idf(hujjatlar, lugat, indeks)
return lugat, tf * idf
korpus = [
"mushuk uyda uxlaydi",
"it uyda uxlaydi",
"mitoxondriya hujayrada energiya ishlab chiqaradi",
]
lugat, matritsa = tf_idf(korpus)
# Har bir hujjat uchun eng muhim 2 so'zni chiqaramiz
for i, qator in enumerate(matritsa):
eng_muhim = np.argsort(qator)[::-1][:2]
sozlar = [f"{lugat[j]} ({qator[j]:.3f})" for j in eng_muhim if qator[j] > 0]
print(f"{i + 1}-hujjat →", ", ".join(sozlar))1-hujjat → mushuk (0.469), uxlaydi (0.333)
2-hujjat → it (0.469), uxlaydi (0.333)
3-hujjat → mitoxondriya (0.281), hujayrada (0.281)
Diqqat qiling: TF-IDF har bir hujjat uchun uni boshqalardan ajratib turadigan so'zni
topdi. uyda so'zi (ikki hujjatda bor) yuqoriga chiqmadi.
Amaliyotda: scikit-learn#
Noldan yozish tushunish uchun. Ishda tayyor vositadan foydalaniladi:
from sklearn.feature_extraction.text import TfidfVectorizer
vectorizer = TfidfVectorizer(
lowercase=True,
token_pattern=r"\w+", # tinish belgilarini tashlaydi
max_features=5000, # eng ko'p uchraydigan 5000 so'z bilan cheklaymiz
min_df=2, # kamida 2 hujjatda uchrasin (xato yozilgan so'zlarni kesadi)
max_df=0.8, # 80% dan ko'p hujjatda uchrasa — tashlaymiz (to'xtash so'zlari)
)
matritsa = vectorizer.fit_transform(korpus)
print(matritsa.shape) # (3, 11)
print(vectorizer.get_feature_names_out()) # lug'atHujjat qidiruv tizimi quring
10-15 ta qisqa matn oling (yangiliklar sarlavhalari, kitob tavsiflari — nima bo'lsa ham). Foydalanuvchi so'rov yozganda unga eng mos 3 ta hujjatni qaytaradigan funksiya yozing.
Talablar:
TfidfVectorizerishlatilsin- O'xshashlik kosinus bo'yicha o'lchansin
- Natijada hujjat matni va o'xshashlik bali chiqsin
Bu — eng sodda qidiruv tizimi. 5-bobda aynan shu g'oya RAG'ning asosi bo'ladi.
Maslahatni ko'rsatish
Yechim yo'li: 1. TfidfVectorizer bilan barcha hujjatlarni fit_transform qiling 2. So'rovni transform qiling (fit_transform EMAS! Aks holda lug'at o'zgaradi) 3. sklearn.metrics.pairwise.cosine_similarity bilan o'xshashlikni hisoblang 4. np.argsort(...)[::-1][:3] bilan eng yuqori 3 tasini oling Asosiy tuzoq: so'rov uchun transform() ishlating, fit_transform() emas. Aks holda so'rov o'z lug'atini yaratadi va o'lchamlar mos kelmaydi.
5-bosqich: embedding — hamma narsa o'zgaradi#
Bu bobning eng muhim qismi. Diqqat bilan o'qing.
Muammoning mohiyati#
Yuqoridagi uch usulning hammasida bitta umumiy kamchilik bor:
Xarita tashbehi#
Tasavvur qiling, xarita bor. Unda Toshkent va Samarqand yaqin, Toshkent va Tokio uzoq. Har bir shaharning koordinatasi bor va koordinatalar orasidagi masofa haqiqiy yaqinlikni bildiradi.
Embedding — bu so'zlar uchun xuddi shunday xarita. Faqat ikki o'lchamli emas, 300 o'lchamli.
choyxona = [ 0.21 -0.44 0.87 0.02 ... 0.13 ]
oshxona = [ 0.19 -0.41 0.83 0.05 ... 0.15 ] ← juda yaqin
traktor = [-0.62 0.11 -0.29 0.77 ... -0.44 ] ← uzoq
Farqni sezing:
one-hot: 200 000 son, ma'no yo'q
embedding: 300 son, to'liq ma'no
666 barobar kichik va cheksiz aqlliroq.
Word2vec bu xaritani qanday chizadi#
G'oya 2013-yilda Tomas Mikolov tomonidan taklif qilingan. Uning mohiyati hayratlanarli darajada oddiy:
So'zning ma'nosi uning qo'shnilari bilan belgilanadi.
Bu distributsion gipoteza deb ataladi. Uni tekshirish oson. Quyidagi gapda zvarch
so'zi nimani anglatishini bilmaysiz:
"Kechqurun issiq zvarch ichib, uyquga ketdim."
Lekin taxmin qila olasiz: bu ichiladigan, issiq bo'ladigan, kechqurun iste'mol qilinadigan narsa. Choy? Sut? Sho'rva? Qo'shnilar sizga ma'noni aytib berdi.
Word2vec aynan shuni qiladi — faqat millionlab marta.
Jarayon to'rt qadamda:
Oynani matn bo'ylab siljitamiz
Markazda bitta so'z
w, atrofidawindowta qo'shni. Oyna bir so'zga siljiydi va jarayon takrorlanadi.Ijobiy va salbiy misollar yig'amiz
Oyna ichidagi so'zlar — ijobiy misol ("ha, bu so'z shu kontekstda uchraydi"). Lug'atdan tasodifiy olingan so'zlar — salbiy misol ("yo'q, bu so'z bu yerda uchramaydi").
Bu negative sampling deyiladi va aynan shu narsa word2vec'ni tez qiladi.
Neyron tarmoqni o'qitamiz
Vazifa juda oddiy — ikkilik tasniflash: "bu ikki so'z bir kontekstdami?" Ha yoki yo'q.
Tarmoqning javobini TASHLAB YUBORAMIZ
Va mana eng chiroyli qismi.
Bizga tarmoqning javobi kerak emas. Bizga uning ichki og'irliklari kerak. O'qish davomida tarmoq ichida har bir so'z uchun shakllangan sonlar qatori — bu bizning embedding'imiz.
Ya'ni: soxta vazifa qo'yib, uning yon mahsulotini olamiz. Bu mashina o'rganishdagi eng nafis hiylalardan biri.
O'z embeddingingizni o'qitish#
Nazariya yetarli. Kod yozamiz.
from gensim.models import Word2Vec
# Matn — jumlalar ro'yxati, har biri tokenlar ro'yxati
jumlalar = [
tokenlash(matn) for matn in korpus
]
model = Word2Vec(
sentences=jumlalar,
sg=1, # 1 = skip-gram, 0 = CBOW
vector_size=100, # har bir so'z uchun 100 ta son
window=5, # chapga 5, o'ngga 5 so'z
min_count=2, # 2 martadan kam uchragan so'zni tashlaymiz
negative=5, # har bir ijobiy misolga 5 ta salbiy
workers=4, # protsessor yadrolari
epochs=10, # korpusni necha marta ko'rib chiqamiz
)
model.save("word2vec.model")
print("Lug'at hajmi:", len(model.wv))Parametrlarni tanlash bo'yicha amaliy qo'llanma:
| Parametr | Kichik qiymat | Katta qiymat | Tavsiya |
|---|---|---|---|
vector_size | Ma'no sig'maydi | Ortiqcha, sekin | 100–300 |
window | So'z turkumini yaxshi ajratadi | Mavzuviy yaqinlikni topadi | 5 |
min_count | Lug'at shovqinga to'ladi | Kam uchraydigan so'zlar yo'qoladi | 2–5 |
epochs | Yetarli o'rganmaydi | Ortiqcha moslashadi | 5–20 |
Embedding bilan nima qilish mumkin#
O'qitilgan modelda uchta asosiy amal bor:
# 1. Eng o'xshash so'zlarni topish
print(model.wv.most_similar("choy", topn=5))
# [('qahva', 0.82), ('piyola', 0.79), ('ichmoq', 0.74), ...]
# 2. Ikki so'zning yaqinligini o'lchash
print(model.wv.similarity("choy", "qahva")) # 0.82
print(model.wv.similarity("choy", "traktor")) # 0.11
# 3. Ortiqchasini topish
print(model.wv.doesnt_match(["choy", "qahva", "sut", "traktor"]))
# 'traktor'Sehrli qism: ma'no arifmetikasi#
Ma'no koordinataga aylangach, u bilan arifmetika qilish mumkin bo'lib qoladi:
podshoh − erkak + ayol ≈ malika
natija = model.wv.most_similar(
positive=["podshoh", "ayol"],
negative=["erkak"],
topn=3,
)
print(natija)Bu shunchaki qiziqarli fokus emas. Bu embedding ma'noning ichki tuzilishini o'zlashtirganini isbotlaydi. Model hech kim aytmasdan turib "hukmdorlik" va "jins" degan tushunchalarni alohida o'lchamlarga ajratib olgan.
O'z embeddingingizni o'qiting va tekshiring
Kamida 10 MB o'zbekcha yoki inglizcha matn toping va unda word2vec o'qiting.
Keyin tekshiring:
most_similar5 ta so'z uchun mantiqiy natija beryaptimi?- Analogiya ishlaydimi? (
erkak → ayolkabi juftliklar bilan sinang) - 10-15 ta so'zni PCA bilan ikki o'lchamga tushirib chizing — o'xshash so'zlar guruhlanyaptimi?
Agar natija yomon bo'lsa, vector_size, window va epochs ni o'zgartirib ko'ring va
farqni yozib boring.
Maslahatni ko'rsatish
Ma'lumot manbalari: - Vikipediya dump: dumps.wikimedia.org (uzwiki) - gensim ichidagi text8 korpusi (inglizcha, tez sinash uchun) - O'z Telegram eksportingiz (JSON dan matnni chiqarib oling) Agar natija yomon chiqsa — bu deyarli har doim MA'LUMOT KAMLIGI. Kamida 10 MB matn kerak. Kamroq bo'lsa fasttext'dan tayyorini yuklab oling. Vizualizatsiya uchun: from sklearn.decomposition import PCA import matplotlib.pyplot as plt sozlar = ["choy", "qahva", "sut", "mashina", "avtobus", "poyezd"] vektorlar = [model.wv[s] for s in sozlar] koord = PCA(n_components=2).fit_transform(vektorlar) plt.scatter(koord[:, 0], koord[:, 1]) for i, s in enumerate(sozlar): plt.annotate(s, koord[i]) plt.show()
6-bosqich: o'xshashlikni o'lchash#
Xarita bor. Endi undagi masofani o'lchash kerak. Bu — butun kitobdagi eng ko'p ishlatiladigan amal.
Skalyar ko'paytmadan boshlaymiz#
Ikki vektorni juft-juft ko'paytirib qo'shamiz:
a = np.array([1, 2, 3])
b = np.array([4, 5, 6])
print(a @ b) # 1*4 + 2*5 + 3*6 = 32
print(np.dot(a, b)) # xuddi shuUch muammosi bor:
- 1
- Uzun vektorlarga ustunlik beradiVektorning uzunligi (magnitudasi) katta bo'lsa, ko'paytma ham katta chiqadi — ma'no yaqin bo'lmasa ham.
- 2
- Katta qiymatlarga ustunlik beradiKatta qiymatlar odatda ko'p uchraydigan (va shuning uchun kam ma'noli) so'zlarga tegishli bo'ladi.
- 3
- Chegarasi yo'qNatija 32 chiqdi — bu ko'pmi yoki ozmi? Bilib bo'lmaydi, chunki taqqoslash uchun shkala yo'q.
Kosinus o'xshashligi — yechim#
Skalyar ko'paytmani vektorlar uzunligiga bo'lamiz. Natijada faqat yo'nalish qoladi, uzunlik hisobga olinmaydi.
cos(a, b) = (a · b) / (|a| × |b|)
def kosinus(a, b):
"""Ikki vektor orasidagi kosinus o'xshashligi: -1 dan 1 gacha."""
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b))Natijani talqin qilish oson:
| Qiymat | Ma'nosi |
|---|---|
1.0 | Bir xil yo'nalish — ma'no bir xil |
0.7 – 0.9 | Juda yaqin — sinonim yoki bir mavzu |
0.3 – 0.5 | Uzoqdan bog'liq |
0.0 | Aloqasiz |
−1.0 | Qarama-qarshi yo'nalish |
Amalda sklearn ishlatiladi — u matritsalar bilan tez ishlaydi:
from sklearn.metrics.pairwise import cosine_similarity
# Bir vektorni ko'plari bilan solishtirish
sorov = np.array([[0.2, 0.5, -0.1]])
baza = np.array([
[0.3, 0.4, -0.2],
[-0.8, 0.1, 0.6],
[0.15, 0.55, -0.05],
])
ballar = cosine_similarity(sorov, baza)[0]
print(ballar) # [0.94 -0.31 0.99]
print("Eng mos:", np.argmax(ballar)) # 27-bosqich: ketma-ketlikni tushunadigan modellar#
Raqamlarimiz bor. Endi ular bilan ishlaydigan model kerak.
Lekin matn — shunchaki so'zlar to'plami emas. U ketma-ketlik:
"It odamni qopdi" ← oddiy holat
"Odam itni qopdi" ← yangilik!
Bir xil so'zlar, butunlay boshqa ma'no. Bag-of-words bu ikkisini ajrata olmaydi — xaltada bir xil so'zlar.
Oddiy neyron tarmoqning muammosi shundaki, uning xotirasi yo'q — har bir kirishni mustaqil deb hisoblaydi.
RNN — xotirasi bor tarmoq#
RNN so'zlarni birin-ketin o'qiydi va har qadamda yashirin holat saqlaydi.
Har qadamda bir xil amal bajariladi (shuning uchun rekurrent — takrorlanuvchi):
h(t) = tanh( W · x(t) + U · h(t-1) + b )
└── yangi so'z ──┘ └─ eski xotira ─┘
PyTorch'da sinab ko'ramiz:
import torch
import torch.nn as nn
# 10 ta misol, har biri 5 ta so'z, har so'z 3 o'lchamli vektor
data = torch.rand(10, 5, 3)
rnn = nn.RNN(input_size=3, hidden_size=16, num_layers=1, batch_first=True)
chiqish, yashirin = rnn(data)
print("chiqish:", chiqish.shape) # torch.Size([10, 5, 16]) — har qadam uchun
print("yashirin:", yashirin.shape) # torch.Size([1, 10, 16]) — faqat oxirgi holatRNN nima uchun yetarli emas#
Uch muammosi bor va ular jiddiy.
1. Unutadi. Bir necha qadamdan keyin boshlang'ich so'zlar xotiradan yo'qoladi.
2. Gradient so'nadi. Bu eng jiddiy muammo. Raqamlar bilan ko'ramiz:
# Har qadamda gradient 0.5 ga ko'payadi deb faraz qilaylik
gradient = 1.0
for qadam in range(1, 21):
gradient *= 0.5
if qadam in (1, 5, 10, 20):
print(f"{qadam:>2} qadam orqaga: gradient = {gradient:.10f}") 1 qadam orqaga: gradient = 0.5000000000
5 qadam orqaga: gradient = 0.0312500000
10 qadam orqaga: gradient = 0.0009765625
20 qadam orqaga: gradient = 0.0000009537
20 qadamdan keyin gradient deyarli nol. Ya'ni gap boshidagi so'zlar model og'irliklariga umuman ta'sir qilmaydi. Model uzoq bog'lanishlarni o'rgana olmaydi.
3. Parallellashmaydi. 5-so'zni hisoblash uchun 4-so'z tayyor bo'lishi shart. GPU'da minglab yadro bo'lsa ham, ular bo'sh turadi.
LSTM — eshiklari bor xotira#
LSTM birinchi ikki muammoni hal qiladi: modelga nimani eslash, nimani unutish kerakligini o'rgatadi.
Buni shunday tasavvur qiling: sizda daftar bor va har yangi so'zni o'qiganda uch qaror qabul qilasiz.
- f
- Unutish eshigi (forget gate)«Daftardagi nimani o'chiraman?» Eski, endi kerak bo'lmagan ma'lumotni tashlaydi. Masalan yangi gap boshlanganda oldingi gapning mavzusi kerak emas.
- i
- Kirish eshigi (input gate)«Yangi nimani yozaman?» Hozirgi so'zdan qaysi ma'lumot uzoq muddatli xotiraga tushishi kerakligini hal qiladi.
- o
- Chiqish eshigi (output gate)«Hozir nimani ishlataman?» Xotiradagi ma'lumotning qaysi qismi shu qadamdagi javob uchun kerakligini tanlaydi.
Eshiklar sigmoid funksiya orqali 0 dan 1 gacha qiymat oladi:
0.0 → butunlay to'sadi
0.5 → yarmini o'tkazadi
1.0 → butunlay o'tkazadi
Va bu qiymatlar o'qitish davomida o'rganiladi — hech kim qo'lda belgilamaydi.
LSTM'ning kaliti — ikkita xotira:
h → qisqa muddatli holat (hozirgi javob uchun)
c → uzoq muddatli kontekst (butun gap bo'ylab saqlanadi)
c deyarli o'zgarmasdan oldinga uzatiladi — faqat eshiklar orqali ozgina tahrirlanadi.
Aynan shu gradient so'nishini oldini oladi: signal c orqali uzoq masofaga
buzilmasdan yetib boradi.
lstm = nn.LSTM(input_size=3, hidden_size=16, num_layers=1, batch_first=True)
chiqish, (h, c) = lstm(data)
print("chiqish:", chiqish.shape) # [10, 5, 16]
print("h (qisqa):", h.shape) # [1, 10, 16]
print("c (uzoq):", c.shape) # [1, 10, 16]GRU — yengil variant#
GRU xuddi shu g'oya, lekin uchta eshik o'rniga ikkita: yangilash va qayta o'rnatish.
gru = nn.GRU(input_size=3, hidden_size=16, num_layers=1, batch_first=True)
chiqish, yashirin = gru(data)- Kamroq parametr → tezroq o'qiydi
- Kamroq xotira talab qiladi
- Ko'p vazifalarda LSTM bilan bir xil natija
- Kichik ma'lumotda ortiqcha moslashish xavfi kamroq
- Ko'proq parametr → sekinroq
- Juda uzoq bog'lanishlarda biroz ustunroq
- Murakkab naqshlarni yaxshiroq o'rganadi
- Ko'proq ma'lumot talab qiladi
Amaliy tavsiya: GRU'dan boshlang. Agar natija yetarli bo'lmasa va sizda ko'p ma'lumot bo'lsa — LSTM'ga o'ting. Farq odatda 1-2 foizdan oshmaydi.
CNN — naqsh qidiruvchi oyna#
CNN aslida rasmlar uchun o'ylab topilgan, lekin matnda ham ishlaydi. Oyna matn ustidan siljiydi va mahalliy naqshlarni qidiradi:
[bugun havo juda issiq edi]
└───oyna───┘ ← "bugun havo juda" — naqsh bormi?
└───oyna───┘ ← "havo juda issiq" — naqsh bormi?
└───oyna───┘ ← "juda issiq edi" — naqsh bormi?
# Diqqat: Conv1d kanal tartibini boshqacha kutadi
data_conv = data.transpose(1, 2) # [10, 5, 3] → [10, 3, 5]
conv = nn.Conv1d(in_channels=3, out_channels=16, kernel_size=3, padding=1)
chiqish = conv(data_conv)
print(chiqish.shape) # [10, 16, 5]CNN RNN'dan ancha tez — oynalar parallel ishlaydi. Lekin u faqat mahalliy naqshlarni ko'radi: 3-5 so'zlik iboralarni yaxshi topadi, gap boshi bilan gap oxiri orasidagi bog'liqlikni ko'rmaydi.
Yakuniy loyiha: kayfiyat tasniflagichi#
Endi hamma narsani birlashtiramiz va ishlaydigan mahsulot quramiz: sharhni ijobiy yoki salbiyga ajratadigan model.
Ma'lumotni tayyorlash
import numpy as np import pandas as pd from sklearn.model_selection import train_test_split df = pd.read_csv("sharhlar.csv") # ustunlar: review, sentiment # Yorliqni raqamga: ijobiy=1, salbiy=0 df["belgi"] = (df["sentiment"] == "positive").astype(int) X = df["review"].values y = df["belgi"].values # Uchga bo'lamiz X_train, X_temp, y_train, y_temp = train_test_split( X, y, test_size=0.3, stratify=y, random_state=42 ) X_val, X_test, y_val, y_test = train_test_split( X_temp, y_temp, test_size=0.5, stratify=y_temp, random_state=42 ) print(f"o'qitish: {len(X_train)} tekshirish: {len(X_val)} sinov: {len(X_test)}")stratify=y— muhim detal. U ijobiy va salbiy sharhlar har uch to'plamda ham teng nisbatda bo'lishini ta'minlaydi. Busiz sinov to'plamiga tasodifan faqat ijobiy sharhlar tushib qolishi va natija noto'g'ri chiqishi mumkin.random_state=42— bo'linish har safar bir xil bo'lishi uchun. Tajribalarni takrorlanadigan qilish uchun zarur.Lug'at qurish
from collections import Counter def lugat_qur(matnlar, max_hajm=10_000): """Eng ko'p uchraydigan so'zlardan lug'at quradi.""" sanoq = Counter() for matn in matnlar: sanoq.update(tokenlash(matn)) # 0 va 1 xizmat tokenlari uchun band lugat = {"<pad>": 0, "<unk>": 1} for soz, _ in sanoq.most_common(max_hajm - 2): lugat[soz] = len(lugat) return lugat lugat = lugat_qur(X_train) # DIQQAT: faqat o'qitish to'plamida! print("Lug'at hajmi:", len(lugat))Ikkita maxsus token nima uchun kerak:
<pad>— qisqa sharhlarni to'ldirish uchun<unk>— lug'atda yo'q so'zlar uchun (sinov to'plamida albatta uchraydi)
Matnni raqamlarga aylantirish
def kodlash(matn, lugat, max_uzunlik=200): """Matnni sonlar ro'yxatiga aylantiradi va uzunlikni tenglashtiradi.""" tokenlar = tokenlash(matn) raqamlar = [lugat.get(t, lugat["<unk>"]) for t in tokenlar] if len(raqamlar) > max_uzunlik: raqamlar = raqamlar[:max_uzunlik] # kesamiz else: raqamlar += [lugat["<pad>"]] * (max_uzunlik - len(raqamlar)) # to'ldiramiz return raqamlar X_train_kod = np.array([kodlash(m, lugat) for m in X_train]) X_val_kod = np.array([kodlash(m, lugat) for m in X_val]) X_test_kod = np.array([kodlash(m, lugat) for m in X_test]) print(X_train_kod.shape) # (N, 200)max_uzunlikni qanday tanlash kerak? Ma'lumotingizga qarang:uzunliklar = [len(tokenlash(m)) for m in X_train] print("o'rtacha:", np.mean(uzunliklar)) print("90-protsentil:", np.percentile(uzunliklar, 90))90-protsentil yaxshi tanlov: sharhlarning 90% i to'liq sig'adi, faqat eng uzunlari kesiladi.
Modelni yozish
import torch import torch.nn as nn class KayfiyatModel(nn.Module): def __init__(self, lugat_hajmi, embed_olcham=128, yashirin=128, qatlamlar=2, dropout=0.4): super().__init__() # So'z raqamini vektorga aylantiradi. padding_idx=0 → <pad> o'rganilmaydi self.embedding = nn.Embedding(lugat_hajmi, embed_olcham, padding_idx=0) self.gru = nn.GRU( embed_olcham, yashirin, num_layers=qatlamlar, batch_first=True, dropout=dropout if qatlamlar > 1 else 0, bidirectional=True, # ikki tomondan o'qiydi ) self.dropout = nn.Dropout(dropout) self.chiqish = nn.Linear(yashirin * 2, 1) # *2 — bidirectional def forward(self, x): vektorlar = self.embedding(x) # [paket, uzunlik, embed] _, yashirin = self.gru(vektorlar) # yashirin: [qatlam*2, paket, yashirin] # Oxirgi qatlamning ikki yo'nalishini birlashtiramiz oxirgi = torch.cat((yashirin[-2], yashirin[-1]), dim=1) return self.chiqish(self.dropout(oxirgi)).squeeze(1)Uch qaror izohga muhtoj:
padding_idx=0—<pad>tokeni uchun gradient hisoblanmaydi. Busiz model bo'sh joyni "o'rganishga" urinadi va bu sifatni pasaytiradi.bidirectional=True— model matnni chapdan o'ngga va o'ngdan chapga o'qiydi. Kayfiyat tahlilida bu sezilarli foyda beradi, chunki inkor ko'pincha oxirida keladi: "yaxshi film emas edi".dropout=0.4— o'qitish paytida neyronlarning 40% i tasodifiy o'chiriladi. Bu modelni yodlab olishdan (overfitting) saqlaydi.
O'qitish sikli
from torch.utils.data import TensorDataset, DataLoader qurilma = torch.device("cuda" if torch.cuda.is_available() else "cpu") def loader_yasa(X, y, paket=64, aralashtir=False): ds = TensorDataset(torch.tensor(X, dtype=torch.long), torch.tensor(y, dtype=torch.float32)) return DataLoader(ds, batch_size=paket, shuffle=aralashtir) train_loader = loader_yasa(X_train_kod, y_train, aralashtir=True) val_loader = loader_yasa(X_val_kod, y_val) model = KayfiyatModel(len(lugat)).to(qurilma) criterion = nn.BCEWithLogitsLoss() # ikkita sinf uchun optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) eng_yaxshi = 0.0 for davr in range(1, 11): # --- o'qitish --- model.train() for X_paket, y_paket in train_loader: X_paket, y_paket = X_paket.to(qurilma), y_paket.to(qurilma) optimizer.zero_grad() # eski gradientni tozalaymiz bashorat = model(X_paket) xato = criterion(bashorat, y_paket) xato.backward() # Gradient portlashining oldini olamiz — RNN oilasida zarur nn.utils.clip_grad_norm_(model.parameters(), max_norm=5.0) optimizer.step() # --- tekshirish --- model.eval() togri, jami = 0, 0 with torch.no_grad(): # gradient hisoblanmaydi — tezroq for X_paket, y_paket in val_loader: X_paket, y_paket = X_paket.to(qurilma), y_paket.to(qurilma) bashorat = torch.sigmoid(model(X_paket)) > 0.5 togri += (bashorat == y_paket.bool()).sum().item() jami += len(y_paket) aniqlik = togri / jami print(f"{davr}-davr aniqlik: {aniqlik:.4f}") if aniqlik > eng_yaxshi: eng_yaxshi = aniqlik torch.save(model.state_dict(), "eng_yaxshi.pt") # eng yaxshisini saqlaymizIshlatish
model.load_state_dict(torch.load("eng_yaxshi.pt")) model.eval() def bashorat_qil(matn): kodlangan = torch.tensor([kodlash(matn, lugat)], dtype=torch.long).to(qurilma) with torch.no_grad(): ehtimollik = torch.sigmoid(model(kodlangan)).item() yorliq = "ijobiy" if ehtimollik > 0.5 else "salbiy" ishonch = ehtimollik if ehtimollik > 0.5 else 1 - ehtimollik return yorliq, ishonch for sharh in ["Ajoyib film, hammaga tavsiya qilaman", "Vaqtimni behuda sarfladim", "Yomon emas, lekin kutganimdek ham emas"]: yorliq, ishonch = bashorat_qil(sharh) print(f"{yorliq:>7} ({ishonch:.0%}) ← {sharh}")ijobiy (94%) ← Ajoyib film, hammaga tavsiya qilaman salbiy (89%) ← Vaqtimni behuda sarfladim salbiy (58%) ← Yomon emas, lekin kutganimdek ham emasUchinchi misolga e'tibor bering — model ikkilanyapti (58%). Va bu to'g'ri, chunki sharhning o'zi ham noaniq.
Ishlamasa nima qilish kerak#
Amaliyotda deyarli har doim biror narsa noto'g'ri ketadi. Eng ko'p uchraydiganlari:
| Belgi | Sabab | Yechim |
|---|---|---|
| Aniqlik 50% da qotib qoldi | lr juda katta yoki juda kichik | 1e-3 dan boshlang, keyin 10 barobar o'zgartirib sinang |
| O'qitishda 99%, tekshirishda 65% | Ortiqcha moslashish | dropout ni oshiring, max_features ni kamaytiring, ma'lumot qo'shing |
CUDA out of memory | Paket juda katta | batch_size ni ikki barobar kamaytiring |
Xato nan bo'lib ketdi | Gradient portlashi | clip_grad_norm_ qo'shing (yuqorida bor) |
| O'lchamlar mos kelmadi | batch_first unutilgan | nn.GRU(..., batch_first=True) |
| Juda sekin o'qiyapti | Protsessorda ishlayapti | Colab'da bepul GPU oling |
Modelni yaxshilang va farqni o'lchang
Yuqoridagi modelni oling va quyidagi o'zgarishlarni birma-bir sinab, har birining tekshirish aniqligiga ta'sirini yozib boring:
bidirectional=Falseqiling — qancha tushdi?yashirino'lchamini 256 ga oshiring- GRU o'rniga LSTM ishlating
dropoutni 0.2 va 0.6 qilib ko'ring- Tayyor word2vec embeddingini yuklang va
requires_grad=Falsebilan muzlating
Qaysi o'zgarish eng ko'p foyda berdi? Nima uchun deb o'ylaysiz?
Maslahatni ko'rsatish
Har bir o'zgarishni ALOHIDA sinang, hammasini birdan emas. Aks holda qaysi biri yordam berganini bilmaysiz. Natijalarni jadvalda yozib boring: variant val aniqlik ------------------------------------ asosiy (GRU, 128) 0.8642 + bidirectional=False 0.8401 + hidden=256 0.8703 + dropout=0.5 0.8688 ... Tayyor embedding uchun: from gensim.models import KeyedVectors w2v = KeyedVectors.load_word2vec_format("cc.uz.300.vec") # Embedding matritsasini to'ldiring matritsa = np.zeros((len(lugat), 300)) for soz, i in lugat.items(): if soz in w2v: matritsa[i] = w2v[soz] model.embedding.weight.data.copy_(torch.tensor(matritsa)) model.embedding.weight.requires_grad = False # muzlatamiz
Xulosa#
Biz matndan songa o'tish yo'lini to'liq bosib o'tdik:
- 01Embedding — ma'noni koordinataga aylantirish. Bu zamonaviy AI'ning poydevori: RAG ham, bilim grafikasi ham, multimodal modellar ham aynan shunga tayanadi.
- 02Kosinus o'xshashligi — ikki ma'no orasidagi masofa. Kitobda oltita turli joyda qaytariladigan yagona formula. Yodda saqlang.
- 03So'z tartibi ma'noni o'zgartiradi. Bag-of-words buni ko'rmaydi; RNN oilasi ko'radi, lekin sekin va unutuvchan.
- 04Gradient so'nishi — RNN'ning halokatli kamchiligi. 20 qadamdan keyin signal deyarli nolga aylanadi.
- 05Parallellashmaslik — eng qimmat cheklov. Aynan shu narsa keyingi bobdagi transformer'ni tug'dirdi.
Keyingi bobda ko'ramizki, 2017-yilda Google tadqiqotchilari RNN'ni yaxshilashga urinmadi. Ular uni butunlay olib tashladi — va bugungi ChatGPT shundan boshlandi.