Spaces:
Runtime error
Runtime error
add files of the application
Browse files- .dockerignore +8 -0
- .gitattributes +8 -0
- .gitignore +7 -0
- Dockerfile +16 -20
- README.md +20 -20
- app.py +346 -0
- classes.py +15 -0
- compression/pruning.py +190 -0
- compression/qat.py +141 -0
- custom_layers/PCAw_Pool.py +74 -0
- custom_layers/SSRP_MS.py +47 -0
- custom_layers/WavKAN.py +108 -0
- custom_layers/__init__.py +0 -0
- model.py +49 -0
- preprocessing.py +88 -0
- requirements.txt +55 -3
- samples/clock_tick.wav +3 -0
- samples/crying_baby.wav +3 -0
- samples/dog_bark.wav +3 -0
- samples/fire_crackling.wav +3 -0
- samples/helicopter.wav +3 -0
- samples/rain.wav +3 -0
- samples/rooster.wav +3 -0
- samples/sneezing.wav +3 -0
- stats/esc50_mel_stats.json +18 -0
- utils.py +61 -0
- weights/esc50_model.pth +3 -0
- weights/esc50_model_compressed.pth +3 -0
.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.
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
COPY
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 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:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 6 |
-
sdk: docker
|
| 7 |
-
app_port: 8501
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
|
|
|
| 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 — 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 {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 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|