
简介基于PyTorch框架实现的SEGAN语音增强生成对抗网络项目面向语音信号处理、深度学习和音频AI领域的开发者与研究人员重点解决嘈杂环境下语音信号清晰化这一实际工程问题。项目完整覆盖数据预处理、模型训练与模型测试三大核心模块预处理负责语音清洗与特征提取训练采用GAN对抗策略迭代优化参数测试则从信噪比、可懂度与失真度等方面评估增强效果。资源包共11个文件以5个Python脚本为主体涵盖数据预处理、模型训练与测试等任务另有说明文档、README及附赠Word资料整体仅43KB结构轻量、便于快速复现与学习。目前已有77人学习下载。通过研读源码与文档可掌握SEGAN网络搭建、语音特征处理、生成对抗训练及效果评估的完整流程适合作为语音增强入门或GAN实战的参考项目。1. SEGAN语音增强项目为什么说它比传统降噪更适合复杂噪声做语音处理的人应该都有这种经验传统的频谱相减和维纳滤波在稳态噪声下效果还行一旦遇到车辆鸣笛、键盘敲击、餐厅人声这类非平稳噪声降噪后的语音要么“音乐噪声”明显要么把清音和辅音一起削掉。SEGANSpeech Enhancement Generative Adversarial Network是Santi Pascual等人在2017年提出的方案核心思路是让生成对抗网络直接学习带噪语音到干净语音的端到端映射用判别器逼着生成器输出听起来自然的语音。这个基于PyTorch实现的SEGAN项目把数据预处理、生成器判别器构建、训练循环和测试推理串成了完整pipeline拿来跑实验和改网络结构都很顺手。适合三种人正在做语音增强课题的学生、想把手头降噪算法换成深度学习方案的工程师、以及刚入门PyTorch但想做生成对抗网络实战的开发者。2. SEGAN 的模型骨架生成器、判别器与损失函数的工程选型2.1 为什么SEGAN选择全卷积U-Net而不是LSTM或Transformer语音增强任务早期有过用LSTM做序列建模的方案但对实时性要求高的场景LSTM的循环结构在推理阶段存在串行依赖GPU利用率上不去。SEGAN的生成器采用全卷积U-Net结构编码器通过strided convolution逐步下采样解码器通过transposed convolution逐步恢复时间分辨率中间用skip connection把编码器各层的特征直接拼到解码器对应层。这样做的直接好处是网络在16kHz采样率下处理1秒音频16000个采样点时前向传播可以完全并行单张1080Ti上推理一个片段只要几十毫秒。生成器的具体结构是输入是带噪语音的原始波形片段经过5层卷积编码器每层卷积核大小从32逐渐增加到1024步长全部为2每层后接Leaky ReLU和batch normalization。编码器输出的latent representation经过一个同样深度对称的解码器最后一层用tanh激活把输出映射到[-1, 1]范围正好对应归一化后的语音波形幅值。下面是生成器的PyTorch实现要点代码里我把每一层的通道数和kernel size都写清楚了import torch import torch.nn as nn class Generator(nn.Module): def __init__(self, input_channels1, latent_channels1024): super(Generator, self).__init__() # 编码器输入 (batch, 1, 16384)逐层下采样 self.enc1 nn.Sequential( nn.Conv1d(input_channels, 32, kernel_size32, stride2, padding15), nn.LeakyReLU(0.2, inplaceTrue), nn.BatchNorm1d(32)) self.enc2 nn.Sequential( nn.Conv1d(32, 64, kernel_size32, stride2, padding15), nn.LeakyReLU(0.2, inplaceTrue), nn.BatchNorm1d(64)) self.enc3 nn.Sequential( nn.Conv1d(64, 128, kernel_size32, stride2, padding15), nn.LeakyReLU(0.2, inplaceTrue), nn.BatchNorm1d(128)) self.enc4 nn.Sequential( nn.Conv1d(128, 256, kernel_size32, stride2, padding15), nn.LeakyReLU(0.2, inplaceTrue), nn.BatchNorm1d(256)) self.enc5 nn.Sequential( nn.Conv1d(256, latent_channels, kernel_size32, stride2, padding15), nn.LeakyReLU(0.2, inplaceTrue), nn.BatchNorm1d(latent_channels)) # 解码器每层先上采样再卷积最后接tanh self.dec5 nn.Sequential( nn.ConvTranspose1d(latent_channels, 256, kernel_size32, stride2, padding15), nn.LeakyReLU(0.2, inplaceTrue), nn.BatchNorm1d(256)) self.dec4 nn.Sequential( nn.ConvTranspose1d(512, 128, kernel_size32, stride2, padding15), nn.LeakyReLU(0.2, inplaceTrue), nn.BatchNorm1d(128)) self.dec3 nn.Sequential( nn.ConvTranspose1d(256, 64, kernel_size32, stride2, padding15), nn.LeakyReLU(0.2, inplaceTrue), nn.BatchNorm1d(64)) self.dec2 nn.Sequential( nn.ConvTranspose1d(128, 32, kernel_size32, stride2, padding15), nn.LeakyReLU(0.2, inplaceTrue), nn.BatchNorm1d(32)) self.dec1 nn.Sequential( nn.ConvTranspose1d(64, 1, kernel_size32, stride2, padding15), nn.Tanh()) def forward(self, x): # 编码 e1 self.enc1(x); e2 self.enc2(e1); e3 self.enc3(e2) e4 self.enc4(e3); e5 self.enc5(e4) # 解码带skip connection d5 self.dec5(e5) d5 torch.cat([d5, e4], dim1) d4 self.dec4(d5) d4 torch.cat([d4, e3], dim1) d3 self.dec3(d4) d3 torch.cat([d3, e2], dim1) d2 self.dec2(d3) d2 torch.cat([d2, e1], dim1) d1 self.dec1(d2) return d1这里有几个关键参数值得细说。第一所有卷积的kernel_size都是32、stride都是2配合padding15保证每一层输出长度是输入长度的一半不会因为尺寸对不上报错。第二decoder里每次transposed conv之后把输入长度翻倍这时和对应的encoder层做torch.cat通道数变成两倍所以dec4、dec3、dec2、dec1的输入通道数分别是512、256、128、64写代码时最容易在这个地方把通道数搞错。第三输入波形长度必须是2的5次方倍数也就是至少16384个采样点如果音频长度不够要先做padding或者重复拼接。2.2 判别器结构与L1损失的博弈平衡判别器和生成器不同不需要输出完整波形只需要判断输入波形是“真干净语音”还是“生成器伪造的增强语音”。实践中SEGAN的判别器采用和编码器类似的卷积堆叠结构输入可以是整段波形也可以是切成patch的子片段这个项目里用的是整段波形输入。损失函数方面SEGAN原文用的是L1损失加GAN损失的组合。L1损失保证增强语音和干净语音在波形幅度上接近GAN损失让生成器学会骗过判别器两者权重用参数λ控制。我一般把λ设在10到30之间——λ太小生成器会只讨好判别器输出带有“伪自然感”但波形失真λ太大GAN的对抗优势就被淹没了增强结果退化成纯L1回归。# 损失计算示意L1_loss_weight 对应论文里的 lambda l1_loss nn.L1Loss() bce_loss nn.BCEWithLogitsLoss() # 生成器损失 L1 lambda * GAN损失 g_l1 l1_loss(fake_audio, clean_audio) g_gan bce_loss(discriminator(fake_audio), torch.ones_like(discriminator(fake_audio))) g_loss g_l1 lambda_param * g_gan # 判别器损失 真实样本loss 伪造样本loss d_real bce_loss(discriminator(clean_audio), torch.ones_like(discriminator(clean_audio))) d_fake bce_loss(discriminator(fake_audio.detach()), torch.zeros_like(discriminator(fake_audio.detach()))) d_loss d_real d_fake这段代码里有个细节计算判别器对伪造样本的损失时fake_audio必须加detach()否则梯度会回传到生成器导致生成器和判别器同时更新——这个坑我在初跑SEGAN时踩过现象是loss曲线震荡剧烈生成音频出现“嗡嗡”的周期性噪声。另外BCEWithLogitsLoss自带sigmoid判别器最后一层不用额外加激活函数直接输出logits就行。2.3 选型复盘这个项目相比wave-u-net和DeepDenoiser的取舍同样在PyTorch里做语音增强还有wave-u-net和DeepDenoiser两个流行方案。wave-u-net把U-Net和WaveNet的dilated convolution结合感受野更大建模长时依赖更强但参数量和计算量都高出SEGAN不少DeepDenoiser是基于LSTM的复数域降噪对相位处理更精细但训练收敛速度慢且需要更多调参经验。SEGAN的优势在于结构简洁——纯卷积无递归、无dilated convolution训练稳定单卡就能跑而且生成器推理时不需要缓存中间状态工程上落地更容易。如果只是想把深度语音增强跑通并出结果SEGAN是性价比最高的起点。3. 数据集预处理wav 怎么变成模型能吃的 waveform pair3.1 训练数据的组织方式干净语音和噪声的配对逻辑SEGAN的训练需要干净语音和带噪语音组成pair。常见做法是从DNS Challenge或LibriSpeech里取干净语音从MUSAN噪声库或DEMAND里取噪声按随机信噪比通常0dB、5dB、10dB三档混合。这个项目里数据的目录结构建议这样组织train文件夹下分clean和noisy两个子目录两个目录下的文件名一一对应。比如clean/001.wav和noisy/001.wav就是一对。测试集单独建test文件夹同样分clean和noisy。生成带噪语音的脚本可以离线跑一次也可以做成dataset里的在线增强。离线生成的优点是训练时没有额外的CPU开销缺点是数据多样性少一点在线增强能模拟出更多的信噪比组合但每个epoch都要在CPU上混音容易成为瓶颈。# 用 sox 生成混合语音600_urban 是MUSAN里的城市噪声片段 # clean_001.wav 是干净语音snr5 表示信噪比5dB sox -m clean_001.wav -v 0.56 600_urban.wav noisy_001.wav # 批量生成脚本片段注意 -v 是噪声增益系数snr越高增益越小 for i in $(seq 1 200); do sox -m clean/${i}.wav -v 0.89 noise/${i}.wav noisy/${i}.wav done这段bash里sox的-v参数需要根据目标信噪比换算增益公式是增益干净语音RMS/(噪声RMS×10^(SNR/20))。如果SNR510^(5/20)1.78意味着噪声幅度要压到原来的约56%。所以上面0.56这个值对应的就是SNR5。用sox批量生成的好处是快速、无依赖缺点是无法随机化每个文件的具体信噪比——如果你需要更细致的多档SNR控制建议用Python的soundfile库在dataset里实时混音。3.2 PyTorch Dataset类加载音频、切片段、归一化一条龙音频加载到模型里不能直接用原始wav要做三步处理采样率统一到16kHz、单声道、幅值归一化到[-1,1]。然后切成固定长度16384个采样点正好1秒的片段。如果音频长度不足1秒需要做循环填充但实际训练时建议直接丢弃这些过短的片段避免模型学到“重复波形”的伪规律。import torch import soundfile as sf from torch.utils.data import Dataset class SEGANDataset(Dataset): def __init__(self, clean_dir, noisy_dir, segment_len16384): self.clean_dir clean_dir self.noisy_dir noisy_dir self.segment_len segment_len # 文件名一一对应直接取交集确保不缺失 clean_files set(f.stem for f in clean_dir.glob(*.wav)) noisy_files set(f.stem for f in noisy_dir.glob(*.wav)) self.file_stems sorted(list(clean_files noisy_files)) def _load_and_pad(self, file_path): audio, sr sf.read(file_path, dtypefloat32) # 如果采样率不是16k先重采样这里省略建议离线处理好 assert sr 16000, f采样率必须为16kHz当前: {sr} if len(audio.shape) 1: audio audio.mean(axis1) # 多声道转单声道 if len(audio) self.segment_len: pad_len self.segment_len - len(audio) audio torch.cat([audio, audio[:pad_len]], dim0) return audio def __getitem__(self, idx): stem self.file_stems[idx] clean_path self.clean_dir / f{stem}.wav noisy_path self.noisy_dir / f{stem}.wav clean self._load_and_pad(clean_path) noisy self._load_and_pad(noisy_path) # 随机切出1秒片段并且clean和noisy用相同起点 start torch.randint(0, len(clean) - self.segment_len 1, (1,)).item() clean clean[start:start self.segment_len] noisy noisy[start:start self.segment_len] # 添加channel维模型输入是 (batch, 1, segment_len) return torch.FloatTensor(noisy).unsqueeze(0), torch.FloatTensor(clean).unsqueeze(0) def __len__(self): return len(self.file_stems)有几个点需要额外注意。第一是随机切片的start必须对clean和noisy用同一个值否则pair就错位了模型学不到正确的映射。第二soundfile读取时直接指定dtypefloat32省去从int16转float的精度损失。第三AudioSegment的归一化在写入数据前做——如果源wav本来就是16-bit PCMsf.read返回的值已经在[-1,1]区间不需要再手动除以32768但如果是32-bit float wav数值范围本身就是[-1,1]也不用额外处理。真正需要小心的是有些数据集是int16格式的flac用soundfile读出来一样是float32但要注意有没有DC偏移。3.3 训练集和验证集的切分比例与混响处理训练集、验证集按8:2切就行但要注意验证集里不能出现训练集同一段语音的不同噪声混合版本——否则验证指标虚高训练到后期发现loss降了但实际增强效果没怎么提升。另外如果只想做“降噪”而不做“去混响”混合数据时不要加RIR卷积保持纯加性噪声模型。如果你手头数据量不够常用的招数是把同一个噪声源切片后拼接到不同的干净语音上变相扩充数据集。# 目录结构参考 # ├── data # │ ├── train # │ │ ├── clean # │ │ └── noisy # │ ├── val # │ │ ├── clean # │ │ └── noisy # │ └── test # │ ├── clean # │ └── noisy4. 训练与测试从 batch size 到 checkpoint 的完整跑通路径4.1 训练启动参数选择与训练循环SEGAN训练的最小配置是batch size64、片段长度16384、Adam优化器学习率0.0002、betas(0.5, 0.999)。batch size和片段长度决定了每step显存消耗一个batch的输入张量形状是(64, 1, 16384)float32占用4MB加上中间特征图和判别器计算12GB显存的卡跑这个配置比较从容。如果你只有8GB显存把batch size降到32同时把片段长度改成81920.5秒编码器层数不变表现不会差太多。import torch.optim as optim def train_one_epoch(model, disc, dataloader, optimizer_g, optimizer_d, lambda_param20.0): for batch_idx, (noisy, clean) in enumerate(dataloader): noisy, clean noisy.cuda(), clean.cuda() # 先更新判别器 optimizer_d.zero_grad() fake model(noisy).detach() d_real_loss bce_loss(disc(clean), torch.ones_like(disc(clean))) d_fake_loss bce_loss(disc(fake), torch.zeros_like(disc(fake))) d_loss (d_real_loss d_fake_loss) * 0.5 d_loss.backward() optimizer_d.step() # 再更新生成器 optimizer_g.zero_grad() fake model(noisy) g_l1 l1_loss(fake, clean) g_gan bce_loss(disc(fake), torch.ones_like(disc(fake))) g_loss g_l1 lambda_param * g_gan g_loss.backward() optimizer_g.step() if batch_idx % 100 0: print(f[{batch_idx}/{len(dataloader)}] D_loss: {d_loss.item():.4f}, G_loss: {g_loss.item():.4f})注意判别器的真实样本和伪造样本loss取平均后再反向传播这是cGAN系列的标准做法防止d_loss值太大导致判别器梯度消失。生成器的GAN loss这里用的也是BCElogits目标为1。另外生成器更新时用的是forward第二次计算出的fake不能复用第一次detach后的结果否则梯度到不了生成器。4.2 学习率调度与训练时长怎么判断模型收敛SEGAN虽然是个GAN但训练相对温和不需要像StyleGAN那样精细的渐进式训练策略。建议在epoch 50时把学习率从0.0002降到0.0001如果用的是CosineAnnealingLR周期设成总epoch数。训练时长上单卡3060Ti跑16000句训练音频约5小时数据量50个epoch大约要20到28小时。判断收敛的手段有两个看生成器L1 loss已经降到平稳比如0.02以下更重要的是让训练集外的带噪语音过一遍模型用耳朵听它输出的音乐噪声是不是明显减少了——听感才是最终标准数值上PESQ或STOI至少比输入带噪语音高0.15个点才算有效训练。# 启动训练日志和checkpoint都落到本地 python train.py \ --train_clean ./data/train/clean \ --train_noisy ./data/train/noisy \ --val_clean ./data/val/clean \ --val_noisy ./data/val/noisy \ --batch_size 64 \ --lr 0.0002 \ --epochs 100 \ --lambda 20 \ --checkpoint_dir ./checkpoints4.3 测试推理加载checkpoint把带噪语音转成干净语音模型训练好之后推理代码很轻量。加载训练时的生成器权重把输入音频切块、逐块推理、再拼接回完整波形。这一步有个容易翻车的点如果切块长度是16384而音频总长度是16000010秒最后一段不足1秒需要处理。常见做法是把最后一段也pad到16384推理完再把padding部分裁掉。但相邻块的边界处会出现“拼接咔哒声”这源于transposed convolution在边界处的边缘效应。解决方式是用1/4片段长度的overlap——也就是每段只取中间部分的有效输出前1/8和后1/8丢弃。import torch import soundfile as sf def enhance(model, noisy_path, output_path, segment_len16384, overlap_len4096): model.eval() audio, sr sf.read(noisy_path, dtypefloat32) if len(audio.shape) 1: audio audio.mean(axis1) # 计算需要多少个片段 step segment_len - overlap_len num_segments max(1, (len(audio) - segment_len step - 1) // step) enhanced_parts [] with torch.no_grad(): for i in range(num_segments): start i * step segment audio[start:start segment_len] if len(segment) segment_len: segment torch.cat([segment, segment[:segment_len - len(segment)]]) inp torch.FloatTensor(segment).unsqueeze(0).unsqueeze(0).cuda() out model(inp).squeeze().cpu().numpy() if i 0: enhanced_parts.append(out) else: # 中间段保留去除overlap后的有效区域 keep_start overlap_len // 2 keep_end segment_len - overlap_len // 2 enhanced_parts.append(out[keep_start:keep_end]) enhanced np.concatenate(enhanced_parts) sf.write(output_path, enhanced, sr)overlap_len设成segment_len的四分之一也就是4096这样每段推理有1/4的计算量是浪费的但换来了平滑衔接。实际测试下来直接用这个拼接方法听感上的边界咔哒声基本消失。如果你追求极致还可以在重叠区做线性交叉淡入淡出但工程上在段与段之间各取半段拼接已经够用。4.4 测试集评估STOI和PESQ的计算细节模型效果不能只靠耳朵还得量化。STOI和PESQ是两个最常用的客观指标分别反映可懂度和语音质量。STOI范围0到10.9以上就算清晰带噪0.75的音频增强到0.85就已经是很大提升。PESQ的范围是-0.5到4.5一般带噪语音1.5左右增强后能做到2.8以上就合格了。# 用 pystoi 计算 STOI注意要求输入是16k采样率 python - EOF from pystoi import stoi from pesq import pesq import soundfile as sf clean, sr sf.read(clean.wav, dtypefloat32) enhanced, sr2 sf.read(enhanced.wav, dtypefloat32) assert sr sr2 16000 # 两个文件长度可能不一致裁剪到相同长度 min_len min(len(clean), len(enhanced)) stoi_score stoi(clean[:min_len], enhanced[:min_len], sr, extendedFalse) pesq_score pesq(sr, clean[:min_len], enhanced[:min_len], wb) # wb 表示宽带 print(fSTOI: {stoi_score:.3f}, PESQ: {pesq_score:.3f}) EOF5. 避坑指南SEGAN 训练里我踩过的五个高频雷区5.1 loss 不下降、生成音频全是一片静音现象训练前几个epoch的损失下降正常到第10个epoch后生成器loss直接崩到很大输出波形全是接近0的静音。原因判别器训练得太强生成器梯度消失。本质上是L1损失和GAN损失的平衡被打破生成器发现“不管怎么学都骗不过判别器”干脆输出均值波形来降低L1损失。有时候是判别器的batch normalization统计量没更新好导致判别器对伪造样本的判别过于自信。解决把lambda从20降到10同时给判别器加label smoothing真实标签用0.9而不是1.0。另外检查一下判别器是否用了leaky relu如果误用了relu负区间梯度为0判别器学不到区分性特征。5.2 训练正常但增强结果有“金属声”或音乐噪声现象PESQ提升明显但听感有细碎的金属声尤其静音段处于模型输出的本底噪声比原始带噪语音更大。原因这是GAN生成器常见的“伪自然”效应。生成器在对抗训练中学会了快速波动的高频成分来骗过判别器这些成分在听感上就是音乐噪声。L1损失对高频细节的约束力不够。解决先确认是不是判别器输入只有整段波形——改成patch判别器也就是把输入切成多个小片段分别判断可以让判别器更关注局部细节生成器就不敢乱加高频。另外可以把L1 loss换成L1加L2的混合比如0.8×L1 0.2×L2强制生成器在波形幅度上更保守。5.3 推理时显存占用超过训练时的一倍现象训练batch size64很流畅但是单条音频推理时显存却狂吃甚至OOM。原因训练时输入是整个batchPyTorch的显存分配是按需的推理时如果你无意中打开了梯度记录没有写with torch.no_grad()模型前向会缓存所有中间激活用于反向传播。另外输入音频长度如果是16384的几十倍又没有切块处理单次前向的特征图叠加起来就很可观。解决推理函数里强制用with torch.no_grad()包裹并且把输入切块到固定segment_len。这两步做到之后单条任意长度音频的推理显存占用就恒定了。5.4 训练集和验证集的SNR分布不一致导致验证指标开倒车现象训练到epoch 30时验证集的STOI不升反降而训练集STOI还在涨。原因数据预处理时训练集混音用了0/5/10dB三档SNR验证集却混了3/6/9dB。模型在0dB的困难样本上没学透验证集全是中等难度样本反而暴露了模型对中等噪声的过拟合。这算是数据泄漏的变种——不是数据交叉是分布不匹配。解决数据准备脚本统一用同一个随机SNR生成器训练和验证共用一份噪声源文件列表只是随机种子不同。如果验证集是从不同来源找的带噪语音也要保证SNR范围覆盖训练集。5.5 音频长度不是16384整数倍导致维度报错现象DataLoader报错说输出张量形状不对一检查是某个wav文件长度是15863比target短。原因预处理里做了padding但padding逻辑只在文件完全不足segment_len时触发15863大于8192但小于16384随机切片的start计算没有做上限截断导致切片越界。解决把random切片换成先做一次中心填充把音频pad到segment_len的整数倍再随机选起始点。或者干脆在加载时先判断len(audio)和segment_len的关系不足1秒直接丢弃超过但非整数倍截断到最近的整数倍长度。6. 进阶验证用消融实验确认模型每一层的贡献SEGAN跑通之后接下来通常是“改结构”阶段。我习惯做一套消融实验来确认哪些设计真的有效而不是盲目堆参数。这个项目里最少值得做三组对照去掉skip connection、减少编码器层数、把L1换成L2。每组实验跑10个epoch就能看出趋势不必等完整训练收敛。做消融实验有一个关键原则每次只改一个变量其他超参数完全固定。比如测skip connection时生成器把torch.cat的代码注释掉decoder的输入通道改成单路其余结构不动。要一并改的只有通道数——dec4输入从512变成256dec3从256变128dec2从128变64dec1从64变32。# 消融实验脚本示例把原train.py的save名加上后缀避免覆盖 # 同时把输出目录改成ablations_config方便横向对比 python train.py \ --train_clean ./data/train/clean \ --train_noisy ./data/train/noisy \ --val_clean ./data/val/clean \ --val_noisy ./data/val/noisy \ --batch_size 64 \ --lr 0.0002 \ --epochs 10 \ --lambda 20 \ --checkpoint_dir ./ablations/no_skip验证阶段除了数值指标我每次都会保留几组固定的测试音频——同一批带噪文件、不同信噪比档位专门用来做“AB听感对比”。因为STOI只反映可懂度PESQ对音乐噪声的惩罚不够狠有些生成器改出来客观指标涨了0.02听感反而变差。拿固定测试集反复听是最快的回归手段。另外一个值得加进去的实验是改变判别器的更新频率。常见的做法是每更新2次判别器才更新1次生成器或者反过来。SEGAN作为相对简单的GAN判别器训练太快容易压过生成器训练太慢又让生成器失去提升动力。我习惯用判别器:生成器1:1起步如果发现d_loss一直比g_loss低很多就把比例改成2:1通常能有效缓解生成器梯度不稳的问题。做完这些验证我对“这个项目跑到什么水平算真正跑通了”的理解也更深了。从那以后我每次跑完一个语音增强模型都会强制走一遍固定测试听感对比加客观指标双确认而不是只看loss曲线。希望帮到你。本文还有配套的精品资源点击获取