import nbdev; nbdev.nbdev_export()Finetune
The fine-tuning layer attaches task heads to the pretrained PRAGMA entity representation. The implemented path supports labelled entity batches, classification, regression, pretrained-weight loading, frozen-backbone training followed by full fine-tuning, and fastai metrics.
Labels remain in a Polars table keyed by entity_col. PRAGMATaskLoader wraps PRAGMADataLoader, looks up the target for each batch uid, and yields (batch, target) pairs. pragma_task_dl and pragma_task_dls provide the single- and train/validation-loader APIs.
pragma_task_dls
def pragma_task_dls(
train_shards, valid_shards, train_labels, valid_labels, entity_col, target_col:str='target',
max_tokens:int=12000, dtype:dtype=torch.int64, tok:NoneType=None, mask:bool=False
):Call self as a function.
pragma_task_dl
def pragma_task_dl(
shards, labels, entity_col, target_col:str='target', max_tokens:int=12000, shuffle:bool=False,
dtype:dtype=torch.int64, tok:NoneType=None, mask:bool=False
):Call self as a function.
PRAGMATaskLoader
def PRAGMATaskLoader(
shards, labels, entity_col, target_col:str='target', max_tokens:int=12000, shuffle:bool=False, tok:NoneType=None,
mask:bool=False, dtype:dtype=torch.int64
):Yield PRAGMA entity batches paired with labels from a keyed Polars table.
TaskHead is a normalized feed-forward prediction head over the entity vector. Set n_out for classification or use n_out=1, squeeze=True for scalar regression.
TaskHead
def TaskHead(
d_model, n_out, hidden:NoneType=None, p:float=0.1, squeeze:bool=False
):Same as nn.Module, but no need for subclasses to call super().__init__
pragma_user_emb reuses the backbone’s profile, event, calendar, and history encoders to produce one vector per entity. PRAGMATaskModel applies a task-specific TaskHead to those vectors.
PRAGMATaskModel
def PRAGMATaskModel(
backbone, head
):Same as nn.Module, but no need for subclasses to call super().__init__
pragma_user_emb
def pragma_user_emb(
m, b
):Call self as a function.
pragma_model supplies the pretrained backbone and its d_model width. get_classification_model and get_regression_model construct task models, optionally loading pretrained weights through load_pretrained. pragma_task_splitter exposes separate backbone and head parameter groups to fastai.
pragma_task_splitter
def pragma_task_splitter(
m
):Call self as a function.
loading model and using it
get_model
def get_model(
keys, vals, n_out, sz:str='S', squeeze:bool=False, head:NoneType=None, pretrain:bool=False,
pretrain_path:NoneType=None
):Call self as a function.
load_pretrained
def load_pretrained(
m, path
):Call self as a function.
get_regression_model
def get_regression_model(
keys, vals, sz:str='S', head:NoneType=None, pretrain:bool=False, pretrain_path:NoneType=None
):Call self as a function.
get_classification_model
def get_classification_model(
keys, vals, sz:str='S', n_classes:int=2, head:NoneType=None, pretrain:bool=False, pretrain_path:NoneType=None
):Call self as a function.
Example
Load the UCI credit dataset, create entity IDs and binary targets, infer categorical and continuous columns, make a reproducible entity-level split, and convert the data into PRAGMA sources and tokenized shards.
from ucimlrepo import fetch_ucirepo
credit = fetch_ucirepo(id=27)
features = pl.from_pandas(credit.data.features).with_row_index('entity_id')
targets = pl.from_pandas(credit.data.targets).with_row_index('entity_id').rename({'A16':'target'})
targets = targets.with_columns((pl.col('target') == '+').cast(pl.Int64))
cats = [c for c,t in features.schema.items() if t == pl.String]
conts = [c for c,t in features.schema.items() if t in (pl.Int64, pl.Float64)]
rng = np.random.default_rng(42)
ids = rng.permutation(features['entity_id'].to_numpy())
n_train = int(len(ids) * .8)
train_ids,valid_ids = ids[:n_train],ids[n_train:]
train_labels = targets.filter(pl.col('entity_id').is_in(train_ids))
valid_labels = targets.filter(pl.col('entity_id').is_in(valid_ids))
profile = DataSource(features.select('entity_id').lazy(), entity_col='entity_id', is_profile=True, name='credit_profile')
event_df = features.with_columns(pl.lit('2020-01-01').str.to_datetime().alias('event_time'))
events = DataSource(event_df.lazy(), cats=cats, conts=conts, entity_col='entity_id', time_col='event_time', name='credit_event')
out = Path('data/credit_pragma')
ds = PRAGMADataset(profile=profile, events=[events], entity_col='entity_id', out_path=out)
out.mkdir(exist_ok=True)
out_dir,k,v = ds.write_kv(eval_time='2026-01-01', n_shards=5)
shards = sorted(out_dir.glob('shard_*.parquet'))Keys: 20, Vals: 137, BPE: none
tokenizing profile
tokenizing event source 0: credit_event
combining sources
Define a helper that filters each tokenized shard by the entity IDs in a label table and writes the filtered shards to a new directory.
def split_shards(shards, labels, entity_col, out):
out = Path(out)
out.mkdir(parents=True, exist_ok=True)
ids = labels[entity_col].to_list()
for i,p in enumerate(shards):
df = pl.read_parquet(p).filter(pl.col(entity_col).is_in(ids))
if len(df): df.write_parquet(out/f'shard_{i}.parquet')
return sorted(out.glob('shard_*.parquet'))Create separate shard directories containing only the entities assigned to training or validation. In the example, it is a random entity-level split:
ids = rng.permutation(features['entity_id'].to_numpy())
n_train = int(len(ids) * .8)
train_ids,valid_ids = ids[:n_train],ids[n_train:]So 80% of entities go to training and 20% to validation. The fixed seed (42) makes the split reproducible.
train_shards = split_shards(shards, train_labels, 'entity_id', out/'train')
valid_shards = split_shards(shards, valid_labels, 'entity_id', out/'valid')Example
The implemented example builds entity-level labels, creates a reproducible train/validation split, filters tokenized shards with split_shards, constructs task dataloaders, and trains classification or regression heads. The same APIs also support loading a pretrained checkpoint before fine-tuning.
cls_model = get_classification_model(k, v)
cls_dls = pragma_task_dls(train_shards, valid_shards, train_labels, valid_labels, 'entity_id',dtype=torch.long)
cls_learn = Learner(cls_dls, cls_model, loss_func=nn.CrossEntropyLoss(), opt_func=Adam, metrics=accuracy,splitter=pragma_task_splitter)
cls_learn.freeze()
cls_learn.fit_one_cycle(2, 1e-3)
cls_learn.unfreeze()
cls_learn.fit_one_cycle(3, slice(1e-5, 1e-4))
cls_preds,cls_targs = cls_learn.get_preds(dl=cls_learn.dls.valid)
accuracy(cls_preds, cls_targs)| epoch | train_loss | valid_loss | accuracy | time |
|---|---|---|---|---|
| 0 | 0.693744 | 0.672728 | 0.608696 | 00:01 |
| 1 | 0.691743 | 0.654900 | 0.608696 | 00:00 |
| epoch | train_loss | valid_loss | accuracy | time |
|---|---|---|---|---|
| 0 | 0.683182 | 0.654728 | 0.608696 | 00:00 |
| 1 | 0.685655 | 0.652424 | 0.608696 | 00:00 |
| 2 | 0.684643 | 0.651335 | 0.608696 | 00:00 |
TensorBase(0.6087)
Extract the learned classification model’s user embeddings without tracking gradients; the final expression checks their shape.
b,y = first(cls_dls.train)
emb = get_emb(cls_model, to_device(b))
emb.shapetorch.Size([552, 192])
Prepare regression labels, split the tokenized shards using the same entity split, build regression dataloaders, train the regression head and backbone, and evaluate with RMSE.
reg_targets = (
pl.from_pandas(credit.data.targets)
.with_row_index('entity_id')
.rename({'A16': 'target'})
.with_columns(
pl.col('target').replace({'+': 1, '-': 0}).cast(pl.Int64),
pl.when(pl.col('entity_id').is_in(train_ids))
.then(pl.lit('train'))
.otherwise(pl.lit('valid'))
.alias('split'))
)
reg_train_labels = reg_targets.filter(pl.col('split') == 'train')
reg_valid_labels = reg_targets.filter(pl.col('split') == 'valid')
reg_train_shards = split_shards(shards, reg_train_labels, 'entity_id', out/'reg_train')
reg_valid_shards = split_shards(shards, reg_valid_labels, 'entity_id', out/'reg_valid')
reg_dls = pragma_task_dls(
reg_train_shards, reg_valid_shards,
reg_train_labels, reg_valid_labels,
'entity_id',
dtype=torch.float32)
reg_model = get_regression_model(k, v)
reg_learn = Learner(reg_dls, reg_model, loss_func=MSELossFlat(), opt_func=Adam, metrics=rmse, splitter=pragma_task_splitter)
reg_learn.freeze()
reg_learn.fit_one_cycle(2, 1e-3)
reg_learn.unfreeze()
reg_learn.fit_one_cycle(3, slice(1e-5, 1e-4))
reg_preds,reg_targs = reg_learn.get_preds(dl=reg_learn.dls.valid)
rmse(reg_preds, reg_targs)| epoch | train_loss | valid_loss | _rmse | time |
|---|---|---|---|---|
| 0 | 0.589223 | 0.471452 | 0.686624 | 00:00 |
| 1 | 0.575011 | 0.267196 | 0.516910 | 00:00 |
| epoch | train_loss | valid_loss | _rmse | time |
|---|---|---|---|---|
| 0 | 0.341010 | 0.265658 | 0.515421 | 00:00 |
| 1 | 0.341791 | 0.249973 | 0.499973 | 00:00 |
| 2 | 0.332000 | 0.249444 | 0.499444 | 00:00 |
TensorBase(0.4994)
Pretrain then finetune
The complete transfer workflow is implemented: pretrain PRAGMAModel on event-value MLM, save the learner checkpoint, load its backbone weights with get_classification_model(..., pretrain=True, pretrain_path=...), then train the task head and optionally unfreeze the backbone.
labels = (
pl.from_pandas(credit.data.targets)
.with_row_index('entity_id')
.rename({'A16':'target'})
.with_columns(pl.col('target').replace({'+':1, '-':0}).cast(pl.Int64)))
rng = np.random.default_rng(42)
ids = rng.permutation(labels['entity_id'].to_numpy())
n_train = int(len(ids) * .8)
train_ids,valid_ids = ids[:n_train],ids[n_train:]
train_labels = labels.filter(pl.col('entity_id').is_in(train_ids))
valid_labels = labels.filter(pl.col('entity_id').is_in(valid_ids))
for d in [out/'train', out/'valid']:
d.mkdir(parents=True, exist_ok=True)
for p in d.glob('shard_*.parquet'): p.unlink()
train_shards = split_shards(shards, train_labels, 'entity_id', out/'train')
valid_shards = split_shards(shards, valid_labels, 'entity_id', out/'valid')
dict(
total=len(labels),
train=len(train_labels),
valid=len(valid_labels),
train_positive=train_labels['target'].mean(),
valid_positive=valid_labels['target'].mean(),
train_shards=len(train_shards),
valid_shards=len(valid_shards)){'total': 690,
'train': 552,
'valid': 138,
'train_positive': 0.4583333333333333,
'valid_positive': 0.391304347826087,
'train_shards': 5,
'valid_shards': 5}
tok = ds.tokenizer
pretrain_dls = pragma_dls(train_shards, valid_shards, tok, max_tokens=1500, valid_batches=1, prefetch=0)
pretrain_learn = pragma_learner(pretrain_dls, tok.n_keys, tok.n_vals,sz='S')
pretrain_learn.fit_one_cycle(1, lr_max=1e-3)
pretrain_learn.save('pragma_pretrain')| epoch | train_loss | valid_loss | time |
|---|---|---|---|
| 0 | 4.484961 | 3.812326 | 00:01 |
Path('models/pragma_pretrain.pth')
pretrain_path = Path('models/pragma_pretrain.pth')
pretrain_path.exists()True
cls_dls = pragma_task_dls(train_shards, valid_shards, train_labels, valid_labels, 'entity_id',dtype=torch.long)
cls_model = get_classification_model(k,v, sz='S',n_classes=2,pretrain=True,pretrain_path=pretrain_path)
cls_learn = Learner(cls_dls, cls_model, loss_func=nn.CrossEntropyLoss(), opt_func=Adam, metrics=accuracy, splitter=pragma_task_splitter)
cls_learn.freeze()
cls_learn.fit_one_cycle(1, 1e-3)
cls_learn.unfreeze()
cls_learn.fit_one_cycle(1, slice(1e-5, 1e-4))| epoch | train_loss | valid_loss | accuracy | time |
|---|---|---|---|---|
| 0 | 0.721444 | 0.735318 | 0.391304 | 00:00 |
| epoch | train_loss | valid_loss | accuracy | time |
|---|---|---|---|---|
| 0 | 0.717924 | 0.734432 | 0.391304 | 00:00 |