{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.12.12","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceType":"competition","sourceId":7634,"databundleVersionId":46676}],"dockerImageVersionId":31260,"isInternetEnabled":false,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"code","source":"!7za x /kaggle/input/tensorflow-speech-recognition-challenge/train.7z -o/kaggle/working > /dev/null\n# !7za x /kaggle/input/tensorflow-speech-recognition-challenge/test.7z -o/kaggle/working > /dev/null","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-05T12:11:10.396825Z","iopub.execute_input":"2026-03-05T12:11:10.397142Z","iopub.status.idle":"2026-03-05T12:12:35.928322Z","shell.execute_reply.started":"2026-03-05T12:11:10.397113Z","shell.execute_reply":"2026-03-05T12:12:35.927536Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import os\nimport pandas as pd\nimport numpy as np\n\nbase_dir = '/kaggle/working/train/audio/'\ncommands = ['yes', 'no', 'up', 'down', 'left', 'right', 'on', 'off', 'stop', 'go']\n\nall_folders = [d for d in os.listdir(base_dir) if os.path.isdir(os.path.join(base_dir, d)) and not d.startswith('_')]\nunknown_folders = [d for d in all_folders if d not in commands]\n\ndata = []\n\nnp.random.seed(42)\n\nfor label in commands:\n    folder_path = os.path.join(base_dir, label)\n    for f in os.listdir(folder_path):\n        if f.endswith('.wav'):\n            data.append({\n                'path': os.path.join(folder_path, f),\n                'label': commands.index(label),\n                'speaker': f.split('_nohash_')[0]\n            })\n\nfor label in unknown_folders:\n    folder_path = os.path.join(base_dir, label)\n    files = os.listdir(folder_path)\n    np.random.shuffle(files)\n    files = [f for f in files if f.endswith('.wav')]\n    for f in files[:125]: \n        data.append({\n            'path': os.path.join(folder_path, f), \n            'label': 10, \n            'speaker': f.split('_nohash_')[0]\n        })\n\nsilence_label = 11\nsilence_data = []\nsilence_dir = '/kaggle/working/train/audio/_background_noise_/'\nfor f in os.listdir(silence_dir):\n    if f.endswith('.wav'):\n        full_path = os.path.join(silence_dir, f)\n        for i in range(400):\n            silence_data.append({\n                'path': full_path, \n                'label': silence_label,\n                'speaker': 'background'\n            })\n\ndf = pd.DataFrame(data)\nsilence_df = pd.DataFrame(silence_data)\nsplit_ratio = 0.2\n\ntrain_indices, val_indices = [], []\n\nfor label in df['label'].unique():\n    class_df = df[df['label'] == label]\n    speakers = class_df['speaker'].unique()\n    \n    target_val_count  = int(len(speakers) * split_ratio)\n    current_val_count = 0\n\n    class_val_speakers = []\n    for sp in speakers:\n        sp_files_count = len(class_df[class_df['speaker'] == sp])\n        if current_val_count < target_val_count:\n            class_val_speakers.append(sp)\n            current_val_count += sp_files_count\n        else:\n            break\n\n    val_indices.extend(class_df[class_df['speaker'].isin(class_val_speakers)].index)\n    train_indices.extend(class_df[~class_df['speaker'].isin(class_val_speakers)].index)\n\ntrain_df, val_df = df.loc[train_indices], df.loc[val_indices]\n\nval_silence_count = int(len(silence_df) * split_ratio)\ntrain_silence_df = silence_df.iloc[val_silence_count:]\nval_silence_df   = silence_df.iloc[:val_silence_count]\n\ntrain_df = pd.concat([train_df, train_silence_df])\nval_df   = pd.concat([val_df, val_silence_df])","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-05T12:12:35.929926Z","iopub.execute_input":"2026-03-05T12:12:35.930663Z","iopub.status.idle":"2026-03-05T12:12:36.772510Z","shell.execute_reply.started":"2026-03-05T12:12:35.930629Z","shell.execute_reply":"2026-03-05T12:12:36.771923Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch\nimport torchaudio\nimport torch.nn.functional as F\n\nfrom torch.utils.data import Dataset, DataLoader\n\nclass DS(Dataset):\n    def __init__(self, df, is_train, win_size=16000, augmentation_kwargs={}):\n        self.df = df\n        self.l = len(self.df)\n\n        self.is_train = is_train\n        self.win_size = win_size\n\n        self.init_augmentation(augmentation_kwargs)\n        \n    def __len__(self):\n        return self.l\n\n    def __getitem__(self, i):\n        row = self.df.iloc[i]\n\n        path  = row['path']\n        label = row['label']\n\n        waveform, sr = torchaudio.load(path)\n        waveform = self.resize(waveform)\n        waveform = waveform.squeeze(0)\n        waveform = waveform / (torch.max(torch.abs(waveform)) + 1e-8)\n        waveform = self.augment(waveform)\n        waveform = torch.clamp(waveform, -1.0, 1.0)\n        return waveform, torch.tensor(label, dtype=torch.long)\n\n    def init_augmentation(self, augmentation_kwargs):\n        self.augmentations = []\n        wrap = lambda x: (x.sample if hasattr(x, 'sample') else lambda: x)\n        \n        raw_factor = augmentation_kwargs.get(\"noise_factor\", 0.0)\n        noises = augmentation_kwargs.get(\"noises\", [])\n\n        is_factor_active = not isinstance(raw_factor, (int, float)) or raw_factor > 0\n        if is_factor_active and len(noises) > 0 and self.is_train: \n            noise_factor_gen = wrap(raw_factor)\n            \n            def apply_noise(x):\n                f = noise_factor_gen()\n                if f <= 0: return x\n                \n                gen = noises[torch.randint(0, len(noises), (1,)).item()]\n                noise = gen(x.shape[-1]).squeeze(0)\n                noise = noise / (torch.max(torch.abs(noise)) + 1e-8)\n                return x + f * noise\n\n            self.augmentations.append(apply_noise)\n\n    def augment(self, waveform):\n        augmented = waveform\n        for augmentation in self.augmentations:\n            augmented = augmentation(augmented)\n        return augmented\n    \n    def resize(self, waveform):\n        total_samples = waveform.shape[-1]\n        \n        if total_samples > self.win_size:\n            if self.is_train: \n                start = torch.randint(0, total_samples - self.win_size + 1, (1,)).item()\n            else: \n                start = 0\n            waveform = waveform[:, start : start + self.win_size]\n            \n        elif total_samples < self.win_size:\n            pad_total = self.win_size - total_samples\n            if self.is_train: \n                pad_left = torch.randint(0, pad_total + 1, (1,)).item()\n            else: \n                pad_left = 0\n            pad_right = pad_total - pad_left\n            waveform = F.pad(waveform, (pad_left, pad_right), value=0)\n            \n        return waveform\n\naug_config = {\n    \"noise_factor\": 0.05,\n    \"noises\": [\n        lambda size: torch.randn(1, size),\n        lambda size: torch.randn(1, size).cumsum(dim=-1).div(10.0),\n        lambda size: torch.randn(1, size).cumsum(dim=-1).cumsum(dim=-1).div(100.0)\n    ]\n}\n\ntrain_ds = DS(train_df, is_train=True, augmentation_kwargs=aug_config)\nval_ds   = DS(val_df,   is_train=False)\n\ntrain_loader = DataLoader(\n    train_ds, \n    batch_size=128, \n    shuffle=True,\n    num_workers=4,\n    pin_memory=True\n)\n\nval_loader = DataLoader(\n    val_ds, \n    batch_size=128, \n    shuffle=False,\n    num_workers=4,\n    pin_memory=True\n)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-05T12:12:36.773253Z","iopub.execute_input":"2026-03-05T12:12:36.773600Z","iopub.status.idle":"2026-03-05T12:12:44.713224Z","shell.execute_reply.started":"2026-03-05T12:12:36.773568Z","shell.execute_reply":"2026-03-05T12:12:44.712647Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-05T12:12:44.714944Z","iopub.execute_input":"2026-03-05T12:12:44.715631Z","iopub.status.idle":"2026-03-05T12:12:44.982008Z","shell.execute_reply.started":"2026-03-05T12:12:44.715605Z","shell.execute_reply":"2026-03-05T12:12:44.981201Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import torch.nn as nn\nimport torchaudio.transforms as T\n\nclass FeatureExtractor(nn.Module):\n    def __init__(self, sr=16000, n_mfcc=13, n_fft=400, hop_length=160, n_mels=40):\n        super().__init__()\n        self.mfcc_gen = T.MFCC(\n            sample_rate=sr,\n            n_mfcc=n_mfcc,\n            melkwargs={\n                \"n_fft\": n_fft, \n                \"hop_length\": hop_length, \n                \"n_mels\": n_mels, \n                \"center\": False\n            }\n        )\n        self.compute_deltas = T.ComputeDeltas()\n\n    def forward(self, waveform):\n        with torch.no_grad():\n            mfcc = self.mfcc_gen(waveform)\n            d_mfcc = self.compute_deltas(mfcc)\n            dd_mfcc = self.compute_deltas(d_mfcc)\n\n            combined = torch.cat([mfcc, d_mfcc, dd_mfcc], dim=1)\n            mu = combined.mean(dim=-1, keepdim=True)\n            sigma = combined.std(dim=-1, keepdim=True) + 1e-8\n        return (combined - mu) / sigma\n\nfeature_extractor = FeatureExtractor(n_mfcc=40).to(device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-05T12:12:44.983602Z","iopub.execute_input":"2026-03-05T12:12:44.983846Z","iopub.status.idle":"2026-03-05T12:12:45.467918Z","shell.execute_reply.started":"2026-03-05T12:12:44.983823Z","shell.execute_reply":"2026-03-05T12:12:45.467280Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"class SpeechModel(nn.Module):\n    def __init__(self, backbone, num_classes=12):\n        super().__init__()\n        self.feature_extractor = feature_extractor\n        self.backbone = backbone\n        self.classifier = nn.Linear(backbone.output_dim, num_classes)\n\n    def forward(self, x):\n        x = self.feature_extractor(x)\n        x = self.backbone(x)\n        return self.classifier(x)\n\nclass CNNBackbone(nn.Module):\n    def __init__(self, input_dim=39):\n        super().__init__()\n        self.output_dim = 128\n        self.net = nn.Sequential(\n            nn.Conv1d(input_dim, 64, kernel_size=3, padding=1),\n            nn.BatchNorm1d(64), nn.ReLU(), nn.MaxPool1d(2),\n            nn.Conv1d(64, 128, kernel_size=3, padding=1),\n            nn.BatchNorm1d(128), nn.ReLU(),\n            nn.AdaptiveAvgPool1d(1)\n        )\n    def forward(self, x):\n        return self.net(x).squeeze(-1)\n\nclass RNNBackbone(nn.Module):\n    def __init__(self, input_dim=39, hidden=128):\n        super().__init__()\n        self.output_dim = hidden\n        self.rnn = nn.GRU(input_dim, hidden, batch_first=True, bidirectional=False)\n        \n    def forward(self, x):\n        x = x.transpose(1, 2)\n        _, hn = self.rnn(x)\n        return hn.squeeze(0)\n\nclass CRNNBackbone(nn.Module):\n    def __init__(self, input_dim=39):\n        super().__init__()\n        self.output_dim = 128\n        self.conv = nn.Sequential(\n            nn.Conv1d(input_dim, 64, 3, padding=1),\n            nn.ReLU(), nn.MaxPool1d(2)\n        )\n        self.rnn = nn.GRU(64, 128, batch_first=True)\n\n    def forward(self, x):\n        x = self.conv(x).transpose(1, 2)\n        _, hn = self.rnn(x)\n        return hn.squeeze(0)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-05T12:12:45.468797Z","iopub.execute_input":"2026-03-05T12:12:45.469008Z","iopub.status.idle":"2026-03-05T12:12:45.477948Z","shell.execute_reply.started":"2026-03-05T12:12:45.468988Z","shell.execute_reply":"2026-03-05T12:12:45.477166Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"import plotly.graph_objects as go\nimport plotly.express as px\nfrom plotly.subplots import make_subplots\nfrom sklearn.metrics import f1_score, confusion_matrix\n\nclass ExperimentRunner:\n    def __init__(self, train_loader, val_loader, class_names, device='cuda'):\n        self.train_loader = train_loader\n        self.val_loader = val_loader\n        self.class_names = class_names\n        self.device = device\n        self.results = {}\n\n    def train_epoch(self, model, criterion, optimizer):\n        model.train()\n        running_loss, correct, total = 0.0, 0, 0\n        for x, y in self.train_loader:\n            x, y = x.to(self.device), y.to(self.device)\n            optimizer.zero_grad()\n            outputs = model(x)\n            loss = criterion(outputs, y)\n            loss.backward()\n            optimizer.step()\n            \n            running_loss += loss.item()\n            correct += (outputs.argmax(1) == y).sum().item()\n            total += y.size(0)\n        return running_loss / len(self.train_loader), correct / total\n\n    @torch.no_grad()\n    def validate_epoch(self, model, criterion):\n        model.eval()\n        running_loss, all_preds, all_labels = 0.0, [], []\n        for x, y in self.val_loader:\n            x, y = x.to(self.device), y.to(self.device)\n            outputs = model(x)\n            loss = criterion(outputs, y)\n            running_loss += loss.item()\n            all_preds.extend(outputs.argmax(1).cpu().numpy())\n            all_labels.extend(y.cpu().numpy())\n        \n        all_preds, all_labels = np.array(all_preds), np.array(all_labels)\n        acc = np.mean(all_preds == all_labels)\n        f1 = f1_score(all_labels, all_preds, average='macro')\n        cm = confusion_matrix(all_labels, all_preds, labels=range(len(self.class_names)))\n        return running_loss / len(self.val_loader), acc, f1, cm\n\n    def run(self, model, experiment_name, epochs=20, lr=1e-3):\n        model.to(self.device)\n        optimizer = torch.optim.Adam(model.parameters(), lr=lr)\n        criterion = torch.nn.CrossEntropyLoss()\n        scheduler = torch.optim.lr_scheduler.ReduceLROnPlateau(optimizer, 'max', patience=3)\n        \n        h = {'train_loss': [], 'val_loss': [], 'train_acc': [], 'val_acc': [], 'val_f1': [], 'cm_history': [], 'lr': []}\n        best_f1 = 0.0\n\n        for epoch in range(epochs):\n            t_loss, t_acc = self.train_epoch(model, criterion, optimizer)\n            v_loss, v_acc, v_f1, cm = self.validate_epoch(model, criterion)\n            \n            for k, v in zip(h.keys(), [t_loss, v_loss, t_acc, v_acc, v_f1, cm, optimizer.param_groups[0]['lr']]):\n                h[k].append(v)\n            \n            scheduler.step(v_f1)\n            if v_f1 > best_f1:\n                best_f1 = v_f1\n                torch.save(model.state_dict(), f\"best_{experiment_name}.pth\")\n            \n        self.results[experiment_name] = h\n        self.plot_results(experiment_name)\n        return h\n\n    def plot_results(self, name):\n        h = self.results[name]\n        epochs = list(range(len(h['train_loss'])))\n        \n        fig = make_subplots(specs=[[{\"secondary_y\": True}]])\n        fig.add_trace(go.Scatter(x=epochs, y=h['train_loss'], name=\"Train Loss\"), secondary_y=False)\n        fig.add_trace(go.Scatter(x=epochs, y=h['val_loss'], name=\"Val Loss\"), secondary_y=False)\n        fig.add_trace(go.Scatter(x=epochs, y=h['val_f1'], name=\"Val F1\", line=dict(dash='dash')), secondary_y=True)\n        fig.update_layout(title=f'Experiment: {name}', xaxis_title='Epoch', template='plotly_dark')\n        fig.write_html(f\"{name}_metrics.html\")\n\n        cms = []\n        for cm in h['cm_history']:\n            cm_norm = cm.astype('float') / (cm.sum(axis=1)[:, np.newaxis] + 1e-8)\n            cms.append(cm_norm)\n        cms = np.array(cms)\n\n        fig_cm = px.imshow(\n            cms,\n            animation_frame=0,\n            x=self.class_names, \n            y=self.class_names,\n            color_continuous_scale='Viridis', \n            origin='upper', \n            text_auto=\".2f\",\n            labels=dict(animation_frame=\"Epoch\")\n        )\n        fig_cm.update_layout(title=f'Confusion Matrix Evolution: {name}')\n        fig_cm.write_html(f\"{name}_cm_animation.html\")\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-05T12:36:26.188862Z","iopub.execute_input":"2026-03-05T12:36:26.189443Z","iopub.status.idle":"2026-03-05T12:36:26.203722Z","shell.execute_reply.started":"2026-03-05T12:36:26.189402Z","shell.execute_reply":"2026-03-05T12:36:26.203199Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"device = torch.device(\"cuda\" if torch.cuda.is_available() else \"cpu\")\nclass_names = commands + ['unknown', 'silence']\nn_mfcc = 40\ninput_dim = n_mfcc * 3\n\nrunner = ExperimentRunner(train_loader, val_loader, class_names, device=device)\n\nexperiments = [\n    (CNNBackbone(input_dim=input_dim), \"CNN_Baseline\"),\n    (RNNBackbone(input_dim=input_dim), \"GRU_Recurrent\"),\n    (CRNNBackbone(input_dim=input_dim), \"CRNN_Hybrid\")\n]\n\nfor backbone, name in experiments:\n    model = SpeechModel(backbone, num_classes=len(class_names)).to(device)\n    runner.run(model, name, epochs=30, lr=1e-4)\n    del model\n    torch.cuda.empty_cache()\n\nimport matplotlib.pyplot as plt\n\nfig, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(22, 6))\n\nfor name, hist in runner.results.items():\n    epochs = range(1, len(hist['val_acc']) + 1)\n    \n    ax1.plot(epochs, hist['val_acc'], 'o-', label=f'{name} (Val)')\n    ax1.plot(epochs, hist['train_acc'], '--', alpha=0.4, label=f'{name} (Train)')\n    ax1.set_title('Accuracy (Точность)', fontsize=14)\n    ax1.set_xlabel('Эпоха')\n    ax1.set_ylabel('Value')\n    ax1.legend()\n    ax1.grid(True, alpha=0.3)\n    \n    ax2.plot(epochs, hist['val_f1'], 's-', label=f'{name} (F1 Val)')\n    ax2.set_title('Macro F1-Score', fontsize=14)\n    ax2.set_xlabel('Эпоха')\n    ax2.set_ylabel('Value')\n    ax2.legend()\n    ax2.grid(True, alpha=0.3)\n    \n    ax3.plot(epochs, hist['val_loss'], '^-', label=f'{name} (Loss Val)')\n    ax3.plot(epochs, hist['train_loss'], '--', alpha=0.4, label=f'{name} (Loss Train)')\n    ax3.set_title('Cross Entropy Loss', fontsize=14)\n    ax3.set_xlabel('Эпоха')\n    ax3.set_ylabel('Loss')\n    ax3.set_yscale('log')\n    ax3.legend()\n    ax3.grid(True, alpha=0.3)\n\nplt.tight_layout()\nplt.show()\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2026-03-05T12:36:43.049483Z","iopub.execute_input":"2026-03-05T12:36:43.050211Z","iopub.status.idle":"2026-03-05T13:16:41.230533Z","shell.execute_reply.started":"2026-03-05T12:36:43.050168Z","shell.execute_reply":"2026-03-05T13:16:41.229685Z"}},"outputs":[],"execution_count":null}]}