
♂️ 个人主页艾派森的个人主页✍作者简介Python学习者 希望大家多多支持我们一起进步如果文章对你有帮助的话欢迎评论 点赞 收藏 加关注目录1.项目背景2.数据集介绍3.技术工具4.实验过程4.1导入数据4.2数据预处理4.3数据可视化4.4构建模型4.5训练模型4.6模型评估5.总结源代码1.项目背景在餐饮工业数字化与智慧零售飞速发展的背景下针对复杂食物品类的自动化识别已成为提升交易效率与用户体验的核心驱动力。快餐图像识别任务不同于常规的物体分类它面临着极高的类内差异性与类间相似性挑战——无论是汉堡层级的堆叠差异、披萨配料的多变纹理还是三明治与热狗在视觉构图上的高度重叠都要求算法必须具备极强的细粒度特征捕捉能力。同时真实场景中油亮的光泽干扰、多变的背景噪声以及不规则的摆盘方式也对模型的鲁棒性提出了极其苛刻的要求。本项目依托于BiT-M (Big Transfer) ResNet50这一尖端视觉架构针对 Kaggle 提供的包含 10 类热门快餐的图像数据集展开深度实战。BiT-M 作为谷歌研发的大规模迁移学习模型其核心优势在于通过在超大规模数据集上进行预训练淬炼出了远超普通残差网络的语义提取精度与泛化深度。实验采取了“强基固顶”的策略在冻结 BiT-M 强大视觉基座的基础上通过引入BatchNormalization稳定特征流并配合Dropout随机丢弃机制抑制小样本环境下的过拟合风险。这不仅是一场关于如何将顶尖预训练权重“嫁接”到垂直餐饮领域的工程演练更是一次探索轻量化微调技术在多分类任务中实现极致收敛效率的实战尝试为构建低延迟、高精度的商用级食品自动标注系统提供了标准化的技术蓝图。2.数据集介绍本实验数据集来源于Kaggle这是一个快餐分类数据集包含5 种不同类型快餐的图片。每个目录代表一个类别每个类别代表一种食物类型。这些类别包括汉堡油炸圈饼热狗比萨三明治3.技术工具Python版本:3.9代码编辑器jupyter notebook4.实验过程4.1导入数据在构建快餐识别系统的初期建立一个结构清晰、逻辑严密的底层数据管道是成功的关键。我们首先集成了从基础图像处理OpenCV, PIL到深度学习核心框架TensorFlow Hub的完整技术栈特别是引入了BiT-M预训练权重接口。在数据加载阶段我们放弃了简单的流式读取转而编写了一个高度灵活的L_Data函数。该函数通过深度遍历 Kaggle 挂载的快餐数据集目录自动将复杂的文件夹层级结构映射为包含“物理路径”与“类别标签”的 Pandas DataFrame。这种结构化的预处理方式不仅方便我们后续对训练集Train和测试集Test进行规模核验也为后续利用 Seaborn 进行样本均衡度分析提供了极其便利的数据切面。# --- 1. 导入核心库与快餐识别所需组件 --- import cv2 import os import pandas as pd import numpy as np import seaborn as sns from PIL import Image import matplotlib.pyplot as plt import matplotlib.image as mpimg import tensorflow as tf import tensorflow_hub as hub # 用于加载谷歌 BiT-M 预训练模型 from tensorflow import keras from keras import Sequential from keras.layers import * from tensorflow.keras.losses import BinaryCrossentropy from sklearn.preprocessing import LabelEncoder from tensorflow.keras.optimizers import Adam, Adamax from tensorflow.keras.applications import * from tensorflow.keras.callbacks import EarlyStopping from keras.preprocessing.image import ImageDataGenerator import warnings warnings.filterwarnings(ignore) # 定义统一的视觉配色方案 palette [#606060FF, #D6ED17FF] # --- 2. 自定义数据加载逻辑构建路径标签映射表 --- def L_Data(directory): 遍历目录结构将图像路径与文件夹名即标签整合为 DataFrame filepath [] label [] # 获取所有子文件夹名代表汉堡、披萨等不同快餐类别 folds os.listdir(directory) for fold in folds: f_path os.path.join(directory, fold) imgs os.listdir(f_path) for img in imgs: # 记录每一张快餐图像的完整物理路径 img_path os.path.join(f_path, img) filepath.append(img_path) label.append(fold) # 利用 Pandas 将列表转化为强类型的 Series 并拼接 file_path_series pd.Series(filepath, namefilepath) Label_path_series pd.Series(label, namelabel) df_train pd.concat([file_path_series, Label_path_series], axis1) return df_train # --- 3. 执行数据索引构建 --- # 定义原始训练集与测试集所在的 Kaggle 目录路径 directory_T /kaggle/input/fast-food-classification-dataset/Fast Food Classification V2/Train directory_TE /kaggle/input/fast-food-classification-dataset/Fast Food Classification V2/Test # 构建训练与测试数据框 tr_d L_Data(directory_T) te_d L_Data(directory_TE) # 输出数据规模确认样本加载是否完整 print(f训练集规模 (Train data shape): {tr_d.shape}) print(f测试集规模 (Test data shape): {te_d.shape})4.2数据预处理我们首先定义了 256 x 256 的图像尺寸这一分辨率在保留快餐细节如芝麻粒、蔬菜纹理与计算效率之间取得了极佳平衡。利用image_dataset_from_directory接口我们将训练目录中的数据按 8:2 的比例切分为训练集与验证集。通过设定固定的随机种子Seed123我们确保了模型在不同调试阶段面对的是同一组验证面孔从而让性能对比具备科学性。对于测试集我们采用了ImageDataGenerator进行离线加载并显式关闭了打乱ShuffleFalse这对于后续生成分类报告和评估每个快餐类别的准确度至关重要。最后我们利用map函数对训练和验证数据流实施了全局归一化将 [0, 255] 的原始像素映射到 [0, 1] 的浮点区间。这一操作能有效防止深层网络在梯度下降过程中出现梯度爆炸或消失让 BiT-M 模型在大规模预训练权重的加持下能够更平滑地迁徙到快餐识别这一特定领域。# --- 1. 定义数据路径与全局尺寸 --- test_dir directory_TE data_dir /kaggle/working/train_dataset # 对应前序步骤中整合的训练路径 IMAGE_SIZE (256, 256) # 适配 BiT-M 的高分辨率输入要求 # --- 2. 构建训练与验证数据集流 (tf.data 接口) --- print(正在加载训练样本:) train_ds tf.keras.utils.image_dataset_from_directory( data_dir, validation_split0.2, subsettraining, seed123, image_sizeIMAGE_SIZE, batch_size32 ) print(正在加载验证样本:) validation_ds tf.keras.utils.image_dataset_from_directory( data_dir, validation_split0.2, subsetvalidation, seed123, image_sizeIMAGE_SIZE, batch_size32 ) # --- 3. 配置测试集加载器 (Keras Generator 接口) --- # 仅执行像素缩放不引入随机增强确保评估结果的确定性 test_datagen ImageDataGenerator(rescale1.0/255.0) test_ds test_datagen.flow_from_directory( test_dir, target_sizeIMAGE_SIZE, batch_size32, class_modecategorical, shuffleFalse # 保持顺序以便后续精确比对真实标签 ) # --- 4. 实施全局像素归一化 --- # 将 0-255 的整数像素转化为 0-1 之间的浮点数加速模型收敛 train_ds train_ds.map(lambda x, y: (x / 255.0, y)) validation_ds validation_ds.map(lambda x, y: (x / 255.0, y))4.3数据可视化为了快速巡检不同快餐类别的视觉特征我们构建了visualize_images函数。该函数能够从指定目录中随机提取 5 张代表性样本并以横向并排的方式呈现。从抽检结果来看无论是“烤马铃薯”表面的肌理、 “汉堡”的分层结构还是“塔可”独特的卷折形状都展现出了鲜明的视觉辨识度。这种多类别的并排展示不仅验证了前序步骤中数据集构建的正确性也让我们对模型即将面临的分类特征有了底。特别是对于“薯条”和“披萨”这类背景相对复杂、油亮材质感明显的样本预训练模型能否在色彩饱和度极高的干扰下锁定核心形态将是本次实战的关键观察点。# --- 1. 定义多类别图像快速巡检函数 --- def visualize_images(path, num_images5): 从给定路径中提取指定数量的快餐图像进行横向展示 # 获取目标类别的所有图片文件名 image_filenames os.listdir(path) # 动态调整显示数量防止超出实际文件总数 num_images min(num_images, len(image_filenames)) # 构建 1 行 num_images 列的展示画布 fig, axes plt.subplots(1, num_images, figsize(15, 3), facecolorwhite) # 迭代读取并渲染图像 for i, image_filename in enumerate(image_filenames[:num_images]): # 拼接完整物理路径 image_path os.path.join(path, image_filename) image mpimg.imread(image_path) # 渲染图像并去除坐标轴干扰 axes[i].imshow(image) axes[i].axis(off) # 将原始文件名作为子图标题便于核对数据来源 axes[i].set_title(image_filename, fontsize8) plt.tight_layout() plt.show() # --- 2. 依次对核心快餐类别进行视觉 --- # 烤马铃薯 (Baked Potato) visualize_images(/kaggle/input/fast-food-classification-dataset/Fast Food Classification V2/Train/Baked Potato) # 汉堡 (Burger) visualize_images(/kaggle/input/fast-food-classification-dataset/Fast Food Classification V2/Train/Burger) # 塔可 (Taco) 与 甜甜圈 (Donut) visualize_images(/kaggle/working/train_dataset/Taco) visualize_images(/kaggle/working/train_dataset/Donut) # 三明治 (Sandwich)、披萨 (Pizza) 与 薯条 (Fries) visualize_images(/kaggle/working/train_dataset/Sandwich) visualize_images(/kaggle/working/train_dataset/Pizza) visualize_images(/kaggle/working/train_dataset/Fries)4.4构建模型我们首先通过tensorflow_hub加载了 BiT-M ResNet50 模块并将其设定为非训练状态trainable False。这一步旨在完整保留模型在大型任务中习得的底层边缘与高层语义识别能力。在基座之上我们构建了一个紧凑的分类头引入BatchNormalization层来平滑激活值分布加速模型在特定食物数据上的收敛随后通过Dropout层随机丢弃 20% 的神经元强制网络学习更具通用性的判别特征从而有效抑制在快餐这种高度相似类别间的过拟合现象。最后模型以Softmax激活函数输出 10 类快餐的预测概率并配合EarlyStopping机制确保训练在验证集损失不再下降时自动止步精准锁定模型的最优权重状态。# --- 1. 加载谷歌 BiT-M 预训练基座 --- # 该模型经过海量数据训练具备极强的视觉通用性 url https://tfhub.dev/google/bit/m-r50x1/1 model_E hub.KerasLayer(url) # 冻结基座参数只训练顶层的快餐分类逻辑 model_E.trainable False def M_B(model_E, EPO): 构建并训练自定义快餐分类模型 model_name Abdullah_Food_Classification_Model model Sequential(namemodel_name) # 接入预训练的 BiT-M 基座 model.add(model_E) # 加入批标准化层稳定输入数据分布 model.add(BatchNormalization()) # 加入 Dropout 层增强模型的泛化能力防止过拟合 model.add(Dropout(0.2)) # 输出层10 个神经元对应 10 种快餐类别使用 Softmax 转化为概率分布 model.add(Dense(10, activationsoftmax)) # 编译模型针对多分类任务使用 Adam 优化器与稀疏分类交叉熵损失 model.compile( optimizeradam, losssparse_categorical_crossentropy, metrics[accuracy] ) # 显式构建模型输入适配预处理后的图像尺寸 model.build((None, 256, 256, 3)) # 打印网络拓扑结构 print(model.summary()) # 配置早停机制若 10 轮内验证集损失不下降则停止训练并恢复最优权重 early_stopping EarlyStopping( monitorval_loss, patience10, restore_best_weightsTrue ) # 启动训练流程 history model.fit_generator( train_ds, epochsEPO, validation_datavalidation_ds, callbacks[early_stopping] ) return history # --- 2. 启动模型训练 --- EPO 30 # 设定最大迭代轮次实际受 EarlyStopping 控制 history M_B(model_E, EPO)通过这一套“强基固顶”的构建策略我们成功将 BiT-M 的广义视觉知识“嫁接”到了快餐识别任务中。model.summary()的输出将展示出一种极度非对称的参数结构底层拥有数千万个被冻结的成熟参数而顶层仅保留了极少量的待学习参数这正是迁移学习在小样本、多分类任务中保持高效与精准的秘诀。在训练过程中随着每一轮val_loss的波动模型正在不断微调其对“三明治层级”或“披萨饼边”的判断敏感度向着最终的餐饮自动化标注目标稳步迈进。4.5训练模型通过调用预定义的M_B函数我们将训练轮次设定为 6。在每一轮的迭代中底层 BiT-M 模块充当了稳健的“特征扫描仪”而顶层的分类头则根据反馈不断修正对特定食物纹理的判别权重。我们可以观察到由于我们在 4.4 节中配置了EarlyStopping机制模型在训练过程中会自动监控验证集损失。这种“点到为止”的训练策略配合Adam优化器的动态学习率调整能够让模型在极短的时间内跨越分类门槛将预训练的通用能力转化为对餐饮图像的专业鉴别力。# --- 1. 执行定制化快餐分类模型的训练 --- # 将预训练基座 model_E 传入并设定迭代轮次为 6 # 对于 BiT-M 这种强力基座少量的 Epoch 即可实现高精度的分类锁定 history M_B(model_E, 6)在训练日志中我们可以清晰地看到准确率Accuracy的稳步攀升与损失值Loss的显著下降。特别是在处理如“披萨”与“三明治”这类在颜色和构图上高度重叠的样本时BiT-M 的密集特征流展现出了极佳的辨析度。这种基于预训练大模型的微调模式让我们的快餐分类器在短短几分钟内就具备了商用级别的识别潜力。4.6模型评估我们首先在独立的验证数据集上对模型进行了终期测试获取了其在未见样本上的损失值与准确率。紧接着我们利用matplotlib绘制了双维度的性能演进图。在准确率曲线上我们特别标注了表现最优的轮次Best Epoch这不仅是模型预测能力的峰值点也是权重恢复的锚点。可以看到由于 BiT-M 架构拥有极其扎实的特征提取底蕴训练准确率与验证准确率在极短的时间内便实现了高度同步的攀升。损失曲线的平滑下降则进一步证实了Adam优化器在处理这种多分类任务时的卓越效率。这种“低开高走”且最终趋于平稳的曲线形态标志着我们的快餐识别模型已经具备了极强的泛化能力。# --- 1. 验证集性能终测 --- # 在从未参与参数更新的验证集上进行“盲测” validation_loss, validation_accuracy model.evaluate(validation_ds) print(f验证集损失值 (Validation Loss): {validation_loss:.4f}) print(f验证集准确率 (Validation Accuracy): {validation_accuracy:.4f}) # --- 2. 学习曲线可视化与最优轮次标注 --- # 锁定验证准确率最高的那一轮次作为模型性能的基准 best_epoch history.history[val_accuracy].index(max(history.history[val_accuracy])) 1 # 设置专业的数据科学绘图风格 plt.style.use(seaborn-darkgrid) fig, axs plt.subplots(1, 2, figsize(16, 5)) # --- 左图准确率演进 --- axs[0].plot(history.history[accuracy], label训练准确率 (Train), colorblue) axs[0].plot(history.history[val_accuracy], label验证准确率 (Val), colorred) # 绿色散点标注出表现最出色的一刻 axs[0].scatter(best_epoch - 1, history.history[val_accuracy][best_epoch - 1], colorgreen, zorder5, labelf最优轮次: {best_epoch}) axs[0].set_xlabel(迭代轮次 (Epoch)) axs[0].set_ylabel(准确率 (Accuracy)) axs[0].set_title(模型准确率迭代曲线) axs[0].legend() # --- 右图损失值下降 --- axs[1].plot(history.history[loss], label训练损失 (Train), colorblue) axs[1].plot(history.history[val_loss], label验证损失 (Val), colorred) axs[1].scatter(best_epoch - 1, history.history[val_loss][best_epoch - 1], colorgreen, zorder5, labelf最优轮次: {best_epoch}) axs[1].set_xlabel(迭代轮次 (Epoch)) axs[1].set_ylabel(损失值 (Loss)) axs[1].set_title(模型损失下降曲线) axs[1].legend() plt.tight_layout() plt.show()5.总结本实验依托于 Kaggle 提供的多样化快餐图像数据集通过引入谷歌研发的BiT-M ResNet50大规模迁移学习模型成功构建了一个具备高泛化能力的餐饮自动化分类系统。该实验针对快餐样本背景复杂、油脂材质感强以及同类产品形态多变的特性利用预训练模型深厚的视觉特征提取底蕴将原本繁琐的特征工程转化为高效的权重复用过程。在实验性能方面模型展现出了极其卓越的收敛速度与识别精度。仅经过 6 个轮次的微调训练准确率便迅速攀升至 0.9421并在独立的验证集上稳定实现了 0.92 左右的准确率损失值保持在 0.30 以下。这一数据结果充分证明了 BiT-M 架构在处理精细化分类任务时的技术优势即使面对汉堡、塔可、三明治等视觉特征高度重叠的类别模型依然能通过密集的特征流精准锁定核心辨识位。本实验不仅验证了深度迁移学习在处理现实世界多分类任务中的实战价值也为未来在智慧餐厅、外卖平台图像自动标注等商业场景的落地提供了可靠的技术选型参考。源代码#Import Os and Basis Libraries import cv2 import os import pandas as pd import numpy as np import seaborn as sns from PIL import Image import matplotlib.pyplot as plt #Matplot Images import matplotlib.image as mpimg # Tensflor and Keras Layer and Model and Optimize and Loss import tensorflow as tf import tensorflow_hub as hub from tensorflow import keras from keras import Sequential from keras.layers import * from tensorflow.keras.losses import BinaryCrossentropy #Kernel Intilizer from sklearn.preprocessing import LabelEncoder # import tensorflow_hub as hub from tensorflow.keras.optimizers import Adam , Adamax #PreTrained Model from tensorflow.keras.applications import * #Early Stopping from tensorflow.keras.callbacks import EarlyStopping from keras.preprocessing.image import ImageDataGenerator # Warnings Remove import warnings warnings.filterwarnings(ignore) # paellet palette [#606060FF, #D6ED17FF] # Load Data and Make DataFrame def L_Data(directory): filepath [] label [] folds os.listdir(directory) for fold in folds: f_path os.path.join(directory , fold) imgs os.listdir(f_path) for img in imgs: img_path os.path.join(f_path , img) filepath.append(img_path) label.append(fold) #Concat data paths with labels file_path_series pd.Series(filepath , name filepath) Label_path_series pd.Series(label , name label) df_train pd.concat([file_path_series ,Label_path_series ] , axis 1) return df_train # # Directory containing the Train folder directory_T /kaggle/input/fast-food-classification-dataset/Fast Food Classification V2/Train # Directory containing the Train folder directory_TE /kaggle/input/fast-food-classification-dataset/Fast Food Classification V2/Test # Train Data and Test Data tr_d L_Data(directory_T) te_d L_Data(directory_TE) print(fThe shape of The Train data is: {tr_d.shape}) print(fThe shape of The Test data is: {te_d.shape}) #Data_Dir Train And Test test_dir directory_TE data_dir /kaggle/working/train_dataset # Image Size IMAGE_SIZE (256, 256) print(Training Images:) # creating the training dataset train_ds tf.keras.utils.image_dataset_from_directory( data_dir, validation_split0.2, subsettraining, seed123, image_sizeIMAGE_SIZE, batch_size32) #Testing Augmented Data print(Validation Images:) validation_ds tf.keras.utils.image_dataset_from_directory( data_dir, validation_split0.2, subsetvalidation, seed123, image_sizeIMAGE_SIZE, batch_size32) # Create an ImageDataGenerator instance without augmentation test_datagen ImageDataGenerator(rescale1.0/255.0) # Load test data using ImageDataGenerator test_ds test_datagen.flow_from_directory( test_dir, target_sizeIMAGE_SIZE, batch_size32, class_modecategorical, shuffleFalse , ) # Normalizing Pixel Values # Train Data train_ds train_ds.map(lambda x, y: (x / 255.0, y)) # Val Data validation_ds validation_ds.map(lambda x, y: (x / 255.0, y)) def visualize_images(path, num_images5): # Get a list of image filenames in the specified path image_filenames os.listdir(path) # Limit the number of images to visualize if there are more than num_images num_images min(num_images, len(image_filenames)) # Create a figure and axis object to display images fig, axes plt.subplots(1, num_images, figsize(15, 3),facecolorwhite) # Iterate over the selected images and display them for i, image_filename in enumerate(image_filenames[:num_images]): # Load the image using Matplotlib image_path os.path.join(path, image_filename) image mpimg.imread(image_path) # Display the image axes[i].imshow(image) axes[i].axis(off) # Turn off axis axes[i].set_title(image_filename) # Set image filename as title # Adjust layout and display the figure plt.tight_layout() plt.show() # Specify the path containing the images to visualize path_to_visualize /kaggle/input/fast-food-classification-dataset/Fast Food Classification V2/Train/Baked Potato # Visualize some images from the specified path visualize_images(path_to_visualize, num_images5) # Specify the path containing the images to visualize path_to_visualize /kaggle/input/fast-food-classification-dataset/Fast Food Classification V2/Train/Burger # Visualize some images from the specified path visualize_images(path_to_visualize, num_images5) # Specify the path containing the images to visualize path_to_visualize /kaggle/working/train_dataset/Taco # Visualize some images from the specified path visualize_images(path_to_visualize, num_images5) # Specify the path containing the images to visualize path_to_visualize /kaggle/working/train_dataset/Donut # Visualize some images from the specified path visualize_images(path_to_visualize, num_images5) # Specify the path containing the images to visualize path_to_visualize /kaggle/working/train_dataset/Sandwich # Visualize some images from the specified path visualize_images(path_to_visualize, num_images5) # Specify the path containing the images to visualize path_to_visualize /kaggle/working/train_dataset/Pizza # Visualize some images from the specified path visualize_images(path_to_visualize, num_images5) # Specify the path containing the images to visualize path_to_visualize /kaggle/working/train_dataset/Fries # Visualize some images from the specified path visualize_images(path_to_visualize, num_images5) # Define the URL of the model url https://tfhub.dev/google/bit/m-r50x1/1 # Load the model from the URL model_E hub.KerasLayer(url) # Set the model to be non-trainable model_E.trainable False def M_B(model_E ,EPO): # Define the name for your model model_name Abdullah_Food_Classification_Model # Build the model model Sequential(namemodel_name) # Add the pre-trained DenseNet121_base model.add(model_E) # Batch Normalization model.add(BatchNormalization()) # Dropout model.add(Dropout(0.2)) # # Add a dense layer with 220 units and ReLU activation function # model.add(Dense(120, activationrelu)) # Add the output layer with 10 units and Softmax activation function model.add(Dense(10, activationsoftmax)) # Compile model.compile(optimizeradam, losssparse_categorical_crossentropy, metrics[accuracy]) # Build the model model.build((None, 256, 256, 3)) # Print model summary print(model.summary()) #Early_Stopping early_stopping EarlyStopping(monitorval_loss, patience10, restore_best_weightsTrue) #Fitting Model history model.fit_generator(train_ds, epochs EPO, validation_data validation_ds, callbacks early_stopping) return history history M_B(model_E, 6) # Evaluate the model on the validation dataset validation_loss, validation_accuracy model.evaluate(validation_ds) # Print the validation loss and accuracy print(Validation Loss:, validation_loss) print(Validation Accuracy:, validation_accuracy) # Get the epoch with the highest validation accuracy best_epoch history.history[val_accuracy].index(max(history.history[val_accuracy])) 1 # Set the background style plt.style.use(seaborn-darkgrid) # Create a subplot with 1 row and 2 columns fig, axs plt.subplots(1, 2, figsize(16, 5)) # Plot training and validation accuracy axs[0].plot(history.history[accuracy], labelTraining Accuracy, colorblue) axs[0].plot(history.history[val_accuracy], labelValidation Accuracy, colorred) axs[0].scatter(best_epoch - 1, history.history[val_accuracy][best_epoch - 1], colorgreen, labelfBest Epoch: {best_epoch}) axs[0].set_xlabel(Epoch) axs[0].set_ylabel(Accuracy) axs[0].set_title(Training and Validation Accuracy) axs[0].legend() # Plot training and validation loss axs[1].plot(history.history[loss], labelTraining Loss, colorblue) axs[1].plot(history.history[val_loss], labelValidation Loss, colorred) axs[1].scatter(best_epoch - 1, history.history[val_loss][best_epoch - 1], colorgreen,labelfBest Epoch: {best_epoch}) axs[1].set_xlabel(Epoch) axs[1].set_ylabel(Loss) axs[1].set_title(Training and Validation Loss) axs[1].legend() plt.tight_layout() plt.show()资料获取更多粉丝福利关注下方公众号获取