Day 17 | 110. Balanced Binary Tree | 257. Binary Tree Paths | 404. Sum of Left Leaves

Day 1 | 704. Binary Search | 27. Remove Element | 35. Search Insert Position | 34. First and Last Position of Element in Sorted Array
Day 2 | 977. Squares of a Sorted Array | 209. Minimum Size Subarray Sum | 59. Spiral Matrix II
Day 3 | 203. Remove Linked List Elements | 707. Design Linked List | 206. Reverse Linked List
Day 4 | 24. Swap Nodes in Pairs| 19. Remove Nth Node From End of List| 160.Intersection of Two Lists
Day 6 | 242. Valid Anagram | 349. Intersection of Two Arrays | 202. Happy Numbe | 1. Two Sum
Day 7 | 454. 4Sum II | 383. Ransom Note | 15. 3Sum | 18. 4Sum
Day 8 | 344. Reverse String | 541. Reverse String II | 替换空格 | 151.Reverse Words in a String | 左旋转字符串
Day 9 | 28. Find the Index of the First Occurrence in a String | 459. Repeated Substring Pattern
Day 10 | 232. Implement Queue using Stacks | 225. Implement Stack using Queue
Day 11 | 20. Valid Parentheses | 1047. Remove All Adjacent Duplicates In String | 150. Evaluate RPN
Day 13 | 239. Sliding Window Maximum | 347. Top K Frequent Elements
Day 14 | 144.Binary Tree Preorder Traversal | 94.Binary Tree Inorder Traversal| 145.Binary Tree Postorder Traversal
Day 15 | 102. Binary Tree Level Order Traversal | 226. Invert Binary Tree | 101. Symmetric Tree
Day 16 | 104.MaximumDepth of BinaryTree| 111.MinimumDepth of BinaryTree| 222.CountComplete TreeNodes


LeetCode 110. Balanced Binary Tree

Question Link

class Solution {
    public boolean isBalanced(TreeNode root) {
        return getHeight(root) != -1;
    }

    int getHeight(TreeNode node){
        if(node == null) 
            return 0;
        int leftHeight = getHeight(node.left);
        if(leftHeight == -1)
            return -1;
        int rightHeight = getHeight(node.right);
        if(rightHeight == -1)
            return -1;
        if(Math.abs(leftHeight-rightHeight) > 1)
            return -1;

        return Math.max(leftHeight, rightHeight) + 1;
    }
}

LeetCode 257. Binary Tree Paths

Question Link

class Solution {
    public List<String> binaryTreePaths(TreeNode root) {
        List<String> res = new ArrayList<>();
        if(root == null)
            return res;
        List<Integer> paths = new ArrayList<>();
        traversal(root, paths, res);  
        return res;  
    }

    void traversal(TreeNode node, List<Integer> paths, List<String> res){
        paths.add(node.val);
        if(node.left == null && node.right == null){
            StringBuilder sb = new StringBuilder();
            for(int i = 0; i < paths.size()-1; i++)
                sb.append(paths.get(i)).append("->");
            sb.append(paths.get(paths.size()-1));
            res.add(sb.toString());
            return;
        }           
        if(node.left != null){
            traversal(node.left, paths, res);
            paths.remove(paths.size() - 1);
        }
        if(node.right != null){
            traversal(node.right, paths, res);
            paths.remove(paths.size() - 1);
        }
    }
}

LeetCode 404. Sum of Left Leaves

Question Link

class Solution {
    public int sumOfLeftLeaves(TreeNode root) {
        if(root == null)
            return 0;    
        return getSum(root);       
    }

    int getSum(TreeNode node){
        if(node == null) return 0;
        if(node.left == null && node.right == null) return 0;

        int leftValue = getSum(node.left);
        int rightValue = getSum(node.right);
        if(node.left!=null && node.left.left == null && node.left.right == null)
            leftValue = node.left.val;
        return leftValue + rightValue;         
    }
}
import os import random import numpy as np import matplotlib.pyplot as plt from PIL import Image import torch import torch.nn as nn import torch.optim as optim from torch.utils.data import Dataset, DataLoader from torchvision import transforms import torch.nn.functional as F from torchsummary import summary from sklearn.metrics import classification_report, confusion_matrix, precision_recall_fscore_support import seaborn as sns import cv2 # 新增:用于图像分析 from collections import defaultdict, Counter # 设置随机种子确保结果可复现 def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed(seed) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False set_seed() # 数据加载模块,修改__getitem__返回图像路径 class DigitDataset(Dataset): def __init__(self, root_dir, transform=None, is_test=False): self.root_dir = root_dir self.transform = transform self.is_test = is_test self.classes = sorted(os.listdir(root_dir)) self.samples = [] # 构建样本列表 for class_idx, class_name in enumerate(self.classes): class_dir = os.path.join(root_dir, class_name) if not os.path.isdir(class_dir): continue for img_name in os.listdir(class_dir): if img_name.endswith('.png'): img_path = os.path.join(class_dir, img_name) self.samples.append((img_path, class_idx)) # 处理类别不平衡 if not is_test: self._balance_classes() def _balance_classes(self): # 统计各类样本数量 class_counts = [0] * len(self.classes) for _, class_idx in self.samples: class_counts[class_idx] += 1 # 找出最大样本数 max_count = max(class_counts) # 复制少数类样本 balanced_samples = [] for class_idx in range(len(self.classes)): class_samples = [s for s in self.samples if s[1] == class_idx] # 复制样本直到达到最大数量 while len(class_samples) < max_count: class_samples.extend( random.sample(class_samples, min(len(class_samples), max_count - len(class_samples)))) balanced_samples.extend(class_samples[:max_count]) # 打乱样本顺序 random.shuffle(balanced_samples) self.samples = balanced_samples def __len__(self): return len(self.samples) def __getitem__(self, idx): img_path, label = self.samples[idx] image = Image.open(img_path).convert('L') # 转为灰度图 if self.transform: image = self.transform(image) return image, label, img_path # 新增:返回图像路径用于错误样本可视化 # 数据预处理 train_transform = transforms.Compose([ transforms.RandomRotation(10), # 随机旋转 transforms.RandomAffine(degrees=0, translate=(0.1, 0.1)), # 随机平移 transforms.ToTensor(), # 转为张量并归一化到[0,1] transforms.Normalize((0.5,), (0.5,)) # 标准化 ]) test_transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.5,), (0.5,)) ]) # 加载数据集 enhanced_dir = "增强后样本" original_dir = "测试样本" # 创建数据集 full_dataset = DigitDataset(enhanced_dir, transform=train_transform) # 划分训练集和验证集 train_size = int(0.8 * len(full_dataset)) val_size = len(full_dataset) - train_size train_dataset, val_dataset = torch.utils.data.random_split(full_dataset, [train_size, val_size]) # 创建测试集 test_dataset = DigitDataset(original_dir, transform=test_transform, is_test=True) # 创建数据加载器 batch_size = 64 train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True, num_workers=4) val_loader = DataLoader(val_dataset, batch_size=batch_size, shuffle=False, num_workers=4) test_loader = DataLoader(test_dataset, batch_size=batch_size, shuffle=False, num_workers=4) # 定义ResNet轻量化版本模型 class BasicBlock(nn.Module): expansion = 1 def __init__(self, in_channels, out_channels, stride=1): super(BasicBlock, 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.relu = nn.ReLU(inplace=True) 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.Sequential() if stride != 1 or in_channels != self.expansion * out_channels: self.shortcut = nn.Sequential( nn.Conv2d(in_channels, self.expansion * out_channels, kernel_size=1, stride=stride, bias=False), nn.BatchNorm2d(self.expansion * out_channels) ) def forward(self, x): out = self.relu(self.bn1(self.conv1(x))) out = self.bn2(self.conv2(out)) out += self.shortcut(x) out = self.relu(out) return out class LightResNet(nn.Module): def __init__(self, block=BasicBlock, num_blocks=[2, 2], num_classes=10): super(LightResNet, self).__init__() self.in_channels = 16 self.conv1 = nn.Conv2d(1, 16, kernel_size=3, stride=1, padding=1, bias=False) self.bn1 = nn.BatchNorm2d(16) self.relu = nn.ReLU(inplace=True) self.layer1 = self._make_layer(block, 16, num_blocks[0], stride=1) self.layer2 = self._make_layer(block, 32, num_blocks[1], stride=2) self.avg_pool = nn.AdaptiveAvgPool2d((7, 7)) self.fc1 = nn.Linear(32 * block.expansion * 7 * 7, 128) self.dropout = nn.Dropout(0.5) self.fc2 = nn.Linear(128, num_classes) def _make_layer(self, block, out_channels, num_blocks, stride): strides = [stride] + [1] * (num_blocks - 1) layers = [] for stride in strides: layers.append(block(self.in_channels, out_channels, stride)) self.in_channels = out_channels * block.expansion return nn.Sequential(*layers) def forward(self, x): out = self.relu(self.bn1(self.conv1(x))) out = self.layer1(out) out = self.layer2(out) out = self.avg_pool(out) out = out.view(out.size(0), -1) out = self.dropout(self.fc1(out)) out = self.fc2(out) return out # 训练函数 def train(model, train_loader, criterion, optimizer, epoch, device): model.train() running_loss = 0.0 correct = 0 total = 0 for batch_idx, (inputs, targets, _) in enumerate(train_loader): inputs, targets = inputs.to(device), targets.to(device) optimizer.zero_grad() outputs = model(inputs) loss = criterion(outputs, targets) loss.backward() optimizer.step() running_loss += loss.item() _, predicted = outputs.max(1) total += targets.size(0) correct += predicted.eq(targets).sum().item() if (batch_idx + 1) % 50 == 0: print(f'Epoch: {epoch + 1}, Batch: {batch_idx + 1}/{len(train_loader)}, ' f'Loss: {running_loss / (batch_idx + 1):.4f}, ' f'Train Acc: {100. * correct / total:.2f}%') return running_loss / len(train_loader), 100. * correct / total # 验证函数 def validate(model, val_loader, criterion, device): model.eval() running_loss = 0.0 correct = 0 total = 0 with torch.no_grad(): for inputs, targets, _ in val_loader: inputs, targets = inputs.to(device), targets.to(device) outputs = model(inputs) loss = criterion(outputs, targets) running_loss += loss.item() _, predicted = outputs.max(1) total += targets.size(0) correct += predicted.eq(targets).sum().item() val_loss = running_loss / len(val_loader) val_acc = 100. * correct / total print(f'Val Loss: {val_loss:.4f}, Val Acc: {val_acc:.2f}%') return val_loss, val_acc # 测试函数(增强版:返回详细指标和误分类样本) def enhanced_test(model, test_loader, device, class_names=None): """执行测试并返回详细评估指标和误分类样本""" if class_names is None: class_names = [str(i) for i in range(10)] model.eval() correct = 0 total = 0 all_targets = [] all_predicted = [] all_paths = [] # 存储图像路径用于错误分析 with torch.no_grad(): for inputs, targets, paths in test_loader: inputs, targets = inputs.to(device), targets.to(device) outputs = model(inputs) _, predicted = outputs.max(1) total += targets.size(0) correct += predicted.eq(targets).sum().item() all_targets.extend(targets.cpu().numpy()) all_predicted.extend(predicted.cpu().numpy()) all_paths.extend(paths) test_acc = 100. * correct / total print(f'Test Acc: {test_acc:.2f}%') # 计算精确率、召回率、F1分数 precision, recall, f1, _ = precision_recall_fscore_support( all_targets, all_predicted, average=None) avg_precision = np.mean(precision) avg_recall = np.mean(recall) avg_f1 = np.mean(f1) # 打印详细指标 print("\n详细评估指标:") print(f"测试集准确率: {test_acc:.2f}%") print(f"平均精确率: {avg_precision:.4f}") print(f"平均召回率: {avg_recall:.4f}") print(f"平均F1分数: {avg_f1:.4f}") # 打印分类报告 print("\n分类报告:") print(classification_report(all_targets, all_predicted, target_names=class_names)) # 计算混淆矩阵 cm = confusion_matrix(all_targets, all_predicted) # 找出误分类样本 misclassified_indices = np.where(np.array(all_targets) != np.array(all_predicted))[0] misclassified_samples = [] for idx in misclassified_indices[:20]: # 最多取20个样本 misclassified_samples.append({ 'image_path': all_paths[idx], 'true_label': all_targets[idx], 'predicted_label': all_predicted[idx] }) return { 'accuracy': test_acc, 'precision': precision, 'recall': recall, 'f1': f1, 'average_precision': avg_precision, 'average_recall': avg_recall, 'average_f1': avg_f1, 'confusion_matrix': cm, 'misclassified_samples': misclassified_samples } # 可视化训练过程 def visualize_training(train_losses, train_accs, val_losses, val_accs): # 设置中文字体支持 plt.rcParams["font.family"] = "SimHei" # 使用 SimHei 显示中文 plt.rcParams["axes.unicode_minus"] = False # 正常显示负号 epochs = range(1, len(train_losses) + 1) # 创建一个2x1的子图 fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(10, 12)) # 绘制损失曲线 ax1.plot(epochs, train_losses, 'b-', label='训练损失') ax1.plot(epochs, val_losses, 'r-', label='验证损失') ax1.set_title('训练和验证损失') ax1.set_xlabel('轮次') ax1.set_ylabel('损失') ax1.legend() ax1.grid(True) # 绘制准确率曲线 ax2.plot(epochs, train_accs, 'b-', label='训练准确率') ax2.plot(epochs, val_accs, 'r-', label='验证准确率') ax2.set_title('训练和验证准确率') ax2.set_xlabel('轮次') ax2.set_ylabel('准确率 (%)') ax2.legend() ax2.grid(True) plt.tight_layout() plt.savefig('training_metrics.png') plt.show() # 可视化混淆矩阵(增强版:包含原始混淆矩阵和错误率热力图) def visualize_enhanced_confusion_matrix(cm, class_names=None): # 设置中文字体支持 plt.rcParams["font.family"] = "SimHei" # 使用 SimHei 显示中文 plt.rcParams["axes.unicode_minus"] = False # 正常显示负号 if class_names is None: class_names = [str(i) for i in range(10)] # 创建两个子图:原始混淆矩阵和错误率矩阵 fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(20, 8)) # 绘制原始混淆矩阵 sns.heatmap(cm, annot=True, fmt='d', cmap='Blues', ax=ax1) ax1.set_title('混淆矩阵') ax1.set_xlabel('预测标签') ax1.set_ylabel('真实标签') ax1.set_xticklabels(class_names) ax1.set_yticklabels(class_names) # 计算错误率矩阵 (对角线为0,非对角线为错误数/真实类别总数) error_matrix = cm.astype('float').copy() for i in range(len(error_matrix)): total = np.sum(error_matrix[i, :]) if total > 0: error_matrix[i, :] = error_matrix[i, :] / total error_matrix[i, i] = 0 # 对角线错误率设为0 # 绘制错误率矩阵热力图 sns.heatmap(error_matrix, annot=True, fmt='.2f', cmap='Reds', ax=ax2) ax2.set_title('类别间错误率热力图') ax2.set_xlabel('预测标签') ax2.set_ylabel('真实标签') ax2.set_xticklabels(class_names) ax2.set_yticklabels(class_names) plt.tight_layout() plt.savefig('confusion_matrix_enhanced.png') plt.show() # 分析图像质量(模糊、笔画断裂等) def analyze_error_reason(img: np.ndarray, true_label: int, pred_label: int): """ 分析图像质量,给出可能的误分类原因 """ similar_pairs = [(3, 8), (8, 3), (1, 7), (7, 1), (2, 7), (7, 2), (4, 9), (9, 4)] if (true_label, pred_label) in similar_pairs: return f"相似数字混淆({true_label}与{pred_label})" img_uint8 = (img * 255).astype(np.uint8) clarity = cv2.Laplacian(img_uint8, cv2.CV_64F).var() blur_threshold = 100 if clarity < blur_threshold: return "笔画断裂/模糊" edges_x = cv2.Sobel(img_uint8, cv2.CV_64F, 1, 0, ksize=3) edges_y = cv2.Sobel(img_uint8, cv2.CV_64F, 0, 1, ksize=3) edge_energy = np.mean(np.abs(edges_x) + np.abs(edges_y)) noise_level = img_uint8.var() if noise_level > 3000 and edge_energy > 50: return "噪声干扰" _, binary = cv2.threshold(img_uint8, 127, 255, cv2.THRESH_BINARY) contours, _ = cv2.findContours(binary, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) if len(contours) > 6: return "笔画断裂/模糊" moments = cv2.moments(binary) if moments['m00'] == 0: center_deviation = float('inf') else: center_x = moments['m10'] / moments['m00'] center_y = moments['m01'] / moments['m00'] height, width = img_uint8.shape center_deviation = abs(center_x - width / 2) + abs(center_y - height / 2) if center_deviation > 15: return "手写风格极端" coords = np.column_stack(np.where(binary > 0)) if len(coords) == 0: return "其他原因" min_row, min_col = coords.min(axis=0) max_row, max_col = coords.max(axis=0) aspect_ratio = (max_row - min_row) / (max_col - min_col) if (max_col - min_col) > 0 else float('inf') normal_aspect = 1.0 deformation_threshold = 0.6 if abs(aspect_ratio - normal_aspect) > deformation_threshold: return "字体变形" return "其他原因" # 修改后的:可视化误分类样本并分析原因 def analyze_misclassified_samples(model, data_loader, device, class_names=None, num_samples=10, per_class_limit=2): """可视化误分类样本并尝试分析错误原因""" if class_names is None: class_names = [str(i) for i in range(10)] model.eval() misclassified_by_class = defaultdict(list) with torch.no_grad(): for inputs, targets, paths in data_loader: inputs, targets = inputs.to(device), targets.to(device) outputs = model(inputs) _, predicted = torch.max(outputs, 1) for i in range(len(predicted)): if predicted[i] != targets[i]: img = inputs[i].cpu().squeeze().numpy() true_label = targets[i].item() pred_label = predicted[i].item() path = paths[i] misclassified_by_class[true_label].append({ 'image': img, 'true_label': true_label, 'predicted_label': pred_label, 'path': path }) selected_samples = [] for cls in range(10): # 数字 0-9 samples = misclassified_by_class.get(cls, []) selected_samples.extend(samples[:per_class_limit]) np.random.shuffle(selected_samples) selected_samples = selected_samples[:num_samples] if not selected_samples: print("没有找到误分类的样本!") return [], [] plt.figure(figsize=(16, num_samples * 1.2)) error_reasons = [] for i, sample in enumerate(selected_samples): img = sample['image'] true_label = sample['true_label'] pred_label = sample['predicted_label'] reason = analyze_error_reason(img, true_label, pred_label) error_reasons.append(reason) plt.subplot(num_samples, 3, i * 3 + 1) plt.imshow(img, cmap='gray') plt.title(f"真实: {class_names[true_label]}\n预测: {class_names[pred_label]}") plt.axis('off') plt.figtext(0.1, 0.95 - i * 0.08, f"路径: {os.path.basename(sample['path'])}", fontsize=8) plt.figtext(0.4, 0.95 - i * 0.08, f"错误原因: {reason}", fontsize=8) img_tensor = torch.tensor(img, dtype=torch.float32).unsqueeze(0).unsqueeze(0).to(device) img_tensor = transforms.Normalize((0.5,), (0.5,))(img_tensor) output = model(img_tensor) probs = F.softmax(output, dim=1).cpu().numpy()[0] plt.subplot(num_samples, 3, i * 3 + 2) x = np.arange(len(class_names)) plt.bar(x, probs, color='skyblue') plt.xticks(x, class_names, rotation=45, fontsize=6) plt.title('预测概率分布') plt.ylim(0, 1) if i * 3 + 2 < num_samples * 3: plt.figtext(0.7, 0.95 - i * 0.08, f"真实标签概率: {probs[true_label]:.2f}\n" f"预测标签概率: {probs[pred_label]:.2f}", fontsize=8) plt.tight_layout(rect=[0, 0, 1, 0.95]) plt.suptitle('误分类样本分析', fontsize=14, fontweight='bold') plt.savefig('misclassified_samples_analysis.png') plt.show() print("\n错误原因统计:") reason_counter = Counter(error_reasons) for reason, count in reason_counter.most_common(): print(f"- {reason}: {count}个样本") return selected_samples, error_reasons if __name__ == '__main__': # 初始化模型 device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = LightResNet().to(device) # 打印模型结构 print("模型结构:") summary(model, (1, 28, 28)) # 定义损失函数和优化器 criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=0.001, betas=(0.9, 0.999)) scheduler = optim.lr_scheduler.ReduceLROnPlateau(optimizer, 'min', patience=3, factor=0.5) # 训练模型 epochs = 10 best_val_acc = 0.0 best_model_path = 'best_model.pth' # 记录训练过程 train_losses, train_accs = [], [] val_losses, val_accs = [], [] for epoch in range(epochs): print(f'\nEpoch {epoch + 1}/{epochs}') train_loss, train_acc = train(model, train_loader, criterion, optimizer, epoch, device) val_loss, val_acc = validate(model, val_loader, criterion, device) # 记录训练过程 train_losses.append(train_loss) train_accs.append(train_acc) val_losses.append(val_loss) val_accs.append(val_acc) # 学习率调整 scheduler.step(val_loss) # 保存最佳模型 if val_acc > best_val_acc: best_val_acc = val_acc torch.save(model.state_dict(), best_model_path) print(f'模型已保存,验证准确率: {best_val_acc:.2f}%') # 加载最佳模型并进行增强测试 model.load_state_dict(torch.load(best_model_path, weights_only=True)) test_results = enhanced_test(model, test_loader, device) # 可视化增强版混淆矩阵(包含错误率热力图) class_names = [str(i) for i in range(10)] visualize_enhanced_confusion_matrix(test_results['confusion_matrix'], class_names) # 可视化训练结果(保持原有功能) visualize_training(train_losses, train_accs, val_losses, val_accs) # 可视化误分类样本并分析原因 print("\n分析误分类样本...") misclassified_samples, error_reasons = analyze_misclassified_samples( model, test_loader, device, class_names, num_samples=10) 以上这份代码照比之前的缺少了网络拓扑图标注各层参数(如卷积核大小、步长、输出维度)的绘制,还有这个错误: 分析误分类样本... Traceback (most recent call last): File "C:\Users\86182\Desktop\短学期\pythonProject\test.py", line 594, in <module> misclassified_samples, error_reasons = analyze_misclassified_samples( File "C:\Users\86182\Desktop\短学期\pythonProject\test.py", line 510, in analyze_misclassified_samples probs = F.softmax(output, dim=1).cpu().numpy()[0] RuntimeError: Can't call numpy() on Tensor that requires grad. Use tensor.detach().numpy() instead. 进程已结束,退出代码为 1
07-01
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值