Mundarija (24)
- 1. Kirish va motivatsiya
- 2. Nazariya — chuqur tushuntirish
- 2.1. Latent faktorlar g'oyasi
- 2.2. SVD va bo'sh kataklar muammosi
- 2.3. Explicit baholar uchun MF: biaslar va SGD
- 2.4. Implicit feedback: ishonch va ALS
- 2.5. BPR: juftlik bo'yicha ranjirlash
- 2.6. Giperparametrlar: k, lambda, alfa
- 2.7. Sovuq start
- 2.8. Ikki bosqichli tizim
- 2.9. Onlayn baholash
- 2.10. Tuzoqlar
- 3. Tez ma'lumotnoma
- 4. Batafsil misollar
- Misol 1 — Explicit baholar: SVD nima uchun ishlamaydi va bias li MF (SGD)
- Misol 2 — Implicit feedback uchun ALS noldan
- Misol 3 — Giperparametrlarni validatsiyada tanlash va BPR
- Misol 4 — Sovuq start: yangi foydalanuvchi va yangi mahsulot
- 5. To'g'ri va noto'g'ri tushunishlar
- 6. Keng tarqalgan xatolar va yechimlari
- 7. Integratsiya — bu bilim qayerda kerak bo'ladi
- 8. Eng yaxshi amaliyotlar
- 9. Amaliy topshiriq
- Xulosa
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
kva 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,lightfmkutubxonalari 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
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'rnigaFaktorizatsiya — 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
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
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 baholaridaBiaslar — 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:
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 beradiBu 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):
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 ustidaALS 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
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
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 marta2.7. Sovuq start
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:
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 cheklashHar 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.
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 kerakOffline — 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
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):
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 - hakam4. 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.
"""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:
=== 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 topadiNatija 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
"""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:
=== 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 shartNatija 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
"""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:
=== 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, yakundaNatija 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
"""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:
=== 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); mashhurlik0.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 = 3dan boshlab ALS sezilarli yaxshi (+0.0547,SE 0.0111), farqnbilan o'sadi (n = 10da+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.1810va0.0766), lekin shaxsiy emas. - Kontent-asosli profil (TF-IDF) eng yaxshi: nDCG
0.2626, hit0.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 atigiR^2 = 0.495qismini 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
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
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 katak3. Biaslarsiz model
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
mf_sgd(tr, reg=0.0, epoxlar=40) # ⚠️ yodlaydi
mf_sgd(tr, reg=0.05) # ✅ reg va epoxalar - validatsiyada5. Giperparametrni test da tanlash
k = max([8, 16, 32], key=lambda k: ndcg_test(k)) # ⚠️
tr2, va = bolish(tr, "vaqt") # ✅ train ichidan6. Yangi foydalanuvchiga CF bali
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'lchangan7. Yangi mahsulotni e'tiborsiz qoldirish
ball = U @ V.T # ⚠️ yangi mahsulot v = 0
ball_yangi = cosine_similarity(profil, tfidf[yangi]) # ✅ kontent / exploration7. 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
Explicit: faqat kuzatilgan kataklar, biaslar bilan; bazaviy — biaslar.
Implicit: hamma kataklar kuchsiz salbiy sifatida; ishonch vazni
alfasozlanadi.Har doim mashhurlik va item-kNN bilan bir xil bo'lishda, juftlashgan SE bilan solishtiring.
k,lambda,alfa, epoxalar — train ichidan validatsiyada; "eng sodda munosib" qoidasi.Sovuq start segmentlarini alohida o'lchang va fallback chegarasini raqam bilan belgilang.
Yangi mahsulotlar uchun kontent yo'li va exploration trafik ajrating.
Katta tizimda ikki bosqich: arzon nomzodlar (recall) + boy qayta ranjirlash (nDCG).
Offline natija — A/B testga chiqarish uchun filtr; qaror — onlayn.
9. Amaliy topshiriq
Vazifa 1: Bashorat qiling
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
(2000 + 800) * 16 = 44800(biaslar bilan yana 2800)- Nolga yaqin (~0.5) — bo'sh kataklar "baho 0" deb olingan; RMSE ~3
- Foydalanuvchi o'rtachadan 0.4 ball qattiqroq baholaydi
- Train pasayishda davom etadi, test bir nuqtadan keyin o'sadi (overfitting)
p = 0,c = 1— "ehtimol yoqmaydi", ishonch past- "Hamma narsa yoqadi" — ranjirlash asosan vektor normasi (mashhurlik) bo'yicha
- Foydalanuvchi ko'rgan mahsulotlar soniga:
O(n_u * k^2 + k^3) - Ko'rilganlarni yodlaydi, qolganlarini farqlamaydi — nDCG tushadi
- AUC (tasodifiy juftlikda to'g'ri tartib ulushi)
- Nol — hamma mahsulot teng, tavsiya ma'nosiz; mashhurlik fallback kerak
- Nol vektor — hech kimga tavsiya qilinmaydi
- Recall@N (N katta, masalan 500) — yaxshi mahsulot ro'yxatga kirdimi
Vazifa 2: Xatolarni tuzating
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'qJavoblar
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-inVazifa 3: Explicit MF
Modellang (1-misol asosida):
regni 0.02, 0.05, 0.1, 0.2 qilib, train ichidan validatsiyada tanlang; test RMSE qancha yaxshilanadi?kni 2, 4, 10, 30 qiling — haqiqiyk = 4atrofida nima bo'ladi?- Klassik (har baho uchun alohida) SGD ni yozing va mini-partiyali variant bilan RMSE ni solishtiring
- Biaslarni o'chirib (
bu = bi = 0qotirilgan), faktorlar biaslarni "o'rnini bosa oladimi"?
Vazifa 4: ALS
Modellang (2-misol asosida):
- Signal kuchini
r = 1 + 2 * xarido'rnigar = 1(hammasi teng) var = 1 + 9 * xaridqiling — optimalalfaqanday o'zgaradi? - Iteratsiyalar sonini 2, 4, 8, 16 qilib, nDCG va yo'qotishni kuzating
- Tezkor
als_qadamni foydalanuvchi bo'yicha oddiyfortsikl bilan yozing va natijalar bir xilligini tekshiring - ALS tavsiyalarining qamrovi va mashhurlar ulushini 28.4 dagi item-kNN bilan solishtiring
Vazifa 5: Tanlash va BPR
Modellang (3-misol asosida):
- BPR da salbiy namunani mashhurlikka proporsional oling (
p_j ~ pop_j^0.75) — natija o'zgaradimi? - BPR ga item biasini olib tashlang va nDCG ni solishtiring
- ALS uchun setkaga
k = 64ni qo'shing — qaror qoidasi uni tanlaydimi? - 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):
- Yangi foydalanuvchi uchun aralash ball:
w(n) * ALS + (1 - w(n)) * mashhurlik,w(n) = n / (n + m)—mni validatsiyada tanlang - Yangi mahsulotlar uchun kontent va gibrid ballarini aralashtiring — sezilarli yutuq bormi?
- Ridge
alphasini sozlang va TF-IDF ga narx darajasini alohida belgi qilib qo'shing —R^2va nDCG - 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:
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.58ga tushirdi (RMSE3.09), faqat kuzatilgan kataklardagi bias li MF esa0.739berdi 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 esa0.1174— 28.4 dagi eng yaxshi natijadan ham yuqori.Y^T Yhiylasi hamma kataklarni qamrab, hisobni faqat nol bo'lmaganlar ustida bajaradi.Sozlash — validatsiyada, qaror — "eng sodda munosib".
k,lambda,alfatrain ichidagi validatsiyada tanlandi; qoida ikki barobar kichikk = 16modelni tanladi (test nDCG0.1118, item-kNN dan+0.0156). Kattaalfazararli 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.Sovuq start — alohida o'lchanadigan segment. Tarixsiz foydalanuvchida CF ma'nosiz,
n = 1da ALS mashhurlikka teng,n = 3dan 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.
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!