IlmHamroh
Data Science va sun'iy intellekt/Maxsus mavzular5/12-dars58 daqiqa
Mundarija (24)

28.5-dars: Matritsa faktorizatsiyasi va implicit feedback

28-QISM — MAXSUS MAVZULAR · 5-dars


1. Kirish va motivatsiya

Oldingi darsda tavsiya tizimini noldan qurdik: mashhurlik, kontent-asosli usul, user-kNN va item-kNN — va ularni vaqt bo'yicha bo'lish, ranjirlash metrikalari va foydalanuvchilar bo'yicha juftlashgan SE bilan halol solishtirdik. kNN usullari "o'xshash foydalanuvchi" yoki "birga olingan mahsulot" ni to'g'ridan-to'g'ri qidiradi. Ular sodda va kuchli, lekin bitta cheklovi bor: o'xshashlik faqat umumiy ta'sirlar orqali ko'rinadi. Ikki foydalanuvchi bitta ham umumiy mahsulot olmagan bo'lsa, kNN ular orasidagi o'xshashlikni ko'rmaydi — garchi biri "qahva va shokolad", ikkinchisi "choy va pechene" olgan bo'lsa ham, ya'ni ikkalasi ham "shirinlik va ichimlik" ixlosmandi bo'lsa ham.

Matritsa faktorizatsiyasi (MF) bu muammoni boshqacha hal qiladi: har foydalanuvchi va har mahsulotni kichik o'lchamli yashirin (latent) vektor bilan ifodalaydi. Vektorlar shunday o'rganiladiki, ularning skalyar ko'paytmasi o'zaro ta'sirni tiklaydi. Natijada "qahva" va "choy" vektorlari bir-biriga yaqin joylashadi — ularni birga olgan hech kim bo'lmasa ham, ular o'xshash odamlar tomonidan olingani uchun.

Real vaziyat. Samarqanddagi kitob va sovg'alar do'koni bosh sahifada "Siz uchun" blokini ishga tushirdi. Birinchi versiya — explicit baholar ustida klassik SVD. Offline RMSE yaxshi chiqdi, lekin tavsiyalar g'alati edi: bitta-ikkita baholangan kam uchraydigan kitoblar hammaga chiqardi. Ikkinchi urinishda jamoa bo'sh kataklarni nol deb, butun matritsaga TruncatedSVD qo'lladi — endi model hamma narsaga "yoqmaydi" dedi. Muammoning ildizi bitta: bo'sh katak bilan qanday ishlash. Explicit baholarda bo'sh kataklar umuman ishtirok etmasligi kerak, implicit signallarda esa ular ishtirok etishi, lekin kichik ishonch bilan ishtirok etishi kerak. Bu darsda aynan shu farqni noldan quramiz.

Bu darsda latent faktorlar g'oyasini va uning ikki asosiy ko'rinishini — explicit baholar uchun bias li MF va implicit feedback uchun ALS ni — noldan quramiz, giperparametrlarni validatsiyada tanlaymiz va sovuq startni o'lchaymiz.

Bu darsda:

  • Latent faktorlar: R ~ P Q^T
  • SVD (TruncatedSVD) va nega bo'sh kataklarni nol deb olish noto'g'ri
  • Explicit baholar uchun MF: biaslar + p_u . q_i, SGD noldan, regularizatsiya, RMSE
  • Implicit feedback uchun ALS noldan: ishonch c = 1 + alfa * r, hamma kataklar ishtirok etadi
  • BPR: juftlik bo'yicha ranjirlash (torch bilan)
  • Faktorlar soni k va regularizatsiyani validatsiyada tanlash
  • Sovuq start: yangi foydalanuvchi va yangi mahsulot — o'lchab
  • Ikki bosqichli tizim va onlayn baholash (nazariyada)

ℹ Misollar real numpy/scipy/sklearn/torch bilan (Python 3.14). implicit, surprise, lightfm kutubxonalari ishlatilmaydi — ular bajaradigan ishni kichik, tushunarli kod bilan noldan quramiz. 2-4-misollar 28.4-darsdagi bir xil sintetik do'kon va bir xil "vaqt" bo'lishidan foydalanadi, shuning uchun raqamlarni bevosita solishtirish mumkin.


2. Nazariya — chuqur tushuntirish

2.1. Latent faktorlar g'oyasi

text
      R (n_user x n_item)      ~      P (n_user x k)   x   Q^T (k x n_item)

   i1 i2 i3 i4 i5 i6               f1   f2                 i1  i2  i3  i4  i5  i6
u1  5  -  4  -  -  1         u1  [ 1.2  -0.3 ]       f1 [ 1.0 0.9 1.1 -0.2 0.1 -0.9 ]
u2  -  4  -  -  2  -    ~    u2  [ 0.9   0.2 ]   x   f2 [ 0.1 0.3 -0.1  1.2 0.8  0.2 ]
u3  1  -  -  5  4  -         u3  [-0.5   1.4 ]

r_hat(u, i) = p_u . q_i = sum_f p_uf * q_if

f1, f2 - NOMSIZ o'qlar; ma'lumotdan o'zi topiladi
   masalan f1 ~ "shirinlik va ichimlik", f2 ~ "sport va faol hayot"
k << n_user, n_item   (odatda 10-200)
parametrlar: (n_user + n_item) * k  -  n_user * n_item o'rniga

Faktorizatsiya — siqish. 2000 x 800 = 1.6 mln katak o'rniga (2000 + 800) x 16 = 44 800 parametr. Siqish majburlaydi: model har foydalanuvchini bir necha "did o'qi" bo'yicha tasvirlashi kerak — va shu o'qlar orqali hech qachon birga ko'rilmagan mahsulotlar o'rtasida ham o'xshashlik paydo bo'ladi. Bu 16.8-darsdagi PCA ning "bo'sh kataklar bor" holatidagi umumlashmasi.

2.2. SVD va bo'sh kataklar muammosi

text
TO'LIQ SVD:  R = U S V^T  -  faqat TO'LIQ matritsa uchun aniqlangan
   bo'sh kataklarni nima bilan to'ldiramiz?

VARIANT 1: bo'sh = 0 (TruncatedSVD siyrak matritsaga shunday qaraydi)
   explicit baholarda: "baho 0" - 1..5 shkalada mavjud bo'lmagan qiymat
   97% katak nol -> eng yaxshi past rangli yaqinlashish HAM ~0 ga yaqin
   test bashoratlari ~0.5, RMSE ~3 (1-misol)

VARIANT 2: o'rtacha bilan to'ldirish (yoki biaslarni ayirib, qoldiqni 0 deb olish)
   yaxshiroq, lekin baribir to'ldirilgan qiymatlarni HAQIQAT deb o'rganadi
   97% "soxta" ma'lumot 3% haqiqiy ma'lumotni bosib ketadi

TO'G'RI YO'L (explicit): yo'qotishni FAQAT kuzatilgan kataklar ustida
   min sum_{(u,i) kuzatilgan} (r_ui - r_hat_ui)^2 + regularizatsiya
   -> bu endi SVD emas, gradient tushish yoki ALS bilan yechiladigan
      optimallashtirish (Netflix Prize dagi "Funk SVD")

Implicit ma'lumotda esa vaziyat teskari: u yerda bo'sh kataklarni tashlab ketish xato 2.4-bob.

2.3. Explicit baholar uchun MF: biaslar va SGD

text
MODEL:
   r_hat(u, i) = mu + b_u + b_i + p_u . q_i
   mu   - global o'rtacha baho
   b_u  - foydalanuvchi biasi (qattiqqo'l -0.4, saxiy +0.5)
   b_i  - mahsulot biasi (sifatli kitob +0.6)
   p_u . q_i - DID bo'yicha moslik (biaslardan tashqari qism)

YO'QOTISH (faqat kuzatilgan kataklar):
   L = sum (r_ui - r_hat_ui)^2 + reg * (b_u^2 + b_i^2 + |p_u|^2 + |q_i|^2)

SGD YANGILASH (har baho yoki mini-partiya uchun):
   e    = r_ui - r_hat_ui
   b_u += lr * (e - reg * b_u)
   b_i += lr * (e - reg * b_i)
   p_u += lr * (e * q_i - reg * p_u)
   q_i += lr * (e * p_u - reg * q_i)       (eski p_u bilan)

BAHOLASH: RMSE = sqrt(mean (r - r_hat)^2)  - test baholarida

Biaslar — eng katta yutuq. Ko'p amaliy holatlarda "global o'rtacha → biaslar" qadami RMSE ni "biaslar → biaslar + faktorlar" qadamidan ko'proq kamaytiradi. Bias li bazaviy — explicit MF uchun majburiy bazaviy (xuddi implicit uchun mashhurlik kabi).

Regularizatsiya zarur: har foydalanuvchida bir necha baho bor, lekin k ta parametr — reg = 0 da model train baholarini yodlab oladi (1-misolda test RMSE 20-epoxadan keyin yana o'sadi).

2.4. Implicit feedback: ishonch va ALS

Hu, Koren va Volinsky (2008) implicit ma'lumot uchun ikki tushunchani ajratdi:

text
AFZALLIK (preference):  p_ui = 1, agar r_ui > 0 (ko'rgan/olgan), aks holda 0
ISHONCH  (confidence):  c_ui = 1 + alfa * r_ui
   r_ui = signal kuchi (bizda: ko'rish = 1, xarid = 3)
   bo'sh katak: p = 0, c = 1   -> "ehtimol yoqmaydi", lekin ISHONCH PAST
   xarid:       p = 1, c = 1 + 3*alfa -> "yoqadi", ishonch yuqori

YO'QOTISH - HAMMA n_user x n_item katak ustida:
   L = sum_{u,i} c_ui (p_ui - x_u . y_i)^2 + lambda (sum |x_u|^2 + sum |y_i|^2)

NEGA HAMMA KATAK:
   faqat kuzatilganlar (hammasi p = 1) -> trivial yechim: hamma
   ko'paytmani 1 qilish; model "hamma narsa yoqadi" deydi
   nollar "kuchsiz salbiy" misol sifatida ranjirlashga yo'nalish beradi

Bu yo'qotishni SGD bilan minimallashtirish qimmat — n_user x n_item katak. Lekin Y qotirilsa, har x_u uchun masala oddiy og'irlikli ridge regressiya bo'ladi va yopiq shaklda yechiladi. Navbatma-navbat X ni, keyin Y ni yechish — ALS (alternating least squares):

text
x_u = (Y^T C_u Y + lambda I)^(-1)  Y^T C_u p_u          C_u = diag(c_u1 .. c_uN)

HIYLA (tezlik):   Y^T C_u Y = Y^T Y  +  Y^T (C_u - I) Y
   Y^T Y             - HAMMA foydalanuvchi uchun bitta, bir marta hisoblanadi
                       (nollarni c = 1 bilan qamraydi)
   Y^T (C_u - I) Y   - faqat u ko'rgan n_u ta mahsulot ustida (c - 1 = 0 qolganida)
   Y^T C_u p_u       - ham faqat ko'rilganlar ustida (p = 0 qolganida)

   narx: O(n_u * k^2 + k^3) har foydalanuvchiga  (O(n_item * k^2) o'rniga)
   hamma katak ishtirok etadi, lekin hisob faqat nol bo'lmaganlar ustida

ALS matematik jihatdan hamma kataklarni hisobga oladi, hisoblashda esa faqat nol bo'lmaganlarni aylanadi. 2-misolda tezkor formula to'liq (800 katakli) hisob bilan aynan bir xil javob berishini tekshiramiz.

alfa — muhim giperparametr. Juda katta alfa ko'rilgan mahsulotlarni "o'ta ishonchli" qiladi va model ularni yodlab, qolganlarini deyarli farqlamaydi; alfa = 0 barcha kataklarni teng ko'radi. To'g'ri qiymat ma'lumotga bog'liq va validatsiyada tanlanadi.

2.5. BPR: juftlik bo'yicha ranjirlash

text
G'OYA (Rendle va boshq., 2009): biz aniq qiymatni emas, TARTIBNI xohlaymiz
   foydalanuvchi u ko'rgan i ni ko'rmagan j dan YUQORI qo'ysin

   har qadamda uchlik (u, i+, j-):  i - ko'rgan, j - tasodifiy (ko'rmagan)
   x_uij = x_ui - x_uj = p_u . (q_i - q_j) + (b_i - b_j)
   L = - sum log sigmoid(x_uij) + reg * |parametrlar|^2

   bu AUC ning silliq yaqinlashuvi: "tasodifiy juftlikda to'g'ri tartib"

AFZALLIGI: to'g'ridan-to'g'ri ranjirlashni optimallashtiradi, SGD/Adam
           bilan har qanday model (neyron tarmoq ham) ga qo'llanadi
MUAMMOSI:  salbiy namuna olish (negative sampling) - tasodifiy j lar
           ko'pincha "oson" (mashhur emas) -> model asosan mashhurlikni
           o'rganadi; sozlash sezgir (lr, reg, epoxalar)

2.6. Giperparametrlar: k, lambda, alfa

text
k (faktorlar soni):   kichik -> "qo'pol" didlar;  katta -> yodlash, sekin
lambda (regularizatsiya): kichik -> overfitting;  katta -> hamma mashhurlikka yaqin
alfa (ishonch):       0 -> hamma katak teng;  katta -> ko'rilganlarni yodlash

TANLASH:
   train ICHIDAN validatsiya: xuddi test kabi bo'lish (har foydalanuvchining
   oxirgi 20% i) - train2 / val
   setka yoki navbatma-navbat qidiruv val nDCG@10 bo'yicha
   QAROR: eng yaxshisidan sezilarli yomon bo'lmagan ENG SODDA
          (kichik k, katta lambda) - juftlashgan SE bilan
   tanlangan sozlama BUTUN train da qayta o'qitiladi, test - bir marta

2.7. Sovuq start

text
YANGI FOYDALANUVCHI (tarixi yo'q yoki 1-2 ta):
   CF uchun vektor yo'q -> ball 0, tavsiya tasodifiy
   FALLBACK: mashhurlik (vaqt oynasi, kategoriya, mintaqa bo'yicha)
   FOLD-IN: model qayta o'qitilmaydi - Y qotirilgan holda faqat x_u
            yechiladi (bitta ridge yechim, millisekundlar)
   ONBOARDING: "3 ta qiziqishingizni tanlang" - n ni sun'iy oshirish
   QOIDA: n < chegara -> mashhurlik, aks holda CF; chegara O'LCHANADI

YANGI MAHSULOT (hali hech kim ko'rmagan):
   CF da vektori yo'q (y_i = 0) -> hech kimga tavsiya qilinmaydi
   -> ta'sir to'planmaydi -> hech qachon tavsiya qilinmaydi (halqa!)
   KONTENT: tavsif/atributlar bo'yicha foydalanuvchi profiliga yaqinlik
   GIBRID "KO'PRIK": kontent belgilari -> CF faktorlari regressiyasi
            (iliq mahsulotlarda o'qitiladi), yangi mahsulotga taxminiy y_i
   EXPLORATION: yangi mahsulotlarga ataylab kichik trafik (27.13 bandit)

Sovuq start — alohida o'lchanadigan segment. Umumiy nDCG da yangi foydalanuvchilar ozchilik bo'lib, ularning yomon tajribasi ko'rinmaydi. Ularni alohida baholang (4-misol).

2.8. Ikki bosqichli tizim

Real tavsiya tizimlari millionlab mahsulot orasidan bir necha o'n millisekundda javob berishi kerak. Shuning uchun bitta model emas, quvur ishlatiladi:

text
1. NOMZOD GENERATSIYASI (candidate generation, recall bosqichi)
   bir nechta arzon manba, har biri 50-500 nomzod:
     ALS / item-kNN (vektorlar yaqinligi - ANN indeksi: Faiss, ScaNN)
     mashhurlik (umumiy, kategoriya, mintaqa, trend)
     "oxirgi ko'rgan mahsulotiga o'xshashlar", kontent
   maqsad - YAXSHI mahsulotni ro'yxatga KIRITISH (recall@500)

2. QAYTA RANJIRLASH (ranking bosqichi)
   bir necha yuz nomzod x boy belgilar -> gradient boosting (15-qism)
   yoki neyron tarmoq
   belgilar: CF ballari, mashhurlik, narx, foydalanuvchi faolligi,
             kontekst (vaqt, qurilma), mahsulot yoshi, oldingi ko'rsatishlar
   maqsad - ANIQ tartib (nDCG@10), biznes maqsadi (xarid ehtimoli)

3. BIZNES QOIDALARI
   omborda yo'q -> olib tashlash; xilma-xillik; homiylik; takrorlarni cheklash

Har bosqich alohida baholanadi: birinchi bosqich uchun recall@N (N katta), ikkinchisi uchun nDCG@10. Birinchi bosqich o'tkazib yuborgan mahsulotni ikkinchisi hech qachon ko'rmaydi.

2.9. Onlayn baholash

Offline metrikalar — tarixiy jurnalda "foydalanuvchi keyin nimani oldi" savoliga javob. Lekin bu jurnal eski tizim ko'rsatgan narsalar ta'sirida yig'ilgan: foydalanuvchi faqat ko'rsatilganlarni ko'ra oladi. Yangi model ko'rsatadigan, eski tizim hech qachon ko'rsatmagan mahsulot uchun jurnalda javob yo'q — offline metrika uni "xato" deb hisoblaydi.

text
OFFLINE:  tez, arzon, ko'p variantni solishtirish mumkin
          lekin ekspozitsiya tarafkashligi, pozitsiya tarafkashligi
ONLINE (A/B test, 27.13): yagona ishonchli javob
          asosiy metrika: xarid/sessiya, daromad; guardrail: qaytarish, tezlik
          ko'pincha offline yutuqning faqat bir qismi saqlanadi
          uzoq muddatli ta'sir (feedback loop) - haftalar kerak

Offline — filtr, onlayn — hakam. Offline baholash qaysi variantlarni A/B testga chiqarishni tanlaydi; qaysi biri yaxshi ekanini onlayn tajriba hal qiladi. Tavsiyaning sababiy ta'sirini (u bo'lmasa ham olarmidi?) o'lchash 28.11-darsdagi sababiy xulosa mavzusi.

2.10. Tuzoqlar

Asosiy tuzoqlar: explicit baholarda bo'sh kataklarni nol deb SVD qilish; implicit ma'lumotda nollarni butunlay tashlab ketish ("hamma narsa yoqadi"); biaslarsiz MF va bias li bazaviysiz taqqoslash; regularizatsiyasiz o'qitish; k, lambda, alfa ni test da tanlash; juda katta alfa; murakkab modelni mashhurlik va item-kNN bilan solishtirmaslik; sovuq start segmentini umumiy o'rtacha ichida yashirish; yangi mahsulotni kontentsiz "o'z holiga" qo'yish; offline yutuqni onlayn natija deb e'lon qilish.


3. Tez ma'lumotnoma

python
import numpy as np
from scipy import sparse

# EXPLICIT: bias li MF, mini-partiyali SGD
e = r - (mu + bu[u] + bi[i] + np.sum(P[u] * Q[i], 1))
np.add.at(bu, u, lr * (e - reg * bu[u]))
np.add.at(bi, i, lr * (e - reg * bi[i]))
dP, dQ = lr * (e[:, None] * Q[i] - reg * P[u]), lr * (e[:, None] * P[u] - reg * Q[i])
np.add.at(P, u, dP)
np.add.at(Q, i, dQ)


# IMPLICIT: ALS qadami (hamma foydalanuvchi bir yo'la)
def als_qadam(R, Y, lam, alfa):
    k = Y.shape[1]
    YtY = Y.T @ Y + lam * np.eye(k)                       # hamma kataklar (c = 1)
    tashqi = (Y[:, :, None] * Y[:, None, :]).reshape(len(Y), k * k)
    W = R.copy()
    W.data = alfa * R.data                                # c - 1, faqat nol bo'lmagan
    A = YtY + (W @ tashqi).reshape(-1, k, k)
    W.data = 1 + alfa * R.data                            # c
    return np.linalg.solve(A, (W @ Y)[:, :, None])[:, :, 0]


V = rng.normal(0, 0.1, (n_item, k))
for _ in range(8):
    U = als_qadam(R, V, lam, alfa)
    V = als_qadam(R.T.tocsr(), U, lam, alfa)
ball = U[users] @ V.T

# yangi foydalanuvchi: fold-in (V qotirilgan)
x_yangi = als_qadam(R_malum, V, lam, alfa)

# yangi mahsulot: kontent -> faktor ko'prigi
v_yangi = Ridge(alpha=1.0).fit(tfidf[iliq], V[iliq]).predict(tfidf[yangi])

Haqiqiy loyihada bu ishni tayyor kutubxonalar tez bajaradi (bu blok ishga tushirilmaydi, faqat tanishish uchun):

python
import implicit                                     # ALS, BPR - C++/GPU
model = implicit.als.AlternatingLeastSquares(factors=64, regularization=0.05,
                                             alpha=2.0, iterations=15)
model.fit(user_items)                               # CSR: foydalanuvchi x mahsulot
ids, ballar = model.recommend(u, user_items[u], N=10)

from surprise import SVD, Dataset                  # explicit baholar (bias li MF)
algo = SVD(n_factors=50, reg_all=0.05).fit(trainset)

Qaysi vaziyatda nima

Vaziyat Yondashuv
1-5 baholar (explicit) biaslar → bias li MF (SGD/ALS), RMSE + ranjirlash metrikasi
Ko'rish/xarid (implicit) ALS (ishonch bilan) yoki BPR; bazaviy — mashhurlik va item-kNN
Katta katalog, tez javob ikki bosqich: ALS/kNN nomzodlar + GBM qayta ranjirlash
Yangi foydalanuvchi mashhurlik fallback → fold-in (n chegarasi o'lchanadi)
Yangi mahsulot kontent-asosli yoki kontent → faktor ko'prigi + exploration
Giperparametrlar train ichidan validatsiya, "eng sodda munosib" qoidasi

MF xulosasi

R ~ P Q^T: foydalanuvchi va mahsulot - k o'lchamli did vektorlari
explicit: faqat kuzatilgan kataklar, mu + b_u + b_i + p.q, regularizatsiya
implicit: hamma kataklar, ishonch c = 1 + alfa*r, ALS yopiq yechim
BPR: tartibni to'g'ridan-to'g'ri, lekin negative sampling va sozlash sezgir
sovuq start: mashhurlik / fold-in / kontent ko'prigi - segment alohida o'lchanadi
offline - filtr, A/B test - hakam

4. Batafsil misollar

Misollar real numpy/scipy/sklearn/torch bilan (Python 3.14). Har misol mustaqil ishlaydi. 1-misol explicit baholar uchun alohida generator ishlatadi; 2-4-misollar 28.4 dagi dokon() (urug' 0) va "vaqt" bo'lishini ishlatadi.

Misol 1 — Explicit baholar: SVD nima uchun ishlamaydi va bias li MF (SGD)

Generator baholarni aynan 2.3-bo'limdagi model bo'yicha yaratadi: global o'rtacha 3.5, foydalanuvchi va mahsulot biaslari, 4 ta yashirin did o'qi va shovqin; qaysi kataklar kuzatilishi foydalanuvchi faolligi va mahsulot mashhurligiga bog'liq.

python
"""Explicit baholar: SVD nima uchun ishlamaydi va bias li MF (SGD) noldan."""

import numpy as np
import pandas as pd
from sklearn.decomposition import TruncatedSVD
from sklearn.linear_model import LinearRegression
from scipy import sparse


def baholar(seed=0, n_user=1500, n_item=600, k=4):
    """Sintetik 1-5 baholar: global o'rtacha + biaslar + k ta yashirin faktor."""
    rng = np.random.default_rng(seed)
    bu, bi = rng.normal(0, 0.35, n_user), rng.normal(0, 0.45, n_item)
    P, Q = rng.normal(0, 0.55, (n_user, k)), rng.normal(0, 0.55, (n_item, k))
    faollik, pop = rng.lognormal(0, 0.8, n_user), rng.lognormal(0, 1.0, n_item)
    ehtimol = np.outer(faollik, pop)
    ehtimol *= 60_000 / ehtimol.sum()
    u, i = np.nonzero(rng.random((n_user, n_item)) < np.minimum(ehtimol, 1))
    r = 3.5 + bu[u] + bi[i] + np.sum(P[u] * Q[i], 1) + rng.normal(0, 0.6, len(u))
    df = pd.DataFrame({"user": u, "item": i, "baho": np.clip(np.round(r), 1, 5)})
    return df, Q


def bolish_baho(df, seed=0):
    """Har foydalanuvchining tasodifiy 20% baholari - test (kamida 5 ta bahosi bo'lsa)."""
    kalit = np.random.default_rng(seed).random(len(df))
    n = df.groupby("user")["baho"].transform("size").to_numpy()
    tartib = df.assign(k=kalit).groupby("user")["k"].rank(method="first").to_numpy()
    test = (tartib <= np.maximum(1, np.round(0.2 * n))) & (n >= 5)
    return df[~test].reset_index(drop=True), df[test].reset_index(drop=True)


def biaslar(tr, n_user, n_item, reg=5.0, iters=10):
    """b_u va b_i navbatma-navbat (regularizatsiyalangan o'rtachalar)."""
    mu = tr["baho"].mean()
    u, i, r = tr["user"].to_numpy(), tr["item"].to_numpy(), tr["baho"].to_numpy()
    bu, bi = np.zeros(n_user), np.zeros(n_item)
    for _ in range(iters):
        bu = np.bincount(u, r - mu - bi[i], n_user) / (np.bincount(u, None, n_user) + reg)
        bi = np.bincount(i, r - mu - bu[u], n_item) / (np.bincount(i, None, n_item) + reg)
    return mu, bu, bi


def mf_sgd(tr, n_user, n_item, k=10, lr=0.02, reg=0.05, epoxlar=40, partiya=256,
           seed=0, kuzat=None):
    """r_hat = mu + b_u + b_i + p_u . q_i; mini-partiyali SGD, L2 regularizatsiya."""
    rng = np.random.default_rng(seed)
    u, i, r = tr["user"].to_numpy(), tr["item"].to_numpy(), tr["baho"].to_numpy()
    mu = r.mean()
    bu, bi = np.zeros(n_user), np.zeros(n_item)
    P, Q = rng.normal(0, 0.1, (n_user, k)), rng.normal(0, 0.1, (n_item, k))
    tarix = []
    for ep in range(1, epoxlar + 1):
        for s in np.array_split(rng.permutation(len(r)), len(r) // partiya):
            uu, ii = u[s], i[s]
            e = r[s] - (mu + bu[uu] + bi[ii] + np.sum(P[uu] * Q[ii], 1))
            np.add.at(bu, uu, lr * (e - reg * bu[uu]))
            np.add.at(bi, ii, lr * (e - reg * bi[ii]))
            dP = lr * (e[:, None] * Q[ii] - reg * P[uu])
            dQ = lr * (e[:, None] * P[uu] - reg * Q[ii])
            np.add.at(P, uu, dP)
            np.add.at(Q, ii, dQ)
        if kuzat is not None and ep in kuzat:
            tarix.append((ep, rmse(tr, (mu, bu, bi, P, Q)), rmse(kuzat[ep], (mu, bu, bi, P, Q))))
    return (mu, bu, bi, P, Q), tarix


def bashorat(model, u, i):
    mu, bu, bi, P, Q = model
    return np.clip(mu + bu[u] + bi[i] + np.sum(P[u] * Q[i], 1), 1, 5)


def rmse(qism, model):
    return float(np.sqrt(np.mean((qism["baho"].to_numpy()
                                  - bashorat(model, qism["user"].to_numpy(),
                                             qism["item"].to_numpy())) ** 2)))


def main() -> None:
    df, Q_haqiqiy = baholar()
    n_user, n_item = 1500, 600
    tr, te = bolish_baho(df)
    print("=== 1. Ma'lumot ===")
    print(f"  baholar {len(df)}, zichlik {len(df) / (n_user * n_item):.1%}, "
          f"train {len(tr)}, test {len(te)} ({te['user'].nunique()} foyd.)")
    print(f"  o'rtacha baho {df['baho'].mean():.2f}, std {df['baho'].std():.2f}")

    u_te, i_te, r_te = te["user"].to_numpy(), te["item"].to_numpy(), te["baho"].to_numpy()
    xato = {}                                          # har test bahosi uchun kvadrat xato

    print("\n=== 2. Bazaviylar ===")
    mu = tr["baho"].mean()
    xato["global o'rtacha"] = (r_te - mu) ** 2
    mu, bu, bi = biaslar(tr, n_user, n_item)
    xato["biaslar"] = (r_te - np.clip(mu + bu[u_te] + bi[i_te], 1, 5)) ** 2
    for nom in xato:
        print(f"  {nom:<16} RMSE {np.sqrt(xato[nom].mean()):.4f}")

    print("\n=== 3. TruncatedSVD: bo'sh kataklar nol deb olinadi ===")
    R = sparse.csr_matrix((tr["baho"], (tr["user"], tr["item"])), shape=(n_user, n_item))
    for k in [4, 10]:
        svd = TruncatedSVD(k, random_state=0).fit(R)
        rek = svd.transform(R) @ svd.components_
        print(f"  xom (nol = baho 0), k={k:>2}: test bashorati o'rtachasi "
              f"{rek[u_te, i_te].mean():.2f}, RMSE {np.sqrt(np.mean((r_te - rek[u_te, i_te]) ** 2)):.4f}")
    qoldiq = tr["baho"] - mu - bu[tr["user"]] - bi[tr["item"]]
    Rq = sparse.csr_matrix((qoldiq, (tr["user"], tr["item"])), shape=(n_user, n_item))
    for k in [4, 10]:
        svd = TruncatedSVD(k, random_state=0).fit(Rq)
        rek = svd.transform(Rq) @ svd.components_
        p = np.clip(mu + bu[u_te] + bi[i_te] + rek[u_te, i_te], 1, 5)
        xato[f"biaslar + SVD k={k}"] = (r_te - p) ** 2
        print(f"  biaslar + qoldiq SVD, k={k:>2}: RMSE {np.sqrt(xato[f'biaslar + SVD k={k}'].mean()):.4f}")

    print("\n=== 4. MF (SGD): regularizatsiya ===")
    kuzat = {ep: te for ep in [2, 5, 10, 20, 40]}
    for reg in [0.0, 0.05]:
        model, tarix = mf_sgd(tr, n_user, n_item, reg=reg, kuzat=kuzat)
        print(f"  reg={reg}: " + "  ".join(f"ep{e} {a:.3f}/{b:.3f}" for e, a, b in tarix))
        if reg > 0:
            xato["MF (SGD)"] = (r_te - bashorat(model, u_te, i_te)) ** 2
    print("  (train RMSE / test RMSE)")

    print("\n=== 5. Juftlashgan taqqoslash (SE foydalanuvchilar bo'yicha) ===")
    asos = pd.Series(xato["biaslar"]).groupby(u_te).mean()
    for nom in ["global o'rtacha", "biaslar + SVD k=4", "MF (SGD)"]:
        d = pd.Series(xato[nom]).groupby(u_te).mean() - asos
        se = d.std(ddof=1) / np.sqrt(len(d))
        print(f"  {nom:<18} - biaslar: MSE farqi {d.mean():+.4f}, SE {se:.4f}, "
              f"sezilarli: {abs(d.mean()) > 2 * se}")

    print("\n=== 6. Yashirin faktorlar haqiqiy tuzilmani tiklaydimi? ===")
    Q_mf = model[4]
    pop = np.bincount(tr["item"], minlength=n_item)
    m = pop >= 20
    r2 = LinearRegression().fit(Q_mf[m], Q_haqiqiy[m]).score(Q_mf[m], Q_haqiqiy[m])
    print(f"  {m.sum()} ta mahsulot (>= 20 baho): o'rganilgan 10 faktor haqiqiy "
          f"4 faktorning R^2 = {r2:.3f} qismini chiziqli tushuntiradi")
    r2_kam = LinearRegression().fit(Q_mf[~m], Q_haqiqiy[~m]).score(Q_mf[~m], Q_haqiqiy[~m])
    print(f"  {(~m).sum()} ta kam baholi mahsulot (< 20): R^2 = {r2_kam:.3f}")
    print("  ⭐ Faktorlar nomsiz, lekin ma'lumotdagi yashirin 'did o'qlari'ni topadi")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Ma'lumot ===
  baholar 59207, zichlik 6.6%, train 47377, test 11830 (1469 foyd.)
  o'rtacha baho 3.43, std 1.00

=== 2. Bazaviylar ===
  global o'rtacha  RMSE 0.9994
  biaslar          RMSE 0.8545

=== 3. TruncatedSVD: bo'sh kataklar nol deb olinadi ===
  xom (nol = baho 0), k= 4: test bashorati o'rtachasi 0.58, RMSE 3.0912
  xom (nol = baho 0), k=10: test bashorati o'rtachasi 0.49, RMSE 3.1733
  biaslar + qoldiq SVD, k= 4: RMSE 0.8137
  biaslar + qoldiq SVD, k=10: RMSE 0.8230

=== 4. MF (SGD): regularizatsiya ===
  reg=0.0: ep2 0.827/0.859  ep5 0.792/0.853  ep10 0.646/0.797  ep20 0.523/0.779  ep40 0.472/0.813
  reg=0.05: ep2 0.830/0.859  ep5 0.812/0.856  ep10 0.726/0.810  ep20 0.582/0.738  ep40 0.521/0.739
  (train RMSE / test RMSE)

=== 5. Juftlashgan taqqoslash (SE foydalanuvchilar bo'yicha) ===
  global o'rtacha    - biaslar: MSE farqi +0.2556, SE 0.0125, sezilarli: True
  biaslar + SVD k=4  - biaslar: MSE farqi -0.0442, SE 0.0018, sezilarli: True
  MF (SGD)           - biaslar: MSE farqi -0.1620, SE 0.0103, sezilarli: True

=== 6. Yashirin faktorlar haqiqiy tuzilmani tiklaydimi? ===
  511 ta mahsulot (>= 20 baho): o'rganilgan 10 faktor haqiqiy 4 faktorning R^2 = 0.846 qismini chiziqli tushuntiradi
  89 ta kam baholi mahsulot (< 20): R^2 = 0.505
  ⭐ Faktorlar nomsiz, lekin ma'lumotdagi yashirin 'did o'qlari'ni topadi

Natija tahlili.

1-bo'lim — 59207 baho, zichlik 6.6%; test — har foydalanuvchining tasodifiy 20% baholari. Reyting bashoratida tasodifiy bo'lish keng tarqalgan (vaqt bo'lmasa), lekin real loyihada bu yerda ham vaqt bo'yicha bo'lish afzal 28.4-bob.

2-bo'lim — bazaviylar. Global o'rtacha RMSE 0.9994 (baholar std siga teng — kutilgan), biaslar 0.8545. Faqat "qattiqqo'l foydalanuvchi" va "yaxshi mahsulot" ni hisobga olish xatoni sezilarli kamaytirdi.

3-bo'lim — asosiy tuzoq. Siyrak matritsaga to'g'ridan-to'g'ri TruncatedSVD: bo'sh kataklar "baho 0" deb olinadi, 93% kataklar nol — shuning uchun test bashoratlarining o'rtachasi atigi 0.58 (haqiqiy baholar 1-5!), RMSE 3.0912. k ni oshirish yordam bermaydi (k=10 da 3.1733). Biaslarni ayirib, qoldiqqa SVD qo'llash ancha yaxshi (0.8137) — endi nol "o'rtacha" degani, "yomon" emas. Lekin k=10 da natija yomonlashdi (0.8230): to'ldirilgan nollar ham "ma'lumot" sifatida yodlanadi.

4-bo'lim — MF (SGD), k = 10. Regularizatsiyasiz (reg = 0) train RMSE 0.472 gacha tushadi, test RMSE esa 20-epoxada 0.779 dan keyin o'sadi (0.813) — klassik overfitting. reg = 0.05 bilan test 0.739 da barqarorlashdi. Baholarga qo'shilgan shovqin (std 0.6) va yaxlitlash tufayli erishib bo'ladigan eng past RMSE taxminan 0.65 — model bu chegaraga yaqinlashyapti, lekin train/test orasidagi farq (0.521 / 0.739) regularizatsiyani yana sozlash mumkinligini ko'rsatadi (buni validatsiyada qilish kerak — 3-misoldagi kabi).

5-bo'lim — juftlashgan taqqoslash: har foydalanuvchining o'rtacha kvadrat xatosi (MSE), farq va SE foydalanuvchilar bo'yicha. MF biaslardan MSE bo'yicha -0.1620 yaxshi (SE 0.0103) — qoldiq SVD dan (-0.0442) uch barobardan ko'proq.

6-bo'lim — faktorlar "nomsiz", lekin haqiqiy tuzilmani topadi: yetarli bahoga ega (>= 20) 511 ta mahsulotda o'rganilgan 10 ta faktor haqiqiy 4 ta did o'qining R^2 = 0.846 qismini chiziqli tushuntiradi. Kam baholi mahsulotlarda esa atigi 0.505 — ma'lumot kam bo'lsa, regularizatsiya vektorni nolga tortadi. Bu dumdagi mahsulotlar uchun CF ning zaifligining yana bir ko'rinishi.

Misol 2 — Implicit feedback uchun ALS noldan

python
"""Implicit feedback uchun ALS noldan: ishonch, hamma kataklar va bazaviylarga qarshi."""

import numpy as np
import pandas as pd
from scipy import sparse

KATEGORIYA = {
    "elektronika": "telefon smartfon quloqchin zaryadlovchi noutbuk planshet "
                   "sichqoncha klaviatura kolonka televizor kamera soat",
    "kiyim": "koylak shim kurtka poyabzal krossovka shapka sharf palto "
             "kostyum futbolka yubka sviter",
    "kitob": "roman darslik detektiv sher tarix fantastika lugat ertak "
             "biografiya psixologiya biznes dasturlash",
    "oziq": "choy qahva asal yongoq shokolad guruch yog un makaron sharbat "
            "pechene ziravor",
    "gozallik": "krem atir shampun labbuyoq boyoq niqob sovun losyon taroq "
                "lak gel upa",
    "uy": "idish qozon choynak gilam parda yostiq chiroq sochiq pichoq tova "
          "vaza javon",
    "sport": "koptok gantel velosiped raketka forma gilamcha skakalka shlem "
             "sumka butsa trenajyor suzish",
    "bolalar": "oyinchoq konstruktor qogirchoq kolyaska taglik mashinka pazl "
               "albom rangli kiyimcha kitobcha samokat",
}
NARX = ["arzon", "orta", "premium"]
UMUMIY = "sifatli yangi chegirma original mashhur qulay kafolat tez yetkazish".split()


def dokon(seed=0, n_user=2000, n_item=800, kunlar=180):
    """Sintetik onlayn do'kon: yashirin didlar, mashhurlik dumi, yangi mahsulotlar."""
    rng = np.random.default_rng(seed)
    katlar = list(KATEGORIYA)
    n_kat, n_sub = len(katlar), 4
    sub = rng.integers(0, n_kat * n_sub, n_item)          # har mahsulot kichik guruhi
    kat = sub // n_sub
    narx = rng.choice(3, n_item, p=[0.5, 0.35, 0.15])
    sifat = rng.lognormal(0, 1.4, n_item)                  # mashhurlik dumi manbai
    chiqish = np.where(rng.random(n_item) < 0.7, 0,
                       rng.integers(0, kunlar - 20, n_item))
    tavsif = []
    for i in range(n_item):
        sozlar = KATEGORIYA[katlar[kat[i]]].split()
        guruh = sozlar[(sub[i] % n_sub) * 3:(sub[i] % n_sub) * 3 + 3]
        tanlov = list(rng.choice(guruh, 2, replace=False))
        tanlov.append(rng.choice([s for s in sozlar if s not in tanlov]))
        tanlov += [katlar[kat[i]], NARX[narx[i]], f"brend{rng.integers(0, 40)}"]
        tanlov += list(rng.choice(UMUMIY, 2, replace=False))
        tavsif.append(" ".join(tanlov))
    # foydalanuvchi didi: kategoriya x kichik guruh x narx
    kat_pref = rng.dirichlet(np.full(n_kat, 0.3), n_user)
    sub_pref = rng.dirichlet(np.full(n_sub, 0.5), (n_user, n_kat))
    narx_pref = rng.dirichlet(np.full(3, 1.0), n_user)
    did = (kat_pref[:, kat] * sub_pref[:, kat, sub % n_sub]
           * narx_pref[:, narx])                           # (user, item)
    did /= did.sum(1, keepdims=True)
    hafta = np.arange(0, kunlar, 7)
    yosh = np.clip((hafta[:, None] - chiqish[None, :]) / 7, 0, None)
    jalb = sifat * (0.6 + 1.2 * np.exp(-yosh / 6)) * (hafta[:, None] >= chiqish)
    qatorlar = []
    for u in range(n_user):
        n_u = int(np.clip(rng.lognormal(2.8, 0.8), 3, 150))
        vaqtlar = np.sort(rng.uniform(0, kunlar, n_u))
        korgan = np.zeros(n_item, dtype=bool)
        for t in vaqtlar:
            a = jalb[int(t) // 7] * ~korgan
            p = 0.75 * did[u] * a / (did[u] @ a) + 0.25 * a / a.sum()
            i = int(np.searchsorted(np.cumsum(p), rng.random() * p.sum()))
            korgan[i] = True
            mos = did[u, i] / did[u].max()
            xarid = rng.random() < 0.15 + 0.35 * mos
            baho = 0
            if xarid and rng.random() < 0.3:
                baho = int(np.clip(round(2.4 + 2.2 * mos + 0.3 * np.log(sifat[i])
                                         + rng.normal(0, 0.7)), 1, 5))
            qatorlar.append((u, i, t, int(xarid), baho))
    df = pd.DataFrame(qatorlar, columns=["user", "item", "vaqt", "xarid", "baho"])
    items = pd.DataFrame({"item": np.arange(n_item), "kategoriya": np.array(katlar)[kat],
                          "sub": sub, "narx": narx, "chiqish": chiqish,
                          "tavsif": tavsif})
    return df, items


def bolish(df, usul="vaqt", ulush=0.2, seed=0, chegara=150.0):
    """'vaqt' - har foydalanuvchining oxirgi 20% i test; 'tasodifiy'; 'global'."""
    df = df.sort_values(["user", "vaqt"]).reset_index(drop=True)
    if usul == "global":
        tarix = df[df["vaqt"] < chegara].groupby("user").size()
        yetarli = df["user"].map(tarix).fillna(0).to_numpy() >= 3
        test = (df["vaqt"] >= chegara).to_numpy() & yetarli
        return df[df["vaqt"] < chegara].copy(), df[test].copy()
    n = df.groupby("user")["item"].transform("size").to_numpy()
    if usul == "tasodifiy":
        kalit = np.random.default_rng(seed).random(len(df))
        tartib = (df.assign(k=kalit).groupby("user")["k"]
                  .rank(method="first").to_numpy() - 1)
    else:
        tartib = df.groupby("user").cumcount().to_numpy()
    n_test = np.maximum(1, np.round(ulush * n)).astype(int)
    test = (tartib >= n - n_test) & (n >= 5)
    return df[~test].copy(), df[test].copy()


def matritsa(qism, n_user, n_item):
    return sparse.csr_matrix((np.ones(len(qism)), (qism["user"], qism["item"])),
                             shape=(n_user, n_item))


def metrikalar(top, T):
    """top: (n, k) tavsiya indekslari; T: (n, n_item) test to'plami (bool)."""
    k = top.shape[1]
    rel = np.take_along_axis(T, top, axis=1)
    n_rel = T.sum(1)
    pozitsiya = np.arange(1, k + 1)
    ap = (np.cumsum(rel, 1) / pozitsiya * rel).sum(1) / np.minimum(n_rel, k)
    chegirma = 1 / np.log2(pozitsiya + 1)
    idcg = np.array([chegirma[:min(r, k)].sum() for r in n_rel])
    return {"precision": rel.sum(1) / k, "recall": rel.sum(1) / n_rel,
            "hit": rel.any(1).astype(float), "MAP": ap,
            "nDCG": (rel * chegirma).sum(1) / idcg}


def baholash(ball, X_train, test, users, k=10):
    """Train da ko'rilgan mahsulotlar tavsiya qilinmaydi; har foydalanuvchi metrikasi."""
    ball = np.array(ball, dtype=float)
    ball[X_train[users].toarray() > 0] = -np.inf
    top = np.argsort(-ball, axis=1, kind="stable")[:, :k]
    qator = pd.Series(np.arange(len(users)), index=users)
    tt = test[test["user"].isin(users)]
    T = np.zeros(ball.shape, dtype=bool)
    T[qator[tt["user"]].to_numpy(), tt["item"].to_numpy()] = True
    return metrikalar(top, T), top


def als_qadam(R, Y, lam, alfa):
    """Hamma qatorlar uchun bir yo'la: (Y^T Y + Y^T (C_u - I) Y + lam*I) x_u = Y^T C_u p_u.

    Y^T Y bitta marta hisoblanadi va HAMMA kataklarni (nollarni ham, c = 1 bilan)
    qamraydi; qo'shimcha had sum_i (c_ui - 1) y_i y_i^T faqat nol bo'lmagan
    kataklar ustida - siyrak matritsa x "tashqi ko'paytmalar" jadvali.
    """
    k = Y.shape[1]
    YtY = Y.T @ Y + lam * np.eye(k)
    tashqi = (Y[:, :, None] * Y[:, None, :]).reshape(len(Y), k * k)
    W = R.copy()
    W.data = alfa * R.data                           # c - 1
    A = YtY + (W @ tashqi).reshape(-1, k, k)
    W.data = 1 + alfa * R.data                       # c (p = 1 kuzatilganda)
    return np.linalg.solve(A, (W @ Y)[:, :, None])[:, :, 0]


def als(R, k=32, lam=10.0, alfa=10.0, iters=10, seed=0):
    """Implicit ALS (Hu, Koren, Volinsky 2008): ishonch c = 1 + alfa * r."""
    rng = np.random.default_rng(seed)
    R = R.tocsr()
    Rt = R.T.tocsr()
    V = rng.normal(0, 0.1, (R.shape[1], k))
    for _ in range(iters):
        U = als_qadam(R, V, lam, alfa)
        V = als_qadam(Rt, U, lam, alfa)
    return U, V


def kuch_matritsa(qism, n_user, n_item):
    """r = 1 (ko'rish) yoki 3 (xarid) - implicit signal kuchi."""
    return sparse.csr_matrix((1.0 + 2.0 * qism["xarid"].to_numpy(),
                              (qism["user"], qism["item"])), shape=(n_user, n_item))


def item_knn(X, users, k=50):
    norma = np.sqrt(np.asarray(X.multiply(X).sum(0)).ravel())
    Xn = X @ sparse.diags(1 / np.maximum(norma, 1e-12))
    S = (Xn.T @ Xn).toarray()
    np.fill_diagonal(S, 0)
    kesish = np.argpartition(-S, k, axis=1)[:, k:]
    np.put_along_axis(S, kesish, 0, axis=1)
    return X[users] @ S


def als_faqat_kuzatilgan(R, k=32, lam=10.0, alfa=1.0, iters=8, seed=0):
    """XATO variant: faqat kuzatilgan kataklar (nollar tashlab ketilgan)."""
    def qadam(R, Y):
        tashqi = (Y[:, :, None] * Y[:, None, :]).reshape(len(Y), k * k)
        W = R.copy()
        W.data = 1 + alfa * R.data
        A = (W @ tashqi).reshape(-1, k, k) + lam * np.eye(k)   # Y^T Y YO'Q
        return np.linalg.solve(A, (W @ Y)[:, :, None])[:, :, 0]
    rng = np.random.default_rng(seed)
    R, Rt = R.tocsr(), R.T.tocsr()
    V = rng.normal(0, 0.1, (R.shape[1], k))
    for _ in range(iters):
        U = qadam(R, V)
        V = qadam(Rt, U)
    return U, V


def yoqotish(R, U, V, lam, alfa):
    """sum_{hamma u,i} c_ui (p_ui - u.v)^2 + lam (|U|^2 + |V|^2)."""
    D = R.toarray()
    P, C = (D > 0).astype(float), 1 + alfa * D
    return float((C * (P - U @ V.T) ** 2).sum() + lam * ((U ** 2).sum() + (V ** 2).sum()))


def main() -> None:
    df, items = dokon()
    n_user, n_item = 2000, len(items)
    tr, te = bolish(df, "vaqt")
    X = matritsa(tr, n_user, n_item)
    R = kuch_matritsa(tr, n_user, n_item)
    users = np.sort(te["user"].unique())
    lam, alfa = 10.0, 1.0                            # boshlang'ich qiymatlar

    print("=== 1. Tezkor formula = to'liq hisob ===")
    V = np.random.default_rng(0).normal(0, 0.3, (n_item, 32))
    tez = als_qadam(R, V, lam, alfa)
    D = R.toarray()
    for u in [0, 1, 2]:
        c = 1 + alfa * D[u]                          # HAMMA 800 katak
        p = (D[u] > 0).astype(float)
        toliq = np.linalg.solve((V.T * c) @ V + lam * np.eye(32), V.T @ (c * p))
        print(f"  foydalanuvchi {u}: kuzatilgan {int(p.sum()):>2} ta, to'liq hisob "
              f"{n_item} ta katak; bir xil (1e-10): {np.allclose(toliq, tez[u], atol=1e-10)}")

    print("\n=== 2. O'qitish: yo'qotish har iteratsiyada kamayadi ===")
    rng = np.random.default_rng(0)
    Rt = R.T.tocsr()
    V = rng.normal(0, 0.1, (n_item, 32))
    for it in range(1, 9):
        U = als_qadam(R, V, lam, alfa)
        V = als_qadam(Rt, U, lam, alfa)
        if it in (1, 2, 3, 5, 8):
            print(f"  iteratsiya {it:>2}: yo'qotish {yoqotish(R, U, V, lam, alfa):,.0f}")

    print("\n=== 3. Taqqoslash ('vaqt' bo'lishi, k=10) ===")
    pop = np.asarray(X.sum(0)).ravel()
    usullar = {"mashhurlik": np.tile(pop, (len(users), 1)).astype(float),
               "item-kNN": item_knn(X, users), "ALS (k=32)": U[users] @ V.T}
    natija = {}
    print(f"  {'usul':<12}" + "".join(f"{k:>10}" for k in
                                     ["precision", "recall", "hit", "MAP", "nDCG"]))
    for nom, b in usullar.items():
        natija[nom], _ = baholash(b, X, te, users)
        print(f"  {nom:<12}" + "".join(f"{v.mean():>10.4f}"
                                        for v in natija[nom].values()))

    print("\n=== 4. Juftlashgan taqqoslash (nDCG@10, foydalanuvchilar bo'yicha) ===")
    for a, b in [("ALS (k=32)", "mashhurlik"), ("ALS (k=32)", "item-kNN")]:
        d = natija[a]["nDCG"] - natija[b]["nDCG"]
        se = d.std(ddof=1) / np.sqrt(len(d))
        print(f"  {a} - {b}: {d.mean():+.4f}, SE {se:.4f}, "
              f"sezilarli: {abs(d.mean()) > 2 * se}")

    print("\n=== 5. Nega nollar kerak: to'rt variant ===")
    bosh = pop >= np.quantile(pop, 0.9)
    variantlar = {"faqat kuzatilgan": als_faqat_kuzatilgan(R),
                  "alfa=0 (hamma teng)": als(R, alfa=0.0, iters=8),
                  "alfa=1": (U, V),
                  "alfa=10": als(R, alfa=10.0, iters=8)}
    for nom, (U2, V2) in variantlar.items():
        r, top = baholash(U2[users] @ V2.T, X, te, users)
        print(f"  {nom:<19} nDCG {r['nDCG'].mean():.4f}, "
              f"tavsiyalarda top10% ulushi {bosh[top].mean():.1%}, "
              f"qamrov {len(np.unique(top)) / n_item:.1%}")
    print("  ⭐ Bo'sh kataklar 'kuchsiz salbiy' sifatida ishtirok etishi shart")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Tezkor formula = to'liq hisob ===
  foydalanuvchi 0: kuzatilgan 10 ta, to'liq hisob 800 ta katak; bir xil (1e-10): True
  foydalanuvchi 1: kuzatilgan  4 ta, to'liq hisob 800 ta katak; bir xil (1e-10): True
  foydalanuvchi 2: kuzatilgan  7 ta, to'liq hisob 800 ta katak; bir xil (1e-10): True

=== 2. O'qitish: yo'qotish har iteratsiyada kamayadi ===
  iteratsiya  1: yo'qotish 72,626
  iteratsiya  2: yo'qotish 61,889
  iteratsiya  3: yo'qotish 60,145
  iteratsiya  5: yo'qotish 59,152
  iteratsiya  8: yo'qotish 58,783

=== 3. Taqqoslash ('vaqt' bo'lishi, k=10) ===
  usul         precision    recall       hit       MAP      nDCG
  mashhurlik      0.0383    0.0924    0.3203    0.0351    0.0706
  item-kNN        0.0508    0.1234    0.3954    0.0501    0.0961
  ALS (k=32)      0.0598    0.1408    0.4378    0.0638    0.1174

=== 4. Juftlashgan taqqoslash (nDCG@10, foydalanuvchilar bo'yicha) ===
  ALS (k=32) - mashhurlik: +0.0468, SE 0.0048, sezilarli: True
  ALS (k=32) - item-kNN: +0.0213, SE 0.0040, sezilarli: True

=== 5. Nega nollar kerak: to'rt variant ===
  faqat kuzatilgan    nDCG 0.0687, tavsiyalarda top10% ulushi 100.0%, qamrov 5.1%
  alfa=0 (hamma teng) nDCG 0.1167, tavsiyalarda top10% ulushi 83.7%, qamrov 26.5%
  alfa=1              nDCG 0.1174, tavsiyalarda top10% ulushi 69.0%, qamrov 36.0%
  alfa=10             nDCG 0.0833, tavsiyalarda top10% ulushi 60.6%, qamrov 50.9%
  ⭐ Bo'sh kataklar 'kuchsiz salbiy' sifatida ishtirok etishi shart

Natija tahlili.

1-bo'lim — 2.4-bo'limdagi hiyla to'g'ri: foydalanuvchi 4-10 ta mahsulot ko'rgan, tezkor formula faqat shularni aylanadi, lekin natija barcha 800 katakni (nollarni c = 1 bilan) ishtirok ettirgan to'liq hisob bilan bir xil. als_qadam bundan tashqari hamma foydalanuvchini bir yo'la yechadi: (c - 1) * y y^T tashqi ko'paytmalar siyrak matritsa ko'paytmasi bilan yig'iladi va np.linalg.solve 2000 ta kichik tizimni birdaniga hal qiladi.

2-bo'lim — har ALS qadami o'z qismi bo'yicha aniq minimum topadi, shuning uchun to'liq yo'qotish (hamma 1.6 mln katak ustida) monoton kamayadi: 72,626 → 58,783. 5-8-iteratsiyalardan keyin o'zgarish kichik — amalda 10-20 iteratsiya yetadi.

3-bo'lim — bir xil bo'lish, bir xil metrikalar. ALS (k = 32, lambda = 10, alfa = 1 — boshlang'ich qiymatlar) nDCG@10 0.1174, recall 0.1408, hit 0.4378. Bu 28.4-darsdagi eng yaxshi natijadan (user-kNN 0.1070) ham yuqori.

4-bo'lim — ALS mashhurlikdan +0.0468 (SE 0.0048), item-kNN dan +0.0213 (SE 0.0040) yaxshi — ikkala farq ham sezilarli.

5-bo'lim — darsning asosiy fikri raqamda. Faqat kuzatilgan kataklar bilan o'qitilgan model nDCG 0.0687 — mashhurlikdan (0.0706) ham yomon, tavsiyalarining 100% i eng mashhur 10% mahsulotdan, qamrov 5.1%: nollarsiz model "hamma narsa yoqadi" deb o'rganadi va natijada faqat vektor normasi katta (mashhur) mahsulotlarni chiqaradi. alfa = 0 (hamma katak teng) va alfa = 1 deyarli bir xil (0.1167 va 0.1174) — bizning ma'lumotda ishonch vazni aniqlikka kam ta'sir qildi, lekin qamrovni oshirdi (26.5% → 36.0%). alfa = 10 esa zararli (0.0833): model ko'rilgan mahsulotlarni haddan tashqari ishonch bilan yodlaydi. alfa — sozlanadigan parametr, "Hu va boshq. 40 ishlatgan" degan qiymat bizning ma'lumotga mos emas.

Misol 3 — Giperparametrlarni validatsiyada tanlash va BPR

python
"""Giperparametrlarni validatsiyada tanlash (ALS) va BPR (torch) bilan taqqoslash."""

import numpy as np
import pandas as pd
import torch
from scipy import sparse

KATEGORIYA = {
    "elektronika": "telefon smartfon quloqchin zaryadlovchi noutbuk planshet "
                   "sichqoncha klaviatura kolonka televizor kamera soat",
    "kiyim": "koylak shim kurtka poyabzal krossovka shapka sharf palto "
             "kostyum futbolka yubka sviter",
    "kitob": "roman darslik detektiv sher tarix fantastika lugat ertak "
             "biografiya psixologiya biznes dasturlash",
    "oziq": "choy qahva asal yongoq shokolad guruch yog un makaron sharbat "
            "pechene ziravor",
    "gozallik": "krem atir shampun labbuyoq boyoq niqob sovun losyon taroq "
                "lak gel upa",
    "uy": "idish qozon choynak gilam parda yostiq chiroq sochiq pichoq tova "
          "vaza javon",
    "sport": "koptok gantel velosiped raketka forma gilamcha skakalka shlem "
             "sumka butsa trenajyor suzish",
    "bolalar": "oyinchoq konstruktor qogirchoq kolyaska taglik mashinka pazl "
               "albom rangli kiyimcha kitobcha samokat",
}
NARX = ["arzon", "orta", "premium"]
UMUMIY = "sifatli yangi chegirma original mashhur qulay kafolat tez yetkazish".split()


def dokon(seed=0, n_user=2000, n_item=800, kunlar=180):
    """Sintetik onlayn do'kon: yashirin didlar, mashhurlik dumi, yangi mahsulotlar."""
    rng = np.random.default_rng(seed)
    katlar = list(KATEGORIYA)
    n_kat, n_sub = len(katlar), 4
    sub = rng.integers(0, n_kat * n_sub, n_item)          # har mahsulot kichik guruhi
    kat = sub // n_sub
    narx = rng.choice(3, n_item, p=[0.5, 0.35, 0.15])
    sifat = rng.lognormal(0, 1.4, n_item)                  # mashhurlik dumi manbai
    chiqish = np.where(rng.random(n_item) < 0.7, 0,
                       rng.integers(0, kunlar - 20, n_item))
    tavsif = []
    for i in range(n_item):
        sozlar = KATEGORIYA[katlar[kat[i]]].split()
        guruh = sozlar[(sub[i] % n_sub) * 3:(sub[i] % n_sub) * 3 + 3]
        tanlov = list(rng.choice(guruh, 2, replace=False))
        tanlov.append(rng.choice([s for s in sozlar if s not in tanlov]))
        tanlov += [katlar[kat[i]], NARX[narx[i]], f"brend{rng.integers(0, 40)}"]
        tanlov += list(rng.choice(UMUMIY, 2, replace=False))
        tavsif.append(" ".join(tanlov))
    # foydalanuvchi didi: kategoriya x kichik guruh x narx
    kat_pref = rng.dirichlet(np.full(n_kat, 0.3), n_user)
    sub_pref = rng.dirichlet(np.full(n_sub, 0.5), (n_user, n_kat))
    narx_pref = rng.dirichlet(np.full(3, 1.0), n_user)
    did = (kat_pref[:, kat] * sub_pref[:, kat, sub % n_sub]
           * narx_pref[:, narx])                           # (user, item)
    did /= did.sum(1, keepdims=True)
    hafta = np.arange(0, kunlar, 7)
    yosh = np.clip((hafta[:, None] - chiqish[None, :]) / 7, 0, None)
    jalb = sifat * (0.6 + 1.2 * np.exp(-yosh / 6)) * (hafta[:, None] >= chiqish)
    qatorlar = []
    for u in range(n_user):
        n_u = int(np.clip(rng.lognormal(2.8, 0.8), 3, 150))
        vaqtlar = np.sort(rng.uniform(0, kunlar, n_u))
        korgan = np.zeros(n_item, dtype=bool)
        for t in vaqtlar:
            a = jalb[int(t) // 7] * ~korgan
            p = 0.75 * did[u] * a / (did[u] @ a) + 0.25 * a / a.sum()
            i = int(np.searchsorted(np.cumsum(p), rng.random() * p.sum()))
            korgan[i] = True
            mos = did[u, i] / did[u].max()
            xarid = rng.random() < 0.15 + 0.35 * mos
            baho = 0
            if xarid and rng.random() < 0.3:
                baho = int(np.clip(round(2.4 + 2.2 * mos + 0.3 * np.log(sifat[i])
                                         + rng.normal(0, 0.7)), 1, 5))
            qatorlar.append((u, i, t, int(xarid), baho))
    df = pd.DataFrame(qatorlar, columns=["user", "item", "vaqt", "xarid", "baho"])
    items = pd.DataFrame({"item": np.arange(n_item), "kategoriya": np.array(katlar)[kat],
                          "sub": sub, "narx": narx, "chiqish": chiqish,
                          "tavsif": tavsif})
    return df, items


def bolish(df, usul="vaqt", ulush=0.2, seed=0, chegara=150.0):
    """'vaqt' - har foydalanuvchining oxirgi 20% i test; 'tasodifiy'; 'global'."""
    df = df.sort_values(["user", "vaqt"]).reset_index(drop=True)
    if usul == "global":
        tarix = df[df["vaqt"] < chegara].groupby("user").size()
        yetarli = df["user"].map(tarix).fillna(0).to_numpy() >= 3
        test = (df["vaqt"] >= chegara).to_numpy() & yetarli
        return df[df["vaqt"] < chegara].copy(), df[test].copy()
    n = df.groupby("user")["item"].transform("size").to_numpy()
    if usul == "tasodifiy":
        kalit = np.random.default_rng(seed).random(len(df))
        tartib = (df.assign(k=kalit).groupby("user")["k"]
                  .rank(method="first").to_numpy() - 1)
    else:
        tartib = df.groupby("user").cumcount().to_numpy()
    n_test = np.maximum(1, np.round(ulush * n)).astype(int)
    test = (tartib >= n - n_test) & (n >= 5)
    return df[~test].copy(), df[test].copy()


def matritsa(qism, n_user, n_item):
    return sparse.csr_matrix((np.ones(len(qism)), (qism["user"], qism["item"])),
                             shape=(n_user, n_item))


def metrikalar(top, T):
    """top: (n, k) tavsiya indekslari; T: (n, n_item) test to'plami (bool)."""
    k = top.shape[1]
    rel = np.take_along_axis(T, top, axis=1)
    n_rel = T.sum(1)
    pozitsiya = np.arange(1, k + 1)
    ap = (np.cumsum(rel, 1) / pozitsiya * rel).sum(1) / np.minimum(n_rel, k)
    chegirma = 1 / np.log2(pozitsiya + 1)
    idcg = np.array([chegirma[:min(r, k)].sum() for r in n_rel])
    return {"precision": rel.sum(1) / k, "recall": rel.sum(1) / n_rel,
            "hit": rel.any(1).astype(float), "MAP": ap,
            "nDCG": (rel * chegirma).sum(1) / idcg}


def baholash(ball, X_train, test, users, k=10):
    """Train da ko'rilgan mahsulotlar tavsiya qilinmaydi; har foydalanuvchi metrikasi."""
    ball = np.array(ball, dtype=float)
    ball[X_train[users].toarray() > 0] = -np.inf
    top = np.argsort(-ball, axis=1, kind="stable")[:, :k]
    qator = pd.Series(np.arange(len(users)), index=users)
    tt = test[test["user"].isin(users)]
    T = np.zeros(ball.shape, dtype=bool)
    T[qator[tt["user"]].to_numpy(), tt["item"].to_numpy()] = True
    return metrikalar(top, T), top


def als_qadam(R, Y, lam, alfa):
    """Hamma qatorlar uchun bir yo'la: (Y^T Y + Y^T (C_u - I) Y + lam*I) x_u = Y^T C_u p_u.

    Y^T Y bitta marta hisoblanadi va HAMMA kataklarni (nollarni ham, c = 1 bilan)
    qamraydi; qo'shimcha had sum_i (c_ui - 1) y_i y_i^T faqat nol bo'lmagan
    kataklar ustida - siyrak matritsa x "tashqi ko'paytmalar" jadvali.
    """
    k = Y.shape[1]
    YtY = Y.T @ Y + lam * np.eye(k)
    tashqi = (Y[:, :, None] * Y[:, None, :]).reshape(len(Y), k * k)
    W = R.copy()
    W.data = alfa * R.data                           # c - 1
    A = YtY + (W @ tashqi).reshape(-1, k, k)
    W.data = 1 + alfa * R.data                       # c (p = 1 kuzatilganda)
    return np.linalg.solve(A, (W @ Y)[:, :, None])[:, :, 0]


def als(R, k=32, lam=10.0, alfa=10.0, iters=10, seed=0):
    """Implicit ALS (Hu, Koren, Volinsky 2008): ishonch c = 1 + alfa * r."""
    rng = np.random.default_rng(seed)
    R = R.tocsr()
    Rt = R.T.tocsr()
    V = rng.normal(0, 0.1, (R.shape[1], k))
    for _ in range(iters):
        U = als_qadam(R, V, lam, alfa)
        V = als_qadam(Rt, U, lam, alfa)
    return U, V


def kuch_matritsa(qism, n_user, n_item):
    """r = 1 (ko'rish) yoki 3 (xarid) - implicit signal kuchi."""
    return sparse.csr_matrix((1.0 + 2.0 * qism["xarid"].to_numpy(),
                              (qism["user"], qism["item"])), shape=(n_user, n_item))


def item_knn(X, users, k=50):
    norma = np.sqrt(np.asarray(X.multiply(X).sum(0)).ravel())
    Xn = X @ sparse.diags(1 / np.maximum(norma, 1e-12))
    S = (Xn.T @ Xn).toarray()
    np.fill_diagonal(S, 0)
    kesish = np.argpartition(-S, k, axis=1)[:, k:]
    np.put_along_axis(S, kesish, 0, axis=1)
    return X[users] @ S


def bpr(X, k=32, epoxlar=16, lr=0.05, reg=1e-5, partiya=2048, seed=0, kuzat=None):
    """BPR: har (u, i+) juftligiga tasodifiy j- olinadi; loss = -log sigmoid(x_ui - x_uj)."""
    torch.manual_seed(seed)
    torch.set_flush_denormal(True)       # Adam dagi juda kichik sonlar CPU ni sekinlatmasin
    g = torch.Generator().manual_seed(seed)
    n_user, n_item = X.shape
    U = torch.nn.Embedding(n_user, k)
    V = torch.nn.Embedding(n_item, k)
    b = torch.nn.Embedding(n_item, 1)
    for e in (U, V):
        torch.nn.init.normal_(e.weight, 0, 0.1)
    torch.nn.init.zeros_(b.weight)
    opt = torch.optim.Adam([*U.parameters(), *V.parameters(), *b.parameters()], lr=lr)
    Xc = X.tocoo()
    uu, ii = torch.tensor(Xc.row, dtype=torch.long), torch.tensor(Xc.col, dtype=torch.long)
    tarix = {}
    for ep in range(1, epoxlar + 1):
        tartib = torch.randperm(len(uu), generator=g)
        jj_hammasi = torch.randint(0, n_item, (len(uu),), generator=g)
        for s in range(0, len(uu), partiya):
            idx = tartib[s:s + partiya]
            u, i, j = uu[idx], ii[idx], jj_hammasi[idx]
            pu = U(u)
            x_ui = (pu * V(i)).sum(1) + b(i).squeeze(1)
            x_uj = (pu * V(j)).sum(1) + b(j).squeeze(1)
            loss = (-torch.nn.functional.logsigmoid(x_ui - x_uj).mean()
                    + reg * (pu.pow(2).sum() + V(i).pow(2).sum() + V(j).pow(2).sum()))
            opt.zero_grad()
            loss.backward()
            opt.step()
        if kuzat is not None and ep in kuzat:
            tarix[ep] = kuzat[ep](bpr_ball(U, V, b))
    return (U, V, b), tarix


def bpr_ball(U, V, b):
    with torch.no_grad():
        return (U.weight @ V.weight.T + b.weight.T).numpy()


def main() -> None:
    df, items = dokon()
    n_user, n_item = 2000, len(items)
    tr, te = bolish(df, "vaqt")
    tr2, va = bolish(tr, "vaqt")                   # validatsiya - train ICHIDAN
    X2, R2 = matritsa(tr2, n_user, n_item), kuch_matritsa(tr2, n_user, n_item)
    u_va = np.sort(va["user"].unique())

    def val_ndcg(ball_hammasi):
        r, _ = baholash(ball_hammasi[u_va], X2, va, u_va)
        return r["nDCG"]

    print("=== 1. ALS: k va lambda validatsiyada (alfa = 1) ===")
    natija = {}
    for k in [8, 16, 32]:
        for lam in [3.0, 10.0, 30.0]:
            U, V = als(R2, k=k, lam=lam, alfa=1.0, iters=6)
            natija[(k, lam, 1.0)] = val_ndcg(U @ V.T)
    eng = max(natija, key=lambda c: natija[c].mean())
    print(f"  {'k':>4}" + "".join(f"{f'lambda={l:g}':>12}" for l in [3, 10, 30]))
    for k in [8, 16, 32]:
        print(f"  {k:>4}" + "".join(f"{natija[(k, l, 1.0)].mean():>12.4f}"
                                    for l in [3.0, 10.0, 30.0]))
    print(f"  eng yaxshi: k={eng[0]}, lambda={eng[1]:g}")

    print("\n=== 2. alfa (eng yaxshi k va lambda bilan) ===")
    for alfa in [0.0, 3.0]:
        U, V = als(R2, k=eng[0], lam=eng[1], alfa=alfa, iters=6)
        natija[(eng[0], eng[1], alfa)] = val_ndcg(U @ V.T)
    for alfa in [0.0, 1.0, 3.0]:
        print(f"  alfa={alfa:g}: val nDCG {natija[(eng[0], eng[1], alfa)].mean():.4f}")
    eng = max(natija, key=lambda c: natija[c].mean())

    print("\n=== 3. Qaror: eng yaxshisidan sezilarli yomon bo'lmagan eng sodda ===")
    mos = []
    for c, v in natija.items():
        d = v - natija[eng]
        if c == eng or d.mean() >= -2 * d.std(ddof=1) / np.sqrt(len(d)):
            mos.append(c)
    tanlov = min(mos, key=lambda c: (c[0], -c[1]))    # kichik k, katta lambda
    print(f"  eng yaxshi {eng}, sezilarli yomon bo'lmaganlar: {len(mos)} ta")
    print(f"  tanlov (k, lambda, alfa) = {tanlov}")

    print("\n=== 4. BPR: epoxalar soni validatsiyada ===")
    kuzat = {ep: (lambda b: float(val_ndcg(b).mean())) for ep in [4, 8, 12, 16]}
    _, tarix = bpr(X2, kuzat=kuzat)
    print("  " + ", ".join(f"ep{e}: {v:.4f}" for e, v in tarix.items()))
    ep_eng = max(tarix, key=tarix.get)
    print(f"  tanlov: {ep_eng} epoxa")

    print("\n=== 5. TEST: tanlangan sozlamalar butun train da qayta o'qitiladi ===")
    X, R = matritsa(tr, n_user, n_item), kuch_matritsa(tr, n_user, n_item)
    users = np.sort(te["user"].unique())
    U, V = als(R, k=tanlov[0], lam=tanlov[1], alfa=tanlov[2], iters=6)
    model, _ = bpr(X, epoxlar=ep_eng)
    pop = np.asarray(X.sum(0)).ravel()
    usullar = {"mashhurlik": np.tile(pop, (len(users), 1)).astype(float),
               "item-kNN": item_knn(X, users), "ALS": U[users] @ V.T,
               "BPR": bpr_ball(*model)[users]}
    test = {}
    for nom, b in usullar.items():
        test[nom], _ = baholash(b, X, te, users)
    eng_t = max(test, key=lambda n: test[n]["nDCG"].mean())
    for nom in usullar:
        d = test[nom]["nDCG"] - test[eng_t]["nDCG"]
        se = d.std(ddof=1) / np.sqrt(len(d))
        print(f"  {nom:<11} nDCG {test[nom]['nDCG'].mean():.4f}, recall "
              f"{test[nom]['recall'].mean():.4f}; - {eng_t}: {d.mean():+.4f} (SE {se:.4f})")
    print("  ⭐ Sozlash - validatsiyada; test - faqat bir marta, yakunda")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. ALS: k va lambda validatsiyada (alfa = 1) ===
     k    lambda=3   lambda=10   lambda=30
     8      0.0968      0.0961      0.0917
    16      0.1028      0.1039      0.1014
    32      0.1026      0.1087      0.1054
  eng yaxshi: k=32, lambda=10

=== 2. alfa (eng yaxshi k va lambda bilan) ===
  alfa=0: val nDCG 0.1090
  alfa=1: val nDCG 0.1087
  alfa=3: val nDCG 0.1006

=== 3. Qaror: eng yaxshisidan sezilarli yomon bo'lmagan eng sodda ===
  eng yaxshi (32, 10.0, 0.0), sezilarli yomon bo'lmaganlar: 4 ta
  tanlov (k, lambda, alfa) = (16, 10.0, 1.0)

=== 4. BPR: epoxalar soni validatsiyada ===
  ep4: 0.0635, ep8: 0.0699, ep12: 0.0698, ep16: 0.0696
  tanlov: 8 epoxa

=== 5. TEST: tanlangan sozlamalar butun train da qayta o'qitiladi ===
  mashhurlik  nDCG 0.0706, recall 0.0924; - ALS: -0.0412 (SE 0.0044)
  item-kNN    nDCG 0.0961, recall 0.1234; - ALS: -0.0156 (SE 0.0035)
  ALS         nDCG 0.1118, recall 0.1348; - ALS: +0.0000 (SE 0.0000)
  BPR         nDCG 0.0803, recall 0.1035; - ALS: -0.0314 (SE 0.0040)
  ⭐ Sozlash - validatsiyada; test - faqat bir marta, yakunda

Natija tahlili.

1-bo'lim — validatsiya train ichidan xuddi test kabi olindi (har foydalanuvchining train dagi oxirgi 20% i). k = 8 hamma lambda da pastroq (~0.092-0.097), k = 16 va k = 32 yaqin; eng yaxshisi k = 32, lambda = 10 (0.1087). lambda = 30 hamma k da biroz yomon — ortiqcha regularizatsiya vektorlarni mashhurlik yo'nalishiga siqadi.

2-bo'lim — alfa: 0 va 1 deyarli teng (0.1090 va 0.1087), 3 da yomonlashdi (0.1006) — 2-misoldagi manzara validatsiyada ham takrorlandi.

3-bo'lim — qaror qoidasi. Eng yaxshisi (32, 10, 0), lekin undan sezilarli yomon bo'lmagan 4 ta sozlama bor; ular ichida eng soddasi — k = 16, lambda = 10, alfa = 1. Ikki barobar kam parametr — tezroq o'qitish va xizmat, kamroq xotira — va farq shovqin chegarasida.

4-bo'lim — BPR (torch, k = 32, Adam). Validatsiya nDCG 8-epoxada eng yuqori (0.0699), keyin o'sishdan to'xtaydi — 8 epoxa tanlandi. Kodda torch.set_flush_denormal(True) bor: Adam ning kamdan-kam yangilanadigan qatorlaridagi juda kichik (subnormal) sonlar CPU hisobini bir necha barobar sekinlashtiradi; bu sozlama ularni nolga tenglashtiradi va natijaga ta'sir qilmaydi.

5-bo'lim — test, bir marta. Tanlangan ALS nDCG 0.1118 — 2-misoldagi "boshlang'ich" k = 32 (0.1174) dan biroz past: validatsiya bo'yicha tanlangan soddaroq model testda kichik narx to'ladi, lekin bu narx oldindan belgilangan qoida bilan ongli ravishda qabul qilindi (test ga qarab tanlash esa optimistik bo'lardi). ALS item-kNN dan 0.0156 (SE 0.0035) va mashhurlikdan 0.0412 yaxshi. BPR kutilgandan zaif: 0.0803 — mashhurlikdan biroz yaxshi, lekin item-kNN dan ham past. Sababi 2.5-bo'limda: bir xil tasodifiy salbiy namunalar asosan "oson" (mashhur bo'lmagan) mahsulotlar, model tezda mashhurlikni o'rganadi va shaxsiy farqlarga kam o'tadi. BPR kuchli bo'lishi uchun qiyin salbiy namunalar, ko'proq ma'lumot va puxta sozlash kerak — bu kichik do'konda ALS aniq yutdi. Nazariy jihatdan "to'g'ri" yo'qotish amalda avtomatik ravishda yaxshi model bermaydi.

Misol 4 — Sovuq start: yangi foydalanuvchi va yangi mahsulot

python
"""Sovuq start: yangi foydalanuvchi (fold-in, mashhurlik fallback) va yangi mahsulot (gibrid)."""

import numpy as np
import pandas as pd
from scipy import sparse
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import Ridge
from sklearn.metrics.pairwise import cosine_similarity

KATEGORIYA = {
    "elektronika": "telefon smartfon quloqchin zaryadlovchi noutbuk planshet "
                   "sichqoncha klaviatura kolonka televizor kamera soat",
    "kiyim": "koylak shim kurtka poyabzal krossovka shapka sharf palto "
             "kostyum futbolka yubka sviter",
    "kitob": "roman darslik detektiv sher tarix fantastika lugat ertak "
             "biografiya psixologiya biznes dasturlash",
    "oziq": "choy qahva asal yongoq shokolad guruch yog un makaron sharbat "
            "pechene ziravor",
    "gozallik": "krem atir shampun labbuyoq boyoq niqob sovun losyon taroq "
                "lak gel upa",
    "uy": "idish qozon choynak gilam parda yostiq chiroq sochiq pichoq tova "
          "vaza javon",
    "sport": "koptok gantel velosiped raketka forma gilamcha skakalka shlem "
             "sumka butsa trenajyor suzish",
    "bolalar": "oyinchoq konstruktor qogirchoq kolyaska taglik mashinka pazl "
               "albom rangli kiyimcha kitobcha samokat",
}
NARX = ["arzon", "orta", "premium"]
UMUMIY = "sifatli yangi chegirma original mashhur qulay kafolat tez yetkazish".split()


def dokon(seed=0, n_user=2000, n_item=800, kunlar=180):
    """Sintetik onlayn do'kon: yashirin didlar, mashhurlik dumi, yangi mahsulotlar."""
    rng = np.random.default_rng(seed)
    katlar = list(KATEGORIYA)
    n_kat, n_sub = len(katlar), 4
    sub = rng.integers(0, n_kat * n_sub, n_item)          # har mahsulot kichik guruhi
    kat = sub // n_sub
    narx = rng.choice(3, n_item, p=[0.5, 0.35, 0.15])
    sifat = rng.lognormal(0, 1.4, n_item)                  # mashhurlik dumi manbai
    chiqish = np.where(rng.random(n_item) < 0.7, 0,
                       rng.integers(0, kunlar - 20, n_item))
    tavsif = []
    for i in range(n_item):
        sozlar = KATEGORIYA[katlar[kat[i]]].split()
        guruh = sozlar[(sub[i] % n_sub) * 3:(sub[i] % n_sub) * 3 + 3]
        tanlov = list(rng.choice(guruh, 2, replace=False))
        tanlov.append(rng.choice([s for s in sozlar if s not in tanlov]))
        tanlov += [katlar[kat[i]], NARX[narx[i]], f"brend{rng.integers(0, 40)}"]
        tanlov += list(rng.choice(UMUMIY, 2, replace=False))
        tavsif.append(" ".join(tanlov))
    # foydalanuvchi didi: kategoriya x kichik guruh x narx
    kat_pref = rng.dirichlet(np.full(n_kat, 0.3), n_user)
    sub_pref = rng.dirichlet(np.full(n_sub, 0.5), (n_user, n_kat))
    narx_pref = rng.dirichlet(np.full(3, 1.0), n_user)
    did = (kat_pref[:, kat] * sub_pref[:, kat, sub % n_sub]
           * narx_pref[:, narx])                           # (user, item)
    did /= did.sum(1, keepdims=True)
    hafta = np.arange(0, kunlar, 7)
    yosh = np.clip((hafta[:, None] - chiqish[None, :]) / 7, 0, None)
    jalb = sifat * (0.6 + 1.2 * np.exp(-yosh / 6)) * (hafta[:, None] >= chiqish)
    qatorlar = []
    for u in range(n_user):
        n_u = int(np.clip(rng.lognormal(2.8, 0.8), 3, 150))
        vaqtlar = np.sort(rng.uniform(0, kunlar, n_u))
        korgan = np.zeros(n_item, dtype=bool)
        for t in vaqtlar:
            a = jalb[int(t) // 7] * ~korgan
            p = 0.75 * did[u] * a / (did[u] @ a) + 0.25 * a / a.sum()
            i = int(np.searchsorted(np.cumsum(p), rng.random() * p.sum()))
            korgan[i] = True
            mos = did[u, i] / did[u].max()
            xarid = rng.random() < 0.15 + 0.35 * mos
            baho = 0
            if xarid and rng.random() < 0.3:
                baho = int(np.clip(round(2.4 + 2.2 * mos + 0.3 * np.log(sifat[i])
                                         + rng.normal(0, 0.7)), 1, 5))
            qatorlar.append((u, i, t, int(xarid), baho))
    df = pd.DataFrame(qatorlar, columns=["user", "item", "vaqt", "xarid", "baho"])
    items = pd.DataFrame({"item": np.arange(n_item), "kategoriya": np.array(katlar)[kat],
                          "sub": sub, "narx": narx, "chiqish": chiqish,
                          "tavsif": tavsif})
    return df, items


def bolish(df, usul="vaqt", ulush=0.2, seed=0, chegara=150.0):
    """'vaqt' - har foydalanuvchining oxirgi 20% i test; 'tasodifiy'; 'global'."""
    df = df.sort_values(["user", "vaqt"]).reset_index(drop=True)
    if usul == "global":
        tarix = df[df["vaqt"] < chegara].groupby("user").size()
        yetarli = df["user"].map(tarix).fillna(0).to_numpy() >= 3
        test = (df["vaqt"] >= chegara).to_numpy() & yetarli
        return df[df["vaqt"] < chegara].copy(), df[test].copy()
    n = df.groupby("user")["item"].transform("size").to_numpy()
    if usul == "tasodifiy":
        kalit = np.random.default_rng(seed).random(len(df))
        tartib = (df.assign(k=kalit).groupby("user")["k"]
                  .rank(method="first").to_numpy() - 1)
    else:
        tartib = df.groupby("user").cumcount().to_numpy()
    n_test = np.maximum(1, np.round(ulush * n)).astype(int)
    test = (tartib >= n - n_test) & (n >= 5)
    return df[~test].copy(), df[test].copy()


def matritsa(qism, n_user, n_item):
    return sparse.csr_matrix((np.ones(len(qism)), (qism["user"], qism["item"])),
                             shape=(n_user, n_item))


def metrikalar(top, T):
    """top: (n, k) tavsiya indekslari; T: (n, n_item) test to'plami (bool)."""
    k = top.shape[1]
    rel = np.take_along_axis(T, top, axis=1)
    n_rel = T.sum(1)
    pozitsiya = np.arange(1, k + 1)
    ap = (np.cumsum(rel, 1) / pozitsiya * rel).sum(1) / np.minimum(n_rel, k)
    chegirma = 1 / np.log2(pozitsiya + 1)
    idcg = np.array([chegirma[:min(r, k)].sum() for r in n_rel])
    return {"precision": rel.sum(1) / k, "recall": rel.sum(1) / n_rel,
            "hit": rel.any(1).astype(float), "MAP": ap,
            "nDCG": (rel * chegirma).sum(1) / idcg}


def baholash(ball, X_train, test, users, k=10):
    """Train da ko'rilgan mahsulotlar tavsiya qilinmaydi; har foydalanuvchi metrikasi."""
    ball = np.array(ball, dtype=float)
    ball[X_train[users].toarray() > 0] = -np.inf
    top = np.argsort(-ball, axis=1, kind="stable")[:, :k]
    qator = pd.Series(np.arange(len(users)), index=users)
    tt = test[test["user"].isin(users)]
    T = np.zeros(ball.shape, dtype=bool)
    T[qator[tt["user"]].to_numpy(), tt["item"].to_numpy()] = True
    return metrikalar(top, T), top


def als_qadam(R, Y, lam, alfa):
    """Hamma qatorlar uchun bir yo'la: (Y^T Y + Y^T (C_u - I) Y + lam*I) x_u = Y^T C_u p_u.

    Y^T Y bitta marta hisoblanadi va HAMMA kataklarni (nollarni ham, c = 1 bilan)
    qamraydi; qo'shimcha had sum_i (c_ui - 1) y_i y_i^T faqat nol bo'lmagan
    kataklar ustida - siyrak matritsa x "tashqi ko'paytmalar" jadvali.
    """
    k = Y.shape[1]
    YtY = Y.T @ Y + lam * np.eye(k)
    tashqi = (Y[:, :, None] * Y[:, None, :]).reshape(len(Y), k * k)
    W = R.copy()
    W.data = alfa * R.data                           # c - 1
    A = YtY + (W @ tashqi).reshape(-1, k, k)
    W.data = 1 + alfa * R.data                       # c (p = 1 kuzatilganda)
    return np.linalg.solve(A, (W @ Y)[:, :, None])[:, :, 0]


def als(R, k=32, lam=10.0, alfa=10.0, iters=10, seed=0):
    """Implicit ALS (Hu, Koren, Volinsky 2008): ishonch c = 1 + alfa * r."""
    rng = np.random.default_rng(seed)
    R = R.tocsr()
    Rt = R.T.tocsr()
    V = rng.normal(0, 0.1, (R.shape[1], k))
    for _ in range(iters):
        U = als_qadam(R, V, lam, alfa)
        V = als_qadam(Rt, U, lam, alfa)
    return U, V


def kuch_matritsa(qism, n_user, n_item):
    """r = 1 (ko'rish) yoki 3 (xarid) - implicit signal kuchi."""
    return sparse.csr_matrix((1.0 + 2.0 * qism["xarid"].to_numpy(),
                              (qism["user"], qism["item"])), shape=(n_user, n_item))


def item_knn(X, users, k=50):
    norma = np.sqrt(np.asarray(X.multiply(X).sum(0)).ravel())
    Xn = X @ sparse.diags(1 / np.maximum(norma, 1e-12))
    S = (Xn.T @ Xn).toarray()
    np.fill_diagonal(S, 0)
    kesish = np.argpartition(-S, k, axis=1)[:, k:]
    np.put_along_axis(S, kesish, 0, axis=1)
    return X[users] @ S


def juft(a, b):
    d = a - b
    return d.mean(), d.std(ddof=1) / np.sqrt(len(d))


def main() -> None:
    df, items = dokon()
    n_user, n_item = 2000, len(items)
    k, lam, alfa = 16, 10.0, 1.0                  # 3-misolda validatsiyada tanlangan
    rng = np.random.default_rng(5)

    print("=== 1. Yangi foydalanuvchilar: 400 tasi o'qitishdan butunlay chiqarildi ===")
    faol = df.groupby("user").size()
    yangi_u = np.sort(rng.choice(faol[faol >= 15].index.to_numpy(), 400, replace=False))
    eski = df[~df["user"].isin(yangi_u)]
    R = kuch_matritsa(eski, n_user, n_item)
    X = matritsa(eski, n_user, n_item)
    U, V = als(R, k=k, lam=lam, alfa=alfa, iters=6)
    pop = np.asarray(X.sum(0)).ravel()
    norma = np.sqrt(pop)
    Xn = X @ sparse.diags(1 / np.maximum(norma, 1e-12))
    S = (Xn.T @ Xn).toarray()
    np.fill_diagonal(S, 0)
    yd = df[df["user"].isin(yangi_u)].sort_values(["user", "vaqt"])
    tartib = yd.groupby("user").cumcount().to_numpy()
    print(f"  ALS o'qitildi: {len(eski)} ta'sir; yangi foydalanuvchilarda "
          f"{len(yd)} ta'sir (o'qitishda yo'q)")
    print(f"  {'ma_lum n':>9} {'mashhurlik':>11} {'item-kNN':>9} {'ALS fold-in':>12} "
          f"{'ALS - mashh.':>13} {'SE':>7}")
    natija = {}
    for n in [0, 1, 3, 5, 10]:
        malum, test = yd[tartib < n], yd[tartib >= n]
        Xm = matritsa(malum, n_user, n_item)
        Rm = kuch_matritsa(malum, n_user, n_item)
        fold = als_qadam(Rm[yangi_u], V, lam, alfa)   # V qotirilgan, faqat x_u yechiladi
        r_pop, _ = baholash(np.tile(pop, (400, 1)).astype(float), Xm, test, yangi_u)
        r_knn, _ = baholash(Xm[yangi_u] @ S, Xm, test, yangi_u)
        r_als, _ = baholash(fold @ V.T, Xm, test, yangi_u)
        natija[n] = (r_pop["nDCG"], r_als["nDCG"])
        m, se = juft(r_als["nDCG"], r_pop["nDCG"])
        print(f"  {n:>9} {r_pop['nDCG'].mean():>11.4f} {r_knn['nDCG'].mean():>9.4f} "
              f"{r_als['nDCG'].mean():>12.4f} {m:>+13.4f} {se:>7.4f}")
    chegara = None
    for n in [1, 3, 5, 10]:
        m, se = juft(natija[n][1], natija[n][0])
        if chegara is None and m > 2 * se:
            chegara = n
    print(f"  ALS mashhurlikdan sezilarli yaxshi bo'ladigan eng kichik n: {chegara}")
    print(f"  qoida: n < {chegara} -> mashhurlik, aks holda ALS fold-in")

    print("\n=== 2. Yangi mahsulotlar: 80 tasining barcha ta'sirlari yashirildi ===")
    yangi_i = np.sort(rng.choice(n_item, 80, replace=False))
    iliq = df[~df["item"].isin(yangi_i)]
    sovuq = df[df["item"].isin(yangi_i)]
    R = kuch_matritsa(iliq, n_user, n_item)
    Xw = matritsa(iliq, n_user, n_item)
    U, V = als(R, k=k, lam=lam, alfa=alfa, iters=6)
    tfidf = TfidfVectorizer().fit_transform(items["tavsif"])
    iliq_i = np.setdiff1d(np.arange(n_item), yangi_i)
    ridge = Ridge(alpha=1.0).fit(tfidf[iliq_i], V[iliq_i])
    V_taxmin = ridge.predict(tfidf[yangi_i])
    print(f"  CF uchun yangi mahsulot vektori: nol (ALS da ta'sir yo'q) -> "
          f"|v| = {np.abs(V[yangi_i]).max():.1e}")
    print(f"  Ridge (TF-IDF -> ALS faktorlari) iliq mahsulotlarda R^2 = "
          f"{ridge.score(tfidf[iliq_i], V[iliq_i]):.3f}")
    us = np.sort(sovuq["user"].unique())
    us = us[np.asarray(Xw[us].sum(1)).ravel() >= 3]
    blok = np.full((len(us), n_item), -np.inf)      # faqat yangi mahsulotlar orasida
    usullar = {
        "tasodifiy": rng.random((len(us), len(yangi_i))),
        "mashhurlik (kategoriya)": np.tile(
            iliq.merge(items[["item", "kategoriya"]])["kategoriya"].value_counts()
            .reindex(items.loc[yangi_i, "kategoriya"]).to_numpy(float), (len(us), 1)),
        "kontent (TF-IDF)": cosine_similarity(Xw[us] @ tfidf, tfidf[yangi_i]),
        "gibrid (TF-IDF -> ALS)": U[us] @ V_taxmin.T,
    }
    test = sovuq[sovuq["user"].isin(us)]
    print(f"  {len(us)} foydalanuvchi, {len(test)} ta yangi mahsulot ta'siri; "
          f"har biriga 80 ta ichidan top-10")
    r = {}
    for nom, b in usullar.items():
        ball = blok.copy()
        ball[:, yangi_i] = b
        r[nom], _ = baholash(ball, Xw, test, us)
    eng = max(r, key=lambda n: r[n]["nDCG"].mean())
    for nom in usullar:
        m, se = juft(r[nom]["nDCG"], r[eng]["nDCG"])
        print(f"  {nom:<24} nDCG {r[nom]['nDCG'].mean():.4f}, hit "
              f"{r[nom]['hit'].mean():.3f}; - eng yaxshi: {m:+.4f} (SE {se:.4f})")
    print(f"  ⭐ Yangi mahsulotga CF o'zi yetmaydi - kontent kerak; eng yaxshisi: {eng}")


if __name__ == "__main__":
    main()

Natijaning muhim qismi:

text
=== 1. Yangi foydalanuvchilar: 400 tasi o'qitishdan butunlay chiqarildi ===
  ALS o'qitildi: 30760 ta'sir; yangi foydalanuvchilarda 14259 ta'sir (o'qitishda yo'q)
   ma_lum n  mashhurlik  item-kNN  ALS fold-in  ALS - mashh.      SE
          0      0.3338    0.0554       0.0554       -0.2784  0.0098
          1      0.3199    0.2943       0.3250       +0.0052  0.0126
          3      0.2986    0.3837       0.3533       +0.0547  0.0111
          5      0.2716    0.3668       0.3428       +0.0712  0.0105
         10      0.2270    0.3222       0.3194       +0.0924  0.0096
  ALS mashhurlikdan sezilarli yaxshi bo'ladigan eng kichik n: 3
  qoida: n < 3 -> mashhurlik, aks holda ALS fold-in

=== 2. Yangi mahsulotlar: 80 tasining barcha ta'sirlari yashirildi ===
  CF uchun yangi mahsulot vektori: nol (ALS da ta'sir yo'q) -> |v| = 0.0e+00
  Ridge (TF-IDF -> ALS faktorlari) iliq mahsulotlarda R^2 = 0.495
  1525 foydalanuvchi, 4553 ta yangi mahsulot ta'siri; har biriga 80 ta ichidan top-10
  tasodifiy                nDCG 0.0766, hit 0.301; - eng yaxshi: -0.1860 (SE 0.0075)
  mashhurlik (kategoriya)  nDCG 0.1810, hit 0.378; - eng yaxshi: -0.0816 (SE 0.0095)
  kontent (TF-IDF)         nDCG 0.2626, hit 0.646; - eng yaxshi: +0.0000 (SE 0.0000)
  gibrid (TF-IDF -> ALS)   nDCG 0.2210, hit 0.578; - eng yaxshi: -0.0416 (SE 0.0057)
  ⭐ Yangi mahsulotga CF o'zi yetmaydi - kontent kerak; eng yaxshisi: kontent (TF-IDF)

Natija tahlili.

1-bo'lim — yangi foydalanuvchilar. 400 ta faol foydalanuvchi o'qitishdan butunlay chiqarildi; ALS qolganlarida o'qitildi. Yangi foydalanuvchining birinchi n ta ta'siri "ma'lum", qolganlari test. (Soddalik uchun mashhurlik va ALS boshqa foydalanuvchilarning butun davr ma'lumotidan o'qitilgan; bu ikkala usulga bir xil ta'sir qiladi.)

  • n = 0 — tarix yo'q: CF ballari nol, tavsiya ma'nosiz (0.0554); mashhurlik 0.3338. Bu yerda yagona variant — fallback.
  • n = 1 — ALS fold-in (0.3250) mashhurlikka (0.3199) teng: farq +0.0052, SE 0.0126 — sezilarli emas.
  • n = 3 dan boshlab ALS sezilarli yaxshi (+0.0547, SE 0.0111), farq n bilan o'sadi (n = 10 da +0.0924).

Natijadan hisoblangan qoida: n < 3 → mashhurlik, aks holda ALS fold-in. Fold-in modelni qayta o'qitmaydi — faqat V qotirilgan holda x_u ni yechadi, shuning uchun yangi foydalanuvchi uchinchi mahsulotni ko'rishi bilanoq shaxsiy tavsiya oladi. Qiziq tafsilot: item-kNN n = 3 va n = 5 da fold-in dan ham yaxshi (0.3837 va 0.3533, 0.3668 va 0.3428) — qisqa tarixda "oxirgi ko'rganiga o'xshash" oddiy qoidasi kuchli. Bu ikki bosqichli tizimda item-kNN ni nomzod manbai sifatida saqlashga yana bir sabab.

2-bo'lim — yangi mahsulotlar. 80 ta mahsulotning barcha ta'sirlari yashirildi. ALS ular uchun nol vektor beradi (|v| = 0) — CF bu mahsulotlarni hech kimga tavsiya qilmaydi. Vazifa: foydalanuvchi keyinchalik ko'rgan yangi mahsulotni 80 ta yangi mahsulot ichidan top-10 ga chiqarish.

  • Kategoriya mashhurligi tasodifiydan ancha yaxshi (0.1810 va 0.0766), lekin shaxsiy emas.
  • Kontent-asosli profil (TF-IDF) eng yaxshi: nDCG 0.2626, hit 0.646.
  • Gibrid "ko'prik" (TF-IDF → ALS faktorlari, Ridge) — 0.2210, kontentdan sezilarli yomon (-0.0416, SE 0.0057). Sabab — Ridge iliq mahsulotlarda ham faktorlarning atigi R^2 = 0.495 qismini tushuntiradi: tavsif so'zlari mahsulotning did o'qidagi o'rnini yarimtaligina aniqlaydi, va bu xato to'g'ridan-to'g'ri ballga o'tadi.

Kutilgan "gibrid yaxshiroq" natija chiqmadi — va bu muhim saboq: ko'prik faqat kontent belgilari faktorlarni yaxshi tushuntirsa foydali. Bizning sintetik tavsiflarda did asosan kategoriya va kichik guruhda, TF-IDF esa aynan shuni bevosita ushlaydi. Real loyihada ikkalasini (va ularning aralashmasini) o'lchab tanlash kerak; yangi mahsulot bir necha o'n ta'sir to'plagach, u odatdagi CF ga o'tkaziladi.


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

Noto'g'ri fikr To'g'risi
"Siyrak matritsaga TruncatedSVD — bu MF" Bo'sh kataklar "baho 0" bo'lib qoladi; RMSE ~3 (1-misol)
"Implicit ma'lumotda faqat kuzatilganlarni o'qitish yetadi" Nollarsiz model "hamma narsa yoqadi" deydi; mashhurlikdan ham yomon (2-misol)
"ALS hamma kataklarni aylanib chiqadi — sekin" Y^T Y hiylasi: hisob faqat nol bo'lmaganlar ustida, natija bir xil
"alfa qancha katta bo'lsa, shuncha yaxshi" alfa = 10 nDCG ni 0.117 dan 0.083 ga tushirdi
"BPR ranjirlashni optimallashtiradi — demak eng yaxshisi" Tasodifiy salbiy namunalar bilan kichik ma'lumotda item-kNN dan ham zaif chiqdi
"Ko'p faktor — yaxshiroq" k va lambda validatsiyada; soddaroq munosib model afzal
"Yangi foydalanuvchiga ham ALS" Tarixsiz ball 0; n < 3 da mashhurlik bilan teng yoki yomon
"Gibrid har doim kontentdan yaxshi" Kontent → faktor ko'prigi R^2 past bo'lsa, yutqazadi (4-misol)
"Offline nDCG yuqori — onlayn ham yaxshi" Ekspozitsiya tarafkashligi; yakuniy hakam — A/B test

6. Keng tarqalgan xatolar va yechimlari

1. Explicit baholarga to'g'ridan-to'g'ri SVD

python
rek = TruncatedSVD(10).fit_transform(R) @ svd.components_          # ⚠️ nol = baho 0
# faqat kuzatilgan kataklar ustida bias li MF (SGD yoki ALS)          # ✅

2. Implicit ALS da nollarni tashlab ketish

python
A = Yi.T @ (c[:, None] * Yi) + lam * np.eye(k)                     # ⚠️ Y^T Y yo'q
A = Y.T @ Y + Yi.T @ ((c - 1)[:, None] * Yi) + lam * np.eye(k)     # ✅ hamma katak

3. Biaslarsiz model

python
r_hat = np.sum(P[u] * Q[i], 1)                                     # ⚠️
r_hat = mu + bu[u] + bi[i] + np.sum(P[u] * Q[i], 1)                # ✅

4. Regularizatsiyasiz SGD

python
mf_sgd(tr, reg=0.0, epoxlar=40)                                    # ⚠️ yodlaydi
mf_sgd(tr, reg=0.05)            # ✅ reg va epoxalar - validatsiyada

5. Giperparametrni test da tanlash

python
k = max([8, 16, 32], key=lambda k: ndcg_test(k))                   # ⚠️
tr2, va = bolish(tr, "vaqt")                                       # ✅ train ichidan

6. Yangi foydalanuvchiga CF bali

python
top = np.argsort(-(x_u @ V.T))[:10]          # ⚠️ x_u = 0 -> tasodifiy tartib
top = mashhurlik_top if n_u < 3 else cf_top   # ✅ chegara o'lchangan

7. Yangi mahsulotni e'tiborsiz qoldirish

python
ball = U @ V.T                                   # ⚠️ yangi mahsulot v = 0
ball_yangi = cosine_similarity(profil, tfidf[yangi])   # ✅ kontent / exploration

7. Integratsiya — bu bilim qayerda kerak bo'ladi

  • 10-qism, 16.8-dars (o'tilgan): Matritsa ko'paytmasi, SVD va PCA — faktorizatsiyaning asosi
  • 13-qism (o'tilgan): Ridge regressiya — ALS ning har qadami og'irlikli ridge
  • 20-21-qismlar (o'tilgan): Gradient tushish, Adam, embedding qatlamlari — SGD MF va BPR
  • 15-qism (o'tilgan): Gradient boosting — ikki bosqichli tizimdagi qayta ranjirlash modeli
  • 27.13-dars (o'tilgan): A/B test va bandit — onlayn baholash va yangi mahsulotlar uchun exploration
  • 28.4-dars (o'tilgan): Bo'lish, metrikalar, mashhurlik va item-kNN bazaviylari
  • 28.11-dars: Sababiy xulosa — tavsiyaning haqiqiy ta'sirini baholash

8. Eng yaxshi amaliyotlar

  1. Explicit: faqat kuzatilgan kataklar, biaslar bilan; bazaviy — biaslar.

  2. Implicit: hamma kataklar kuchsiz salbiy sifatida; ishonch vazni alfa sozlanadi.

  3. Har doim mashhurlik va item-kNN bilan bir xil bo'lishda, juftlashgan SE bilan solishtiring.

  4. k, lambda, alfa, epoxalar — train ichidan validatsiyada; "eng sodda munosib" qoidasi.

  5. Sovuq start segmentlarini alohida o'lchang va fallback chegarasini raqam bilan belgilang.

  6. Yangi mahsulotlar uchun kontent yo'li va exploration trafik ajrating.

  7. Katta tizimda ikki bosqich: arzon nomzodlar (recall) + boy qayta ranjirlash (nDCG).

  8. Offline natija — A/B testga chiqarish uchun filtr; qaror — onlayn.


9. Amaliy topshiriq

Vazifa 1: Bashorat qiling

python
1.  # 2000 foydalanuvchi, 800 mahsulot, k=16 - MF parametrlari soni?
2.  # siyrak baholar matritsasiga TruncatedSVD - test bashoratlari qanday bo'ladi?
3.  # bias li modelda b_u = -0.4 nimani bildiradi?
4.  # reg = 0 bilan uzoq SGD - train va test RMSE?
5.  # implicit ALS da bo'sh katakning p va c qiymati?
6.  # faqat kuzatilgan kataklar bilan implicit model nimani o'rganadi?
7.  # Y^T Y hiylasida bitta foydalanuvchi uchun hisob narxi nimaga bog'liq?
8.  # alfa juda katta bo'lsa?
9.  # BPR yo'qotishi qaysi metrikaning silliq yaqinlashuvi?
10. # yangi foydalanuvchi (n=0) uchun ALS bali?
11. # yangi mahsulotning ALS vektori (lambda > 0)?
12. # ikki bosqichli tizimda birinchi bosqich qaysi metrika bilan baholanadi?
Javoblar
  1. (2000 + 800) * 16 = 44800 (biaslar bilan yana 2800)
  2. Nolga yaqin (~0.5) — bo'sh kataklar "baho 0" deb olingan; RMSE ~3
  3. Foydalanuvchi o'rtachadan 0.4 ball qattiqroq baholaydi
  4. Train pasayishda davom etadi, test bir nuqtadan keyin o'sadi (overfitting)
  5. p = 0, c = 1 — "ehtimol yoqmaydi", ishonch past
  6. "Hamma narsa yoqadi" — ranjirlash asosan vektor normasi (mashhurlik) bo'yicha
  7. Foydalanuvchi ko'rgan mahsulotlar soniga: O(n_u * k^2 + k^3)
  8. Ko'rilganlarni yodlaydi, qolganlarini farqlamaydi — nDCG tushadi
  9. AUC (tasodifiy juftlikda to'g'ri tartib ulushi)
  10. Nol — hamma mahsulot teng, tavsiya ma'nosiz; mashhurlik fallback kerak
  11. Nol vektor — hech kimga tavsiya qilinmaydi
  12. Recall@N (N katta, masalan 500) — yaxshi mahsulot ro'yxatga kirdimi

Vazifa 2: Xatolarni tuzating

python
1.  svd = TruncatedSVD(10).fit(R_baholar); r_hat = svd.transform(R) @ svd.components_

2.  A = (Yi.T * c) @ Yi + lam * np.eye(k)        # implicit ALS

3.  r_hat = mu + np.sum(P[u] * Q[i], 1)

4.  k = max([8, 16, 32], key=lambda k: ndcg(test, als(R, k=k)))

5.  tavsiya = np.argsort(-(U[yangi] @ V.T))[:10]   # yangi foydalanuvchi, tarix yo'q
Javoblar
python
1.  # faqat kuzatilgan baholar ustida bias li MF:
    model, _ = mf_sgd(train, n_user, n_item, k=10, reg=0.05)

2.  A = Y.T @ Y + (Yi.T * (c - 1)) @ Yi + lam * np.eye(k)

3.  r_hat = mu + bu[u] + bi[i] + np.sum(P[u] * Q[i], 1)

4.  tr2, va = bolish(train, "vaqt")
    k = max([8, 16, 32], key=lambda k: ndcg(va, als(R_tr2, k=k)))

5.  tavsiya = mashhurlik_top[:10]    # n < chegara bo'lsa; keyin fold-in

Vazifa 3: Explicit MF

Modellang (1-misol asosida):

  1. reg ni 0.02, 0.05, 0.1, 0.2 qilib, train ichidan validatsiyada tanlang; test RMSE qancha yaxshilanadi?
  2. k ni 2, 4, 10, 30 qiling — haqiqiy k = 4 atrofida nima bo'ladi?
  3. Klassik (har baho uchun alohida) SGD ni yozing va mini-partiyali variant bilan RMSE ni solishtiring
  4. Biaslarni o'chirib (bu = bi = 0 qotirilgan), faktorlar biaslarni "o'rnini bosa oladimi"?

Vazifa 4: ALS

Modellang (2-misol asosida):

  1. Signal kuchini r = 1 + 2 * xarid o'rniga r = 1 (hammasi teng) va r = 1 + 9 * xarid qiling — optimal alfa qanday o'zgaradi?
  2. Iteratsiyalar sonini 2, 4, 8, 16 qilib, nDCG va yo'qotishni kuzating
  3. Tezkor als_qadam ni foydalanuvchi bo'yicha oddiy for tsikl bilan yozing va natijalar bir xilligini tekshiring
  4. ALS tavsiyalarining qamrovi va mashhurlar ulushini 28.4 dagi item-kNN bilan solishtiring

Vazifa 5: Tanlash va BPR

Modellang (3-misol asosida):

  1. BPR da salbiy namunani mashhurlikka proporsional oling (p_j ~ pop_j^0.75) — natija o'zgaradimi?
  2. BPR ga item biasini olib tashlang va nDCG ni solishtiring
  3. ALS uchun setkaga k = 64 ni qo'shing — qaror qoidasi uni tanlaydimi?
  4. Validatsiya va test da tanlangan sozlamalar bo'yicha "optimistik tuzatma" ni o'lchang: test da eng yaxshisi validatsiyadagidan qancha farq qiladi?

Vazifa 6: Sovuq start

Modellang (4-misol asosida):

  1. Yangi foydalanuvchi uchun aralash ball: w(n) * ALS + (1 - w(n)) * mashhurlik, w(n) = n / (n + m) — m ni validatsiyada tanlang
  2. Yangi mahsulotlar uchun kontent va gibrid ballarini aralashtiring — sezilarli yutuq bormi?
  3. Ridge alpha sini sozlang va TF-IDF ga narx darajasini alohida belgi qilib qo'shing — R^2 va nDCG
  4. Yangi mahsulotlar 1, 5, 20 ta ta'sir to'plagandan keyin CF qachon kontentdan o'zib ketadi?

Vazifa 7: O'ylash

Siz yangi ALS modelini offline baholadingiz: nDCG@10 item-kNN dan +0.02 (SE 0.004) yaxshi. Rahbar: "Ajoyib, ertadan hamma foydalanuvchilarga ALS. Item-kNN va mashhurlik kodini o'chirib tashlang, ortiqcha narsa kerak emas." Nima deysiz?

Javob

Qisqa javob: ALS ni chiqarish mumkin (A/B test orqali), lekin item-kNN va mashhurlikni o'chirish — xato. Ular tizimning zarur qismlari.

1. Offline natija — hali hakam emas. Farq sezilarli (+0.02, 5 SE), lekin u eski tizim ko'rsatganlari ta'sirida yig'ilgan jurnalda o'lchangan. Avval kichik trafikda A/B test 27.13-bob — asosiy metrika (xarid/sessiya) va guardrail lar bilan. Onlayn yutuq offline dan kichik bo'lishi odatiy.

2. Mashhurlik — sovuq start fallbacki. 4-misolda tarixsiz foydalanuvchi uchun ALS bali nol edi va tavsiya ma'nosiz chiqdi (0.0554, mashhurlik 0.3338); n = 1 da ham ALS mashhurlikdan yaxshi emas edi. Har kuni yangi ro'yxatdan o'tganlar — aynan shu segment. Mashhurliksiz ular tasodifiy ro'yxat ko'radi.

3. Item-kNN — kuchli nomzod manbai. 4-misolda qisqa tarixli (n = 3-5) foydalanuvchilarda item-kNN ALS fold-in dan ham yaxshi chiqdi. Bundan tashqari u "siz X ni olgansiz" tushuntirishini beradi va mahsulot sahifasidagi "o'xshash mahsulotlar" bloki uchun tabiiy.

4. Yangi mahsulotlar. ALS ularni umuman ko'rsatmaydi (vektor nol). Kontent-asosli yo'l (4-misolda eng yaxshi) va kichik exploration trafik kerak — aks holda yangi mahsulotlar hech qachon ta'sir to'plamaydi.

5. Arxitektura. To'g'ri tuzilma — ikki bosqich: ALS, item-kNN, mashhurlik va kontent — nomzod manbalari; ularning ustida qayta ranjirlash modeli va qoidalar. Bitta modelga tayanish yagona nosozlik nuqtasini ham yaratadi: ALS ni qayta o'qitish buzilsa, fallback kerak.

Rahbarga javob: "ALS ni A/B testga chiqaramiz — offline yutuq ishonchli. Lekin mashhurlik va item-kNN o'chirilmaydi: birinchisi yangi foydalanuvchilar uchun yagona ishlaydigan usul, ikkinchisi qisqa tarixda ALS dan yaxshi va 'o'xshash mahsulotlar' blokini ta'minlaydi. Uchalasini nomzod manbai sifatida saqlab, ustiga qayta ranjirlash qo'yish — keyingi qadam."

Nimani mustahkamlaydi: 2.2, 2.4, 2.6, 2.7-bo'limlar.


Xulosa

Bu darsda latent faktorlar g'oyasini explicit va implicit ma'lumotga qo'lladik, giperparametrlarni halol tanladik va sovuq startni o'lchadik.

Eng muhim uch fikr:

  1. Bo'sh katak bilan qanday ishlash — hamma narsani hal qiladi. Explicit baholarda bo'sh kataklar ishtirok etmasligi kerak: siyrak matritsaga to'g'ridan-to'g'ri SVD test bashoratlarini 0.58 ga tushirdi (RMSE 3.09), faqat kuzatilgan kataklardagi bias li MF esa 0.739 berdi va biaslardan sezilarli yaxshi chiqdi. Implicit ma'lumotda esa teskari: faqat kuzatilganlar bilan o'qitilgan model mashhurlikdan ham yomon (0.0687), hamma kataklarni ishonch bilan qamragan ALS esa 0.1174 — 28.4 dagi eng yaxshi natijadan ham yuqori. Y^T Y hiylasi hamma kataklarni qamrab, hisobni faqat nol bo'lmaganlar ustida bajaradi.

  2. Sozlash — validatsiyada, qaror — "eng sodda munosib". k, lambda, alfa train ichidagi validatsiyada tanlandi; qoida ikki barobar kichik k = 16 modelni tanladi (test nDCG 0.1118, item-kNN dan +0.0156). Katta alfa zararli chiqdi; BPR esa nazariy jozibasiga qaramay bu ma'lumotda item-kNN dan ham zaif (0.0803) — "to'g'ri" yo'qotish avtomatik yaxshi model bermaydi.

  3. Sovuq start — alohida o'lchanadigan segment. Tarixsiz foydalanuvchida CF ma'nosiz, n = 1 da ALS mashhurlikka teng, n = 3 dan sezilarli yaxshi — shundan "n < 3 → mashhurlik" qoidasi chiqdi. Yangi mahsulotlarni CF umuman ko'rmaydi; ular uchun kontent-asosli profil eng yaxshi bo'ldi, TF-IDF → faktor ko'prigi esa (R^2 = 0.495) undan yutqazdi. Real tizim bir nechta usulni nomzod manbai sifatida birlashtiradi va yakuniy qarorni onlayn A/B test chiqaradi.

Keyingi darsda Ilg'or anomaliya aniqlash: vaqt qatorlaridagi nuqtali, kontekstli va kollektiv anomaliyalar (rolling va robust z-score, STL qoldig'i, bashorat qoldig'i), juda nomutanosib bank firibgarligi (nazoratsiz, nazoratli va yarim nazoratli usullar, PR-AUC, top-k precision, xarajatga asoslangan chegara) va avtoenkoderning IsolationForest bilan halol taqqoslanishi.

Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
28.5-dars: Matritsa faktorizatsiyasi va implicit feedback — IlmHamroh