Text Classification
Transformers
Safetensors
Chinese
spam-detection
advertising-detection
ad-classification
chinese
modernbert
mmbert
multi-task
four-head
Instructions to use x6nux/laya-ad with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Transformers
How to use x6nux/laya-ad with Transformers:
# Use a pipeline as a high-level helper from transformers import pipeline pipe = pipeline("text-classification", model="x6nux/laya-ad")# pip install -U transformers accelerate # Load model directly from transformers import AutoModel model = AutoModel.from_pretrained("x6nux/laya-ad", device_map="auto") - Notebooks
- Google Colab
- Kaggle
Download code/scripts/dual_head_model.py from x6nux/laya-ad: direct link, hf CLI and curl.
- Browser
- Download file 8.1 kB
-
https://huggingface.co/x6nux/laya-ad/resolve/main/code/scripts/dual_head_model.py
- Command line
-
hf download hf://x6nux/laya-ad/code/scripts/dual_head_model.py
-
curl -L -o dual_head_model.py https://huggingface.co/x6nux/laya-ad/resolve/main/code/scripts/dual_head_model.py
8.1 kB
| #!/usr/bin/env python3 | |
| """单编码器双头模型:共享 ModernBERT encoder,两个任务头各自保留自己的 prediction head。 | |
| - binary branch: head + classifier 来自 v1.1(768 → 2) | |
| - kind branch: head + classifier 来自已有 7 类模型(768 → 7) | |
| - 一次前向共享 encoder,两个 head 只多约 0.6M 参数;总参数 ≈ 一个 322M 编码器, | |
| 而不是两个完整模型(约 644M)。 | |
| """ | |
| import copy | |
| import json | |
| from pathlib import Path | |
| import torch | |
| import torch.nn as nn | |
| from safetensors import safe_open | |
| from safetensors.torch import load_file | |
| from transformers import ModernBertConfig, ModernBertForSequenceClassification | |
| INT4_GROUP = 64 | |
| class DualHeadModernBert(nn.Module): | |
| def __init__(self, base_bin, base_kind, kind_num_labels=7, use_kind_adapter=False, | |
| kind_adapter_scope="pooled", use_scope_head=False, use_severity_head=False): | |
| super().__init__() | |
| self.config = base_bin.config | |
| self.model = base_bin.model | |
| self.drop = base_bin.drop | |
| self.binary_head = base_bin.head | |
| self.binary_classifier = base_bin.classifier | |
| self.kind_head = base_kind.head | |
| self.kind_classifier = base_kind.classifier | |
| assert self.kind_classifier.out_features == kind_num_labels | |
| self.kind_adapter_scope = kind_adapter_scope | |
| h = self.config.hidden_size | |
| if use_kind_adapter: | |
| self.kind_adapter = nn.Sequential( | |
| nn.Linear(h, h), nn.GELU(), nn.Dropout(0.1), nn.Linear(h, h)) | |
| nn.init.zeros_(self.kind_adapter[-1].weight) | |
| nn.init.zeros_(self.kind_adapter[-1].bias) | |
| else: | |
| self.kind_adapter = None | |
| if use_scope_head: | |
| self.scope_head = copy.deepcopy(base_kind.head) | |
| self.scope_classifier = nn.Linear(h, 2) | |
| else: | |
| self.scope_head = None | |
| self.scope_classifier = None | |
| if use_severity_head: | |
| self.severity_head = copy.deepcopy(base_kind.head) | |
| self.severity_classifier = nn.Linear(h, 4) | |
| else: | |
| self.severity_head = None | |
| self.severity_classifier = None | |
| def _pool(self, h, attention_mask): | |
| if self.config.classifier_pooling == "cls": | |
| return h[:, 0] | |
| if attention_mask is None: | |
| attention_mask = torch.ones(h.shape[:2], device=h.device, dtype=torch.bool) | |
| return (h * attention_mask.unsqueeze(-1)).sum(1) / attention_mask.sum(1, keepdim=True) | |
| def forward_all(self, input_ids, attention_mask=None, position_ids=None): | |
| outputs = self.model(input_ids=input_ids, attention_mask=attention_mask, | |
| position_ids=position_ids) | |
| tokens = outputs[0] | |
| h = self._pool(tokens, attention_mask) | |
| bin_logits = self.binary_classifier(self.drop(self.binary_head(h))) | |
| if self.kind_adapter is None: | |
| hk = h | |
| elif self.kind_adapter_scope == "token": | |
| hk = self._pool(tokens + self.kind_adapter(tokens), attention_mask) | |
| else: | |
| hk = h + self.kind_adapter(h) | |
| kind_logits = self.kind_classifier(self.drop(self.kind_head(hk))) | |
| scope_logits = (self.scope_classifier(self.drop(self.scope_head(h))) | |
| if self.scope_head is not None else None) | |
| severity_logits = (self.severity_classifier(self.drop(self.severity_head(h))) | |
| if self.severity_head is not None else None) | |
| return bin_logits, kind_logits, scope_logits, severity_logits, h | |
| def forward(self, input_ids, attention_mask=None, position_ids=None): | |
| bin_logits, kind_logits, _, _, _ = self.forward_all( | |
| input_ids, attention_mask=attention_mask, position_ids=position_ids) | |
| return bin_logits, kind_logits | |
| def load_dual_head(model_dir, device="cpu", dtype=torch.bfloat16): | |
| model_dir = Path(model_dir) | |
| dc = json.loads((model_dir / "dual_config.json").read_text(encoding="utf-8")) | |
| heads = dc.get("heads") or [] | |
| config = ModernBertConfig.from_pretrained(str(model_dir)) | |
| cfg_kind = copy.deepcopy(config) | |
| cfg_kind.num_labels = len(dc["kind_labels"]) | |
| base_bin = ModernBertForSequenceClassification(config) | |
| base_kind = ModernBertForSequenceClassification(cfg_kind) | |
| model = DualHeadModernBert(base_bin, base_kind, len(dc["kind_labels"]), | |
| use_kind_adapter=bool(dc.get("use_kind_adapter", False)), | |
| kind_adapter_scope=dc.get("kind_adapter_scope", "pooled"), | |
| use_scope_head=("scope" in heads) or bool(dc.get("use_scope_head", False)), | |
| use_severity_head=("severity" in heads) or bool(dc.get("use_severity_head", False))) | |
| state = load_file(str(model_dir / "model.safetensors")) | |
| model.load_state_dict(state) | |
| return model.to(device=device, dtype=dtype).eval() | |
| def _dual_skeleton(model_dir): | |
| model_dir = Path(model_dir) | |
| dc = json.loads((model_dir / "dual_config.json").read_text(encoding="utf-8")) | |
| heads = dc.get("heads") or [] | |
| config = ModernBertConfig.from_pretrained(str(model_dir)) | |
| cfg_kind = copy.deepcopy(config) | |
| cfg_kind.num_labels = len(dc["kind_labels"]) | |
| base_bin = ModernBertForSequenceClassification(config) | |
| base_kind = ModernBertForSequenceClassification(cfg_kind) | |
| model = DualHeadModernBert(base_bin, base_kind, len(dc["kind_labels"]), | |
| use_kind_adapter=bool(dc.get("use_kind_adapter", False)), | |
| kind_adapter_scope=dc.get("kind_adapter_scope", "pooled"), | |
| use_scope_head=("scope" in heads) or bool(dc.get("use_scope_head", False)), | |
| use_severity_head=("severity" in heads) or bool(dc.get("use_severity_head", False))) | |
| return model, dc | |
| def _dtypes(path): | |
| with safe_open(str(path), framework="pt") as f: | |
| return {f.get_slice(k).get_dtype() for k in f.keys()} | |
| def load_dual_head_quantized(model_dir, device="cpu", dtype=torch.bfloat16): | |
| """加载 fp8 / int8 / int4 变体:反量化成双头模型。""" | |
| model_dir = Path(model_dir) | |
| model, _ = _dual_skeleton(model_dir) | |
| param_shapes = {k: v.shape for k, v in model.state_dict().items()} | |
| raw = load_file(str(model_dir / "model.safetensors")) | |
| state = {} | |
| for k, v in raw.items(): | |
| if k.endswith(".__scale__") or k.endswith(".__pad__"): | |
| continue | |
| if v.dtype == torch.float8_e4m3fn: | |
| state[k] = v.to(dtype) | |
| elif v.dtype == torch.int8 and k + ".__scale__" in raw: | |
| scale = raw[k + ".__scale__"].float().view(-1, *([1] * (v.dim() - 1))) | |
| state[k] = (v.float() * scale).to(dtype) | |
| elif v.dtype == torch.uint8 and k + ".__scale__" in raw: | |
| hi = (v >> 4).to(torch.int16) - 8 | |
| lo = (v & 0xF).to(torch.int16) - 8 | |
| q = torch.stack([hi, lo], dim=-1).reshape(-1).float() | |
| scale = raw[k + ".__scale__"].float() | |
| s_exp = scale.repeat_interleave(INT4_GROUP)[:q.numel()] | |
| w = q * s_exp | |
| pad = int(raw[k + ".__pad__"].item()) if k + ".__pad__" in raw else 0 | |
| if pad: | |
| w = w[:-pad] | |
| state[k] = w.reshape(param_shapes[k]).to(dtype) | |
| else: | |
| state[k] = v.to(dtype) | |
| model.load_state_dict(state, strict=True) | |
| return model.to(device=device, dtype=dtype).eval() | |
| def load_dual_head_any(model_dir, device="cpu", dtype=torch.bfloat16): | |
| """按 safetensors 存储类型自动选择普通/量化加载器。""" | |
| dts = _dtypes(Path(model_dir) / "model.safetensors") | |
| if {"I8", "U8", "F8_E4M3"} & dts: | |
| return load_dual_head_quantized(model_dir, device=device, dtype=dtype) | |
| return load_dual_head(model_dir, device=device, dtype=dtype) | |
| if __name__ == "__main__": | |
| import sys | |
| m = load_dual_head_any(sys.argv[1] if len(sys.argv) > 1 else "outputs/ad-dual/best") | |
| print("加载成功 | 参数", sum(p.numel() for p in m.parameters()) / 1e6, "M") | |