contemmcm/shieldstral-1.0-3b-cls-full-bluesky-moderation
shieldstral-1.0-3b-cls-full-bluesky-moderation
This model is a fine-tuned version of mistralai/Shieldstral-1.0-3B on the ModerationBenchV2 dataset. It achieves the following results on the evaluation set:
- Loss: 0.5876
- Micro F1: 0.6811
- Macro F1: 0.2808
- Macro F1 Seen: 0.3251
- Macro Ap: 0.3927
- Exact Match: 0.8071
- Safe Accuracy: 0.8897
Model description
A multi-label content-moderation classifier for Bluesky posts (text and images). It is a full fine-tune of Shieldstral 1.0 3B, a safety model on the Mistral 3 / Pixtral backbone. Shieldstral's original yes/no generative output is replaced by a classification head: one forward pass returns 22 independent sigmoid scores, one per moderation label, trained with binary cross-entropy. A post can carry several labels at once, and no label reaching its threshold means "safe". There is no generated text to parse.
The 22 labels, in output order (also in label_space.json):
How to use
transformers ships no classification head for Mistral 3, so the script defines one: three lines composing the library's generic head with the Mistral 3 backbone. The weights load into it directly, with nothing missing and no key mapping. One detail fails silently, so keep it:
- Image size: shrink images so the longest side is at most 896 pixels, as in training. The saved processor accepts up to 1,540 pixels, so an unresized photo is passed through at a resolution the model never saw (2.6 times the visual tokens for the example below).
Requires transformers>=5. The checkpoint is stored in float32; the script loads it in bfloat16, which gives the same predictions.
import json
from urllib.request import urlopen
import torch
from huggingface_hub import hf_hub_download
from PIL import Image
from transformers import AutoProcessor
from transformers.modeling_layers import GenericForSequenceClassification
from transformers.models.mistral3.modeling_mistral3 import Mistral3PreTrainedModel
MODEL = "contemmcm/shieldstral-1.0-3b-cls-full-bluesky-moderation"
# must be exactly this text: the model was trained with it
SYSTEM_PROMPT = (
"You are a content moderation classifier for social media posts. "
"Read the post and assess which moderation labels apply."
)
class Mistral3ForSequenceClassification(
GenericForSequenceClassification, Mistral3PreTrainedModel
):
"""transformers ships no classification head for Mistral 3. This is its generic one
(a linear layer over the last token) on the Mistral 3 / Pixtral backbone, which is
the class the checkpoint was trained as."""
processor = AutoProcessor.from_pretrained(MODEL)
model = Mistral3ForSequenceClassification.from_pretrained(
MODEL, dtype=torch.bfloat16, device_map="cuda:0"
).eval()
labels = json.load(open(hf_hub_download(MODEL, "label_space.json")))["labels"]
thresholds = json.load(open(hf_hub_download(MODEL, "thresholds.json")))["thresholds"]
# A real post: https://bsky.app/profile/did:plc:3htuuatm2fchjvon7tpc2jge/post/3mtd2ffcsvk22
text = (
"Get Your Summer Tees Here & Please FOLLOW & SHARE. Tees Starting At Just $16. (Wait for "
"the 35-40% off sales, 2-3 times a month.) I, Also, Have Mugs, Tote Bags, Magnets, "
"Stickers, Etc., Available. Over 250 Designs! If You DO Make A Purchase, Thanks In "
"Advance! www.teepublic.com/user/roszelle-art"
)
image = Image.open(urlopen(
"https://cdn.bsky.app/img/feed_fullsize/plain/did:plc:3htuuatm2fchjvon7tpc2jge/"
"bafkreigp5m25icqkxlrm6pvbpzvey2g5bchrqonlyaypo7cs6ahqsekaey"
)).convert("RGB")
# Pixtral spends visual tokens in proportion to resolution, and the model was trained on
# images whose longest side is at most 896 pixels. Larger ones must be shrunk first.
image.thumbnail((896, 896), Image.BILINEAR)
messages = [
{"role": "system", "content": [{"type": "text", "text": SYSTEM_PROMPT}]},
{"role": "user", "content": [{"type": "image", "image": image},
{"type": "text", "text": text}]},
]
inputs = processor.apply_chat_template(
messages, add_generation_prompt=True, tokenize=True, return_dict=True, return_tensors="pt"
).to(model.device, dtype=torch.bfloat16)
with torch.no_grad():
probs = torch.sigmoid(model(**inputs).logits[0].float()).tolist()
# a label applies when its probability reaches that label's threshold; none means "safe"
for name, p, cut in zip(labels, probs, thresholds):
if p >= cut:
print(f"{name}: {p:.3f}")Output:
spam: 1.000Pass every image of the post, in order, as its own {"type": "image", "image": ...} entry before the text, each resized the same way. A text-only post simply has no image entries. Images of 28 pixels or less per side (tracking pixels, placeholders) crash the Mistral 3 vision path in transformers 5.12; drop them or upscale them to 56 pixels before use.
Training procedure
Training hyperparameters
The following hyperparameters were used during training:
- learning_rate: 1e-05
- trainbatchsize: 2
- evalbatchsize: 8
- seed: 42
- distributed_type: multi-GPU
- num_devices: 8
- gradientaccumulationsteps: 2
- totaltrainbatch_size: 32
- totalevalbatch_size: 64
- optimizer: Use OptimizerNames.ADAMWTORCH with betas=(0.9,0.999) and epsilon=1e-08 and optimizerargs=No additional optimizer arguments
- lrschedulertype: cosine
- lrschedulerwarmup_steps: 0.03
- num_epochs: 4.0
Training results
Framework versions
- Transformers 5.12.1
- Pytorch 2.6.0+cu124
- Datasets 5.0.1
- Tokenizers 0.22.2
