ADVERTISING / NOTEBOOK LESSON

55 · A10 · Real CTR: linear versus FM interactions at a fixed budget

Compare real linear and FM training at one controlled budget, retaining FM overfitting.

Real-data experimentRead-only · saved experiment

Saved outputs come from recorded teaching runs; opening this page does not rerun an experiment. The code and original outputs are preserved; original-language data and annotations may remain.

A10 · Real CTR: linear versus FM interactions at a fixed budget

Prerequisites: foundations 31–34; A09 (chapters 19–20).

Evidence: real sampled-impression training and select diagnostics. This chapter does not claim industrial-system reproduction or online gains.

Learning order: predict → change one variable → run → explain.

English companion. All instructional Markdown is translated. Experiment code, Chinese language examples, execution counts, and saved outputs remain as in the source edition; historical output text may therefore be Chinese. The initial setup cell anchors the working directory and Python import path for this companion's deeper folder; it does not change the experiment. Saved outputs are historical evidence, not a new run. Run from the top after preparing the required data and models.

PYTHON · 01
# English companions live one directory deeper; keep the original experiment code unchanged.
from pathlib import Path as _TranslationPath
import os as _translation_os
import sys as _translation_sys
_translation_root = next((p for p in [_TranslationPath.cwd(), *_TranslationPath.cwd().parents]
                          if (p / "reclab").is_dir()), None)
if _translation_root is None:
    raise RuntimeError("Open this notebook from inside the Search Recommendation Ads Lab checkout.")
_translation_os.chdir(_translation_root)
if str(_translation_root) not in _translation_sys.path:
    _translation_sys.path.insert(0, str(_translation_root))

Problem and input/output contract

FM logit=b+Σw_i+Σ_{i<j}<v_i,v_j>. Reuse the recommendation course’s fm_interaction vector identity instead of duplicating algorithms. Both arms use the same 26 categorical fields, train-only top-64 vocabulary, training batches, eight epochs, and Adam. The only architectural change is four-dimensional interaction factors; differing parameter counts are explicitly reported. Use the first 10k real impressions of the original pinned Criteo shard without new data. These are select development diagnostics, never audit selection or replacement of formal latest.

Before running, predict which result the changed variable will affect and which invariants must hold.

PYTHON · 02
from pathlib import Path
import sys
root=Path.cwd()
if not (root/'reclab').exists(): root=root.parent
if str(root) not in sys.path: sys.path.insert(0,str(root))
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from reclab import ads_curriculum as ac

Run the one-variable comparison

Read a saved actual training run. Explicit retraining is available through the source function; opening the lesson does not automatically change models.

PYTHON · 03
from reclab.paths import ROOT,read_json
summary=read_json(ROOT/'results/2026-10-30-ads-curriculum/fm.json')
print('run',summary['run_id']);display(pd.DataFrame([{'method':r['method'],'parameters':r['parameters'],**{k:r['select'][k] for k in ('n','auc','logloss','brier')}} for r in summary['result']['rows']]))
for row in summary['result']['rows']: plt.plot(row['train_loss'],label=row['method'])
plt.xlabel('epoch');plt.ylabel('train logloss');plt.legend();plt.show()
from reclab.learning.networks import fm_interaction,fm_explicit
import torch
x=torch.randn(3,4,5);assert torch.allclose(fm_interaction(x),fm_explicit(x),atol=1e-5)
Saved notebook output · not executed on this page
run 20261030T143314-b0538124
method parameters n auc logloss brier
0 linear 1691 1534 0.607724 0.132267 0.027532
1 fm 8451 1534 0.558951 0.178256 0.031998
<Figure size 640x480 with 1 Axes>
Saved notebook figure

Interpretation and limits

Lower training loss means better training fit. Better select results still do not establish a formal audit improvement. One seed, a small vocabulary, and categorical-only features intentionally limit budget. Saved results come from actual training; explicitly call ctr_interaction_workshop to retrain, creating a new registry record and retaining negative results.

Actual saved output: select logloss is 0.1323 for linear and 0.1783 for FM. FM achieves lower training loss but worse select loss, showing overfitting in this small-budget configuration. Keep this negative result without dismissing FM generally or claiming real online performance.

Inspect source, then try an exercise

Predict why FM might overfit. In a new workshop version, change only factor dimension and record parameter count. A repeatedly optimized select set is not an independent test.

PYTHON · 04
import inspect
print(inspect.getsource(ac.ctr_interaction_workshop))
Saved notebook output · not executed on this page
def ctr_interaction_workshop(rows=10000, epochs=8):
    """Real sampled Criteo categorical LR versus FM; shared recommendation FM operator."""
    import torch
    import time
    from torch import nn
    from .learning.networks import fm_interaction
    from .ads import load_data, split_groups, vocabulary, encode, verify_sources, CAT
    from .probability import evaluate
    from .experiments import begin, finish
    from .paths import ROOT, read_json, atomic_json
    if rows!=10000 or epochs!=8: raise ValueError('Frozen teaching budget: 10k rows, 8 epochs')
    folder=begin('ads',{'workshop':'categorical_fm','rows':rows,'epochs':epochs,'seed':42,'evaluation':'select only'},read_json(ROOT/'docs/ads_sources.json'))
    try:
        verify_sources();frame,_=load_data(rows);masks,_=split_groups(frame)
        vocab=vocabulary(frame.loc[masks['train']],64)
        x=encode(frame,vocab)[:,13:].astype(np.int64)+np.arange(len(CAT))*65
        y=frame.label.to_numpy(dtype=np.float32)
        xt=torch.tensor(x);yt=torch.tensor(y)
        tr=torch.tensor(np.flatnonzero(masks['train'])); se=torch.tensor(np.flatnonzero(masks['select']))
        class Response(nn.Module):
            def __init__(self, interaction):
                super().__init__();self.linear=nn.Embedding(26*65,1);self.bias=nn.Parameter(torch.zeros(()));self.interaction=interaction
                nn.init.zeros_(self.linear.weight)
                if interaction: self.factors=nn.Embedding(26*65,4);nn.init.normal_(self.factors.weight,std=.01)
            def forward(self,x):
                logit=self.linear(x).sum((1,2))+self.bias
                return logit+fm_interaction(self.factors(x)) if self.interaction else logit
        results=[]; old_threads=torch.get_num_threads();torch.set_num_threads(2)
        try:
            for interaction in (False,True):
                started=time.perf_counter()
                torch.manual_seed(42);model=Response(interaction);optimizer=torch.optim.Adam(model.parameters(),lr=.02,weight_decay=1e-5)
                generator=torch.Generator().manual_seed(42);curve=[]
                for _ in range(epochs):
                    order=tr[torch.randperm(len(tr),generator=generator)]
                    for batch in order.split(256):
                        optimizer.zero_grad();loss=nn.functional.binary_cross_entropy_with_logits(model(xt[batch]),yt[batch]);loss.backward();optimizer.step()
                    with torch.no_grad():curve.append(float(nn.functional.binary_cross_entropy_with_logits(model(xt[tr]),yt[tr])))
                with torch.no_grad():p=model(xt[se]).sigmoid().numpy()
                name='fm' if interaction else 'linear'
                torch.save(model.state_dict(),folder/(name+'.pt'))
                np.savez_compressed(folder/(name+'.npz'),select_probability=p,select_row=se.numpy(),label=y[se.numpy()])
                results.append(dict(method=name,features=26,fit_seconds=time.perf_counter()-started,parameters=sum(p.numel() for p in model.parameters()),train_loss=curve,select=evaluate(y[se.numpy()],p)))
        finally: torch.set_num_threads(old_threads)
        atomic_json(folder/'vocabulary.json',vocab)
        result=dict(kind='workshop',rows=results,shapes={'raw':[rows,39],'categorical':[rows,26],'train':len(tr),'select':len(se)},
                    changed_variable='FM pair interactions; extra factor parameters explicitly counted; same rows/order/epochs/optimizer',
                    scope='Real sampled Criteo categorical-only select diagnostic, not independent audit or population CTR.',
                    limits='Capped train vocab, small prefix, one seed, numeric features omitted equally; no production selection or revenue claim.')
        finish(folder,result);return folder.name,result
    except Exception as exc:
        receipt=read_json(folder/'receipt.json');receipt.update(state='failed',error=str(exc));atomic_json(folder/'receipt.json',receipt);raise

Sources and next step

A-RTB pp. 39–69, with FM on p. 58; reuses the Lab’s recommendation chapter 06 mechanism. This is an original Lab exercise, not a copy of the complete lecture text.

Chinese GitHub chapter · Authors’ RTB slides. The Chinese deck is not a complete modern advertising-engineering course. Historical descriptions of platforms are not presented as current market facts.

After the exercise, return to the ads learning path and continue to the next semantic unit.

GO TO THE SOURCE

Sources for this lesson

Author tutorialEnglish

Weinan Zhang / Jian Xu · CIKM 2016 RTB Tutorial

PDF page 58: factorization machines

164 author slides covering response prediction, bidding, attribution, pacing, targeting and reserve prices. Lab mechanisms are independently authored, not full paper replications.

Author tutorialEnglish

Weinan Zhang / Jian Xu · CIKM 2016 RTB Tutorial

164-page PDF: unit links open the relevant page

164 author slides covering response prediction, bidding, attribution, pacing, targeting and reserve prices. Lab mechanisms are independently authored, not full paper replications.

Completion marks are optional and stored only in this browser. Reading a page does not mark it complete.