作者|Vysakh Nair
編譯|VK
來源|Towards Data Science
目錄
-
了解問題
-
要求技能
-
數據
-
獲取結構化數據
-
准備文本數據-自然語言處理
-
獲取圖像特征-遷移學習
-
輸入管道-數據生成器
-
編-解碼器模型-訓練,貪婪搜索,束搜索,BLEU
-
注意機制-訓練,貪婪搜索,束搜索,BLEU
-
摘要
-
未來工作
-
引用
1.了解問題
圖像字幕是一個具有挑戰性的人工智能問題,它是指根據圖像內容從圖像中生成文本描述的過程。例如,請看下圖:
一個常見的答案是“一個彈吉他的女人”。作為人類,我們可以用適當的語言,看着一幅圖畫,描述其中的一切。這很簡單。我再給你看一個:
好吧,你怎么形容這個?
對於我們所有的“非放射科醫生”,一個常見的答案是“胸部x光”。
對於放射科醫生,他們撰寫文本報告,敘述在影像學檢查中身體各個部位的檢查結果,特別是每個部位是正常、異常還是潛在異常。他們可以從一張這樣的圖像中獲得有價值的信息並做出醫療報告。
對於經驗不足的放射科醫生和病理學家,尤其是那些在醫療質量相對較低的農村地區工作的人來說,撰寫醫學影像報告是很困難的,而另一方面,對於有經驗的放射科醫生和病理學家來說,寫成像報告可能是乏味和耗時的。
所以,為了解決所有這些問題,如果一台計算機可以像上面這樣的胸部x光片作為輸入,並像放射科醫生那樣以文本形式輸出結果,那豈不是很棒?
2.基本技能
本文假設你對神經網絡、cnn、RNNs、遷移學習、Python編程和Keras庫等主題有一定的了解。下面提到的兩個模型將用於我們的問題,稍后將在本博客中簡要解釋:
-
編解碼器模型
-
注意機制
對它們有足夠的了解會幫助你更好地理解模型。
3.數據
你可以從以下鏈接獲取此問題所需的數據:
- 圖像-包含所有的胸部X光片:http://academictorrents.com/details/5a3a439df24931f410fac269b87b050203d9467d
- 報告-包含上述圖像的相應報告:http://academictorrents.com/details/66450ba52ba3f83fbf82ef9c91f2bde0e845aba9
圖像數據集包含一個人的多個胸部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文件中獲得結果后,在我們將其輸入模型之前,應該對它們進行適當的清理和准備。下面的圖片展示了幾個例子,展示了清洗前的發現是什么樣子。
我們將按以下方式清理文本:
-
將所有字符轉換為小寫。
-
執行基本的解壓,即將won’t、can’t等詞分別轉換為will not、can not等。
-
刪除文本中的標點符號。注意,句號不會被刪除,因為結果包含多個句子,所以我們需要模型通過識別句子以類似的方式生成報告。
-
從文本中刪除所有數字。
-
刪除長度小於或等於2的所有單詞。例如,“is”、“to”等被刪除。這些詞不能提供太多信息。但是“no”這個詞不會被刪除,因為它增加了語義信息。在句子中加上“no”會完全改變它的意思。所以我們在執行這些清理步驟時必須小心。你需要確定哪些詞應該保留,哪些詞應該避免。
-
還發現一些文本包含多個句號或空格,或“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/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進行了整體編碼之后,圖像被引入到語言模型中。這是一種后期綁定體系結構,它不會隨每個時間步修改圖像表示。
他的論文中的一些重要結論被用於我們實現的體系結構中。他們是:
-
RNN輸出需要正則化,並帶有丟失。
-
圖像向量不應該有一個非線性的激活函數,或者使用dropout進行正則化。
-
從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 貪婪搜索算法
貪婪搜索是一種算法范式,它逐塊構建解決方案,每次總是選擇最好的。
貪婪搜索步驟:
-
編碼器輸出圖像的特征。編碼器的工作到此結束。一旦我們有了我們需要的特征,我們就不需要關注編碼器了。
-
這個特征向量和起始標識“startseq”(我們的初始輸入序列)被作為解碼器的第一個輸入。
-
譯碼器預測整個詞匯表的概率分布,概率最大的單詞將被選為下一個單詞。
-
這個預測得到的單詞和前一個輸入序列將是我們下一個輸入序列,並且傳遞到解碼器。
-
繼續執行步驟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.引用
- https://www.appliedaicourse.com/
- https://arxiv.org/abs/1502.03044
- https://www.aclweb.org/anthology/P18-1240/
- https://arxiv.org/abs/1703.09137
- https://arxiv.org/abs/1409.0473
- 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/