作者|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 ETimg = []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 Tokenizertokenizer = 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 densenetchex = densenet.DenseNet121(include_top=False, weights = None,   input_shape=(224,224,3), pooling="avg")X = chex.outputX = 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 XXnet_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, reportdef 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 datasettrain_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').outputencoder_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').outputadd1 = 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_jtsscore = 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').outputencoder_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').outputatt = 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/