news 2026/9/9 22:58:49

CIFAR-10图像分类实战:轻量CNN训练与调参全记录

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
CIFAR-10图像分类实战:轻量CNN训练与调参全记录

上个月刚在MNIST上跑完第一个CNN项目,这个月我就直接把目标换成了CIFAR-10。选择CIFAR-10作为第二个深度学习项目,其实是个很经典的进阶路径:它比MNIST难了一个档次,又没有难到必须上ResNet这种大网络才能跑动的地步。CIFAR-10配合自己手搭的轻量CNN,几乎是每个想从“会跑代码”走向“理解CNN到底在做什么”的人都会经过的一站。这篇文章记录的是我这次从环境配置、数据加载、网络设计、训练调参到踩坑解决的完整过程,尤其是那些教程里很少写、但上手一定会碰到的细节。如果你也刚跑完手写数字识别,想试试真正的彩色图片分类,这篇文章应该能帮你省下不少折腾的时间。

1. 从MNIST到CIFAR-10:第二个项目跨过的那道坎

1.1 两个数据集到底差在哪

很多初学者对CIFAR-10的理解就是“图片稍微复杂一点的MNIST”,实际动手才发现根本不是一回事。我在项目开始前把两个数据集放在一起做了个对比,这个对比决定了后续所有设计思路。

对比项MNISTCIFAR-10
图片尺寸28×2832×32
通道数1(灰度)3(RGB彩色)
类别数1010
训练集图片数6000050000
测试集图片数1000010000
图片内容手写数字飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船、卡车
背景复杂度背景干净背景复杂,物体位置和大小不固定
类间差异差异很大存在猫和狗、鹿和马这类相似对

注意看,通道数从1变成3,意味着第一个卷积层的输入从单通道矩阵变成了三通道张量,卷积核的数量和计算方式都要跟着变。图片尺寸从28×28变成32×32,只多了4个像素,但物体不再居中,同一类别的样本可能出现在画面的任何位置,还可能带有复杂背景干扰。

CIFAR-10真正难的地方不是图片分辨率,而是类间相似性。“猫”和“狗”在很多样本里形态接近,“汽车”和“卡车”轮廓也像,这种细粒度差异在MNIST里几乎不存在。手写数字1和7虽然也有相似之处,但相比猫狗、鹿马这些现实物体,区别度还是高多了。所以,MNIST上随便一个简单网络都能轻松跑出99%的准确率,而CIFAR-10用轻量模型想上80%都需要认真调一轮。

1.2 第二个项目要定什么目标,别一上来就追高精度

CIFAR-10的SOTA准确率早就超过99%了,但第二个项目如果直接把目标定成“追上SOTA”,方向就偏了。想刷高精度最简单的方式是加载EfficientNet或Vision Transformer的预训练模型,但那等于跳过了所有该学的底层知识。

我给这次项目定的目标非常朴素,拆成四条:

  1. 跑通完整流程:数据下载、预处理、网络定义、训练、评估、保存权重,一个环节都不能少。
  2. 搞清楚每个模块为什么存在,包括BatchNorm、Dropout、池化、激活函数各自在干什么。
  3. 在轻量CNN上打到一个合理基线,80%上下是一个比较现实的目标。
  4. 学会看训练曲线定位问题,能判断欠拟合、过拟合、学习率不合适等情况。

这里有个很实际的建议:第一次跑CIFAR-10,不要一上来就堆数据增强、学习率调度、预训练权重这些高级手段。全加上的话,最后模型是能到85%甚至更高,但中间出了问题你完全不知道是哪一环引起的。先做一个不加任何trick的baseline,后续再逐步加东西,每一步的效果变化都清清楚楚。

2. 环境配置与数据集准备:两个容易被低估的环节

2.1 版本对齐比安装本身更麻烦

我在做第一个项目时用的是CPU版本PyTorch,这个项目开始前先升级到了CUDA版本。环境配置这块最大的坑不是“不会装”,而是“装完之后发现版不齐”,各种莫名其妙的CUDA报错基本都是这个原因。

我这次用的是这套组合:

组件版本说明
Python3.9.18兼容性比较稳
PyTorch2.1.2当前主力版本
torchvision0.16.2自带CIFAR-10数据集和常用预处理方法
CUDA12.1和PyTorch的cu121构建对应
numpy / matplotlib最新即可数据处理和画图

安装PyTorch时最容易踩的坑是:明明显卡驱动支持CUDA,但用默认命令pip install torch torchvision装到的是CPU版本。装了CPU版之后代码也能跑,只是慢到怀疑人生,而且torch.cuda.is_available()会返回False。正确的做法是在PyTorch官网找到对应CUDA版本的安装命令,指定index-url安装,比如:

pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121

装完之后做一步验证,确认环境没问题再继续:

import torch print(torch.__version__) print(torch.cuda.is_available()) print(torch.cuda.get_device_name(0))

如果打印结果是True和你的显卡型号,环境就算齐了。如果返回False,说明CUDA版本不匹配或者装成了CPU版,建议直接重装。

2.2 CIFAR-10下载的两种姿势与DataLoader细节

torchvision里直接封装了CIFAR-10的下载和读取,代码非常简洁:

from torchvision import datasets train_dataset = datasets.CIFAR10( root='./data', train=True, download=True, transform=transforms.ToTensor() ) test_dataset = datasets.CIFAR10( root='./data', train=False, download=True, transform=transforms.ToTensor() )

但第一次运行的时候,download=True会从官方服务器拉取约170MB的压缩包。国内网络拉到一半断掉是常有的事,下载失败后程序会报错,不会自动断点续传。

我当时的处理办法是:先用浏览器手动下载cifar-10-python.tar.gz,然后放到./data目录下,再运行上面的代码。torchvision检测到本地已经有这个文件,就会直接解压而不会重新下载。如果手动下载也慢,就去网上找一个可用的镜像地址,自己下载好之后放到正确位置即可。核心逻辑就是让本地存在一个完好的tar.gz文件,torchvision会自己校验。

数据集准备好之后,封装DataLoader时有个细节值得单独说:

from torch.utils.data import DataLoader train_loader = DataLoader( train_dataset, batch_size=128, shuffle=True, num_workers=0, pin_memory=True ) test_loader = DataLoader( test_dataset, batch_size=128, shuffle=False, num_workers=0, pin_memory=True )

num_workers这个参数在Windows上非常坑。如果设置大于0,经常会出现An attempt has been made to start a new process before the current process has finished its bootstrapping phase之类的报错。我的做法是先老老实实设成0,等流程全部跑通之后再去研究多进程加速。pin_memory=True在GPU训练时能加快数据传输,但如果你用CPU训练,这个参数影响不大。

2.3 归一化与数据增强:先搞清楚为什么,再决定做不做

CIFAR-10的像素值范围是0到255,直接用原始数值喂给网络,会让梯度更新变得很不稳定。常见的做法是把像素缩放到接近0的范围内,这就是Normalize做的事。CIFAR-10有一套公开的统计值,训任何网络都可以直接用:

transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize( mean=(0.4914, 0.4822, 0.4465), std=(0.2470, 0.2435, 0.2616) ) ])

这组mean和std是CIFAR-10训练集每个RGB通道的均值和标准差,数值是官方社区统计出来的。使用它们之后,三通道数据会被标准化成近似标准正态分布,模型的收敛速度和稳定性都会有明显改善。需要提醒的是,测试集也必须做完全相同的Normalize,我见过有人训练集做了、测试集忘了做,结果测试准确率直接掉了好几个百分点。

数据增强是我在第二个项目里刻意没做的东西。不是因为它没用,而是我想先把“一张原图直接进网络”的baseline跑出来。如果一开始就加RandomCrop和RandomFlip,模型精度确实会提高,但你会分不清提高来自数据增强还是网络结构的变化。等baseline稳定之后再加增强,效果对比会更明确。

后续想加时可以参考这套配置,注意增强只加在训练集:

train_transform = transforms.Compose([ transforms.RandomCrop(32, padding=4), transforms.RandomHorizontalFlip(), transforms.ToTensor(), transforms.Normalize( mean=(0.4914, 0.4822, 0.4465), std=(0.2470, 0.2435, 0.2616) ) ]) test_transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize( mean=(0.4914, 0.4822, 0.4465), std=(0.2470, 0.2435, 0.2616) ) ])

RandomCrop的padding=4表示先把32×32的图扩到40×40,再从里面随机裁剪出32×32的区域,相当于给图片增加了随机平移。RandomHorizontalFlip按50%概率水平翻转。这两个操作对CIFAR-10效果很好,因为图片里的物体没有固定的方向性,翻转和微小平移不改变语义,却能让网络看到更多样的样本,缓解过拟合。

3. 轻量CNN的结构设计与参数量核算

3.1 为什么选轻量网络

很多人第二步项目就急着上ResNet,但我这次坚持用了轻量CNN。原因有几个:一是训练速度快,迭代试错效率高,一个网络从零开始训练30轮在GPU上只要几分钟,省下来的时间都用来分析问题了;二是轻量网络参数少一些,相比大网络更不容易严重过拟合,更适合观察真实的训练曲线;三是对初学者来说,轻量网络的结构一目了然,每一层的作用、每一个张量的形状变化都能手动算出来,真正建立起对CNN的直觉。

重型网络当然精度更高,但代价是训练时间长、参数多、调试困难。如果你连CIFAR-10在轻量模型上的过拟合现象都没亲眼见过,直接上手大网络只会更迷茫。

3.2 网络结构:三层卷积加两层全连接

我参考了VGG的思路,全部使用3×3卷积和2×2最大池化,结构如下:

  • 输入:3×32×32
  • Conv1:3→32通道,3×3卷积,padding=1,后接BatchNorm、ReLU、MaxPool
  • Conv2:32→64通道,3×3卷积,padding=1,后接BatchNorm、ReLU、MaxPool
  • Conv3:64→128通道,3×3卷积,padding=1,后接BatchNorm、ReLU、MaxPool
  • 展平后维度:128×4×4 = 2048
  • FC1:2048→512,后接ReLU和Dropout(0.5)
  • FC2:512→10

对应代码:

import torch.nn as nn class LightCNN(nn.Module): def __init__(self, num_classes=10): super().__init__() self.features = nn.Sequential( nn.Conv2d(3, 32, kernel_size=3, padding=1), nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(2), nn.Conv2d(64, 128, kernel_size=3, padding=1), nn.BatchNorm2d(128), nn.ReLU(inplace=True), nn.MaxPool2d(2), ) self.classifier = nn.Sequential( nn.Linear(128 * 4 * 4, 512), nn.ReLU(inplace=True), nn.Dropout(0.5), nn.Linear(512, num_classes) ) def forward(self, x): x = self.features(x) x = x.view(x.size(0), -1) x = self.classifier(x) return x

这里用padding=1是为了让卷积不改变特征图的宽高,只有MaxPool负责缩小尺寸。32×32经过三次MaxPool后变成4×4,特征图数量从32翻到128。这个“宽高减半、通道数翻倍”的设计思路很经典,既控制了计算量,又保留了足够的特征表达空间。

3.3 参数量的手算过程

我当时手动算了一遍参数量,这一步对理解CNN帮助极大。计算规则是:卷积层参数量 = 输入通道数 × 输出通道数 × 卷积核宽 × 卷积核高 + 输出偏置数;全连接层参数量 = 输入维度 × 输出维度 + 输出偏置数。

模块输入形状输出形状参数量
Conv1(3, 32, 32)(32, 32, 32)3×32×3×3 + 32 = 896
Conv2(32, 16, 16)(64, 16, 16)32×64×3×3 + 64 = 18496
Conv3(64, 8, 8)(128, 8, 8)64×128×3×3 + 128 = 73856
BN1/2/3--2×(32+64+128) = 448
FC120485122048×512 + 512 = 1049088
FC251210512×10 + 10 = 5130
合计--约1.15M

加起来大约115万个可训练参数。对比一下,VGG16的参数规模约1.38亿,ResNet18约1120万。1.15M的参数量确实是“轻量”,在CPU上都能比较快地跑完一轮训练。

拉低参数量的主要功臣是卷积的权值共享。同一个3×3卷积核在整张图上滑动,参数不会随图片变大而增加,这和全连接层完全不同。FC1的参数量之所以占了大头,就是因为2048×512的权重矩阵需要约100万个参数,可见全连接层在高维特征上非常“吃参数”,这也是为什么很多现代网络会倾向使用全局平均池化替代全连接。

3.4 激活函数、BatchNorm、Dropout各自干了什么

激活函数我选的是ReLU。原因很实际:Sigmoid在深层网络里容易造成梯度消失,靠近两端的区域导数趋近于0,反向传播时梯度乘不了几层就没了;Tanh虽然零中心化,但同样存在两端饱和问题。ReLU计算量极小,正区间梯度恒为1,收敛速度快得多。

BatchNorm解决的是中间层数据分布漂移问题。没有BN时,每一层的输入分布会随着前层参数变化而不断变化,训练容易不稳定。加了BN之后,每个batch会先做归一化再送进下一层,模型对学习率的敏感度明显下降。BN还有一个副产物:它的可训练参数(缩放和偏移)每个通道只有两个,整个网络加起来才448个,可以忽略不计。

Dropout放在全连接层之前,训练时每次前向传播随机让一半神经元失活,相当于强迫网络学到更多冗余特征,不依赖某几个特定神经元。这个策略在全连接层这种参数密集的地方尤其有效。但要记得:Dropout在推理时必须关闭,推理时如果还开着,输出会被随机性干扰,导致同样的输入每次预测结果都不一样。

4. 训练配置与过程观察:学习率、优化器与Loss曲线

4.1 超参数选型:Adam先验证,SGD再精细

训练超参数的选取要讲策略。我的做法是分两个阶段:

第一个阶段用Adam,学习率1e-3,目标是快速验证网络结构能不能收敛。Adam对学习率的敏感度低,自带自适应调整,非常适合做结构验证。如果Adam跑了好几轮loss都不降,那问题多半出在网络结构或数据预处理上,而不是优化器。

第二个阶段换SGD,加momentum和weight_decay,做最终训练。SGD+momentum在CIFAR-10这类任务上往往能比Adam拿到更高的精度上限,代价是对学习率更敏感,需要多试几个值。

我最终的训练配置大概是这样的:

超参数第一阶段(验证)第二阶段(最终)
优化器AdamSGD
学习率0.0010.01
momentum-0.9
weight_decay-5e-4
batch_size128128
epochs1030
损失函数CrossEntropyLossCrossEntropyLoss

损失函数直接用了nn.CrossEntropyLoss()。这里有一个必须注意的点:CrossEntropyLoss内部已经完成了LogSoftmax和NLLLoss的结合,所以网络最后一层输出的是原始logits,不能再手动加一层Softmax。有人在最后一层用了torch.softmax(x, dim=1)再传给CrossEntropyLoss,损失值看起来很高,准确率也上不去,因为梯度被Softmax和Loss里的Softmax重复计算了,数值稳定性反而变差。

4.2 训练与验证循环参考实现

这里给出完整的训练代码,我建议直接把它保存成一个train.py,后面的实验都基于这个框架改:

import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import datasets, transforms device = torch.device("cuda" if torch.cuda.is_available() else "cpu") transform = transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616)) ]) train_dataset = datasets.CIFAR10(root='./data', train=True, download=True, transform=transform) test_dataset = datasets.CIFAR10(root='./data', train=False, download=True, transform=transform) train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True, num_workers=0, pin_memory=True) test_loader = DataLoader(test_dataset, batch_size=128, shuffle=False, num_workers=0, pin_memory=True) model = LightCNN().to(device) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9, weight_decay=5e-4) def train_one_epoch(model, loader, optimizer, criterion, device): model.train() running_loss = 0.0 correct = 0 total = 0 for inputs, labels in loader: inputs, labels = inputs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() * inputs.size(0) _, predicted = outputs.max(1) total += labels.size(0) correct += predicted.eq(labels).sum().item() return running_loss / total, 100.0 * correct / total def evaluate(model, loader, criterion, device): model.eval() running_loss = 0.0 correct = 0 total = 0 with torch.no_grad(): for inputs, labels in loader: inputs, labels = inputs.to(device), labels.to(device) outputs = model(inputs) loss = criterion(outputs, labels) running_loss += loss.item() * inputs.size(0) _, predicted = outputs.max(1) total += labels.size(0) correct += predicted.eq(labels).sum().item() return running_loss / total, 100.0 * correct / total best_acc = 0.0 epochs = 30 for epoch in range(1, epochs + 1): train_loss, train_acc = train_one_epoch(model, train_loader, optimizer, criterion, device) val_loss, val_acc = evaluate(model, test_loader, criterion, device) print(f"Epoch {epoch:02d} | Train Loss {train_loss:.4f} | Train Acc {train_acc:.2f}% | Val Loss {val_loss:.4f} | Val Acc {val_acc:.2f}%") if val_acc > best_acc: best_acc = val_acc torch.save(model.state_dict(), "best_model.pth")

在训练循环里,outputs.max(1)返回每个样本的最大logit以及对应的索引,索引就是预测类别。predicted.eq(labels).sum()统计预测正确的数量。这段代码里的evaluate和train_one_epoch必须区分开,evaluate函数内不能用optimizer.step(),而且要在torch.no_grad()下执行,否则会白白占用显存,还可能在验证集上计算出错误的梯度。

4.3 看曲线判断训练状态

我用上面这套配置实际跑了一轮,记录了每个阶段的分界点,大致是这个表现:

EpochTrain AccVal Acc状态判断
565%58%正常上升,尚未收敛
1078%70%正常上升,仍有欠拟合
2088%76%速度放缓,开始出现过拟合苗头
3094%78%明显过拟合,train和val差距拉大

从这个曲线能学到很关键的一点:只看测试准确率是不够的,一定要同时看训练集表现。训练集94%而测试集78%,这中间16个百分点的差距,就是模型在“背诵”训练数据的具体表现。它虽然对训练集学得很好,但泛化到新数据的能力停滞了。

如果train和val都低,比如train只有60%、val只有55%,那是欠拟合,说明模型容量不够或者训练不够充分,应该加深网络或增加epoch数。如果train已经90%以上但val只有70%左右,那就是过拟合,应该加数据增强、加Dropout、加weight_decay,或者减小模型规模。只有train和val同步上升、差距不大的阶段,才是真正在学“通用特征”的阶段。

4.4 别忘了model.train()和model.eval()

我在训练循环里反复调用了model.train()model.eval(),这两个方法不是摆设。它们改变的是BatchNorm和Dropout的行为。

model.train()模式下,BatchNorm会使用当前batch的均值和方差来归一化,Dropout会随机丢弃神经元。model.eval()模式下,BatchNorm改用训练阶段累计的全局均值和方差,Dropout完全不丢神经元。如果验证时忘了切回eval模式,BatchNorm对每个batch独立归一化,测试结果会不稳定,同一个模型跑两次能得到不同的准确率。这个坑在第一次写训练循环时特别容易踩,我当时就因为这个白折腾了半个多小时。

5. 踩坑记录与后续优化方向

5.1 这次实际踩过的五个坑

第一个坑是归一化参数写错。我一开始为了省事,直接在网上复制了一组Normalize参数,没仔细看是mean还是std的数值,训练时发现loss下降特别慢。后来排查到是std写反了,修正之后收敛速度立刻正常。建议手写这组参数时,专门注释一下,并且在训练前打印一小批预处理后的数据,检查均值是否接近0、标准差是否接近1。

第二个坑是验证阶段忘了调用model.eval()。症状很迷惑:前一轮val acc是76%,下一轮同样的代码变成72%,再下一轮又变回75%,来回跳。后来才意识到是Dropout在验证时还在生效,输出带随机性。加了一行model.eval()之后曲线立刻稳定了。训练前和验证前各检查一遍train/eval状态,应该是每个训练循环的默认动作。

第三个坑和Softmax有关。我第一次用CrossEntropyLoss时觉得“分类任务最后一层当然要加Softmax”,结果在classifier的最后加了softmax,训练时loss一直偏高。原因是CrossEntropyLoss内部已经做了LogSoftmax,再把softmax后的概率喂进去,相当于计算了两次,数值分布完全变了。这个知识点理论课上都讲过,但自己动手真踩一遍才会记住。

第四个坑是Windows下DataLoader多进程报错。只要num_workers设为大于0,就会在加载数据时直接崩掉。解决办法是把训练代码包在if __name__ == "__main__":里,更省事的是直接把num_workers设为0。Linux下可以设成2或4,Windows下就老老实实用0。

第五个坑相对隐蔽:装环境时装成了CPU版本PyTorch。当时跑MNIST项目没觉得慢,换了CIFAR-10之后发现每次迭代都要等很久。排查后才发现torch.cuda.is_available()返回False,CUDA和PyTorch版本根本对不上。重装成cu121版本之后,训练时间缩短了一个数量级。任何深度学习项目开始前,先打印一下CUDA可用性,这个习惯能省掉很多不必要的等待。

5.2 从78%到85%的低门槛改进路线

在baseline跑出78%之后,我按“一次只改一个变量”的原则做了几轮改进,效果都记录下来了。如果你也想在轻量CNN基础上提升,建议按这个顺序来:

  1. 加数据增强:RandomCrop(32, padding=4) + RandomHorizontalFlip,val acc能到81%左右。这一步解决的是过拟合问题,不用改模型结构。
  2. 优化器从Adam换成SGD,加momentum=0.9和weight_decay=5e-4,学习率调到0.01,val acc约83%。SGD配合weight_decay的正则效果在CIFAR-10上比Adam更稳。
  3. 使用CosineAnnealingLR余弦退火学习率调度,让学习率在训练后期逐步下降,val acc约84%。这一步能让模型进入更精细的参数区间。
  4. 把网络加宽,通道从32-64-128改成64-128-256,参数量增加到约4.6M,val acc约85%。加宽比加深在这个数据规模下更划算。

再往上走,可以用Cutout随机遮挡、Mixup混合样本等更强的增强策略,配合更长训练周期,能摸到87%左右。但这些trick的调参成本会明显增加,而且不同随机种子下的波动也会变大。

5.3 完成第二个项目之后可以怎么走

CIFAR-10的轻量CNN项目做完之后,下一步的路径其实是开放的。比较自然的方向是:换成ResNet18从头训练,加深网络深度,观察深度带来的精度提升和训练难度;加载ResNet18的预训练权重做迁移学习,感受大规模数据集预训练给小数据集带来的巨大优势;用Grad-CAM可视化模型对样本的注意区域,看看分类决策的依据是否合理;或者用t-SNE对全连接层特征降维可视化,观察不同类别在特征空间中的分布。每一条路都能从CIFAR-10这个起点延伸出去,但建议不要同时做,一次只挑一个方向深入。

这次项目做完,我自己最大的一个体会是:深度学习入门阶段,模型复杂度从来不是越高越好。CIFAR-10在轻量CNN上虽然只有八成出头的准确率,但正因为网络足够简单,我才有机会把数据流、梯度行为、训练和推理差异这些底层的细节看得清清楚楚。第二个项目的核心价值,不在于把准确率刷到多高,而在于第一次拥有“自己设计一个网络并完整验证它”的经验。以后你再换ResNet、换YOLO、换Transformer,很多思路其实都是一脉相承的。最后再给一个小建议:做实验时保持“一次只改一个变量”的习惯,把每次实验的配置和结果记在笔记里。不然过两个星期再回来看,你根本想不起是哪个改动让准确率涨了0.5个百分点。

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

复现文本抑郁症检测项目全流程:从数据清洗到特征工程实践

简介:面向文本抑郁症检测方向,这份源代码是论文“基于文本的抑郁症检测”的配套实现,围绕文本特征提取、模型训练与结果分析搭建完整实验链路,适合自然语言处理研究者和对心理健康计算感兴趣的中高级开发者参考。压缩包共29个文件…

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

Audacity:从录音到混音,3 步掌握免费音频编辑

Audacity:从录音到混音,3 步掌握免费音频编辑 【免费下载链接】audacity Audio Editor 项目地址: https://gitcode.com/GitHub_Trending/au/audacity 你刚录完一段播客,背景里全是空调的嗡嗡声。Audacity 是一款免费开源的数字音频编…

作者头像 李华
网站建设 2026/9/9 22:53:19

ST7789V2驱动详解:从零点亮1.3寸IPS屏,字符图片显示全记录

简介:面向嵌入式开发者和电子制作爱好者,这份ST7789V2驱动示例代码展示了在SPI/I2C接口液晶屏上显示字符与图片的完整实现。压缩包一共包含2个文件,分别为C源文件与头文件,整体只有6KB,代码量非常精简。其中C源文件承担…

作者头像 李华
网站建设 2026/9/9 22:51:48

胡广书《数字信号处理》Matlab代码实战:FFT、滤波器与谱估计全解析

简介:配套胡广书《数字信号处理理论、算法与实现》的Matlab代码及参考文献合集,面向正在系统学习数字信号处理课程的高校学生与自学者。内容围绕经典数字信号处理与统计数字信号处理两大体系,覆盖离散时间信号与系统、Z变换、傅里叶变换、数字…

作者头像 李华