3
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

記事投稿キャンペーン 「AI、機械学習」

気象データから状況説明や注意事項の文を自動生成するTransformerを実装した

Last updated at Posted at 2023-11-21

0. 更新

生成文の改善(ネットワーク構造および学習データの与え方を変更)について 5.に記載しました。
文が生成される様子の動画を添付しました。

1. はじめに

 気象予報ではコンピュータシミュレーションの結果と観測状況から、今後想定される気象状況の推移、実際の注意事項などが重要な事項として各種メデイアやネットワークを通じて公表されていきます。
このような文章は、経験を積んだ気象庁の予報官や、民間の気象予報士が時間と闘いながら作成されているのだと思います。こういう文章をデータから機械学習によって直接作成するということを試みました。
現状ではまだプロの人間様に太刀打ちできるものではありませんが、方向性として手応えもありましたので、今回手法と初期の結果をまとめました。

実際に解説文が生成されていく様子です。

image

 これまでも、気象データを見て概況を説明する文章を自動生成させてみるという記事で、気象データを可視化した画像を元にその気象状況の解説文を自動生成する、というニューラルネットワークを作ったりしています。この時は状況を説明するだけの50〜60ワードの文章でした。今回はより長い500ワード程度の文が対象です。

具体的な文章として、短期予報解説資料と呼ばれる解説資料に含まれている文章を対象とします。このくらいの長さになってくるとこの時のShow and TellではLSTMの学習に時間がかかりすぎます。そこでネットワーク部分を並列計算が可能なTransformerに変更することで現実的な学習時間となりました。

入力となる画像は、気象観測とスーパーコンピュータが計算した結果を可視化した画像です。具体的には下のような、複数の気象要素を地図上に可視化したものです。

この画像を、気象データをもとに「天気図っぽい前線」を機械学習で描いてみるという私の別の取り組みで使用したニューラルネットワークに与えて、途中のレイヤーのデータ(画像特徴量)に変換します。

front detect
左:「天気図っぽい前線」を描くニューラルネットワークの生成画像
右:実際に気象庁が作成した前線など(Ground Truth)

この気象画像を分析するネットワークは、前線や台風・高低気圧の記号を生成するために、可視化画像から何らかの凝縮したデータ、つまり画像特徴量を作成していると考えられます。

そしてこの画像特徴量からTransformerによって短期予報解説資料の文章を生成するわけです。

2. 短期予報解説資料について

短期予報解説資料は1日に2度発表されます。
実物はこのようなものです。

Screenshot 2023-11-17 at 4.07.04 PM.png
(出典:気象庁ホームページ)

この解説資料では、1.実況上の着目点という節でその時の気象状況を説明し、2.主要じょう乱の予想根拠と解説上の留意点という節で、1.節で上げた事象が今後どのようになっていくか、注意すべきことは何であるか、といったことが述べられています。各節には①、②、...といった小項目があるという構造は毎回同じです。

今回のデータとしては、1.節から2.節までを対象として取り上げ、3.以降は無視します。
文章の単語長としては、500ワード前後となっています。

3. TensorflowでTransformerを実装

3.1 Tensorflowチュートリアルのサンプル実装

Tensorflowチュートリアルとして、写真を入力してキャプションを生成させるTransformerのサンプル(Image captioning with visual attention)が解説されています。
今回はこのサンプル実装を参考にしました。

チュートリアルの方は、カラー写真(RGBの3ch)を入力しますが、私の実装は7種類の可視化画像(7xRGB=21ch)を入力としています。そのほか、下記のような違いがあります。

# 項目 Tensorflowチュートリアル 気象解説生成(私のネットワーク)
1 画像データ カラー写真 3チャンネル
(Flickr8k)
気象庁全球予報モデルGPVの可視化画像
(7種類を結合して21チャンネルで入力)
2 キャプション
データ
Conceptual Captions 短期予報解説資料(節1.と節2.)
3 画像特徴量を
抽出するCNN
画像分類用ネットワーク
(MobileNetV3Small)
気象前線を生成する
Semantic Segmentation(独自)
4 学習時の
データ供給
tf.data.Dataset形式
(fitメソッドで供給)
Generatorを作成
(fitメソッドで供給)

最後の項目は少々細かい話ですが、Macシリコン版のTensorflowでtensorflow.data.Dataset形式を使用して学習を実行すると、徐々にメモリが増大してしまうという不具合があるらしく、途中でプロセスがkillされたりして計算ができなくなってしまうという問題があります。(例えばここ)。そこで、Tensorflowのチュートリアルにある実装を、Dataset形式を使わないように書き換えました。

3.2 全体の構造

全体的な処理とデータの流れは、解説文を処理する部分(言語処理側)と、画像を処理する部分(画像処理側)に分かれていて、画像処理側から画像特徴量が言語処理側に渡ってきます。

学習の時には、Transformerは、画像特徴量と解説文を入力とします。
そして入力の解説文を1ワードシフトした文が出力になるように学習させます。

「1ワードシフト」というのは、
入力:startseq 日本 の 南 に 熱帯低気圧 が
出力:日本 の 南 に 熱帯低気圧 が ある
というような入力・出力の関係になるように学習させます。

こうすると、入力の文(単語列)に対して、引き続く単語「ある」を予測したことになるわけです。

下図に、処理の流れを示します。

3.3 全体の処理

全体のプログラム構造を示します。
データ入力の範囲指定に係る部分は私のデータの独自範囲なので省略しました。

main

'''
(省略した処理)
 :指定した範囲の月日の画像、テキストのリストを作成)
'''

'''
画像処理部
'''
# 前線描画CNNの生成 (3.4)
cnn_p = IMG_NetGen.IMG_CNN_AE( 省略  ) 
model_new = cnn_p.ImgProcNet()

# 前線描画CNNで画像特徴量を取得し辞書型データとして保存 (3.4)
image_feature_dict = cnn_p.MakeFeatureDict(\
    model=model_new, 以降略   )
cnn_p.SaveDict(filename=image_feature_file)

'''
言語処理部
'''
# 解説文の分かち書き処理の定義、実行、辞書型データとして保存(3.5.1)
tp = TXT_Proc.TextPreprocessor( 省略 )
image_text_dict = tp.MakeTextsDics()
tp.SaveDict(filename=image_text_file)

'''
Transformerの生成・実行
'''
# 画像特徴量、テキストの辞書の読み込み(3.5.2)
image_feature_dict = load( open(image_feature_file, 'rb') )
image_text_dict    = load( open(image_text_file,    'rb') )

# Transformerネットワークの定義(3.5.3)

tr = IMG_Captng.Trainer( \
      features_dict=image_feature_dict, \
      texts_dict=image_text_dict, 以降略)

# 学習実行(3.5.3)
tr.TrainModel(SnT_model_file, SnT_model_input_file,\
                  SnT_weight_file, SnT_weight_input_file, 以降略)

# 予測用画像特徴量の取得と保存
image_feature_dict_prd = cnn_p.MakeFeatureDict(
 model=model_new, 以降略)
cnn_p.SaveDict(filename=image_feature_file_prd)

# 予測ネットワーク生成(3.6)
pr = TXT_Predict.Predictor( \
      model_file=model_file, weight_file=weight_file, \
      token_file=t_file , max_length=tr.GetMaxLength(), \
      image_feature_dict=image_feature_dict_prd
     )

# 予測実行(3.6)
pred_text_list =pr.do_simple_gen()

# 予測したテキストを出力
f = open(output_text_file, 'w')
for pair in pred_text_list:
	f.write
f.close()

以降の節で、上記の詳細を説明していきます。

3.4 画像処理部

前線描画CNNはあらかじめモデルと重みを保管してあります。
これらはload_modelおよびload_weightsを用いてNN_1として取り込みます。
その上で、特定のレイヤーを出力として新しくNN_2を定義します。

NN_2の定義
NN_2 = Model(
           inputs=NN_1.input, 
           outputs=NN_1.get_layer(self.lastlayer_name).output
        )

これが特徴量を抽出するネットワークとなります。

「特定の層」は、NN_1summary()の表示などで調べておきます。
今回のモデルは、conv2dtranspose層に入る直前のレイヤ(下記からconv2d_17)を指定しています。

NN_1.summary()の表示
NN_1.summary()

 Layer (type)                Output Shape                 Param #   Connected to                  
==================================================================================================
 input_1 (InputLayer)        [(None, 256, 256, 21)]       0         []                            
                                                                                                  
 conv2d (Conv2D)             (None, 256, 256, 24)         4560      ['input_1[0][0]']             
                                                                                                  
 input_2 (InputLayer)        [(None, 128, 128, 21)]       0         []                            
                                                                                                  
 conv2d_1 (Conv2D)           (None, 256, 256, 24)         5208      ['conv2d[0][0]']              
                                                                                                  
 conv2d_3 (Conv2D)           (None, 128, 128, 24)         4560      ['input_2[0][0]']             
                                                                                                  
 conv2d_2 (Conv2D)           (None, 128, 128, 24)         5208      ['conv2d_1[0][0]']            
                                                                                                  
 conv2d_4 (Conv2D)           (None, 128, 128, 24)         5208      ['conv2d_3[0][0]']            
                                                                                                  
 concatenate (Concatenate)   (None, 128, 128, 48)         0         ['conv2d_2[0][0]',            
                                                                     'conv2d_4[0][0]']            
                                                                                                  
 conv2d_5 (Conv2D)           (None, 128, 128, 36)         15588     ['concatenate[0][0]']         
                                                                                                  
 input_3 (InputLayer)        [(None, 64, 64, 21)]         0         []                            
                                                                                                  
 conv2d_6 (Conv2D)           (None, 128, 128, 36)         11700     ['conv2d_5[0][0]']            
                                                                                                  
 conv2d_8 (Conv2D)           (None, 64, 64, 36)           6840      ['input_3[0][0]']             
                                                                                                  
 conv2d_7 (Conv2D)           (None, 64, 64, 36)           11700     ['conv2d_6[0][0]']            
                                                                                                  
 conv2d_9 (Conv2D)           (None, 64, 64, 36)           11700     ['conv2d_8[0][0]']            
                                                                                                  
 concatenate_1 (Concatenate  (None, 64, 64, 72)           0         ['conv2d_7[0][0]',            
 )                                                                   'conv2d_9[0][0]']            
                                                                                                  
 conv2d_10 (Conv2D)          (None, 64, 64, 54)           35046     ['concatenate_1[0][0]']       
                                                                                                  
 conv2d_11 (Conv2D)          (None, 64, 64, 54)           26298     ['conv2d_10[0][0]']           
                                                                                                  
 conv2d_12 (Conv2D)          (None, 32, 32, 54)           26298     ['conv2d_11[0][0]']           
                                                                                                  
 conv2d_13 (Conv2D)          (None, 32, 32, 80)           38960     ['conv2d_12[0][0]']           
                                                                                                  
 conv2d_14 (Conv2D)          (None, 32, 32, 80)           57680     ['conv2d_13[0][0]']           
                                                                                                  
 conv2d_15 (Conv2D)          (None, 16, 16, 80)           57680     ['conv2d_14[0][0]']           
                                                                                                  
 conv2d_16 (Conv2D)          (None, 16, 16, 120)          86520     ['conv2d_15[0][0]']           
                                                                                                  
 conv2d_17 (Conv2D)          (None, 16, 16, 120)          129720    ['conv2d_16[0][0]']           
                                                                                                  
 conv2d_transpose (Conv2DTr  (None, 32, 32, 80)           86480     ['conv2d_17[0][0]']           
 anspose)                                                                                         
                                                                                                  
 concatenate_2 (Concatenate  (None, 32, 32, 160)          0         ['conv2d_transpose[0][0]',    
 )                                                                   'conv2d_14[0][0]']           
                                                                                                  
 conv2d_18 (Conv2D)          (None, 32, 32, 80)           115280    ['concatenate_2[0][0]']       
                                                                                                  
 conv2d_transpose_1 (Conv2D  (None, 64, 64, 54)           38934     ['conv2d_18[0][0]']           
 Transpose)                                                                                       
                                                                                                  
 concatenate_3 (Concatenate  (None, 64, 64, 144)          0         ['conv2d_transpose_1[0][0]',  
 )                                                                   'conv2d_11[0][0]',           
                                                                     'conv2d_9[0][0]']            
                                                                                                  
 conv2d_19 (Conv2D)          (None, 64, 64, 54)           70038     ['concatenate_3[0][0]']       
                                                                                                  
 conv2d_transpose_2 (Conv2D  (None, 128, 128, 36)         17532     ['conv2d_19[0][0]']           
 Transpose)                                                                                       
                                                                                                  
 concatenate_4 (Concatenate  (None, 128, 128, 96)         0         ['conv2d_transpose_2[0][0]',  
 )                                                                   'conv2d_6[0][0]',            
                                                                     'conv2d_4[0][0]']            
                                                                                                  
 conv2d_20 (Conv2D)          (None, 128, 128, 36)         31140     ['concatenate_4[0][0]']       
                                                                                                  
 conv2d_transpose_3 (Conv2D  (None, 256, 256, 24)         7800      ['conv2d_20[0][0]']           
 Transpose)                                                                                       
                                                                                                  
 concatenate_5 (Concatenate  (None, 256, 256, 48)         0         ['conv2d_transpose_3[0][0]',  
 )                                                                   'conv2d_1[0][0]']            
                                                                                                  
 conv2d_21 (Conv2D)          (None, 256, 256, 24)         10392     ['concatenate_5[0][0]']       
                                                                                                  
 conv2d_22 (Conv2D)          (None, 256, 256, 4)          100       ['conv2d_21[0][0]']           
                                                                                                  
 tf.nn.softmax (TFOpLambda)  (None, 256, 256, 4)          0         ['conv2d_22[0][0]']           
                                                                                                  
==================================================================================================

画像処理部全体を下記に示します。

画像処理部

from pickle import dump, load

class IMG_CNN_AE():

 def __init__(self, use_saved_model_flag , 
                    model_input_file , weight_input_file , 
                    lastlayer_name ):

		self.use_saved_model_flag = use_saved_model_flag
		self.model_input_file     = model_input_file
		self.weight_input_file    = weight_input_file
		self.lastlayer_name       = lastlayer_name

		self.image_feature_dict   = {}

		return

    def ImgProcNet(self):

        # 前線描画CNNのモデルと重みをNN_1に読み込む
		NN_1 = load_model( self.model_input_file )
		NN_1.load_weights(self.weight_input_file)
        NN_1.summary()

        # 中間層(self.lastlayer_name)をoutputとするネットワークを
        # 定義してNN_2とする
        NN_2 = Model(
           inputs=NN_1.input, 
           outputs=NN_1.get_layer(self.lastlayer_name).output
        )

		NN_2.summary()

		return NN_2

	def GetGPVData(self, dflist, gflist):

        # dflistで指定された日付群のGPV画像を読み込む
        # GPV画像ファイルはgflistに記載されている
        
		i_data1_list, i_data2_list, i_data3_list = [] , [] , []

		for i_date_cnt, datestr in enumerate( dflist ):

			p_gpv = gflist[i_date_cnt] # 該当する日付の画像ファイル名を取得
			gpvlen = len(p_gpv)        # GPVファイル種別の数(7)

            # 以下はi_data_2, i_data_3の処理を省略
			i_data_1 = np.empty((256, 256, 3*gpvlen)) #配列を初期化

			for c_gpv in range(gpvlen): #GPV種別分繰り返し

                #画像を読み込み、配列に変換してチャネル方向に結合
				img1 = (Image.open(p_gpv[c_gpv]).convert('RGB')).resize((256,256),Image.HAMMING)
				idata1 = np.asarray(img1) / 255
				i_data_1[:, :, 3*c_gpv:3*c_gpv+3] = idata1

			i_data1_list.append(i_data_1)
   
		i_data1_array = np.array(i_data1_list) # (batch, width, height, gpv_types)

		return i_data1_array, i_data2_array, i_data3_array


	def MakeFeatureDict(self, model, gflist, dflist):

		self.image_feature_dict  = {}

        # GPV画像を取得
        in_data1, in_data2, in_data3 = self.GetGPVData(dflist, gflist)

        # 前線描画CNNに入力して特徴量を取得
        prd_data = model.predict( [in_data1, in_data2, in_data3] )

        # データ数を取得
		num_data = prd_data.shape[0]

        # 日付をキーとして、特徴量の辞書を作成
		for cnt_data in range(num_data):
			image_id = dflist[cnt_data]
			self.image_feature_dict[image_id] = prd_data[cnt_data] 

		return self.image_feature_dict

	def SaveDict(self, filename):

        # filenameで指定したファイルに辞書を保存
		dump(self.image_feature_dict, open(filename, 'wb'))

		print("End.")

		return

3.5 言語処理部

3.5.1 日本語処理

短期予報解説は日本語の文章です。文章を扱うにはこれを「分かち書き」という形にして品詞ごとに空白で区切った形に変換する必要があります。
例えば、
トラフに対応する低気圧が前線を伴って中国東北区を東南東進。
という文は、
トラフ" "に" "対応" "する" "低" "気圧" "が" "前線" "を" "伴っ" "て" "中国東北区" "を" "東南東進" "。"
となります。こうしてこの一語一語を「トークン」と呼ばれる数値に変換することができるようになります。

英語などの場合は、
Low pressure corresponding to the trough moves east-southeastward over northeastern China with a front.
というように元々が分かち書きになっているので、文をそのままトークン化に進めることができます。日本語は余分にひと手間必要なわけですね。

このために、janomeというライブラリをインポートしています。janomeは他にも色々なことができます。
以前に「文章からの気象解析〜WordCloudで遊ぶ〜」という記事を書いていますのでご覧ください。

日本語処理の部分は下記となります。短期予報解説を入力して、分かち書き形式にした上で、画像同様に日付をキーとした辞書として保存します。

日本語処理

from janome.analyzer    import Analyzer
from janome.charfilter  import *
from janome.tokenfilter import *

from pickle import dump, load

class TextPreprocessor():

	def __init__(self, datefile_list , txtdirroot, savefiledir, trnprd_mode ):

		self.datefile_list = datefile_list
		self.txtdirroot    = txtdirroot
		self.savfiledir    = savefiledir
		self.trnprd_mode   = trnprd_mode
		self.a             = Analyzer( token_filters=[CompoundNounFilter()] )
		self.image_texts_dict = {}

		return

	def MakeTxtFileName(self, date_str):
 
        # 日付文字列から短期予報解説のファイル名を作成
		self.txtfile_name = self.txtdirroot + 短期予報解説の年月日時のファイル名

		return self.txtfile_name

	def LoadTexts( self, filename ):

        # 指定したファイルから文を読み込む
		with open(filename, "r", encoding="utf-8") as file:
			text = file.read()
		return  text

	def MakeTextsDics(self):

        # テキストファイルを読み込み、分かち書きして辞書を作る
        
		for date_str in self.datefile_list :

            # 指定した日付(年月日時)のファイルを読み込む
			text_id = date_str
			image_text = self.LoadTexts( self.MakeTxtFileName( date_str ) )

			image_text = image_text.replace("\n","") #改行を削除

            # テキストを文ごとに処理
            # 複数文に対応しているが今回は1ファイルが1文になる
            for cnt_sent, sentence in enumerate(image_text):

				word_list = []

				if sentence == '' : #空行をスキップ
					continue

                # janomeアナライザを用いて形態要素解析を行い、品詞ごとに分解した上で
                # word_listにappendしていく
				for tok in self.a.analyze(sentence):
					word_list.append(tok.surface)

                # 分解した品詞を空白を挟んで結合する
                image_text_proc  = ' '.join(word_list)

                # 文の初めにstartseq, 最後にendseqという単語を追加する
                image_text_final = 'startseq ' + image_text_proc + ' endseq' 

                # 分かち書きした文を日付をキーとして辞書化する
				sent_id = text_id + str(cnt_sent).zfill(2)
				if sent_id not in self.image_texts_dict:
					self.image_texts_dict[sent_id] = []
				self.image_texts_dict[sent_id].append(image_text_final)

		return self.image_texts_dict

	def SaveDict(self, filename):

        # 辞書を保存する
		dump(self.image_texts_dict, open(filename, 'wb') )

       return

これで、画像とテキストが日付をキーとした辞書型データとなりました。

3.5.2 画像辞書・テキスト辞書の読み込み

以下のように一度保存してあった画像・テキストの辞書を読み込みます

画像辞書とテキスト辞書の読み込み
# 画像特徴量、テキストの辞書の読み込み
image_feature_dict = load( open(image_feature_file, 'rb') )
image_text_dict    = load( open(image_text_file,    'rb') )

3.5.3 Transformerネットワークの定義と学習

さてネットワーク定義の部分、ここはTensorflowチュートリアルほぼそのままです。
データの扱いのみがチュートリアルと異なります(後述)。

チュートリアルでは、ネットワークはCaptionerというクラスで定義されていますが、以下ではTrainerというクラスになっています。

ネットワーク定義の本体はMakeCaptioningModel_2にあります。
自作の画像処理ネットワークの特徴量を抽出してきますが、値をテキスト側の値と同じようなレンジの数値に正規化する処理をBatchNormalizationとして追加します。

この正規化処理は重要です。これをしないと画像とテキストのデータのレンジが違いすぎたりして、画像の特徴がうまくテキストに伝わらなかったり、逆にテキストの値が画像の値に埋没して文章の学習が進まなかったりします。自作ネットワークから抽出してくる場合は注意が必要です。

Tensorflowのチュートリアルでは、学習のfit関数にDataset形式のデータを渡しています。
Dataset形式は、従来のGenerator関数を書いてデータを渡す方式に比べて、抽象化された方式でデータに対する処理を記述することができます。

Dataset形式による抽象化された記載方法
Dataset
  # 保存した画像、テキストを辞書型からリスト化
  img_train_list, txt_train_list = [], []
      
  for key in image_feature_dict.keys():
    img_train_list.append(image_feature_dict[key])
    txt_train_list.append(image_text_dict[key][0])

  # from_tensor_slicesという関数によってDataset型に変換する
  train_raw_wk = tf.data.Dataset.from_tensor_slices((img_train_list, txt_train_list))
  

具体的にいうとGeneratorのような処理をあらわに書く必要はありませんし、データの前処理も便利に記載できます。

Dataset前処理

      
def prepare_txt(imgs, txts):
  tokens = tokenizer(txts)
      
  input_tokens = tokens[..., :-1]
  label_tokens = tokens[..., 1:]
  return (imgs, input_tokens), label_tokens

def prepare_dataset(ds, tokenizer, batch_size=32, shuffle_buffer=1000):
  # Load the images and make batches.
  ds = (ds.batch(batch_size))

  def to_tensor(inputs, labels): # 
    (images, in_tok), out_tok = inputs, labels
    return (images, in_tok.to_tensor()), out_tok.to_tensor()
  
  return (ds
          .unbatch()
          .shuffle(shuffle_buffer)
          .batch(batch_size)
          .map(prepare_txt, tf.data.AUTOTUNE) #入力、出力テキストの作成
          .map(to_tensor, tf.data.AUTOTUNE) # Tensor化
          )

# 下記によって、データの前処理とgenerator相当の分割処理が記載できる

train_ds = prepare_dataset(train_raw, tokenizer)

ところがMac版のTensorflowでこの方式を利用するとメモリリークするという問題があって、学習が進まなくなります。実際私も当初はTutorialの方法そのままでやっていたのですが、メモリが増大してエポックあたりの学習時間が2倍、3倍と増大した挙句に最後はOSにkillされてしまうという憂き目にあいました。

さて前置きが長くなりましたが、Transformerの部分は以下のようになります。

Transformer部品
class TokenOutput(tf.keras.layers.Layer):
  # ネットワークの最後の層。ここでDense層によって単語数を要素数とする配列に戻ります。
  def __init__(self, tokenizer, banned_tokens=('', '[UNK]', 'startseq'), **kwargs):
    super().__init__()

    self.dense = tf.keras.layers.Dense(
        units=tokenizer.vocabulary_size(), **kwargs)
    self.tokenizer = tokenizer
    self.banned_tokens = banned_tokens

    self.bias = None

  def adapt(self, entire_tokens):
    # 初期化の際に、一部の単語の出現確率を意図的に下げるようにする処理を行う
    counts = collections.Counter()
    vocab_dict = {name: id 
                  for id, name in enumerate(self.tokenizer.get_vocabulary())}

    for tokens in entire_tokens:
      counts.update(tokens.numpy().flatten())

    counts_arr = np.zeros(shape=(self.tokenizer.vocabulary_size(),))
    counts_arr[np.array(list(counts.keys()), dtype=np.int32)] = list(counts.values())

    counts_arr = counts_arr[:]
    for token in self.banned_tokens:
      counts_arr[vocab_dict[token]] = 0

    total = counts_arr.sum()
    p = counts_arr/total
    p[counts_arr==0] = 1.0
    log_p = np.log(p)  # log(1) == 0

    entropy = -(log_p*p).sum()

    print()
    print(f"Uniform entropy: {np.log(self.tokenizer.vocabulary_size()):0.2f}")
    print(f"Marginal entropy: {entropy:0.2f}")

    self.bias = log_p
    self.bias[counts_arr==0] = -1e9

  def call(self, x):
    x = self.dense(x)
    return x + self.bias


class SeqEmbedding(tf.keras.layers.Layer):
    # 入力テキストをトークン化する処理と、位置エンコーディングを行う
	def __init__(self, vocab_size, max_length, depth):
		super().__init__()
		self.pos_embedding = tf.keras.layers.Embedding(input_dim=max_length, \
                               output_dim=depth)
		self.token_embedding = tf.keras.layers.Embedding(\
                                 input_dim=vocab_size,\
                                 output_dim=depth,\
                                 mask_zero=True)
		
		self.add = tf.keras.layers.Add()

	def call(self, seq):
		seq = self.token_embedding(seq) # (batch, seq, depth)
		x = tf.range(tf.shape(seq)[1])  # (seq)
		x = x[tf.newaxis, :]  # (1, seq)
		x = self.pos_embedding(x)  # (1, seq, depth)

		return self.add([seq,x])

class CausalSelfAttention(tf.keras.layers.Layer):
    # 入力テキスト同士のクロスアテンションをとるレイヤー
	def __init__(self, **kwargs):
		super().__init__()
		self.mha = tf.keras.layers.MultiHeadAttention(**kwargs)
		self.add = tf.keras.layers.Add() 
		self.layernorm = tf.keras.layers.LayerNormalization()

	def call(self, x):
		attn = self.mha(query=x, value=x,\
                    use_causal_mask=True)
		x = self.add([x, attn])
		return self.layernorm(x)

class CrossAttention(tf.keras.layers.Layer):
    # 入力テキストと画像特徴量のクロスアテンションをとるレイヤー
	def __init__(self,**kwargs):
		super().__init__()
		self.mha = tf.keras.layers.MultiHeadAttention(**kwargs)
		self.add = tf.keras.layers.Add() 
		self.layernorm = tf.keras.layers.LayerNormalization()

	def call(self, x, y, **kwargs):
		attn, attention_scores = self.mha(\
             query=x, value=y,\
             return_attention_scores=True)

		self.last_attention_scores = attention_scores

		x = self.add([x, attn])
		return self.layernorm(x)


class FeedForward(tf.keras.layers.Layer):
    # Fully Connectedレイヤーによって元のunitサイズに戻すレイヤー
	def __init__(self, units, dropout_rate=0.1):
		super().__init__()
		self.seq = tf.keras.Sequential([\
        tf.keras.layers.Dense(units=2*units, activation='relu'),\
        tf.keras.layers.Dense(units=units),\
        tf.keras.layers.Dropout(rate=dropout_rate),\
		])

		self.layernorm = tf.keras.layers.LayerNormalization()
  
	def call(self, x):
		x = x + self.seq(x)
		return self.layernorm(x)

class DecoderLayer(tf.keras.layers.Layer):
 	# セルフアテンション、クロスアテンション、フィードフォーワード層をまとめる
    def __init__(self, units, num_heads=1, dropout_rate=0.1):
		super().__init__()

		self.self_attention = CausalSelfAttention(num_heads=num_heads,\
                                              key_dim=units,\
                                              dropout=dropout_rate)
		self.cross_attention = CrossAttention(num_heads=num_heads,\
                                          key_dim=units,\
                                          dropout=dropout_rate)
		self.ff = FeedForward(units=units, dropout_rate=dropout_rate)


	def call(self, inputs, training=False):
		in_seq, out_seq = inputs

		# Text input
		out_seq = self.self_attention(out_seq)

		out_seq = self.cross_attention(out_seq, in_seq)

		self.last_attention_scores = self.cross_attention.last_attention_scores

		out_seq = self.ff(out_seq)

		return out_seq

def standardize(s):
	s = tf.strings.lower(s)

	return s

# ロス関数の定義(意図的に大きくした一部のトークンを除外するため
# 1e8以下のみを対象として計算しなおしている
def masked_loss(labels, preds):
  loss = tf.nn.sparse_softmax_cross_entropy_with_logits(labels, preds)

  mask = (labels != 0) & (loss < 1e8)
  mask = tf.cast(mask, loss.dtype)

  loss = loss*mask
  loss = tf.reduce_sum(loss)/tf.reduce_sum(mask)
  return loss

# 評価用の関数。非ゼロの結果で、予測が一致したトークンの割合
def masked_acc(labels, preds):
  mask = tf.cast(labels!=0, tf.float32)
  preds = tf.argmax(preds, axis=-1)
  labels = tf.cast(labels, tf.int64)
  match = tf.cast(preds == labels, mask.dtype)
  acc = tf.reduce_sum(match*mask)/tf.reduce_sum(mask)
  return acc

ここまでで部品定義です。これはほぼチュートリアルそのまま。

続いてネットワーク本体です。チュートリアルに比べるとデータの扱い方がDataset形式でないことによる違いが出てきています。

本体

# Transoformer定義の本体
class Trainer(TXT_Proc.TextPreprocessor):

	def __init__(self, features_dict, texts_dict, epochs, use_flag,\
               token_file, batch_size, \
               num_layers=1,vsize=2900,\
               units=256, max_length=500, num_heads=1, dropout_rate=0.1):

		self.train_texts_dict    = texts_dict
		self.train_features_dict = features_dict
		self.epochs              = epochs
		self.use_saved_model_flag = use_flag
		self.token_file          = token_file
		self.units = units
		self.num_layers = num_layers
		self.num_heads = num_heads
		self.dropout_rate = dropout_rate
		self.vocab_size = vsize
		self.batch_size = batch_size

        #Transformerの部品たちをどんどん生成します
		self.tokenizer = tf.keras.layers.TextVectorization(\
			max_tokens=self.vocab_size, standardize=standardize,\
            output_sequence_length=max_length\
		)
		self.MakeTokenizer() # self.tokonizerを用いたトークン化を実行する

        # トークンから単語、単語からトークンに変換する処理を定義
		self.index_to_word = tf.keras.layers.StringLookup( \
                               mask_token="", \
                               vocabulary=self.tokenizer.get_vocabulary(), \
                               invert=True)
		self.max_length = max_length
		self.word_to_index = tf.keras.layers.StringLookup(\
                               mask_token="",\
                               vocabulary=self.tokenizer.get_vocabulary() )

		self.seq_embedding = SeqEmbedding(\
                               vocab_size=self.vocab_size,\
                               depth=self.units,\
                               max_length=self.max_length)

		self.decoder_layers = [ \
              DecoderLayer(self.units, \
                           num_heads=self.num_heads, \
                           dropout_rate=self.dropout_rate) \
                         for n in range(self.num_layers)]


		self.output_layer = TokenOutput(\
             self.tokenizer, banned_tokens=('', '[UNK]', 'startseq'))
		self.output_layer.adapt(entire_tokens=self.entire_tokens)

		return



	def __dictToList(self, texts_dict):
        # MakeTokenizerで使用される。辞書からリストへの変換用。
		texts_list = []

		for key in texts_dict.keys():
			for d in texts_dict[key]:
				texts_list.append(d)

		return texts_list

	def MakeTokenizer(self):
        # テキスト辞書からトークン化を実行する
		train_texts_list = self.__dictToList(self.train_texts_dict)
		self.tokenizer.adapt(train_texts_list)
		self.entire_tokens = self.tokenizer(train_texts_list)

        return None

	def GetVocabSize(self):
        # 今回は未使用
		self.vocab_size = len(self.tokenizer.word_index) + 1
		print("Vocabulary Size of Texts: " , self.vocab_size)

		return

	def GetMaxLength(self):

		lists = self.__dictToList(self.train_texts_dict)

        self.max_length = max(len(d.split()) for d in lists )

		print("Max Length of Texts: ", self.max_length)

		return self.max_length

	def MakeCaptioningModel_2(self, modelfile, weightfile, modelflag):

        # 画像処理部の定義
		inputs1 = tf.keras.Input(shape=(16,16,120)) # cond2d_17
        # 画像特徴量を正規化する(重要)
		ie1     = tf.keras.layers.BatchNormalization(axis=-1)(inputs1)
		ie1     = tf.keras.layers.Reshape((16*16,120))(ie1)

		# テキスト処理部の定義
		inputs2 = tf.keras.Input(shape=(self.max_length,))
		se1     = self.seq_embedding(inputs2)

        # Transformer部分の定義
		for dec_layer in self.decoder_layers:
			se1 = dec_layer(inputs=(ie1, se1)) # 画像特徴量が合流

		outputs = self.output_layer(se1)

		# model コンパイル
		c_model = tf.keras.Model(inputs=[inputs1, inputs2], outputs=outputs)

        # チェックポイントデータからの再開の時に利用(重みをロード)
		if modelflag != 'new' :
			checkpoint_new = tf.train.Checkpoint(model=c_model)
			checkpoint_new.restore(modelfile)

		c_model.compile(
          loss=masked_loss,optimizer=tf.keras.optimizers.legacy.Adam(
                                                     learning_rate=1e-3
                                                    ),
          metrics=[masked_acc]
        )

		c_model.summary()

		self.model = c_model

		return

	def MakeInputOutput(self, image_texts, image_features):
        # 画像とテキストから入力と出力データを作成する
        # 入力:画像、テキスト(最初から最後から1トークン前までの文)
        # 出力:テキスト(最初のトークンを除外して、最後までの文)
		X1, X2, y = [], [], []

		for image_text , image_feature in zip(image_texts, image_features):
			seq = self.tokenizer(image_text[0]) # トークン化された文全体を取り出す
            # 入力用と出力用トークンを作成
			in_seq, out_seq = seq[...,:-1], seq[...,1:] 

			X1.append(image_feature)
			X2.append(in_seq)
			y.append(out_seq)

        # numpy形式にして返却
		return np.array(X1), np.array(X2), np.array(y) 

	def DataGenerator(self):
        # DataGeneratorとして、バッチサイズ分の入力・出力(教師データ)を作成
		num_d = 0
		while 1:
			img_list, txt_list = [], []
			for key, img_text in self.train_texts_dict.items():
                # テキストの辞書からキーと本体を取得。対応する画像特徴量を取得
				key_for_image=str(key)[:-2]
				image_feature = self.train_features_dict[key_for_image]

                # バッチサイズになるまでlistに追加
				txt_list.append(img_text)
				img_list.append(image_feature)
				num_d = num_d + 1

                # バッチサイズになったらMakeInputOutputを呼び出して、
                # 入力と出力(教師データ)に変換し、yieldする
                if num_d == self.batch_size:
					in_img, in_seq, out_word = self.MakeInputOutput(txt_list, img_list)
					yield [[in_img, in_seq], out_word]
                    # yieldから戻ったら各種初期化を行う
					img_list, txt_list = [], []
					num_d = 0


	def TrainModel(self, model_file, saved_model_file, weight_file, 
                        saved_weight_file,  \
                        use_saved_model_flag):

        # 学習実行のメソッド
        self.saved_model_file  = saved_model_file #未使用
		self.model_file        = model_file       #チェックポイント用
		self.saved_weight_file = saved_weight_file #未使用
		self.weight_file       = weight_file       #未使用
    	self.saved_flag = use_saved_model_flag    #未使用
     
		self.MakeCaptioningModel_2(
         self.saved_model_file, self.saved_weight_file, self.saved_flag
        )

		steps=len(self.train_texts_dict)//self.batch_size

		print("Training Start...")

		generator = self.DataGenerator() # generatorのセット

        # 学習実施
		self.model.fit(generator, 
                batch_size=self.batch_size, epochs=self.epochs, 
                steps_per_epoch=steps, verbose=1
        )

		checkpoint_final = tf.train.Checkpoint(model=self.model)
		checkpoint_final.write(self.model_file)

		print("Training End.")

		return None

3.6 テキスト予測

学習済みネットワークができたら、テキストを予測してみます。

これまでみてきたように、入力は画像特徴量とテキストです。

テキストとして「startseq」を与えて、出力を生成させます。
ネットワークは後続単語列を出力するように学習されていますから、出力のいちばん最後の単語を取ってくることで入力文に続く、最初の単語を予測したことになります。

日本の南に熱帯低気圧があるの例でいうと、最初に
入力:startseq
出力:日本
となります。これをstartseqに付け加えて、
入力:startseq 日本
出力:日本 の
となります。

同じく、新しく予測されて「の」を付け加えると、入力はstartseq 日本 のとなります。
同様にしていくと、
日本 の 南 に 熱帯 低気圧 が ある
のようになっていきます。
こうやってendseqを予測するか、最大長に到達するまで予測を繰り返すことで文を生成するわけです。
その様子が本投稿の冒頭にも記載した動画です。

image

これは文の語の長さ分だけ、Feed Forward処理が繰り返されるということです。
つまり、はっきり言ってBack Propagationの時間より長くなります。つまり1エポックの学習より1データの予測の方が時間がかかるわけです。

余談ですが、ChatGPTのような大規模言語モデルを利用したアプリケーションも、文章の生成は同様の処理を行なっているはずです。「次の単語の予測」というのは、モデルが持っているボキャブラリの最大語数のうち最も確率が高いものを選ぶという処理です。ボキャブラリ数が多くなれば多くなるほど、最終層(Denseレイヤ相当)の計算が膨大になってきますから、これを猛烈なスピードで繰り返して文章を生成していることになります。凄まじい計算速度に思えますが、何か特別な処理があるのでしょうか。

予測
class Predictor():

	def __init__(self, model, max_length, 
                 tknzer, id2word, word2id, image_feature_dict ):
		self.model      = model # 学習済みモデル
		self.tokenizer  = tknzer
		self.max_length = max_length
		self.image_feature_dict = image_feature_dict
		self.id2word = id2word
		self.word2id = word2id

		#self.model.load_weights(self.weight_file)

		return

	def simple_gen(self, image_c, temperature=0):

        # 最初にstartseqを入力する
		initial = self.word2id([['startseq']]) # (batch, sequence)

        if image_c.ndim < 4 : #写真1枚の場合(未使用)
			img_features = image_c[tf.newaxis, ...]
		else:
			img_features = image_c

		tokens = initial # (batch, sequence)
  
		for n in range(self.max_length):
  
            # モデルに入力して予測する
			preds = self.model.predict((img_features, tokens),verbose=0) 
            # (batch, sequence, vocab)

            #最後の配列を取り出す(入力文からみて、予測された次の最初のトークンとなる)
			preds = preds[:,-1, :]  #(batch, vocab)

            #ボキャブラリ全体から、対応する配列の値が最も大きいものが次の予測トークンとなる
            #以下ではランダムに取ってくる場合との比較ができるような処理になっているが、
            #今回は未使用
			if temperature==0:
				next = tf.argmax(preds, axis=-1)[:, tf.newaxis]
                # (batch, 1)
			else: #未使用
				next = tf.random.categorical(preds/temperature, num_samples=1)
                # (batch, 1)
                
			# 予測したトークンを入力トークンに付加する
            tokens = tf.concat([tokens, next], axis=1)  # (batch, sequence) 

            # endseqを予測した場合、文終了とする
			if next[0] == self.word2id('endseq'):
				break

        # トークン列を単語に戻し、文章として接続する
		words = self.id2word(tokens[0, 1:-1])
		result = tf.strings.reduce_join(words, axis=-1, separator=' ')
		return result.numpy().decode()


    # 上記のメソッドを呼び出してファイルに記録するための処理
    # GPUを使うとオーバーヘッドが大きくかえって遅くなるので、CPUモードで実行させている
	def do_simple_gen(self):
		img_prd_list = []
		with tf.device('/cpu:0'):
			for key in self.image_feature_dict.keys():
				result = self.simple_gen(self.image_feature_dict[key], temperature=0)
				print(f"key={str(key)} result={result}")
				text_pair = [key, result]
				img_prd_list.append(text_pair)
		return img_prd_list

4. 生成結果

4.1 学習

2020年1月から2022年12月までの1日2件づつの気象画像と解説文のペア(2,152組)を学習させました。
MultiHeadAttenstionのヘッド数は2、Transformerブロックは2個というネットワークです。

MacStudio(M2 Max)で実行した1エポックあたりの学習時間は、65秒程度です。
LSTMベースのShow and Tellでは半分のデータ量(1日1件づつ)の学習でも1エポック1時間でした。LSTMがMac M2対応できていないこともありますが、Transformerモデルの計算速度は見事な物でした。

4.2 生成結果の例

(1) まずまずの例

入力する画像は、次のようなものです。

gsm_hlayout_2023080106.png
(2023年8月1日6時(UTC)の可視化画像。上段は風・温度・気圧(左から地表、850hPa、500hPa)、
下段は左から850hPa相当温位、700hPa湿数、700hPa上昇気流、300hPa風速絶対値)

この出力として、以下の文が出力されました。
天気図は参考としてつけたもので、Transformerはあくまで上の7種類の図のみを処理しています。

SPAS_COLOR_202308010600.v8.512.png

Transformerの生成文
1実況上実況上の着目点
①台風第8号は、沖縄の南を北上している。台風周辺には発達した対流雲がかかり、
非常に激しい雨を解析、発雷を検知。
②台風第9号が日本の南を北上中。台風周辺には、台風周辺の下層暖湿気が流入し、
海上中心と1時間に5日21時には500hpaで-30℃以下の寒気を伴うトラフが東日本を東進中。
北日本から東日本の気温上昇も加わり、北日本の広い範囲で大気の状態が非常に不安定。

2.主要じょう乱の予想根拠と解説上の留意点
①台風第8号は、上層の台風の動きが遅くから4日はじめにかけて先島諸島にかなり接近し、
一部の住家が倒壊するおそれもある猛烈な風が吹き、猛烈なしけとなる所がある。
また、3日午後は猛烈な風が吹く見込み。暴風やうねりを伴った高波に厳重に警戒。
また、台風の接近・奄美地方では台風本体や周辺の下層暖湿気や台風周辺の影響で
大気の状態が非常に不安定となり、雷を伴って激しい雨や激しい雨が降り大雨となる所がある。
土砂災害、低い土地の浸水、河川の増水、落雷や竜巻などの激しい突風、降ひょうに注意。
②1項②の台風の接近・通過によっては、西日本~南西諸島にかけて伊豆諸島付近を中心に
大気の状態が非常に不安定となり、雷を伴った激しい雨や非常に激しい雨が降り、
大雨となるおそれがある。
土砂災害、低い土地の浸水、河川の増水に注意・警戒し、落雷や竜巻などの激しい突風に注意。
③台風第9号は大陸東岸を北上し、31日夜には小笠原諸島に接近する見込み。
その後は、台風第8号は日本の南を北上し、24日午後には九州南部・奄美地方にかなり
接近する見込み。台風周辺や台風周辺の下層暖湿気が流入し大気の状態が非常に不安定となり、
雷を伴って激しい雨や非常に激しい雨が降り、大雨となる所がある。土砂災害、低い土地の浸水、
河川の増水に注意・警戒し落雷や竜巻などの激しい突風、降ひょうに注意。台風の台風は、
まだ、24日は沖縄地方や千島近海から遠ざかるが、北日本太平洋側ではしける所がある。
強風や高波に注意。北海道地方では、2日は暴風やうねりを伴った高波、高潮に注意・警戒し

最大長制限で途中で打ち切られていますが、1. 2.という節、項目としての①、②、といった
構造は学習できていて、日本語的にもまずまず意味が通る文章を生成できています。

気象的には台風に関する説明と、注意事項を生成できていますが、日付や地域はでたらめになっています。
「台風第9号は大陸東岸を北上し、31日夜には小笠原諸島に接近する見込み。」というのも地理感覚が変ですね。

この日気象庁が実際に発表した解説文は下記でした。

気象庁発表
1.実況上の着目点
① 大型で非常に強い台風第6 号が那覇市の南東にあって、西北西進。
南西諸島では非常に強い風が吹き、猛烈なしけとなっている。西~東日本太平洋側では
台風のうねりが到達し、波が高い所がある。沖縄地方では台風周辺の雨雲がかかり、
海上では強い雨を解析している。
② 太平洋高気圧が後退し、本州付近は高気圧縁辺の下層暖湿気が流入。
また、500hPa 5880m 付近の -6℃以下の寒気を伴ったトラフが日本付近を通過中。
1 日 9 時の館野高層観測では、500hPa -9.1℃を観測。東日本では非常に激しい雨を解析し、
発雷を多数検知。

2.主要じょう乱の予想根拠と解説上の留意点
① 1 項①の台風は、非常に強い勢力を維持したまま 1 日夜から 2 日にかけて沖縄地方に
かなり接近する見込み。沖縄地方では、猛烈な風が吹き、猛烈なしけとなる。
台風の接近と大潮の時期が重なるため、南西諸島では潮位が高くなる所がある。
2 日にかけて暴風や高波、高潮に厳重に警戒。台風の接近に伴い、2 日にかけて台風周辺や
台風本体の発達した雨雲の影響により、雷を伴った非常に激しい雨や局地的に猛烈な雨が降り、
大雨となる所がある。土砂災害や低い土地の浸水、河川の増水や氾濫に警戒し、
落雷や竜巻などの激しい突風に注意。台風は 3 日以降も東シナ海をゆっくり進むため、
沖縄地方では大荒れの天気が続くおそれがあることに留意。西~東日本太平洋側では、
3 日にかけて波が高くなりしける所がある。うねりを伴った高波に注意。西日本太平洋側の南東斜面では、
台風周辺の下層暖湿気が流入し大雨となるおそれがあることに留意。土砂災害や低い土地の浸水、
河川の増水に注意。
② 2 日にかけて、西~北日本では 1 項②の高気圧縁辺の下層暖湿気が流入し、
1 項②のトラフが通過するため、大気の状態が非常に不安定。雷を伴った激しい雨や
非常に激しい雨が降り、大雨となる所がある。土砂災害や低い土地の浸水、河川の増水、
落雷や竜巻などの激しい突風、降ひょうに注意。
③ 3 日は、北海道付近に前線がのびる見込み。前線に向かって下層暖湿気が流入し、
北海道地方では大気の状態が不安定となる。雷を伴った激しい雨が降り、大雨となるおそれがある。
土砂災害、低い土地の浸水、河川の増水や氾濫に警戒し、落雷や突風、降ひょう、強風や高波に注意。

(3) ダメな例

今の例はかなり成功した方で、ダメなものは、このようなものもあります。
同じ学習済みネットワークなのに、このクォリティの差が生じるのも謎です。

更新 これらの問題を改善しました。次章に記載しています。

かなりダメな例
1着目点①で高まっ着目点
①東日本日本海の代わっの500paの北東の500hpa5760~5820m付近の北東の北東の前線の北東の
500hpa5760m付近の前線の北東のミリの前線の500hpa5700~5760m付近所が
西日本と東日本太平洋側を暴風域を伴った所が暴風域を中心に東日本~東北地方の雨雲が暴風域を
解析とする所が入ってと日本海の別の500hpa5760~5820m付近の前線の暴風域の台風本体の雨雲が
暴風域を中心に台風本体の台風本体の雨雲が暴風域の暴風域の台風本体の雨雲が暴風域を伴ったする。
台風本体の雨雲に雨雲は台風本体の雨雲が暴風域を暴風域を伴ったた猛烈な所が非常に猛烈な所がある。
西日本へと台風本体の雨雲が台風本体の雨雲が台風本体のた猛烈な所がある。
台風本体の雨雲に台風本体の台風本体の雨雲が暴風域を中心に非常に猛烈な雨や猛烈な所がする所がある。
台風本体や台風周辺の台風本体や猛烈な所がある。
台風本体の台風本体や台風本体の雨雲が暴風域は暴風域を中心に非常に非常に台風本体のすると
台風本体の雨雲の雨雲がする。
台風本体や台風本体や台風本体の雨雲が非常に猛烈なとする。
猛烈なの台風本体の雨雲がた猛烈なとするとたた猛烈な所や台風本体の台風本体の台風本体の
雨雲の雨雲の雨雲の台風本体や台風本体の雨雲の雨雲の指向するとた猛烈なとのとするとするとの
雨雲の雨雲の雨雲のミリを伴った猛烈なとの猛烈なとの猛烈なとのとの流路がの雨量が非常と猛烈な所がある。
猛烈な大しけとなる所や猛烈なとなる所がある。台風本体や台風本体の暴風や台風周辺の台風本体の流路が
の北側と台風周辺のする台風本体の台風周辺の台風周辺の台風本体の雨雲の台風周辺の指向する台風本体の
台風周辺の雨雲の指向する台風本体の雨雲のと台風周辺の下層暖湿気のとの指向も台風本体のののの
指向するとの雨雲がの大雨のとなる所がある。の大雨との指向日ののの指向する所がある。
の流路がある猛烈なしけとの大しけとなるとの大雨のおそれがある。
暴風や高潮と高潮、大雨による大雨とする大雨と暴風や大雨との大雨による大雨との浸水や暴風や暴風や
うねりを伴った高波に厳重に警戒し、台風本体や台風周辺と

全くダメな例
実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上
実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上
実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上
実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上
実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上
実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上
実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上
実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上
実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上
実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上実況上
(以下略)

5 ネットワークとデータの変更による改善(Dec.10 2023)

前の章で登場した「全くダメな例」のような崩壊しているレベルの生成はネットワークの構造を変更した結果改善しました。また、注意事項や予測の記載の中には、「9日にかけて高波に注意」というような日付が登場しますが、ネットワークは日付を知らないためにでたらめな日付となってしまっていました。これを入力画像の示す気象の日付から起算した日付になるようにできないか、改善を試みました。具体的にはネットワーク構造の改善と、テキストの与え方の改善です。これら効果があった項目を記載します。

5.1 ネットワーク構造の改善

ネットワーク構造として2点を改善しました。

(1) Multi Head Attentionのヘッド数およびAttentionレイヤーのレイヤー数の変更

ヘッド数を6、レイヤ数を4に変更しました。もともとそれぞれ2であったものの変更です。
私のソースでいうと、Trainerクラスを呼び出す際に、以下のnum_layersnum_headsの数を指定します。

ヘッド数とレイヤ数
# Transoformer定義の本体
class Trainer(TXT_Proc.TextPreprocessor):

	def __init__(self, features_dict, texts_dict, epochs, use_flag,\
               token_file, batch_size, \
               num_layers=1,vsize=2900,\
               units=256, max_length=500, num_heads=1, dropout_rate=0.1):

(2) 入力する気象データの多段階化

短期予報解説資料は、ある時刻の観測結果と、48時間先までの予想データを12時間刻みで話題にしています。
そこで、これら将来の観測結果(を可視化した特徴量)も合わせて与えます。

このためネットワークを以下のように修正します。

複数時刻のデータを入力するネットワーク
def MakeCaptioningModel_2(self, modelfile, weightfile, modelflag):

		self.modelfile = modelfile

		if modelflag != "cont" :
            # 画像処理部の定義
			inputs1 = tf.keras.Input(shape=(16,16,120)) # cond2d_17
            # 画像特徴量を正規化する(重要)
			ie1     = tf.keras.layers.BatchNormalization(axis=-1)(inputs1)
			ie1_00     = tf.keras.layers.Reshape((16*16,120))(ie1)

            # 修正ここから
            # 12時間後の気象場特徴量
			inputs1_12 = tf.keras.Input(shape=(16,16,120)) # cond2d_17
			ie1_12     = tf.keras.layers.BatchNormalization(axis=-1)(inputs1_12)
			ie1_12     = tf.keras.layers.Reshape((16*16,120))(ie1_12)

            # 24時間後の気象場特徴量
			inputs1_24 = tf.keras.Input(shape=(16,16,120)) # cond2d_17
			ie1_24     = tf.keras.layers.BatchNormalization(axis=-1)(inputs1_24)
			ie1_24     = tf.keras.layers.Reshape((16*16,120))(ie1_24)

            # 36時間後の気象場特徴量
			inputs1_36 = tf.keras.Input(shape=(16,16,120)) # cond2d_17
			ie1_36     = tf.keras.layers.BatchNormalization(axis=-1)(inputs1_36)
			ie1_36     = tf.keras.layers.Reshape((16*16,120))(ie1_36)

            # 48時間後の気象場特徴量
			inputs1_48 = tf.keras.Input(shape=(16,16,120)) # cond2d_17
			ie1_48     = tf.keras.layers.BatchNormalization(axis=-1)(inputs1_48)
			ie1_48     = tf.keras.layers.Reshape((16*16,120))(ie1_48)
            # 修正ここまで

			# テキスト処理部の定義
			inputs2 = tf.keras.Input(shape=(self.max_length,))
			se1     = self.seq_embedding(inputs2)

            # Transformer部分の定義(修正あり)
            # 複数の画像特徴量の合流
			for dec_layer in self.decoder_layers:
				se1 = dec_layer(inputs=(ie1_00, ie1_12, ie1_24, ie1_36, ie1_48, se1))

		outputs = self.output_layer(se1)

		# model コンパイル
		c_model = tf.keras.Model(inputs=[inputs1, inputs2], outputs=outputs)

        # チェックポイントデータからの再開の時に利用(重みをロード)
		if modelflag != 'new' :
			checkpoint_new = tf.train.Checkpoint(model=c_model)
			checkpoint_new.restore(modelfile)

		c_model.compile(
          loss=masked_loss,optimizer=tf.keras.optimizers.legacy.Adam(
                                                     learning_rate=1e-3
                                                    ),
          metrics=[masked_acc]
        )

		c_model.summary()

		self.model = c_model

		return

追加されたデータは以下のようにTransformer層に渡されます。

Transformerの部品修正
class DecoderLayer(tf.keras.layers.Layer):
	def __init__(self, units, num_heads=1, dropout_rate=0.1):
		super().__init__()
     ()

	def call(self, inputs, training=False):
		in_seq0, in_seq12, in_seq24, in_seq36, in_seq48, out_seq = inputs

		# Text input
		out_seq = self.self_attention(out_seq)

        # 画像特徴量とのCross AttentionとFeed Forward層にレイヤーに、各時刻の特徴量を与える
		out_seq0 = self.cross_attention(out_seq, in_seq0)
		out_seq0 = self.ff(out_seq0)

		out_seq1 = self.cross_attention(out_seq, in_seq12)
		out_seq1 = self.ff(out_seq1)

		out_seq2 = self.cross_attention(out_seq, in_seq24)
		out_seq2 = self.ff(out_seq2)

		out_seq3 = self.cross_attention(out_seq, in_seq36)
		out_seq3 = self.ff(out_seq3)

		out_seq4 = self.cross_attention(out_seq, in_seq48)
		out_seq4 = self.ff(out_seq4)

  
        # 各時刻からのデータを足し合わせるレイヤーを追加
		out_seq = self.add([out_seq0, out_seq1, out_seq2, out_seq3, out_seq4])
		#out_seq = self.ff(out_seq5)

		return out_seq

この他、GPVデータを取り出す部分や、学習時にデータを供給するGeneratorの部分も修正する必要がありましたが、データに特化した話なので省略します。

5.2 テキストの与え方の改善

例えば現在が9日であったとすると、予報解説文の中には日本 の 南 の 低気圧 が 10 日 に かけて 接近 するというような文が登場します。ここで10日という単語は、9日から12時間ないし24時間先であることを意味していますが、ニューラルネットワークは現在が9日という事実を知らないために、適当な日付を生成してしまいます。入力するテキストに「今日は9日です」というような言葉が含まれていれば、それをとっかかりとして正しい日付を生成するようになると考えられます。
そこで、入力テキストを構成する際に、
startseq 9 10 10 11 12 日本 の...というように現在と48時間までの日付を示す数値を入れるようにしました。
推論する際には、startseqから始めて、ネットワークが何という単語を予測しようとも上記の日付に置き換えてしまいます。そして7単語目からを予測テキストとして扱うようにします。

この結果、ほぼ正しい日付レンジを生成するようになりました。
以下の例は2日18時を基点としたデータに対する生成文です。与えた日付は、現在である2日、12、24時間後である3日、36、48時間ごとなる4日です。
つまり、
startseq 2 3 3 4 4 ...
で始まるテキストが開始符号となります。

日付が改善された生成文(2023/6/2 18時基点)
1.実況上の着目点

①華中の500hpa5760m付近にはトラフがあって東進。衛星赤外画像では、低気圧性循環が明瞭。
トラフに対応する低気圧が日本の東にあって東北東進。
低気圧に向かって下層暖湿気が流入し、東シナ海では発雷を多数検知。
②中国東北区の500hpa5580m~5640mには、-15℃以下の寒気を伴うトラフがあって東進。
③日本の南には高気圧があって、ほとんど停滞。
④華中の500hpa5760m付近には、トラフがあって東進。
トラフの正渦度極大域に対応して、前線が華中から東シナ海へのびている。

2.主要じょう乱の予想根拠と解説上の留意点
①1項①のトラフは東進し、4日には日本の東で低気圧が発生する。
トラフの東進に伴い、1項②の低気圧は、5日朝には日本の東へ進む。
トラフに対応して、1項③の低気圧は、発達しながら東北東進し、
4日朝には最大風速35kt[gw級]の勢力に達する。低気圧に伴う前線は、5日には日本の南を東北東進、
低気圧や前線に向かって、850hpaθe336k以上の下層暖湿気が流入し、大気の状態が非常に不安定となり、
西日本から東日本の太平洋側を中心に、雷を伴った激しい雨が降る所がある。
土砂災害や低い土地の浸水、河川の増水に注意・警戒し、落雷や突風に注意。
②1項②のトラフは、4日朝には日本海へ進み、5日朝には日本海に達する。
トラフに対応する低気圧が、5日朝にはオホーツク海に達する。低気圧からのびる寒冷前線が、
北日本から西日本には、上空500hpaには-27℃以下の寒気が流入、大気の状態が不安定となり、
雷を伴った激しい雨の降る所がある。また、低気圧や前線の近傍では気圧の傾きが急になり、
海上を中心に強い風が吹いて波が高くなる。強風や高波に注意。
③2項①の低気圧や前線に向かって、850hpaθe324k以上の下層暖湿気が流入し、
大気の状態が不安定となる。西日本や東日本では、4日にかけて落雷や突風、短時間強雨に注意。

6 まとめ

 画像を読み込んで説明文を生成するImage Captioningの技術を応用して、気象可視化画像から解説や今後の注意事項を生成するニューラルネットワークを作成しました。Transformerをベースとして、画像特徴量を合わせて処理します。
このニューラルネットワークを3年分のデータを用いて学習しました。
ネットワークの改善と入力するテキストの改善から、文形態としてはかなりそれらしい説明と数日後の予想が生成できたものが出てきている、という状況です。

3
6
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
6

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?