Saniter commited on
Commit
12ab067
·
verified ·
1 Parent(s): a472778

Upload inference.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. inference.py +187 -0
inference.py ADDED
@@ -0,0 +1,187 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import json
3
+ import torch
4
+ import torch.nn as nn
5
+ from transformers import AutoTokenizer, AutoModel
6
+ import argparse
7
+ from loguru import logger
8
+ from tqdm import tqdm
9
+ from torch.utils.data import DataLoader, Dataset
10
+
11
+ class GlobalMeanPoolClassifier(nn.Module):
12
+ def __init__(self, base_model, num_labels=2, dropout=0.0):
13
+ super().__init__()
14
+ self.base_model = base_model
15
+ if hasattr(base_model.config, "text_config"):
16
+ hidden_size = base_model.config.text_config.hidden_size
17
+ else:
18
+ hidden_size = base_model.config.hidden_size
19
+
20
+ self.classifier = nn.Sequential(
21
+ nn.LayerNorm(hidden_size),
22
+ nn.Linear(hidden_size, 1024),
23
+ nn.SiLU(),
24
+ nn.Dropout(dropout),
25
+ nn.Linear(1024, num_labels),
26
+ )
27
+
28
+ def forward(self, input_ids, attention_mask, **kwargs):
29
+ outputs = self.base_model(
30
+ input_ids=input_ids,
31
+ attention_mask=attention_mask,
32
+ output_hidden_states=True,
33
+ )
34
+ # Layer 19 for pure local features
35
+ features = outputs.hidden_states[19]
36
+
37
+ batch_size = input_ids.size(0)
38
+ pooled_features = []
39
+
40
+ for i in range(batch_size):
41
+ mask = attention_mask[i].bool()
42
+ # Global Mean Pooling on non-pad tokens
43
+ global_feat = features[i, mask, :].mean(dim=0)
44
+ pooled_features.append(global_feat)
45
+
46
+ pooled_features = torch.stack(pooled_features)
47
+ logits = self.classifier(pooled_features)
48
+ return logits
49
+
50
+
51
+ PROMPT_TEMPLATE = (
52
+ "任务:论坛新词发现。\n"
53
+ "根据候选词及其上下文,判断该词是否具有稳定独立的语义。\n"
54
+ "候选词:{word}\n"
55
+ "上下文:{context}"
56
+ )
57
+
58
+ class InferenceDataset(Dataset):
59
+ def __init__(self, items, tokenizer):
60
+ self.items = items
61
+ self.tokenizer = tokenizer
62
+
63
+ def __len__(self):
64
+ return len(self.items)
65
+
66
+ def __getitem__(self, idx):
67
+ item = self.items[idx]
68
+ text = PROMPT_TEMPLATE.format(word=item["word"], context=item["context"])
69
+ return {
70
+ "word": item["word"],
71
+ "text": text
72
+ }
73
+
74
+ def collate_fn(batch, tokenizer):
75
+ words = [b["word"] for b in batch]
76
+ texts = [b["text"] for b in batch]
77
+ encodings = tokenizer(texts, padding=True, truncation=True, max_length=256, return_tensors="pt")
78
+ return words, encodings
79
+
80
+ def run_inference(input_path, model_dir, output_dir, batch_size=32):
81
+ device = "cuda" if torch.cuda.is_available() else "cpu"
82
+ logger.info(f"Using device: {device}")
83
+
84
+ logger.info(f"Loading Base Model and Tokenizer from {model_dir}...")
85
+ tokenizer = AutoTokenizer.from_pretrained(model_dir, trust_remote_code=True)
86
+ if tokenizer.pad_token is None:
87
+ tokenizer.pad_token = tokenizer.eos_token
88
+
89
+ base_model = AutoModel.from_pretrained(
90
+ model_dir,
91
+ trust_remote_code=True,
92
+ torch_dtype=torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16,
93
+ device_map="auto"
94
+ )
95
+
96
+ logger.info("Initializing GlobalMeanPoolClassifier...")
97
+ model = GlobalMeanPoolClassifier(base_model, num_labels=2)
98
+
99
+ head_path = os.path.join(model_dir, "classifier_head.pt")
100
+ if os.path.exists(head_path):
101
+ logger.info("Loading trained classifier head weights...")
102
+ model.classifier.load_state_dict(torch.load(head_path, map_location="cpu"))
103
+ else:
104
+ logger.warning(f"No classifier_head.pt found at {head_path}! Model will use random head weights.")
105
+
106
+ model.to(device)
107
+ model.eval()
108
+
109
+ logger.info(f"Reading input data from {input_path}...")
110
+ with open(input_path, "r", encoding="utf-8") as f:
111
+ data = json.load(f)
112
+
113
+ # Flatten data for batching
114
+ items = []
115
+ for word, info in data.items():
116
+ contexts = info.get("contexts", [])
117
+ if not contexts:
118
+ continue
119
+ # Take up to 5 contexts per word to speed up and avoid bias
120
+ for ctx in contexts[:5]:
121
+ items.append({"word": word, "context": ctx})
122
+
123
+ dataset = InferenceDataset(items, tokenizer)
124
+ dataloader = DataLoader(dataset, batch_size=batch_size, shuffle=False, collate_fn=lambda b: collate_fn(b, tokenizer))
125
+
126
+ logger.info("Running inference...")
127
+ word_scores = {}
128
+
129
+ with torch.no_grad():
130
+ for words, encodings in tqdm(dataloader):
131
+ input_ids = encodings["input_ids"].to(device)
132
+ attention_mask = encodings["attention_mask"].to(device)
133
+
134
+ logits = model(input_ids=input_ids, attention_mask=attention_mask)
135
+ probs = torch.softmax(logits, dim=-1)
136
+
137
+ # Prob for class 0 (Accept)
138
+ accept_probs = probs[:, 0].cpu().numpy()
139
+
140
+ for word, prob in zip(words, accept_probs):
141
+ if word not in word_scores:
142
+ word_scores[word] = []
143
+ word_scores[word].append(float(prob))
144
+
145
+ # Aggregate scores (Mean probability across all contexts)
146
+ accepted = {}
147
+ rejected = {}
148
+
149
+ logger.info("Aggregating results...")
150
+ for word, probs in word_scores.items():
151
+ mean_prob = sum(probs) / len(probs)
152
+ result_entry = {
153
+ "score": round(mean_prob, 4),
154
+ "contexts_analyzed": len(probs)
155
+ }
156
+
157
+ # Threshold at 0.5
158
+ if mean_prob >= 0.5:
159
+ accepted[word] = result_entry
160
+ else:
161
+ rejected[word] = result_entry
162
+
163
+ os.makedirs(output_dir, exist_ok=True)
164
+
165
+ accept_path = os.path.join(output_dir, "accepted_words.json")
166
+ reject_path = os.path.join(output_dir, "rejected_words.json")
167
+
168
+ with open(accept_path, "w", encoding="utf-8") as f:
169
+ json.dump(accepted, f, ensure_ascii=False, indent=2)
170
+
171
+ with open(reject_path, "w", encoding="utf-8") as f:
172
+ json.dump(rejected, f, ensure_ascii=False, indent=2)
173
+
174
+ logger.info(f"Done! Evaluated {len(word_scores)} unique words.")
175
+ logger.info(f"Accepted: {len(accepted)} words -> {accept_path}")
176
+ logger.info(f"Rejected: {len(rejected)} words -> {reject_path}")
177
+
178
+
179
+ if __name__ == "__main__":
180
+ parser = argparse.ArgumentParser(description="Batch inference for BBS New Words Discovery")
181
+ parser.add_argument("--input", type=str, required=True, help="Path to input JSON file (e.g., data.json)")
182
+ parser.add_argument("--model_dir", type=str, default="./final_production_model", help="Path to merged production model")
183
+ parser.add_argument("--output_dir", type=str, default="./inference_results", help="Directory to save accepted/rejected files")
184
+ parser.add_argument("--batch_size", type=int, default=32, help="Batch size for inference")
185
+
186
+ args = parser.parse_args()
187
+ run_inference(args.input, args.model_dir, args.output_dir, args.batch_size)