第J2周:ResNetV2算法学习

发布时间:2026/7/11 3:53:34
第J2周:ResNetV2算法学习 本文为365天深度学习训练营中的学习记录博客原作者K同学啊一、前期准备import torch import torch.nn as nn import torch.optim as optim import torch.nn.functional as F from torchvision import transforms, datasets from torchsummary import summary import matplotlib.pyplot as plt from datetime import datetime import warnings import copy import random import numpy as np warnings.filterwarnings(ignore) def set_seed(seed42): 固定随机种子使每次运行结果尽量保持一致 random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) if torch.cuda.is_available(): torch.cuda.manual_seed(seed) torch.cuda.manual_seed_all(seed) # 保证卷积等操作结果尽量稳定 torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False set_seed(42) plt.rcParams[figure.dpi] 100 # 设置图片清晰度 plt.rcParams[font.sans-serif] [SimHei] # 设置中文字体 plt.rcParams[axes.unicode_minus] False # 正常显示负号 device torch.device(cuda if torch.cuda.is_available() else cpu) print(当前运行设备, device) if device.type cuda: print(GPU名称, torch.cuda.get_device_name(0)) else: print(当前未检测到GPU程序将在CPU上运行)二、导入数据与划分数据data_dir rD:\Code\J2 train_transforms transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize( mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225] ) ]) total_data datasets.ImageFolder(data_dir, transformtrain_transforms) print(fClasses: {total_data.class_to_idx}) print(fTotal samples: {len(total_data)}) train_size int(0.8 * len(total_data)) test_size len(total_data) - train_size generator torch.Generator().manual_seed(42) train_dataset, test_dataset torch.utils.data.random_split( total_data, [train_size, test_size], generatorgenerator ) batch_size 16 train_dl torch.utils.data.DataLoader( train_dataset, batch_sizebatch_size, shuffleTrue ) test_dl torch.utils.data.DataLoader( test_dataset, batch_sizebatch_size, shuffleFalse ) for X, y in test_dl: print(fBatch shape: {X.shape}, Labels: {y.shape}) break三、定义模型import torch import torch.nn as nn import torch.nn.functional as F class BasicBlockV1(nn.Module): ResNetV1 基本残差块 主分支 Conv → BN → ReLU → Conv → BN 最后 主分支 shortcut → ReLU expansion 1 def __init__(self, in_channels, out_channels, stride1): super(BasicBlockV1, self).__init__() self.conv1 nn.Conv2d( in_channels, out_channels, kernel_size3, stridestride, padding1, biasFalse ) self.bn1 nn.BatchNorm2d(out_channels) self.conv2 nn.Conv2d( out_channels, out_channels, kernel_size3, stride1, padding1, biasFalse ) self.bn2 nn.BatchNorm2d(out_channels) # 输入、输出尺寸相同时直接使用恒等连接 self.shortcut nn.Identity() # 尺寸或通道数不同时使用1×1卷积调整 if stride ! 1 or in_channels ! out_channels: self.shortcut nn.Sequential( nn.Conv2d( in_channels, out_channels, kernel_size1, stridestride, biasFalse ), nn.BatchNorm2d(out_channels) ) def forward(self, x): identity self.shortcut(x) out self.conv1(x) out self.bn1(out) out F.relu(out, inplaceTrue) out self.conv2(out) out self.bn2(out) out out identity out F.relu(out, inplaceTrue) return out class BasicBlockV2(nn.Module): ResNetV2 基本残差块 主分支 BN → ReLU → Conv → BN → ReLU → Conv 最后 主分支 shortcut 注意残差相加后没有ReLU expansion 1 def __init__(self, in_channels, out_channels, stride1): super(BasicBlockV2, self).__init__() self.bn1 nn.BatchNorm2d(in_channels) self.conv1 nn.Conv2d( in_channels, out_channels, kernel_size3, stridestride, padding1, biasFalse ) self.bn2 nn.BatchNorm2d(out_channels) self.conv2 nn.Conv2d( out_channels, out_channels, kernel_size3, stride1, padding1, biasFalse ) self.shortcut None # 只有尺寸或通道数变化时才使用1×1卷积 if stride ! 1 or in_channels ! out_channels: self.shortcut nn.Conv2d( in_channels, out_channels, kernel_size1, stridestride, biasFalse ) def forward(self, x): # 先进行预激活 preactivated F.relu(self.bn1(x), inplaceTrue) # 维度发生变化时对预激活结果进行投影 if self.shortcut is not None: identity self.shortcut(preactivated) else: identity x out self.conv1(preactivated) out self.bn2(out) out F.relu(out, inplaceTrue) out self.conv2(out) # ResNetV2相加之后不再进行ReLU out out identity return out def __init__(self, block, num_blocks, num_classes): super(ResNetV1, self).__init__() self.in_channels 64 # ResNetV1开头Conv → BN → ReLU → MaxPool self.conv1 nn.Conv2d( 3, 64, kernel_size7, stride2, padding3, biasFalse ) self.bn1 nn.BatchNorm2d(64) self.max_pool nn.MaxPool2d( kernel_size3, stride2, padding1 ) self.layer1 self._make_layer( block, 64, num_blocks[0], stride1 ) self.layer2 self._make_layer( block, 128, num_blocks[1], stride2 ) self.layer3 self._make_layer( block, 256, num_blocks[2], stride2 ) self.layer4 self._make_layer( block, 512, num_blocks[3], stride2 ) self.avg_pool nn.AdaptiveAvgPool2d((1, 1)) self.fc nn.Linear( 512 * block.expansion, num_classes ) self._initialize_weights() def _make_layer(self, block, out_channels, num_blocks, stride): layers [] # 每一层的第一个残差块可能需要降采样 layers.append( block( self.in_channels, out_channels, stride ) ) self.in_channels out_channels * block.expansion # 后续残差块不再降采样 for _ in range(1, num_blocks): layers.append( block( self.in_channels, out_channels, stride1 ) ) return nn.Sequential(*layers) def _initialize_weights(self): 初始化网络参数 for module in self.modules(): if isinstance(module, nn.Conv2d): nn.init.kaiming_normal_( module.weight, modefan_out, nonlinearityrelu ) elif isinstance(module, nn.BatchNorm2d): nn.init.constant_(module.weight, 1) nn.init.constant_(module.bias, 0) def forward(self, x): out self.conv1(x) out self.bn1(out) out F.relu(out, inplaceTrue) out self.max_pool(out) out self.layer1(out) out self.layer2(out) out self.layer3(out) out self.layer4(out) out self.avg_pool(out) out torch.flatten(out, 1) out self.fc(out) return out class ResNetV2(nn.Module): def __init__(self, block, num_blocks, num_classes): super(ResNetV2, self).__init__() self.in_channels 64 # ResNetV2开头只有卷积不立即进行BN和ReLU self.conv1 nn.Conv2d( 3, 64, kernel_size7, stride2, padding3, biasFalse ) self.max_pool nn.MaxPool2d( kernel_size3, stride2, padding1 ) self.layer1 self._make_layer( block, 64, num_blocks[0], stride1 ) self.layer2 self._make_layer( block, 128, num_blocks[1], stride2 ) self.layer3 self._make_layer( block, 256, num_blocks[2], stride2 ) self.layer4 self._make_layer( block, 512, num_blocks[3], stride2 ) # ResNetV2最后一个残差块后需要BN和ReLU self.final_bn nn.BatchNorm2d( 512 * block.expansion ) self.avg_pool nn.AdaptiveAvgPool2d((1, 1)) self.fc nn.Linear( 512 * block.expansion, num_classes ) self._initialize_weights() def _make_layer(self, block, out_channels, num_blocks, stride): layers [] layers.append( block( self.in_channels, out_channels, stride ) ) self.in_channels out_channels * block.expansion for _ in range(1, num_blocks): layers.append( block( self.in_channels, out_channels, stride1 ) ) return nn.Sequential(*layers) def _initialize_weights(self): 初始化网络参数 for module in self.modules(): if isinstance(module, nn.Conv2d): nn.init.kaiming_normal_( module.weight, modefan_out, nonlinearityrelu ) elif isinstance(module, nn.BatchNorm2d): nn.init.constant_(module.weight, 1) nn.init.constant_(module.bias, 0) def forward(self, x): # V2第一层卷积后不立即使用BN和ReLU out self.conv1(x) out self.max_pool(out) out self.layer1(out) out self.layer2(out) out self.layer3(out) out self.layer4(out) # V2在所有残差块结束后统一进行BN和ReLU out self.final_bn(out) out F.relu(out, inplaceTrue) out self.avg_pool(out) out torch.flatten(out, 1) out self.fc(out) return out def ResNet18V1(num_classes): return ResNetV1( blockBasicBlockV1, num_blocks[2, 2, 2, 2], num_classesnum_classes ) def ResNet18V2(num_classes): return ResNetV2( blockBasicBlockV2, num_blocks[2, 2, 2, 2], num_classesnum_classes )四、创建模型num_classes len(total_data.classes) model_v1 ResNet18V1(num_classesnum_classes).to(device) model_v2 ResNet18V2(num_classesnum_classes).to(device) models { ResNetV1: model_v1, ResNetV2: model_v2 } for model_name, model in models.items(): model.eval() x torch.randn(1, 3, 224, 224).to(device) with torch.no_grad(): out model(x) print( * 40) print(model_name) print(fInput shape: {x.shape}) print(fOutput shape: {out.shape}) pred torch.argmax(out, dim1) print(fPredicted class index: {pred.item()}) total_params sum(p.numel() for p in model.parameters()) trainable_params sum( p.numel() for p in model.parameters() if p.requires_grad ) print(fTotal params: {total_params:,}) print(fTrainable params: {trainable_params:,}) assert out.shape (1, num_classes), 模型输出形状不正确请检查 num_classes print(f{model_name} 前向传播测试通过)五、编写训练函数和测试函数def train(dataloader, model, loss_fn, optimizer, device): model.train() size len(dataloader.dataset) train_loss 0.0 train_correct 0 for X, y in dataloader: X X.to(device) y y.to(device) pred model(X) loss loss_fn(pred, y) optimizer.zero_grad() loss.backward() optimizer.step() batch_size X.size(0) train_loss loss.item() * batch_size train_correct (pred.argmax(dim1) y).sum().item() avg_loss train_loss / size avg_acc train_correct / size return avg_loss, avg_acc def test(dataloader, model, loss_fn, device): # 切换到评估模式 model.eval() size len(dataloader.dataset) test_loss 0.0 test_correct 0 with torch.no_grad(): for X, y in dataloader: X X.to(device) y y.to(device) pred model(X) loss loss_fn(pred, y) batch_size X.size(0) test_loss loss.item() * batch_size test_correct (pred.argmax(dim1) y).sum().item() avg_loss test_loss / size avg_acc test_correct / size return avg_loss, avg_acc六、定义训练流程def fit_model(model, model_name, train_dl, test_dl, device, epochs20, learning_rate0.001): loss_fn nn.CrossEntropyLoss() optimizer optim.Adam( model.parameters(), lrlearning_rate ) history { train_loss: [], train_acc: [], test_loss: [], test_acc: [] } best_acc 0.0 # 最优模型参数保存在CPU中避免额外占用GPU显存 best_model_state { key: value.detach().cpu().clone() for key, value in model.state_dict().items() } print( * 60) print(f开始训练 {model_name}) print( * 60) for epoch in range(epochs): train_loss, train_acc train( dataloadertrain_dl, modelmodel, loss_fnloss_fn, optimizeroptimizer, devicedevice ) test_loss, test_acc test( dataloadertest_dl, modelmodel, loss_fnloss_fn, devicedevice ) history[train_loss].append(float(train_loss)) history[train_acc].append(float(train_acc)) history[test_loss].append(float(test_loss)) history[test_acc].append(float(test_acc)) # 保存测试准确率最高的一轮模型 if test_acc best_acc: best_acc test_acc # 把最优参数复制到CPU减少GPU显存占用 best_model_state { key: value.detach().cpu().clone() for key, value in model.state_dict().items() } print( fEpoch [{epoch 1:02d}/{epochs}] fTrain Loss: {train_loss:.4f} fTrain ACC: {train_acc * 100:.2f}% fTest Loss: {test_loss:.4f} fTest ACC: {test_acc * 100:.2f}% ) # 将CPU上的最优参数重新加载到模型 model.load_state_dict(best_model_state) print(- * 60) print( f{model_name} 训练完成 f最高 Test ACC{best_acc * 100:.2f}% ) return history七、正式训练import gc import torch epochs 20 learning_rate 0.001 num_classes len(total_data.classes) set_seed(42) model_v1 ResNet18V1( num_classesnum_classes ).to(device) history_v1 fit_model( modelmodel_v1, model_nameResNetV1, train_dltrain_dl, test_dltest_dl, devicedevice, epochsepochs, learning_ratelearning_rate ) # 保存模型 model_v1_state { key: value.detach().cpu() for key, value in model_v1.state_dict().items() } torch.save( model_v1_state, resnetv1_best.pth ) # 立即保存V1训练记录 torch.save( history_v1, history_v1.pth ) print(ResNetV1训练结果已保存。) # 把V1移出GPU并释放内存 model_v1.to(cpu) del model_v1 del model_v1_state gc.collect() if torch.cuda.is_available(): torch.cuda.empty_cache() print(ResNetV1占用的GPU内存已释放。) set_seed(42) model_v2 ResNet18V2( num_classesnum_classes ).to(device) history_v2 fit_model( modelmodel_v2, model_nameResNetV2, train_dltrain_dl, test_dltest_dl, devicedevice, epochsepochs, learning_ratelearning_rate ) # 保存模型 model_v2_state { key: value.detach().cpu() for key, value in model_v2.state_dict().items() } torch.save( model_v2_state, resnetv2_best.pth ) # 保存V2训练记录 torch.save( history_v2, history_v2.pth ) # 保存两个模型的训练记录 torch.save( { history_v1: history_v1, history_v2: history_v2 }, training_history.pth ) print(ResNetV2训练结果已保存。) # 释放V2占用的GPU内存 model_v2.to(cpu) del model_v2 del model_v2_state gc.collect() if torch.cuda.is_available(): torch.cuda.empty_cache() print(ResNetV2占用的GPU内存已释放。) print(两个模型训练结束可以重新启动内核后绘图。)torch.save( { history_v1: history_v1, history_v2: history_v2 }, training_history.pth ) print(训练记录保存成功)八、绘图import os os.environ[MPLBACKEND] Agg import matplotlib matplotlib.use(Agg, forceTrue) import matplotlib.pyplot as plt import torch checkpoint torch.load( training_history.pth, map_locationcpu ) history_v1 checkpoint[history_v1] history_v2 checkpoint[history_v2] print(训练记录读取成功) print(V1训练轮数, len(history_v1[test_acc])) print(V2训练轮数, len(history_v2[test_acc]))from pathlib import Path from datetime import datetime import csv import torch from PIL import Image, ImageDraw, ImageFont history_path Path(training_history.pth) current_time datetime.now() display_time current_time.strftime(%Y-%m-%d %H:%M:%S) file_time current_time.strftime(%Y%m%d_%H%M%S) if not history_path.exists(): raise FileNotFoundError( 没有找到 training_history.pth。\n 请确认训练记录文件与当前代码位于同一个文件夹中。 ) try: checkpoint torch.load( history_path, map_locationcpu, weights_onlyFalse ) except TypeError: checkpoint torch.load( history_path, map_locationcpu ) if history_v1 not in checkpoint: raise KeyError(training_history.pth 中没有 history_v1。) if history_v2 not in checkpoint: raise KeyError(training_history.pth 中没有 history_v2。) history_v1 checkpoint[history_v1] history_v2 checkpoint[history_v2] def to_float_list(values): result [] for value in values: if torch.is_tensor(value): value value.detach().cpu().item() result.append(float(value)) return result if test_acc not in history_v1: raise KeyError(history_v1 中没有 test_acc。) if test_acc not in history_v2: raise KeyError(history_v2 中没有 test_acc。) acc_v1 to_float_list(history_v1[test_acc]) acc_v2 to_float_list(history_v2[test_acc]) if len(acc_v1) 0: raise ValueError(ResNetV1没有测试准确率数据。) if len(acc_v2) 0: raise ValueError(ResNetV2没有测试准确率数据。) print(训练记录读取成功。) print(ResNetV1训练轮数, len(acc_v1)) print(ResNetV2训练轮数, len(acc_v2)) print(打卡时间, display_time) output_dir Path(training_results) output_dir.mkdir(parentsTrue, exist_okTrue) csv_path output_dir / fresnet_acc_result_{file_time}.csv max_epochs max(len(acc_v1), len(acc_v2)) with open( csv_path, w, newline, encodingutf-8-sig ) as file: writer csv.writer(file) writer.writerow([ Epoch, ResNetV1 Test ACC, ResNetV2 Test ACC ]) for index in range(max_epochs): if index len(acc_v1): v1_value acc_v1[index] else: v1_value if index len(acc_v2): v2_value acc_v2[index] else: v2_value writer.writerow([ index 1, v1_value, v2_value ]) print(ACC数据已保存, csv_path.resolve()) image_width 1100 image_height 700 left_margin 110 right_margin 60 top_margin 130 bottom_margin 110 plot_width image_width - left_margin - right_margin plot_height image_height - top_margin - bottom_margin x_axis_y image_height - bottom_margin y_axis_x left_margin image Image.new( RGB, (image_width, image_height), white ) draw ImageDraw.Draw(image) def load_font(size): possible_font_paths [ rC:\Windows\Fonts\arial.ttf, rC:\Windows\Fonts\calibri.ttf, rC:\Windows\Fonts\tahoma.ttf ] for font_path in possible_font_paths: if Path(font_path).exists(): return ImageFont.truetype(font_path, size) return ImageFont.load_default() title_font load_font(26) time_font load_font(18) label_font load_font(16) small_font load_font(14) def draw_centered_text(draw_object, text, y, font, fillblack): text_box draw_object.textbbox( (0, 0), text, fontfont ) text_width text_box[2] - text_box[0] x (image_width - text_width) // 2 draw_object.text( (x, y), text, fontfont, fillfill ) draw_centered_text( draw, ResNetV1 vs ResNetV2 Fracture Recognition, 25, title_font ) draw_centered_text( draw, Test Accuracy Comparison, 58, title_font ) draw_centered_text( draw, fCheck-in Time: {display_time}, 95, time_font ) draw.line( ( y_axis_x, top_margin, y_axis_x, x_axis_y ), fillblack, width3 ) draw.line( ( y_axis_x, x_axis_y, image_width - right_margin, x_axis_y ), fillblack, width3 ) for index in range(11): accuracy index * 0.1 y x_axis_y - int( accuracy * plot_height ) draw.line( ( left_margin, y, image_width - right_margin, y ), fill(220, 220, 220), width1 ) label f{accuracy:.1f} draw.text( (55, y - 8), label, fontsmall_font, fillblack ) epoch_count max_epochs # 最多显示约10个横坐标刻度避免文字重叠 tick_interval max(1, epoch_count // 10) for epoch_index in range(epoch_count): if epoch_count 1: x left_margin else: x left_margin int( epoch_index / (epoch_count - 1) * plot_width ) should_draw_tick ( epoch_index 0 or epoch_index epoch_count - 1 or (epoch_index 1) % tick_interval 0 ) if should_draw_tick: draw.line( ( x, x_axis_y, x, x_axis_y 7 ), fillblack, width2 ) epoch_text str(epoch_index 1) text_box draw.textbbox( (0, 0), epoch_text, fontsmall_font ) text_width text_box[2] - text_box[0] draw.text( ( x - text_width // 2, x_axis_y 12 ), epoch_text, fontsmall_font, fillblack ) def get_points(values): points [] for index, accuracy in enumerate(values): if len(values) 1: x left_margin else: x left_margin int( index / (len(values) - 1) * plot_width ) # 防止异常数值超出01范围 accuracy max( 0.0, min(1.0, accuracy) ) y x_axis_y - int( accuracy * plot_height ) points.append((x, y)) return points points_v1 get_points(acc_v1) points_v2 get_points(acc_v2) v1_color (40, 100, 200) v2_color (220, 70, 70) if len(points_v1) 2: draw.line( points_v1, fillv1_color, width4 ) if len(points_v2) 2: draw.line( points_v2, fillv2_color, width4 ) # 绘制每一个Epoch的数据点 for x, y in points_v1: draw.ellipse( ( x - 4, y - 4, x 4, y 4 ), fillv1_color ) for x, y in points_v2: draw.ellipse( ( x - 4, y - 4, x 4, y 4 ), fillv2_color ) epoch_label Epoch epoch_box draw.textbbox( (0, 0), epoch_label, fontlabel_font ) epoch_label_width epoch_box[2] - epoch_box[0] draw.text( ( left_margin plot_width // 2 - epoch_label_width // 2, image_height - 65 ), epoch_label, fontlabel_font, fillblack ) draw.text( ( 15, top_margin - 30 ), Accuracy, fontlabel_font, fillblack ) legend_y image_height - 32 draw.line( ( 310, legend_y, 360, legend_y ), fillv1_color, width5 ) draw.text( ( 370, legend_y - 9 ), ResNetV1 Test ACC, fontsmall_font, fillblack ) draw.line( ( 610, legend_y, 660, legend_y ), fillv2_color, width5 ) draw.text( ( 670, legend_y - 9 ), ResNetV2 Test ACC, fontsmall_font, fillblack ) best_acc_v1 max(acc_v1) best_acc_v2 max(acc_v2) best_epoch_v1 acc_v1.index(best_acc_v1) 1 best_epoch_v2 acc_v2.index(best_acc_v2) 1 acc_gap best_acc_v2 - best_acc_v1 result_text ( fBest ACC: V1{best_acc_v1 * 100:.2f}% f(Epoch {best_epoch_v1}) fV2{best_acc_v2 * 100:.2f}% f(Epoch {best_epoch_v2}) fGap(V2-V1){acc_gap * 100:.2f}% ) draw_centered_text( draw, result_text, image_height - 92, small_font ) image_path ( output_dir / fresnet_acc_compare_{file_time}.png ) image.save(image_path) print(带打卡时间的ACC对比图已保存) print(image_path.resolve()) print() print( * 65) print(ResNetV1与ResNetV2骨折识别结果对比) print( * 65) print( fResNetV1最高Test ACC f{best_acc_v1 * 100:.2f}% f第{best_epoch_v1}轮 ) print( fResNetV2最高Test ACC f{best_acc_v2 * 100:.2f}% f第{best_epoch_v2}轮 ) print( fACC差距V2-V1 f{acc_gap * 100:.2f}个百分点 ) print(打卡时间, display_time) if acc_gap 0: print(结论ResNetV2的最高测试准确率高于ResNetV1。) elif acc_gap 0: print(结论ResNetV1的最高测试准确率高于ResNetV2。) else: print(结论两个模型的最高测试准确率相同。) print( * 65)九、个人总结1. ResNet基本思想ResNet通过残差连接解决深层神经网络难训练的问题。它不直接学习完整映射而是学习残差最终输出为F(x)x其中F(x)表示卷积分支学习到的特征x表示通过快捷连接直接传递的输入。残差连接可以缓解梯度消失和网络退化问题。2.ResNetV1结构ResNetV1采用后激活结构基本顺序为Conv → BN → ReLU → Conv → BN → 相加 → ReLU其主要特点是主分支与快捷分支相加后还要再进行一次ReLU激活。3.ResNetV2结构ResNetV2采用预激活结构基本顺序为BN → ReLU → Conv → BN → ReLU → Conv → 相加其主要特点是 先进行BN和ReLU再进行卷积残差相加后不再进行ReLU信息和梯度传播更加顺畅。4. ResNetV1与ResNetV2的区别ResNetV1采用Conv-BN-ReLU结构ResNetV2采用BN-ReLU-Conv结构ResNetV1在残差相加后还有ReLUResNetV2在残差相加后直接输出ResNetV1在较浅网络中表现较稳定ResNetV2更适合较深网络。5. 流程使用ImageFolder读取骨折图像数据集将图像统一调整为224×224将图像转为张量并进行归一化按照80%和20%划分训练集和测试集分别建立ResNetV1和ResNetV2模型分别训练两个模型计算两个模型的测试准确率对比两者的ACC差距。6. 公平对比条件为了保证实验公平两个模型应保持以下条件一致使用相同的数据集使用相同的数据划分使用相同的训练轮数使用相同的学习率使用相同的优化器使用相同的batch size。7. ACC计算方法ACC表示分类准确率计算方法为预测正确的样本数 ÷ 测试集总样本数ACC越高说明模型整体分类正确率越高。8. 为什么ResNetV1可能比ResNetV2高可能原因包括本次使用的是较浅的ResNet18数据集规模较小只进行了一次随机划分没有进行较丰富的数据增强当前训练参数可能更适合ResNetV1batch size较小可能影响BatchNorm效果。因此不能因为ResNetV2理论上更先进就认为它的ACC一定更高。9. 实验结论在相同数据集和训练条件下本次实验中ResNetV1的测试准确率高于ResNetV2。这只能说明ResNetV1在当前数据集和参数设置下表现更好不能说明ResNetV1始终优于ResNetV2。10. 改进思路后续可以从以下方面继续改进增加数据增强调整学习率增加训练轮数使用学习率调度器使用预训练模型使用多个随机种子重复实验使用更深的ResNet网络进行比较。11.matplotlib 一直崩溃改用了pillow