README.md ADDED
@@ -0,0 +1,208 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ language:
3
+ - pt
4
+ license: other
5
+ license_name: custom-agplv3-dual-license
6
+ license_link: https://huggingface.co/inesctec/CitiLink-SpanBERT-Coreference-pt/blob/main/LICENSE
7
+ tags:
8
+ - coreference-resolution
9
+ - meeting-minutes
10
+ - portuguese
11
+ - spanbert
12
+ - administrative-documents
13
+ library_name: transformers
14
+ base_model:
15
+ - neuralmind/bert-base-portuguese-cased
16
+ pipeline_tag: token-classification
17
+ ---
18
+
19
+ # SpanBERT-Coreference: Coreference Resolution for Portuguese Municipal Meeting Minutes
20
+
21
+ This model consists of a fine-tuned **SpanBERT** for coreference resolution in minutes of Portuguese municipal meetings, identifying when different expressions in a document refer to the same real-world entity.
22
+
23
+ ## Model Description
24
+
25
+ The **SpanBERT-PT-Coreference** uses a mention-ranking architecture (Lee et al., 2017) on top of a SpanBERT encoder pre-trained in Portuguese, specifically optimized for the linguistic and formal structure of administrative minutes in Portugal. The model detects coreferent mention clusters across 17 entity categories, enabling consistent pseudonymization of sensitive personal data throughout long documents.
26
+
27
+ ### Key Features
28
+
29
+ - 🏛️ **Specialized for Municipal Minutes**: Fine-tuned on authentic Portuguese council meeting minutes
30
+ - 🔗 **Coreference-Aware Pseudonymization**: Links multiple mentions of the same entity, ensuring consistent ID assignment across the document
31
+ - 📄 **Long Document Support**: Handles documents of up to ~28,000 tokens via sliding window encoding with stride overlap
32
+ - ⚙️ **Mention-Ranking Architecture**: Scores all antecedent candidates for each mention span and clusters them via Union-Find
33
+
34
+ ## Model Details
35
+
36
+ - **Base Model**: SpanBERT (Portuguese continued pre-training)
37
+ - **Architecture**: Mention-ranking coreference (Lee et al., 2017)
38
+ - **Parameters**: ~110M (encoder) + ~25M (coreference heads)
39
+ - **Max Window Size**: 512 tokens (stride: 128)
40
+ - **Fine-tuning Dataset**: 120 Portuguese meeting minutes (6 municipalities, ~1.3M tokens)
41
+ - **Evaluation Metrics**: MUC, B³, CEAF-e, CoNLL F1, LEA
42
+ - **Training Framework**: PyTorch + Transformers
43
+
44
+ ## Entity Types
45
+
46
+ The model resolves coreference across 17 entity categories:
47
+
48
+ | Entity Type | Description | Example |
49
+ |------------|-------------|---------|
50
+ | `PERSONAL-NAME` | Proper names of individuals | João Silva → João → ele |
51
+ | `PERSONAL-ADMIN` | Administrative identifiers and process numbers | 5597/2023 → o processo |
52
+ | `PERSONAL-POSITION` | Professional roles and political positions | Presidente da Câmara → o Presidente |
53
+ | `PERSONAL-ADDRESS` | Addresses and street names | Rua das Flores n.º 10 → Rua das Flores |
54
+ | `PERSONAL-LOCATION` | Cities, parishes, or geographic locations | Porto → o concelho |
55
+ | `PERSONAL-DATE` | Dates of events or decisions | 20/05/2023 |
56
+ | `PERSONAL-COMPANY` | Companies or private legal entities | Construções & Filho, Lda |
57
+ | `PERSONAL-INFO` | Biographical or sensitive personal data | NIF, número de contribuinte |
58
+ | `PERSONAL-DEGREE` | Academic titles or professional degrees | Licenciatura de Psicologia |
59
+ | `PERSONAL-TIME` | References to specific times | 14:30h |
60
+ | `PERSONAL-LICENSE` | License plates or registration numbers | 48-RF-99 |
61
+ | `PERSONAL-JOB` | Person's profession or occupation | Professor |
62
+ | `PERSONAL-VEHICLE` | Vehicle identification and models | Mercedes-Benz Classe S |
63
+ | `PERSONAL-FACULTY` | Higher education institutions | Faculdade de Economia |
64
+ | `PERSONAL-FAMILY` | Kinship or family relationships | Marido |
65
+ | `PERSONAL-ARTISTIC` | Artistic names or pseudonyms | Pintura |
66
+ | `PERSONAL-OTHER` | Miscellaneous personal information | Dados diversos |
67
+
68
+ ## Cluster Distribution (Training Set)
69
+
70
+ | Entity Class | Number of Clusters |
71
+ |-------------|-------------------|
72
+ | `PERSONAL-ADMIN` | 273 |
73
+ | `PERSONAL-NAME` | 259 |
74
+ | `PERSONAL-POSITION` | 174 |
75
+ | `PERSONAL-ADDRESS` | 115 |
76
+ | `PERSONAL-LOCATION` | 64 |
77
+ | `PERSONAL-DATE` | 49 |
78
+ | `PERSONAL-OTHER` | 16 |
79
+ | `PERSONAL-INFO` | 12 |
80
+ | `PERSONAL-COMPANY` | 10 |
81
+ | `PERSONAL-DEGREE` | 6 |
82
+ | `PERSONAL-LICENSE` | 6 |
83
+ | `PERSONAL-VEHICLE` | 4 |
84
+ | `PERSONAL-JOB` | 4 |
85
+ | `PERSONAL-TIME` | 3 |
86
+ | `PERSONAL-FAMILY` | 2 |
87
+ | `PERSONAL-ARTISTIC` | 1 |
88
+ | **Total** | **998** |
89
+
90
+ ## How It Works
91
+
92
+ The model uses a two-stage pipeline. First, entity spans are identified by a NER model ([inesctec/CitiLink-XLMR-Anonymization-pt](https://huggingface.co/inesctec/CitiLink-XLMR-Anonymization-pt)). Then, the coreference model groups spans that refer to the same entity, assigning consistent IDs throughout the document.
93
+
94
+ **INPUT:**
95
+ ```
96
+ João Paulo Rosinha Daniel apresentou o requerimento n.º 2683/20020627.
97
+ O pedido de João Paulo, referente ao processo 2683/20020627, foi aprovado.
98
+ ```
99
+
100
+ **OUTPUT:**
101
+ ```
102
+ <NAME-1> apresentou o requerimento n.º <ADMIN-1>.
103
+ O pedido de <NAME-1>, referente ao processo <ADMIN-1>, foi aprovado.
104
+ ```
105
+
106
+ ### Architecture Details
107
+
108
+ The mention-ranking model represents each span as:
109
+
110
+ ```
111
+ span_repr = [h_start ; h_end ; head_attn ; width_emb]
112
+ = [768 ; 768 ; 768 ; 20] = 2324 dims
113
+ ```
114
+
115
+ Antecedent scoring uses:
116
+
117
+ ```
118
+ score(i,j) = mention_score(i) + mention_score(j) + antecedent_score([vi; vj; vi*vj; dist_emb])
119
+ ```
120
+
121
+ Clusters are built via Union-Find, restricted to spans of the same NER class and scores above threshold.
122
+
123
+ ## Results
124
+
125
+ ### Overall Performance (Test Set)
126
+
127
+ | Metric | Precision (%) | Recall (%) | F1 Score (%) |
128
+ |--------|--------------|-----------|-------------|
129
+ | **MUC** | 90.20 | 92.80 | **91.50** |
130
+ | **B³** | 86.70 | 92.50 | **89.50** |
131
+ | **CEAF-e** | 89.20 | 74.40 | **81.20** |
132
+ | **CoNLL F1** | — | — | **87.40** |
133
+ | **LEA** | 64.80 | 88.20 | **74.80** |
134
+
135
+
136
+ ## Usage
137
+
138
+ ### Quick Start
139
+
140
+ The simplest way to use the model:
141
+
142
+ ```python
143
+ import torch
144
+ import json
145
+ from transformers import AutoTokenizer, AutoModel
146
+ from huggingface_hub import hf_hub_download
147
+
148
+ model_id = "inesctec/CitiLink-SpanBERT-Coreference-pt"
149
+
150
+ # Load tokenizer and encoder
151
+ tokenizer = AutoTokenizer.from_pretrained(model_id)
152
+ encoder = AutoModel.from_pretrained(model_id)
153
+
154
+ # Load coreference heads and config
155
+ cabecas_path = hf_hub_download(repo_id=model_id, filename="cabecas_coref.pt")
156
+ coref_cfg_path = hf_hub_download(repo_id=model_id, filename="coref_config.json")
157
+
158
+ with open(coref_cfg_path) as f:
159
+ coref_cfg = json.load(f)
160
+
161
+ cabecas = torch.load(cabecas_path, map_location="cpu", weights_only=False)
162
+
163
+ # Input text (already tokenized by whitespace)
164
+ tokens = [
165
+ "João", "Paulo", "Rosinha", "Daniel", "apresentou", "o",
166
+ "requerimento", "n.º", "2683/20020627", ".",
167
+ "O", "pedido", "de", "João", "Paulo", "foi", "aprovado",
168
+ "ao", "abrigo", "do", "processo", "2683/20020627", "."
169
+ ]
170
+
171
+ # NER spans already extracted by a NER model — (start, end, class)
172
+ ner_spans = [
173
+ (0, 3, "NAME"), # "João Paulo Rosinha Daniel"
174
+ (8, 8, "ADMIN"), # "2683/20020627"
175
+ (13, 14, "NAME"), # "João Paulo"
176
+ (21, 21, "ADMIN"), # "2683/20020627" (segunda ocorrência)
177
+ ]
178
+
179
+ # Tokenize for encoder
180
+ inputs = tokenizer(tokens, is_split_into_words=True, return_tensors="pt")
181
+
182
+ # Run encoder
183
+ with torch.no_grad():
184
+ outputs = encoder(**inputs)
185
+
186
+ print("Coreference model loaded successfully.")
187
+ print(f"Hidden states shape: {outputs.last_hidden_state.shape}")
188
+
189
+ ```
190
+
191
+ ## Limitations
192
+
193
+ - **Domain Specificity**: Best performance on administrative/governmental meeting minutes
194
+ - **Language**: Trained exclusively on European Portuguese (PT-PT)
195
+ - **Window Size**: Processes documents via 512-token sliding windows; very long-range coreference (>5,000 tokens apart) may be missed
196
+ - **NER Dependency**: Requires entity spans from a NER model as input — does not perform span detection independently
197
+ - **Same-class Restriction**: Only links mentions of the same NER entity class (e.g., NAME to NAME, never NAME to POSITION)
198
+ - **Dataset Size**: Fine-tuned on 61 training documents; performance on highly specific sub-domains may vary
199
+
200
+ **Version**: 1.0
201
+ **Last Updated**: 2026-06-25
202
+
203
+ ---
204
+ ## License
205
+
206
+ This project uses a custom dual-license based on AGPL v3.
207
+
208
+ See the full license terms here: [LICENSE](./LICENSE)
cabecas_coref.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:8f44f3a6118075ae948cda07a1eafc8ea5acf518d6a4caac00991cc68781aadb
3
+ size 46720064
config.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "BertModel"
4
+ ],
5
+ "attention_probs_dropout_prob": 0.1,
6
+ "classifier_dropout": null,
7
+ "directionality": "bidi",
8
+ "dtype": "float32",
9
+ "hidden_act": "gelu",
10
+ "hidden_dropout_prob": 0.1,
11
+ "hidden_size": 768,
12
+ "initializer_range": 0.02,
13
+ "intermediate_size": 3072,
14
+ "layer_norm_eps": 1e-12,
15
+ "max_position_embeddings": 512,
16
+ "model_type": "bert",
17
+ "num_attention_heads": 12,
18
+ "num_hidden_layers": 12,
19
+ "pad_token_id": 0,
20
+ "position_embedding_type": "absolute",
21
+ "transformers_version": "4.57.3",
22
+ "type_vocab_size": 2,
23
+ "use_cache": true,
24
+ "vocab_size": 28996
25
+ }
coref_config.json ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "hidden_size": 768,
3
+ "ffnn_dim": 1024,
4
+ "max_span_width": 52,
5
+ "max_antecedents": 50,
6
+ "max_cluster_size": 12,
7
+ "dropout": 0.3,
8
+ "dist_buckets": [
9
+ 1,
10
+ 2,
11
+ 3,
12
+ 4,
13
+ 5,
14
+ 8,
15
+ 16,
16
+ 32,
17
+ 64
18
+ ],
19
+ "ner_union_margin": 0.5,
20
+ "min_cluster_score": 0.3,
21
+ "epsilon": 0.00040307387826032937,
22
+ "domain": "atas municipais portuguesas",
23
+ "language": "pt",
24
+ "best_val_conll": 0.9187203268708114,
25
+ "best_epoch": 13
26
+ }
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4929f0963b466f25a25ed32dca2cc2d64e8a8462191077536dd50e130c086136
3
+ size 433263448
special_tokens_map.json ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "cls_token": {
3
+ "content": "[CLS]",
4
+ "lstrip": false,
5
+ "normalized": false,
6
+ "rstrip": false,
7
+ "single_word": false
8
+ },
9
+ "mask_token": {
10
+ "content": "[MASK]",
11
+ "lstrip": false,
12
+ "normalized": false,
13
+ "rstrip": false,
14
+ "single_word": false
15
+ },
16
+ "pad_token": {
17
+ "content": "[PAD]",
18
+ "lstrip": false,
19
+ "normalized": false,
20
+ "rstrip": false,
21
+ "single_word": false
22
+ },
23
+ "sep_token": {
24
+ "content": "[SEP]",
25
+ "lstrip": false,
26
+ "normalized": false,
27
+ "rstrip": false,
28
+ "single_word": false
29
+ },
30
+ "unk_token": {
31
+ "content": "[UNK]",
32
+ "lstrip": false,
33
+ "normalized": false,
34
+ "rstrip": false,
35
+ "single_word": false
36
+ }
37
+ }
tokenizer.json ADDED
The diff for this file is too large to render. See raw diff
 
tokenizer_config.json ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "added_tokens_decoder": {
3
+ "0": {
4
+ "content": "[PAD]",
5
+ "lstrip": false,
6
+ "normalized": false,
7
+ "rstrip": false,
8
+ "single_word": false,
9
+ "special": true
10
+ },
11
+ "100": {
12
+ "content": "[UNK]",
13
+ "lstrip": false,
14
+ "normalized": false,
15
+ "rstrip": false,
16
+ "single_word": false,
17
+ "special": true
18
+ },
19
+ "101": {
20
+ "content": "[CLS]",
21
+ "lstrip": false,
22
+ "normalized": false,
23
+ "rstrip": false,
24
+ "single_word": false,
25
+ "special": true
26
+ },
27
+ "102": {
28
+ "content": "[SEP]",
29
+ "lstrip": false,
30
+ "normalized": false,
31
+ "rstrip": false,
32
+ "single_word": false,
33
+ "special": true
34
+ },
35
+ "103": {
36
+ "content": "[MASK]",
37
+ "lstrip": false,
38
+ "normalized": false,
39
+ "rstrip": false,
40
+ "single_word": false,
41
+ "special": true
42
+ }
43
+ },
44
+ "clean_up_tokenization_spaces": true,
45
+ "cls_token": "[CLS]",
46
+ "do_basic_tokenize": true,
47
+ "do_lower_case": true,
48
+ "extra_special_tokens": {},
49
+ "mask_token": "[MASK]",
50
+ "model_max_length": 1000000000000000019884624838656,
51
+ "never_split": null,
52
+ "pad_token": "[PAD]",
53
+ "sep_token": "[SEP]",
54
+ "strip_accents": null,
55
+ "tokenize_chinese_chars": true,
56
+ "tokenizer_class": "BertTokenizer",
57
+ "unk_token": "[UNK]"
58
+ }
vocab.txt ADDED
The diff for this file is too large to render. See raw diff