thesoikindustries24 commited on
Commit
824fa9b
·
1 Parent(s): 218658e

Add custom trained hybrid model for Hen and Peacock (using LFS)

Browse files
.gitattributes ADDED
@@ -0,0 +1 @@
 
 
1
+ *.safetensors filter=lfs diff=lfs merge=lfs -text
.gitignore CHANGED
@@ -4,3 +4,4 @@ uploads/
4
  .vscode/
5
  *.pyc
6
  BirdNET-Analyzer-main*/
 
 
4
  .vscode/
5
  *.pyc
6
  BirdNET-Analyzer-main*/
7
+ dataset/
app.py CHANGED
@@ -39,6 +39,9 @@ os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
39
  models = {
40
  'image_processor': None,
41
  'image_model': None,
 
 
 
42
  'loaded': False,
43
  'loading': False,
44
  'error': None
@@ -60,9 +63,23 @@ def load_models():
60
 
61
  models['image_processor'] = AutoImageProcessor.from_pretrained("chriamue/bird-species-classifier")
62
  models['image_model'] = AutoModelForImageClassification.from_pretrained("chriamue/bird-species-classifier")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  models['loaded'] = True
64
  models['error'] = None
65
- print("Image model loaded successfully.")
66
  except Exception as e:
67
  models['loaded'] = False
68
  models['error'] = str(e)
@@ -250,6 +267,27 @@ def classify_image():
250
  file.save(filepath)
251
 
252
  image = Image.open(filepath).convert("RGB")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  inputs = models['image_processor'](images=image, return_tensors="pt")
254
 
255
  with torch.no_grad():
 
39
  models = {
40
  'image_processor': None,
41
  'image_model': None,
42
+ 'custom_processor': None,
43
+ 'custom_model': None,
44
+ 'custom_loaded': False,
45
  'loaded': False,
46
  'loading': False,
47
  'error': None
 
63
 
64
  models['image_processor'] = AutoImageProcessor.from_pretrained("chriamue/bird-species-classifier")
65
  models['image_model'] = AutoModelForImageClassification.from_pretrained("chriamue/bird-species-classifier")
66
+
67
+ # Try loading local custom fine-tuned model if it exists
68
+ custom_dir = "custom_model"
69
+ if os.path.exists(custom_dir) and os.path.exists(os.path.join(custom_dir, "config.json")):
70
+ print("Found custom model locally. Loading...")
71
+ try:
72
+ models['custom_processor'] = AutoImageProcessor.from_pretrained(custom_dir)
73
+ models['custom_model'] = AutoModelForImageClassification.from_pretrained(custom_dir)
74
+ models['custom_loaded'] = True
75
+ print("Custom model loaded successfully.")
76
+ except Exception as custom_err:
77
+ print(f"Error loading custom model: {custom_err}")
78
+ models['custom_loaded'] = False
79
+
80
  models['loaded'] = True
81
  models['error'] = None
82
+ print("Image models loaded successfully.")
83
  except Exception as e:
84
  models['loaded'] = False
85
  models['error'] = str(e)
 
267
  file.save(filepath)
268
 
269
  image = Image.open(filepath).convert("RGB")
270
+
271
+ # 1. Attempt classification with local custom model first (if loaded)
272
+ if models['custom_loaded'] and models['custom_model'] is not None:
273
+ custom_inputs = models['custom_processor'](images=image, return_tensors="pt")
274
+ with torch.no_grad():
275
+ custom_outputs = models['custom_model'](**custom_inputs)
276
+
277
+ custom_logits = custom_outputs.logits
278
+ probs = torch.softmax(custom_logits, dim=-1)
279
+ pred_idx = torch.argmax(probs, dim=-1).item()
280
+ label = models['custom_model'].config.id2label[pred_idx]
281
+ confidence = probs[0][pred_idx].item()
282
+
283
+ print(f"Custom model prediction: {label} (confidence: {confidence:.4f})")
284
+
285
+ # Route to custom predictions if it's HEN or PEACOCK and confidence is high
286
+ if label in ["HEN", "PEACOCK"] and confidence >= 0.70:
287
+ species_display = "Hen" if label == "HEN" else "Peacock"
288
+ return jsonify({'species': species_display, 'type': 'image'})
289
+
290
+ # 2. Fallback to original online model
291
  inputs = models['image_processor'](images=image, return_tensors="pt")
292
 
293
  with torch.no_grad():
custom_model/config.json ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "EfficientNetForImageClassification"
4
+ ],
5
+ "batch_norm_eps": 0.001,
6
+ "batch_norm_momentum": 0.99,
7
+ "depth_coefficient": 1.2,
8
+ "depth_divisor": 8,
9
+ "depthwise_padding": [
10
+ 5,
11
+ 8,
12
+ 16
13
+ ],
14
+ "drop_connect_rate": 0.2,
15
+ "dropout_rate": 0.3,
16
+ "dtype": "float32",
17
+ "expand_ratios": [
18
+ 1,
19
+ 6,
20
+ 6,
21
+ 6,
22
+ 6,
23
+ 6,
24
+ 6
25
+ ],
26
+ "hidden_act": "swish",
27
+ "hidden_dim": 1408,
28
+ "id2label": {
29
+ "0": "HEN",
30
+ "1": "OTHER",
31
+ "2": "PEACOCK"
32
+ },
33
+ "image_size": 260,
34
+ "in_channels": [
35
+ 32,
36
+ 16,
37
+ 24,
38
+ 40,
39
+ 80,
40
+ 112,
41
+ 192
42
+ ],
43
+ "initializer_range": 0.02,
44
+ "kernel_sizes": [
45
+ 3,
46
+ 3,
47
+ 5,
48
+ 3,
49
+ 5,
50
+ 5,
51
+ 3
52
+ ],
53
+ "label2id": {
54
+ "HEN": 0,
55
+ "OTHER": 1,
56
+ "PEACOCK": 2
57
+ },
58
+ "model_type": "efficientnet",
59
+ "num_block_repeats": [
60
+ 1,
61
+ 2,
62
+ 2,
63
+ 3,
64
+ 3,
65
+ 4,
66
+ 1
67
+ ],
68
+ "num_channels": 3,
69
+ "num_hidden_layers": 64,
70
+ "out_channels": [
71
+ 16,
72
+ 24,
73
+ 40,
74
+ 80,
75
+ 112,
76
+ 192,
77
+ 320
78
+ ],
79
+ "pooling_type": "mean",
80
+ "problem_type": "single_label_classification",
81
+ "squeeze_expansion_ratio": 0.25,
82
+ "strides": [
83
+ 1,
84
+ 2,
85
+ 2,
86
+ 2,
87
+ 1,
88
+ 2,
89
+ 1
90
+ ],
91
+ "transformers_version": "4.57.1",
92
+ "width_coefficient": 1.1
93
+ }
custom_model/model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:b1b22957e43ced5f8df96569c9969e89798229275810c5c491a13d8f9a6f250b
3
+ size 31157276
custom_model/preprocessor_config.json ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "crop_size": {
3
+ "height": 289,
4
+ "width": 289
5
+ },
6
+ "do_center_crop": false,
7
+ "do_normalize": true,
8
+ "do_rescale": true,
9
+ "do_resize": true,
10
+ "image_mean": [
11
+ 0.485,
12
+ 0.456,
13
+ 0.406
14
+ ],
15
+ "image_processor_type": "EfficientNetImageProcessor",
16
+ "image_std": [
17
+ 0.47853944,
18
+ 0.4732864,
19
+ 0.47434163
20
+ ],
21
+ "include_top": true,
22
+ "resample": 0,
23
+ "rescale_factor": 0.00392156862745098,
24
+ "rescale_offset": false,
25
+ "size": {
26
+ "height": 260,
27
+ "width": 260
28
+ }
29
+ }
main.py CHANGED
@@ -6,6 +6,7 @@ from transformers import AutoImageProcessor, AutoModelForImageClassification
6
  from transformers import AutoFeatureExtractor, AutoModelForAudioClassification
7
  import librosa
8
  import threading
 
9
 
10
  class BirdClassifierApp:
11
  def __init__(self, root):
@@ -16,6 +17,8 @@ class BirdClassifierApp:
16
 
17
  self.image_model = None
18
  self.image_processor = None
 
 
19
  self.audio_model = None
20
  self.audio_extractor = None
21
 
@@ -118,6 +121,17 @@ class BirdClassifierApp:
118
  self.image_processor = AutoImageProcessor.from_pretrained("chriamue/bird-species-classifier")
119
  self.image_model = AutoModelForImageClassification.from_pretrained("chriamue/bird-species-classifier")
120
 
 
 
 
 
 
 
 
 
 
 
 
121
  self.audio_extractor = AutoFeatureExtractor.from_pretrained("greenarcade/wav2vec2-vd-bird-sound-classification")
122
  self.audio_model = AutoModelForAudioClassification.from_pretrained("greenarcade/wav2vec2-vd-bird-sound-classification")
123
 
@@ -164,14 +178,34 @@ class BirdClassifierApp:
164
  self.preview_label.config(image=photo, text="")
165
  self.preview_label.image = photo
166
 
167
- inputs = self.image_processor(images=image, return_tensors="pt")
168
-
169
- with torch.no_grad():
170
- outputs = self.image_model(**inputs)
171
 
172
- logits = outputs.logits
173
- pred = torch.argmax(logits, dim=-1).item()
174
- label = self.image_model.config.id2label[pred]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
 
176
  self.result_label.config(text=f"✅ Predicted Species: {label}")
177
  self.status_label.config(text="✅ Classification complete!")
 
6
  from transformers import AutoFeatureExtractor, AutoModelForAudioClassification
7
  import librosa
8
  import threading
9
+ import os
10
 
11
  class BirdClassifierApp:
12
  def __init__(self, root):
 
17
 
18
  self.image_model = None
19
  self.image_processor = None
20
+ self.custom_model = None
21
+ self.custom_processor = None
22
  self.audio_model = None
23
  self.audio_extractor = None
24
 
 
121
  self.image_processor = AutoImageProcessor.from_pretrained("chriamue/bird-species-classifier")
122
  self.image_model = AutoModelForImageClassification.from_pretrained("chriamue/bird-species-classifier")
123
 
124
+ # Load custom fine-tuned model if available
125
+ custom_dir = "custom_model"
126
+ if os.path.exists(custom_dir) and os.path.exists(os.path.join(custom_dir, "config.json")):
127
+ try:
128
+ self.custom_processor = AutoImageProcessor.from_pretrained(custom_dir)
129
+ self.custom_model = AutoModelForImageClassification.from_pretrained(custom_dir)
130
+ print("Custom local model loaded.")
131
+ except Exception as custom_err:
132
+ print(f"Error loading custom local model: {custom_err}")
133
+ self.custom_model = None
134
+
135
  self.audio_extractor = AutoFeatureExtractor.from_pretrained("greenarcade/wav2vec2-vd-bird-sound-classification")
136
  self.audio_model = AutoModelForAudioClassification.from_pretrained("greenarcade/wav2vec2-vd-bird-sound-classification")
137
 
 
178
  self.preview_label.config(image=photo, text="")
179
  self.preview_label.image = photo
180
 
181
+ label = None
 
 
 
182
 
183
+ # 1. Attempt classification with custom model first
184
+ if self.custom_model is not None:
185
+ custom_inputs = self.custom_processor(images=image, return_tensors="pt")
186
+ with torch.no_grad():
187
+ custom_outputs = self.custom_model(**custom_inputs)
188
+
189
+ custom_logits = custom_outputs.logits
190
+ probs = torch.softmax(custom_logits, dim=-1)
191
+ pred_idx = torch.argmax(probs, dim=-1).item()
192
+ custom_label = self.custom_model.config.id2label[pred_idx]
193
+ confidence = probs[0][pred_idx].item()
194
+
195
+ print(f"Custom model prediction: {custom_label} (confidence: {confidence:.4f})")
196
+
197
+ if custom_label in ["HEN", "PEACOCK"] and confidence >= 0.70:
198
+ label = "Hen" if custom_label == "HEN" else "Peacock"
199
+
200
+ # 2. Fallback to original online model
201
+ if label is None:
202
+ inputs = self.image_processor(images=image, return_tensors="pt")
203
+ with torch.no_grad():
204
+ outputs = self.image_model(**inputs)
205
+
206
+ logits = outputs.logits
207
+ pred = torch.argmax(logits, dim=-1).item()
208
+ label = self.image_model.config.id2label[pred]
209
 
210
  self.result_label.config(text=f"✅ Predicted Species: {label}")
211
  self.status_label.config(text="✅ Classification complete!")
train.py ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import os
3
+ import sys
4
+ import urllib.request
5
+ from pathlib import Path
6
+ from PIL import Image
7
+
8
+ import torch
9
+ import torch.nn as nn
10
+ from torch.utils.data import Dataset, DataLoader
11
+ from transformers import AutoImageProcessor, AutoModelForImageClassification
12
+
13
+ # List of public domain images for the 'OTHER' class (using Unsplash to avoid rate limits)
14
+ OTHER_BIRD_URLS = [
15
+ f"https://images.unsplash.com/{photo_id}?w=500&auto=format&fit=crop&q=80"
16
+ for photo_id in [
17
+ "photo-1452570053594-1b985d6ea890",
18
+ "photo-1480044965905-02098d419e96",
19
+ "photo-1516233758813-a38d024919c5",
20
+ "photo-1551085254-e96b210db58a",
21
+ "photo-1522441815192-d9f04eb0615c",
22
+ "photo-1506220926022-cc5c12abdb35",
23
+ "photo-1518998053901-5348d3961a04",
24
+ "photo-1511823794984-b87716139b88",
25
+ "photo-1470116890351-be0a9b418409",
26
+ "photo-1539664030485-a936c7d29fc0",
27
+ "photo-1444464666168-49d633b86797",
28
+ "photo-1504386106331-3e4e71712b38",
29
+ "photo-1555041469-a586c61ea9bc",
30
+ "photo-1525462519782-b55cef2f882a",
31
+ "photo-1509023467868-1c40786379f6",
32
+ "photo-1454496522488-7a8e488e8606",
33
+ "photo-1549488344-1f9b8d2bd1f3",
34
+ "photo-1528183429752-a97d0bf99b5a",
35
+ "photo-1510137600163-2729bc695ac1",
36
+ "photo-1465153690352-10c1b295ec7e",
37
+ "photo-1497250681960-ef046c08a56e",
38
+ "photo-1526336024438-db9b601fc211",
39
+ "photo-1534067783941-51c9c23eccfd",
40
+ "photo-1560015534-eca11e59876a",
41
+ "photo-1551806235-a05ff789c3c7",
42
+ ]
43
+ ]
44
+
45
+ def download_image(url, filepath):
46
+ """Downloads an image from a URL with a standard browser User-Agent header."""
47
+ req = urllib.request.Request(
48
+ url,
49
+ headers={'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'}
50
+ )
51
+ try:
52
+ with urllib.request.urlopen(req, timeout=15) as response:
53
+ with open(filepath, 'wb') as f:
54
+ f.write(response.read())
55
+ # Verify it is a valid image
56
+ with Image.open(filepath) as img:
57
+ img.verify()
58
+ return True
59
+ except Exception as e:
60
+ if os.path.exists(filepath):
61
+ os.remove(filepath)
62
+ print(f" [Warning] Failed to download or verify {url}: {e}")
63
+ return False
64
+
65
+ def setup_other_class(other_dir):
66
+ """Sets up the OTHER class directory and downloads sample bird images."""
67
+ os.makedirs(other_dir, exist_ok=True)
68
+ existing_files = [f for f in os.listdir(other_dir) if f.lower().endswith(('.png', '.jpg', '.jpeg', '.webp'))]
69
+ if len(existing_files) >= 10:
70
+ print(f"-> 'OTHER' directory already has {len(existing_files)} images. Skipping download.")
71
+ return
72
+
73
+ print("-> Downloading background bird images for 'OTHER' class (to avoid forgetting)...")
74
+ downloaded_count = 0
75
+ for i, url in enumerate(OTHER_BIRD_URLS):
76
+ dest_path = os.path.join(other_dir, f"other_bird_{i}.jpg")
77
+ print(f" Downloading image {i+1}/{len(OTHER_BIRD_URLS)}...")
78
+ if download_image(url, dest_path):
79
+ downloaded_count += 1
80
+
81
+ print(f"-> Completed downloading. Added {downloaded_count} images to 'OTHER' directory.")
82
+
83
+ class BirdDataset(Dataset):
84
+ def __init__(self, image_paths, labels, image_processor):
85
+ self.image_paths = image_paths
86
+ self.labels = labels
87
+ self.image_processor = image_processor
88
+
89
+ def __len__(self):
90
+ return len(self.image_paths)
91
+
92
+ def __getitem__(self, idx):
93
+ img_path = self.image_paths[idx]
94
+ label = self.labels[idx]
95
+ try:
96
+ image = Image.open(img_path).convert("RGB")
97
+ inputs = self.image_processor(images=image, return_tensors="pt")
98
+ pixel_values = inputs["pixel_values"].squeeze(0)
99
+ return pixel_values, torch.tensor(label, dtype=torch.long)
100
+ except Exception as e:
101
+ # Fallback for corrupt images during training
102
+ # We return a dummy image (zeros) and the label
103
+ print(f" [Warning] Error loading image {img_path}: {e}. Using zero-filled tensor.")
104
+ dummy_pixel = torch.zeros((3, 224, 224))
105
+ return dummy_pixel, torch.tensor(label, dtype=torch.long)
106
+
107
+ def train_model(args):
108
+ dataset_path = Path(args.dataset_dir)
109
+ hen_dir = dataset_path / "HEN"
110
+ peacock_dir = dataset_path / "PEACOCK"
111
+ other_dir = dataset_path / "OTHER"
112
+
113
+ # 1. Verify dataset structure
114
+ if not hen_dir.exists() or not peacock_dir.exists():
115
+ print("Error: Dataset directory must contain 'HEN' and 'PEACOCK' subdirectories.")
116
+ print(f"Looked in: {args.dataset_dir}")
117
+ sys.exit(1)
118
+
119
+ # Count source images
120
+ hen_imgs = list(hen_dir.glob("*.[jJ][pP][gG]")) + list(hen_dir.glob("*.[jJ][pP][eE][gG]")) + list(hen_dir.glob("*.[pP][nN][gG]"))
121
+ peacock_imgs = list(peacock_dir.glob("*.[jJ][pP][gG]")) + list(peacock_dir.glob("*.[jJ][pP][eE][gG]")) + list(peacock_dir.glob("*.[pP][nN][gG]"))
122
+
123
+ print(f"Found {len(hen_imgs)} images in HEN/")
124
+ print(f"Found {len(peacock_imgs)} images in PEACOCK/")
125
+
126
+ if len(hen_imgs) == 0 or len(peacock_imgs) == 0:
127
+ print("Error: Both HEN and PEACOCK folders must contain at least a few images to train.")
128
+ sys.exit(1)
129
+
130
+ # 2. Setup OTHER class automatically
131
+ setup_other_class(str(other_dir))
132
+ other_imgs = list(other_dir.glob("*.[jJ][pP][gG]")) + list(other_dir.glob("*.[jJ][pP][eE][gG]")) + list(other_dir.glob("*.[pP][nN][gG]"))
133
+
134
+ # 3. Gather paths and labels
135
+ class_names = ["HEN", "OTHER", "PEACOCK"] # Alphabetical order
136
+ class_to_idx = {name: i for i, name in enumerate(class_names)}
137
+
138
+ image_paths = []
139
+ labels = []
140
+
141
+ for name in class_names:
142
+ dir_path = dataset_path / name
143
+ imgs = list(dir_path.glob("*.[jJ][pP][gG]")) + list(dir_path.glob("*.[jJ][pP][eE][gG]")) + list(dir_path.glob("*.[pP][nN][gG]")) + list(dir_path.glob("*.[wW][eE][bB][pP]"))
144
+ for img in imgs:
145
+ # Simple pre-check to make sure it loads
146
+ try:
147
+ with Image.open(img) as temp_img:
148
+ temp_img.draft("RGB", (32, 32))
149
+ image_paths.append(str(img))
150
+ labels.append(class_to_idx[name])
151
+ except Exception:
152
+ print(f" [Warning] Skipping corrupt file: {img}")
153
+
154
+ print(f"Total valid training samples: {len(image_paths)}")
155
+ for name in class_names:
156
+ idx = class_to_idx[name]
157
+ count = labels.count(idx)
158
+ print(f" Class '{name}': {count} images")
159
+
160
+ # 4. Load online pretrained model and processor
161
+ print("-> Loading pre-trained base model and image processor...")
162
+ image_processor = AutoImageProcessor.from_pretrained("chriamue/bird-species-classifier")
163
+ model = AutoModelForImageClassification.from_pretrained("chriamue/bird-species-classifier")
164
+
165
+ # 5. Freeze base layers to make training super fast and prevent catastrophic overfitting
166
+ print("-> Freezing feature extractor base weights (training classification head only)...")
167
+ for param in model.parameters():
168
+ param.requires_grad = False
169
+
170
+ # 6. Replace classification head
171
+ # The original was: (classifier): Linear(in_features=1408, out_features=525, bias=True)
172
+ num_features = model.classifier.in_features
173
+ model.classifier = nn.Linear(num_features, len(class_names))
174
+
175
+ # Configure model config metadata so Hugging Face saves labels correctly
176
+ model.config.id2label = {i: name for i, name in enumerate(class_names)}
177
+ model.config.label2id = {name: i for i, name in enumerate(class_names)}
178
+ model.config.num_labels = len(class_names)
179
+
180
+ # 7. Create DataLoader
181
+ dataset = BirdDataset(image_paths, labels, image_processor)
182
+ dataloader = DataLoader(dataset, batch_size=args.batch_size, shuffle=True)
183
+
184
+ # 8. Setup optimizer and loss
185
+ optimizer = torch.optim.AdamW(model.classifier.parameters(), lr=args.lr)
186
+ criterion = nn.CrossEntropyLoss()
187
+
188
+ # 9. Training Loop
189
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
190
+ model.to(device)
191
+ model.train()
192
+
193
+ print(f"-> Starting training on device: {device}...")
194
+ for epoch in range(args.epochs):
195
+ running_loss = 0.0
196
+ correct = 0
197
+ total = 0
198
+
199
+ for batch_x, batch_y in dataloader:
200
+ batch_x, batch_y = batch_x.to(device), batch_y.to(device)
201
+
202
+ optimizer.zero_grad()
203
+ outputs = model(pixel_values=batch_x)
204
+ logits = outputs.logits
205
+
206
+ loss = criterion(logits, batch_y)
207
+ loss.backward()
208
+ optimizer.step()
209
+
210
+ running_loss += loss.item() * batch_x.size(0)
211
+ _, predicted = torch.max(logits, 1)
212
+ total += batch_y.size(0)
213
+ correct += (predicted == batch_y).sum().item()
214
+
215
+ epoch_loss = running_loss / total
216
+ epoch_acc = correct / total
217
+ print(f" Epoch {epoch+1}/{args.epochs} - Loss: {epoch_loss:.4f} - Accuracy: {epoch_acc:.4f}")
218
+
219
+ # 10. Save fine-tuned model
220
+ print(f"-> Saving fine-tuned model to {args.output_dir}...")
221
+ os.makedirs(args.output_dir, exist_ok=True)
222
+ model.save_pretrained(args.output_dir)
223
+ image_processor.save_pretrained(args.output_dir)
224
+ print("-> Done! Custom model training completed successfully.")
225
+
226
+ if __name__ == "__main__":
227
+ parser = argparse.ArgumentParser(description="Fine-tune bird species classifier on custom dataset.")
228
+ parser.add_argument("--dataset_dir", type=str, default="dataset", help="Directory containing HEN and PEACOCK folders")
229
+ parser.add_argument("--output_dir", type=str, default="custom_model", help="Directory to save custom model weights")
230
+ parser.add_argument("--epochs", type=int, default=8, help="Number of training epochs")
231
+ parser.add_argument("--batch_size", type=int, default=8, help="DataLoader batch size")
232
+ parser.add_argument("--lr", type=float, default=1e-3, help="Learning rate for the classification head")
233
+
234
+ args = parser.parse_args()
235
+ train_model(args)
verify_custom_model.py ADDED
@@ -0,0 +1,85 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import torch
4
+ from PIL import Image
5
+ from transformers import AutoImageProcessor, AutoModelForImageClassification
6
+
7
+ def verify():
8
+ custom_dir = "custom_model"
9
+ if not os.path.exists(custom_dir):
10
+ print(f"Error: custom_model directory '{custom_dir}' does not exist.")
11
+ sys.exit(1)
12
+
13
+ print("-> Loading custom fine-tuned model...")
14
+ try:
15
+ processor = AutoImageProcessor.from_pretrained(custom_dir)
16
+ model = AutoModelForImageClassification.from_pretrained(custom_dir)
17
+ print("[OK] Custom model loaded successfully!")
18
+ except Exception as e:
19
+ print(f"[ERROR] Failed to load custom model: {e}")
20
+ sys.exit(1)
21
+
22
+ # Print labels
23
+ labels = list(model.config.id2label.values())
24
+ print(f"Model Labels: {labels}")
25
+ assert "HEN" in labels, "HEN label missing"
26
+ assert "PEACOCK" in labels, "PEACOCK label missing"
27
+ assert "OTHER" in labels, "OTHER label missing"
28
+
29
+ # Define test images
30
+ test_cases = [
31
+ ("dataset/HEN/hen1.jpg", "HEN"),
32
+ ("dataset/PEACOCK/peacock1.jpg", "PEACOCK"),
33
+ ]
34
+
35
+ # Find a file in OTHER to test
36
+ other_dir = "dataset/OTHER"
37
+ if os.path.exists(other_dir):
38
+ other_files = [f for f in os.listdir(other_dir) if f.lower().endswith(('.jpg', '.jpeg', '.png'))]
39
+ if other_files:
40
+ test_cases.append((os.path.join(other_dir, other_files[0]), "OTHER"))
41
+
42
+ print("\n-> Running prediction tests...")
43
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
44
+ model.to(device)
45
+ model.eval()
46
+
47
+ all_passed = True
48
+ for img_path, expected_class in test_cases:
49
+ if not os.path.exists(img_path):
50
+ print(f"[Warning] Test image '{img_path}' not found. Skipping test.")
51
+ continue
52
+
53
+ try:
54
+ image = Image.open(img_path).convert("RGB")
55
+ inputs = processor(images=image, return_tensors="pt").to(device)
56
+
57
+ with torch.no_grad():
58
+ outputs = model(**inputs)
59
+
60
+ logits = outputs.logits
61
+ probs = torch.softmax(logits, dim=-1)
62
+ pred_idx = torch.argmax(probs, dim=-1).item()
63
+ pred_label = model.config.id2label[pred_idx]
64
+ confidence = probs[0][pred_idx].item()
65
+
66
+ print(f"Image: {img_path}")
67
+ print(f" Expected: {expected_class}")
68
+ print(f" Predicted: {pred_label} (confidence: {confidence:.4f})")
69
+
70
+ if pred_label == expected_class:
71
+ print(" [PASS]")
72
+ else:
73
+ print(" [FAIL] (Mismatch)")
74
+ all_passed = False
75
+ except Exception as e:
76
+ print(f" [FAIL] (Error: {e})")
77
+ all_passed = False
78
+
79
+ if all_passed:
80
+ print("\nAll verification tests passed successfully!")
81
+ else:
82
+ print("\nSome verification tests failed. Please inspect the outputs.")
83
+
84
+ if __name__ == "__main__":
85
+ verify()