Remove v1 root files from main (v1 available at tag v1)
Browse files- genderize_cora.calibration.json +0 -1
- genderize_cora.config.json +0 -1
- genderize_cora.pt +0 -3
- genderize_infer.py +0 -146
- genderize_ultra.calibration.json +0 -1
- genderize_ultra.config.json +0 -1
- genderize_ultra.pt +0 -3
- maps.json +0 -1
genderize_cora.calibration.json
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
{"temp_gender": 1.0096479654312134, "temp_country": 1.0}
|
|
|
|
|
|
genderize_cora.config.json
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
{"ch": 160}
|
|
|
|
|
|
genderize_cora.pt
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:c0f466b2221f71910b4c83f18a6d207adf837d4cdac2d37da20a5bfdbf068c30
|
| 3 |
-
size 3101302
|
|
|
|
|
|
|
|
|
|
|
|
genderize_infer.py
DELETED
|
@@ -1,146 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""genderize (cora / ultra) — standalone inference for the released weights.
|
| 3 |
-
|
| 4 |
-
What the model is
|
| 5 |
-
-----------------
|
| 6 |
-
A byte-level dual-head convolutional classifier. A personal name is normalised
|
| 7 |
-
(Unicode NFC, lowercase, whitespace collapsed), UTF-8 encoded and truncated to
|
| 8 |
-
48 bytes. Each byte is embedded (256 x 64), projected to `ch` channels and passed
|
| 9 |
-
through four residual 1-D convolution blocks (kernels 3, 5, 7, 3; BatchNorm +
|
| 10 |
-
GELU). Masked mean-pooling and max-pooling over the byte positions are
|
| 11 |
-
concatenated and fed to a shared 512-unit layer, from which two linear heads
|
| 12 |
-
read out: gender (2 classes, M/F) and country (226 ISO-3166 alpha-2 codes).
|
| 13 |
-
`cora` uses ch=160 (~0.77M parameters), `ultra` uses ch=384 (~3.2M).
|
| 14 |
-
Logits are divided by a per-head temperature (calibration.json) before softmax.
|
| 15 |
-
|
| 16 |
-
Files expected next to this script (or pass --models-dir):
|
| 17 |
-
genderize_<variant>.pt state_dict (PyTorch)
|
| 18 |
-
genderize_<variant>.config.json {"ch": 160 | 384}
|
| 19 |
-
genderize_<variant>.calibration.json {"temp_gender": t, "temp_country": t}
|
| 20 |
-
maps.json {"gender": {"M":0,"F":1}, "country": {"AD":0, ...}}
|
| 21 |
-
|
| 22 |
-
Usage
|
| 23 |
-
-----
|
| 24 |
-
python genderize_infer.py --variant ultra "Andrea Rossi" "Yuki Tanaka" "María García"
|
| 25 |
-
python genderize_infer.py --variant cora --top 3 --json "Chen Wei"
|
| 26 |
-
|
| 27 |
-
Requires only torch and numpy. CPU is enough.
|
| 28 |
-
"""
|
| 29 |
-
from __future__ import annotations
|
| 30 |
-
|
| 31 |
-
import argparse
|
| 32 |
-
import json
|
| 33 |
-
import sys
|
| 34 |
-
import unicodedata
|
| 35 |
-
from pathlib import Path
|
| 36 |
-
|
| 37 |
-
import numpy as np
|
| 38 |
-
import torch
|
| 39 |
-
import torch.nn as nn
|
| 40 |
-
import torch.nn.functional as F
|
| 41 |
-
|
| 42 |
-
MAXLEN = 48
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
class _GBlock(nn.Module):
|
| 46 |
-
def __init__(self, ch: int, k: int):
|
| 47 |
-
super().__init__()
|
| 48 |
-
self.conv = nn.Conv1d(ch, ch, k, padding=k // 2)
|
| 49 |
-
self.norm = nn.BatchNorm1d(ch)
|
| 50 |
-
|
| 51 |
-
def forward(self, x):
|
| 52 |
-
return x + F.gelu(self.norm(self.conv(x)))
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
class NameModel(nn.Module):
|
| 56 |
-
"""Byte-level dual-head classifier: gender (2) + country (n_countries)."""
|
| 57 |
-
|
| 58 |
-
def __init__(self, n_countries: int, emb: int = 64, ch: int = 160):
|
| 59 |
-
super().__init__()
|
| 60 |
-
self.emb = nn.Embedding(256, emb, padding_idx=0)
|
| 61 |
-
self.proj = nn.Conv1d(emb, ch, 1)
|
| 62 |
-
self.blocks = nn.Sequential(_GBlock(ch, 3), _GBlock(ch, 5), _GBlock(ch, 7), _GBlock(ch, 3))
|
| 63 |
-
self.shared = nn.Sequential(nn.Linear(ch * 2, 512), nn.GELU(), nn.Dropout(0.15))
|
| 64 |
-
self.head_g = nn.Linear(512, 2)
|
| 65 |
-
self.head_c = nn.Linear(512, n_countries)
|
| 66 |
-
|
| 67 |
-
def forward(self, x):
|
| 68 |
-
mask = (x != 0).float().unsqueeze(1)
|
| 69 |
-
h = self.proj(self.emb(x.long()).transpose(1, 2))
|
| 70 |
-
h = self.blocks(h) * mask
|
| 71 |
-
mean = h.sum(-1) / mask.sum(-1).clamp(min=1)
|
| 72 |
-
mx = h.masked_fill(mask == 0, -1e9).max(-1).values
|
| 73 |
-
z = self.shared(torch.cat([mean, mx], -1))
|
| 74 |
-
return self.head_g(z), self.head_c(z)
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
def normalise(name: str) -> str:
|
| 78 |
-
"""NFC + lowercase + collapse whitespace. Must match what the weights saw."""
|
| 79 |
-
return " ".join(unicodedata.normalize("NFC", (name or "").lower()).split())
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
def encode(texts: list[str], maxlen: int = MAXLEN) -> torch.Tensor:
|
| 83 |
-
X = np.zeros((len(texts), maxlen), dtype=np.uint8)
|
| 84 |
-
for i, t in enumerate(texts):
|
| 85 |
-
b = t.encode("utf-8")[:maxlen]
|
| 86 |
-
X[i, : len(b)] = np.frombuffer(b, dtype=np.uint8)
|
| 87 |
-
return torch.from_numpy(X)
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
class Genderize:
|
| 91 |
-
def __init__(self, variant: str = "ultra", models_dir: str | Path | None = None):
|
| 92 |
-
md = Path(models_dir) if models_dir else Path(__file__).resolve().parent
|
| 93 |
-
maps = json.loads((md / "maps.json").read_text())
|
| 94 |
-
self.inv_country = {v: k for k, v in maps["country"].items()}
|
| 95 |
-
self.idx_m = maps["gender"].get("M", 0)
|
| 96 |
-
cfg = json.loads((md / f"genderize_{variant}.config.json").read_text())
|
| 97 |
-
cal_path = md / f"genderize_{variant}.calibration.json"
|
| 98 |
-
self.cal = json.loads(cal_path.read_text()) if cal_path.exists() else {}
|
| 99 |
-
self.model = NameModel(len(maps["country"]), ch=int(cfg.get("ch", 160)))
|
| 100 |
-
self.model.load_state_dict(torch.load(md / f"genderize_{variant}.pt", map_location="cpu"))
|
| 101 |
-
self.model.eval()
|
| 102 |
-
self.variant = variant
|
| 103 |
-
|
| 104 |
-
@torch.no_grad()
|
| 105 |
-
def predict(self, names: list[str], top: int = 5) -> list[dict]:
|
| 106 |
-
texts = [normalise(n) for n in names]
|
| 107 |
-
lg, lc = self.model(encode(texts))
|
| 108 |
-
pg = F.softmax(lg / self.cal.get("temp_gender", 1.0), dim=-1)
|
| 109 |
-
pc = F.softmax(lc / self.cal.get("temp_country", 1.0), dim=-1)
|
| 110 |
-
vals, idx = pc.topk(min(top, pc.shape[-1]), dim=-1)
|
| 111 |
-
out = []
|
| 112 |
-
for i, name in enumerate(names):
|
| 113 |
-
pm = float(pg[i, self.idx_m])
|
| 114 |
-
gender = "male" if pm >= 0.5 else "female"
|
| 115 |
-
out.append({
|
| 116 |
-
"name": name,
|
| 117 |
-
"gender": gender,
|
| 118 |
-
"probability": round(pm if gender == "male" else 1.0 - pm, 4),
|
| 119 |
-
"countries": [{"code": self.inv_country.get(int(j), "??"), "probability": round(float(v), 4)}
|
| 120 |
-
for v, j in zip(vals[i].tolist(), idx[i].tolist())],
|
| 121 |
-
"model": self.variant,
|
| 122 |
-
})
|
| 123 |
-
return out
|
| 124 |
-
|
| 125 |
-
|
| 126 |
-
def main() -> int:
|
| 127 |
-
ap = argparse.ArgumentParser(description="genderize cora/ultra — gender + nationality from a name")
|
| 128 |
-
ap.add_argument("names", nargs="+")
|
| 129 |
-
ap.add_argument("--variant", choices=["cora", "ultra"], default="ultra")
|
| 130 |
-
ap.add_argument("--models-dir", default=None)
|
| 131 |
-
ap.add_argument("--top", type=int, default=5)
|
| 132 |
-
ap.add_argument("--json", action="store_true", help="print JSON instead of a table")
|
| 133 |
-
a = ap.parse_args()
|
| 134 |
-
g = Genderize(a.variant, a.models_dir)
|
| 135 |
-
rows = g.predict(a.names, top=a.top)
|
| 136 |
-
if a.json:
|
| 137 |
-
print(json.dumps(rows, ensure_ascii=False, indent=1))
|
| 138 |
-
else:
|
| 139 |
-
for r in rows:
|
| 140 |
-
cs = ", ".join(f"{c['code']} {c['probability']:.2f}" for c in r["countries"])
|
| 141 |
-
print(f"{r['name']:<28} {r['gender']:<7} {r['probability']:.3f} {cs}")
|
| 142 |
-
return 0
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
if __name__ == "__main__":
|
| 146 |
-
sys.exit(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
genderize_ultra.calibration.json
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
{"temp_gender": 1.0460342168807983, "temp_country": 0.939997673034668, "ece_gender_pre": 0.0012399105666638377, "ece_gender_post": 0.0007778512481309008}
|
|
|
|
|
|
genderize_ultra.config.json
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
{"ch": 384}
|
|
|
|
|
|
genderize_ultra.pt
DELETED
|
@@ -1,3 +0,0 @@
|
|
| 1 |
-
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:e0b61d9a547aa1b11acad39af573ad63897578e35aa609c26b3de33156303b83
|
| 3 |
-
size 12868705
|
|
|
|
|
|
|
|
|
|
|
|
maps.json
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
{"gender": {"M": 0, "F": 1}, "country": {"AD": 0, "AE": 1, "AF": 2, "AG": 3, "AI": 4, "AL": 5, "AM": 6, "AO": 7, "AR": 8, "AT": 9, "AU": 10, "AX": 11, "AZ": 12, "BA": 13, "BB": 14, "BD": 15, "BE": 16, "BF": 17, "BG": 18, "BH": 19, "BI": 20, "BJ": 21, "BM": 22, "BN": 23, "BO": 24, "BR": 25, "BS": 26, "BT": 27, "BW": 28, "BY": 29, "BZ": 30, "CA": 31, "CC": 32, "CD": 33, "CF": 34, "CG": 35, "CH": 36, "CI": 37, "CK": 38, "CL": 39, "CM": 40, "CN": 41, "CO": 42, "CR": 43, "CU": 44, "CV": 45, "CY": 46, "CZ": 47, "DE": 48, "DJ": 49, "DK": 50, "DM": 51, "DO": 52, "DZ": 53, "EC": 54, "EE": 55, "EG": 56, "EH": 57, "ER": 58, "ES": 59, "ET": 60, "FI": 61, "FJ": 62, "FK": 63, "FM": 64, "FR": 65, "GA": 66, "GB": 67, "GD": 68, "GE": 69, "GG": 70, "GH": 71, "GI": 72, "GM": 73, "GN": 74, "GP": 75, "GQ": 76, "GR": 77, "GT": 78, "GW": 79, "GY": 80, "HK": 81, "HN": 82, "HR": 83, "HT": 84, "HU": 85, "ID": 86, "IE": 87, "IL": 88, "IM": 89, "IN": 90, "IO": 91, "IQ": 92, "IR": 93, "IS": 94, "IT": 95, "JE": 96, "JM": 97, "JO": 98, "JP": 99, "KE": 100, "KG": 101, "KH": 102, "KI": 103, "KM": 104, "KN": 105, "KP": 106, "KR": 107, "KW": 108, "KY": 109, "KZ": 110, "LA": 111, "LB": 112, "LC": 113, "LI": 114, "LK": 115, "LR": 116, "LS": 117, "LT": 118, "LU": 119, "LV": 120, "LY": 121, "MA": 122, "MC": 123, "MD": 124, "ME": 125, "MF": 126, "MG": 127, "MH": 128, "MK": 129, "ML": 130, "MM": 131, "MN": 132, "MO": 133, "MQ": 134, "MR": 135, "MS": 136, "MT": 137, "MU": 138, "MV": 139, "MW": 140, "MX": 141, "MY": 142, "MZ": 143, "NA": 144, "NC": 145, "NE": 146, "NF": 147, "NG": 148, "NI": 149, "NL": 150, "NO": 151, "NP": 152, "NR": 153, "NU": 154, "NZ": 155, "OM": 156, "PA": 157, "PE": 158, "PG": 159, "PH": 160, "PK": 161, "PL": 162, "PN": 163, "PR": 164, "PS": 165, "PT": 166, "PW": 167, "PY": 168, "QA": 169, "RO": 170, "RS": 171, "RU": 172, "RW": 173, "SA": 174, "SB": 175, "SC": 176, "SD": 177, "SE": 178, "SG": 179, "SI": 180, "SK": 181, "SL": 182, "SM": 183, "SN": 184, "SO": 185, "SR": 186, "SS": 187, "ST": 188, "SV": 189, "SX": 190, "SY": 191, "SZ": 192, "TC": 193, "TD": 194, "TG": 195, "TH": 196, "TJ": 197, "TK": 198, "TL": 199, "TM": 200, "TN": 201, "TO": 202, "TR": 203, "TT": 204, "TV": 205, "TW": 206, "TZ": 207, "UA": 208, "UG": 209, "US": 210, "UY": 211, "UZ": 212, "VA": 213, "VC": 214, "VE": 215, "VG": 216, "VN": 217, "VU": 218, "WF": 219, "WS": 220, "XK": 221, "YE": 222, "ZA": 223, "ZM": 224, "ZW": 225}}
|
|
|
|
|
|