{"metadata":{"kernelspec":{"language":"python","display_name":"Python 3","name":"python3"},"language_info":{"name":"python","version":"3.11.13","mimetype":"text/x-python","codemirror_mode":{"name":"ipython","version":3},"pygments_lexer":"ipython3","nbconvert_exporter":"python","file_extension":".py"},"kaggle":{"accelerator":"nvidiaTeslaT4","dataSources":[{"sourceId":3004,"databundleVersionId":861823,"sourceType":"competition"}],"dockerImageVersionId":31090,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"### 导入库 打印目录","metadata":{}},{"cell_type":"code","source":"import os\nimport warnings\n\nimport numpy as np \nimport pandas as pd \n\nimport matplotlib.pyplot as plt\nimport seaborn as sns\n\nfrom sklearn.model_selection import train_test_split,StratifiedKFold\nfrom sklearn.metrics import  accuracy_score,confusion_matrix,classification_report\nfrom sklearn.metrics import accuracy_score,classification_report \n\nimport torch\nimport torch.nn as nn\nimport torch.optim as optim \nfrom torchvision import transforms\nfrom torch.utils.data import Dataset,DataLoader\n\nfrom tqdm import tqdm\nimport time\n\nplt.rcParams[\"font.family\"] = [\"SimHei\"]\nplt.rcParams[\"axes.unicode_minus\"]=False \n\nfor dirname, _, filenames in os.walk('/kaggle/input'):\n    for filename in filenames:\n        print(os.path.join(dirname, filename))","metadata":{"_uuid":"8f2839f25d086af736a60e9eeb907d3b93b6e0e5","_cell_guid":"b1076dfc-b9ad-4769-8c92-a6c4dae69d19","trusted":true,"execution":{"iopub.status.busy":"2025-10-05T09:18:07.820617Z","iopub.execute_input":"2025-10-05T09:18:07.821150Z","iopub.status.idle":"2025-10-05T09:18:07.828325Z","shell.execute_reply.started":"2025-10-05T09:18:07.821126Z","shell.execute_reply":"2025-10-05T09:18:07.827550Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 定义全局参数","metadata":{}},{"cell_type":"code","source":"class Config:\n    def __init__(self):\n        self.seed = 666  # 随机种子\n        \n        # 训练参数\n        self.epochs = 666  # 轮数\n        self.batch_size = 50  # 批次大小（\n        self.val_ratio = 0.2  # 分割比例\n        self.val_batch_size = 30  # 验证集批次大小\n        self.learning_rate = 0.001  # 学习率\n        self.weight_decay = 0.00001  # 权重衰减（\n        self.lr_scheduler = True  # 是否启用学习率调度器\n        self.scheduler_patience = 5  # 调度patience\n        self.early_stopping = True\n        self.early_stop_patience = 50 # 早停patience\n\n        self.device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n        self.num_workers = 16 \n\n        # 图像参数\n        self.image_cols = None  # 调用update_from_dataset函数检测确定\n        self.label_col = \"label\"  # 标签列名\n        self.image_height = 28  # 图像高度\n        self.image_width = 28  # 图像宽度\n        self.num_channels = 1  # 通道数（1为灰度图，3为RGB）\n        self.input_size = None  # 输入维度-调用下面函数计算\n        self._calculate_input_size()  # 计算输入维度\n        self.num_classes = 10  #输出分类\n\n        #预处理\n        self.normalize = True\n        self.shuffle = True\n        self.stratify  =True\n\n        # 输入\n        self.data_path = \"/kaggle/input/\"  # 数据根路径\n        self.train_csv_path = os.path.join(self.data_path, \"digit-recognizer/train.csv\")  # 训练数据CSV路径\n        self.image_dir = os.path.join(self.data_path, \"images/\")  # 图像文件夹路径\n        \n        # 输出\n        self.output_dir = \"/kaggle/working/outputs/\"  # 输出根目录\n        self.model_save_path = os.path.join(self.output_dir, \"best_model/\")  # 模型保存路径\n        self.submission_path = os.path.join(self.output_dir, \"submission/\")  # 提交文件路径\n        \n\n        os.makedirs(self.output_dir, exist_ok=True)\n\n        \n\n    def _calculate_input_size(self):\n        self.input_size = self.image_height * self.image_width * self.num_channels  \n        \n\n    def update_from_dataset(self, df):\n        # 检查标签列是否存在\n        if self.label_col in df.columns:\n            print(f\"标签列: {self.label_col}\")\n        else:\n            print(\"标签列不存在\")\n\n        # 自动检测图像特征列\n        if self.image_cols is None:\n            exclude_cols = [self.label_col, 'id', 'index']  # 排除非特征列\n            self.image_cols = [col for col in df.columns if col not in exclude_cols]\n            print(f\"自动检测到 {len(self.image_cols)} 个图像特征列\")\n\n        # 确定类别数量\n        if self.label_col in df.columns:\n            self.num_classes = len(df[self.label_col].unique())\n            print(f\"类别数量: {self.num_classes}\")\n        else:\n            print(\"标签列不存在，无法确定类别数量\")\n\n    def print_info(self):\n        print(\"=\" * 50)\n        print(f\"设备: {self.device}\")\n        print(f\"随机种子: {self.seed}\")\n        print(f\"训练轮数: {self.epochs}\")\n        print(f\"批次大小: 训练={self.batch_size}, 验证={self.val_batch_size}\")\n        print(f\"学习率: {self.learning_rate}\")\n        print(f\"图像尺寸: {self.image_height}x{self.image_width} (通道数: {self.num_channels})\")\n        print(f\"输入维度: {self.input_size}\")\n        print(f\"类别数量: {self.num_classes}\")\n        print(f\"训练数据路径: {self.train_csv_path}\")\n        print(f\"输出目录: {self.output_dir}\")\n        print(\"=\" * 50)\n    ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T09:18:09.895236Z","iopub.execute_input":"2025-10-05T09:18:09.895713Z","iopub.status.idle":"2025-10-05T09:18:09.905822Z","shell.execute_reply.started":"2025-10-05T09:18:09.895690Z","shell.execute_reply":"2025-10-05T09:18:09.905093Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"CONFIG = Config()\nprint(f\"使用设备：{CONFIG.device}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T09:18:12.306856Z","iopub.execute_input":"2025-10-05T09:18:12.307443Z","iopub.status.idle":"2025-10-05T09:18:12.311676Z","shell.execute_reply.started":"2025-10-05T09:18:12.307419Z","shell.execute_reply":"2025-10-05T09:18:12.311005Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"CONFIG.print_info()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T09:18:13.783578Z","iopub.execute_input":"2025-10-05T09:18:13.783850Z","iopub.status.idle":"2025-10-05T09:18:13.787808Z","shell.execute_reply.started":"2025-10-05T09:18:13.783828Z","shell.execute_reply":"2025-10-05T09:18:13.787095Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 定义随机种子","metadata":{}},{"cell_type":"code","source":"def set_random_seed(seed=CONFIG.seed):\n    np.random.seed(seed)\n    torch.manual_seed(seed)\n    if torch.cuda.is_available():\n        torch.cuda.manual_seed(seed)\n        #为当前GPU设定种子\n        torch.cuda.manual_seed_all(seed)\n        #如果存在多个GPU 设定为同一个种子\n        torch.backends.cudnn.deterministic = True\n        #设置cuDNN使用确定性算法，这样每次运行的结果都会一致。但注意，这可能会降低性能。\n        torch.backends.cudnn.benchmark = False\n        #关闭cuDNN的benchmark模式。benchmark模式会根据硬件和输入大小选择最优的算法，\n        #但这样会导致每次运行可能选择不同的算法，从而造成结果不一致。关闭它可以确保确定性。","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T09:18:16.155561Z","iopub.execute_input":"2025-10-05T09:18:16.155796Z","iopub.status.idle":"2025-10-05T09:18:16.160228Z","shell.execute_reply.started":"2025-10-05T09:18:16.155781Z","shell.execute_reply":"2025-10-05T09:18:16.159420Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"set_random_seed()\nprint(f\"随即种子已设定：{CONFIG.seed}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T09:18:17.098997Z","iopub.execute_input":"2025-10-05T09:18:17.099312Z","iopub.status.idle":"2025-10-05T09:18:17.113912Z","shell.execute_reply.started":"2025-10-05T09:18:17.099292Z","shell.execute_reply":"2025-10-05T09:18:17.113379Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_look_df = pd.read_csv(CONFIG.train_csv_path)\ntrain_look_df.head(10)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T09:18:17.818206Z","iopub.execute_input":"2025-10-05T09:18:17.818490Z","iopub.status.idle":"2025-10-05T09:18:20.533136Z","shell.execute_reply.started":"2025-10-05T09:18:17.818469Z","shell.execute_reply":"2025-10-05T09:18:20.532461Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"# train_data,val_data = load_and_split_data(CONFIG)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T09:18:22.139331Z","iopub.execute_input":"2025-10-05T09:18:22.140062Z","iopub.status.idle":"2025-10-05T09:18:22.143130Z","shell.execute_reply.started":"2025-10-05T09:18:22.140026Z","shell.execute_reply":"2025-10-05T09:18:22.142333Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 加载和预处理数据","metadata":{}},{"cell_type":"code","source":"def load_and_preprocess_data(config):\n    \n    if not os.path.exists(config.train_csv_path):\n        raise FileNotFoundError(f\"训练文件数据不存在：{config.train_csv_path}\")\n    \n    train_path = config.train_csv_path\n    train_df = pd.read_csv(train_path)\n    print(f\"原始训练数据形状：{train_df.shape}\")\n    \n    config.update_from_dataset(train_df)\n\n    if config.label_col in train_df.columns:\n        x = train_df.drop(config.label_col,axis = 1).values\n        y = train_df[config.label_col].values\n    else:\n        print(\"傻逼去看看你的标签，没找到分什么分\")\n        return \n\n    if config.normalize:\n        x = x.astype(np.float32)/255.0\n        print(\"归一化[0,1]了\")\n\n    if y is not None:\n        stratify = y if config.stratify else None\n\n        x_train,x_val,y_train,y_val = train_test_split(\n            x,y,test_size = config.val_ratio,\n            shuffle = config.shuffle,stratify = stratify\n        )\n    print(f\"训练集：{len(x_train)}条，验证集：{len(x_val)}条\")\n    return (x_train,y_train),(x_val,y_val)\n\n    ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T09:18:23.419986Z","iopub.execute_input":"2025-10-05T09:18:23.420273Z","iopub.status.idle":"2025-10-05T09:18:23.426340Z","shell.execute_reply.started":"2025-10-05T09:18:23.420254Z","shell.execute_reply":"2025-10-05T09:18:23.425572Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_data,val_data = load_and_preprocess_data(CONFIG)\nx_train,y_train = train_data\nx_val,y_val  = val_data","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T09:18:25.079036Z","iopub.execute_input":"2025-10-05T09:18:25.079621Z","iopub.status.idle":"2025-10-05T09:18:27.495360Z","shell.execute_reply.started":"2025-10-05T09:18:25.079596Z","shell.execute_reply":"2025-10-05T09:18:27.494639Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 封装成自定义数据集类","metadata":{}},{"cell_type":"code","source":"class CustomDataset(Dataset):  \n    def __init__(self, features, labels=None, transform=None): \n        self.features = features  \n        self.labels = labels      \n        self.transform = transform \n\n    def __len__(self):\n        return len(self.features)\n\n    def __getitem__(self, idx):\n        #获取单个样本（特征+标签，或仅特征）\n        # 获取特征并转换为张量\n        feature = self.features[idx]\n        feature_tensor = torch.tensor(feature, dtype=torch.float32)\n        \n        if self.transform:\n            feature_tensor = self.transform(feature_tensor)\n\n        if self.labels is not None:  \n            label = self.labels[idx]\n            label_tensor = torch.tensor(label, dtype=torch.long)\n            return feature_tensor, label_tensor\n        else:\n            print(\"检查标签\")\n            return feature_tensor\n    ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T09:18:30.058229Z","iopub.execute_input":"2025-10-05T09:18:30.058510Z","iopub.status.idle":"2025-10-05T09:18:30.064003Z","shell.execute_reply.started":"2025-10-05T09:18:30.058489Z","shell.execute_reply":"2025-10-05T09:18:30.063201Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 创建训练和验证数据加载器","metadata":{}},{"cell_type":"code","source":"def get_dataloaders(config,train_data,val_data):\n    x_train,y_train = train_data\n    x_val,y_val = val_data\n\n    #创建数据集\n    train_dataset = CustomDataset(x_train,y_train)\n    val_dataset = CustomDataset(x_val,y_val)\n\n    #创建数据加载器\n    train_loader = DataLoader(\n        train_dataset,\n        batch_size=config.batch_size,\n        shuffle = config.shuffle,\n        num_workers = 0, #默认值 指定用于加载数据的子进程数量\n        pin_memory = True \n        #开启 “固定内存” 模式，当使用 GPU 时，会将加载到 CPU 的数据直接复制到 CUDA 固定内存中，\n        #减少 CPU 到 GPU 的数据传输时间，提高效率。\n    )\n    val_loader = DataLoader(\n        val_dataset,\n        batch_size=config.batch_size,\n        shuffle = False,\n        num_workers = 0,\n        pin_memory = True\n    )\n    print(\"数据加载器创建完成：\")\n    print(f\"  训练集: {len(train_dataset)} 样本, {len(train_loader)} 批次\")\n    print(f\"  验证集: {len(val_dataset)} 样本, {len(val_loader)} 批次\")\n\n    return train_loader,val_loader","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T09:18:32.006837Z","iopub.execute_input":"2025-10-05T09:18:32.007130Z","iopub.status.idle":"2025-10-05T09:18:32.012556Z","shell.execute_reply.started":"2025-10-05T09:18:32.007107Z","shell.execute_reply":"2025-10-05T09:18:32.011750Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"train_loader,val_loader = get_dataloaders(CONFIG,train_data,val_data)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T09:18:34.914751Z","iopub.execute_input":"2025-10-05T09:18:34.915029Z","iopub.status.idle":"2025-10-05T09:18:34.919442Z","shell.execute_reply.started":"2025-10-05T09:18:34.915010Z","shell.execute_reply":"2025-10-05T09:18:34.918628Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 训练模型","metadata":{}},{"cell_type":"code","source":"class MLP(nn.Module):\n    def __init__(self,dropout_rate = 0.1):\n        super(MLP,self).__init__()\n        self.l1 = nn.Sequential(\n            nn.Linear(28*28, 2048, bias=True), \n            nn.BatchNorm1d(2048),\n            nn.ReLU(inplace=True), \n            nn.Dropout(dropout_rate)    #使用dropout时候epoch要大一点\n        )\n\n        self.l2 = nn.Sequential(\n            nn.Linear(2048, 512, bias=True),\n            nn.BatchNorm1d(512),\n            nn.ReLU(inplace=True),\n            nn.Dropout(dropout_rate)   \n        )\n        \n        self.l3 = nn.Sequential(\n            nn.Linear(512, 10, bias=True),\n            nn.BatchNorm1d(10),\n            nn.ReLU(inplace=True), \n            nn.Dropout(dropout_rate)   \n        )\n    \n    def forward(self, x):\n        x = x.view(-1, 28*28)\n        \n        x = self.l1(x)\n        x = self.l2(x)\n        x = self.l3(x)\n        \n        return x ","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T09:18:36.404594Z","iopub.execute_input":"2025-10-05T09:18:36.404868Z","iopub.status.idle":"2025-10-05T09:18:36.410772Z","shell.execute_reply.started":"2025-10-05T09:18:36.404846Z","shell.execute_reply":"2025-10-05T09:18:36.410128Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"mlp_model = MLP()\nmlp_model = mlp_model.to(CONFIG.device)  # 同样要转换device","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T09:18:53.236605Z","iopub.execute_input":"2025-10-05T09:18:53.236875Z","iopub.status.idle":"2025-10-05T09:18:53.482780Z","shell.execute_reply.started":"2025-10-05T09:18:53.236856Z","shell.execute_reply":"2025-10-05T09:18:53.482236Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 训练函数","metadata":{}},{"cell_type":"code","source":"def train_epoch(model,train_loader,criterion,optimizer,device):\n    model.train() #切换至训练模式，与eval()相对应\n    running_loss = 0.0 #累计损失\n    correct_predictions = 0# 累计正确预测数\n    total_samples = 0 # 累计总样本\n\n    for features,labels in tqdm(train_loader,desc='训练ing...'):\n        #遍历数据加载器的每一个批次\n        features = features.to(device)\n        labels = labels.to(device)\n\n        #梯度清零\n        optimizer.zero_grad()\n\n        #前向传播\n        outputs = model(features)\n        loss = criterion(outputs,labels)\n        #反向传播\n        loss.backward() #自动计算所有可训练参数的梯度\n        optimizer.step() #根据梯度更新参数\n        \n        running_loss+=loss.item()*features.size(0)\n        _,preds = torch.max(outputs,1)\n        correct_predictions+=torch.sum(preds==labels.data)\n        total_samples+=features.size(0)\n    epoch_loss = running_loss / total_samples\n    epoch_acc = correct_predictions.double() / total_samples\n\n    return epoch_loss,epoch_acc.item()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T09:18:55.379072Z","iopub.execute_input":"2025-10-05T09:18:55.379348Z","iopub.status.idle":"2025-10-05T09:18:55.385269Z","shell.execute_reply.started":"2025-10-05T09:18:55.379329Z","shell.execute_reply":"2025-10-05T09:18:55.384449Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 验证函数","metadata":{}},{"cell_type":"code","source":"def validate_epoch(model, val_loader, criterion, device):\n    model.eval()  # 切换到评估模式\n    running_loss = 0.0\n    correct_predictions = 0\n    total_samples = 0 \n\n    with torch.no_grad():  \n        for features, labels in tqdm(val_loader, desc='验证ing...'):\n            features = features.to(device)\n            labels = labels.to(device)\n\n            # 前向传播\n            outputs = model(features)\n            loss = criterion(outputs, labels)\n\n            running_loss += loss.item() * features.size(0)  \n            _, preds = torch.max(outputs, 1)  # 预测类别\n            correct_predictions += torch.sum(preds == labels.data)  \n            total_samples += features.size(0) \n        \n        # 循环结束后计算整个epoch的平均损失和准确率\n        epoch_loss = running_loss / total_samples\n        epoch_acc = correct_predictions.double() / total_samples\n\n        return epoch_loss, epoch_acc.item()","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T09:18:56.789390Z","iopub.execute_input":"2025-10-05T09:18:56.790062Z","iopub.status.idle":"2025-10-05T09:18:56.795419Z","shell.execute_reply.started":"2025-10-05T09:18:56.790022Z","shell.execute_reply":"2025-10-05T09:18:56.794629Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 完整训练函数","metadata":{}},{"cell_type":"code","source":"def train_model(model, train_loader, val_loader, config):\n    criterion = nn.CrossEntropyLoss() #交叉熵\n    # 优化器（Adam带L2正则化）\n    optimizer = optim.Adam(\n        model.parameters(),\n        lr=config.learning_rate,\n        weight_decay=config.weight_decay\n    )\n\n    scheduler = None\n    if config.lr_scheduler:  \n        scheduler = optim.lr_scheduler.ReduceLROnPlateau( \n            optimizer,\n            mode='max',  # 监控验证准确率最大化 要是 loss 则改min\n            factor=0.5,  # 学习率衰减为原来的1/2\n            patience=config.scheduler_patience,  # 连续patience轮无提升则衰减\n            verbose=True  \n        )\n\n    history = {\n        'train_loss': [], 'val_loss': [],\n        'train_acc': [], 'val_acc': [],\n        'learning_rates': [] \n    }\n\n    # 早停\n    best_val_acc = 0.0\n    best_model_state = None\n    epochs_no_improve = 0\n    \n    print(f\"开始训练，共{config.epochs}轮\")\n    start_time = time.time()\n\n    for epoch in range(config.epochs):\n        print(f\"\\n {epoch+1}/{config.epochs}\") \n        print(\"-\" * 30)\n\n        current_lr = optimizer.param_groups[0]['lr']\n        history['learning_rates'].append(current_lr)\n        print(f\"当前学习率：{current_lr:.6f}\")\n\n        # 训练一个epoch\n        train_loss, train_acc = train_epoch(model, train_loader, criterion, optimizer, config.device)\n        # 验证一个epoch\n        val_loss, val_acc = validate_epoch(model, val_loader, criterion, config.device)\n    \n        history['train_loss'].append(train_loss)\n        history['val_loss'].append(val_loss)\n        history['train_acc'].append(train_acc)\n        history['val_acc'].append(val_acc)\n        print(f\"训练集  损失: {train_loss:.4f}, 准确率: {train_acc:.4f}\")\n        print(f\"验证集  损失: {val_loss:.4f}, 准确率: {val_acc:.4f}\")\n\n        if scheduler:\n            scheduler.step(val_acc)\n\n        if val_acc > best_val_acc:\n            best_val_acc = val_acc\n            epochs_no_improve = 0\n            best_model_state = model.state_dict().copy()  # 深拷\n\n            save_path = os.path.join(config.model_save_path, \"3层mlp_best_model.pth\")  \n            save_dir = os.path.dirname(save_path)  \n            os.makedirs(save_dir, exist_ok=True)  # 创建目录 \n            \n            torch.save(best_model_state, save_path)\n            print(f\"最佳模型权重已保存至：{save_path}，验证准确率：{best_val_acc:.4f}\")\n        else:\n            epochs_no_improve += 1\n            print(f\"验证准确率未提升，已持续 {epochs_no_improve} 轮\")\n\n        # 早停检查\n        if config.early_stopping and epochs_no_improve >= config.early_stop_patience:\n            print(f\"第 {epoch+1} 轮无提升，已早停\") \n            break\n\n    return best_val_acc, history","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T09:18:59.099975Z","iopub.execute_input":"2025-10-05T09:18:59.100576Z","iopub.status.idle":"2025-10-05T09:18:59.109054Z","shell.execute_reply.started":"2025-10-05T09:18:59.100547Z","shell.execute_reply":"2025-10-05T09:18:59.108371Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"best_val_acc,history = train_model(mlp_model, train_loader, val_loader, CONFIG)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-10-05T09:19:00.843227Z","iopub.execute_input":"2025-10-05T09:19:00.843502Z","iopub.status.idle":"2025-10-05T09:25:22.130793Z","shell.execute_reply.started":"2025-10-05T09:19:00.843481Z","shell.execute_reply":"2025-10-05T09:25:22.130079Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}