bipulsardar commited on
Commit
6f67d1c
·
verified ·
1 Parent(s): 5e49dc1

Update README.md

Browse files
Files changed (1) hide show
  1. README.md +465 -0
README.md CHANGED
@@ -1,3 +1,468 @@
1
  ---
2
  license: apache-2.0
3
  ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  license: apache-2.0
3
  ---
4
+ # VisA Anomaly Classifier: Complete Runnable Setup
5
+
6
+ A Mixture of Experts (MoE) image classification system that identifies industrial product categories and detects anomalies using a product classification router and 12 specialized ResNet-18 experts.
7
+
8
+ **Hugging Face Model:** https://huggingface.co/bipulsardar/Anomaly_Classifier
9
+
10
+ ## 1. Project Structure
11
+
12
+ Create the following project structure:
13
+
14
+ ```text
15
+ visa-anomaly-classifier/
16
+ ├── app.py
17
+ ├── requirements.txt
18
+ └── README.md
19
+ ```
20
+
21
+ Model weights are downloaded automatically from Hugging Face when the application starts.
22
+
23
+ ## 2. Requirements
24
+
25
+ - Python 3.10 or later
26
+ - Internet access for downloading model weights
27
+ - CPU or CUDA-compatible GPU
28
+ - Approximately 600 MB for model weights, plus additional runtime memory
29
+
30
+ ## 3. Install Dependencies
31
+
32
+ Create a virtual environment and install the dependencies.
33
+
34
+ ### Windows
35
+
36
+ ```powershell
37
+ python -m venv .venv
38
+ .venv\Scripts\activate
39
+ pip install -r requirements.txt
40
+ ```
41
+
42
+ ### Linux / macOS / Google Colab
43
+
44
+ ```bash
45
+ python -m venv .venv
46
+ source .venv/bin/activate
47
+ pip install -r requirements.txt
48
+ ```
49
+
50
+ Create `requirements.txt`:
51
+
52
+ ```text
53
+ torch
54
+ torchvision
55
+ huggingface_hub
56
+ pillow
57
+ numpy
58
+ pandas
59
+ scikit-learn
60
+ tqdm
61
+ ```
62
+
63
+ For GPU acceleration, install the PyTorch build compatible with your CUDA environment using the official selector: https://pytorch.org/get-started/locally/
64
+
65
+ ## 4. Complete Application
66
+
67
+ Create `app.py` with the following code.
68
+
69
+ ```python
70
+ import argparse
71
+ import json
72
+ import time
73
+ from pathlib import Path
74
+
75
+ import numpy as np
76
+ import pandas as pd
77
+ import torch
78
+ import torch.nn as nn
79
+
80
+ from PIL import Image
81
+ from torchvision import models, transforms
82
+ from huggingface_hub import snapshot_download
83
+ from sklearn.metrics import (
84
+ accuracy_score,
85
+ precision_score,
86
+ recall_score,
87
+ f1_score,
88
+ roc_auc_score,
89
+ classification_report,
90
+ confusion_matrix
91
+ )
92
+
93
+ CATEGORIES = [
94
+ "candle", "capsules", "cashew", "chewinggum",
95
+ "fryum", "macaroni1", "macaroni2", "pcb1",
96
+ "pcb2", "pcb3", "pcb4", "pipe_fryum"
97
+ ]
98
+
99
+ IMAGE_EXTENSIONS = {
100
+ ".jpg", ".jpeg", ".png", ".bmp", ".tif", ".tiff"
101
+ }
102
+
103
+ REPOSITORY = "bipulsardar/Anomaly_Classifier"
104
+ IMAGE_SIZE = 224
105
+ DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
106
+
107
+ TRANSFORM = transforms.Compose([
108
+ transforms.Resize((IMAGE_SIZE, IMAGE_SIZE)),
109
+ transforms.ToTensor(),
110
+ transforms.Normalize(
111
+ mean=[0.485, 0.456, 0.406],
112
+ std=[0.229, 0.224, 0.225]
113
+ )
114
+ ])
115
+
116
+ def create_model(num_classes):
117
+ model = models.resnet18(weights=None)
118
+ model.fc = nn.Linear(model.fc.in_features, num_classes)
119
+ return model
120
+
121
+ def load_model(path, num_classes):
122
+ checkpoint = torch.load(
123
+ path,
124
+ map_location=DEVICE,
125
+ weights_only=True
126
+ )
127
+
128
+ state_dict = checkpoint.get("model_state_dict", checkpoint)
129
+
130
+ model = create_model(num_classes)
131
+ model.load_state_dict(state_dict)
132
+ model.to(DEVICE)
133
+ model.eval()
134
+
135
+ return model
136
+
137
+ def load_models(model_dir=None, token=None):
138
+ if model_dir:
139
+ directory = Path(model_dir)
140
+ else:
141
+ directory = Path(snapshot_download(
142
+ repo_id=REPOSITORY,
143
+ repo_type="model",
144
+ token=token
145
+ ))
146
+
147
+ router = load_model(directory / "router.pth", len(CATEGORIES))
148
+
149
+ experts = {
150
+ category: load_model(
151
+ directory / f"expert_{category}.pth",
152
+ 2
153
+ )
154
+ for category in CATEGORIES
155
+ }
156
+
157
+ return router, experts
158
+
159
+ def predict_image(image_path, router, experts):
160
+ with Image.open(image_path) as image:
161
+ tensor = TRANSFORM(image.convert("RGB")).unsqueeze(0).to(DEVICE)
162
+
163
+ if DEVICE.type == "cuda":
164
+ torch.cuda.synchronize()
165
+
166
+ start = time.perf_counter()
167
+
168
+ with torch.inference_mode():
169
+ router_probabilities = torch.softmax(router(tensor), dim=1)
170
+
171
+ category_id = int(router_probabilities.argmax(dim=1).item())
172
+ category = CATEGORIES[category_id]
173
+
174
+ expert_probabilities = torch.softmax(
175
+ experts[category](tensor),
176
+ dim=1
177
+ )
178
+
179
+ anomaly_score = float(expert_probabilities[0, 1].item())
180
+ prediction = int(expert_probabilities.argmax(dim=1).item())
181
+
182
+ if DEVICE.type == "cuda":
183
+ torch.cuda.synchronize()
184
+
185
+ elapsed_ms = (time.perf_counter() - start) * 1000
186
+
187
+ return {
188
+ "image_path": str(image_path),
189
+ "category": category,
190
+ "category_confidence": float(
191
+ router_probabilities[0, category_id].item()
192
+ ),
193
+ "prediction": "anomaly" if prediction == 1 else "normal",
194
+ "anomaly_score": anomaly_score,
195
+ "latency_ms": elapsed_ms
196
+ }
197
+
198
+ def discover_dataset(dataset_dir):
199
+ root = Path(dataset_dir)
200
+ records = []
201
+
202
+ for category in CATEGORIES:
203
+ image_root = root / category / "Data" / "Images"
204
+
205
+ for folder, label in [("Normal", 0), ("Anomaly", 1)]:
206
+ directory = image_root / folder
207
+
208
+ if not directory.exists():
209
+ continue
210
+
211
+ for path in directory.rglob("*"):
212
+ if path.is_file() and path.suffix.lower() in IMAGE_EXTENSIONS:
213
+ records.append({
214
+ "image_path": str(path),
215
+ "true_category": category,
216
+ "true_label": label
217
+ })
218
+
219
+ dataframe = pd.DataFrame(records)
220
+
221
+ if dataframe.empty:
222
+ raise RuntimeError(
223
+ f"No VisA images found under {root}. "
224
+ "Expected category/Data/Images/Normal and Anomaly folders."
225
+ )
226
+
227
+ return dataframe.drop_duplicates("image_path").reset_index(drop=True)
228
+
229
+ def benchmark(dataset_dir, router, experts, output_dir):
230
+ output = Path(output_dir)
231
+ output.mkdir(parents=True, exist_ok=True)
232
+
233
+ dataframe = discover_dataset(dataset_dir)
234
+ results = []
235
+
236
+ for row in dataframe.itertuples(index=False):
237
+ try:
238
+ result = predict_image(row.image_path, router, experts)
239
+ result["true_category"] = row.true_category
240
+ result["true_label"] = int(row.true_label)
241
+ result["category_correct"] = (
242
+ result["category"] == row.true_category
243
+ )
244
+ results.append(result)
245
+ except Exception as error:
246
+ print(f"Failed: {row.image_path}: {error}")
247
+
248
+ result_df = pd.DataFrame(results)
249
+
250
+ if result_df.empty:
251
+ raise RuntimeError("No images were successfully evaluated.")
252
+
253
+ y_true = result_df["true_label"].to_numpy()
254
+ y_pred = (
255
+ result_df["prediction"].eq("anomaly").astype(int).to_numpy()
256
+ )
257
+ y_score = result_df["anomaly_score"].to_numpy()
258
+
259
+ try:
260
+ auc = float(roc_auc_score(y_true, y_score))
261
+ except ValueError:
262
+ auc = None
263
+
264
+ summary = {
265
+ "repository": REPOSITORY,
266
+ "device": str(DEVICE),
267
+ "images_evaluated": len(result_df),
268
+ "images_failed": len(dataframe) - len(result_df),
269
+ "router_accuracy": float(
270
+ result_df["category_correct"].mean()
271
+ ),
272
+ "anomaly_accuracy": float(accuracy_score(y_true, y_pred)),
273
+ "anomaly_precision": float(
274
+ precision_score(y_true, y_pred, zero_division=0)
275
+ ),
276
+ "anomaly_recall": float(
277
+ recall_score(y_true, y_pred, zero_division=0)
278
+ ),
279
+ "anomaly_f1": float(
280
+ f1_score(y_true, y_pred, zero_division=0)
281
+ ),
282
+ "anomaly_roc_auc": auc,
283
+ "mean_latency_ms": float(result_df["latency_ms"].mean()),
284
+ "median_latency_ms": float(result_df["latency_ms"].median()),
285
+ "p95_latency_ms": float(result_df["latency_ms"].quantile(0.95))
286
+ }
287
+
288
+ result_df.to_csv(output / "predictions.csv", index=False)
289
+
290
+ with open(output / "benchmark_summary.json", "w") as file:
291
+ json.dump(summary, file, indent=2)
292
+
293
+ print("\nBenchmark summary")
294
+ print(json.dumps(summary, indent=2))
295
+
296
+ print("\nClassification report")
297
+ print(classification_report(
298
+ y_true,
299
+ y_pred,
300
+ labels=[0, 1],
301
+ target_names=["Normal", "Anomaly"],
302
+ zero_division=0
303
+ ))
304
+
305
+ print("\nConfusion matrix")
306
+ print(confusion_matrix(y_true, y_pred, labels=[0, 1]))
307
+
308
+ return result_df, summary
309
+
310
+ def main():
311
+ parser = argparse.ArgumentParser()
312
+ parser.add_argument("--image", type=str)
313
+ parser.add_argument("--image-dir", type=str)
314
+ parser.add_argument("--dataset-dir", type=str)
315
+ parser.add_argument("--output-dir", default="benchmark_results")
316
+ parser.add_argument("--model-dir", type=str)
317
+ parser.add_argument("--hf-token", type=str)
318
+ args = parser.parse_args()
319
+
320
+ router, experts = load_models(args.model_dir, args.hf_token)
321
+
322
+ print(f"Device: {DEVICE}")
323
+ print(f"Loaded {len(experts)} experts.")
324
+
325
+ if args.image:
326
+ print(json.dumps(
327
+ predict_image(args.image, router, experts),
328
+ indent=2
329
+ ))
330
+ return
331
+
332
+ if args.dataset_dir:
333
+ benchmark(
334
+ args.dataset_dir,
335
+ router,
336
+ experts,
337
+ args.output_dir
338
+ )
339
+ return
340
+
341
+ if args.image_dir:
342
+ image_paths = [
343
+ path for path in Path(args.image_dir).rglob("*")
344
+ if path.is_file() and path.suffix.lower() in IMAGE_EXTENSIONS
345
+ ]
346
+
347
+ output = []
348
+
349
+ for path in image_paths:
350
+ try:
351
+ output.append(predict_image(path, router, experts))
352
+ except Exception as error:
353
+ output.append({
354
+ "image_path": str(path),
355
+ "error": str(error)
356
+ })
357
+
358
+ Path(args.output_dir).mkdir(parents=True, exist_ok=True)
359
+
360
+ pd.DataFrame(output).to_csv(
361
+ Path(args.output_dir) / "predictions.csv",
362
+ index=False
363
+ )
364
+
365
+ print(f"Processed {len(output)} images.")
366
+ return
367
+
368
+ parser.error(
369
+ "Specify --image, --image-dir, or --dataset-dir."
370
+ )
371
+
372
+ if __name__ == "__main__":
373
+ main()
374
+ ```
375
+
376
+ ## 5. Run Predictions
377
+
378
+ ### Single image
379
+
380
+ ```bash
381
+ python app.py --image ./product.jpg
382
+ ```
383
+
384
+ ### Batch image directory
385
+
386
+ ```bash
387
+ python app.py --image-dir ./product_images --output-dir ./results
388
+ ```
389
+
390
+ The predictions will be saved to `results/predictions.csv`.
391
+
392
+ ### Evaluate the VisA dataset
393
+
394
+ If the dataset is extracted to `/content/VisA` in Google Colab:
395
+
396
+ ```bash
397
+ python app.py --dataset-dir /content/VisA --output-dir /content/benchmark_results
398
+ ```
399
+
400
+ On Windows:
401
+
402
+ ```powershell
403
+ python app.py --dataset-dir "E:\Datasets\VisA" --output-dir ".\benchmark_results"
404
+ ```
405
+
406
+ The benchmark outputs include per-image predictions and aggregate metrics.
407
+
408
+ ## 6. Use in Google Colab
409
+
410
+ Run these cells in order.
411
+
412
+ ```python
413
+ !pip -q install torch torchvision huggingface_hub pillow numpy pandas scikit-learn tqdm
414
+ ```
415
+
416
+ Upload `app.py` to Colab or create it using a notebook cell, then run:
417
+
418
+ ```python
419
+ !python app.py --image "/content/product.jpg"
420
+ ```
421
+
422
+ Benchmark all VisA images:
423
+
424
+ ```python
425
+ !python app.py --dataset-dir "/content/VisA" --output-dir "/content/benchmark_results"
426
+ ```
427
+
428
+ ## 7. Private Hugging Face Repositories
429
+
430
+ If the repository is private, authenticate before running the application.
431
+
432
+ ```python
433
+ from huggingface_hub import login
434
+ from kaggle_secrets import UserSecretsClient
435
+
436
+ secrets = UserSecretsClient()
437
+ token = secrets.get_secret("HF_091")
438
+
439
+ login(token=token)
440
+ ```
441
+
442
+ Alternatively, set `HF_TOKEN` in the environment and pass it to `load_models()`. Do not commit access tokens to source control.
443
+
444
+ ## 8. Interpreting Results
445
+
446
+ The application returns:
447
+
448
+ - `category`: predicted product category.
449
+ - `category_confidence`: router softmax score for the selected category.
450
+ - `prediction`: normal or anomaly.
451
+ - `anomaly_score`: expert softmax score for the anomaly class.
452
+ - `latency_ms`: inference time for the router and selected expert.
453
+
454
+ These scores are not necessarily calibrated probabilities. Set production thresholds using a separate validation dataset.
455
+
456
+ ## 9. Important Evaluation Notes
457
+
458
+ The previously reported full-dataset results are preliminary because the evaluation included training images. For a reliable estimate of generalization, evaluate on held-out test images that were never used to train or select the models.
459
+
460
+ This system performs image-level anomaly classification. It does not currently produce pixel-level defect localization.
461
+
462
+ ## 10. References
463
+
464
+ - Model repository: https://huggingface.co/bipulsardar/Anomaly_Classifier
465
+ - VisA dataset: https://github.com/amazon-science/spot-diff
466
+ - PyTorch: https://pytorch.org/
467
+ - Torchvision: https://pytorch.org/vision/
468
+ - Hugging Face Hub: https://huggingface.co/docs/huggingface_hub