File size: 9,581 Bytes
6530920
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
"""v16seq: next-click sequence models (SASRec/gBCE, GRU4Rec) as feature generators. All sequences strictly before the fold cutoff C (click log of train events ts<C, i.e. their histories + their clicks)
plus the TARGET event's own history array (known at event time). No eval-row aggregates, no labels of events >= C.
Usage (python seqfeat.py MODEL CUTOFF_NAME TABLES... ): see seqfeat.py."""
import os, sys, time, math, numpy as np, polars as pl, torch, torch.nn as nn, torch.nn.functional as F
os.environ.setdefault('DW_WORK','/workspace/huawei/v12x/dwdata'); sys.path.insert(0,'/workspace/huawei/v16seq'); sys.dont_write_bytecode=True
from common import *
L=50; F3='/workspace/huawei/v12/feat_v3'; DW='/workspace/huawei/v12x/dwdata'
CUTS={**{f'C{k}':J20+k*DAY for k in range(1,8)}}
TABLE_CUT={'f1':'C1','f2':'C2','f3':'C3','f4':'C4','f5':'C5','f6':'C6','val23':'C3','val25':'C5','val26':'C6','eval':'C7'}
_D=None
def data():
    global _D
    if _D is None: _D=Data()
    return _D
def gapb(sec):  # log2-minute bucket 1..21 (0 = pad)
    return 1+np.clip(np.log2(np.maximum(sec,60)/60.),0,19).astype(np.int64)
def table_events(name):
    """-> dict(rowid, u[n_ev] (user idx or -1), ts, hist list of (items idx array, ts array), cand[n_ev,10] item idx (NI+1 = unknown))"""
    D=data(); v=pl.read_parquet(f'{F3}/{name}.parquet',columns=['rowid']); rid=v['rowid'].to_numpy()
    if name=='eval':
        ee=pl.read_parquet(f'{DW}/evaluation_events.parquet'); er=pl.read_parquet(f'{DW}/evaluation_rows.parquet')
        m=v.join(er.select('rowid','eid','item'),on='rowid',how='left',maintain_order='left'); assert m['eid'].null_count()==0
        eid=m['eid'].to_numpy().reshape(-1,10); assert (eid==eid[:,:1]).all(); eids=eid[:,0]
        e2=pl.DataFrame({'eid':eids}).join(ee,on='eid',how='left',maintain_order='left'); assert e2['ts'].null_count()==0
        ui=np.searchsorted(D.uu,e2['user'].to_numpy()); ok=(ui<D.NU)&(D.uu[np.minimum(ui,D.NU-1)]==e2['user'].to_numpy()); u=np.where(ok,ui,-1)
        ts=e2['ts'].to_numpy(); hi=e2['h_items'].to_list(); ht=e2['h_ts'].to_list()
        def mp(a): a=np.asarray(a,dtype=D.items.dtype) if len(a) else np.zeros(0,D.items.dtype); p=np.minimum(np.searchsorted(D.items,a),D.NI-1); return np.where(D.items[p]==a,p,D.NI+1) if len(a) else np.zeros(0,np.int64)
        H=[(mp(a or []),np.asarray(b or [],np.int64)) for a,b in zip(hi,ht)]
        ci=m['item'].to_numpy(); p=np.minimum(np.searchsorted(D.items,ci),D.NI-1); cand=np.where(D.items[p]==ci,p,D.NI+1).reshape(-1,10)
    else:
        m=v.join(D.rows_rowid,on='rowid',how='left',maintain_order='left'); assert m['e'].null_count()==0
        e=m['e'].to_numpy().astype(np.int64).reshape(-1,10); assert (e==e[:,:1]).all(); es=e[:,0]
        u=D.EU[es].astype(np.int64); ts=D.ETS[es]; H=[(D.HI[D.OFF[x]:D.OFF[x+1]].astype(np.int64),D.HT[D.OFF[x]:D.OFF[x+1]]) for x in es]
        cand=np.searchsorted(D.items,m['item'].to_numpy()).astype(np.int64).reshape(-1,10)
    return dict(rowid=rid,u=u,ts=ts,H=H,cand=cand)
def click_seqs(C):
    """per-user click sequences (items, minute-times) from train events with ts<C. -> (offsets by user, items, times)"""
    D=data(); S=D.snap(C); cu,ci,ct=S['cu'].astype(np.int64),S['ci'].astype(np.int64),S['ct'].astype(np.int64)
    o=np.lexsort((ci,ct,cu)); cu,ci,ct=cu[o],ci[o],ct[o]; off=np.zeros(D.NU+1,np.int64); off[1:]=np.cumsum(np.bincount(cu,minlength=D.NU)); return off,ci,ct
def windows(off,it,tm,stride=25):
    """training windows of L+1 consecutive clicks; right-anchored padding. returns inp,tgt,gap [N,L] int64 (pad item = NI)"""
    D=data(); NI=D.NI; inp=[];tgt=[];gp=[]
    for u in range(len(off)-1):
        a,b=off[u],off[u+1]; n=b-a
        if n<2: continue
        ends=list(range(n,0,-stride)); 
        for e in ends:
            s=max(0,e-(L+1)); w=it[a+s:a+e]; t=tm[a+s:a+e]; k=len(w)-1
            if k<1: continue
            i=np.full(L,NI,np.int64); g=np.zeros(L,np.int64); y=np.full(L,NI,np.int64)
            i[L-k:]=w[:-1]; y[L-k:]=w[1:]; g[L-k:]=gapb(t[1:]-t[:-1]); inp.append(i);tgt.append(y);gp.append(g)
    return np.stack(inp),np.stack(tgt),np.stack(gp)
class Block(nn.Module):
    def __init__(s,d,H,dr):
        super().__init__(); s.H=H; s.n1=nn.LayerNorm(d); s.qkv=nn.Linear(d,3*d); s.o=nn.Linear(d,d); s.n2=nn.LayerNorm(d); s.f=nn.Sequential(nn.Linear(d,2*d),nn.GELU(),nn.Dropout(dr),nn.Linear(2*d,d)); s.dr=nn.Dropout(dr)
    def forward(s,x,mask):
        B,T,d=x.shape; q,k,v=s.qkv(s.n1(x)).view(B,T,3,s.H,d//s.H).permute(2,0,3,1,4)
        a=F.scaled_dot_product_attention(q,k,v,attn_mask=mask,dropout_p=s.dr.p if s.training else 0.).transpose(1,2).reshape(B,T,d)
        x=x+s.dr(s.o(a)); return x+s.dr(s.f(s.n2(x)))
class Seq(nn.Module):
    def __init__(s,NI,d=64,kind='sas',nb=2,H=2,dr=0.2):
        super().__init__(); s.kind=kind; s.NI=NI; s.E=nn.Embedding(NI+2,d,padding_idx=NI); nn.init.normal_(s.E.weight,std=d**-0.5); s.G=nn.Embedding(22,d,padding_idx=0); s.dr=nn.Dropout(dr)
        if kind=='sas': s.P=nn.Embedding(L,d); s.B=nn.ModuleList([Block(d,H,dr) for _ in range(nb)]); s.nf=nn.LayerNorm(d)
        else: s.gru=nn.GRU(d,d,batch_first=True); s.nf=nn.LayerNorm(d)
    def forward(s,it,gp):
        x=s.E(it)+s.G(gp)
        if s.kind=='sas':
            B,T=it.shape; x=s.dr(x+s.P.weight[None,:T]); pad=(it==s.NI); i=torch.arange(T)
            allow=(i[None,:]<=i[:,None])[None]&(~pad)[:,None,:]; allow=allow|torch.eye(T,dtype=torch.bool)[None]
            m=allow[:,None]; 
            for b in s.B: x=b(x,m)
        else:
            x=s.dr(x); x,_=s.gru(x)
        return s.nf(x)
def train(kind,C,epochs=12,K=256,t=0.75,bs=256,lr=2e-3,seed=0,d=64,loss='gbce',log_=print,maxsec=None):
    D=data(); NI=D.NI; torch.manual_seed(seed); np.random.seed(seed); off,it,tm=click_seqs(C); inp,tgt,gp=windows(off,it,tm); N=len(inp)
    inp,tgt,gp=map(torch.from_numpy,(inp,tgt,gp)); m=Seq(NI,d,kind); opt=torch.optim.Adam(m.parameters(),lr=lr); t0=time.time()
    alpha=K/(NI-1); beta=alpha*(t*(1-1/alpha)+1/alpha); nstep=epochs*math.ceil(N/bs); sch=torch.optim.lr_scheduler.OneCycleLR(opt,lr,total_steps=nstep,pct_start=.1)
    for ep in range(epochs):
        m.train(); perm=torch.randperm(N); tl=0;nb=0
        for b in range(0,N,bs):
            j=perm[b:b+bs]; x,y,g=inp[j],tgt[j],gp[j]; h=m(x,g); msk=y!=NI; hv=h[msk]; yv=y[msk]
            neg=torch.randint(0,NI,(K,)); En=m.E.weight[neg]; pos=(hv*m.E.weight[yv]).sum(-1,keepdim=True); ng=hv@En.T; hit=(yv[:,None]==neg[None])
            if loss=='gbce':
                ng=ng.masked_fill(hit,-1e4); p=torch.sigmoid(pos.double()).clamp(1e-10,1-1e-10); pa=p.pow(-beta).clamp(min=1+1e-10); pl_=torch.log((1/(pa-1)).clamp(min=1e-10)).float()
                lg=torch.cat([pl_,ng],1); lab=torch.zeros_like(lg); lab[:,0]=1; l=F.binary_cross_entropy_with_logits(lg,lab,reduction='none'); l=l.sum(1).mean()
            else:
                ng=ng.masked_fill(hit,-1e4); l=F.cross_entropy(torch.cat([pos,ng],1),torch.zeros(len(hv),dtype=torch.long))
            opt.zero_grad(); l.backward(); nn.utils.clip_grad_norm_(m.parameters(),5.); opt.step(); sch.step(); tl+=l.item(); nb+=1
        log_(f'  [{kind} C={(C-J20)//DAY}] ep{ep} loss {tl/nb:.4f} N={N} {time.time()-t0:.0f}s')
    m.eval(); return m
def states(m,tab,C,sub):
    """user state at last position for full history, last-1 and last-3 tokens. returns [n_ev,3,d] (full,last1,last3) """
    D=data(); NI=D.NI; off,it,tm=click_seqs(C); n=len(tab['u']); ITs=np.full((3,n,L),NI,np.int64); Gs=np.zeros((3,n,L),np.int64)
    for i in range(n):
        u=tab['u'][i]; hi,ht=tab['H'][i]; ts=tab['ts'][i]
        if u>=0: a,b=off[u],off[u+1]; li,lt=it[a:b],tm[a:b]
        else: li=np.zeros(0,np.int64); lt=np.zeros(0,np.int64)
        ok=ht<ts; hi,ht=hi[ok],ht[ok]   # history clicks strictly before the event (defensive)
        si=np.concatenate([li,hi]); st=np.concatenate([lt,ht])
        if len(si):
            key=si.astype(np.int64)*(1<<22)+(st-J20+DAY*30)//60; _,ix=np.unique(key,return_index=True); ix=np.sort(ix); si,st=si[ix],st[ix]; o=np.argsort(st,kind='stable'); si,st=si[o],st[o]
            si,st=si[-L:],st[-L:]; k=len(si); g=gapb(np.concatenate([st[1:],[ts]])-st)
            for r,kk in enumerate((L,1,3)):
                kk=min(kk,k); ITs[r,i,L-kk:]=si[k-kk:]; Gs[r,i,L-kk:]=g[k-kk:]
    out=np.zeros((n,3,m.E.weight.shape[1]),np.float32)
    with torch.no_grad():
        for r in range(3):
            for b in range(0,n,4096): out[b:b+4096,r]=m(torch.from_numpy(ITs[r,b:b+4096]),torch.from_numpy(Gs[r,b:b+4096]))[:,-1].numpy()
    has=np.array([len(h[0])>0 or (u>=0 and off[u+1]>off[u]) for u,h in zip(tab['u'],tab['H'])]); return out,has
def featurize(m,tab,C,name):
    st,has=states(m,tab,C,name); W=m.E.weight.detach().numpy(); cand=tab['cand']; Ec=W[cand]  # [n,10,d]
    def sc(h): return np.einsum('nd,ncd->nc',h,Ec)
    def cs(h): return sc(h)/((np.linalg.norm(h,axis=1,keepdims=True)+1e-6)*(np.linalg.norm(Ec,axis=2)+1e-6))
    S=sc(st[:,0]); cols={}
    mu=S.mean(1,keepdims=True); sd=S.std(1,keepdims=True)+1e-6; ls=S-np.log(np.exp(S-S.max(1,keepdims=True)).sum(1,keepdims=True))-S.max(1,keepdims=True)
    cols['dot']=S; cols['cos']=cs(st[:,0]); cols['z']=(S-mu)/sd; cols['gap']=S-S.max(1,keepdims=True); cols['lsm']=ls
    cols['rank']=(np.argsort(np.argsort(-S,1,kind='stable'),1)+1).astype(np.float32)
    for r,nm in ((1,'1'),(2,'3')):
        s_=sc(st[:,r]); cols['dot'+nm+'_dm']=s_-s_.mean(1,keepdims=True); cols['cos'+nm]=cs(st[:,r])
    out={k:np.where(has[:,None],v,np.nan).astype(np.float32).ravel() for k,v in cols.items()}; return out