Text Classification
Transformers
ONNX
Safetensors
multilingual
xlm-roberta
privacy
pii-detection
text-embeddings-inference
Instructions to use Roblox/roblox-pii-classifier-v2 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use Roblox/roblox-pii-classifier-v2 with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-classification", model="Roblox/roblox-pii-classifier-v2")# Load model directly from transformers import AutoTokenizer, AutoModelForSequenceClassification tokenizer = AutoTokenizer.from_pretrained("Roblox/roblox-pii-classifier-v2") model = AutoModelForSequenceClassification.from_pretrained("Roblox/roblox-pii-classifier-v2", device_map="auto") - Notebooks
- Google Colab
- Kaggle
| #!/usr/bin/env python3 | |
| """Standalone inference for the internal three-label PII classifier.""" | |
| from __future__ import annotations | |
| import argparse | |
| import json | |
| from pathlib import Path | |
| from typing import Any | |
| import torch | |
| from transformers import AutoModelForSequenceClassification, AutoTokenizer | |
| LABELS = ( | |
| "privacy_asking_for_pii", | |
| "privacy_giving_pii", | |
| "directing_users_off_platform", | |
| ) | |
| INSTRUCTION_PREFIX = ( | |
| "Instruct: In the following chat messages from target speaker t and possibly " | |
| "other speakers s1, s2, etc., detect abuse by speaker t.\nQuery:" | |
| ) | |
| INSTRUCTION_SEPARATOR = "\n\n" | |
| TURN_SEPARATOR = " </s> " | |
| TARGET_SPEAKER = "t" | |
| SPEAKER_TEXT_SEPARATOR = ": " | |
| MAX_LENGTH = 512 | |
| def _validate_turn(turn: Any, index: int) -> tuple[str, str]: | |
| if not isinstance(turn, dict): | |
| raise TypeError(f"conversation turn {index} must be a JSON object") | |
| if set(turn) != {"speaker", "text"}: | |
| raise ValueError( | |
| f"conversation turn {index} must contain exactly 'speaker' and 'text'" | |
| ) | |
| speaker = turn["speaker"] | |
| text = turn["text"] | |
| if not isinstance(speaker, str) or not speaker: | |
| raise ValueError(f"conversation turn {index} has an invalid speaker") | |
| if not isinstance(text, str): | |
| raise ValueError(f"conversation turn {index} has non-string text") | |
| return speaker, text | |
| def format_conversation(value: str | list[dict[str, str]]) -> str: | |
| """Format plain text or speaker/text turns exactly like the training formatter.""" | |
| if isinstance(value, str): | |
| turns: list[dict[str, str]] = [{"speaker": TARGET_SPEAKER, "text": value}] | |
| elif isinstance(value, list): | |
| if not value: | |
| raise ValueError("conversation must contain at least one turn") | |
| turns = value | |
| else: | |
| raise TypeError("input must be plain text or a JSON-list conversation") | |
| other_speakers: dict[str, str] = {} | |
| formatted_turns: list[str] = [] | |
| for index, turn in enumerate(turns): | |
| speaker, text = _validate_turn(turn, index) | |
| if speaker == TARGET_SPEAKER: | |
| anonymous_speaker = TARGET_SPEAKER | |
| else: | |
| if speaker not in other_speakers: | |
| other_speakers[speaker] = f"s{len(other_speakers) + 1}" | |
| anonymous_speaker = other_speakers[speaker] | |
| formatted_turns.append( | |
| f"{anonymous_speaker}{SPEAKER_TEXT_SEPARATOR}{text}" | |
| ) | |
| return ( | |
| INSTRUCTION_PREFIX | |
| + INSTRUCTION_SEPARATOR | |
| + TURN_SEPARATOR.join(formatted_turns) | |
| ) | |
| def parse_input(value: str) -> str | list[dict[str, str]]: | |
| """Interpret a JSON list as turns; otherwise retain the value as plain text.""" | |
| try: | |
| decoded = json.loads(value) | |
| except json.JSONDecodeError: | |
| return value | |
| if isinstance(decoded, list): | |
| return decoded | |
| return value | |
| def predict( | |
| value: str | list[dict[str, str]], | |
| model_path: str | Path, | |
| ) -> dict[str, float]: | |
| """Return uncalibrated sigmoid probabilities in the fixed three-label order.""" | |
| formatted = format_conversation(value) | |
| tokenizer = AutoTokenizer.from_pretrained(model_path) | |
| tokenizer.truncation_side = "left" | |
| model = AutoModelForSequenceClassification.from_pretrained(model_path) | |
| model.eval() | |
| device = torch.device("cuda" if torch.cuda.is_available() else "cpu") | |
| model.to(device) | |
| encoded = tokenizer( | |
| formatted, | |
| padding="max_length", | |
| max_length=MAX_LENGTH, | |
| truncation=True, | |
| return_tensors="pt", | |
| ) | |
| encoded = {name: tensor.to(device) for name, tensor in encoded.items()} | |
| with torch.inference_mode(): | |
| logits = model(**encoded).logits | |
| if logits.ndim != 2 or logits.shape[0] != 1 or logits.shape[1] != len(LABELS): | |
| raise ValueError( | |
| f"expected logits shape (1, {len(LABELS)}), got {tuple(logits.shape)}" | |
| ) | |
| probabilities = torch.sigmoid(logits[0]).float().cpu().tolist() | |
| return {label: probability for label, probability in zip(LABELS, probabilities)} | |
| def _arguments() -> argparse.Namespace: | |
| parser = argparse.ArgumentParser( | |
| description="Run the internal three-label PII classifier." | |
| ) | |
| inputs = parser.add_mutually_exclusive_group(required=True) | |
| inputs.add_argument("--text", help="Plain text or a JSON-list conversation.") | |
| inputs.add_argument( | |
| "--input-file", | |
| type=Path, | |
| help="UTF-8 file containing plain text or a JSON-list conversation.", | |
| ) | |
| parser.add_argument("--model-path", type=Path, default=Path(__file__).parent) | |
| return parser.parse_args() | |
| def main() -> None: | |
| args = _arguments() | |
| raw_value = ( | |
| args.text | |
| if args.text is not None | |
| else args.input_file.read_text(encoding="utf-8") | |
| ) | |
| result = predict(parse_input(raw_value), args.model_path) | |
| print(json.dumps(result, indent=2, ensure_ascii=False)) | |
| if __name__ == "__main__": | |
| main() | |