从零开始:基于 PyTorch 的图像分类模型

摘要:本文详细记录了使用 PyTorch 从零搭建一个图像分类模型的过程,涵盖卷积神经网络(CNN)、数据预处理、模型设计、训练调试与优化。通过对 CIFAR-10 数据集的处理实践,结合经典文献和 2025 年最新研究趋势,深入探讨了技术细节,并辅以完整实践源码的过程和结论。我选择用 PyTorch 搭建图像分类模型,既源于对深度学习的兴趣,也因为它在 2025 年的技术社区中热度不减。通过这次实践,我希望掌握 CNN 的核心原理,同时记录过程,为其他初学者提供参考。

关键字:PyTorch、图像分类、CNN、深度学习、模型优化、CIFAR-10、调试经验、前沿趋势

引言

背景介绍

图像分类是计算机视觉的核心任务,广泛应用于自动驾驶、医疗影像分析和人脸识别等领域。

深度学习,特别是卷积神经网络(CNN),极大推动了这一技术的发展。2012年,AlexNet在ImageNet挑战赛中大幅降低分类错误率,标志着深度学习时代的开端。

此后,模型架构从 ResNet 进化到 VisionTransformer(ViT),性能不断提升。到2025年,硬件算力增强和PyTorch 2.x 等框架的优化(如torch.compile()),让图像分类任务更加高效。在实践中,以CIFAR-10等数据集为例,一个简单CNN在现代GPU上几分钟即可训练完成,展现了技术的进步。

本文目标与结构

  • 目标:搭建并优化一个 CNN,完成 CIFAR-10 分类任务。
  • 结构:理论讲解 → 源码实现 → 调试分析 → 优化与展望。

卷积神经网络(CNN)

为何需要 CNN?

传统人工神经网络(ANN)在图像处理上参数量巨大,计算成本高。CNN 通过局部感受野权重共享层级特征提取本文只入门,这三个都不涉及),减少计算量并提升模型能力。

CNN 发展始于 LeNet-5(1998),后续有 AlexNet(2012)VGG(2014)ResNet(2015),极大推动计算机视觉进步。

卷积神经网络(Convolutional Neural Network, CNN)是深度学习中处理图像任务的基石,其核心在于通过卷积操作提取空间特征、优化参数,并逐步构建高级语义表示。

CNN 的核心组成

1、卷积层(Convolutional Layer) :特征提取的“智能滤镜”

CNN 的核心是卷积操作,通过一个小的滑动窗口(卷积核, kernel)在输入图像上扫描,计算局部区域的加权和,提取边缘、纹理等低级特征。

卷积包括填充、步幅等参数,还包括和全连接层一样的偏置参数,卷积运算相当于图像处理中的“滤波器运算”。

卷积运算会先对输入数据进行初始化准备。数据除了高、长方向之外,还需要处理通道方向,形成三维数据(Channel, Height, Width)。再结合批处理参数,按照(batch_num, channel, height, width)顺序保存数据,形成四维数据。

卷积运算通过填充、步幅对数据进行再加工。填充即向输入数据的周围填入0,主要是为了调整输出的大小。避免因每次卷积运算缩小的数据空间最终导致卷积运算不可用的情况。步幅是卷积运算每一次运算的数据位置间隔,通过步幅计算出输出数据矩阵大小。

卷积运算通过卷积核完成数据运算。卷积核即滤波器,是一种“滤镜”,通过它来完成指定运算目标(识别、聚焦等)。

卷积运算通过偏置函数完成数据输出。偏置是在数据输出前针对数据的统一处理,以适配特定场景(全员加一等)。

简单来说,卷积就像用一个“放大镜”在图像上滑动,每次聚焦一小块,找出关键线索。

卷积操作类似于“盲人摸象”的工作过程,举个例子:假设你看一张猫的照片,第一次卷积可能找到耳朵的边缘,第二次找到毛发的纹理,最终拼凑出“猫”的完整轮廓。这种层层递进的过程,正是卷积的魅力。

①输入数据和卷积核

假设输入图像是一个 4x4 的灰度矩阵(单通道),卷积核为 3x3,步幅为 1,无填充。

  • 输入矩阵

\[I = \begin{bmatrix} 1 & 2 & 3 & 0 \\ 0 & 1 & 2 & 3 \\ 3 & 0 & 1 & 2 \\ 2 & 3 & 0 & 1 \end{bmatrix} \]

  • 卷积核(滤波器)

\[K = \begin{bmatrix} 2 & 0 & 1 \\ 0 & 1 & 2 \\ 1 & 0 & 2 \end{bmatrix} \]

②计算过程(可忽略)

卷积运算通过滑动卷积核,在输入矩阵上逐块计算点积。

输出特征图大小为2x2,I[高]-K[高]+步幅 * I[宽]-K[宽]+步幅

\[(4-3+1) \times (4-3+1) = 2 \times 2 \]

  • 公式:对于输入I和卷积核K,输出O[i,j]为:

\[O[i,j]= m ∑ ​n ∑ ​I[i+m,j+n]⋅K[m,n] \]

③手动计算

  1. 左上角 2+0+3+0+1+4+3+0+2=15
  2. 右上角 4+0+0+0+2+6+0+0+4=16
  3. 左下角 0+0+2+0+0+2+2+0+0=6
  4. 右下角 2+0+3+0+1+4+3+0+2=15

输出特征图为:

\[O = \begin{bmatrix} 15 & 16 \\ 6 & 15 \end{bmatrix} \]

2、池化层(Pooling Layer) :降维与聚焦关键信息

池化是缩小高、长方向上的空间的运算。用于压缩特征图,减少计算量,同时保留重要信息。

LeCun 的LeNet-5引入了平均池化(Average Pooling):计算目标区域的平均值。

AlexNet 则普及了最大池化(Max Pooling):计算目标区域的最大值。

想象你在看一幅画,池化就像眯起眼睛,只关注最亮的亮点(最大值),忽略细枝末节。这样既降低了分辨率,又让网络更关注显著特征,比如猫脸上的胡须而非背景噪声。

池化的现代应用更灵活,也体现了CNN的进化,例如全局平均池化(Global Average Pooling)常用于替代全连接层,提升模型泛化能力。

最大池化示例(2×2 池化,步幅 2):

将4x4的输入数据按照步幅2计算,输出为2x2,分为4个区域:

  1. 左上:[1,2,5,6],最大值6
  2. 右上:[2,4,8,7],最大值8
  3. 左下:[9,10,13,15],最大值15
  4. 右下:[12,11,14,16],最大值16

如下图所示:

\[\begin{bmatrix} 1 & 3 & 2 & 4 \\ 5 & 6 & 8 & 7 \\ 9 & 10 & 12 & 11 \\ 13 & 15 & 14 & 16 \end{bmatrix} \Rightarrow \begin{bmatrix} 6 & 8 \\ 15 & 16 \end{bmatrix} \]

以上为Max池化,如果是Average池化,则改为平均值即可。不赘述。

3、激活函数(Activation Function) :赋予非线性表达力

卷积和池化后的特征需要通过激活函数(Activation Function)引入非线性,否则网络只能学到线性变换。

ReLU 是 CNN 最常用的非线性变换:

\[f(x) = \max(0, x) \]

它计算高效,并避免梯度消失问题。

ReLU 的作用像个“开关”:负值关掉,正值保留,让网络学会更复杂的模式,比如区分猫和狗的不同轮廓。

现代还有 Leaky ReLU 和 Swish 等变种,但 ReLU 仍是理解 CNN 非线性的起点。举例来说,ReLU 就像在筛选线索时,只保留“有用的证据”,丢弃无关信息。

4、全连接层(Fully Connected Layer, FC) :特征整合与分类

在经过卷积和池化后,CNN 将特征展平(Flatten)并输入全连接层,用于分类或回归任务。

全连接层就像大脑的“决策中心”,综合所有线索,给出最终判断:这张图是“猫”还是“狗”?

不过,现代趋势逐渐减少全连接层依赖,例如 ResNet(2015)用全局池化简化输出,减轻过拟合风险。

CNN 在计算机视觉中的应用

  • 图像分类(AlexNet、ResNet)
  • 目标检测(YOLO、Faster R-CNN)
  • 语义分割(U-Net、DeepLab)
  • 医学影像分析(CT 诊断)

实践环境与准备

工具与依赖

  • Python 3.10、PyTorch 2.1、torchvision 0.16。
  • 硬件:NVIDIA RTX 3060 GPU,16GB RAM。

Anaconda 环境配置

1. 安装 Anaconda

使用 Homebrew 安装 Anaconda:

brew install --cask anaconda

2. 初始化 Conda

将 conda 添加到 PATH:

echo 'export PATH="/opt/homebrew/anaconda3/bin:$PATH"' >> ~/.zshrc

初始化 conda 以在 zsh 中使用:

conda init zsh

重新加载 shell 配置:

source ~/.zshrc

3. 创建和配置项目环境

创建新的 conda 环境:

conda create -n simpletorch python=3.9 -y

激活环境:

conda activate simpletorch

安装项目依赖:

pip install -r requirements.txt

4. 验证环境

检查 Python 版本和已安装的包:

python --version
pip list | grep -E "torch|numpy|matplotlib"

5. 常用命令

  • 激活环境:conda activate simpletorch
  • 退出环境:conda deactivate
  • 查看所有环境:conda env list
  • 删除环境:conda env remove -n simpletorch

6. 环境信息

  • 环境名称:simpletorch
  • Python 版本:3.9.21
  • 主要包版本:
    • PyTorch 2.6.0
    • torchvision 0.21.0
    • NumPy 2.0.2
    • Matplotlib 3.9.4

数据集选择与加载

CIFAR-10 数据集概述

  • 数据集大小:50,000 张图片
  • 图片大小:3x32x32(RGB图像)
  • 类别数:10个(飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船、卡车)

数据文件内容

data目录下的文件是 CIFAR-10 数据集文件,它们是在运行 main.py 或 show_dataset.py 时通过 torchvision.datasets.CIFAR10 自动下载的。具体来说,这些文件来自:

  • 官方下载地址:https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz
  • 当代码执行到这一行时:
trainset = datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
  • root='./data' 指定下载到当前目录下的 data 文件夹

  • download=True 表示如果数据不存在就自动下载

  • 下载的文件会被自动解压到 data/cifar-10-batches-py/ 目录下

下载的文件包括:

  • data_batch_1 到 data_batch_5:训练数据

  • batches.meta:类别信息

  • test_batch:测试数据(虽然我们目前没有使用)

这些文件是数据集的一部分,通常不需要手动管理,PyTorch 会自动处理下载和解压过程。如果您想重新下载数据集,可以:

  • 删除 data 目录
  • 重新运行程序,它会自动重新下载

数据预处理

transform = transforms.Compose([transforms.ToTensor(),transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])

数据集可视化

运行以下命令查看数据集样本和分布:

python show_dataset.py

这将生成两个可视化文件:

  • cifar10_samples.png:数据集样本图片
  • class_distribution.png:类别分布

源码分析

import torch
from torchvision import datasets, transforms
import matplotlib.pyplot as pltdef show_dataset():# 数据预处理transform = transforms.Compose([transforms.ToTensor(),transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])# 加载训练集print("Loading CIFAR-10 dataset...")trainset = datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)# 定义类别classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')# 显示数据集信息print(f"\nDataset size: {len(trainset)} images")print(f"Image size: {trainset[0][0].shape}")print(f"Number of classes: {len(classes)}")# 显示样本图片plt.figure(figsize=(15, 5))for i in range(5):img, label = trainset[i]img = img / 2 + 0.5img = img.numpy()plt.subplot(1, 5, i + 1)plt.imshow(img.transpose(1, 2, 0))plt.title(f'Class: {classes[label]}')plt.axis('off')plt.tight_layout()plt.savefig('cifar10_samples.png')# 显示每个类别的样本数量class_counts = torch.zeros(10)for _, label in trainset:class_counts[label] += 1plt.figure(figsize=(10, 5))plt.bar(classes, class_counts)plt.title('Number of samples per class')plt.xticks(rotation=45)plt.tight_layout()plt.savefig('class_distribution.png')print("\nVisualization files saved:")print("- cifar10_samples.png")print("- class_distribution.png")if __name__ == '__main__':show_dataset() 

运行结果

Loading CIFAR-10 dataset...Dataset size: 50000 images
Image size: torch.Size([3, 32, 32])
Number of classes: 10Visualization files saved:
- cifar10_samples.png
- class_distribution.png

模型设计与源码分析

网络结构设计

  • 结构:2 层卷积 + 2层池化 + 1 层全连接。

  • 源码:

    import torch
    import torch.nn as nnclass SimpleNet(nn.Module):def __init__(self):super(SimpleNet, self).__init__()# 第一个卷积层# 输入: 3通道(RGB图像) -> 输出: 16个特征图# 例如: 一张32x32的彩色图片(3x32x32) -> 16个32x32的特征图self.conv1 = nn.Conv2d(3, 16, 3, padding=1)  # 448参数# 第二个卷积层# 输入: 16个特征图 -> 输出: 32个特征图# 例如: 16个16x16的特征图 -> 32个16x16的特征图self.conv2 = nn.Conv2d(16, 32, 3, padding=1)  # 4,640参数# 全连接层# 输入: 32 * 8 * 8 = 2048个特征 -> 输出: 10个类别# 例如: 32个8x8的特征图展平后 -> 10个数字(0-9)的概率self.fc1 = nn.Linear(32 * 8 * 8, 10)  # 20,490参数def forward(self, x):# 输入x的形状: [batch_size, 3, 32, 32]# 例如: [32, 3, 32, 32] 表示32张32x32的RGB图片# 第一个卷积层 + ReLU激活# 输出形状: [batch_size, 16, 32, 32]# 例如: [32, 16, 32, 32] 表示32张图片,每张有16个32x32的特征图x = torch.relu(self.conv1(x))# 最大池化层,将特征图尺寸减半# 输出形状: [batch_size, 16, 16, 16]# 例如: [32, 16, 16, 16] 表示32张图片,每张有16个16x16的特征图x = torch.max_pool2d(x, 2)# 第二个卷积层 + ReLU激活# 输出形状: [batch_size, 32, 16, 16]# 例如: [32, 32, 16, 16] 表示32张图片,每张有32个16x16的特征图x = torch.relu(self.conv2(x))# 最大池化层,再次将特征图尺寸减半# 输出形状: [batch_size, 32, 8, 8]# 例如: [32, 32, 8, 8] 表示32张图片,每张有32个8x8的特征图x = torch.max_pool2d(x, 2)# 将特征图展平成一维向量# 输出形状: [batch_size, 32 * 8 * 8]# 例如: [32, 2048] 表示32张图片,每张图片的特征被展平成2048维向量x = x.view(x.size(0), -1)# 全连接层,得到最终的类别预测# 输出形状: [batch_size, 10]# 例如: [32, 10] 表示32张图片,每张图片对应10个类别的预测概率x = self.fc1(x)return x 
    

参数与设计分析

  • 参数总体情况:这是一个"轻特征提取,重分类"的模型,总参数量25,578

    • 全连接层参数占大多数(约80%)
    • 卷积层参数相对较少(约20%)
    • 总参数量适中,适合入门学习
  • 卷积核:3x3卷积核

    • 计算效率高(9个参数)
    • 感受野适中(能捕捉局部特征)
    • 是CNN中的标准选择
  • Padding填充设计:padding=1,输入32x32,输出32x32

    • 保持特征图尺寸不变
    • 避免边缘信息丢失
    • 便于网络设计

3x3卷积核 + padding=1)是CNN中的经典配置,既保证了特征提取效果,又维持了计算效率。

整体设计分析,最关键的三点如下:

  • 使用3x3卷积核配合padding=1,在保持特征图尺寸的同时高效提取局部特征。
  • 通过两层卷积(3→16→32通道)和两次池化(32x32→16x16→8x8),实现了从基本特征到复杂特征的渐进提取。
  • 最后用全连接层(2048→10)直接分类,总参数量25,578,结构简单但有效。

训练与调试

训练流程

  • 源码:

    		# 训练一个epochprint("Training for 1 epoch...")model.train()  # 设置为训练模式(启用dropout等训练特定层)# 遍历数据加载器for i, (inputs, labels) in enumerate(trainloader):# 将数据移到指定设备(GPU/CPU)inputs, labels = inputs.to(device), labels.to(device)# 前向传播optimizer.zero_grad()  # 清空梯度outputs = model(inputs)  # 模型前向传播loss = criterion(outputs, labels)  # 计算损失loss.backward()  # 反向传播,计算梯度optimizer.step()  # 更新模型参数# 每100个batch打印一次损失if (i + 1) % 100 == 0:print(f'Batch [{i + 1}], Loss: {loss.item():.4f}')print("Training finished!")
    

调试经验

  • 数据先行:检查数据加载和预处理,确保输入数据的形状、范围和分布符合预期。
  • 监控为王:密切关注损失值变化趋势,通过定期打印损失、学习率和梯度信息来诊断训练状态。
  • 调参有度:根据训练效果合理调整超参数,如batch_size、学习率等,避免过拟合或欠拟合。

测试执行与结果分析

测试执行

python main.py

结果与分析

Using device: cpu
Loading CIFAR-10 dataset...
Training for 1 epoch...
Batch [100], Loss: 2.2419
Batch [200], Loss: 2.2013
Batch [300], Loss: 1.8651
Batch [400], Loss: 1.9359
Batch [500], Loss: 1.9718
Batch [600], Loss: 1.9448
Batch [700], Loss: 1.7974
Batch [800], Loss: 1.6378
Batch [900], Loss: 1.5137
Batch [1000], Loss: 1.5045
Batch [1100], Loss: 1.8072
Batch [1200], Loss: 1.9754
Batch [1300], Loss: 1.8177
Batch [1400], Loss: 1.7377
Batch [1500], Loss: 1.8140
Training finished!
  • 设备使用:程序在 CPU 上运行
  • 数据集:成功加载了 CIFAR-10 数据集
  • 训练过程:
    • 完成了 1 个 epoch 的训练
    • 每 100 个 batch 打印一次 loss
    • Loss 值从初始的 2.24 逐渐下降到约 1.81,说明模型在学习
    • 整个训练过程顺利完成

Loss(损失)是衡量模型预测结果与真实值之间差距的指标,就像考试打分一样 - 比如模型预测一张图片是猫的概率为60%,而实际上确实是猫(100%),这个40%的差距就反映在loss值上,loss越小代表模型预测越准确。

优化与前沿探索

架构优势

  • 轻量级设计:总参数量约25K,适合快速部署和迭代

  • 结构清晰:采用经典的CNN+池化层组合,便于理解和优化

  • 模块化实现:代码组织合理,便于扩展

与前沿趋势的差距

  • 缺少注意力机制(如Transformer结构)

  • 没有使用残差连接(ResNet特性)

  • 缺乏正则化策略(如Dropout)

可优化方向

  • 添加BatchNorm提高训练稳定性

  • 引入现代激活函数(如GELU、Swish)

  • 实现学习率调度策略

总结与反思

收获与不足

  • 掌握 CNN 与 PyTorch 基本流程。
  • 不足:模型深度有限,准确率待提升。

下一步计划

  • 添加验证集评估和模型保存机制
  • 实现训练过程可视化

从玩具到工具,从黑盒到透明

项目源码

参考文献

  • LeCun, Y., Bottou, L., Bengio, Y., & Haffner, P. (1998). Gradient-based learning applied to document recognition. Proceedings of the IEEE, 86(11), 2278-2324. DOI:10.1109/5.726791

  • Krizhevsky, A., Sutskever, I., & Hinton, G. E. (2012). ImageNet classification with deep convolutional neural networks. Advances in Neural Information Processing Systems, 25, 1097-1105. DOI:10.1145/3065386

  • Simonyan, K., & Zisserman, A. (2014). Very deep convolutional networks for large-scale image recognition. arXiv preprint arXiv:1409.1556. arXiv:1409.1556

更多文章请转到一面千人的博客园

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/902690.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Vue3 slot

6.9. 【slot】 1. 默认插槽父组件中:<Category title="今日热门游戏"><ul><li v-for="g in games" :key="g.id">{{ g.name }}</li></ul></Category> 子组件中:<template><div class="item&…

英语四级计划第三天

第三天 单词阅读 Smaller Museums From Niche to Mainstream “小而精”的小众博物馆,正在出圈出彩 Chinas museum boom has continued to rise, leading to increased attention and visibility for smaller but more specialized museums. 中国的博物馆热潮持续升温,导致规…

今日总结(app链接数据库的简单实现以及AI训练学习)

所花时间:145min 代码量(行):120 博客量:14 了解到的知识点: 今天又到了周五了,如同往常一样下午进行自学测试,今天是实现一个app数据库连接 在手机上进行查询显示的任务。 因为对于AS开发我并没有进行系统的学习,只是知道大体的框架和对AI进行训练,但是 我发现了一个…

学嵌入式C语言,看这一篇就够了(6)

C语言的语句和块 C语言标准中一共提供6种语句 注意:C语言中的语句要指明执行的操作,并且没有特殊情况,语句是按照顺序执行的一般把实现某些功能的语句整合在一起,构成一个语法单元,C语言标准的语法单元也被称为块,也被称为块语句 复合语句 复合语句可以限制语句的作用范围…

P3375 【模板】KMP

P3375 【模板】KMP 题目描述 给出两个字符串 \(s_1\) 和 \(s_2\),若 \(s_1\) 的区间 \([l, r]\) 子串与 \(s_2\) 完全相同,则称 \(s_2\) 在 \(s_1\) 中出现了,其出现位置为 \(l\)。 现在请你求出 \(s_2\) 在 \(s_1\) 中所有出现的位置。 定义一个字符串 \(s\) 的 border 为 …

Cursor 使用教程

目录核心功能TabAICHATCOMPOSER@DocsWebGitNotepadCodebase技巧和思路如果Cursor乱改代码怎么办? 核心功能 Tab 这个是Cursor最为强大的功能,使用也很简单,在编写代码的时候,如果出现了灰色的提示词,直接按Tab就可以自动补全了最后的灰色的就是提示词 Cursor的光标预测也很…

C语言学习打卡第二天(2025.3.21)

时间有点少,今天也没学多少,只把指针基础概念学了一下(例如指针变量,下标法等),题也只做了三道。

3.21 学习记录

设计科技查询手机端系统,实现了政策关键字进行模糊匹配查询,点击下方的政策名称,可以打开新窗口,显示政策全文,实现分页查询,分类查询 采用springboot+vue3实现

P8436 【模板】边双连通分量

P8436 【模板】边双连通分量 题目描述 对于一个 \(n\) 个节点 \(m\) 条无向边的图,请输出其边双连通分量的个数,并且输出每个边双连通分量。 输入格式 第一行,两个整数 \(n\) 和 \(m\)。 接下来 \(m\) 行,每行两个整数 \(u, v\),表示一条无向边。 不保证图为简单图,图中可…

SvelteKit 最新中文文档教程(8)—— 部署 Node 服务端

前言 Svelte,一个语法简洁、入门容易,面向未来的前端框架。 从 Svelte 诞生之初,就备受开发者的喜爱,根据统计,从 2019 年到 2024 年,连续 6 年一直是开发者最感兴趣的前端框架 No.1:Svelte 以其独特的编译时优化机制著称,具有轻量级、高性能、易上手等特性,非常适合构…

第四篇:本地私有化部署大模型(Ollama、vLLM、LMDeploy)

魔塔社区平台介绍 量化:将高精度数值转换成低精度表示的过程(通常用于减少模型的内存和提高推理速度) Ollama部署大模型 linux安装miniconda# 下载 Miniconda 安装脚本 wget https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh # 运行安装脚本 bash…

用Adobe Illustrator编辑ArcGIS地图的方法

本文介绍在ArcGIS下属的ArcMap软件中,将绘制好的地图导出为.ai或者.eps格式文件,并在Adobe Illustrator软件中进一步编辑地图,并最终导出为图片格式的结果文件的方法~本文介绍在ArcGIS下属的ArcMap软件中,将绘制好的地图导出为.ai或者.eps格式文件,并在Adobe Illustrator软…