使用CNN和Python实施的肺炎检测
作者|Muhammad Ardi 编译|Flin 来源|analyticsvidhya
介绍
嘿!几个小时之前我刚完成了一个深度学习项目并想向大家介绍一下自己所做的工作。这项任务的主要目标是判断一个人是否患有肺炎如果是的话还需确定其病因类型是细菌还是由病毒引起的。我认为这个项目更适合称为分类任务而非检测任务。

换句话说,在这个问题中我们面临的是一个多分类任务其标签类别包括正常样本病毒样本以及细菌样本为此我们需要设计相应的分类模型以实现对这些不同类别的识别目标为此我们采用了卷积神经网络架构该架构不仅展现出卓越的图像分类能力而且还能通过实施图像增强技术从而提升模型性能值得一提的是在测试集上我们的模型达到了80%的准确率这对我来说确实是一个令人印象深刻的结果
可以从该Kaggle链接下载此项目中使用的数据集。
该数据集的总体容量约为1 GB这可能导致下载过程耗时较长。或者我们可以直接在Kaggle Notebook中建立并将其中的数据进行完整编码无需下载任何额外资源。下一步是浏览数据集目录结构其中包含三个子目录:train、test和val。
听起来像是显而易见的情况。此外,在train文件夹中包含的数据分别为正常类别下的1,341个样本、病毒类别下的1,345个样本以及细菌类别下的2,530个样本。总结一下就是我已经完成了全部介绍。接下来让我们开始编写代码部分!
注意:我在本文结尾处放置了该项目中使用的全部代码。
加载模块和训练图像
在进行计算机视觉项目时,请首先安装并导入所有必要的模块以及原始图像数据。为了更好地跟踪项目进展,请调用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)
接下来,我为后续工作准备了两个数据加载函数。从每个目录中读取图像数据是一个初步 glance 下看似无异的两个模块:一个专门处理NORMAL目录中的图片文件(即带有正常肺结节的标本),另一个则集中处理PNEUMONIA目录下的样本(代表了肺结核患者)。这种设计源于NORMAL和PNEUMONIA文件夹中名称格式存在细微差别这一客观事实。尽管在某些细节上有细微差别——比如目录下文件名的具体构成——但在核心流程上具有高度的一致性。
首先,将所有图像调整为200 x 200像素。
这一要点不可或缺, 因为每个文件夹内的图像尺寸各异, 而仅能接收固定大小数据的神经网络无法处理这些差异.
接下来,在大多数情况下(或情况中),所有图像都包含三个颜色通道(或颜色通道的数量),这对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)

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

显示多张图像
可以说,在这个阶段,并非强制要求必须显示几张图像。然而我想通过这一操作来确认图片是否已经被正确加载并进行了必要的预处理工作。下面的代码段将用于展示从X_train数组中随机选取的14张图片及其对应的标签信息。
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)
此外
# 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时需要先增加一个新的维度(因为我们增加了新的维度以适应One_hot_encoder的需求)
y_train = y_train[:, np.newaxis]
y_test = y_test[:, np.newaxis]
请特别注意的是,在当前配置中,我会将稀疏参数设置为False以简化后续操作。然而,在考虑使用稀疏矩阵的情况下,请确保该参数的设置不会影响后续计算的稳定性
one_hot_encoder = OneHotEncoder(sparse=False)
最终我们会采用one_hot_encoder来进行这些y数据的一热转换工作;接着我们将编码后的标签分别存储在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()函数来实现它们的可视化展示. 然而, 这种形状不具备卷积层所需的特点, 因为该类型的层通常需要每个通道作为一个独立的输入.
因为该图像本质上是灰度图像的原因在于其单色特性。因此,在构建神经网络模型时需要在数据预处理阶段增加一个一维的新维度来表示颜色信息这一关键特征。尽管这一过程本身的实现并非十分复杂:
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的形状时
数据扩充
收集数据(或者更具体地说是收集训练数据)的关键点在于我们计划通过生成更多样化的样本(每个样本都具备特定的随机特性)来提升用于训练的数据量。这种多样性主要体现在图像间的平移变换、旋转变换以及尺寸缩放等操作上。
这种技术有助于我们的神经网络分类器减少过拟合;或者换句话说,它能够使模型更有效地泛化数据样本.值得庆幸的是,在Keras模块中可以导入ImageDataGenerator对象实现起来非常简便.
datagen = ImageDataGenerator(
rotation_range = 10,
zoom_range = 0.1,
width_shift_range = 0.1,
height_shift_range = 0.1)
由此可见,在该代码中所进行的操作主要是设定一个随机区间范围。如果希望获取更多细节,请访问该生成器文档
在初始化 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)
通过上述代码构建了神经网络后, 该模型通过调用summary()方法可以展示摘要信息。让我们来看一下我们的CNN模型的具体参数分布情况。经过计算得出该模型拥有800万个参数——数量相当庞大。这表明在Kaggle Notebook环境中运行该代码时会遇到这样的参数量问题。

完成模型搭建后,并非直接采用标准损失函数即可达到预期效果;为此我们决定分别采用分类交叉熵损失函数与Adam优化器来进行神经网络的编译工作。其中因该指标为其为广泛应用于多类分类任务的关键指标;此外,在多数神经网络训练过程中追求最低损失的效率下;而Adam则因其在提升训练效果方面表现出色而被选为主导优化算法
model.compile(loss='categorical_crossentropy',
optimizer='adam', metrics=['acc'])
此时恰逢训练模型的时机,在此处我们选择性地采用的是fit_generator()函数而非传统的fit()方法。其原因在于我们从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))
其独特之处在于,在训练过程中采用具有随机性的样本来完成特定任务。因此,在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()


通过查看这两个图表可知,在这期间尽管测试准确性和损失值均呈现波动状态但整体上看模型的整体性能持续提升
这里需要注意的另一个关键点在于:我们已经在项目初期采用了数据增强方法这一举措。这表明该模型已经成功地避免了过拟合现象的发生。通过观察在最后一次迭代过程中所获得的数据表现情况可知:训练集与测试集上的准确率分别为79%和80%。
在未采用数据增强方法时,在训练集上达到了100%的准确率,在测试集上获得了64%的准确率。由此可见,在此情境下明显存在过拟合问题。然而通过增加训练集的数据量不仅显著提升了测试集的准确率而且有效降低了过拟合现象
模型评估
接下来我们将深入分析借助混淆矩阵计算出的测试数据准确性的评估结果具体步骤如下首先我们需要对所有待测样本进行预测并将预测结果从独热编码格式转换为其实际对应的分类标签
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()

根据上述混淆矩阵分析可知,在X射线图像识别任务中我们观察到有45幅感染病毒的影像被系统误判为细菌类别这可能与两类疾病之间的相似性有关然而我们仍能从成功地将其中的232个样本(占总测试数据量96%)正确分类这一事实中看出我们的模型在预测细菌引发肺炎方面表现出了较高的准确性
这就是整个项目!谢谢阅读!下面是运行整个项目所需的所有代码。
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/jedrzejdudzicz/pneumonia-detection-on-chest-x-ray-accuracy-92 提供了有关使用 chest X-ray 对 pneumonia 进行检测的详细信息,并且其准确率达到 92%。
Kerian ImageDataGenerator和Adrian Rosebrock的数据增强
欢迎关注磐创AI博客站: http://panchuang.net/
sklearn机器学习中文官方文档: http://sklearn123.com/
欢迎关注磐创博客资源汇总站: http://docs.panchuang.net/
