textpie commited on
Commit
dccd936
·
verified ·
1 Parent(s): 5e6f6db

Remove v1 root files from main (v1 available at tag v1)

Browse files
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}}