关于人工智能:利用深度学习生成医疗报告

8次阅读

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

作者 |Vysakh Nair
编译 |VK
起源 |Towards Data Science

目录

  1. 理解问题
  2. 要求技能
  3. 数据
  4. 获取结构化数据
  5. 筹备文本数据 - 自然语言解决
  6. 获取图像特色 - 迁徙学习
  7. 输出管道 - 数据生成器
  8. 编 - 解码器模型 - 训练,贪心搜寻,束搜寻,BLEU
  9. 留神机制 - 训练,贪心搜寻,束搜寻,BLEU
  10. 摘要
  11. 将来工作
  12. 援用

1. 理解问题

图像字幕是一个具备挑战性的人工智能问题,它是指依据图像内容从图像中生成文本形容的过程。例如,请看下图:

一个常见的答案是“一个弹吉他的女人”。作为人类,咱们能够用适当的语言,看着一幅图画,形容其中的所有。这很简略。我再给你看一个:

好吧,你怎么形容这个?

对于咱们所有的“非放射科医生”,一个常见的答案是“胸部 x 光”。

对于放射科医生,他们撰写文本报告,叙述在影像学查看中身材各个部位的查看后果,特地是每个部位是失常、异样还是潜在异样。他们能够从一张这样的图像中取得有价值的信息并做出医疗报告。

对于经验不足的放射科医生和病理学家,尤其是那些在医疗品质绝对较低的农村地区工作的人来说,撰写医学影像报告是很艰难的,而另一方面,对于有教训的放射科医生和病理学家来说,写成像报告可能是乏味和耗时的。

所以,为了解决所有这些问题,如果一台计算机能够像下面这样的胸部 x 光片作为输出,并像放射科医生那样以文本模式输入后果,那岂不是很棒?

2. 基本技能

本文假如你对神经网络、cnn、RNNs、迁徙学习、Python 编程和 Keras 库等主题有肯定的理解。上面提到的两个模型将用于咱们的问题,稍后将在本博客中简要解释:

  1. 编解码器模型
  2. 留神机制

对它们有足够的理解会帮忙你更好地了解模型。

3. 数据

你能够从以下链接获取此问题所需的数据:

  • 图像- 蕴含所有的胸部 X 光片:http://academictorrents.com/d…
  • 报告- 蕴含上述图像的相应报告:http://academictorrents.com/d…

图像数据集蕴含一个人的多个胸部 x 光片。例如:x 光片的侧视图、多个侧面视图等。

正如放射科医生应用所有这些图像来编写报告,模型也将应用所有这些图像一起生成相应的后果。数据集中有 3955 个报告,每个报告都有一个或多个与之关联的图像。

3.1 从 XML 文件中提取所需的数据

数据集中的报表是 XML 文件,其中每个文件对应一个独自的。这些文件中蕴含了与此人相干的图像 id 和相应的后果。示例如下:

突出显示的信息是你须要从这些文件中提取的内容。这能够在 python 的 XML 库的帮忙下实现。

:调查结果也将称为报告。它们将在博客的其余局部调换应用。

import xml.etree.ElementTree as ET
img = []
img_impression = []
img_finding = []
# directory 蕴含报告文件
for filename in tqdm(os.listdir(directory)):
    if filename.endswith(".xml"):
        f = directory + '/' + filename
        tree = ET.parse(f)
        root = tree.getroot()
        for child in root:
            if child.tag == 'MedlineCitation':
                for attr in child:
                    if attr.tag == 'Article':
                        for i in attr:
                            if i.tag == 'Abstract':
                                for name in i:
                                    if name.get('Label') == 'FINDINGS':
                                        finding=name.text   
        for p_image in root.findall('parentImage'):
            img.append(p_image.get('id'))
            img_finding.append(finding)

4. 获取结构化数据

从 XML 文件中提取所需的数据后,数据将转换为结构化格局,以便于了解和拜访。

如前所述,有多个图像与单个报表关联。因而,咱们的模型在生成报告时也须要看到这些图像。但有些报告只有 1 张图片与之相干,而有些报告有 2 张,最多的只有 4 张。

所以问题就呈现了,咱们一次应该向模型输出多少图像来生成报告?为了使模型输出统一,一次抉择一对图像(即两个图像)作为输出。如果一个报表只有一个图像,那么同一个图像将被复制为第二个输出。

当初咱们有了一个适合且可了解的结构化数据。图像按其相对地址的名称保留。这将有助于加载数据。

5. 筹备文本数据

从 XML 文件中取得后果后,在咱们将其输出模型之前,应该对它们进行适当的清理和筹备。上面的图片展现了几个例子,展现了荡涤前的发现是什么样子。

咱们将按以下形式清理文本:

  1. 将所有字符转换为小写。
  2. 执行根本的解压,行将 won’t、can’t 等词别离转换为 will not、can not 等。
  3. 删除文本中的标点符号。留神,句号不会被删除,因为后果蕴含多个句子,所以咱们须要模型通过辨认句子以相似的形式生成报告。
  4. 从文本中删除所有数字。
  5. 删除长度小于或等于 2 的所有单词。例如,“is”、“to”等被删除。这些词不能提供太多信息。然而“no”这个词不会被删除,因为它减少了语义信息。在句子中加上“no”会齐全扭转它的意思。所以咱们在执行这些清理步骤时必须小心。你须要确定哪些词应该保留,哪些词应该防止。
  6. 还发现一些文本蕴含多个句号或空格,或“X”反复屡次。这样的字符也会被删除。

咱们将开发的模型将生成一个由两个图像组合而成的报告,该报告将一次生成一个单词。先前生成的单词序列将作为输出提供。

因而,咱们须要一个“第一个词”来启动生成过程,并用“最初一个词”来示意报告的完结。为此,咱们将应用字符串“startseq”和“endseq”。这些字符串被增加到咱们的数据中。当初这样做很重要,因为当咱们对文本进行编码时,须要正确地对这些字符串进行编码。

编码文本的次要步骤是创立从单词到惟一整数值的统一映射,称为标识化。为了让咱们的计算机可能了解任何文本,咱们须要以机器可能了解的形式将单词或句子合成。如果不执行标识化,就无奈解决文本数据。

标识化是将一段文本宰割成更小的单元(称为标识)的一种办法。标识能够是单词或字符,但在咱们的例子中,它将是单词。Keras 为此提供了一个内置库。

from tensorflow.keras.preprocessing.text import Tokenizer
tokenizer = Tokenizer(filters='!"#$%&()*+,-/:;<=>?@[\\]^_`{|}~\t\n')
tokenizer.fit_on_texts(reports)

当初,咱们曾经对文本进行了适当的清理和标识,以备未来应用。所有这些的残缺代码都能够在我的 GitHub 帐户中找到,这个帐户的链接在本文开端提供。

6. 获取图像特色

图像和局部报告是咱们模型的输出。咱们须要将每个图像转换成一个固定大小的向量,而后将其作为输出传递到模型中。为此,咱们将应用迁徙学习。

“在迁徙学习中,咱们首先在根本数据集和工作上训练根底网络,而后咱们将学习到的特色从新指定用处,或将其转移到第二个指标网络,以便在指标数据集和工作上进行训练。如果特色是通用的,也就是说既适宜根本工作也适宜指标工作,而不是特定于根本工作,那此过程将趋于无效。”

VGG16、VGG19 或 InceptionV3 是用于迁徙学习的常见 cnn。这些都是在像 Imagenets 这样的数据集上训练的,这些数据集的图像与胸部 x 光齐全不同。所以从逻辑上讲,他们仿佛不是咱们工作的好抉择。那么咱们应该应用哪种网络来解决咱们的问题呢?

如果你不相熟,让我介绍你意识 CheXNet。CheXNet 是一个 121 层的卷积神经网络,训练于胸片 X 射线 14 上,目前是最大的公开胸片 X 射线数据集,蕴含 10 万多张侧面视图的 14 种疾病的 X 射线图像。然而,咱们在这里的目标不是对图像进行分类,而是获取每个图像的特色。因而,不须要该网络的最初一个分类层。

你能够从这里下载 CheXNet 的训练权重:https://drive.google.com/file…。

from tensorflow.keras.applications import densenet

chex = densenet.DenseNet121(include_top=False, weights = None,   input_shape=(224,224,3), pooling="avg")

X = chex.output
X = Dense(14, activation="sigmoid", name="predictions")(X)

model = Model(inputs=chex.input, outputs=X)

model.load_weights('load_the_downloaded_weights.h5')

chexnet = Model(inputs = model.input, outputs = model.layers[-2].output)

如果你忘了,咱们有两个图像作为输出到咱们的模型。上面是如何取得特色:

每个图像的大小被调整为 (224,224,3),并通过 CheXNet 传递,失去 1024 长度的特征向量。随后,将这两个特征向量串联以取得 2048 特征向量。

如果你留神到,咱们增加了一个均匀池层作为最初一层。这是有起因的。因为咱们要连贯两个图像,所以模型可能会学习一些连贯程序。例如,image1 总是在 image2 之后,反之亦然,但这里不是这样。咱们在连贯它们时不放弃任何程序。这个问题是通过池来解决的。

代码如下:

def load_image(img_name):
'''加载图片函数'''
    image = Image.open(img_name)
    image_array = np.asarray(image.convert("RGB"))
    image_array = image_array / 255.
    image_array = resize(image_array, (224,224))
    X = np.expand_dims(image_array, axis=0)
    X = np.asarray(X) 
    return X
Xnet_features = {}
for key, img1, img2, finding in tqdm(dataset.values):
    i1 = load_image(img1)
    img1_features = chexnet.predict(i1)    
    i2 = load_image(img2)
    img2_features = chexnet.predict(i2)
    input_ = np.concatenate((img1_features, img2_features), axis=1)
    Xnet_features[key] = input_

这些特色以 pickle 格局存储在字典中,可供未来应用。

7. 输出管道

思考这样一个场景:你有大量的数据,以至于你不能一次将所有数据都保留在 RAM 中。购买更多的内存显然不是每个人都能够进行的抉择。

解决方案能够是动静地将小批量的数据输出到模型中。这正是数据生成器所做的。它们能够动静生成模型输出,从而造成从存储器到 RAM 的管道,以便在须要时加载数据。

这种管道的另一个长处是,当这些小批量数据筹备输出模型时,能够轻松的利用。

为了咱们的问题咱们将应用 tf.data。

咱们首先将数据集分为两局部,一个训练数据集和一个验证数据集。在进行划分时,要确保你有足够的数据点用于训练,并且有足够数量的数据用于验证。我抉择的比例容许我在训练集中有 2560 个数据点,在验证集中有 1147 个数据点。

当初是时候为咱们的数据集创立生成器了。

X_train_img, X_cv_img, y_train_rep, y_cv_rep = train_test_split(dataset['Person_id'], dataset['Report'],
                                                                test_size = split_size, random_state=97)
def load_image(id_, report):
    '''加载具备相应 id 的图像特色'''
    img_feature = Xnet_Features[id_.decode('utf-8')][0]
    return img_feature, report
def create_dataset(img_name_train, report_train):
    dataset = tf.data.Dataset.from_tensor_slices((img_name_train, report_train))
  # 应用 map 并行加载 numpy 文件
    dataset = dataset.map(lambda item1, item2: tf.numpy_function(load_image, [item1, item2],
                          [tf.float32, tf.string]),
                          num_parallel_calls=tf.data.experimental.AUTOTUNE)
  # 随机并 batch 化
    dataset = dataset.shuffle(500).batch(BATCH_SIZE).prefetch(buffer_size=tf.data.experimental.AUTOTUNE)
    return dataset
train_dataset = create_dataset(X_train_img, y_train_rep)
cv_dataset = create_dataset(X_cv_img, y_cv_rep)

在这里,咱们创立了两个数据生成器,用于训练的 train_dataset 和用于验证的 cv_dataset。create_dataset 函数获取 id(对于后面创立的特色,这是字典的键)和预处理的报告,并创立生成器。生成器一次生成 batch 大小的数据点数量。

如前所述,咱们要创立的模型将是一个逐字的模型。该模型以图像特色和局部序列为输出,生成序列中的下一个单词。

例如:让“图像特色”对应的报告为“startseq the cardiac silhouette and mediastinum size are within normal limits endseq”。

而后将输出序列分成 11 个输入输出对来训练模型:

留神,咱们不是通过生成器创立这些输入输出对。生成器一次只向咱们提供图像特色的 batch 解决大小数量及其相应的残缺报告。输入输出对在训练过程中稍后生成,稍后将对此进行解释。

8. 编解码器模型

sequence-to-sequence 模型是一个深度学习模型,它承受一个序列(在咱们的例子中,是图像的特色)并输入另一个序列(报告)。

编码器解决输出序列中的每一项,它将捕捉的信息编译成一个称为上下文的向量。在解决残缺个输出序列后,编码器将上下文发送到解码器,解码器开始逐项生成输入序列。

本例中的编码器是一个 CNN,它通过获取图像特色来生成上下文向量。译码器是一个循环神经网络。

Marc Tanti 在他的论文 Where to put the Image in an Image Caption Generator, 中介绍了 init-inject、par-inject、pre-inject 和 merge 等多种体系结构。在创立一个图像题目生成器时,指定了图像应该注入的地位。咱们将应用他论文中指定的架构来解决咱们的问题。

在“Merge”架构中,RNN 在任何时候都不裸露于图像向量(或从图像向量派生的向量)。取而代之的是,在 RNN 进行了整体编码之后,图像被引入到语言模型中。这是一种前期绑定体系结构,它不会随每个工夫步批改图像示意。

他的论文中的一些重要论断被用于咱们实现的体系结构中。他们是:

  1. RNN 输入须要正则化,并带有失落。
  2. 图像向量不应该有一个非线性的激活函数,或者应用 dropout 进行正则化。
  3. 从 CheXNet 中提取特色时,图像输出向量在输出到神经网络之前必须进行归一化解决。

嵌入层

词嵌入是一类应用密集向量示意来示意单词和文档的办法。Keras 提供了一个嵌入层,能够用于文本数据上的神经网络。它也能够应用在别处学过的词嵌入。在自然语言解决畛域,学习、保留词嵌入是很常见的。

在咱们的模型中,嵌入层应用预训练的 GLOVE 模型将每个单词映射到 300 维示意中。应用预训练的嵌入时,请记住,应该通过设置参数“trainable=False”解冻层的权重,这样权重在训练时不会更新。

模型代码:

input1 = Input(shape=(2048), name='Image_1')
dense1 = Dense(256, kernel_initializer=tf.keras.initializers.glorot_uniform(seed = 56),
               name='dense_encoder')(input1)

input2 = Input(shape=(155), name='Text_Input')
emb_layer = Embedding(input_dim = vocab_size, output_dim = 300, input_length=155, mask_zero=True,
                      trainable=False, weights=[embedding_matrix], name="Embedding_layer")
emb = emb_layer(input2)
LSTM2 = LSTM(units=256, activation='tanh', recurrent_activation='sigmoid', use_bias=True, 
            kernel_initializer=tf.keras.initializers.glorot_uniform(seed=23),
            recurrent_initializer=tf.keras.initializers.orthogonal(seed=7),
            bias_initializer=tf.keras.initializers.zeros(), name="LSTM2")
LSTM2_output = LSTM2(emb)
dropout1 = Dropout(0.5, name='dropout1')(LSTM2_output)

dec =  tf.keras.layers.Add()([dense1, dropout1])

fc1 = Dense(256, activation='relu', kernel_initializer=tf.keras.initializers.he_normal(seed = 63),
            name='fc1')
fc1_output = fc1(dec)
output_layer = Dense(vocab_size, activation='softmax', name='Output_layer')
output = output_layer(fc1_output)

encoder_decoder = Model(inputs = [input1, input2], outputs = output)

模型摘要:

8.1 训练

损失函数

为此问题建设了一个掩蔽损失函数。例如:

如果咱们有一系列标识[3],[10],[7],[0],[0],[0],[0],[0]

咱们在这个序列中只有 3 个单词,0 对应于填充,实际上这不是报告的一部分。然而模型会认为零也是序列的一部分,并开始学习它们。

当模型开始正确预测零时,损失将缩小,因为对于模型来说,它是正确学习的。但对于咱们来说,只有当模型正确地预测理论单词(非零)时,损失才应该缩小。

因而,咱们应该屏蔽序列中的零,这样模型就不会关注它们,而只学习报告中须要的单词。

loss_function = tf.keras.losses.CategoricalCrossentropy(from_logits=False, reduction='auto')

def maskedLoss(y_true, y_pred):
    #获取掩码
    mask = tf.math.logical_not(tf.math.equal(y_true, 0))
    
    #计算 loss
    loss_ = loss_function(y_true, y_pred)
    
    #转换为 loss_ dtype 类型
    mask = tf.cast(mask, dtype=loss_.dtype)
    
    #给损失函数利用掩码
    loss_ = loss_*mask
    
    #获取均值
    loss_ = tf.reduce_mean(loss_)
    return loss_

输入词是一个 one-hot 编码,因而分类穿插熵将是咱们的损失函数。

optimizer = tf.keras.optimizers.Adam(0.001)
encoder_decoder.compile(optimizer, loss = maskedLoss)

还记得咱们的数据生成器吗?当初是时候应用它们了。

这里,生成器提供的 batch 不是咱们用于训练的理论数据 batch。请记住,它们不是逐字输入输出对。它们只返回图像及其相应的整个报告。

咱们将从生成器中检索每个 batch,并将从该 batch 中手动创立输入输出序列,也就是说,咱们将创立咱们本人的定制的 batch 数据以供训练。所以在这里,batch 解决大小逻辑上是模型在一个 batch 中看到的图像对的数量。咱们能够依据咱们的零碎能力扭转它。我发现这种办法比其余博客中提到的传统定制生成器要快得多。

因为咱们正在创立本人的 batch 数据用于训练,因而咱们将应用“train_on_batch”来训练咱们的模型。

epoch_train_loss = []
epoch_val_loss = []

for epoch in range(EPOCH):
    print('EPOCH :',epoch+1)
    start = time.time()
    batch_loss_tr = 0
    batch_loss_vl = 0
    
    for img, report in train_dataset:
       
        r1 = bytes_to_string(report.numpy())
        img_input, rep_input, output_word = convert(img.numpy(), r1)
        rep_input = pad_sequences(rep_input, maxlen=MAX_INPUT_LEN, padding='post')
        results = encoder_decoder.train_on_batch([img_input, rep_input], output_word)
        
        batch_loss_tr += results
    train_loss = batch_loss_tr/(X_train_img.shape[0]//BATCH_SIZE)
    with train_summary_writer.as_default():
        tf.summary.scalar('loss', train_loss, step = epoch)
    
    for img, report in cv_dataset:
        
        r1 = bytes_to_string(report.numpy())
        img_input, rep_input, output_word = convert(img.numpy(), r1)
        rep_input = pad_sequences(rep_input, maxlen=MAX_INPUT_LEN, padding='post')
        results = encoder_decoder.test_on_batch([img_input, rep_input], output_word)
        batch_loss_vl += results
    
    val_loss = batch_loss_vl/(X_cv_img.shape[0]//BATCH_SIZE)
    with val_summary_writer.as_default():
        tf.summary.scalar('loss', val_loss, step = epoch)

    epoch_train_loss.append(train_loss)
    epoch_val_loss.append(val_loss)
    
    print('Training Loss: {},  Val Loss: {}'.format(train_loss, val_loss))
    print('Time Taken for this Epoch : {} sec'.format(time.time()-start))   
    encoder_decoder.save_weights('Weights/BM7_new_model1_epoch_'+ str(epoch+1) + '.h5')

代码中提到的 convert 函数将生成器中的数据转换为逐字输入输出对示意。而后将残余报告填充到报告的最大长度。

Convert 函数:

def convert(images, reports):
    '''此函数承受 batch 数据并将其转换为新数据集'''
    imgs = []
    in_reports = []
    out_reports = []
    for i in range(len(images)):
        sequence = [tokenizer.word_index[e] for e in reports[i].split() if e in tokenizer.word_index.keys()]
        for j in range(1,len(sequence)):
            
            in_seq = sequence[:j]
            out_seq = sequence[j]
            out_seq = tf.keras.utils.to_categorical(out_seq, num_classes=vocab_size)

            imgs.append(images[i])
            in_reports.append(in_seq)
            out_reports.append(out_seq)
    return np.array(imgs), np.array(in_reports), np.array(out_reports)

Adam 优化器的学习率为 0.001。该模型训练了 40 个 epoch,但在第 35 个 epoch 失去了最好的后果。因为随机性,你失去的后果可能会有所不同。

:以上训练在 Tensorflow 2.1 中实现。

8.2 推理

当初咱们曾经训练了咱们的模型,是时候筹备咱们的模型来预测报告了。

为此,咱们必须对咱们的模型作一些调整。这将在测试期间节俭一些工夫。

首先,咱们将从模型中拆散出编码器和解码器局部。由编码器预测的特色将被用作咱们的解码器的输出。

# 编码器
encoder_input = encoder_decoder.input[0]
encoder_output = encoder_decoder.get_layer('dense_encoder').output
encoder_model = Model(encoder_input, encoder_output)


# 解码器
text_input = encoder_decoder.input[1]
enc_output = Input(shape=(256,), name='Enc_Output')
text_output = encoder_decoder.get_layer('LSTM2').output
add1 = tf.keras.layers.Add()([text_output, enc_output])
fc_1 = fc1(add1)
decoder_output = output_layer(fc_1)
decoder_model = Model(inputs = [text_input, enc_output], outputs = decoder_output)

通过这样做,咱们只须要预测一次编码器的特色,而咱们将其用于贪心搜寻和束 (beam) 搜索算法。

咱们将实现这两种生成文本的算法,并看看哪一种算法最无效。

8.3 贪心搜索算法

贪心搜寻是一种算法范式,它逐块构建解决方案,每次总是抉择最好的。

贪心搜寻步骤

  1. 编码器输入图像的特色。编码器的工作到此结束。一旦咱们有了咱们须要的特色,咱们就不须要关注编码器了。
  2. 这个特征向量和起始标识“startseq”(咱们的初始输出序列)被作为解码器的第一个输出。
  3. 译码器预测整个词汇表的概率分布,概率最大的单词将被选为下一个单词。
  4. 这个预测失去的单词和前一个输出序列将是咱们下一个输出序列,并且传递到解码器。
  5. 继续执行步骤 3 -4,直到遇到完结标识,即“endseq”。
def greedysearch(img):
    image = Xnet_Features[img] # 提取图像的初始 chexnet 特色
    input_ = 'startseq'  # 报告的起始标识
    image_features = encoder_model.predict(image) # 编码输入
    
    result = [] 
    for i in range(MAX_REP_LEN):
        input_tok = [tokenizer.word_index[w] for w in input_.split()]
        input_padded = pad_sequences([input_tok], 155, padding='post')
        predictions = decoder_model.predict([input_padded, image_features])
        arg = np.argmax(predictions)
        if arg != tokenizer.word_index['endseq']:   # endseq 标识
            result.append(tokenizer.index_word[arg])
            input_ = input_ + ' ' + tokenizer.index_word[arg]
        else:
            break
    rep = ' '.join(e for e in result)
    return rep

让咱们检查一下在应用 greedysearch 生成报告后,咱们的模型的性能如何。

BLEU 分数 - 贪心搜寻:

双语评估替补分数,简称 BLEU,是掂量生成句到参考句的一个指标。

完满匹配的后果是 1.0 分,而齐全不匹配的后果是 0.0 分。该办法通过计算候选文本中匹配的 n 个单词到参考文本中的 n 个单词,其中 uni-gram 是每个标识,bigram 比拟是每个单词对。

在实践中不可能失去完满的分数,因为译文必须与参考文献齐全匹配。这甚至连人类的翻译人员都不可能做到。

要理解无关 BLEU 的更多信息,请单击此处:https://machinelearningmaster…

8.4 束搜寻

Beam search(束搜寻)是一种在贪心搜寻的根底上扩大并返回最有可能的输入序列列表的算法。每个序列都有一个与之相干的分数。以得分最高的程序作为最终后果。

在构建序列时,束搜寻不是贪心地抉择最有可能的下一步,而是扩大所有可能的下一步并放弃 k 个最有可能的后果,其中 k(即束宽度)是用户指定的参数,并通过概率序列管制束数或并行搜寻。

束宽度为 1 的束搜寻就是贪心搜寻。常见的束宽度值为 5 -10,但钻研中甚至应用了高达 1000 或 2000 以上的值,以从模型中挤出最佳性能。要理解更多无关束搜寻的信息,请单击此处。

但请记住,随着束宽度的减少,工夫复杂度也会减少。因而,这些比贪心搜寻慢得多。

def beamsearch(image, beam_width):
    
    start = [tokenizer.word_index['startseq']]

    sequences = [[start, 0]]
    
    img_features = Xnet_Features[image]
    img_features = encoder_model.predict(img_features)
    finished_seq = []
    
    for i in range(max_rep_length):
        all_candidates = []
        new_seq = []
        for s in sequences:

            text_input = pad_sequences([s[0]], 155, padding='post')
            predictions = decoder_model.predict([img_features, text_input])
            top_words = np.argsort(predictions[0])[-beam_width:]
            seq, score = s
            
            for t in top_words:
                candidates = [seq + [t], score - log(predictions[0][t])]
                all_candidates.append(candidates)
                
        sequences = sorted(all_candidates, key = lambda l: l[1])[:beam_width]
        # 查看波束中每个序列中的 'endseq'
        count = 0
        for seq,score in sequences:
            if seq[len(seq)-1] == tokenizer.word_index['endseq']:
                score = score/len(seq)   # 标准化
                finished_seq.append([seq, score])
                count+=1
            else:
                new_seq.append([seq, score])
        beam_width -= count
        sequences = new_seq
        
        # 如果所有序列在 155 个工夫步之前完结
        if not sequences:
            break
        else:
            continue
        
    sequences = finished_seq[-1] 
    rep = sequences[0]
    score = sequences[1]
    temp = []
    rep.pop(0)
    for word in rep:
        if word != tokenizer.word_index['endseq']:
            temp.append(tokenizer.index_word[word])
        else:
            break    
    rep = ' '.join(e for e in temp)        
    
    return rep, score

束搜寻并不总是能保障更好的后果,但在大多数状况下,它会给你一个更好的后果。

你能够应用下面给出的函数查看束搜寻的 BLEU 分数。但请记住,评估它们须要一段时间(几个小时)。

8.5 示例

当初让咱们看看胸部 X 光片的预测报告:

图像对 1 的原始报告:“心脏失常大小。纵隔不显著。肺部很洁净。”

图像对 1 的预测报告:“心脏失常大小。纵隔不显著。肺部很洁净。”

对于这个例子,模型预测的是完全相同的报告。

图像对 2 的原始报告:“心脏大小和肺血管在失常范畴内。未发现局灶性浸润性气胸胸腔积液

图像对 2 的预测报告:“心脏大小和肺血管在失常范畴内呈现。肺为游离灶性空域病变。未见胸腔积液气胸

尽管不完全相同,但预测后果与最后的报告简直类似。

图像对 3 的原始报告:“肺适度收缩但清晰。无局灶性浸润性渗出。心脏和纵隔轮廓在失常范畴内。发现有钙化的纵隔

图像对 3 的预测报告:“心脏大小失常。纵隔轮廓在失常范畴内。肺部没有任何病灶浸润。没有结节肿块。无显著气胸。无可见胸膜液。这是十分失常的。横膈膜下没有可见的游离腹腔内空气。”

你没想到这个模型能完满地工作,是吗?没有一个模型是完满的,这个也不是完满的。只管存在从图像对 3 正确辨认的一些细节,然而产生的许多额定细节可能是正确的,也可能是不正确的。

咱们创立的模型并不是一个完满的模型,但它的确为咱们的图像生成了体面的报告。

当初让咱们来看看一个高级模型,看看它是否进步了以后的性能!!

9. 留神机制

留神机制是对编解码模型的改良。事实证明,上下文向量是这些类型模型的瓶颈。这使他们很难解决长句。Bahdanau et al.,2014 和 Luong et al.,2015 提出了解决方案。

这些论文介绍并改良了一种叫做“留神机制”的技术,它极大地提高了机器翻译零碎的品质。留神容许模型依据须要关注输出序列的相干局部。起初,这一思维被利用于图像题目。

那么,咱们如何为图像建设注意力机制呢?

对于文本,咱们对输出序列的每个地位都有一个示意。然而对于图像,咱们通常应用网络中一个全连贯层示意,然而这种示意不蕴含任何地位信息(想想看,它们是全连贯的)。

咱们须要查看图像的特定局部(地位)来形容其中的内容。例如,要从 x 光片上形容一个人的心脏大小,咱们只须要察看他的心脏区域,而不是他的手臂或任何其余部位。那么,注意力机制的输出应该是什么呢?

咱们应用卷积层(迁徙学习)的输入,而不是全连贯的示意,因为卷积层的输入具备空间信息。

例如,让最初一个卷积层的输入是(7×14×1024)大小的特色。这里,“7×14”是与图像中某些局部绝对应的理论地位,1024 个是通道。咱们关注的不是通道而是图像的地位。因而,这里咱们有 7 *14=98 个这样的地位。咱们能够把它看作是 98 个地位,每个地位都有 1024 维示意。

当初咱们有 98 个工夫步,每个工夫步有 1024 个维示意。咱们当初须要决定模型应该如何关注这 98 个工夫点或地位。

一个简略的办法是给每个地位调配一些权重,而后失去所有这 98 个地位的加权和。如果一个特定的工夫步长对于预测一个输入十分重要,那么这个工夫步长将具备更高的权重。让这些分量用字母示意。

当初咱们晓得了,alpha 决定了一个特定地点的重要性。alpha 值越高,重要性越高。然而咱们如何找到 alpha 的值呢?没有人会给咱们这些值,模型自身应该从数据中学习这些值。为此,咱们定义了一个函数:

这个量示意第 j 个输出对于解码第 t 个输入的重要性。h_j 是第 j 个地位示意,s_t- 1 是解码器到该点的状态。咱们须要这两个量来确定 e_jt。f_ATT 只是一个函数,咱们将在前面定义。

在所有输出中,咱们心愿这个量(e_jt)的总和为 1。这就像是用概率分布来示意输出的重要性。利用 softmax 将 e_jt 转换为概率分布。

当初咱们有了 alphas!alphas 是咱们的权重。alpha_jt 示意聚焦于第 j 个输出以产生第 t 个输入的概率。

当初是时候定义咱们的函数 f_ATT 了。以下是许多可能的抉择之一:

V、U 和 W 是在训练过程中学习的参数,用于确定 e_jt 的值。

咱们有 alphas,咱们有输出,当初咱们只须要失去加权和,产生新的上下文向量,它将被输出解码器。在实践中,这些模型比编解码器模型工作得更好。

模型实现:

和下面提到的编解码器模型一样,这个模型也将由两局部组成,一个编码器和一个解码器,但这次解码器中会有一个额定的注意力成分,即注意力解码器。为了更好地了解,当初让咱们用代码编写:

# 计算 e_jts
score = self.Vattn(tf.nn.tanh(self.Uattn(features) + self.Wattn(hidden_with_time_axis)))

# 应用 softmax 将分数转换为概率分布
attention_weights = tf.nn.softmax(score, axis=1)

# 计算上下文向量(加权和)context_vector = attention_weights * features

在构建模型时,咱们不用从头开始编写这些代码行。keras 库曾经为这个目标内置了一个留神层。咱们将间接应用增加层或其余称为 Bahdanau 的注意力。你能够从文档自身理解无关该层的更多信息。链接:https://www.tensorflow.org/ap…

这个模型的文本输出将放弃不变,然而对于图像特色,这次咱们将从 CheXNet 网络的最初一个 conv 层获取特色。

合并两幅图像后的最终输入形态为(None,7,14,1024)。所以整形后编码器的输出将是(None,981024)。为什么要重塑图像?好吧,这曾经在注意力介绍中解释过了,如果你有任何疑难,肯定要把解释再读一遍。

模型

input1 = Input(shape=(98,1024), name='Image_1')
maxpool1 = tf.keras.layers.MaxPool1D()(input1)
dense1 = Dense(256, kernel_initializer=tf.keras.initializers.glorot_uniform(seed = 56), name='dense_encoder')(maxpool1)

input2 = Input(shape=(155), name='Text_Input')
emb_layer = Embedding(input_dim = vocab_size, output_dim = 300, input_length=155, mask_zero=True, trainable=False, 
                      weights=[embedding_matrix], name="Embedding_layer")
emb = emb_layer(input2)

LSTM1 = LSTM(units=256, activation='tanh', recurrent_activation='sigmoid', use_bias=True, 
            kernel_initializer=tf.keras.initializers.glorot_uniform(seed=23),
            recurrent_initializer=tf.keras.initializers.orthogonal(seed=7),
            bias_initializer=tf.keras.initializers.zeros(), return_sequences=True, return_state=True, name="LSTM1")
lstm_output, h_state, c_state = LSTM1(emb)

LSTM2 = LSTM(units=256, activation='tanh', recurrent_activation='sigmoid', use_bias=True, 
            kernel_initializer=tf.keras.initializers.glorot_uniform(seed=23),
            recurrent_initializer=tf.keras.initializers.orthogonal(seed=7),
            bias_initializer=tf.keras.initializers.zeros(), return_sequences=True, return_state=True, name="LSTM2")

lstm_output, h_state, c_state = LSTM2(lstm_output)

dropout1 = Dropout(0.5)(lstm_output)

attention_layer = tf.keras.layers.AdditiveAttention(name='Attention')
attention_output = attention_layer([dense1, dropout1], training=True)

dense_glob = tf.keras.layers.GlobalAveragePooling1D()(dense1)
att_glob = tf.keras.layers.GlobalAveragePooling1D()(attention_output)

concat = Concatenate()([dense_glob, att_glob])
dropout2 = Dropout(0.5)(concat)
FC1 = Dense(256, activation='relu', kernel_initializer=tf.keras.initializers.he_normal(seed = 56), name='fc1')
fc1 = FC1(dropout2)
OUTPUT_LAYER = Dense(vocab_size, activation='softmax', name='Output_Layer')
output = OUTPUT_LAYER(fc1)

attention_model = Model(inputs=[input1, input2], outputs = output)

该模型相似于咱们之前看到的编解码器模型,但有留神组件和一些小的更新。如果你违心,你能够尝试本人的扭转,它们可能会产生更好的后果。

模型架构

模型摘要

9.1 训练

训练步骤将与咱们的编解码器模型完全相同。咱们将应用雷同的“convert”函数生成批处理,从而取得逐字输入输出序列,并应用 train_on_batch 对其进行训练。

与编解码器模型相比,注意力模型须要更多的内存和计算能力。因而,你可能须要减小这个 batch 的大小。全过程请参考编解码器模型的训练局部。

为了留神机制,应用了 adam 优化器,学习率为 0.0001。这个模型被训练了 20 个 epoch。因为随机性,你失去的后果可能会有所不同。

所有代码都能够从我的 GitHub 拜访。它的链接曾经在这个博客的开端提供了。

9.2 推理

与之前中一样,咱们将从模型中拆散编码器和解码器局部。

# 编码器
encoder_input = attention_model.input[0]
encoder_output = attention_model.get_layer('dense_encoder').output
encoder_model = Model(encoder_input, encoder_output)

# 有注意力机制的解码器
text_input = attention_model.input[1]
cnn_input = Input(shape=(49,256))
lstm, h_s, c_s = attention_model.get_layer('LSTM2').output
att = attention_layer([cnn_input, lstm])
d_g = tf.keras.layers.GlobalAveragePooling1D()(cnn_input)
a_g = tf.keras.layers.GlobalAveragePooling1D()(att)
con = Concatenate()([d_g, a_g])
fc_1 = FC1(con)
out = OUTPUT_LAYER(fc_1)
decoder_model = Model([cnn_input, text_input], out)

这为咱们节俭了一些测试工夫。

9.3 贪心搜寻

当初,咱们曾经构建了模型,让咱们查看取得的 BLEU 分数是否的确比以前的模型有所改进:

咱们能够看出它比贪心搜寻的编解码模型有更好的性能。因而,它相对是比前一个改良。

9.4 束搜寻

当初让咱们看看束搜寻的一些分数:

BLEU 得分低于贪心算法,但差距并不大。但值得注意的是,随着束宽度的减少,分数实际上在减少。因而,可能存在束宽度的某个值,其中分数与贪心算法的分数穿插。

9.5 示例

以下是模型应用贪心搜寻生成的一些报告:

图像对 1 的原始报告:“心脏大小和肺血管在失常范畴内。未发现局灶性浸润性气胸胸腔积液

图像对 1 的预测报告:“心脏大小和纵隔轮廓在失常范畴内。肺是洁净的。没有气胸胸腔积液。没有急性骨性发现。”

这些预测与最后的报告简直类似。

图像对 2 的原始报告:“心脏大小和肺血管在失常范畴内呈现。肺为游离灶性空域病变。未见胸腔积液气胸

图像对 2 的预测报告:“心脏大小和肺血管在失常范畴内呈现。肺为游离灶性空域病变。未见胸腔积液气胸

预测的报告齐全一样!!

图像对 3 的原始报告:“心脏失常大小。纵隔不显著。肺部很洁净。”

图像对 3 的预测报告:“心脏失常大小。纵隔不显著。肺部很洁净。”

在这个例子中,模型也做得很好。

图像对 4 的原始报告:“双侧肺清晰。明确无病灶实变气胸胸腔积液。心肺纵隔轮廓不显著。可见骨构造胸部无急性异样

图像对 4 的预测报告:“心脏大小和纵隔轮廓在失常范畴内。肺是洁净的。没有气胸胸腔积液

你能够看到这个预测并不真正令人信服。

“然而,这个例子的束搜寻预测的是完全相同的报告,即便它产生的 BLEU 分数比整个测试数据的总和要低!!!”

那么,抉择哪一个呢?好吧,这取决于咱们。只需抉择一个通用性好的办法。

在这里,即便咱们的注意力模型也不能精确地预测每一幅图像。如果咱们查看原始报告中的单词,则会发现一些简单的单词,通过一些 EDA 能够发现它并不经常出现。这些可能是咱们在某些状况下没有很好的预测的一些起因。

请记住,咱们只是在 2560 个数据点上训练这个模型。为了学习更简单的特色,模型须要更多的数据。

10. 摘要

当初咱们曾经完结了这个我的项目,让咱们总结一下咱们所做的:

  • 咱们刚刚看到了图像字幕在医学畛域的利用。咱们了解这个问题,也了解这种利用的必要性。
  • 咱们理解了如何为输出管道应用数据生成器。
  • 创立了一个编解码器模型,给了咱们不错的后果。
  • 通过建设一个留神模型来改良根本后果。

11. 今后的工作

正如咱们提到的,咱们没有大的数据集来实现这个工作。较大的数据集将产生更好的后果。

没有对任何模型进行超参数调整。因而,一个更好的超参数调整可能会产生更好的后果。

利用一些更先进的技术,如 transformers 或 Bert,可能会产生更好的后果。

12. 援用

  1. https://www.appliedaicourse.com/
  2. https://arxiv.org/abs/1502.03044
  3. https://www.aclweb.org/anthol…
  4. https://arxiv.org/abs/1703.09137
  5. https://arxiv.org/abs/1409.0473
  6. https://machinelearningmaster…

这个我的项目的整个代码能够从我的 GitHub 拜访:https://github.com/vysakh10/I…

原文链接:https://towardsdatascience.co…

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

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

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

正文完
 0