{"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":"gpu","dataSources":[{"sourceId":3004,"databundleVersionId":861823,"isSourceIdPinned":false,"sourceType":"competition"},{"sourceId":13218220,"sourceType":"datasetVersion","datasetId":8378316}],"dockerImageVersionId":31089,"isInternetEnabled":true,"language":"python","sourceType":"notebook","isGpuEnabled":true}},"nbformat_minor":4,"nbformat":4,"cells":[{"cell_type":"markdown","source":"### 导\n","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-09-30T13:46:25.547345Z","iopub.execute_input":"2025-09-30T13:46:25.54756Z","iopub.status.idle":"2025-09-30T13:46:36.656972Z","shell.execute_reply.started":"2025-09-30T13:46:25.547543Z","shell.execute_reply":"2025-09-30T13:46:36.656296Z"}},"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.test_csv_path = os.path.join(self.data_path, \"digit-recognizer/test.csv\")  # 测试数据CSV路径\n        self.sample_submission_csv_path = os.path.join(self.data_path,\"digit-recognizer/sample_submission.csv\") #模板\n        self.submission_path = \"/kaggle/working/outputs/submission/\"  # 提交文件保存目录\n        self.model_weight_path = \"/kaggle/input/d/boluosnow/digit-recognizer-3mlp/3mlp_best_model.pth\"\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        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  # 正确引用num_channels\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-09-30T13:46:40.431902Z","iopub.execute_input":"2025-09-30T13:46:40.432511Z","iopub.status.idle":"2025-09-30T13:46:40.442789Z","shell.execute_reply.started":"2025-09-30T13:46:40.432487Z","shell.execute_reply":"2025-09-30T13:46:40.442072Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"CONFIG = Config()\nprint(f\"使用设备：{CONFIG.device}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-30T13:46:42.011543Z","iopub.execute_input":"2025-09-30T13:46:42.01182Z","iopub.status.idle":"2025-09-30T13:46:42.076905Z","shell.execute_reply.started":"2025-09-30T13:46:42.0118Z","shell.execute_reply":"2025-09-30T13:46:42.076007Z"}},"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-09-30T13:46:44.354549Z","iopub.execute_input":"2025-09-30T13:46:44.355017Z","iopub.status.idle":"2025-09-30T13:46:44.359192Z","shell.execute_reply.started":"2025-09-30T13:46:44.354992Z","shell.execute_reply":"2025-09-30T13:46:44.358616Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"set_random_seed()\nprint(f\"随即种子已设定：{CONFIG.seed}\")","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-30T13:46:44.747672Z","iopub.execute_input":"2025-09-30T13:46:44.747917Z","iopub.status.idle":"2025-09-30T13:46:44.759091Z","shell.execute_reply.started":"2025-09-30T13:46:44.747899Z","shell.execute_reply":"2025-09-30T13:46:44.758582Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"def load_and_preprocess_data(config):\n    \n    if not os.path.exists(config.test_csv_path):\n        raise FileNotFoundError(f\"测试文件数据不存在：{config.test_csv_path}\")\n    \n    test_path = config.test_csv_path\n    test_df = pd.read_csv(test_path)\n    print(f\"测试集数据形状：{test_df.shape}\")\n    \n    #config.update_from_dataset(test_df) 不用这一列了因为没有标签\n    x_test = test_df.values  # 转换为numpy数组\n\n    if config.normalize:\n        x_test = x_test.astype(np.float32)/255.0\n        print(\"归一化[0,1]了\")\n\n \n    return x_test,test_df\n","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-30T13:46:45.239685Z","iopub.execute_input":"2025-09-30T13:46:45.240298Z","iopub.status.idle":"2025-09-30T13:46:45.24458Z","shell.execute_reply.started":"2025-09-30T13:46:45.240276Z","shell.execute_reply":"2025-09-30T13:46:45.243912Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"x_test,test_df= load_and_preprocess_data(CONFIG)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-30T13:46:45.68271Z","iopub.execute_input":"2025-09-30T13:46:45.68293Z","iopub.status.idle":"2025-09-30T13:46:48.18084Z","shell.execute_reply.started":"2025-09-30T13:46:45.682914Z","shell.execute_reply":"2025-09-30T13:46:48.180172Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 测试数据集类","metadata":{}},{"cell_type":"code","source":"class TestDataset(Dataset):\n    def __init__(self,features,transform=None):\n        self.features = features\n        self.transform = transform\n\n    def __len__(self):\n        return len(self.features)\n\n    def __getitem__(self,idx): #与训练一致\n        feature = self.features[idx]\n        feature_tensor = torch.tensor(feature,dtype=torch.float32)\n\n        if self.transform:\n            feature_tendor = self.transform(feature_tensor)\n\n        return feature_tensor","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-30T13:49:32.310469Z","iopub.execute_input":"2025-09-30T13:49:32.310822Z","iopub.status.idle":"2025-09-30T13:49:32.317304Z","shell.execute_reply.started":"2025-09-30T13:49:32.310786Z","shell.execute_reply":"2025-09-30T13:49:32.316572Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 测试数据加载器","metadata":{}},{"cell_type":"code","source":"def get_dataloaders(config,test_features):\n    test_dataset = TestDataset(test_features)\n    test_loader = DataLoader(\n        test_dataset,\n        batch_size = config.batch_size,\n        shuffle = False,\n        num_workers = config.num_workers,\n        pin_memory=True\n    )\n    print(f\"测试集加载完成：{len(test_dataset)}样本，{len(test_loader)}批次\")\n    return test_loader","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-30T13:49:34.323415Z","iopub.execute_input":"2025-09-30T13:49:34.324103Z","iopub.status.idle":"2025-09-30T13:49:34.328649Z","shell.execute_reply.started":"2025-09-30T13:49:34.324078Z","shell.execute_reply":"2025-09-30T13:49:34.327881Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"test_loader = get_dataloaders(CONFIG,x_test)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-30T13:49:34.815707Z","iopub.execute_input":"2025-09-30T13:49:34.816354Z","iopub.status.idle":"2025-09-30T13:49:34.821392Z","shell.execute_reply.started":"2025-09-30T13:49:34.816304Z","shell.execute_reply":"2025-09-30T13:49:34.820434Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 模型","metadata":{}},{"cell_type":"code","source":"class MLP(nn.Module):\n    def __init__(self,input_size,hidden_sizes,output_size,dropout_rate = 0.1):\n        super(MLP,self).__init__()\n        self.layers = nn.Sequential(\n            #1\n            nn.Linear(input_size,hidden_sizes[0]),\n            nn.ReLU(inplace=True),\n            nn.Dropout(dropout_rate),\n\n            #2\n            nn.Linear(hidden_sizes[0],hidden_sizes[1]),\n            nn.ReLU(inplace=True),\n            nn.Dropout(dropout_rate),\n\n            #3\n            nn.Linear(hidden_sizes[1],hidden_sizes[2]),  \n            nn.ReLU(),\n            nn.Dropout(0.3),\n            \n            nn.Linear(hidden_sizes[2], output_size)\n        )\n\n    def forward(self,x):\n        return self.layers(x)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-30T13:49:35.568094Z","iopub.execute_input":"2025-09-30T13:49:35.568591Z","iopub.status.idle":"2025-09-30T13:49:35.573849Z","shell.execute_reply.started":"2025-09-30T13:49:35.568571Z","shell.execute_reply":"2025-09-30T13:49:35.573156Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 加载最佳权重","metadata":{}},{"cell_type":"code","source":"def load_best_model(config):\n    model = MLP(input_size = config.input_size,\n               hidden_sizes = [512,256,128],\n               output_size = config.num_classes\n               )\n\n    model.load_state_dict(torch.load(\n        config.model_weight_path,\n        map_location = config.device\n    ))\n\n    model = model.to(config.device)\n    model.eval() #切回评估\n    \n    return model","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-30T13:52:29.246335Z","iopub.execute_input":"2025-09-30T13:52:29.246641Z","iopub.status.idle":"2025-09-30T13:52:29.2517Z","shell.execute_reply.started":"2025-09-30T13:52:29.246616Z","shell.execute_reply":"2025-09-30T13:52:29.251062Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"best_model = load_best_model(CONFIG)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-30T13:52:30.519428Z","iopub.execute_input":"2025-09-30T13:52:30.519708Z","iopub.status.idle":"2025-09-30T13:52:30.541053Z","shell.execute_reply.started":"2025-09-30T13:52:30.519689Z","shell.execute_reply":"2025-09-30T13:52:30.540468Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 推理函数","metadata":{}},{"cell_type":"code","source":"def inference(model,test_loader,device):\n    all_preds = []\n    print(\"推理开始\")\n\n    with torch.no_grad(): #关闭梯度计算加速推理\n        for features in tqdm(test_loader, desc=\"推理中...\"):\n            features = features.to(device)\n            outputs = model(features)\n            preds = torch.argmax(outputs,dim = 1).cpu().numpy() #移动回cpu并转换numpy\n            all_preds.extend(preds)\n        print(\"推理完成\")\n\n    return all_preds","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-30T13:55:10.817867Z","iopub.execute_input":"2025-09-30T13:55:10.818732Z","iopub.status.idle":"2025-09-30T13:55:10.823786Z","shell.execute_reply.started":"2025-09-30T13:55:10.818698Z","shell.execute_reply":"2025-09-30T13:55:10.823139Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"all_preds = inference(best_model,test_loader,CONFIG.device)","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-30T13:55:12.470705Z","iopub.execute_input":"2025-09-30T13:55:12.471442Z","iopub.status.idle":"2025-09-30T13:55:13.732409Z","shell.execute_reply.started":"2025-09-30T13:55:12.471419Z","shell.execute_reply":"2025-09-30T13:55:13.73154Z"}},"outputs":[],"execution_count":null},{"cell_type":"markdown","source":"### 提交","metadata":{}},{"cell_type":"code","source":"sample_submission_csv_path = CONFIG.sample_submission_csv_path\nif not os.path.exists(sample_submission_csv_path):\n    raise FileNotFoundError(f\"未找到提交模板文件：{sample_submission_csv_path}\\n请检查路径是否正确\")\nsample_df = pd.read_csv(sample_submission_csv_path)\nsubmission_df = sample_df.copy()\n\nprint(f\"预测结果数量:（{len(all_preds)}）,模板行数:（{len(submission_df)}）\")\n\nfinal_submit_path = os.path.join(CONFIG.submission_path,\"MLP_submission1\")\nsubmission_df.to_csv(final_submit_path,index = False)\nprint(\"提交文件前5行：\")\nprint(submission_df.head())","metadata":{"trusted":true,"execution":{"iopub.status.busy":"2025-09-30T13:57:55.730401Z","iopub.execute_input":"2025-09-30T13:57:55.731042Z","iopub.status.idle":"2025-09-30T13:57:55.766761Z","shell.execute_reply.started":"2025-09-30T13:57:55.731021Z","shell.execute_reply":"2025-09-30T13:57:55.765839Z"}},"outputs":[],"execution_count":null},{"cell_type":"code","source":"","metadata":{"trusted":true},"outputs":[],"execution_count":null}]}