This guide explains how labels are defined, how datasets are mapped to those labels, and how to add new labels or new datasets.
Data flows through four layers before reaching the trainer. Understanding them makes configuration straightforward:
1. LabelConfig – declares every label that can exist (and labels that are active, i.e. trained on)
↓
2. DatasetColumnMapping – maps raw dataset columns → label names
↓
3. DatasetLabelMapping – declares which labels each dataset provides
↓
4. Data loaders – load files, run column extraction, return (df, dataset_type)
The trainer uses the label mask from step 3 to ensure that loss is only computed on labels a given dataset actually annotates. For example, the phishing dataset only annotates scam, so all other label positions in its rows are masked out during training.
File: cockatoo_ml/registry/labels.py
class LabelConfig:
ALL_LABELS = ['scam', 'violence', 'nsfw', 'harassment',
'hate_speech', 'toxicity', 'obscenity', 'jailbreaking']
ACTIVE_LABELS = ['scam', 'violence', 'harassment',
'hate_speech', 'toxicity', 'obscenity', 'jailbreaking']| Field | Purpose |
|---|---|
ALL_LABELS |
Every label the system is aware of. Used for validation — nothing outside this list can be referenced elsewhere. |
ACTIVE_LABELS |
The labels actually used in the current training run. Controls the size of the output head and the order of every label vector. |
ACTIVE_LABELS can be a subset of ALL_LABELS. Labels in ALL_LABELS but not in ACTIVE_LABELS are simply never trained on; they don't need to be removed from column mappings or dataset registrations.
Order matters. The position of each label in
ACTIVE_LABELSdefines its index in the output vector[scam, violence, harassment, ...]. Changing the order is a breaking change for saved checkpoints.
File: cockatoo_ml/registry/column_mapping.py
This class describes how to extract a normalised label value from the raw columns of each dataset file. Each dataset has a dict with two keys:
SOME_DATASET = {
'text_col': '<name of the text column in the raw file>',
'labels': {
'<label_name>': '<raw_column>' # single column
'<label_name>': ['<col_a>', '<col_b>'], # merge multiple columns
}
}'toxicity': 'toxicity'The raw column is read as-is. If it is a float column it is binarised using the threshold from LABEL_THRESHOLDS (default 0.5). If it is already int/bool it is cast to int directly.
'harassment': ['insult', 'humiliate', 'dehumanize', 'attack_defend']All listed columns are combined using DATASET_MERGING_STRATEGY (default 'or'). With 'or', the resulting label is 1 if any column is 1. See the merge strategies table below.
| Strategy | Behaviour | When to use |
|---|---|---|
'or' |
1 if any column ≥ 1 | Boolean signals, any-positive is sufficient |
'and' |
1 only if all columns ≥ 1 | Require consensus across signals |
'max' |
max value across columns | Continuous scores, want the highest |
'mean' |
mean value across columns | Continuous scores, want the average |
DATASET_MERGING_STRATEGY sets the global default. You can also pass a strategy explicitly to merge_multi_column_labels(df, cols, strategy) if you need per-label control in a custom loader.
LABEL_THRESHOLDS provides a per-label cutoff for continuous float columns:
LABEL_THRESHOLDS = {
'toxicity': 0.5,
'hate_speech': 0.5,
...
}If a label does not appear here, get_label_threshold() returns 0.5 as a default.
Add the dict as a class attribute and add it to DATASET_MAPPINGS:
MY_DATASET = {
'text_col': 'body',
'labels': {
'toxicity': 'tox_score', # float column, will be thresholded
'harassment': ['rude', 'abusive'], # OR-merged
}
}
DATASET_MAPPINGS = {
...
'my_dataset': MY_DATASET,
}The key in DATASET_MAPPINGS must match the dataset_type string returned by the data loader (see step 4).
File: cockatoo_ml/registry/dataset_label_mapping.py
This registry declares which labels each dataset provides. It drives the label mask used during training — columns for labels a dataset does not annotate are masked out so they don't contribute to the loss.
_mappings = {
'phishing': {
'labels': ['scam'],
'description': 'Phishing dataset - contains scam labels'
},
'jigsaw': {
'labels': ['toxicity', 'obscenity', 'violence', 'harassment', 'hate_speech'],
'description': 'Jigsaw toxicity - multi-label toxic content classification'
},
...
}The labels list must be a subset of LabelConfig.ALL_LABELS. If you include a label that isn't in ALL_LABELS, register() will raise a ValueError at startup.
Labels here must match what the column mapping actually extracts. If
column_mapping.pyextracts atoxicitycolumn but the dataset is registered here withlabels: ['hate_speech'], thetoxicitycolumn will be silently ignored.
To add a new dataset registration:
mapping = get_dataset_label_mapping()
mapping.register(
'my_dataset',
labels=['toxicity', 'harassment'],
description='My custom dataset'
)Or add it directly to _initialize_defaults() for it to be available on every run without any extra call.
File: train/data_loaders.py
Each loader:
- Reads the raw file from disk.
- Optionally pre-processes columns (e.g. converts a multiclass label to binary).
- Calls
extract_labels_from_df(df, mapping, dataset_name)which applies the column mapping. - Returns
(df, dataset_type)wheredataset_typeis the key used in steps 2 and 3.
load_all_datasets() calls every registered loader in order and collects the results.
Most datasets need only a few lines:
def load_my_dataset(base_dir=None):
if base_dir is None:
base_dir = PathConfig.BASE_DATA_DIR
path = os.path.join(base_dir, 'my_dataset', 'train.csv')
if os.path.exists(path):
df = pd.read_csv(path)
logger.info(f"My dataset raw columns: {df.columns.tolist()}")
mapping = DatasetColumnMapping.get_mapping('my_dataset')
df = extract_labels_from_df(df, mapping, 'my_dataset')
if df is not None:
df = df.dropna(subset=[DatasetColumns.TEXT_COL])
logger.info(f"My dataset loaded: {len(df)} samples")
return df, 'my_dataset'
else:
logger.warning("My dataset file not found")
return None, NoneThen add it to load_all_datasets:
loaders = [
...
load_my_dataset,
]If the raw dataset doesn't directly expose boolean labels (e.g. a multiclass column), transform the column before handing off to extract_labels_from_df. The tweet_eval emotion dataset is an example — it has a label column with values 0=anger, 1=joy, 2=optimism, 3=sadness:
# convert multiclass → binary before extraction
df['anger'] = (df['label'] == 0).astype(int)
mapping = DatasetColumnMapping.get_mapping('tweet_emotion')
df = extract_labels_from_df(df, mapping, 'tweet_emotion')The column mapping then routes the synthetic anger column to the hate_speech label.
1. Add to LabelConfig
# cockatoo_ml/registry/labels.py
ALL_LABELS = [..., 'spam']
ACTIVE_LABELS = [..., 'spam'] # include only when ready to train. Labels defined in ALL_LABELS but not in ACTIVE_LABELS are ignored by the system, so you can add to ALL_LABELS early without breaking anything.
# ACTIVE_LABELS exists to control what the model is trained on, and ALL_LABELS exists to verify ACTIVE_LABELS are valid.2. Add a threshold (optional)
# cockatoo_ml/registry/column_mapping.py — LABEL_THRESHOLDS
'spam': 0.5,3. Map it in each relevant dataset
For any dataset whose raw columns can indicate spam, add the mapping:
MY_DATASET = {
'text_col': 'text',
'labels': {
...
'spam': 'is_spam', # raw boolean column
}
}4. Register it in DatasetLabelMapping
'my_dataset': {
'labels': [..., 'spam'],
'description': '...'
},That's all. The label vector builder (LabelConfig.make_labels) and mask logic pick up the new label automatically.
1. Add the download source (if from Hugging Face)
This configures where to download the data from (HF only)
# cockatoo_ml/registry/datasets.py — DatasetSources.DATASETS
("ealvaradob/phishing-dataset", "phishing"),2. Add path constants
# cockatoo_ml/registry/datasets.py — DatasetPaths
PHISHING_FILE = "combined_reduced.json"
PHISHING_DIR = "phishing"3. Add a column mapping
# cockatoo_ml/registry/column_mapping.py
PHISHING = {
'text_col': 'text',
'labels': {
'scam': 'label' # binary column - 1 if phishing, 0 otherwise
}
}
DATASET_MAPPINGS = {
'phishing': PHISHING,
...
}4. Register labels
# cockatoo_ml/registry/dataset_label_mapping.py — _initialize_defaults
'phishing': {
'labels': ['scam'],
'description': 'Phishing dataset - contains scam labels'
},5. Write the loader
# train/data_loaders.py
def load_phishing_dataset(base_dir=None):
if base_dir is None:
base_dir = PathConfig.BASE_DATA_DIR
phish_path = os.path.join(base_dir, DatasetPaths.PHISHING_DIR, DatasetPaths.PHISHING_FILE)
if os.path.exists(phish_path):
df_phish = pd.read_json(phish_path)
logger.info(f"Phishing raw columns: {df_phish.columns.tolist()}")
mapping = DatasetColumnMapping.get_mapping('phishing')
df_phish = extract_labels_from_df(df_phish, mapping, 'phishing')
if df_phish is not None:
df_phish = df_phish.dropna(subset=[DatasetColumns.TEXT_COL])
logger.info(f"Phishing loaded: {len(df_phish)} samples")
return df_phish, 'phishing'
else:
logger.warning("Phishing file not found")
return None, None6. Register the loader
# train/data_loaders.py — load_all_datasets
loaders = [
load_phishing_dataset,
...
]| Dataset key | Labels annotated | Text column | Notes |
|---|---|---|---|
phishing |
scam |
text |
Binary label column |
hate_speech |
hate_speech, violence, harassment |
text |
Boolean columns; violence+genocide merged; harassment is OR of insult/humiliate/dehumanize/attack_defend |
tweet_hate |
hate_speech |
text |
Binary label column (1=hate) |
tweet_emotion |
hate_speech |
text |
Multiclass label pre-processed: anger (0) → hate_speech |
toxicchat |
toxicity, jailbreaking |
user_input |
Continuous toxicity, thresholded at 0.5 |
jigsaw |
toxicity, obscenity, violence, harassment, hate_speech |
comment_text |
toxic+severe_toxic OR-merged; threat→violence; insult→harassment; identity_hate→hate_speech |
| Label | Included in ACTIVE_LABELS |
Notes |
|---|---|---|
scam |
✅ | Phishing / fraud content |
violence |
✅ | Threats, violent language, genocide references |
harassment |
✅ | Insults, humiliation, targeted abuse |
hate_speech |
✅ | Identity-based hate |
toxicity |
✅ | General toxic language |
obscenity |
✅ | Obscene/explicit language |
jailbreaking |
✅ | LLM jailbreak attempts |
nsfw |
❌ | In ALL_LABELS but not active — no dataset currently annotates this |