PyTorch二分类器用nn.BCEWithLogitsLoss后准确率大于1的问题
二分类模型切换为BCEWithLogitsLoss后准确率大于1的异常排查
我将原本使用nn.CrossEntropyLoss()的二分类模型更换为nn.BCEWithLogitsLoss(),调整训练函数适配该损失函数后,模型训练时准确率数值出现大于1的异常情况。以下是相关代码及训练输出,恳请协助排查:
数据处理与训练函数代码
# Data augmentation and normalization for training # Just normalization for validation data_transforms = { 'train': transforms.Compose([ transforms.RandomResizedCrop(224), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]), 'val': transforms.Compose([ transforms.Resize(256), transforms.CenterCrop(224), transforms.ToTensor(), transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]) ]), } data_dir = '/kaggle/input/catsndogsorg/hymenoptera_data' image_datasets = {x: datasets.ImageFolder(os.path.join(data_dir, x), data_transforms[x]) for x in ['train', 'val']} dataloaders = {x: torch.utils.data.DataLoader(image_datasets[x], batch_size=4, shuffle=True, num_workers=4) for x in ['train', 'val']} dataset_sizes = {x: len(image_datasets[x]) for x in ['train', 'val']} class_names = image_datasets['train'].classes device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu") ############# def train_model(model, criterion, optimizer, scheduler, num_epochs=25): since = time.time() best_model_wts = copy.deepcopy(model.state_dict()) best_acc = 0.0 for epoch in range(num_epochs): print(f'Epoch {epoch}/{num_epochs - 1}') print('-' * 10) # Each epoch has a training and validation phase for phase in ['train', 'val']: if phase == 'train': model.train() # Set model to training mode else: model.eval() # Set model to evaluate mode running_loss = 0.0 running_corrects = 0 # Iterate over data. for inputs, labels in dataloaders[phase]: inputs = inputs.to(device) labels = labels.to(device).unsqueeze(1) # zero the parameter gradients optimizer.zero_grad() # forward # track history if only in train with torch.set_grad_enabled(phase == 'train'): outputs = model(inputs) _, preds = torch.max(outputs, 1) #print(outputs, labels) loss = criterion(outputs, labels.float()) print(loss) # backward + optimize only if in training phase if phase == 'train': loss.backward() optimizer.step() # statistics running_loss += loss.item() * inputs.size(0) running_corrects += torch.sum(preds == labels.data) if phase == 'train': scheduler.step() epoch_loss = running_loss / dataset_sizes[phase] epoch_acc = running_corrects.double() / dataset_sizes[phase] print(f'{phase} Loss: {epoch_loss:.4f} Acc: {epoch_acc:.4f}') # deep copy the model if phase == 'val' and epoch_acc > best_acc: best_acc = epoch_acc best_model_wts = copy.deepcopy(model.state_dict()) print() time_elapsed = time.time() - since print(f'Training complete in {time_elapsed // 60:.0f}m {time_elapsed % 60:.0f}s') print(f'Best val Acc: {best_acc:4f}') # load best model weights model.load_state_dict(best_model_wts) return model
模型定义代码
model_ft = models.resnet18(weights='ResNet18_Weights.DEFAULT') num_ftrs = model_ft.fc.in_features # Here the size of each output sample is set to 2. # Alternatively, it can be generalized to nn.Linear(num_ftrs, len(class_names)). model_ft.fc = nn.Linear(num_ftrs, 1) model_ft = model_ft.to(device) criterion = nn.BCEWithLogitsLoss() # Observe that all parameters are being optimized optimizer_ft = optim.SGD(model_ft.parameters(), lr=0.001, momentum=0.9) # Decay LR by a factor of 0.1 every 7 epochs exp_lr_scheduler = lr_scheduler.StepLR(optimizer_ft, step_size=7, gamma=0.1) model_ft = train_model(model_ft, criterion, optimizer_ft, exp_lr_scheduler,num_epochs=25)
训练输出
outputs shape: torch.Size([4, 1]) labels shape: torch.Size([4, 1]) logits: tensor(0.3511,grad_fn<BinaryCrossEntropyWithLogitsBackward0>) train Loss: 1.0000 Acc: 2.0164 val Loss: 1.0000 Acc: 1.8105
问题根源与解决方案
问题分析
- 预测逻辑错误:
torch.max(outputs, 1)是针对多分类(输出维度等于类别数)的逻辑,但切换为BCEWithLogitsLoss后,模型输出为单维度[batch_size,1](代表正类的对数几率),此时torch.max无法正确生成类别预测,反而返回全0的索引张量。 - 维度不匹配导致广播错误:
preds是[batch_size,]形状,而labels被unsqueeze(1)处理为[batch_size,1]形状,两者比较时触发PyTorch广播规则,生成[batch_size,batch_size]的布尔张量,torch.sum会累加所有元素,导致running_corrects数值远大于实际样本数,最终准确率超过1。
修正代码
修改训练函数中的预测与准确率计算逻辑,适配单输出二分类的场景:
def train_model(model, criterion, optimizer, scheduler, num_epochs=25): since = time.time() best_model_wts = copy.deepcopy(model.state_dict()) best_acc = 0.0 for epoch in range(num_epochs): print(f'Epoch {epoch}/{num_epochs - 1}') print('-' * 10) for phase in ['train', 'val']: if phase == 'train': model.train() else: model.eval() running_loss = 0.0 running_corrects = 0 for inputs, labels in dataloaders[phase]: inputs = inputs.to(device) labels = labels.to(device) # 保持原始[batch_size,]形状 optimizer.zero_grad() with torch.set_grad_enabled(phase == 'train'): outputs = model(inputs) # 单输出二分类的预测逻辑:sigmoid后判断是否大于0.5,压缩维度匹配labels preds = (torch.sigmoid(outputs) > 0.5).squeeze() # 损失计算需要labels与outputs形状一致,所以给labels加一维 loss = criterion(outputs, labels.unsqueeze(1).float()) if phase == 'train': loss.backward() optimizer.step() running_loss += loss.item() * inputs.size(0) # 此时preds与labels形状一致,sum统计正确匹配的样本数 running_corrects += torch.sum(preds == labels.data) if phase == 'train': scheduler.step() epoch_loss = running_loss / dataset_sizes[phase] epoch_acc = running_corrects.double() / dataset_sizes[phase] print(f'{phase} Loss: {epoch_loss:.4f} Acc: {epoch_acc:.4f}') if phase == 'val' and epoch_acc > best_acc: best_acc = epoch_acc best_model_wts = copy.deepcopy(model.state_dict()) print() time_elapsed = time.time() - since print(f'Training complete in {time_elapsed // 60:.0f}m {time_elapsed % 60:.0f}s') print(f'Best val Acc: {best_acc:.4f}') model.load_state_dict(best_model_wts) return model
关键说明
BCEWithLogitsLoss要求目标张量labels与模型输出outputs形状一致,因此损失计算时需将labels转为[batch_size,1]的float张量。- 准确率计算时,将
preds压缩为[batch_size,]形状,与原始labels形状匹配,避免广播错误,确保统计的是正确匹配的样本数量。
内容的提问来源于stack exchange,提问作者Alain Michael Janith Schroter
相关产品推荐
相关产品推荐

