Upload 5 files
Browse files- app.py +71 -0
- best_model.pth +3 -0
- config.py +204 -0
- model.py +128 -0
- requirements.txt +33 -0
app.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
import torch
|
| 3 |
+
import librosa
|
| 4 |
+
import numpy as np
|
| 5 |
+
import noisereduce as nr # <-- CRITICAL ADDITION
|
| 6 |
+
from model import MachineSoundCNN # Make sure this matches your model class name!
|
| 7 |
+
|
| 8 |
+
# 1. Define your exact classes
|
| 9 |
+
CLASSES = [
|
| 10 |
+
"Machine 1 Normal", "Machine 1 Abnormal",
|
| 11 |
+
"Machine 2 Normal", "Machine 2 Abnormal",
|
| 12 |
+
"Machine 3 Normal", "Machine 3 Abnormal"
|
| 13 |
+
]
|
| 14 |
+
|
| 15 |
+
# 2. Load the Model (Forcing CPU for the Hugging Face Free Tier)
|
| 16 |
+
device = torch.device('cpu')
|
| 17 |
+
model = MachineSoundCNN() # Initialize EL sir's architecture
|
| 18 |
+
model.load_state_dict(torch.load('best_model.pth', map_location=device))
|
| 19 |
+
model.eval() # Set to evaluation mode (freezes BatchNorm/Dropout)
|
| 20 |
+
|
| 21 |
+
# 3. Preprocessing & Inference Function
|
| 22 |
+
def predict_machine_sound(audio_path):
|
| 23 |
+
if audio_path is None:
|
| 24 |
+
return "Please upload an audio file."
|
| 25 |
+
|
| 26 |
+
# Load audio at exactly 16kHz
|
| 27 |
+
y, sr = librosa.load(audio_path, sr=16000)
|
| 28 |
+
|
| 29 |
+
# --- CRITICAL FIX: NOISE REDUCTION ---
|
| 30 |
+
# Replicating your specific 0.5-second noise profile logic
|
| 31 |
+
profile_length = int(0.5 * sr)
|
| 32 |
+
if len(y) > profile_length:
|
| 33 |
+
noise_profile = y[:profile_length]
|
| 34 |
+
y_clean = nr.reduce_noise(y=y, sr=sr, y_noise=noise_profile, prop_decrease=0.8)
|
| 35 |
+
else:
|
| 36 |
+
y_clean = nr.reduce_noise(y=y, sr=sr, y_noise=y, prop_decrease=0.5)
|
| 37 |
+
|
| 38 |
+
# Clip signal to [-1.0, 1.0] just like your training pipeline
|
| 39 |
+
y_clean = np.clip(y_clean, -1.0, 1.0)
|
| 40 |
+
# -------------------------------------
|
| 41 |
+
|
| 42 |
+
# Generate Mel Spectrogram (128 bins) using the CLEAN audio
|
| 43 |
+
mel_spec = librosa.feature.melspectrogram(y=y_clean, sr=sr, n_mels=128)
|
| 44 |
+
mel_spec_db = librosa.power_to_db(mel_spec, ref=np.max)
|
| 45 |
+
|
| 46 |
+
# Reshape for the CNN: (Batch, Channel, Height, Time) -> (1, 1, 128, time_steps)
|
| 47 |
+
input_tensor = torch.tensor(mel_spec_db, dtype=torch.float32).unsqueeze(0).unsqueeze(0)
|
| 48 |
+
|
| 49 |
+
# Forward Pass through the CNN
|
| 50 |
+
with torch.no_grad():
|
| 51 |
+
outputs = model(input_tensor)
|
| 52 |
+
# Convert raw output numbers into percentages (0.0 to 1.0)
|
| 53 |
+
probabilities = torch.nn.functional.softmax(outputs[0], dim=0)
|
| 54 |
+
|
| 55 |
+
# Format output for the Gradio UI: a dictionary of {Class Name: Probability}
|
| 56 |
+
result = {CLASSES[i]: float(probabilities[i]) for i in range(len(CLASSES))}
|
| 57 |
+
return result
|
| 58 |
+
|
| 59 |
+
# 4. Build the Web Interface
|
| 60 |
+
interface = gr.Interface(
|
| 61 |
+
fn=predict_machine_sound,
|
| 62 |
+
inputs=gr.Audio(type="filepath", label="Upload Machine Audio (.wav)"),
|
| 63 |
+
outputs=gr.Label(num_top_classes=6, label="CNN Prediction Confidence"),
|
| 64 |
+
title="Industrial Machine Sound Anomaly Detector",
|
| 65 |
+
description="Upload an audio clip of an industrial machine. The Custom CNN will analyze the Mel Spectrogram and predict if it is operating normally or failing.",
|
| 66 |
+
allow_flagging="never"
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
# 5. Launch the App
|
| 70 |
+
if __name__ == "__main__":
|
| 71 |
+
interface.launch()
|
best_model.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:c1a1f9eb14f2604bd2493cd2130b198a8d6ee4d032154bc9a9922eb94950f930
|
| 3 |
+
size 9970018
|
config.py
ADDED
|
@@ -0,0 +1,204 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
===============================================================================
|
| 3 |
+
config.py — Central Configuration for the Machine Sound Classification Pipeline
|
| 4 |
+
===============================================================================
|
| 5 |
+
|
| 6 |
+
PURPOSE:
|
| 7 |
+
This file is the SINGLE SOURCE OF TRUTH for every hyperparameter,
|
| 8 |
+
file path, and constant used across the project. Every team member
|
| 9 |
+
should import values from here instead of hard-coding numbers.
|
| 10 |
+
|
| 11 |
+
If you need to change a value (e.g. sample rate, n_mels, batch size),
|
| 12 |
+
change it HERE and it propagates everywhere automatically.
|
| 13 |
+
|
| 14 |
+
OWNER: Shared (everyone imports from here)
|
| 15 |
+
===============================================================================
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
import os
|
| 19 |
+
|
| 20 |
+
# =============================================================================
|
| 21 |
+
# 1. PATH CONFIGURATION
|
| 22 |
+
# =============================================================================
|
| 23 |
+
# Root directory of the project (where this file lives)
|
| 24 |
+
PROJECT_ROOT = os.path.dirname(os.path.abspath(__file__))
|
| 25 |
+
|
| 26 |
+
# Directory where the examiner places test .wav files
|
| 27 |
+
# CRITICAL: infer.py reads from this exact folder
|
| 28 |
+
DATA_DIR = os.path.join(PROJECT_ROOT, "data")
|
| 29 |
+
|
| 30 |
+
# Directory for the training dataset (organized by class)
|
| 31 |
+
TRAIN_DATA_DIR = os.path.join(PROJECT_ROOT, "train_data")
|
| 32 |
+
|
| 33 |
+
PROCESSED_DATA_DIR = os.path.join(PROJECT_ROOT, "processed_features")
|
| 34 |
+
|
| 35 |
+
# Directory where trained model checkpoints are saved
|
| 36 |
+
CHECKPOINT_DIR = os.path.join(PROJECT_ROOT, "checkpoints")
|
| 37 |
+
|
| 38 |
+
# Output files required by the submission
|
| 39 |
+
RESULTS_FILE = os.path.join(PROJECT_ROOT, "results.txt")
|
| 40 |
+
TIME_FILE = os.path.join(PROJECT_ROOT, "time.txt")
|
| 41 |
+
|
| 42 |
+
# Directory for split metadata (train/val/test file lists)
|
| 43 |
+
SPLITS_DIR = os.path.join(PROJECT_ROOT, "splits")
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
# =============================================================================
|
| 47 |
+
# 2. AUDIO PREPROCESSING CONSTANTS
|
| 48 |
+
# =============================================================================
|
| 49 |
+
# Target sampling rate — all audio is resampled to this before processing.
|
| 50 |
+
# 16 kHz is standard for machine sound analysis; Nyquist limit = 8 kHz.
|
| 51 |
+
# Owner: JSON (resampling.py)
|
| 52 |
+
TARGET_SR = 16000
|
| 53 |
+
|
| 54 |
+
# Silence removal threshold in dB.
|
| 55 |
+
# Frames quieter than this (relative to peak) are considered silence.
|
| 56 |
+
# 20 dB is a safe starting point for factory recordings.
|
| 57 |
+
# Owner: EL sir (silence_removal.py)
|
| 58 |
+
SILENCE_TOP_DB = 40
|
| 59 |
+
|
| 60 |
+
# Noise reduction — duration (in seconds) of the noise profile sample.
|
| 61 |
+
# We estimate the noise floor from the first N seconds of each clip.
|
| 62 |
+
# Owner: EL sir (noise_reduction.py)
|
| 63 |
+
NOISE_PROFILE_DURATION = 0.5 # seconds
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
# =============================================================================
|
| 67 |
+
# 3. MEL SPECTROGRAM PARAMETERS
|
| 68 |
+
# =============================================================================
|
| 69 |
+
# These MUST be agreed upon by sala7 (feature extraction) and EL sir (CNN input).
|
| 70 |
+
# Changing n_mels here changes the CNN input height automatically.
|
| 71 |
+
# Owner: sala7 (mel_spectrogram.py) — coordinated with EL sir
|
| 72 |
+
|
| 73 |
+
# Number of mel filter banks (height of the spectrogram "image")
|
| 74 |
+
N_MELS = 128
|
| 75 |
+
|
| 76 |
+
# FFT window size — 1024 samples @ 16 kHz = 64 ms analysis window.
|
| 77 |
+
# For machine fault detection, 64 ms captures one full rotation of many motors.
|
| 78 |
+
N_FFT = 1024
|
| 79 |
+
|
| 80 |
+
# Hop length — 512 samples = 50% overlap between consecutive frames.
|
| 81 |
+
# Good time resolution without quadrupling computation.
|
| 82 |
+
HOP_LENGTH = 512
|
| 83 |
+
|
| 84 |
+
# Maximum frequency for the mel filterbank.
|
| 85 |
+
# At 16 kHz sampling rate, Nyquist = 8 kHz, so fmax = 8000.
|
| 86 |
+
FMAX = 8000
|
| 87 |
+
|
| 88 |
+
# Power for the mel spectrogram (2.0 = power spectrogram)
|
| 89 |
+
MEL_POWER = 2.0
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
# =============================================================================
|
| 93 |
+
# 4. FIXED-SIZE TENSOR PARAMETERS
|
| 94 |
+
# =============================================================================
|
| 95 |
+
# After silence removal, audio clips vary in length. We must pad/trim
|
| 96 |
+
# spectrograms to a uniform time dimension for batching.
|
| 97 |
+
#
|
| 98 |
+
# CALCULATION:
|
| 99 |
+
# Original audio ≈ 11 seconds → 11 * 16000 = 176,000 samples
|
| 100 |
+
# Time frames = ceil(176000 / 512) = 344 frames (full 11s)
|
| 101 |
+
# After silence trimming, clips are shorter → 256 frames ≈ 8.2 seconds
|
| 102 |
+
# is a reasonable target that captures the machine sound while
|
| 103 |
+
# discarding silence.
|
| 104 |
+
#
|
| 105 |
+
|
| 106 |
+
# Final input shape to the CNN: (batch, channels, n_mels, time_frames)
|
| 107 |
+
# channels = 1 (grayscale spectrogram)
|
| 108 |
+
CNN_INPUT_CHANNELS = 1
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
# =============================================================================
|
| 112 |
+
# 5. DATA SPLIT RATIOS
|
| 113 |
+
# =============================================================================
|
| 114 |
+
# Stratified split ratios — every class appears in every split at the same proportion.
|
| 115 |
+
# Owner: JSON (splits.py)
|
| 116 |
+
TRAIN_RATIO = 0.70
|
| 117 |
+
VAL_RATIO = 0.15
|
| 118 |
+
TEST_RATIO = 0.15
|
| 119 |
+
|
| 120 |
+
# Random seed for reproducibility across all random operations
|
| 121 |
+
RANDOM_SEED = 42
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
# =============================================================================
|
| 125 |
+
# 6. MODEL ARCHITECTURE PARAMETERS
|
| 126 |
+
# =============================================================================
|
| 127 |
+
# Number of output classes:
|
| 128 |
+
# 0 = Machine 1 Normal, 1 = Machine 1 Abnormal,
|
| 129 |
+
# 2 = Machine 2 Normal, 3 = Machine 2 Abnormal,
|
| 130 |
+
# 4 = Machine 3 Normal, 5 = Machine 3 Abnormal
|
| 131 |
+
# Owner: EL sir (cnn.py)
|
| 132 |
+
NUM_CLASSES = 6
|
| 133 |
+
|
| 134 |
+
# Convolutional layer filter counts (depth progression)
|
| 135 |
+
# Each successive layer doubles the filters to capture more complex patterns.
|
| 136 |
+
CNN_FILTERS = [32, 64, 128, 256]
|
| 137 |
+
|
| 138 |
+
# Kernel size for all Conv2D layers
|
| 139 |
+
CNN_KERNEL_SIZE = 3
|
| 140 |
+
|
| 141 |
+
# Padding for Conv2D layers (1 = 'same' padding with kernel_size=3)
|
| 142 |
+
CNN_PADDING = 1
|
| 143 |
+
|
| 144 |
+
# Pool size for MaxPool2d layers
|
| 145 |
+
CNN_POOL_SIZE = 2
|
| 146 |
+
|
| 147 |
+
# Output size of AdaptiveAvgPool2d before the classifier head
|
| 148 |
+
# This makes the model accept any time-length input gracefully.
|
| 149 |
+
ADAPTIVE_POOL_OUTPUT = (4, 4)
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
# =============================================================================
|
| 153 |
+
# 7. TRAINING HYPERPARAMETERS
|
| 154 |
+
# =============================================================================
|
| 155 |
+
# Owner: Osama (trainer.py)
|
| 156 |
+
|
| 157 |
+
# Optimizer: AdamW (corrects weight decay application vs vanilla Adam)
|
| 158 |
+
LEARNING_RATE = 1e-3
|
| 159 |
+
WEIGHT_DECAY = 1e-4
|
| 160 |
+
|
| 161 |
+
# Batch size for training DataLoader
|
| 162 |
+
BATCH_SIZE = 64
|
| 163 |
+
|
| 164 |
+
# Maximum number of training epochs
|
| 165 |
+
MAX_EPOCHS = 100
|
| 166 |
+
|
| 167 |
+
# Early stopping — stop if val loss doesn't improve for this many epochs
|
| 168 |
+
EARLY_STOPPING_PATIENCE = 10
|
| 169 |
+
|
| 170 |
+
# Learning rate scheduler — cosine annealing
|
| 171 |
+
LR_SCHEDULER_T_MAX = MAX_EPOCHS # period of the cosine cycle
|
| 172 |
+
|
| 173 |
+
# Number of DataLoader workers for parallel data loading
|
| 174 |
+
NUM_WORKERS = 8
|
| 175 |
+
|
| 176 |
+
|
| 177 |
+
# =============================================================================
|
| 178 |
+
# 8. AUGMENTATION PARAMETERS
|
| 179 |
+
# =============================================================================
|
| 180 |
+
# Owner: sala7 (augmentation.py)
|
| 181 |
+
# These are applied ONLY during training (not val/test).
|
| 182 |
+
|
| 183 |
+
# SpecAugment: number of frequency bands to mask
|
| 184 |
+
FREQ_MASK_PARAM = 20
|
| 185 |
+
|
| 186 |
+
# SpecAugment: number of time steps to mask
|
| 187 |
+
TIME_MASK_PARAM = 30
|
| 188 |
+
|
| 189 |
+
# Gaussian noise injection — standard deviation
|
| 190 |
+
NOISE_STD = 0.005
|
| 191 |
+
|
| 192 |
+
# Probability of applying each augmentation
|
| 193 |
+
AUGMENT_PROB = 0.5
|
| 194 |
+
|
| 195 |
+
|
| 196 |
+
# =============================================================================
|
| 197 |
+
# 9. INFERENCE PARAMETERS
|
| 198 |
+
# =============================================================================
|
| 199 |
+
# Path to the best saved model checkpoint (used by infer.py)
|
| 200 |
+
BEST_MODEL_PATH = os.path.join(CHECKPOINT_DIR, "best_model.pth")
|
| 201 |
+
|
| 202 |
+
# Device selection for inference (auto-detect GPU)
|
| 203 |
+
import torch
|
| 204 |
+
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
model.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
===============================================================================
|
| 3 |
+
model/cnn.py — CNN Architecture for Machine Sound Classification
|
| 4 |
+
===============================================================================
|
| 5 |
+
|
| 6 |
+
OWNER: EL sir
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import torch
|
| 10 |
+
import torch.nn as nn
|
| 11 |
+
from config import (
|
| 12 |
+
CNN_INPUT_CHANNELS,
|
| 13 |
+
NUM_CLASSES,
|
| 14 |
+
CNN_FILTERS,
|
| 15 |
+
CNN_KERNEL_SIZE,
|
| 16 |
+
CNN_PADDING,
|
| 17 |
+
CNN_POOL_SIZE,
|
| 18 |
+
ADAPTIVE_POOL_OUTPUT,
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
class ConvBlock(nn.Module):
|
| 22 |
+
"""
|
| 23 |
+
A single convolutional block: Conv2d → BatchNorm → ReLU → MaxPool2d.
|
| 24 |
+
"""
|
| 25 |
+
def __init__(self, in_channels, out_channels):
|
| 26 |
+
super(ConvBlock, self).__init__()
|
| 27 |
+
self.conv = nn.Conv2d(
|
| 28 |
+
in_channels, out_channels,
|
| 29 |
+
kernel_size=CNN_KERNEL_SIZE,
|
| 30 |
+
padding=CNN_PADDING,
|
| 31 |
+
bias=False # Bias is redundant when followed by BatchNorm
|
| 32 |
+
)
|
| 33 |
+
self.bn = nn.BatchNorm2d(out_channels)
|
| 34 |
+
self.relu = nn.ReLU(inplace=True)
|
| 35 |
+
self.pool = nn.MaxPool2d(CNN_POOL_SIZE)
|
| 36 |
+
|
| 37 |
+
def forward(self, x):
|
| 38 |
+
x = self.conv(x)
|
| 39 |
+
x = self.bn(x)
|
| 40 |
+
x = self.relu(x)
|
| 41 |
+
x = self.pool(x)
|
| 42 |
+
return x
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
class MachineSoundCNN(nn.Module):
|
| 46 |
+
"""
|
| 47 |
+
Custom CNN for 6-class machine sound classification from mel spectrograms.
|
| 48 |
+
"""
|
| 49 |
+
def __init__(self, num_classes=NUM_CLASSES):
|
| 50 |
+
super(MachineSoundCNN, self).__init__()
|
| 51 |
+
|
| 52 |
+
# ---------------------------------------------------------------------
|
| 53 |
+
# 1. Feature Extractor (Dynamic Conv Blocks)
|
| 54 |
+
# ---------------------------------------------------------------------
|
| 55 |
+
layers = []
|
| 56 |
+
in_channels = CNN_INPUT_CHANNELS
|
| 57 |
+
|
| 58 |
+
# Dynamically build blocks based on config.py (e.g., 32 -> 64 -> 128 -> 256)
|
| 59 |
+
for out_channels in CNN_FILTERS:
|
| 60 |
+
layers.append(ConvBlock(in_channels, out_channels))
|
| 61 |
+
in_channels = out_channels # The output of this layer is the input to the next
|
| 62 |
+
|
| 63 |
+
self.features = nn.Sequential(*layers)
|
| 64 |
+
|
| 65 |
+
# ---------------------------------------------------------------------
|
| 66 |
+
# 2. Adaptive Pooling
|
| 67 |
+
# ---------------------------------------------------------------------
|
| 68 |
+
# This squashes whatever time dimension is left into a fixed (4, 4) grid
|
| 69 |
+
self.adaptive_pool = nn.AdaptiveAvgPool2d(ADAPTIVE_POOL_OUTPUT)
|
| 70 |
+
|
| 71 |
+
# ---------------------------------------------------------------------
|
| 72 |
+
# 3. Classifier Head (Fully Connected)
|
| 73 |
+
# ---------------------------------------------------------------------
|
| 74 |
+
# Calculate flattened size: Last filter size (256) * height (4) * width (4) = 4096
|
| 75 |
+
flattened_size = CNN_FILTERS[-1] * ADAPTIVE_POOL_OUTPUT[0] * ADAPTIVE_POOL_OUTPUT[1]
|
| 76 |
+
|
| 77 |
+
self.classifier = nn.Sequential(
|
| 78 |
+
nn.Flatten(),
|
| 79 |
+
nn.Dropout(p=0.5), # Regularization: Prevent overfitting
|
| 80 |
+
nn.Linear(flattened_size, 512), # Hidden layer to compress features
|
| 81 |
+
nn.ReLU(inplace=True),
|
| 82 |
+
nn.Dropout(p=0.5), # Regularization
|
| 83 |
+
nn.Linear(512, num_classes) # Output raw logits (6 classes)
|
| 84 |
+
)
|
| 85 |
+
|
| 86 |
+
def forward(self, x):
|
| 87 |
+
# Extract visual features from the spectrogram
|
| 88 |
+
x = self.features(x)
|
| 89 |
+
# Pool them to a fixed mathematical size
|
| 90 |
+
x = self.adaptive_pool(x)
|
| 91 |
+
# Make the final classification
|
| 92 |
+
x = self.classifier(x)
|
| 93 |
+
return x
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
# =============================================================================
|
| 97 |
+
# SHAPE SANITY CHECK
|
| 98 |
+
# =============================================================================
|
| 99 |
+
if __name__ == "__main__":
|
| 100 |
+
print("=" * 60)
|
| 101 |
+
print("CNN Architecture Shape Sanity Check")
|
| 102 |
+
print("=" * 60)
|
| 103 |
+
|
| 104 |
+
model = MachineSoundCNN()
|
| 105 |
+
|
| 106 |
+
# Create a dummy batch based on JSON's exact tensor output (batch=4, ch=1, mels=128, time=281)
|
| 107 |
+
dummy_input = torch.randn(4, 1, 128, 281)
|
| 108 |
+
print(f"\nInput shape: {dummy_input.shape}")
|
| 109 |
+
|
| 110 |
+
# Trace through the architecture
|
| 111 |
+
x = dummy_input
|
| 112 |
+
for i, block in enumerate(model.features):
|
| 113 |
+
x = block(x)
|
| 114 |
+
print(f"After Conv Block {i+1} ({CNN_FILTERS[i]} filters): {x.shape}")
|
| 115 |
+
|
| 116 |
+
x = model.adaptive_pool(x)
|
| 117 |
+
print(f"After AdaptivePool: {x.shape}")
|
| 118 |
+
|
| 119 |
+
x = model.classifier(x)
|
| 120 |
+
print(f"After Classifier: {x.shape}")
|
| 121 |
+
|
| 122 |
+
print(f"\n✓ Output shape is correct: {x.shape} (batch=4, classes={NUM_CLASSES})")
|
| 123 |
+
|
| 124 |
+
# Count parameters
|
| 125 |
+
total_params = sum(p.numel() for p in model.parameters())
|
| 126 |
+
trainable_params = sum(p.numel() for p in model.parameters() if p.requires_grad)
|
| 127 |
+
print(f"\nTotal parameters: {total_params:,}")
|
| 128 |
+
print("=" * 60)
|
requirements.txt
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# =============================================================================
|
| 2 |
+
# requirements.txt
|
| 3 |
+
# =============================================================================
|
| 4 |
+
|
| 5 |
+
gradio==3.36.0
|
| 6 |
+
|
| 7 |
+
# Core deep learning
|
| 8 |
+
torch==2.9.1
|
| 9 |
+
torchaudio==2.9.1
|
| 10 |
+
torchvision==0.24.1
|
| 11 |
+
|
| 12 |
+
# Audio processing
|
| 13 |
+
librosa==0.10.1
|
| 14 |
+
soundfile==0.12.1
|
| 15 |
+
resampy==0.4.3 #added this for kaiser window in resampling
|
| 16 |
+
|
| 17 |
+
# Noise reduction
|
| 18 |
+
noisereduce==3.0.2
|
| 19 |
+
|
| 20 |
+
# Numerical / scientific
|
| 21 |
+
numpy==1.26.4
|
| 22 |
+
scipy==1.12.0
|
| 23 |
+
|
| 24 |
+
# Data handling
|
| 25 |
+
scikit-learn==1.4.1.post1
|
| 26 |
+
pandas==2.2.1
|
| 27 |
+
|
| 28 |
+
# Visualization
|
| 29 |
+
matplotlib==3.8.3
|
| 30 |
+
seaborn==0.13.2
|
| 31 |
+
|
| 32 |
+
# Progress bars
|
| 33 |
+
tqdm==4.66.2
|