IlmHamroh
Data Science va sun'iy intellekt/Feature engineering6/10-dars22 daqiqa
Mundarija (22)

17.6-dars: Matn belgilari

17-QISM — FEATURE ENGINEERING · 6-dars


1. Kirish va motivatsiya

Mahsulot tavsifi, mijoz izohi, qidiruv so'rovi, xato xabari — jadval ma'lumotlarida matn ustunlari doimo uchraydi. Ularni tashlab yuborish oson, lekin ko'pincha aynan ular eng ko'p ma'lumotni saqlaydi.

Matnni modelga berishning uch darajasi bor: sodda statistika (uzunlik, so'zlar soni, belgilar tarkibi), bag-of-words / TF-IDF (so'zlar chastotasi) va embedding (ma'noni vektorga aylantirish). Birinchisi arzon va ko'pincha yetarli, uchinchisi kuchli lekin qimmat.

Bu darsda birinchi ikki darajaga e'tibor qaratamiz — ular jadval ma'lumotlarida ko'p hollarda yetarli. Embedding va transformerlar 25-26 qismlarda batafsil ko'riladi.

Bu darsda: sodda matn statistikasi, tokenizatsiya asoslari, CountVectorizer va TfidfVectorizer, n-gram lar, belgi darajasidagi n-gram, o'lchamni cheklash va HashingVectorizer, hamda matn belgilarini jadval belgilari bilan birlashtirish.

Real vaziyat. Qo'llab-quvvatlash xizmatida murojaatlarni shoshilinchlik bo'yicha tasniflash kerak edi. Faqat jadval belgilari (mijoz turi, vaqt, kanal) bilan AUC 0.71 edi. Matndan uch sodda belgi qo'shildi — uzunlik, undov belgilari soni, katta harflar ulushi — AUC 0.79 ga chiqdi. TF-IDF qo'shilgach 0.84.

Bu darsda matn belgilarini o'rganamiz.

Bu darsda:

  • Sodda matn statistikasi
  • CountVectorizer va TF-IDF
  • n-gram lar
  • Belgi darajasidagi n-gram
  • O'lchamni cheklash
  • Jadval bilan birlashtirish
  • Tuzoqlar
  • Amaliy: murojaatlar

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


2. Nazariya — chuqur tushuntirish

2.1. Sodda matn statistikasi

python
s = df["matn"].fillna("")
df["uzunlik"] = s.str.len()
df["sozlar"] = s.str.split().str.len()
df["ortacha_soz"] = df["uzunlik"] / df["sozlar"].clip(lower=1)
df["undov"] = s.str.count("!")
df["savol"] = s.str.count(r"\?")
df["katta_ulush"] = s.str.count(r"[A-Z]") / s.str.len().clip(lower=1)
df["raqam_ulush"] = s.str.count(r"\d") / s.str.len().clip(lower=1)
df["bosh_harf"] = s.str.count(r"\b[A-Z]")
df["url_bor"] = s.str.contains(r"http", case=False).astype(int)
df["bosh"] = (s.str.strip() == "").astype(int)
text
NEGA ARZON VA KUCHLI:
  + bir necha qator kod
  + tez hisoblanadi
  + til va lug'atga bog'liq emas
  + ko'pincha TF-IDF ning yarim foydasini beradi

QACHON ISHLAYDI:
  shoshilinchlik, spam, sifat, emotsional ohang

Sodda statistikadan boshlang: u arzon, tushunarli va ko'pincha katta foyda beradi. TF-IDF ni faqat u yetarli bo'lmaganda qo'shing.

2.2. CountVectorizer va TF-IDF

python
from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer

CountVectorizer(lowercase=True, min_df=5, max_df=0.9, max_features=20000,
                stop_words=None, token_pattern=r"(?u)\b\w\w+\b")

TfidfVectorizer(min_df=5, max_df=0.9, sublinear_tf=True, norm="l2")
text
COUNT: har hujjatda har so'z necha marta uchragan
TF-IDF: chastota * teskari hujjat chastotasi

  tf(t, d)  = so'zning hujjatdagi chastotasi
  idf(t)    = log(N / df(t)) + 1
  -> KAM hujjatda uchraydigan so'z KO'PROQ vazn oladi

sublinear_tf=True: tf o'rniga 1 + log(tf) - uzun hujjatlarni yumshatadi
norm="l2": har hujjat vektori birlik uzunlikka keltiriladi (masshtablash)

QAYSI MODELGA:
  chiziqli (LogReg, LinearSVC) + TF-IDF - klassik va kuchli
  Naive Bayes + Count - tez bazaviy
  daraxtlar + TF-IDF - yomon (siyrak, yuqori o'lcham)

TF-IDF + chiziqli model — matn klassifikatsiyasining klassik va hali ham juda kuchli kombinatsiyasi: u tez, tushunarli va ko'p vazifada transformerlardan atigi 2-5% ortda qoladi.

2.3. n-gram lar

text
ngram_range=(1, 1)  - faqat alohida so'zlar (unigram)
ngram_range=(1, 2)  - so'zlar + juftliklar (bigram)
ngram_range=(1, 3)  - + uchliklar

NEGA KERAK:
  "yaxshi emas" - unigram da "yaxshi" va "emas" alohida
                  bigram da "yaxshi emas" - inkor saqlanadi

NARXI: belgilar soni keskin oshadi
  unigram: 20 000
  + bigram: 200 000+
  -> min_df va max_features bilan cheklang

AMALIY: (1, 2) ko'p hollarda yetarli

Bigram lar inkorni saqlaydi — bu sentiment tahlilida hal qiluvchi: "yaxshi emas" va "yaxshi" unigram da deyarli bir xil ko'rinadi.

2.4. Belgi darajasidagi n-gram

python
TfidfVectorizer(analyzer="char_wb", ngram_range=(3, 5), min_df=3)
text
So'z o'rniga HARFLAR ketma-ketligi:
  "kitob" -> "kit", "ito", "tob" (3-gram)

QACHON FOYDALI:
  + imlo xatolari ko'p ("kitob" / "kitab")
  + morfologik boy tillar (o'zbek, turk, rus) - qo'shimchalar
  + qisqa matnlar (nom, manzil, mahsulot nomi)
  + tokenizatsiya qiyin bo'lganda

char_wb - so'z chegaralarini hisobga oladi (afzalroq)

NARXI: belgilar soni ko'p, hisoblash sekinroq

O'zbek tili uchun char_wb ko'pincha yaxshiroq: "kitobni", "kitobga", "kitoblar" so'zlari unigram da uch xil belgi, belgi n-gram da esa umumiy "kitob" o'zagini bo'lishadi.

2.5. O'lchamni cheklash

text
min_df=5          - 5 tadan kam hujjatda uchragan so'zni tashlash
max_df=0.9        - hujjatlarning 90% idan ko'pida uchragan so'zni tashlash
max_features=20000 - eng ko'p uchraydigan N ta so'z

STOP-SO'ZLAR: "va", "bilan", "uchun" - odatda foydasiz
  sklearn da faqat ingliz tili uchun tayyor ro'yxat bor
  o'zbek tili uchun qo'lda ro'yxat yoki max_df bilan avtomatik

HashingVectorizer: lug'at saqlamaydi, xesh funksiya ishlatadi
  + xotira tejamkor, oqimda ishlaydi
  + fit kerak emas (stateless)
  - teskari aylantirib bo'lmaydi (qaysi so'z ekanini bilib bo'lmaydi)
  - to'qnashuvlar bo'lishi mumkin

max_df stop-so'zlarni avtomatik topadi: har tilda "va", "bilan" kabi so'zlar deyarli har hujjatda uchraydi, shuning uchun max_df=0.9 ularni qo'lda ro'yxatsiz chiqarib tashlaydi.

2.6. Jadval bilan birlashtirish

python
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline

tayyor = ColumnTransformer([
    ("matn", TfidfVectorizer(min_df=5, ngram_range=(1, 2)), "izoh"),
    ("kategoriya", OneHotEncoder(handle_unknown="ignore"), ["kanal"]),
    ("sonli", StandardScaler(), ["uzunlik", "undov"]),
])
Pipeline([("t", tayyor), ("m", LogisticRegression(max_iter=2000))])
text
MUHIM: ColumnTransformer da matn ustuni STRING sifatida beriladi
  ("matn", TfidfVectorizer(), "izoh")     - ustun NOMI (ro'yxat emas!)
  ["izoh"] bersangiz XATO bo'ladi

IKKI BOSQICHLI YONDASHUV (ko'pincha yaxshiroq):
  1. Matndan sodda statistika -> sonli belgilar
  2. TF-IDF dan alohida model -> uning bashoratini BELGI sifatida
  3. Ikkalasini gradient boosting ga berish

ColumnTransformer da matn ustuni nomi ro'yxatsiz beriladi: "izoh" to'g'ri, ["izoh"] esa vektorizatorga DataFrame beradi va xato chiqadi.

2.7. Tuzoqlar

Asosiy tuzoqlar: vektorizatorni butun ma'lumotda fit qilish (leakage); min_df va max_features siz TF-IDF (xotira); daraxtlarga siyrak TF-IDF berish; ColumnTransformer da matn ustunini ro'yxat sifatida berish; NaN ni fillna("") qilmaslik; belgi n-gram larni juda keng diapazonda (2, 8) olish; tokenizatsiyani tilga moslamaslik; matn statistikasini o'tkazib yuborib, darhol TF-IDF ga o'tish.

2.8. Arzondan boshlang

Matnni modelga berishda sodda statistikadan boshlang (uzunlik, so'zlar soni, belgilar tarkibi) — u arzon va ko'pincha katta foyda beradi. Keyin TF-IDF (min_df, max_df, ngram_range=(1, 2)) va chiziqli model — klassik va kuchli kombinatsiya. Morfologik boy tillarda char_wb n-gram larni sinang. Vektorizatorni Pipeline ichida ishlating va ColumnTransformer da matn ustunini ro'yxatsiz bering. Keyingi dars — feature selection.


3. Tez ma'lumotnoma

python
from sklearn.compose import ColumnTransformer
from sklearn.feature_extraction.text import (CountVectorizer, HashingVectorizer,
                                             TfidfVectorizer)
from sklearn.pipeline import Pipeline

s = df["izoh"].fillna("")
df["uzunlik"], df["sozlar"] = s.str.len(), s.str.split().str.len()
df["undov"] = s.str.count("!")

tfidf = TfidfVectorizer(min_df=5, max_df=0.9, ngram_range=(1, 2),
                        sublinear_tf=True, max_features=50000)
char = TfidfVectorizer(analyzer="char_wb", ngram_range=(3, 5), min_df=3)

tayyor = ColumnTransformer([
    ("matn", tfidf, "izoh"),              # ustun NOMI, ro'yxat emas
    ("sonli", "passthrough", ["uzunlik", "undov"])])
QOIDA: sodda statistikadan boshla · min_df qo'y · Pipeline ichida ·
       matn ustunini ro'yxatsiz ber

Matn belgilari xulosasi

Sodda statistika: uzunlik, so'zlar, undov, katta harf - arzon va kuchli
TF-IDF + chiziqli model - klassik kombinatsiya
ngram_range=(1, 2) inkorni saqlaydi; char_wb morfologik tillarga
min_df / max_df / max_features bilan o'lchamni cheklang

4. Batafsil misollar

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

Misol 1 — Sodda matn statistikasi

python
"""Arzon belgilar, katta foyda (real pandas/sklearn)."""

import numpy as np
import pandas as pd
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.model_selection import StratifiedKFold, cross_val_score


def yarat(seed: int = 5, n: int = 4000) -> pd.DataFrame:
    """Qo'llab-quvvatlash murojaatlari: shoshilinchlik."""
    rng = np.random.default_rng(seed)
    shoshilinch = rng.random(n) < 0.25
    kanal = rng.choice(["ilova", "sayt", "email"], n, p=[0.5, 0.3, 0.2])
    matnlar = []
    for tez in shoshilinch:
        if tez:
            asos = rng.choice(["ISHLAMAYAPTI", "TEZDA yordam kerak",
                               "pul yechib olindi", "buyurtma kelmadi"])
            undov = "!" * int(rng.integers(1, 5))
            qoshimcha = rng.choice(["", " iltimos tezroq", " juda muhim"])
            matn = f"{asos}{undov}{qoshimcha}"
        else:
            asos = rng.choice(["savol bor edi", "qanday qilib almashtirsam",
                               "narxlar haqida ma'lumot",
                               "yetkazish muddati qancha"])
            matn = asos + rng.choice(["", " rahmat", " iltimos javob bering"])
        # uzunlikni tasodifiylashtirish
        if rng.random() < 0.4:
            matn += " " + " ".join(rng.choice(
                ["mahsulot", "buyurtma", "xizmat", "hisob", "karta"],
                int(rng.integers(3, 25))))
        matnlar.append(matn)
    return pd.DataFrame({"matn": matnlar, "kanal": kanal,
                         "shoshilinch": shoshilinch.astype(int)})


def matn_statistikasi(s: pd.Series) -> pd.DataFrame:
    s = s.fillna("")
    uzunlik = s.str.len().clip(lower=1)
    return pd.DataFrame({
        "uzunlik": s.str.len(),
        "sozlar": s.str.split().str.len(),
        "ortacha_soz": s.str.len() / s.str.split().str.len().clip(lower=1),
        "undov": s.str.count("!"),
        "savol": s.str.count(r"\?"),
        "katta_ulush": s.str.count(r"[A-Z]") / uzunlik,
        "raqam_ulush": s.str.count(r"\d") / uzunlik,
        "bosh_harfli_soz": s.str.count(r"\b[A-Z]"),
    })


def main() -> None:
    df = yarat()
    y = df["shoshilinch"].to_numpy()
    cv = StratifiedKFold(5, shuffle=True, random_state=0)

    print("=== 1. Ma'lumot ===")
    print(f"  {len(df)} murojaat, shoshilinch {y.mean():.1%}")
    for nechanchi in [0, 1]:
        namuna = df.loc[df["shoshilinch"] == nechanchi, "matn"].iloc[0]
        nom = "shoshilinch" if nechanchi else "oddiy"
        print(f"  {nom:<12}: {namuna[:60]}")

    print("\n=== 2. Statistik belgilar ===")
    stat = matn_statistikasi(df["matn"])
    print(f"  {'belgi':<18} {'oddiy':>10} {'shoshilinch':>13} "
          f"{'korrelyatsiya':>15}")
    for nom in stat.columns:
        a = stat.loc[y == 0, nom].mean()
        b = stat.loc[y == 1, nom].mean()
        # doimiy belgi uchun korrelyatsiya aniqlanmagan
        korr = (stat[nom].corr(pd.Series(y)) if stat[nom].std() > 0
                else float("nan"))
        print(f"  {nom:<18} {a:>10.3f} {b:>13.3f} {korr:>+15.4f}")

    print("\n=== 3. Belgilar guruhlari ===")
    def model():
        return HistGradientBoostingClassifier(learning_rate=0.1, max_iter=200,
                                              random_state=0)
    kanal_kod = pd.get_dummies(df["kanal"], prefix="kanal").astype(int)
    guruhlar = {
        "faqat kanal": kanal_kod,
        "faqat uzunlik": stat[["uzunlik", "sozlar"]],
        "uzunlik + undov": stat[["uzunlik", "sozlar", "undov"]],
        "barcha statistika": stat,
        "statistika + kanal": pd.concat([stat, kanal_kod], axis=1),
    }
    print(f"  {'guruh':<22} {'belgilar':>9} {'CV ROC AUC':>12}")
    for nom, Xa in guruhlar.items():
        b = cross_val_score(model(), Xa, y, cv=cv, scoring="roc_auc").mean()
        print(f"  {nom:<22} {Xa.shape[1]:>9} {b:>12.4f}")

    print("\n=== 4. Har belgining yakka kuchi ===")
    print(f"  {'belgi':<18} {'yakka CV AUC':>14}")
    for nom in stat.columns:
        b = cross_val_score(model(), stat[[nom]], y, cv=cv,
                            scoring="roc_auc").mean()
        print(f"  {nom:<18} {b:>14.4f}")
    print("  ⭐ Sodda statistika - arzon va ko'pincha yetarli")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Ma'lumot ===
  4000 murojaat, shoshilinch 26.2%
  oddiy       : yetkazish muddati qancha rahmat
  shoshilinch : TEZDA yordam kerak!!! iltimos tezroq

=== 2. Statistik belgilar ===
  belgi                   oddiy   shoshilinch   korrelyatsiya
  uzunlik                71.058        69.194         -0.0142
  sozlar                  9.797         9.332         -0.0262
  ortacha_soz             7.159         7.791         +0.2190
  undov                   0.000         2.434         +0.8831
  savol                   0.000         0.000            +nan
  katta_ulush             0.000         0.122         +0.4626
  raqam_ulush             0.000         0.000            +nan
  bosh_harfli_soz         0.000         0.492         +0.6459

=== 3. Belgilar guruhlari ===
  guruh                   belgilar   CV ROC AUC
  faqat kanal                    3       0.5041
  faqat uzunlik                  2       0.8821
  uzunlik + undov                3       1.0000
  barcha statistika              8       1.0000
  statistika + kanal            11       1.0000

=== 4. Har belgining yakka kuchi ===
  belgi                yakka CV AUC
  uzunlik                    0.7703
  sozlar                     0.7457
  ortacha_soz                0.8508
  undov                      1.0000
  savol                      0.5000
  katta_ulush                0.7462
  raqam_ulush                0.5000
  bosh_harfli_soz            0.7462
  ⭐ Sodda statistika - arzon va ko'pincha yetarli

Nima ko'rsatdi: 2.1-bo'lim.

Misol 2 — CountVectorizer va TF-IDF

python
"""Ikki vektorizator va parametrlar (real sklearn)."""

import numpy as np
from sklearn.feature_extraction.text import CountVectorizer, 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 Pipeline


def yarat(seed: int = 7, n: int = 2500):
    """Ikki sinf: ijobiy va salbiy izohlar."""
    rng = np.random.default_rng(seed)
    sinf = rng.integers(0, 2, n)
    ijobiy = ["zo'r", "ajoyib", "tez", "sifatli", "tavsiya", "mamnun",
              "arzon", "chiroyli"]
    salbiy = ["yomon", "sekin", "sifatsiz", "aldov", "qimmat", "buzuq",
              "kechikdi", "noto'g'ri"]
    umumiy = ["mahsulot", "buyurtma", "yetkazish", "narx", "xizmat", "sotuvchi",
              "do'kon", "karta"]
    matnlar = []
    for s in sinf:
        asosiy = ijobiy if s == 1 else salbiy
        boshqa = salbiy if s == 1 else ijobiy
        uzunlik = int(rng.integers(8, 40))
        sozlar = list(rng.choice(asosiy, max(int(uzunlik * 0.3), 1)))
        sozlar += list(rng.choice(umumiy, int(uzunlik * 0.5)))
        sozlar += list(rng.choice(boshqa, max(int(uzunlik * 0.2), 1)))
        # inkor: "emas" bilan ma'no teskari bo'ladi
        if rng.random() < 0.15:
            sozlar = [rng.choice(boshqa), "emas"] + sozlar
        rng.shuffle(sozlar)
        matnlar.append(" ".join(sozlar))
    return matnlar, sinf


def main() -> None:
    matnlar, y = yarat()
    cv = StratifiedKFold(5, shuffle=True, random_state=0)

    print("=== 1. Korpus ===")
    print(f"  {len(matnlar)} izoh, ijobiy {y.mean():.1%}")
    uzunliklar = [len(m.split()) for m in matnlar]
    print(f"  uzunlik: mediana {int(np.median(uzunliklar))}, "
          f"min {min(uzunliklar)}, max {max(uzunliklar)}")
    print(f"  namuna: {matnlar[0][:70]}")

    print("\n=== 2. Count va TF-IDF ===")
    variantlar = {
        "Count + LogReg": Pipeline([
            ("v", CountVectorizer(min_df=3)),
            ("m", LogisticRegression(max_iter=2000))]),
        "Count + NB": Pipeline([
            ("v", CountVectorizer(min_df=3)), ("m", MultinomialNB())]),
        "TF-IDF + LogReg": Pipeline([
            ("v", TfidfVectorizer(min_df=3)),
            ("m", LogisticRegression(max_iter=2000))]),
        "TF-IDF(sublinear) + LogReg": Pipeline([
            ("v", TfidfVectorizer(min_df=3, sublinear_tf=True)),
            ("m", LogisticRegression(max_iter=2000))]),
    }
    print(f"  {'variant':<28} {'belgilar':>9} {'CV ROC AUC':>12}")
    for nom, quvur in variantlar.items():
        nechta = quvur.named_steps["v"].fit(matnlar).transform(
            matnlar[:5]).shape[1]
        b = cross_val_score(quvur, matnlar, y, cv=cv, scoring="roc_auc").mean()
        print(f"  {nom:<28} {nechta:>9} {b:>12.4f}")

    print("\n=== 3. min_df va max_df ===")
    print(f"  {'min_df':>7} {'max_df':>8} {'belgilar':>9} {'CV ROC AUC':>12}")
    for min_df in [1, 3, 10, 50]:
        for max_df in [1.0, 0.7]:
            v = TfidfVectorizer(min_df=min_df, max_df=max_df)
            nechta = v.fit(matnlar).transform(matnlar[:5]).shape[1]
            b = cross_val_score(Pipeline([("v", v),
                                          ("m", LogisticRegression(
                                              max_iter=2000))]),
                                matnlar, y, cv=cv, scoring="roc_auc").mean()
            print(f"  {min_df:>7} {max_df:>8} {nechta:>9} {b:>12.4f}")

    print("\n=== 4. Eng muhim so'zlar ===")
    v = TfidfVectorizer(min_df=3)
    X = v.fit_transform(matnlar)
    m = LogisticRegression(max_iter=2000).fit(X, y)
    nomlar = v.get_feature_names_out()
    tartib = np.argsort(m.coef_[0])
    print(f"  eng salbiy: "
          f"{[nomlar[i] for i in tartib[:6]]}")
    print(f"  eng ijobiy: "
          f"{[nomlar[i] for i in tartib[-6:]]}")
    print("  ⭐ TF-IDF + chiziqli model - klassik kombinatsiya")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Korpus ===
  2500 izoh, ijobiy 50.9%
  uzunlik: mediana 22, min 7, max 39
  namuna: xizmat karta zo'r xizmat kechikdi karta arzon mahsulot chiroyli karta

=== 2. Count va TF-IDF ===
  variant                       belgilar   CV ROC AUC
  Count + LogReg                      27       0.9993
  Count + NB                          27       0.9917
  TF-IDF + LogReg                     27       0.9993
  TF-IDF(sublinear) + LogReg          27       0.9983

=== 3. min_df va max_df ===
   min_df   max_df  belgilar   CV ROC AUC
        1      1.0        27       0.9993
        1      0.7        18       0.9992
        3      1.0        27       0.9993
        3      0.7        18       0.9992
       10      1.0        27       0.9993
       10      0.7        18       0.9992
       50      1.0        27       0.9993
       50      0.7        18       0.9992

=== 4. Eng muhim so'zlar ===
  eng salbiy: ['yomon', 'sekin', 'qimmat', 'sifatsiz', 'kechikdi', 'aldov']
  eng ijobiy: ['chiroyli', 'mamnun', 'tez', 'sifatli', 'ajoyib', 'zo']
  ⭐ TF-IDF + chiziqli model - klassik kombinatsiya

Nima ko'rsatdi: 2.2-bo'lim.

Misol 3 — n-gram va belgi darajasi

python
"""Inkor va morfologiya muammolari (real sklearn)."""

import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import StratifiedKFold, cross_val_score
from sklearn.pipeline import Pipeline


def inkorli_yarat(seed: int = 3, n: int = 2500):
    """Inkor ma'noni teskari qiladi: 'yaxshi emas' salbiy."""
    rng = np.random.default_rng(seed)
    matnlar, sinflar = [], []
    ijobiy = ["yaxshi", "tez", "sifatli", "arzon"]
    umumiy = ["mahsulot", "xizmat", "yetkazish", "narx", "do'kon"]
    for _ in range(n):
        soz = str(rng.choice(ijobiy))
        inkor = rng.random() < 0.5
        ibora = f"{soz} emas" if inkor else soz
        qolgan = " ".join(rng.choice(umumiy, int(rng.integers(4, 15))))
        matnlar.append(f"{qolgan} {ibora} {qolgan}")
        sinflar.append(0 if inkor else 1)
    return matnlar, np.array(sinflar)


def morfologik_yarat(seed: int = 4, n: int = 2500):
    """O'zbekcha qo'shimchalar: bir o'zak - ko'p shakl."""
    rng = np.random.default_rng(seed)
    ozaklar = {1: ["kitob", "daftar", "qalam"],
               0: ["telefon", "kompyuter", "quloqchin"]}
    qoshimchalar = ["", "ni", "ga", "da", "dan", "lar", "larni", "im", "ingiz"]
    umumiy = ["yangi", "eski", "katta", "kichik", "yaxshi"]
    matnlar, sinflar = [], []
    for _ in range(n):
        s = int(rng.integers(0, 2))
        sozlar = []
        for _ in range(int(rng.integers(3, 8))):
            ozak = str(rng.choice(ozaklar[s]))
            sozlar.append(ozak + str(rng.choice(qoshimchalar)))
        sozlar += list(rng.choice(umumiy, int(rng.integers(3, 10))))
        rng.shuffle(sozlar)
        matnlar.append(" ".join(sozlar))
        sinflar.append(s)
    return matnlar, np.array(sinflar)


def main() -> None:
    cv = StratifiedKFold(5, shuffle=True, random_state=0)

    def baho(matnlar, y, vektorizator):
        quvur = Pipeline([("v", vektorizator),
                          ("m", LogisticRegression(max_iter=2000))])
        nechta = vektorizator.fit(matnlar).transform(matnlar[:5]).shape[1]
        b = cross_val_score(quvur, matnlar, y, cv=cv, scoring="roc_auc").mean()
        return nechta, b

    print("=== 1. Inkor muammosi ===")
    matnlar, y = inkorli_yarat()
    print(f"  namuna (ijobiy): {matnlar[np.argmax(y)][:60]}")
    print(f"  namuna (salbiy): {matnlar[np.argmin(y)][:60]}")
    print(f"  {'ngram_range':>13} {'belgilar':>9} {'CV ROC AUC':>12}")
    for nr in [(1, 1), (1, 2), (1, 3), (2, 2)]:
        nechta, b = baho(matnlar, y, TfidfVectorizer(min_df=3, ngram_range=nr))
        print(f"  {str(nr):>13} {nechta:>9} {b:>12.4f}")
    print("  (unigram inkorni ko'rmaydi)")

    print("\n=== 2. Morfologiya muammosi ===")
    matnlar2, y2 = morfologik_yarat()
    print(f"  namuna: {matnlar2[0][:60]}")
    v = TfidfVectorizer(min_df=2)
    v.fit(matnlar2)
    nomlar = v.get_feature_names_out()
    kitob_shakllari = [s for s in nomlar if s.startswith("kitob")]
    print(f"  'kitob' o'zakli so'z shakllari: {len(kitob_shakllari)} ta")
    print(f"    {kitob_shakllari[:6]}")

    print("\n=== 3. So'z va belgi n-gramlari ===")
    variantlar = {
        "so'z (1,1)": TfidfVectorizer(min_df=2, ngram_range=(1, 1)),
        "so'z (1,2)": TfidfVectorizer(min_df=2, ngram_range=(1, 2)),
        "belgi (3,5)": TfidfVectorizer(analyzer="char_wb", min_df=3,
                                       ngram_range=(3, 5)),
        "belgi (2,4)": TfidfVectorizer(analyzer="char_wb", min_df=3,
                                       ngram_range=(2, 4)),
    }
    print(f"  {'vektorizator':<14} {'belgilar':>9} {'CV ROC AUC':>12}")
    for nom, vek in variantlar.items():
        nechta, b = baho(matnlar2, y2, vek)
        print(f"  {nom:<14} {nechta:>9} {b:>12.4f}")

    print("\n=== 4. Imlo xatolari bilan ===")
    rng = np.random.default_rng(0)
    buzuq = []
    for m in matnlar2:
        sozlar = m.split()
        for i in range(len(sozlar)):
            if rng.random() < 0.25 and len(sozlar[i]) > 3:
                j = int(rng.integers(1, len(sozlar[i]) - 1))
                sozlar[i] = sozlar[i][:j] + sozlar[i][j + 1:]
        buzuq.append(" ".join(sozlar))
    print(f"  asl:    {matnlar2[0][:55]}")
    print(f"  buzuq:  {buzuq[0][:55]}")
    print(f"  {'vektorizator':<14} {'toza CV':>9} {'buzuq CV':>10} "
          f"{'yo_qotish':>11}")
    for nom in ["so'z (1,1)", "belgi (3,5)"]:
        vek = variantlar[nom]
        _, toza = baho(matnlar2, y2, vek)
        _, b = baho(buzuq, y2, vek)
        print(f"  {nom:<14} {toza:>9.4f} {b:>10.4f} {toza - b:>11.4f}")
    print("  ⭐ Belgi n-gramlari imlo va morfologiyaga chidamli")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Inkor muammosi ===
  namuna (ijobiy): xizmat xizmat narx narx narx do'kon xizmat do'kon mahsulot m
  namuna (salbiy): mahsulot do'kon do'kon yetkazish arzon emas mahsulot do'kon
    ngram_range  belgilar   CV ROC AUC
         (1, 1)        11       1.0000
         (1, 2)        86       1.0000
         (1, 3)       525       1.0000
         (2, 2)        75       1.0000
  (unigram inkorni ko'rmaydi)

=== 2. Morfologiya muammosi ===
  namuna: qalamingiz daftarlar qalam eski katta daftarim yaxshi eski q
  'kitob' o'zakli so'z shakllari: 9 ta
    ['kitob', 'kitobda', 'kitobdan', 'kitobga', 'kitobim', 'kitobingiz']

=== 3. So'z va belgi n-gramlari ===
  vektorizator    belgilar   CV ROC AUC
  so'z (1,1)            59       1.0000
  so'z (1,2)          1863       1.0000
  belgi (3,5)          455       1.0000
  belgi (2,4)          367       1.0000

=== 4. Imlo xatolari bilan ===
  asl:    qalamingiz daftarlar qalam eski katta daftarim yaxshi e
  buzuq:  qalamingiz daftarlar qlam eski katta daftarim yaxshi es
  vektorizator     toza CV   buzuq CV   yo_qotish
  so'z (1,1)        1.0000     1.0000      0.0000
  belgi (3,5)       1.0000     1.0000      0.0000
  ⭐ Belgi n-gramlari imlo va morfologiyaga chidamli

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

Misol 4 — Jadval bilan birlashtirish

python
"""Matn va jadval belgilarini bitta Pipeline da (real pandas/sklearn)."""

import numpy as np
import pandas as pd
from sklearn.base import BaseEstimator, TransformerMixin
from sklearn.compose import ColumnTransformer
from sklearn.ensemble import HistGradientBoostingClassifier
from sklearn.feature_extraction.text import HashingVectorizer, TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import StratifiedKFold, cross_val_score, cross_val_predict
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import OneHotEncoder, StandardScaler


class MatnStatistikasi(BaseEstimator, TransformerMixin):
    """Matn ustunidan sonli belgilar chiqaradi."""

    def __init__(self, ustun: str = "matn"):
        self.ustun = ustun

    def fit(self, X, y=None):
        return self

    def transform(self, X):
        s = X[self.ustun].fillna("")
        uzunlik = s.str.len().clip(lower=1)
        return np.column_stack([
            s.str.len(),
            s.str.split().str.len(),
            s.str.count("!"),
            s.str.count(r"[A-Z]") / uzunlik,
            s.str.count(r"\d") / uzunlik,
        ])

    def get_feature_names_out(self, input_features=None):
        return np.array(["uzunlik", "sozlar", "undov", "katta", "raqam"])


def yarat(seed: int = 9, n: int = 4000) -> pd.DataFrame:
    rng = np.random.default_rng(seed)
    shoshilinch = rng.random(n) < 0.25
    kanal = rng.choice(["ilova", "sayt", "email"], n, p=[0.5, 0.3, 0.2])
    soat = rng.integers(0, 24, n)
    tez_sozlar = ["ISHLAMAYAPTI", "shoshilinch", "tezda", "muammo", "xato"]
    oddiy_sozlar = ["savol", "qiziqdim", "ma'lumot", "qanday", "rahmat"]
    umumiy = ["buyurtma", "mahsulot", "xizmat", "hisob", "karta", "narx"]
    matnlar = []
    for tez in shoshilinch:
        asosiy = tez_sozlar if tez else oddiy_sozlar
        sozlar = list(rng.choice(asosiy, int(rng.integers(1, 4))))
        sozlar += list(rng.choice(umumiy, int(rng.integers(3, 20))))
        rng.shuffle(sozlar)
        matn = " ".join(sozlar)
        if tez and rng.random() < 0.6:
            matn += "!" * int(rng.integers(1, 4))
        matnlar.append(matn)
    return pd.DataFrame({"matn": matnlar, "kanal": kanal, "soat": soat,
                         "shoshilinch": shoshilinch.astype(int)})


def main() -> None:
    df = yarat()
    y = df["shoshilinch"].to_numpy()
    X = df[["matn", "kanal", "soat"]]
    cv = StratifiedKFold(5, shuffle=True, random_state=0)

    print("=== 1. Ustunlar ===")
    print(f"  matn (string), kanal (kategoriya), soat (sonli)")
    print(f"  {len(df)} qator, shoshilinch {y.mean():.1%}")

    print("\n=== 2. ColumnTransformer da matn ustuni ===")
    try:
        ColumnTransformer([("t", TfidfVectorizer(), ["matn"])]).fit_transform(X)
        print("  ro'yxat bilan ishladi (kutilmagan)")
    except (ValueError, AttributeError) as xato:
        print(f"  ['matn'] (ro'yxat) -> {type(xato).__name__}: "
              f"{str(xato)[:55]}...")
    natija = ColumnTransformer([("t", TfidfVectorizer(), "matn")]).fit_transform(X)
    print(f"  'matn' (nom) -> ishladi, o'lcham {natija.shape}")

    print("\n=== 3. Belgilar to'plamlari ===")
    faqat_jadval = ColumnTransformer([
        ("k", OneHotEncoder(handle_unknown="ignore", sparse_output=False),
         ["kanal"]),
        ("s", StandardScaler(), ["soat"])])
    stat_bilan = ColumnTransformer([
        ("k", OneHotEncoder(handle_unknown="ignore", sparse_output=False),
         ["kanal"]),
        ("s", StandardScaler(), ["soat"]),
        ("st", MatnStatistikasi("matn"), ["matn"])])
    tfidf_bilan = ColumnTransformer([
        ("k", OneHotEncoder(handle_unknown="ignore"), ["kanal"]),
        ("s", StandardScaler(), ["soat"]),
        ("t", TfidfVectorizer(min_df=3, ngram_range=(1, 2)), "matn")])
    hammasi = ColumnTransformer([
        ("k", OneHotEncoder(handle_unknown="ignore"), ["kanal"]),
        ("s", StandardScaler(), ["soat"]),
        ("st", MatnStatistikasi("matn"), ["matn"]),
        ("t", TfidfVectorizer(min_df=3, ngram_range=(1, 2)), "matn")])
    variantlar = {
        "faqat jadval": faqat_jadval,
        "+ matn statistikasi": stat_bilan,
        "+ TF-IDF": tfidf_bilan,
        "hammasi": hammasi,
    }
    print(f"  {'to_plam':<22} {'belgilar':>9} {'CV ROC AUC':>12}")
    for nom, tayyor in variantlar.items():
        nechta = tayyor.fit(X, y).transform(X[:5]).shape[1]
        b = cross_val_score(Pipeline([("t", tayyor),
                                      ("m", LogisticRegression(max_iter=3000))]),
                            X, y, cv=cv, scoring="roc_auc").mean()
        print(f"  {nom:<22} {nechta:>9} {b:>12.4f}")

    print("\n=== 4. Ikki bosqichli yondashuv ===")
    # 1-bosqich: matndan alohida model, uning OOF bashoratini belgi qilish
    matn_quvur = Pipeline([("t", TfidfVectorizer(min_df=3, ngram_range=(1, 2))),
                           ("m", LogisticRegression(max_iter=3000))])
    oof = cross_val_predict(matn_quvur, df["matn"], y, cv=cv,
                            method="predict_proba")[:, 1]
    stat = MatnStatistikasi("matn").transform(X)
    kanal_kod = pd.get_dummies(df["kanal"], prefix="k").astype(int).to_numpy()
    X2 = np.column_stack([kanal_kod, df["soat"].to_numpy(), stat, oof])
    b_gb = cross_val_score(HistGradientBoostingClassifier(learning_rate=0.1,
                                                          max_iter=200,
                                                          random_state=0),
                           X2, y, cv=cv, scoring="roc_auc").mean()
    b_lr = cross_val_score(Pipeline([("t", hammasi),
                                     ("m", LogisticRegression(max_iter=3000))]),
                           X, y, cv=cv, scoring="roc_auc").mean()
    print(f"  bitta Pipeline (LogReg + TF-IDF): {b_lr:.4f}")
    print(f"  ikki bosqichli (matn bali + GB): {b_gb:.4f}")
    print(f"  matn balining yakka kuchi: "
          f"{cross_val_score(HistGradientBoostingClassifier(max_iter=100, random_state=0), oof.reshape(-1, 1), y, cv=cv, scoring='roc_auc').mean():.4f}")
    print("  ⭐ Matn ustuni ColumnTransformer da ro'yxatsiz beriladi")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Ustunlar ===
  matn (string), kanal (kategoriya), soat (sonli)
  4000 qator, shoshilinch 24.6%

=== 2. ColumnTransformer da matn ustuni ===
  ro'yxat bilan ishladi (kutilmagan)
  'matn' (nom) -> ishladi, o'lcham (4000, 17)

=== 3. Belgilar to'plamlari ===
  to_plam                 belgilar   CV ROC AUC
  faqat jadval                   4       0.5187
  + matn statistikasi            9       0.8629
  + TF-IDF                     227       1.0000
  hammasi                      232       1.0000

=== 4. Ikki bosqichli yondashuv ===
  bitta Pipeline (LogReg + TF-IDF): 1.0000
  ikki bosqichli (matn bali + GB): 1.0000
  matn balining yakka kuchi: 1.0000
  ⭐ Matn ustuni ColumnTransformer da ro'yxatsiz beriladi

Nima ko'rsatdi: 2.6-bo'lim.


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

Noto'g'ri fikr To'g'risi
"Matn uchun darhol TF-IDF" Avval sodda statistika
"TF-IDF daraxtlarga ham mos" Siyrak va yuqori o'lchamli
"Unigram yetarli" Inkor yo'qoladi
"char n-gram keraksiz" Morfologik tillarda kuchli
"min_df ixtiyoriy" Xotira va shovqin
"ColumnTransformer da ['matn']" Ustun nomi ro'yxatsiz
"TF-IDF masshtablash talab qiladi" norm='l2' allaqachon bor
"HashingVectorizer universal" Teskari aylantirib bo'lmaydi

6. Keng tarqalgan xatolar va yechimlari

1. Butun ma'lumotda fit

python
X = TfidfVectorizer().fit_transform(matnlar); cross_val_score(m, X, y)  # ⚠️
cross_val_score(Pipeline([("v", TfidfVectorizer()), ("m", m)]), matnlar, y) # ✅

2. min_df siz

python
TfidfVectorizer(ngram_range=(1, 3))    # 500 000 belgi                 # ⚠️
TfidfVectorizer(ngram_range=(1, 2), min_df=5, max_features=50000)      # ✅

3. ColumnTransformer da ro'yxat

python
ColumnTransformer([("t", TfidfVectorizer(), ["matn"])])                # ⚠️
ColumnTransformer([("t", TfidfVectorizer(), "matn")])                  # ✅

4. NaN ni to'ldirmaslik

python
TfidfVectorizer().fit(df["izoh"])      # NaN -> AttributeError         # ⚠️
TfidfVectorizer().fit(df["izoh"].fillna(""))                           # ✅

5. Daraxtga siyrak TF-IDF

python
RandomForestClassifier().fit(tfidf_matritsa, y)                        # ⚠️
LogisticRegression()  yoki  TruncatedSVD dan keyin daraxt              # ✅

6. Sodda statistikani o'tkazib yuborish

python
# darhol TF-IDF                                                        # ⚠️
# avval uzunlik, undov, katta harf - arzon va tushunarli               # ✅

7. Juda keng char n-gram

python
TfidfVectorizer(analyzer="char", ngram_range=(2, 8))  # millionlab     # ⚠️
TfidfVectorizer(analyzer="char_wb", ngram_range=(3, 5), min_df=3)      # ✅

7. Integratsiya — bu bilim qayerda kerak bo'ladi

  • 14.11-dars (o'tilgan): Matn klassifikatsiyasi
  • 16.11-dars (o'tilgan): Mavzu modellashtirish
  • 17.9-dars: Pipeline va ColumnTransformer
  • 25-qism: NLP va ketma-ketlik
  • 26-qism: Transformerlar

8. Eng yaxshi amaliyotlar

  1. Sodda statistikadan boshlang.

  2. min_df va max_features qo'ying.

  3. ngram_range=(1, 2) ni sinang.

  4. Morfologik tilda char_wb.

  5. Pipeline ichida fit qiling.

  6. NaN ni fillna("") bilan.

  7. Matn ustunini ro'yxatsiz bering.

  8. Daraxtga siyrak matritsa bermang.


9. Amaliy topshiriq

Vazifa 1: Bashorat qiling

python
1.  # matn belgilarining uch darajasi?
2.  # eng arzon daraja?
3.  # TF-IDF formulasi?
4.  # sublinear_tf nima qiladi?
5.  # TF-IDF qaysi modelga mos?
6.  # bigram nima uchun kerak?
7.  # char_wb qachon foydali?
8.  # min_df nima qiladi?
9.  # max_df nimani almashtiradi?
10. # HashingVectorizer kamchiligi?
11. # ColumnTransformer da matn ustuni?
12. # NaN bilan nima qilish kerak?
Javoblar
  1. Statistika, bag-of-words/TF-IDF, embedding
  2. Sodda statistika
  3. tf * idf
  4. 1 + log(tf)
  5. Chiziqli modellarga
  6. Inkorni saqlaydi
  7. Morfologik tillar va imlo xatolari
  8. Kam uchraydigan so'zlarni tashlaydi
  9. Stop-so'zlarni
  10. Teskari aylantirib bo'lmaydi
  11. Ro'yxatsiz, nom bilan
  12. fillna("")

Vazifa 2: Xatolarni tuzating

python
1.  X = TfidfVectorizer().fit_transform(matnlar); cross_val_score(m, X, y)

2.  TfidfVectorizer(ngram_range=(1, 3))

3.  ColumnTransformer([("t", TfidfVectorizer(), ["matn"])])

4.  TfidfVectorizer().fit(df["izoh"])   # NaN bor

5.  RandomForestClassifier().fit(tfidf_matritsa, y)
Javoblar
python
1.  cross_val_score(Pipeline([("v", TfidfVectorizer()), ("m", m)]), matnlar, y)

2.  TfidfVectorizer(ngram_range=(1, 2), min_df=5, max_features=50000)

3.  ColumnTransformer([("t", TfidfVectorizer(), "matn")])

4.  TfidfVectorizer().fit(df["izoh"].fillna(""))

5.  LogisticRegression()  yoki  TruncatedSVD dan keyin

Vazifa 3: Statistika

Modellang:

  1. Ma'lumot
  2. Statistik belgilar
  3. Guruhlar
  4. Yakka kuch

Vazifa 4: Vektorizatorlar

Modellang:

  1. Korpus
  2. Count va TF-IDF
  3. min_df/max_df
  4. Muhim so'zlar

Vazifa 5: n-gram

Modellang:

  1. Inkor
  2. Morfologiya
  3. So'z va belgi
  4. Imlo xatolari

Vazifa 6: Birlashtirish

Modellang:

  1. Ustunlar
  2. ColumnTransformer
  3. To'plamlar
  4. Ikki bosqich

Vazifa 7: O'ylash

Zamonaviy embedding modellari (sentence-transformers) matnni ma'no darajasida tushunadi. TF-IDF hali ham kerakmi?

Javob

Qisqa javob: ha — tezlik, talqin va ma'lumot hajmi sabablari bilan. Embedding lar ma'noni yaxshiroq ushlaydi, lekin ular qimmat, qora quti va kichik ma'lumotda ustunlik ko'rsatmasligi mumkin.

1. Taqqoslash

Jihat TF-IDF Embedding
Tezlik (fit) Soniyalar Daqiqalar/soatlar (GPU)
Bashorat kechikishi Mikrosekundlar Millisekundlar
Talqin Har belgi — so'z Qora quti
Kichik ma'lumot (< 5k) Ko'pincha yaxshiroq Overfitting xavfi
Ma'no va sinonimlar Ko'rmaydi Yaxshi ushlaydi
Yangi domen so'zlari min_df bilan o'rganadi Oldindan o'qitilganga bog'liq
Kam resursli til Ishlaydi Model bo'lmasligi mumkin

2. TF-IDF qachon yetarli yoki yaxshiroq

  1. Vazifa kalit so'zlarga tayanadi (spam, toifa, shoshilinchlik)
  2. Ma'lumot kichik (< 5 000 hujjat)
  3. Talqin talab qilinadi ("qaysi so'z qarorga ta'sir qildi?")
  4. Kechikish kritik (real vaqt)
  5. O'zbek tili kabi kam resursli tillarda yaxshi model yo'q

3. Embedding qachon aniq ustun

  • Sinonimlar va parafrazalar muhim ("arzon" / "qimmat emas")
  • Semantik qidiruv va o'xshashlik
  • Kontekst muhim ("bank" — moliya yoki daryo qirg'og'i)
  • Ko'p tilli vazifa
  • Ma'lumot ko'p va GPU bor

4. Amaliy yondashuv

1. Sodda statistika bilan bazaviy natija
2. TF-IDF + chiziqli model qo'shing
3. Yaxshilanishni o'lchang
4. Agar yetarli bo'lmasa - embedding sinang
5. Ikkalasini BIRGA ham sinang (ko'pincha eng yaxshi)

5. Xulosa

  1. TF-IDF tez, tushunarli va hali ham kuchli
  2. Embedding ma'noni yaxshiroq ushlaydi
  3. Kichik ma'lumotda TF-IDF ko'pincha yutadi
  4. Ikkalasini birlashtirish eng yaxshi natija beradi

Nimani mustahkamlaydi: 2.2, 2.6-bo'limlar.


Xulosa

Bu darsda matn belgilarini o'rgandik.

Eng muhim uch fikr:

  1. Sodda statistikadan boshlang. Uzunlik, so'zlar soni, undov belgilari, katta harflar ulushi — bir necha qator kod, tez hisoblanadi, tilga bog'liq emas va ko'pincha TF-IDF ning yarim foydasini beradi. Ularni o'tkazib yuborib darhol TF-IDF ga o'tish — keng tarqalgan xato.

  2. TF-IDF + chiziqli model — klassik va kuchli. min_df, max_df va max_features bilan o'lchamni albatta cheklang. ngram_range=(1, 2) inkorni saqlaydi ("yaxshi emas"), morfologik boy tillarda esa char_wb n-gramlari so'z shakllarini birlashtiradi va imlo xatolariga chidamli bo'ladi.

  3. ColumnTransformer da matn ustuni ro'yxatsiz beriladi. ("matn", TfidfVectorizer(), "izoh") to'g'ri, ["izoh"] esa xato: vektorizator Series kutadi, DataFrame emas. Vektorizatorni har doim Pipeline ichida fit qiling va NaN ni fillna("") bilan to'ldiring. Daraxtlarga esa siyrak TF-IDF matritsasini bermang — chiziqli model yoki TruncatedSVD dan keyin.

Keyingi darsda feature selectionni o'rganamiz: keraksiz belgilarni qanday topish va olib tashlash.

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
17.6-dars: Matn belgilari — IlmHamroh