news 2026/10/17 5:51:10

六个经典CNN模型:从AlexNet到MobileNet的进化与PyTorch实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
六个经典CNN模型:从AlexNet到MobileNet的进化与PyTorch实践

把这六个词并排放在一起的时候,你可能觉得它们只是六个可以替换的模型,顶多在排行榜上差一两个点。但我第一次把这六个网络挨个跑完、再回去翻原始论文的时候,一个很直观的感觉是:这不是六个选项,这是一份浓缩到不能再浓缩的视觉深度学习进化史。每个名字都代表一种设计思想的选择——AlexNet证明深度卷积网络第一次能在计算机视觉上真正立住,VGG把“深”这个字推到极致,Inception选择在宽度上同时看多尺度,ResNet用一条短路让几百层的网络也能收敛,DenseNet在特征复用上做到极端,MobileNet则把目标换成了“在手机上能跑”。你会用哪个,取决于你手上的设备、数据量和精度要求。

这篇文章我打算换个讲法,不走“逐个列论文结构”的流水账,而是顺着这条思想演进的线,把六个网络的核心逻辑、关键突破和PyTorch里的落地细节串起来讲。尤其会集中说一说那些只有真正跑过代码、看过训练日志才会注意到的坑:预训练权重的加载方式、分类头的替换位置、辅助分类器带来的输出结构变化、批量归一化在冻结阶段的脾气,以及同一批数据上六个网络到底差在哪。

1. 六个网络背后,是一整段视觉进化史

1.1 为什么学习顺序就是历史顺序

我见过不少新人直接跳去读某个最新网络,回头发现基础网络反而一团雾水。原因很简单:新网络的所有“改进”都是相对旧网络的缺陷提出来的,你不知道旧网络哪里别扭,就理解不了新网络为什么长成那样。

打个不太恰当的比方,这六个网络像六代车型的迭代。AlexNet是第一辆能上路的车,VGG是把发动机做大的暴力版本,Inception是给车装了多个视角的雷达,ResNet是给引擎加了涡轮增压让车可以继续堆马力,DenseNet是重新设计成一辆信息共享型的混动车,MobileNet则是把整车拆开减重,为了能塞进手机里。你按这个顺序理解,会发现每个模型都预留了上一个模型的影子,但解决的是不同层面的问题。

这种演进关系在PyTorch实践里也能看到:换一个网络,往往只需要改一行模型名、改一下最后一层分类头,前面的特征提取骨架几乎可以原样搬。明白了这条线,你以后再看SENet、EfficientNet,甚至视觉Transformer,都能自动往上挂靠,知道自己看的是“注意力机制”“复合缩放”还是“全局建模”那一层的创新。

1.2 先用一张表建立整体坐标

为了不让后面几个章节样板化,我先把六个网络的核心差异放在一起,后面再逐个展开原理和代码。这算是一张“索引表”,读的过程中可以随时回来对照。

网络出现时间线核心思想参数量量级ImageNet Top-1(约为)典型场景
AlexNet2012深度卷积+GPU训练,重新定义视觉基线约6100万约56.6%教学入门、特征提取实验
VGG1620143x3小卷积核堆叠,规则化加深约1.38亿约71.6%风格迁移、特征可视化
Inception v1/GoogLeNet2014多尺度卷积并行,1x1卷积降维约700万约69.8%多尺度场景、参数敏感任务
ResNet502015残差捷径解决退化问题约2560万约76.2%通用基线、检测分割骨干
DenseNet1212017密集连接实现特征复用约800万约74.6%参数效率优先、深层训练研究
MobileNetV22017深度可分离卷积轻量化约350万约71.9%移动端、边缘设备、嵌入式推理

这几条数据是常见公开权重的参考值,不是严格评测榜单。真正在你自己数据集上,顺序可能会变,但量级关系不会颠覆。后面第六节我会用一个小规模实验说明“跑的动”和“能落地”之间的差距。

2. AlexNet与VGG:奠基者的深度实验

2.1 AlexNet:深度学习站稳脚跟的三板斧

如果只看现在的网络,AlexNet就像一张粗糙的老照片,但它三个核心操作至今在所有现代化网络里都是标配。

第一是ReLU激活。在AlexNet之前,主流是sigmoid/tanh这类饱和激活,层一深,靠反向传播回传的梯度会逐层缩水,训练基本推不动。ReLU本质上是个max(0, x),正半轴导数为1,梯度不会在激活函数这一步衰减,深层网络才第一次变得“可训练”。PyTorch里的torch.nn.ReLU就是这件事。

第二是Dropout。AlexNet两个全连接层之间插了Dropout,训练时随机丢一半神经元,让网络没法依赖某一个神经元的“独裁”,相当于一种极端但有效的集成策略。放到今天,全连接层已经不再是主流,但Dropout的变体仍然是防止小数据集过拟合的好手段。

第三是数据增强。原始ImageNet那点图不够喂给几千万参数的模型,AlexNet用随机裁剪、水平翻转、RGB通道强度扰动,把训练样本数量人为撑了几十倍。这比改模型结构便宜得多,也是我一直建议小数据集项目的第一条路:先做增强,再谈换大模型。

在PyTorch里调出它非常直接:

import torchvision model = torchvision.models.alexnet(weights=torchvision.models.AlexNet_Weights.DEFAULT) # 如果要换成自己的分类数 num_classes = 10 model.classifier[6] = torch.nn.Linear(4096, num_classes)

注意AlexNet的结构是5层卷积加3层全连接,真正的分类输出在classifier[6]这一层。如果你只改了前面卷积层卡分类数,训练时会直接报维度不匹配。

2.2 VGG:把小卷积核堆到底,感受野的账算给你看

VGG的思想用一句话说:与其用一个大卷积核一步看很大范围,不如用多个小卷积核串起来慢慢看。它把卷积核固定为3x3,padding为1,连续堆叠。

从数学上算这笔账特别有意思。一个5x5卷积的“视野范围”是5x5=25,但你用两个3x3卷积串联,重叠覆盖范围同样是5x5,参数量却是3x3x2=18(只看单个通道),比25少了28%。换成7x7感受野,三个3x3是27,比49少了将近45%。而且中间多夹了一层ReLU,非线性表达能力反而更强。这就是VGG用“更少的参数实现同样感受野”的核心窍门。

但VGG有一个看起来自相矛盾的缺点:总参数量高达1.38亿,其中绝大部分堆在最后三个全连接层上。卷积部分本身并不算太夸张,全连接层直接把前面提取的特征拍平成向量再全连接,特征图一多,参数立刻爆炸。这也解释了为什么在PyTorch里,VGG16单算模型文件就有500MB左右,而ResNet50只有不到100MB。

VGG至今还有一个意想不到的用处——风格迁移的感知损失。VGG中间层特征被大量用于计算“内容相似度”和“风格相似度”,因为它的特征层次特别规整:浅层管线条纹理,中层管结构,深层管语义。我自己做图像风格迁移实验的时候,也一直用VGG16做特征提取器,而不是ResNet。分类上它已经不是最优,但在“理解特征”这个维度上依然顺手。

import torchvision model = torchvision.models.vgg16(weights=torchvision.models.VGG16_Weights.DEFAULT) # 替换最后的1000类分类层 num_classes = 10 model.classifier[6] = torch.nn.Linear(4096, num_classes)

如果要大幅度削减VGG的体积,一般做法是把三个全连接层换成全局平均池化加一层Linear,参数量能砍掉一大半。但那是改结构的事了,直接用官方预训练权重时还是保持原结构更省心。

3. Inception与ResNet:两条截然不同的深网络突围路线

3.1 Inception:一个模块里同时看“近处”和“远处”

到了2014年前后,大家疯狂加层数,VGG已经把“深”吃透了。Inception却换了个思路:在一个模块里,用1x1、3x3、5x5三种卷积核并行去卷同一份输入,最后把所有通道拼在一起。这样同一个位置的像素,近处细节和远处上下文被同时捕捉,让网络自己决定该信任哪个尺度。

这套思路实现要面对一个直接问题:5x5卷积的计算量太大。于是Inception模块里塞了大量1x1卷积,先降维再做大卷积核。1x1卷积在空间上什么都不做,但它能在通道维度上做线性组合,把比如256通道先压缩到64,再送进5x5卷积,计算量直接变成原来的四分之一,信息却没丢太多。

实际使用Inception时,有个非常容易翻车的点:GoogLeNet在官方的torchvision实现里,训练阶段默认会启用两个辅助分类器,forward输出不再是单个张量,而是一个InceptionOutputs对象,包含主输出和两个辅助输出。如果你写训练循环时直接outputs = model(x); loss = criterion(outputs, targets),大概率会报类型错误或得到奇怪的结果。

正确做法是取主输出,或者干脆关掉辅助分类器:

import torchvision model = torchvision.models.googlenet(weights=torchvision.models.GoogLeNet_Weights.DEFAULT, aux_logits=False) # 替换分类层 num_classes = 10 model.fc = torch.nn.Linear(1024, num_classes)

如果保留辅助分类器,可以这样用:

outputs = model(x) logits = outputs.logits aux1, aux2 = outputs.aux_logits

另外注意,GoogLeNet默认输入是224x224,而Inception v3系列默认是299x299。如果你迁移到别的数据集,预处理尺寸必须跟着模型走,否则后面的平均池化和分类层尺寸会对不上。

3.2 ResNet:残差映射如何解决“越深越差”的怪病

ResNet提出之前,整个领域被一个反直觉现象困扰:网络加到几十层以后,训练集误差反而比浅层网络更高。这不是过拟合,而是优化本身出了问题——深层网络的梯度在反向传播时穿过太多层,即使有ReLU,也很难稳定地逼近一个恒等映射。

ResNet的做法很直接:在两层或三层的卷积块旁边加一条恒等捷径(shortcut),让这一块的输出变成“输入 + 残差”。假如这一块什么都不想学,那只需要把残差部分逼近0,网络至少不会比“直接跳过这块”更差。换句话讲,ResNet把“学习一个复杂映射”降级成了“学习一个和输入的差值”,优化难度一下子降低了。

这个思想在实现上几乎没有任何额外代价。PyTorch里你只需要用torchvision.models.resnet50,剩下的残差块全封装好了。当然,今天做图像分类,ResNet50已经不是一个需要自己从零实现的网络,但理解它为什么能训练上百层,是以后读懂各种现代模型的基础。

import torchvision model = torchvision.models.resnet50(weights=torchvision.models.ResNet50_Weights.DEFAULT) num_classes = 10 model.fc = torch.nn.Linear(model.fc.in_features, num_classes) print(model.fc.in_features) # 2048

ResNet内部有BasicBlock和BottleNeck两种块。ResNet18/34用BasicBlock,结构简单,适合轻量任务;ResNet50/101/152用BottleNeck,先1x1降维、3x3卷、再1x1升维,把计算量控制住。换分类头时不用关心这些细节,只要记住ResNet的最后一层叫fc,是个全连接层,改它就行。

3.3 两条路线后来的交锋

有趣的是,Inception和ResNet后来还融合过一次。把残差捷径塞进Inception模块,既有多尺度感知又有残差易优化的特性,效果比单独用还要好。这告诉我们,设计网络不是非此即彼的单选题,很多好模型是几套思想的组合拳。这也是为什么我建议先把几个经典网络都跑一遍——只看一个网络永远学不会“组合”的直觉。

4. DenseNet与MobileNet:特征复用和轻量化,两个极端的胜利

4.1 DenseNet:每层都和所有前层“共享记忆”

ResNet用相加的方式复用前层特征,DenseNet换成了更极端的做法:每一层都把前面所有层的输出拼在一起,作为自己的输入。第l层的输入通道数是k*(l-1)加初始通道数。这里的k被称为增长率(growth rate),通常取32,意思是每一层只贡献32个新通道。

这种设计的收益是梯度路径极短。反向传播时,梯度可以沿着密集连接几乎“直达”任意前层,深层的训练难度显著降低。同时它天然带正则化效果,同样的参数量下,DenseNet不容易过拟合,这也是为什么它参数没比MobileNet大多少,精度却能压过VGG16。

代价也明显:显存开销不比ResNet小。因为每一层的输出特征图都要保留给后面所有层拼接,训练时中间状态非常多。我曾在单卡上试过DenseNet-121配大batch size,结果先爆的不是算力,而是显存。推理时相对好一些,但训练时如果显存紧凑,优先考虑ResNet而不是DenseNet。

import torchvision model = torchvision.models.densenet121(weights=torchvision.models.DenseNet121_Weights.DEFAULT) num_classes = 10 model.classifier = torch.nn.Linear(model.classifier.in_features, num_classes) print(model.classifier.in_features) # 1024

DenseNet的比较有迷惑性:它参数量只有800万左右,但显存占用不算小。你如果只看参数量就认定它“轻量”,上服务器一跑会愣住,这个坑希望你别踩。

4.2 MobileNet:把卷积拆开,计算量立刻降一个量级

MobileNet和前面几个走的路完全相反:精度不是唯一目标,要在手机和嵌入式设备上跑起来才是目标。它的核心操作叫深度可分离卷积(Depthwise Separable Convolution),把标准卷积拆成两步。

第一步是深度卷积,对输入的每个通道单独用一个3x3卷积做空间滤波,不跨通道。第二步是点卷积,用1x1卷积把所有通道信息融合起来。标准卷积的计算量大约正比于输入通道数乘以输出通道数再乘以卷积核尺寸;拆开之后,深度卷积只处理逐通道的空间信息,点卷积只处理通道混合,整体计算量大约降到标准卷积的八分之一到九分之一。

MobileNet还通过两个超参数控制规模和速度:宽度乘子α负责等比例缩放所有通道数,常用0.25到1.0;分辨率乘子ρ负责缩放输入图像尺寸。实际部署时,我一般先固定ρ,再试着把α从1.0降到0.5,看精度掉了多少。掉得少就说明模型本来就有冗余,可以放心压缩。

import torchvision model = torchvision.models.mobilenet_v2(weights=torchvision.models.MobileNet_V2_Weights.DEFAULT) num_classes = 10 model.classifier[1] = torch.nn.Linear(model.classifier[1].in_features, num_classes) print(model.classifier[1].in_features) # 1280

MobileNetV2在V1基础上加了两个重要改进:线性瓶颈和倒残差结构。简单说,在低维空间后接卷积,再把结果通过1x1卷积升维回高维空间,中间那块特征用线性激活而不是ReLU,避免低维信息被ReLU“撕碎”。如果你想在资源受限的平台上做分类任务,这个细节值得了解,但使用上依然是换一个分类头的事。

4.3 两个极端的共同启示

DenseNet和MobileNet一个追求信息复用最大化,一个追求计算量最小化,放在一起看特别有意思。前者证明了“连接方式改变比单纯加深层数更有效”,后者证明了“计算不是越密越好,拆开反而更省”。之后的EfficientNet、SENet都能看到这两条思路的影子。

5. PyTorch落地实操:预训练、迁移学习与几个容易踩的坑

5.1 新版torchvision的weights API,别再写pretrained=True

如果你是照着老教程写的,很可能会看到一行pretrained=True。在torchvision 0.13之后,这个参数已经被标记为弃用,官方强烈建议用weights参数。原因也很实际:同一个架构可能会有多套预训练权重,pretrained=True只能默认选其中一套,换成weights可以明确指定版本。

import torchvision # 推荐写法(torchvision新版本) model = torchvision.models.resnet50(weights=torchvision.models.ResNet50_Weights.DEFAULT) # 或者明确指定具体版本 model = torchvision.models.resnet50(weights=torchvision.models.ResNet50_Weights.IMAGENET1K_V2) # 不推荐但还能跑的旧写法 # model = torchvision.models.resnet50(pretrained=True)

DEFAULT会自动指向当前推荐的权重。注意不同模型可能对应不同预处理输入尺寸,我用torchvision.models里每个模型自带的transforms来生成预处理方案,比自己手写归一化参数靠谱——GoogLeNet和Inception v3这类输入尺寸不同的模型,特别容易被手写预处理带坑里。

from torchvision.models import ResNet50_Weights weights = ResNet50_Weights.DEFAULT preprocess = weights.transforms()

这个transforms()会帮你把图像缩放、裁剪、归一化一次性配好。如果你非要手写,ImageNet标准均值方差是mean=[0.485, 0.456, 0.406]、std=[0.229, 0.224, 0.225],绝大多数官方预训练模型都按这个来。

5.2 迁移学习的两个阶段:只训分类头 vs 全模型微调

迁移学习基本分两档。第一档是数据集特别小、或者只是想快速验证想法,那就冻结全部backbone,只训练新换的分类层。第二档是你的数据集和ImageNet领域差异比较大,或者数据量足够,可以把整个网络解冻做微调,但学习率必须小。

代码上区分很简单:

# 阶段一:冻结backbone for param in model.parameters(): param.requires_grad = False # 替换分类层后,新层的requires_grad默认是True model.fc = torch.nn.Linear(2048, num_classes) # 优化器只更新需要梯度的层 optimizer = torch.optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lr=1e-3)

这里有个容易忽视的细节:如果你冻结全部backbone,但backbone里的BatchNorm仍然处于训练模式,它会继续更新自身的running mean和running variance。虽然不会影响你刚换上去的分类层训练,但如果你中途又想解冻微调,BN统计量可能已经被污染了。更稳妥的做法是冻结阶段就把BN也踢出训练模式:

def set_bn_eval(model): for m in model.modules(): if isinstance(m, torch.nn.BatchNorm2d): m.eval() # 在训练循环外部调用一次 set_bn_eval(model)

第二档微调时,学习率建议从5e-5到1e-4起步,Adam优化器配合weight_decay,比直接用1e-3稳得多。我见过太多人解冻整个网络还开1e-3的学习率,跑两三个epoch就把ImageNet预训练的特征冲得七零八落,最后精度反而不如只训分类头。

5.3 输入尺寸、辅助分类器、分类头位置:三个容易翻车的地方

先说输入尺寸。AlexNet、VGG、GoogLeNet、ResNet、DenseNet、MobileNetV2基本都按224x224输入设计,但Inception v3是299x299。如果你用了ResNet50_Weights.DEFAULT.transforms(),它内部会替你处理;否则你会看到mat1 and mat2 shapes cannot be multiplied这类维度报错。

再说辅助分类器。除了GoogLeNet带有辅助分类器以外,有些Inception变体也有类似结构。训练时输出可能不是一个张量,我前面已经演示过取.logits的处理方法。如果你只是拿预训练模型做推理,一般是安全的,因为推理时辅助分支不参与。

最后说分类头替换位置,这是新手最容易找错的地方:

模型分类层位置示例替换代码
AlexNetclassifier[6]model.classifier[6] = nn.Linear(4096, num_classes)
VGG16classifier[6]model.classifier[6] = nn.Linear(4096, num_classes)
GoogLeNetfcmodel.fc = nn.Linear(1024, num_classes)
ResNet50fcmodel.fc = nn.Linear(2048, num_classes)
DenseNet121classifiermodel.classifier = nn.Linear(1024, num_classes)
MobileNetV2classifier[1]model.classifier[1] = nn.Linear(1280, num_classes)

不知道怎么确定的,最笨也最有效的办法是打印一遍模型结构:

print(model)

一眼看到最后一层长什么样,再决定改哪里。这比背代码快得多。

5.4 训练配置与超参参考

关于优化器和学习率,我没有一套放之四海而皆准的参数,但有几条经验值得记录。

SGD+momentum在ImageNet大模型从头训练时还是经典配方,初始学习率0.1、动量0.9、weight decay 1e-4,配合余弦退火或者阶梯下降。迁移学习场景下,我常用AdamW,初始学习率从1e-4到3e-4,weight decay取1e-4到5e-4,然后用ReduceLROnPlateau或者CosineAnnealingLR让学习率慢慢降下去。

Batch size方面,BatchNorm对batch size比较敏感,小于16的时候BN统计量抖动很大,精度容易翻车。如果显存放不下,优先用梯度累积把有效batch撑到32以上,而不是硬用batch size 8训练。数据增强方面,RandomResizedCrop(224)加RandomHorizontalFlip()是经典组合,再激进一点的RandAugment或MixUp不一定每次都有正收益,我会在固定模型结构后做一组开关对比再决定。

6. 同批数据对比跑分:模型不是越多越好,关键是选型

6.1 一次公平对比的实验口径设计

我拿一个十类的自定义数据集做对比,每类图数量控制在500张左右。这不是标准基准,但足够说明趋势。固定训练轮数、固定输入尺寸、固定Batch size为32,只用官方预训练权重做微调,得到的相对关系才可参考。你要是想自己复现,建议也把训练轮数、优化器、学习率策略全部锁死,只改模型名,否则对比结果会被超参差异污染。

6.2 对比结果的核心结论

衡量维度AlexNetVGG16GoogLeNetResNet50DenseNet121MobileNetV2
收敛速度较慢慢中等较快较快中等
准确率上限最低中等中上高接近ResNet稍低于ResNet
单轮训练耗时(基准=1)约0.5约2约1.21约1.4约0.6
显存压力小大中等中等偏大小
调参难度低低中等低中等低

跑完我最明显的感受是:VGG16的糟糕效率在自定义数据上被放大了。它参数最多、速度最慢、精度还不占优,除了做特征可视化,真的不建议在分类任务上首选。AlexNet的精度垫底,但结构简单、训练快,拿来跑通流程、调试代码很合适。GoogLeNet在中型数据集上表现不错,多尺度结构对小目标友好。ResNet50是综合最省心的,DenseNet121参数少但显存没省下来,MobileNetV2精度只比ResNet50低几个点,速度和显存却是最大优势。

6.3 选型决策建议

直接给你一个可抄的路径。数据量小、想快速出基线,先跑ResNet50,只训分类头;部署目标是手机或者边缘设备,直接考虑MobileNetV2/V3加量化,没必要拿ResNet硬扛;参数存储有严格限制,但训练资源充足,DenseNet121值得一试;任务里小目标多、或者输入图像本身就很大,比如遥感、医疗影像,GoogLeNet/Inception的多尺度结构会更从容;如果单纯是为了学习CNN特征层次、做可视化,VGG16依然是很好的教材。

我个人现在做项目,90%的情况是ResNet50开局,等摸清数据难度再考虑是否换成MobileNet或DenseNet。把一个模型的脾气摸透比同时浅尝六个模型更出活。

六个网络跑下来以后,我最大的体会是:模型结构和你的数据条件、部署条件是强绑定的,没有绝对最优。每个名字背后都对应一个明确的工程权衡,而PyTorch只是把这份权衡摊开了放在你面前,你用哪个,取决于你想在哪一头省钱。

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

数字人源码包实战:从跑通到口型同步调参避坑指南

简介:这份数字人源码下载包面向对虚拟角色生成、人机交互感兴趣的开发者与研究人员,尤其适合具备一定前端或小程序开发基础、希望从源码层面理解数字人实现机制的学习者。资源包共129个文件,压缩后约687KB,以JavaScript逻辑脚本、…

作者头像 李华
网站建设 2026/10/17 5:49:25

Vue面试核心原理与高频题拆解:响应式、虚拟DOM、diff与组件通信

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

作者头像 李华
网站建设 2026/10/17 5:49:09

HarmonyOS实战:算术闯关App串联状态管理、路由与组件生命周期

做了几十个独立的小功能之后,很多人会遇到一个共同的瓶颈:单个知识点都看得懂,但真要组合成一个完整应用,脑子里还是一团浆糊。我身边就有同事卡在“组件会写了,路由也会跳了,状态管理背了一堆概念&#xf…

作者头像 李华
网站建设 2026/10/17 5:48:54

Kotlin 与 Flutter 移动端开发选型深度对比:从原理到实践

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

作者头像 李华
网站建设 2026/10/17 5:48:27

Spring Boot+Shiro+CAS单点登录生产实践:多应用与分布式会话

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

作者头像 李华
网站建设 2026/10/17 5:48:25

ARMv8-M TrustZone 双世界工程从零实现:SAU配置与安全调用指南

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

作者头像 李华