AnneQtao commited on
Commit
7decdbf
·
verified ·
1 Parent(s): faecca4

Upload score_pilot_mdna_v7_hf_jobs.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. score_pilot_mdna_v7_hf_jobs.py +431 -0
score_pilot_mdna_v7_hf_jobs.py ADDED
@@ -0,0 +1,431 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # /// script
2
+ # dependencies = [
3
+ # "transformers",
4
+ # "accelerate",
5
+ # "peft",
6
+ # "bitsandbytes",
7
+ # "sentencepiece",
8
+ # "huggingface_hub",
9
+ # "pandas",
10
+ # "tqdm",
11
+ # "scikit-learn"
12
+ # ]
13
+ # ///
14
+
15
+ import os
16
+ import json
17
+ import gc
18
+ from datetime import datetime
19
+
20
+ import pandas as pd
21
+ import torch
22
+ import torch.nn.functional as F
23
+ from tqdm.auto import tqdm
24
+
25
+ from huggingface_hub import hf_hub_download, HfApi
26
+ from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
27
+ from peft import PeftModel
28
+
29
+
30
+ # =========================
31
+ # 1. Basic configuration
32
+ # =========================
33
+
34
+ HF_TOKEN = os.environ.get("HF_TOKEN")
35
+
36
+ if HF_TOKEN is None:
37
+ raise ValueError("HF_TOKEN is missing. Please pass it with --secrets HF_TOKEN.")
38
+
39
+ MODEL_REPO = "AnneQtao/deepseek-r1-finsent-lora-v7"
40
+ BASE_MODEL_NAME = "deepseek-ai/DeepSeek-R1-Distill-Llama-8B"
41
+
42
+ INPUT_CSV = "pilot_mdna_2022_2024_30.csv"
43
+
44
+ OUTPUT_CSV = "pilot_mdna_v7_full_document_scores_a100.csv"
45
+ OUTPUT_METADATA = "pilot_mdna_v7_full_document_scores_a100_metadata.json"
46
+
47
+ MAX_INPUT_TOKENS = 32768
48
+
49
+ LABEL_OPTIONS = {
50
+ "Positive": "positive",
51
+ "Negative": "negative",
52
+ "Neutral": "neutral"
53
+ }
54
+
55
+ DISPLAY_LABELS = ["Positive", "Negative", "Neutral"]
56
+
57
+
58
+ # =========================
59
+ # 2. Helper functions
60
+ # =========================
61
+
62
+ def make_prompt_full_mdna(mdna_text: str) -> str:
63
+ return (
64
+ "Classify the sentiment of the following MD&A disclosure as "
65
+ "positive, negative, or neutral based on expected implications for "
66
+ "firm performance, demand, costs, margins, risks, uncertainty, and outlook.\n\n"
67
+ f"MD&A:\n{mdna_text}\n\n"
68
+ "Sentiment:"
69
+ )
70
+
71
+
72
+ def safe_cuda_empty_cache():
73
+ gc.collect()
74
+ if torch.cuda.is_available():
75
+ torch.cuda.empty_cache()
76
+
77
+
78
+ @torch.no_grad()
79
+ def label_logprob_no_truncation(model, tokenizer, prompt: str, label_text: str):
80
+ full_token_count = len(tokenizer(prompt, add_special_tokens=False).input_ids)
81
+
82
+ if full_token_count > MAX_INPUT_TOKENS:
83
+ return None, full_token_count, True
84
+
85
+ prompt_ids = tokenizer(
86
+ prompt,
87
+ return_tensors="pt",
88
+ truncation=False
89
+ ).input_ids.to(model.device)
90
+
91
+ label_ids = tokenizer(
92
+ label_text,
93
+ add_special_tokens=False,
94
+ return_tensors="pt"
95
+ ).input_ids.to(model.device)
96
+
97
+ input_ids = torch.cat([prompt_ids, label_ids], dim=1)
98
+
99
+ outputs = model(input_ids=input_ids, use_cache=False)
100
+ logits = outputs.logits
101
+
102
+ log_probs = F.log_softmax(logits[:, :-1, :], dim=-1)
103
+
104
+ prompt_len = prompt_ids.shape[1]
105
+ total_logprob = 0.0
106
+
107
+ for j in range(label_ids.shape[1]):
108
+ token_id = label_ids[0, j]
109
+ position = prompt_len + j - 1
110
+ total_logprob += log_probs[0, position, token_id].item()
111
+
112
+ del prompt_ids, label_ids, input_ids, outputs, logits, log_probs
113
+ safe_cuda_empty_cache()
114
+
115
+ return total_logprob, full_token_count, False
116
+
117
+
118
+ def score_one_mdna(model, tokenizer, mdna_text: str):
119
+ prompt = make_prompt_full_mdna(mdna_text)
120
+ full_token_count = len(tokenizer(prompt, add_special_tokens=False).input_ids)
121
+
122
+ if full_token_count > MAX_INPUT_TOKENS:
123
+ return {
124
+ "score_status": "SKIPPED_TOO_LONG",
125
+ "score_token_count": int(full_token_count),
126
+ "max_input_tokens": int(MAX_INPUT_TOKENS),
127
+ "truncated": False,
128
+ "too_long_for_current_limit": True,
129
+ "P_pos": None,
130
+ "P_neg": None,
131
+ "P_neu": None,
132
+ "S_DS": None,
133
+ "predicted_label": "SKIPPED_TOO_LONG"
134
+ }
135
+
136
+ try:
137
+ scores = []
138
+
139
+ for label in DISPLAY_LABELS:
140
+ score, tc, too_long = label_logprob_no_truncation(
141
+ model=model,
142
+ tokenizer=tokenizer,
143
+ prompt=prompt,
144
+ label_text=LABEL_OPTIONS[label],
145
+ )
146
+
147
+ if too_long:
148
+ return {
149
+ "score_status": "SKIPPED_TOO_LONG",
150
+ "score_token_count": int(tc),
151
+ "max_input_tokens": int(MAX_INPUT_TOKENS),
152
+ "truncated": False,
153
+ "too_long_for_current_limit": True,
154
+ "P_pos": None,
155
+ "P_neg": None,
156
+ "P_neu": None,
157
+ "S_DS": None,
158
+ "predicted_label": "SKIPPED_TOO_LONG"
159
+ }
160
+
161
+ scores.append(score)
162
+
163
+ scores = torch.tensor(scores, dtype=torch.float32)
164
+ probs = F.softmax(scores, dim=0).cpu().numpy()
165
+
166
+ return {
167
+ "score_status": "SUCCESS",
168
+ "score_token_count": int(full_token_count),
169
+ "max_input_tokens": int(MAX_INPUT_TOKENS),
170
+ "truncated": False,
171
+ "too_long_for_current_limit": False,
172
+ "P_pos": float(probs[0]),
173
+ "P_neg": float(probs[1]),
174
+ "P_neu": float(probs[2]),
175
+ "S_DS": float(probs[0] - probs[1]),
176
+ "predicted_label": DISPLAY_LABELS[int(probs.argmax())]
177
+ }
178
+
179
+ except RuntimeError as e:
180
+ message = str(e)
181
+
182
+ if "out of memory" in message.lower() or "cuda" in message.lower():
183
+ safe_cuda_empty_cache()
184
+
185
+ return {
186
+ "score_status": "CUDA_OOM",
187
+ "score_token_count": int(full_token_count),
188
+ "max_input_tokens": int(MAX_INPUT_TOKENS),
189
+ "truncated": False,
190
+ "too_long_for_current_limit": False,
191
+ "P_pos": None,
192
+ "P_neg": None,
193
+ "P_neu": None,
194
+ "S_DS": None,
195
+ "predicted_label": "CUDA_OOM"
196
+ }
197
+
198
+ raise e
199
+
200
+
201
+ # =========================
202
+ # 3. Start job
203
+ # =========================
204
+
205
+ print("=" * 100)
206
+ print("Starting V7 full-document MD&A scoring job")
207
+ print("Time:", datetime.utcnow().isoformat())
208
+ print("Model repo:", MODEL_REPO)
209
+ print("Base model:", BASE_MODEL_NAME)
210
+ print("Max input tokens:", MAX_INPUT_TOKENS)
211
+ print("=" * 100)
212
+
213
+ print("CUDA available:", torch.cuda.is_available())
214
+ if torch.cuda.is_available():
215
+ print("GPU:", torch.cuda.get_device_name(0))
216
+ print("GPU memory allocated:", torch.cuda.memory_allocated() / 1024**3, "GB")
217
+
218
+
219
+ # =========================
220
+ # 4. Download input CSV
221
+ # =========================
222
+
223
+ print("Downloading input CSV from Hugging Face repo...")
224
+
225
+ input_path = hf_hub_download(
226
+ repo_id=MODEL_REPO,
227
+ filename=INPUT_CSV,
228
+ repo_type="model",
229
+ token=HF_TOKEN
230
+ )
231
+
232
+ pilot_mdna_df = pd.read_csv(input_path)
233
+
234
+ print("Input shape:", pilot_mdna_df.shape)
235
+ print("Input columns:", pilot_mdna_df.columns.tolist())
236
+
237
+
238
+ # =========================
239
+ # 5. Keep valid MD&A
240
+ # =========================
241
+
242
+ pilot_ok_df = pilot_mdna_df[
243
+ (pilot_mdna_df["extract_status"] == "OK") &
244
+ (pilot_mdna_df["mdna_text"].notna()) &
245
+ (pilot_mdna_df["mdna_char_len"] > 2000)
246
+ ].copy()
247
+
248
+ print("Usable MD&A filings:", len(pilot_ok_df))
249
+
250
+ if len(pilot_ok_df) == 0:
251
+ raise ValueError("No usable MD&A filings found.")
252
+
253
+
254
+ # =========================
255
+ # 6. Load tokenizer + model + adapter
256
+ # =========================
257
+
258
+ print("Loading tokenizer...")
259
+
260
+ tokenizer = AutoTokenizer.from_pretrained(
261
+ MODEL_REPO,
262
+ trust_remote_code=True,
263
+ token=HF_TOKEN
264
+ )
265
+
266
+ if tokenizer.pad_token is None:
267
+ tokenizer.pad_token = tokenizer.eos_token
268
+
269
+ # Avoid tokenizer warning from shorter stored tokenizer_config
270
+ tokenizer.model_max_length = MAX_INPUT_TOKENS
271
+
272
+ print("Loading base model in 4-bit...")
273
+
274
+ bnb_config = BitsAndBytesConfig(
275
+ load_in_4bit=True,
276
+ bnb_4bit_compute_dtype=torch.float16,
277
+ bnb_4bit_use_double_quant=True,
278
+ bnb_4bit_quant_type="nf4"
279
+ )
280
+
281
+ base_model = AutoModelForCausalLM.from_pretrained(
282
+ BASE_MODEL_NAME,
283
+ quantization_config=bnb_config,
284
+ device_map="auto",
285
+ torch_dtype=torch.float16,
286
+ trust_remote_code=True,
287
+ attn_implementation="sdpa"
288
+ )
289
+
290
+ base_model.config.use_cache = False
291
+
292
+ print("Loading V7 LoRA adapter...")
293
+
294
+ model = PeftModel.from_pretrained(
295
+ base_model,
296
+ MODEL_REPO,
297
+ token=HF_TOKEN
298
+ )
299
+
300
+ model.eval()
301
+ model.config.use_cache = False
302
+
303
+ print("✅ Base model + V7 LoRA adapter loaded successfully!")
304
+
305
+
306
+ # =========================
307
+ # 7. Token count
308
+ # =========================
309
+
310
+ print("Counting tokens...")
311
+
312
+ pilot_ok_df["token_count"] = pilot_ok_df["mdna_text"].apply(
313
+ lambda x: len(tokenizer(make_prompt_full_mdna(x), add_special_tokens=False).input_ids)
314
+ )
315
+
316
+ print("Token count summary:")
317
+ print(pilot_ok_df["token_count"].describe())
318
+
319
+ for limit in [4096, 8192, 16384, 32768, 65536]:
320
+ n_over = (pilot_ok_df["token_count"] > limit).sum()
321
+ print(f"Over {limit} tokens: {n_over} / {len(pilot_ok_df)}")
322
+
323
+
324
+ # =========================
325
+ # 8. Score each MD&A
326
+ # =========================
327
+
328
+ print("Scoring full MD&A documents...")
329
+
330
+ score_rows = []
331
+
332
+ for idx, row in tqdm(pilot_ok_df.iterrows(), total=len(pilot_ok_df)):
333
+ ticker = row.get("ticker", "UNKNOWN")
334
+ filing_date = row.get("filing_date", "UNKNOWN")
335
+ token_count = row.get("token_count", None)
336
+
337
+ print("-" * 100)
338
+ print(f"Scoring: {ticker} | {filing_date} | tokens={token_count}")
339
+
340
+ result = score_one_mdna(
341
+ model=model,
342
+ tokenizer=tokenizer,
343
+ mdna_text=row["mdna_text"]
344
+ )
345
+
346
+ print("Result:", result)
347
+
348
+ merged = row.to_dict()
349
+ merged.update(result)
350
+ score_rows.append(merged)
351
+
352
+ safe_cuda_empty_cache()
353
+
354
+
355
+ pilot_scored_df = pd.DataFrame(score_rows)
356
+
357
+ # Put important columns first
358
+ important_cols = [
359
+ "pilot_group", "ticker", "company_name", "form", "filing_date", "report_date",
360
+ "extract_status", "mdna_char_len", "token_count", "score_token_count",
361
+ "max_input_tokens", "score_status", "truncated", "too_long_for_current_limit",
362
+ "P_pos", "P_neg", "P_neu", "S_DS", "predicted_label", "filing_url"
363
+ ]
364
+
365
+ existing_important_cols = [c for c in important_cols if c in pilot_scored_df.columns]
366
+ other_cols = [c for c in pilot_scored_df.columns if c not in existing_important_cols]
367
+
368
+ pilot_scored_df = pilot_scored_df[existing_important_cols + other_cols]
369
+
370
+ pilot_scored_df.to_csv(OUTPUT_CSV, index=False)
371
+
372
+ print("Saved output:", OUTPUT_CSV)
373
+ print(pilot_scored_df[[
374
+ "pilot_group", "ticker", "form", "filing_date",
375
+ "token_count", "score_status",
376
+ "P_pos", "P_neg", "P_neu", "S_DS", "predicted_label"
377
+ ]])
378
+
379
+
380
+ # =========================
381
+ # 9. Save metadata
382
+ # =========================
383
+
384
+ metadata = {
385
+ "run_time_utc": datetime.utcnow().isoformat(),
386
+ "model_repo": MODEL_REPO,
387
+ "base_model": BASE_MODEL_NAME,
388
+ "input_csv": INPUT_CSV,
389
+ "output_csv": OUTPUT_CSV,
390
+ "max_input_tokens": MAX_INPUT_TOKENS,
391
+ "n_input_rows": int(len(pilot_mdna_df)),
392
+ "n_usable_mdna": int(len(pilot_ok_df)),
393
+ "n_success": int((pilot_scored_df["score_status"] == "SUCCESS").sum()),
394
+ "n_skipped_too_long": int((pilot_scored_df["score_status"] == "SKIPPED_TOO_LONG").sum()),
395
+ "n_cuda_oom": int((pilot_scored_df["score_status"] == "CUDA_OOM").sum()),
396
+ "token_count_summary": pilot_ok_df["token_count"].describe().to_dict(),
397
+ }
398
+
399
+ with open(OUTPUT_METADATA, "w") as f:
400
+ json.dump(metadata, f, indent=2)
401
+
402
+ print("Saved metadata:", OUTPUT_METADATA)
403
+ print(json.dumps(metadata, indent=2))
404
+
405
+
406
+ # =========================
407
+ # 10. Upload outputs to Hugging Face repo
408
+ # =========================
409
+
410
+ print("Uploading outputs to Hugging Face repo...")
411
+
412
+ api = HfApi(token=HF_TOKEN)
413
+
414
+ api.upload_file(
415
+ path_or_fileobj=OUTPUT_CSV,
416
+ path_in_repo=OUTPUT_CSV,
417
+ repo_id=MODEL_REPO,
418
+ repo_type="model"
419
+ )
420
+
421
+ api.upload_file(
422
+ path_or_fileobj=OUTPUT_METADATA,
423
+ path_in_repo=OUTPUT_METADATA,
424
+ repo_id=MODEL_REPO,
425
+ repo_type="model"
426
+ )
427
+
428
+ print("✅ Uploaded outputs to:", MODEL_REPO)
429
+ print("Output CSV:", OUTPUT_CSV)
430
+ print("Metadata:", OUTPUT_METADATA)
431
+ print("Job finished.")