🔱 大家好,我是 👉 K同學啊,《深度學習100例》系列將持續更新 歡迎 點贊👍、收藏?、關注👀
本文將采用 Inception V3 模型實作手語識別,重點是了解 Inception V3 模型的結構及其搭建方法,
一、前期作業
我的環境:
- 語言環境:Python3.6.5
- 編譯器:jupyter notebook
- 深度學習環境:TensorFlow2.4.1
🚀 本文選自專欄:《深度學習100例》
🚀 深度學習新人必看:《小白入門深度學習》
- 小白入門深度學習 | 第一篇:配置深度學習環境
- 小白入門深度學習 | 第二篇:編譯器的使用-Jupyter Notebook
- 小白入門深度學習 | 第三篇:深度學習初體驗
- 小白入門深度學習 | 第四篇:配置PyTorch環境
🚀 往期精彩-卷積神經網路篇:
- 深度學習100例-卷積神經網路(CNN)實作mnist手寫數字識別 | 第1天
- 深度學習100例-卷積神經網路(CNN)彩色圖片分類 | 第2天
- 深度學習100例-卷積神經網路(CNN)服裝影像分類 | 第3天
- 深度學習100例-卷積神經網路(CNN)花朵識別 | 第4天
- 深度學習100例-卷積神經網路(CNN)天氣識別 | 第5天
- 深度學習100例-卷積神經網路(VGG-16)識別海賊王草帽一伙 | 第6天
- 深度學習100例-卷積神經網路(VGG-19)識別靈籠中的人物 | 第7天
- 深度學習100例-卷積神經網路(ResNet-50)鳥類識別 | 第8天
- 深度學習100例-卷積神經網路(AlexNet)手把手教學 | 第11天
- 深度學習100例-卷積神經網路(CNN)識別驗證碼 | 第12天
- 深度學習100例-卷積神經網路(Inception V3)識別手語 | 第13天
- 深度學習100例-卷積神經網路(Inception-ResNet-v2)識別交通標志 | 第14天
- 深度學習100例-卷積神經網路(CNN)實作車牌識別 | 第15天
- 深度學習100例-卷積神經網路(CNN)識別神奇寶貝小智一伙 | 第16天
- 深度學習100例-卷積神經網路(CNN)注意力檢測 | 第17天
- 深度學習100例-卷積神經網路(VGG-16)貓狗識別 | 第21天
- 深度學習100例-卷積神經網路(LeNet-5)深度學習里的“Hello Word” | 第22天
- 深度學習100例-卷積神經網路(CNN)3D醫療影像識別 | 第23天
🚀 往期精彩-回圈神經網路篇:
- 深度學習100例-回圈神經網路(RNN)實作股票預測 | 第9天
- 深度學習100例-回圈神經網路(LSTM)實作股票預測 | 第10天
🚀 往期精彩-生成對抗網路篇:
- 深度學習100例-生成對抗網路(GAN)手寫數字生成 | 第18天
- 深度學習100例-生成對抗網路(DCGAN)手寫數字生成 | 第19天
- 深度學習100例-生成對抗網路(DCGAN)生成動漫小姐姐 | 第20天
1. 設定GPU
如果使用的是CPU可以注釋掉這部分的代碼,
import tensorflow as tf
gpus = tf.config.list_physical_devices("GPU")
if gpus:
tf.config.experimental.set_memory_growth(gpus[0], True) #設定GPU顯存用量按需使用
tf.config.set_visible_devices([gpus[0]],"GPU")
2. 匯入資料
import matplotlib.pyplot as plt
# 支持中文
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用來正常顯示中文標簽
plt.rcParams['axes.unicode_minus'] = False # 用來正常顯示負號
import os,PIL,pathlib
# 設定隨機種子盡可能使結果可以重現
import numpy as np
np.random.seed(1)
# 設定隨機種子盡可能使結果可以重現
import tensorflow as tf
tf.random.set_seed(1)
from tensorflow import keras
from tensorflow.keras import layers,models
data_dir = "D:/jupyter notebook/DL-100-days/datasets/gestures"
data_dir = pathlib.Path(data_dir)
3. 查看資料
image_count = len(list(data_dir.glob('*/*')))
print("圖片總數為:",image_count)
圖片總數為: 12547
二、資料預處理
本文主要是識別24個英文字母的手語姿勢(另外兩個字母的手語是動作),其中每一個手語姿勢圖片均有500+張,
1. 加載資料
使用image_dataset_from_directory方法將磁盤中的資料加載到tf.data.Dataset中
batch_size = 8
img_height = 224
img_width = 224
TensorFlow版本是2.2.0的同學可能會遇到module 'tensorflow.keras.preprocessing' has no attribute 'image_dataset_from_directory'的報錯,升級一下TensorFlow就OK了,
"""
關于image_dataset_from_directory()的詳細介紹可以參考文章:https://mtyjkh.blog.csdn.net/article/details/117018789
"""
train_ds = tf.keras.preprocessing.image_dataset_from_directory(
data_dir,
validation_split=0.2,
subset="training",
seed=123,
image_size=(img_height, img_width),
batch_size=batch_size)
Found 12547 files belonging to 24 classes.
Using 10038 files for training.
"""
關于image_dataset_from_directory()的詳細介紹可以參考文章:https://mtyjkh.blog.csdn.net/article/details/117018789
"""
val_ds = tf.keras.preprocessing.image_dataset_from_directory(
data_dir,
validation_split=0.2,
subset="validation",
seed=123,
image_size=(img_height, img_width),
batch_size=batch_size)
Found 12547 files belonging to 24 classes.
Using 2509 files for validation.
我們可以通過class_names輸出資料集的標簽,標簽將按字母順序對應于目錄名稱,
class_names = train_ds.class_names
print(class_names)
['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y']
2. 可視化資料
plt.figure(figsize=(10, 5)) # 圖形的寬為10高為5
for images, labels in train_ds.take(1):
for i in range(8):
ax = plt.subplot(2, 4, i + 1)
plt.imshow(images[i].numpy().astype("uint8"))
plt.title(class_names[labels[i]])
plt.axis("off")

plt.imshow(images[1].numpy().astype("uint8"))

3. 再次檢查資料
for image_batch, labels_batch in train_ds:
print(image_batch.shape)
print(labels_batch.shape)
break
(8, 224, 224, 3)
(8,)
Image_batch是形狀的張量(8, 224, 224, 3),這是一批形狀240x240x3的8張圖片(最后一維指的是彩色通道RGB),Label_batch是形狀(8,)的張量,這些標簽對應8張圖片
4. 配置資料集
- shuffle() : 打亂資料,關于此函式的詳細介紹可以參考:https://zhuanlan.zhihu.com/p/42417456
- prefetch() :預取資料,加速運行,其詳細介紹可以參考我前兩篇文章,里面都有講解,
- cache() :將資料集快取到記憶體當中,加速運行
AUTOTUNE = tf.data.AUTOTUNE
train_ds = train_ds.cache().shuffle(1000).prefetch(buffer_size=AUTOTUNE)
val_ds = val_ds.cache().prefetch(buffer_size=AUTOTUNE)
如果報錯AttributeError: module 'tensorflow._api.v2.data' has no attribute 'AUTOTUNE',可以將AUTOTUNE=tf.data.AUTOTUNE更換為AUTOTUNE = tf.data.experimental.AUTOTUNE
三、Inception V3介紹
關于Inception系列的介紹可以見:https://baike.baidu.com/item/Inception%E7%BB%93%E6%9E%84 ,個人認為這些在現階段只需要將模型走一遍(學會搭建),后期如果需要的話,可以再回頭來進行詳細研究,
這個模型相比之前寫過的一些模型可能較為復雜一些,先放一張圖整體感受一下它

結構圖再來一張,這張更為詳細,可點擊查看大圖

關于上面卷積的計算還比較蒙的同學可以參考我這篇文章哈:卷積的計算
四、構建Inception V3網路模型
1.自己搭建
下面是本文的重點 Inception V3 網路模型的構建,可以試著按照上面的圖自己構建一下 Inception V3,這部分我主要是參考官網的構建程序,將其單獨拎了出來,
#=============================================================
# Inception V3 網路
#=============================================================
from tensorflow.keras.models import Model
from tensorflow.keras import layers
from tensorflow.keras.layers import Activation,Dense,Input,BatchNormalization,Conv2D,AveragePooling2D
from tensorflow.keras.layers import GlobalAveragePooling2D,MaxPooling2D
def conv2d_bn(x,filters,num_row,num_col,padding='same',strides=(1, 1),name=None):
if name is not None:
bn_name = name + '_bn'
conv_name = name + '_conv'
else:
bn_name = None
conv_name = None
x = Conv2D(filters,(num_row, num_col),strides=strides,padding=padding,use_bias=False,name=conv_name)(x)
x = BatchNormalization(scale=False, name=bn_name)(x)
x = Activation('relu', name=name)(x)
return x
def InceptionV3(input_shape=[224,224,3],classes=1000):
img_input = Input(shape=input_shape)
x = conv2d_bn(img_input, 32, 3, 3, strides=(2, 2), padding='valid')
x = conv2d_bn(x, 32, 3, 3, padding='valid')
x = conv2d_bn(x, 64, 3, 3)
x = MaxPooling2D((3, 3), strides=(2, 2))(x)
x = conv2d_bn(x, 80, 1, 1, padding='valid')
x = conv2d_bn(x, 192, 3, 3, padding='valid')
x = MaxPooling2D((3, 3), strides=(2, 2))(x)
#================================#
# Block1 35x35
#================================#
# Block1 part1
# 35 x 35 x 192 -> 35 x 35 x 256
branch1x1 = conv2d_bn(x, 64, 1, 1)
branch5x5 = conv2d_bn(x, 48, 1, 1)
branch5x5 = conv2d_bn(branch5x5, 64, 5, 5)
branch3x3dbl = conv2d_bn(x, 64, 1, 1)
branch3x3dbl = conv2d_bn(branch3x3dbl, 96, 3, 3)
branch3x3dbl = conv2d_bn(branch3x3dbl, 96, 3, 3)
branch_pool = AveragePooling2D((3, 3), strides=(1, 1), padding='same')(x)
branch_pool = conv2d_bn(branch_pool, 32, 1, 1)
x = layers.concatenate([branch1x1, branch5x5, branch3x3dbl, branch_pool],axis=3,name='mixed0')
# Block1 part2
# 35 x 35 x 256 -> 35 x 35 x 288
branch1x1 = conv2d_bn(x, 64, 1, 1)
branch5x5 = conv2d_bn(x, 48, 1, 1)
branch5x5 = conv2d_bn(branch5x5, 64, 5, 5)
branch3x3dbl = conv2d_bn(x, 64, 1, 1)
branch3x3dbl = conv2d_bn(branch3x3dbl, 96, 3, 3)
branch3x3dbl = conv2d_bn(branch3x3dbl, 96, 3, 3)
branch_pool = AveragePooling2D((3, 3), strides=(1, 1), padding='same')(x)
branch_pool = conv2d_bn(branch_pool, 64, 1, 1)
x = layers.concatenate([branch1x1, branch5x5, branch3x3dbl, branch_pool],axis=3,name='mixed1')
# Block1 part3
# 35 x 35 x 288 -> 35 x 35 x 288
branch1x1 = conv2d_bn(x, 64, 1, 1)
branch5x5 = conv2d_bn(x, 48, 1, 1)
branch5x5 = conv2d_bn(branch5x5, 64, 5, 5)
branch3x3dbl = conv2d_bn(x, 64, 1, 1)
branch3x3dbl = conv2d_bn(branch3x3dbl, 96, 3, 3)
branch3x3dbl = conv2d_bn(branch3x3dbl, 96, 3, 3)
branch_pool = AveragePooling2D((3, 3), strides=(1, 1), padding='same')(x)
branch_pool = conv2d_bn(branch_pool, 64, 1, 1)
x = layers.concatenate([branch1x1, branch5x5, branch3x3dbl, branch_pool],axis=3,name='mixed2')
#================================#
# Block2 17x17
#================================#
# Block2 part1
# 35 x 35 x 288 -> 17 x 17 x 768
branch3x3 = conv2d_bn(x, 384, 3, 3, strides=(2, 2), padding='valid')
branch3x3dbl = conv2d_bn(x, 64, 1, 1)
branch3x3dbl = conv2d_bn(branch3x3dbl, 96, 3, 3)
branch3x3dbl = conv2d_bn(branch3x3dbl, 96, 3, 3, strides=(2, 2), padding='valid')
branch_pool = MaxPooling2D((3, 3), strides=(2, 2))(x)
x = layers.concatenate([branch3x3, branch3x3dbl, branch_pool], axis=3, name='mixed3')
# Block2 part2
# 17 x 17 x 768 -> 17 x 17 x 768
branch1x1 = conv2d_bn(x, 192, 1, 1)
branch7x7 = conv2d_bn(x, 128, 1, 1)
branch7x7 = conv2d_bn(branch7x7, 128, 1, 7)
branch7x7 = conv2d_bn(branch7x7, 192, 7, 1)
branch7x7dbl = conv2d_bn(x, 128, 1, 1)
branch7x7dbl = conv2d_bn(branch7x7dbl, 128, 7, 1)
branch7x7dbl = conv2d_bn(branch7x7dbl, 128, 1, 7)
branch7x7dbl = conv2d_bn(branch7x7dbl, 128, 7, 1)
branch7x7dbl = conv2d_bn(branch7x7dbl, 192, 1, 7)
branch_pool = AveragePooling2D((3, 3), strides=(1, 1), padding='same')(x)
branch_pool = conv2d_bn(branch_pool, 192, 1, 1)
x = layers.concatenate([branch1x1, branch7x7, branch7x7dbl, branch_pool],axis=3,name='mixed4')
# Block2 part3 and part4
# 17 x 17 x 768 -> 17 x 17 x 768 -> 17 x 17 x 768
for i in range(2):
branch1x1 = conv2d_bn(x, 192, 1, 1)
branch7x7 = conv2d_bn(x, 160, 1, 1)
branch7x7 = conv2d_bn(branch7x7, 160, 1, 7)
branch7x7 = conv2d_bn(branch7x7, 192, 7, 1)
branch7x7dbl = conv2d_bn(x, 160, 1, 1)
branch7x7dbl = conv2d_bn(branch7x7dbl, 160, 7, 1)
branch7x7dbl = conv2d_bn(branch7x7dbl, 160, 1, 7)
branch7x7dbl = conv2d_bn(branch7x7dbl, 160, 7, 1)
branch7x7dbl = conv2d_bn(branch7x7dbl, 192, 1, 7)
branch_pool = AveragePooling2D(
(3, 3), strides=(1, 1), padding='same')(x)
branch_pool = conv2d_bn(branch_pool, 192, 1, 1)
x = layers.concatenate([branch1x1, branch7x7, branch7x7dbl, branch_pool],axis=3,name='mixed' + str(5 + i))
# Block2 part5
# 17 x 17 x 768 -> 17 x 17 x 768
branch1x1 = conv2d_bn(x, 192, 1, 1)
branch7x7 = conv2d_bn(x, 192, 1, 1)
branch7x7 = conv2d_bn(branch7x7, 192, 1, 7)
branch7x7 = conv2d_bn(branch7x7, 192, 7, 1)
branch7x7dbl = conv2d_bn(x, 192, 1, 1)
branch7x7dbl = conv2d_bn(branch7x7dbl, 192, 7, 1)
branch7x7dbl = conv2d_bn(branch7x7dbl, 192, 1, 7)
branch7x7dbl = conv2d_bn(branch7x7dbl, 192, 7, 1)
branch7x7dbl = conv2d_bn(branch7x7dbl, 192, 1, 7)
branch_pool = AveragePooling2D((3, 3), strides=(1, 1), padding='same')(x)
branch_pool = conv2d_bn(branch_pool, 192, 1, 1)
x = layers.concatenate([branch1x1, branch7x7, branch7x7dbl, branch_pool],axis=3,name='mixed7')
#================================#
# Block3 8x8
#================================#
# Block3 part1
# 17 x 17 x 768 -> 8 x 8 x 1280
branch3x3 = conv2d_bn(x, 192, 1, 1)
branch3x3 = conv2d_bn(branch3x3, 320, 3, 3,strides=(2, 2), padding='valid')
branch7x7x3 = conv2d_bn(x, 192, 1, 1)
branch7x7x3 = conv2d_bn(branch7x7x3, 192, 1, 7)
branch7x7x3 = conv2d_bn(branch7x7x3, 192, 7, 1)
branch7x7x3 = conv2d_bn(branch7x7x3, 192, 3, 3, strides=(2, 2), padding='valid')
branch_pool = MaxPooling2D((3, 3), strides=(2, 2))(x)
x = layers.concatenate([branch3x3, branch7x7x3, branch_pool], axis=3, name='mixed8')
# Block3 part2 part3
# 8 x 8 x 1280 -> 8 x 8 x 2048 -> 8 x 8 x 2048
for i in range(2):
branch1x1 = conv2d_bn(x, 320, 1, 1)
branch3x3 = conv2d_bn(x, 384, 1, 1)
branch3x3_1 = conv2d_bn(branch3x3, 384, 1, 3)
branch3x3_2 = conv2d_bn(branch3x3, 384, 3, 1)
branch3x3 = layers.concatenate(
[branch3x3_1, branch3x3_2], axis=3, name='mixed9_' + str(i))
branch3x3dbl = conv2d_bn(x, 448, 1, 1)
branch3x3dbl = conv2d_bn(branch3x3dbl, 384, 3, 3)
branch3x3dbl_1 = conv2d_bn(branch3x3dbl, 384, 1, 3)
branch3x3dbl_2 = conv2d_bn(branch3x3dbl, 384, 3, 1)
branch3x3dbl = layers.concatenate([branch3x3dbl_1, branch3x3dbl_2], axis=3)
branch_pool = AveragePooling2D((3, 3), strides=(1, 1), padding='same')(x)
branch_pool = conv2d_bn(branch_pool, 192, 1, 1)
x = layers.concatenate([branch1x1, branch3x3, branch3x3dbl, branch_pool],axis=3,name='mixed' + str(9 + i))
# 平均池化后全連接,
x = GlobalAveragePooling2D(name='avg_pool')(x)
x = Dense(classes, activation='softmax', name='predictions')(x)
inputs = img_input
model = Model(inputs, x, name='inception_v3')
return model
model = InceptionV3()
model.summary()
Model: "inception_v3"
__________________________________________________________________________________________________
Layer (type) Output Shape Param # Connected to
==================================================================================================
input_1 (InputLayer) [(None, 224, 224, 3) 0
__________________________________________________________________________________________________
conv2d (Conv2D) (None, 111, 111, 32) 864 input_1[0][0]
__________________________________________________________________________________________________
batch_normalization (BatchNorma (None, 111, 111, 32) 96 conv2d[0][0]
__________________________________________________________________________________________________
activation (Activation) (None, 111, 111, 32) 0 batch_normalization[0][0]
__________________________________________________________________________________________________
conv2d_1 (Conv2D) (None, 109, 109, 32) 9216 activation[0][0]
......
__________________________________________________________________________________________________
avg_pool (GlobalAveragePooling2 (None, 2048) 0 mixed10[0][0]
__________________________________________________________________________________________________
predictions (Dense) (None, 1000) 2049000 avg_pool[0][0]
==================================================================================================
Total params: 23,851,784
Trainable params: 23,817,352
Non-trainable params: 34,432
__________________________________________________________________________________________________
2.官方模型
# import tensorflow as tf
# model_2 = tf.keras.applications.InceptionV3()
# model_2.summary()
五、編譯
在準備對模型進行訓練之前,還需要再對其進行一些設定,以下內容是在模型的編譯步驟中添加的:
- 損失函式(loss):用于衡量模型在訓練期間的準確率,
- 優化器(optimizer):決定模型如何根據其看到的資料和自身的損失函式進行更新,
- 指標(metrics):用于監控訓練和測驗步驟,以下示例使用了準確率,即被正確分類的影像的比率,
# 設定優化器,我這里改變了學習率,
opt = tf.keras.optimizers.Adam(learning_rate=1e-5)
model.compile(optimizer=opt,
loss='sparse_categorical_crossentropy',
metrics=['accuracy'])
六、訓練模型
epochs = 10
history = model.fit(
train_ds,
validation_data=val_ds,
epochs=epochs
)
Epoch 1/10
1255/1255 [==============================] - 130s 75ms/step - loss: 3.5247 - accuracy: 0.3366 - val_loss: 0.4606 - val_accuracy: 0.8776
Epoch 2/10
1255/1255 [==============================] - 68s 54ms/step - loss: 0.5796 - accuracy: 0.8711 - val_loss: 0.1501 - val_accuracy: 0.9530
Epoch 3/10
1255/1255 [==============================] - 68s 54ms/step - loss: 0.2236 - accuracy: 0.9589 - val_loss: 0.0639 - val_accuracy: 0.9825
Epoch 4/10
1255/1255 [==============================] - 69s 55ms/step - loss: 0.0803 - accuracy: 0.9917 - val_loss: 0.0403 - val_accuracy: 0.9884
Epoch 5/10
1255/1255 [==============================] - 71s 56ms/step - loss: 0.0333 - accuracy: 0.9989 - val_loss: 0.0239 - val_accuracy: 0.9928
Epoch 6/10
1255/1255 [==============================] - 70s 56ms/step - loss: 0.0165 - accuracy: 0.9992 - val_loss: 0.0168 - val_accuracy: 0.9944
Epoch 7/10
1255/1255 [==============================] - 70s 56ms/step - loss: 0.0076 - accuracy: 1.0000 - val_loss: 0.0160 - val_accuracy: 0.9944
Epoch 8/10
1255/1255 [==============================] - 70s 56ms/step - loss: 0.0041 - accuracy: 0.9999 - val_loss: 0.1108 - val_accuracy: 0.9737
Epoch 9/10
1255/1255 [==============================] - 70s 56ms/step - loss: 0.0358 - accuracy: 0.9919 - val_loss: 0.0312 - val_accuracy: 0.9888
Epoch 10/10
1255/1255 [==============================] - 69s 55ms/step - loss: 0.0111 - accuracy: 0.9985 - val_loss: 0.0068 - val_accuracy: 0.9980
七、模型評估
acc = history.history['accuracy']
val_acc = history.history['val_accuracy']
loss = history.history['loss']
val_loss = history.history['val_loss']
epochs_range = range(epochs)
plt.figure(figsize=(12, 4))
plt.subplot(1, 2, 1)
plt.plot(epochs_range, acc, label='Training Accuracy')
plt.plot(epochs_range, val_acc, label='Validation Accuracy')
plt.legend(loc='lower right')
plt.title('Training and Validation Accuracy')
plt.subplot(1, 2, 2)
plt.plot(epochs_range, loss, label='Training Loss')
plt.plot(epochs_range, val_loss, label='Validation Loss')
plt.legend(loc='upper right')
plt.title('Training and Validation Loss')
plt.show()

八、保存and加載模型
這是最簡單的模型保存與加載方法哈
# 保存模型
model.save('model/13_model.h5')
# 加載模型
new_model = keras.models.load_model('model/13_model.h5')
九、預測
# 采用加載的模型(new_model)來看預測結果
plt.figure(figsize=(10, 5)) # 圖形的寬為10高為5
for images, labels in val_ds.take(1):
for i in range(8):
ax = plt.subplot(2, 4, i + 1)
# 顯示圖片
plt.imshow(images[i].numpy().astype("uint8"))
# 需要給圖片增加一個維度
img_array = tf.expand_dims(images[i], 0)
# 使用模型預測圖片中的人物
predictions = new_model.predict(img_array)
plt.title(class_names[np.argmax(predictions)])
plt.axis("off")

推薦閱讀
? 這也太強了吧,我用它識別交通標志,準確率竟然高達97.9%
? 卷積神經網路(AlexNet)手把手教學-深度學習100例 | 第11天
? 回圈神經網路(LSTM)實作股票預測-深度學習100例 | 第10天
? 深度學習100例-卷積神經網路(CNN)實作mnist手寫數字識別 | 第1天
🚀 來自專欄:《深度學習100例》
最后再送大家一本,幫助大家拿到 BAT 等一線大廠 offer 的資料結構刷題筆記,是谷歌和阿里的大佬寫的,對于演算法薄榷訓者需要提高的同學都十分受用(提取碼:9go2 ):
谷歌和阿里大佬的Leetcode刷題筆記
以及我整理的7K+本開源電子書,總有一本可以幫到你 💖(提取碼:4eg0)
7K+本開源電子書
未完~
持續更新 歡迎 點贊👍、收藏?、關注👀
- 點贊👍:點贊給我持續更新的動力
- 收藏??:收藏后你能夠隨時找到文章
- 關注👀:關注我第一時間接收最新文章
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/296807.html
標籤:AI
