CoolFace
Modelpublic

contemmcm/shieldstral-1.0-3b-cls-full-bluesky-moderation

sourceHugging Faceapache-2.0updated 6d agoView on Hugging Face
0likes68downloads
Model Card

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):

pornsexualnuditysexual-figurative
graphic-mediaself-harmsensitiveextremist
intolerantthreatrudeillicit
securityunsafe-linkimpersonationmisinformation
rumormisleadingscamengagement-farming
spaminauthentic

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.

python
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.000

Pass 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

Training LossEpochStepValidation LossMicro F1Macro F1Macro F1 SeenMacro ApExact MatchSafe Accuracy
0.67370.28452000.38120.52560.14200.16440.27900.75520.8272
0.66430.56904000.31710.61040.16950.19630.32270.77080.8634
0.64050.85356000.29470.59450.19450.22530.32910.77620.8468
0.36291.13808000.36920.57630.18110.20980.33630.78030.8594
0.32581.422510000.39210.60450.22130.25620.36270.77390.8625
0.45441.707012000.30230.62260.20980.24290.37070.78340.8511
0.36741.991514000.29040.63820.23680.27420.39860.79670.8767
0.20862.276016000.36550.65000.23720.27470.39550.79730.8846
0.22402.560518000.35710.66110.25220.29210.39390.80050.8864
0.23112.845020000.34640.67440.27210.31510.40480.80610.8889
0.10403.129422000.47700.67850.28170.32610.39650.80650.8875
0.03983.413924000.54750.68390.27700.32070.38880.80320.8893
0.04803.698426000.58730.68030.27910.32310.39370.80790.8900
0.05213.982928000.58770.68050.28320.32790.39250.80670.8900
0.05214.028120.58760.68110.28080.32510.39270.80710.8897

Framework versions

  • —Transformers 5.12.1
  • —Pytorch 2.6.0+cu124
  • —Datasets 5.0.1
  • —Tokenizers 0.22.2