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

23.12-dars: Sentiment tahlili

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


1. Kirish va motivatsiya

Oldingi darslarda matnni sonlarga aylantirishning uch yo'lini ko'rdik: so'z xaltasi va TF-IDF 23.4-bob, embeddinglar (23.6–23.7) va ketma-ketlikni o'qiydigan RNN, LSTM (23.8–23.9). Endi ularni NLP ning eng ko'p so'raladigan amaliy vazifasida sinaymiz — sentiment tahlili: sharh ijobiymi, salbiymi yoki neytralmi?

Vazifa oddiy ko'rinadi: "yaxshi" bo'lsa — ijobiy, "yomon" bo'lsa — salbiy. Lekin o'zbek tilida bu qoida birinchi qadamdayoq sinadi. "Sifati yaxshi emas" — salbiy. "Narxi yomon emas" — ijobiy. "Menga yoqmadi" — salbiy, "bir oyda ham buzilmadi" — ijobiy: inkor qo'shimchasi so'zning ichida turadi. "Narxi arzon, lekin sifati yomon" — ikkala qutb bor, qarorni esa ikkinchi qism qiladi. Bunday misollar kam emas: ular sharhlarning uchdan biridan ko'pini tashkil qilishi mumkin.

Shuning uchun bu darsda bitta umumiy aniqlik bilan cheklanmaymiz. Korpusdagi har sharhga guruh belgisi qo'yamiz — oddiy, yumshoq, inkor, kontrast, aralash, neytral — va har bir model uchun guruh bo'yicha aniqlik jadvali tuzamiz. Umumiy aniqlik o'rtacha narsani aytadi, jadval esa model qayerda sinishini ko'rsatadi.

Real vaziyat. Onlayn do'kon sharhlarni avtomatik saralash uchun lug'atga asoslangan tizim qo'ydi: ijobiy va salbiy so'zlar ro'yxati, ularning farqi — ball. Bir oydan keyin qo'llab-quvvatlash bo'limi shikoyat qildi: "Umuman ishlamadi, pulimga achindim" kabi sharhlar hisobotda "ijobiy" deb ko'rinayotgan ekan — tizim "ishla" ildizini ijobiy so'z deb sanagan, "-ma-" ni esa ko'rmagan. Umumiy aniqlik 70 foizdan yuqori edi, chunki oddiy sharhlar ko'pchilik edi. Inkorli sharhlar bo'yicha alohida o'lchov qilinganda aniqlik tasodifiydan ham past chiqdi.

Bu darsda sentiment modelini qurish va uni "qiyin" guruhlar bo'yicha halol baholashni o'rganamiz.

Bu darsda:

  • Sintetik o'zbekcha sharhlar korpusi va guruhlar
  • Lug'atga asoslangan bazaviy va uning inkor, kontrastda yiqilishi
  • TF-IDF + LogisticRegression: unigram va bigram
  • EmbeddingBag va LSTM: tartibni ko'radigan model
  • Guruh bo'yicha aniqlik jadvali va juftlashgan taqqoslash
  • Neytral sinfning qiyinligi
  • Kalibratsiya: model ishonchi qanchalik rost
  • Tuzoqlar

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


2. Nazariya — chuqur tushuntirish

2.1. Sentiment vazifasi va korpus

text
VAZIFA:
  kirish: sharh matni          chiqish: salbiy / neytral / ijobiy
  3 sinf, nomutanosib: ijobiy 44%, salbiy 35%, neytral 21%

SINTETIK KORPUS (6000 sharh, seed bilan):
  jihat + sifat:        "narxi arzon", "kuryer imillagan"
  fe'l:                 "menga yoqdi", "ikki kunda buzildi"
  faktlar (fikrsiz):    "buyurtma juma kuni keldi", "rangi qora"
  shovqin:              apostrof variantlari, 4% so'zda imlo xatosi,
                        3% tasodifiy yorliq xatosi

GURUHLAR (har sharhda bittasi):
  oddiy     "sotuvchi xushmuomala, narxi qulay"          -> ijobiy
  yumshoq   "yomon emas", "biroz kechikdi"               -> ijobiy/salbiy
            (annotatorlar 25% holda "neytral" deb qo'ygan)
  inkor     "sifati a'lo emas", "menga yoqmadi"          -> teskari qutb
  kontrast  "narxi arzon, lekin sifati chala"            -> 2-qism qutbi
  aralash   "sifati chala va bahosi hamyonbop"           -> neytral
  neytral   "hali ishlatib ko'rmadim", faktlar           -> neytral

Real korpus o'rniga sintetik korpus ishlatishimizning sababi bitta: har sharhning qaysi hodisa ekanini bilamiz. Real ma'lumotda "bu sharhda inkor bormi?" savoliga javob berish uchun qo'shimcha yorliqlash kerak bo'ladi. Bu yerda esa guruh generatorning o'zidan keladi va har model uchun inkor, kontrast va aralash sharhlardagi aniqlikni alohida o'lchay olamiz.

Korpus ataylab "iflos": 1-misolda sharhlarning 38.9% ida nostandart apostrof (\u02bb, \u2019 yoki teskari tirnoq) bor, 4% so'zda imlo xatosi. "zo'r" so'zi to'rt xil yozilishda uchradi. Normallashtirish 23.1-bob noyob tokenlar sonini 993 dan 880 ga tushirdi — aynan shu farq normallashtirilmagan modelda "bir so'zning to'rt nusxasi" bo'lib qolardi.

Sentiment modelini guruh bo'yicha baholash uchun har misolning hodisa turi ma'lum bo'lishi kerak — sintetik korpus buni tekin beradi, real loyihada esa kichik qo'lda yorliqlangan "qiyin to'plam" yig'iladi.

2.2. Lug'atga asoslangan bazaviy va uning chegarasi

text
LUG'AT BAZAVIYSI:
  IJOBIY_ILDIZ = ["yaxshi", "zo'r", "arzon", "yoq", ...]      (12 ta)
  SALBIY_ILDIZ = ["yomon", "qimmat", "kech", "buzil", ...]    (12 ta)
  ball = (ijobiy ildizlar soni) - (salbiy ildizlar soni)
  ball > 0 -> ijobiy,  ball < 0 -> salbiy,  ball = 0 -> neytral

QOIDA 1 - INKOR:
  keyingi so'z "emas"            -> ishora teskari   ("yaxshi emas")
  ildizdan keyin "ma..."         -> ishora teskari   ("yoq-ma-di")
QOIDA 2 - KONTRAST:
  oxirgi "lekin / ammo / biroq" dan keyingi qism hal qiladi

NIMADA YIQILADI:
  "kech-ik-ma-sdan keldi"     -> "-ma-" ildizdan keyin emas, ichkarida
  "tavsiya qil-ma-yman"       -> inkor boshqa so'zda
  "operator e'tiborsiz"       -> so'z lug'atda yo'q (qamrov)
  "sifati chala va narxi arzon" (aralash) -> ball +1, neytral emas

Lug'at bazaviysi har loyihada qurilishi kerak: u tez, tushunarli va o'rgatish ma'lumotini talab qilmaydi. Lekin 1-misol uning ikki asosiy kamchiligini raqam bilan ko'rsatdi. Birinchisi — inkor: faqat lug'at inkor guruhida 0.013 aniqlik berdi, ya'ni deyarli har doim teskari javob. Ikkinchisi — qamrov: fikr bildirgan sharhlarning 33.1% ida lug'atdagi birorta ildiz topilmadi, chunki til sinonimlarga boy ("imillagan", "e'tiborsiz", "hamyonbop").

Qoidalar yordam beradi, lekin qisman: inkor qoidasi inkor guruhini 0.452 ga, "lekin" qoidasi kontrastni 0.258 dan 0.546 ga ko'tardi. Umumiy aniqlik 0.483 dan 0.628 ga chiqdi — va baribir 0.7 dan past. Har yangi qoida yangi istisno ochadi: "-ma-" qoidasi "kechikmasdan" ni tanimaydi, chunki inkor ildizdan keyin emas, ichkarida.

Sarkazm haqida alohida. "Zo'r, uch kunda ikki marta buzildi" — so'zlar ijobiy, ma'no salbiy. Bu darsda sarkazm o'lchanmaydi, va buni ochiq aytish kerak: sintetik generatorda sarkazm yo'q, real korpusda esa uni ajratish uchun kontekst, ohang va dunyo bilimi kerak — hatto annotatorlar ham ko'pincha kelisha olmaydi. Shuning uchun sarkazm bo'yicha ball keltirilmaydi; real loyihada uning ulushini kichik qo'lda tekshirilgan namunada baholash mumkin, xolos.

Lug'at bazaviysi — pastki chegara: inkor, kontrast va qamrovda sinadi, qoidalar esa har safar yangi istisno ochadi.

2.3. TF-IDF + LogisticRegression: bigram inkorni "ko'radi"

text
TfidfVectorizer(preprocessor=normallashtir, ngram_range=(1, 2),
                sublinear_tf=True, min_df=2)
LogisticRegression(C=5.0)       # vektorizator FAQAT o'quvda fit qilinadi

UNIGRAM (1,1):
  "sifati yaxshi emas" = {sifati, yaxshi, emas}
  "yaxshi" -> ijobiy vazn, "emas" -> ikkala sinfda uchraydi
  => inkorni faqat qisman ushlaydi

BIGRAM (1,2):
  + {"sifati yaxshi", "yaxshi emas"}
  "yaxshi emas" - alohida belgi, o'z vazni bilan
  => inkor = yangi so'z

KONTRAST - TARTIB MUAMMOSI:
  "narxi arzon lekin sifati yomon"   -> salbiy
  "sifati yomon lekin narxi arzon"   -> ijobiy
  unigram xaltasi IKKALASIDA BIR XIL -> bir xil bashorat
  bigram: "lekin sifati" va "lekin narxi" farqlanadi -> qisman yordam

LogisticRegression har bir belgiga (so'z yoki so'z juftiga) vazn beradi. 2-misolda o'rganilgan vaznlar inkorni juda aniq ko'rsatdi: 'yaxshi' ning ijobiy yo'nalishdagi vazni +2.68, 'yaxshi emas' niki esa -8.07; 'yomon' — -4.66, 'yomon emas' — +8.25. Bigram inkorli iborani alohida so'z sifatida o'rgandi va uning vazni asl so'znikidan kuchliroq — chunki u asl so'zning ta'sirini ham "yopishi" kerak.

Bigram inkor guruhida aniqlikni 0.827 dan 0.934 ga ko'tardi. Fe'l inkori ("yoqmadi") uchun esa bigram ham shart emas: "yoqdi" va "yoqmadi" — ikki xil token, unigram ularni allaqachon farqlaydi (+5.56 va -4.31). O'zbek tilining agglyutinativligi bu yerda foydaga ishlaydi.

Kontrastda holat boshqacha. Unigram modeli uchun "narxi arzon lekin sifati yomon" va "sifati yomon lekin narxi arzon" — bitta xalta, shuning uchun 2-misolda ikkalasi ham aynan 0.658 ijobiy ehtimol oldi. Bigram modeli ularni ajratdi (0.253 va 0.939) — "lekin sifati" va "lekin narxi" juftlari qaysi jihat ikkinchi kelganini bildiradi. Lekin bu yechim mo'rt: u faqat jihat nomi "lekin" dan keyin bevosita kelganda ishlaydi.

Bigram inkorni arzon yo'l bilan hal qiladi; kontrast esa tartib masalasi — so'z xaltasi uni faqat qisman ushlaydi.

2.4. EmbeddingBag va LSTM

text
LUG'AT 23.2-bob: faqat o'quvdan, min_son=2; 0 - <pad>, 1 - <unk>
  kodla: matn -> [indekslar], uzunliklar L

EmbeddingBag(V, 32, mode="mean")  -> Linear(32, 3)
  = neyron so'z xaltasi: embeddinglar o'rtachasi, TARTIB YO'Q
  unigram TF-IDF bilan bir oilada

Embedding(V, 32) -> LSTM(32, 48) -> oxirgi holat h[-1] -> Linear(48, 3)
  pack_padded_sequence(..., enforce_sorted=False)   23.9-bob
  -> h[-1] - HAQIQIY oxirgi tokendan keyingi holat, pad dan emas
  chapdan o'ngga o'qiydi: "lekin" dan keyingi qism oxirgi
  holatga eng yaqin -> kontrast uchun tabiiy

O'RGATISH:
  AdamW(lr=5e-3), batch 128, 10 davr
  eng yaxshi val aniqlik -> copy.deepcopy(state_dict()) (21.5)

EmbeddingBag — neyron tarmoq, lekin ma'no jihatidan u unigram so'z xaltasi: embeddinglarni o'rtachalaydi va tartibni yo'qotadi. 3-misolda u 0.750 aniqlik berdi — unigram TF-IDF (0.769) bilan bir darajada, bigram TF-IDF (0.845) dan ancha past. Aralash guruhda esa atigi 0.092: "+1 va -1 teng" degan tushuncha o'rtachada yo'qoladi. Bu muhim saboq: neyron tarmoq o'z-o'zidan yaxshiroq emas — agar u ma'lumotdagi tuzilmani ko'rmasa, oddiy chiziqli modeldan ham yutqazadi.

LSTM tartibni ko'radi. U sharhni chapdan o'ngga o'qiydi va oxirgi holatda butun matnning "xulosasi" qoladi. Kontrast uchun bu tabiiy: qarorni hal qiluvchi ikkinchi qism oxirgi holatga eng yaqin. pack_padded_sequence shart — aks holda qisqa sharhlarning oxirgi holati pad tokenlaridan keyingi holat bo'lardi 23.9-bob.

EmbeddingBag — neyron so'z xaltasi, LSTM — tartibni ko'radigan model; "neyron" so'zi emas, model ko'ra oladigan tuzilma natijani belgilaydi.

2.5. Qiyin guruhlar jadvali va juftlashgan taqqoslash

text
GURUH BO'YICHA ANIQLIK (3-misol, test, 1200 sharh):
                 hamma  oddiy yumshoq inkor kontrast aralash neytral
  lug'at+qoida   0.632  0.686  0.574  0.438  0.594   0.421   0.979
  TF-IDF (1,2)   0.845  0.888  0.694  0.934  0.866   0.316   0.937
  EmbeddingBag   0.750  0.814  0.611  0.810  0.708   0.092   0.972
  LSTM           0.906  0.928  0.694  0.942  0.946   0.737   0.972

JUFTLASHGAN FARQ (bir xil test sharhlari):
  d_i = [LSTM to'g'ri] - [TF-IDF to'g'ri]     (har sharh uchun -1, 0 yoki 1)
  farq = mean(d),  SE = std(d) / sqrt(n)
  |farq| > 2 * SE -> sezilarli

Jadval umumiy aniqlik yashiradigan narsani ochadi. LSTM TF-IDF dan umuman +0.061 ga yaxshi (2*SE = 0.019) — sezilarli. Lekin bu yutuq qayerdan keladi? Juftlashgan taqqoslash javob beradi: aralash guruhda +0.421 va kontrast da +0.079 (2*SE = 0.049) — ikkalasi ham tartib va ikki qism orasidagi munosabat masalasi. Inkor guruhida esa farq +0.009 — sezilmas: bigram inkorni LSTM chalik yaxshi ushlaydi.

Yumshoq guruhida ikkala model ham 0.694 da to'xtadi. Bu modelning emas, ma'lumotning chegarasi: yumshoq sharhlarning chorak qismini annotatorlar "neytral" deb belgilagan, va "yomon emas" matni bir xil bo'lgani uchun hech bir model ularni ajrata olmaydi. Bunday guruhda aniqlikni ko'tarishga urinish — yorliq shovqinini yodlashga urinish.

Bitta seed natijasi tasodif bo'lishi mumkin. 3-misolda LSTM uch seedda umumiy 0.898 +- 0.015, kontrastda 0.959 +- 0.012 berdi — TF-IDF bilan farq seedlar tebranishidan ancha katta.

Guruh jadvali "qayerda?" savoliga, juftlashgan farq "haqiqatanmi?" savoliga javob beradi; yutuq faqat sezilarli bo'lgan guruhlarda da'vo qilinadi.

2.6. Neytral sinf va kalibratsiya

text
NEYTRAL NEGA QIYIN:
  1) "fikrsiz" matn (faktlar) - oson
  2) aralash: ijobiy + salbiy so'zlar, lekin umumiy qutb yo'q - qiyin
  3) yumshoq ifodalar: annotatorlar kelishmaydi - yorliqning o'zi shovqinli
  4) kam sinf (21%) - model uni "chetlab o'tishga" moyil

KALIBRATSIYA - ishonch rostmi?
  ishonch = max softmax ehtimoli
  kalibrlangan model: ishonch 0.9 bo'lgan sharhlarning ~90% i to'g'ri
  ECE = sum_b (n_b / n) * |o'rtacha ishonch_b - aniqlik_b|

HARORAT BILAN KALIBRLASH (temperature scaling):
  p = softmax(logit / T)
  T validatsiyadagi NLL ni minimallashtiradi   (T > 1 -> ishonch pasayadi)
  argmax o'zgarmaydi -> aniqlik o'zgarmaydi, faqat ishonch "rostlanadi"

ISHLATILISHI:
  ishonch < chegara -> odamga (qo'lda ko'rish navbati)

4-misoldagi chalkashlik matritsasi neytralning qiyinligini ko'rsatdi: LSTM uchun neytral recall 0.801, salbiy — 0.922, ijobiy — 0.942. TF-IDF da farq yanada katta (neytral 160/246, ya'ni 0.650). Neytral sharhlar kelib chiqishi bo'yicha ajratilganda manzara aniq bo'ladi: sof neytral sharhlarda recall 0.986, aralashlarda 0.757, annotator "neytral" deb qo'ygan yumshoq sharhlarda esa atigi 0.115. Oxirgisi — yorliq shovqini, uni hech bir model "tuzata" olmaydi.

Kalibratsiya boshqa savolga javob beradi: model "90% ishonaman" desa, unga ishonsa bo'ladimi? 4-misolda ikki model qarama-qarshi tomonga adashdi. LogisticRegression o'ziga ishonmaydi: o'rtacha ishonch 0.787, aniqlik esa 0.845 (C=5 bilan ham regularizatsiya ehtimollarni o'rtaga tortadi). LSTM esa o'ziga ortiqcha ishonadi: ishonch 0.942, aniqlik 0.906. Validatsiyada tanlangan harorat T = 1.45 LSTM ning ECE sini 0.044 dan 0.029 ga tushirdi — aniqlikka tegmasdan.

Kalibrlangan ishonchning amaliy qiymati — qaysi sharhni odamga berish. Chegara 0.8 bo'lganda sharhlarning 82.2% i avtomatik qayta ishlanadi va ularda aniqlik 0.945 ga chiqadi; qolgan 214 tasi odamga ketadi. Past ishonchli sharhlarning 40.7% i neytral — umumiy ulushdan (20.5%) ikki barobar ko'p. Model qayerda "ikkilanishini" to'g'ri biladi.

Neytral — eng qiyin sinf, chunki uning chegarasi odamlar uchun ham noaniq; kalibrlangan ishonch esa aynan shu chegarani odamga yo'naltirish vositasi.

2.7. Tuzoqlar

Asosiy tuzoqlar: faqat umumiy aniqlik bilan hisobot berish (inkor va kontrastdagi sinish ko'rinmaydi); lug'at bazaviysini qurmasdan to'g'ridan-to'g'ri neyron modelga o'tish; apostrof variantlarini normallashtirmaslik ("zo'r" to'rt xil token bo'ladi); inkorni lug'at qoidasi bilan "tuzatishga" urinib, cheksiz istisnolar yozish; unigram modeldan kontrastni kutish; EmbeddingBag ni "neyron, demak yaxshiroq" deb hisoblash; lug'at va vektorizatorni butun korpusda qurish; LSTM da pack_padded_sequence siz oxirgi holatni pad dan olish; guruhdagi farqni juftlashgan SE siz "yutuq" deb e'lon qilish; yorliq shovqini bor guruhda (yumshoq) aniqlikni cheksiz ko'tarishga urinish; sarkazm bo'yicha o'lchanmagan ball keltirish; ishonchni kalibrlashsiz qaror chegarasi sifatida ishlatish; haroratni test to'plamida tanlash.


3. Tez ma'lumotnoma

python
matnlar, y, g = korpus(6000, seed=42)                  # g - guruh belgisi
tr, va, te = bolish(y)                                 # stratifikatsiya, test qulf
p = [lugat_bashorat(m, inkor=True, kontrast=True) for m in matnlar[te]]
v = TfidfVectorizer(preprocessor=normallashtir, ngram_range=(1, 2),
                    sublinear_tf=True, min_df=2)
lr = LogisticRegression(C=5.0, max_iter=3000).fit(v.fit_transform(matnlar[tr]), y[tr])
lug = Lugat(matnlar[tr]); x, L = lug.kodla(matnlar[te])        # faqat o'quvdan
model = LSTMModel(len(lug)); orgat(model, D["tr"], D["va"])     # deepcopy
for gr in GURUHLAR:                                    # guruh bo'yicha aniqlik
    print(gr, np.mean(p[g[te] == gr] == y[te][g[te] == gr]))
d = (p_lstm == y_te).astype(float) - (p_tfidf == y_te)   # juftlashgan farq
T = argmin_T NLL(logit_val / T); ehtimol = softmax(logit_test / T)

Sentiment tahlili xulosasi

normallashtirish -> lug'at bazaviysi (pastki chegara)
guruh belgisi: oddiy / yumshoq / inkor / kontrast / aralash / neytral
TF-IDF bigram: inkorni ushlaydi, kontrastni qisman
EmbeddingBag = neyron so'z xaltasi; LSTM = tartib -> kontrast, aralash
guruh jadvali + juftlashgan farq (2*SE) -> yutuq qayerda va haqiqiymi
neytral eng qiyin; harorat bilan kalibrlash; past ishonch -> odamga
sarkazm o'lchanmaydi - buni ochiq ayting

4. Batafsil misollar

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

Misol 1 — Korpus, normallashtirish va lug'at bazaviysi

python
"""Sintetik sharhlar korpusi, normallashtirish va lug'atga asoslangan bazaviy."""

import re
from collections import Counter

import numpy as np

SINFLAR = ["salbiy", "neytral", "ijobiy"]
GURUHLAR = ["oddiy", "yumshoq", "inkor", "kontrast", "aralash", "neytral"]
ULUSH = [0.36, 0.09, 0.20, 0.17, 0.06, 0.12]
APOSTROFLAR = ["'", "'", "'", "\u02bb", "\u2019", "`"]
ASPEKTLAR = [   # (jihat nomlari, ijobiy sifatlar, salbiy sifatlar)
    (["sifati", "materiali", "ishlanishi"],
     ["yaxshi", "zo'r", "a'lo", "ajoyib", "mustahkam", "puxta"],
     ["yomon", "past", "bo'sh", "nochor", "xom", "chala"]),
    (["narxi", "bahosi"], ["arzon", "hamyonbop", "qulay", "maqbul"],
     ["qimmat", "baland", "osmonda", "haddan tashqari"]),
    (["yetkazib berish", "dostavka", "kuryer"],
     ["tez", "o'z vaqtida", "chaqqon", "tezkor"],
     ["sekin", "kech", "imillagan", "sust"]),
    (["xizmat", "sotuvchi", "operator"],
     ["yaxshi", "xushmuomala", "a'lo", "e'tiborli"],
     ["yomon", "qo'pol", "e'tiborsiz", "beparvo"]),
    (["qadog'i", "o'rami"], ["chiroyli", "butun", "puxta", "ozoda"],
     ["yirtilgan", "ezilgan", "yomon", "iflos"]),
]
KUCHAYTIR = ["", "", "", "juda ", "ancha ", "rosa "]
FEL_IJOBIY = [("menga yoqdi", "menga yoqmadi"),       # (asl, -ma- shakli)
              ("tavsiya qilaman", "tavsiya qilmayman"),
              ("yana buyurtma beraman", "boshqa buyurtma bermayman"),
              ("mamnun qoldim", "mamnun qolmadim"),
              ("pulimga arzidi", "pulimga arzimadi"),
              ("kutganimdek chiqdi", "kutganimdek chiqmadi")]
FEL_SALBIY = [("ikki kunda buzildi", "bir oyda ham buzilmadi"),
              ("pulimga achindim", "pulimga achinmadim"),
              ("afsuslandim", "afsuslanmadim"),
              ("hafsalam pir bo'ldi", "hafsalam pir bo'lmadi"),
              ("kechikib keldi", "kechikmasdan keldi"),
              ("qaytarib berdim", "qaytarishga hojat bo'lmadi")]
YUMSHOQ = {1: ["yomon emas", "chidasa bo'ladi", "umuman olganda normal",
               "narxiga arziydi"],
           -1: ["biroz kechikdi", "kutganimdan sal pastroq", "unchalik emas",
                "ozgina hafsalam pir bo'ldi"]}
FAKTLAR = ["buyurtma {kun} kuni keldi", "rangi {rang}", "o'lchami {olcham}",
           "sovg'a uchun oldim", "qutisida yo'riqnoma bor",
           "narxi {son} ming so'm", "{shahar}ga yetkazishdi",
           "onamga olib berdim", "ikki dona oldim"]
NEYTRAL_FIKR = ["hali ishlatib ko'rmadim", "oddiy mahsulot", "sifati o'rtacha",
                "narxiga yarasha", "na yaxshi, na yomon", "hozircha fikr yo'q",
                "yaxshi ham emas, yomon ham emas", "boshqasidan farqi yo'q",
                "yaxshi yoki yomonligini keyin bilaman"]
TOLDIR = {"kun": ["dushanba", "seshanba", "juma", "shanba"],
          "rang": ["qora", "oq", "ko'k", "kulrang", "qizil"],
          "olcham": ["o'rtacha", "katta", "kichik", "standart"],
          "son": ["120", "85", "240", "56", "310"],
          "shahar": ["Toshkent", "Samarqand", "Buxoro", "Namangan", "Qarshi"]}
BOGLOVCHI = ["lekin", "ammo", "biroq"]


def tanla(rng, royxat):
    return royxat[int(rng.integers(len(royxat)))]


def fakt(rng):
    s = tanla(rng, FAKTLAR)
    for k, v in TOLDIR.items():
        s = s.replace("{" + k + "}", tanla(rng, v))
    return s


def birlik(rng, qutb, inkor=False, jihat=None):
    """Bitta fikr bo'lagi; qutb (+1/-1) - bo'lakning HOSIL bo'lgan ma'nosi.
    jihat: None - tasodifiy, son - shu jihat sifati, "fel" - fe'lli bo'lak."""
    if jihat is None:
        jihat = int(rng.integers(len(ASPEKTLAR))) if rng.random() < 0.55 else "fel"
    if jihat != "fel":                               # "narxi arzon"
        nomlar, ij, sl = ASPEKTLAR[jihat]
        nom = tanla(rng, nomlar)
        if not inkor:
            return f"{nom} {tanla(rng, KUCHAYTIR)}{tanla(rng, ij if qutb > 0 else sl)}"
        oldi = "unchalik " if qutb < 0 and rng.random() < 0.3 else ""
        return f"{nom} {oldi}{tanla(rng, sl if qutb > 0 else ij)} emas"
    ij, sl = tanla(rng, FEL_IJOBIY), tanla(rng, FEL_SALBIY)
    if not inkor:
        return (ij if qutb > 0 else sl)[0]
    return (sl if qutb > 0 else ij)[1]               # "-ma-": buzilmadi -> ijobiy


def ikki_jihat(rng, q1, q2, inkor2=False):
    """Ikki xil jihat haqida ikki bo'lak (masalan narx va sifat)."""
    i, j = rng.choice(len(ASPEKTLAR), size=2, replace=False)
    ikkinchi = int(j) if rng.random() < 0.6 else "fel"
    return birlik(rng, q1, jihat=int(i)), birlik(rng, q2, inkor2, ikkinchi)


def yozuvga(rng, bolaklar):
    matn = ""
    for i, b in enumerate(bolaklar):
        if i == 0:
            matn = b[0].upper() + b[1:]
        elif b.split()[0] in BOGLOVCHI:
            matn += ", " + b
        else:
            ayir = tanla(rng, [". ", ", ", ". ", " va "])
            matn += ayir + (b[0].upper() + b[1:] if ayir == ". " else b)
    return matn + tanla(rng, [".", ".", "!", "", "!!"])


def buz(rng, matn, p_imlo):
    """Apostrof variantlari va imlo xatolari (tushgan, ikkilangan, almashgan harf)."""
    sozlar = []
    for s in matn.split(" "):
        if "'" in s:
            s = s.replace("'", tanla(rng, APOSTROFLAR))
        if len(s) >= 5 and rng.random() < p_imlo:
            j, amal = int(rng.integers(2, len(s) - 1)), int(rng.integers(3))
            if amal == 0:
                s = s[:j] + s[j + 1:]
            elif amal == 1:
                s = s[:j] + s[j] + s[j:]
            else:
                s = s[:j - 1] + s[j] + s[j - 1] + s[j + 1:]
        sozlar.append(s)
    return " ".join(sozlar)


def sharh(rng):
    g = int(rng.choice(len(GURUHLAR), p=ULUSH))
    qutb = 1 if rng.random() < 0.55 else -1
    faktlar = [fakt(rng) for _ in range(int(rng.integers(0, 3)))]
    yorliq = 2 if qutb > 0 else 0
    if g == 0:                                       # oddiy
        bol = [birlik(rng, qutb) for _ in range(int(rng.integers(1, 3)))]
    elif g == 1:                                     # yumshoq: "yomon emas"
        bol = [tanla(rng, YUMSHOQ[qutb])]
        if rng.random() < 0.25:                      # annotatorlar kelisha olmaydi
            yorliq = 1
    elif g == 2:                                     # inkor: "emas", "-ma-"
        bol = [birlik(rng, qutb, inkor=True)
               for _ in range(int(rng.integers(1, 3)))]
    elif g == 3:                                     # kontrast: 2-qism hal qiladi
        a, b = ikki_jihat(rng, -qutb, qutb, inkor2=rng.random() < 0.25)
        bol, faktlar = [a, tanla(rng, BOGLOVCHI) + " " + b], faktlar[:1]
        return yozuvga(rng, faktlar + bol), yorliq, g
    elif g == 4:                                     # aralash: +/- teng -> neytral
        a, b = ikki_jihat(rng, qutb, -qutb)
        bol, yorliq = [a + tanla(rng, [", ", " va "]) + b], 1
    else:                                            # neytral
        bol = [tanla(rng, NEYTRAL_FIKR)] if rng.random() < 0.7 else []
        faktlar = faktlar or [fakt(rng)]
        yorliq = 1
    bol += faktlar
    rng.shuffle(bol)
    return yozuvga(rng, bol), yorliq, g


def korpus(n, seed, p_imlo=0.04, p_shovqin=0.03):
    """Sintetik o'zbekcha sharhlar: matnlar, yorliqlar, guruhlar."""
    rng = np.random.default_rng(seed)
    matnlar, y, guruh = [], [], []
    for _ in range(n):
        m, yl, g = sharh(rng)
        if rng.random() < p_shovqin:                # tasodifiy yorliq xatosi
            yl = int(rng.integers(3))
        matnlar.append(buz(rng, m, p_imlo))
        y.append(yl)
        guruh.append(GURUHLAR[g])
    return np.array(matnlar, dtype=object), np.array(y), np.array(guruh)


def normallashtir(s):
    s = s.lower()
    for v in ("\u02bb", "\u2019", "\u2018", "\u02bc", "`"):
        s = s.replace(v, "'")
    s = re.sub(r"[^a-z0-9' ]+", " ", s)
    return " ".join(s.split())


IJOBIY_ILDIZ = ["yaxshi", "zo'r", "a'lo", "ajoyib", "arzon", "qulay", "tez",
                "chiroyli", "yoq", "tavsiya", "mamnun", "arzi"]
SALBIY_ILDIZ = ["yomon", "past", "qimmat", "sekin", "kech", "qo'pol", "buzil",
                "achin", "afsuslan", "hafsala", "qaytar", "iflos"]
ILDIZLAR = [(s, 1) for s in IJOBIY_ILDIZ] + [(s, -1) for s in SALBIY_ILDIZ]


def lugat_bashorat(matn, inkor=False, kontrast=False):
    """Lug'at bazaviysi: ildiz mos kelsa +1/-1; ball ishorasi -> sinf."""
    t = normallashtir(matn).split()
    if kontrast:                        # oxirgi "lekin" dan keyingi qism hal qiladi
        for j in range(len(t) - 1, -1, -1):
            if t[j] in BOGLOVCHI:
                t = t[j + 1:]
                break
    ball = 0
    for i, soz in enumerate(t):
        for ildiz, q in ILDIZLAR:
            if soz.startswith(ildiz):
                if inkor and (soz[len(ildiz):].startswith("ma")
                              or t[i + 1:i + 2] == ["emas"]):
                    q = -q              # "yoqmadi", "yaxshi emas"
                ball += q
                break
    return 2 if ball > 0 else 0 if ball < 0 else 1


def main() -> None:
    matnlar, y, g = korpus(6000, seed=42)
    print("=== 1. Korpus ===")
    print(f"  sharhlar: {len(y)}")
    for k, s in enumerate(SINFLAR):
        print(f"    {s:<8} {np.sum(y == k):>5}  ({np.mean(y == k):.1%})")
    print("  guruhlar: " + ", ".join(f"{gr} {np.sum(g == gr)}" for gr in GURUHLAR))

    print("\n=== 2. Har guruhdan namuna (normallashtirilgan) ===")
    for gr in GURUHLAR:
        i = int(np.flatnonzero(g == gr)[0])
        print(f"  [{gr:<8}] {SINFLAR[y[i]]:<7} {normallashtir(matnlar[i])}")

    print("\n=== 3. Normallashtirish ===")
    nostandart = np.mean([bool(re.search("[\u02bb\u2019`]", m)) for m in matnlar])
    print(f"  nostandart apostrofli sharhlar: {nostandart:.1%}")
    xom = Counter(s.strip(".,!").lower() for m in matnlar for s in m.split())
    toza = Counter(s for m in matnlar for s in normallashtir(m).split())
    print(f"  noyob tokenlar: xom {len(xom)} -> normallangan {len(toza)}")
    zor = sorted((n, s) for s, n in xom.items() if normallashtir(s) == "zo'r")
    print("  zo'r so'zining yozilishlari: " + ", ".join(
        f"{s.encode('ascii', 'backslashreplace').decode()} ({n})"
        for n, s in reversed(zor)))

    print("\n=== 4. Lug'at bazaviysi: guruh bo'yicha aniqlik ===")
    print(f"  {'variant':<16}{'hamma':>8}" + "".join(f"{gr:>9}" for gr in GURUHLAR))
    variantlar = [("faqat lug'at", {}), ("+ inkor", {"inkor": True}),
                  ("+ inkor + lekin", {"inkor": True, "kontrast": True})]
    natija = {}
    for nom, kw in variantlar:
        p = np.array([lugat_bashorat(m, **kw) for m in matnlar])
        natija[nom] = p
        q = [np.mean(p == y)] + [np.mean(p[g == gr] == y[g == gr])
                                 for gr in GURUHLAR]
        print(f"  {nom:<16}" + "".join(f"{v:>8.3f} " for v in q).rstrip())

    print("\n=== 5. Qoidalar qayerda yiqiladi (eng to'liq variant) ===")
    p = natija["+ inkor + lekin"]
    for gr in ["inkor", "kontrast", "aralash"]:
        xato = np.flatnonzero((g == gr) & (p != y))
        i = int(xato[0])
        print(f"  [{gr}] {normallashtir(matnlar[i])}")
        print(f"      haqiqiy {SINFLAR[y[i]]}, lug'at {SINFLAR[p[i]]}")

    print("\n=== 6. Lug'at qamrovi ===")
    bos = np.array([lugat_bashorat(m) == 1 for m in matnlar])
    fikrli = y != 1
    print(f"  fikrli sharhlarda lug'at so'zi topilmadi: {np.mean(bos[fikrli]):.1%}")
    print(f"  qoidalar soni: ildiz {len(ILDIZLAR)}, inkor 2, kontrast 1")
    yaxshilanish = np.mean(p == y) - np.mean(natija["faqat lug'at"] == y)
    print(f"  qoidalar qo'shgan aniqlik: {yaxshilanish:+.3f}")
    if np.mean(p == y) < 0.7:
        print("  ⭐ Qoidalar bilan ham aniqlik 0.7 dan past - "
              "o'rganiladigan model kerak")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Korpus ===
  sharhlar: 6000
    salbiy    2110  (35.2%)
    neytral   1229  (20.5%)
    ijobiy    2661  (44.4%)
  guruhlar: oddiy 2140, yumshoq 545, inkor 1190, kontrast 1031, aralash 363, neytral 731

=== 2. Har guruhdan namuna (normallashtirilgan) ===
  [oddiy   ] ijobiy  yetkazib berish chaqqon toshkentga yetkazishdi va dostavka ancha o'z vaqtida
  [yumshoq ] ijobiy  chidasa bo'ladi rangi qora
  [inkor   ] ijobiy  narxi baland emas va kechikmasdan keldi va onamga olib berdim o'lchami kichik
  [kontrast] ijobiy  onamga olib berdim operator e'tiborsiz biroq qadog'i ozoda
  [aralash ] neytral sifati juda chala va bahosi hamyonbop
  [neytral ] neytral qutisida yo'riqnoma bor toshkentga yetkazishdi

=== 3. Normallashtirish ===
  nostandart apostrofli sharhlar: 38.9%
  noyob tokenlar: xom 993 -> normallangan 880
  zo'r so'zining yozilishlari: zo'r (41), zo\u2019r (18), zo\u02bbr (9), zo`r (8)

=== 4. Lug'at bazaviysi: guruh bo'yicha aniqlik ===
  variant            hamma    oddiy  yumshoq    inkor kontrast  aralash  neytral
  faqat lug'at       0.483    0.699    0.444    0.013    0.258    0.482    0.967
  + inkor            0.589    0.699    0.532    0.452    0.320    0.482    0.967
  + inkor + lekin    0.628    0.699    0.532    0.452    0.546    0.482    0.967

=== 5. Qoidalar qayerda yiqiladi (eng to'liq variant) ===
  [inkor] narxi baland emas va kechikmasdan keldi va onamga olib berdim o'lchami kichik
      haqiqiy ijobiy, lug'at salbiy
  [kontrast] onamga olib berdim operator e'tiborsiz biroq qadog'i ozoda
      haqiqiy ijobiy, lug'at neytral
  [aralash] kuryer rosa imillagan va sotuvchi a'lo rangi ko'k va buyurtma seshanba kuni keldi
      haqiqiy neytral, lug'at ijobiy

=== 6. Lug'at qamrovi ===
  fikrli sharhlarda lug'at so'zi topilmadi: 33.1%
  qoidalar soni: ildiz 24, inkor 2, kontrast 1
  qoidalar qo'shgan aniqlik: +0.145
  ⭐ Qoidalar bilan ham aniqlik 0.7 dan past - o'rganiladigan model kerak

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

Misol 2 — TF-IDF + LogisticRegression: unigram va bigram

python
"""TF-IDF + LogisticRegression: unigram va bigram, guruh va sinf bo'yicha."""

import re

import numpy as np
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import classification_report
from sklearn.model_selection import train_test_split

SINFLAR = ["salbiy", "neytral", "ijobiy"]
GURUHLAR = ["oddiy", "yumshoq", "inkor", "kontrast", "aralash", "neytral"]
ULUSH = [0.36, 0.09, 0.20, 0.17, 0.06, 0.12]
APOSTROFLAR = ["'", "'", "'", "\u02bb", "\u2019", "`"]
ASPEKTLAR = [   # (jihat nomlari, ijobiy sifatlar, salbiy sifatlar)
    (["sifati", "materiali", "ishlanishi"],
     ["yaxshi", "zo'r", "a'lo", "ajoyib", "mustahkam", "puxta"],
     ["yomon", "past", "bo'sh", "nochor", "xom", "chala"]),
    (["narxi", "bahosi"], ["arzon", "hamyonbop", "qulay", "maqbul"],
     ["qimmat", "baland", "osmonda", "haddan tashqari"]),
    (["yetkazib berish", "dostavka", "kuryer"],
     ["tez", "o'z vaqtida", "chaqqon", "tezkor"],
     ["sekin", "kech", "imillagan", "sust"]),
    (["xizmat", "sotuvchi", "operator"],
     ["yaxshi", "xushmuomala", "a'lo", "e'tiborli"],
     ["yomon", "qo'pol", "e'tiborsiz", "beparvo"]),
    (["qadog'i", "o'rami"], ["chiroyli", "butun", "puxta", "ozoda"],
     ["yirtilgan", "ezilgan", "yomon", "iflos"]),
]
KUCHAYTIR = ["", "", "", "juda ", "ancha ", "rosa "]
FEL_IJOBIY = [("menga yoqdi", "menga yoqmadi"),       # (asl, -ma- shakli)
              ("tavsiya qilaman", "tavsiya qilmayman"),
              ("yana buyurtma beraman", "boshqa buyurtma bermayman"),
              ("mamnun qoldim", "mamnun qolmadim"),
              ("pulimga arzidi", "pulimga arzimadi"),
              ("kutganimdek chiqdi", "kutganimdek chiqmadi")]
FEL_SALBIY = [("ikki kunda buzildi", "bir oyda ham buzilmadi"),
              ("pulimga achindim", "pulimga achinmadim"),
              ("afsuslandim", "afsuslanmadim"),
              ("hafsalam pir bo'ldi", "hafsalam pir bo'lmadi"),
              ("kechikib keldi", "kechikmasdan keldi"),
              ("qaytarib berdim", "qaytarishga hojat bo'lmadi")]
YUMSHOQ = {1: ["yomon emas", "chidasa bo'ladi", "umuman olganda normal",
               "narxiga arziydi"],
           -1: ["biroz kechikdi", "kutganimdan sal pastroq", "unchalik emas",
                "ozgina hafsalam pir bo'ldi"]}
FAKTLAR = ["buyurtma {kun} kuni keldi", "rangi {rang}", "o'lchami {olcham}",
           "sovg'a uchun oldim", "qutisida yo'riqnoma bor",
           "narxi {son} ming so'm", "{shahar}ga yetkazishdi",
           "onamga olib berdim", "ikki dona oldim"]
NEYTRAL_FIKR = ["hali ishlatib ko'rmadim", "oddiy mahsulot", "sifati o'rtacha",
                "narxiga yarasha", "na yaxshi, na yomon", "hozircha fikr yo'q",
                "yaxshi ham emas, yomon ham emas", "boshqasidan farqi yo'q",
                "yaxshi yoki yomonligini keyin bilaman"]
TOLDIR = {"kun": ["dushanba", "seshanba", "juma", "shanba"],
          "rang": ["qora", "oq", "ko'k", "kulrang", "qizil"],
          "olcham": ["o'rtacha", "katta", "kichik", "standart"],
          "son": ["120", "85", "240", "56", "310"],
          "shahar": ["Toshkent", "Samarqand", "Buxoro", "Namangan", "Qarshi"]}
BOGLOVCHI = ["lekin", "ammo", "biroq"]


def tanla(rng, royxat):
    return royxat[int(rng.integers(len(royxat)))]


def fakt(rng):
    s = tanla(rng, FAKTLAR)
    for k, v in TOLDIR.items():
        s = s.replace("{" + k + "}", tanla(rng, v))
    return s


def birlik(rng, qutb, inkor=False, jihat=None):
    """Bitta fikr bo'lagi; qutb (+1/-1) - bo'lakning HOSIL bo'lgan ma'nosi.
    jihat: None - tasodifiy, son - shu jihat sifati, "fel" - fe'lli bo'lak."""
    if jihat is None:
        jihat = int(rng.integers(len(ASPEKTLAR))) if rng.random() < 0.55 else "fel"
    if jihat != "fel":                               # "narxi arzon"
        nomlar, ij, sl = ASPEKTLAR[jihat]
        nom = tanla(rng, nomlar)
        if not inkor:
            return f"{nom} {tanla(rng, KUCHAYTIR)}{tanla(rng, ij if qutb > 0 else sl)}"
        oldi = "unchalik " if qutb < 0 and rng.random() < 0.3 else ""
        return f"{nom} {oldi}{tanla(rng, sl if qutb > 0 else ij)} emas"
    ij, sl = tanla(rng, FEL_IJOBIY), tanla(rng, FEL_SALBIY)
    if not inkor:
        return (ij if qutb > 0 else sl)[0]
    return (sl if qutb > 0 else ij)[1]               # "-ma-": buzilmadi -> ijobiy


def ikki_jihat(rng, q1, q2, inkor2=False):
    """Ikki xil jihat haqida ikki bo'lak (masalan narx va sifat)."""
    i, j = rng.choice(len(ASPEKTLAR), size=2, replace=False)
    ikkinchi = int(j) if rng.random() < 0.6 else "fel"
    return birlik(rng, q1, jihat=int(i)), birlik(rng, q2, inkor2, ikkinchi)


def yozuvga(rng, bolaklar):
    matn = ""
    for i, b in enumerate(bolaklar):
        if i == 0:
            matn = b[0].upper() + b[1:]
        elif b.split()[0] in BOGLOVCHI:
            matn += ", " + b
        else:
            ayir = tanla(rng, [". ", ", ", ". ", " va "])
            matn += ayir + (b[0].upper() + b[1:] if ayir == ". " else b)
    return matn + tanla(rng, [".", ".", "!", "", "!!"])


def buz(rng, matn, p_imlo):
    """Apostrof variantlari va imlo xatolari (tushgan, ikkilangan, almashgan harf)."""
    sozlar = []
    for s in matn.split(" "):
        if "'" in s:
            s = s.replace("'", tanla(rng, APOSTROFLAR))
        if len(s) >= 5 and rng.random() < p_imlo:
            j, amal = int(rng.integers(2, len(s) - 1)), int(rng.integers(3))
            if amal == 0:
                s = s[:j] + s[j + 1:]
            elif amal == 1:
                s = s[:j] + s[j] + s[j:]
            else:
                s = s[:j - 1] + s[j] + s[j - 1] + s[j + 1:]
        sozlar.append(s)
    return " ".join(sozlar)


def sharh(rng):
    g = int(rng.choice(len(GURUHLAR), p=ULUSH))
    qutb = 1 if rng.random() < 0.55 else -1
    faktlar = [fakt(rng) for _ in range(int(rng.integers(0, 3)))]
    yorliq = 2 if qutb > 0 else 0
    if g == 0:                                       # oddiy
        bol = [birlik(rng, qutb) for _ in range(int(rng.integers(1, 3)))]
    elif g == 1:                                     # yumshoq: "yomon emas"
        bol = [tanla(rng, YUMSHOQ[qutb])]
        if rng.random() < 0.25:                      # annotatorlar kelisha olmaydi
            yorliq = 1
    elif g == 2:                                     # inkor: "emas", "-ma-"
        bol = [birlik(rng, qutb, inkor=True)
               for _ in range(int(rng.integers(1, 3)))]
    elif g == 3:                                     # kontrast: 2-qism hal qiladi
        a, b = ikki_jihat(rng, -qutb, qutb, inkor2=rng.random() < 0.25)
        bol, faktlar = [a, tanla(rng, BOGLOVCHI) + " " + b], faktlar[:1]
        return yozuvga(rng, faktlar + bol), yorliq, g
    elif g == 4:                                     # aralash: +/- teng -> neytral
        a, b = ikki_jihat(rng, qutb, -qutb)
        bol, yorliq = [a + tanla(rng, [", ", " va "]) + b], 1
    else:                                            # neytral
        bol = [tanla(rng, NEYTRAL_FIKR)] if rng.random() < 0.7 else []
        faktlar = faktlar or [fakt(rng)]
        yorliq = 1
    bol += faktlar
    rng.shuffle(bol)
    return yozuvga(rng, bol), yorliq, g


def korpus(n, seed, p_imlo=0.04, p_shovqin=0.03):
    """Sintetik o'zbekcha sharhlar: matnlar, yorliqlar, guruhlar."""
    rng = np.random.default_rng(seed)
    matnlar, y, guruh = [], [], []
    for _ in range(n):
        m, yl, g = sharh(rng)
        if rng.random() < p_shovqin:                # tasodifiy yorliq xatosi
            yl = int(rng.integers(3))
        matnlar.append(buz(rng, m, p_imlo))
        y.append(yl)
        guruh.append(GURUHLAR[g])
    return np.array(matnlar, dtype=object), np.array(y), np.array(guruh)


def normallashtir(s):
    s = s.lower()
    for v in ("\u02bb", "\u2019", "\u2018", "\u02bc", "`"):
        s = s.replace(v, "'")
    s = re.sub(r"[^a-z0-9' ]+", " ", s)
    return " ".join(s.split())


def bolish(y, seed=0):
    """60/20/20, sinf bo'yicha stratifikatsiya; test shu yerda qulflanadi."""
    idx = np.arange(len(y))
    ish, te = train_test_split(idx, test_size=0.2, stratify=y, random_state=seed)
    tr, va = train_test_split(ish, test_size=0.25, stratify=y[ish],
                              random_state=seed)
    return tr, va, te


def jadval_sarlavha():
    print(f"  {'model':<16}{'hamma':>8}" + "".join(f"{g:>9}" for g in GURUHLAR))


def jadval_qator(nom, y, p, g):
    """Umumiy aniqlik va har bir guruh bo'yicha aniqlik."""
    qiymat = [np.mean(p == y)] + [np.mean(p[g == gr] == y[g == gr])
                                  for gr in GURUHLAR]
    print(f"  {nom:<16}" + "".join(f"{v:>8.3f} " for v in qiymat).rstrip())
    return qiymat


def tfidf_model(matnlar, y, ngram):
    v = TfidfVectorizer(preprocessor=normallashtir, ngram_range=ngram,
                        sublinear_tf=True, min_df=2)
    lr = LogisticRegression(C=5.0, max_iter=3000)
    lr.fit(v.fit_transform(matnlar), y)          # vektorizator FAQAT o'quvda
    return v, lr


def main() -> None:
    matnlar, y, g = korpus(6000, seed=42)
    tr, va, te = bolish(y)
    print("=== 1. Bo'lish (60/20/20, stratifikatsiya) ===")
    for nom, ii in [("o'quv", tr), ("validatsiya", va), ("test", te)]:
        print(f"  {nom:<12} {len(ii):>5}  neytral ulushi {np.mean(y[ii] == 1):.3f}")

    print("\n=== 2. Unigram va bigram: guruh bo'yicha aniqlik (test) ===")
    jadval_sarlavha()
    modellar, qiymat = {}, {}
    for nom, ng in [("TF-IDF (1,1)", (1, 1)), ("TF-IDF (1,2)", (1, 2))]:
        v, lr = tfidf_model(matnlar[tr], y[tr], ng)
        modellar[nom] = (v, lr)
        p = lr.predict(v.transform(matnlar[te]))
        qiymat[nom] = jadval_qator(nom, y[te], p, g[te])
    farq = np.array(qiymat["TF-IDF (1,2)"]) - np.array(qiymat["TF-IDF (1,1)"])
    eng = int(np.argmax(farq[1:]))
    print(f"  bigramning eng katta foydasi: {GURUHLAR[eng]} ({farq[1 + eng]:+.3f})")

    print("\n=== 3. Sinf bo'yicha (TF-IDF (1,2), test) ===")
    v, lr = modellar["TF-IDF (1,2)"]
    p = lr.predict(v.transform(matnlar[te]))
    hisobot = classification_report(y[te], p, target_names=SINFLAR,
                                    output_dict=True)
    print(f"  {'sinf':<8} {'precision':>9} {'recall':>7} {'F1':>6} {'n':>5}")
    for s in SINFLAR:
        h = hisobot[s]
        print(f"  {s:<8} {h['precision']:>9.3f} {h['recall']:>7.3f} "
              f"{h['f1-score']:>6.3f} {int(h['support']):>5}")
    print(f"  makro-F1 {hisobot['macro avg']['f1-score']:.3f}, "
          f"aniqlik {hisobot['accuracy']:.3f}")
    eng_past = min(SINFLAR, key=lambda s: hisobot[s]["f1-score"])
    print(f"  eng qiyin sinf: {eng_past}")

    print("\n=== 4. Inkor bigramlari: model nimani o'rgandi ===")
    nomlar = v.get_feature_names_out()
    w = lr.coef_[2] - lr.coef_[0]                # ijobiy - salbiy yo'nalishi
    for soz in ["yaxshi", "yomon", "qimmat", "arzon", "yoqdi", "yoqmadi"]:
        juft = [soz, soz + " emas"] if soz not in ("yoqdi", "yoqmadi") else [soz]
        qism = []
        for f in juft:
            j = np.flatnonzero(nomlar == f)
            if len(j):
                qism.append(f"{f!r}: {w[j[0]]:+.2f}")
        print("  " + ",  ".join(qism))

    print("\n=== 5. Tartib muammosi: bir xil so'zlar, teskari ma'no ===")
    juftlar = ["narxi arzon lekin sifati yomon", "sifati yomon lekin narxi arzon",
               "materiali puxta ammo dostavka sekin",
               "dostavka sekin ammo materiali puxta"]
    print(f"  {'matn':<37} {'(1,1) P(ijobiy)':>15} {'(1,2) P(ijobiy)':>15}")
    for m in juftlar:
        pr = [modellar[k][1].predict_proba(modellar[k][0].transform([m]))[0, 2]
              for k in ["TF-IDF (1,1)", "TF-IDF (1,2)"]]
        print(f"  {m:<37} {pr[0]:>15.3f} {pr[1]:>15.3f}")
    print("  (1,1) uchun juftlik ichidagi ikki matn - bitta so'z xaltasi")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Bo'lish (60/20/20, stratifikatsiya) ===
  o'quv         3600  neytral ulushi 0.205
  validatsiya   1200  neytral ulushi 0.205
  test          1200  neytral ulushi 0.205

=== 2. Unigram va bigram: guruh bo'yicha aniqlik (test) ===
  model              hamma    oddiy  yumshoq    inkor kontrast  aralash  neytral
  TF-IDF (1,1)       0.769    0.814    0.704    0.827    0.723    0.158    0.979
  TF-IDF (1,2)       0.845    0.888    0.694    0.934    0.866    0.316    0.937
  bigramning eng katta foydasi: aralash (+0.158)

=== 3. Sinf bo'yicha (TF-IDF (1,2), test) ===
  sinf     precision  recall     F1     n
  salbiy       0.839   0.886  0.862   422
  neytral      0.800   0.650  0.717   246
  ijobiy       0.866   0.902  0.884   532
  makro-F1 0.821, aniqlik 0.845
  eng qiyin sinf: neytral

=== 4. Inkor bigramlari: model nimani o'rgandi ===
  'yaxshi': +2.68,  'yaxshi emas': -8.07
  'yomon': -4.66,  'yomon emas': +8.25
  'qimmat': -2.93,  'qimmat emas': +5.95
  'arzon': +3.36,  'arzon emas': -5.29
  'yoqdi': +5.56
  'yoqmadi': -4.31

=== 5. Tartib muammosi: bir xil so'zlar, teskari ma'no ===
  matn                                  (1,1) P(ijobiy) (1,2) P(ijobiy)
  narxi arzon lekin sifati yomon                  0.658           0.253
  sifati yomon lekin narxi arzon                  0.658           0.939
  materiali puxta ammo dostavka sekin             0.815           0.674
  dostavka sekin ammo materiali puxta             0.815           0.923
  (1,1) uchun juftlik ichidagi ikki matn - bitta so'z xaltasi

Nima ko'rsatdi: 2.3-bo'lim.

Misol 3 — EmbeddingBag, LSTM va qiyin guruhlar jadvali

python
"""EmbeddingBag va LSTM: qiyin guruhlar jadvali va juftlashgan taqqoslash."""

import copy
import re
from collections import Counter

import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from torch.nn.utils.rnn import pack_padded_sequence

SINFLAR = ["salbiy", "neytral", "ijobiy"]
GURUHLAR = ["oddiy", "yumshoq", "inkor", "kontrast", "aralash", "neytral"]
ULUSH = [0.36, 0.09, 0.20, 0.17, 0.06, 0.12]
APOSTROFLAR = ["'", "'", "'", "\u02bb", "\u2019", "`"]
ASPEKTLAR = [   # (jihat nomlari, ijobiy sifatlar, salbiy sifatlar)
    (["sifati", "materiali", "ishlanishi"],
     ["yaxshi", "zo'r", "a'lo", "ajoyib", "mustahkam", "puxta"],
     ["yomon", "past", "bo'sh", "nochor", "xom", "chala"]),
    (["narxi", "bahosi"], ["arzon", "hamyonbop", "qulay", "maqbul"],
     ["qimmat", "baland", "osmonda", "haddan tashqari"]),
    (["yetkazib berish", "dostavka", "kuryer"],
     ["tez", "o'z vaqtida", "chaqqon", "tezkor"],
     ["sekin", "kech", "imillagan", "sust"]),
    (["xizmat", "sotuvchi", "operator"],
     ["yaxshi", "xushmuomala", "a'lo", "e'tiborli"],
     ["yomon", "qo'pol", "e'tiborsiz", "beparvo"]),
    (["qadog'i", "o'rami"], ["chiroyli", "butun", "puxta", "ozoda"],
     ["yirtilgan", "ezilgan", "yomon", "iflos"]),
]
KUCHAYTIR = ["", "", "", "juda ", "ancha ", "rosa "]
FEL_IJOBIY = [("menga yoqdi", "menga yoqmadi"),       # (asl, -ma- shakli)
              ("tavsiya qilaman", "tavsiya qilmayman"),
              ("yana buyurtma beraman", "boshqa buyurtma bermayman"),
              ("mamnun qoldim", "mamnun qolmadim"),
              ("pulimga arzidi", "pulimga arzimadi"),
              ("kutganimdek chiqdi", "kutganimdek chiqmadi")]
FEL_SALBIY = [("ikki kunda buzildi", "bir oyda ham buzilmadi"),
              ("pulimga achindim", "pulimga achinmadim"),
              ("afsuslandim", "afsuslanmadim"),
              ("hafsalam pir bo'ldi", "hafsalam pir bo'lmadi"),
              ("kechikib keldi", "kechikmasdan keldi"),
              ("qaytarib berdim", "qaytarishga hojat bo'lmadi")]
YUMSHOQ = {1: ["yomon emas", "chidasa bo'ladi", "umuman olganda normal",
               "narxiga arziydi"],
           -1: ["biroz kechikdi", "kutganimdan sal pastroq", "unchalik emas",
                "ozgina hafsalam pir bo'ldi"]}
FAKTLAR = ["buyurtma {kun} kuni keldi", "rangi {rang}", "o'lchami {olcham}",
           "sovg'a uchun oldim", "qutisida yo'riqnoma bor",
           "narxi {son} ming so'm", "{shahar}ga yetkazishdi",
           "onamga olib berdim", "ikki dona oldim"]
NEYTRAL_FIKR = ["hali ishlatib ko'rmadim", "oddiy mahsulot", "sifati o'rtacha",
                "narxiga yarasha", "na yaxshi, na yomon", "hozircha fikr yo'q",
                "yaxshi ham emas, yomon ham emas", "boshqasidan farqi yo'q",
                "yaxshi yoki yomonligini keyin bilaman"]
TOLDIR = {"kun": ["dushanba", "seshanba", "juma", "shanba"],
          "rang": ["qora", "oq", "ko'k", "kulrang", "qizil"],
          "olcham": ["o'rtacha", "katta", "kichik", "standart"],
          "son": ["120", "85", "240", "56", "310"],
          "shahar": ["Toshkent", "Samarqand", "Buxoro", "Namangan", "Qarshi"]}
BOGLOVCHI = ["lekin", "ammo", "biroq"]


def tanla(rng, royxat):
    return royxat[int(rng.integers(len(royxat)))]


def fakt(rng):
    s = tanla(rng, FAKTLAR)
    for k, v in TOLDIR.items():
        s = s.replace("{" + k + "}", tanla(rng, v))
    return s


def birlik(rng, qutb, inkor=False, jihat=None):
    """Bitta fikr bo'lagi; qutb (+1/-1) - bo'lakning HOSIL bo'lgan ma'nosi.
    jihat: None - tasodifiy, son - shu jihat sifati, "fel" - fe'lli bo'lak."""
    if jihat is None:
        jihat = int(rng.integers(len(ASPEKTLAR))) if rng.random() < 0.55 else "fel"
    if jihat != "fel":                               # "narxi arzon"
        nomlar, ij, sl = ASPEKTLAR[jihat]
        nom = tanla(rng, nomlar)
        if not inkor:
            return f"{nom} {tanla(rng, KUCHAYTIR)}{tanla(rng, ij if qutb > 0 else sl)}"
        oldi = "unchalik " if qutb < 0 and rng.random() < 0.3 else ""
        return f"{nom} {oldi}{tanla(rng, sl if qutb > 0 else ij)} emas"
    ij, sl = tanla(rng, FEL_IJOBIY), tanla(rng, FEL_SALBIY)
    if not inkor:
        return (ij if qutb > 0 else sl)[0]
    return (sl if qutb > 0 else ij)[1]               # "-ma-": buzilmadi -> ijobiy


def ikki_jihat(rng, q1, q2, inkor2=False):
    """Ikki xil jihat haqida ikki bo'lak (masalan narx va sifat)."""
    i, j = rng.choice(len(ASPEKTLAR), size=2, replace=False)
    ikkinchi = int(j) if rng.random() < 0.6 else "fel"
    return birlik(rng, q1, jihat=int(i)), birlik(rng, q2, inkor2, ikkinchi)


def yozuvga(rng, bolaklar):
    matn = ""
    for i, b in enumerate(bolaklar):
        if i == 0:
            matn = b[0].upper() + b[1:]
        elif b.split()[0] in BOGLOVCHI:
            matn += ", " + b
        else:
            ayir = tanla(rng, [". ", ", ", ". ", " va "])
            matn += ayir + (b[0].upper() + b[1:] if ayir == ". " else b)
    return matn + tanla(rng, [".", ".", "!", "", "!!"])


def buz(rng, matn, p_imlo):
    """Apostrof variantlari va imlo xatolari (tushgan, ikkilangan, almashgan harf)."""
    sozlar = []
    for s in matn.split(" "):
        if "'" in s:
            s = s.replace("'", tanla(rng, APOSTROFLAR))
        if len(s) >= 5 and rng.random() < p_imlo:
            j, amal = int(rng.integers(2, len(s) - 1)), int(rng.integers(3))
            if amal == 0:
                s = s[:j] + s[j + 1:]
            elif amal == 1:
                s = s[:j] + s[j] + s[j:]
            else:
                s = s[:j - 1] + s[j] + s[j - 1] + s[j + 1:]
        sozlar.append(s)
    return " ".join(sozlar)


def sharh(rng):
    g = int(rng.choice(len(GURUHLAR), p=ULUSH))
    qutb = 1 if rng.random() < 0.55 else -1
    faktlar = [fakt(rng) for _ in range(int(rng.integers(0, 3)))]
    yorliq = 2 if qutb > 0 else 0
    if g == 0:                                       # oddiy
        bol = [birlik(rng, qutb) for _ in range(int(rng.integers(1, 3)))]
    elif g == 1:                                     # yumshoq: "yomon emas"
        bol = [tanla(rng, YUMSHOQ[qutb])]
        if rng.random() < 0.25:                      # annotatorlar kelisha olmaydi
            yorliq = 1
    elif g == 2:                                     # inkor: "emas", "-ma-"
        bol = [birlik(rng, qutb, inkor=True)
               for _ in range(int(rng.integers(1, 3)))]
    elif g == 3:                                     # kontrast: 2-qism hal qiladi
        a, b = ikki_jihat(rng, -qutb, qutb, inkor2=rng.random() < 0.25)
        bol, faktlar = [a, tanla(rng, BOGLOVCHI) + " " + b], faktlar[:1]
        return yozuvga(rng, faktlar + bol), yorliq, g
    elif g == 4:                                     # aralash: +/- teng -> neytral
        a, b = ikki_jihat(rng, qutb, -qutb)
        bol, yorliq = [a + tanla(rng, [", ", " va "]) + b], 1
    else:                                            # neytral
        bol = [tanla(rng, NEYTRAL_FIKR)] if rng.random() < 0.7 else []
        faktlar = faktlar or [fakt(rng)]
        yorliq = 1
    bol += faktlar
    rng.shuffle(bol)
    return yozuvga(rng, bol), yorliq, g


def korpus(n, seed, p_imlo=0.04, p_shovqin=0.03):
    """Sintetik o'zbekcha sharhlar: matnlar, yorliqlar, guruhlar."""
    rng = np.random.default_rng(seed)
    matnlar, y, guruh = [], [], []
    for _ in range(n):
        m, yl, g = sharh(rng)
        if rng.random() < p_shovqin:                # tasodifiy yorliq xatosi
            yl = int(rng.integers(3))
        matnlar.append(buz(rng, m, p_imlo))
        y.append(yl)
        guruh.append(GURUHLAR[g])
    return np.array(matnlar, dtype=object), np.array(y), np.array(guruh)


def normallashtir(s):
    s = s.lower()
    for v in ("\u02bb", "\u2019", "\u2018", "\u02bc", "`"):
        s = s.replace(v, "'")
    s = re.sub(r"[^a-z0-9' ]+", " ", s)
    return " ".join(s.split())


def bolish(y, seed=0):
    """60/20/20, sinf bo'yicha stratifikatsiya; test shu yerda qulflanadi."""
    idx = np.arange(len(y))
    ish, te = train_test_split(idx, test_size=0.2, stratify=y, random_state=seed)
    tr, va = train_test_split(ish, test_size=0.25, stratify=y[ish],
                              random_state=seed)
    return tr, va, te


def jadval_sarlavha():
    print(f"  {'model':<16}{'hamma':>8}" + "".join(f"{g:>9}" for g in GURUHLAR))


def jadval_qator(nom, y, p, g):
    """Umumiy aniqlik va har bir guruh bo'yicha aniqlik."""
    qiymat = [np.mean(p == y)] + [np.mean(p[g == gr] == y[g == gr])
                                  for gr in GURUHLAR]
    print(f"  {nom:<16}" + "".join(f"{v:>8.3f} " for v in qiymat).rstrip())
    return qiymat


IJOBIY_ILDIZ = ["yaxshi", "zo'r", "a'lo", "ajoyib", "arzon", "qulay", "tez",
                "chiroyli", "yoq", "tavsiya", "mamnun", "arzi"]
SALBIY_ILDIZ = ["yomon", "past", "qimmat", "sekin", "kech", "qo'pol", "buzil",
                "achin", "afsuslan", "hafsala", "qaytar", "iflos"]
ILDIZLAR = [(s, 1) for s in IJOBIY_ILDIZ] + [(s, -1) for s in SALBIY_ILDIZ]


def lugat_bashorat(matn, inkor=False, kontrast=False):
    """Lug'at bazaviysi: ildiz mos kelsa +1/-1; ball ishorasi -> sinf."""
    t = normallashtir(matn).split()
    if kontrast:                        # oxirgi "lekin" dan keyingi qism hal qiladi
        for j in range(len(t) - 1, -1, -1):
            if t[j] in BOGLOVCHI:
                t = t[j + 1:]
                break
    ball = 0
    for i, soz in enumerate(t):
        for ildiz, q in ILDIZLAR:
            if soz.startswith(ildiz):
                if inkor and (soz[len(ildiz):].startswith("ma")
                              or t[i + 1:i + 2] == ["emas"]):
                    q = -q              # "yoqmadi", "yaxshi emas"
                ball += q
                break
    return 2 if ball > 0 else 0 if ball < 0 else 1


class Lugat:
    """Token -> indeks; FAQAT o'quv matnlaridan quriladi (0 - pad, 1 - unk)."""

    def __init__(self, matnlar, min_son=2):
        c = Counter(s for m in matnlar for s in normallashtir(m).split())
        self.itos = ["<pad>", "<unk>"] + sorted(s for s, n in c.items()
                                                if n >= min_son)
        self.stoi = {s: i for i, s in enumerate(self.itos)}

    def __len__(self):
        return len(self.itos)

    def kodla(self, matnlar):
        seqs = [[self.stoi.get(s, 1) for s in normallashtir(m).split()] or [1]
                for m in matnlar]
        L = torch.tensor([len(s) for s in seqs])
        x = torch.zeros(len(seqs), int(L.max()), dtype=torch.long)
        for i, s in enumerate(seqs):
            x[i, :len(s)] = torch.tensor(s)
        return x, L


class BagModel(nn.Module):
    """Neyron so'z xaltasi: o'rtacha embedding -> Linear (tartib yo'q)."""

    def __init__(self, V, d=32):
        super().__init__()
        self.emb = nn.EmbeddingBag(V, d, mode="mean", padding_idx=0)
        self.bosh = nn.Linear(d, 3)

    def forward(self, x, L):
        return self.bosh(self.emb(x))


class LSTMModel(nn.Module):
    """LSTM chapdan o'ngga o'qiydi; pad lar pack_padded_sequence bilan chetlanadi."""

    def __init__(self, V, d=32, h=48):
        super().__init__()
        self.emb = nn.Embedding(V, d, padding_idx=0)
        self.lstm = nn.LSTM(d, h, batch_first=True)
        self.bosh = nn.Linear(h, 3)

    def forward(self, x, L):
        pk = pack_padded_sequence(self.emb(x), L, batch_first=True,
                                  enforce_sorted=False)
        _, (h, _) = self.lstm(pk)
        return self.bosh(h[-1])                   # oxirgi HAQIQIY token holati


def orgat(model, tr, va, davrlar=10, seed=0):
    """tr, va = (x, L, y). Eng yaxshi val aniqlikdagi holat deepcopy bilan."""
    torch.manual_seed(seed)
    opt = torch.optim.AdamW(model.parameters(), lr=5e-3, weight_decay=1e-4)
    g = torch.Generator().manual_seed(seed)
    eng, eng_davr, holat = -1.0, 0, None
    for davr in range(1, davrlar + 1):
        model.train()
        tartib = torch.randperm(len(tr[2]), generator=g)
        for i in range(0, len(tartib), 128):
            j = tartib[i:i + 128]
            opt.zero_grad()
            F.cross_entropy(model(tr[0][j], tr[1][j]), tr[2][j]).backward()
            opt.step()
        aniqlik = float((logitlar(model, va[0], va[1]).argmax(1)
                         == va[2]).float().mean())
        if aniqlik > eng:
            eng, eng_davr = aniqlik, davr
            holat = copy.deepcopy(model.state_dict())
    model.load_state_dict(holat)
    return eng, eng_davr


def logitlar(model, x, L):
    model.eval()
    with torch.no_grad():
        return model(x, L)


def main() -> None:
    matnlar, y, g = korpus(6000, seed=42)
    tr, va, te = bolish(y)
    lug = Lugat(matnlar[tr])
    D = {k: (*lug.kodla(matnlar[ii]), torch.tensor(y[ii]))
         for k, ii in [("tr", tr), ("va", va), ("te", te)]}
    print("=== 1. Lug'at (faqat o'quvdan) ===")
    unk = float((D["te"][0] == 1).sum() / D["te"][1].sum())
    print(f"  hajm {len(lug)}, testdagi <unk> ulushi {unk:.3f}, "
          f"eng uzun sharh {int(D['tr'][1].max())} token")

    print("\n=== 2. O'rgatish (eng yaxshi holat - deepcopy) ===")
    neyron = {}
    for nom, sinf in [("EmbeddingBag", BagModel), ("LSTM", LSTMModel)]:
        torch.manual_seed(0)
        model = sinf(len(lug))
        eng, davr = orgat(model, D["tr"], D["va"])
        neyron[nom] = model
        print(f"  {nom:<13} val aniqlik {eng:.3f} ({davr}-davr), "
              f"parametrlar {sum(p.numel() for p in model.parameters())}")

    print("\n=== 3. Qiyin guruhlar: aniqlik jadvali (test) ===")
    bash = {"lug'at+qoida": np.array([lugat_bashorat(m, True, True)
                                      for m in matnlar[te]])}
    v = TfidfVectorizer(preprocessor=normallashtir, ngram_range=(1, 2),
                        sublinear_tf=True, min_df=2)
    lr = LogisticRegression(C=5.0, max_iter=3000)
    lr.fit(v.fit_transform(matnlar[tr]), y[tr])
    bash["TF-IDF (1,2)"] = lr.predict(v.transform(matnlar[te]))
    for nom, model in neyron.items():
        bash[nom] = logitlar(model, *D["te"][:2]).argmax(1).numpy()
    jadval_sarlavha()
    for nom, p in bash.items():
        jadval_qator(nom, y[te], p, g[te])

    print("\n=== 4. Juftlashgan taqqoslash: LSTM - TF-IDF (1,2) ===")
    d = ((bash["LSTM"] == y[te]).astype(float)
         - (bash["TF-IDF (1,2)"] == y[te]).astype(float))
    print(f"  {'guruh':<9} {'n':>4} {'farq':>7} {'2*SE':>6}  xulosa")
    for gr in ["hamma"] + GURUHLAR:
        m = np.ones(len(d), bool) if gr == "hamma" else g[te] == gr
        o, se = d[m].mean(), d[m].std(ddof=1) / np.sqrt(m.sum())
        xulosa = ("LSTM yaxshi" if o > 2 * se else
                  "TF-IDF yaxshi" if o < -2 * se else "farq sezilmas")
        print(f"  {gr:<9} {int(m.sum()):>4} {o:>+7.3f} {2 * se:>6.3f}  {xulosa}")

    print("\n=== 5. LSTM seedlarga qanchalik sezgir ===")
    natija = []
    for seed in (0, 1, 2):
        if seed == 0:
            model = neyron["LSTM"]                   # 2-bo'limda o'rgatilgan
        else:
            torch.manual_seed(seed)
            model = LSTMModel(len(lug))
            orgat(model, D["tr"], D["va"], seed=seed)
        p = logitlar(model, *D["te"][:2]).argmax(1).numpy()
        natija.append([np.mean(p == y[te]),
                       np.mean(p[g[te] == "kontrast"] == y[te][g[te] == "kontrast"])])
    natija = np.array(natija)
    for nom, ustun in [("hamma", 0), ("kontrast", 1)]:
        print(f"  {nom:<9} seedlar {natija[:, ustun].round(3).tolist()}  "
              f"o'rtacha {natija[:, ustun].mean():.3f} "
              f"+- {natija[:, ustun].std(ddof=1):.3f}")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Lug'at (faqat o'quvdan) ===
  hajm 330, testdagi <unk> ulushi 0.021, eng uzun sharh 16 token

=== 2. O'rgatish (eng yaxshi holat - deepcopy) ===
  EmbeddingBag  val aniqlik 0.752 (10-davr), parametrlar 10659
  LSTM          val aniqlik 0.923 (10-davr), parametrlar 26451

=== 3. Qiyin guruhlar: aniqlik jadvali (test) ===
  model              hamma    oddiy  yumshoq    inkor kontrast  aralash  neytral
  lug'at+qoida       0.632    0.686    0.574    0.438    0.594    0.421    0.979
  TF-IDF (1,2)       0.845    0.888    0.694    0.934    0.866    0.316    0.937
  EmbeddingBag       0.750    0.814    0.611    0.810    0.708    0.092    0.972
  LSTM               0.906    0.928    0.694    0.942    0.946    0.737    0.972

=== 4. Juftlashgan taqqoslash: LSTM - TF-IDF (1,2) ===
  guruh        n    farq   2*SE  xulosa
  hamma     1200  +0.061  0.019  LSTM yaxshi
  oddiy      446  +0.040  0.026  LSTM yaxshi
  yumshoq    108  +0.000  0.079  farq sezilmas
  inkor      226  +0.009  0.025  farq sezilmas
  kontrast   202  +0.079  0.049  LSTM yaxshi
  aralash     76  +0.421  0.136  LSTM yaxshi
  neytral    142  +0.035  0.042  farq sezilmas

=== 5. LSTM seedlarga qanchalik sezgir ===
  hamma     seedlar [0.906, 0.881, 0.908]  o'rtacha 0.898 +- 0.015
  kontrast  seedlar [0.946, 0.96, 0.97]  o'rtacha 0.959 +- 0.012

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

Misol 4 — Neytral sinf va kalibratsiya

python
"""Neytral sinfning qiyinligi va ishonch kalibratsiyasi (ECE, harorat)."""

import copy
import re
from collections import Counter

import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import confusion_matrix
from sklearn.model_selection import train_test_split
from torch.nn.utils.rnn import pack_padded_sequence

SINFLAR = ["salbiy", "neytral", "ijobiy"]
GURUHLAR = ["oddiy", "yumshoq", "inkor", "kontrast", "aralash", "neytral"]
ULUSH = [0.36, 0.09, 0.20, 0.17, 0.06, 0.12]
APOSTROFLAR = ["'", "'", "'", "\u02bb", "\u2019", "`"]
ASPEKTLAR = [   # (jihat nomlari, ijobiy sifatlar, salbiy sifatlar)
    (["sifati", "materiali", "ishlanishi"],
     ["yaxshi", "zo'r", "a'lo", "ajoyib", "mustahkam", "puxta"],
     ["yomon", "past", "bo'sh", "nochor", "xom", "chala"]),
    (["narxi", "bahosi"], ["arzon", "hamyonbop", "qulay", "maqbul"],
     ["qimmat", "baland", "osmonda", "haddan tashqari"]),
    (["yetkazib berish", "dostavka", "kuryer"],
     ["tez", "o'z vaqtida", "chaqqon", "tezkor"],
     ["sekin", "kech", "imillagan", "sust"]),
    (["xizmat", "sotuvchi", "operator"],
     ["yaxshi", "xushmuomala", "a'lo", "e'tiborli"],
     ["yomon", "qo'pol", "e'tiborsiz", "beparvo"]),
    (["qadog'i", "o'rami"], ["chiroyli", "butun", "puxta", "ozoda"],
     ["yirtilgan", "ezilgan", "yomon", "iflos"]),
]
KUCHAYTIR = ["", "", "", "juda ", "ancha ", "rosa "]
FEL_IJOBIY = [("menga yoqdi", "menga yoqmadi"),       # (asl, -ma- shakli)
              ("tavsiya qilaman", "tavsiya qilmayman"),
              ("yana buyurtma beraman", "boshqa buyurtma bermayman"),
              ("mamnun qoldim", "mamnun qolmadim"),
              ("pulimga arzidi", "pulimga arzimadi"),
              ("kutganimdek chiqdi", "kutganimdek chiqmadi")]
FEL_SALBIY = [("ikki kunda buzildi", "bir oyda ham buzilmadi"),
              ("pulimga achindim", "pulimga achinmadim"),
              ("afsuslandim", "afsuslanmadim"),
              ("hafsalam pir bo'ldi", "hafsalam pir bo'lmadi"),
              ("kechikib keldi", "kechikmasdan keldi"),
              ("qaytarib berdim", "qaytarishga hojat bo'lmadi")]
YUMSHOQ = {1: ["yomon emas", "chidasa bo'ladi", "umuman olganda normal",
               "narxiga arziydi"],
           -1: ["biroz kechikdi", "kutganimdan sal pastroq", "unchalik emas",
                "ozgina hafsalam pir bo'ldi"]}
FAKTLAR = ["buyurtma {kun} kuni keldi", "rangi {rang}", "o'lchami {olcham}",
           "sovg'a uchun oldim", "qutisida yo'riqnoma bor",
           "narxi {son} ming so'm", "{shahar}ga yetkazishdi",
           "onamga olib berdim", "ikki dona oldim"]
NEYTRAL_FIKR = ["hali ishlatib ko'rmadim", "oddiy mahsulot", "sifati o'rtacha",
                "narxiga yarasha", "na yaxshi, na yomon", "hozircha fikr yo'q",
                "yaxshi ham emas, yomon ham emas", "boshqasidan farqi yo'q",
                "yaxshi yoki yomonligini keyin bilaman"]
TOLDIR = {"kun": ["dushanba", "seshanba", "juma", "shanba"],
          "rang": ["qora", "oq", "ko'k", "kulrang", "qizil"],
          "olcham": ["o'rtacha", "katta", "kichik", "standart"],
          "son": ["120", "85", "240", "56", "310"],
          "shahar": ["Toshkent", "Samarqand", "Buxoro", "Namangan", "Qarshi"]}
BOGLOVCHI = ["lekin", "ammo", "biroq"]


def tanla(rng, royxat):
    return royxat[int(rng.integers(len(royxat)))]


def fakt(rng):
    s = tanla(rng, FAKTLAR)
    for k, v in TOLDIR.items():
        s = s.replace("{" + k + "}", tanla(rng, v))
    return s


def birlik(rng, qutb, inkor=False, jihat=None):
    """Bitta fikr bo'lagi; qutb (+1/-1) - bo'lakning HOSIL bo'lgan ma'nosi.
    jihat: None - tasodifiy, son - shu jihat sifati, "fel" - fe'lli bo'lak."""
    if jihat is None:
        jihat = int(rng.integers(len(ASPEKTLAR))) if rng.random() < 0.55 else "fel"
    if jihat != "fel":                               # "narxi arzon"
        nomlar, ij, sl = ASPEKTLAR[jihat]
        nom = tanla(rng, nomlar)
        if not inkor:
            return f"{nom} {tanla(rng, KUCHAYTIR)}{tanla(rng, ij if qutb > 0 else sl)}"
        oldi = "unchalik " if qutb < 0 and rng.random() < 0.3 else ""
        return f"{nom} {oldi}{tanla(rng, sl if qutb > 0 else ij)} emas"
    ij, sl = tanla(rng, FEL_IJOBIY), tanla(rng, FEL_SALBIY)
    if not inkor:
        return (ij if qutb > 0 else sl)[0]
    return (sl if qutb > 0 else ij)[1]               # "-ma-": buzilmadi -> ijobiy


def ikki_jihat(rng, q1, q2, inkor2=False):
    """Ikki xil jihat haqida ikki bo'lak (masalan narx va sifat)."""
    i, j = rng.choice(len(ASPEKTLAR), size=2, replace=False)
    ikkinchi = int(j) if rng.random() < 0.6 else "fel"
    return birlik(rng, q1, jihat=int(i)), birlik(rng, q2, inkor2, ikkinchi)


def yozuvga(rng, bolaklar):
    matn = ""
    for i, b in enumerate(bolaklar):
        if i == 0:
            matn = b[0].upper() + b[1:]
        elif b.split()[0] in BOGLOVCHI:
            matn += ", " + b
        else:
            ayir = tanla(rng, [". ", ", ", ". ", " va "])
            matn += ayir + (b[0].upper() + b[1:] if ayir == ". " else b)
    return matn + tanla(rng, [".", ".", "!", "", "!!"])


def buz(rng, matn, p_imlo):
    """Apostrof variantlari va imlo xatolari (tushgan, ikkilangan, almashgan harf)."""
    sozlar = []
    for s in matn.split(" "):
        if "'" in s:
            s = s.replace("'", tanla(rng, APOSTROFLAR))
        if len(s) >= 5 and rng.random() < p_imlo:
            j, amal = int(rng.integers(2, len(s) - 1)), int(rng.integers(3))
            if amal == 0:
                s = s[:j] + s[j + 1:]
            elif amal == 1:
                s = s[:j] + s[j] + s[j:]
            else:
                s = s[:j - 1] + s[j] + s[j - 1] + s[j + 1:]
        sozlar.append(s)
    return " ".join(sozlar)


def sharh(rng):
    g = int(rng.choice(len(GURUHLAR), p=ULUSH))
    qutb = 1 if rng.random() < 0.55 else -1
    faktlar = [fakt(rng) for _ in range(int(rng.integers(0, 3)))]
    yorliq = 2 if qutb > 0 else 0
    if g == 0:                                       # oddiy
        bol = [birlik(rng, qutb) for _ in range(int(rng.integers(1, 3)))]
    elif g == 1:                                     # yumshoq: "yomon emas"
        bol = [tanla(rng, YUMSHOQ[qutb])]
        if rng.random() < 0.25:                      # annotatorlar kelisha olmaydi
            yorliq = 1
    elif g == 2:                                     # inkor: "emas", "-ma-"
        bol = [birlik(rng, qutb, inkor=True)
               for _ in range(int(rng.integers(1, 3)))]
    elif g == 3:                                     # kontrast: 2-qism hal qiladi
        a, b = ikki_jihat(rng, -qutb, qutb, inkor2=rng.random() < 0.25)
        bol, faktlar = [a, tanla(rng, BOGLOVCHI) + " " + b], faktlar[:1]
        return yozuvga(rng, faktlar + bol), yorliq, g
    elif g == 4:                                     # aralash: +/- teng -> neytral
        a, b = ikki_jihat(rng, qutb, -qutb)
        bol, yorliq = [a + tanla(rng, [", ", " va "]) + b], 1
    else:                                            # neytral
        bol = [tanla(rng, NEYTRAL_FIKR)] if rng.random() < 0.7 else []
        faktlar = faktlar or [fakt(rng)]
        yorliq = 1
    bol += faktlar
    rng.shuffle(bol)
    return yozuvga(rng, bol), yorliq, g


def korpus(n, seed, p_imlo=0.04, p_shovqin=0.03):
    """Sintetik o'zbekcha sharhlar: matnlar, yorliqlar, guruhlar."""
    rng = np.random.default_rng(seed)
    matnlar, y, guruh = [], [], []
    for _ in range(n):
        m, yl, g = sharh(rng)
        if rng.random() < p_shovqin:                # tasodifiy yorliq xatosi
            yl = int(rng.integers(3))
        matnlar.append(buz(rng, m, p_imlo))
        y.append(yl)
        guruh.append(GURUHLAR[g])
    return np.array(matnlar, dtype=object), np.array(y), np.array(guruh)


def normallashtir(s):
    s = s.lower()
    for v in ("\u02bb", "\u2019", "\u2018", "\u02bc", "`"):
        s = s.replace(v, "'")
    s = re.sub(r"[^a-z0-9' ]+", " ", s)
    return " ".join(s.split())


def bolish(y, seed=0):
    """60/20/20, sinf bo'yicha stratifikatsiya; test shu yerda qulflanadi."""
    idx = np.arange(len(y))
    ish, te = train_test_split(idx, test_size=0.2, stratify=y, random_state=seed)
    tr, va = train_test_split(ish, test_size=0.25, stratify=y[ish],
                              random_state=seed)
    return tr, va, te


def jadval_sarlavha():
    print(f"  {'model':<16}{'hamma':>8}" + "".join(f"{g:>9}" for g in GURUHLAR))


def jadval_qator(nom, y, p, g):
    """Umumiy aniqlik va har bir guruh bo'yicha aniqlik."""
    qiymat = [np.mean(p == y)] + [np.mean(p[g == gr] == y[g == gr])
                                  for gr in GURUHLAR]
    print(f"  {nom:<16}" + "".join(f"{v:>8.3f} " for v in qiymat).rstrip())
    return qiymat


class Lugat:
    """Token -> indeks; FAQAT o'quv matnlaridan quriladi (0 - pad, 1 - unk)."""

    def __init__(self, matnlar, min_son=2):
        c = Counter(s for m in matnlar for s in normallashtir(m).split())
        self.itos = ["<pad>", "<unk>"] + sorted(s for s, n in c.items()
                                                if n >= min_son)
        self.stoi = {s: i for i, s in enumerate(self.itos)}

    def __len__(self):
        return len(self.itos)

    def kodla(self, matnlar):
        seqs = [[self.stoi.get(s, 1) for s in normallashtir(m).split()] or [1]
                for m in matnlar]
        L = torch.tensor([len(s) for s in seqs])
        x = torch.zeros(len(seqs), int(L.max()), dtype=torch.long)
        for i, s in enumerate(seqs):
            x[i, :len(s)] = torch.tensor(s)
        return x, L


class BagModel(nn.Module):
    """Neyron so'z xaltasi: o'rtacha embedding -> Linear (tartib yo'q)."""

    def __init__(self, V, d=32):
        super().__init__()
        self.emb = nn.EmbeddingBag(V, d, mode="mean", padding_idx=0)
        self.bosh = nn.Linear(d, 3)

    def forward(self, x, L):
        return self.bosh(self.emb(x))


class LSTMModel(nn.Module):
    """LSTM chapdan o'ngga o'qiydi; pad lar pack_padded_sequence bilan chetlanadi."""

    def __init__(self, V, d=32, h=48):
        super().__init__()
        self.emb = nn.Embedding(V, d, padding_idx=0)
        self.lstm = nn.LSTM(d, h, batch_first=True)
        self.bosh = nn.Linear(h, 3)

    def forward(self, x, L):
        pk = pack_padded_sequence(self.emb(x), L, batch_first=True,
                                  enforce_sorted=False)
        _, (h, _) = self.lstm(pk)
        return self.bosh(h[-1])                   # oxirgi HAQIQIY token holati


def orgat(model, tr, va, davrlar=10, seed=0):
    """tr, va = (x, L, y). Eng yaxshi val aniqlikdagi holat deepcopy bilan."""
    torch.manual_seed(seed)
    opt = torch.optim.AdamW(model.parameters(), lr=5e-3, weight_decay=1e-4)
    g = torch.Generator().manual_seed(seed)
    eng, eng_davr, holat = -1.0, 0, None
    for davr in range(1, davrlar + 1):
        model.train()
        tartib = torch.randperm(len(tr[2]), generator=g)
        for i in range(0, len(tartib), 128):
            j = tartib[i:i + 128]
            opt.zero_grad()
            F.cross_entropy(model(tr[0][j], tr[1][j]), tr[2][j]).backward()
            opt.step()
        aniqlik = float((logitlar(model, va[0], va[1]).argmax(1)
                         == va[2]).float().mean())
        if aniqlik > eng:
            eng, eng_davr = aniqlik, davr
            holat = copy.deepcopy(model.state_dict())
    model.load_state_dict(holat)
    return eng, eng_davr


def logitlar(model, x, L):
    model.eval()
    with torch.no_grad():
        return model(x, L)


def ece(ehtimol, y, qutilar=10):
    """Expected Calibration Error: |ishonch - aniqlik| ning vaznli o'rtachasi."""
    ishonch, p = ehtimol.max(1), ehtimol.argmax(1)
    chegaralar = np.linspace(0, 1, qutilar + 1)
    jami = 0.0
    for a, b in zip(chegaralar[:-1], chegaralar[1:]):
        m = (ishonch > a) & (ishonch <= b)
        if m.any():
            jami += m.mean() * abs(ishonch[m].mean() - (p[m] == y[m]).mean())
    return jami


def main() -> None:
    matnlar, y, g = korpus(6000, seed=42)
    tr, va, te = bolish(y)
    lug = Lugat(matnlar[tr])
    D = {k: (*lug.kodla(matnlar[ii]), torch.tensor(y[ii]))
         for k, ii in [("tr", tr), ("va", va), ("te", te)]}
    v = TfidfVectorizer(preprocessor=normallashtir, ngram_range=(1, 2),
                        sublinear_tf=True, min_df=2)
    lr = LogisticRegression(C=5.0, max_iter=3000)
    lr.fit(v.fit_transform(matnlar[tr]), y[tr])
    torch.manual_seed(0)
    lstm = LSTMModel(len(lug))
    orgat(lstm, D["tr"], D["va"])
    ehtimol = {"TF-IDF (1,2)": lr.predict_proba(v.transform(matnlar[te])),
               "LSTM": torch.softmax(logitlar(lstm, *D["te"][:2]), 1).numpy()}

    print("=== 1. Chalkashlik matritsasi (qator - haqiqiy, ustun - bashorat) ===")
    for nom, P in ehtimol.items():
        cm = confusion_matrix(y[te], P.argmax(1))
        print(f"  {nom}:  {'':<8}" + "".join(f"{s:>8}" for s in SINFLAR))
        for k, s in enumerate(SINFLAR):
            print(f"  {'':<{len(nom) + 3}}{s:<8}" + "".join(f"{c:>8}" for c in cm[k]))

    print("\n=== 2. Neytral sharhlar qayerdan keladi va qayerda yo'qoladi ===")
    p = ehtimol["LSTM"].argmax(1)
    neyt = y[te] == 1
    print(f"  {'guruh':<9} {'neytral n':>9} {'LSTM recall':>12}")
    for gr in ["yumshoq", "aralash", "neytral", "boshqa"]:
        m = neyt & (np.isin(g[te], ["oddiy", "inkor", "kontrast"])
                    if gr == "boshqa" else (g[te] == gr))
        print(f"  {gr:<9} {int(m.sum()):>9} {np.mean(p[m] == 1):>12.3f}")
    print(f"  neytral recall: {np.mean(p[neyt] == 1):.3f}, "
          f"salbiy {np.mean(p[y[te] == 0] == 0):.3f}, "
          f"ijobiy {np.mean(p[y[te] == 2] == 2):.3f}")

    print("\n=== 3. Ishonch jadvali (LSTM, test) ===")
    P = ehtimol["LSTM"]
    ishonch, togri = P.max(1), P.argmax(1) == y[te]
    print(f"  {'oraliq':<12} {'n':>5} {'ishonch':>13} {'aniqlik':>8}")
    for a, b in [(0.0, 0.6), (0.6, 0.8), (0.8, 0.95), (0.95, 1.0)]:
        m = (ishonch > a) & (ishonch <= b)
        print(f"  ({a:.2f}, {b:.2f}] {int(m.sum()):>5} {ishonch[m].mean():>13.3f} "
              f"{togri[m].mean():>8.3f}")

    print("\n=== 4. ECE va harorat bilan kalibrlash ===")
    lv, yv = logitlar(lstm, *D["va"][:2]), D["va"][2]
    haroratlar = np.round(np.arange(0.5, 3.01, 0.05), 2)
    nll = [F.cross_entropy(lv / T, yv).item() for T in haroratlar]
    T = float(haroratlar[int(np.argmin(nll))])          # T FAQAT validatsiyadan
    lt = logitlar(lstm, *D["te"][:2])
    kalibr = torch.softmax(lt / T, 1).numpy()
    print(f"  validatsiyada tanlangan harorat T = {T:.2f}")
    print(f"  {'model':<18} {'ECE':>6} {'ishonch':>13} {'aniqlik':>8}")
    for nom, Q in list(ehtimol.items()) + [("LSTM / T", kalibr)]:
        print(f"  {nom:<18} {ece(Q, y[te]):>6.3f} {Q.max(1).mean():>13.3f} "
              f"{np.mean(Q.argmax(1) == y[te]):>8.3f}")
    oldin, keyin = ece(ehtimol["LSTM"], y[te]), ece(kalibr, y[te])
    print("  harorat ECE ni " + ("kamaytirdi" if keyin < oldin else "kamaytirmadi")
          + "; aniqlik o'zgarmaydi (argmax bir xil)")

    print("\n=== 5. Past ishonchni odamga yuborish (kalibrlangan LSTM) ===")
    print(f"  {'chegara':>8} {'avtomatik':>10} {'aniqlik':>8} {'odamga':>7}")
    for c in [0.0, 0.6, 0.8, 0.9]:
        m = kalibr.max(1) >= c
        print(f"  {c:>8.1f} {m.mean():>10.1%} "
              f"{np.mean(kalibr.argmax(1)[m] == y[te][m]):>8.3f} "
              f"{int((~m).sum()):>7}")
    m = kalibr.max(1) < 0.8
    ulush = np.mean(y[te][m] == 1)
    print(f"  ishonch < 0.8 bo'lgan sharhlarning {ulush:.1%} i haqiqatan neytral "
          f"(umumiy ulush {np.mean(y[te] == 1):.1%})")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Chalkashlik matritsasi (qator - haqiqiy, ustun - bashorat) ===
  TF-IDF (1,2):            salbiy neytral  ijobiy
                 salbiy       374      16      32
                 neytral       44     160      42
                 ijobiy        28      24     480
  LSTM:            salbiy neytral  ijobiy
         salbiy       389      16      17
         neytral       18     197      31
         ijobiy        22       9     501

=== 2. Neytral sharhlar qayerdan keladi va qayerda yo'qoladi ===
  guruh     neytral n  LSTM recall
  yumshoq          26        0.115
  aralash          74        0.757
  neytral         140        0.986
  boshqa            6        0.000
  neytral recall: 0.801, salbiy 0.922, ijobiy 0.942

=== 3. Ishonch jadvali (LSTM, test) ===
  oraliq           n       ishonch  aniqlik
  (0.00, 0.60]    36         0.538    0.500
  (0.60, 0.80]    83         0.718    0.771
  (0.80, 0.95]   211         0.895    0.834
  (0.95, 1.00]   870         0.991    0.953

=== 4. ECE va harorat bilan kalibrlash ===
  validatsiyada tanlangan harorat T = 1.45
  model                 ECE       ishonch  aniqlik
  TF-IDF (1,2)        0.058         0.787    0.845
  LSTM                0.044         0.942    0.906
  LSTM / T            0.029         0.898    0.906
  harorat ECE ni kamaytirdi; aniqlik o'zgarmaydi (argmax bir xil)

=== 5. Past ishonchni odamga yuborish (kalibrlangan LSTM) ===
   chegara  avtomatik  aniqlik  odamga
       0.0     100.0%    0.906       0
       0.6      95.1%    0.920      59
       0.8      82.2%    0.945     214
       0.9      68.3%    0.954     380
  ishonch < 0.8 bo'lgan sharhlarning 40.7% i haqiqatan neytral (umumiy ulush 20.5%)

Nima ko'rsatdi: 2.6-bo'lim.


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

Noto'g'ri fikr To'g'risi
"Umumiy aniqlik 85% — model inkorni ham tushunadi" Guruh bo'yicha aniqlikka qarang
"Lug'atga yana bir qoida qo'shsak — tuzaladi" Har qoida yangi istisno ochadi
"emas so'zi — shovqin, uni o'chirish kerak" emas ma'noni teskari qiladi; bigram uni ushlaydi
"Neyron tarmoq doim TF-IDF dan yaxshi" EmbeddingBag bigram TF-IDF dan yutqazdi
"LSTM hamma guruhda yaxshiroq" Inkor va yumshoqda farq sezilmas
"Neytral — oddiy uchinchi sinf" Uning chegarasi annotatorlar uchun ham noaniq
"Softmax ehtimoli — haqiqiy ehtimol" Kalibratsiyani o'lchang (ECE)
"Sarkazmni ham shu model aniqlaydi" O'lchanmagan narsa haqida da'vo qilinmaydi

6. Keng tarqalgan xatolar va yechimlari

1. Faqat umumiy aniqlik

python
print(np.mean(p == y))                                  # ⚠️ inkordagi sinish ko'rinmaydi
for gr in GURUHLAR:                                     # ✅
    print(gr, np.mean(p[g == gr] == y[g == gr]))

2. Normallashtirishsiz tokenlash

python
TfidfVectorizer(ngram_range=(1, 2))                     # ⚠️ "zo'r" to'rt xil token
TfidfVectorizer(preprocessor=normallashtir, ngram_range=(1, 2))   # ✅

3. Inkor uchun unigram

python
TfidfVectorizer(preprocessor=normallashtir, ngram_range=(1, 1))   # ⚠️ "yaxshi emas" = "yaxshi" + "emas"
TfidfVectorizer(preprocessor=normallashtir, ngram_range=(1, 2))   # ✅ "yaxshi emas" - alohida belgi

4. Stop-so'zlar ro'yxatida emas

python
TfidfVectorizer(stop_words=["va", "emas", "lekin"])     # ⚠️ inkor va kontrast o'chadi
TfidfVectorizer(stop_words=None)                        # ✅ sentimentda stop-so'z xavfli

5. Padsiz oxirgi holat

python
out, _ = model.lstm(model.emb(x)); h = out[:, -1]       # ⚠️ qisqa sharhda - pad holati
_, (h, _) = model.lstm(pack_padded_sequence(e, L, batch_first=True,
                                            enforce_sorted=False))   # ✅ h[-1]

6. Haroratni testda tanlash

python
T = min(Ts, key=lambda t: nll(logit_test / t, y_test))  # ⚠️ test sizib ketdi
T = min(Ts, key=lambda t: nll(logit_val / t, y_val))    # ✅ faqat validatsiya

7. Yutuqni SE siz e'lon qilish

python
print("LSTM yumshoqda yaxshi:", acc_lstm > acc_tfidf)   # ⚠️ 108 ta sharh, tasodif
print(d.mean(), 2 * d.std(ddof=1) / np.sqrt(len(d)))    # ✅ juftlashgan farq va 2*SE

7. Integratsiya — bu bilim qayerda kerak bo'ladi

  • 23.1–23.5-darslar (o'tilgan): Normallashtirish, tokenizatsiya, TF-IDF va klassik klassifikatsiya — bu darsning bazaviylari
  • 23.6–23.9-darslar (o'tilgan): Embedding, EmbeddingBag, LSTM va pack_padded_sequence
  • 18-qism (o'tilgan): Juftlashgan taqqoslash, kalibratsiya va xato tahlili
  • 21.5-dars (o'tilgan): Trainer va eng yaxshi holatni deepcopy bilan saqlash
  • Keyingi dars: NLP baholash tuzoqlari — takroriy matnlar, muallif bo'yicha bo'lish, domen siljishi
  • Transformerlar qismida: Diqqat mexanizmi "lekin" dan keyingi qismga to'g'ridan-to'g'ri qaray oladi — kontrast va aralash guruhlar yana sinaladi
  • Katta til modellari qismida: Sentimentni o'rgatishsiz (zero-shot) aniqlash va uni xuddi shu guruh jadvali bilan baholash

8. Eng yaxshi amaliyotlar

  1. Har doim lug'at bazaviysidan boshlang — u pastki chegarani va qiyin guruhlarni ko'rsatadi.

  2. Normallashtirishni birinchi qadam qiling — apostrof variantlari va registr.

  3. "Qiyin" guruhlar uchun alohida o'lchov yig'ing — inkor, kontrast, aralash.

  4. Sentimentda stop-so'zlarni olib tashlamang — emas, lekin, na — ma'no tashuvchilar.

  5. Bigramni sinab ko'ring — inkorni arzon yo'l bilan ushlaydi.

  6. Neyron modelni bazaviy bilan juftlashgan farq va 2*SE bilan taqqoslang — guruh bo'yicha ham.

  7. Neytral sinfni alohida tahlil qiling — qaysi qismi model xatosi, qaysi qismi yorliq shovqini.

  8. Ishonchni kalibrlang va past ishonchli sharhlarni odamga yo'naltiring; sarkazm kabi o'lchanmagan hodisalarni hisobotda ochiq ayting.


9. Amaliy topshiriq

Vazifa 1: Bashorat qiling

python
1.  # "sifati yaxshi emas" unigram xaltasi qanday tokenlardan iborat?
2.  # "yomon emas" bigrami ijobiy yoki salbiy vazn oladi?
3.  # "yoqdi" va "yoqmadi" ni farqlash uchun bigram kerakmi?
4.  # lug'at bazaviysi "kechikmasdan keldi" ni qanday baholaydi va nega?
5.  # "narxi arzon lekin sifati yomon" va "sifati yomon lekin narxi arzon" -
    # unigram modelda ehtimollar qanday?
6.  # EmbeddingBag qaysi klassik modelga eng yaqin?
7.  # LSTM da nega pack_padded_sequence kerak?
8.  # juftlashgan taqqoslashda d_i qanday qiymatlar oladi?
9.  # yumshoq guruhda aniqlik nega ~0.7 dan oshmaydi?
10. # model o'rtacha ishonchi 0.94, aniqligi 0.91 - bu qanday holat?
11. # harorat T > 1 aniqlikni o'zgartiradimi?
12. # sarkazm bo'yicha aniqlikni bu korpusda o'lchash mumkinmi?
Javoblar
  1. {sifati, yaxshi, emas} — tartib yo'q
  2. Ijobiy (2-misolda +8.25)
  3. Yo'q — ular ikki xil token, unigram ham farqlaydi
  4. Salbiy: "kech" ildizi topiladi, "-ma-" esa ildizdan keyin emas, so'z ichida
  5. Bir xil — so'z xaltasi bir xil (2-misolda ikkalasi 0.658)
  6. Unigram so'z xaltasi (TF-IDF (1,1) + chiziqli model)
  7. Oxirgi holat pad tokenlaridan emas, haqiqiy oxirgi tokendan olinishi uchun
  8. -1, 0 yoki 1
  9. Yorliqlarning chorak qismi annotatorlar tomonidan "neytral" qo'yilgan — matn bir xil, yorliq har xil
  10. Ortiqcha ishonch (overconfidence)
  11. Yo'q — argmax o'zgarmaydi, faqat ishonch pasayadi
  12. Yo'q — generatorda sarkazm yo'q; real ma'lumotda ham alohida yorliqlash kerak

Vazifa 2: Xatolarni tuzating

python
1.  v = TfidfVectorizer(stop_words=["emas", "va", "lekin"])

2.  v = TfidfVectorizer(ngram_range=(1, 2)).fit(butun_korpus)

3.  lug = Lugat(matnlar)                               # butun korpus

4.  out, _ = lstm(emb(x)); logit = bosh(out[:, -1])     # x - padlangan

5.  print(f"LSTM kontrastda yaxshiroq: {acc_lstm_k:.3f} > {acc_tf_k:.3f}")
Javoblar
python
1.  v = TfidfVectorizer(preprocessor=normallashtir, ngram_range=(1, 2))

2.  v = TfidfVectorizer(preprocessor=normallashtir, ngram_range=(1, 2))
    X_tr = v.fit_transform(matnlar[tr])                # faqat o'quvda fit

3.  lug = Lugat(matnlar[tr])

4.  pk = pack_padded_sequence(emb(x), L, batch_first=True, enforce_sorted=False)
    _, (h, _) = lstm(pk); logit = bosh(h[-1])

5.  d = (p_lstm[k] == y[k]).astype(float) - (p_tf[k] == y[k])
    print(f"farq {d.mean():+.3f}, 2*SE {2 * d.std(ddof=1) / np.sqrt(len(d)):.3f}")

Vazifa 3: Lug'at bazaviysi

Modellang:

  1. Lug'atga 10 ta sinonim qo'shing — qamrov va umumiy aniqlik qanchaga o'zgaradi?
  2. "-ma-" qoidasini "kechikmasdan" ni ham ushlaydigan qiling — qaysi yangi xatolar paydo bo'ladi?
  3. Normallashtirishsiz lug'at bazaviysi: apostrof variantlari qancha xato beradi?
  4. Guruh bo'yicha jadval

Vazifa 4: TF-IDF

Modellang:

  1. ngram_range=(1, 3) — qaysi guruh yaxshilanadi, qaysi biri yomonlashadi?
  2. Belgi n-gramlari (analyzer="char_wb", 2–5) — imlo xatolariga chidamliroqmi?
  3. C ni validatsiyada tanlang
  4. Eng kuchli 10 ta ijobiy va salbiy belgi

Vazifa 5: Neyron modellar

Modellang:

  1. Ikki yo'nalishli LSTM — kontrast va aralash guruhlar o'zgaradimi?
  2. EmbeddingBag ga bigram tokenlar qo'shing (fastText g'oyasi)
  3. 3 seed bilan juftlashgan taqqoslash
  4. Qaysi guruhda LSTM sezilarli yutadi — natijadan hisoblab chop eting

Vazifa 6: Kalibratsiya

Modellang:

  1. TF-IDF modeli uchun ham harorat bilan kalibrlash (decision_function / T)
  2. 10 bo'lakli ishonch jadvali
  3. Chegara 0.7, 0.8, 0.9 — avtomatik ulush va aniqlik
  4. Past ishonchli sharhlar tarkibi guruh bo'yicha

Vazifa 7: O'ylash

Sizning sentiment modelingiz test to'plamida 0.91 aniqlik berdi va mahsulot jamoasi uni ishga tushirishni so'ramoqda. Ikki hafta o'tib marketing bo'limi yangi aksiya boshladi va sharhlarda "narxi zo'r, lekin sifati ham chakki emas", "aksiyasiz olmasdim" kabi yangi iboralar ko'paydi. Model hisobotlarida hech qanday xato yo'q. Nimalar noto'g'ri ketishi mumkin, qanday tekshirasiz va nima qilasiz?

Javob

Qisqa javob: bu til siljishi — sharhlarda model hech qachon ko'rmagan inkor va kontrast shakllari paydo bo'lgan. Umumiy aniqlik yashiradi, guruh jadvali esa ochib beradi.

Nima noto'g'ri ketishi mumkin:

1. Yangi inkor shakllari. "chakki emas" (= yaxshi) — ikki marta inkor. "chakki" o'quv lug'atida yo'q bo'lsa, u <unk> ga aylanadi va model faqat "emas" ni ko'radi. TF-IDF bigrami "chakki emas" ni umuman bilmaydi — bunday belgi o'quvda yo'q edi.

2. Yangi kontrast bog'lovchilari. "ham" bilan tuzilgan ijobiy kontrast ("narxi zo'r, sifati ham yaxshi") va shartli gaplar ("aksiyasiz olmasdim" — narx haqida ijobiy, mahsulot haqida esa neytral yoki salbiy). Bizning modellar "lekin/ammo/biroq" dan keyingi qismni o'rgangan edi.

3. Taqsimot o'zgargan. Aksiya davrida ijobiy sharhlar ulushi o'sadi. Kalibrlangan ishonch eski taqsimotda sozlangan edi; harorat endi to'g'ri bo'lmasligi mumkin.

Qanday tekshirasiz:

  • <unk> ulushini kuzating: 3-misolda testda u 0.021 edi — yangi sharhlarda keskin oshsa, bu birinchi signal.
  • Bashoratlar taqsimotini va o'rtacha ishonchni haftalik kuzating.
  • 100–200 ta yangi sharhni qo'lda yorliqlang, ularga guruh belgisi qo'ying (yangi inkor, yangi kontrast) va guruh bo'yicha aniqlikni qayta hisoblang.
  • Past ishonchli sharhlar ulushini kuzating: agar u 18% dan (4-misoldagi chegara 0.8) sezilarli oshsa — model yangi hodisalarga duch kelgan.

Nima qilasiz:

  • Qisqa muddat: ishonch chegarasini ko'tarib, ko'proq sharhni odamga yo'naltirish.
  • O'rta muddat: yangi sharhlarni yorliqlab, lug'at va modelni qayta o'rgatish; "qiyin to'plam" ga yangi guruhlarni qo'shish.
  • Uzoq muddat: bog'lovchi va inkor shakllari ko'p bo'lgan til uchun kontekstni butunlay ko'radigan modellar (Transformerlar qismida) va doimiy monitoring (MLOps qismida).

Muhim nuans: "hisobotlarda xato yo'q" — til siljishining eng xavfli tomoni. Umumiy aniqlikni kuzatish uchun ham yorliqlangan yangi ma'lumot kerak, va u bo'lmasa model jimgina yomonlashadi. Shuning uchun <unk> ulushi, ishonch taqsimoti va past ishonchli ulush kabi yorliqsiz signallar birinchi o'rinda turadi.

Nimani mustahkamlaydi: 2.2, 2.3, 2.5, 2.6-bo'limlar.


Xulosa

Bu darsda sentiment modelini qurdik va uni "qiyin" guruhlar bo'yicha baholadik.

Eng muhim uch fikr:

  1. Lug'at bazaviysi pastki chegarani va qiyin joylarni ko'rsatadi. 1-misolda faqat lug'at inkor guruhida 0.013 aniqlik berdi — deyarli doim teskari javob. Inkor va "lekin" qoidalari umumiy aniqlikni 0.483 dan 0.628 ga ko'tardi, lekin fikr bildirgan sharhlarning 33.1% ida lug'at so'zi umuman topilmadi. Qoidalar bilan tuzatish cheksiz istisnolarga olib keladi.

  2. Bigram inkorni, LSTM esa tartibni ushlaydi — va buni guruh jadvali ko'rsatadi. TF-IDF bigrami inkor guruhida aniqlikni 0.827 dan 0.934 ga ko'tardi ('yomon emas' vazni +8.25). LSTM umumiy aniqlikda 0.906 bilan TF-IDF dan +0.061 ga yaxshi bo'ldi (2*SE = 0.019), lekin yutuq asosan aralash (+0.421) va kontrast (+0.079) guruhlaridan keldi; inkorda farq sezilmadi. EmbeddingBag esa (0.750) so'z xaltasi bo'lgani uchun bigram TF-IDF dan yutqazdi.

  3. Neytral — eng qiyin sinf, ishonch esa kalibrlanishi kerak. LSTM da neytral recall 0.801, qolgan ikki sinfda 0.92 dan yuqori; annotatorlar kelisha olmagan yumshoq sharhlarda esa atigi 0.115. LSTM ortiqcha ishondi (0.942 ishonch, 0.906 aniqlik), harorat T = 1.45 ECE ni 0.044 dan 0.029 ga tushirdi, va 0.8 chegara bilan sharhlarning 82.2% i 0.945 aniqlik bilan avtomatik qayta ishlandi.

Keyingi darsda matn ma'lumotini baholashning o'ziga xos tuzoqlarini ko'ramiz: takroriy va deyarli takroriy matnlar, vektorizator leakage i, muallif bo'yicha bo'lish va domen siljishi — ya'ni bu darsdagi raqamlar qachon ishonchli ekanini qanday tekshirish.

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
23.12-dars: Sentiment tahlili — IlmHamroh