简介基于深度学习卷积神经网络的垃圾识别分类系统完整项目包面向高校计算机、人工智能等专业需要完成课程设计或期末大作业的学生。项目提供完整可运行的Python源码配套近2000张垃圾图片数据集和训练好的模型覆盖电池、塑料瓶等多类常见垃圾。压缩包共2000个文件其中1196张jpg与789张jpeg构成训练/验证图像数据13个py文件实现数据加载、模型训练、预测评估等流程另有json配置与md说明文档整体大小约564.69MB。目前已有263人学习使用特别适合作为高分开题参考或直接提交的课程设计成果。项目经导师指导并获得97分下载后无需修改即可复现实验帮助读者理解CNN图像分类的完整技术链路。1. 垃圾识别分类系统CNN 课程设计到底在做什么一个 97 分的课程设计项目放在面前时大多数人第一反应是「能不能跑通」第二反应才是「原理是什么」。这套基于深度学习卷积神经网络的垃圾识别分类系统本质上解决的是一个问题给定一张垃圾图片让模型判断它属于哪一类——电池、塑料瓶这类可回收物还是其他类。它不依赖人工设计的颜色、纹理特征而是让卷积神经网络从原始像素里自己学出判别依据这也是它比传统图像处理方案更稳的核心原因。这套项目拿到手里面是完整的 Python 源码、整理好的数据集和已经训练好的模型权重不需要自己再从头收集图片、标注类别下载后在本地把环境配好就能直接跑训练和推理。适合谁用很明确正在做课程设计、期末大作业或者想快速跑通一个 CNN 图像分类全流程的读者。你不需要先啃完一整本深度学习教材只要懂基本的 Python 语法和一点点 PyTorch 用法就能顺着代码把整个系统跑起来。先提醒一句课程设计类项目代码能不能跑只占一半分数另一半在于你能否说清楚网络结构为什么这么搭、数据为什么这么处理、训练参数为什么这么设。所以下文不只讲操作步骤还会把每个环节的选型理由和参数逻辑拆开方便你做答辩准备。2. 项目结构与运行准备先搞清楚资源包里有什么拿到压缩包先别急着解压运行先花十分钟把目录结构看明白。课程设计项目最怕的不是代码复杂而是你连哪个文件是入口、哪个文件是训练好的权重都分不清。下面是我通常建议的一步步操作顺序。2.1 解压后的目录分层与关键文件定位解压后典型的目录结构是这样的garbage_classification/ ├── data/ │ ├── train/ │ │ ├── battery/ # 电池图片 │ │ ├── plastic_bottle/ # 塑料瓶图片 │ │ └── ... │ ├── valid/ │ └── test/ ├── models/ │ ├── cnn_model.py # 网络结构定义 │ └── best_model.pth # 训练好的权重文件 ├── utils/ │ ├── dataset.py # 数据加载与预处理 │ └── train.py # 训练脚本 ├── predict.py # 单张图片预测入口 ├── train.py # 主训练入口 └── requirements.txt # 依赖包列表先解释一下为什么这样组织。data目录按 train/valid/test 三个子集划分这是图像分类任务的标配做法目的是让模型训练时见过的数据、调参时用的数据和最终评估用的数据完全隔离避免数据泄漏导致的虚高准确率。models/cnn_model.py和权重文件分开存放意味着你可以只加载权重做推理不必重新训练——这在答辩演示时很实用因为训练可能要跑十几分钟而加载权重做预测只需要几秒钟。依赖清单在requirements.txt里我建议先看这个文件再装环境不要上来就pip install torch乱装一通导致版本冲突。重点确认三件事PyTorch 版本、torchvision 版本、Python 版本要求。提示文件命名里带_452、_244这类编号的图片只是原始图片的序号与类别标签无关类别信息完全由所在文件夹名或者文件名里的中文类别词决定。2.2 环境配置版本匹配是运行的第一道坎根据我拆过的课程设计项目经验90% 的「代码跑不起来」都出在环境版本不匹配上。先看requirements.txt里锁定的版本号然后按下面的顺序装# 创建独立虚拟环境避免污染系统 Python conda create -n garbage_cnn python3.8 conda activate garbage_cnn # 安装 CPU 版 PyTorch无 NVIDIA 显卡时 pip install torch1.13.1cpu torchvision0.14.1cpu -f https://download.pytorch.org/whl/torch_stable.html # 安装其余依赖 pip install -r requirements.txt如果你有 NVIDIA 显卡并且已装好 CUDA把安装源里的cpu去掉即可GPU 版训练速度会快很多。这里有两个关键参数值得说明Python 3.8 和 torch 1.13.1 是这对组合里最稳妥的搭配因为torchvision的 transform 接口在 2.0 之后有部分行为变化老代码在 2.x 上可能直接报module torchvision has no attribute transforms之类的错误。装完依赖后跑一个最小验证python -c import torch, torchvision; print(torch.__version__, torchvision.__version__)能正常输出版本号说明环境这一关过了。如果报了ModuleNotFoundError优先检查是不是当前激活的环境不对或者 pip 安装到了别的 Python 解释器路径下。2.3 跑通一次预测用训练好的模型验证环境环境就绪后最快的验证方式是直接用models/best_model.pth跑一次单图预测而不是马上启动训练。这样可以先把「加载模型 → 预处理图片 → 输出类别」这条链路打通再决定要不要重新训练。python predict.py --image data/test/battery/img_电池_452.jpeg --model models/best_model.pth如果终端里输出了类似预测结果: 电池 (confidence: 0.97)的内容说明环境、模型权重、数据路径三者的配置都对了。这个环节有个实操技巧预测脚本里通常会有一个类别名到索引的映射字典比如{battery: 0, plastic_bottle: 1}注意看输出时是否把它反转成索引 → 类别名很多初版代码在这里写反导致结果看着没头没尾。3. CNN 网络构建与训练流程从数据到模型的核心链路这一章是整个系统的核心部分也是答辩时老师最爱追问的地方。CNN 不是黑匣子它的每一层都在做可解释的变换卷积层提取局部特征、池化层压缩特征尺寸、全连接层做最终分类。理解了这个逻辑你才能跟老师解释清楚为什么选这个网络结构而不是只会说「调参跑出来的」。3.1 数据加载与预处理图片不是拿过来就能喂给模型的从文件名可以看出数据集里的图片是img_电池_452.jpeg这样的命名方式类别信息嵌在文件名里。这种命名其实很有讲究它意味着你不需要额外维护一份标签 CSV直接从文件名里解析出类别即可。常见的解析做法是import os from torch.utils.data import Dataset from PIL import Image class GarbageDataset(Dataset): def __init__(self, img_dir, transformNone): self.img_dir img_dir self.transform transform self.class_names [battery, plastic_bottle] self.class_to_idx {name: idx for idx, name in enumerate(self.class_names)} self.samples [] # 遍历目录下的所有图片文件 for filename in os.listdir(img_dir): if not filename.endswith(.jpeg): continue # 从文件名中解析类别例如 img_电池_452.jpeg - 电池 # 注意这里需要把中文类别映射到英文类别 for name in self.class_names: if name in filename: label self.class_to_idx[name] self.samples.append((os.path.join(img_dir, filename), label)) break def __len__(self): return len(self.samples) def __getitem__(self, idx): img_path, label self.samples[idx] image Image.open(img_path).convert(RGB) if self.transform: image self.transform(image) return image, label这段代码的逻辑做了三件事遍历指定目录、按文件名匹配类别关键字、返回图片张量和对应的整数标签。有一个坑是中文类别名的匹配顺序——如果类别名之间存在包含关系比如「电池」和「充电电池」遍历顺序靠前的会先匹配成功导致错标。实际做的时候我会在文件名解析前先把类别列表按名称长度从长到短排序避免包含关系误匹配。预处理环节同样容易踩坑。CNN 要求输入图片尺寸固定但数据集里的原图尺寸不一常见做法是统一缩放到 224×224再做标准化。注意这里的标准化不是简单的除以 255而是用 ImageNet 数据集的均值和方差做归一化from torchvision import transforms transform_train transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(), # 随机水平翻转数据增强 transforms.ColorJitter(brightness0.2), # 亮度扰动增加鲁棒性 transforms.ToTensor(), # 转张量并归一化到 [0, 1] transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) # ImageNet 标准化参数 ]) transform_valid transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])RandomHorizontalFlip和ColorJitter属于训练集独有的数据增强策略验证集和测试集不做增强只做最基本的缩放和标准化。这样设计的理由是训练时让模型见过更多变体提升泛化能力评估时保证所有图片经过一致的预处理结果才有可比性。这里有个答辩高频问题训练集的增强会不会导致模型没见过「干净」的图片答案是会但实际影响不大因为增强是在训练过程中逐批次随机施加的模型在多个 epoch 里也会看到未增强的版本。3.2 卷积神经网络结构解析每层在做什么这套系统的网络结构是一个典型的轻量级 CNN和经典 LeNet-5 的骨架思路一致。核心结构定义如下import torch.nn as nn class GarbageCNN(nn.Module): def __init__(self, num_classes2): super(GarbageCNN, self).__init__() self.features nn.Sequential( # 第一组卷积3通道输入 - 32通道输出 nn.Conv2d(3, 32, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2), # 224 - 112 # 第二组卷积32通道 - 64通道 nn.Conv2d(32, 64, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2), # 112 - 56 # 第三组卷积64通道 - 128通道 nn.Conv2d(64, 128, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(kernel_size2, stride2), # 56 - 28 ) self.classifier nn.Sequential( nn.Flatten(), # 摊平成向量 nn.Linear(128 * 28 * 28, 256), nn.ReLU(inplaceTrue), nn.Dropout(p0.5), # 随机丢弃一半神经元抑制过拟合 nn.Linear(256, num_classes) # 输出每个类别的得分 ) def forward(self, x): x self.features(x) x self.classifier(x) return x逐层拆解一下三个卷积模块的通道数从 32 到 64 再到 128 逐层翻倍对应特征从低级到高级的抽象过程——浅层学的是边缘、纹理深层学的是瓶身形状、电池轮廓这类语义特征。池化层把特征图尺寸减半一方面减少计算量另一方面带来一定的平移不变性。Dropout 只在训练时生效推理时自动关闭这是 PyTorch 默认行为不需要你手动切换。有一个关键的参数隐忧全连接层的输入维度128 * 28 * 28是和输入图片尺寸强绑定的。如果你把图片尺寸从 224×224 改成其他值这里必须同步修改否则会报size mismatch错误。很多同学在调图片尺寸时只改了Resize忘了改全连接层输入维度这是典型的低级翻车现场。3.3 训练循环与超参数选择损失函数、优化器、学习率的配合训练脚本是课程设计的重头戏也是最容易出问题的部分。核心训练逻辑如下import torch import torch.nn as nn from torch.utils.data import DataLoader from torchvision import datasets, transforms def train_one_epoch(model, dataloader, criterion, optimizer, device): model.train() # 切换到训练模式 running_loss 0.0 correct 0 total 0 for step, (images, labels) in enumerate(dataloader): images, labels images.to(device), labels.to(device) optimizer.zero_grad() # 梯度清零否则会累加 outputs model(images) # 前向传播 loss criterion(outputs, labels) # 计算交叉熵损失 loss.backward() # 反向传播 optimizer.step() # 更新参数 running_loss loss.item() _, predicted outputs.max(1) total labels.size(0) correct predicted.eq(labels).sum().item() epoch_loss running_loss / len(dataloader) epoch_acc 100.0 * correct / total return epoch_loss, epoch_acc # 超参数配置 batch_size 32 # 批次大小一次喂给模型多少张图 learning_rate 0.001 # 学习率参数更新的步长 num_epochs 30 # 训练轮数训练时最需要注意的就是optimizer.zero_grad()这一步。PyTorch 的梯度是默认累加的不清零的话每个 batch 的梯度会叠加到上一个 batch 上参数更新方向完全错乱loss 曲线会呈现出诡异的周期性震荡。这个错误不报异常模型也能训,就是准确率上不去排查起来非常耗时。超参选择方面batch size 32、学习率 0.001、30 个 epoch 是一组比较稳妥的起点。学习率 0.001 对应 Adam 优化器的默认设置这也是为什么很多课程设计直接用optim.Adam(model.parameters())就能跑出不错的结果。如果你用的是 SGD学习率通常要调到 0.01 甚至更高否则收敛很慢。3.4 训练执行命令行入口与模型保存策略准备好之后直接跑主训练入口python train.py --epochs 30 --batch-size 32 --lr 0.001训练过程中建议每隔一个 epoch 打印一次 loss 和准确率观察趋势。正常情况下 loss 应该整体下降准确率逐步上升。如果 loss 在前几个 epoch 后不再下降甚至反弹先检查学习率是否过大。保存模型时常见策略是同时保存最佳模型和最后一个 epoch 的模型# 保存最佳模型 torch.save(model.state_dict(), models/best_model.pth)保存的只是state_dict()也就是模型的权重参数不包含网络结构本身。这意味着加载时必须先实例化GarbageCNN再load_state_dict很多同学只保存了权重到推理时忘了构建模型结构然后报KeyError或missing keys。所以我在每个训练脚本末尾都会加一个验证步骤保存完后立刻重新加载一次确认权重可以正常恢复这个习惯帮我省掉了大量答辩前的低级错误。训练完成后记录三个数据最终测试集准确率、各类别的单独准确率、每个 epoch 的 loss 曲线。这三个数据在答辩时会被老师逐个追问尤其类别单独准确率——如果你的数据集里电池图片明显多于塑料瓶模型可能偏向把塑料瓶认成电池这是类别不平衡的表现需要提前发现并想好对策。4. 推理与结果解读让模型真正用起来训练只是手段能对新图片做出正确判断才是目的。这一章讲清楚从训练好的模型到「输入一张图片、输出一个类别」中间发生了什么。4.1 单图预测完整推理流程拆解推理脚本核心代码如下import torch import torch.nn.functional as F from PIL import Image from torchvision import transforms def predict_image(model, image_path, class_names, device): # 加载并预处理图片 image Image.open(image_path).convert(RGB) transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ]) image_tensor transform(image).unsqueeze(0) # 加 batch 维度 image_tensor image_tensor.to(device) # 推理 model.eval() # 切换到评估模式 with torch.no_grad(): # 不计算梯度省内存 output model(image_tensor) probabilities F.softmax(output, dim1) # 转成概率分布 confidence, predicted_idx torch.max(probabilities, 1) # 映射回类别名 class_name class_names[predicted_idx.item()] confidence_score confidence.item() return class_name, confidence_score这段代码里有三个细节容易被忽略unsqueeze(0)是因为模型接受的是四维张量(batch_size, channels, height, width)单张图片只有三个维度必须补上 batch 维model.eval()和with torch.no_grad()的作用不同前者影响 Dropout 和 BatchNorm 的行为后者只是关闭梯度计算以节省内存两者最好同时使用预测结果先过softmax再取最大值是为了拿到「模型对这个判断有多自信」的概率值控制在 0 到 1 之间便于展示置信度。4.2 置信度阈值模型说「不确定」比说「错误答案」更有价值推理输出的置信度不是摆设。当模型对一张图片输出塑料瓶 (confidence: 0.55)时这个置信度其实很低说明模型并不确信自己的判断。实际使用中我会设一个阈值比如 0.8置信度 ≥ 0.8接受模型的判断把类别作为最终结果置信度 0.8提示「无法确认请人工复核」。这样的处理在课程设计答辩中是个加分项因为它体现了两层理解模型不是一个完美的分类器它也会困惑而我们能通过置信度信号识别出模型的困惑时刻并给出兜底方案。这个思路放到真实垃圾分类场景里同样成立——机器分不准的垃圾宁可让人再看一眼也不要硬分。4.3 批量预测评估整个测试集的准确率单张预测只是演示效果评估模型真实水平需要跑完整测试集def evaluate_model(model, dataloader, device): model.eval() correct 0 total 0 class_correct [0] * 2 class_total [0] * 2 with torch.no_grad(): for images, labels in dataloader: images, labels images.to(device), labels.to(device) outputs model(images) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() for i in range(labels.size(0)): label labels[i].item() class_total[label] 1 if predicted[i].item() label: class_correct[label] 1 overall_acc 100.0 * correct / total print(f整体准确率: {overall_acc:.2f}%) for i in range(2): acc 100.0 * class_correct[i] / class_total[i] print(f类别 {i} 准确率: {acc:.2f}%)批量评估的价值在于能发现「看起来总体准确率不错但某个类别特别差」的问题。比如整体准确率 92%但电池类 98%、塑料瓶类只有 85%说明模型对塑料瓶的辨识能力弱于电池。可能的原因是塑料瓶在数据集中外观差异更大——透明瓶、有色瓶、带标签的、压扁的——而电池外观相对统一。这种情况下解决方向不是盲目调网络结构而是增加塑料瓶类别的数据量或数据增强强度。5. 课程设计避坑指南五个真实踩坑记录这一章的每一条都是我从课程设计项目里真实踩过、或者帮别人排查过的坑全部按「现象 → 原因 → 解决」的格式整理。答辩前把这五条过一遍能避免 80% 的常见事故。5.1 报错size mismatch全连接层维度对不上现象训练到第一个 batch 时报size mismatch for classifier.2.weight或者加载权重时报Missing key(s) in state_dict。原因修改了输入图片尺寸比如从 224 改成 128但全连接层的nn.Linear(128 * 28 * 28, 256)没有同步改。全连接层的输入维度是由特征图尺寸决定的特征图尺寸随输入图片尺寸变化而变化。解决把图片尺寸固定下来不要中途改动如果确实要改重新计算全连接层输入维度。224×224 的输入经过三次池化后是 28×28这个 28 是拿224 ÷ 2 ÷ 2 ÷ 2算出来的不是拍脑袋定的。5.2 训练 loss 不降反升学习率和梯度问题现象loss 在前几个 epoch 震荡甚至上升准确率在 50% 左右徘徊不上涨。原因最常见的是学习率过大导致参数在最优解附近来回震荡无法收敛。其次是梯度没有清零也就是漏了optimizer.zero_grad()梯度在 batch 间累加更新方向被历史梯度污染。解决先把学习率降到 0.0001 试试如果 loss 能下降说明是学习率问题。如果 loss 还乱跳检查optimizer.zero_grad()是否在loss.backward()之前被调用。我的排查习惯是把学习率和梯度清零检查放在并列优先级第一位因为它们发生在训练最前期越早排除后面越省时间。5.3 加载权重报错KeyError结构名和权重名对不上现象load_state_dict时提示Error(s) in loading state_dict for GarbageCNN: Missing key(s) in state_dict或Unexpected key(s)。原因训练时和推理时实例化的模型结构不一致。比如训练时用了两层全连接推理时只保留了一层或者层名改了——self.fc1改成self.classifier后权重里存的键名和模型当前定义的键名对不上了。解决加载权重前先打印模型的state_dict().keys()再打印权重文件的键名逐一对比。最稳妥的做法是训练完立刻在当前脚本里实例化一个完全相同的模型做回载验证确认无误再单独写推理脚本。不要靠猜因为键名和网络结构定义时__init__里的属性名强绑定。5.4 图片中文路径报错OpenCV 读不了中文文件名现象cv2.imread(data/train/电池/xxx.jpeg)返回None但文件确实存在。原因OpenCV 的imread在 Windows 下不支持非 ASCII 字符路径中文文件夹名或文件名会导致读取失败返回空值。这套系统的数据集里文件名就带中文比如img_电池_452.jpeg直接踩中这个坑。解决放弃 OpenCV 读图改用 PIL 的Image.open()它对中文路径支持良好。这就是为什么 3.1 节里的数据加载代码用的是PIL.Image而不是cv2.imread。两种方式读进来的图片格式略有不同PIL 读进来是 RGBOpenCV 读进来是 BGR如果你需要从 OpenCV 切换回 PIL记得检查颜色通道顺序是否会影响结果。5.5 训练准确率高但测试准确率低过拟合的典型信号现象训练集准确率 98%测试集只有 72%两者差距超过 20 个百分点。原因模型在训练集上死记硬背了样本特征没有学到可泛化的判别模式。尤其在类别少只有 2 类、数据量小几百张的情况下模型容量过大会迅速过拟合。解决按优先级依次做三件事——加大数据增强强度给ColorJitter增加亮度、饱和度、对比度扰动的范围增大 Dropout 比例从 0.5 提到 0.6 或 0.7引入早停机制在验证集准确率连续 5 个 epoch 不提升时停止训练保存验证集表现最好的模型而不是最后一个 epoch 的模型。6. 进阶玩法把自己拍的图片加进数据集重新训练跑通整套系统只是第一步更值得做的是把这套流程迁移到你自己收集的垃圾图片上让它真正对你有用。这一章分享一个具体的扩展链路。6.1 自制小数据集拍照、命名、按比例切分在你宿舍或家里找几个垃圾类别——比如纸箱、易拉罐、果皮——每类拍 30 到 50 张不同角度、不同光线条件下的照片。命名规则严谨一点因为后续解析全靠文件名img_纸箱_01.jpeg img_纸箱_02.jpeg img_易拉罐_01.jpeg img_果皮_01.jpegimg_打头接着是中文类别名最后是序号。拍完照复制到项目根目录用脚本按 7:2:1 的比例切成 train/valid/test 三个子集import os import random import shutil def split_data(src_dir, train_dir, valid_dir, test_dir, ratios(0.7, 0.2, 0.1)): for filename in os.listdir(src_dir): if not filename.endswith(.jpeg): continue rand random.random() if rand ratios[0]: dest os.path.join(train_dir, filename) elif rand ratios[0] ratios[1]: dest os.path.join(valid_dir, filename) else: dest os.path.join(test_dir, filename) shutil.copy(os.path.join(src_dir, filename), dest)切分时注意每个类别的图片数量要均衡类别间差异大时模型会偏向样本多的类。我拍的三类图片数量差距不到 5 张训练出的模型各类别准确率基本在同一水平。切分完成后检查一下目录结构train/纸箱、train/易拉罐、train/果皮各一个子目录类别名就是文件夹名。6.2 迁移学习用预训练权重省下大量训练时间自建数据集通常只有一两百张图片从零训练 CNN 很容易过拟合。这里更推荐加载 ImageNet 预训练的 ResNet18 或 MobileNetV2只替换最后的全连接层做迁移学习import torchvision.models as models def build_transfer_model(num_classes): # 加载在 ImageNet 上预训练的 ResNet18自动下载权重 model models.resnet18(weightsmodels.ResNet18_Weights.DEFAULT) # 替换最后一层全连接 in_features model.fc.in_features model.fc nn.Linear(in_features, num_classes) return modelin_features model.fc.in_features这行的作用是把原模型的最后一层输入维度取出来保证替换时维度对得上。预训练模型学到了通用视觉特征——边缘、纹理、形状——这些特征对垃圾图片依然有效你只需要微调最后几层让模型适应自己的类别。我在自建数据集上从零训练 CNN 的准确率只有 85% 左右改用迁移学习后涨到 94%训练时间还少了 30%。这个差距在答辩时完全可以作为「为什么选迁移学习」的实证论据。训练迁移模型时学习率通常要调小一点比如 0.0001因为预训练权重已经在一个大数据集上收敛过一次步长太大会把学好的特征破坏掉。训练时只更新最后一层还是全部参数取决于数据量数据少就冻结前四层只训练最后一层数据多就全部放开训练。一个更精细的做法是先用只训练最后一层的方式跑几个 epoch验证集准确率不再提升后再解冻所有层用小学习率继续微调。我从这次拆包得到的教训是课程设计项目模型结构选得花哨不如选得合适——类别少、数据量小的时候一个结构清晰的轻量 CNN 加合理的迁移学习策略往往是性价比最高的解法。从那以后我每次做图像分类的小项目都会先问自己三个问题数据集多少张、类别分布均衡吗、有没有可用的预训练权重这三个答案基本决定了大部分超参数和结构选型。希望这套流程和上面记录的踩坑经历能在你自己动手做垃圾分类识别时少走点弯路。本文还有配套的精品资源点击获取