作者|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/
轉載請註明出處,本文鏈接:https://www.uj5u.com/qita/710.html
標籤:其他
上一篇:遷移學習概論
下一篇:如何使用Q-Q圖驗證資料的分布
