news 2026/7/11 3:53:32

第J2周:ResNetV2算法学习

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
第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(seed=42): """ 固定随机种子,使每次运行结果尽量保持一致 """ 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 = r"D:\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, transform=train_transforms) print(f"Classes: {total_data.class_to_idx}") print(f"Total 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], generator=generator ) batch_size = 16 train_dl = torch.utils.data.DataLoader( train_dataset, batch_size=batch_size, shuffle=True ) test_dl = torch.utils.data.DataLoader( test_dataset, batch_size=batch_size, shuffle=False ) for X, y in test_dl: print(f"Batch 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, stride=1): super(BasicBlockV1, self).__init__() self.conv1 = nn.Conv2d( in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False ) self.bn1 = nn.BatchNorm2d(out_channels) self.conv2 = nn.Conv2d( out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False ) 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_size=1, stride=stride, bias=False ), nn.BatchNorm2d(out_channels) ) def forward(self, x): identity = self.shortcut(x) out = self.conv1(x) out = self.bn1(out) out = F.relu(out, inplace=True) out = self.conv2(out) out = self.bn2(out) out = out + identity out = F.relu(out, inplace=True) return out class BasicBlockV2(nn.Module): """ ResNetV2 基本残差块 主分支: BN → ReLU → Conv → BN → ReLU → Conv 最后: 主分支 + shortcut 注意:残差相加后没有ReLU """ expansion = 1 def __init__(self, in_channels, out_channels, stride=1): super(BasicBlockV2, self).__init__() self.bn1 = nn.BatchNorm2d(in_channels) self.conv1 = nn.Conv2d( in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False ) self.bn2 = nn.BatchNorm2d(out_channels) self.conv2 = nn.Conv2d( out_channels, out_channels, kernel_size=3, stride=1, padding=1, bias=False ) self.shortcut = None # 只有尺寸或通道数变化时才使用1×1卷积 if stride != 1 or in_channels != out_channels: self.shortcut = nn.Conv2d( in_channels, out_channels, kernel_size=1, stride=stride, bias=False ) def forward(self, x): # 先进行预激活 preactivated = F.relu(self.bn1(x), inplace=True) # 维度发生变化时,对预激活结果进行投影 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, inplace=True) 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_size=7, stride=2, padding=3, bias=False ) self.bn1 = nn.BatchNorm2d(64) self.max_pool = nn.MaxPool2d( kernel_size=3, stride=2, padding=1 ) self.layer1 = self._make_layer( block, 64, num_blocks[0], stride=1 ) self.layer2 = self._make_layer( block, 128, num_blocks[1], stride=2 ) self.layer3 = self._make_layer( block, 256, num_blocks[2], stride=2 ) self.layer4 = self._make_layer( block, 512, num_blocks[3], stride=2 ) 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, stride=1 ) ) return nn.Sequential(*layers) def _initialize_weights(self): """ 初始化网络参数 """ for module in self.modules(): if isinstance(module, nn.Conv2d): nn.init.kaiming_normal_( module.weight, mode="fan_out", nonlinearity="relu" ) 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, inplace=True) 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_size=7, stride=2, padding=3, bias=False ) self.max_pool = nn.MaxPool2d( kernel_size=3, stride=2, padding=1 ) self.layer1 = self._make_layer( block, 64, num_blocks[0], stride=1 ) self.layer2 = self._make_layer( block, 128, num_blocks[1], stride=2 ) self.layer3 = self._make_layer( block, 256, num_blocks[2], stride=2 ) self.layer4 = self._make_layer( block, 512, num_blocks[3], stride=2 ) # 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, stride=1 ) ) return nn.Sequential(*layers) def _initialize_weights(self): """ 初始化网络参数 """ for module in self.modules(): if isinstance(module, nn.Conv2d): nn.init.kaiming_normal_( module.weight, mode="fan_out", nonlinearity="relu" ) 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, inplace=True) out = self.avg_pool(out) out = torch.flatten(out, 1) out = self.fc(out) return out def ResNet18V1(num_classes): return ResNetV1( block=BasicBlockV1, num_blocks=[2, 2, 2, 2], num_classes=num_classes ) def ResNet18V2(num_classes): return ResNetV2( block=BasicBlockV2, num_blocks=[2, 2, 2, 2], num_classes=num_classes )
四、创建模型
num_classes = len(total_data.classes) model_v1 = ResNet18V1(num_classes=num_classes).to(device) model_v2 = ResNet18V2(num_classes=num_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(f"Input shape: {x.shape}") print(f"Output shape: {out.shape}") pred = torch.argmax(out, dim=1) print(f"Predicted 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(f"Total params: {total_params:,}") print(f"Trainable 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(dim=1) == 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(dim=1) == 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, epochs=20, learning_rate=0.001): loss_fn = nn.CrossEntropyLoss() optimizer = optim.Adam( model.parameters(), lr=learning_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( dataloader=train_dl, model=model, loss_fn=loss_fn, optimizer=optimizer, device=device ) test_loss, test_acc = test( dataloader=test_dl, model=model, loss_fn=loss_fn, device=device ) 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( f"Epoch [{epoch + 1:02d}/{epochs}] " f"Train Loss: {train_loss:.4f} " f"Train ACC: {train_acc * 100:.2f}% " f"Test Loss: {test_loss:.4f} " f"Test 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_classes=num_classes ).to(device) history_v1 = fit_model( model=model_v1, model_name="ResNetV1", train_dl=train_dl, test_dl=test_dl, device=device, epochs=epochs, learning_rate=learning_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_classes=num_classes ).to(device) history_v2 = fit_model( model=model_v2, model_name="ResNetV2", train_dl=train_dl, test_dl=test_dl, device=device, epochs=epochs, learning_rate=learning_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", force=True) import matplotlib.pyplot as plt import torch checkpoint = torch.load( "training_history.pth", map_location="cpu" ) 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_location="cpu", weights_only=False ) except TypeError: checkpoint = torch.load( history_path, map_location="cpu" ) 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(parents=True, exist_ok=True) csv_path = output_dir / f"resnet_acc_result_{file_time}.csv" max_epochs = max(len(acc_v1), len(acc_v2)) with open( csv_path, "w", newline="", encoding="utf-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 = [ r"C:\Windows\Fonts\arial.ttf", r"C:\Windows\Fonts\calibri.ttf", r"C:\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, fill="black"): text_box = draw_object.textbbox( (0, 0), text, font=font ) text_width = text_box[2] - text_box[0] x = (image_width - text_width) // 2 draw_object.text( (x, y), text, font=font, fill=fill ) 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, f"Check-in Time: {display_time}", 95, time_font ) draw.line( ( y_axis_x, top_margin, y_axis_x, x_axis_y ), fill="black", width=3 ) draw.line( ( y_axis_x, x_axis_y, image_width - right_margin, x_axis_y ), fill="black", width=3 ) 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), width=1 ) label = f"{accuracy:.1f}" draw.text( (55, y - 8), label, font=small_font, fill="black" ) 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 ), fill="black", width=2 ) epoch_text = str(epoch_index + 1) text_box = draw.textbbox( (0, 0), epoch_text, font=small_font ) text_width = text_box[2] - text_box[0] draw.text( ( x - text_width // 2, x_axis_y + 12 ), epoch_text, font=small_font, fill="black" ) 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 ) # 防止异常数值超出0~1范围 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, fill=v1_color, width=4 ) if len(points_v2) >= 2: draw.line( points_v2, fill=v2_color, width=4 ) # 绘制每一个Epoch的数据点 for x, y in points_v1: draw.ellipse( ( x - 4, y - 4, x + 4, y + 4 ), fill=v1_color ) for x, y in points_v2: draw.ellipse( ( x - 4, y - 4, x + 4, y + 4 ), fill=v2_color ) epoch_label = "Epoch" epoch_box = draw.textbbox( (0, 0), epoch_label, font=label_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, font=label_font, fill="black" ) draw.text( ( 15, top_margin - 30 ), "Accuracy", font=label_font, fill="black" ) legend_y = image_height - 32 draw.line( ( 310, legend_y, 360, legend_y ), fill=v1_color, width=5 ) draw.text( ( 370, legend_y - 9 ), "ResNetV1 Test ACC", font=small_font, fill="black" ) draw.line( ( 610, legend_y, 660, legend_y ), fill=v2_color, width=5 ) draw.text( ( 670, legend_y - 9 ), "ResNetV2 Test ACC", font=small_font, fill="black" ) 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 = ( f"Best ACC: V1={best_acc_v1 * 100:.2f}% " f"(Epoch {best_epoch_v1}) " f"V2={best_acc_v2 * 100:.2f}% " f"(Epoch {best_epoch_v2}) " f"Gap(V2-V1)={acc_gap * 100:.2f}%" ) draw_centered_text( draw, result_text, image_height - 92, small_font ) image_path = ( output_dir / f"resnet_acc_compare_{file_time}.png" ) image.save(image_path) print("带打卡时间的ACC对比图已保存:") print(image_path.resolve()) print() print("=" * 65) print("ResNetV1与ResNetV2骨折识别结果对比") print("=" * 65) print( f"ResNetV1最高Test ACC:" f"{best_acc_v1 * 100:.2f}%" f"(第{best_epoch_v1}轮)" ) print( f"ResNetV2最高Test ACC:" f"{best_acc_v2 * 100:.2f}%" f"(第{best_epoch_v2}轮)" ) print( f"ACC差距(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在残差相加后还有ReLU;ResNetV2在残差相加后直接输出;
ResNetV1在较浅网络中表现较稳定;ResNetV2更适合较深网络。

5. 流程

使用ImageFolder读取骨折图像数据集;将图像统一调整为224×224;将图像转为张量并进行归一化;按照80%和20%划分训练集和测试集;分别建立ResNetV1和ResNetV2模型;分别训练两个模型;计算两个模型的测试准确率;对比两者的ACC差距。

6. 公平对比条件

为了保证实验公平,两个模型应保持以下条件一致:使用相同的数据集;使用相同的数据划分;使用相同的训练轮数;使用相同的学习率;使用相同的优化器;使用相同的batch size。

7. ACC计算方法

ACC表示分类准确率,计算方法为:预测正确的样本数 ÷ 测试集总样本数

ACC越高,说明模型整体分类正确率越高。

8. 为什么ResNetV1可能比ResNetV2高

可能原因包括:本次使用的是较浅的ResNet18;数据集规模较小;只进行了一次随机划分;没有进行较丰富的数据增强;当前训练参数可能更适合ResNetV1;batch size较小,可能影响BatchNorm效果。

因此,不能因为ResNetV2理论上更先进,就认为它的ACC一定更高。

9. 实验结论

在相同数据集和训练条件下,本次实验中ResNetV1的测试准确率高于ResNetV2。这只能说明ResNetV1在当前数据集和参数设置下表现更好,不能说明ResNetV1始终优于ResNetV2。

10. 改进思路

后续可以从以下方面继续改进:增加数据增强;调整学习率;增加训练轮数;使用学习率调度器;使用预训练模型;使用多个随机种子重复实验;使用更深的ResNet网络进行比较。

11.matplotlib 一直崩溃,改用了pillow

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/11 3:52:46

AI信任不是性能问题,而是人机博弈的动态校准

1. 项目概述&#xff1a;当人把鼠标交给AI时&#xff0c;到底在交出什么&#xff1f;“马里兰大学揭秘人类与AI协作中的信任博弈”——这个标题一出来&#xff0c;我立刻放下手头三个正在跑的模型训练任务&#xff0c;点开论文原文和配套实验视频。不是因为马里兰大学有多神秘&…

作者头像 李华
网站建设 2026/7/11 3:52:44

Token经济爆发,国产AI算力越过“好用”分水岭

截至2026年3月&#xff0c;我国日均Token调用量已突破140万亿&#xff0c;两年增长超千倍。Token经济的指数级增长&#xff0c;背后是一个大趋势&#xff1a;各行各业对AI的应用已经从对话、内容生成&#xff0c;全面升级为智能体和复杂任务协同。 在此背景下&#xff0c;产业界…

作者头像 李华
网站建设 2026/7/11 3:48:01

ZYNQ 7000 动态局部重构实战:PCAP与ICAP接口对比与19MB/s配置实测

ZYNQ 7000 动态局部重构实战&#xff1a;PCAP与ICAP接口深度对比与19MB/s配置优化指南在当今嵌入式系统设计中&#xff0c;动态局部重构&#xff08;Dynamic Partial Reconfiguration&#xff0c;DPR&#xff09;技术正逐渐成为提升FPGA灵活性和资源利用率的关键手段。作为Xili…

作者头像 李华
网站建设 2026/7/11 3:47:59

深度拆解MFi伪认证四大套路!企业与采购必避坑

在苹果配件行业高速发展的同时&#xff0c;MFi伪认证、假认证乱象层出不穷。大量作坊工厂、不良商家通过造假手段&#xff0c;冒充正规MFi认证产品&#xff0c;扰乱市场秩序。对于生产企业、采购商家、渠道经销商而言&#xff0c;误用、售卖伪认证产品&#xff0c;不仅会面临产…

作者头像 李华
网站建设 2026/7/11 3:47:42

2026 商城小程序开发:功能、价格、维护成本对比

商城小程序开发最容易判断失真的地方&#xff0c;不是功能写得不够多&#xff0c;而是很多方案在前期看起来都能做&#xff0c;到了后续维护阶段差异才真正显出来。功能、价格和维护成本如果分开看&#xff0c;选型很容易偏。对 2026 年准备做商城小程序的商家来说&#xff0c;…

作者头像 李华