Mundarija (21)
- 1. Kirish va motivatsiya
- 2. Nazariya — chuqur tushuntirish
- 2.1. Loyiha xaritasi
- 2.2. Ma'lumot va quvur
- 2.3. Model va o'rgatish
- 2.4. Taqqoslash va qaror
- 2.5. Test, toifa bo'yicha recall va xato tahlili
- 2.6. Topshirish paketi va hisobot
- 2.7. Tuzoqlar
- 3. Tez ma'lumotnoma
- 4. Batafsil misollar
- Misol 1 — Konfig, ma'lumot, bo'lish va tekshiruvlar
- Misol 2 — Bazaviylar, EmbeddingBag va Trainer
- Misol 3 — GroupKFold da juftlashgan taqqoslash va qaror
- Misol 4 — Test, xato tahlili, paket va hisobot
- 5. To'g'ri va noto'g'ri tushunishlar
- 6. Keng tarqalgan xatolar va yechimlari
- 7. Integratsiya — bu bilim qayerda kerak bo'ladi
- 8. Eng yaxshi amaliyotlar
- 9. Amaliy topshiriq
- Xulosa
23.14-dars: Amaliyot — bank murojaatlarini toifalash loyihasi
23-QISM — NLP VA KETMA-KETLIKLAR · 14-dars
1. Kirish va motivatsiya
23-qismda matn bilan ishlashning butun zanjirini ko'rdik: matn ma'lumoti va o'zbek apostrofi, tokenizatsiya va BPE, so'z xaltasi va TF-IDF, klassik klassifikatorlar, embeddinglar, RNN, LSTM va GRU, til modeli, seq2seq va diqqat, sentiment tahlili va NLP baholash tuzoqlari. Endi ularni bitta ishlaydigan loyihaga yig'amiz.
Vazifa — bank mijozlarining yozma murojaatlarini besh toifaga ajratish: karta (bloklanish, PIN kod), o'tkazma (pul kelmadi, rekvizit), kredit (foiz, grafik), ilova (kira olmayapman, SMS kod) va firibgarlik (bilmagan holda pul yechildi, kod so'rashdi). Toifalar nomutanosib: firibgarlik murojaatlari atigi 8 foiz atrofida — lekin aynan ular eng qimmat: har soat kechikish mijozning yo'qotgan puliga teng.
Ma'lumot real hayotdagidek iflos: apostrof to'rt xil yoziladi yoki umuman tushib qoladi ("otkazdim"), imlo xatolari ko'p, bir murojaatda ikki toifaning iborasi uchraydi ("kartamdan pul yechildi" — karta yoki firibgarlik?), bitta mijoz bir necha murojaat yozadi va ba'zan aynan bir matnni qayta yuboradi. 23.13-darsdan bilamizki, bu oxirgi holat tasodifiy bo'lishda bahoni sun'iy oshiradi.
Loyiha 21.12 va 22.14-darslardagi skeletni saqlaydi: konfig, testni qulflash, tez tekshiruvlar, Trainer va eng yaxshi holat, bir necha seed, juftlashgan taqqoslash, testni bir marta ochish va weights_only=True bilan yuklanadigan paket. Matnga xos qo'shimchalar: normallashtirish, o'quvda quriladigan lug'at, mijoz bo'yicha bo'lish va xato tahlili.
Real vaziyat. Bank qo'llab-quvvatlash xizmati murojaatlarni avtomatik yo'naltirish uchun LSTM modelini joriy qildi — "eng zamonaviy yechim" sifatida. Uch oydan keyin audit ikki narsani topdi. Birinchisi: model firibgarlik murojaatlarining uchdan birini "karta" navbatiga yuborgan, u yerda ular o'rtacha 6 soat kutgan. Ikkinchisi: xuddi shu ma'lumotda belgi n-gramli oddiy chiziqli model LSTM dan yomon emas edi, lekin o'n barobar tez ishlardi va uni tushuntirish oson edi. Hech kim ikkala modelni bir xil foldlarda solishtirmagan va toifa bo'yicha recall ni hisobotga kiritmagan edi.
Bu darsda to'liq NLP loyihasini quramiz: xom matndan topshiriladigan paketgacha.
Bu darsda:
- Normallashtirish, mijoz bo'yicha bo'lish va o'quvda quriladigan lug'at
- Bazaviylar: so'z TF-IDF va belgi n-gram TF-IDF
EmbeddingBag+Trainer+ eng yaxshi holat (copy.deepcopy)GroupKFoldda juftlashgan taqqoslash va natijadan hisoblangan qaror- Test bir marta: toifa bo'yicha recall va xato tahlili
weights_only=Truepaket vaBashoratchi- Tuzoqlar
ℹ Misollar real numpy/sklearn/torch bilan (Python 3.14).
2. Nazariya — chuqur tushuntirish
2.1. Loyiha xaritasi
Konfig (frozen dataclass)
|
v
Ma'lumot: matn, toifa, mijoz (sintetik, seed bilan)
|
v
Normallashtirish: kichik harf, apostrof variantlari -> ', tinish yo'q 23.1-bob
|
v
Mijoz bo'yicha bo'lish (GroupShuffleSplit) -> TEST QULFLANADI 23.13-bob
|
v
Lug'at / vektorizator FAQAT o'quvdan 23.2-bob
|
v
Tez tekshiruvlar + bitta batch testi 21.9-bob
|
v
Nomzodlar: ko'pchilik < so'z TF-IDF < belgi TF-IDF < EmbeddingBag
|
v
GroupKFold(5) juftlashgan farq -> eng sodda munosib model (18-qism)
|
v
Test BIR MARTA -> toifa bo'yicha recall -> xato tahlili
|
v
Paket (weights_only) -> Bashoratchi -> hisobot22.14-darsdagi xarita bilan solishtirsak, tuzilma yana deyarli o'zgarmadi — rasm o'rnida matn, mean/std o'rnida lug'at, augmentatsiya o'rnida normallashtirish, Grad-CAM o'rnida xato tahlili. Matn loyihasiga xos eng muhim farq — bo'lish birligi. Rasm loyihasida har rasm mustaqil edi; bu yerda bir mijozning murojaatlari bir-biriga o'xshaydi (uslub, apostrof odati, takror yuborilgan matn), shuning uchun bo'lish mijoz bo'yicha qilinadi.
Ikkinchi farq — nomzodlar ro'yxatida neyron model oxirida turadi. Soddalik tartibi qarorning bir qismi: agar oddiy model sezilarli yomon bo'lmasa — u tanlanadi.
Skelet 21.12 niki; matnga xos qo'shimchalar — normallashtirish, o'quvdagi lug'at va mijoz bo'yicha bo'lish.
2.2. Ma'lumot va quvur
MA'LUMOT (1-misol):
4542 murojaat, 1500 mijoz (bitta mijozda 1-22 ta)
karta 30.3%, o'tkazma 27.0%, kredit 18.2%, ilova 16.5%, firibgarlik 7.9%
shovqin: 41.9% murojaatda nostandart apostrof, apostrof tushishi,
imlo xatolari, 35% murojaatda qo'shimcha ibora (ko'pincha BOSHQA toifadan),
325 ta ketma-ket qayta yuborilgan matn, 4% yorliq xatosi
NORMALLASHTIRISH 23.1-bob:
"O\u2019tkazma" "o\u02bbtkazma" "o`tkazma" -> "o'tkazma"
"otkazma" (apostrof tushgan) -> normallashtirish TUZATMAYDI
-> belgi n-gramlari: " otk", "tkaz", "kazm" - "o'tkazma" bilan umumiy
BO'LISH:
GroupShuffleSplit(groups=mijoz): ish 80% / test 20%, umumiy mijoz 0
ish ichida: o'quv / validatsiya yana mijoz bo'yicha
qayta yuborilgan matnlar mijoz bilan birga -> takror leakage i yo'q
LUG'AT (EmbeddingBag uchun):
tokenlar = so'zlar + qo'shni so'z juftlari (bigram)
min_son = 2, 0 - <unk>; FAQAT o'quvdan; kalitlar - oddiy strNormallashtirish hamma narsani hal qilmaydi va buni bilish muhim. 1-misolda noyob so'zlar soni 2100 dan 1976 ga tushdi, lekin "o'tkaz-" oilasi normallashtirishdan keyin ham 12 xil yozilishda qoldi: imlo xatolari ("o'tkazdiim") va apostrofsiz shakllar ("otkazma") — ularni hech qanday almashtirish qoidasi ishonchli tuzatmaydi. Bu belgi n-gramlari uchun asosiy dalil: "otkazma" va "o'tkazma" so'z darajasida ikki xil token, lekin ularning tkaz, kazm, azma n-gramlari umumiy.
Mijoz bo'yicha bo'lish bir yo'la ikki muammoni hal qiladi: mijoz uslubi testga "sizmaydi" va qayta yuborilgan matnlar (bir mijozniki) bitta tomonda qoladi. 1-misolda ish va test orasida umumiy mijoz 0, firibgarlik ulushi esa ikkala tomonda yaqin (0.077 va 0.088).
Normallashtirish — birinchi qadam, lekin yetarli emas; bo'lish mijoz bo'yicha, lug'at esa faqat o'quvdan.
2.3. Model va o'rgatish
NOMZODLAR (soddalik tartibida):
1) ko'pchilik doim "karta" (pastki chegara)
2) so'z TF-IDF so'z 1-2 gram + LogisticRegression(class_weight="balanced")
3) belgi TF-IDF char_wb 2-5 gram + LogisticRegression(balanced)
4) EmbeddingBag (so'z + bigram) -> mean embedding(64) -> Linear(5)
EmbeddingBag KIRISHI (pad kerak emas):
x = [t1 t2 t3 | t4 t5 | t6 t7 t8 t9] bitta 1D tensor
offsets = [0, 3, 5] har murojaat boshi
jamla() - DataLoader ning collate_fn i
O'RGATISH 21.5-bob:
AdamW(lr=1e-2), batch 128, 15 davrgacha
loss: cross_entropy(weight = sinf_vazni) nomutanosiblik
EngYaxshisi(monitor="val_mf1", sabr=3)
-> copy.deepcopy(state_dict()), oxirida tiklash
METRIKA: makro-F1 (toifalar teng vaznda) + toifa bo'yicha recall
"doim karta": makro-F1 ~0.09 - darhol ko'rinadiEmbeddingBag — bu vazifa uchun tabiiy neyron model: toifani asosan qaysi so'zlar borligi belgilaydi, tartib emas. U LSTM dan ko'p marta tez va pad talab qilmaydi — murojaatlar bitta 1D tensorga ulanadi, offsets esa har birining boshini ko'rsatadi. Bigram tokenlar ("sms kod", "pul yechildi") ozgina tartib ma'lumotini qaytaradi — bu fastText g'oyasi.
2-misoldagi Trainer jurnali eng yaxshi holat nega kerakligini yana ko'rsatdi: validatsiya makro-F1 5-davrda 0.8155 ga yetdi, keyin train loss pasayishda davom etdi, val loss esa o'sdi — 8-davrda 0.8093 bilan to'xtadi. deepcopy siz paket oxirgi, yodlay boshlagan modelni olardi. Uch seedda natija 0.8156 +- 0.0003 — EmbeddingBag seedga deyarli sezgir emas.
Bitta validatsiya bo'lagida belgi TF-IDF 0.8342, so'z TF-IDF 0.8286, EmbeddingBag 0.8155 berdi. Lekin 735 ta murojaatli bitta bo'lak — qaror uchun kam. Qarorni GroupKFold qiladi.
Matn toifalashda EmbeddingBag — tabiiy neyron bazaviy; eng yaxshi holat deepcopy bilan, metrika esa makro-F1.
2.4. Taqqoslash va qaror
BIR XIL 5 TA FOLD: GroupKFold(5), groups = mijoz (ish to'plamida, test TEGILMAYDI)
har foldda: vektorizator / lug'at - fold o'quvidan
EmbeddingBag: erta to'xtatish uchun validatsiya fold O'QUVINING ichidan
(fold validatsiyasi ham tanlashga, ham baholashga ishlatilmaydi)
JUFTLASHGAN FARQ (eng yaxshisi - nomzod):
d_f = mf1_eng,f - mf1_nomzod,f
SE = std(d) / sqrt(5); d > 2 * SE -> nomzod sezilarli yomon
QOIDA: eng yaxshisidan SEZILARLI yomon bo'lmagan ENG SODDA model
tartib: ko'pchilik < so'z TF-IDF < belgi TF-IDF < EmbeddingBag3-misolda o'rtacha makro-F1: so'z TF-IDF 0.8015, belgi TF-IDF 0.8051, EmbeddingBag 0.7996. Eng yuqori — belgi TF-IDF. Juftlashgan farqlar qarorni belgiladi: so'z TF-IDF undan +0.0036 ga yomon, va bu kichik farq 2*SE = 0.0032 dan katta — beshala foldda ham belgi n-gram biroz yaxshi chiqdi. EmbeddingBag bilan farq +0.0055, lekin 2*SE = 0.0079 ichida — sezilmas.
Qoida bo'yicha tanlov — belgi TF-IDF: undan soddaroq yagona munosib nomzod (so'z TF-IDF) sezilarli yomon, EmbeddingBag esa murakkabroq va yaxshiroq emas. E'tibor bering: neyron model yutqazmadi ham — u shunchaki murakkabligini oqlamadi. Bu bizning "Real vaziyat" dagi auditning xulosasi bilan bir xil.
Farq kichikligini ham halol aytish kerak: 0.0036 makro-F1 — amaliyotda sezilmaydigan farq. U statistik jihatdan barqaror (har foldda bir tomonga), lekin agar so'z TF-IDF boshqa sabab bilan (masalan, tushuntirish osonligi) afzal bo'lsa, uni tanlash ham asosli bo'lardi. Qoida — qaror uchun boshlang'ich nuqta, fikrlashning o'rnini bosmaydi.
Qarorni juftlashgan farq va SE, so'ng soddalik tartibi belgilaydi; neyron model faqat sezilarli yaxshi bo'lsa tanlanadi.
2.5. Test, toifa bo'yicha recall va xato tahlili
TEST BIR MARTA (tanlangan model butun ish to'plamida o'rgatiladi):
makro-F1 + aniqlik
TOIFA BO'YICHA recall va precision - asosiy jadval
chalkashlik qatori: firibgarlik -> [karta, o'tkazma, kredit, ilova, firibgarlik]
XATO NARXI:
firibgarlikni o'tkazib yuborish -> mijoz puli, bank obro'si
yolg'on firibgarlik signali -> xavfsizlik bo'limiga ortiqcha ish
class_weight="balanced" - recall ni precision hisobiga ko'taradi
XATO TAHLILI 23.13-bob:
segment: imlo xatosi, apostrof tushgan, aralash (2+ toifa iborasi)
bor / yo'q: xato ulushi, farqning 2*SE siTest oxirida bir marta ochiladi. 4-misolda makro-F1 0.8115 — CV bahosiga (0.8051) yaqin, ya'ni mijoz bo'yicha bo'lish ishonchli baho bergan. Toifa jadvali muhimroq narsani ko'rsatdi: eng past recall — ilova (0.741), firibgarlik recall i 0.821 (84 tadan 15 tasi o'tkazib yuborilgan va boshqa to'rt toifaga deyarli teng tarqalgan). Firibgarlik precision i esa 0.697: 30 ta boshqa murojaat "firibgarlik" deb belgilangan — sinf vazni recall ni precision hisobiga ko'targan. Xavfsizlik bo'limi uchun bu odatda to'g'ri almashuv.
Xato tahlili 23.13-darsdagi saboqni takrorladi. Imlo xatoli murojaatlarda xato 0.203 va xatosizlarda 0.153, apostrofi tushganlarda 0.209 va 0.165 — farqlar 2*SE ichida: belgi n-gramlari bu shovqinni yaxshi ko'targan. Haqiqiy muammo boshqa joyda: ikki toifaning iborasi bor murojaatlarda xato 0.531, qolganlarida 0.118. "Kartamdan pul yechildi, ilovaga kira olmayapman" — bu ma'noli noaniqlik, uni model emas, jarayon hal qiladi: bunday murojaatlarni ikki navbatga yoki odamga yuborish.
Hisobotda umumiy ball emas, toifa bo'yicha recall va eng xavfli segment; xato tahlili keyingi ish qayerda ekanini ko'rsatadi.
2.6. Topshirish paketi va hisobot
MUAMMO: tanlangan model - sklearn Pipeline. Uni pickle bilan saqlash mumkin,
lekin torch.load(weights_only=True) pickle ni rad etadi (xavfsizlik, 21.7).
YECHIM: chiziqli modelni TENZORLAR va oddiy turlar sifatida saqlash
paket = {
"format": 1,
"konfig": {...asdict(k), "model": "belgi TF-IDF", "ngram": [2, 5]},
"lugat": {" kart": 619, ...}, # n-gram -> ustun, SATR kalitlar, int qiymat
"idf": tensor(V), # float64
"W": tensor(5, V), "b": tensor(5), # LogisticRegression coef_ / intercept_
"toifalar": ["karta", "o'tkazma", ...], # TARTIB muhim
"metrika": {"test_makro_f1": ..., "test_recall": {...}},
"versiyalar": {"torch": str(torch.__version__), "sklearn": ..., "python": ...},
"nazorat": {"matn": 8 ta xom murojaat, "logit": sklearn decision_function},
}
BASHORATCHI (torch, sklearn siz):
normallashtir -> char_wb n-gramlar -> 1 + log(soni) -> * idf -> l2
-> X @ W.T + b -> softmax -> (toifa, ehtimol)
nazorat: qayta hisoblangan logitlar == sklearn logitlari (atol 1e-8)Bu paket 22.14-darsdagidan bir jihatdan farq qiladi: unda neyron tarmoq yo'q, lekin tamoyil bir xil — faqat xavfsiz turlar (tenzor, str, int, float, ro'yxat, lug'at). Chiziqli model — bu vazn matritsasi va bias; TF-IDF esa lug'at va IDF vektori. Ularni tenzor sifatida saqlab, transformatsiyani Bashoratchi da qayta yozamiz. char_wb n-gram algoritmi sklearn nikining aynan takrori bo'lishi kerak — nazorat namunasi aynan shuni tekshiradi: 4-misolda qayta hisoblangan logitlar sklearn logitlari bilan 1e-8 aniqlikda mos keldi.
Nazorat namunasi xom matnlarda saqlanadi — u normallashtirish, n-gram ajratish, IDF va vaznlarning hammasini birga tekshiradi. Agar kimdir keyinroq normallashtirish qoidasini "yaxshilasa" (masalan, apostrofni butunlay olib tashlasa), nazorat darhol XATO beradi.
Bashoratchi 4-misolda qo'lda yozilgan to'rtta yangi murojaatni to'g'ri toifaladi, shu jumladan apostrofi tushgan "pul otkazdim lekin kelmadi" ni (0.99 ishonch bilan).
sklearn modelini ham weights_only=True paketiga aylantirish mumkin — tenzorlar, satr kalitli lug'at va nazorat namunasi bilan.
2.7. Tuzoqlar
Asosiy tuzoqlar: murojaatlarni tasodifiy bo'lish (bir mijozning uslubi va qayta yuborilgan matnlari ikki tomonga tushadi); normallashtirishsiz tokenlash; normallashtirish apostrof tushishini tuzatadi deb o'ylash; lug'at yoki vektorizatorni butun ma'lumotda qurish; nomutanosib toifalarda aniqlik bilan hisobot berish; sinf vaznisiz o'rgatib, firibgarlikni "yo'qotish"; eng yaxshi holatni havola bilan saqlash; neyron modelni bazaviylarsiz tanlash; bitta validatsiya bo'lagida qaror qabul qilish; fold validatsiyasini erta to'xtatishga ham, baholashga ham ishlatish; juftlashgan farqsiz "yaxshiroq" deyish; testni bir necha marta ochish; toifalar tartibini paketga yozmaslik; sklearn obyektini pickle bilan paketga qo'yib weights_only=True ni o'chirish; torch.__version__ ni satrga aylantirmaslik; nazorat namunasini normallashtirilgan matnda saqlash; aralash (ikki toifali) murojaatlarni model "tuzatadi" deb kutish.
3. Tez ma'lumotnoma
k = Konfig(); seed_everything(k.seed)
matn, y, mijoz = malumot(seed=0)
ish, te = guruhli_bolish(y, mijoz, k.test_ulushi, k.seed) # test QULF, mijoz bo'yicha
soz_tfidf(k); belgi_tfidf(k) # Pipeline: fit fold ichida
model, lug, eyx, tarix = bag_orgat(k, m_tr, y_tr, m_va, y_va) # Trainer + deepcopy
for tr, va in GroupKFold(5).split(m, yy, mj): ... # bir xil foldlar
d = ball[eng] - ball[nomzod]; yomon = d.mean() > 2 * d.std(ddof=1) / np.sqrt(5)
tanlov = next(n for n in SODDALIK if n == eng or not yomon[n]) # eng sodda munosib
p = model.predict(matn[te]) # test BIR MARTA
paket = {"lugat": {str(g): int(i) ...}, "idf": ..., "W": ..., "nazorat": ...}
torch.save(paket, yol); Bashoratchi(yol) # weights_only=TrueAmaliyot xulosasi
konfig -> ma'lumot -> normallashtirish -> mijoz bo'yicha bo'lish (test qulf)
lug'at / vektorizator faqat o'quvda; tez tekshiruvlar (ln 5, bitta batch)
nomzodlar: ko'pchilik < so'z TF-IDF < belgi TF-IDF < EmbeddingBag
GroupKFold juftlashgan farq -> eng sodda munosib model (bu yerda: belgi TF-IDF)
test bir marta: toifa bo'yicha recall, firibgarlik, xato tahlili
paket: tenzorlar + satr lug'at + str(torch.__version__) + xom nazorat -> hisobot4. Batafsil misollar
Misollar real numpy/sklearn/torch bilan (Python 3.14).
Misol 1 — Konfig, ma'lumot, bo'lish va tekshiruvlar
"""1-qadam: konfig, ma'lumot, normallashtirish, mijoz bo'yicha bo'lish va tekshiruvlar."""
import copy
import math
import random
import re
from collections import Counter
from dataclasses import dataclass
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import f1_score
from sklearn.model_selection import GroupShuffleSplit
from sklearn.pipeline import make_pipeline
from torch.utils.data import DataLoader, Dataset
TOIFALAR = ["karta", "o'tkazma", "kredit", "ilova", "firibgarlik"]
ULUSH = [0.32, 0.26, 0.17, 0.18, 0.07]
IBORALAR = {
"karta": ["kartam bloklanib qoldi", "pin kodni unutib qo'ydim",
"kartamning muddati tugadi", "bankomat kartamni qaytarmadi",
"kartani qayta chiqarish kerak", "kartam do'konda o'tmayapti",
"karta limitini oshirmoqchiman", "yangi karta qachon tayyor bo'ladi"],
"o'tkazma": ["pul o'tkazdim lekin qabul qiluvchiga kelmadi",
"o'tkazma qaytib keldi", "boshqa bankka o'tkazma qilolmayapman",
"xalqaro o'tkazma qancha vaqt oladi", "rekvizitni xato yozib yubordim",
"o'tkazma komissiyasi juda baland", "o'tkazma hali ham kutilmoqda"],
"kredit": ["kredit olmoqchiman", "foiz stavkasi qancha", "kredit to'lovini kechiktirdim",
"kreditni muddatidan oldin yopmoqchiman", "to'lov grafigini yuboring",
"kredit tarixi haqida ma'lumot kerak", "avtokredit shartlari qanday"],
"ilova": ["ilovaga kira olmayapman", "parolni tiklay olmayapman",
"ilova qotib qolyapti", "yangilanishdan keyin ilova ochilmayapti",
"barmoq izi bilan kirish ishlamayapti", "ilovada balans ko'rinmayapti",
"tasdiqlash sms kodi kelmayapti"],
"firibgarlik": ["kartamdan men bilmagan holda pul yechildi",
"kimdir qo'ng'iroq qilib sms kodni so'radi",
"men qilmagan to'lov ko'rinyapti", "shubhali havolaga kirib qo'ydim",
"hisobimdan pul g'oyib bo'ldi", "o'zini bank xodimi deb tanishtirdi",
"kartam ma'lumotlarini begona saytga kiritib qo'ydim"],
}
TAFSILOT = ["{son} ming so'm", "kecha kechqurun", "bugun ertalab", "uch kundan beri",
"8600 karta raqami {raqam}", "operator bilan bog'lanib bo'lmadi",
"filialga borishga vaqtim yo'q", "bu ikkinchi murojaatim",
"ilova orqali ko'rdim", "sms xabar keldi"]
SALOM = ["", "assalomu alaykum", "salom", "hurmatli bank", "iltimos"]
XAYR = ["", "rahmat", "tezroq javob bering", "yordam bering", "iltimos hal qiling"]
APOSTROFLAR = ["'", "\u02bb", "\u2019", "`", ""]
def tanla(rng, royxat):
return royxat[int(rng.integers(len(royxat)))]
def buz(rng, soz, p):
"""Imlo xatosi: harf tushib qoladi, ikkilanadi yoki qo'shnisi bilan almashadi."""
if len(soz) < 5 or rng.random() >= p:
return soz
j, amal = int(rng.integers(2, len(soz) - 1)), int(rng.integers(3))
if amal == 0:
return soz[:j] + soz[j + 1:]
if amal == 1:
return soz[:j] + soz[j] + soz[j:]
return soz[:j - 1] + soz[j] + soz[j - 1] + soz[j + 1:]
def murojaat(rng, mijoz, toifa):
qismlar = [tanla(rng, IBORALAR[TOIFALAR[toifa]])]
if rng.random() < 0.35: # boshqa toifa iborasi ham tilga olinadi
qismlar.append(tanla(rng, IBORALAR[tanla(rng, TOIFALAR)]))
for _ in range(int(rng.integers(0, 3))):
qismlar.append(tanla(rng, TAFSILOT).replace(
"{son}", str(int(rng.integers(5, 900)))).replace(
"{raqam}", str(int(rng.integers(1000, 9999)))))
rng.shuffle(qismlar)
qismlar = [mijoz["salom"]] * bool(mijoz["salom"]) + qismlar \
+ [mijoz["xayr"]] * bool(mijoz["xayr"])
matn = ", ".join(qismlar)
matn = " ".join(buz(rng, s, mijoz["imlo"]) for s in matn.split())
return matn.replace("'", mijoz["apostrof"]) + tanla(rng, [".", "!", "?", ""])
def malumot(seed=0, n_mijoz=1500, p_takror=0.1, p_shovqin=0.04):
"""Bank mijozlari murojaatlari: matn, toifa, mijoz."""
rng = np.random.default_rng(seed)
matn, y, mijoz = [], [], []
for m in range(n_mijoz):
mj = {"salom": tanla(rng, SALOM), "xayr": tanla(rng, XAYR),
"apostrof": tanla(rng, APOSTROFLAR), "imlo": float(rng.uniform(0, 0.12)),
"asosiy": int(rng.choice(5, p=ULUSH))}
for _ in range(int(rng.geometric(1 / 3))):
if matn and mijoz[-1] == m and rng.random() < p_takror: # qayta yozish
matn.append(matn[-1].lower() + tanla(rng, ["", "!!", " ???"]))
y.append(y[-1])
else:
t = mj["asosiy"] if rng.random() < 0.5 else int(rng.choice(5, p=ULUSH))
matn.append(murojaat(rng, mj, t))
y.append(int(rng.integers(5)) if rng.random() < p_shovqin else t)
mijoz.append(m)
return np.array(matn, dtype=object), np.array(y), np.array(mijoz)
def normallashtir(s):
s = s.lower()
for v in ("\u02bb", "\u2019", "\u2018", "\u02bc", "`"):
s = s.replace(v, "'")
s = re.sub(r"[^a-z0-9' ]+", " ", s)
return " ".join(s.split())
QISQA = ["karta", "o'tkaz.", "kredit", "ilova", "firib."] # jadval ustunlari
@dataclass(frozen=True)
class Konfig:
seed: int = 42
test_ulushi: float = 0.2
min_son: int = 2
emb: int = 64
lr: float = 1e-2
batch: int = 128
davrlar: int = 15
sabr: int = 3
C: float = 10.0
def seed_everything(seed):
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
def guruhli_bolish(y, guruh, ulush, seed):
"""Mijoz bo'yicha bo'lish: bir mijozning hamma murojaati bitta tomonda."""
gss = GroupShuffleSplit(n_splits=1, test_size=ulush, random_state=seed)
return next(gss.split(np.zeros(len(y)), y, guruh))
def makro_f1(y, p):
return f1_score(y, p, average="macro", labels=list(range(len(TOIFALAR))))
def soz_tfidf(k):
return make_pipeline(
TfidfVectorizer(preprocessor=normallashtir, token_pattern=r"[^ ]+",
ngram_range=(1, 2), sublinear_tf=True, min_df=k.min_son),
LogisticRegression(C=k.C, max_iter=3000, class_weight="balanced"))
def belgi_tfidf(k):
return make_pipeline(
TfidfVectorizer(preprocessor=normallashtir, analyzer="char_wb",
ngram_range=(2, 5), sublinear_tf=True, min_df=k.min_son),
LogisticRegression(C=k.C, max_iter=3000, class_weight="balanced"))
def tokenlar(matn):
"""So'zlar + qo'shni so'z juftlari (bigram) - fastText g'oyasi."""
s = normallashtir(matn).split()
return s + [a + " " + b for a, b in zip(s, s[1:])]
class Lugat:
"""Token -> indeks, FAQAT o'quvdan; 0 - <unk>. Kalitlar - oddiy satrlar."""
def __init__(self, matnlar, min_son):
c = Counter(t for m in matnlar for t in tokenlar(m))
itos = ["<unk>"] + sorted(t for t, n in c.items() if n >= min_son)
self.stoi = {t: i for i, t in enumerate(itos)}
def __len__(self):
return len(self.stoi)
def kodla(self, matn):
return torch.tensor([self.stoi.get(t, 0) for t in tokenlar(matn)] or [0])
class MurojaatDataset(Dataset):
def __init__(self, matnlar, y, lugat):
self.x = [lugat.kodla(m) for m in matnlar]
self.y = torch.as_tensor(y)
def __len__(self):
return len(self.y)
def __getitem__(self, i):
return self.x[i], self.y[i]
def jamla(batch):
"""EmbeddingBag uchun: tokenlar bitta qatorga, offsets - har matn boshi."""
xs, ys = zip(*batch)
offsets = torch.tensor([0] + [len(x) for x in xs[:-1]]).cumsum(0)
return torch.cat(xs), offsets, torch.stack(ys)
class MurojaatBag(nn.Module):
"""EmbeddingBag(mean) -> Linear: tartibsiz, lekin bigram tokenlar bilan."""
def __init__(self, V, d):
super().__init__()
self.emb = nn.EmbeddingBag(V, d, mode="mean")
self.bosh = nn.Linear(d, len(TOIFALAR))
def forward(self, x, offsets):
return self.bosh(self.emb(x, offsets))
class EngYaxshisi:
"""21.5 dagi callback: eng yaxshi holat deepcopy bilan, sabr bilan to'xtash."""
def __init__(self, monitor, sabr):
self.monitor, self.sabr = monitor, sabr
def fit_boshi(self, tr):
self.eng, self.holat, self.davr, self.hisob = -np.inf, None, 0, 0
def davr_oxiri(self, tr, davr, log):
if log[self.monitor] > self.eng:
self.eng, self.davr, self.hisob = log[self.monitor], davr, 0
self.holat = copy.deepcopy(tr.model.state_dict())
else:
self.hisob += 1
tr.toxtash = self.hisob >= self.sabr
def fit_oxiri(self, tr):
tr.model.load_state_dict(self.holat)
class Trainer:
def __init__(self, model, opt, vazn, callbacklar):
self.model, self.opt, self.vazn = model, opt, vazn
self.callbacklar, self.tarix, self.toxtash = callbacklar, [], False
def _davr(self, dl, orgatish):
self.model.train(orgatish)
jami, n, plar, ylar = 0.0, 0, [], []
with torch.set_grad_enabled(orgatish):
for x, off, y in dl:
ch = self.model(x, off)
loss = F.cross_entropy(ch, y, weight=self.vazn)
if orgatish:
self.opt.zero_grad()
loss.backward()
self.opt.step()
jami += loss.item() * len(y)
n += len(y)
plar.append(ch.argmax(1))
ylar.append(y)
y, p = torch.cat(ylar).numpy(), torch.cat(plar).numpy()
return {"loss": jami / n, "mf1": makro_f1(y, p)}
def fit(self, dl_tr, dl_va, davrlar):
for cb in self.callbacklar:
cb.fit_boshi(self)
for davr in range(1, davrlar + 1):
t, v = self._davr(dl_tr, True), self._davr(dl_va, False)
log = {"train_loss": t["loss"], "val_loss": v["loss"], "val_mf1": v["mf1"]}
self.tarix.append({"davr": davr, **log})
for cb in self.callbacklar:
cb.davr_oxiri(self, davr, log)
if self.toxtash:
break
for cb in self.callbacklar:
cb.fit_oxiri(self)
return self.tarix
def sinf_vazni(y):
return torch.tensor(len(y) / (len(TOIFALAR) * np.bincount(y, minlength=len(TOIFALAR))),
dtype=torch.float32)
def bag_orgat(k, m_tr, y_tr, m_va, y_va, seed=None):
"""Lug'at o'quvdan -> DataLoader -> Trainer + EngYaxshisi. (model, lug'at, eyx, tarix)"""
seed = k.seed if seed is None else seed
seed_everything(seed)
lug = Lugat(m_tr, k.min_son)
g = torch.Generator().manual_seed(seed)
dl_tr = DataLoader(MurojaatDataset(m_tr, y_tr, lug), batch_size=k.batch,
shuffle=True, generator=g, collate_fn=jamla)
dl_va = DataLoader(MurojaatDataset(m_va, y_va, lug), batch_size=512,
collate_fn=jamla)
model = MurojaatBag(len(lug), k.emb)
opt = torch.optim.AdamW(model.parameters(), lr=k.lr, weight_decay=1e-4)
eyx = EngYaxshisi("val_mf1", k.sabr)
tarix = Trainer(model, opt, sinf_vazni(y_tr), [eyx]).fit(dl_tr, dl_va, k.davrlar)
return model, lug, eyx, tarix
def bag_bashorat(model, lug, matnlar):
x, off, _ = jamla([(lug.kodla(m), torch.tensor(0)) for m in matnlar])
model.eval()
with torch.no_grad():
return model(x, off)
def main() -> None:
k = Konfig()
seed_everything(k.seed)
matn, y, mijoz = malumot(seed=0)
print("=== 1. Ma'lumot ===")
print(f" murojaatlar {len(y)}, mijozlar {len(set(mijoz.tolist()))}, "
f"bir mijozda eng ko'p {np.bincount(mijoz).max()}")
for t, s in enumerate(TOIFALAR):
print(f" {s:<12} {np.sum(y == t):>5} ({np.mean(y == t):.1%})")
print(" namunalar (normallashtirilgan):")
for t in range(len(TOIFALAR)):
i = int(np.flatnonzero(y == t)[0])
print(f" [{TOIFALAR[t]}] {normallashtir(matn[i])}")
print("\n=== 2. Normallashtirish va shovqin ===")
nostandart = np.mean([bool(re.search("[\u02bb\u2019`]", m)) for m in matn])
print(f" nostandart apostrof: {nostandart:.1%} murojaatda")
xom = {s.strip(".,!?").lower() for m in matn for s in m.split()}
toza = {s for m in matn for s in normallashtir(m).split()}
print(f" noyob so'zlar: xom {len(xom)} -> normallangan {len(toza)}")
otk = sorted(s for s in toza if s.replace("'", "").startswith("otkaz"))
apostrofsiz = [s for s in otk if "'" not in s]
print(f" o'tkaz- oilasi normallashdan keyin ham {len(otk)} xil yozilish,")
print(f" ulardan apostrofsiz: {len(apostrofsiz)}, masalan {apostrofsiz[:3]}")
takror = sum(normallashtir(a) == normallashtir(b)
for a, b, ma, mb in zip(matn[:-1], matn[1:], mijoz[:-1], mijoz[1:])
if ma == mb)
print(f" ketma-ket qayta yozilgan murojaatlar: {takror}")
print("\n=== 3. Mijoz bo'yicha bo'lish: test QULF ===")
ish, te = guruhli_bolish(y, mijoz, k.test_ulushi, k.seed)
print(f" ish {len(ish)}, test {len(te)}; umumiy mijoz: "
f"{len(set(mijoz[ish].tolist()) & set(mijoz[te].tolist()))}")
for nom, ii in [("ish", ish), ("test", te)]:
print(f" {nom:<5} firibgarlik ulushi {np.mean(y[ii] == 4):.3f}")
tr_i, va_i = guruhli_bolish(y[ish], mijoz[ish], 0.2, k.seed)
tr, va = ish[tr_i], ish[va_i]
print(f" ish ichida: o'quv {len(tr)}, validatsiya {len(va)}")
print("\n=== 4. Lug'at FAQAT o'quvdan ===")
lug = Lugat(matn[tr], k.min_son)
kod_va = [lug.kodla(m) for m in matn[va]]
unk = sum(int((x == 0).sum()) for x in kod_va) / sum(len(x) for x in kod_va)
print(f" hajm {len(lug)} (so'z + bigram), validatsiyada <unk> {unk:.1%}")
print(f" kalit turi: {type(next(iter(lug.stoi))).__name__}, "
f"'kartam' -> {lug.stoi.get('kartam')}")
print("\n=== 5. Tez tekshiruvlar ===")
g = torch.Generator().manual_seed(k.seed)
dl = DataLoader(MurojaatDataset(matn[tr], y[tr], lug), batch_size=k.batch,
shuffle=True, generator=g, collate_fn=jamla)
x, off, yb = next(iter(dl))
model = MurojaatBag(len(lug), k.emb)
with torch.no_grad():
ch = model(x, off)
boshl = F.cross_entropy(ch, yb).item()
tekshiruvlar = [
("tokenlar (1D)", tuple(x.shape), x.dim() == 1),
("offsets", tuple(off.shape), len(off) == len(yb)),
("chiqish shakli", tuple(ch.shape), tuple(ch.shape) == (len(yb), 5)),
("boshlang'ich loss", round(boshl, 3), abs(boshl - math.log(5)) < 0.2),
]
for nom, q, ok in tekshiruvlar:
print(f" {nom:<18} {str(q):>12} {'OK' if ok else 'XATO':>6}")
print(f" kutilgan boshlang'ich loss ln(5) = {math.log(5):.3f}")
print("\n=== 6. Bitta batch testi ===")
opt = torch.optim.Adam(model.parameters(), lr=0.05)
for _ in range(100):
opt.zero_grad()
loss = F.cross_entropy(model(x, off), yb)
loss.backward()
opt.step()
print(f" {len(yb)} murojaat, 100 qadam: loss {loss.item():.4f} -> "
f"{'OTDI' if loss.item() < 0.05 else 'OTMADI'}")
if __name__ == "__main__":
main()Natijaning muhim qismi:
=== 1. Ma'lumot ===
murojaatlar 4542, mijozlar 1500, bir mijozda eng ko'p 22
karta 1378 (30.3%)
o'tkazma 1226 (27.0%)
kredit 828 (18.2%)
ilova 749 (16.5%)
firibgarlik 361 (7.9%)
namunalar (normallashtirilgan):
[karta] iltimos kartani qayta chiqarish kerak yordam bering
[o'tkazma] iltimos boshqa bankka o'tkazma qilolmayapman yordam bering
[kredit] iltimoss operator bilan bog'lanib bo'lmadi foiz stavkasi qancha yordam bering
[ilova] ilovada balans ko'rinmayapti operator bian bog'lanib bo'lmadi kecha kechqurun yordam bering
[firibgarlik] assalomu alaykum kartamdan men bilmgaan hollda pul yechildi yordam bering
=== 2. Normallashtirish va shovqin ===
nostandart apostrof: 41.9% murojaatda
noyob so'zlar: xom 2100 -> normallangan 1976
o'tkaz- oilasi normallashdan keyin ham 12 xil yozilish,
ulardan apostrofsiz: 3, masalan ['otkazdim', 'otkazma', 'otkazmma']
ketma-ket qayta yozilgan murojaatlar: 325
=== 3. Mijoz bo'yicha bo'lish: test QULF ===
ish 3590, test 952; umumiy mijoz: 0
ish firibgarlik ulushi 0.077
test firibgarlik ulushi 0.088
ish ichida: o'quv 2855, validatsiya 735
=== 4. Lug'at FAQAT o'quvdan ===
hajm 2340 (so'z + bigram), validatsiyada <unk> 9.0%
kalit turi: str, 'kartam' -> 891
=== 5. Tez tekshiruvlar ===
tokenlar (1D) (2804,) OK
offsets (128,) OK
chiqish shakli (128, 5) OK
boshlang'ich loss 1.574 OK
kutilgan boshlang'ich loss ln(5) = 1.609
=== 6. Bitta batch testi ===
128 murojaat, 100 qadam: loss 0.0000 -> OTDINima ko'rsatdi: 2.1, 2.2-bo'limlar.
Misol 2 — Bazaviylar, EmbeddingBag va Trainer
"""2-qadam: bazaviylar va EmbeddingBag (Trainer, eng yaxshi holat, seedlar) validatsiyada."""
import copy
import random
import re
from collections import Counter
from dataclasses import dataclass, replace
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import f1_score
from sklearn.model_selection import GroupShuffleSplit
from sklearn.pipeline import make_pipeline
from torch.utils.data import DataLoader, Dataset
TOIFALAR = ["karta", "o'tkazma", "kredit", "ilova", "firibgarlik"]
ULUSH = [0.32, 0.26, 0.17, 0.18, 0.07]
IBORALAR = {
"karta": ["kartam bloklanib qoldi", "pin kodni unutib qo'ydim",
"kartamning muddati tugadi", "bankomat kartamni qaytarmadi",
"kartani qayta chiqarish kerak", "kartam do'konda o'tmayapti",
"karta limitini oshirmoqchiman", "yangi karta qachon tayyor bo'ladi"],
"o'tkazma": ["pul o'tkazdim lekin qabul qiluvchiga kelmadi",
"o'tkazma qaytib keldi", "boshqa bankka o'tkazma qilolmayapman",
"xalqaro o'tkazma qancha vaqt oladi", "rekvizitni xato yozib yubordim",
"o'tkazma komissiyasi juda baland", "o'tkazma hali ham kutilmoqda"],
"kredit": ["kredit olmoqchiman", "foiz stavkasi qancha", "kredit to'lovini kechiktirdim",
"kreditni muddatidan oldin yopmoqchiman", "to'lov grafigini yuboring",
"kredit tarixi haqida ma'lumot kerak", "avtokredit shartlari qanday"],
"ilova": ["ilovaga kira olmayapman", "parolni tiklay olmayapman",
"ilova qotib qolyapti", "yangilanishdan keyin ilova ochilmayapti",
"barmoq izi bilan kirish ishlamayapti", "ilovada balans ko'rinmayapti",
"tasdiqlash sms kodi kelmayapti"],
"firibgarlik": ["kartamdan men bilmagan holda pul yechildi",
"kimdir qo'ng'iroq qilib sms kodni so'radi",
"men qilmagan to'lov ko'rinyapti", "shubhali havolaga kirib qo'ydim",
"hisobimdan pul g'oyib bo'ldi", "o'zini bank xodimi deb tanishtirdi",
"kartam ma'lumotlarini begona saytga kiritib qo'ydim"],
}
TAFSILOT = ["{son} ming so'm", "kecha kechqurun", "bugun ertalab", "uch kundan beri",
"8600 karta raqami {raqam}", "operator bilan bog'lanib bo'lmadi",
"filialga borishga vaqtim yo'q", "bu ikkinchi murojaatim",
"ilova orqali ko'rdim", "sms xabar keldi"]
SALOM = ["", "assalomu alaykum", "salom", "hurmatli bank", "iltimos"]
XAYR = ["", "rahmat", "tezroq javob bering", "yordam bering", "iltimos hal qiling"]
APOSTROFLAR = ["'", "\u02bb", "\u2019", "`", ""]
def tanla(rng, royxat):
return royxat[int(rng.integers(len(royxat)))]
def buz(rng, soz, p):
"""Imlo xatosi: harf tushib qoladi, ikkilanadi yoki qo'shnisi bilan almashadi."""
if len(soz) < 5 or rng.random() >= p:
return soz
j, amal = int(rng.integers(2, len(soz) - 1)), int(rng.integers(3))
if amal == 0:
return soz[:j] + soz[j + 1:]
if amal == 1:
return soz[:j] + soz[j] + soz[j:]
return soz[:j - 1] + soz[j] + soz[j - 1] + soz[j + 1:]
def murojaat(rng, mijoz, toifa):
qismlar = [tanla(rng, IBORALAR[TOIFALAR[toifa]])]
if rng.random() < 0.35: # boshqa toifa iborasi ham tilga olinadi
qismlar.append(tanla(rng, IBORALAR[tanla(rng, TOIFALAR)]))
for _ in range(int(rng.integers(0, 3))):
qismlar.append(tanla(rng, TAFSILOT).replace(
"{son}", str(int(rng.integers(5, 900)))).replace(
"{raqam}", str(int(rng.integers(1000, 9999)))))
rng.shuffle(qismlar)
qismlar = [mijoz["salom"]] * bool(mijoz["salom"]) + qismlar \
+ [mijoz["xayr"]] * bool(mijoz["xayr"])
matn = ", ".join(qismlar)
matn = " ".join(buz(rng, s, mijoz["imlo"]) for s in matn.split())
return matn.replace("'", mijoz["apostrof"]) + tanla(rng, [".", "!", "?", ""])
def malumot(seed=0, n_mijoz=1500, p_takror=0.1, p_shovqin=0.04):
"""Bank mijozlari murojaatlari: matn, toifa, mijoz."""
rng = np.random.default_rng(seed)
matn, y, mijoz = [], [], []
for m in range(n_mijoz):
mj = {"salom": tanla(rng, SALOM), "xayr": tanla(rng, XAYR),
"apostrof": tanla(rng, APOSTROFLAR), "imlo": float(rng.uniform(0, 0.12)),
"asosiy": int(rng.choice(5, p=ULUSH))}
for _ in range(int(rng.geometric(1 / 3))):
if matn and mijoz[-1] == m and rng.random() < p_takror: # qayta yozish
matn.append(matn[-1].lower() + tanla(rng, ["", "!!", " ???"]))
y.append(y[-1])
else:
t = mj["asosiy"] if rng.random() < 0.5 else int(rng.choice(5, p=ULUSH))
matn.append(murojaat(rng, mj, t))
y.append(int(rng.integers(5)) if rng.random() < p_shovqin else t)
mijoz.append(m)
return np.array(matn, dtype=object), np.array(y), np.array(mijoz)
def normallashtir(s):
s = s.lower()
for v in ("\u02bb", "\u2019", "\u2018", "\u02bc", "`"):
s = s.replace(v, "'")
s = re.sub(r"[^a-z0-9' ]+", " ", s)
return " ".join(s.split())
QISQA = ["karta", "o'tkaz.", "kredit", "ilova", "firib."] # jadval ustunlari
@dataclass(frozen=True)
class Konfig:
seed: int = 42
test_ulushi: float = 0.2
min_son: int = 2
emb: int = 64
lr: float = 1e-2
batch: int = 128
davrlar: int = 15
sabr: int = 3
C: float = 10.0
def seed_everything(seed):
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
def guruhli_bolish(y, guruh, ulush, seed):
"""Mijoz bo'yicha bo'lish: bir mijozning hamma murojaati bitta tomonda."""
gss = GroupShuffleSplit(n_splits=1, test_size=ulush, random_state=seed)
return next(gss.split(np.zeros(len(y)), y, guruh))
def makro_f1(y, p):
return f1_score(y, p, average="macro", labels=list(range(len(TOIFALAR))))
def soz_tfidf(k):
return make_pipeline(
TfidfVectorizer(preprocessor=normallashtir, token_pattern=r"[^ ]+",
ngram_range=(1, 2), sublinear_tf=True, min_df=k.min_son),
LogisticRegression(C=k.C, max_iter=3000, class_weight="balanced"))
def belgi_tfidf(k):
return make_pipeline(
TfidfVectorizer(preprocessor=normallashtir, analyzer="char_wb",
ngram_range=(2, 5), sublinear_tf=True, min_df=k.min_son),
LogisticRegression(C=k.C, max_iter=3000, class_weight="balanced"))
def tokenlar(matn):
"""So'zlar + qo'shni so'z juftlari (bigram) - fastText g'oyasi."""
s = normallashtir(matn).split()
return s + [a + " " + b for a, b in zip(s, s[1:])]
class Lugat:
"""Token -> indeks, FAQAT o'quvdan; 0 - <unk>. Kalitlar - oddiy satrlar."""
def __init__(self, matnlar, min_son):
c = Counter(t for m in matnlar for t in tokenlar(m))
itos = ["<unk>"] + sorted(t for t, n in c.items() if n >= min_son)
self.stoi = {t: i for i, t in enumerate(itos)}
def __len__(self):
return len(self.stoi)
def kodla(self, matn):
return torch.tensor([self.stoi.get(t, 0) for t in tokenlar(matn)] or [0])
class MurojaatDataset(Dataset):
def __init__(self, matnlar, y, lugat):
self.x = [lugat.kodla(m) for m in matnlar]
self.y = torch.as_tensor(y)
def __len__(self):
return len(self.y)
def __getitem__(self, i):
return self.x[i], self.y[i]
def jamla(batch):
"""EmbeddingBag uchun: tokenlar bitta qatorga, offsets - har matn boshi."""
xs, ys = zip(*batch)
offsets = torch.tensor([0] + [len(x) for x in xs[:-1]]).cumsum(0)
return torch.cat(xs), offsets, torch.stack(ys)
class MurojaatBag(nn.Module):
"""EmbeddingBag(mean) -> Linear: tartibsiz, lekin bigram tokenlar bilan."""
def __init__(self, V, d):
super().__init__()
self.emb = nn.EmbeddingBag(V, d, mode="mean")
self.bosh = nn.Linear(d, len(TOIFALAR))
def forward(self, x, offsets):
return self.bosh(self.emb(x, offsets))
class EngYaxshisi:
"""21.5 dagi callback: eng yaxshi holat deepcopy bilan, sabr bilan to'xtash."""
def __init__(self, monitor, sabr):
self.monitor, self.sabr = monitor, sabr
def fit_boshi(self, tr):
self.eng, self.holat, self.davr, self.hisob = -np.inf, None, 0, 0
def davr_oxiri(self, tr, davr, log):
if log[self.monitor] > self.eng:
self.eng, self.davr, self.hisob = log[self.monitor], davr, 0
self.holat = copy.deepcopy(tr.model.state_dict())
else:
self.hisob += 1
tr.toxtash = self.hisob >= self.sabr
def fit_oxiri(self, tr):
tr.model.load_state_dict(self.holat)
class Trainer:
def __init__(self, model, opt, vazn, callbacklar):
self.model, self.opt, self.vazn = model, opt, vazn
self.callbacklar, self.tarix, self.toxtash = callbacklar, [], False
def _davr(self, dl, orgatish):
self.model.train(orgatish)
jami, n, plar, ylar = 0.0, 0, [], []
with torch.set_grad_enabled(orgatish):
for x, off, y in dl:
ch = self.model(x, off)
loss = F.cross_entropy(ch, y, weight=self.vazn)
if orgatish:
self.opt.zero_grad()
loss.backward()
self.opt.step()
jami += loss.item() * len(y)
n += len(y)
plar.append(ch.argmax(1))
ylar.append(y)
y, p = torch.cat(ylar).numpy(), torch.cat(plar).numpy()
return {"loss": jami / n, "mf1": makro_f1(y, p)}
def fit(self, dl_tr, dl_va, davrlar):
for cb in self.callbacklar:
cb.fit_boshi(self)
for davr in range(1, davrlar + 1):
t, v = self._davr(dl_tr, True), self._davr(dl_va, False)
log = {"train_loss": t["loss"], "val_loss": v["loss"], "val_mf1": v["mf1"]}
self.tarix.append({"davr": davr, **log})
for cb in self.callbacklar:
cb.davr_oxiri(self, davr, log)
if self.toxtash:
break
for cb in self.callbacklar:
cb.fit_oxiri(self)
return self.tarix
def sinf_vazni(y):
return torch.tensor(len(y) / (len(TOIFALAR) * np.bincount(y, minlength=len(TOIFALAR))),
dtype=torch.float32)
def bag_orgat(k, m_tr, y_tr, m_va, y_va, seed=None):
"""Lug'at o'quvdan -> DataLoader -> Trainer + EngYaxshisi. (model, lug'at, eyx, tarix)"""
seed = k.seed if seed is None else seed
seed_everything(seed)
lug = Lugat(m_tr, k.min_son)
g = torch.Generator().manual_seed(seed)
dl_tr = DataLoader(MurojaatDataset(m_tr, y_tr, lug), batch_size=k.batch,
shuffle=True, generator=g, collate_fn=jamla)
dl_va = DataLoader(MurojaatDataset(m_va, y_va, lug), batch_size=512,
collate_fn=jamla)
model = MurojaatBag(len(lug), k.emb)
opt = torch.optim.AdamW(model.parameters(), lr=k.lr, weight_decay=1e-4)
eyx = EngYaxshisi("val_mf1", k.sabr)
tarix = Trainer(model, opt, sinf_vazni(y_tr), [eyx]).fit(dl_tr, dl_va, k.davrlar)
return model, lug, eyx, tarix
def bag_bashorat(model, lug, matnlar):
x, off, _ = jamla([(lug.kodla(m), torch.tensor(0)) for m in matnlar])
model.eval()
with torch.no_grad():
return model(x, off)
def main() -> None:
k = Konfig()
matn, y, mijoz = malumot(seed=0)
ish, te = guruhli_bolish(y, mijoz, k.test_ulushi, k.seed) # test tegilmaydi
tr_i, va_i = guruhli_bolish(y[ish], mijoz[ish], 0.2, k.seed)
tr, va = ish[tr_i], ish[va_i]
print("=== 1. EmbeddingBag: Trainer jurnali (seed 42) ===")
model, lug, eyx, tarix = bag_orgat(k, matn[tr], y[tr], matn[va], y[va])
print(f" {'davr':>5} {'train_loss':>11} {'val_loss':>9} {'val_mf1':>8}")
for q in tarix:
if q["davr"] in (1, 2, eyx.davr, len(tarix)) or q["davr"] % 5 == 0:
print(f" {q['davr']:>5} {q['train_loss']:>11.4f} {q['val_loss']:>9.4f} "
f"{q['val_mf1']:>8.4f}")
print(f" to'xtadi: {len(tarix)}-davr (sabr {k.sabr}), eng yaxshi {eyx.davr}-davr")
p = bag_bashorat(model, lug, matn[va]).argmax(1).numpy()
print(f" tiklangan model val makro-F1 {makro_f1(y[va], p):.4f}, "
f"callback eslagan {eyx.eng:.4f}")
if eyx.davr < len(tarix):
print(f" oxirgi davr {tarix[-1]['val_mf1']:.4f} edi - deepcopy siz shu qolardi")
print("\n=== 2. Bir xil validatsiyada hamma nomzodlar ===")
bash = {"ko'pchilik": np.full(len(va), np.bincount(y[tr]).argmax()),
"so'z TF-IDF": soz_tfidf(k).fit(matn[tr], y[tr]).predict(matn[va]),
"belgi TF-IDF": belgi_tfidf(k).fit(matn[tr], y[tr]).predict(matn[va]),
"EmbeddingBag": p}
print(f" {'model':<14} {'makro-F1':>8} recall: "
+ " ".join(f"{s:>7}" for s in QISQA))
for nom, pp in bash.items():
rec = [np.mean(pp[y[va] == t] == t) for t in range(len(TOIFALAR))]
print(f" {nom:<14} {makro_f1(y[va], pp):>8.4f} "
+ " ".join(f"{r:>7.3f}" for r in rec))
print("\n=== 3. EmbeddingBag uch seedda ===")
ballar = [eyx.eng]
for s in (1, 2):
ballar.append(bag_orgat(k, matn[tr], y[tr], matn[va], y[va], seed=s)[2].eng)
ballar = np.array(ballar)
print(f" seedlar 42, 1, 2: {ballar.round(4).tolist()}")
print(f" o'rtacha {ballar.mean():.4f} +- {ballar.std(ddof=1):.4f}")
print(" bitta validatsiya bo'lagi - qaror uchun kam: keyingi qadam - GroupKFold")
if __name__ == "__main__":
main()Natijaning muhim qismi:
=== 1. EmbeddingBag: Trainer jurnali (seed 42) ===
davr train_loss val_loss val_mf1
1 1.3751 1.0826 0.7270
2 0.7472 0.6430 0.8088
5 0.3148 0.6042 0.8155
8 0.2215 0.6507 0.8093
to'xtadi: 8-davr (sabr 3), eng yaxshi 5-davr
tiklangan model val makro-F1 0.8155, callback eslagan 0.8155
oxirgi davr 0.8093 edi - deepcopy siz shu qolardi
=== 2. Bir xil validatsiyada hamma nomzodlar ===
model makro-F1 recall: karta o'tkaz. kredit ilova firib.
ko'pchilik 0.0876 1.000 0.000 0.000 0.000 0.000
so'z TF-IDF 0.8286 0.883 0.860 0.788 0.798 0.817
belgi TF-IDF 0.8342 0.874 0.833 0.825 0.831 0.829
EmbeddingBag 0.8155 0.854 0.844 0.745 0.806 0.854
=== 3. EmbeddingBag uch seedda ===
seedlar 42, 1, 2: [0.8155, 0.816, 0.8154]
o'rtacha 0.8156 +- 0.0003
bitta validatsiya bo'lagi - qaror uchun kam: keyingi qadam - GroupKFoldNima ko'rsatdi: 2.3-bo'lim.
Misol 3 — GroupKFold da juftlashgan taqqoslash va qaror
"""3-qadam: GroupKFold (mijoz) da juftlashgan taqqoslash va qaror."""
import copy
import random
import re
from collections import Counter
from dataclasses import dataclass
import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import f1_score
from sklearn.model_selection import GroupKFold, GroupShuffleSplit
from sklearn.pipeline import make_pipeline
from torch.utils.data import DataLoader, Dataset
TOIFALAR = ["karta", "o'tkazma", "kredit", "ilova", "firibgarlik"]
ULUSH = [0.32, 0.26, 0.17, 0.18, 0.07]
IBORALAR = {
"karta": ["kartam bloklanib qoldi", "pin kodni unutib qo'ydim",
"kartamning muddati tugadi", "bankomat kartamni qaytarmadi",
"kartani qayta chiqarish kerak", "kartam do'konda o'tmayapti",
"karta limitini oshirmoqchiman", "yangi karta qachon tayyor bo'ladi"],
"o'tkazma": ["pul o'tkazdim lekin qabul qiluvchiga kelmadi",
"o'tkazma qaytib keldi", "boshqa bankka o'tkazma qilolmayapman",
"xalqaro o'tkazma qancha vaqt oladi", "rekvizitni xato yozib yubordim",
"o'tkazma komissiyasi juda baland", "o'tkazma hali ham kutilmoqda"],
"kredit": ["kredit olmoqchiman", "foiz stavkasi qancha", "kredit to'lovini kechiktirdim",
"kreditni muddatidan oldin yopmoqchiman", "to'lov grafigini yuboring",
"kredit tarixi haqida ma'lumot kerak", "avtokredit shartlari qanday"],
"ilova": ["ilovaga kira olmayapman", "parolni tiklay olmayapman",
"ilova qotib qolyapti", "yangilanishdan keyin ilova ochilmayapti",
"barmoq izi bilan kirish ishlamayapti", "ilovada balans ko'rinmayapti",
"tasdiqlash sms kodi kelmayapti"],
"firibgarlik": ["kartamdan men bilmagan holda pul yechildi",
"kimdir qo'ng'iroq qilib sms kodni so'radi",
"men qilmagan to'lov ko'rinyapti", "shubhali havolaga kirib qo'ydim",
"hisobimdan pul g'oyib bo'ldi", "o'zini bank xodimi deb tanishtirdi",
"kartam ma'lumotlarini begona saytga kiritib qo'ydim"],
}
TAFSILOT = ["{son} ming so'm", "kecha kechqurun", "bugun ertalab", "uch kundan beri",
"8600 karta raqami {raqam}", "operator bilan bog'lanib bo'lmadi",
"filialga borishga vaqtim yo'q", "bu ikkinchi murojaatim",
"ilova orqali ko'rdim", "sms xabar keldi"]
SALOM = ["", "assalomu alaykum", "salom", "hurmatli bank", "iltimos"]
XAYR = ["", "rahmat", "tezroq javob bering", "yordam bering", "iltimos hal qiling"]
APOSTROFLAR = ["'", "\u02bb", "\u2019", "`", ""]
def tanla(rng, royxat):
return royxat[int(rng.integers(len(royxat)))]
def buz(rng, soz, p):
"""Imlo xatosi: harf tushib qoladi, ikkilanadi yoki qo'shnisi bilan almashadi."""
if len(soz) < 5 or rng.random() >= p:
return soz
j, amal = int(rng.integers(2, len(soz) - 1)), int(rng.integers(3))
if amal == 0:
return soz[:j] + soz[j + 1:]
if amal == 1:
return soz[:j] + soz[j] + soz[j:]
return soz[:j - 1] + soz[j] + soz[j - 1] + soz[j + 1:]
def murojaat(rng, mijoz, toifa):
qismlar = [tanla(rng, IBORALAR[TOIFALAR[toifa]])]
if rng.random() < 0.35: # boshqa toifa iborasi ham tilga olinadi
qismlar.append(tanla(rng, IBORALAR[tanla(rng, TOIFALAR)]))
for _ in range(int(rng.integers(0, 3))):
qismlar.append(tanla(rng, TAFSILOT).replace(
"{son}", str(int(rng.integers(5, 900)))).replace(
"{raqam}", str(int(rng.integers(1000, 9999)))))
rng.shuffle(qismlar)
qismlar = [mijoz["salom"]] * bool(mijoz["salom"]) + qismlar \
+ [mijoz["xayr"]] * bool(mijoz["xayr"])
matn = ", ".join(qismlar)
matn = " ".join(buz(rng, s, mijoz["imlo"]) for s in matn.split())
return matn.replace("'", mijoz["apostrof"]) + tanla(rng, [".", "!", "?", ""])
def malumot(seed=0, n_mijoz=1500, p_takror=0.1, p_shovqin=0.04):
"""Bank mijozlari murojaatlari: matn, toifa, mijoz."""
rng = np.random.default_rng(seed)
matn, y, mijoz = [], [], []
for m in range(n_mijoz):
mj = {"salom": tanla(rng, SALOM), "xayr": tanla(rng, XAYR),
"apostrof": tanla(rng, APOSTROFLAR), "imlo": float(rng.uniform(0, 0.12)),
"asosiy": int(rng.choice(5, p=ULUSH))}
for _ in range(int(rng.geometric(1 / 3))):
if matn and mijoz[-1] == m and rng.random() < p_takror: # qayta yozish
matn.append(matn[-1].lower() + tanla(rng, ["", "!!", " ???"]))
y.append(y[-1])
else:
t = mj["asosiy"] if rng.random() < 0.5 else int(rng.choice(5, p=ULUSH))
matn.append(murojaat(rng, mj, t))
y.append(int(rng.integers(5)) if rng.random() < p_shovqin else t)
mijoz.append(m)
return np.array(matn, dtype=object), np.array(y), np.array(mijoz)
def normallashtir(s):
s = s.lower()
for v in ("\u02bb", "\u2019", "\u2018", "\u02bc", "`"):
s = s.replace(v, "'")
s = re.sub(r"[^a-z0-9' ]+", " ", s)
return " ".join(s.split())
QISQA = ["karta", "o'tkaz.", "kredit", "ilova", "firib."] # jadval ustunlari
@dataclass(frozen=True)
class Konfig:
seed: int = 42
test_ulushi: float = 0.2
min_son: int = 2
emb: int = 64
lr: float = 1e-2
batch: int = 128
davrlar: int = 15
sabr: int = 3
C: float = 10.0
def seed_everything(seed):
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
def guruhli_bolish(y, guruh, ulush, seed):
"""Mijoz bo'yicha bo'lish: bir mijozning hamma murojaati bitta tomonda."""
gss = GroupShuffleSplit(n_splits=1, test_size=ulush, random_state=seed)
return next(gss.split(np.zeros(len(y)), y, guruh))
def makro_f1(y, p):
return f1_score(y, p, average="macro", labels=list(range(len(TOIFALAR))))
def soz_tfidf(k):
return make_pipeline(
TfidfVectorizer(preprocessor=normallashtir, token_pattern=r"[^ ]+",
ngram_range=(1, 2), sublinear_tf=True, min_df=k.min_son),
LogisticRegression(C=k.C, max_iter=3000, class_weight="balanced"))
def belgi_tfidf(k):
return make_pipeline(
TfidfVectorizer(preprocessor=normallashtir, analyzer="char_wb",
ngram_range=(2, 5), sublinear_tf=True, min_df=k.min_son),
LogisticRegression(C=k.C, max_iter=3000, class_weight="balanced"))
def tokenlar(matn):
"""So'zlar + qo'shni so'z juftlari (bigram) - fastText g'oyasi."""
s = normallashtir(matn).split()
return s + [a + " " + b for a, b in zip(s, s[1:])]
class Lugat:
"""Token -> indeks, FAQAT o'quvdan; 0 - <unk>. Kalitlar - oddiy satrlar."""
def __init__(self, matnlar, min_son):
c = Counter(t for m in matnlar for t in tokenlar(m))
itos = ["<unk>"] + sorted(t for t, n in c.items() if n >= min_son)
self.stoi = {t: i for i, t in enumerate(itos)}
def __len__(self):
return len(self.stoi)
def kodla(self, matn):
return torch.tensor([self.stoi.get(t, 0) for t in tokenlar(matn)] or [0])
class MurojaatDataset(Dataset):
def __init__(self, matnlar, y, lugat):
self.x = [lugat.kodla(m) for m in matnlar]
self.y = torch.as_tensor(y)
def __len__(self):
return len(self.y)
def __getitem__(self, i):
return self.x[i], self.y[i]
def jamla(batch):
"""EmbeddingBag uchun: tokenlar bitta qatorga, offsets - har matn boshi."""
xs, ys = zip(*batch)
offsets = torch.tensor([0] + [len(x) for x in xs[:-1]]).cumsum(0)
return torch.cat(xs), offsets, torch.stack(ys)
class MurojaatBag(nn.Module):
"""EmbeddingBag(mean) -> Linear: tartibsiz, lekin bigram tokenlar bilan."""
def __init__(self, V, d):
super().__init__()
self.emb = nn.EmbeddingBag(V, d, mode="mean")
self.bosh = nn.Linear(d, len(TOIFALAR))
def forward(self, x, offsets):
return self.bosh(self.emb(x, offsets))
class EngYaxshisi:
"""21.5 dagi callback: eng yaxshi holat deepcopy bilan, sabr bilan to'xtash."""
def __init__(self, monitor, sabr):
self.monitor, self.sabr = monitor, sabr
def fit_boshi(self, tr):
self.eng, self.holat, self.davr, self.hisob = -np.inf, None, 0, 0
def davr_oxiri(self, tr, davr, log):
if log[self.monitor] > self.eng:
self.eng, self.davr, self.hisob = log[self.monitor], davr, 0
self.holat = copy.deepcopy(tr.model.state_dict())
else:
self.hisob += 1
tr.toxtash = self.hisob >= self.sabr
def fit_oxiri(self, tr):
tr.model.load_state_dict(self.holat)
class Trainer:
def __init__(self, model, opt, vazn, callbacklar):
self.model, self.opt, self.vazn = model, opt, vazn
self.callbacklar, self.tarix, self.toxtash = callbacklar, [], False
def _davr(self, dl, orgatish):
self.model.train(orgatish)
jami, n, plar, ylar = 0.0, 0, [], []
with torch.set_grad_enabled(orgatish):
for x, off, y in dl:
ch = self.model(x, off)
loss = F.cross_entropy(ch, y, weight=self.vazn)
if orgatish:
self.opt.zero_grad()
loss.backward()
self.opt.step()
jami += loss.item() * len(y)
n += len(y)
plar.append(ch.argmax(1))
ylar.append(y)
y, p = torch.cat(ylar).numpy(), torch.cat(plar).numpy()
return {"loss": jami / n, "mf1": makro_f1(y, p)}
def fit(self, dl_tr, dl_va, davrlar):
for cb in self.callbacklar:
cb.fit_boshi(self)
for davr in range(1, davrlar + 1):
t, v = self._davr(dl_tr, True), self._davr(dl_va, False)
log = {"train_loss": t["loss"], "val_loss": v["loss"], "val_mf1": v["mf1"]}
self.tarix.append({"davr": davr, **log})
for cb in self.callbacklar:
cb.davr_oxiri(self, davr, log)
if self.toxtash:
break
for cb in self.callbacklar:
cb.fit_oxiri(self)
return self.tarix
def sinf_vazni(y):
return torch.tensor(len(y) / (len(TOIFALAR) * np.bincount(y, minlength=len(TOIFALAR))),
dtype=torch.float32)
def bag_orgat(k, m_tr, y_tr, m_va, y_va, seed=None):
"""Lug'at o'quvdan -> DataLoader -> Trainer + EngYaxshisi. (model, lug'at, eyx, tarix)"""
seed = k.seed if seed is None else seed
seed_everything(seed)
lug = Lugat(m_tr, k.min_son)
g = torch.Generator().manual_seed(seed)
dl_tr = DataLoader(MurojaatDataset(m_tr, y_tr, lug), batch_size=k.batch,
shuffle=True, generator=g, collate_fn=jamla)
dl_va = DataLoader(MurojaatDataset(m_va, y_va, lug), batch_size=512,
collate_fn=jamla)
model = MurojaatBag(len(lug), k.emb)
opt = torch.optim.AdamW(model.parameters(), lr=k.lr, weight_decay=1e-4)
eyx = EngYaxshisi("val_mf1", k.sabr)
tarix = Trainer(model, opt, sinf_vazni(y_tr), [eyx]).fit(dl_tr, dl_va, k.davrlar)
return model, lug, eyx, tarix
def bag_bashorat(model, lug, matnlar):
x, off, _ = jamla([(lug.kodla(m), torch.tensor(0)) for m in matnlar])
model.eval()
with torch.no_grad():
return model(x, off)
SODDALIK = ["ko'pchilik", "so'z TF-IDF", "belgi TF-IDF", "EmbeddingBag"] # oddiydan
def main() -> None:
k = Konfig()
matn, y, mijoz = malumot(seed=0)
ish, te = guruhli_bolish(y, mijoz, k.test_ulushi, k.seed) # test tegilmaydi
m, yy, mj = matn[ish], y[ish], mijoz[ish]
print("=== 1. GroupKFold(5): mijoz bo'yicha, bir xil foldlar ===")
ball = {nom: [] for nom in SODDALIK}
for f, (tr, va) in enumerate(GroupKFold(5).split(m, yy, mj), 1):
ball["ko'pchilik"].append(makro_f1(yy[va], np.full(len(va), np.bincount(yy[tr]).argmax())))
ball["so'z TF-IDF"].append(makro_f1(yy[va], soz_tfidf(k).fit(m[tr], yy[tr]).predict(m[va])))
ball["belgi TF-IDF"].append(makro_f1(yy[va], belgi_tfidf(k).fit(m[tr], yy[tr]).predict(m[va])))
# EmbeddingBag: erta to'xtatish uchun fold o'quvining ICHIDAN validatsiya
a, b = guruhli_bolish(yy[tr], mj[tr], 0.15, k.seed)
model, lug, _, _ = bag_orgat(k, m[tr][a], yy[tr][a], m[tr][b], yy[tr][b])
ball["EmbeddingBag"].append(makro_f1(yy[va], bag_bashorat(model, lug, m[va]).argmax(1).numpy()))
print(f" fold {f}: " + ", ".join(f"{n.split()[0]} {ball[n][-1]:.3f}" for n in SODDALIK[1:]))
ball = {n: np.array(v) for n, v in ball.items()}
print("\n=== 2. Natijalar ===")
print(f" {'model':<14} {'makro-F1':>9} {'+-std':>7}")
for n in SODDALIK:
print(f" {n:<14} {ball[n].mean():>9.4f} {ball[n].std(ddof=1):>7.4f}")
eng = max(SODDALIK, key=lambda n: ball[n].mean())
print(f" eng yuqori o'rtacha: {eng}")
print(f"\n=== 3. Juftlashgan farq: eng yaxshisi ({eng}) - model ===")
print(f" {'model':<14} {'farq':>8} {'2*SE':>7} sezilarli yomonmi")
yomon = {}
for n in SODDALIK:
if n == eng:
continue
d = ball[eng] - ball[n]
se = d.std(ddof=1) / np.sqrt(len(d))
yomon[n] = d.mean() > 2 * se
print(f" {n:<14} {d.mean():>+8.4f} {2 * se:>7.4f} {'ha' if yomon[n] else "yo'q"}")
print("\n=== 4. Qaror ===")
tanlov = next(n for n in SODDALIK if n == eng or not yomon[n])
print(" qoida: eng yaxshisidan SEZILARLI yomon bo'lmagan ENG SODDA model")
print(f" soddalik tartibi: {' < '.join(SODDALIK)}")
print(f" TANLOV: {tanlov}")
if tanlov != eng:
print(f" ({eng} o'rtachada yuqori, lekin farq 2*SE ichida - "
"murakkablik o'zini oqlamadi)")
if __name__ == "__main__":
main()Natijaning muhim qismi:
=== 1. GroupKFold(5): mijoz bo'yicha, bir xil foldlar ===
fold 1: so'z 0.816, belgi 0.826, EmbeddingBag 0.826
fold 2: so'z 0.808, belgi 0.810, EmbeddingBag 0.796
fold 3: so'z 0.787, belgi 0.790, EmbeddingBag 0.774
fold 4: so'z 0.800, belgi 0.802, EmbeddingBag 0.805
fold 5: so'z 0.796, belgi 0.797, EmbeddingBag 0.797
=== 2. Natijalar ===
model makro-F1 +-std
ko'pchilik 0.0933 0.0074
so'z TF-IDF 0.8015 0.0112
belgi TF-IDF 0.8051 0.0136
EmbeddingBag 0.7996 0.0189
eng yuqori o'rtacha: belgi TF-IDF
=== 3. Juftlashgan farq: eng yaxshisi (belgi TF-IDF) - model ===
model farq 2*SE sezilarli yomonmi
ko'pchilik +0.7118 0.0111 ha
so'z TF-IDF +0.0036 0.0032 ha
EmbeddingBag +0.0055 0.0079 yo'q
=== 4. Qaror ===
qoida: eng yaxshisidan SEZILARLI yomon bo'lmagan ENG SODDA model
soddalik tartibi: ko'pchilik < so'z TF-IDF < belgi TF-IDF < EmbeddingBag
TANLOV: belgi TF-IDFNima ko'rsatdi: 2.4-bo'lim.
Misol 4 — Test, xato tahlili, paket va hisobot
"""4-qadam: test bir marta, xato tahlili, weights_only paket va hisobot."""
import copy
import math
import random
import re
import shutil
import sys
import tempfile
from collections import Counter
from dataclasses import asdict, dataclass
from pathlib import Path
import numpy as np
import sklearn
import torch
import torch.nn as nn
import torch.nn.functional as F
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import confusion_matrix, f1_score
from sklearn.model_selection import GroupShuffleSplit
from sklearn.pipeline import make_pipeline
from torch.utils.data import DataLoader, Dataset
TOIFALAR = ["karta", "o'tkazma", "kredit", "ilova", "firibgarlik"]
ULUSH = [0.32, 0.26, 0.17, 0.18, 0.07]
IBORALAR = {
"karta": ["kartam bloklanib qoldi", "pin kodni unutib qo'ydim",
"kartamning muddati tugadi", "bankomat kartamni qaytarmadi",
"kartani qayta chiqarish kerak", "kartam do'konda o'tmayapti",
"karta limitini oshirmoqchiman", "yangi karta qachon tayyor bo'ladi"],
"o'tkazma": ["pul o'tkazdim lekin qabul qiluvchiga kelmadi",
"o'tkazma qaytib keldi", "boshqa bankka o'tkazma qilolmayapman",
"xalqaro o'tkazma qancha vaqt oladi", "rekvizitni xato yozib yubordim",
"o'tkazma komissiyasi juda baland", "o'tkazma hali ham kutilmoqda"],
"kredit": ["kredit olmoqchiman", "foiz stavkasi qancha", "kredit to'lovini kechiktirdim",
"kreditni muddatidan oldin yopmoqchiman", "to'lov grafigini yuboring",
"kredit tarixi haqida ma'lumot kerak", "avtokredit shartlari qanday"],
"ilova": ["ilovaga kira olmayapman", "parolni tiklay olmayapman",
"ilova qotib qolyapti", "yangilanishdan keyin ilova ochilmayapti",
"barmoq izi bilan kirish ishlamayapti", "ilovada balans ko'rinmayapti",
"tasdiqlash sms kodi kelmayapti"],
"firibgarlik": ["kartamdan men bilmagan holda pul yechildi",
"kimdir qo'ng'iroq qilib sms kodni so'radi",
"men qilmagan to'lov ko'rinyapti", "shubhali havolaga kirib qo'ydim",
"hisobimdan pul g'oyib bo'ldi", "o'zini bank xodimi deb tanishtirdi",
"kartam ma'lumotlarini begona saytga kiritib qo'ydim"],
}
TAFSILOT = ["{son} ming so'm", "kecha kechqurun", "bugun ertalab", "uch kundan beri",
"8600 karta raqami {raqam}", "operator bilan bog'lanib bo'lmadi",
"filialga borishga vaqtim yo'q", "bu ikkinchi murojaatim",
"ilova orqali ko'rdim", "sms xabar keldi"]
SALOM = ["", "assalomu alaykum", "salom", "hurmatli bank", "iltimos"]
XAYR = ["", "rahmat", "tezroq javob bering", "yordam bering", "iltimos hal qiling"]
APOSTROFLAR = ["'", "\u02bb", "\u2019", "`", ""]
def tanla(rng, royxat):
return royxat[int(rng.integers(len(royxat)))]
def buz(rng, soz, p):
"""Imlo xatosi: harf tushib qoladi, ikkilanadi yoki qo'shnisi bilan almashadi."""
if len(soz) < 5 or rng.random() >= p:
return soz
j, amal = int(rng.integers(2, len(soz) - 1)), int(rng.integers(3))
if amal == 0:
return soz[:j] + soz[j + 1:]
if amal == 1:
return soz[:j] + soz[j] + soz[j:]
return soz[:j - 1] + soz[j] + soz[j - 1] + soz[j + 1:]
def murojaat(rng, mijoz, toifa):
qismlar = [tanla(rng, IBORALAR[TOIFALAR[toifa]])]
if rng.random() < 0.35: # boshqa toifa iborasi ham tilga olinadi
qismlar.append(tanla(rng, IBORALAR[tanla(rng, TOIFALAR)]))
for _ in range(int(rng.integers(0, 3))):
qismlar.append(tanla(rng, TAFSILOT).replace(
"{son}", str(int(rng.integers(5, 900)))).replace(
"{raqam}", str(int(rng.integers(1000, 9999)))))
rng.shuffle(qismlar)
qismlar = [mijoz["salom"]] * bool(mijoz["salom"]) + qismlar \
+ [mijoz["xayr"]] * bool(mijoz["xayr"])
matn = ", ".join(qismlar)
matn = " ".join(buz(rng, s, mijoz["imlo"]) for s in matn.split())
return matn.replace("'", mijoz["apostrof"]) + tanla(rng, [".", "!", "?", ""])
def malumot(seed=0, n_mijoz=1500, p_takror=0.1, p_shovqin=0.04):
"""Bank mijozlari murojaatlari: matn, toifa, mijoz."""
rng = np.random.default_rng(seed)
matn, y, mijoz = [], [], []
for m in range(n_mijoz):
mj = {"salom": tanla(rng, SALOM), "xayr": tanla(rng, XAYR),
"apostrof": tanla(rng, APOSTROFLAR), "imlo": float(rng.uniform(0, 0.12)),
"asosiy": int(rng.choice(5, p=ULUSH))}
for _ in range(int(rng.geometric(1 / 3))):
if matn and mijoz[-1] == m and rng.random() < p_takror: # qayta yozish
matn.append(matn[-1].lower() + tanla(rng, ["", "!!", " ???"]))
y.append(y[-1])
else:
t = mj["asosiy"] if rng.random() < 0.5 else int(rng.choice(5, p=ULUSH))
matn.append(murojaat(rng, mj, t))
y.append(int(rng.integers(5)) if rng.random() < p_shovqin else t)
mijoz.append(m)
return np.array(matn, dtype=object), np.array(y), np.array(mijoz)
def normallashtir(s):
s = s.lower()
for v in ("\u02bb", "\u2019", "\u2018", "\u02bc", "`"):
s = s.replace(v, "'")
s = re.sub(r"[^a-z0-9' ]+", " ", s)
return " ".join(s.split())
QISQA = ["karta", "o'tkaz.", "kredit", "ilova", "firib."] # jadval ustunlari
@dataclass(frozen=True)
class Konfig:
seed: int = 42
test_ulushi: float = 0.2
min_son: int = 2
emb: int = 64
lr: float = 1e-2
batch: int = 128
davrlar: int = 15
sabr: int = 3
C: float = 10.0
def seed_everything(seed):
random.seed(seed)
np.random.seed(seed)
torch.manual_seed(seed)
def guruhli_bolish(y, guruh, ulush, seed):
"""Mijoz bo'yicha bo'lish: bir mijozning hamma murojaati bitta tomonda."""
gss = GroupShuffleSplit(n_splits=1, test_size=ulush, random_state=seed)
return next(gss.split(np.zeros(len(y)), y, guruh))
def makro_f1(y, p):
return f1_score(y, p, average="macro", labels=list(range(len(TOIFALAR))))
def soz_tfidf(k):
return make_pipeline(
TfidfVectorizer(preprocessor=normallashtir, token_pattern=r"[^ ]+",
ngram_range=(1, 2), sublinear_tf=True, min_df=k.min_son),
LogisticRegression(C=k.C, max_iter=3000, class_weight="balanced"))
def belgi_tfidf(k):
return make_pipeline(
TfidfVectorizer(preprocessor=normallashtir, analyzer="char_wb",
ngram_range=(2, 5), sublinear_tf=True, min_df=k.min_son),
LogisticRegression(C=k.C, max_iter=3000, class_weight="balanced"))
def tokenlar(matn):
"""So'zlar + qo'shni so'z juftlari (bigram) - fastText g'oyasi."""
s = normallashtir(matn).split()
return s + [a + " " + b for a, b in zip(s, s[1:])]
class Lugat:
"""Token -> indeks, FAQAT o'quvdan; 0 - <unk>. Kalitlar - oddiy satrlar."""
def __init__(self, matnlar, min_son):
c = Counter(t for m in matnlar for t in tokenlar(m))
itos = ["<unk>"] + sorted(t for t, n in c.items() if n >= min_son)
self.stoi = {t: i for i, t in enumerate(itos)}
def __len__(self):
return len(self.stoi)
def kodla(self, matn):
return torch.tensor([self.stoi.get(t, 0) for t in tokenlar(matn)] or [0])
class MurojaatDataset(Dataset):
def __init__(self, matnlar, y, lugat):
self.x = [lugat.kodla(m) for m in matnlar]
self.y = torch.as_tensor(y)
def __len__(self):
return len(self.y)
def __getitem__(self, i):
return self.x[i], self.y[i]
def jamla(batch):
"""EmbeddingBag uchun: tokenlar bitta qatorga, offsets - har matn boshi."""
xs, ys = zip(*batch)
offsets = torch.tensor([0] + [len(x) for x in xs[:-1]]).cumsum(0)
return torch.cat(xs), offsets, torch.stack(ys)
class MurojaatBag(nn.Module):
"""EmbeddingBag(mean) -> Linear: tartibsiz, lekin bigram tokenlar bilan."""
def __init__(self, V, d):
super().__init__()
self.emb = nn.EmbeddingBag(V, d, mode="mean")
self.bosh = nn.Linear(d, len(TOIFALAR))
def forward(self, x, offsets):
return self.bosh(self.emb(x, offsets))
class EngYaxshisi:
"""21.5 dagi callback: eng yaxshi holat deepcopy bilan, sabr bilan to'xtash."""
def __init__(self, monitor, sabr):
self.monitor, self.sabr = monitor, sabr
def fit_boshi(self, tr):
self.eng, self.holat, self.davr, self.hisob = -np.inf, None, 0, 0
def davr_oxiri(self, tr, davr, log):
if log[self.monitor] > self.eng:
self.eng, self.davr, self.hisob = log[self.monitor], davr, 0
self.holat = copy.deepcopy(tr.model.state_dict())
else:
self.hisob += 1
tr.toxtash = self.hisob >= self.sabr
def fit_oxiri(self, tr):
tr.model.load_state_dict(self.holat)
class Trainer:
def __init__(self, model, opt, vazn, callbacklar):
self.model, self.opt, self.vazn = model, opt, vazn
self.callbacklar, self.tarix, self.toxtash = callbacklar, [], False
def _davr(self, dl, orgatish):
self.model.train(orgatish)
jami, n, plar, ylar = 0.0, 0, [], []
with torch.set_grad_enabled(orgatish):
for x, off, y in dl:
ch = self.model(x, off)
loss = F.cross_entropy(ch, y, weight=self.vazn)
if orgatish:
self.opt.zero_grad()
loss.backward()
self.opt.step()
jami += loss.item() * len(y)
n += len(y)
plar.append(ch.argmax(1))
ylar.append(y)
y, p = torch.cat(ylar).numpy(), torch.cat(plar).numpy()
return {"loss": jami / n, "mf1": makro_f1(y, p)}
def fit(self, dl_tr, dl_va, davrlar):
for cb in self.callbacklar:
cb.fit_boshi(self)
for davr in range(1, davrlar + 1):
t, v = self._davr(dl_tr, True), self._davr(dl_va, False)
log = {"train_loss": t["loss"], "val_loss": v["loss"], "val_mf1": v["mf1"]}
self.tarix.append({"davr": davr, **log})
for cb in self.callbacklar:
cb.davr_oxiri(self, davr, log)
if self.toxtash:
break
for cb in self.callbacklar:
cb.fit_oxiri(self)
return self.tarix
def sinf_vazni(y):
return torch.tensor(len(y) / (len(TOIFALAR) * np.bincount(y, minlength=len(TOIFALAR))),
dtype=torch.float32)
def bag_orgat(k, m_tr, y_tr, m_va, y_va, seed=None):
"""Lug'at o'quvdan -> DataLoader -> Trainer + EngYaxshisi. (model, lug'at, eyx, tarix)"""
seed = k.seed if seed is None else seed
seed_everything(seed)
lug = Lugat(m_tr, k.min_son)
g = torch.Generator().manual_seed(seed)
dl_tr = DataLoader(MurojaatDataset(m_tr, y_tr, lug), batch_size=k.batch,
shuffle=True, generator=g, collate_fn=jamla)
dl_va = DataLoader(MurojaatDataset(m_va, y_va, lug), batch_size=512,
collate_fn=jamla)
model = MurojaatBag(len(lug), k.emb)
opt = torch.optim.AdamW(model.parameters(), lr=k.lr, weight_decay=1e-4)
eyx = EngYaxshisi("val_mf1", k.sabr)
tarix = Trainer(model, opt, sinf_vazni(y_tr), [eyx]).fit(dl_tr, dl_va, k.davrlar)
return model, lug, eyx, tarix
def bag_bashorat(model, lug, matnlar):
x, off, _ = jamla([(lug.kodla(m), torch.tensor(0)) for m in matnlar])
model.eval()
with torch.no_grad():
return model(x, off)
FORMAT = 1
TANLOV = "belgi TF-IDF" # 3-qadam qarori
def char_wb(matn, kichik, katta):
"""sklearn analyzer="char_wb" ning aynan takrori: so'z chetiga bo'shliq, n-gramlar."""
chiqish = []
for w in matn.split():
w = " " + w + " "
for n in range(kichik, katta + 1):
siljish = 0
chiqish.append(w[siljish:siljish + n])
while siljish + n < len(w):
siljish += 1
chiqish.append(w[siljish:siljish + n])
if siljish == 0:
break
return chiqish
class Bashoratchi:
"""Paketdan (weights_only=True) TF-IDF + chiziqli modelni torch da tiklaydi."""
def __init__(self, yol):
p = torch.load(yol, weights_only=True)
if p["format"] != FORMAT:
raise ValueError("format mos emas")
self.lugat, self.toifalar = p["lugat"], p["toifalar"]
self.ngram = tuple(p["konfig"]["ngram"])
self.idf, self.W, self.b = p["idf"], p["W"], p["b"]
q = self.logit(p["nazorat"]["matn"])
self.nazorat_ok = bool(torch.allclose(q, p["nazorat"]["logit"], atol=1e-8))
def logit(self, matnlar):
X = torch.zeros(len(matnlar), len(self.lugat), dtype=torch.float64)
for i, m in enumerate(matnlar):
c = Counter(g for g in char_wb(normallashtir(m), *self.ngram) if g in self.lugat)
for g, n in c.items():
X[i, self.lugat[g]] = 1 + math.log(n) # sublinear_tf
X = X * self.idf
X = X / X.norm(dim=1, keepdim=True).clamp(min=1e-12) # l2
return X @ self.W.T + self.b
def bashorat(self, matnlar):
p = torch.softmax(self.logit(matnlar), 1)
return [(self.toifalar[int(i)], float(v)) for v, i in zip(*p.max(1))]
def main() -> None:
k = Konfig()
matn, y, mijoz = malumot(seed=0)
ish, te = guruhli_bolish(y, mijoz, k.test_ulushi, k.seed)
print(f"=== 1. Yakuniy model (3-qadam qarori: {TANLOV}) ish to'plamida ===")
model = belgi_tfidf(k).fit(matn[ish], y[ish])
v, lr = model.steps[0][1], model.steps[1][1]
print(f" o'quv {len(ish)} murojaat, belgilar {len(v.vocabulary_)}")
print("\n=== 2. Testni BIR MARTA ochamiz ===")
yt, p = y[te], model.predict(matn[te])
mf1 = makro_f1(yt, p)
print(f" test makro-F1 {mf1:.4f}, aniqlik {np.mean(p == yt):.4f}")
cm = confusion_matrix(yt, p, labels=range(len(TOIFALAR)))
print(f" {'toifa':<12} {'n':>4} {'recall':>7} {'precision':>9} qator: "
+ " ".join(f"{q:>7}" for q in QISQA))
recall = {}
for t, s in enumerate(TOIFALAR):
recall[s] = cm[t, t] / cm[t].sum()
prec = cm[t, t] / max(cm[:, t].sum(), 1)
print(f" {s:<12} {cm[t].sum():>4} {recall[s]:>7.3f} {prec:>9.3f} "
+ " ".join(f"{c:>7}" for c in cm[t]))
eng_past = min(recall, key=recall.get)
print(f" eng past recall: {eng_past} ({recall[eng_past]:.3f})")
fir = cm[4]
print(f" firibgarlik o'tkazib yuborildi: {fir.sum() - fir[4]} / {fir.sum()} -> "
+ ", ".join(f"{QISQA[t]} {fir[t]}" for t in range(4)))
yolgon = cm[:4, 4].sum()
print(f" yolg'on firibgarlik signali: {yolgon} ta (boshqa toifadan)")
print("\n=== 3. Xato tahlili (test) ===")
tanish = {s for r in IBORALAR.values() for f in r for s in normallashtir(f).split()}
tanish |= {s for r in [TAFSILOT, SALOM, XAYR] for f in r for s in normallashtir(f).split()}
apostrofsiz = {s.replace("'", "") for s in tanish if "'" in s}
iboralar = {t: [normallashtir(f) for f in IBORALAR[t]] for t in TOIFALAR}
xato = p != yt
seg = {"imlo xatosi": [], "apostrof tushgan": [], "aralash (2+ toifa)": []}
for m in matn[te]:
s = normallashtir(m).split()
seg["imlo xatosi"].append(any(w not in tanish and not w.isdigit()
and w not in apostrofsiz for w in s))
seg["apostrof tushgan"].append(any(w in apostrofsiz for w in s))
n = normallashtir(m)
seg["aralash (2+ toifa)"].append(
sum(any(f in n for f in iboralar[t]) for t in TOIFALAR) >= 2)
print(f" {'segment':<20} {'bor: n':>7} {'xato':>6} {'2*SE':>6} "
f"{'yo`q: n':>7} {'xato':>6}".replace("`", "'"))
eng_xavfli, eng_farq = "", 0.0
for nom, b in seg.items():
b = np.array(b)
e1, e0 = xato[b].mean(), xato[~b].mean()
se = np.sqrt(e1 * (1 - e1) / b.sum() + e0 * (1 - e0) / (~b).sum())
print(f" {nom:<20} {b.sum():>7} {e1:>6.3f} {2 * se:>6.3f} {(~b).sum():>7} {e0:>6.3f}")
if e1 - e0 > 2 * se and e1 - e0 > eng_farq:
eng_xavfli, eng_farq = nom, e1 - e0
print(f" sezilarli xavfli segment: {eng_xavfli or 'topilmadi'}")
papka = Path(tempfile.mkdtemp(prefix="murojaat_paket_"))
try:
print("\n=== 4. Topshirish paketi (weights_only) ===")
nazorat = [str(s) for s in matn[te[:8]]]
paket = {
"format": FORMAT,
"konfig": {**asdict(k), "model": TANLOV, "ngram": [2, 5],
"normallashtirish": "kichik harf + apostrof -> ' + tinish yo'q"},
"lugat": {str(g): int(i) for g, i in v.vocabulary_.items()},
"idf": torch.tensor(v.idf_),
"W": torch.tensor(lr.coef_), "b": torch.tensor(lr.intercept_),
"toifalar": list(TOIFALAR),
"metrika": {"test_makro_f1": round(float(mf1), 4),
"test_recall": {s: round(float(r), 4) for s, r in recall.items()}},
"versiyalar": {"torch": str(torch.__version__), "sklearn": sklearn.__version__,
"python": sys.version.split()[0]},
"nazorat": {"matn": nazorat,
"logit": torch.tensor(lr.decision_function(v.transform(nazorat)))},
}
yol = papka / "murojaat_model.pt"
torch.save(paket, yol)
print(f" kalitlar: {sorted(paket)}")
misol = " kart"
print(f" lug'at: {len(paket['lugat'])} ta satr kalit, masalan "
f"{misol!r} -> {paket['lugat'][misol]}")
print("\n=== 5. Bashoratchi (torch.load(..., weights_only=True)) ===")
b = Bashoratchi(yol)
print(f" nazorat (sklearn logitlari bilan): {'OK' if b.nazorat_ok else 'XATO'}")
yangi = ["Assalomu alaykum, kartamdan bilmagan holda 300 ming so\u2019m yechildi!",
"ilovaga kira olmayapman, parolni ham tiklay olmadim",
"pul otkazdim lekin kelmadi, rekvizit tog\u02bbri edi",
"Kredit foizi qancha? muddatidan oldin yopsa bo'ladimi"]
for m, (t, e) in zip(yangi, b.bashorat(yangi)):
print(f" {normallashtir(m)[:44]:<44} -> {t:<11} ({e:.2f})")
print("\n=== 6. Yakuniy hisobot ===")
print(" VAZIFA: bank murojaatlarini 5 toifaga ajratish")
print(" METRIKA: makro-F1 + toifa bo'yicha recall (firibgarlik - eng qimmat)")
print(" DIZAYN: mijoz bo'yicha bo'lish; lug'at/vektorizator o'quvda; test bir marta")
print(f" QAROR: GroupKFold juftlashgan taqqoslash -> {TANLOV}")
print(f" TEST: makro-F1 {mf1:.4f}; firibgarlik recall {recall['firibgarlik']:.3f}")
print(f" ZAIF JOY: {eng_past} recall {recall[eng_past]:.3f}; "
f"xavfli segment: {eng_xavfli or 'yo`q'}".replace("`", "'"))
print(f" PAKET: format {FORMAT}, weights_only=True, nazorat "
f"{'OK' if b.nazorat_ok else 'XATO'}")
print(" CHEKLOVLAR: sintetik murojaatlar; bitta bank lug'ati; vaqt siljishi")
print(" (yangi xizmatlar, yangi firibgarlik usullari) tekshirilmagan")
finally:
shutil.rmtree(papka, ignore_errors=True)
if __name__ == "__main__":
main()Natijaning muhim qismi:
=== 1. Yakuniy model (3-qadam qarori: belgi TF-IDF) ish to'plamida ===
o'quv 3590 murojaat, belgilar 7027
=== 2. Testni BIR MARTA ochamiz ===
test makro-F1 0.8115, aniqlik 0.8267
toifa n recall precision qator: karta o'tkaz. kredit ilova firib.
karta 283 0.859 0.850 243 5 13 8 14
o'tkazma 245 0.865 0.902 12 212 11 6 4
kredit 166 0.807 0.744 11 9 134 5 7
ilova 174 0.741 0.849 17 5 18 129 5
firibgarlik 84 0.821 0.697 3 4 4 4 69
eng past recall: ilova 0.741-bob
firibgarlik o'tkazib yuborildi: 15 / 84 -> karta 3, o'tkaz. 4, kredit 4, ilova 4
yolg'on firibgarlik signali: 30 ta (boshqa toifadan)
=== 3. Xato tahlili (test) ===
segment bor: n xato 2*SE yo'q: n xato
imlo xatosi 390 0.203 0.051 562 0.153
apostrof tushgan 182 0.209 0.066 770 0.165
aralash (2+ toifa) 128 0.531 0.091 824 0.118
sezilarli xavfli segment: aralash (2+ toifa)
=== 4. Topshirish paketi (weights_only) ===
kalitlar: ['W', 'b', 'format', 'idf', 'konfig', 'lugat', 'metrika', 'nazorat', 'toifalar', 'versiyalar']
lug'at: 7027 ta satr kalit, masalan ' kart' -> 619
=== 5. Bashoratchi (torch.load(..., weights_only=True)) ===
nazorat (sklearn logitlari bilan): OK
assalomu alaykum kartamdan bilmagan holda 30 -> firibgarlik 0.90-bob
ilovaga kira olmayapman parolni ham tiklay o -> ilova 1.00-bob
pul otkazdim lekin kelmadi rekvizit tog'ri e -> o'tkazma 0.99-bob
kredit foizi qancha muddatidan oldin yopsa b -> kredit 0.92-bob
=== 6. Yakuniy hisobot ===
VAZIFA: bank murojaatlarini 5 toifaga ajratish
METRIKA: makro-F1 + toifa bo'yicha recall (firibgarlik - eng qimmat)
DIZAYN: mijoz bo'yicha bo'lish; lug'at/vektorizator o'quvda; test bir marta
QAROR: GroupKFold juftlashgan taqqoslash -> belgi TF-IDF
TEST: makro-F1 0.8115; firibgarlik recall 0.821
ZAIF JOY: ilova recall 0.741; xavfli segment: aralash (2+ toifa)
PAKET: format 1, weights_only=True, nazorat OK
CHEKLOVLAR: sintetik murojaatlar; bitta bank lug'ati; vaqt siljishi
(yangi xizmatlar, yangi firibgarlik usullari) tekshirilmaganNima ko'rsatdi: 2.5, 2.6-bo'limlar.
5. To'g'ri va noto'g'ri tushunishlar
| Noto'g'ri fikr | To'g'risi |
|---|---|
| "Neyron model matnda doim yaxshiroq" | Juftlashgan farq hal qiladi — bu yerda belgi TF-IDF tanlandi |
| "Normallashtirish apostrof muammosini to'liq hal qiladi" | Tushib qolgan apostrofni belgi n-gramlari ushlaydi |
| "Murojaatlarni tasodifiy bo'lish yetarli" | Mijoz bo'yicha — uslub va takrorlar bir tomonda |
| "Aniqlik 83% — yaxshi" | Firibgarlik recall i va precision i alohida |
| "Imlo xatolari asosiy xato manbai" | Xato tahlili: aralash murojaatlar, imlo emas |
| "Farq 2*SE dan katta — demak amaliy ahamiyatli" | 0.0036 sezilarli, lekin kichik; qoida — boshlang'ich nuqta |
| "sklearn modelini faqat pickle bilan saqlash mumkin" | Tenzorlar + lug'at + qayta yozilgan transformatsiya |
| "Test CV dan yuqori chiqsa — model zo'r" | Test va CV yaqin bo'lsa — bo'lish to'g'ri bo'lgan |
6. Keng tarqalgan xatolar va yechimlari
1. Tasodifiy bo'lish
train_test_split(matn, y, test_size=0.2) # ⚠️ bir mijoz ikki tomonda
guruhli_bolish(y, mijoz, 0.2, seed) # ✅ GroupShuffleSplit2. Lug'at butun ma'lumotda
lug = Lugat(matn, k.min_son) # ⚠️ testdagi so'zlar ham
lug = Lugat(matn[tr], k.min_son) # ✅ faqat o'quvdan3. EmbeddingBag ga padlangan matritsa
model.emb(x_pad) # ⚠️ pad tokenlar o'rtachaga kiradi
model.emb(torch.cat(xs), offsets) # ✅ 1D tokenlar + offsets4. Fold validatsiyasida erta to'xtatish
bag_orgat(k, m[tr], yy[tr], m[va], yy[va]) # ⚠️ va - ham to'xtatish, ham baho
a, b = guruhli_bolish(yy[tr], mj[tr], 0.15, k.seed) # ✅ to'xtatish fold o'quvi ichidan
bag_orgat(k, m[tr][a], yy[tr][a], m[tr][b], yy[tr][b])5. "Eng yuqori o'rtacha" bilan qaror
tanlov = max(SODDALIK, key=lambda n: ball[n].mean()) # ⚠️ farq shovqin bo'lishi mumkin
tanlov = next(n for n in SODDALIK if n == eng or not yomon[n]) # ✅ eng sodda munosib6. sklearn obyektini paketga qo'yish
torch.save({"model": pipeline}, yol); torch.load(yol, weights_only=True) # ⚠️ rad etiladi
torch.save({"W": torch.tensor(lr.coef_), "lugat": {str(g): int(i) ...}}, yol) # ✅7. Nazorat normallashtirilgan matnda
"nazorat": {"matn": [normallashtir(m) for m in namuna]} # ⚠️ normallashtirish tekshirilmaydi
"nazorat": {"matn": [str(m) for m in namuna]} # ✅ xom matn - butun zanjir7. Integratsiya — bu bilim qayerda kerak bo'ladi
- 21.5, 21.7, 21.12-darslar (o'tilgan):
Trainer, checkpoint vaweights_only, birinchi to'liq loyiha skeleti - 22.14-dars (o'tilgan): Xuddi shu skelet rasm bilan — stratifikatsiya, sinf vazni, paket
- 18-qism (o'tilgan):
GroupKFold, juftlashgan taqqoslash, xato narxi - 23.1–23.13-darslar (o'tilgan): Butun qism — normallashtirish, tokenizatsiya, TF-IDF, embedding, ketma-ketlik modellari, baholash tuzoqlari
- Transformerlar qismida: Oldindan o'rgatilgan model aralash murojaatlar va yangi so'zlarda yordam beradimi — xuddi shu
GroupKFoldva juftlashgan qaror bilan tekshiriladi - Katta til modellari qismida: O'rgatishsiz toifalash (zero-shot) — va uni shu darsdagi bazaviy bilan solishtirish
- MLOps qismida: Paketni xizmatga aylantirish, OOV ulushi va toifa taqsimotini kuzatish
8. Eng yaxshi amaliyotlar
Bo'lish birligini birinchi aniqlang — bu yerda mijoz.
Normallashtirishni birinchi qadam qiling va uning chegarasini biling — apostrof tushishi, imlo.
Lug'at va vektorizator faqat o'quvda; hamma narsa
Pipelineyoki fold ichida.Nomzodlarni soddalik tartibida yozing va ko'pchilik bazaviysini qo'shing.
Neyron modelni
Trainer, sinf vazni vadeepcopybilan o'rgating; bir necha seed bering.Qarorni
GroupKFolddagi juftlashgan farq va soddalik qoidasi bilan qabul qiling.Testni bir marta oching; toifa bo'yicha recall, precision va xato tahlilini hisobotga yozing.
Paketni faqat xavfsiz turlardan tuzing: tenzorlar, satr kalitli lug'at,
str(torch.__version__), xom nazorat namunasi.
9. Amaliy topshiriq
Vazifa 1: Bashorat qiling
1. # 5 toifada boshlang'ich loss taxminan?
2. # "otkazma" ni normallashtirish "o'tkazma" ga aylantiradimi?
3. # nega bo'lish mijoz bo'yicha?
4. # EmbeddingBag ga offsets nima uchun kerak?
5. # "doim karta" modelining makro-F1 i taxminan?
6. # fold ichida EmbeddingBag ni qaysi to'plamda erta to'xtatamiz?
7. # juftlashgan taqqoslashda "sezilarli yomon" mezoni?
8. # eng yaxshi va soddaroq model farqi 2*SE ichida - qaysi biri tanlanadi?
9. # firibgarlik uchun recall va precision dan qaysi biri muhimroq?
10. # xato tahlilida farqni qachon jiddiy deymiz?
11. # nega paketda sklearn Pipeline obyekti emas, tenzorlar?
12. # nazorat namunasi qaysi ko'rinishda saqlanadi?Javoblar
ln(5) = 1.609(1-misolda1.574)- Yo'q — faqat apostrof variantlarini birlashtiradi; tushib qolgan apostrofni tiklamaydi
- Bir mijozning uslubi va qayta yuborilgan matnlari bitta tomonda qolishi uchun
- Bitta 1D tensorda har murojaat qayerdan boshlanishini ko'rsatadi — pad kerak emas
- Taxminan
0.09(3-misolda0.0933) - Fold o'quvining ichidan ajratilgan bo'lakda (mijoz bo'yicha)
mean(d) > 2 * SE,SE = std(d) / sqrt(5)- Soddarog'i
- Odatda recall — o'tkazib yuborish qimmatroq; precision esa xavfsizlik bo'limi yukini belgilaydi
- Farq ikkala segment SE laridan yig'ilgan
2*SEdan katta bo'lsa torch.load(weights_only=True)pickle obyektlarini rad etadi; tenzorlar xavfsiz- Xom (normallashtirilmagan) matn — butun zanjirni tekshirish uchun
Vazifa 2: Xatolarni tuzating
1. tr, te = train_test_split(np.arange(len(y)), test_size=0.2, stratify=y)
2. v = TfidfVectorizer(analyzer="char_wb", ngram_range=(2, 5)).fit(matn)
X_tr, X_te = v.transform(matn[tr]), v.transform(matn[te])
3. tanlov = "EmbeddingBag" # neyron - zamonaviy
4. paket = {"model": model, "torch": torch.__version__}
5. print(f"test aniqligi {np.mean(p == y[te]):.3f}") # hisobotdagi yagona qatorJavoblar
1. ish, te = guruhli_bolish(y, mijoz, 0.2, k.seed)
2. model = belgi_tfidf(k).fit(matn[ish], y[ish]) # vektorizator Pipeline ichida
3. tanlov = next(n for n in SODDALIK if n == eng or not yomon[n])
4. paket = {"lugat": {str(g): int(i) for g, i in v.vocabulary_.items()},
"idf": torch.tensor(v.idf_), "W": torch.tensor(lr.coef_),
"b": torch.tensor(lr.intercept_),
"versiyalar": {"torch": str(torch.__version__)}}
5. print(f"makro-F1 {mf1:.3f}, firibgarlik recall {recall['firibgarlik']:.3f}, "
f"eng past: {eng_past} {recall[eng_past]:.3f}")Vazifa 3: Ma'lumot quvuri
Modellang:
- Mijoz bo'yicha va tasodifiy bo'lish — CV bahosi qanchaga farq qiladi?
- Normallashtirishsiz belgi TF-IDF — makro-F1 qanchaga tushadi?
- Apostrofni butunlay olib tashlaydigan normallashtirish ("o'tkazma" -> "otkazma") — so'z TF-IDF yaxshilanadimi?
- Lug'at
min_son= 1, 2, 5 —<unk>ulushi va makro-F1
Vazifa 4: O'rgatish
Modellang:
EmbeddingBagsinf vaznisiz — firibgarlik recall i qanday o'zgaradi?- Bigram tokenlarsiz
EmbeddingBag - LSTM 23.9-bob bilan solishtirish — vaqt va makro-F1
- 3 seed va jurnal yozuvi (konfig + metrika)
Vazifa 5: Taqqoslash
Modellang:
- 10 fold
GroupKFold— qaror o'zgaradimi? - So'z + belgi TF-IDF birlashmasi (
FeatureUnion) nomzod sifatida - Juftlashgan farq va SE jadvali hamma juftliklar uchun
- Qaror qoidasi va uning natijadan hisoblanishi
Vazifa 6: Topshirish
Modellang:
- Firibgarlik ehtimoli 0.3 dan yuqori bo'lsa — xavfsizlik navbatiga: recall va yuk qanday o'zgaradi?
- Aralash murojaatlar uchun "ikki navbatga yuborish" qoidasi
- So'z TF-IDF uchun paket va
Bashoratchi(so'z n-gramlari) - Hisobotga CV va test bahosini yonma-yon yozish
Vazifa 7: O'ylash
Model uch oy ishladi. Keyin bank yangi xizmat — "Bir martalik virtual karta" ni ishga tushirdi, va firibgarlar ham yangi usul topdi: "Sizning virtual kartangiz bloklandi, havolaga kiring" degan xabarlar. Model hisobotlarida xato yo'q, lekin xavfsizlik bo'limi "firibgarlik murojaatlari kamaydi" deb xavotirlanmoqda. Nima bo'lgan bo'lishi mumkin, qanday tekshirasiz va nima qilasiz?
Javob
Qisqa javob: firibgarlik kamaymagan bo'lishi mumkin — ular boshqa toifalarga "yashirinib" ketgan. Bu tushuncha siljishi (concept drift): yangi firibgarlik usuli eski so'zlar ("virtual karta", "bloklandi", "havola") bilan yoziladi, va bu so'zlar o'quvda asosan karta va ilova toifalariga tegishli edi.
Nima uchun aynan shunday:
1. So'zlar eski, ma'no yangi. "Virtual kartam bloklandi, havolaga kirdim" — model "karta" + "bloklandi" ni ko'radi va karta deydi. "Shubhali havola" o'quvda firibgarlik iborasi edi, lekin bu yerda u boshqa so'zlar orasida kam vaznga ega.
2. Aralash murojaatlar zaif joy edi. 4-misolda ikki toifali murojaatlarda xato 0.531 edi — yangi firibgarlik murojaatlari aynan shunday aralash ko'rinishda.
3. Hisobot o'zgarmaydi. Paketdagi nazorat namunasi faqat model o'zgarmaganini tasdiqlaydi. Test to'plami eski usullarni o'lchagan. Toifalar taqsimoti o'zgargani esa "xato" emas — uni kuzatish kerak.
Qanday tekshirasiz:
- Toifa taqsimotini haftalik kuzating: firibgarlik ulushi
0.079dan tushib,kartayokiilovaulushi oshganmi? - "virtual", "havola", "bloklandi" so'zlari birga kelgan murojaatlarni ajratib, 100 tasini qo'lda yorliqlang.
- Yangi so'zlar (
<unk>yoki yangi n-gramlar) ulushini kuzating. - Bashorat ishonchi taqsimotini tekshiring: past ishonchli murojaatlar ko'paygan bo'lishi mumkin.
Nima qilasiz:
- Qisqa muddat: xavfsizlik bo'limi bilan kalit so'zlar ro'yxati ("havola" + "bloklandi") — model yoniga oddiy qoida sifatida, firibgarlik navbatiga majburiy nusxa bilan. Bu vaqtinchalik, lekin tez.
- O'rta muddat: yangi usul bo'yicha yorliqlangan murojaatlarni qo'shib, qayta o'rgatish va xuddi shu
GroupKFold+ juftlashgan taqqoslash bilan eski model bilan solishtirish. - Uzoq muddat: toifa taqsimoti, OOV ulushi va past ishonch ulushini avtomatik kuzatish (MLOps qismida) va xavfsizlik bo'limidan yangi firibgarlik usullari haqida muntazam xabar olish jarayoni.
Muhim nuans: firibgarlik — raqib bilan ishlaydigan toifa. Firibgarlar modelni bilmaydi, lekin ularning usullari doim o'zgaradi — shuning uchun bu toifa uchun model hech qachon "tugagan" bo'lmaydi. Hisobotdagi "CHEKLOVLAR: yangi firibgarlik usullari tekshirilmagan" qatori aynan shu vaziyat haqidagi ogohlantirish edi.
Nimani mustahkamlaydi: 2.2, 2.4, 2.5, 2.6-bo'limlar.
Xulosa
Bu darsda to'liq NLP loyihasini qurdik: xom murojaatlardan topshiriladigan paketgacha.
Eng muhim uch fikr:
Skelet 21.12 niki, matnga xos qo'shimchalar — normallashtirish, o'quvdagi lug'at va mijoz bo'yicha bo'lish. 1-misolda 4542 ta murojaatning
41.9%ida nostandart apostrof bor edi; normallashtirish noyob so'zlarni2100dan1976ga tushirdi, lekin "o'tkaz-" oilasi baribir12xil yozilishda qoldi. Mijoz bo'yicha bo'lishda ish va test orasida umumiy mijoz0, tez tekshiruvlar esa (boshlang'ich loss1.574valn 5 = 1.609, bitta batch testi) quvurni o'rgatishdan oldin tasdiqladi.Qaror juftlashgan farq bilan: neyron model murakkabligini oqlamadi.
GroupKFoldda belgi TF-IDF0.8051, so'z TF-IDF0.8015,EmbeddingBag0.7996makro-F1 berdi. So'z TF-IDF sezilarli yomon chiqdi (+0.0036,2*SE = 0.0032),EmbeddingBagbilan farq sezilmadi — qoida belgi TF-IDF ni tanladi. Test bir marta ochildi: makro-F10.8115(CV ga yaqin), firibgarlik recall i0.821, eng zaif toifailova(0.741), eng xavfli segment esa ikki toifaning iborasi bor murojaatlar (0.531xato, qolganlarida0.118).sklearn modeli ham xavfsiz paketga aylandi. Lug'at (
7027ta satr kalitli n-gram), IDF, vaznlar vastr(torch.__version__)tenzorlar va oddiy turlar sifatida saqlandi;BashoratchiTF-IDF ni torch da qayta hisoblab, xom nazorat namunasida sklearn logitlari bilan mos keldi va yangi murojaatlarni, shu jumladan apostrofi tushganini, to'g'ri toifaladi.
Bu bilan 23-qism — NLP va ketma-ketliklar yakunlandi. Endi bizda matn bilan ishlashning to'liq zanjiri bor: apostrof va normallashtirishdan tokenizatsiya va BPE gacha, TF-IDF dan embedding va LSTM gacha, til modeli va diqqat mexanizmidan sentiment tahlili va halol baholashgacha, va nihoyat — tekshiriladigan va topshiriladigan loyihagacha. Keyingi qism — Transformerlar: 23.11-darsda ko'rgan diqqat mexanizmi butun arxitekturaning asosiga aylanadi, va biz bu yerda qurgan baholash skeleti oldindan o'rgatilgan modellar bazaviydan haqiqatan yaxshimi degan savolga javob berishda yana kerak bo'ladi.
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!