Sign In

Training the T5 Small to be a lora for diffusion models.

2

May 20, 2025

(Updated: 4 months ago)

ML Research
Training the T5 Small to be a lora for diffusion models.

Why?

It's tiny. 60m parameters and it can be easily fitted into... well anything.

It's insanely easy to lobotomize though, so I've devised a multitude of methods to teach this model new behaviors without squashing it like a bug.

WHY THOUGH!?!?!

Guidance. It's being taught, the rule of 3. It's going to learn, exactly how to guide the image generation in a way that no guidance tool has ever accomplished before.

https://huggingface.co/AbstractPhil/T5-Small-Human-Attentive-Try2-Pass3
Not only that, but... it's working. It's adhering internally to large amounts of data; learning exponential amounts of methods to normalize these captions into solidified rule of 3 depictions based on object association, reason, and more.

The input goes in, the output is starting to diverge from it's echo.

Essentially, I've devised a way to train it's math, without destroying it.

The T5 small is a powerful tool.

You can teach it almost anything in a short period of time, and it will be capable of using that anything with almost no training steps.

If you train it in the wrong way though, it'll fall apart like a house of cards on the roof of a bullet train.

Below is a colab drag and drop - required to set the output repo for uploading to a huggingface repo, or removing the lines that upload to repo - blueprint for training the T5-small's internals in a robust and careful methodology.

I'm essentially DESTROYING it's capability currently. So with this, it's reshaping the internals to conform to the goals of... captions.

The t5 small is trained with 3 forms of translation and 1 command for "summarize: ", which are all inherently incapable of their baseline jobs.

However, I've given it a new purpose; caption. This, is currently defining the rule of 3 pathways that will be trainable into diffusion models soon enough.

This version of the T5-small is different. As it's repurposed, it'll become more and more fluent at these pathways, devising more and more complex methodologies based on it's internals - forming even more accurate guidance than before in more complex methodologies and ways.

Given the drop of adherence and the reduction of loss reaching a certain point, with the multi-prompt response from BLEU reaching a certain peak point; this model will be ready to trained into SDXL as a flat guidance spine lora.

What does it mean though?

I'm teaching the T5 to be a solid guidance structure for any diffusion model; as a supplement to the existing encoders. Like a cybernetic limb, or an ear, or in this case; a part of the brain implanted in a way that guides the vectors in a carefully utilizable way.

It means... it'll guide SDXL in a way that has never been guided before; both speeding up the inference, improving fidelity, quality, and more; all because the baseline SDXL variations will be allowed to focus more of their internal mechanisms ON THEIR OWN GOALS.

It's like having an extra set of eyes helping guide the output - not through a blanket mathematics response, but through an interpolated learned response that can simply be snapped on like a lora.

# train_t5_small_human_attentive_full_pipeline.py

import os, random, csv, collections
from pathlib import Path

import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import IterableDataset

from transformers import (
    T5ForConditionalGeneration,
    T5TokenizerFast,
    DataCollatorForSeq2Seq,
    TrainingArguments,
    Trainer,
    TrainerCallback,
    get_cosine_schedule_with_warmup,
)
from huggingface_hub import login, hf_hub_download
from sacrebleu import corpus_bleu

# ──────────────────────────────────────────────────────────
# 🔐 Login to HF Hub
# ──────────────────────────────────────────────────────────
from google.colab import userdata

os.environ["HF_TOKEN"] = userdata.get('HF_TOKEN')
login(token=os.getenv("HF_TOKEN"))

# ──────────────────────────────────────────────────────────
# ⚙️ Model & Tokenizer (+ higher dropout)
# ──────────────────────────────────────────────────────────
model = T5ForConditionalGeneration.from_pretrained("AbstractPhil/T5-Small-Human-Attentive-Try2-Pass2")
tokenizer = T5TokenizerFast.from_pretrained("t5-small")

# bump dropout for heavy‐LR regime (Tier 6)
model.config.dropout_rate = 0.3
model.config.attention_dropout = 0.3

# Task prefix config
model.config.task_specific_params = {
    "caption": {
        "prefix":         "caption: ",
        "max_length":     64,
        "num_beams":      4,
        "length_penalty": 1.0,
        "early_stopping": True,
    }
}

# unfreeze everything
for p in model.parameters(): p.requires_grad = True

# ──────────────────────────────────────────────────────────
# ░ Tier 2 ░ WeightScheduler “slow-bleed”
# ──────────────────────────────────────────────────────────
class WeightScheduler:
    def __init__(self, total_steps, high_start=5.0, low_start=0.2, end=1.0):
        """
        total_steps: number of steps over which to decay extremes → 1.0
        high_start: initial multiplier for 'high' tokens
        low_start:  initial multiplier for 'low' tokens
        end:        final multiplier for both
        """
        self.total_steps = total_steps
        self.high_start, self.low_start, self.end = high_start, low_start, end

    def get_weight(self, step, token_class):
        """
        step: current global step
        token_class: 0=neutral, 1=high, 2=low (scalar or tensor)
        returns: same shape as token_class, float weights
        """
        frac = min(step / self.total_steps, 1.0)
        high_w = self.high_start + frac * (self.end - self.high_start)
        low_w  = self.low_start  + frac * (self.end - self.low_start)
        # branch on class
        if isinstance(token_class, torch.Tensor):
            w = torch.ones_like(token_class, dtype=torch.float, device=token_class.device)
            w = torch.where(token_class == 1, high_w, w)
            w = torch.where(token_class == 2, low_w,  w)
            return w
        else:
            return {0:1.0, 1:high_w, 2:low_w}[int(token_class)]

# ──────────────────────────────────────────────────────────
# ░ Tier 4 ░ MaskRescaler
# ──────────────────────────────────────────────────────────
class MaskRescaler:
    def __init__(self, window=2, factor=1.2):
        """
        window: number of tokens on each side of a masked position
        factor: multiplier for neighbor weights
        """
        self.window, self.factor = window, factor

    def rescale(self, weights, mask_positions):
        """
        weights: [B, L] tensor of per-token weights
        mask_positions: list of lists of masked indices per example
        """
        B, L = weights.shape
        for b, positions in enumerate(mask_positions):
            for idx in positions:
                left  = max(0, idx - self.window)
                right = min(L, idx + self.window + 1)
                weights[b, left:right] *= self.factor
        return weights

# ──────────────────────────────────────────────────────────
# ░ Tier 5 ░ MixedCaptionDataset (mask+half-swap)
# ──────────────────────────────────────────────────────────
class MixedCaptionDataset(IterableDataset):
    def __init__(self,
                 tokenizer,
                 repo_id: str,
                 examples_per_file: int = 5_000_000,
                 num_files: int = 10,
                 batch_size: int = 256,
                 low_q: float = 0.4,      # mid-quantile mask
                 high_q: float = 0.6,
                 decay_step: int = 1000,  # decay masked counts
                 swap_prob: float = 0.5   # Tier 5 mixing
    ):
        self.tokenizer   = tokenizer
        self.repo_id     = repo_id
        self.files       = [f"captions/caption_{i+2:03d}.csv" for i in range(num_files)]
        self.batch_size  = batch_size
        self.low_q, self.high_q, self.decay_step = low_q, high_q, decay_step
        self.swap_prob   = swap_prob
        self.examples    = examples_per_file * num_files

        # running global counts of label‐tokens
        self.token_counts = collections.Counter()

    def __len__(self):
        return self.examples

    def __iter__(self):
        # stream each file, batch‐tokenize, update counts & mask
        for rel in self.files:
            path = hf_hub_download(self.repo_id, rel, repo_type="dataset")
            with open(path, encoding="utf-8") as f:
                reader, buf = csv.DictReader(f), []
                for row in reader:
                    text = row.get("text","").strip()
                    if not text: continue
                    buf.append(("caption: "+text, text))
                    if len(buf) >= self.batch_size:
                        for sample in self._batch_tokenize(buf):
                            yield sample
                        buf.clear()
                if buf:
                    for sample in self._batch_tokenize(buf):
                        yield sample

    def _batch_tokenize(self, batch):
        inputs, targets = zip(*batch)
        enc = self.tokenizer(
            list(inputs),
            padding="max_length", truncation=True, max_length=256,
            return_tensors="pt"
        )
        lbl = self.tokenizer(
            list(targets),
            padding="max_length", truncation=True, max_length=64,
            return_tensors="pt"
        )

        # 1) update global counts
        flat = lbl.input_ids.view(-1).tolist()
        valid = [tok for tok in flat if tok != self.tokenizer.pad_token_id]
        self.token_counts.update(valid)

        # 2) select mid-quantile to mask
        items = sorted(self.token_counts.items(), key=lambda x: x[1])
        n = len(items)
        if n:
            toks, cnts = zip(*items)
            low_i  = int(n * self.low_q)
            high_i = int(n * self.high_q)
            mask_set = set(toks[low_i:high_i])
        else:
            mask_set = set()

        # 3) decay their counts
        for t in mask_set:
            self.token_counts[t] = max(0, self.token_counts[t] - self.decay_step)

        # 4) now build samples, either masked or half-swap
        samples = []
        pad = self.tokenizer.pad_token_id
        for i in range(len(batch)):
            label_ids = lbl.input_ids[i]
            if random.random() < self.swap_prob:
                # we'll handle swapping higher up: mark with special flag
                samples.append({
                    "input_ids":      enc.input_ids[i].tolist(),
                    "attention_mask": enc.attention_mask[i].tolist(),
                    "labels":         lbl.input_ids[i].tolist(),
                    "_swap":          True
                })
            else:
                # mask mid-quantile tokens
                mask_bool = torch.tensor([tok in mask_set for tok in label_ids])
                masked    = torch.where(mask_bool, -100, label_ids)
                samples.append({
                    "input_ids":      enc.input_ids[i].tolist(),
                    "attention_mask": enc.attention_mask[i].tolist(),
                    "labels":         masked.tolist(),
                    "_swap":          False
                })
        # if any swaps, pair them sequentially
        out = []
        i = 0
        while i < len(samples):
            s = samples[i]
            if s["_swap"] and i+1 < len(samples):
                t = samples[i+1]
                # half-swap inputs & labels between s and t
                i_len = len(s["input_ids"])
                l_len = len(s["labels"])
                hi = i_len//2; hl = l_len//2
                i1 = torch.tensor(s["input_ids"]); i2 = torch.tensor(t["input_ids"])
                l1 = torch.tensor(s["labels"]);     l2 = torch.tensor(t["labels"])
                # swap halves
                new_i1 = torch.cat([i1[:hi], i2[hi:]], dim=0)
                new_i2 = torch.cat([i2[:hi], i1[hi:]], dim=0)
                new_l1 = torch.cat([l1[:hl], l2[hl:]], dim=0)
                new_l2 = torch.cat([l2[:hl], l1[hl:]], dim=0)
                for rec, ni, nl in [(s,new_i1,new_l1),(t,new_i2,new_l2)]:
                    rec["input_ids"]      = ni.tolist()
                    rec["attention_mask"] = ((ni!=pad).long()).tolist()
                    rec["labels"]         = nl.tolist()
                out.extend([s,t])
                i += 2
            else:
                out.append(s)
                i += 1
        return out

# ──────────────────────────────────────────────────────────
# ░ Tier 2/3/4 ░ WeightedTrainer
# ──────────────────────────────────────────────────────────
class WeightedTrainer(Trainer):
    def __init__(self, *args,
                 weight_scheduler: WeightScheduler,
                 mask_rescaler:    MaskRescaler,
                 low_q_extreme:  float = 0.1,
                 high_q_extreme: float = 0.9,
                 **kwargs):
        super().__init__(*args, **kwargs)
        # inject our scheduler & rescaler
        self.weight_scheduler = weight_scheduler
        self.mask_rescaler    = mask_rescaler
        self.low_q_extreme    = low_q_extreme
        self.high_q_extreme   = high_q_extreme

        # precompute total steps for scheduler
        if self.args.max_steps > 0:
            total = self.args.max_steps
        else:
            # approximate
            ds_len = len(self.train_dataset)
            bs     = self.args.per_device_train_batch_size * self.args.gradient_accumulation_steps
            total  = int(ds_len/bs) * self.args.num_train_epochs
        self.weight_scheduler.total_steps = total

    def compute_loss(self, model, inputs, return_outputs=False, **kwargs):
        # 1) Remove labels and our custom swap flag
        labels = inputs.pop("labels")
        inputs.pop("_swap", None)

        # 2) Forward pass
        outputs = model(**inputs, labels=labels)
        logits  = outputs.logits       # shape: [B, L, V]
        B, L, V = logits.size()

        # 3) Flatten labels and select valid positions (ignore_index = -100)
        flat_labels = labels.view(-1)
        valid_mask  = flat_labels != -100
        valid_idx   = valid_mask.nonzero(as_tuple=True)[0]
        valid_lbls  = flat_labels[valid_idx]

        # 4) Get current global step
        step = self.state.global_step

        # ── Tier 2: classify tokens into high/low/neutral based on global counts ──
        gc    = self.train_dataset.token_counts
        items = sorted(gc.items(), key=lambda x: x[1])
        n     = len(items)
        low_i  = int(n * self.low_q_extreme)
        high_i = int(n * self.high_q_extreme)
        low_set  = set(tok for tok, _ in items[:low_i])
        high_set = set(tok for tok, _ in items[high_i:])
        # Build a class vector: 0=neutral, 1=high, 2=low
        classes = torch.zeros_like(flat_labels)
        cls_list = []
        for tid in valid_lbls.tolist():
            if tid in high_set:
                cls_list.append(1)
            elif tid in low_set:
                cls_list.append(2)
            else:
                cls_list.append(0)
        classes[valid_idx] = torch.tensor(cls_list, device=classes.device)

        # 5) Tier 2: compute slow-bleed weights
        bleed_w = self.weight_scheduler.get_weight(step, classes)

        # ── Tier 3: inverse-frequency weighting (per-batch) ──
        uniq, cnts = valid_lbls.unique(return_counts=True)
        inv        = cnts.max().float() / cnts.float()
        inv_freq   = torch.ones(V, device=logits.device)
        inv_freq[uniq] = inv
        inv_w_flat = torch.ones_like(flat_labels, dtype=logits.dtype)
        inv_w_flat[valid_idx] = inv_freq[valid_lbls]

        # 6) Combine bleed + inverse-frequency
        w_flat  = bleed_w * inv_w_flat
        weights = w_flat.view(B, L)

        # ── Tier 4: mask-triggered local rescaling ──
        mask_pos = [(labels[b] == -100).nonzero(as_tuple=True)[0].tolist() for b in range(B)]
        weights  = self.mask_rescaler.rescale(weights, mask_pos)

        # 7) Compute weighted negative log-likelihood loss
        logprobs = F.log_softmax(logits, dim=-1).view(-1, V)
        sel_lp   = logprobs[valid_idx, valid_lbls]
        w_sel    = w_flat[valid_idx]
        loss     = -(w_sel * sel_lp).sum() / w_sel.sum()

        return (loss, outputs) if return_outputs else loss


# ──────────────────────────────────────────────────────────
# ░ Tier 7 ░ BLEU Eval Callback
# ──────────────────────────────────────────────────────────
class BLEUCallback(TrainerCallback):
    def __init__(self, model, tokenizer, references, prompts, every_n_steps=500):
        self.model     = model
        self.tokenizer = tokenizer
        self.refs      = references  # list of list of str
        self.prompts   = prompts     # list of str
        self.every_n   = every_n_steps
        self.metric    = corpus_bleu

    def on_step_end(self, args, state, control, **kwargs):
        if state.global_step and state.global_step % self.every_n == 0:
            self.model.eval()
            preds = []
            for p in self.prompts:
                batch = self.tokenizer(
                    "caption: " + p,
                    truncation=True, padding="max_length",
                    max_length=256, return_tensors="pt"
                ).to(self.model.device)
                out = self.model.generate(
                    **batch,
                    max_length=64,
                    num_beams=4,
                    early_stopping=True,
                )
                dec = self.tokenizer.decode(out[0], skip_special_tokens=True)
                preds.append(dec)
            score = self.metric(preds, [self.refs]).score
            print(f"▶️  Step {state.global_step}: mixed‐caption BLEU = {score:.2f}")
            self.model.train()

# ──────────────────────────────────────────────────────────
# 🧠 Assemble & Launch
# ──────────────────────────────────────────────────────────

# held‐out mix references & prompts for BLEU
# held‐out mix references & prompts for BLEU
mix_prompts = [
    "a room of tacos",
    "a forest of neon mushrooms",
    "a beach covered in colorful seashells",
    "a mountain peak above the clouds",
    "a desk cluttered with vintage cameras",
    "a city skyline at dusk",
    "a cat sleeping on a windowsill",
    "a bouquet of wildflowers in a mason jar",
    "a red sports car on a desert road",
    "a steaming cup of coffee on a book",
    "a group of hot air balloons",
    "a snowy village at night",
    "a winding forest path in autumn",
    "a pair of sneakers by the door",
    "a stack of pancakes with syrup",
    "a woodland stream with stones",
    "a baby elephant playing in mud",
    "a painted guitar leaning on a chair",
    "a row of colorful umbrellas on a beach",
    "a vintage typewriter on a desk",
]

mix_references = [
    "brightly lit room filled entirely with tacos",
    "an enchanted forest glowing under neon mushrooms",
    "a sunlit beach strewn with a rainbow of seashells",
    "a solitary mountain peak rising above a sea of clouds",
    "a wooden desk overflowing with vintage film cameras",
    "a city skyline silhouetted against a dusky purple sky",
    "a fluffy cat curled up on a sunlit windowsill",
    "a mason jar brimming with wildflowers picked from a meadow",
    "a sleek red sports car speeding along an empty desert highway",
    "a steaming cup of coffee resting on an open leather-bound book",
    "a colorful fleet of hot air balloons drifting across a clear sky",
    "a quiet snowy village illuminated by warm street lamps",
    "a winding forest trail blanketed in golden autumn leaves",
    "a worn pair of sneakers casually placed by the wooden door",
    "a stack of fluffy pancakes drenched in maple syrup",
    "a gentle woodland stream flowing over moss-covered stones",
    "a baby elephant joyfully splashing mud with its trunk",
    "a brightly painted guitar leaning against a wooden chair",
    "a line of vibrant umbrellas casting shade on the sandy beach",
    "a retro typewriter sitting on a cluttered writing desk",
]


dataset  = MixedCaptionDataset(
    tokenizer,
    repo_id="AbstractPhil/human-templated-captions-1b",
    examples_per_file=5_000_000,
    num_files=2,
    batch_size=256,
    low_q=0.4, high_q=0.6,
    decay_step=1000,
    swap_prob=0.5,
)
collator = DataCollatorForSeq2Seq(tokenizer, model=model, label_pad_token_id=-100)

# build scheduler + rescaler
# we’ll fill total_steps after Trainer init
dummy_total = 1_000_000
weight_scheduler = WeightScheduler(total_steps=dummy_total,
                                   high_start=5.0, low_start=0.2, end=1.0)
mask_rescaler    = MaskRescaler(window=2, factor=1.2)

training_args = TrainingArguments(
    output_dir                 = "./checkpoints-full",
    per_device_train_batch_size=256,
    gradient_accumulation_steps=4,
    dataloader_num_workers     =6,
    learning_rate              =1e-3,
    optim                      ="adafactor",
    weight_decay               =0.01,
    lr_scheduler_type          ="cosine",
    warmup_steps               =500,
    num_train_epochs           =4,
    logging_steps              =50,
    save_steps                 =500,
    save_total_limit           =5,
    max_grad_norm              =1.0,           
    bf16                       =True,
    remove_unused_columns      =False,
    push_to_hub                =True,
    hub_model_id               ="AbstractPhil/T5-Small-Human-Attentive-Try2-Pass3",
    report_to                  ="none",
)

trainer = WeightedTrainer(
    model            =model,
    args             =training_args,
    train_dataset    =dataset,
    data_collator    =collator,
    callbacks        =[
        BLEUCallback(model, tokenizer, mix_references, mix_prompts, every_n_steps=50)
    ],
    weight_scheduler =weight_scheduler,
    mask_rescaler    =mask_rescaler,
    low_q_extreme    =0.1,
    high_q_extreme   =0.9,
)

if __name__ == "__main__":
    trainer.train()
    trainer.push_to_hub()

2