简介一份面向高校课程设计、期末大作业与毕业设计场景的Python多模态情感分析完整项目覆盖文本、语音、图像和视频四种输入代码附详细注释新手也能快速读懂并完成本地部署。资源共20个文件包含9个pickle预处理数据、5个Python源文件、3个zip数据集、1份PDF文档、1份Markdown说明及1张结果图整体约56.91MB目录按数据集MOSI/MOSEI/IEMOCAP与功能模块划分同时附有项目文档与运行说明便于按需检索和二次开发。已有306人学习下载。项目经过严格调试可直接运行覆盖从数据准备、特征提取到多模态融合与情感分类评估的完整链路代码中的注释能帮助理解跨模态特征对齐、融合策略及性能评估等关键环节。附带的pickle数据已处理好常用特征可节省重复预训练时间适合作为高分课程作业或毕业设计的参考模板。1. 多模态情感分析为什么单看文本或视频都会翻车多模态情感分析就是把文本、语音、图像、视频四路信号同时喂进模型让它们互相打配合而不是各算各的。单模态的问题很典型一段视频里台词说“太棒了”但语音带着哭腔人脸也皱着眉光看文本会判定成积极光看音频和画面却又像是消极——真实世界里情绪本来就是多通道表达的漏掉任何一路都可能翻车。这份资源把四路输入的代码、文档和数据集都打包好了能直接作为课程设计或期末大作业的骨架文本用预训练模型、语音提MFCC、图像做表情识别、视频抽帧后按帧投票最后在融合层汇总成最终情感标签。适合想速成多模态项目、又不想从零造轮子的高年级学生也适合想快速验证融合思路的从业者。整个工程用 Python 写一套 pip 依赖就能装完跟着下面的步骤可以直接复现。2. 项目结构与运行主线把文本、语音、图像、视频四路输入先对齐多模态项目和单模态项目最大的不同在于你得先让四路数据的“样本轴”和“时间轴”对齐。文本是按条读的语音是连续波形图像是单帧视频是几十秒的帧序列——如果不事先约定好每个样本的 id 怎么共享融合阶段一定会乱套。这份资源在 data 目录里按模态分了子目录但每个样本都有全局唯一的 sample_id这个 id 是四路数据对齐的钥匙。2.1 目录结构每个文件夹里放了什么拿到手之后先不要急着跑 train.py。我一般会把目录过一遍确认数据格式和代码入口对得上。典型的目录长这样multimodal-sentiment/ ├── README.md # 环境说明和训练流程 ├── requirements.txt # 依赖清单 ├── config.yaml # 全局超参统一管理 ├── data/ │ ├── text/ # 文本样本csv格式 │ ├── audio/ # wav音频 标签表 │ ├── image/ # jpg图像 标签表 │ └── video/ # mp4视频 标签表 ├── src/ │ ├── feature_text.py # 文本特征提取 │ ├── feature_audio.py # 语音MFCC提取 │ ├── feature_image.py # 人脸检测表情分类 │ ├── feature_video.py # 视频抽帧帧级评分 │ ├── fusion.py # 融合层网络 │ ├── train.py # 训练入口 │ └── predict.py # 单样本预测入口 └── checkpoints/ # 保存模型权重data 目录下的标签表我建议统一成同样的字段名比如 label 列。文本数据集每一行是一条语料语音、图像、视频在 csv 里写上文件相对路径和对应的样本 id。这样 train.py 只需要写一个统一的 DataLoader不用为每个模态各写一套读取逻辑。另外要检查一个容易忽略的点四个模态在数据量上往往是不均衡的。有的音频样本可能缺了有的视频文件是坏的跑训练之前先过一遍完整性校验能省掉后面排查 bug 的几个小时。这里贴一个我常用的校验脚本import os import pandas as pd def check_modal_files(data_root: str): for mod in [text, audio, image, video]: label_file os.path.join(data_root, mod, labels.csv) if not os.path.exists(label_file): print(f[{mod}] 缺少 labels.csv) continue df pd.read_csv(label_file) missing [] for _, row in df.iterrows(): # 约定好列名sample_id, label, path fp os.path.join(data_root, mod, row[path]) if not os.path.exists(fp): missing.append(row[sample_id]) if missing: print(f[{mod}] 缺失 {len(missing)} 个文件例如 {missing[:3]}) else: print(f[{mod}] 文件完整共 {len(df)} 条)逻辑说明按模态逐个打开 labels.csv检查 path 列指向的文件是否真实存在。缺失的样本会在融合阶段产生空特征直接污染后续训练。你在用这份资源时如果复现过程很正常但训练指标异常八成是某个模态的样本缺失或路径错位跑一下这个函数能快速定位。2.2 环境依赖一套 pip 能装完的清单在装依赖之前先确认 Python 版本。这份代码我一般用 Python 3.8 到 3.10 之间跑太低有些新语法不兼容太高部分老库没有预编译 wheel。核心依赖如下库主要用途备注torch模型训练1.x 和 2.x 均可transformers文本预训练模型加载 BERT 类模型librosa语音 MFCC 提取0.9.x 以上opencv-python图像/视频读取4.xscikit-learn分类指标计算 F1 等pandas标签表处理—h5py缓存特征避免反复抽特征安装命令很简单但要注意librosa 会连带安装 numba 和 numpy如果系统里之前有旧版本 numpy可能被反向降级影响其他项目。建议在一个干净的 conda 环境里装别在系统 Python 里硬怼。conda create -n msa python3.9 -y conda activate msa pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 pip install transformers librosa opencv-python scikit-learn pandas h5py参数说明torch 的 --index-url 指向 CUDA 11.8 的预编译轮子如果你的机器没有 NVIDIA 显卡把这一行换成 CPU 版即可pip install torch torchvision --cpu。transformers 第一次运行会自动下载预训练权重到缓存目录网络不稳时可以手动下载后放到 HuggingFace 缓存路径下。librosa、opencv 这些库版本比较稳定按 requirements.txt 里的版本装就不会有兼容问题。2.3 跑通一次 demo入口脚本与预期输出环境装好、目录解压好之后我会先跑 predict.py 而不是 train.py。因为训练脚本要遍历整个数据集如果代码或环境有问题报错信息会很混乱predict.py 只处理一条样本能把问题暴露得最直接也能最快验证四路输入管线是否打通。python src/predict.py \ --text 这个项目代码结构很清晰文档也齐全 \ --audio data/audio/00001.wav \ --image data/image/00001.jpg \ --video data/video/00001.mp4预期输出是一段 JSON 日志大致格式{label: positive, prob: 0.82, modals: {text: 0.91, audio: 0.77, image: 0.65, video: 0.71}}。modals 字段是每个单模态分类器给出的置信度向量里 positive 那一类的分值融合层最后会把这些分数汇总。如果这个输出能出现说明四路特征的提取和对齐都没问题后面再调训练参数就有底了。提示多模态项目跑通 demo 后第一个动作是检查 modals 四个分数是否有明显差异。如果某个模态常年接近 0.3随机猜的水平别急着调融合层先回头检查那个模态的输入管线。3. 文本与语音模态落地预训练特征与手工特征的取舍文本和语音属于时序类模态一个按 token 组织一个按帧组织。两者都能用预训练模型端到端微调也都能用传统特征快速出基线取舍的核心在于数据量和机器显存。3.1 文本情感BERT 微调与轻量基线的取舍文本模态在这份资源里走的是 transformers 路线加载中文 BERT 或 RoBERTa 权重在情感标签上微调。数据集如果只有几千条没必要从零预训练加载现成的中文预训练模型再微调几个 epoch 就能出结果。这里给出最简的微调骨架from transformers import AutoTokenizer, AutoModelForSequenceClassification, Trainer, TrainingArguments tokenizer AutoTokenizer.from_pretrained(bert-base-chinese) model AutoModelForSequenceClassification.from_pretrained( bert-base-chinese, num_labels3 ) # load_text_dataset 返回 HuggingFace Dataset列名含 text 和 label train_data load_text_dataset(data/text/train.csv) valid_data load_text_dataset(data/text/valid.csv) args TrainingArguments( output_dir./checkpoints/text_bert, num_train_epochs3, per_device_train_batch_size16, learning_rate2e-5, weight_decay0.01, evaluation_strategyepoch, save_strategyepoch, load_best_model_at_endTrue, metric_for_best_modelf1, ) trainer Trainer( modelmodel, argsargs, train_datasettrain_data, eval_datasetvalid_data, ) trainer.train()代码说明per_device_train_batch_size16 是在单张 12G 显存卡上的稳妥值显存小可以改成 8 再加梯度累积learning_rate2e-5 是 BERT 微调的经典区间超过 1e-4 容易把预训练权重冲坏。metric_for_best_modelf1 是因为情感数据集往往类别不均衡只看 accuracy 会选出次优 checkpoint。如果你只是想先跑通再慢慢精调文本部分也可以先用 TF-IDF 加逻辑回归做基线一个下午就能出结果。这份资源没有强制要求必须用 BERTREADME 里也提到两种方案。我的建议是先跑 LR 基线拿到准确率下限再上 BERT这样才能看出预训练模型到底给你带来了多少提升。上来就微调 BERT反而说不清效果是模型贡献还是数据本身好。3.2 语音情感MFCC 与 opensmile 的取舍语音情感最常用的特征是 MFCC。MFCC 把语音切成几十毫秒的帧每帧算一组倒谱系数能抓住音色和韵律的一部分信息。代码里提供的是 40 维 MFCC 加一阶、二阶差分拼成 120 维向量再交给 LSTM 或一维 CNN 分类。import librosa import numpy as np def extract_mfcc(wav_path: str, n_mfcc: int 40, max_len: int 300) - np.ndarray: # sr16000 是语音常用采样率低于会损失细节太高则计算量翻倍 y, sr librosa.load(wav_path, sr16000) mfcc librosa.feature.mfcc(yy, srsr, n_mfccn_mfcc) delta1 librosa.feature.delta(mfcc, order1) delta2 librosa.feature.delta(mfcc, order2) feat np.concatenate([mfcc, delta1, delta2], axis0) # 40*3120 维 if feat.shape[1] max_len: feat np.pad(feat, ((0, 0), (0, max_len - feat.shape[1])), modeconstant) else: feat feat[:, :max_len] return feat # shape: [120, max_len]参数说明n_mfcc40 表示前 40 阶倒谱系数再多基本是高频噪声max_len300 帧对应约 3 秒音频超过直接截断不足补零。这个参数和数据集中音频的平均长度强相关——如果样本普遍在 10 秒以上建议把 max_len 提到 500 到 600不然信息会全被截掉。librosa.load 默认会把音频重采样到 sr 指定的数值mp3 也能读但底层需要 ffmpeg 解码这点放在避坑部分细说。opensmile 是另一条路线一次性抽出几千维手工特征覆盖基频、过零率、能量等统计量。缺点是它是个独立二进制工具部分 Linux 机器上编译比较折腾。这份资源的主代码用的是 MFCCopensmile 作为备选写在文档里适合做交叉验证。MFCC 提取后一般接一个两层 LSTM把时序信息压成固定维度import torch import torch.nn as nn class AudioClassifier(nn.Module): def __init__(self, input_dim120, hidden_dim128, num_classes3): super().__init__() self.lstm nn.LSTM(input_dim, hidden_dim, batch_firstTrue, bidirectionalTrue) self.classifier nn.Sequential( nn.Linear(hidden_dim * 2, 64), nn.ReLU(), nn.Linear(64, num_classes), ) def forward(self, x): # x: [batch, 300, 120] out, _ self.lstm(x) # 时序维度做全局平均池化 pooled out.mean(dim1) # [batch, hidden*2] return self.classifier(pooled)这是个常见的分类头结构双向 LSTM 从正反两个方向读语音帧拼接成 256 维表示然后时序全局平均池化成一条向量。hidden_dim128 在课程作业场景下足够太大容易过拟合。注意这里在 LSTM 后面做均值池化而不是只用最后一步是因为语音情感是整段韵律的叠加单独看最后一帧很容易被尾音带偏。3.3 文本与语音特征对齐统一维度、统一池化两路特征维度不一致是从 demo 到正式训练最容易卡住的地方。文本 BERT 的输出是 768 维语音 LSTM 输出是 256 维直接拼接会让融合层的输入维度失衡。常见做法是各接一层全连接把各自压到同一维度比如 256 或 512。def align_text_feature(text_last_hidden, out_dim256): # text_last_hidden: [batch, seq_len, 768] - 取 CLS 向量池化 pooled text_last_hidden[:, 0, :] # CLS token return torch.relu(nn.Linear(768, out_dim)(pooled)) # [batch, 256] def align_audio_feature(audio_pooled, out_dim256): # audio_pooled: [batch, 256] 已经过时序池化 return torch.relu(nn.Linear(256, out_dim)(audio_pooled)) # [batch, 256]逻辑是这样文本用 CLS 向量代表整句语义语音用 LSTM 全局池化的输出各自过一个线性层压到 256 维。两个 256 维向量拼接成 512 维就是融合层的输入。这里要提醒一句线性层别加太狠一个 ReLU 够用别在这个位置堆三层 MLP模态内部的信息还没融合就被冲淡了。4. 图像与视频模态落地抽帧、人脸对齐与帧级置信度图像和视频在技术上是一条线先检测人脸再做表情分类最后把帧级结果池化成视频级结果。区别只在视频要先抽帧后处理要多考虑时间信息。如果只做图像不做视频可以跳过抽帧那一步直接进 4.2。4.1 视频抽帧按 fps 自适应间隔而不是固定帧数视频抽帧有个常见坑固定每 10 帧抽一帧不同视频的 fps 不一样有的 30 帧每秒有的 24 帧每秒抽出来的帧数天差地别后续帧级投票的基数就完全不可比。我一般按原始帧率自适应取间隔保证每秒抽 1 帧左右import cv2 def extract_frames(video_path: str, output_dir: str, interval_seconds: float 1.0): cap cv2.VideoCapture(video_path) fps cap.get(cv2.CAP_PROP_FPS) # 原视频帧率 interval max(1, int(fps * interval_seconds)) # 每隔多少帧抽一张 frame_idx 0 saved 0 while True: ret, frame cap.read() if not ret: break if frame_idx % interval 0: out_path f{output_dir}/frame_{saved:04d}.jpg cv2.imwrite(out_path, frame) saved 1 frame_idx 1 cap.release() print(f抽出 {saved} 帧视频时长约 {frame_idx / fps:.1f} 秒)参数说明interval_seconds1 表示每秒保留一帧对短视频足够长视频可以改成 2 到 3减少后续人脸检测的计算量。如果视频里说话速度很快建议降到 0.5 秒一帧不然嘴型和情绪变化容易被漏掉。interval 先算 fps 再取整保证了不同帧率视频抽出来的帧数比例一致。抽完帧之后接着做人脸检测。这里有个我常踩的坑如果某一帧没有检测到人脸直接丢弃这一帧不要拿整张画面去做情感分类。因为画面里的背景、光线、文字板都会干扰模型模型会学到“场景情绪”而不是“人脸情绪”。如果一段视频中途镜头切走那几帧的画面基本全是噪声标签。4.2 图像表情识别从 FER 类别映射到三分类情感人脸检测完成后把人脸区域裁剪、缩放到统一尺寸常见 48x48 或 224x224送进表情分类模型。表情模型输出通常是 7 类或 8 类生气、厌恶、害怕、开心、难过、惊讶、中性。多模态情感分析最终要的是积极/中性/消极三分类映射规则通常这样写表情类别映射结果开心积极中性中性生气、厌恶、害怕、难过消极惊讶看置信度阈值以上算积极否则中性映射表在代码里是个 dict改起来不费事。但强烈建议保留原始 7 类输出别只存映射后的 3 类。因为后面做消融实验或可视化时想细分情绪成因还得用原始类别。帧级的置信度还有一个好处视频池化时可以不直接用帧级 one-hot 标签而是保留 3 维概率向量。这样如果某一帧人脸模糊导致分类置信度很低它的概率分布会比较平在池化阶段不会被当成一个坚定的“开心”或“难过”来投票对最终判决的影响更小。4.3 人脸清晰度筛选与检测边界问题抽出来的帧里经常有模糊、侧脸过大、多人脸这些边缘情况。我在复现时发现模型对模糊人脸的输出极不稳定一会儿积极一会儿消极能明显拉低视频级指标。解法是加一个清晰度过滤器import cv2 def is_blurry(face_img, threshold100.0): gray cv2.cvtColor(face_img, cv2.COLOR_BGR2GRAY) # Laplacian 算子对边缘敏感清晰图方差大模糊图方差小 laplacian_var cv2.Laplacian(gray, cv2.CV_64F).var() return laplacian_var threshold逻辑说明Laplacian 算子求图像二阶导清晰图像边缘锐利、方差大模糊图方差小。threshold100 是我在多数视频上试出来的经验值屏幕录制内容可以放宽到 60户外实拍运动模糊多可以调到 150。强烈建议先抽几帧人工看一下再定阈值不要照搬参数。过滤完之后再做去重连续帧里人脸位置几乎没变的保留置信度最高的那一帧即可减少重复计算。多人脸的情况保留面积最大的那张脸。直播类视频偶尔出现背景里有人、近处才是主播面积最大策略通常不会出错。如果项目定位是多人对话需要按人头分别抽特征记录那样要把模态对齐从“样本级”改成“说话人级”工作量会多一个量级课程作业一般不要求。5. 融合训练与避坑指南超参、评估与四个高频翻车点到这里四个模态已经各有各的特征向量了。融合层负责把这四路向量合成一个三维的情感概率分布。融合看似只是拼接一个全连接层但训练时的超参设置、类别不均衡处理以及多路数据的缓存方式决定了准确率到底是大作业能见人的水平还是只能跑通 demo。5.1 融合时机拼接、投票还是注意力融合策略常见分三类早融合、晚融合、中间融合。这份资源按简单可复现的原则实现了两种特征拼接的早融合以及四个单模态软投票的晚融合。特征拼接的优势是让模型自己学出模态之间的交叉作用缺点是训练数据不够多时容易过拟合。软投票完全不训练只对四个单模态的置信度向量做加权平均或乘积规则稳定、可解释性强缺点是无法建模“某模态只在特定场景下可信”。建议实验时两个都做对比之后再定稿。import torch import torch.nn as nn class EarlyFusion(nn.Module): def __init__(self, in_dims, num_classes3, hidden256): super().__init__() # in_dims: [text_dim, audio_dim, image_dim, video_dim] fusion_dim sum(in_dims) self.fc1 nn.Linear(fusion_dim, hidden) self.fc2 nn.Linear(hidden, num_classes) self.dropout nn.Dropout(0.3) self.act nn.ReLU() def forward(self, feats): # feats: [batch, 4, feat_dim]每个模态已对齐到统一维度 x torch.cat(list(feats), dim-1) # [batch, 4*feat_dim] x self.dropout(self.act(self.fc1(x))) return self.fc2(x) # [batch, num_classes]参数说明hidden256 在课程作业场景下够用。in_dims 是四路对齐后的向量维度如果每路是 256拼接后 fusion_dim 就是 1024。dropout0.3 是对抗过拟合的关键四路特征拼接后维度变大不 dropout 很容易把训练集背下来。如果你发现训练集准确率 99%、测试集只有 70%先把 dropout 提到 0.5再砍 hidden 到 128通常能救回来。晚融合不需要写网络对四个单模态的 prob 向量做加权平均就行def late_fusion(text_prob, audio_prob, image_prob, video_prob, wNone): # 权重默认按验证集单模态 F1 比例折算下面是个经验值 if w is None: w [0.3, 0.2, 0.2, 0.3] # 文本和视频权重略高 probs [text_prob, audio_prob, image_prob, video_prob] fused sum(wi * p for wi, p in zip(w, probs)) return fused / sum(w)权重这块写的是经验值。文本在表达语义上最直接视频包含动态表情信息所以各占三成音频和图像作为佐证各占两成。如果你跑完单模态实验发现语音模型的 F1 最高就把 audio 权重调到 0.4。权重一定来自单模态验证集指标不要拍脑袋定死。5.2 训练策略与评估指标别让准确率骗了你融合模型和 BERT 微调的优化器要分开设置。BERT 动 2e-5融合层动 1e-3 是常见配置。如果用一个学习率同时更新两部分预训练权重很容易被破坏。代码里用两个参数组分别命名走的是 AdamW 加线性衰减的 warmup。warmup 比例建议设 0.1让模型在前几个 step 用小步长热身防止一上来就闯进损失爆炸的区域。评估指标只看准确率在答辩时很容易被老师 challenge。情感数据集通常有明显偏好比如“中性”样本最多模型全输出中性也能拿到很高的 acc但实际完全不可用。这里至少报告三个数Macro-F1、Weighted-F1、混淆矩阵。Macro-F1 对少数类更敏感类别偏移时能看出模型是在“装死”还是真学了特征。一份可以放进答辩 PPT 的指标表模型AccMacro-F1Weighted-F1仅文本0.780.710.76仅语音0.650.550.62仅图像0.640.530.61仅视频0.700.620.68早融合0.830.780.82晚融合0.800.750.79这张表的价值在于证明“融合比单模态更好”是靠对比出来的。如果融合后的指标反而低于最好的单模态别急着改网络结构先查数据对齐、查特征有没有张冠李戴。提示跑多模态实验前先固定随机种子否则每次跑出来的 F1 会有零点几个点的波动后面对比结果很难解释清楚。5.3 踩坑记录四个高频翻车点与排查方法坑位一mp3 加载报错librosa 读不出文件。现象librosa.load 一个 mp3 文件抛 RuntimeError: Error opening xxx.mp3或者返回空数据。原因librosa 的底层解码依赖 libsndfile而 libsndfile 本身不带 mp3 解码协议需要额外的系统库支持。解决conda install -c conda-forge ffmpeg然后重装 soundfile。更稳的做法是数据预处理阶段统一转 wavffmpeg -i in.mp3 -ar 16000 -ac 1 out.wav后续流程只用 wav避免每次都隐式解码。坑位二视频级特征为零向量融合结果完全被文本主导。现象预测阶段 video 模态输出全 0融合结果和文本单模态几乎一样。原因视频抽帧时 OpenCV 读不到文件或者路径里有中文cap.read() 一直返回 False帧列表是空的。人脸检测也可能一直失败。解决先打印 cap.isOpened() 确认视频读取成功中文路径用 np.fromfile 配合 cv2.imdecode 处理绕过 OpenCV 对中文路径的限制确实检测不到人脸时用整帧画面做一次弱分类并把置信度打低不要把空特征硬拼进融合层。坑位三训练 OOM刚起步就崩。现象一跑 train.pyGPU 直接报 CUDA out of memorybatch_size 调成 1 也没用。原因代码把视频所有帧的特征一次性放进了显存。视频段有几十帧每帧一次 forward特征累积起来很可观。解决先跑一遍 feature_video.py 把所有视频特征导出到 npy 或 h5py训练时直接从磁盘读取特征矩阵batch_size 用 2 起步确认显存占用平稳再往上调。这个做法还能明显缩短每个 epoch 的时间。坑位四明明加了“融合”结果跟单模态一模一样。现象融合模型的 F1 和文本单模态几乎一致融合层像没用。原因输入特征没有按模态归一化。文本 BERT 的特征值范围在十以内图像 CNN 的输出可能几十甚至上百大数值模态在拼接后会压制其他模态的梯度。解决对四路特征各自做 Z-Score 归一化或者在融合层前加 LayerNorm。我复现时只加了一层 LayerNormF1 就涨了零点几个百分点。6. 进阶验证从能跑到能演示的三个习惯训练完模型不是终点。答辩或演示场景里真正难的是被问一句“你的融合到底比单模态好在哪里”答不上来就尴尬了。我建议养成三个习惯都是低成本、高回报的验证动作。第一个习惯每次实验把四路单模态的 F1 都打出来。没有单模态基线融合提升无从谈起。前面 5.2 那张表多跑几步就能产出来把它焊死在实验记录里。第二个习惯把融合前的四个概率向量可视化。用 matplotlib 拼一张四格小图每格显示一个模态的概率条形分布融合层的输出加粗框标红。答辩时把图放一页 PPT评委很容易看懂“融合是怎么把几个模态的置信度综合起来的”。人不爱读代码爱看图。第三个习惯固定随机种子重复运行三次取均值。多模态模型在小数据集上对随机种子很敏感不同的 seed 可能让 F1 差到 0.4 个点。单次结果说服力不足重复实验取均值才能站得住。代码里用 seed_everything 跑三遍把 mean 和 std 一起写进汇报里。讲一个小血泪经验我做过一次多模态融合融合准确率 92%自信满满拿到组里汇报结果一查发现文本单模态也有 90%融合带来的提升根本不足以支撑那套部署成本。从那以后我每次开始新项目都强制走一遍“先单模态、后融合、再消融”的流程——用最小成本先把基线钉死再谈技术花活。这个习惯也分享给你希望帮到你。本文还有配套的精品资源点击获取
