关于人工智能:使用CNN和Python实施的肺炎检测

39次阅读

共计 15009 个字符,预计需要花费 38 分钟才能阅读完成。

作者 |Muhammad Ardi
编译 |Flin
起源 |analyticsvidhya

介绍

嘿!几个小时前我刚刚实现一个深度学习我的项目,当初我想分享一下我所做的事件。这一挑战的指标是确定一个人是否患有肺炎。如果是,则确定是否由细菌或病毒引起。好吧,我感觉这个我的项目应该叫做分类而不是检测。

换句话说,此工作将是一个多分类问题,其中标签名称为:normal(失常),virus(病毒)和 bacteria(细菌)。为了解决这个问题,我将应用 CNN(卷积神经网络),它具备杰出的图像分类能力,。不仅如此,在这里我还实现了图像增强技术,以进步模型性能。顺便说一句,我取得了 80%的测试数据准确性,这对我来说是十分令人印象粗浅的。

能够从该 Kaggle 链接下载此我的项目中应用的数据集。

  • https://www.kaggle.com/paulti…

整个数据集自身的大小约为 1 GB,因而下载可能须要一段时间。或者,咱们也能够间接创立一个 Kaggle Notebook 并在那里编码整个我的项目,因而咱们甚至不须要下载任何内容。接下来,如果浏览数据集文件夹,你将看到有 3 个子文件夹,即 train,test 和 val。

好吧,我认为这些文件夹名称是不言自明的。此外,train 文件夹中的数据别离包含失常,病毒和细菌类别的 1341、1345 和 2530 个样本。我想这就是我介绍的全部内容了,当初让咱们进入代码的编写!

留神:我在本文结尾处搁置了该我的项目中应用的全副代码。

加载模块和训练图像

应用计算机视觉我的项目时,要做的第一件事是加载所有必须的模块和图像数据自身。我应用 tqdm 模块显示进度条,稍后你将看到它有用的起因。

我最初导入的是来自 Keras 模块的 ImageDataGenerator。该模块将帮忙咱们在训练过程中施行图像增强技术。

import os
import cv2
import pickle
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from tqdm import tqdm
from sklearn.preprocessing import OneHotEncoder
from sklearn.metrics import confusion_matrix
from keras.models import Model, load_model
from keras.layers import Dense, Input, Conv2D, MaxPool2D, Flatten
from keras.preprocessing.image import ImageDataGeneratornp.random.seed(22)

接下来,我定义两个函数以从每个文件夹加载图像数据。乍一看,上面的两个性能可能看起来齐全一样,然而在应用粗体显示的行上实际上存在一些差别。这样做是因为 NORMAL 和 PNEUMONIA 文件夹中的文件名构造略有不同。只管有所不同,但两个性能执行的其余过程基本相同。

首先,将所有图像调整为 200 x 200 像素。

这一点很重要,因为所有文件夹中的图像都有不同的尺寸,而神经网络只能承受具备固定数组大小的数据。

接下来,基本上所有图像都存储有 3 个色彩通道,这对 X 射线图像来说是多余的。因而,我的想法是将这些彩色图像都转换为灰度图像。

# Do not forget to include the last slash
def load_normal(norm_path):
    norm_files = np.array(os.listdir(norm_path))
    norm_labels = np.array(['normal']*len(norm_files))
    
    norm_images = []
    for image in tqdm(norm_files):
        image = cv2.imread(norm_path + image)
        image = cv2.resize(image, dsize=(200,200))
        image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
        norm_images.append(image)
        
    norm_images = np.array(norm_images)
    
    return norm_images, norm_labels
def load_pneumonia(pneu_path):
    pneu_files = np.array(os.listdir(pneu_path))
    pneu_labels = np.array([pneu_file.split('_')[1] for pneu_file in pneu_files])
    
    pneu_images = []
    for image in tqdm(pneu_files):
        image = cv2.imread(pneu_path + image)
        image = cv2.resize(image, dsize=(200,200))
        image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
        pneu_images.append(image)
        
    pneu_images = np.array(pneu_images)
    
    return pneu_images, pneu_labels

申明了以上两个函数后,当初咱们能够应用它来加载训练数据了。如果你运行上面的代码,你还将看到为什么我抉择在该我的项目中实现 tqdm 模块。

norm_images, norm_labels = load_normal('/kaggle/input/chest-xray-pneumonia/chest_xray/train/NORMAL/')pneu_images, pneu_labels = load_pneumonia('/kaggle/input/chest-xray-pneumonia/chest_xray/train/PNEUMONIA/')

到目前为止,咱们曾经取得了几个数组:norm_images,norm_labels,pneu_images 和 pneu_labels。

带_images 后缀的示意它蕴含预处理的图像,而带_labels 后缀的数组示意它存储了所有根本信息(也称为标签)。换句话说,norm_images 和 pneu_images 都将成为咱们的 X 数据,其余的将成为 y 数据。

为了使我的项目看起来更简略,我将这些数组的值连接起来并存储在 X_train 和 y_train 数组中。

X_train = np.append(norm_images, pneu_images, axis=0)
y_train = np.append(norm_labels, pneu_labels)

顺便说一句,我应用以下代码获取每个类的图像数:

显示多张图像

好吧,在这个阶段,显示几个图像并不是强制性的。但我想做是为了确保图片是否曾经加载和预处理好。上面的代码用于显示 14 张从 X_train 阵列随机拍摄的图像以及标签。

fig, axes = plt.subplots(ncols=7, nrows=2, figsize=(16, 4))

indices = np.random.choice(len(X_train), 14)
counter = 0

for i in range(2):
    for j in range(7):
        axes[i,j].set_title(y_train[indices[counter]])
        axes[i,j].imshow(X_train[indices[counter]], cmap='gray')
        axes[i,j].get_xaxis().set_visible(False)
        axes[i,j].get_yaxis().set_visible(False)
        counter += 1
plt.show()

咱们能够看到上图,所有图像当初都具备完全相同的大小,这与我用于本帖子封面图片的图像不同。

加载测试图像

咱们曾经晓得所有训练数据都已胜利加载,当初咱们能够应用完全相同的函数加载测试数据。步骤简直雷同,然而这里我将那些加载的数据存储在 X_test 和 y_test 数组中。用于测试的数据自身蕴含 624 个样本。

norm_images_test, norm_labels_test = load_normal('/kaggle/input/chest-xray-pneumonia/chest_xray/test/NORMAL/')pneu_images_test, pneu_labels_test = load_pneumonia('/kaggle/input/chest-xray-pneumonia/chest_xray/test/PNEUMONIA/')X_test = np.append(norm_images_test, pneu_images_test, axis=0)
y_test = np.append(norm_labels_test, pneu_labels_test)

此外,我留神到仅加载整个数据集就须要很长时间。因而,我将应用 pickle 模块将 X_train,X_test,y_train 和 y_test 保留在独自的文件中。这样我下次想再应用这些数据的时候,就不须要再次运行这些代码了。

# Use this to save variables
with open('pneumonia_data.pickle', 'wb') as f:
    pickle.dump((X_train, X_test, y_train, y_test), f)# Use this to load variables
with open('pneumonia_data.pickle', 'rb') as f:
    (X_train, X_test, y_train, y_test) = pickle.load(f)

因为所有 X 数据都通过了很好的预处理,因而当初应用标签 y_train 和 y_test 了。

标签预处理

此时,两个 y 变量都由以字符串数据类型编写的失常,细菌或病毒组成。实际上,这样的标签只是神经网络所不能承受的。因而,咱们须要将其转换为繁多格局。

侥幸的是,咱们从 Scikit-Learn 模块获取了 OneHotEncoder 对象,它对实现转换十分有帮忙。为此,咱们须要先在 y_train 和 y_test 上创立一个新轴。(咱们创立了这个新轴,因为那是 OneHotEncoder 冀望的形态)。

y_train = y_train[:, np.newaxis]
y_test = y_test[:, np.newaxis]

接下来,像这样初始化 one_hot_encoder。请留神,在这里我将 False 作为稠密参数传递,以便简化下一步。然而,如果你想应用稠密矩阵,则只需应用 sparse = True 或将参数保留为空即可。

one_hot_encoder = OneHotEncoder(sparse=False)

最初,咱们将应用 one_hot_encoder 将这些 y 数据转换为 one-hot。而后将编码后的标签存储在 y_train_one_hot 和 y_test_one_hot 中。这两个数组是咱们将用于训练的标签。

y_train_one_hot = one_hot_encoder.fit_transform(y_train)
y_test_one_hot = one_hot_encoder.transform(y_test)

将数据 X 重塑为(None,200,200,1)

当初让咱们回到 X_train 和 X_test。重要的是要晓得这两个数组的形态别离为(5216、200、200)和(624、200、200)。

乍一看,这两个形态看起来还能够,因为咱们能够应用 plt.imshow() 函数进行显示。然而,这种形态卷积层不可承受,因为它心愿将一个色彩通道作为其输出。

因而,因为该图像实质上是灰度图像,因而咱们须要增加一个 1 维的新轴,该轴将被卷积层辨认为惟一的色彩通道。尽管它的实现并不像我的解释那么简单:

X_train = X_train.reshape(X_train.shape[0], X_train.shape[1], X_train.shape[2], 1)
X_test = X_test.reshape(X_test.shape[0], X_test.shape[1], X_test.shape[2], 1)

运行上述代码后,如果咱们同时查看 X_train 和 X_test 的形态,那么咱们将看到当初的形态别离是(5216,200,200,1)和(624,200,200,1)。

数据裁减

减少数据(或者更具体地说是减少训练数据)的要点是,咱们将通过创立更多的样本(每个样本都具备某种随机性)来减少用于训练的数据数量。这些随机性可能包含平移、旋转、缩放、剪切和翻转。

这种技术能够帮忙咱们的神经网络分类器缩小过拟合,或者说,它能够使模型更好地泛化数据样本。侥幸的是,因为存在能够从 Keras 模块导入的 ImageDataGenerator 对象,实现非常简单。

datagen = ImageDataGenerator(
        rotation_range = 10,  
        zoom_range = 0.1, 
        width_shift_range = 0.1, 
        height_shift_range = 0.1)

因而,我在下面的代码中所做的基本上是设置随机范畴。如果你想理解每个参数的详细信息,请点击这里链接到 ImageDataGenerator 的文档。

  • https://keras.io/api/preproce…

接下来,在初始化 datagen 对象之后,咱们须要做的是使它和咱们的 X_train 相匹配。而后,该过程被随后施加的 flow() 的办法,该步骤中是十分有用的,使得所述 train_gen 对象当初可能产生加强数据的批次。

datagen.fit(X_train)train_gen = datagen.flow(X_train, y_train_one_hot, batch_size=32)

CNN(卷积神经网络)

当初是时候真正构建神经网络架构了。让咱们从输出层(input1)开始。因而,这一层基本上会获取 X 数据中的所有图像样本。因而,咱们须要确保第一层承受与图像尺寸完全相同的形态。值得注意的是,咱们仅须要定义(宽度,高度,通道),而不是(样本,宽度,高度,通道)。

尔后,此输出层连贯到几对卷积池层对,而后最终连贯到全连贯层。请留神,因为 ReLU 的计算速度比 S 型更快,因而模型中的所有暗藏层都应用 ReLU 激活函数,因而所需的训练工夫更短。最初,要连贯的最初一层是 output1,它由 3 个具备 softmax 激活函数的神经元组成。

这里应用 softmax 是因为咱们心愿输入是每个类别的概率值。

input1 = Input(shape=(X_train.shape[1], X_train.shape[2], 1))

cnn = Conv2D(16, (3, 3), activation='relu', strides=(1, 1), 
    padding='same')(input1)
cnn = Conv2D(32, (3, 3), activation='relu', strides=(1, 1), 
    padding='same')(cnn)
cnn = MaxPool2D((2, 2))(cnn)

cnn = Conv2D(16, (2, 2), activation='relu', strides=(1, 1), 
    padding='same')(cnn)
cnn = Conv2D(32, (2, 2), activation='relu', strides=(1, 1), 
    padding='same')(cnn)
cnn = MaxPool2D((2, 2))(cnn)

cnn = Flatten()(cnn)
cnn = Dense(100, activation='relu')(cnn)
cnn = Dense(50, activation='relu')(cnn)
output1 = Dense(3, activation='softmax')(cnn)

model = Model(inputs=input1, outputs=output1)

在应用下面的代码结构了神经网络之后,咱们能够通过对 model 对象利用 summary() 来显示模型的摘要。上面是咱们的 CNN 模型的详细情况。咱们能够看到咱们总共有 800 万个参数——这的确很多。好吧,这就是为什么我在 Kaggle Notebook 上运行这个代码。

总之,在构建模型之后,咱们须要应用分类穿插熵损失函数和 Adam 优化器来编译神经网络。应用这个损失函数,因为它只是多类分类工作中罕用的函数。同时,我抉择 Adam 作为优化器,因为它是在大多数神经网络工作中最小化损失的最佳抉择。

model.compile(loss='categorical_crossentropy', 
              optimizer='adam', metrics=['acc'])

当初是时候训练模型了!在这里,咱们将应用 fit_generator() 而不是 fit(),因为咱们将从 train_gen 对象获取训练数据。如果你关注数据裁减局部,你会留神到 train_gen 是应用 X_train 和 y_train_one_hot 创立的。因而,咱们不须要在 fit_generator() 办法中显式定义 X - y 对。

history = model.fit_generator(train_gen, epochs=30, 
          validation_data=(X_test, y_test_one_hot))

train_gen 的非凡之处在于,训练过程中将应用具备肯定随机性的样本来实现。因而,咱们在 X_train 中领有的所有训练数据都不会间接输出到神经网络中。取而代之的是,这些样本将被用作生成器的根底,通过一些随机变换生成一个新图像。

此外,该生成器在每个期间产生不同的图像,这对于咱们的神经网络分类器更好地泛化测试集中的样本十分无利。上面是训练的过程。

Epoch 1/30
163/163 [==============================] - 19s 114ms/step - loss: 5.7014 - acc: 0.6133 - val_loss: 0.7971 - val_acc: 0.7228
.
.
.
Epoch 10/30
163/163 [==============================] - 18s 111ms/step - loss: 0.5575 - acc: 0.7650 - val_loss: 0.8788 - val_acc: 0.7308
.
.
.
Epoch 20/30
163/163 [==============================] - 17s 102ms/step - loss: 0.5267 - acc: 0.7784 - val_loss: 0.6668 - val_acc: 0.7917
.
.
.
Epoch 30/30
163/163 [==============================] - 17s 104ms/step - loss: 0.4915 - acc: 0.7922 - val_loss: 0.7079 - val_acc: 0.8045

整个训练自身在我的 Kaggle Notebook 上破费了大概 10 分钟。所以要急躁点!经过训练后,咱们能够绘制出准确度得分的进步和损失值的升高,如下所示:

plt.figure(figsize=(8,6))
plt.title('Accuracy scores')
plt.plot(history.history['acc'])
plt.plot(history.history['val_acc'])
plt.legend(['acc', 'val_acc'])
plt.show()plt.figure(figsize=(8,6))
plt.title('Loss value')
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.legend(['loss', 'val_loss'])
plt.show()

依据下面的两个图,咱们能够说,即便在这 30 个期间内测试准确性和损失值都在稳定,模型的性能仍在一直进步。

这里要留神的另一重要事件是,因为咱们在我的项目的晚期利用了数据加强办法,因而该模型不会蒙受过拟合的困扰。咱们在这里能够看到,在最终迭代中,训练和测试数据的准确性别离为 79%和 80%。

乏味的事实:在施行数据加强办法之前,我在训练数据上取得了 100%的准确性,在测试数据上取得了 64%的准确性,这显然是过拟合了。因而,咱们能够在此处分明地看到,减少训练数据对于进步测试准确性得分十分无效,同时也能够缩小过拟合。

模型评估

当初,让咱们深刻理解应用混同矩阵得出的测试数据的准确性。首先,咱们须要预测所有 X_test 并将后果从独热格局转换回其理论的分类标签。

predictions = model.predict(X_test)
predictions = one_hot_encoder.inverse_transform(predictions)

接下来,咱们能够像这样应用 confusion_matrix() 函数:

cm = confusion_matrix(y_test, predictions)

重要的是要留神函数中应用的参数是(理论值,预测值)。该混同矩阵函数的返回值是一个二维数组,用于存储预测散布。为了使矩阵更易于解释,咱们能够应用 Seaborn 模块中的 heatmap() 函数进行显示。顺便说一句,这里的类名列表的值是依据 one_hot_encoder.categories_返回的程序获取的。

classnames = ['bacteria', 'normal', 'virus']plt.figure(figsize=(8,8))
plt.title('Confusion matrix')
sns.heatmap(cm, cbar=False, xticklabels=classnames, yticklabels=classnames, fmt='d', annot=True, cmap=plt.cm.Blues)
plt.xlabel('Predicted')
plt.ylabel('Actual')
plt.show()

依据下面的混同矩阵,咱们能够看到 45 张病毒 X 射线图像被预测为细菌。这可能是因为很难辨别这两种肺炎。然而,至多因为咱们对 242 个样本中的 232 个进行了正确分类,所以咱们的模型至多可能很好地预测由细菌引起的肺炎。

这就是整个我的项目!谢谢浏览!上面是运行整个我的项目所需的所有代码。

import os
import cv2
import pickle    # Used to save variables
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from tqdm import tqdm    # Used to display progress bar
from sklearn.preprocessing import OneHotEncoder
from sklearn.metrics import confusion_matrix
from keras.models import Model, load_model
from keras.layers import Dense, Input, Conv2D, MaxPool2D, Flatten
from keras.preprocessing.image import ImageDataGenerator    # Used to generate images

np.random.seed(22)

# Do not forget to include the last slash
def load_normal(norm_path):
    norm_files = np.array(os.listdir(norm_path))
    norm_labels = np.array(['normal']*len(norm_files))
    
    norm_images = []
    for image in tqdm(norm_files):
        # Read image
        image = cv2.imread(norm_path + image)
        # Resize image to 200x200 px
        image = cv2.resize(image, dsize=(200,200))
        # Convert to grayscale
        image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
        norm_images.append(image)
        
    norm_images = np.array(norm_images)
    
    return norm_images, norm_labels

def load_pneumonia(pneu_path):
    pneu_files = np.array(os.listdir(pneu_path))
    pneu_labels = np.array([pneu_file.split('_')[1] for pneu_file in pneu_files])
    
    pneu_images = []
    for image in tqdm(pneu_files):
        # Read image
        image = cv2.imread(pneu_path + image)
        # Resize image to 200x200 px
        image = cv2.resize(image, dsize=(200,200))
        # Convert to grayscale
        image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
        pneu_images.append(image)
        
    pneu_images = np.array(pneu_images)
    
    return pneu_images, pneu_labels


print('Loading images')
# All images are stored in _images, all labels are in _labels
norm_images, norm_labels = load_normal('/kaggle/input/chest-xray-pneumonia/chest_xray/train/NORMAL/')
pneu_images, pneu_labels = load_pneumonia('/kaggle/input/chest-xray-pneumonia/chest_xray/train/PNEUMONIA/')

# Put all train images to X_train 
X_train = np.append(norm_images, pneu_images, axis=0)

# Put all train labels to y_train
y_train = np.append(norm_labels, pneu_labels)

print(X_train.shape)
print(y_train.shape)
# Finding out the number of samples of each class
print(np.unique(y_train, return_counts=True))

print('Display several images')
fig, axes = plt.subplots(ncols=7, nrows=2, figsize=(16, 4))

indices = np.random.choice(len(X_train), 14)
counter = 0

for i in range(2):
    for j in range(7):
        axes[i,j].set_title(y_train[indices[counter]])
        axes[i,j].imshow(X_train[indices[counter]], cmap='gray')
        axes[i,j].get_xaxis().set_visible(False)
        axes[i,j].get_yaxis().set_visible(False)
        counter += 1
plt.show()


print('Loading test images')
# Do the exact same thing as what we have done on train data
norm_images_test, norm_labels_test = load_normal('/kaggle/input/chest-xray-pneumonia/chest_xray/test/NORMAL/')
pneu_images_test, pneu_labels_test = load_pneumonia('/kaggle/input/chest-xray-pneumonia/chest_xray/test/PNEUMONIA/')
X_test = np.append(norm_images_test, pneu_images_test, axis=0)
y_test = np.append(norm_labels_test, pneu_labels_test)

# Save the loaded images to pickle file for future use
with open('pneumonia_data.pickle', 'wb') as f:
    pickle.dump((X_train, X_test, y_train, y_test), f)

# Here's how to load it
with open('pneumonia_data.pickle', 'rb') as f:
    (X_train, X_test, y_train, y_test) = pickle.load(f)

print('Label preprocessing')

# Create new axis on all y data
y_train = y_train[:, np.newaxis]
y_test = y_test[:, np.newaxis]

# Initialize OneHotEncoder object
one_hot_encoder = OneHotEncoder(sparse=False)

# Convert all labels to one-hot
y_train_one_hot = one_hot_encoder.fit_transform(y_train)
y_test_one_hot = one_hot_encoder.transform(y_test)

print('Reshaping X data')
# Reshape the data into (no of samples, height, width, 1), where 1 represents a single color channel
X_train = X_train.reshape(X_train.shape[0], X_train.shape[1], X_train.shape[2], 1)
X_test = X_test.reshape(X_test.shape[0], X_test.shape[1], X_test.shape[2], 1)

print('Data augmentation')
# Generate new images with some randomness
datagen = ImageDataGenerator(
        rotation_range = 10,  
        zoom_range = 0.1, 
        width_shift_range = 0.1, 
        height_shift_range = 0.1)

datagen.fit(X_train)
train_gen = datagen.flow(X_train, y_train_one_hot, batch_size = 32)

print('CNN')

# Define the input shape of the neural network
input_shape = (X_train.shape[1], X_train.shape[2], 1)
print(input_shape)

input1 = Input(shape=input_shape)

cnn = Conv2D(16, (3, 3), activation='relu', strides=(1, 1), 
    padding='same')(input1)
cnn = Conv2D(32, (3, 3), activation='relu', strides=(1, 1), 
    padding='same')(cnn)
cnn = MaxPool2D((2, 2))(cnn)

cnn = Conv2D(16, (2, 2), activation='relu', strides=(1, 1), 
    padding='same')(cnn)
cnn = Conv2D(32, (2, 2), activation='relu', strides=(1, 1), 
    padding='same')(cnn)
cnn = MaxPool2D((2, 2))(cnn)

cnn = Flatten()(cnn)
cnn = Dense(100, activation='relu')(cnn)
cnn = Dense(50, activation='relu')(cnn)
output1 = Dense(3, activation='softmax')(cnn)

model = Model(inputs=input1, outputs=output1)

model.compile(loss='categorical_crossentropy', 
              optimizer='adam', metrics=['acc'])

# Using fit_generator() instead of fit() because we are going to use data
# taken from the generator. Note that the randomness is changing
# on each epoch
history = model.fit_generator(train_gen, epochs=30, 
          validation_data=(X_test, y_test_one_hot))

# Saving model
model.save('pneumonia_cnn.h5')

print('Displaying accuracy')
plt.figure(figsize=(8,6))
plt.title('Accuracy scores')
plt.plot(history.history['acc'])
plt.plot(history.history['val_acc'])
plt.legend(['acc', 'val_acc'])
plt.show()

print('Displaying loss')
plt.figure(figsize=(8,6))
plt.title('Loss value')
plt.plot(history.history['loss'])
plt.plot(history.history['val_loss'])
plt.legend(['loss', 'val_loss'])
plt.show()

# Predicting test data
predictions = model.predict(X_test)
print(predictions)

predictions = one_hot_encoder.inverse_transform(predictions)

print('Model evaluation')
print(one_hot_encoder.categories_)

classnames = ['bacteria', 'normal', 'virus']

# Display confusion matrix
cm = confusion_matrix(y_test, predictions)
plt.figure(figsize=(8,8))
plt.title('Confusion matrix')
sns.heatmap(cm, cbar=False, xticklabels=classnames, yticklabels=classnames, fmt='d', annot=True, cmap=plt.cm.Blues)
plt.xlabel('Predicted')
plt.ylabel('Actual')
plt.show()

参考文献

JędrzejDudzicz 对胸部 X 线查看的肺炎检出率约为 92%

  • https://www.kaggle.com/jedrze…

Kerian ImageDataGenerator 和 Adrian Rosebrock 的数据加强

  • https://www.pyimagesearch.com…

原文链接:https://www.analyticsvidhya.c…

欢送关注磐创 AI 博客站:
http://panchuang.net/

sklearn 机器学习中文官网文档:
http://sklearn123.com/

欢送关注磐创博客资源汇总站:
http://docs.panchuang.net/

正文完
 0