File size: 7,281 Bytes
440bdf5 | 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 | ---
language: en
license: cc-by-nc-sa-4.0
base_model: ProsusAI/finbert
pipeline_tag: text-classification
tags:
- finance
- financial-nlp
- sentiment-analysis
- macroeconomics
- bert
metrics:
- f1
- accuracy
---
# finbert-macro
FinBERT fine-tuned to read macroeconomic headlines by their effect on equities, without losing the
base model's grip on company news.
ProsusAI/finbert scores 39.0 macro-F1 on the macro test set, which is near chance for three classes.
This checkpoint scores 68.8, and corporate news improves at the same time, 70.2 to 84.0.
## Labels
`0 = positive`, `1 = negative`, `2 = neutral`, the same ids as the base model.
On macro text the label is market impact, not tone. Dovish maps to positive, hawkish maps to
negative. A hot CPI print or a strong payrolls number comes back `negative` here because it implies
tighter policy. If you want literal sentiment, use ProsusAI/finbert instead.
## Usage
```python
from transformers import AutoTokenizer, AutoModelForSequenceClassification
import torch
name = "ryousuf569/finbert-macro"
tok = AutoTokenizer.from_pretrained(name)
model = AutoModelForSequenceClassification.from_pretrained(name).eval()
texts = ["Powell signals rate cuts are coming as price pressures ease",
"Payrolls blow past forecasts, wage growth accelerates",
"GDP growth unchanged from prior quarter"]
with torch.no_grad():
logits = model(**tok(texts, padding=True, truncation=True,
max_length=64, return_tensors="pt")).logits
for t, p in zip(texts, logits.argmax(-1).tolist()):
print(model.config.id2label[p], "|", t)
# positive | Powell signals rate cuts are coming as price pressures ease
# negative | Payrolls blow past forecasts, wage growth accelerates
# neutral | GDP growth unchanged from prior quarter
```
"Jobless claims rise, cooling a tight labor market" also comes back positive, which is the mapping
working as intended rather than a bug.
## Results
Macro and corporate are always reported separately. Averaging them would hide a macro gain paid for
by forgetting corporate news.
| model | macro acc | macro F1 | corporate acc | corporate F1 |
|---|---|---|---|---|
| ProsusAI/finbert | 38.06 | 39.03 | 71.60 | 70.24 |
| finbert-macro | 68.89 | 68.82 | 85.03 | 83.97 |
| change | +30.83 | +29.79 | +13.43 | +13.73 |
Macro test set is 6,101 real held-out rows, corporate is 4,169. No class collapsed on either set.
Lowest per-class F1 is 65.9 on macro, 81.3 on corporate.
## Seed replication
One seed cannot separate a real gap from run-to-run noise, so the winning configuration (u6@32k: 6
unfrozen encoder blocks, 32,000 training rows) and the runner-up (u4@20k: 4 blocks, 20,000 rows)
were each retrained from scratch at seeds 42, 1 and 2.
**u6@32k beats u4@20k by 1.87 points of macro-F1 and 1.67 points of corporate F1. Both gaps survive
the change of seed, and both domains improve at once, so this is not a macro-for-corporate trade.**
| config | seed 42 | seed 1 | seed 2 | mean | std | min to max |
|---|---|---|---|---|---|---|
| macro-F1, u4@20k | 67.59 | 67.29 | 67.41 | 67.43 | 0.15 | 67.29 to 67.59 |
| macro-F1, u6@32k | 68.82 | 69.34 | 69.73 | 69.30 | 0.46 | 68.82 to 69.73 |
| corporate F1, u4@20k | 81.39 | 82.06 | 82.32 | 81.92 | 0.48 | 81.39 to 82.32 |
| corporate F1, u6@32k | 83.97 | 82.66 | 84.16 | 83.60 | 0.82 | 82.66 to 84.16 |
The macro gap of 1.87 points is wider than the widest seed range of 0.91, and the corporate gap of
1.67 is wider than its widest range of 1.50. Every u6@32k seed beats every u4@20k seed on both
metrics. Three seeds gives a range, not a significance test, so there is no p-value here and none
should be inferred.
The published weights are the seed 42 run, the seed fixed before the replication study. Seed 2
scored higher at 69.73, but picking it would be selecting a checkpoint on the test set.
## What did not replicate
Four other metrics moved more between seeds than between configurations, so they were dropped from
the decision and are listed here only as description.
| metric | gap between configs | widest seed range |
|---|---|---|
| probe accuracy (48 probes) | 8.33 | 14.58 |
| minimal pairs solved (of 18) | 1.67 | 7.00 |
| polarity-agreeing probe accuracy (14 probes) | 7.14 | 7.14 |
| Financial PhraseBank F1 | 12.31 | 13.29 |
The probe suite is small enough that this is structural. With 14 polarity-agreeing probes, one probe
changing its answer is worth 7.14 points, which is the entire gap. More probes would fix this. More
seeds would not.
## Limitations
The PhraseBank comparison is not clean. This model gets 73.1 PhraseBank macro-F1 against the base
model's 96.3, but ProsusAI/finbert was itself fine-tuned on Financial PhraseBank, so 96.3 is close
to its training score. PhraseBank was never trained on here and every pool was filtered against it
for leakage. Read 73.1 as retained company-level ability rather than a 23-point regression.
Two things changed at once between u6@32k and u4@20k, unfreeze depth and training rows. The gain is
real and repeatable, but this experiment cannot say which of the two produced it.
1,817 of the 20,800 macro training rows are synthetic templates. They are scored as a separate
evaluation set and never folded into the reported macro number, but they are in the training mix.
Scope is short English headlines and policy sentences, 64 tokens. Central-bank language from 1996 to
2024 dominates the macro side. Not built for filings, transcripts, long documents or other
languages. The output is a text label, not a market forecast.
## Training
32,000 rows, 20,800 macro and 11,200 corporate, sampled without replacement. The corporate share is
an anchor against forgetting. Everything is frozen except the top 6 encoder blocks, the pooler and
the head, since full fine-tuning is the fastest way to lose corporate competence. Cross-entropy with
inverse-frequency class weights, lr 2e-5, weight decay 0.01, warmup 0.1, batch 32, fp16.
Both a macro and a corporate validation set are scored each epoch. The selected epoch maximizes
macro-F1 while corporate validation F1 stays within 0.01 of the base checkpoint. Epoch 5 of 7 won
and its weights were restored, against a ceiling of 16 epochs with early stopping. Training took 145
minutes on one GTX 1660 Ti (6 GB, Turing).
On that card `torch.cuda.is_bf16_supported()` returns True but counts emulated bf16, which is
unusably slow, so this was trained in fp16.
## Data and license
Macro pool: World Central Banks, 16,870 rows, CC BY-NC-SA 4.0. FOMC / Trillion Dollar Words, 1,660
rows, CC BY-NC 4.0. FinMarBa headlines, 749 rows, MIT. Synthetic templates, 1,848 rows.
Corporate pool: SEntFiN, Twitter financial news and FiQA, 14,591 rows, all MIT.
Splits are date-blocked per source where dates exist and grouped-random otherwise.
The largest macro source is CC BY-NC-SA 4.0, so this model is released under CC BY-NC-SA 4.0,
non-commercial and share-alike. That is stricter than the base model's Apache-2.0 licence, and the
restriction comes from the training data.
## Reproducing the seed study
```bash
python seed_rep.py --criterion macro_floored --seeds 42 1 2 \
--config "u4@20k:unfreeze=4,total=20000" \
--config "u6@32k:unfreeze=6,total=32000" --yes
```
|