IlmHamroh
Data Science va sun'iy intellekt/NLP5/14-dars34 daqiqa
Mundarija (21)

23.5-dars: Matn klassifikatsiyasi — klassik bazaviylar

23-QISM — NLP VA KETMA-KETLIKLAR · 5-dars


1. Kirish va motivatsiya

Oldingi to'rt darsda matnni songa aylantirishni o'rgandik: 23.1-darsda Unicode va o'zbekcha apostrof variantlarini normallashtirdik, 23.2-darsda tokenizatsiya va lug'at qurdik, 23.3-darsda BPE bilan so'z qismlariga ajratdik, 23.4-darsda esa matnni bag-of-words va TF-IDF vektoriga aylantirdik. Endi bu vektorlardan birinchi foydali natijani olamiz: matnni sinflarga ajratamiz.

Bu darsda ishlatadigan ma'lumot — o'zbekcha mahsulot sharhlari: "kecha telefon oldim ekrani juda zo'r lekin batareyasi sust". Har sharh uch sinfdan biriga tegishli: salbiy, neytral, ijobiy. Sharhlarni kod ichida yaratamiz (shablon + lug'at + tasodif), lekin ularni atayin real ko'rinishli qilamiz: turli uzunlik, sinonimlar ("zo'r", "a'lo", "ajoyib"), inkor ("sifatli emas", "muammo yo'q"), aralash fikrlar ("... lekin ...") va taxminan har 15 so'zdan birida imlo xatosi.

Nega "klassik" modellar? Chunki keyingi darslarda embedding, RNN va boshqa neyron modellarni ko'ramiz — va ularning har birini nimadir bilan solishtirish kerak. TF-IDF + logistik regressiya bir necha soniyada o'rganadi, deyarli sozlashsiz ishlaydi va ko'p amaliy vazifalarda hali ham kuchli raqib. Uni o'lchamay turib neyron tarmoq qurish — 20.12-darsda tabular ma'lumotda ko'rgan xatoning aynan o'zi.

Real vaziyat. Onlayn do'kon jamoasi sharhlarni avtomatik saralash uchun katta neyron model o'rgatdi va 84% aniqlikdan xursand bo'ldi. Keyinroq kimdir oddiy TF-IDF + logistik regressiyani sinab ko'rdi: 83%, o'rgatish vaqti 1 soniya, har bashorat mikrosekundlarda, va model qaysi so'zlarga qarab qaror qilishini ko'rsata oladi. Bir foiz punkt farq esa bir necha tasodifiy bo'linishda sezilarli chiqmadi. Bu darsda aynan shunday o'lchashni o'rganamiz.

Bu darsda matn klassifikatsiyasi uchun kuchli va halol bazaviyni quramiz.

Bu darsda:

  • Vazifa va eng oddiy bazaviy
  • Pipeline: vektorizator faqat o'quvda
  • Uch klassik model: logistik regressiya, Naive Bayes, LinearSVC
  • So'z va belgi n-gramlari
  • CV bilan juftlashgan taqqoslash
  • Model nimani o'rgandi va xato tahlili
  • Tuzoqlar

ℹ Misollar real numpy/sklearn bilan (Python 3.14).


2. Nazariya — chuqur tushuntirish

2.1. Vazifa va eng oddiy bazaviy

text
MATN KLASSIFIKATSIYASI:
  kirish:  matn (turli uzunlikdagi so'zlar ketma-ketligi)
  chiqish: sinf (salbiy / neytral / ijobiy, spam / spam emas, mavzu ...)

ZANJIR:
  xom matn -> normallashtirish 23.1-bob -> tokenlar 23.2-bob
           -> vektor (TF-IDF, 23.4) -> klassifikator -> sinf

ENG ODDIY BAZAVIY (DummyClassifier):
  hamma sharhga eng ko'p uchraydigan sinfni aytadi
  3 ta teng sinfda aniqlik ~ 0.33
  har qanday model shundan ANCHA yaxshi bo'lishi kerak

BIZNING KORPUS:
  3000 sharh, 3 sinf (taxminan teng)
  yorliq = gaplar "ball"larining o'rtachasi (+1, 0, -1) + ozgina shovqin
    o'rtacha > 0.35 -> ijobiy, < -0.35 -> salbiy, aks holda neytral
    "... sifatli ham ... sifatli boshqa olmayman" -> (1 + 1 - 1) / 3 = 0.33
    -> NEYTRAL (ikki maqtov va bitta shikoyat)
  inkor: "sifatli emas" -> salbiy, "sust emas" -> ijobiy
  imlo xatolari: so'zlarning ~7% ida (tushib qolgan, ikkilangan,
                 o'rin almashgan harf)

Birinchi qadam — DummyClassifier — u "model hech narsa o'rganmaganda" qanday natija bo'lishini ko'rsatadi.

2.2. Pipeline: vektorizator faqat o'quvda

text
TF-IDF HAM O'RGANADI:
  fit:  lug'at (qaysi so'zlar/n-gramlar bor) + IDF (har so'z nechta
        hujjatda uchraydi)
  bu statistikalar - modelning bir qismi

NOTO'G'RI (leakage, 19-qism):
  vek = TfidfVectorizer().fit(barcha_matnlar)   # test ham ichida
  X = vek.transform(barcha_matnlar)
  train_test_split(X, y) ...
  -> lug'atda FAQAT testda uchragan so'zlar bor
  -> IDF test hujjatlarini ham sanagan

TO'G'RI:
  model = Pipeline([("vek", TfidfVectorizer()),
                    ("clf", LogisticRegression())])
  model.fit(X_oquv, y_oquv)        # vektorizator faqat o'quvda
  cross_val_score(model, X, y)     # har foldda QAYTA fit

AMALDA:
  TF-IDF dagi leakage odatda kichik natija farqini beradi
  lekin u "kichik" ekanini bilmaysiz - tekshirmaguncha
  Pipeline bilan bu savol umuman tug'ilmaydi

Vektorizator — modelning bir qismi; u Pipeline ichida faqat o'quv ma'lumotida fit qilinadi.

2.3. Uch klassik model

text
LOGISTIK REGRESSIYA (14-qism):
  har sinf uchun: ball_k = w_k . x + b_k,  softmax -> ehtimollar
  C - regularizatsiya teskarisi (katta C = kuchsiz regularizatsiya)
  + ehtimol beradi, koeffitsiyentlarni o'qish mumkin 2.6-bob

MULTINOMIAL NAIVE BAYES:
  P(sinf | matn) ~ P(sinf) * ko'paytma P(so'z | sinf)
  "so'zlar sinf ichida bir-biriga bog'liq emas" degan sodda faraz
  alpha - silliqlash (ko'rilmagan so'z uchun nol ehtimol bo'lmasin)
  + juda tez, juda kam ma'lumotda ham ishlaydi
  - "sifatli" va "emas" ni alohida sanaydi - inkorni tushunmaydi

LinearSVC:
  sinflar orasidagi chegarani maksimal "oraliq" bilan qidiradi
  C - regularizatsiya teskarisi
  + siyrak, ko'p o'lchamli matn vektorlarida kuchli
  - ehtimol bermaydi (faqat decision_function)

UMUMIY XOSSA:
  uchalasi ham CHIZIQLI: har belgi (so'z/n-gram) o'z og'irligini oladi
  matn vektori 10 000+ o'lchamli bo'lsa ham o'rgatish soniyalar oladi

Matnda chiziqli modellar kuchli — o'lcham katta, ma'lumot siyrak, va har so'z o'z "ovozi"ni beradi.

2.4. So'z va belgi n-gramlari

text
SO'Z UNIGRAMLARI (ngram_range=(1, 1)):
  "sifati sifatli emas" -> {sifati, sifatli, emas}
  "sifatli" ijobiy og'irlik oladi, "emas" - deyarli neytral
  -> inkor YO'QOLADI

SO'Z BIGRAMLARI (ngram_range=(1, 2)):
  + {sifati sifatli, sifatli emas}
  "sifatli emas" - alohida belgi, o'z (salbiy) og'irligi bilan

BELGI N-GRAMLARI:
  analyzer="char_wb", (2, 4): faqat SO'Z ICHIDA
    "sifatli" -> " s", "si", "if", ... "atli ", ...
    so'zlar chegarasidan o'tmaydi -> "emas" bilan bog'lanish yo'q
  analyzer="char", (2, 4): bo'shliqdan ham o'tadi
    "li em", "i ema" -> inkor ham ko'rinadi

IMLO XATOSI:
  "sifatli" -> "sifatil" (harflar o'rin almashgan)
  so'z darajasida: butunlay yangi so'z, lug'atda yo'q -> e'tiborsiz
  belgi darajasida: "sif", "ifa", "fat" saqlanadi -> signal qisman qoladi

NARX:
  har matnda nol bo'lmagan belgilar ~10 barobar ko'p
  (bizning korpusda o'rtacha: so'z+bigram ~24, belgi (2, 4) ~230)
  -> vektorlash, o'rgatish va bashorat sekinroq

Bigramlar inkorni, belgi n-gramlari imlo xatosini "ko'radi" — qaysi biri muhimligini ma'lumot va o'lchov hal qiladi.

2.5. CV bilan juftlashgan taqqoslash

text
BITTA BO'LINISH YETMAYDI:
  1000 ta test sharhida aniqlikning SE si ~ sqrt(0.8*0.2/1000) ~ 0.013
  0.01 farq - shovqin ichida bo'lishi mumkin

JUFTLASHGAN CV (18-qism):
  cv = StratifiedKFold(5, shuffle=True, random_state=0)
  HAMMA modelga BIR XIL foldlar
  har foldda: d_k = aniq_A_k - aniq_B_k
  o'rtacha d, SE = std(d, ddof=1) / sqrt(5)
  |d| > 2 * SE -> sezilarli (taxminiy qoida)

QAROR QOIDASI:
  1. eng yaxshi o'rtacha natijali modelni toping
  2. har modelni u bilan juftlab solishtiring
  3. "sezilarli yomon EMAS" modellar ichidan ENG SODDASINI tanlang

SODDALIK TARTIBI (bizning darsda):
  NB so'z < LR so'z < NB so'z+bigram < LR so'z+bigram < SVC so'z+bigram
  < LR belgi (so'z ichida) < LR belgi
  (belgi modellari: lug'at katta, sekin, tushuntirish qiyin)

Qarorni farq va SE hal qiladi — va teng natijalar ichida eng sodda model yutadi.

2.6. Model nimani o'rgandi va xato tahlili

text
KOEFFITSIYENTLAR (logistik regressiya):
  clf.coef_ shakli (n_sinf, n_belgi)
  har sinf uchun eng katta musbat og'irlikli belgilar -
  "bu sinfga eng kuchli ovoz beradigan so'zlar"
  vek.get_feature_names_out() - indeks -> so'z

NIMANI TEKSHIRAMIZ:
  mantiqiy so'zlarmi? ("zo'r", "sust emas" -> ijobiy)
  "shubhali" so'zlarmi? (mahsulot nomi, sana, do'kon nomi ->
    ma'lumotda noxolis bog'liqlik yoki leakage belgisi)

XATO TAHLILI:
  chalkashlik matritsasi: qaysi sinflar adashadi?
    odatda neytral <-> ijobiy/salbiy, salbiy <-> ijobiy kam
  guruhlar bo'yicha aniqlik: inkorli / inkorsiz, qisqa / uzun
  eng ISHONCHLI xatolar: ko'pincha yorliq xatosi yoki model
    tushunmaydigan tuzilish ("... emas", "... lekin ...")

KEYINGI QADAM:
  xato guruhi -> belgi (bigram), ma'lumot yoki boshqa model

Koeffitsiyentlar va xatolar — modelning "tushuntirish xati"; ular raqamlar yashirgan muammolarni ochadi.

2.7. Tuzoqlar

Asosiy tuzoqlar: vektorizatorni butun ma'lumotda fit qilish (leakage); DummyClassifier bazaviysiz "84% — yaxshi" deyish; sukut token_pattern o'zbekcha apostrofli so'zlarni bo'lib yuborishi ("zo'r" → "zo"); unigramlar bilan inkorni yo'qotish; bitta bo'linishdagi kichik farqqa ishonish; turli modellarni turli foldlarda baholash; MultinomialNB ga manfiy qiymatli belgi berish (masalan, StandardScaler dan keyin); LinearSVC dan ehtimol kutish; koeffitsiyentlarni ko'rmay modelni ishlab chiqarishga chiqarish.


3. Tez ma'lumotnoma

python
from sklearn.dummy import DummyClassifier
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.naive_bayes import MultinomialNB
from sklearn.pipeline import make_pipeline
from sklearn.svm import LinearSVC

TP = r"[\w']+"                     # apostrofli so'zlar butun qoladi

soz = make_pipeline(
    TfidfVectorizer(token_pattern=TP, ngram_range=(1, 2)),
    LogisticRegression(C=10, max_iter=2000))
belgi = make_pipeline(
    TfidfVectorizer(analyzer="char", ngram_range=(2, 4)),
    LogisticRegression(C=10, max_iter=2000))
nb = make_pipeline(TfidfVectorizer(token_pattern=TP, ngram_range=(1, 2)),
                   MultinomialNB(alpha=0.1))
svc = make_pipeline(TfidfVectorizer(token_pattern=TP, ngram_range=(1, 2)),
                    LinearSVC(C=0.5))

cv = StratifiedKFold(5, shuffle=True, random_state=0)
a = cross_val_score(soz, matnlar, y, cv=cv)      # bir xil foldlar
b = cross_val_score(belgi, matnlar, y, cv=cv)
d = b - a
se = d.std(ddof=1) / len(d) ** 0.5

vek, clf = soz.fit(X_oquv, y_oquv).named_steps.values()
nomlar = vek.get_feature_names_out()
top = nomlar[clf.coef_[2].argsort()[::-1][:10]]  # ijobiy sinf so'zlari

Matn klassifikatsiyasi xulosasi

DummyClassifier -> TF-IDF + LogReg (Pipeline) -> n-gramlar
token_pattern apostrofni saqlasin
bigram - inkor, belgi n-gram - imlo xatosi
juftlashgan CV: bir xil foldlar, farq + SE
sezilarli yomon bo'lmagan ENG SODDA model
koeffitsiyentlar va eng ishonchli xatolarni ko'ring

4. Batafsil misollar

Misollar real numpy/sklearn bilan (Python 3.14).

Misol 1 — Korpus, bazaviy va Pipeline

python
"""Sintetik o'zbekcha sharhlar: bazaviy, Pipeline va leakage."""

import numpy as np
from sklearn.dummy import DummyClassifier
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, f1_score
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline

IJOBIY = ["yaxshi", "zo'r", "a'lo", "ajoyib", "sifatli", "qulay",
          "chiroyli", "mustahkam"]
SALBIY = ["yomon", "sifatsiz", "noqulay", "xunuk", "nosoz", "mo'rt",
          "sust", "chatoq"]
NEYTRAL = ["o'rtacha", "oddiy", "normal", "odatiy", "kutilgandek"]
JIHAT = ["sifati", "narxi", "ekrani", "batareyasi", "ovozi", "qadoqlash",
         "yetkazib berish", "sotuvchi", "dizayni", "materiali"]
MAHSULOT = ["telefon", "noutbuk", "quloqchin", "soat", "choynak", "kurtka",
            "krossovka", "sumka", "planshet", "kolonka"]
BOSH = ["kecha", "o'tgan hafta", "bir oy oldin", "do'kondan", "internetdan",
        "sovg'a uchun", "akam uchun", ""]
FEL = ["oldim", "buyurtma qildim", "sotib oldim", "ishlatib ko'rdim"]
YAKUN = {1: ["tavsiya qilaman", "yana olaman", "hammaga maslahat beraman"],
         -1: ["tavsiya qilmayman", "boshqa olmayman", "pulimga achinaman"],
         0: ["hozircha shu", "vaqt ko'rsatadi", "yana ishlatib ko'ramiz"]}
ULUSH = {1: [0.8, 0.1, 0.1], -1: [0.1, 0.8, 0.1], 0: [0.15, 0.15, 0.7]}
SINFLAR = ["salbiy", "neytral", "ijobiy"]
TP = r"[\w']+"


def gap(qutb, rng):
    j = rng.choice(JIHAT)
    juda = "juda " if rng.random() < 0.3 else ""
    r = rng.random()
    if qutb == 1:
        if r < 0.2:
            return f"{j} {rng.choice(SALBIY)} emas"
        if r < 0.3:
            return f"{j} bilan muammo yo'q"
        return f"{j} {juda}{rng.choice(IJOBIY)}"
    if qutb == -1:
        if r < 0.25:
            return f"{j} {rng.choice(IJOBIY)} emas"
        if r < 0.35:
            return f"{j} bilan muammo bor"
        return f"{j} {juda}{rng.choice(SALBIY)}"
    return f"{j} {rng.choice(NEYTRAL)}"


def sharh(rng):
    """Kayfiyat -> gaplar -> yorliq (gaplar ballining o'rtachasi)."""
    kayfiyat = int(rng.integers(-1, 2))
    qismlar = [f"{rng.choice(BOSH)} {rng.choice(MAHSULOT)} "
               f"{rng.choice(FEL)}".strip()]
    ballar = []
    for t in range(int(rng.integers(1, 5))):
        q = int(rng.choice([1, -1, 0], p=ULUSH[kayfiyat]))
        bog = rng.choice(["lekin", "va", "ammo", "ham", ""]) if t else ""
        qismlar.append(f"{bog} {gap(q, rng)}".strip())
        ballar.append(q)
    if rng.random() < 0.5:
        q = int(rng.choice([1, -1, 0], p=ULUSH[kayfiyat]))
        qismlar.append(rng.choice(YAKUN[q]))
        ballar.append(q)
    ball = np.mean(ballar) + rng.normal(0, 0.1)
    return " ".join(qismlar), 2 if ball > 0.35 else (0 if ball < -0.35 else 1)


def imlo_xato(matn, ulush, rng):
    chiqish = []
    for s in matn.split():
        if len(s) >= 4 and rng.random() < ulush:
            i = int(rng.integers(1, len(s) - 1))
            tur = int(rng.integers(0, 3))
            if tur == 0:
                s = s[:i] + s[i + 1:]
            elif tur == 1:
                s = s[:i] + s[i] + s[i:]
            else:
                s = s[:i - 1] + s[i] + s[i - 1] + s[i + 1:]
        chiqish.append(s)
    return " ".join(chiqish)


def korpus(n, seed=0, xato=0.07):
    rng = np.random.default_rng(seed)
    toza, y = zip(*[sharh(rng) for _ in range(n)])
    rng2 = np.random.default_rng(seed + 1000)
    return [imlo_xato(t, xato, rng2) for t in toza], np.array(y), list(toza)


def main() -> None:
    matnlar, y, toza = korpus(3000)

    print("=== 1. Korpus ===")
    soni = np.bincount(y)
    for k, nom in enumerate(SINFLAR):
        print(f"  {nom:<8} {soni[k]:>5} ({soni[k] / len(y):.1%})")
    uzun = np.array([len(m.split()) for m in matnlar])
    print(f"  so'zlar soni: min {uzun.min()}, mediana "
          f"{int(np.median(uzun))}, max {uzun.max()}")
    toza_soz = [s for t in toza for s in t.split()]
    xato_soz = [s for t in matnlar for s in t.split()]
    ozgargan = np.mean([a != b for a, b in zip(toza_soz, xato_soz)])
    print(f"  imlo xatosi bor so'zlar: {ozgargan:.1%}")
    for k in (2, 1, 0):
        i = int(np.where(y == k)[0][0])
        print(f"  [{SINFLAR[k]}] {matnlar[i]}")

    Xtr, Xte, ytr, yte = train_test_split(matnlar, y, test_size=0.2,
                                          random_state=0, stratify=y)
    print(f"\n  o'quv {len(Xtr)}, test {len(Xte)}")

    print("\n=== 2. Eng oddiy bazaviy ===")
    dummy = DummyClassifier(strategy="most_frequent").fit(Xtr, ytr)
    print(f"  DummyClassifier aniqligi: "
          f"{accuracy_score(yte, dummy.predict(Xte)):.4f}")

    print("\n=== 3. Pipeline: TF-IDF (so'z + bigram) + LogReg ===")
    model = make_pipeline(
        TfidfVectorizer(token_pattern=TP, ngram_range=(1, 2)),
        LogisticRegression(C=10, max_iter=2000))
    model.fit(Xtr, ytr)
    p = model.predict(Xte)
    print(f"  test aniqligi: {accuracy_score(yte, p):.4f}")
    f1 = f1_score(yte, p, average=None)
    for k, nom in enumerate(SINFLAR):
        print(f"  F1 {nom:<8} {f1[k]:.4f}")
    print(f"  lug'at hajmi (faqat o'quv): "
          f"{len(model[0].vocabulary_)} belgi")

    print("\n=== 4. Sukut token_pattern va apostrof ===")
    sukut = TfidfVectorizer().fit(["sotuvchi zo'r va a'lo"])
    print(f"  sukut:   {sorted(sukut.vocabulary_)}")
    bizniki = TfidfVectorizer(token_pattern=TP).fit(["sotuvchi zo'r va a'lo"])
    print(f"  [\\w']+: {sorted(bizniki.vocabulary_)}")

    print("\n=== 5. Leakage: vektorizator butun ma'lumotda ===")
    vek_xato = TfidfVectorizer(token_pattern=TP, ngram_range=(1, 2))
    vek_xato.fit(matnlar)
    faqat_test = set(vek_xato.vocabulary_) - set(model[0].vocabulary_)
    print(f"  butun ma'lumot lug'ati: {len(vek_xato.vocabulary_)} belgi")
    print(f"  shundan faqat testda uchraganlari: {len(faqat_test)}")
    misollar = sorted(f for f in faqat_test if " " not in f)[:5]
    print(f"  masalan: {misollar}")
    clf = LogisticRegression(C=10, max_iter=2000)
    clf.fit(vek_xato.transform(Xtr), ytr)
    aniq_xato = accuracy_score(yte, clf.predict(vek_xato.transform(Xte)))
    aniq_togri = accuracy_score(yte, p)
    print(f"  leakage bilan test aniqligi: {aniq_xato:.4f} "
          f"(Pipeline: {aniq_togri:.4f})")
    if abs(aniq_xato - aniq_togri) < 0.01:
        print("  bu yerda farq 0.01 dan kichik - lekin buni faqat "
              "o'lchab bildik")
    else:
        print("  farq sezilarli - leakage bahoni buzdi")
    print("  ⭐ Vektorizator Pipeline ichida - leakage savoli tug'ilmaydi")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Korpus ===
  salbiy     941 (31.4%)
  neytral   1040 (34.7%)
  ijobiy    1019 (34.0%)
  so'zlar soni: min 4, mediana 13, max 25
  imlo xatosi bor so'zlar: 6.6%
  [ijobiy] sumka sotib oldim qadoqlash xunuk emas
  [neytral] sovg'a uchun kurtka buyurtma qildim sifati sifatli ham yetkazib berish sifatli boshqa olmayman
  [salbiy] akam uchun noutbuk oldim sifati a'lo emas lekin yetkazib berish sust va dizayni kutilganek ham batareyasi juda sust boshqa olmayman

  o'quv 2400, test 600

=== 2. Eng oddiy bazaviy ===
  DummyClassifier aniqligi: 0.3467

=== 3. Pipeline: TF-IDF (so'z + bigram) + LogReg ===
  test aniqligi: 0.8150
  F1 salbiy   0.8248
  F1 neytral  0.7536
  F1 ijobiy   0.8675
  lug'at hajmi (faqat o'quv): 4612 belgi

=== 4. Sukut token_pattern va apostrof ===
  sukut:   ['lo', 'sotuvchi', 'va', 'zo']
  [\w']+: ["a'lo", 'sotuvchi', 'va', "zo'r"]

=== 5. Leakage: vektorizator butun ma'lumotda ===
  butun ma'lumot lug'ati: 5251 belgi
  shundan faqat testda uchraganlari: 639
  masalan: ['aakm', 'achinamaan', 'ahinaman', 'amteriali', 'batareasi']
  leakage bilan test aniqligi: 0.8167 (Pipeline: 0.8150)
  bu yerda farq 0.01 dan kichik - lekin buni faqat o'lchab bildik
  ⭐ Vektorizator Pipeline ichida - leakage savoli tug'ilmaydi

Nima ko'rsatdi: 2.1, 2.2-bo'limlar.

Misol 2 — Uch model va n-gramlar: juftlashgan CV

python
"""8 ta klassik model, bir xil 5 fold, juftlashgan farq va qaror."""

import warnings

import numpy as np
from sklearn.exceptions import ConvergenceWarning
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import StratifiedKFold, cross_validate
from sklearn.naive_bayes import MultinomialNB
from sklearn.pipeline import make_pipeline
from sklearn.svm import LinearSVC

IJOBIY = ["yaxshi", "zo'r", "a'lo", "ajoyib", "sifatli", "qulay",
          "chiroyli", "mustahkam"]
SALBIY = ["yomon", "sifatsiz", "noqulay", "xunuk", "nosoz", "mo'rt",
          "sust", "chatoq"]
NEYTRAL = ["o'rtacha", "oddiy", "normal", "odatiy", "kutilgandek"]
JIHAT = ["sifati", "narxi", "ekrani", "batareyasi", "ovozi", "qadoqlash",
         "yetkazib berish", "sotuvchi", "dizayni", "materiali"]
MAHSULOT = ["telefon", "noutbuk", "quloqchin", "soat", "choynak", "kurtka",
            "krossovka", "sumka", "planshet", "kolonka"]
BOSH = ["kecha", "o'tgan hafta", "bir oy oldin", "do'kondan", "internetdan",
        "sovg'a uchun", "akam uchun", ""]
FEL = ["oldim", "buyurtma qildim", "sotib oldim", "ishlatib ko'rdim"]
YAKUN = {1: ["tavsiya qilaman", "yana olaman", "hammaga maslahat beraman"],
         -1: ["tavsiya qilmayman", "boshqa olmayman", "pulimga achinaman"],
         0: ["hozircha shu", "vaqt ko'rsatadi", "yana ishlatib ko'ramiz"]}
ULUSH = {1: [0.8, 0.1, 0.1], -1: [0.1, 0.8, 0.1], 0: [0.15, 0.15, 0.7]}
TP = r"[\w']+"


def gap(qutb, rng):
    j = rng.choice(JIHAT)
    juda = "juda " if rng.random() < 0.3 else ""
    r = rng.random()
    if qutb == 1:
        if r < 0.2:
            return f"{j} {rng.choice(SALBIY)} emas"
        if r < 0.3:
            return f"{j} bilan muammo yo'q"
        return f"{j} {juda}{rng.choice(IJOBIY)}"
    if qutb == -1:
        if r < 0.25:
            return f"{j} {rng.choice(IJOBIY)} emas"
        if r < 0.35:
            return f"{j} bilan muammo bor"
        return f"{j} {juda}{rng.choice(SALBIY)}"
    return f"{j} {rng.choice(NEYTRAL)}"


def sharh(rng):
    kayfiyat = int(rng.integers(-1, 2))
    qismlar = [f"{rng.choice(BOSH)} {rng.choice(MAHSULOT)} "
               f"{rng.choice(FEL)}".strip()]
    ballar = []
    for t in range(int(rng.integers(1, 5))):
        q = int(rng.choice([1, -1, 0], p=ULUSH[kayfiyat]))
        bog = rng.choice(["lekin", "va", "ammo", "ham", ""]) if t else ""
        qismlar.append(f"{bog} {gap(q, rng)}".strip())
        ballar.append(q)
    if rng.random() < 0.5:
        q = int(rng.choice([1, -1, 0], p=ULUSH[kayfiyat]))
        qismlar.append(rng.choice(YAKUN[q]))
        ballar.append(q)
    ball = np.mean(ballar) + rng.normal(0, 0.1)
    return " ".join(qismlar), 2 if ball > 0.35 else (0 if ball < -0.35 else 1)


def imlo_xato(matn, ulush, rng):
    chiqish = []
    for s in matn.split():
        if len(s) >= 4 and rng.random() < ulush:
            i = int(rng.integers(1, len(s) - 1))
            tur = int(rng.integers(0, 3))
            if tur == 0:
                s = s[:i] + s[i + 1:]
            elif tur == 1:
                s = s[:i] + s[i] + s[i:]
            else:
                s = s[:i - 1] + s[i] + s[i - 1] + s[i + 1:]
        chiqish.append(s)
    return " ".join(chiqish)


def korpus(n, seed=0, xato=0.07):
    rng = np.random.default_rng(seed)
    toza, y = zip(*[sharh(rng) for _ in range(n)])
    rng2 = np.random.default_rng(seed + 1000)
    return [imlo_xato(t, xato, rng2) for t in toza], np.array(y), list(toza)


def soz(ng):
    return TfidfVectorizer(token_pattern=TP, ngram_range=ng)


def belgi(analyzer):
    return TfidfVectorizer(analyzer=analyzer, ngram_range=(2, 4))


def lr():
    return LogisticRegression(C=10, max_iter=3000)


def main() -> None:
    matnlar, y, _ = korpus(3000)
    # modellar SODDALIK tartibida (2.5-bo'lim)
    modellar = {
        "NB so'z": make_pipeline(soz((1, 1)), MultinomialNB(alpha=0.1)),
        "LR so'z": make_pipeline(soz((1, 1)), lr()),
        "NB so'z+bi": make_pipeline(soz((1, 2)), MultinomialNB(alpha=0.1)),
        "LR so'z+bi": make_pipeline(soz((1, 2)), lr()),
        "SVC so'z+bi": make_pipeline(soz((1, 2)),
                                     LinearSVC(C=0.5, max_iter=5000)),
        "LR char_wb": make_pipeline(belgi("char_wb"), lr()),
        "LR char": make_pipeline(belgi("char"), lr()),
    }
    cv = StratifiedKFold(5, shuffle=True, random_state=0)

    natija, lugat = {}, {}
    with warnings.catch_warnings():
        # LinearSVC ba'zi foldlarda to'liq yaqinlashmasligi mumkin;
        # natijaga ta'siri yo'q, ogohlantirish chiqishni ifloslamasin
        warnings.simplefilter("ignore", ConvergenceWarning)
        for nom, m in modellar.items():
            r = cross_validate(m, matnlar, y, cv=cv, return_estimator=True)
            natija[nom] = r["test_score"]
            lugat[nom] = len(r["estimator"][0][0].vocabulary_)

    print("=== 1. 5-fold CV aniqligi (bir xil foldlar) ===")
    print(f"  {'model':<13} {'lug_at':>7} {'o_rtacha':>9} {'min':>7} "
          f"{'max':>7}")
    for nom, s in natija.items():
        print(f"  {nom:<13} {lugat[nom]:>7} {s.mean():>9.4f} "
              f"{s.min():>7.4f} {s.max():>7.4f}")
    print("  (lug'at - 1-fold o'quv qismidagi belgilar soni)")

    print("\n=== 2. Eng yaxshisi bilan juftlashgan farq ===")
    eng = max(natija, key=lambda k: natija[k].mean())
    print(f"  eng yaxshi: {eng}")
    print(f"  {'model':<13} {'farq':>8} {'SE':>7} {'xulosa':>22}")
    tanlov = None
    for nom, s in natija.items():
        if nom == eng:
            print(f"  {nom:<13} {'-':>8} {'-':>7} {'eng yaxshi':>22}")
            if tanlov is None:
                tanlov = nom
            continue
        d = s - natija[eng]
        se = d.std(ddof=1) / np.sqrt(len(d))
        yomon = d.mean() < -2 * se
        xulosa = "sezilarli yomon" if yomon else "farq sezilarli emas"
        print(f"  {nom:<13} {d.mean():>+8.4f} {se:>7.4f} {xulosa:>22}")
        if not yomon and tanlov is None:
            tanlov = nom
    print(f"\n  QAROR (sezilarli yomon bo'lmagan eng sodda): {tanlov}")

    print("\n=== 3. Bigram va belgi n-gramlari nima qo'shdi ===")
    for a, b in [("LR so'z", "LR so'z+bi"), ("NB so'z", "NB so'z+bi"),
                 ("LR char_wb", "LR char")]:
        d = natija[b] - natija[a]
        se = d.std(ddof=1) / np.sqrt(len(d))
        belgi_ = "sezilarli" if abs(d.mean()) > 2 * se else "sezilarli emas"
        print(f"  {b:<11} - {a:<11} {d.mean():>+8.4f}  SE {se:.4f}  {belgi_}")
    print("  ⭐ Qaror jadvaldan kod bilan chiqdi - oldindan yozilmagan")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. 5-fold CV aniqligi (bir xil foldlar) ===
  model          lug_at  o_rtacha     min     max
  NB so'z           861    0.6627  0.6533  0.6767
  LR so'z           861    0.7197  0.6967  0.7383
  NB so'z+bi       4619    0.7667  0.7533  0.7783
  LR so'z+bi       4619    0.8073  0.7950  0.8300
  SVC so'z+bi      4619    0.8123  0.8000  0.8283
  LR char_wb       4155    0.7327  0.7100  0.7450
  LR char          5638    0.8287  0.8133  0.8350
  (lug'at - 1-fold o'quv qismidagi belgilar soni)

=== 2. Eng yaxshisi bilan juftlashgan farq ===
  eng yaxshi: LR char
  model             farq      SE                 xulosa
  NB so'z        -0.1660  0.0056        sezilarli yomon
  LR so'z        -0.1090  0.0046        sezilarli yomon
  NB so'z+bi     -0.0620  0.0053        sezilarli yomon
  LR so'z+bi     -0.0213  0.0056        sezilarli yomon
  SVC so'z+bi    -0.0163  0.0041        sezilarli yomon
  LR char_wb     -0.0960  0.0034        sezilarli yomon
  LR char              -       -             eng yaxshi

  QAROR (sezilarli yomon bo'lmagan eng sodda): LR char

=== 3. Bigram va belgi n-gramlari nima qo'shdi ===
  LR so'z+bi  - LR so'z      +0.0877  SE 0.0064  sezilarli
  NB so'z+bi  - NB so'z      +0.1040  SE 0.0034  sezilarli
  LR char     - LR char_wb   +0.0960  SE 0.0034  sezilarli
  ⭐ Qaror jadvaldan kod bilan chiqdi - oldindan yozilmagan

Nima ko'rsatdi: 2.3, 2.4, 2.5-bo'limlar.

Misol 3 — Imlo xatolariga chidamlilik

python
"""Test sharhlariga ko'proq imlo xatosi qo'shsak: so'z va belgi modellari."""

import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import StratifiedKFold
from sklearn.pipeline import make_pipeline

IJOBIY = ["yaxshi", "zo'r", "a'lo", "ajoyib", "sifatli", "qulay",
          "chiroyli", "mustahkam"]
SALBIY = ["yomon", "sifatsiz", "noqulay", "xunuk", "nosoz", "mo'rt",
          "sust", "chatoq"]
NEYTRAL = ["o'rtacha", "oddiy", "normal", "odatiy", "kutilgandek"]
JIHAT = ["sifati", "narxi", "ekrani", "batareyasi", "ovozi", "qadoqlash",
         "yetkazib berish", "sotuvchi", "dizayni", "materiali"]
MAHSULOT = ["telefon", "noutbuk", "quloqchin", "soat", "choynak", "kurtka",
            "krossovka", "sumka", "planshet", "kolonka"]
BOSH = ["kecha", "o'tgan hafta", "bir oy oldin", "do'kondan", "internetdan",
        "sovg'a uchun", "akam uchun", ""]
FEL = ["oldim", "buyurtma qildim", "sotib oldim", "ishlatib ko'rdim"]
YAKUN = {1: ["tavsiya qilaman", "yana olaman", "hammaga maslahat beraman"],
         -1: ["tavsiya qilmayman", "boshqa olmayman", "pulimga achinaman"],
         0: ["hozircha shu", "vaqt ko'rsatadi", "yana ishlatib ko'ramiz"]}
ULUSH = {1: [0.8, 0.1, 0.1], -1: [0.1, 0.8, 0.1], 0: [0.15, 0.15, 0.7]}
TP = r"[\w']+"


def gap(qutb, rng):
    j = rng.choice(JIHAT)
    juda = "juda " if rng.random() < 0.3 else ""
    r = rng.random()
    if qutb == 1:
        if r < 0.2:
            return f"{j} {rng.choice(SALBIY)} emas"
        if r < 0.3:
            return f"{j} bilan muammo yo'q"
        return f"{j} {juda}{rng.choice(IJOBIY)}"
    if qutb == -1:
        if r < 0.25:
            return f"{j} {rng.choice(IJOBIY)} emas"
        if r < 0.35:
            return f"{j} bilan muammo bor"
        return f"{j} {juda}{rng.choice(SALBIY)}"
    return f"{j} {rng.choice(NEYTRAL)}"


def sharh(rng):
    kayfiyat = int(rng.integers(-1, 2))
    qismlar = [f"{rng.choice(BOSH)} {rng.choice(MAHSULOT)} "
               f"{rng.choice(FEL)}".strip()]
    ballar = []
    for t in range(int(rng.integers(1, 5))):
        q = int(rng.choice([1, -1, 0], p=ULUSH[kayfiyat]))
        bog = rng.choice(["lekin", "va", "ammo", "ham", ""]) if t else ""
        qismlar.append(f"{bog} {gap(q, rng)}".strip())
        ballar.append(q)
    if rng.random() < 0.5:
        q = int(rng.choice([1, -1, 0], p=ULUSH[kayfiyat]))
        qismlar.append(rng.choice(YAKUN[q]))
        ballar.append(q)
    ball = np.mean(ballar) + rng.normal(0, 0.1)
    return " ".join(qismlar), 2 if ball > 0.35 else (0 if ball < -0.35 else 1)


def imlo_xato(matn, ulush, rng):
    chiqish = []
    for s in matn.split():
        if len(s) >= 4 and rng.random() < ulush:
            i = int(rng.integers(1, len(s) - 1))
            tur = int(rng.integers(0, 3))
            if tur == 0:
                s = s[:i] + s[i + 1:]
            elif tur == 1:
                s = s[:i] + s[i] + s[i:]
            else:
                s = s[:i - 1] + s[i] + s[i - 1] + s[i + 1:]
        chiqish.append(s)
    return " ".join(chiqish)


def korpus(n, seed=0, xato=0.07):
    rng = np.random.default_rng(seed)
    toza, y = zip(*[sharh(rng) for _ in range(n)])
    rng2 = np.random.default_rng(seed + 1000)
    return [imlo_xato(t, xato, rng2) for t in toza], np.array(y), list(toza)


def modellar():
    lr = lambda: LogisticRegression(C=10, max_iter=3000)
    soz = lambda: TfidfVectorizer(token_pattern=TP, ngram_range=(1, 2))
    belgi = lambda: TfidfVectorizer(analyzer="char", ngram_range=(2, 4))
    return {"so'z+bigram": make_pipeline(soz(), lr()),
            "belgi (char)": make_pipeline(belgi(), lr())}


def main() -> None:
    matnlar, y, toza = korpus(3000)
    darajalar = [0.0, 0.15, 0.3]
    cv = StratifiedKFold(5, shuffle=True, random_state=0)
    nomlar = list(modellar())
    natija = {n: {d: [] for d in darajalar} for n in nomlar}
    oov = {d: [] for d in darajalar}

    for k, (tr, te) in enumerate(cv.split(matnlar, y)):
        oquv = [matnlar[i] for i in tr]
        lugat = {s for m in oquv for s in m.split()}
        testlar = {}
        for d in darajalar:
            rng = np.random.default_rng(100 + k)
            testlar[d] = [imlo_xato(toza[i], d, rng) for i in te]
            sozlar = [s for m in testlar[d] for s in m.split()]
            oov[d].append(np.mean([s not in lugat for s in sozlar]))
        for nom, m in modellar().items():
            m.fit(oquv, y[tr])
            for d in darajalar:
                natija[nom][d].append(np.mean(m.predict(testlar[d]) == y[te]))

    print("=== 1. Test so'zlarining lug'atdan tashqari (OOV) ulushi ===")
    for d in darajalar:
        print(f"  xato darajasi {d:.2f}: OOV {np.mean(oov[d]):.1%}")

    print("\n=== 2. Aniqlik (5 fold o'rtachasi) ===")
    print(f"  {'xato':>5} " + " ".join(f"{n:>13}" for n in nomlar))
    for d in darajalar:
        print(f"  {d:>5.2f} " + " ".join(f"{np.mean(natija[n][d]):>13.4f}"
                                         for n in nomlar))

    print("\n=== 3. Aniqlik yo'qotishi (0.0 dan 0.3 gacha) ===")
    for n in nomlar:
        a0 = np.mean(natija[n][0.0])
        a4 = np.mean(natija[n][0.3])
        print(f"  {n:<13} {a0:.4f} -> {a4:.4f}  "
              f"(yo'qotish {a0 - a4:.4f})")

    print("\n=== 4. Juftlashgan farq: belgi - so'z+bigram ===")
    print(f"  {'xato':>5} {'farq':>8} {'SE':>7} {'xulosa':>22}")
    for d in darajalar:
        f = (np.array(natija["belgi (char)"][d])
             - np.array(natija["so'z+bigram"][d]))
        se = f.std(ddof=1) / np.sqrt(len(f))
        if abs(f.mean()) <= 2 * se:
            xulosa = "sezilarli emas"
        elif f.mean() > 0:
            xulosa = "belgi sezilarli yaxshi"
        else:
            xulosa = "so'z sezilarli yaxshi"
        print(f"  {d:>5.2f} {f.mean():>+8.4f} {se:>7.4f} {xulosa:>22}")
    y0 = np.mean(natija["so'z+bigram"][0.0]) - np.mean(
        natija["so'z+bigram"][0.3])
    y1 = np.mean(natija["belgi (char)"][0.0]) - np.mean(
        natija["belgi (char)"][0.3])
    if y1 < y0:
        print(f"  belgi modeli {y0 / y1:.1f} barobar kam yo'qotdi")
    else:
        print("  belgi modeli kamroq yo'qotmadi")
    print("  ⭐ Imlo xatosi ko'p bo'lsa - belgi n-gramlari")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Test so'zlarining lug'atdan tashqari (OOV) ulushi ===
  xato darajasi 0.00: OOV 0.0%
  xato darajasi 0.15: OOV 1.9%
  xato darajasi 0.30: OOV 3.8%

=== 2. Aniqlik (5 fold o'rtachasi) ===
   xato   so'z+bigram  belgi (char)
   0.00        0.8320        0.8367
   0.15        0.7883        0.8113
   0.30        0.7267        0.7863

=== 3. Aniqlik yo'qotishi (0.0 dan 0.3 gacha) ===
  so'z+bigram   0.8320 -> 0.7267  (yo'qotish 0.1053)
  belgi (char)  0.8367 -> 0.7863  (yo'qotish 0.0503)

=== 4. Juftlashgan farq: belgi - so'z+bigram ===
   xato     farq      SE                 xulosa
   0.00  +0.0047  0.0051         sezilarli emas
   0.15  +0.0230  0.0029 belgi sezilarli yaxshi
   0.30  +0.0597  0.0083 belgi sezilarli yaxshi
  belgi modeli 2.1 barobar kam yo'qotdi
  ⭐ Imlo xatosi ko'p bo'lsa - belgi n-gramlari

Nima ko'rsatdi: 2.4, 2.5-bo'limlar.

Misol 4 — Model nimani o'rgandi va xato tahlili

python
"""LogReg koeffitsiyentlari, chalkashlik matritsasi va eng ishonchli xatolar."""

import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import confusion_matrix
from sklearn.model_selection import train_test_split
from sklearn.pipeline import make_pipeline

IJOBIY = ["yaxshi", "zo'r", "a'lo", "ajoyib", "sifatli", "qulay",
          "chiroyli", "mustahkam"]
SALBIY = ["yomon", "sifatsiz", "noqulay", "xunuk", "nosoz", "mo'rt",
          "sust", "chatoq"]
NEYTRAL = ["o'rtacha", "oddiy", "normal", "odatiy", "kutilgandek"]
JIHAT = ["sifati", "narxi", "ekrani", "batareyasi", "ovozi", "qadoqlash",
         "yetkazib berish", "sotuvchi", "dizayni", "materiali"]
MAHSULOT = ["telefon", "noutbuk", "quloqchin", "soat", "choynak", "kurtka",
            "krossovka", "sumka", "planshet", "kolonka"]
BOSH = ["kecha", "o'tgan hafta", "bir oy oldin", "do'kondan", "internetdan",
        "sovg'a uchun", "akam uchun", ""]
FEL = ["oldim", "buyurtma qildim", "sotib oldim", "ishlatib ko'rdim"]
YAKUN = {1: ["tavsiya qilaman", "yana olaman", "hammaga maslahat beraman"],
         -1: ["tavsiya qilmayman", "boshqa olmayman", "pulimga achinaman"],
         0: ["hozircha shu", "vaqt ko'rsatadi", "yana ishlatib ko'ramiz"]}
ULUSH = {1: [0.8, 0.1, 0.1], -1: [0.1, 0.8, 0.1], 0: [0.15, 0.15, 0.7]}
SINFLAR = ["salbiy", "neytral", "ijobiy"]
TP = r"[\w']+"


def gap(qutb, rng):
    j = rng.choice(JIHAT)
    juda = "juda " if rng.random() < 0.3 else ""
    r = rng.random()
    if qutb == 1:
        if r < 0.2:
            return f"{j} {rng.choice(SALBIY)} emas"
        if r < 0.3:
            return f"{j} bilan muammo yo'q"
        return f"{j} {juda}{rng.choice(IJOBIY)}"
    if qutb == -1:
        if r < 0.25:
            return f"{j} {rng.choice(IJOBIY)} emas"
        if r < 0.35:
            return f"{j} bilan muammo bor"
        return f"{j} {juda}{rng.choice(SALBIY)}"
    return f"{j} {rng.choice(NEYTRAL)}"


def sharh(rng):
    kayfiyat = int(rng.integers(-1, 2))
    qismlar = [f"{rng.choice(BOSH)} {rng.choice(MAHSULOT)} "
               f"{rng.choice(FEL)}".strip()]
    ballar = []
    for t in range(int(rng.integers(1, 5))):
        q = int(rng.choice([1, -1, 0], p=ULUSH[kayfiyat]))
        bog = rng.choice(["lekin", "va", "ammo", "ham", ""]) if t else ""
        qismlar.append(f"{bog} {gap(q, rng)}".strip())
        ballar.append(q)
    if rng.random() < 0.5:
        q = int(rng.choice([1, -1, 0], p=ULUSH[kayfiyat]))
        qismlar.append(rng.choice(YAKUN[q]))
        ballar.append(q)
    ball = np.mean(ballar) + rng.normal(0, 0.1)
    return " ".join(qismlar), 2 if ball > 0.35 else (0 if ball < -0.35 else 1)


def imlo_xato(matn, ulush, rng):
    chiqish = []
    for s in matn.split():
        if len(s) >= 4 and rng.random() < ulush:
            i = int(rng.integers(1, len(s) - 1))
            tur = int(rng.integers(0, 3))
            if tur == 0:
                s = s[:i] + s[i + 1:]
            elif tur == 1:
                s = s[:i] + s[i] + s[i:]
            else:
                s = s[:i - 1] + s[i] + s[i - 1] + s[i + 1:]
        chiqish.append(s)
    return " ".join(chiqish)


def korpus(n, seed=0, xato=0.07):
    rng = np.random.default_rng(seed)
    toza, y = zip(*[sharh(rng) for _ in range(n)])
    rng2 = np.random.default_rng(seed + 1000)
    return [imlo_xato(t, xato, rng2) for t in toza], np.array(y), list(toza)


def main() -> None:
    matnlar, y, _ = korpus(3000)
    Xtr, Xte, ytr, yte = train_test_split(matnlar, y, test_size=0.2,
                                          random_state=0, stratify=y)
    model = make_pipeline(
        TfidfVectorizer(token_pattern=TP, ngram_range=(1, 2)),
        LogisticRegression(C=10, max_iter=3000))
    model.fit(Xtr, ytr)
    vek, clf = model[0], model[1]
    nomlar = vek.get_feature_names_out()

    print("=== 1. Har sinf uchun eng kuchli belgilar ===")
    for k in (2, 0, 1):
        top = np.argsort(clf.coef_[k])[::-1][:7]
        print(f"  {SINFLAR[k]}:")
        print("    " + ", ".join(f"{nomlar[i]} ({clf.coef_[k, i]:.1f})"
                                 for i in top[:4]))
        print("    " + ", ".join(f"{nomlar[i]} ({clf.coef_[k, i]:.1f})"
                                 for i in top[4:]))

    print("\n=== 2. Inkor: bir so'z, ikki ma'no ===")
    j = {n: i for i, n in enumerate(nomlar)}
    for f in ["sifatli", "sifatli emas", "sust", "sust emas", "emas"]:
        if f in j:
            w = clf.coef_[2, j[f]] - clf.coef_[0, j[f]]
            print(f"  {f:<14} ijobiy - salbiy og'irlik: {w:+.2f}")
    print("  unigram 'emas' deyarli neytral - ma'noni bigram tashiydi")

    print("\n=== 3. Shubhali belgilar: mahsulot nomlari ===")
    kuch = np.abs(clf.coef_).max(0)
    tartib = np.argsort(-kuch)
    rang = {nomlar[i]: r for r, i in enumerate(tartib)}
    for m in ["telefon", "kurtka", "soat"]:
        print(f"  {m:<9} eng katta |og'irlik| {kuch[j[m]]:.2f}, "
              f"o'rni {rang[m] + 1} / {len(nomlar)}")
    print("  mahsulot nomi sinfga bog'liq emas - og'irligi kichik bo'lishi kerak")

    print("\n=== 4. Chalkashlik matritsasi (qator - haqiqiy) ===")
    p = model.predict_proba(Xte)
    b = p.argmax(1)
    M = confusion_matrix(yte, b)
    print("           " + "".join(f"{s:>9}" for s in SINFLAR))
    for i, s in enumerate(SINFLAR):
        print(f"  {s:<8} " + "".join(f"{M[i, k]:>9}" for k in range(3)))
    print(f"  aniqlik: {np.trace(M) / M.sum():.4f}")
    print(f"  salbiy <-> ijobiy xatolar: {M[0, 2] + M[2, 0]}, "
          f"neytral bilan bog'liq: {M.sum() - np.trace(M) - M[0, 2] - M[2, 0]}")

    print("\n=== 5. Guruhlar bo'yicha aniqlik ===")
    togri = b == yte
    inkor = np.array([" emas" in m for m in Xte])
    gaplar = np.array([sum(m.count(f" {c} ") + m.startswith(c)
                           for c in JIHAT) for m in Xte])
    for nom, g in [("inkorli", inkor), ("inkorsiz", ~inkor),
                   ("1 jihat", gaplar <= 1), ("3+ jihat", gaplar >= 3)]:
        print(f"  {nom:<9} {int(g.sum()):>4} ta, aniqlik {togri[g].mean():.4f}")

    print("\n=== 6. Eng ishonchli xatolar ===")
    xato = np.where(~togri)[0]
    ishonch = p[xato, b[xato]]
    for i in xato[np.argsort(-ishonch)[:3]]:
        print(f"  haqiqiy {SINFLAR[yte[i]]}, bashorat {SINFLAR[b[i]]} "
              f"(p={p[i, b[i]]:.2f})")
        print(f"    {Xte[i]}")
    print(f"  xatolardagi o'rtacha ishonch: {ishonch.mean():.3f}, "
          f"to'g'rilarda: {p[togri, b[togri]].mean():.3f}")
    print("  ⭐ Koeffitsiyent va xatolar - modelni tushunish yo'li")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Har sinf uchun eng kuchli belgilar ===
  ijobiy:
    nosoz emas 7.3-bob, noqulay emas 6.4-bob, yomon emas 5.7-bob, mo'rt emas 5.6-bob
    sust emas 5.5-bob, sifatsiz emas 5.1-bob, zo'r 4.9-bob
  salbiy:
    mustahkam emas 6.3-bob, zo'r emas 5.9-bob, sifatli emas 5.7-bob, yaxshi emas 5.1-bob
    chatoq 5.0-bob, chiroyli emas 4.9-bob, bor 4.9-bob
  neytral:
    odatiy 5.7-bob, o'rtacha 5.3-bob, oddiy 5.1-bob, kutilgandek 5.0-bob
    normal 4.4-bob, ekrani odatiy 3.3-bob, lekin qadoqlash 2.8-bob

=== 2. Inkor: bir so'z, ikki ma'no ===
  sifatli        ijobiy - salbiy og'irlik: +7.12
  sifatli emas   ijobiy - salbiy og'irlik: -11.01
  sust           ijobiy - salbiy og'irlik: -6.77
  sust emas      ijobiy - salbiy og'irlik: +10.50
  emas           ijobiy - salbiy og'irlik: -2.25
  unigram 'emas' deyarli neytral - ma'noni bigram tashiydi

=== 3. Shubhali belgilar: mahsulot nomlari ===
  telefon   eng katta |og'irlik| 0.63, o'rni 1470 / 4612
  kurtka    eng katta |og'irlik| 0.44, o'rni 2177 / 4612
  soat      eng katta |og'irlik| 0.83, o'rni 953 / 4612
  mahsulot nomi sinfga bog'liq emas - og'irligi kichik bo'lishi kerak

=== 4. Chalkashlik matritsasi (qator - haqiqiy) ===
              salbiy  neytral   ijobiy
  salbiy         153       28        7
  neytral         28      156       24
  ijobiy           2       22      180
  aniqlik: 0.8150
  salbiy <-> ijobiy xatolar: 9, neytral bilan bog'liq: 102

=== 5. Guruhlar bo'yicha aniqlik ===
  inkorli    190 ta, aniqlik 0.7684
  inkorsiz   410 ta, aniqlik 0.8366
  1 jihat    171 ta, aniqlik 0.8538
  3+ jihat   283 ta, aniqlik 0.8269

=== 6. Eng ishonchli xatolar ===
  haqiqiy neytral, bashorat ijobiy (p=0.99)
    noutbuk sotib oldim materiali chiroyli ammo sifati mo'rt qadoqlash juda a'lo yana olaman
  haqiqiy ijobiy, bashorat neytral (p=0.98)
    bir oy oldin sumka oldim batareyasi kutilgandek lekin sifati chatoq emas ammo ovozi kutilgandek ammo sifati kutilgandek
  haqiqiy neytral, bashorat salbiy (p=0.97)
    akam uchun krossovka buyurtma qildim narxi juda mo'rt ham batareeyasi kutilgandek ham ekrani zo'r emas lekin sifati juuda sifatsiz
  xatolardagi o'rtacha ishonch: 0.681, to'g'rilarda: 0.847
  ⭐ Koeffitsiyent va xatolar - modelni tushunish yo'li

Nima ko'rsatdi: 2.6-bo'lim.


5. To'g'ri va noto'g'ri tushunishlar

Noto'g'ri fikr To'g'risi
"Neyron model har doim TF-IDF dan yaxshi" Avval klassik bazaviyni o'lchang — u ko'pincha juda yaqin
"TF-IDF ni butun ma'lumotda fit qilish zararsiz" Bu leakage; Pipeline bilan savol umuman tug'ilmaydi
"Unigramlar sentimentga yetadi" Inkor ("sifatli emas") faqat bigram yoki belgi n-gramda ko'rinadi
"Belgi n-gramlari har doim yaxshiroq" Ular sekinroq; faqat o'lchov ularning foydasini ko'rsatadi
"Naive Bayes eskirgan" Tez va kam ma'lumotda kuchli, lekin inkorni tushunmaydi
"0.005 farq — yaxshilanish" SE bilan tekshirilmagan farq — shovqin bo'lishi mumkin
"Eng yuqori aniqlikli modelni olamiz" Sezilarli yomon bo'lmagan eng soddasini oling
"Aniqlik yetarli ma'lumot beradi" Koeffitsiyentlar va xatolar sababni ko'rsatadi

6. Keng tarqalgan xatolar va yechimlari

1. Leakage: vektorizator butun ma'lumotda

python
X = TfidfVectorizer().fit_transform(matnlar); train_test_split(X, y)  # ⚠️
model = make_pipeline(TfidfVectorizer(), LogisticRegression())       # ✅

2. Apostrofli so'zlar bo'linadi

python
TfidfVectorizer()                            # "zo'r" -> "zo"      # ⚠️
TfidfVectorizer(token_pattern=r"[\w']+")     # "zo'r" butun        # ✅

3. Inkor yo'qoladi

python
TfidfVectorizer(ngram_range=(1, 1))          # "emas" alohida      # ⚠️
TfidfVectorizer(ngram_range=(1, 2))          # "sifatli emas"      # ✅

4. Turli foldlar

python
cross_val_score(a, X, y, cv=5); cross_val_score(b, X, y, cv=KFold(5, shuffle=True))  # ⚠️
cv = StratifiedKFold(5, shuffle=True, random_state=0)  # ikkalasiga bir xil  # ✅

5. Naive Bayes ga manfiy belgilar

python
make_pipeline(TfidfVectorizer(), StandardScaler(with_mean=False), MultinomialNB())  # ⚠️ o'lchov buziladi
make_pipeline(TfidfVectorizer(), MultinomialNB())  # sanoq/TF-IDF - manfiy emas     # ✅

6. LinearSVC dan ehtimol

python
LinearSVC().predict_proba(X)                 # AttributeError      # ⚠️
LogisticRegression().predict_proba(X)        # yoki CalibratedClassifierCV  # ✅

7. Bitta raqam bilan qaror

python
print("char yaxshi" if aniq_char > aniq_soz else "so'z yaxshi")    # ⚠️
d = char_s - soz_s; abs(d.mean()) > 2 * d.std(ddof=1) / len(d) ** 0.5  # ✅

7. Integratsiya — bu bilim qayerda kerak bo'ladi

  • 14-qism (o'tilgan): Logistik regressiya, Naive Bayes, SVM
  • 18-qism (o'tilgan): Juftlashgan CV taqqoslash va SE
  • 19-qism (o'tilgan): Pipeline, FeatureUnion, leakage
  • 23.4-dars (o'tilgan): Bag-of-words va TF-IDF
  • Keyingi darslar: so'z embeddinglari, embedding bilan klassifikatsiya (bu darsdagi bazaviy bilan solishtiriladi), RNN
  • Transformerlar qismida: katta oldindan o'rgatilgan modellar — ular ham shu bazaviy bilan o'lchanadi

8. Eng yaxshi amaliyotlar

  1. Har doim DummyClassifier dan boshlang.

  2. Vektorizatorni Pipeline ichiga qo'ying.

  3. token_pattern ni o'zbekcha apostrofga moslang (yoki 23.1 dagi kabi normallashtiring).

  4. Kamida so'z + bigram TF-IDF + LogReg ni bazaviy qiling.

  5. Imlo xatosi ko'p bo'lsa belgi n-gramlarini sinang.

  6. Barcha modellarni bir xil foldlarda, juftlashgan farq + SE bilan solishtiring.

  7. Sezilarli yomon bo'lmagan eng sodda modelni tanlang.

  8. Koeffitsiyentlar va eng ishonchli xatolarni ko'zdan kechiring.


9. Amaliy topshiriq

Vazifa 1: Bashorat qiling

python
1.  # 3 ta teng sinfda DummyClassifier aniqligi taxminan?
2.  # TfidfVectorizer ning fit qismi nimalarni o'rganadi?
3.  # sukut token_pattern "zo'r" ni qanday bo'ladi?
4.  # "sifatli emas" ni unigram modeli qanday ko'radi?
5.  # analyzer="char_wb" va "char" farqi?
6.  # MultinomialNB dagi alpha nima uchun?
7.  # LinearSVC predict_proba beradimi?
8.  # juftlashgan CV da nima bir xil bo'lishi shart?
9.  # SE formulasi (5 fold)?
10. # qaror qoidasi: qaysi model tanlanadi?
11. # clf.coef_ shakli (3 sinf, 20000 belgi)?
12. # mahsulot nomi katta og'irlik olsa - nima belgisi?
Javoblar
  1. ~0.33 (eng katta sinf ulushi)
  2. Lug'at (qaysi so'z/n-gramlar bor) va IDF og'irliklari
  3. zo — apostrofdan keyingi r bir harfli token sifatida tashlanadi
  4. sifatli (ijobiy) va emas (deyarli neytral) alohida — inkor yo'qoladi
  5. char_wb faqat so'z ichida, char bo'shliqdan ham o'tadi
  6. Silliqlash: ko'rilmagan so'z nol ehtimol bermasin
  7. Yo'q — faqat decision_function
  8. Foldlar (bo'linish) va metrika
  9. std(d, ddof=1) / sqrt(5)
  10. Eng yaxshisidan sezilarli yomon bo'lmagan eng soddasi
  11. (3, 20000)
  12. Ma'lumotda noxolis bog'liqlik yoki leakage

Vazifa 2: Xatolarni tuzating

python
1.  X = TfidfVectorizer().fit_transform(matnlar)
    Xtr, Xte, ytr, yte = train_test_split(X, y)

2.  vek = TfidfVectorizer()          # o'zbekcha sharhlar uchun

3.  make_pipeline(TfidfVectorizer(), StandardScaler(with_mean=False),
                  MultinomialNB())

4.  a = cross_val_score(m1, X, y, cv=KFold(5, shuffle=True))
    b = cross_val_score(m2, X, y, cv=KFold(5, shuffle=True))

5.  p = make_pipeline(TfidfVectorizer(), LinearSVC()).fit(X, y).predict_proba(X2)
Javoblar
python
1.  model = make_pipeline(TfidfVectorizer(), LogisticRegression())
    Xtr, Xte, ytr, yte = train_test_split(matnlar, y)
    model.fit(Xtr, ytr)

2.  vek = TfidfVectorizer(token_pattern=r"[\w']+", ngram_range=(1, 2))

3.  make_pipeline(TfidfVectorizer(), MultinomialNB())

4.  cv = StratifiedKFold(5, shuffle=True, random_state=0)
    a = cross_val_score(m1, X, y, cv=cv)
    b = cross_val_score(m2, X, y, cv=cv)

5.  p = make_pipeline(TfidfVectorizer(),
                      LogisticRegression()).fit(X, y).predict_proba(X2)

Vazifa 3: Bazaviy va Pipeline

Modellang:

  1. Korpus statistikasi
  2. DummyClassifier
  3. TF-IDF + LogReg Pipeline
  4. Leakage bilan taqqoslash

Vazifa 4: Juftlashgan taqqoslash

Modellang:

  1. NB, LogReg, LinearSVC
  2. So'z va bigram
  3. Belgi n-gramlari
  4. Qaror qoidasi

Vazifa 5: Imlo xatolari

Modellang:

  1. OOV ulushi
  2. Xato darajalari
  3. So'z va belgi yo'qotishi
  4. Juftlashgan farq

Vazifa 6: Model nimani o'rgandi

Modellang:

  1. Top koeffitsiyentlar
  2. Inkor og'irliklari
  3. Chalkashlik matritsasi
  4. Eng ishonchli xatolar

Vazifa 7: O'ylash

Boshqa jamoa o'z sharhlarida uch variantni o'lchadi: so'z + bigram TF-IDF + LogReg (5 fold o'rtachasi 0.834), belgi n-gram LogReg 0.851-bob va ikkalasining FeatureUnion birlashmasi 0.853-bob. Menejer so'radi: "Eng yuqorisini — birlashmani olamiz, to'g'rimi?" Nima deysiz?

Javob

Qisqa javob: avval farqlarni SE bilan tekshiring, keyin ishlab chiqarish sharoitini hisobga oling. Eng yuqori o'rtacha — avtomatik g'olib emas.

1. Farq sezilarlimi? Birlashma va belgi modeli orasidagi 0.002 farq — deyarli shubhasiz shovqin ichida. Belgi modeli va so'z modeli orasidagi 0.017 esa sezilarli bo'lishi mumkin — lekin buni juftlashgan fold farqlari va SE ko'rsatadi, o'rtachalar emas:

python
d = char_s - soz_s                      # bir xil foldlar!
se = d.std(ddof=1) / np.sqrt(len(d))
print(d.mean(), se, abs(d.mean()) > 2 * se)

2. Qaror qoidasi. Sezilarli yomon bo'lmagan eng sodda model. Agar birlashma belgi modelidan sezilarli yaxshi bo'lmasa — birlashmaning qo'shimcha murakkabligi (ikki vektorizator, ikki barobar katta lug'at) oqlanmaydi.

3. Ishlab chiqarish sharoiti. Real sharhlarda imlo xatosi ko'pmi? 3-misolda xato darajasi oshgani sari so'z modelining aniqligi belgi modelinikidan tezroq tushdi. Agar foydalanuvchilar telefondan tez yozsa, bu farq toza testdagidan katta bo'ladi. Uni ishlab chiqarishga o'xshash test to'plamida o'lchang (masalan, real sharhlardan 500 tasini qo'lda belgilab).

4. Xarajat. Belgi n-gram vektorida har matn uchun nol bo'lmagan qiymatlar so'z vektoridagidan taxminan 10 barobar ko'p: vektorlash va bashorat vaqti, xotira. Kuniga millionlab sharh bo'lsa, bu ham hisobga olinadi.

5. Tushuntirish. So'z modelining koeffitsiyentlari o'qiladi ("sifatli emas" → salbiy). Belgi n-gramlari ("li em") — kamroq tushunarli. Agar biznes "nega bu sharh salbiy deb topildi?" deb so'rasa, bu muhim.

Menejerga javob: "Uchalasi yaqin. Avval juftlashgan farqlarni SE bilan tekshiramiz: agar birlashma belgi modelidan sezilarli yaxshi bo'lmasa, soddaroq belgi modelini olamiz. Agar belgi modeli ham so'z modelidan sezilarli yaxshi bo'lmasa — so'z modelini olamiz, chunki u tezroq va tushuntiriladi. Qarorni imlo xatolari ko'p real sharhlar to'plamida yakuniy tekshiramiz."

Nimani mustahkamlaydi: 2.4, 2.5-bo'limlar.


Xulosa

Bu darsda matn klassifikatsiyasi uchun klassik bazaviylarni qurdik va halol taqqosladik.

Eng muhim uch fikr:

  1. Bazaviy — Pipeline ichidagi TF-IDF + logistik regressiya. 1-misolda DummyClassifier 0.3467 aniqlik berdi — shu "hech narsa o'rganmagan" chegara. So'z + bigram TF-IDF va logistik regressiya Pipeline ichida 0.8150 ga chiqdi; eng qiyin sinf neytral bo'ldi (F1 0.7536, ijobiy 0.8675). Vektorizatorni butun ma'lumotda fit qilganda lug'atga faqat testda uchragan 639 ta belgi (imlo xatoli so'zlar va yangi bigramlar) kirib qoldi; aniqlik farqi bu yerda kichik chiqdi (0.8167 va 0.8150) — lekin buni faqat o'lchab bildik, Pipeline bilan esa bu savol umuman tug'ilmaydi. Sukut token_pattern esa "zo'r" ni "zo" ga, "a'lo" ni "lo" ga aylantirdi.

  2. N-gramlar ma'noni tashiydi. 2-misolda bir xil 5 foldda sakkiz model solishtirildi. Bigramlar logistik regressiyaga +0.0877, Naive Bayes ga +0.1040 qo'shdi — ikkalasi ham 2 × SE dan ancha katta: inkor ("sifatli emas") faqat bigramda ko'rinadi. Bo'shliqdan o'tadigan belgi n-gramlari (char) so'z ichidagi (char_wb) variantdan +0.0960 yaxshi chiqdi — chunki char_wb ham inkorni ko'rmaydi. Eng yaxshisi LR char (0.8287) bo'ldi va qolgan yetti modelning hammasi undan sezilarli yomon chiqdi, shuning uchun qaror qoidasi uni tanladi; eng yaqin raqib SVC so'z+bi (-0.0163, SE 0.0041). 3-misolda imlo xatosi bo'lmagan testda belgi va so'z modellari orasidagi farq sezilarli emas edi (+0.0047, SE 0.0051), lekin xato darajasi 0.30 ga yetganda so'z modeli 0.1053, belgi modeli 0.0503 aniqlik yo'qotdi — 2.1 barobar kam.

  3. Model nimani o'rganganini ko'ring. 4-misolda logistik regressiya koeffitsiyentlari mantiqiy chiqdi: ijobiy sinfga eng kuchli ovoz "nosoz emas", "yomon emas", salbiyga — "zo'r emas", "sifatli emas". "sifatli" ning og'irligi +7.12, "sifatli emas" niki -11.01 — ma'noni bigram tashiydi. Mahsulot nomlari past o'rinlarda qoldi ("telefon" 4612 belgidan 1470-o'rinda) — noxolis bog'liqlik yo'q. Chalkashlik matritsasida 111 xatodan atigi 9 tasi salbiy va ijobiy orasida, qolgan 102 tasi neytral bilan bog'liq; inkorli sharhlarda aniqlik 0.7684, inkorsizlarda 0.8366. Eng ishonchli xatolar esa yorliq shovqinini ko'rsatdi: masalan, to'rt gapdan uchtasi salbiy bo'lgan sharh "neytral" deb belgilangan edi — model to'g'ri, yorliq xato.

Keyingi darsda so'z embeddinglari: taqsimot gipotezasi, birga uchrash matritsasi, PPMI va SVD, va word2vec ni noldan o'rgatish.

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
23.5-dars: Matn klassifikatsiyasi — klassik bazaviylar — IlmHamroh