cs231n作业1:KNN向量化实现与调参实战笔记
1. 为什么KNN值得单独拿出来写一篇笔记很多人第一次接触cs231n作业1的时候注意力都被后面的两层神经网络或者图像特征部分吸引走了觉得KNN这个算法太简单sklearn调个包就完事了。但我实际做下来发现KNN这部分恰恰是整个作业1里最能帮你建立向量化思维和距离度量直觉的模块。它看起来简单但当你真的要从零实现一个不依赖任何循环的向量化版本时坑一个接一个。这篇笔记面向的是正在做或者准备做cs231n作业1的读者也适合任何想真正搞懂KNN底层实现细节的人。我会把KNN在CIFAR-10上的完整流程拆开讲——从数据加载、距离矩阵计算、交叉验证调参到最终的可视化验证。重点不是告诉你调这个函数就行而是把每一步为什么这么做、参数怎么选、踩过什么坑都讲清楚。KNN的核心思想其实一句话就能说清给定一张测试图片找到训练集中跟它最像的K张图片然后让这K张图片投票决定测试图片的类别。但最像怎么定义、K张怎么选、投票怎么投每一个环节都有讲究。下面我按实际做作业的顺序一步步展开。2. 数据准备与预处理的关键细节2.1 CIFAR-10数据集的加载与结构理解cs231n作业1用的是CIFAR-10数据集包含60000张32x32的彩色图片分10个类别每个类别6000张。官方给的数据格式是pickle文件加载之后你会得到一个字典里面有几个关键的keydata、labels、batch_label。data的形状是N×3072这里的3072就是32×32×3展开成一维的结果。这里第一个容易困惑的点是为什么是3072而不是32×32×3分开存因为后续计算距离的时候我们需要把每张图片当成一个3072维的向量来处理。展开成一维之后两张图片之间的欧氏距离就是这3072个维度上差值的平方和开根号。这个展开顺序是RGB通道分别展平的也就是说前1024个数是R通道中间1024个是G通道最后1024个是B通道。你在做可视化的时候必须按这个顺序reshape回去否则颜色会乱掉。加载数据的代码大概长这样import pickle import numpy as np def load_cifar10_batch(filepath): with open(filepath, rb) as f: datadict pickle.load(f, encodingbytes) X datadict[bdata] Y datadict[blabels] X X.reshape(10000, 3, 32, 32).transpose(0, 2, 3, 1).astype(float) Y np.array(Y) return X, Y注意这里有个transpose(0, 2, 3, 1)的操作把原来的(N, C, H, W)转成(N, H, W, C)。为什么要转因为后面可视化的时候用matplotlib显示图片它期望的格式是(H, W, C)。如果你不转显示出来的图片颜色通道就错位了。2.2 数据划分与采样策略作业要求把50000张训练集再拆出一部分做验证集。标准做法是取最后5000张作为验证集前45000张作为训练集。但实际调试的时候我强烈建议你先从训练集里采样一个很小的子集来跑通流程比如只取500张训练、100张测试。原因很简单KNN的计算量随训练集大小线性增长你如果一上来就用45000张跑光是距离矩阵计算就能让你等好几分钟调参的时候根本受不了。采样的时候要注意类别平衡。CIFAR-10每个类别的样本数是一样的如果你随机采样导致某些类别样本过少验证准确率会波动很大。我一般用每个类别取相同数量的方式来做子采样这样得到的准确率更有参考价值。还有一个细节数据要不要归一化对于KNN来说如果你用的是欧氏距离特征的尺度会直接影响距离计算。CIFAR-10的像素值范围是0到255不同通道之间的尺度差异不大所以归一化不是必须的。但如果你后面要跟其他特征拼接比如HOG特征那就必须做标准化否则量纲大的特征会主导距离计算。我在做作业1的时候试过归一化和不归一化在纯像素上差别不大准确率波动在1%以内。3. 距离计算的两种实现与性能对比3.1 双循环版本的实现与理解KNN最核心的计算就是测试样本和训练样本之间的距离矩阵。假设训练集有N张图测试集有M张图那距离矩阵的维度就是M×N。最直观的实现是两层循环def compute_distances_two_loops(X_train, X_test): num_test X_test.shape[0] num_train X_train.shape[0] dists np.zeros((num_test, num_train)) for i in range(num_test): for j in range(num_train): diff X_test[i] - X_train[j] dists[i, j] np.sqrt(np.sum(diff ** 2)) return dists这个版本最好理解就是老老实实算每对图片之间的欧氏距离。但它的性能极差在45000×5000的规模下Python的双层循环大概要跑好几分钟。不过它的价值在于你可以用它来验证后面向量化版本的正确性。我每次写完向量化版本都会先用小规模数据跟双循环版本对比一下确保结果一致。这里有个小坑np.sqrt(np.sum(diff ** 2))和np.linalg.norm(diff)是等价的但后者在某些numpy版本上会慢一点点。另外如果你不需要开根号比如只比较距离大小可以省掉sqrt因为平方距离的排序和距离的排序是一样的。但在KNN里我们需要真实的距离值来做阈值判断所以还是得开。3.2 单循环版本的优化思路单循环版本的核心思路是每次处理一张测试图片但把跟所有训练图片的距离计算向量化。def compute_distances_one_loop(X_train, X_test): num_test X_test.shape[0] num_train X_train.shape[0] dists np.zeros((num_test, num_train)) for i in range(num_test): diff X_train - X_test[i] dists[i, :] np.sqrt(np.sum(diff ** 2, axis1)) return dists这里的关键是X_train - X_test[i]利用了numpy的广播机制得到一个(N, 3072)的矩阵然后沿axis1求和得到N个距离值。这个版本比双循环快了几十倍因为内层循环被numpy的底层C实现替代了。但要注意内存问题当N45000的时候X_train - X_test[i]会临时创建一个45000×3072的矩阵大概占500MB内存。如果你的机器内存不够这个操作可能会触发swap反而变慢。我一般会确保至少有8GB可用内存再跑这个版本。3.3 完全向量化版本的数学推导完全向量化版本是作业1里最精华的部分也是面试常考的点。核心公式是$$||a - b||^2 ||a||^2 ||b||^2 - 2a \cdot b$$把测试集矩阵记为$X_{test}$M×D训练集矩阵记为$X_{train}$N×D那么距离矩阵的平方可以写成$$D^2 \text{sum}(X_{test}^2, \text{axis}1)[:, \text{None}] \text{sum}(X_{train}^2, \text{axis}1)[\text{None}, :] - 2 X_{test} X_{train}^T$$对应的代码def compute_distances_no_loops(X_train, X_test): train_sq np.sum(X_train ** 2, axis1) test_sq np.sum(X_test ** 2, axis1) cross X_test.dot(X_train.T) dists np.sqrt(test_sq[:, None] train_sq[None, :] - 2 * cross) return dists这个版本跑45000×5000的距离矩阵只需要几秒钟。但有一个隐藏的坑由于浮点数精度问题test_sq[:, None] train_sq[None, :] - 2 * cross可能会算出很小的负数比如-1e-12然后sqrt就会产生nan。解决办法是在sqrt之前加一个clipdists np.sqrt(np.maximum(dists, 0))这个坑我在第一次做的时候踩得很惨距离矩阵里出现了一堆nan导致后续预测全部出错排查了半天才发现是数值精度问题。3.4 三种实现的性能对比我在同一台机器上8核CPU16GB内存用5000张训练、500张测试的数据跑了三种实现结果如下实现方式耗时相对加速比内存占用双循环约45秒1x低单循环约1.2秒约37x中完全向量化约0.08秒约560x高这个对比很直观地说明了向量化的重要性。但要注意完全向量化版本在训练集特别大的时候比如全量45000张内存占用会很高因为要同时存储M×N的距离矩阵。如果内存吃紧可以考虑分块计算每次只算一部分测试样本的距离。4. 交叉验证调参与预测逻辑4.1 K值选择的交叉验证流程KNN里最重要的超参数就是K。K太小模型对噪声敏感容易过拟合K太大决策边界过于平滑容易欠拟合。作业要求用交叉验证来选K具体做法是把训练集分成5折每次用4折训练、1折验证取平均准确率最高的K。def cross_validate(X_train, y_train, k_choices, num_folds5): fold_size X_train.shape[0] // num_folds k_to_accuracies {k: [] for k in k_choices} for fold in range(num_folds): val_start fold * fold_size val_end (fold 1) * fold_size X_val X_train[val_start:val_end] y_val y_train[val_start:val_end] X_tr np.concatenate([X_train[:val_start], X_train[val_end:]]) y_tr np.concatenate([y_train[:val_start], y_train[val_end:]]) dists compute_distances_no_loops(X_tr, X_val) for k in k_choices: y_pred predict_labels(dists, y_tr, kk) acc np.mean(y_pred y_val) k_to_accuracies[k].append(acc) return k_to_accuracies这里有个实操细节交叉验证的时候每次都要重新计算距离矩阵因为训练集变了。如果你用全量数据算一次距离矩阵然后复用那是不对的因为验证集的样本在训练时不应该被看到。我一开始为了省时间想复用距离矩阵后来发现准确率偏高才意识到这个问题。K的候选值一般取[1, 3, 5, 8, 10, 12, 15, 20, 50, 100]。在CIFAR-10上我实测下来K10左右通常表现最好准确率大概在28%到30%之间。这个数字看起来很低但要知道CIFAR-10有10个类别随机猜的准确率是10%KNN能到28%已经说明像素层面的相似性确实包含了一些判别信息。4.2 预测函数的实现与投票机制预测函数的逻辑是对每个测试样本找到距离最近的K个训练样本然后看这K个样本里哪个类别最多就预测为哪个类别。def predict_labels(dists, y_train, k1): num_test dists.shape[0] y_pred np.zeros(num_test) for i in range(num_test): closest_y y_train[np.argsort(dists[i])[:k]] y_pred[i] np.argmax(np.bincount(closest_y)) return y_pred这里有几个细节值得说。np.argsort返回的是排序后的索引取前K个就得到了最近的K个训练样本的索引。np.bincount统计每个类别出现的次数np.argmax取最多的那个。但np.bincount要求输入是非负整数如果你的标签是浮点数得先转成int。还有一个投票平局的问题如果K个邻居里两个类别的票数一样多np.argmax会返回索引较小的那个。这在实践中影响不大但如果你追求严谨可以随机选一个或者按距离加权投票。距离加权投票的思路是距离越近的邻居话语权越大权重可以取距离的倒数。我试过加权投票在CIFAR-10上准确率提升了不到0.5%但计算量增加了一些所以作业里用简单投票就够了。4.3 准确率评估与结果解读最终在测试集上的准确率用K10大概能到28%左右。这个结果说明两个问题第一原始像素层面的KNN确实能捕捉到一些类别间的差异比如汽车和船的像素分布差异比较大第二KNN的天花板很低因为像素距离并不能很好地反映语义相似性一张稍微平移过的猫的图片在像素空间里可能跟另一张猫的图片距离很远。作业里还要求可视化一些错误分类的案例。我看了之后发现很多错误是因为背景干扰。比如一张马的图片背景是大片绿色草地KNN可能会把它分到鹿或者青蛙类别因为绿色像素占比很高。这也解释了为什么后面要引入颜色直方图或者HOG特征来提升KNN的表现。5. 实操中踩过的坑与排查技巧5.1 距离矩阵出现nan的排查前面提到过完全向量化版本因为浮点精度问题可能产生nan。排查方法很简单在sqrt之前检查是否有负数。dists_sq test_sq[:, None] train_sq[None, :] - 2 * cross print(np.min(dists_sq)) # 如果输出是负数说明有精度问题解决办法就是加np.maximum(dists_sq, 0)。这个坑的隐蔽性在于它只在某些数据上出现而且nan会传播到后续所有计算导致准确率变成0或者报错。我建议在写完向量化版本后先用小数据跑一遍检查距离矩阵里有没有nan。5.2 内存不足与分块计算当训练集是全量45000张、测试集是5000张的时候距离矩阵是5000×45000每个元素是float64大概占1.8GB内存。加上中间变量峰值内存可能超过4GB。如果你的机器内存不够可以分块计算def compute_distances_chunked(X_train, X_test, chunk_size500): num_test X_test.shape[0] num_train X_train.shape[0] dists np.zeros((num_test, num_train)) train_sq np.sum(X_train ** 2, axis1) for start in range(0, num_test, chunk_size): end min(start chunk_size, num_test) X_chunk X_test[start:end] test_sq np.sum(X_chunk ** 2, axis1) cross X_chunk.dot(X_train.T) dists[start:end] np.sqrt(np.maximum( test_sq[:, None] train_sq[None, :] - 2 * cross, 0)) return dists这样每次只处理500张测试图片内存占用降到原来的十分之一。代价是稍微慢一点因为循环次数增加了但总体还是比单循环快很多。5.3 交叉验证中的常见错误交叉验证最容易犯的错误是数据泄露。比如你在划分折之前就对整个训练集做了归一化那验证集的信息就泄露到了训练过程中。正确的做法是先划分折然后在每折内部单独做归一化用训练折的均值和方差去标准化验证折。另一个错误是K值选择时的过拟合。如果你用测试集来选K那选出来的K在测试集上表现很好但这不是真实的泛化性能。作业里明确要求用验证集选K最后再用测试集评估一次。我见过有人直接用测试集调参然后报告测试准确率这是不严谨的。5.4 常见问题速查表问题现象可能原因解决方法距离矩阵出现nan浮点精度导致负数开根号sqrt前加np.maximum(dists, 0)准确率异常低接近10%距离计算错误或标签错位用双循环版本对比验证内存溢出距离矩阵太大分块计算或减小数据集交叉验证准确率波动大折数太少或数据不平衡增加折数或分层采样预测结果全为同一类K值过大或距离矩阵全零检查K值和距离计算运行速度极慢用了双循环或单循环改用完全向量化版本6. 从KNN延伸出的思考与后续优化方向KNN在CIFAR-10上的准确率虽然不高但它是一个很好的基线。我在做完KNN之后尝试了几个改进方向这里分享一下实际效果。第一个方向是换距离度量。欧氏距离对像素的绝对差异敏感如果图片整体亮度有偏移距离就会很大。换成余弦距离或者曼哈顿距离在某些类别上会有提升。我试过曼哈顿距离准确率大概提升了1%左右但计算量增加了因为曼哈顿距离不能像欧氏距离那样用矩阵乘法加速。第二个方向是降维。3072维的像素空间里很多维度是冗余的。用PCA降到100维左右KNN的准确率基本不变但速度快了很多。这个思路在后面做特征工程的时候很有用因为HOG特征维度更高降维能显著减少计算量。第三个方向是特征工程。作业1后面会要求你实现HOG特征把HOG特征拼到像素特征上KNN的准确率能提升到40%左右。这说明KNN的性能瓶颈不在分类器本身而在特征的表征能力。同样的KNN换一组更好的特征效果就能大幅提升。我在实际做作业的过程中最大的体会是不要因为KNN简单就跳过它。它涉及的向量化计算、距离度量、交叉验证、超参数调优都是后面更复杂模型的基础。你把KNN这部分吃透了后面写两层神经网络的时候很多代码结构和调试思路都是相通的。最后分享一个小技巧在调试KNN的时候先用100张训练、20张测试的小数据跑通全流程确认准确率合理比如K1时训练集上应该接近100%然后再逐步扩大数据量。这样能快速定位问题是出在算法逻辑还是数据规模上。我一开始直接上全量数据结果跑了十分钟才出结果发现准确率不对又得重新跑浪费了很多时间。小数据快速迭代大数据最终验证这个节奏在做任何机器学习作业的时候都适用。

相关新闻

机器视觉基石:相机标定技术全解析

机器视觉基石:相机标定技术全解析

相机标定是机器视觉领域不可替代的基础性核心技术,其精度直接决定了整个视觉系统的测量精度和可靠性。在智能制造全面升级的今天,从3C电子的芯片引脚检测到汽车制造的焊接机器人引导,从自动驾驶的环境感知到医疗影像的微创手术导航&#xff0…

2026/10/16 13:43:30 阅读更多 →
微信骰子功能随机数生成机制与合规实现方案

微信骰子功能随机数生成机制与合规实现方案

1. 项目背景与需求解析微信骰子功能作为社交娱乐场景中的经典互动工具,其随机性机制一直备受用户关注。20260115版本中,微信对骰子功能的底层实现进行了技术升级,这为研究其点数生成规律提供了新的观察窗口。本文将基于逆向工程视角&#xff…

2026/10/15 10:50:49 阅读更多 →
DeepSeek在Linux下的手动部署:环境准备、推理引擎与显存调优

DeepSeek在Linux下的手动部署:环境准备、推理引擎与显存调优

简介:《DeepSeek 在 Linux 系统下的手动部署步骤与注意事项》是一份面向深度学习开发者及 Linux 运维人员的 PDF 操作指南,聚焦如何在 Ubuntu 等系统中从零完成 DeepSeek 的安装、配置与运行,适合准备用 GPU 加速训练或推理的读者参考。资源共…

2026/10/16 15:31:16 阅读更多 →

最新新闻

YM Services文旅APP以垫付模式适配“即兴出行”消费趋势

YM Services文旅APP以垫付模式适配“即兴出行”消费趋势

​旅游消费者的决策行为正在发生深刻变化。艾瑞咨询发布的《2026年在线旅游平台用户洞察研究报告》显示,旅游消费决策周期呈现缩短趋势,Z世代青年群体的决策周期仅1-3天,高度依赖短视频种草和即时下单。QuestMobile 2026年文旅经济洞察报告指出,出游往情感需求方向延伸,出行方式…

2026/10/16 22:49:15 阅读更多 →
2026年智能语音机器人,依托Synerow智能体平台驱动电话业务

2026年智能语音机器人,依托Synerow智能体平台驱动电话业务

本文核心要点摘要:智能语音机器人正从单一外呼工具向电话业务智能体演进。合力亿捷以自研Synerow全渠道客户联络智能体平台为底座,将二十余年电话客服经验沉淀为Agent执行能力,在对话拟人度、系统衔接、过程追踪与通信部署等维度形成完整方案…

2026/10/16 22:49:15 阅读更多 →
三自由度并联机器人工作空间求解:MATLAB从散点到可用边界复现

三自由度并联机器人工作空间求解:MATLAB从散点到可用边界复现

简介:这份资源面向机器人学与自动化工程方向的学习者和研究者,聚焦三自由度并联机器人工作空间的求解问题。并联机器人凭借结构紧凑、刚度高等优势,广泛用于精密制造、航空航天与医疗设备,而工作空间作为其可达位置的集合&#xf…

2026/10/16 22:49:15 阅读更多 →
综合布线验收实测指南:11801-1标准等级参数与现场避坑

综合布线验收实测指南:11801-1标准等级参数与现场避坑

简介:ISO/IEC 11801-1:2017国际标准原版PDF,适用于网络布线工程师、IT基础设施规划人员及数据中心运维人员,用于规范客户场所通用布线系统的设计与实施。标准涵盖总体要求、术语定义、布线系统设计安装、测试验收及维护管理等内容&#xff0c…

2026/10/16 22:49:15 阅读更多 →
华为云ModelArts Baseline实战避坑指南:从OBS配置到训练排错

华为云ModelArts Baseline实战避坑指南:从OBS配置到训练排错

1. 这不是“抄作业”,而是帮你绕开37个坑的实战手记“华为云-开发者AI青年班黑客松大赛Baseline操作教程”——看到这个标题,你大概率正站在两个路口:一边是刚报名完、对着控制台发懵的新手,一边是赶在截止前想快速跑通baseline、…

2026/10/16 22:49:15 阅读更多 →
Nanosaur2-670M的DiT架构深度解析:SPRINT稀疏块、2D RoPE、QK-norm与670M参数设计哲学

Nanosaur2-670M的DiT架构深度解析:SPRINT稀疏块、2D RoPE、QK-norm与670M参数设计哲学

大模型人工智能媒体生成计算机视觉基础模型本地部署 【免费下载链接】Nanosaur2-670M 项目地址: https://ai.gitcode.com/hf_mirrors/well9472/Nanosaur2-670M 点击查看 免费下载 Nanosaur2-670M 是一个仅花 600 美元算力训练出的 670M 参数插画文生图 DiT&#xf…

2026/10/16 22:48:15 阅读更多 →

日新闻

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

个人 AI Agent 平台接入 TaoToken:统一 Key 与 API 通道的配置大纲

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

2026/10/16 0:00:05 阅读更多 →
信号与系统频域分析作业解题指南

信号与系统频域分析作业解题指南

1. 作业背景与核心要求解析信号与系统作为电子信息类专业的核心基础课,其作业设计往往直接关联课程的核心知识框架。2026年春季第六次作业的布置,通常对应着课程进展到频域分析的关键阶段。根据常规教学进度推测,本次作业可能涉及以下核心内容…

2026/10/16 0:00:05 阅读更多 →
Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

Django MVT实战:从零构建图书借阅系统,彻底搞懂Model-View-Template分层

1. 从一个"四不像"的练手项目说起如果你已经看完了Django的官方教程前几章,跟着敲完了投票应用,大概会有一种"我好像会了,又好像什么都不会"的割裂感。教程里每个知识点都是拆开的——这一章讲模型,下一章讲视…

2026/10/16 0:00:05 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/16 21:35:51 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/15 19:41:51 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/16 15:04:24 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

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

2026/10/16 21:54:38 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

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

2026/10/16 3:49:43 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

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

2026/10/16 14:23:36 阅读更多 →