Instructions to use echoforest/market-psych-classifier-v1 with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use echoforest/market-psych-classifier-v1 with PEFT:
Task type is invalid.
- Notebooks
- Google Colab
- Kaggle
File size: 9,354 Bytes
3d7c238 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 | """Round-2 candidate list from the pool scores (pure Python). Run as ``python -m classifier.candidates``.
Spec section 7, "Round-2 candidates": rank the local-context pool posts by the predicted probability of the
thin classes and by per-field entropy, with a Korean quota and at most 50 posts per author. The output is a
ranked list only; context lookups and Astra labelling need the user's go.
Rules (fixed here, before the list is read):
- **Probabilities:** calibrated with the run's dev temperatures, not gated (``calibrate.probs``). The gate is a
decision rule that renormalises a field after the hard argmax of an earlier one: with valence ``mixed`` it
sets fine emotion ``mixed`` to 1, which made rare fine emotions certain for 35 of the first gated top 100. The
ungated heads keep the model's own uncertainty, which is what a ranking for labelling needs.
- **Thin events:** an event's probability is the sum of its options' probabilities.
- direction bearish; emotion valence negative;
- any behaviour; any P&L;
- any of the five specific commercial intents (not ``unclear``);
- the rare fine emotions, the options other than the absence that hold under 1% of the train labels.
- **thin_score** is the largest event probability; **entropy** is the mean over all fields of H(p) / ln K.
- **Order:** thin_score, then entropy (both descending), then id.
- **Author cap:** an author's posts after the first 50 in that order are left out.
- **Korean quota:** position n is filled from the Korean queue whenever fewer than ceil(0.30 n) of the first
n - 1 are Korean (while Korean posts remain); otherwise the better-ranked head of the two queues is taken.
30% sits above the train (26.8%) and pool (23.6%) Korean shares, because Korean bearish labels are the thinnest.
Pool rows already exclude labelled ids and text keys, P4-window context and unresolved context (the export's
``pool`` command); this module refuses any pool row flagged for P4 context or overlap anyway. Posts whose
context post is unavailable (deleted) stay in, as in training, and carry ``has_unavailable_context``.
"""
from __future__ import annotations
import argparse
from collections import Counter
import hashlib
import json
import math
from pathlib import Path
from .calibrate import probs as calibrated
from .data import load_jsonl, write_jsonl
from .taxonomy import Taxonomy
AUTHOR_CAP = 50
KOREAN_SHARE = 0.30
RARE_SHARE = 0.01
COMMERCIAL = ('sponsored_disclosed', 'own_paid_service', 'referral_or_affiliate', 'giveaway_or_bait', 'asset_shill')
VERSION = 'round2-candidates-v2'
def rare_fine(train_rows, tax: Taxonomy, share: float = RARE_SHARE) -> list[str]:
counts = Counter(r['targets'].get('emotion_fine') for r in train_rows if r.get('targets'))
counts.pop(None, None)
total = sum(counts.values())
absence = tax.absence['emotion_fine']
return [o for o in tax.fields['emotion_fine'] if o != absence and counts.get(o, 0) < share * total]
def events(tax: Taxonomy, rare: list[str]) -> dict[str, tuple[str, list[str]]]:
def present(f):
return [o for o in tax.fields[f] if o != tax.absence[f]]
return {'bearish': ('direction_primary', ['bearish']), 'negative': ('emotion_valence', ['negative']),
'behavior': ('behavior', present('behavior')), 'pnl': ('pnl', present('pnl')),
'commercial': ('commercial_intent', list(COMMERCIAL)), 'rare_fine': ('emotion_fine', list(rare))}
def score(pred: dict, tax: Taxonomy, temps: dict, ev: dict) -> dict:
p = calibrated(pred, tax, temps)
probs = {name: sum(p[f][o] for o in opts) for name, (f, opts) in ev.items()}
top = max(probs, key=lambda k: (probs[k], k))
ent = []
for f in tax.names:
k = len(tax.fields[f])
ent.append(-sum(x * math.log(x) for x in p[f].values() if x > 0) / math.log(k))
return {'thin_score': probs[top], 'top_event': top, 'events': probs, 'entropy': sum(ent) / len(ent)}
def rank(items: list[dict], cap: int = AUTHOR_CAP, korean: float = KOREAN_SHARE) -> tuple[list[dict], list[dict]]:
"""items carry id, author_id, lang, thin_score, entropy. Returns (ranked, left out by the author cap)."""
order = sorted(items, key=lambda x: (-x['thin_score'], -x['entropy'], x['id']))
seen, kept, capped = Counter(), [], []
for x in order:
seen[x['author_id']] += 1
(kept if seen[x['author_id']] <= cap else capped).append(x)
ko = [x for x in kept if x['lang'] == 'ko']
other = [x for x in kept if x['lang'] != 'ko']
position = {x['id']: i for i, x in enumerate(kept)}
out, i, j, n_ko = [], 0, 0, 0
while i < len(ko) or j < len(other):
n = len(out) + 1
need_ko = n_ko < math.ceil(korean * n)
if i < len(ko) and (j >= len(other) or need_ko or position[ko[i]['id']] < position[other[j]['id']]):
out.append(ko[i]); i += 1; n_ko += 1
else:
out.append(other[j]); j += 1
return out, capped
def sha256(path) -> str:
return hashlib.sha256(Path(path).read_bytes()).hexdigest()
def main(argv=None):
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
p.add_argument('--data', required=True, help='Export directory (pool/, train.jsonl, taxonomy.json)')
p.add_argument('--run', required=True, help='Run directory with temperatures.json and predictions/pool-*.jsonl')
p.add_argument('--out', required=True, help='Output directory (refuses to overwrite)')
args = p.parse_args(argv)
data, run, out = Path(args.data), Path(args.run), Path(args.out)
if (out / 'candidates.jsonl').exists() or (out / 'summary.json').exists():
raise SystemExit('Refusing to overwrite ' + str(out))
tax = Taxonomy.load(data / 'taxonomy.json')
if Taxonomy.load(run / 'taxonomy.json').to_json() != tax.to_json():
raise SystemExit('The run taxonomy differs from the export taxonomy')
temps = json.loads((run / 'temperatures.json').read_text())
rare = rare_fine(load_jsonl(data / 'train.jsonl'), tax)
ev = events(tax, rare)
pool_files = sorted((data / 'pool').glob('*.jsonl'))
pred_files = [run / 'predictions' / ('pool-' + f.name) for f in pool_files]
rows = [r for f in pool_files for r in load_jsonl(f)]
preds = {}
for f in pred_files:
for r in load_jsonl(f):
if r['id'] in preds:
raise SystemExit('Duplicate prediction id: ' + r['id'])
preds[r['id']] = r
ids = [r['id'] for r in rows]
if len(set(ids)) != len(ids) or set(ids) != set(preds):
raise SystemExit('Pool rows and pool predictions do not match one to one')
flagged = [r['id'] for r in rows if r.get('p4_context') or any((r.get('overlap') or {}).values())]
if flagged:
raise SystemExit('Pool rows carry exclusion flags: %s' % flagged[:5])
items = []
for r in rows:
s = score(preds[r['id']], tax, temps, ev)
items.append({'id': r['id'], 'tweet_id': r['tweet_id'], 'author_id': r['author_id'], 'lang': r['lang'],
'day_utc': r['day_utc'], 'has_unavailable_context': bool(r.get('has_unavailable_context')), **s})
ranked, capped = rank(items)
out.mkdir(parents=True, exist_ok=True)
rounded = []
for n, x in enumerate(ranked, 1):
rounded.append({'rank': n, **{k: x[k] for k in ('id', 'tweet_id', 'author_id', 'lang', 'day_utc', 'has_unavailable_context',
'top_event')},
'thin_score': round(x['thin_score'], 6), 'entropy': round(x['entropy'], 6),
'events': {k: round(v, 6) for k, v in x['events'].items()}})
write_jsonl(out / 'candidates.jsonl', rounded)
def block(xs):
return {'posts': len(xs), 'korean': sum(1 for x in xs if x['lang'] == 'ko'),
'top_event': dict(Counter(x['top_event'] for x in xs).most_common()),
'expected_events': {k: round(sum(x['events'][k] for x in xs), 1) for k in ev},
'authors': len({x['author_id'] for x in xs})}
summary = {
'version': VERSION, 'rules': {'author_cap': AUTHOR_CAP, 'korean_share': KOREAN_SHARE, 'rare_share': RARE_SHARE,
'events': {k: {'field': f, 'options': o} for k, (f, o) in ev.items()},
'order': 'thin_score desc, entropy desc, id', 'probabilities': 'calibrated, not gated'},
'pool_posts': len(rows), 'ranked': len(ranked), 'left_out_by_author_cap': len(capped),
'authors_capped': len({x['author_id'] for x in capped}),
'top_100': block(ranked[:100]), 'top_1000': block(ranked[:1000]), 'top_4000': block(ranked[:4000]),
'all': block(ranked),
'inputs': {str(f): sha256(f) for f in [data / 'taxonomy.json', data / 'train.jsonl', run / 'temperatures.json',
*pool_files, *pred_files]},
'candidates_sha256': sha256(out / 'candidates.jsonl'),
'note': 'A ranked list only: context lookups and Astra labelling need the user\'s go.'}
(out / 'summary.json').write_text(json.dumps(summary, indent=1, ensure_ascii=False) + '\n')
print(json.dumps({k: summary[k] for k in ('pool_posts', 'ranked', 'left_out_by_author_cap', 'authors_capped')}))
return 0
if __name__ == '__main__':
raise SystemExit(main())
|