news 2026/9/12 1:58:54

机器学习课设实战:线性回归分类与手写数字识别的从零实现和调库对比

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
机器学习课设实战:线性回归分类与手写数字识别的从零实现和调库对比

简介:面向机器学习课程结课大课设的完整资源包,聚焦线性回归分类与手写数字识别两大基础主题,适合初学机器学习、需要完成课程设计或巩固建模流程的学生。压缩包内共11个文件,包括7个Python脚本和4个MNIST手写数字数据集文件(idx格式),整体约11MB。其中线性回归分类部分同时提供自设计实现与调用sklearn库的高斯函数回归、幂函数回归等对比版本;手写数字识别部分覆盖原始像素建模、带标签数据训练及KMeans调库方案,便于从原理到工具逐层理解。已有646人学习下载,资源自带完整数据集,省去单独下载MNIST的麻烦,适合对照两种实现方式梳理数据预处理、模型训练、评估和调参思路,也能为课程答辩和实验报告提供参考。

1. 这门课设真正要交的不是模型,而是两套程序的对比

很多人在学期末拿到机器学习课设题目"线性回归分类、手写数字识别"时,第一反应是找现成仓库把代码一放,跑通就算完。但你仔细看标题里括号那句"设计程序和调库程序都有",就知道这门课设的考核点根本不是模型的准确率,而是你能不能分别用"从零实现"和"调现成库"这两条路径解决同一个问题,并且说清楚两者之间的差距。设计程序要求你从随机初始化、梯度下降、反向传播这些底层步骤写起,调库程序则要求你用 sklearn 或 PyTorch 快速构建一个可用的方案。对于打算应付过去的人,这文能帮你在三天内把两条路都走通;对于想真正弄懂机器学习流程的人,这里讲的边界和参数思想,能让你答辩时不露怯。

我会顺着课设最常见的拆分方式来讲:先用线性回归做二分类,看你手写梯度下降和调 sklearn 差多少;再上手写数字识别,手写一个两层 BP 神经网络,对比 PyTorch 里的 CNN。中间穿插数据预处理、损失函数、调参与可视化这些课设报告里必须有的环节,最后给你答辩前必须验证的几个点。

2. 线性回归分类:从最小二乘到梯度下降的手写实现与调库对照

2.1 为什么课设里"线性回归分类"不等于分类器

线性回归本质是拟合连续值输出,但课设题目里把"线性回归分类"放在一起,实际场景通常有两种:一种是用线性回归拟合 0/1 类别标签,然后在预测时加一个阈值(比如输出大于 0.5 判为正类);另一种就是直接用逻辑回归,线性部分接一个 sigmoid 函数。前者是"线性回归做分类"的朴素直译,后者的数学推导里保留着线性假设的核心,所以很多课设报告干脆把逻辑回归也归在"线性回归分类"一节。

你需要清楚两个关键差异:线性回归直接拟合标签,损失是均方误差,梯度对离群点非常敏感;逻辑回归通过 sigmoid 压缩输出,损失用交叉熵,梯度更平稳。课设里如果只给"线性回归分类"这个说法,我一般建议两份代码都写:手写部分就用线性回归加阈值,因为实现简单、原理好讲;调库部分用逻辑回归,因为 sklearn 里没有"线性回归分类器"这种现成命名,你可以在报告里注明逻辑回归是线性回归在分类任务上的扩展。答辩时这个问题几乎是必问的。

再往下说,无论选哪种,核心都是线性假设:输入特征与输出关系近似一条直线(或者高维超平面)。手写实现的意义不是造轮子,而是让你亲眼看到损失函数怎么收敛、梯度方向怎么更新、偏置项从哪来。调库实现的意义则是让你理解封装好的 API 背后替你做了什么——标准化、正则化、迭代终止条件。

2.2 手写线性回归分类:纯 NumPy 梯度下降

2.2.1 数据准备与模型假设

我用 sklearn 自带的乳腺癌数据集做演示,它适合做二分类,特征维数不多,能直观看到线性模型的效果。设计程序的核心代码放在一个模块里,方便和调库程序共享数据加载逻辑。

import numpy as np from sklearn.datasets import load_breast_cancer from sklearn.model_selection import train_test_split data = load_breast_cancer() X = data.data y = data.target.astype(float) # 0/1 标签 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) # 标准化,对手写梯度下降非常重要 mean = X_train.mean(axis=0) std = X_train.std(axis=0) X_train = (X_train - mean) / std X_test = (X_test - mean) / std

标准化这里不能省。线性回归的梯度与特征尺度成正比,如果不做标准化,学习率只能取很小值,否则会震荡;标准化以后所有特征均值为 0、方差为 1,梯度方向更接近球形等高线,收敛速度快很多。train_test_split 里的 random_state 固定下来,是为了让手写程序和调库程序在同一份数据上比较。

2.2.2 训练函数与损失计算
class LinearRegressionClassifier: def __init__(self, lr=0.01, epochs=1000, threshold=0.5): self.lr = lr self.epochs = epochs self.threshold = threshold self.w = None self.b = None def fit(self, X, y): m, n = X.shape self.w = np.random.randn(n) * 0.01 self.b = 0.0 for epoch in range(self.epochs): pred = X @ self.w + self.b loss = np.mean((pred - y) ** 2) # 梯度的向量化表达 grad_w = (2 / m) * X.T @ (pred - y) grad_b = (2 / m) * np.sum(pred - y) self.w -= self.lr * grad_w self.b -= self.lr * grad_b if epoch % 200 == 0: print(f"epoch {epoch}, loss {loss:.4f}") def predict(self, X): return (X @ self.w + self.b > self.threshold).astype(int) model = LinearRegressionClassifier(lr=0.1, epochs=2000) model.fit(X_train, y_train) acc = np.mean(model.predict(X_test) == y_test) print("手工线性回归分类准确率:", acc)

代码逻辑很简单:每次迭代计算预测值与真实标签的差值,然后分别对 w 和 b 求梯度,用学习率更新参数。注意这里损失函数用了均方误差,梯度除以 m 是为了让梯度值不随样本数变大而变大。grad_w是关键,X.T @ (pred - y)是特征矩阵转置乘残差,形状正好是 (n,),也就是每个特征的梯度。实际课设中经常有人把转置写反,导致维度对不上,可以用X.T @ 残差这个固定句式记忆。

调学习率时要注意:lr=0.1 在标准化后的数据上通常没问题;如果 loss 交叉震荡,就降到 0.01;如果 loss 下降太慢,可以尝试 0.5 并观察是否发散。epochs 不宜死磕固定值,最好在循环里加一个终止条件——当连续 20 次迭代 loss 变化小于 1e-5 就停止,省时间。

2.3 调库版本:用 sklearn 一行完成

from sklearn.linear_model import LinearRegression, LogisticRegression # 线性回归 + 阈值 lr_clf = LinearRegression() lr_clf.fit(X_train, y_train) pred_linear = (lr_clf.predict(X_test) > 0.5).astype(int) acc_linear = np.mean(pred_linear == y_test) # 逻辑回归(更推荐作为本题的调库程序) lgr = LogisticRegression(max_iter=1000, C=1.0) lgr.fit(X_train, y_train) acc_lgr = lgr.score(X_test, y_test) print("sklearn LinearRegression + 阈值:", acc_linear) print("sklearn LogisticRegression:", acc_lgr)

sklearn 的 LinearRegression 用的是最小二乘解析解,没有迭代,也没有学习率,它内部通过 SVD 分解直接求闭式解。这个调库程序和手写程序放在一起能说明一件重要的事:解析解和梯度下降解的差距不在结果,而在路径。你的手写代码能展示"优化过程"和学习率的影响,调库代码能展示"当最优解可以直接算出来时,迭代是否必要"。另一个建议是同时调用 LogisticRegression,因为它在分类任务里的实际表现明显更好,把它作为调库程序,你报告里的对比就有三层:手写线性回归、sklearn 线性回归、sklearn 逻辑回归,这样内容量足够支撑三千字的报告。

需要注意C=1.0是正则化强度的倒数,C 越小正则化越强。在特征数不多且标准化良好的数据上,C 等于 1.0 是默认值,效果稳定。如果训练集准确率高于测试集 3 个点以上,把 C 降到 0.1 试试,可以缓解过拟合。

2.4 参数怎么调:学习率、迭代次数、特征缩放

手写线性回归分类里,你真正能调的超参数只有三个:学习率 lr、迭代次数 epochs、阈值 threshold。学习率的常见范围是 0.001 到 1.0,指数级尝试,不要用 0.3 到 0.4 这种线性猜测。判断标准是 loss 曲线:如果前几百次迭代 loss 持续下降,说明 lr 合适;如果 loss 发散,立刻调小 lr;如果 loss 下降缓慢,可以调大 lr 但要注意震荡。

迭代次数不是越多越好。你可以观察最后 10% 迭代的 loss 变化,如果小于 1e-5,说明模型已经收敛,再训也没有用。阈值 threshold 按理说不需要调,对于二分类 0.5 是天然的,但如果你的类别不平衡,比如正类样本只占 10%,手写程序的输出平均值会偏向负类,这时把阈值调到训练集正样本比例附近,准确率会提升。这种调法体现了"分类阈值是可优化决策边界"的思想,答辩时提一句很加分。

特征缩放是手写线性回归的生命线。除了标准化到均值为 0、方差为 1,还可以用 min-max 缩放到 [0,1] 区间。两者的差别是:标准化保留了分布形状,对离群点不敏感;min-max 会把离群点压缩到边界。对于线性回归,这两种都行,但 sklearn 的线性回归接口不接受 NaN,所以缩放前要确认数据没有缺失值。

3. 手写数字识别:从零实现 BP 神经网络与 PyTorch 调库之间的差距

3.1 MNIST 数据集加载与预处理

手写数字识别最经典的基准是 MNIST:28×28 的灰度图,每张像素值范围 0 到 255,共 10 个类别。课设里一般要求训练集 60000 张、测试集 10000 张。但很多人加载数据直接用 torchvision 或 keras 的下载接口,这没问题,不过手写程序部分需要的是纯 NumPy 数据格式,所以我建议统一用 torchvision 下载后转成 ndarray,或者直接加载 scikit-learn 自带的fetch_openml版本。后者对教育场景更友好,不需要碰深度学习框架。

from sklearn.datasets import fetch_openml import numpy as np mnist = fetch_openml('mnist_784', version=1, as_frame=False) X = mnist.data.astype(np.float32) / 255.0 y = mnist.target.astype(np.int32) # 划分训练/测试集:前 60000 张训练,后 10000 张测试 X_train, y_train = X[:60000], y[:60000] X_test, y_test = X[60000:], y[60000:]

as_frame=False直接返回 ndarray,省去转换;除以 255 是必需的归一化操作,否则手写 BP 的梯度极容易爆炸。这个数据集里每张图被拉成 784 维向量,但标签还是整型,而手写神经网络的输出层需要 one-hot 编码:

def one_hot(labels, num_classes=10): eye = np.eye(num_classes) return eye[labels] y_train_oh = one_hot(y_train) y_test_oh = one_hot(y_test)

np.eye(num_classes)[labels]是构造 one-hot 的快速写法,不需要写循环。

3.2 设计程序:手写两层神经网络(NumPy 实现)

3.2.1 前向传播与反向传播的实现

课设里的"设计程序"部分,我不想直接给一个三层以上的复杂网络,因为反向传播的链式法则在层数多了之后很难手动查错。这里实现一个隐藏层 64 个神经元、输出层 10 神经元的全连接网络,激活函数用 ReLU,输出层用 softmax 加交叉熵损失,这就是 Mini-Batch 梯度下降的核心结构。

class SimpleNN: def __init__(self, input_size=784, hidden_size=64, output_size=10, lr=0.1): self.lr = lr self.W1 = np.random.randn(input_size, hidden_size) * np.sqrt(1.0 / input_size) self.b1 = np.zeros(hidden_size) self.W2 = np.random.randn(hidden_size, output_size) * np.sqrt(1.0 / hidden_size) self.b2 = np.zeros(output_size) def forward(self, X): self.z1 = X @ self.W1 + self.b1 self.a1 = np.maximum(0, self.z1) # ReLU 激活 self.z2 = self.a1 @ self.W2 + self.b2 exp_z = np.exp(self.z2 - np.max(self.z2, axis=1, keepdims=True)) self.a2 = exp_z / exp_z.sum(axis=1, keepdims=True) # softmax return self.a2 def backward(self, X, y_oh): m = X.shape[0] pred = self.a2 dz2 = pred - y_oh # softmax 交叉熵的简化梯度 dW2 = self.a1.T @ dz2 / m db2 = dz2.mean(axis=0) da1 = dz2 @ self.W2.T dz1 = da1 * (self.z1 > 0) # ReLU 的导数 dW1 = X.T @ dz1 / m db1 = dz1.mean(axis=0) self.W2 -= self.lr * dW2 self.b2 -= self.lr * db2 self.W1 -= self.lr * dW1 self.b1 -= self.lr * db1

初始化权重的尺度乘以sqrt(1/input_size)是 Xavier 初始化的简化版本,防止前向传播时神经元输出方差过大或者过小。dz2 = pred - y_oh是这段代码里最值得记住的结论——如果你使用 softmax 输出和交叉熵损失,反向传播到输出层之前的梯度恰好等于预测值减真值,不需要手算 log 的导数。ReLU 的导数在正向大于零时是 1,否则是 0,代码里(self.z1 > 0)生成的就是一个掩码矩阵。

3.2.2 训练循环与准确率计算
def train(model, X, y_oh, epochs=20, batch_size=64): n = X.shape[0] for epoch in range(epochs): indices = np.random.permutation(n) for start in range(0, n, batch_size): batch_idx = indices[start:start + batch_size] model.forward(X[batch_idx]) model.backward(X[batch_idx], y_oh[batch_idx]) pred = model.forward(X) acc = np.mean(np.argmax(pred, axis=1) == np.argmax(y_oh, axis=1)) if epoch % 5 == 0: print(f"epoch {epoch}, train acc {acc:.4f}") model = SimpleNN(lr=0.3, hidden_size=64) train(model, X_train, y_train_oh, epochs=20) test_pred = model.forward(X_test) print("手写 BP 测试准确率:", np.mean(np.argmax(test_pred, axis=1) == y_test))

训练循环里每个 epoch 都打乱样本顺序,这是 Mini-Batch 梯度下降能否跳出局部鞍点的关键。batch_size 取多少合适?课设场景下 64 是稳妥选择——太小(比如 1)梯度噪声大,收敛慢;太大(比如 512)每个 epoch 更新次数少,也慢。lr 取 0.3 是配合 ReLU 初始化和数据归一化的经验值,你可以尝试 0.1 到 1.0。这个手写网络在 MNIST 测试集上通常能到 0.90 到 0.93 的准确率,跑 20 轮在普通笔记本上大约需要一到两分钟。如果你发现准确率只有 0.8,先检查标准化:像素没除以 255 的话,梯度会以 784 的规模累积,等于没调。

别忘了在验证集上算准确率时不再做反向传播,只是 forward。argmax把每个样本的输出向量变成预测类别,和真实标签比较就算出准确率。

3.3 调库程序:PyTorch 实现一个简单 CNN

调库部分如果继续用全连接网络,和手写 BP 的差别只是框架,不够有说服力。我一般建议直接用 PyTorch 搭一个小型 CNN,因为卷积层能展示"调库让我们可以轻松使用高级结构"这个观点。

import torch import torch.nn as nn class CNN(nn.Module): def __init__(self): super().__init__() self.conv = nn.Sequential( nn.Conv2d(1, 32, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.ReLU(), nn.MaxPool2d(2) ) self.fc = nn.Sequential( nn.Linear(64 * 7 * 7, 128), nn.ReLU(), nn.Linear(128, 10) ) def forward(self, x): x = self.conv(x) x = x.view(x.size(0), -1) x = self.fc(x) return x def train_pytorch(model, train_loader, epochs=5): criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=0.001) model.train() for epoch in range(epochs): total_loss = 0 for images, labels in train_loader: optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() total_loss += loss.item() print(f"epoch {epoch}, loss {total_loss / len(train_loader):.4f}")

PyTorch 代码的核心是 DataLoader 和优化器。CrossEntropyLoss内部封装了 softmax 和负对数似然,所以你不需要在模型 forward 里手动加 softmax;Adam优化器自带学习率自适应,不需要手动调整学习率衰减策略。训练一个 epoch 只需遍历 DataLoader,每批次数据送到模型前向,算 loss 后反向传播,优化器更新参数。这个代码在 MNIST 上训练 5 个 epoch,测试准确率可以轻松到达 0.99,而手写 BP 到 0.93 就已经触顶。

注意输入形状:nn.Conv2d期望输入是(batch, channel, height, weight),所以加载数据时要把(batch, 28, 28)reshape 成(batch, 1, 28, 28)。如果你用 DataLoader 默认的 MNIST 数据集,它已经自动把尺寸转成 4 维张量了。

3.4 手写BP和调库CNN的边界在哪

两个程序的差距体现在三个地方:参数量、运算结构和训练技巧。手写 BP 的参数是 784×64 + 64×10 = 50760 个,而硬编码的两层卷积 CNN 参数约 5 万个左右,其实相差不大。真正决定准确率差异的是结构:CNN 通过卷积的局部感受野和参数共享,天然编码了 "像素邻近关系" 的先验知识;全连接网络每个像素都是独立特征,要求大量数据才能学到空间不变性。

调库程序除了框架,还带来了批归一化、Dropout、数据增强这些现成组件。你可以在 PyTorch 里加两个批归一化层nn.BatchNorm2d,然后和手写程序对比,结论会更有说服力:同样的迭代次数,加了归一化的模型收敛更快,测试准确率更高。答辩时你不需要背诵论文,只需要说清楚一点:调库是让框架替我们处理了底层梯度计算,但模型结构的选择、损失函数的设计、超参数调整仍然需要我们决策。

4. 两套程序的工程化组装:数据共享、结果对比与可视化

4.1 工程目录怎么安排

课设提交通常要一个压缩包,里面包含源码、数据、报告。我习惯按功能分模块,避免老师打开就是一个 Jupyter Notebook 跑到底。推荐目录结构如下:

. ├── data/ # 原始数据,脚本会自动下载或加载 ├── models/ │ ├── manual/ │ │ ├── linear_regression.py │ │ ├── bp_network.py │ │ └── train_bp.py │ └── library/ │ ├── sklearn_classifiers.py │ └── pytorch_cnn.py ├── utils/ │ ├── load_data.py # 统一的数据加载与预处理 │ └── metrics.py # 准确率、混淆矩阵、绘图 ├── results/ # 生成的图表和模型参数 └── report.md

这个结构的好处是:手写程序和调库程序之间存在清晰的隔离,但数据预处理共享。load_data.py应当包含 MNIST 的两种加载方式(sklearn 和 torchvision),并统一输出 NumPy 数组;metrics.py里放plot_confusion_matrixplot_loss_curve两个函数,避免在两个程序里重复写。

4.2 统一的数据加载与评估指标

无论手写还是调库,都要保证训练集和测试集划分一致。MNIST 是官方划分好的,但为了防作弊,很多学校会要求自定义划分。我提供一个简单可靠的划分逻辑:

from sklearn.model_selection import StratifiedShuffleSplit def split_train_test(X, y, test_ratio=0.2, save_path='results/split_indices.npy'): split = StratifiedShuffleSplit(n_splits=1, test_size=test_ratio, random_state=42) train_idx, test_idx = next(split.split(X, y)) np.save(save_path, np.vstack([train_idx, test_idx])) return train_idx, test_idx

StratifiedShuffleSplit会保证训练集和测试集里每个数字的比例接近原始分布,比随机切分更公平。保存索引到 npy 文件,可以保证手写程序和调库程序用同一批索引,结果才具备可比性。评估指标用准确率只是基本要求,建议加一个混淆矩阵,尤其是手写数字识别里 9 和 4、3 和 8 这种易混淆数字,混淆矩阵能直观看出模型在哪类样本上犯错。

4.3 对比表格:从准确率、训练时间、代码量看差异

程序模型结构准确率(约)训练时间(约)代码行数(约)
手写线性回归分类线性层 + 阈值0.94(乳腺癌)<1s40
sklearn 逻辑回归线性层0.96<1s8
手写 BP 网络784-64-100.921-2 分钟90
PyTorch CNN卷积+全连接0.992-3 分钟60

这个表格的明确信息有两个:同一类任务中,手写与调库的准确率差距大约 2 到 7 个百分点,差距主要来自模型结构而非实现方式;代码行数并不直接反映复杂度,sklearn 的 8 行封装了矩阵分解、优化、交叉验证等等机制,而手写 BP 的 90 行是纯计算逻辑。写报告时你应该把训练时间、准确率、代码行数都标注上,但更重要的是在表格后追加一段分析——为什么手写 BP 用时和 CNN 差不多,准确率却低?因为 CNN 用卷积减少了参数冗余,同时在同样参数规模下有了归纳偏置。这个分析段比表格本身更能体现你做了思考。

可视化方面,至少要有三个图:线性回归的 loss 下降曲线、手写 BP 的训练准确率曲线、CNN 在测试集上的混淆矩阵。loss 曲线用 matplotlib 画,X 轴是 epoch,Y 轴是 loss;注意手写 BP 的 loss 是每个 epoch 的整体平均,画出来应该是平滑下界的曲线,如果有剧烈抖动说明学习率太大。

5. 答辩前必须会回答的四个问题和三个验证技巧

5.1 老师必问:为什么手写的收敛慢

这个问题通常是答辩顺序里第一个出现的。你不需要回避"手写程序慢",可以从三方面回答:一是手写网络只用全连接,没有卷积和池化,所以输入图像的两维空间结构没有被利用;二是手写用了固定学习率,而 PyTorch 的 Adam 会自适应调整每个参数的更新步长;三是手写代码没有批归一化,特征分布在每层传导后会剧烈变化,导致收敛需要更多 epoch。如果你用了 Mini-Batch,还可以补充说"已经通过打乱样本顺序缓解了梯度噪声,但用一个合适的 batch size 本身就是超参数搜索的工作"。

5.2 用学习曲线判断欠拟合与过拟合

在你的训练循环里记录每个 epoch 的训练集准确率和验证集准确率,然后画在同一张图上。如果训练集准确率一直高于验证集且两者差距变大,说明过拟合,解决办法是加正则化或减小模型容量;如果两条曲线都低且平行,说明欠拟合,需要增加迭代次数或调整初始化。手写 BP 里最简单的正则化是 L2 权重衰减:在backward里把dW加上self.lr * self.W的一部分,比如self.W2 -= self.lr * (dW2 + 0.001 * self.W2)。调库版本里 PyTorch 的 Adam 优化器带了weight_decay参数,设成1e-4即可。

5.3 输出权重可视化验证模型真的学到了特征

线性回归分类里,你可以把model.w按特征维度排序,找出权重绝对值最大的前 10 个特征,打印出这些特征的名字。在乳腺癌数据集里,这些特征通常对应"最差面积"、"凹陷点"等医学指标。BP 神经网络里,可以把第一层权重矩阵的第 i 行 reshape 成 28×28 图像并显示,你会看到类似"这种数字的笔画方向"的模式——某些行看起来像 1 的形状,某些像 0 的空心轮廓。这是证明你的手写模型不是随机拟合的最好方式,答辩时展示一张 64 个小格子的权重图,比任何文字都有说服力。

最后提一个重要验证技巧:把调库程序和手写程序的特征图放在一起对比,你会直观看到 CNN 第一层卷积核学习到的是边缘和角点检测器,而 BP 学习到的是全局的笔画分布。这个对比既能展示你对两种模型的理解,也正好呼应了课设题目落的"设计程序和调库程序都有"的初心——你要做的就是清楚地把它们各自的长处和局限说出来,而不是让两个程序互相掩盖。

本文还有配套的精品资源,点击获取

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

Tomcat性能优化核心配置与实战技巧

1. Tomcat性能优化核心面试题解析作为Java Web开发中最常用的Servlet容器&#xff0c;Tomcat的性能优化一直是中高级开发者面试的必考点。我在电商和金融行业做过多次Tomcat调优&#xff0c;发现90%的性能问题都集中在以下几个关键环节&#xff1a;1.1 连接器(Connector)配置优…

作者头像 李华
网站建设 2026/9/12 1:56:22

区间操作问题的树状数组与线段树解法详解

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/12 1:53:48

PyTorch+Mask R-CNN 建筑物识别:从训练到部署的完整实践

简介&#xff1a;这是一份基于Python语言与PyTorch框架的建筑物识别器源码项目&#xff0c;面向机器视觉初学者、遥感及地理信息分析人员&#xff0c;解决从卫星或航拍影像中自动检测、识别并框出建筑物的实际需求。资源包共16个文件&#xff0c;包含13个Python脚本&#xff0c…

作者头像 李华