#気象データをもとに「天気図っぽい前線」を機械学習で描いてみる(4) 白黒天気図をカラー化してみた
前回(第3回)では気象データをもとに「天気図っぽい前線」を機械学習で描いてみる(3) として、教師画像を作成するために「速報天気図」から前線要素を切り抜く話を投稿しました。
今回の話は、(まったく個人的事情ですが)前線要素を切り抜くためのカラー版「速報天気図」の入手可能な対象期間が少なかったため、従来から蓄積していた白黒版「速報天気図」をカラー化してみた、というものです。
白黒版「速報天気図」のカラー化
なぜカラー化か
最近、白黒写真やフィルムを機械学習でカラー化するという話を聞きます。
過去の、どこか現実感がなかった戦時中の映像など、圧倒的に現実感が増して、同時代感まで匂わせて迫ってくるように感じます。
今回のカラー化はそういう話とは全く関係なく、前線要素抽出のアルゴリズム事情だけです。
白黒版の天気図ですと、当然のことながら、前線と、経度緯度線・地図線・等圧線すべてが同じ黒色です。
ですから、前線だけそこから切り出すうまい方法が思いつかず、カラー化する必要が生じました。
というか、ホントはやったことを時系列でいうと、
(1) 出力画像として、前線要素を切り出すとかせず、速報天気図そのものを教師画像として学習するニューラルネットワークを最初に作った。うまく前線が学習されなかった。
(2) 学習対象を前線に絞ろうと考えて、前線要素を抜き出す方法を考えていた時に、カラー版「速報天気図」の存在に気づいた。
(3) 入手可能なカラー版「速報天気図」をダウンロードし、色をベースに前線画像を抜き出すことができた。
(4) 前線だけとした教師画像で学習させたところ、ある程度うまく行きそうとわかったので、もっと天気図例を増やそうと思った。この時点ではカラー版は半年分くらいしか入手できなかった。
(5) このころ作っていたニューラルネットワークは画像から画像を生成するものなので、ひょっとしたら白黒天気図からカラー天気図に変換することもできるのでは?と思いいたった。
というものでした。
画像から画像のニューラルネットワークがある程度動き始めていたので、応用できるかな、と思ったのでした。
結果として、これは思った以上にうまくいきました。
半年ほどしかなかった教師画像を、いっきに2年分以上に増やすことができました。
春夏秋冬を2まわり分得られることで、教師画像のバリエーションも増えました。
作成したニューラルネットワーク
ハードウェア、ソフトウェア環境
Mac mini(2018)
プロセッサ 3.2GHz 6コア Intel Core i7
メモリ 32GB 2667 MHz DDR4
OS macOS Catalina
python3.7
Keras 2.0
を用いております。
CNNの構造
作成していたネットワークはかなりシンプルな構造です。Sequential構造で、1チャネルのグレースケール画像を、Conv2Dによって特徴量を抽出して、最下層で4段のCNNを通した後に、Conv2DTransposeによって戻しながら3チャネルのカラー画像にするというネットワークです。
入力画像は256x256ですが、Conv2Dのstridesを4回かけて小さくし、16x16 にした後でさらにConv2Dを4回通し、
その後Conv2DTransposeによって元のサイズに戻すというもの。
学習はmean_squared_error
を用いていますので、ピクセル値として教師画像に寄せるような学習です。
# parameter settings
num_hidden1 = 32
num_hidden2 = 64
num_hidden3 = 128
num_hidden4 = 64
num_hidden5 = 32
num_hidden6 = 32
######### start of network definition
NN_1 = Sequential()
#--- encode start
NN_1.add(Conv2D(num_hidden1, data_format='channels_first', kernel_size=(3,3), strides=(2,2), activation='relu', input_shape=(in_ch, i_dmlat, i_dmlon), padding='same'))
NN_1.add(Conv2D(num_hidden1, data_format='channels_first', kernel_size=(3,3), activation='relu', input_shape=(in_ch, i_dmlat, i_dmlon), padding='same'))
NN_1.add(Conv2D(num_hidden1, data_format='channels_first', kernel_size=(3,3), strides=(2,2), activation='relu', input_shape=(in_ch, i_dmlat, i_dmlon), padding='same'))
NN_1.add(Conv2D(num_hidden2, data_format='channels_first', kernel_size=(3,3), strides=(2,2), activation='relu', padding='same'))
NN_1.add(Conv2D(num_hidden2, data_format='channels_first', kernel_size=(3,3), activation='relu', padding='same'))
NN_1.add(Conv2D(num_hidden2, data_format='channels_first', kernel_size=(3,3), strides=(2,2), activation='relu', padding='same'))
#--- encode out
NN_1.add(Conv2D(num_hidden3, data_format='channels_first', kernel_size=(3,3), activation='relu', padding='same'))
NN_1.add(Conv2D(num_hidden3, data_format='channels_first', kernel_size=(3,3), activation='relu', padding='same'))
NN_1.add(Conv2D(num_hidden3, data_format='channels_first', kernel_size=(3,3), activation='relu', padding='same'))
NN_1.add(Conv2D(num_hidden3, data_format='channels_first', kernel_size=(3,3), activation='relu', padding='same'))
#--- decode start
NN_1.add(Conv2DTranspose(num_hidden4, data_format='channels_first', kernel_size=(3,3), strides=(2,2), activation='relu', padding='same'))
NN_1.add(Conv2DTranspose(num_hidden4, data_format='channels_first', kernel_size=(3,3), strides=(2,2), activation='relu', padding='same'))
NN_1.add(Conv2DTranspose(num_hidden5, data_format='channels_first', kernel_size=(3,3), strides=(2,2), activation='relu', padding='same'))
NN_1.add(Conv2DTranspose(num_hidden5, data_format='channels_first', kernel_size=(3,3), strides=(2,2), activation='relu', padding='same'))
NN_1.add(Conv2D(num_hidden5, data_format='channels_first', kernel_size=(3,3), activation='relu', padding='same'))
NN_1.add(Conv2D(num_hidden6, data_format='channels_first', kernel_size=(3,3), activation='relu', padding='same'))
#--- back to 3 channel
NN_1.add(Conv2D(3, data_format='channels_first', kernel_size=(3,3), activation='relu', padding='same'))
####### end of network definition
# compile network
NN_1.compile(optimizer='adam', loss='mean_squared_error' , metrics=['accuracy'])
# do training
NN_1.fit(np_i_data_train_tr, np_t_data_train_tr, epochs=num_itter , callbacks=cbks, batch_size=8 , validation_split=0.2 )
# Save model and weights
json_string = NN_1.to_json()
open(os.path.join(paramfiledir, 'cnnSPAStoColSPAS2_011_model.json'),'w').write(json_string)
NN_1.save_weights(os.path.join(paramfiledir, 'cnnSPAStoColSPAS2_011_weight.hdf5'))
学習と変換
入力と出力
入力画像は例えばこんなもの(速報天気図 2018/9/30 21UTC)。
学習結果
半年分のデータのうち、2ヶ月分を評価用にしようとして、およそ4ヶ月分(1日に6枚あるので、700枚強)を用いて白黒版とカラー版を学習させました。
学習データはわりと早いうちにこのような画像に収束するようになります。
地図や経度緯度線、日付はあっという間に過学習していくのでしょう。
予測結果
学習したネットワークを用いて、実際の白黒天気図をカラー化してみました。
右が元となった白黒天気図、左がカラー化された天気図です。
色をベースに前線要素を切り抜く目的としては、十分なカラー化だと思います。
細かい話ですが、左上の年月日時刻が記載されているボックスも画像として学習・予測した結果です。
天気図の中に出てくる数値(気圧とか、移動速度)は素直に予測しているように見えるのに、
「平成29年7月」はガンとして「平成30年1月」としか予測していません。
学習に用いているデータが平成30年以降のもので、かつ7月が含まれていないので、
「入力にないものは予測できません」
といっている感じです。
同じことは代変わりの令和の文字列についても、ガンとして平成と変換していました。
ここは何が来ても平成になるように、重みはゼロでバイアスだけ残ったような学習になったのでしょう。
遊び
前線記号の手書きは私の方が一日の長があるようです。
まとめ
今回は白黒天気図をカラー化するという話についてまとめました。これにより前線切り抜き画像が大幅に増え、最終目的の「天気図っぽい前線」の描画学習の精度が上がっていきました。
次回は最終回として、前線描画のニューラルネットワーク気象データをもとに「天気図っぽい前線」を機械学習で描いてみる(5)機械学習編 Automatic Front Detection in Weather Dataについて投稿予定です。
2020.2.9現在 Qiitaサイトへの画像投稿容量の月間制限に達してしまって最終回は3月に持ち越します。