PasanSarathchandra commited on
Commit
320e2b9
·
verified ·
1 Parent(s): efa89cb

add files of the application

Browse files
.dockerignore ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ venv
2
+ __pycache__
3
+ *.pyc
4
+ *.pyo
5
+ *.pyd
6
+ .git
7
+ .gitignore
8
+ .ipynb_checkpoints
.gitattributes CHANGED
@@ -33,3 +33,11 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ samples/clock_tick.wav filter=lfs diff=lfs merge=lfs -text
37
+ samples/crying_baby.wav filter=lfs diff=lfs merge=lfs -text
38
+ samples/dog_bark.wav filter=lfs diff=lfs merge=lfs -text
39
+ samples/fire_crackling.wav filter=lfs diff=lfs merge=lfs -text
40
+ samples/helicopter.wav filter=lfs diff=lfs merge=lfs -text
41
+ samples/rain.wav filter=lfs diff=lfs merge=lfs -text
42
+ samples/rooster.wav filter=lfs diff=lfs merge=lfs -text
43
+ samples/sneezing.wav filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ venv/
2
+ __pycache__/
3
+ *.pyc
4
+ *.pyo
5
+ weights/
6
+ stats/
7
+ samples/
Dockerfile CHANGED
@@ -1,20 +1,16 @@
1
- FROM python:3.13.5-slim
2
-
3
- WORKDIR /app
4
-
5
- RUN apt-get update && apt-get install -y \
6
- build-essential \
7
- curl \
8
- git \
9
- && rm -rf /var/lib/apt/lists/*
10
-
11
- COPY requirements.txt ./
12
- COPY src/ ./src/
13
-
14
- RUN pip3 install -r requirements.txt
15
-
16
- EXPOSE 8501
17
-
18
- HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health
19
-
20
- ENTRYPOINT ["streamlit", "run", "src/streamlit_app.py", "--server.port=8501", "--server.address=0.0.0.0"]
 
1
+ FROM python:3.10-slim
2
+
3
+ ENV PYTHONDONTWRITEBYTECODE=1
4
+ ENV PYTHONUNBUFFERED=1
5
+
6
+ WORKDIR /app
7
+
8
+ COPY requirements.txt .
9
+ RUN pip install --no-cache-dir --upgrade pip && \
10
+ pip install --no-cache-dir -r requirements.txt
11
+
12
+ COPY . .
13
+
14
+ EXPOSE 8501
15
+
16
+ CMD ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"]
 
 
 
 
README.md CHANGED
@@ -1,20 +1,20 @@
1
- ---
2
- title: Soundedge Esc Application
3
- emoji: 🚀
4
- colorFrom: red
5
- colorTo: red
6
- sdk: docker
7
- app_port: 8501
8
- tags:
9
- - streamlit
10
- pinned: false
11
- short_description: 'Environmental sound classification application '
12
- license: mit
13
- ---
14
-
15
- # Welcome to Streamlit!
16
-
17
- Edit `/src/streamlit_app.py` to customize this app to your heart's desire. :heart:
18
-
19
- If you have any questions, checkout our [documentation](https://docs.streamlit.io) and [community
20
- forums](https://discuss.streamlit.io).
 
1
+ ---
2
+ title: SoundEdge ESC50 Classifier
3
+ emoji: 🔊
4
+ colorFrom: blue
5
+ colorTo: indigo
6
+ sdk: docker
7
+ app_port: 8501
8
+ pinned: false
9
+ short_description: ESC Application
10
+ ---
11
+
12
+ # SoundEdge ESC50 Classifier
13
+
14
+ This app classifies environmental sounds from WAV audio clips.
15
+
16
+ ## Features
17
+ - Upload a `.wav` file
18
+ - Try built-in sample clips
19
+ - Switch between the original and compressed models
20
+ - View predicted sound class and probabilities
app.py ADDED
@@ -0,0 +1,346 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import tempfile
3
+
4
+ import streamlit as st
5
+ import torch
6
+ import matplotlib.pyplot as plt
7
+
8
+ from preprocessing import preprocess_audio
9
+ from utils import load_compressed_model, load_model, predict
10
+ from classes import ESC50_CLASSES
11
+
12
+
13
+ ORIGINAL_MODEL_PATH = "weights/esc50_model.pth"
14
+ COMPRESSED_MODEL_PATH = "weights/esc50_model_compressed.pth"
15
+ STATS_PATH = "stats/esc50_mel_stats.json"
16
+ SAMPLES_DIR = "samples"
17
+
18
+ # ── Page config ───────────────────────────────────────────────────────────────
19
+ st.set_page_config(
20
+ page_title="SoundEdge - Environmental Sound Classification",
21
+ page_icon="🔊",
22
+ layout="centered",
23
+ )
24
+
25
+ # ── Custom CSS ────────────────────────────────────────────────────────────────
26
+ st.markdown("""
27
+ <style>
28
+ /* Global */
29
+ html, body, [data-testid="stAppViewContainer"] {
30
+ background-color: #0f1117;
31
+ color: #e0e0e0;
32
+ }
33
+ [data-testid="stSidebar"] { display: none; }
34
+ .block-container { padding-top: 1.5rem !important; }
35
+ #MainMenu, footer, header { visibility: hidden; }
36
+
37
+ /* Hero banner */
38
+ .hero {
39
+ background: linear-gradient(135deg, #1a1f35 0%, #0d2137 60%, #112240 100%);
40
+ border: 1px solid #2a3a5c;
41
+ border-radius: 16px;
42
+ padding: 1rem 2rem 1rem 2rem;
43
+ margin-bottom: 2rem;
44
+ text-align: center;
45
+ }
46
+ .hero h1 {
47
+ font-size: 2.6rem;
48
+ font-weight: 800;
49
+ background: linear-gradient(90deg, #4fc3f7, #a78bfa);
50
+ -webkit-background-clip: text;
51
+ -webkit-text-fill-color: transparent;
52
+ margin: 0 0 0.4rem 0;
53
+ }
54
+ .hero p {
55
+ color: #94a3b8;
56
+ font-size: 1.05rem;
57
+ margin: 0;
58
+ }
59
+
60
+ /* Section headings */
61
+ .section-title {
62
+ font-size: 0.8rem;
63
+ font-weight: 700;
64
+ color: #a78bfa;
65
+ text-transform: uppercase;
66
+ letter-spacing: 0.1em;
67
+ margin-bottom: 0.75rem;
68
+ }
69
+
70
+ /* Class badge pills */
71
+ .badge-grid {
72
+ display: flex;
73
+ flex-wrap: wrap;
74
+ gap: 0.4rem;
75
+ margin-bottom: 0.5rem;
76
+ }
77
+ .badge {
78
+ background: #1e293b;
79
+ border: 1px solid #334155;
80
+ border-radius: 9px;
81
+ padding: 0.2rem 0.4rem;
82
+ font-size: 0.7rem;
83
+ color: #cbd5e1;
84
+ display: inline-flex;
85
+ align-items: center;
86
+ gap: 0.4rem;
87
+ }
88
+
89
+ /* Result card */
90
+ .result-card {
91
+ background: linear-gradient(135deg, #1e3a5f 0%, #1a2744 100%);
92
+ border: 1px solid #3b82f6;
93
+ border-radius: 14px;
94
+ padding: 1.8rem 2rem;
95
+ margin: 1.2rem 0 1.4rem 0;
96
+ text-align: center;
97
+ }
98
+ .result-card .label {
99
+ font-size: 0.78rem;
100
+ color: #94a3b8;
101
+ text-transform: uppercase;
102
+ letter-spacing: 0.09em;
103
+ margin-bottom: 0.4rem;
104
+ }
105
+ .result-card .class-name {
106
+ font-size: 2.1rem;
107
+ font-weight: 800;
108
+ color: #4fc3f7;
109
+ margin: 0;
110
+ }
111
+ .result-card .confidence {
112
+ font-size: 1.15rem;
113
+ color: #a78bfa;
114
+ margin-top: 0.45rem;
115
+ }
116
+
117
+ /* Top-3 progress bars */
118
+ .prob-row { margin-bottom: 0.9rem; }
119
+ .prob-label {
120
+ display: flex;
121
+ justify-content: space-between;
122
+ font-size: 0.9rem;
123
+ margin-bottom: 0.3rem;
124
+ color: #cbd5e1;
125
+ }
126
+ .prob-bar-bg {
127
+ background: #1e293b;
128
+ border-radius: 999px;
129
+ height: 10px;
130
+ width: 100%;
131
+ overflow: hidden;
132
+ }
133
+ .prob-bar-fill {
134
+ height: 100%;
135
+ border-radius: 999px;
136
+ background: linear-gradient(90deg, #4fc3f7, #a78bfa);
137
+ }
138
+
139
+ /* Divider */
140
+ hr { border-color: #1e293b !important; }
141
+ </style>
142
+ """, unsafe_allow_html=True)
143
+
144
+ # ── Hero ──────────────────────────────────────────────────────────────────────
145
+ st.markdown("""
146
+ <div class="hero">
147
+ <h1>🔊 SoundEdge</h1>
148
+ <p>Environmental Sound Classification &mdash; upload a short audio clip and let the model identify the sound.</p>
149
+ </div>
150
+ """, unsafe_allow_html=True)
151
+
152
+ # ── Supported classes ─────────────────────────────────────────────────────────
153
+ st.markdown('<div class="section-title">Supported Sound Classes</div>', unsafe_allow_html=True)
154
+ badges_html = '<div class="badge-grid">'
155
+ for cls in ESC50_CLASSES:
156
+ label = cls.replace('_', ' ').title()
157
+ badges_html += f'<span class="badge">{label}</span>'
158
+ badges_html += '</div>'
159
+ st.markdown(badges_html, unsafe_allow_html=True)
160
+
161
+ st.divider()
162
+
163
+ # ── Model ─────────────────────────────────────────────────────────────────────
164
+ device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
165
+
166
+ @st.cache_resource
167
+ def get_original_model():
168
+ return load_model(ORIGINAL_MODEL_PATH, device, num_classes=len(ESC50_CLASSES))
169
+
170
+ @st.cache_resource
171
+ def get_compressed_model():
172
+ return load_compressed_model(ORIGINAL_MODEL_PATH, COMPRESSED_MODEL_PATH, num_classes=len(ESC50_CLASSES))
173
+
174
+ # ── Model selector ────────────────────────────────────────────────────────────
175
+ st.markdown('<div class="section-title">Select Model</div>', unsafe_allow_html=True)
176
+ _model_choice = st.radio(
177
+ "model_selector",
178
+ options=["Original", "Compressed"],
179
+ horizontal=True,
180
+ label_visibility="collapsed",
181
+ )
182
+ if _model_choice == "Compressed":
183
+ active_model = get_compressed_model()
184
+ active_device = torch.device("cpu")
185
+ else:
186
+ active_model = get_original_model()
187
+ active_device = device
188
+
189
+ st.divider()
190
+
191
+ # ── Session state ────────────────────────────────────────────────────────────
192
+ if "sample_to_classify" not in st.session_state:
193
+ st.session_state.sample_to_classify = None
194
+ if "active_source" not in st.session_state:
195
+ st.session_state.active_source = None
196
+
197
+ def _clear_classification_output():
198
+ st.session_state.sample_to_classify = None
199
+ st.session_state.active_source = None
200
+
201
+ def _activate_upload_source():
202
+ st.session_state.sample_to_classify = None
203
+ st.session_state.active_source = "upload"
204
+
205
+ # ── Audio input tabs ──────────────────────────────────────────────────────────
206
+ st.markdown('<div class="section-title">Choose Audio Input</div>', unsafe_allow_html=True)
207
+
208
+ tab_upload, tab_sample = st.tabs([" ⬆️ Upload a File ", " 🎵 Try a Sample "])
209
+
210
+ uploaded_file = None
211
+
212
+ with tab_upload:
213
+ uploaded_file = st.file_uploader(
214
+ "WAV audio file (5 seconds recommended)",
215
+ type=["wav"],
216
+ key="uploaded_audio",
217
+ on_change=_activate_upload_source,
218
+ label_visibility="collapsed",
219
+ )
220
+ if uploaded_file is not None:
221
+ if not uploaded_file.name.lower().endswith(".wav"):
222
+ st.error("Only .wav files are supported.")
223
+ st.stop()
224
+ st.audio(uploaded_file)
225
+ elif st.session_state.active_source == "upload":
226
+ st.session_state.active_source = None
227
+
228
+ with tab_sample:
229
+ _sample_files = (
230
+ sorted([f for f in os.listdir(SAMPLES_DIR) if f.lower().endswith(".wav")])
231
+ if os.path.isdir(SAMPLES_DIR) else []
232
+ )
233
+
234
+ if not _sample_files:
235
+ st.info("No sample files found in the samples/ folder.")
236
+ else:
237
+ def _parse_sample_name(fn: str) -> str:
238
+ name = os.path.splitext(fn)[0]
239
+ return name.replace('_', ' ').title()
240
+
241
+ _options = {_parse_sample_name(f): f for f in _sample_files}
242
+ _selected = st.selectbox(
243
+ "Pick a sample clip",
244
+ options=list(_options.keys()),
245
+ key="sample_selector",
246
+ on_change=_clear_classification_output,
247
+ label_visibility="collapsed",
248
+ )
249
+ _sample_path = os.path.join(SAMPLES_DIR, _options[_selected])
250
+ st.audio(_sample_path)
251
+ if st.button("Classify this sample ›", use_container_width=True):
252
+ st.session_state.sample_to_classify = _sample_path
253
+ st.session_state.active_source = "sample"
254
+
255
+ # ── Determine active source ───────────────────────────────────────────────────
256
+ source_path = None
257
+ cleanup_temp = False
258
+ temp_path = None
259
+
260
+ if st.session_state.active_source == "upload" and uploaded_file is not None:
261
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".wav") as _tmp:
262
+ uploaded_file.seek(0)
263
+ _tmp.write(uploaded_file.read())
264
+ temp_path = _tmp.name
265
+ source_path = temp_path
266
+ cleanup_temp = True
267
+ elif st.session_state.active_source == "sample" and st.session_state.sample_to_classify:
268
+ source_path = st.session_state.sample_to_classify
269
+
270
+ # ── Inference & results ───────────────────────────────────────────────────────
271
+ if source_path:
272
+ try:
273
+ with st.spinner("Analysing audio…"):
274
+ input_tensor = preprocess_audio(source_path, STATS_PATH)
275
+ top_class, top_prob, all_probs = predict(active_model, input_tensor, active_device)
276
+
277
+ # ── Low-confidence guard ──────────────────────────────────────────────
278
+ if top_prob < 0.6:
279
+ st.markdown("""
280
+ <div style="background:#1e293b;border:1px solid #475569;border-radius:14px;
281
+ padding:1.8rem 2rem;margin:1.2rem 0 1.4rem 0;text-align:center;">
282
+ <div style="font-size:2rem;margin-bottom:0.6rem;">🎙️</div>
283
+ <div style="font-size:1.1rem;font-weight:700;color:#f87171;margin-bottom:0.4rem;">
284
+ Unable to confidently identify the sound
285
+ </div>
286
+ <div style="color:#94a3b8;font-size:0.95rem;">
287
+ Please upload a clearer audio file and try again.
288
+ </div>
289
+ </div>
290
+ """, unsafe_allow_html=True)
291
+ else:
292
+ # ── Result card ───────────────────────────────────────────────────
293
+ top_label = top_class.replace('_', ' ').title()
294
+ st.markdown(f"""
295
+ <div class="result-card">
296
+ <div class="label">Predicted Sound</div>
297
+ <div class="class-name">{top_label}</div>
298
+ <div class="confidence">Confidence &nbsp;{top_prob * 100:.1f}%</div>
299
+ </div>
300
+ """, unsafe_allow_html=True)
301
+
302
+ # ── Top-3 predictions ─────────────────────────────────────────────
303
+ st.markdown('<div class="section-title">Top 3 Predictions</div>', unsafe_allow_html=True)
304
+ for item in all_probs[:3]:
305
+ pct = item['probability'] * 100
306
+ lbl = item['class_name'].replace('_', ' ').title()
307
+ st.markdown(f"""
308
+ <div class="prob-row">
309
+ <div class="prob-label">
310
+ <span>{lbl}</span><span>{pct:.1f}%</span>
311
+ </div>
312
+ <div class="prob-bar-bg">
313
+ <div class="prob-bar-fill" style="width:{pct:.1f}%;"></div>
314
+ </div>
315
+ </div>
316
+ """, unsafe_allow_html=True)
317
+
318
+ # ── All-class probability chart ───────────────────────────────────
319
+ st.markdown('<div class="section-title" style="margin-top:1.6rem;">All Class Probabilities</div>', unsafe_allow_html=True)
320
+
321
+ chart_labels = [x["class_name"].replace('_', ' ').title() for x in all_probs]
322
+ chart_values = [x["probability"] * 100 for x in all_probs]
323
+ bar_colors = ['#4fc3f7' if chart_labels[i] == top_label else '#334155'
324
+ for i in range(len(chart_labels))]
325
+ # increase figure height if many classes to avoid cramped labels
326
+ fig, ax = plt.subplots(figsize=(9, 4 + len(chart_labels) * 0.1))
327
+ fig.patch.set_facecolor('#0f1117')
328
+ ax.set_facecolor('#0f1117')
329
+ ax.barh(chart_labels[::-1], chart_values[::-1],
330
+ color=bar_colors[::-1], height=0.6, edgecolor='none')
331
+ ax.set_xlabel("Probability (%)", color='#94a3b8', fontsize=9)
332
+ ax.tick_params(colors='#cbd5e1', labelsize=8.5)
333
+ for spine in ax.spines.values():
334
+ spine.set_visible(False)
335
+ ax.grid(axis='x', color='#1e293b', linewidth=0.8)
336
+ ax.set_xlim(0, max(chart_values) * 1.18 if max(chart_values) > 0 else 100)
337
+ plt.tight_layout(pad=1.0)
338
+ st.pyplot(fig)
339
+ plt.close(fig)
340
+
341
+ except Exception as e:
342
+ st.error(f"Error during inference: {e}")
343
+
344
+ finally:
345
+ if cleanup_temp and temp_path and os.path.exists(temp_path):
346
+ os.remove(temp_path)
classes.py ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ESC10_CLASSES = [
2
+ 'chainsaw',
3
+ 'clock_tick',
4
+ 'crackling_fire',
5
+ 'crying_baby',
6
+ 'dog',
7
+ 'helicopter',
8
+ 'rain',
9
+ 'rooster',
10
+ 'sea_waves',
11
+ 'sneezing'
12
+ ]
13
+
14
+
15
+ ESC50_CLASSES = ['dog', 'rooster', 'pig', 'cow', 'frog', 'cat', 'hen', 'insects', 'sheep', 'crow', 'rain', 'sea_waves', 'crackling_fire', 'crickets', 'chirping_birds', 'water_drops', 'wind', 'pouring_water', 'toilet_flush', 'thunderstorm', 'crying_baby', 'sneezing', 'clapping', 'breathing', 'coughing', 'footsteps', 'laughing', 'brushing_teeth', 'snoring', 'drinking_sipping', 'door_wood_knock', 'mouse_click', 'keyboard_typing', 'door_wood_creaks', 'can_opening', 'washing_machine', 'vacuum_cleaner', 'clock_alarm', 'clock_tick', 'glass_breaking', 'helicopter', 'chainsaw', 'siren', 'car_horn', 'engine', 'train', 'church_bells', 'airplane', 'fireworks', 'hand_saw']
compression/pruning.py ADDED
@@ -0,0 +1,190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+
4
+ def _make_pruned_conv(old_conv: nn.Conv2d, keep_out_idx: torch.Tensor) -> nn.Conv2d:
5
+ """
6
+ Create a new Conv2d with fewer output channels (keep_out_idx),
7
+ copying weights/bias from old_conv.
8
+ """
9
+ device = old_conv.weight.device
10
+ dtype = old_conv.weight.dtype
11
+
12
+ new_out = keep_out_idx.numel()
13
+ new_conv = nn.Conv2d(
14
+ in_channels=old_conv.in_channels,
15
+ out_channels=new_out,
16
+ kernel_size=old_conv.kernel_size,
17
+ stride=old_conv.stride,
18
+ padding=old_conv.padding,
19
+ dilation=old_conv.dilation,
20
+ groups=old_conv.groups,
21
+ bias=(old_conv.bias is not None),
22
+ padding_mode=old_conv.padding_mode,
23
+ ).to(device=device, dtype=dtype)
24
+
25
+ with torch.no_grad():
26
+ new_conv.weight.copy_(old_conv.weight.data[keep_out_idx].contiguous())
27
+ if old_conv.bias is not None:
28
+ new_conv.bias.copy_(old_conv.bias.data[keep_out_idx].contiguous())
29
+
30
+ return new_conv
31
+
32
+
33
+ def _make_pruned_bn(old_bn: nn.BatchNorm2d, keep_idx: torch.Tensor) -> nn.BatchNorm2d:
34
+ """
35
+ Create a new BatchNorm2d with fewer channels, copying params + running stats.
36
+ """
37
+ device = old_bn.weight.device
38
+ dtype = old_bn.weight.dtype
39
+
40
+ new_nf = keep_idx.numel()
41
+ new_bn = nn.BatchNorm2d(
42
+ num_features=new_nf,
43
+ eps=old_bn.eps,
44
+ momentum=old_bn.momentum,
45
+ affine=old_bn.affine,
46
+ track_running_stats=old_bn.track_running_stats,
47
+ ).to(device=device, dtype=dtype)
48
+
49
+ with torch.no_grad():
50
+ if old_bn.affine:
51
+ new_bn.weight.copy_(old_bn.weight.data[keep_idx].contiguous())
52
+ new_bn.bias.copy_(old_bn.bias.data[keep_idx].contiguous())
53
+
54
+ if old_bn.track_running_stats:
55
+ new_bn.running_mean.copy_(old_bn.running_mean.data[keep_idx].contiguous())
56
+ new_bn.running_var.copy_(old_bn.running_var.data[keep_idx].contiguous())
57
+ new_bn.num_batches_tracked.copy_(old_bn.num_batches_tracked)
58
+
59
+ return new_bn
60
+
61
+
62
+ def prune_conv_bn_pair(conv: nn.Conv2d, bn: nn.BatchNorm2d, amount: float = 0.3):
63
+ """
64
+ Structurally prune Conv2d output channels using L1 norm.
65
+ Returns: (new_conv, new_bn, keep_out_idx)
66
+ """
67
+ if not (0.0 <= amount < 1.0):
68
+ raise ValueError("amount must be in [0, 1).")
69
+
70
+ W = conv.weight.data # (out, in, kH, kW)
71
+ out_ch = W.shape[0]
72
+ num_prune = int(round(amount * out_ch))
73
+
74
+ if num_prune <= 0:
75
+ keep_idx = torch.arange(out_ch, device=W.device)
76
+ return conv, bn, keep_idx
77
+
78
+ # L1 norm per output channel
79
+ channel_l1 = W.abs().sum(dim=(1, 2, 3)) # (out,)
80
+
81
+ # Keep the highest-L1 channels
82
+ sorted_idx = torch.argsort(channel_l1, descending=True)
83
+ keep_idx = sorted_idx[num_prune:] # (kept,)
84
+
85
+ # Keep indices sorted for nicer determinism
86
+ keep_idx, _ = torch.sort(keep_idx)
87
+
88
+ new_conv = _make_pruned_conv(conv, keep_idx)
89
+ new_bn = _make_pruned_bn(bn, keep_idx)
90
+
91
+ return new_conv, new_bn, keep_idx
92
+
93
+
94
+ def prune_conv_input_channels(conv: nn.Conv2d, keep_in_idx: torch.Tensor) -> nn.Conv2d:
95
+ """
96
+ Prune Conv2d input channels by selecting keep_in_idx on dim=1 of weight.
97
+ Returns a new conv with in_channels = len(keep_in_idx).
98
+ """
99
+ device = conv.weight.device
100
+ dtype = conv.weight.dtype
101
+
102
+ new_in = keep_in_idx.numel()
103
+ new_conv = nn.Conv2d(
104
+ in_channels=new_in,
105
+ out_channels=conv.out_channels,
106
+ kernel_size=conv.kernel_size,
107
+ stride=conv.stride,
108
+ padding=conv.padding,
109
+ dilation=conv.dilation,
110
+ groups=conv.groups, # assumes groups-compatible; your model uses groups=1
111
+ bias=(conv.bias is not None),
112
+ padding_mode=conv.padding_mode,
113
+ ).to(device=device, dtype=dtype)
114
+
115
+ with torch.no_grad():
116
+ # weight shape: (out, in, kH, kW)
117
+ new_conv.weight.copy_(conv.weight.data[:, keep_in_idx].contiguous())
118
+ if conv.bias is not None:
119
+ new_conv.bias.copy_(conv.bias.data.contiguous())
120
+
121
+ return new_conv
122
+
123
+
124
+ def rebuild_fc_after_pruning(model: nn.Module, example_input: torch.Tensor) -> None:
125
+ """
126
+ Rebuild model.fc input dim based on the current conv/ssrp path.
127
+ Assumes model has attributes: conv1, conv2, conv3, ssrp_ms, flatten, fc.
128
+ """
129
+ device = next(model.parameters()).device
130
+ model.eval()
131
+ with torch.no_grad():
132
+ x = example_input.to(device)
133
+ feats = model.flatten(model.ssrp_ms(model.conv3(model.conv2(model.conv1(x)))))
134
+ in_dim = feats.shape[1]
135
+
136
+ old_out = model.fc.out_features
137
+ model.fc = nn.Linear(in_dim, old_out).to(device)
138
+ model.train()
139
+
140
+ def apply_structural_pruning(model: nn.Module, amount: float = 0.8, example_input: torch.Tensor = torch.randn(1, 1, 40, 862)) -> nn.Module:
141
+ """
142
+ Structural channel pruning for your CNN_PCAw_SSRPMS_KAN conv blocks.
143
+
144
+ - Prunes conv1 out channels + bn1, updates conv2 input channels accordingly
145
+ - Prunes conv2 out channels + bn2, updates conv3 input channels accordingly
146
+ - Prunes conv3 out channels + bn3
147
+ - Optionally rebuilds fc using example_input
148
+
149
+ example_input should be shaped like your model input, e.g. (1, 1, F, T)
150
+ """
151
+ device = next(model.parameters()).device
152
+
153
+ # ---- conv1 prune (conv1[1] is Conv2d, conv1[2] is BN) ----
154
+ conv1_old = model.conv1[1]
155
+ bn1_old = model.conv1[2]
156
+ conv1_new, bn1_new, keep1 = prune_conv_bn_pair(conv1_old, bn1_old, amount)
157
+
158
+ model.conv1[1] = conv1_new
159
+ model.conv1[2] = bn1_new
160
+
161
+ # ---- conv2 input prune to match conv1 kept outputs ----
162
+ model.conv2[1] = prune_conv_input_channels(model.conv2[1], keep1)
163
+
164
+ # ---- conv2 prune ----
165
+ conv2_old = model.conv2[1]
166
+ bn2_old = model.conv2[2]
167
+ conv2_new, bn2_new, keep2 = prune_conv_bn_pair(conv2_old, bn2_old, amount)
168
+
169
+ model.conv2[1] = conv2_new
170
+ model.conv2[2] = bn2_new
171
+
172
+ # ---- conv3 input prune to match conv2 kept outputs ----
173
+ model.conv3[0] = prune_conv_input_channels(model.conv3[0], keep2)
174
+
175
+ # ---- conv3 prune ----
176
+ conv3_old = model.conv3[0]
177
+ bn3_old = model.conv3[1]
178
+ conv3_new, bn3_new, keep3 = prune_conv_bn_pair(conv3_old, bn3_old, amount)
179
+
180
+ model.conv3[0] = conv3_new
181
+ model.conv3[1] = bn3_new
182
+
183
+ # Ensure the whole model stays on the same device
184
+ model.to(device)
185
+
186
+ # ---- fc rebuild (needed because flatten dim changes) ----
187
+ if example_input is not None:
188
+ rebuild_fc_after_pruning(model, example_input)
189
+
190
+ return model
compression/qat.py ADDED
@@ -0,0 +1,141 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import copy
2
+ import torch
3
+ import torch.nn as nn
4
+ import torch.ao.quantization as tq
5
+
6
+
7
+ class CNN_PSK_QATWrapper(nn.Module):
8
+ def __init__(self, base_model: nn.Module):
9
+ super().__init__()
10
+ self.m = base_model
11
+ self.quant_in_conv3 = tq.QuantStub()
12
+ self.dequant_after_conv3 = tq.DeQuantStub()
13
+ self.quant_in_fc = tq.QuantStub()
14
+ self.dequant_after_fc = tq.DeQuantStub()
15
+
16
+ def forward(self, x):
17
+ x = self.m.conv1(x) # float
18
+ x = self.m.conv2(x) # float
19
+
20
+ x = self.quant_in_conv3(x) # int8-sim
21
+ x = self.m.conv3(x) # quantized block
22
+ x = self.dequant_after_conv3(x)
23
+
24
+ x = self.m.ssrp_ms(x) # float
25
+ x = self.m.flatten(x)
26
+
27
+ x = self.quant_in_fc(x) # int8-sim
28
+ x = self.m.fc(x) # quantized linear
29
+ x = self.dequant_after_fc(x)
30
+
31
+ x = self.m.kan(x) # float
32
+ return x
33
+
34
+
35
+ def _get_available_backend(preferred: str = "fbgemm") -> str:
36
+ supported = torch.backends.quantized.supported_engines
37
+ if preferred in supported:
38
+ return preferred
39
+ for fallback in ("qnnpack", "fbgemm", "onednn"):
40
+ if fallback in supported:
41
+ return fallback
42
+ raise RuntimeError(
43
+ f"No supported quantization backend found. Available: {supported}"
44
+ )
45
+
46
+
47
+ def _set_backend(backend: str):
48
+ backend = _get_available_backend(backend)
49
+ torch.backends.quantized.engine = backend
50
+ return backend
51
+
52
+
53
+ def _fuse_conv_bn_relu_for_qat(base: nn.Module):
54
+ if not (hasattr(base, "conv3") and isinstance(base.conv3, nn.Sequential)):
55
+ return
56
+ if len(base.conv3) < 3:
57
+ return
58
+
59
+ was_training = base.training
60
+ base.eval()
61
+ tq.fuse_modules(base.conv3, [["0", "1", "2"]], inplace=True)
62
+ if was_training:
63
+ base.train()
64
+
65
+ def get_qat_qconfig_compatible(backend: str = "fbgemm"):
66
+ """
67
+ QAT config compatible with eager-mode convert() for Conv/Linear:
68
+ - Activations: quint8 per-tensor affine
69
+ - Weights: qint8 per-channel symmetric (supported by quantized conv/linear)
70
+ """
71
+ backend = _get_available_backend(backend)
72
+
73
+ act_fq = tq.FusedMovingAvgObsFakeQuantize.with_args(
74
+ observer=tq.MovingAverageMinMaxObserver,
75
+ dtype=torch.quint8,
76
+ qscheme=torch.per_tensor_affine,
77
+ quant_min=0,
78
+ quant_max=255,
79
+ reduce_range=False,
80
+ )
81
+
82
+ weight_fq = tq.FusedMovingAvgObsFakeQuantize.with_args(
83
+ observer=tq.MovingAveragePerChannelMinMaxObserver,
84
+ dtype=torch.qint8,
85
+ qscheme=torch.per_channel_symmetric, # key change
86
+ quant_min=-128,
87
+ quant_max=127,
88
+ reduce_range=False,
89
+ ch_axis=0, # Conv2d out_channels axis, Linear out_features axis
90
+ )
91
+
92
+ return tq.QConfig(activation=act_fq, weight=weight_fq)
93
+
94
+
95
+ def prepare_qat_model(
96
+ model_fp32: nn.Module,
97
+ backend: str = "fbgemm",
98
+ inplace: bool = False,
99
+ ) -> nn.Module:
100
+ backend = _set_backend(backend)
101
+
102
+ base = model_fp32 if inplace else copy.deepcopy(model_fp32)
103
+
104
+ # 1) Fuse conv3 safely
105
+ _fuse_conv_bn_relu_for_qat(base)
106
+
107
+ # 2) Wrap
108
+ qat_wrapped = CNN_PSK_QATWrapper(base)
109
+
110
+ # 3) Attach QAT qconfig
111
+ qat_wrapped.qconfig = tq.get_default_qat_qconfig(backend) # backend already resolved
112
+
113
+ # Keep custom / unsupported parts in float
114
+ qat_wrapped.m.conv1.qconfig = None
115
+ qat_wrapped.m.conv2.qconfig = None
116
+ qat_wrapped.m.ssrp_ms.qconfig = None
117
+ qat_wrapped.m.flatten.qconfig = None
118
+ qat_wrapped.m.kan.qconfig = None
119
+
120
+ # Keep PCAw_Pool float (inside conv1)
121
+ for mod in qat_wrapped.m.conv1.modules():
122
+ if mod.__class__.__name__ == "PCAw_Pool":
123
+ mod.qconfig = None
124
+
125
+ # 4) Prepare for QAT (inserts fake quant modules)
126
+ qat_wrapped.train()
127
+ tq.prepare_qat(qat_wrapped, inplace=True)
128
+
129
+ return qat_wrapped
130
+
131
+
132
+ def convert_qat_model(
133
+ qat_model: nn.Module,
134
+ backend: str = "fbgemm",
135
+ inplace: bool = False,
136
+ ) -> nn.Module:
137
+ _set_backend(backend) # will auto-resolve to supported backend
138
+ m = qat_model if inplace else copy.deepcopy(qat_model)
139
+ m.eval()
140
+ m = m.cpu()
141
+ return tq.convert(m, inplace=True)
custom_layers/PCAw_Pool.py ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ import torch.nn.functional as F
4
+
5
+ class PCAw_Pool(nn.Module):
6
+ def __init__(self, kernel_size, stride=(1, 1), eps: float = 1e-4, normalize_weights: bool = True):
7
+ super().__init__()
8
+ self.kernel_size = kernel_size
9
+ self.stride = stride
10
+ self.eps = eps
11
+ self.normalize_weights = normalize_weights
12
+
13
+ # D = number of features per patch = F_k * T_k
14
+ F_k, T_k = kernel_size
15
+ D = F_k * T_k
16
+
17
+ # Trainable weights over PCA components (columns). Shape: (D,)
18
+ # Initialized small to avoid overpowering early training.
19
+ self.weights = nn.Parameter(0.01 * torch.randn(D))
20
+
21
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
22
+ B, C, Fh, Tw = x.shape
23
+ F_k, T_k = self.kernel_size
24
+
25
+ # Unfold per-channel
26
+ x_bc = x.view(B * C, 1, Fh, Tw) # (B*C, 1, F, T)
27
+ patches = F.unfold(x_bc, kernel_size=(F_k, T_k), stride=self.stride) # (B*C, D, NumPatches)
28
+ D = patches.shape[1]
29
+ NumPatches = patches.shape[2]
30
+
31
+ # Arrange to (B, C, NumPatches, D) -> (B, C*NumPatches, D)
32
+ patches = patches.permute(0, 2, 1).contiguous() # (B*C, NumPatches, D)
33
+ patches = patches.view(B, C, NumPatches, D) # (B, C, NumPatches, D)
34
+ X = patches.view(B, C * NumPatches, D) # (B, N, D) with N = C*NumPatches
35
+ N = X.shape[1]
36
+
37
+ # Output spatial size
38
+ H = (Fh - F_k) // self.stride[0] + 1
39
+ W = (Tw - T_k) // self.stride[1] + 1
40
+
41
+ # Center features
42
+ mean = X.mean(dim=1, keepdim=True) # (B, 1, D)
43
+ Xc = X - mean # (B, N, D)
44
+
45
+ # Optional: standardize per-feature to tame scale explosions from DSConv
46
+ var = Xc.pow(2).mean(dim=1, keepdim=True) # (B, 1, D)
47
+ Xc = Xc / torch.sqrt(var + 1e-6)
48
+
49
+ # --------- Stable projection basis via SVD (more robust than eigh) ---------
50
+ # Xc = U S V^T -> principal components are columns of V
51
+ # Use full_matrices=False for efficiency and stable backward
52
+ U, S, Vh = torch.linalg.svd(Xc, full_matrices=False) # Vh: (B, D, D)
53
+ eigvecs = Vh.transpose(1, 2) # (B, D, D)
54
+
55
+ # Detach eigenvectors to avoid backprop through SVD
56
+ eigvecs = eigvecs.detach()
57
+
58
+ # Weighted projection direction v = E @ w
59
+ if self.normalize_weights:
60
+ w = torch.softmax(self.weights, dim=0) # (D,)
61
+ else:
62
+ w = self.weights
63
+
64
+ v = torch.matmul(eigvecs, w) # (B, D)
65
+ v = v / (v.norm(dim=1, keepdim=True) + 1e-8) # normalize
66
+
67
+ # Project samples onto v -> scalar per sample
68
+ scores = torch.matmul(Xc, v.unsqueeze(-1)).squeeze(-1) # (B, N)
69
+
70
+ return scores.view(B, C, H, W)
71
+
72
+ def extra_repr(self) -> str:
73
+ return (f"kernel_size={self.kernel_size}, stride={self.stride}, "
74
+ f"eps={self.eps}, normalize_weights={self.normalize_weights}")
custom_layers/SSRP_MS.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ from torch import nn
3
+ import torch.nn.functional as F
4
+
5
+ class SSRP_MS(nn.Module):
6
+ """
7
+ Multi-Scale Sparse Shift-Invariant Representation Pooling (SSRP-MS)
8
+
9
+ Given feature maps x with shape (B, C, F, T):
10
+ - For L scales with window sizes W_l = l * W0 (l = 1..L),
11
+ compute moving means along time with stride=1 (no padding).
12
+ - Take max over time at each scale (one max per frequency band).
13
+ - Average these L maxima to obtain the pooled descriptor z_c(f).
14
+
15
+ Output: (B, C, F)
16
+ """
17
+ def __init__(self, base_window: int, num_levels: int):
18
+ super().__init__()
19
+ assert base_window >= 1 and num_levels >= 1
20
+ self.base_window = base_window
21
+ self.num_levels = num_levels
22
+
23
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
24
+ """
25
+ x: (B, C, F, T)
26
+ returns: (B, C, F)
27
+ """
28
+ B, C, Freq, T = x.shape
29
+ x_1d = x.reshape(B * C * Freq, 1, T) # (BCF, 1, T)
30
+
31
+ level_maxes = []
32
+ # W_l = l * W0 (clamped to T to avoid empty pooling)
33
+ for l in range(1, self.num_levels + 1):
34
+ k = min(l * self.base_window, T)
35
+ # avg over time windows of length k (stride 1)
36
+ local_means = F.avg_pool1d(x_1d, kernel_size=k, stride=1) # (BCF, 1, T - k + 1)
37
+ # max over time (Eq. 5's inner max_t)
38
+ max_over_t = local_means.max(dim=2)[0].squeeze(1) # (BCF,)
39
+ level_maxes.append(max_over_t)
40
+
41
+ # Average across levels (1/L * sum_l ...)
42
+ z = torch.stack(level_maxes, dim=0).mean(dim=0) # (BCF,)
43
+
44
+ return z.view(B, C, Freq)
45
+
46
+ def extra_repr(self) -> str:
47
+ return f"base_window={self.base_window}, num_levels={self.num_levels}"
custom_layers/WavKAN.py ADDED
@@ -0,0 +1,108 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+ import torch.nn.functional as F
4
+ import math
5
+
6
+ class WavKANLinear(nn.Module):
7
+ def __init__(self, in_features, out_features, wavelet_type='dog'):
8
+ super(WavKANLinear, self).__init__()
9
+ self.in_features = in_features
10
+ self.out_features = out_features
11
+ self.wavelet_type = wavelet_type
12
+ # Parameters for wavelet transformation
13
+ self.scale = nn.Parameter(torch.ones(out_features, in_features))
14
+ self.translation = nn.Parameter(torch.zeros(out_features, in_features))
15
+ self.wavelet_weights = nn.Parameter(torch.Tensor(out_features, in_features))
16
+
17
+ nn.init.kaiming_uniform_(self.wavelet_weights, a=math.sqrt(5))
18
+
19
+ # Base activation function #not used for this experiment
20
+ self.base_activation = nn.SiLU()
21
+
22
+ # Batch normalization
23
+ self.bn = nn.BatchNorm1d(out_features)
24
+
25
+ def wavelet_transform(self, x):
26
+ if x.dim() == 2:
27
+ x_expanded = x.unsqueeze(1)
28
+ else:
29
+ x_expanded = x
30
+
31
+ translation_expanded = self.translation.unsqueeze(0).expand(x.size(0), -1, -1)
32
+ scale_expanded = self.scale.unsqueeze(0).expand(x.size(0), -1, -1)
33
+ x_scaled = (x_expanded - translation_expanded) / scale_expanded
34
+
35
+ # Implementation of different wavelet types
36
+ if self.wavelet_type == 'mexican_hat':
37
+ term1 = ((x_scaled ** 2)-1)
38
+ term2 = torch.exp(-0.5 * x_scaled ** 2)
39
+ wavelet = (2 / (math.sqrt(3) * math.pi**0.25)) * term1 * term2
40
+ wavelet_weighted = wavelet * self.wavelet_weights.unsqueeze(0).expand_as(wavelet)
41
+ wavelet_output = wavelet_weighted.sum(dim=2)
42
+ elif self.wavelet_type == 'morlet':
43
+ omega0 = 5.0 # Central frequency
44
+ real = torch.cos(omega0 * x_scaled)
45
+ envelope = torch.exp(-0.5 * x_scaled ** 2)
46
+ wavelet = envelope * real
47
+ wavelet_weighted = wavelet * self.wavelet_weights.unsqueeze(0).expand_as(wavelet)
48
+ wavelet_output = wavelet_weighted.sum(dim=2)
49
+
50
+ elif self.wavelet_type == 'dog':
51
+ # Implementing Derivative of Gaussian Wavelet
52
+ dog = -x_scaled * torch.exp(-0.5 * x_scaled ** 2)
53
+ wavelet = dog
54
+ wavelet_weighted = wavelet * self.wavelet_weights.unsqueeze(0).expand_as(wavelet)
55
+ wavelet_output = wavelet_weighted.sum(dim=2)
56
+ elif self.wavelet_type == 'meyer':
57
+ # Implement Meyer Wavelet here
58
+ # Constants for the Meyer wavelet transition boundaries
59
+ v = torch.abs(x_scaled)
60
+ pi = math.pi
61
+
62
+ def meyer_aux(v):
63
+ return torch.where(v <= 1/2,torch.ones_like(v),torch.where(v >= 1,torch.zeros_like(v),torch.cos(pi / 2 * nu(2 * v - 1))))
64
+
65
+ def nu(t):
66
+ return t**4 * (35 - 84*t + 70*t**2 - 20*t**3)
67
+ # Meyer wavelet calculation using the auxiliary function
68
+ wavelet = torch.sin(pi * v) * meyer_aux(v)
69
+ wavelet_weighted = wavelet * self.wavelet_weights.unsqueeze(0).expand_as(wavelet)
70
+ wavelet_output = wavelet_weighted.sum(dim=2)
71
+ elif self.wavelet_type == 'shannon':
72
+ # Windowing the sinc function to limit its support
73
+ pi = math.pi
74
+ sinc = torch.sinc(x_scaled / pi) # sinc(x) = sin(pi*x) / (pi*x)
75
+
76
+ # Applying a Hamming window to limit the infinite support of the sinc function
77
+ window = torch.hamming_window(x_scaled.size(-1), periodic=False, dtype=x_scaled.dtype, device=x_scaled.device)
78
+ # Shannon wavelet is the product of the sinc function and the window
79
+ wavelet = sinc * window
80
+ wavelet_weighted = wavelet * self.wavelet_weights.unsqueeze(0).expand_as(wavelet)
81
+ wavelet_output = wavelet_weighted.sum(dim=2)
82
+ #You can try many more wavelet types ...
83
+ else:
84
+ raise ValueError("Unsupported wavelet type")
85
+
86
+ return wavelet_output
87
+
88
+ def forward(self, x):
89
+ return self.bn(self.wavelet_transform(x))
90
+
91
+ def extra_repr(self) -> str:
92
+ return (f"in_features={self.in_features}, out_features={self.out_features}, "
93
+ f"wavelet_type={self.wavelet_type}")
94
+
95
+ class WavKAN(nn.Module):
96
+ def __init__(self, layers_hidden, wavelet_type='dog'):
97
+ super(WavKAN, self).__init__()
98
+ self.layers = nn.ModuleList()
99
+ self.dropout = nn.Dropout(0.5)
100
+ for in_features, out_features in zip(layers_hidden[:-1], layers_hidden[1:]):
101
+ self.layers.append(WavKANLinear(in_features, out_features, wavelet_type))
102
+
103
+ def forward(self, x):
104
+ for i, layer in enumerate(self.layers):
105
+ x = layer(x)
106
+ if i < len(self.layers) - 1:
107
+ x = self.dropout(x)
108
+ return x
custom_layers/__init__.py ADDED
File without changes
model.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+
4
+ from custom_layers.SSRP_MS import SSRP_MS
5
+ from custom_layers.WavKAN import WavKANLinear
6
+ from custom_layers.PCAw_Pool import PCAw_Pool
7
+
8
+
9
+ class CNN_PCAw_SSRPMS_KAN(nn.Module):
10
+ def __init__(self, num_classes):
11
+ super(CNN_PCAw_SSRPMS_KAN, self).__init__()
12
+
13
+ self.conv1 = nn.Sequential(
14
+ nn.ZeroPad2d((0, 0, 0, 1)),
15
+ nn.Conv2d(1, 64, kernel_size=3),
16
+ nn.BatchNorm2d(64),
17
+ nn.ReLU(),
18
+ PCAw_Pool(kernel_size=(3, 3), stride=(3, 3)),
19
+ )
20
+
21
+ self.conv2 = nn.Sequential(
22
+ nn.ZeroPad2d((0, 0, 0, 1)),
23
+ nn.Conv2d(64, 128, kernel_size=3),
24
+ nn.BatchNorm2d(128),
25
+ nn.ReLU(),
26
+ nn.AvgPool2d(kernel_size=(2, 2), stride=(2, 2)),
27
+ )
28
+
29
+ self.conv3 = nn.Sequential(
30
+ nn.Conv2d(128, 256, kernel_size=3),
31
+ nn.BatchNorm2d(256),
32
+ nn.ReLU()
33
+ )
34
+
35
+ self.ssrp_ms = SSRP_MS(base_window=3, num_levels=5)
36
+ self.flatten = nn.Flatten()
37
+
38
+ self.fc = nn.Linear(1024, 128)
39
+ self.kan = WavKANLinear(128, num_classes)
40
+
41
+ def forward(self, x):
42
+ x = self.conv1(x)
43
+ x = self.conv2(x)
44
+ x = self.conv3(x)
45
+ x = self.ssrp_ms(x)
46
+ x = self.flatten(x)
47
+ x = self.fc(x)
48
+ x = self.kan(x)
49
+ return x
preprocessing.py ADDED
@@ -0,0 +1,88 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import torch
3
+ import torch.nn as nn
4
+ import torch.nn.functional as F
5
+ import torchaudio
6
+ import soundfile as sf
7
+
8
+ TARGET_SR = 44100
9
+ TARGET_DURATION = 5
10
+ TARGET_NUM_SAMPLES = TARGET_SR * TARGET_DURATION
11
+
12
+
13
+ class NormalizeMeanStd(nn.Module):
14
+ def __init__(self, mean: float, std: float, eps: float = 1e-6):
15
+ super().__init__()
16
+ self.register_buffer("mean", torch.tensor(mean).view(1, 1, 1))
17
+ self.register_buffer("std", torch.tensor(std).view(1, 1, 1))
18
+ self.eps = eps
19
+
20
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
21
+ return (x - self.mean) / (self.std + self.eps)
22
+
23
+
24
+ def mel_transform_from_stats(
25
+ stats_path: str,
26
+ sample_rate: int = 44100,
27
+ n_fft: int = 1024,
28
+ hop_length: int = 256,
29
+ n_mels: int = 40,
30
+ ):
31
+ with open(stats_path, "r", encoding="utf-8") as f:
32
+ s = json.load(f)
33
+
34
+ return nn.Sequential(
35
+ torchaudio.transforms.MelSpectrogram(
36
+ sample_rate=sample_rate,
37
+ n_fft=n_fft,
38
+ hop_length=hop_length,
39
+ n_mels=n_mels,
40
+ ),
41
+ torchaudio.transforms.AmplitudeToDB(),
42
+ NormalizeMeanStd(s["mean"], s["std"]),
43
+ )
44
+
45
+
46
+ def load_audio_with_soundfile(file_path: str):
47
+ waveform, sr = sf.read(file_path, always_2d=True) # [samples, channels]
48
+ waveform = torch.tensor(waveform, dtype=torch.float32).transpose(0, 1) # [channels, samples]
49
+ return waveform, sr
50
+
51
+
52
+ def convert_to_mono(waveform: torch.Tensor) -> torch.Tensor:
53
+ if waveform.shape[0] > 1:
54
+ waveform = waveform.mean(dim=0, keepdim=True)
55
+ return waveform
56
+
57
+
58
+ def resample_if_needed(waveform: torch.Tensor, orig_sr: int, target_sr: int = TARGET_SR) -> torch.Tensor:
59
+ if orig_sr != target_sr:
60
+ resampler = torchaudio.transforms.Resample(orig_freq=orig_sr, new_freq=target_sr)
61
+ waveform = resampler(waveform)
62
+ return waveform
63
+
64
+
65
+ def pad_or_trim(waveform: torch.Tensor, target_num_samples: int = TARGET_NUM_SAMPLES) -> torch.Tensor:
66
+ num_samples = waveform.shape[1]
67
+
68
+ if num_samples > target_num_samples:
69
+ waveform = waveform[:, :target_num_samples]
70
+ elif num_samples < target_num_samples:
71
+ pad_amount = target_num_samples - num_samples
72
+ waveform = F.pad(waveform, (0, pad_amount))
73
+
74
+ return waveform
75
+
76
+
77
+ def preprocess_audio(file_path: str, stats_path: str) -> torch.Tensor:
78
+ waveform, sr = load_audio_with_soundfile(file_path)
79
+
80
+ waveform = convert_to_mono(waveform)
81
+ waveform = resample_if_needed(waveform, sr, TARGET_SR)
82
+ waveform = pad_or_trim(waveform, TARGET_NUM_SAMPLES)
83
+
84
+ mel_transform = mel_transform_from_stats(stats_path=stats_path)
85
+ features = mel_transform(waveform) # [1, 40, time]
86
+
87
+ features = features.unsqueeze(0) # [1, 1, 40, time]
88
+ return features
requirements.txt CHANGED
@@ -1,3 +1,55 @@
1
- altair
2
- pandas
3
- streamlit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ altair==6.0.0
2
+ attrs==25.4.0
3
+ blinker==1.9.0
4
+ cachetools==7.0.3
5
+ certifi==2026.2.25
6
+ cffi==2.0.0
7
+ charset-normalizer==3.4.5
8
+ click==8.3.1
9
+ colorama==0.4.6
10
+ contourpy==1.3.2
11
+ cycler==0.12.1
12
+ filelock==3.25.0
13
+ fonttools==4.61.1
14
+ fsspec==2026.2.0
15
+ gitdb==4.0.12
16
+ GitPython==3.1.46
17
+ idna==3.11
18
+ Jinja2==3.1.6
19
+ jsonschema==4.26.0
20
+ jsonschema-specifications==2025.9.1
21
+ kiwisolver==1.4.9
22
+ MarkupSafe==3.0.3
23
+ matplotlib==3.10.8
24
+ mpmath==1.3.0
25
+ narwhals==2.17.0
26
+ networkx==3.4.2
27
+ numpy==2.2.6
28
+ packaging==26.0
29
+ pandas==2.3.3
30
+ pillow==12.1.1
31
+ protobuf==6.33.5
32
+ pyarrow==23.0.1
33
+ pycparser==3.0
34
+ pydeck==0.9.1
35
+ pyparsing==3.3.2
36
+ python-dateutil==2.9.0.post0
37
+ pytz==2026.1.post1
38
+ referencing==0.37.0
39
+ requests==2.32.5
40
+ rpds-py==0.30.0
41
+ six==1.17.0
42
+ smmap==5.0.2
43
+ soundfile==0.13.1
44
+ streamlit==1.55.0
45
+ sympy==1.14.0
46
+ tenacity==9.1.4
47
+ toml==0.10.2
48
+ torch==2.10.0
49
+ torchaudio==2.10.0
50
+ torchcodec==0.10.0
51
+ tornado==6.5.4
52
+ typing_extensions==4.15.0
53
+ tzdata==2025.3
54
+ urllib3==2.6.3
55
+ watchdog==6.0.0
samples/clock_tick.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:be4ee5f3dbf6cfdf65a4afa6b0dc98914e5e36728d1d3778039de70f21579303
3
+ size 441044
samples/crying_baby.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e97893bf8a391f769576ee70497ba51640d3bc646cbf20e719f6d22cc033b2e3
3
+ size 441044
samples/dog_bark.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:52a876cff473fe9b6deb4feea9545a2b976e63e03744921afc72f9800758ce30
3
+ size 441044
samples/fire_crackling.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6e8d15a855d1622c1f7dcd12e00ba2bfc3b65a316947e1bbdcaa0be0157175db
3
+ size 441044
samples/helicopter.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5faf5c216d7573ba4f0a07c5495d3666925c5287b97752341661614ea68d87dd
3
+ size 441044
samples/rain.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7a03695543a56d61464ea2bd87e0947bb994d38c914492c02be686693b6e1dac
3
+ size 441044
samples/rooster.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:55c3e6b3ce6299d1c1675fc9655327aa2dbc2130d87c08f5f38cac7d33e336cc
3
+ size 441044
samples/sneezing.wav ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:4c1cdb30db5fd43eb62c17c051efaf7dde38938c0449e6bfcfb998dba92cc443
3
+ size 441044
stats/esc50_mel_stats.json ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "test_fold": 1,
3
+ "train_folds": [
4
+ 2,
5
+ 3,
6
+ 4,
7
+ 5
8
+ ],
9
+ "sample_rate": 44100,
10
+ "n_fft": 1024,
11
+ "hop_length": 256,
12
+ "n_mels": 40,
13
+ "space": "mel_db",
14
+ "stat_type": "global_scalar_over_all_bins_and_frames",
15
+ "count": 55168000,
16
+ "mean": -18.729941406024793,
17
+ "std": 34.19103885429348
18
+ }
utils.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn.functional as F
3
+
4
+ from model import CNN_PCAw_SSRPMS_KAN
5
+ from classes import ESC50_CLASSES
6
+ from compression.pruning import apply_structural_pruning
7
+ from compression.qat import prepare_qat_model, convert_qat_model
8
+
9
+ def load_model(model_path: str, device: torch.device, num_classes: int):
10
+ model = CNN_PCAw_SSRPMS_KAN(num_classes=num_classes)
11
+
12
+ checkpoint = torch.load(model_path, map_location=device)
13
+
14
+ # Case 1: pure state_dict saved with torch.save(model.state_dict(), path)
15
+ try:
16
+ model.load_state_dict(checkpoint)
17
+ except RuntimeError:
18
+ raise ValueError("Unsupported checkpoint format.")
19
+
20
+ model.to(device)
21
+ model.eval()
22
+ return model
23
+
24
+ def load_compressed_model(model_path: str, compressed_model_path: str, num_classes: int):
25
+ model = CNN_PCAw_SSRPMS_KAN(num_classes=num_classes)
26
+
27
+ original_checkpoint = torch.load(model_path, map_location="cpu")
28
+ compressed_checkpoint = torch.load(compressed_model_path, map_location="cpu")
29
+ try:
30
+ model.load_state_dict(original_checkpoint)
31
+ model = apply_structural_pruning(model)
32
+ model = prepare_qat_model(model)
33
+ model = convert_qat_model(model)
34
+ model.load_state_dict(compressed_checkpoint)
35
+ except RuntimeError as e:
36
+ print(f"Error loading compressed model: {e}")
37
+ raise ValueError("Unsupported checkpoint format.")
38
+
39
+ model.to("cpu")
40
+ model.eval()
41
+ return model
42
+
43
+ @torch.no_grad()
44
+ def predict(model, input_tensor: torch.Tensor, device: torch.device):
45
+ input_tensor = input_tensor.to(device)
46
+
47
+ logits = model(input_tensor)
48
+ probs = F.softmax(logits, dim=1).squeeze(0).cpu()
49
+
50
+ top_idx = torch.argmax(probs).item()
51
+ top_class = ESC50_CLASSES[top_idx]
52
+ top_prob = probs[top_idx].item()
53
+
54
+ all_probs = [
55
+ {"class_name": ESC50_CLASSES[i], "probability": float(probs[i])}
56
+ for i in range(len(ESC50_CLASSES))
57
+ ]
58
+
59
+ all_probs = sorted(all_probs, key=lambda x: x["probability"], reverse=True)
60
+
61
+ return top_class, top_prob, all_probs
weights/esc50_model.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:5495dad32213c28eee48f0c191faa5617ff62cc97ed8c93f7266a569909f414b
3
+ size 2100103
weights/esc50_model_compressed.pth ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:7ddce164e3ba926b5ebc1e84e94a42d35e7d13f958a6a0074e46f91ad4d2d378
3
+ size 147278