利用深度學習生成醫療報告

做者|Vysakh Nair
編譯|VK
來源|Towards Data Sciencepython

目錄

  1. 瞭解問題git

  2. 要求技能github

  3. 數據web

  4. 獲取結構化數據算法

  5. 準備文本數據-天然語言處理編程

  6. 獲取圖像特徵-遷移學習api

  7. 輸入管道-數據生成器網絡

  8. 編-解碼器模型-訓練,貪婪搜索,束搜索,BLEU架構

  9. 注意機制-訓練,貪婪搜索,束搜索,BLEUapp

  10. 摘要

  11. 將來工做

  12. 引用

1.瞭解問題

圖像字幕是一個具備挑戰性的人工智能問題,它是指根據圖像內容從圖像中生成文本描述的過程。例如,請看下圖:

一個常見的答案是「一個彈吉他的女人」。做爲人類,咱們能夠用適當的語言,看着一幅圖畫,描述其中的一切。這很簡單。我再給你看一個:

好吧,你怎麼形容這個?

對於咱們全部的「非放射科醫生」,一個常見的答案是「胸部x光」。

對於放射科醫生,他們撰寫文本報告,敘述在影像學檢查中身體各個部位的檢查結果,特別是每一個部位是正常、異常仍是潛在異常。他們能夠從一張這樣的圖像中得到有價值的信息並作出醫療報告。

對於經驗不足的放射科醫生和病理學家,尤爲是那些在醫療質量相對較低的農村地區工做的人來講,撰寫醫學影像報告是很困難的,而另外一方面,對於有經驗的放射科醫生和病理學家來講,寫成像報告多是乏味和耗時的。

因此,爲了解決全部這些問題,若是一臺計算機能夠像上面這樣的胸部x光片做爲輸入,並像放射科醫生那樣以文本形式輸出結果,那豈不是很棒?

2.基本技能

本文假設你對神經網絡、cnn、RNNs、遷移學習、Python編程和Keras庫等主題有必定的瞭解。下面提到的兩個模型將用於咱們的問題,稍後將在本博客中簡要解釋:

  1. 編解碼器模型

  2. 注意機制

對它們有足夠的瞭解會幫助你更好地理解模型。

3.數據

你能夠從如下連接獲取此問題所需的數據:

圖像數據集包含一我的的多個胸部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.獲取圖像特徵

圖像和部分報告是咱們模型的輸入。咱們須要將每一個圖像轉換成一個固定大小的向量,而後將其做爲輸入傳遞到模型中。爲此,咱們將使用遷移學習。

「在遷移學習中,咱們首先在基本數據集和任務上訓練基礎網絡,而後咱們將學習到的特徵從新指定用途,或將其轉移到第二個目標網絡,以便在目標數據集和任務上進行訓練。若是特徵是通用的,也就是說既適合基本任務也適合目標任務,而不是特定於基本任務,那此過程將趨於有效。」

VGG1六、VGG19或InceptionV3是用於遷移學習的常見cnn。這些都是在像Imagenets這樣的數據集上訓練的,這些數據集的圖像與胸部x光徹底不一樣。因此從邏輯上講,他們彷佛不是咱們任務的好選擇。那麼咱們應該使用哪一種網絡來解決咱們的問題呢?

若是你不熟悉,讓我介紹你認識CheXNet。CheXNet是一個121層的卷積神經網絡,訓練於胸片X射線14上,目前是最大的公開胸片X射線數據集,包含10萬多張正面視圖的14種疾病的X射線圖像。然而,咱們在這裏的目的不是對圖像進行分類,而是獲取每一個圖像的特徵。所以,不須要該網絡的最後一個分類層。

你能夠從這裏下載CheXNet的訓練權重:https://drive.google.com/file/d/19BllaOvs2x5PLV_vlWMy4i8LapLb2j6b/view。

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://machinelearningmastery.com/calculate-bleu-score-for-text-python/

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/api_docs/python/tf/keras/layers/AdditiveAttention

這個模型的文本輸入將保持不變,可是對於圖像特徵,此次咱們將從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/anthology/P18-1240/
  4. https://arxiv.org/abs/1703.09137
  5. https://arxiv.org/abs/1409.0473
  6. https://machinelearningmastery.com/develop-a-deep-learning-caption-generation-model-in-python/

這個項目的整個代碼能夠從個人GitHub訪問:https://github.com/vysakh10/Image-Captioning

原文連接:https://towardsdatascience.com/image-captioning-using-deep-learning-fe0d929cf337

歡迎關注磐創AI博客站:
http://panchuang.net/

sklearn機器學習中文官方文檔:
http://sklearn123.com/

歡迎關注磐創博客資源彙總站:
http://docs.panchuang.net/

相關文章
相關標籤/搜索