JustAGeek commited on
Commit
778cc2b
·
verified ·
1 Parent(s): c71a246

Upload scripts/train_distillation.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. scripts/train_distillation.py +239 -0
scripts/train_distillation.py ADDED
@@ -0,0 +1,239 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/python
2
+ """
3
+ train_distillation.py
4
+ Knowledge distillation: DLoc teacher -> lightweight student.
5
+
6
+ Loss: L = ALPHA * MSE(student, ground_truth) + BETA * MSE(student, teacher_output)
7
+ Default: ALPHA=0.7, BETA=0.3 (from professor's spec)
8
+
9
+ Requires pre-computed teacher outputs (run precompute_teacher.py first).
10
+ Set MODE='standalone' to train without distillation (ground truth only).
11
+
12
+ Usage:
13
+ python precompute_teacher.py # run once
14
+ python train_distillation.py # distillation (default: mobilenet)
15
+
16
+ To change student or mode, edit CONFIG below.
17
+ """
18
+ import sys
19
+ import os
20
+ import time
21
+ import torch
22
+ import torch.nn as nn
23
+ import numpy as np
24
+ import warnings
25
+ with warnings.catch_warnings():
26
+ warnings.filterwarnings("ignore", category=FutureWarning)
27
+
28
+ from utils import localization_error, write_log
29
+ from data_loader import load_data
30
+ from student_mobilenet import MobileNetStudent
31
+ from student_tinycnn import TinyCNNStudent
32
+
33
+ # ============================================================
34
+ # CONFIG — edit these before running
35
+ # ============================================================
36
+ STUDENT_TYPE = 'tinycnn'
37
+ MODE = 'distill'
38
+
39
+ ALPHA = 0.7 # ground truth loss weight
40
+ BETA = 0.3 # distillation loss weight (ignored in standalone)
41
+ EPOCHS = 50
42
+ BATCH_SIZE = 32
43
+ LR = 1e-4
44
+ NUM_WORKERS = 4
45
+
46
+ TEACHER_TRAIN_PT = './data/teacher_train_outputs.pt'
47
+ TEACHER_TEST_PT = './data/teacher_test_outputs.pt'
48
+
49
+ # ============================================================
50
+ # DATA PATHS (Fig 10b — Jacobs Hall)
51
+ # ============================================================
52
+ trainpath = [
53
+ './data/dataset_jacobs_July28.mat',
54
+ './data/dataset_non_fov_train_jacobs_July28_2.mat',
55
+ './data/dataset_fov_train_jacobs_July28_2.mat',
56
+ ]
57
+ testpath = [
58
+ './data/dataset_fov_test_jacobs_July28_2.mat',
59
+ './data/dataset_non_fov_test_jacobs_July28_2.mat',
60
+ ]
61
+
62
+ # ============================================================
63
+ # SETUP
64
+ # ============================================================
65
+ torch.manual_seed(0)
66
+ np.random.seed(0)
67
+
68
+ device = torch.device('cuda:0')
69
+ save_name = time.strftime("%Y-%m-%d-%H-%M-%S", time.localtime())
70
+ run_label = f'{MODE}_{STUDENT_TYPE}'
71
+ save_dir = os.path.join('./runs', f'{run_label}_{save_name}')
72
+ os.makedirs(save_dir, exist_ok=True)
73
+
74
+ print(f'Mode: {MODE}')
75
+ print(f'Student: {STUDENT_TYPE}')
76
+ if MODE == 'distill':
77
+ print(f'Loss: {ALPHA}*L_GT + {BETA}*L_KD')
78
+ else:
79
+ print(f'Loss: L_GT (standalone, no distillation)')
80
+ print(f'Save dir: {save_dir}')
81
+
82
+ # ============================================================
83
+ # LOAD DATA INTO RAM
84
+ # ============================================================
85
+ print('Loading training data...')
86
+ train_wo, train_w, train_lbl = [], [], []
87
+ for fp in trainpath:
88
+ wo, w, lbl = load_data(fp)
89
+ if lbl.dim() == 3:
90
+ lbl = lbl.unsqueeze(1)
91
+ train_wo.append(wo)
92
+ train_w.append(w)
93
+ train_lbl.append(lbl)
94
+ train_w = torch.cat(train_w, dim=0)
95
+ train_lbl = torch.cat(train_lbl, dim=0)
96
+ print(f'Training samples: {train_w.shape[0]}')
97
+
98
+ print('Loading testing data...')
99
+ test_wo, test_w, test_lbl = [], [], []
100
+ for fp in testpath:
101
+ wo, w, lbl = load_data(fp)
102
+ if lbl.dim() == 3:
103
+ lbl = lbl.unsqueeze(1)
104
+ test_wo.append(wo)
105
+ test_w.append(w)
106
+ test_lbl.append(lbl)
107
+ test_w = torch.cat(test_w, dim=0)
108
+ test_lbl = torch.cat(test_lbl, dim=0)
109
+ print(f'Testing samples: {test_w.shape[0]}')
110
+
111
+ # Load pre-computed teacher outputs if distilling
112
+ if MODE == 'distill':
113
+ print('Loading pre-computed teacher outputs...')
114
+ train_teacher = torch.load(TEACHER_TRAIN_PT, weights_only=True)
115
+ test_teacher = torch.load(TEACHER_TEST_PT, weights_only=True)
116
+ assert train_teacher.shape[0] == train_w.shape[0], \
117
+ f'Teacher/data mismatch: {train_teacher.shape[0]} vs {train_w.shape[0]}'
118
+ print(f'Teacher outputs loaded: train={train_teacher.shape}, test={test_teacher.shape}')
119
+ train_data = torch.utils.data.TensorDataset(train_w, train_lbl, train_teacher)
120
+ test_data = torch.utils.data.TensorDataset(test_w, test_lbl, test_teacher)
121
+ else:
122
+ train_data = torch.utils.data.TensorDataset(train_w, train_lbl)
123
+ test_data = torch.utils.data.TensorDataset(test_w, test_lbl)
124
+
125
+ train_loader = torch.utils.data.DataLoader(
126
+ train_data, batch_size=BATCH_SIZE, shuffle=True,
127
+ num_workers=NUM_WORKERS, pin_memory=True, persistent_workers=True,
128
+ )
129
+ test_loader = torch.utils.data.DataLoader(
130
+ test_data, batch_size=BATCH_SIZE, shuffle=False,
131
+ num_workers=NUM_WORKERS, pin_memory=True, persistent_workers=True,
132
+ )
133
+
134
+ # ============================================================
135
+ # CREATE STUDENT
136
+ # ============================================================
137
+ if STUDENT_TYPE == 'mobilenet':
138
+ student = MobileNetStudent(input_nc=4).to(device)
139
+ elif STUDENT_TYPE == 'tinycnn':
140
+ student = TinyCNNStudent(input_nc=4).to(device)
141
+ else:
142
+ raise ValueError(f'Unknown student type: {STUDENT_TYPE}')
143
+
144
+ n_params = sum(p.numel() for p in student.parameters())
145
+ print(f'Student params: {n_params:,} ({n_params/1e6:.2f}M)')
146
+
147
+ optimizer = torch.optim.Adam(student.parameters(), lr=LR)
148
+ criterion = nn.MSELoss()
149
+
150
+ # ============================================================
151
+ # TRAINING LOOP
152
+ # ============================================================
153
+ print(f'Starting {MODE} training...')
154
+ best_median = float('inf')
155
+
156
+ for epoch in range(1, EPOCHS + 1):
157
+ epoch_start = time.time()
158
+
159
+ # --- TRAIN ---
160
+ student.train()
161
+ train_loss_sum = 0
162
+ train_errors = []
163
+
164
+ for data in train_loader:
165
+ inputs = data[0].to(device)
166
+ labels = data[1].to(device)
167
+
168
+ student_output = student(inputs)
169
+ L_GT = criterion(student_output, labels)
170
+
171
+ if MODE == 'distill':
172
+ teacher_out = data[2].to(device)
173
+ L_KD = criterion(student_output, teacher_out)
174
+ loss = ALPHA * L_GT + BETA * L_KD
175
+ else:
176
+ loss = L_GT
177
+
178
+ optimizer.zero_grad()
179
+ loss.backward()
180
+ optimizer.step()
181
+
182
+ train_loss_sum += loss.item()
183
+ train_errors.extend(localization_error(
184
+ student_output.detach().cpu().numpy(),
185
+ labels.cpu().numpy(),
186
+ scale=0.1,
187
+ ))
188
+
189
+ train_loss_avg = train_loss_sum / len(train_loader)
190
+ train_median = np.median(train_errors)
191
+
192
+ # --- TEST ---
193
+ student.eval()
194
+ test_loss_sum = 0
195
+ test_errors = []
196
+
197
+ with torch.no_grad():
198
+ for data in test_loader:
199
+ inputs = data[0].to(device)
200
+ labels = data[1].to(device)
201
+
202
+ student_output = student(inputs)
203
+ loss = criterion(student_output, labels)
204
+
205
+ test_loss_sum += loss.item()
206
+ test_errors.extend(localization_error(
207
+ student_output.cpu().numpy(),
208
+ labels.cpu().numpy(),
209
+ scale=0.1,
210
+ ))
211
+
212
+ test_loss_avg = test_loss_sum / len(test_loader)
213
+ test_median = np.median(test_errors)
214
+ test_p90 = np.percentile(test_errors, 90)
215
+ test_p99 = np.percentile(test_errors, 99)
216
+ elapsed = time.time() - epoch_start
217
+
218
+ # --- LOG ---
219
+ write_log([str(train_loss_avg)], run_label, log_dir=save_dir, log_type='train_loss')
220
+ write_log([str(train_median)], run_label, log_dir=save_dir, log_type='train_median_error')
221
+ write_log([str(test_loss_avg)], run_label, log_dir=save_dir, log_type='test_loss')
222
+ write_log([str(test_median)], run_label, log_dir=save_dir, log_type='test_median_error')
223
+ write_log([str(test_p90)], run_label, log_dir=save_dir, log_type='test_90_error')
224
+ write_log([str(test_p99)], run_label, log_dir=save_dir, log_type='test_99_error')
225
+
226
+ print(f'Epoch {epoch}/{EPOCHS} | {elapsed:.0f}s | '
227
+ f'train_loss={train_loss_avg:.6f} train_med={train_median:.3f}m | '
228
+ f'test_med={test_median:.3f}m P90={test_p90:.3f}m P99={test_p99:.3f}m')
229
+
230
+ # --- SAVE ---
231
+ if test_median < best_median:
232
+ best_median = test_median
233
+ torch.save(student.state_dict(), os.path.join(save_dir, f'best_{STUDENT_TYPE}.pth'))
234
+ print(f' -> New best: {best_median:.4f}m')
235
+
236
+ torch.save(student.state_dict(), os.path.join(save_dir, f'latest_{STUDENT_TYPE}.pth'))
237
+
238
+ print(f'\nDone. Best median error: {best_median:.4f}m')
239
+ print(f'Results saved to: {save_dir}')