最近のディープ ニューラル ネットワークの開発以前は、業界で最も優秀な人材でもこの問題を解決できませんでしたが、ディープ ニューラル ネットワークの登場後は、必要なデータセットがあれば解決することが完全に可能になりました。 たとえば、ネットワーク モデルは、以下の画像に関連するキャプションを生成できます。つまり、「草地にいる白い犬」、「茶色の斑点のある白い犬」、または「草地とピンクの花の上にいる犬」などです。 [[395666]]
データセット選択したデータセットは「Flickr 8k」です。 このデータを選んだのは、簡単にアクセスでき、通常の PC でトレーニングするのに最適なサイズであり、適切なキャプションを生成するようにネットワークをトレーニングするのに十分な大きさであるためです。 データは、主に 6k 枚の画像を含むトレーニング セット、1k 枚の画像を含む開発セット、および 1k 枚の画像を含むテスト セットの 3 つのセットに分かれています。 各画像には 5 つのキャプションが含まれています。 例の1つは次のとおりです。
ピンクのドレスを着た子供が玄関の階段を登っています。 木造の建物に入っていく女の子。 木製のプレイハウスに登る小さな女の子。 小さな女の子が遊び場へ向かって階段を登っています。 ピンクのドレスを着た小さな女の子が木造の小屋に入っていきます。 データクリーニングあらゆる機械学習プログラムにおける最初の、そして最も重要なステップは、データをクリーンアップし、不要なデータをすべて削除することです。タイトルのテキスト データを処理する際には、コンピューター内ですべての文字を小文字に変換して「Hey」と「hey」が完全に異なる単語になるようにする、*、(、£、$、% などの特殊記号や句読点を削除する、数字を含むすべての単語を排除するなどの基本的なクリーニング手順を実行します。 まず、データセット内のすべての一意のコンテンツの語彙を作成します。つまり、8000 (画像数) * 5 (画像あたりのキャプション) = 40000 キャプションです。 8763 に等しいことがわかります。しかし、これらの単語のほとんどは 1 回か 2 回しか出現しないため、モデルが外れ値に対して堅牢にならないため、モデルにこれらの単語を含める必要はありません。したがって、語彙に含める単語の最小出現回数を 10 というしきい値に設定しました。これは、1652 個の固有単語に相当します。 もう 1 つ行うことは、各説明に 2 つのマーカーを追加して、字幕の開始と終了を示すことです。 2 つのマーカーは「startseq」と「endseq」で、それぞれ字幕の開始と終了を示します。 まず、必要なライブラリをすべてインポートします。 - numpyをnpとしてインポートする
- numpyから配列をインポート
- pandasをpdとしてインポートする
- matplotlib.pyplot をpltとしてインポートします。
- インポート文字列
- インポートOS
- PIL インポート画像から
- インポートグロブ
- 輸入ピクルス
- から 時間インポート時間
- keras.preprocessing インポートシーケンスから
- keras.modelsからSequentialをインポートする
- keras.layersからLSTM、埋め込み、高密度、平坦化、再形成、連結、ドロップアウトをインポートします
- keras.optimizersからAdamをインポートする
- keras.layers.mergeからインポートを追加
- keras.applications.inception_v3からInceptionV3 をインポートします
- keras.preprocessingから画像をインポート
- keras.modelsからモデルをインポート
- kerasから入力、レイヤーをインポート
- keras.applications.inception_v3からpreprocess_input をインポートします
- keras.preprocessing.sequenceからpad_sequencesをインポートします
- keras.utilsからto_categoricalをインポートする
いくつかのヘルパー関数を定義しましょう: - #ロードの説明
- def load_doc(ファイル名):
- file = open ( ファイル名, 'r' )
- テキスト =ファイル.read ()
- ファイルを閉じる()
- テキストを返す
-
-
- def load_descriptions(doc):
- マッピング = dict()
- doc.split( '\n' )内の行の場合:
- トークン = line.split()
- len(行) < 2の場合:
- 続く
- image_id、image_desc = トークン[0]、トークン[1:]
- image_id = image_id.split( '.' )[0]
- image_desc = ' ' . join (image_desc)
- image_idが マッピング:
- マッピング[image_id] = list()
- マッピング[image_id].append(image_desc)
- リターンマッピング
-
- def clean_descriptions(説明):
- テーブル= str.maketrans( '' , '' , 文字列.句読点)
- のために descriptions.items()内のkey 、 desc_list :
- i が範囲(len(desc_list))内にある場合:
- desc = desc_list[i]
- desc = desc.split ()
- desc = [word.lower ( )単語の [説明]
- desc = [w.translate(テーブル) for w in [説明]
- desc = [逐語的に 長さ(単語)>1の場合は降順]
- desc = [逐語的に desc if word.isalpha()]
- desc_list[i] = ' ' . join ( desc )
-
- 返品説明
-
- # 1行に1つずつ説明をファイルに保存します
- def save_descriptions(説明、ファイル名):
- 行 = リスト()
- のために descriptions.items()内のkey 、 desc_list :
- のために 説明 desc_list内:
- 行の追加(キー+ ' ' + desc )
- データ = '\n' . join ( 行 )
- file = open ( ファイル名, 'w' )
- ファイルへの書き込み(データ)
- ファイルを閉じる()
-
-
- #クリーンな説明をメモリにロードする
- def load_clean_descriptions(ファイル名, データセット):
- doc = load_doc(ファイル名)
- 説明 = dict()
- doc.split( '\n' )内の行の場合:
- トークン = line.split()
- image_id、image_desc = トークン[0]、トークン[1:]
- データセットにimage_idがある場合:
- image_idが 説明文:
- 説明[画像ID] = リスト()
- desc = 'startseq ' + ' ' . join (image_desc) + ' endseq'
- 説明[画像ID].append( desc )
- 返品説明
-
- def load_set(ファイル名):
- doc = load_doc(ファイル名)
- データセット = リスト()
- doc.split( '\n' )内の行の場合:
- len(行) < 1の場合:
- 続く
- 識別子 = 行.split( '.' )[0]
- データセット.append(識別子)
- 戻る セット(データセット)
-
- #トレーニングデータセットをロードする
-
-
- ファイル名 = "dataset/Flickr8k_text/Flickr8k.token.txt"
- doc = load_doc(ファイル名)
- 説明 = load_descriptions(doc)
- 説明 = clean_descriptions(説明)
- save_descriptions(説明、 'descriptions.txt' )
- ファイル名 = 'dataset/Flickr8k_text/Flickr_8k.trainImages.txt'
- トレーニング = load_set(ファイル名)
- train_descriptions = load_clean_descriptions( 'descriptions.txt' 、 トレイン)
一つずつ説明しましょう: load_doc: ファイルのパスを取得し、ファイルの内容を返します load_descriptions: 説明を含むファイルの内容を取得し、画像 ID をキーとして、説明を値として持つ辞書を生成します。 clean_descriptions: すべての文字を小文字に変換し、数字と句読点、および1文字のみの単語を無視して説明をクリーンアップします。 save_descriptions: 説明辞書をテキストファイルとしてメモリに保存します load_set: テキストファイルから画像の一意の識別子をすべて読み込みます load_clean_descriptions: 上記で抽出した一意の識別子を使用して、クリーンアップされたすべての説明をロードします。 データ前処理次に、画像とキャプションに対してデータの前処理を行います。 画像は基本的に特徴ベクトルであり、ネットワークへの入力となります。 したがって、ニューラル ネットワークに渡す前に、それらを固定サイズのベクトルに変換する必要があります。 この目的のために、転移学習にはGoogle Research [3]が作成したInception V3モデル(畳み込みニューラルネットワーク)を使用しました。 このモデルは「ImageNet」データセット[4]でトレーニングされており、1000枚の画像を分類することができますが、私たちの目標は分類ではないため、最後のソフトマックス層を削除し、以下に示すように各画像に対して2048個の固定ベクトルを抽出しました。
タイトル テキストはモデルの出力、つまり予測対象です。 しかし、予測は一度に行われるわけではなく、字幕が単語ごとに予測されます。 これを行うには、各単語を固定サイズのベクトルにエンコードする必要があります (これは次のセクションで行います)。 これを行うには、まず、各単語をインデックス (この場合は 1 ~ 1652) にマッピングする「単語からインデックス」辞書と、各インデックスを対応する単語辞書にマッピングする「インデックスから単語」辞書の 2 つの辞書を作成する必要があります。 最後に、データセット内で最も長い説明の長さを計算して、他のすべてを埋め込んで固定長を維持できるようにします。 この場合、この長さは 34 になります。 単語埋め込み前述したように、各単語を固定サイズ (つまり 200) のベクトルにマッピングし、事前トレーニング済みの GLOVE モデルを使用します。 最後に、語彙内の各単語の固定サイズのベクトルを含む、語彙内の 1652 語すべての埋め込み行列を作成します。 - #リストを作成する すべてのトレーニングキャプション
- すべての列車のキャプション = []
- のために train_descriptions.items()のキー、値:
- valのcapの場合:
- all_train_captions.append(キャップ)
-
-
- #コーパス内で少なくとも10回出現する単語のみを考慮する
- 単語数しきい値 = 10
- 単語数 = {}
- nsents = 0
- all_train_captionsで送信:
- nsents += 1
- sent.split( ' ' )内のwの場合:
- word_counts[w] = word_counts.get(w, 0) + 1
-
- vocab = [w for w in word_counts if word_counts[w] >= word_count_threshold]
- print( '前処理された単語 {} -> {}' .format(len(word_counts), len(vocab)))
-
-
- ixtoword = {}
- ワードトイクス = {}
-
- ix = 1
- 語彙のwについて:
- 単語toix[w] = ix
- ixtoword[ix] = w
- ix += 1
-
- vocab_size = len(ixtoword) + 1 # 0が追加された分だけ
-
- #グローブベクトルをロードする
- グローブ_dir = 'グローブ.6B'
- 埋め込みインデックス = {}
- f = open (os.path.join ( glove_dir 、 'glove.6B.200d.txt' )、エンコーディング = "utf-8" )
-
- fの行の場合:
- 値= line.split()
- 単語 =値[0]
- coefs = np.asarray(値[1:], dtype = 'float32' )
- 埋め込みインデックス[単語] = 係数
- f.close ()関数
-
- 埋め込み次元 = 200
-
- #それぞれの単語について200次元の密ベクトルを取得する 語彙力不足
- 埋め込み行列 = np.zeros((vocab_size, 埋め込み次元))
-
- wordtoix.items()のword, i の場合:
- 埋め込みベクトル = 埋め込みインデックス.get(単語)
- 埋め込みベクトルが なしではない:
- 埋め込み行列[i] = 埋め込みベクトル
-
次のコードを見てみましょう: 1行目から5行目: すべてのトレーニング画像の説明をリストに抽出します 9-18行目: 語彙に10回以上出現する単語のみを選択します 21~30 行目: インデックスを作成する単語と単語辞書のインデックスを作成します。 33~42行目: 単語をキー、ベクトル埋め込みを値として、グローブ埋め込みを辞書に読み込みます。 44~52行目: 上記で読み込んだ埋め込みを使用して、語彙内の単語の埋め込み行列を作成します。 データ準備これはプロジェクトの最も重要な側面の 1 つです。 画像については、前述したように Inception V3 モデルを使用して固定サイズのベクトルに変換する必要があります。 - # 以下のパスには すべての画像
- all_images_path = 'dataset/Flickr8k_Dataset/Flicker8k_Dataset/'
- #リストを作成する ディレクトリ内のすべての画像名
- all_images = glob.glob(all_images_path + '*.jpg' )
-
- #リストを作成する すべてのトレーニングおよびテスト画像とそのフルパス名
- def create_list_of_images(ファイルパス):
- images_names = set ( open (file_path, 'r' ) .read ().strip().split( '\n' ))
- 画像 = []
-
- all_images内の画像の場合:
- image_namesにimage[len(all_images_path):] が含まれている場合:
- images.append(画像)
-
- 画像を返す
-
-
- train_images_path = 'dataset/Flickr8k_text/Flickr_8k.trainImages.txt'
- test_images_path = 'dataset/Flickr8k_text/Flickr_8k.testImages.txt'
-
- train_images = 画像のリストを作成します(train_images_path)
- test_images = 画像のリストを作成します(test_images_path)
-
- #画像の前処理
- def preprocess(image_path):
- img = image.load_img(image_path, target_size=(299, 299))
- x = image.img_to_array(画像)
- x = np.expand_dims(x, 軸=0)
- x = 前処理入力(x)
- xを返す
-
- # Inception v3モデルをロードする
- モデル = InceptionV3(重み = 'imagenet' )
-
- # Inception v3から最後のレイヤー (出力レイヤー)を削除して新しいモデルを作成します。
- model_new = モデル(model.input、model.layers[-2] .output )
-
- # 与えられた画像をベクトルにエンコードする サイズ(2048, )
- def encode(画像):
- 画像 = 前処理(画像)
- fea_vec = model_new.predict(画像)
- fea_vec = np.reshape(fea_vec, fea_vec.shape[1])
- fea_vecを返す
-
-
- エンコードトレイン = {}
- train_imagesのimgの場合:
- encoding_train[img[len(all_images_path):]] = encode(img)
-
-
- エンコードテスト = {}
- test_imagesのimgの場合:
- encoding_test[img[len(all_images_path):]] = encode(img)
-
- # ボトルネック機能をディスクに保存する
- と open ( "encoded_files/encoded_train_images.pkl" 、 "wb" )をencoded_pickleとして実行します:
- pickle.dump(encoding_train、encoded_pickle) の形式
-
- と open ( "encoded_files/encoded_test_images.pkl" 、 "wb" )をencoded_pickleとして実行します:
- pickle.dump(encoding_test、encoded_pickle) の形式
-
-
- train_features =ロード( open ( "encoded_files/encoded_train_images.pkl" 、 "rb" ))
- 1-22行目: トレーニング画像とテスト画像へのパスを別々のリストに読み込みます。
- 25~53 行目: トレーニング セットとテスト セットの各画像をループし、固定サイズで読み込み、前処理し、InceptionV3 モデルを使用して特徴を抽出し、最後に形状を変更します。
- 56~63行目: 抽出した特徴をディスクに保存する
ここで、画像をコンピューターに入力してテキストを生成するように要求するだけではないことから、キャプション テキストをすべて一度に予測するわけではありません。 私たちがしなければならないのは、画像の特徴ベクトルとキャプションの最初の単語を与えて、2 番目の単語を予測するように依頼することだけです。 次に、最初の 2 つの単語を与えて、3 番目の単語を予測するように依頼します。 データセットセクションに示されている画像と「少女が木造の建物に入っている」というキャプションを考えてみましょう。 この場合、トークン「startseq」と「endseq」を追加した後の入力(Xi)と出力(Yi)はそれぞれ次のようになります。
この後、作成した「インデックス」辞書を使用して、入力と出力の各単語を変更し、インデックスをマッピングします。 バッチ処理では、すべてのシーケンスの長さを等しくする必要があるため、最大長 (上記のように 34 と計算) に達するまで各シーケンスに 0 を追加します。 ご覧のとおり、これは大量のデータであり、一度にメモリにロードすることは不可能です。そのため、データ ジェネレーターを使用してデータを小さなチャンクに分割してロードし、メモリ使用量を削減します。 - # データジェネレーター。model.fit_generator()の呼び出しで使用することを目的としています。
- def data_generator(説明、写真、wordtoix、最大長さ、バッチあたりの写真数):
- X1、X2、y = リスト()、リスト()、リスト()
- 0件
- #画像を永遠にループする
- 一方1:
- のために descriptions.items()内のkey 、 desc_list :
- 1+=1 です
- # 写真機能を取得する
- photo = 写真[キー+ '.jpg' ]
- のために 説明 desc_list内:
- #シーケンスをエンコードする
- seq = [wordtoix[word] 単語の単語の desc .split( ' ' ) 単語がwordtoix内にある場合]
- # 1つのシーケンスを分割 複数のX、Yペアに
- iが範囲(1, len(seq))内にある場合:
- #入力に分割し、 出力ペア
- in_seq、out_seq = seq[:i]、seq[i]
- # パッド入力シーケンス
- in_seq = pad_sequences([in_seq], maxlen=最大長さ)[0]
- #エンコード出力 順序
- out_seq = to_categorical([out_seq], num_classes=vocab_size)[0]
- #店
- X1.append(写真)
- X2.append(in_seq)
- y.append(out_seq)
- # バッチデータを生成する
- n==バッチあたりの写真数の場合:
- [[配列(X1), 配列(X2)], 配列(y)] を生成します。
- X1、X2、y = リスト()、リスト()、リスト()
- 0件
上記のコードは、すべての画像と説明をループし、テーブルにデータ項目を生成します。 yieldは同じ行から関数を再度実行します。そのため、データをバッチでロードしましょう。 モデルアーキテクチャとトレーニング前述したように、モデルには各ポイントに 2 つの入力があり、1 つは特徴画像ベクトル用、もう 1 つは部分テキスト用です。 まず、画像ベクトルに 0.5 のドロップアウトを適用し、それを 256 ニューロン層と連結します。 テキストの一部については、まずそれを埋め込みレイヤーに接続し、上記のように GLOVE でトレーニングされた埋め込みマトリックスの重みを使用します。 次に、Dropout 0.5 と LSTM (Long Short-Term Memory) を適用します。 最後に、両方の方法を組み合わせて 256 ニューロン レイヤーに接続し、最後に語彙内の各単語の確率を予測するソフトマックス レイヤーに接続します。 高レベルのアーキテクチャは、次の図を使用して要約できます。 トレーニング中に選択されたハイパーパラメータは次のとおりです。損失は「カテゴリ損失エントロピー」として選択され、オプティマイザーは「Adam」です。 モデルは合計 30 エポックにわたってトレーニングされましたが、最初の 20 エポックではバッチ サイズと学習率はそれぞれ 0.001 と 3 であり、次の 10 エポックではバッチ サイズと学習率はそれぞれ 0.0001 と 6 でした。 - 入力1 = 入力(形状=(2048,))
- fe1 = ドロップアウト(0.5)(入力1)
- fe2 = 高密度(256、アクティベーション= 'relu' )(fe1)
- 入力2 = 入力(形状=(最大長さ1,))
- se1 = 埋め込み(vocab_size、embedding_dim、mask_zero= True )(入力2)
- se2 = ドロップアウト(0.5)(se1)
- se3 = LSTM(256)(se2)
- デコーダー1 = ([fe2, se3])を追加します
- デコーダー2 = Dense(256, アクティベーション= 'relu' )(デコーダー1)
- 出力 = Dense(vocab_size, activation= 'softmax' )(decoder2)
- モデル = モデル(入力=[入力1, 入力2], 出力=出力)
-
- model.layers[2].set_weights([埋め込み行列])
- model.layers[2].trainable = False
-
- モデルをコンパイルします(損失 = 'categorical_crossentropy' 、オプティマイザー = 'adam' )
-
- エポック = 20
- バッチあたりの写真数 = 3
- steps = len(train_descriptions) // バッチあたりの画像数
-
- ジェネレータ = data_generator(train_descriptions、train_features、wordtoix、max_length1、number_pics_per_batch)
- 履歴 = model.fit_generator(ジェネレータ、エポック=20、エポックごとのステップ数=ステップ、詳細=1)
-
-
- モデル.オプティマイザー.lr = 0.0001
- エポック = 10
- バッチあたりの写真数 = 6
- steps = len(train_descriptions) // バッチあたりの画像数
-
- ジェネレータ = data_generator(train_descriptions、train_features、wordtoix、max_length1、number_pics_per_batch)
- history1 = model.fit_generator(ジェネレータ、エポック=10、エポックごとのステップ数=ステップ、詳細=1)
- model.save( 'saved_model/model_' + str(30) + '.h5' )
コードを説明しましょう: 1~11行目: モデルアーキテクチャを定義する 13 行目~ 14 行目: 埋め込み層の重みを上記で作成した埋め込み行列に設定し、さらに trainable = False に設定して、層がこれ以上トレーニングされないようにします。 16~33行目: 上記のハイパーパラメータを使用して、2つの別々の間隔でモデルをトレーニングします。 推論以下は、最初の 20 エポックのトレーニング損失と、次の 10 エポックのトレーニング損失を示しています。
推論を行うには、モデル(つまり貪欲)に従って次の単語を最大確率で予測する関数を作成します。 - def greedySearch(写真):
- in_text = 'startseq'
- iが範囲内(max_length1)の場合:
- シーケンス= [wordtoix[w] 、 wがin_text.split()内にある場合、w が wordtoix内にある]
- シーケンス= pad_sequences([シーケンス], maxlen=max_length1)
- yhat = model.predict([写真、シーケンス]、詳細=0)
- yhat = np.argmax(yhat)
- 単語 = ixtoword[yhat]
- in_text += ' ' + 単語
- word == 'endseq'の場合:
- 壊す
- 最終 = in_text.split()
- 最終 = 最終[1:-1]
- final = ' ' . join (final)
- 最終返却
-
- 1 = 1
- pic = リスト(encoding_test.keys())[999]
- 画像 = encoding_test[pic].reshape((1,2048))
- x = plt.imread(画像+pic)
- plt.imshow(x)
- plt.show()
- print( "貪欲:" ,greedySearch(image))
効果はかなり良いです |