気象データを見て概況を説明する文章を自動生成させてみる


## 気象データの可視化画像から、天気図の解説文を生成するAI

### 0.はじめに

天気予報の元となる、スーパーコンピューターによって解析・予測された気象データを種類別や地表、上空などの高度別に可視化した画像をインプットすると、専門家が書いたような天気概況の解説文をアウトプットする機械学習プログラムを作成してみました。

具体的には、例えば次のような気象データ可視化画像をインプットします。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411112/5e23a49a-a2d8-98ec-718d-7f7a519a4817.jpeg" width=1200>

すると、このような一般向けの解説文を出力する、というもの。
**「前線が停滞し、沖縄・奄美は雨。気圧の谷の影響により北日本は曇りや雨で、午前中東日本も雨の所も。次第に高気圧に覆われ、西日本は晴れや曇り、午後は東日本も晴れ。」**　

「解説文」と私が書いているのは明確な定義はありませんが、イメージとしてはテレビで天気予報が始まると、最初に天気図をもとに「*日本付近は西高東低の冬型の気圧配置が強まっていて日本海側では雪、太平洋側では晴れて空気が乾燥しています*」みたいな**簡潔な現在の状況**が説明されますよね。その説明文の話です。


手法は、写真を見て写っているものや状況を説明する、**Image captioning**の**Show and Tell**という技術の応用です。Image captioningでは例えば、下の写真を見せて
```a surfer riding on a wave```
といった文を作成します([Image captioning with visual attention (Tensorflow Tutorialのページ）](https://www.tensorflow.org/text/tutorials/image_captioning)より)。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411112/c6296c87-8ad7-834e-85e6-c2b48d71fb19.jpeg" width="400">

**Show and Tell**では写真画像を分類するニューラルネットワークから特徴量を抽出して単語予測に利用しますが、同じように、気象可視化画像を処理するニューラルネットワークをあらかじめ訓練しておき、これを用いて特徴量を抽出します。


一般向け解説文は、気象庁が公開している「日々の天気図」のものを利用します。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411112/a01d1ef3-d5cf-bbbb-0924-b1066e61552b.jpeg" width="450">

これを収集し、1日ごとの天気概況を説明している文を12年分抽出して学習しました。

結果を先に示しますと、この様な解説文が生成できました。
|項目|内容|
|:-:|:-|
|日付|2022/05/17 |
|入力画像群|![gsm_hlayout_rdc_2022051700.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411112/c718fe71-d95f-e857-aed7-8a40db6efb76.png)|
|**生成文**|**前線は本州の南海上に進み西～東日本は雨や曇り。本州の日本海側は晴れ。南西諸島は前線の影響で雨。北海道は気圧の谷の影響で雨や曇り。**|
|気象庁<br>作成解説文|南西諸島～日本の東にのびる前線や日本の東からの湿った空気の影響により本州以南は曇りで太平洋側では雨。前線の影響を受けにくい北海道や西日本の日本海側は晴れや曇り。|
|天気図|![SPAS_COLOR_202205170000.v8.256.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411112/4f0b6277-c726-6c46-9747-e02851b55d4a.png)|


なお、天気図は入力データではありません。

生成文は日本語の文法レベルはほぼクリアしていて、気象学的にどこまでキャプションができているか、を問うことができるかと思います。

この投稿はこのようなテキスト生成についてのお話です。

### 1. 大まかな全体の話

画像から文を生成する大きな流れは、こちらの方の解説；

https://qiita.com/yutaoba/items/6eb0e12ba0d169a480df

の通りです。この解説は素晴らしく分かりやすくて、大いに参考にさせて頂きました。

私の話は自作のCNNもあったりして少々全体の話は長くなっていますので、全体の流れを最初に整理しておきます。

##### (1) 天気概況解説文の話

　生成する解説文の元となる単語は、ある期間(2009年から2021年）の気象庁の[「日々の気象図」](https://www.data.jma.go.jp/fcd/yoho/hibiten/index.html)に記載されている文章に含まれているものです。この文達を収集して単語を抽出、文としてのシーケンス予測に用います。
 
 ##### (2) 気象データを可視化して画像特徴量を抽出する話
　画像特徴量を抽出するCNNとしては、気象データ可視化画像に前線を自動描画するためのものを使用します。可視化した気象画像をCNNに入力して、途中の層からの出力データを抽出します。
このCNNは自作したもので、入力データとラベルデータ（マスク画像）について説明します。
Image Captionの本筋の話から少し逸れますので、読み飛ばしていただいても大丈夫です。

 ##### (3) Image Caption本体（画像特徴量から解説文を生成する）の話
 Image Captioning本体（画像から解説文を生成する）のネットワークを作成します。
解説文を処理する「言語処理」と画像を処理する「画像処理」に大別されます。画像処理部のCNNの中間層で特徴量データを取り出して、言語処理側の解説文生成ネットワークに合流させます。図で示すとこんな感じです。


```mermaid
graph TB
 subgraph 画像処理側
  K(["(2)気象データ画像"])===>D[\CNN encoder/];
  subgraph "(2)前線描画CNN"
  D==>E("中間層<br>(特徴量抽出)");
E-..->F[/CNN decoder\];
  F-.->G("前線画像(出力)");
  L(["(2)前線画像<br>(教師データ)"])-.学習.->G;
  end
 end
 subgraph 言語処理側
  P([Start Token])-->B
  Q(["(1)収集した解説文"])-."テキスト(辞書)".->R("(3)トークン化処理")
  R--->B
  R-..->M
  subgraph 解説文生成ネットワーク
　E=="特徴量"==>C
  B("(3)単語列予測<br>(Embbeding+LSTM)" ) -->C("(3)結合")
  M(["(3)解説文<br>(教師データ)"])-.学習.->H
  C==次トークンを<br>逐次予測===>H("(3)解説文(出力)")
  H-.->B
  end
　end

```
##### (4)作成したネットワークの学習の話
作成したShow and Tellのネットワークを用いて、画像とテキストの関係を学習して、学習に含まれていない画像を見せて解説文を生成させてみます。

##### (5)生成した解説文の評価の話
生成された解説文を見てみます。定量的評価を、実際に気象庁の方が記載された解説文との類似度を定量化する **WRD(Word Rotator's Distance)** という手法でやってみました。

こんな感じで全体を説明していこうと思います。




## 2. 天気概況解説文の話

まずテキストデータ（天気解説文）の話です。
天気解説文は気象庁の[「日々の気象図」](https://www.data.jma.go.jp/fcd/yoho/hibiten/index.html)に記載されている文章です。元ファイルのPDFを収集して、必要な文章を抽出し、ラベルデータとしてニューラルネットワークで利用するための前処理を行います。

```mermaid
graph TB
 subgraph 画像処理側
  K(["(2)気象データ画像"])===>D[\CNN encoder/];
  subgraph "(2)前線描画CNN"
  D==>E("中間層<br>(特徴量抽出)");
E-..->F[/CNN decoder\];
  F-.->G("前線画像(出力)");
  L(["(2)前線画像<br>(教師データ)"])-.学習.->G;
  end
 end
 subgraph 言語処理側
  P([Start Token])-->B
  Q(["(1)収集した解説文"])-."テキスト(辞書)".->R("(3)トークン化処理")
  style Q fill:#f9f
  R--->B
  R-..->M
  subgraph 解説文生成ネットワーク
　E=="特徴量"==>C
  B("(3)単語列予測<br>(Embbeding+LSTM)" ) -->C("(3)結合")
  M(["(3)解説文<br>(教師データ)"])-.学習.->H
  C==次トークンを<br>逐次予測===>H("(3)解説文(出力)")
  H-.->B
  end
　end

```

### 2.1 天気解説文〜「日々の天気図」とは
気象庁は[「日々の天気図」](https://www.data.jma.go.jp/fcd/yoho/hibiten/index.html)というサイトでひと月分の毎日の天気図と、日々の天気の概況を簡潔な文章にしてまとめています。

https://www.data.jma.go.jp/fcd/yoho/hibiten/index.html

この日々の天気概況文を収集して、解説文の教師データとして用います。

本記事の執筆時点では、2002年からの天気図が掲載されています。最新のものは3ヶ月前のものとなっているので、天気図の校正や文章の最終的な確認を終えて掲載されるまで少しタイムラグがあるようです。ここから、1日ごとの天気図のすぐ下に5行で記載されているテキストを抜き出します。

### 2.2 解説文の抽出
データとしてはPDF形式ですので、抜き出しには`pdftotext`というツールを用いました。
抜き出したばかりのテキストには、文章の途中で改行があったりしています。
これらの改ページ記号や改行記号は削除しておきます。
また、事例は少ないのですが、`pdftotext`で抽出すると異なる日の説明がぐちゃっと混じってしまう日もありました。これは目で見て整形してやる必要がありました。あまり洗練された処理ではないですが、内容を知りたい方は下記からどうぞ。

<details><summary>PDFからの抽出処理の具体的内容</summary>

```bash:text_parse1.sh

PDF_FILE=202101.pdf
# ex. 202101.pdf

pdftotext 202101.pdf
# 202101.pdf -> 202101.txt

cat 202101.txt | tr -d "\f" > 202101.2.txt # delete form feed character
# 202101.txt -> 202101.2.txt

awk -v FHDR=202101 -v RS= '{Filename=sprintf("%s-%03d.txt",FHDR,NR) ; print > Filename}' 202101.2.txt
# 202101-???.txt

```
ここまでの処理で、PDFに含まれる様々なテキストが抽出されます。ここから行数が1行のものを除外すると、
「2021年1月」といった表題、天気図の下に記載されている文の表題「1日(金)大雪続く」などが除外されます。

```bash:text_parse2.sh
f_cnt=0
for fn in `ls 202101-???.txt`
 do
  lnum=`cat ${fn} | grep '$' | wc -l`
  if test ${lnum} -eq 1 ; then
    continue # skip sigle line files
  else
    FFILENAME=`printf "%s_CONTENT_%03d.txt" 202101 ${f_cnt}`
    # setting files name as 202101_CONTENT_???.txt

    awk '{printf $0}END{printf "\n"}' ${fn} > ${FFILENAME}
    # add return character to the end of the lines.
    f_cnt=$(($f_cnt+1))
  fi
 done

```

こうして生成されたファイル群には、さらに表題「日々の天気図　No.228」とか、表題下部に記載されている月単位の概況、
「・7日〜11日、強い冬型の気圧配置で、日本海側で大雪。大規模交通障害や除雪作業中の死者も多数。...」
などが含まれます。これら２個のファイルを除外して３個目以降が日々の解説文として抽出されます。
先頭2個のみが除外ファイルなのか、日々の文章が他の日と混じっていないかについては保証の限りではないので、できたファイルは目視確認が必要でした。

</details>

## 3. 気象データを可視化して画像特徴量を抽出する話

さてもう片方の画像データです。
画像から特徴量を抽出するには、**画像分類のためのニューラルネットワーク** を訓練しておいて、データを与えた際の中間層の出力を取得します。


```mermaid
graph TD
 subgraph 画像処理側
  K([気象データ画像])==>D[\CNN encoder/];
 subgraph 前線描画CNN
  D==>E("中間層<br>(特徴量抽出)");
  style E fill:#f9f
  E-->F[/CNN decoder\];
  F-->G("前線画像(出力)");
  L(["前線画像<br>(教師データ)"])-.学習.->G;
 end
 end
```

このようなネットワークとしては例えば **ImageNet** のようなものが有名で、あらかじめ学習済みのデータと合わせてよく用いられています。これに対して、今回は気象画像から前線を描画するネットワークを自作して学習させたものを使います。詳しくは[気象データをもとに「天気図っぽい前線」を機械学習で描いてみる（5）](https://qiita.com/m-taque/items/2788f623365418db4078)に記載しています。


#### 3.1 気象画像の可視化

##### (1)入力気象データ

世界各国の気象機関で、スーパーコンピュータが初期時刻のデータを元に将来の大気の状態を計算して予測したデータが用いられています。日本の気象庁で、計算結果を元に配信しているデータが**全球数値予報モデルGPV**などです。風速や気温といった複数種類の値が、緯度・経度・高度の3次元の格子点上のデータ(GPV;Grid Point Value)として、GRIB2という特別なフォーマットでエンコードされた形式です。世の中にはいくつかGRIB2をデコードするツールがありますが、私はPythonでの利用が便利な **pygrib** というライブラリを使用しています。

実際に可視化した画像はこんな感じです。説明のため2019年の1月分と8月分についてgifアニメ化してあります。解説文には**前線、高気圧、低気圧、台風**といった単語が登場しますが、ニューラルネットワークはこの画像からそれを読み取る必要があります。

地上風・気圧・気温 2019年8月（赤:暖）850hPa 相当温位 2019年1月（赤:暖湿）


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411112/526166d6-45fa-5c30-d97e-588fc3b70afd.gif" width=300>  <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411112/a57e793c-c3a4-24fe-c481-3d9e015452c0.gif" width=300>





直接GPVに含まれていない気象学的な値（相当温位とか湿数）の算出には、[ **metpy** ](https://unidata.github.io/MetPy/latest/index.html#)というライブラリを使用します。

Pythonのコーディングとして可視化に必要なノウハウの詳細は、[気象データをもとに「天気図っぽい前線」を機械学習で描いてみる(2)](https://qiita.com/m-taque/items/988b08185097dca5f5b5)をご参照ください。


<details><summary>GPVについて少し詳しく</summary>


気象庁の配信資料の仕様書では、「 *地球全体の大気を対象に、格子間隔(水平分解能)約 20km 約 13km として、未来の気温、風、水蒸気量、日射量等の状態について、スーパーコンピュータを用いて3次元の格子で予測したデータです。132 時間先まで(9 時、21 時(日本時間)初期値のものに限り 264 時間先まで)の予測を 6 時間毎に発表します。* 」と記載されています（[配信資料に関する技術情報第 601 号 ~ 全球数値予報モデルGPV(日本域)の高解像度化について ~](https://www.data.jma.go.jp/suishin/jyouhou/pdf/601.pdf))。

全球数値予報モデルGPVには次の2種類があって、含まれているデータや時間が微妙に異なっていて、ニーズに合わせて必要なデータを利用することになっています。

|#|GPV名称| |
|:-:|:---|:---|
|1|全球数値予報モデルGPV(全球域)|地球全体のデータが含まれる。6時間ごとの予報値|
|2|全球数値予報モデルGPV(日本域)|日本周囲のみのデータが含まれている。地表面の値は1時間ごとの予報値。上空の値は3〜6時間ごとの予報値|





今回は、このうち「全球数値予報モデルGPV(全球域)」の時刻0（初期時刻）のデータを用います。
データの要素として、以下の7要素を可視化した画像を用意します。
GPVに直接含まれていないもの(#3, #5)もあります。これらは気象状況の解析に用いられる気象学的な値です。GPVに含まれている複数の値から計算します。


|#|気象データ種別|内容|
|:-:|:---------------|:---------------|
|1|地表　風・気圧・気温|地表面での風ベクトル、気圧(等高線)、気温(等高線)|
|2|850hPa 風・高度・気温|850hPa面での風ベクトル、気圧高度(等高度線)、気温(等高線）|
|3|850hPa 相当温位|850hPa面での相当温位(等高線）、空気の暖湿度を示す。気圧、気温、相対湿度から計算する|
|4|700hPa 上昇流|700hPa面での鉛直方向の風速。対流的な空気の流れの強さを表す|
|5|700hPa 湿数|700hPa面の空気の露点温度との温度差。降水活動と関連がある。露点温度は気温と相対湿度から計算する|
|6|500hPa 風・高度・気温|500hPa面での風ベクトル、気圧高度(等高度線)、気温(等高線）|
|7|300hPa 風速(等値線)|300hPa面での風速分布。強風軸の所在を示す|

それぞれ年間を通じて同じカラースケールで可視化します。
![FDN-inputs.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411112/5e23a49a-a2d8-98ec-718d-7f7a519a4817.jpeg)


</details>




##### (2)セマンティックセグメンテーションのラベル画像（前線、気圧等）

セマンティックセグメンテーションを行う上でのGround Truthのマスク画像は、気象庁が作成しているカラー版の天気図を用います。天気図は地図の上に前線や高気圧、低気圧といった記号を表記しているので、天気図から海、陸地、緯度経度線などの地図要素及び等圧線を除くと、マスク画像となります。具体的には赤、青、ピンクのピクセルのみ残して他は白色でマスクします。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411112/f9764ae1-366f-0da0-a784-af03c9c1e8c6.png" width=256>  <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411112/03ff8b3d-3c89-7d5d-7626-49e89fe9301f.png" width=256>
左：速報天気図　右：赤・青・ピンクを切り出したもの 2023/9/30 12時UTC

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411112/0cdc763f-8122-06f9-2d6c-905d5c93134d.png"  width=256> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411112/14b6463c-f1ea-6950-6409-dcc79af58125.png" width=256>
左：速報天気図　右：赤・青・ピンクを切り出したもの 2023/9/7 18時UTC

マスク画像の各ピクセルを色別にクラス化すると、セマンティックセグメンテーションのラベルデータとして用いることができます（[気象データをもとに「天気図っぽい前線」を機械学習で描いてみる(3)](https://qiita.com/m-taque/items/4d5bb45e6b5dc42dc833)）。


###### ラスタ版天気図画像の難点

先ほどの、「色による分類方法」では下記4クラスに分類されます。

|#|分類（ピクセルの色）|対応する気象要素・記号|
|:-:|:-:|:-|
|1|赤|温暖前線、低気圧記号、停滞前線の一部、熱帯低気圧記号|
|2|青|寒冷前線、高気圧記号、停滞前線の一部|
|3|ピンク|閉塞前線、台風記号|
|4|上記以外|上記以外の場所(背景)|

つまり、赤色には温暖前線、低気圧記号、停滞前線の一部が該当しています（停滞前線は赤と青の記号から構成されます）これらは同じクラスに属してしまう、という難点がありました。しかし当初はそのレベルの差異は気にせず、というよりもマスク画像を作るうまい方法だわい、と思って進めていたのが実状です。

###### ベクトル版天気図画像の要素別の抜き出し

その後、気象庁がベクタ版の天気図の配信を行なっていることに気づきました。

https://www.data.jma.go.jp/suishin/jyouhou/pdf/358.pdf

画像ファイルの形式はSVG形式で、ホームページに仕様が説明されています([例えばここ](https://www.data.jma.go.jp/suishin/shiyou/pdf/no12202))。

この形式では、テキスト情報として、要素ごとに記号の位置が記載されています。

```XML:寒冷前線
<g class='coldFront'>
　　<path class='blueSymbol' 
     d='M219.102,232.598 
        A12.4016 12.4016 0 0,1 233.421 242.725 A12.4016 12.40160
        0,1 243.548 228.406 L242.703,228.688 L242.475,228.764 
        L241.398,229.104 L240.314,229.431 L239.227,229.74 
        (中略)
        L220.239,232.469 L219.102,232.598' stroke-width='2'/>
```

元来、ベクトル形式の画像の利点は、画像の拡大・縮小によらずスケーラブルに綺麗な画像を作成することができることです。それに対して、セマンティックセグメンテーションのマスク画像として、この情報を元に分類すると、各クラスに該当する気象要素を完全に一対一にすることができます。


|#|気象要素、記号|
|:-:|:-|
|1|温暖前線|
|2|寒冷前線|
|3|閉塞前線|
|4|停滞前線|
|5|低気圧|
|6|高気圧|
|7|台風|
|8|熱帯低気圧|
|9|上記以外の場所(背景)|

セマンティックセグメーテーションの分類は9種類となります。
ラスタ版速報天気図を用いた場合に比べて、分類に所属する要素が明確になります。
先ほどの9月7日と9月30日のデータは下記のように要素別に抽出できます。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411112/5c00700d-5ab0-2e1c-bc53-e39db9722450.png" width=300>


<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411112/f8d68e0a-eabd-6083-3a04-2cdc062b8461.png" width=300>


9月30日12時(世界協定時)

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411112/b49780f1-163c-f180-cf5f-4d05c5840dae.png" width=300>

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411112/4f00eab8-be27-291e-d9e4-95738acca9e8.png" width=300>

（要素切り出し図は、上段左から高気圧、低気圧、台風記号、中段左から寒冷前線、温暖前線、停滞前線、下段左 閉塞前線、右 熱帯低気圧。両図とも熱帯低気圧は該当なし）

分類クラス数が増えるとセマンティックセグメンテーション学習速度が低下するかな？とも思ったのですが、意外にも学習速度は速くなりました。画像特徴と分類の対応が明確になって、ネットワークが判断しやすくなったのだと思います。

##### (3) 前線画像生成

前線画像生成のCNNの概念図です([詳細はこちら](https://qiita.com/m-taque/items/988b08185097dca5f5b5))。

![FDNet-3.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411112/5a1df9be-369c-9d1b-9316-2465aa2cf2ca.jpeg)

気象データ可視化画像は、256x256、128x128、64x64の3種類のサイズでネットワークに入ってきます。U-NETの深い層に進む（ピクセルサイズが半分になる）とチャネルを倍のサイズにするのが一般的なUNETですが、このネットワークではチャネルはおおよそ1.5倍くらいにしか増やさない構造です。これによってパラメタの数がかなり抑えられています。
最終的に、1枚の画像の各ピクセルが前線要素、擾乱要素（高気圧、台風など）、それ以外のどれに所属するかの確率を計算します。十分学習したネットワークで、確率が最も高い要素に応じた色をピクセルに与えると、前線画像が描画されます。
　
 そのようなモデルによる自動描画された前線の動画(Gifアニメ）の例です。これはベクタ版の教師データを元に学習したものです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411112/ad31704f-f18e-577b-db3a-9adfa9898ae7.gif" width=800>
左側が「前線検出CNN」によって自動描画されたもの、右側が気象庁の実際の解析です。

かなり的確に前線や擾乱を検出しており、解説文の生成に必要な気象画像の認識能力を有していると思います。

#### 3.2 CNNの途中から画像特徴量を抽出する 
画像特徴量のデータはこの「前線検出CNN」の最も深い層で取り出します。先ほどのネットワーク構造図の17の位置です。

<details><summary>画像特徴量抽出して保存する部分のコードです</summary>


```Python:特徴量抽出
 # ネットワークモデルと重みファイル名をセット

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):
        orig_paramfile    = self.model_input_file
        orig_weightfile   = self.model_weight_file

        # ネットワーク全体と重みをロードして、NN_1というネットワークとする
        NN_1 = load_model( orig_paramfile )
        NN_1.load_weights(orig_weightfile)
        #NN_1.summary()

        # NN_1のconv2d_17の結果を取り出し、これをoutputとするネットワークをNN_2として作成する
        layer_name = "conv2d_17"
        NN_2 = Model(inputs=NN_1.input, outputs=NN_1.get_layer(layer_name).output)
        NN_2.summary()

        return NN_2

    def GetGPVData(self, dflist, gflist):

        # 可視化した画像を読み取り、3種類のサイズの画像の配列を返す

		i_data1_list, i_data2_list, i_data3_list = [] , [] , []

		for i_date_cnt, datestr in enumerate( dflist ):

          (中略）

        i_data1_array = np.array(i_data1_list)
		i_data2_array = np.array(i_data2_list)
		i_data3_array = np.array(i_data3_list)

		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)

        # NN_2モデルで予測を行い、特徴量を抽出する
        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):

        # 辞書データを保存する
		dump(self.image_feature_dict, open(filename, 'wb'))

		return
```
</details>

## 4.Image Captioning本体の話

画像処理側の説明が長くなってしまいましたが、ここからがImage Captioningの本体です。

### 4.1 画像特徴量とトークンから解説文を作成するネットワーク

Image Captioningの言語処理側の処理の流れを見ると、
(1)解説文を「トークン化処理」する
(2)トークン情報と画像特徴量から「単語列予測」を行う
という流れになっているのが分かります。

```mermaid
graph TB
 subgraph 画像処理側
  K(["(2)気象データ画像"])==>D[\CNN encoder/];
  subgraph "(2)前線描画CNN"
  D==>E("中間層<br>(特徴量抽出)");
  end
 end
 subgraph 言語処理側
  P([Start Token])-->B
  Q(["(1)収集した解説文"])-."テキスト(辞書)".->R("(3)トークン化処理")
  style R fill:#f9f
  R--->B
  R-..->M
  subgraph 解説文生成ネットワーク
　E=="特徴量"==>C
  B("(3)単語列予測<br>(Embbeding+LSTM)" ) -->C("(3)結合")
  style B fill:#f9f
  style C fill:#f9f
  M(["(3)解説文<br>(教師データ)"])-.学習.->H
  C==次トークンを<br>逐次予測==>H("(3)解説文(出力)")
  style H fill:#f9f
  H-.->B
  end
　end
```


今回の処理では、トークン化処理した言語情報と、特徴量化した画像情報は両方ともdictionary形式で保存した上で、単語列予測処理に渡します。

:::note warn
この投稿で用いているコードはKeras形式です。最近はTensorflow2の形式で書くことが一般的ですし、少し古い形式であることから、コードは折りたたんでおきます。
:::


##### 4.1.1 トークン化処理

トークン化処理とは、文章を単語に分けて、ニューラルネットワークで処理できるように番号を振る処理です。このために[janome](https://mocobeta.github.io/janome/)をimportして使用します。

###### 1) 分かち書き文へ変換

トークン化処理は、文章の単語と単語の間にあるある空白をもとに単語を識別し、単語集（＝ボキャブラリ）を作成して番号を付与するものです。英語のようにもともと単語と単語の間に空白がある言語は、そのまま空白で区切られた単語に番号を振ればトークン化処理ができます。例えば、

```Typhoon No. 21 moves northward south of Japan. Cloudy and rainy in western to eastern Japan. Heavy rainfall of 58.5mm/1h at Kumejima, Okinawa. High pressure over Hokkaido will bring clear skies. ```

のような文章は、空白で区切られているため、```Typhoon, No., 21, moves``` といったように単語を識別することができます。

これに対して日本語は単語と単語は続けて書かれるため、文法や品詞を理解した上で分離する必要があります。

分離のためには、janomeのAnalyzerをimportします。文の判断して単語に分解してくれます。分解した単語列を空白を用いて結合すると、分かち書き文になります。さらに、文の先頭に"startseq"、最後に"endseq"と言う単語を追加してきます。

**元の文**
```台風第21号は日本の南を北上。西～東日本は曇りや雨。沖縄県久米島では58.5mm/1hの激しい雨 。 北海道は高気圧 に覆われて晴れ 。```

**分かち書き**
```台風 第 21号 は 日本 の 南 を 北上 。 西～東日本 は 曇り や 雨 。 沖縄県久米島 で は 58 . 5mm / 1h の 激しい 雨 。 北海道 は 高気圧 に 覆わ れ て 晴れ 。```


このようにして解説文を分かち書きすると「台風」、「第」、...と分離することができます。
この状態で辞書型データとして保管します。

<details><summary>分かち書き処理コード</summary>

```Python:Separate

from janome.tokenizer   import Tokenizer
from janome.analyzer    import Analyzer

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 = (filename_from_date_str)

		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):

		print(f"Creating text dictionary...")

        # 対象日付のリスト(self.datefile_list)から対応するファイルを読み込む
		for date_str in self.datefile_list :

			text_id = date_str
			image_text = self.LoadTexts( self.MakeTxtFileName( date_str ) )

			image_text_separated =\
                                image_text.replace("、","").replace(",","")

			word_list = []

            # janomeの字句解析機能を用いて語句に分離してword_listに追加していく
			for tok in self.a.analyze(image_text_separated):
					word_list.append(tok.surface)

            # word_listに登録された文字を空白を挟みながら結合する
            # これにより、「分かち書き」された文章となる
			image_text_proc  = ' '.join(word_list).replace("、","")

            # 先頭と最後に、開始符号のstartseq、終端符号のendseqをつける。
			image_text_final = 'startseq ' + image_text_proc + ' endseq' 
            
            #分かち書きしたテキストを、テキスト辞書に追加していく 

			if text_id not in self.image_texts_dict:
				self.image_texts_dict[text_id] = []
			self.image_texts_dict[text_id].append(image_text_final)

		print("Done.")

		return self.image_texts_dict

	def SaveDict(self, filename):

		print(f"text dictionary saving to {filename}... ")

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

		print("Done.")

		return
```

</details>

###### 2) 各単語に番号を付与

続いて、入力された全ての分かち書きした文からなる語彙全体に対して一意な番号（トークン）を付与します。
この処理には、KerasのTokenizerを用います。「分かち書きされた文の辞書データ」をTokenizerに渡します。Tokenizerが番号を振った後、ファイルにも保存して、オプションによって再利用できるようにします。

<details><summary>単語への番号付与のコード</summary>

```Python:トークナイズ
from keras.preprocessing.text     import Tokenizer

class Trainer(TXT_Proc.TextPreprocessor):

	def __init__(self, \
                   features_dict, texts_dict, epochs, \
                   use_flag, token_file\
                ):

		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.tokenizer           = Tokenizer()
        # Kerasのトークナイザ

		return

	def __dictToList(self, texts_dict):

		texts_list = []

        # 辞書型データからリストに変換
		for key in texts_dict.keys():
			for d in texts_dict[key]:
				texts_list.append(d)

		return texts_list

	def MakeTokenizer(self,flag):

        # 通常は"new"で実施
        # テキスト辞書を受けとり、リストに変換した上でTokenizerを用いて番号を付与
		if flag == "new" :
			train_texts_list = self.__dictToList(self.train_texts_dict)
			self.tokenizer.fit_on_texts(train_texts_list)
			dump(self.tokenizer, open(self.token_file, 'wb') )
		else:
			self.tokenizer = load( open(self.token_file,    'rb') )
		
		return None
```

</details>


##### 4.1.2 単語列予測ネットワーク

画像特徴量、トークン化された解説文と言う必要データが揃いました。
これをもとに、単語列予測するネットワークを作成します。

###### 1) 画像特徴量の1次元化

画像特徴量は中間層から16x16(サイズ)x120(チャネル)のデータで抽出されます。
これをFlatten層及びDense層を通すことで、**256要素の1次元ベクトル**に変換します。

###### 2)解説文(トークン）のEmbbedingと次のベクトルの予測

トークン化された解説文は数列ですが、各トークンが256要素のベクトルで表現できるように、Embedding層で変換します。文の最大長、ボキャブラリサイズ（トークンの最大値）をそれぞれ$L_s, N_v$とすると、

トークン化された解説文 
``3, 8, 10, ...　``（$L_s$の長さの数列）
は、one hot化されて
``　[[0,0,0,1,...Nv], [0,0,...,1,...,Nv],...] ``
という$N_v ✖️ L_s$の配列になります。この配列をEmbeddingする際に、要素次元数を画像特徴量のベクトルと合わせます。この結果、一つの単語を表すのに用いられる1個の$N_v$次元のone-hotベクトルは、1個の256要素のベクトルとなります。そしてひとつの解説文は、$256✖️L_s$の配列として表現されます。
（なおそれぞれの解説文は長さがまちまちなので、長さが$L_s$に満たない場合、残りの要素はゼロでpaddingされます）

###### 3)LSTM層で入力ベクトル列に基づいて次のベクトルの予測
ベクトル列はLSTM層に通して、引き続く1個のベクトル(256要素)を予測します。

###### 4)画像特徴量ベクトルと予測ベクトルの足し合せ
　**予測された1個のトークンベクトル**（256要素）、画像処理部から渡ってきた1個の256要素の**特徴量ベクトル**が足し合わされます。これによって次の単語を予測する際に、可視化画像の情報が組み込まれます。

###### 5)次トークンの確率計算
足し合わされたベクトルはDense層によって$Nv$を要素数とするベクトルに変換されます。このベクトルは、ボキャブラリを構成している各語に関して、次の単語として尤もらしい確率を示しています。softmax層を通して、最も確率が大きな要素が次の単語のトークンとなります。学習の際はこのように「文字列＋画像特徴量」から「次の文字」の組みを予測できるようにtrainします。

予測の際は、最初は「startseq」を入力して画像特徴量と合わせて「次の文字」を予測します。これを入力した文字に継ぎ足したものを新しい文字列として、再度その次の文字を予測する...を繰り返します。そうして「endseq」が予測されるか、最大単語数に到達するまで予測を繰り返します。こうすると、画像のみが入力されて、これを元に予測された文字列が出力されます。


```Python:単語列予測
def MakeCaptioningModel(self, modelfile, weightfile, modelflag):

		if modelflag == 'new' :

			# 1) 画像特徴量の1次元化 -> batch x 256
            inputs1 = tf.keras.Input(shape=(16,16,120)) # cond2d_17
			ie1x    = tf.keras.layers.Flatten()(inputs1)
			ie1     = tf.keras.layers.Dropout(0.5)(ie1x) 
			ie2     = tf.keras.layers.Dense(256, activation='tanh')(ie1)

			# 2）解説文（トークン）のEmbedding -> batch x 256
			inputs2 = tf.keras.Input(shape=(self.max_length,))
			se1     = tf.keras.layers.Embedding(\
                        self.vocab_size, 256, mask_zero=True\
                      )(inputs2)
			se2     = tf.keras.layers.Dropout(0.5)(se1) ###

            # 3) 次ベクトルの予測（LSTM）
			se3     = tf.keras.layers.LSTM(256)(se2)

			# 4) 画像特徴量、次トークンの足し合せ
			decoder1 = tf.keras.layers.Add()([ie2, se3])
			decoder2 = tf.keras.layers.Dense(\
                         256, activation='sigmoid')(decoder1)
            # 5) 次トークンの確率計算
			outputsx = tf.keras.layers.Dense(self.vocab_size)\
                         (decoder2)
			outputs  = tf.keras.activations.softmax( outputsx )

			# model compile
			c_model = tf.keras.Model(\
                      inputs=[inputs1, inputs2], outputs=outputs\
                      )

		else: # 保存モデルや保存重みから再開する場合

			print(f"caption network model read from {modelfile}")
			print(f"caption network weight read from {weightfile}")
			c_model = load_model(modelfile)
			c_model.load_weights(weightfile)

		c_model.compile(\
           loss=tf.keras.losses.CategoricalCrossentropy(), \
           optimizer='adam' \
         )
		c_model.summary()

		self.model = c_model

		return
```


#### 4.1.3 学習処理

このメソッドが呼ばれると、
 トークナイザ```MakeTokenizer```
 ネットワーク定義```MakeCaptionModel```
などが呼ばれて、続いて```fit_generator```を用いた学習が実行されます。
学習にあたっては```DataGenerator```を用いてデータを与えます。

<details><summary>ネットワーク学習部分のコード</summary>

```Python:学習
	def MakeInputOutput(self, image_texts, image_feature):
        #テキストを入力のトークン列と次のトークンに分割する処理

		X1, X2, y = [], [], []

		for image_text in image_texts:
            # テキストをトークンに置き換える
			seq = self.tokenizer.texts_to_sequences([image_text])[0]

            # トークンを（1からi番目のi個のトークン）、（i+1番目の1個のトークン）に分割
			for i in range(1, len(seq)):
				in_seq, out_seq = seq[:i], seq[i]
				in_seq  = pad_sequences([in_seq], maxlen=self.max_length)[0]
				out_seq = to_categorical([out_seq], num_classes=self.vocab_size)[0]
				X1.append(image_feature) # 画像データ
				X2.append(in_seq) # （1からi番目のi個のトークン）
				y.append(out_seq) #  (i+1番目の1個のトークン）

		return np.array(X1), np.array(X2), np.array(y) 

	def DataGenerator(self):
        # fit_generatorから呼ばれるgeneratorの処理
		while 1:
			for key, image_texts in self.train_texts_dict.items():
                # 解説文テキストの辞書からテキストを順次取り出す

				image_feature = self.train_features_dict[key]
                # 対応する画像を読み出す

                # テキストを分割する
				in_img, in_seq, out_word = \
                  self.MakeInputOutput(image_texts, image_feature)

                # 入力画像＋入力トークン列、出力トークンをyieldで返却
				yield [[in_img, in_seq], out_word]

	def TrainModel(self, \
          model_file, saved_model_file, \
          weight_file, saved_weight_file, 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
		saved_flag = flag
		self.MakeTokenizer(flag) #トークン化処理
		self.GetVocabSize()      #全解説文に存在する語彙の数を取得
		self.GetMaxLength()      #解説文の最大長を取得

        # ネットワークを定義
		self.MakeCaptioningModel(\
            self.saved_model_file, self.saved_weight_file, \
            saved_flag)

		steps=len(self.train_texts_dict)

		_mfile = self.model_file 
		_wfile = self.weight_file 

		for i in range(1):
			print("epoch:{0}/{1} steps={2}".format(i,self.epochs,steps))
			generator = self.DataGenerator() #generatorを定義

			# Mac M1/2のLSTMがGPUで遅い問題のためCPU処理で対応
			with tf.device('/cpu:0'):
				self.model.fit_generator(generator, \
                   epochs=self.epochs, steps_per_epoch=steps, verbose=1)

			self.model.save(_mfile)
			self.model.save_weights(_wfile )

		print("Training End.")

		return None
```

</details>

入力データは、```DataGenerator```メソッドで生成されます。

画像特徴量は辞書である```train_features_dict```から読み取られます。
トークンは解説文全体が辞書```train_texts_dict```から読み取られます。

読み取った解説文は、```MakeInputOutput```メソッドに渡されて、「部分トークン(列)」と「次の1個のトークン」というペアに分割され、それぞれ入力と出力になります。

具体的な文字列で見てみると、例えば
「startseq 前線　や 低気圧 の 接近 と 日本 の 東 の 高気圧 から の 湿った 空気 endseq 」
が入力文字列の場合、

 入力：画像特徴量、"**startseq**"   　　    　　 ->出力:"**前線**"
 入力：画像特徴量、"startseq **前線**" 　　-> 出力:"**や**"
 入力：画像特徴量、"startseq 前線 **や**"  　-> 出力:"**低気圧**"

...となるようにデータのセットが用意されます。これを用いてトークン列の次に来る単語を予測するように学習が行われます。

このほか、最大長や語彙数の取得のためのメソッドを含めてClass定義が完了します。

<details><summary>最大文字列長、語彙数取得</summary>

```Python:utils
	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
```
</details>

#### 4.1.4 予測処理

前述したように、予測の際は、最初は「startseq」を入力して画像特徴量と合わせて「次の文字」を予測します。これを入力した文字に継ぎ足したものを新しい文字列として、再度その次の文字を予測する...を繰り返します。そうして「endseq」が予測されるか、最大単語数に到達するまで予測を繰り返します。こうすると、画像のみが入力されて、これを元に予測された文字列が出力されます。

<details><summary>予測処理</summary>


```Python:予測

from janome.tokenizer   import Tokenizer
from janome.analyzer    import Analyzer

class Predictor():

	def __init__(self, model_file, weight_file, token_file, max_length,
        image_feature_dict):
		self.model_file = model_file
		self.weight_file = weight_file
        # 学習したモデルのロード
		self.model      = load_model(self.model_file)
        #　トークン情報のロード
		self.tokenizer  = load(open(token_file, "rb"))
		self.max_length = max_length
		self.image_feature_dict = image_feature_dict
        # 学習したモデルの重みのロード
		self.model.load_weights(self.weight_file)

		return

	def IDToWord(self, integer):

        for word, index in self.tokenizer.word_index.items():
		  if index == integer :
		    return word

		return None


	def Inference(self):


		text_list = [] # 結果格納用リストの初期化

		for key , image_f in self.image_feature_dict.items():
            # 予測対象の画像特徴量を辞書型ファイルから読み出す
            # 1日分の画像情報(16,16,120)をネットワークに入力するため(1,16,16,120)にリサイズ
			image_feature = (self.image_feature_dict[key]).reshape(1,16,16,120)

			text = "startseq"　#最初にstart文字を与える

			for i in range(self.max_length):
				seq = self.tokenizer.texts_to_sequences([text])[0]
                # テキストをトークン情報に変換

				seq = pad_sequences([seq], maxlen=self.max_length)
                # 単語が無い部分を最大長までパディング

				yhat = self.model.predict([image_feature, seq], verbose=0)
                # 次の単語の確率を予測

				yhat = np.argmax(yhat)
                # 最も確率の高い単語を取得
                
                word = self.IDToWord(yhat)
				if word is None:
					break
				text += " " + word   
				if word == "endseq":
					break

　　　　　　　text_pair = [ key , text ] 

			text_list.append(text_pair)

		return text_list	
```

</details>

### 4.2 全体処理

ここまでの処理を呼び出すmainのコードは下記のようになります。
入力画像、テキストデータの処理は対象日付のファイル名を取ってくる部分に特化したコードなので省略しています。

<details><summary>全体処理</summary>

```Python:main

'''
（省略した処理）
 :指定した範囲の月日の画像、テキストのリストを作成）
 :画像、テキストの読み込み処理
'''

# 前線抽出ネットワークの定義 
cnn_p = IMG_NetGen.IMG_CNN_AE( 省略  ) 
model_new = cnn_p.ImgProcNet()

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

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

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

# Image Captioning ネットワークの定義

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

with tf.device('/cpu:0'):
	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)

with tf.device('/cpu:0'):
	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
     )
with tf.device('/cpu:0'):
	pred_text_list=pr.Inference()

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

</details>



## 5.作成したネットワークの学習の話
### 5.1 学習の状況

 2009年から2021年の日々の解説文、その日の気象データの可視化画像を学習しました。
そして2022年から2023年(9月まで）のデータを用いて推論させました。


|#|種別|数値|
|:-:|:-|:-:|
|1|学習対象データ数|4746|
|2|語彙最大数|9959|
|3|最大文字列長|55|
|4|学習パラメタ数|13.5M(13,564,647)|
|5|学習時間|約880秒/epoch|
|6|推論対象データ数|638|

学習に用いたのは、Mac Studio(2023モデル）です。

|#|項目|スペック|
|:-:|:-|:-|
|1|Chip|Apple M2 Max CPU:12core(4xEfficiency 8xPerformance)<br> 30core GPU|
|2|Memory|64GB|
|3|Memory B/W|400GB/s|
|3|OS|Mac OS Ventura 13.6|
|4|Storage|1TB SSD|

MacシリコンのMシリーズに対応したTensorflowやPytorchが出現したことで、Macでの機械学習が素晴らしくスピードアップしました。ところがLSTMがGPU対応していないらしく、現在のところCPUのみで実行するwalk aroundを行なっています。それでも以前使用していたMac Mini 2018に比べると桁違いの速度です。このMac Studioを入手したことで、このネットワークの学習が可能となりました。

なお、前線検出CNNの学習はGPUがフル活用されて高速です。

### 5.2 解説文の生成例

予測は、学習データに含まれていない2022年1月1日〜2023年9月30日の画像を対象に行いました。

#### (1) 比較的うまく予測した例

比較的うまく予測した例の入力画像と生成された解説文を、気象庁が実際に作成した解説文と合わせて示します。

##### 1) 2022年1月30日

|項目|内容|
|:-|:-|
|入力画像群|![gsm_hlayout_rdc_2022013000.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411112/0cf441a3-7285-eb76-2b48-722c88e49724.png)||
|生成解説文|冬型の気圧配置が続き日本海側で雪や雨。太平洋側は晴れ。北海道では最低気温が平年より5℃以上低い所も。|
|発表された<br>解説文|北海道上空約5000mに-40℃以下の寒気が流入。山陰～北日本日本海側は雪。東～北日本太平洋側は概ね晴れ。沖縄～西日本は低気圧の影響で曇りとなり、沖縄～九州では所々雨。
|天気図|![SPAS_COLOR_202201300000.v8.256.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411112/a960b633-19d8-8ce4-5fc7-2e849f4fac0f.png)|


上段のAI Generatedが生成した文章、下段が気象庁による実際の解説と天気図です。
この日の生成文は、日本海側が雪で太平洋側は晴れであること、北海道に寒気が流入していることを捉えているようです。

##### 2) 2022年3月7日
|項目|内容|
|:-|:-|
|入力画像群|![gsm_hlayout_rdc_2022030700.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411112/8923062b-aeb0-9709-c0f2-d66bf09e469a.png)|
|生成解説文|東シナ海の低気圧に近い沖縄で雨。西～東日本の日本海側では雪や雨太平洋側は晴れ。北日本は上空の寒気の影響で北日本日本海側では雪。沖縄は気圧の谷の影響で曇りや雨。|
|発表された<br>解説文|冬型の気圧配置は西から次第に緩み、夜は広く高気圧に覆われる。近畿から北の日本海側は雪や雨。西～東日本は太平洋側中心に概ね晴れ。南西諸島は前線を伴う低気圧の影響で雨。|
|天気図|![SPAS_COLOR_202203070000.v8.256.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411112/c6fd1b96-5970-2316-d2a0-6233f5ac9be8.png)|

この生成文は東シナ海の低気圧について触れています。天気図を入力しているわけでは無いのですが気象データ可視化画像のパターンから、低気圧を認識しているようです。


##### 3) 2022年6月14日
|項目|内容|
|:-|:-|
|入力画像群|![gsm_hlayout_rdc_2022061400.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411112/457ae46a-9960-2d43-f1f5-71911d5b45ec.png)|
|生成解説文|梅雨前線上の低気圧が本州南岸に停滞し前線上の低気圧が本州南岸へ進む。九州南部で非常に激しい雨。長崎県島原で775mm1hの非常に激しい雨。|
|発表された<br>解説文|西日本南岸の梅雨前線上を低気圧が東進。北日本は千島近海の高気圧に覆われ概ね晴れ。その他は雨や曇り。南西諸島は朝晩激しい雷雨。近畿・東海で梅雨入り。|
|天気図|![SPAS_COLOR_202206140000.v8.256.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411112/55bcaf24-c01f-6fff-5864-4627a32d4cae.png)|


梅雨前線や前線上の低気圧を認識しています。九州南部で激しい雨と言っています。問題は775mhとい表記です。これは77.5mhという数値が学習した解説文にあるからですが、「.」を予測していなのでおかしな数値に見えてしまいます。

##### 4) 2022年9月1日
|項目|内容|
|:-|:-|
|入力画像群|![gsm_hlayout_rdc_2022090100.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411112/057ca70e-3337-f5d5-c9ba-a15aff747abf.png)|
|生成解説文|台風第10号の影響で西日本太平洋側を中心に大雨。非常に激しい台風第10号は非常に強い勢力で非常に激しい雨。九州北部・東北を中心に非常に激しい雨。|
|発表された<br>解説文|2022/09/01 JMA captioned: 南西諸島は沖縄の南の台風第11号により雨。西～北日本は、低気圧や前線、湿った空気の影響で雨や雷雨となり各地で非常に激しい雨。広島県甲田73.5mm/1hは観測史上1位。|
|天気図|![SPAS_COLOR_202209010000.v8.256.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411112/cd918247-d016-677c-c9d9-db10d9ce3905.png)|

上図は台風とその影響で雨が強いことを認識しているようです。台風の番号はニューラルネットワークとしては知りようがありませんので、でたらめに生成しているようです。


他の図も確認したところでは生成された予測文の全般として、
・各季節の特徴的なパターンの認識はできている模様（冬型気圧配置、梅雨前線、台風など）。
・低気圧などの特徴的な擾乱の位置を、日本海、東シナ海や太平洋側といった場所で学習している
・激しい雨、強い風の生じているところを押さえている
ということが言えそうです。

#### (2) その他の予測の実情

気象庁の「日々の天気図」の様に、生成文を1ヶ月分を並べてみます。

2022年1月の生成解説文です。

![ww.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411112/9057fe01-b3b5-9b9b-002a-fb837b7cd569.png)


このうち、2022年1月1日〜1月5日の生成文について、生成解説文と気象庁作成文を並べてみます。
気象庁作成文と<font color="blue">文意が似ている生成箇所に青字</font>、<font color="red">異なっている生成箇所に赤字</font>としてみました。

|項目|値|
|:-|:-|
|年月日|2022/01/01|
|生成解説文|<font color="red">冬型の気圧配置が続き</font><font color="blue">全国的に雪や雨太平洋側では概ね晴れ</font>。気温は全国的に平年より低く冬日の所が。気温は全国的に平年より低く冬日の所も。
|気象庁作成文|<font color="red">強い冬型の気圧配置は西から次第に緩む</font>。<font color="blue">近畿以北の日本海側中心に雪</font>となり、大阪で初雪。新潟県や山形県では非常に強い風の吹いた所も。<font color="blue">九州や太平洋側は概ね晴れ</font>。|

→なぜか、**気温は全国的に平年より低い**ことを繰り返して言っています。

|項目|値|
|:-|:-|
|年月日|2022/01/02|
|生成解説文|日本海の<font color="blue">低気圧の影響で近畿～東北の日本海側で雪や雨</font>。太平洋側で風強く<font color="red">太平洋側でも雪</font>。西寄りの風が強く全国の観測地点でも氷点下。横浜市でウメ開花。|
|気象庁作成文|前線を伴った低気圧が夕方以降、北海道を通過。<font color="blue">前線や低気圧の通過により西～東日本日本海側と北日本は次第に雪や雨</font>。沖縄・奄美は曇りや雨。<font color="red">西～東日本太平洋側は概ね晴れ</font>。|

->「**横浜市でウメ開花**」なんてどこから捻り出したんでしょう。

|項目|値|
|:-|:-|
|年月日|2022/01/03|
|生成解説文|<font color="blue">冬型の気圧配置が強まり西～北日本日本海側は雪や雨</font>。北陸や北日本日本海側では雪や雨。北陸も雪が降りふぶきの所も。東北では最低気温が平年より1～2℃低い所も。大分市でウメ開花。|
|気象庁作成文|<font color="blue">冬型の気圧配置となり、東～北日本日本海側を中心に雪や雨</font>。西日本と東～北日本太平洋側では概ね晴れ。沖縄・奄美は概ね晴れたが、湿った空気の影響で曇りや雨の所も。|


|項目|値|
|:-|:-|
|年月日|2022/01/04|
|生成解説文|<font color="blue">北日本の上空約1500mには12℃以下の寒気が入り日本海側は雪太平洋側は晴れ</font>。日本海側は雪や雨または雪。山形県肘折では最も遅い積雪の深でも積雪。|
|気象庁作成文|冬型の気圧配置が強まり<font color="blue">北日本の上空約1500mには-12℃以下の寒気が流入。山陰以北の日本海側中心に雪</font>。福島県桧枝岐で日降雪量54cm。九州や関東、<font color="blue">北海道太平洋側は概ね晴れ</font>。|

->細かく見ると、「**日本海側は雪や雨または雪**」とか、「**積雪の深でも積雪**」とか変な日本語がある日でした。

|項目|値|
|:-|:-|
|年月日|2022/01/05|
|生成解説文|<font color="red">冬型の気圧配置が続き日本海側は雪や雨</font>。最低気温は全国的に平年より低く北海道枝幸町歌登で最低気温314℃と史上1位の日降雪量。<font color="red">水戸で初雪</font>。松山市でツバキ開花。|
|気象庁作成文|<font color="red">冬型の気圧配置が次第に緩み、日本海側の雪の範囲は縮小</font>。華南から東シナ海に前線が進み沖縄・奄美と西日本は次第に雨で山間部では雪。<font color="red">東～北日本太平洋側は晴れ</font>。|

-> 実際より全国的に雪がちで寒い日であった様な解説文が生成されており、概況の認識が間違っている様です。

次に、夏季のデータです。2022年8月を例とします。

![ww2208.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411112/4c5034e3-9c32-ddf0-f7eb-3d7377040dfa.png)

同じように最初の5日分を比較してみます。

**2022/08/01**
**生成解説**  : 台風第8号は日本の東を北上。<font color="blue">湿った空気の影響で西日本は曇りや雨</font>。<font color="blue">その他は太平洋高気圧に覆われ晴れて気温が上昇</font>し北海道を中心に猛暑日。その他の地方は曇りや雨。
**気象庁作成文**: <font color="blue">高気圧に覆われ中国～東日本は晴れて高温</font>。前線の影響を受けた<font color="red">北日本と、</font><font color="blue">湿った空気の影響を受けた沖縄～四国は曇りや雨</font>。最高気温は山陰～東北南部の11地点で観測史上1位。


|項目|値|
|:-|:-|
|年月日|2022/08/02|
|生成解説|<font color="red">台風第5号は温帯低気圧に。影響で沖縄・奄美は台風第5号や台風第5号は北上し東進。北日本は気圧の谷や湿った空気の影響で曇りや雨。と</font>|
|気象庁作成文|<font color="red">高気圧に覆われ沖縄～東日本は概ね晴れ。低気圧や前線により北関東や北日本は雨や雷雨で猛烈な雨の所も。最高気温は茨城県鹿嶋と山梨県山中で観測史上1位、225地点で猛暑日。</font>|

-> 残念ながらこの日は上手くいっていないです。概況レベルの認識が間違えています。日本語的にちょっと怪しい。


|項目|値|
|:-|:-|
|年月日|2022/08/03|
|生成解説文|低気圧や前線の影響で西日本日本海側や北日本は雨。北日本は低気圧や前線の影響で雨。前線が東進し東進。北日本は前線の影響で雨。|
|気象庁作成文|日本海側から非常に湿った空気流入し北陸以北は雨や曇りで猛烈な雨、線状降水帯も多発。他は晴れで所々雷雨。新潟県三面94.5mm/1h、高根395.5mm/日など観測史上1位多数。|

-> たどたどしい日本語ですが、全国的に**雨**であることは認識している様です。このように極端な事象は学習文中の例が少ないので予測がなかなか難しいかもしれません。

|項目|値|
|:-|:-|
|年月日|2022/08/04|
|生成解説文|台風第8号から変わった<font color="blue">暖かく湿った空気の影響</font>で九州～<font color="blue">東北</font>の広い範囲で<font color="blue">雨</font>。東北は上空寒気の影響で大気の状態が不安定となり所々で雷雨。|
|気象庁作成文|前線に向かって<font color="blue">暖かく湿った空気が流入し</font>北陸や<font color="blue">東北で記録的大雨</font>、大雨特別警報発表。最上川氾濫。福井県では線状降水帯発生。新潟県下関で149mm/1hなど観測史上1位。|

->　実際の解説文からは、東北地方の雨が凄まじいことがわかりますが、生成文はそこまでのニュアンスは感じられません。8月3日の例と同様に、極端な事象の予測は課題です。

|項目|値|
|:-|:-|
|年月日|2022/08/05|
|生成解説文|台風第8号の影響で近畿～東北の一部で<font color="red">にわか雨。その他は高気圧に覆われて概ね晴れ</font>。台風第8号は小笠原近海を北上。|
|気象庁作成文|上空寒気や暖かく湿った空気の流入により西日本や北陸で<font color="red">大雨。福井･滋賀･島根県では記録的短時間大雨情報発表が同地域に複数回発表も</font>。福井県今庄74mm/1hなど観測史上1位。|

-> これも実際は**大雨**、生成文のニュアンスは**にわか雨**なので大きく外れているといえそうです。


## 6.生成した解説文の評価の話

### 6.1 WRDについて
これまでみてきた解説文の評価は主観的な判定なのですが、気象庁が実際に作成した文との類似度を客観的に判定できないでしょうか？　東北大学の乾研究室が発表しているWRD(Word Rotator's Distance)という文の「類似度」を定量化する手法があります。解説、コードの実装が下記にあります。

https://qiita.com/kenta1984/items/bad7e2f68331849d0053

https://github.com/kenta1984/wrd

この手法を用いて、今回の生成テキストについて、気象庁が実際に記述した解説文と比較をしてみました。
WRDの類似度は、文章に含まれる単語をWord2Vecによって数値化したものをベースに測定されます。単語の重要度を考慮して、文の意味の離れ具合を単語の分散表現空間上で算定します。数値が小さいほど2つの文章が類似していることを表します。またWord2Vecが使用する辞書によって数値が異なってきます。

### 6.2 使用した辞書
辞書としては、先ほどの解説と実装をされた方のページで推薦されている、日本語のwikipediaページでWord2Vecを学習したもの（jawiki.word_vectors.200d.txt）及び、今回の学習に用いた解説文のみを用いてWord2Vecで作成したものの両方で実施してみました。

### 6.3 全体的な傾向

まず、生成した文章（2022年1月から2023年6月まで）の全体的な傾向を見ておきます。
|#|項目|Wikipedia辞書|解説文のみの辞書|
|:-:|:-:|:-:|:-:|
|1|文書サンプルの数|515|515|
|2|類似度平均|0.290|0.517|
|3|類似度最小|0.148|0.318|
|4|類似度最大|0.408|0.734|
|5|類似度上位の日付|2022/5/17<br>2022/1/28<br>2022/5/15<br>2023/5/11<br>2022/4/10<br>|2022/5/17<br>2023/5/11<br>2022/5/15<br>2023/4/20<br>2022/6/28|

「解説文のみの辞書」を用いた方が、母集団の単語と文が気象解説文に偏った中の判断となることから、数値としては厳しめに判定される（数値が大きくなる）のだと思われます。

### 6.4 生成文個別の類似度

さてこれを用いて**5.2**で主観的に「比較的うまく予測できた」と判断して記載した生成文についてみてみます。

|項目|内容|
|:-|:-|
|日付|2022/01/30|
|生成文|冬型の気圧配置が続き日本海側で雪や雨。太平洋側は晴れ。北海道では最低気温が平年より5℃以上低い所も。|
|気象庁作成文|北海道上空約5000mに-40℃以下の寒気が流入。山陰～北日本日本海側は雪。東～北日本太平洋側は概ね晴れ。沖>縄～西日本は低気圧の影響で曇りとなり、沖縄～九州では所々雨。
|類似度|**0.3033(Wikipedia辞書:平均0.290)**<br> **0.5310(解説文のみの辞書:平均0.517)**|


|項目|内容|
|:-|:-|
|日付|2022/03/07|
|生成文|東シナ海の低気圧に近い沖縄で雨。西～東日本の日本海側では雪や雨太平洋側は晴れ。北日本は上空の寒気の影響で北日本日本海側では雪。沖縄は気圧の谷の影響で曇りや雨。|
|気象庁作成文|冬型の気圧配置は西から次第に緩み、夜は広く高気圧に覆われる。近畿から北の日本海側は雪や雨。西～東日本は太平洋側中心に概ね晴れ。南西諸島は前線を伴う低気圧の影響で雨。|
|類似度|**0.1973(Wikipedia辞書:平均0.290)**<br> **0.4029(解説文のみの辞書:平均0.517)**|


|項目|内容|
|:-|:-|
|日付|2022/06/14|
|生成文|梅雨前線上の低気圧が本州南岸に停滞し前線上の低気圧が本州南岸へ進む。九州南部で非常に激しい雨。長崎県島原で775mm1hの非常に激しい雨。|
|気象庁作成文|西日本南岸の梅雨前線上を低気圧が東進。北日本は千島近海の高気圧に覆われ概ね晴れ。その他は雨や曇り。南西諸島は朝晩激しい雷雨。近畿・東海で梅雨入り。|
|類似度|**0.3357(Wikipedia辞書平均:0.290)** <br> **0.5896(解説文のみの辞書:平均0.517)**|

|項目|内容|
|:-|:-|
|日付|2022/09/01|
|生成文|台風第10号の影響で西日本太平洋側を中心に大雨。非常に激しい台風第10号は非常に強い勢力で非常に激しい雨。九州北部・東北を中心に非常に激しい雨。|
|気象庁作成文|南西諸島は沖縄の南の台風第11号により雨。西～北日本は、低気圧や前線、湿った空気の影響で雨や雷雨となり各地で非常に激しい雨。広島県甲田73.5mm/1hは観測史上1位。|
|類似度|**0.3358(Wikipedia辞書平均:0.290)** <br> **0.6724(解説文のみの辞書:平均0.517)** |

主観的に選んだものは、類似度としては全体の平均より悪いと判定されるものもありました。

### 6.5 類似度が高いと判断されたデータ

両方の辞書のトップ5に共通に登場している3個のデータについて記載します。


|項目|内容|
|:-:|:-|
|日付|2022/05/17 |
|生成文|前線は本州の南海上に進み西～東日本は雨や曇り。本州の日本海側は晴れ。南西諸島は前線の影響で雨。北海道は気圧の谷の影響で雨や曇り。|
|気象庁|南西諸島～日本の東にのびる前線や日本の東からの湿った空気の影響により本州以南は曇りで太平洋側では雨。前線の影響を受けにくい北海道や西日本の日本海側は晴れや曇り。|
|天気図|![SPAS_COLOR_202205170000.v8.256.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411112/4f0b6277-c726-6c46-9747-e02851b55d4a.png)|
|類似度|**0.1480(Wikipedia辞書平均:0.290)** <br> **0.3178(解説文のみの辞書:平均0.517)** |

|項目|内容|
|:-:|:-|
|日付|2022/05/15 |
|生成文|南西諸島は前線の影響で雨。北日本は低気圧の影響で雨や曇り。前線の影響で南西諸島は午後所々で雨。西日本～北日本は高気圧に覆われ概ね晴れたが午後は沖縄・奄美は雨。|
|気象庁|南西諸島～日本の南に前線停滞。沖縄・奄美は雨。西～東日本は曇りで南岸中心に雨。北日本は高気圧に覆われ晴れたが寒気の影響で一部で雨や雷雨。沖縄県久米島で44.5mm/1h。|
|天気図|![SPAS_COLOR_202205150000.v8.256.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411112/6a21420f-fe91-ab1a-4d9d-560106b53f9b.png)|
|類似度|**0.1727(Wikipedia辞書平均:0.290)** <br> **0.3359(解説文のみの辞書:平均0.517)** |


|項目|内容|
|:-:|:-|
|日付|2023/05/11 |
|生成文|高気圧に覆われ全国的に晴れ。北日本は寒気の影響で日本海側中心に曇りや雨。北海道は気圧の谷の影響で曇りや雨。北海道は気圧の谷の影響で曇りや雨。北海道では最高気温が平年より高い。|
|気象庁|気圧の谷の影響で北海道は曇りで所々雨。上空の寒気や湿った空気の影響で東海～関東は午後雨や雷雨。他は日本海の高気圧に覆われ概ね晴れ。最高気温は7月上旬並の所も。|
|天気図|![SPAS_COLOR_202305110000.v8.256.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/411112/03134965-ce90-0c6c-41ba-bfda575cc09c.png)|
|類似度|**0.1762(Wikipedia辞書平均:0.290)** <br> **0.3343(解説文のみの辞書:平均0.517)** |


ピタリと一致しているとまではいえないですが、意味的にみて重要な部分（全国的な傾向、天気に影響する事象がどこにあるか、どの地方が晴れて雨なのかというあたり）については類似している様にも思えます。


### 6.6 課題

課題としては、精度を高めていく方向がいまひとつ見えないことがあります。
・前線描画CNNについてはクラス分類が甘いラスタ版マスク画像の方が、気象庁作成文との類似度が高かった。
・Show and Tellのイテレーションを増やしても、lossが微減するものの類似度は良くならない。

また、単語、画像の要素数を256から増やすとどうなるのか、などネットワークの微調整は今後の課題です。


## 7.まとめ

Image Captioningの技法を応用して、気象予報に用いられるスーパーコンピュータの計算結果のデータ（全球予報モデルGPV）を可視化して、得られた画像から天気の概況説明文を生成するニューラルネットワークを作成しました。得られた生成文を実際に気象庁が記述した解説文と比較しました。

全体的な気象概況や擾乱の位置を画像から認識して説明文として組み立てることができそうであることが言えるのではないかと思います。



## A.参考文献

[日本語による画像キャプション自動生成AIを作ったので丁寧に解説します！](https://qiita.com/yutaoba/items/6eb0e12ba0d169a480df)

[Tensorflow Turials Image Captioning](https://www.tensorflow.org/text/tutorials/image_captioning)

[WRD（Word Rotator's Distance）で文書間の距離（類似度）を計算する](https://qiita.com/kenta1984/items/bad7e2f68331849d0053)