用python来检测皮肤癌

皮肤癌是皮肤细胞的异常生长产生的癌症,它是最常见的癌症之一,而且可能致命。但是如果及早发现,您的皮肤科医生可以对其进行治疗并彻底消除。

使用深度学习和神经网络,我们将能够对良性和恶性皮肤疾病进行分类,这可能有助于医生在早期阶段诊断出癌症。在本教程中,我们将创建一个皮肤疾病分类器,尝试使用Python中的TensorFlow框架仅从图像中区分良性(痣和脂溢性角化病)和恶性(黑素瘤)皮肤病。

用python来检测皮肤癌

好了,我们来一步一步操作吧。

▊ 安装所需的库:

pip3 install tensorflow tensorflow_hub matplotlib seaborn numpy pandas sklearn imblearn

打开一个新的笔记本(或bfwstudio)并导入必要的模块:

import tensorflow as tf

import tensorflow_hub as hub

import matplotlib.pyplot as plt

import numpy as np

import pandas as pd

import seaborn as sns

from tensorflow.keras.utils import get_file

from sklearn.metrics import roc_curve, auc, confusion_matrix

from imblearn.metrics import sensitivity_score, specificity_score

import os

import glob

import zipfile

import random

# to get consistent results after multiple runs

tf.random.set_seed(7)

np.random.seed(7)

random.seed(7)

# 0 for benign, 1 for malignant

class_names = ["benign", "malignant"]

▊ 准备数据集

在本教程中,我们将仅使用ISIC存档数据集的一小部分,以下函数下载并将数据集提取到新data文件夹中:

def download_and_extract_dataset():

# dataset from https://github.com/udacity/dermatologist-ai

# 5.3GB

train_url = "https://s3-us-west-1.amazonaws.com/udacity-dlnfd/datasets/skin-cancer/train.zip"

# 824.5MB

valid_url = "https://s3-us-west-1.amazonaws.com/udacity-dlnfd/datasets/skin-cancer/valid.zip"

# 5.1GB

test_url = "https://s3-us-west-1.amazonaws.com/udacity-dlnfd/datasets/skin-cancer/test.zip"

for i, download_link in enumerate([valid_url, train_url, test_url]):

temp_file = f"temp{i}.zip"

data_dir = get_file(origin=download_link, fname=os.path.join(os.getcwd(), temp_file))

print("Extracting", download_link)

with zipfile.ZipFile(data_dir, "r") as z:

z.extractall("data")

# remove the temp file

os.remove(temp_file)

# comment the below line if you already downloaded the dataset


download_and_extract_dataset()
这将花费几分钟,具体取决于您的网速,之后,data将显示包含训练,验证和测试集的文件夹。每个集是一个文件夹,其中包含三类皮肤疾病图像(痣,脂溢性角化病和黑色素瘤)。

注意:如果网速较慢,则可能难以使用上述Python函数下载数据集,在这种情况下,应下载并手动将其提取data到当前目录的文件夹中。

现在,我们已经在机器中拥有了数据集,让我们找到一种方法来标记这些图像,请记住我们将仅对良性和恶性皮肤疾病进行分类,因此我们需要将痣和脂溢性角化病标记为0和黑色素瘤1。

下面的单元格为每个集合生成一个元数据CSV文件,该CSV文件中的每一行对应于图像的路径及其标签(0或1):

# preparing data

# generate CSV metadata file to read img paths and labels from it

def generate_csv(folder, labels):

folder_name = os.path.basename(folder)

# convert comma separated labels into a list

label2int = {}

if labels:

labels = labels.split(",")

for label in labels:

string_label, integer_label = label.split("=")

label2int[string_label] = integer_label

labels = list(label2int)

# generate CSV file

df = pd.DataFrame(columns=["filepath", "label"])

i = 0

for label in labels:

print("Reading", os.path.join(folder, label, "*"))

for filepath in glob.glob(os.path.join(folder, label, "*")):

df.loc[i] = [filepath, label2int[label]]

i += 1

output_file = f"{folder_name}.csv"

print("Saving", output_file)

df.to_csv(output_file)

# generate CSV files for all data portions, labeling nevus and seborrheic keratosis

# as 0 (benign), and melanoma as 1 (malignant)

# you should replace "data" path to your extracted dataset path

# don't replace if you used download_and_extract_dataset() function

generate_csv("data/train", {"nevus": 0, "seborrheic_keratosis": 0, "melanoma": 1})

generate_csv("data/valid", {"nevus": 0, "seborrheic_keratosis": 0, "melanoma": 1})

generate_csv("data/test", {"nevus": 0, "seborrheic_keratosis": 0, "melanoma": 1})


generate_csv()函数接受2个参数,第一个是集合的路径,例如,如果您已下载并提取了中的数据集"E:\datasets\skin-cancer",则训练集应类似于"E:\datasets\skin-cancer\train".

第二个参数是字典,将每种皮肤病类别映射到其相应的标签值(同样,良性为0,恶性为1)。

我这样做的原因是可以在其他皮肤疾病分类(例如黑素细胞分类)上使用它,因此您可以添加更多皮肤疾病并将其用于其他问题。

运行该单元后,您会注意到3个CSV文件将出现在当前目录中。现在,让我们使用tf.data API中的from_tensor_slices()方法来加载这些元数据文件:

# loading data

train_metadata_filename = "train.csv"

valid_metadata_filename = "valid.csv"

# load CSV files as DataFrames

df_train = pd.read_csv(train_metadata_filename)

df_valid = pd.read_csv(valid_metadata_filename)

n_training_samples = len(df_train)

n_validation_samples = len(df_valid)

print("Number of training samples:", n_training_samples)

print("Number of validation samples:", n_validation_samples)

train_ds = tf.data.Dataset.from_tensor_slices((df_train["filepath"], df_train["label"]))

valid_ds = tf.data.Dataset.from_tensor_slices((df_valid["filepath"], df_valid["label"]))


现在我们已经加载了数据集(train_ds和valid_ds),每个样本都是filepath(图像文件路径)和label(良性为0,恶性为1)的元组。
Number of training samples: 2000

Number of validation samples: 150
让我们加载图像:
# preprocess data

def decode_img(img):

# convert the compressed string to a 3D uint8 tensor

img = tf.image.decode_jpeg(img, channels=3)

# Use `convert_image_dtype` to convert to floats in the [0,1] range.

img = tf.image.convert_image_dtype(img, tf.float32)

# resize the image to the desired size.

return tf.image.resize(img, [299, 299])

def process_path(filepath, label):

# load the raw data from the file as a string

img = tf.io.read_file(filepath)

img = decode_img(img)

return img, label

valid_ds = valid_ds.map(process_path)

train_ds = train_ds.map(process_path)

# test_ds = test_ds

for image, label in train_ds.take(1):

print("Image shape:", image.shape)

print("Label:", label.numpy())

上面的代码使用map()方法process_path()在两组样本中的每个样本上执行函数,它将基本上加载图像,解码图像格式,将图像像素转换为该范围[0, 1]并将其调整为(299, 299, 3),然后拍摄一张图像并打印形状:

Image shape: (299, 299, 3)

Label: 0
一切都按预期进行,现在让我们准备进行训练的数据集:

# training parameters

batch_size = 64

optimizer = "rmsprop"

def prepare_for_training(ds, cache=True, batch_size=64, shuffle_buffer_size=1000):

if cache:

if isinstance(cache, str):

ds = ds.cache(cache)

else:

ds = ds.cache()

# shuffle the dataset

ds = ds.shuffle(buffer_size=shuffle_buffer_size)

# Repeat forever

ds = ds.repeat()

# split to batches

ds = ds.batch(batch_size)

# `prefetch` lets the dataset fetch batches in the background while the model

# is training.

ds = ds.prefetch(buffer_size=tf.data.experimental.AUTOTUNE)

return ds

valid_ds = prepare_for_training(valid_ds, batch_size=batch_size, cache="valid-cached-data")

train_ds = prepare_for_training(train_ds, batch_size=batch_size, cache="train-cached-data")


这是我们所做的:

cache():由于我们在每个集合上进行了过多的计算,因此我们使用cache()方法将预处理的数据集保存到本地缓存文件中,这只会在第一次(在训练期间的第一个时期)对其进行预处理。

shuffle():基本上是对数据集进行混洗,因此样本是按随机顺序排列的。

repeat():每次迭代数据集时,它将不断为我们重复生成样本,这将在训练过程中为我们提供帮助。

batch():我们将每个训练步骤的数据集分为64或32个样本。

prefetch():这将使我们能够在训练模型时在后台提取批次。

下面的单元格将获取第一个验证对,并绘制图像及其相应的标签:

batch = next(iter(valid_ds))

def show_batch(batch):

plt.figure(figsize=(12,12))

for n in range(25):

ax = plt.subplot(5,5,n+1)

plt.imshow(batch[0][n])

plt.title(class_names[batch[1][n].numpy()].title())

plt.axis('off')

show_batch(batch)
输出:
用python来检测皮肤癌

良性和恶性皮肤图像样本

如您所见,很难区分恶性和良性疾病,让我们看看我们的模型将如何处理它。

太好了,现在我们的数据集已经准备好了,让我们开始构建模型。

▊ 建立模型

之前请注意,我们将所有图像的大小都调整为(299, 299, 3),这是因为InceptionV3架构希望将其作为输入,因此我们将使用带有TensorFlow Hub库的传递学习来下载和加载InceptionV3架构及其ImageNet预先训练的权重:

# building the model

# InceptionV3 model & pre-trained weights

module_url = "https://tfhub.dev/google/tf2-preview/inception_v3/feature_vector/4"

m = tf.keras.Sequential([

hub.KerasLayer(module_url, output_shape=[2048], trainable=False),

tf.keras.layers.Dense(1, activation="sigmoid")

])

m.build([None, 299, 299, 3])

m.compile(loss="binary_crossentropy", optimizer=optimizer, metrics=["accuracy"])

m.summary()


我们设置trainable为,False因此我们在训练期间将无法调整预训练的权重,我们还添加了一个最终输出层,该层具有1个单位,预期将输出0到1之间的值(接近0表示良性,并且1为恶性)。

之后,由于这是二进制分类,因此我们使用二进制交叉熵损失构建了模型,并使用了准确性作为度量(不是可靠的度量,我们将在后面很快看到原因),这是模型摘要的输出:

Model: "sequential"

_________________________________________________________________

Layer (type) Output Shape Param # 

=================================================================

keras_layer (KerasLayer) multiple 21802784 

_________________________________________________________________

dense (Dense) multiple 2049 

=================================================================

Total params: 21,804,833

Trainable params: 2,049

Non-trainable params: 21,802,784


▊ 训练模型

现在我们有了数据集和模型,让我们将它们放在一起:

model_name = f"benign-vs-malignant_{batch_size}_{optimizer}"

tensorboard = tf.keras.callbacks.TensorBoard(log_dir=os.path.join("logs", model_name))

# saves model checkpoint whenever we reach better weights

modelcheckpoint = tf.keras.callbacks.ModelCheckpoint(model_name + "_{val_loss:.3f}.h5", save_best_only=True, verbose=1)

history = m.fit(train_ds, validation_data=valid_ds, 

steps_per_epoch=n_training_samples // batch_size, 

validation_steps=n_validation_samples // batch_size, verbose=1, epochs=100,

callbacks=[tensorboard, modelcheckpoint])


我们正在使用ModelCheckpoint回调在每个纪元上保存迄今为止最好的权重,这就是为什么我将纪元设置为100,这是因为它可以随时收敛到更好的权重,以节省您的时间,随时将其减少到30左右。

我还添加了tensorboard作为回调,以防您想尝试不同的超参数值。

由于fit()方法不知道数据集中的样本数,因此我们需要为训练集和验证集分别指定迭代次数(样本数除以批量大小)steps_per_epoch和validation_steps参数。

这是训练期间输出的一部分:
Train for 31 steps, validate for 2 steps

Epoch 1/100

30/31 [============================>.] - ETA: 9s - loss: 0.4609 - accuracy: 0.7760 

Epoch 00001: val_loss improved from inf to 0.49703, saving model to benign-vs-malignant_64_rmsprop_0.497.h5

31/31 [==============================] - 282s 9s/step - loss: 0.4646 - accuracy: 0.7722 - val_loss: 0.4970 - val_accuracy: 0.8125

<..SNIPED..>

Epoch 27/100

30/31 [============================>.] - ETA: 0s - loss: 0.2982 - accuracy: 0.8708

Epoch 00027: val_loss improved from 0.40253 to 0.38991, saving model to benign-vs-malignant_64_rmsprop_0.390.h5

31/31 [==============================] - 21s 691ms/step - loss: 0.3025 - accuracy: 0.8684 - val_loss: 0.3899 - val_accuracy: 0.8359

<..SNIPED..>

Epoch 41/100

30/31 [============================>.] - ETA: 0s - loss: 0.2800 - accuracy: 0.8802

Epoch 00041: val_loss did not improve from 0.38991

31/31 [==============================] - 21s 690ms/step - loss: 0.2829 - accuracy: 0.8790 - val_loss: 0.3948 - val_accuracy: 0.8281

Epoch 42/100

30/31 [============================>.] - ETA: 0s - loss: 0.2680 - accuracy: 0.8859

Epoch 00042: val_loss did not improve from 0.38991

31/31 [==============================] - 21s 693ms/step - loss: 0.2722 - accuracy: 0.8831 - val_loss: 0.4572 - val_accuracy: 0.8047


▊ 模型评估

首先,像之前一样加载我们的测试集:

# evaluation

# load testing set

test_metadata_filename = "test.csv"

df_test = pd.read_csv(test_metadata_filename)

n_testing_samples = len(df_test)

print("Number of testing samples:", n_testing_samples)

test_ds = tf.data.Dataset.from_tensor_slices((df_test["filepath"], df_test["label"]))

def prepare_for_testing(ds, cache=True, shuffle_buffer_size=1000):

if cache:

if isinstance(cache, str):

ds = ds.cache(cache)

else:

ds = ds.cache()

ds = ds.shuffle(buffer_size=shuffle_buffer_size)

return ds

test_ds = test_ds.map(process_path)

test_ds = prepare_for_testing(test_ds, cache="test-cached-data")


上面的代码加载了我们的测试数据,并准备进行测试:

Number of testing samples: 600


600形状的图像(299, 299, 3)可以适合我们的内存,让我们将测试集从tf.data转换为numpy数组:

# convert testing set to numpy array to fit in memory (don't do that when testing

# set is too large)

y_test = np.zeros((n_testing_samples,))

X_test = np.zeros((n_testing_samples, 299, 299, 3))

for i, (img, label) in enumerate(test_ds.take(n_testing_samples)):

# print(img.shape, label.shape)

X_test[i] = img

y_test[i] = label.numpy()

print("y_test.shape:", y_test.shape)


上面的单元格将构造我们的数组,第一次执行将花费一些时间,因为它正在执行process_path()和prepare_for_testing()函数中定义的所有预处理。

现在,让我们加载在训练期间由ModelCheckpoint保存的最佳权重:

# load the weights with the least loss

m.load_weights("benign-vs-malignant_64_rmsprop_0.390.h5")


您可能没有最佳权重的确切文件名,您需要在损失最小的当前目录中搜索保存的权重,以下代码使用准确性指标评估模型:

print("Evaluating the model...")

loss, accuracy = m.evaluate(X_test, y_test, verbose=0)

print("Loss:", loss, " Accuracy:", accuracy)


输出:

Evaluating the model...

Loss: 0.4476394319534302 Accuracy: 0.8
我们已经84%在验证集和80%测试集上达到了准确性,但这还不是全部。由于我们的数据集在很大程度上是不平衡的,因此准确性并不能说明所有问题。实际上, 由于每个样本的恶性样本约占总验证集的20%,因此将每个图像都预测为良性的模型的准确度为80%。

结果,我们需要一种更好的方法来评估模型,在接下来的单元格中,我们将使用seaborn和matplotlib库来绘制混淆矩阵,该矩阵进一步告诉我们模型的运行情况。

但是在我们这样做之前,我只想澄清一点:我们都知道,将恶性疾病预测为良性疾病是一个可怕的错误,您可能会杀死这样做的人!因此,即使与良性肿瘤相比,我们只有很少的恶性样本,我们需要一种预测更多恶性病例的方法。一个好的方法是引入阈值。

请记住,神经网络的输出是介于0和1之间的值。正常情况下,当神经网络产生的值介于0到0.5之间时,我们会自动将其分配为良性,而将0.5到1.0分配为恶性。而且,由于我们想知道我们可以将恶性疾病预测为良性的事实(这只是许多原因之一),因此我们可以说,例如,从0到0.3是良性的,从0.3到1.0是恶性的,这意味着我们使用的阈值为0.3,这将改善我们的预测。

下面的函数可以做到这一点:

def get_predictions(threshold=None):

"""

Returns predictions for binary classification given `threshold`

For instance, if threshold is 0.3, then it'll output 1 (malignant) for that sample if

the probability of 1 is 30% or more (instead of 50%)

"""

y_pred = m.predict(X_test)

if not threshold:

threshold = 0.5

result = np.zeros((n_testing_samples,))

for i in range(n_testing_samples):

# test melanoma probability

if y_pred[i][0] >= threshold:

result[i] = 1

# else, it's 0 (benign)

return result

threshold = 0.23

# get predictions with 23% threshold

# which means if the model is 23% sure or more that is malignant,

# it's assigned as malignant, otherwise it's benign

y_pred = get_predictions(threshold)


现在,让我们绘制混淆矩阵并对其进行解释:

def plot_confusion_matrix(y_test, y_pred):

cmn = confusion_matrix(y_test, y_pred)

# Normalise

cmn = cmn.astype('float') / cmn.sum(axis=1)[:, np.newaxis]

# print it

print(cmn)

fig, ax = plt.subplots(figsize=(10,10))

sns.heatmap(cmn, annot=True, fmt='.2f', 

xticklabels=[f"pred_{c}" for c in class_names], 

yticklabels=[f"true_{c}" for c in class_names],

cmap="Blues"

)

plt.ylabel('Actual')

plt.xlabel('Predicted')

# plot the resulting confusion matrix

plt.show()

plot_confusion_matrix(y_test, y_pred)


输出:
用python来检测皮肤癌

良性与恶性皮肤疾病分类的混淆矩阵

▊ 灵敏度

因此0.72,考虑到患者患有疾病(混乱矩阵的右下角),我们的模型将获得阳性测试的概率,这通常称为敏感性。

敏感度是一种统计度量,广泛用于医学领域,由以下公式给出(来自Wikipedia):

灵敏度因此,在我们的示例中,在所有患有恶性皮肤病的患者中,我们成功地将其预测72%为恶性,虽然不错,但需要改善。

特异性

另一个指标是特异性,您可以在混淆矩阵的左上角读取它,我们知道63%。假设患者身体健康,基本上是阴性测试的可能性:

特异性在我们的示例中,在所有具有良性的患者中,我们预测63%他们为良性。

特异性高,该测试很少会给健康患者带来阳性结果,而高灵敏度意味着该模型在结果为阴性时是可靠的,我邀请您在此Wikipedia文章中阅读更多有关该模型的信息。

另外,您可以使用imblearn模块获得这些分数:

sensitivity = sensitivity_score(y_test, y_pred)

specificity = specificity_score(y_test, y_pred)

print("Melanoma Sensitivity:", sensitivity)

print("Melanoma Specificity:", specificity)


输出:

Melanoma Sensitivity: 0.717948717948718

Melanoma Specificity: 0.6252587991718427
接收器工作特性

另一个很好的度量是ROC,这基本上是一个曲线图昭示着我们我们的二元分类的诊断能力,它的功能就真阳性率Ÿ在轴上,假阳性率X轴。我们想要到达的完美点在图的左上角,这是使用matplotlib绘制ROC曲线的代码:

def plot_roc_auc(y_true, y_pred):

"""

This function plots the ROC curves and provides the scores.

"""

# prepare for figure

plt.figure()

fpr, tpr, _ = roc_curve(y_true, y_pred)

# obtain ROC AUC

roc_auc = auc(fpr, tpr)

# print score

print(f"ROC AUC: {roc_auc:.3f}")

# plot ROC curve

plt.plot(fpr, tpr, color="blue", lw=2,

label='ROC curve (area = {f:.2f})'.format(d=1, f=roc_auc))

plt.xlim([0.0, 1.0])

plt.ylim([0.0, 1.05])

plt.xlabel('False Positive Rate')

plt.ylabel('True Positive Rate')

plt.title('ROC curves')

plt.legend(loc="lower right")

plt.show()

plot_roc_auc(y_test, y_pred)


输出:
用python来检测皮肤癌

皮肤癌二元分类器的ROC曲线

ROC AUC: 0.671
太棒了,因为我们想最大化真实的阳性率,最小化错误的阳性率,所以计算ROC曲线下方的面积证明是有用的,我们得到的曲线下面积ROC(ROC AUC)为0.671 ,面积为1该模型适用于所有情况。

▊ 结论

大功告成!有了它,看看如何改进模型,我们只使用了2000训练样本,进入ISIC存档并下载更多内容并将其添加到data文件夹中,根据您添加的样本数量,得分将显著提高。您可以使用ISIC档案下载器,该文件下载器可以帮助您以所需的方式下载数据集。

我还建议您调整超参数,例如我们之前设置的阈值,看看您是否可以获得更好的敏感性和特异性得分。

我使用了InceptionV3模型架构,您可以随意使用任何所需的CNN架构,我邀请您浏览TensorFlow集线器并选择最新的模型。

关注我,每天更新一篇技术好文。

{{collectdata}}

网友评论0