富山県ホタルイカ身投げAI予測モデルを作った (地域別推定)

背景

富山県から新潟県西部の沿岸で、春に産卵のために浮上したホタルイカが沿岸に打ち上げられる現象は「ホタルイカの身投げ」と呼ばれます。この接岸したホタルイカを掬うべく、大勢の人間が真夜中の海岸に集結するわけですが、全くホタルイカが湧かないという日も珍しくないようです。

ホタルイカが湧く条件というのは経験則な面も多く、一般的に言われているのは、

  • 新月前後の夜
  • 月明かりがない
  • 波が穏やか
  • 雨が降っていない(淡水を嫌う?)

…など。

これを機械学習でモデル化し、未来の3日間において、ホタルイカが湧く/湧かないを予測するモデルを作りました。富山県沿岸を4地域に分割し、その地域における湧き確率を推定します。

実際に3日予想を推定 & 公開しているサイトはこちらになります: https://hotaruika.tatuiyo.xyz

ホタルイカ出現データ集め

スクレイピング

大抵のホタルイカハンターは、この掲示板を利用しています。そして「今日はこの場所で取れた/取れない」という情報に翻弄され、現地で右往左往しているようです。

https://rara.jp/hotaruika-toyama

なかなか古風な、香ばしい感じの掲示板ですが、ここが一番活発です。

この掲示板に投稿された全投稿をスクレイピングし、学習データとしました。この掲示板自体は2014年から運用されており、前身の掲示板もあるようですが、そちらはWayback Machineにも残っていませんでした。

2ch等も探してみましたが、あまり使えそうなデータはなさそうです。そもそもホタルイカ掬いが活発になり始めたのは2014年前後からのようなので、それ以上過去のデータは見つからなさそうですね。

投稿データの整形

LLMを用い、各投稿データから、ホタルイカが採集された場所とその量を定量的に集計しました。具体的には、以下のようなプロンプトで、またAPI料金がもったいないので研究室のGPUで勝手に動かしているローカルのQwen3.5-122B-A10Bを用いました。

みんな量を適当に申告する上に、地名は伏せ字にしたりしやがるのでなかなか正確に集計するのは難しいです。最終的には用いませんでしたが、投稿に気象情報が含まれている場合は同時に集計させました。

SYSTEM_PROMPT = """あなたは富山県のホタルイカ身投げ情報を分析するアシスタントです。

1. reason(判定理由):
   なぜその対象日、湧きレベル、天候数値にしたのか、その3点を必ず含めた判断根拠を簡潔に記述してください。

2. datetime(対象となるホタルイカ日付):
   ホタルイカの身投げは、およそ 21:00 から 翌 4:00 に行われ、日付をまたぎます。日付をまたいだ釣果報告の場合は、カレンダー上は前の日付として記述しなさい。(例:3/16 23:00 から 3/17 4:00 まで身投げが発生したとしても、すべて 3/16 とします。)
   21:00 から 4:00 の身投げ時間以外の投稿も注意すること。例えば、3/17 の昼や夕方に「今日は〇匹採れた」という報告があれば、文脈を読み、3/16 としてください。

3. location(場所):
   テキストから地名を抽出してください。伏字や通称も文脈から推測できれば抽出。
   ※場所の記載が一切ない場合は null にしてください。

4. catch_level(湧き量レベル):
   以下の基準で 0 〜 6 の数値で定量化してください。ホタルイカの単位は「匹」および「杯」で、ホタルイカ 1 匹のことを 1 杯と呼びます。バケツ 1 杯という意味ではないので注意しなさい。
    - 0: なし/ゼロ(イカなし、気配なし、ボウズ)
    - 1: 少量(1-9 匹 または 1-9 杯):はぐれイカ、偵察隊レベル
    - 2: 少し(10-49 匹 または 10-49 杯):ポツポツ掬える程度
    - 3: 通常 / プチ湧き(50-199 匹 / 50-199 杯 / 約 350g〜1.4kg):飽きない程度に獲れる
    - 4: 多い / チョイ湧き(200-599 匹 / 200-599 杯 / 約 1.4〜4.2kg):バケツ半分程度
    - 5: 非常に多い / 湧き(600-1499 匹 / 600-1499 杯 / 約 4.2〜10kg):クーラーボックス半分程度
    - 6: 爆湧き(約 1500 匹〜 / 約 10kg〜):クーラーボックス満タン〜複数
   ※釣果への言及がなく、気象条件のみの投稿(例:「波が高い」「風がない」)の場合は null にしてください。


5. weather_and_sea(環境条件):
   テキスト内に現地で確認した事実として言及がある場合のみ抽出してください。言及がない項目や「天気予報では〜」は null にしてください。
   - wave: 0(なし/穏やか), 1(少しあり), 2(高い/荒れている)
   - wind: 0(無風/微風), 1(少しあり), 2(強い/暴風)
   - turbidity: 0(なし/澄んでいる), 1(少しあり/笹濁り), 2(酷い/激濁り/コーヒー色)


【重要な注意事項】
1. 未来の意気込みや願望:「〜する予定」「〜したい」「クーラー満タンにして帰るぞ」など、事実ではないものは null にしてください。
2. 累計釣果:「今シーズン累計で〇匹」「〇回参戦して〇匹」など、その日単体の釣果ではないものは null にしてください。
3. 無関係な話題:ツアー告知、解禁日の解説、単なる挨拶、雑談、スパムなどは null にしてください。


【出力フォーマット】
以下の形式に従い、必ず JSON 配列として出力してください。
{
    "original_text": "日付、投稿時間を含めた投稿の元のテキスト",
    "reason": "対象日、湧きレベル、天候数値の判定理由",
    "datetime": "YYYY-MM-DD",
    "location": "地名または null",
    "catch_level": 数値または null,
    "weather_and_sea": {
    "wave": 数値または null,
    "wind": 数値または null,
    "turbidity": 数値または null
    }
}
"""

出力フォーマットも一応工夫しており、”original_text”で投稿データをまず復唱させたのち、”reason”を先に吐かせることで、矛盾のない判定を引き出すことができます。

ホタルイカ掬いは真夜中に行われるので、投稿日時が日付をまたぐと日付管理が面倒なことになります。上の指示にはどのように日付を扱うか書いていますが、少し複雑なのでLLMは混乱して間違えた日付を吐くことがありました。
仕方ないので、もう一度LLMを通し、正確な日付を出力させました。

BATCH_PROMPT = """以下の{count}件の投稿データがあります。

各投稿について、設定されている日付が正しいか、以下の判断基準に基づき判定してください。

ホタルイカの身投げは、およそ 21:00 から 翌 4:00 に行われ、日付をまたぎます。日付をまたいだ釣果報告の場合は、カレンダー上は前の日付として記述しなさい。(例:3/16 23:00 から 3/17 4:00 まで身投げが発生したとしても、すべて 3/16 とします。)
21:00 から 4:00 の身投げ時間以外の投稿も注意すること。例えば、3/17 の昼や夕方に「今日は〇匹採れた」という報告があれば、文脈を読み、3/16 としてください。

【投稿一覧】
{posts_text}

【出力形式】
以下の JSON 配列で回答してください。各投稿の順序は入力と同じにすること。
各項目は以下の順で出力:
1. original_text: 投稿の元のテキスト(入力と同じ)
2. reason: 日付判定の理由
3. correct_datetime: 正しい日付(YYYY-MM-DD)

[
    {{
        "original_text": "投稿テキスト",
        "reason": "判断理由",
        "correct_datetime": "YYYY-MM-DD"
    }},
    ...
]
"""

そして、地名の抽出も怪しい。最初の指示では、とりあえず投稿内で出現した地名を全てリストアップしてしまっているので、結局どこで取れたのかよくわかりません。更にもう一度LLMを通します。

SYSTEM_PROMPT = """あなたはホタルイカ釣果報告から地名を抽出するアシスタントです。

【出力項目】
1. original_text: 投稿の元のテキスト(そのままコピー)
2. reason(判断理由):なぜその判断をしたか
3. location_primary(主要地名):釣りが行われた場所、または「行った」「釣れた」「採れた」など活動が発生した場所
4. location_secondary(二次地名):比較や言及だけされている場所(例:「四方は釣れたが岩瀬はダメ」の場合、岩瀬)

【出力形式(JSON 配列のみ)】
[
    {{
        "original_text": "投稿の元のテキスト",
        "reason": "判断理由",
        "location_primary": "地名または null",
        "location_secondary": ["地名 1", "地名 2"]
    }}
]

【注意事項】
- 地名の記載が一切ない場合は location_primary を null にすること
- original_text は入力テキストをそのままコピーすること
- 入力の順序と同じ順番で JSON オブジェクトを出力すること
- 現在の location 抽出結果を参考にし、より正確に判断すること"""

最後に、出現した地名を集め、手動でグループ化します。富山県西部から5グループに分け、番号を振りました。これが一番大変!

# 地域グループ定義(地名→グループ番号)
LOCATION_GROUPS = {
    # 西部から
    # アメダス伏木
    "国分": 1,
    "伏木": 1,
    "新湊": 1,

    # アメダス富山
    "海老江": 2,
    "足洗": 2,
    "八重津": 2,
    "打出": 2,
    "四方": 2,
    "岩瀬": 2,
    "浜黒": 2,
    "水橋": 2,

    # アメダス魚津
    "滑川": 3,
    "ホタルイカ": 3,
    "ほたるいか": 3,
    "ミュージアム": 3,
    "魚津": 3,
    "補助港": 3,
    "黒部": 3,
    "石田": 3,
    "生地": 3,

    # アメダス朝日
    "入善": 4,
    "朝日": 4,
    "宮崎": 4,
    "市振": 4,

    # アメダス糸魚川
    "姫川": 5,
    "糸魚川": 5,
    "ヒスイ": 5,

    # 細かな表記ゆれ
    "I 浜": 2,
    "I浜": 2,
    "あさひ": 4,
    "いわせ": 2,
    "うお": 3,
    "うちいで": 2,
    "えび": 2,
    "おさかなランド": 3,
    "けんか山": 1,
    "しんきろうロード": 3,
    "はまくろ": 2,
    "ひすい": 5,
    "ほたみゅー": 3,
    "ホタミュー": 3,
    "ミユージアム": 3,
    "やいづ": 2,
    "やえず": 2,
    "ヤエズ": 2,
    "よかた": 2,
    "キャンプ場": 2,
    "ダイヤ海岸": 4,
    "ハマクロ": 2,
    "ホテル古志": 2,
    "マリーナ": 2,
    "フィッシャリーナ": 2,
    "万葉": 1,
    "八重": 2,
    "八幡": 1,
    "上市川": 3,
    "下新川": 4,
    "古志": 2,
    "呉西": 1,
    "呉東": 3,
    "園": 4,
    "境海岸": 4,
    "大屋海岸": 4,
    "宮﨑": 4,
    "富山市": 2,
    "富山新港": 1,
    "射水": 1,
    "岩" : 2,
    "庄川": 1,
    "小矢部川": 1,
    "新川": 4,
    "新港": 1,
    "東部": 4,
    "東側": 4,
    "氷見": 1,
    "浜○崎": 2,
    "海の駅": 3,
    "海浜公園": 3,
    "海老": 2,
    "火力": 2,
    "発電所": 2,
    "県境": 4,
    "西部": 1,
    "神通川": 2,
    "蜃気楼": 3,
    "雨晴": 1,
    "親知": 5,
    "青海": 5,
    "高岡": 1,
    "高月": 3,
    "黒瀬": 2,
    "瀬浜": 2,
    "浜崎": 2,
    "西の": 2,
    "灯台": 2,
    "新漁港": 1,
    "新潟": 5
}

位置的にはこんな感じ。

最終的に、「日付, 場所, ホタルイカ採取量レベル」が各投稿から得られます。

ただし、ホタルイカ掬いは心理戦なので、掲示板に嘘投稿を流して動揺させる手法が存在します。そのような投稿を除外するため、同じ日・同じ地域の投稿群から90パーセンタイルで最終的な身投げレベルを確定しました。

学習データの補完

大雨や暴風の日は、誰も海岸に行かないのでデータが集まりません。が、このような日には明らかにホタルイカが取れるはずもないので、ホタルイカ湧きレベル0として学習データに追加します。

具体的には、雨量が 30 mm 超 または 平均風速が 8 m/s 以上 の悪条件日を対象としました。

学習データ数

ここまでの操作の結果、最終データの内訳は、正例(湧き) 785 件、負例(湧かない) 996 件となりました。地域別では Location 1 が 285 件、Location 2が 721 件、Location 3 が 438 件、Location 4 が 279 件、Location 5 が 58 件です。思ったより少なくなりました…

気象データの収集

次に、学習に用いる気象データを収集します。

気象データは、各 Location において一番近い気象観測所のデータを収集しました。

https://www.data.jma.go.jp/stats/etrn/index.php

Location No.代表地名使用気象データ
1雨晴、伏木、新湊アメダス伏木
2海老江、八重津、岩瀬、浜黒崎、水橋アメダス富山
3滑川、魚津、黒部、生地アメダス魚津
4入善、朝日、市振アメダス朝日
5糸魚川周辺アメダス糸魚川

これらのデータから、気温、風速、風向、気圧、降水量、日照時間が使えそうです。

また、追加で潮位、波浪、海水温データを収集しました。これは全ての Location で共通の値とします。

波浪データは NOWPHAS の「伏木富山港伏木」および「伏木富山港富山」を使用しました。 2024 年以降は確定値が揃っていないため、利用可能な速報値を採用しています。

https://nowphas.mlit.go.jp/pastdata_select

潮位データは気象庁の富山における潮汐観測資料を使用しました。

https://www.data.jma.go.jp/kaiyou/db/tide/genbo/format.html#hry

海水温データは気象庁の富山湾沿岸域海面水温データを使用しました。

https://www.data.jma.go.jp/kaiyou/data/db/kaikyo/series/engan/engan318.html

学習モデルと特徴量エンジニアリング

機械学習モデルは決定木の LightGBM を用いました。学習データが少なすぎるので、深層学習は不可能です。

特徴量は、以下を用いました。

暦・月

  • 年内の日付を周期変数に変換(sin/cos)
  • 月齢を 29.53 日周期で変換(sin/cos)
  • 富山湾の海水温

LightGBMは日付をそのまま扱えないし、ホタルイカの出現には季節性があるので、円周として周期性を表現します。

気象

  • 当日の降水量合計
  • 当日の気温の平均と標準偏差
  • 当日の風速の平均と標準偏差
  • 当日の気圧の平均と標準偏差
  • 当日の日照時間合計
  • 当日の港ごとの向かい風方向を使ったベクトルスコア
  • 当日20時から翌5時までの晴れ時間

「当日の港ごとの向かい風方向を使ったベクトルスコア」というのは、向かい風なら岸にホタルイカが押し寄せるはずという(根拠のない)推測に基づいた特徴量です。
具体的には、各 Location で「だいたいその方向から吹くと向かい風になる」とみなした角度に対して風速 × cos(風向 - 目標方向) で計算しています。上の地図を眺めながら、それっぽい角度を設定しました。現在の角度設定は、Location 1: 15 deg, Location 2: 0 deg, Location 3: 292.5 deg, Location 4: 337.5 deg, Location 5: 337.5 deg です。

「当日20時から翌5時までの晴れ時間」は、ホタルイカが湧く条件として有名な「月明かりがない日」の特徴を表現するために導入しました。

潮位・波浪

  • 当日24時間内の潮位差
  • 当日の夜間帯の相対潮位スコア
  • 当日の有義波高の平均と標準偏差
  • 当日の有義波周期の平均と標準偏差

「相対潮位スコア」は、ホタルイカの身投げが起きる夜間帯が、一日のうち干潮寄りなのか満潮寄りなのかを示す値です。

潮位スコア =
(夜間帯 20:00-翌4:00 の平均潮位 - その日の最低潮位)
/ (その日の最高潮位 - その日の最低潮位)

夜間帯が干潮寄りなら 0 に近く、満潮寄りなら 1 に近い値になります。

前日のラグ特徴量

気象、潮位、波浪データは前日のラグ特徴量も用います。前日の天候が大荒れだと少なからず影響ありそうなので。

location_group

後述する「統合モデル」においては、location_group 特徴量を用います。

いざ、学習

データは時系列順に 80% を学習、20% を検証に使い、 検証データで閾値と精度を決めます。欠損値は 0 埋めせず、LightGBM に NaN のまま渡しています。Early Stopping は 50 ラウンド。

結論から、特徴量を入れたり抜いたり、違う地域の特徴量を使ったり諸々やった結果、以下のような結論が得られました。
前提として、ホタルイカの湧き量の具体的な推定は全く精度が出なかったため、湧く/湧かないの二値分類としました。

  • Location 5 は、学習データの少なさから精度検証が不能
  • Location 1, 3, 4 は、全Locationデータで学習した「統合モデル」を用いると精度が良い
  • Location 2 は、Location 2のみのデータで学習した「専用モデル」を用いると精度が良い
  • Location 4 は、その地域に最も近い「アメダス朝日」よりも「アメダス富山」の気象データを用いたほうが精度が良い

つまり、こうです

Location No.モデル使用気象データ
1統合モデル伏木アメダス
2専用モデル富山アメダス
3統合モデル魚津アメダス
4統合モデル富山アメダス

その条件において、精度は以下のようになりました。

Location No.閾値湧く Precision湧かない PrecisionF1Accuracy
10.500.82350.88890.82350.8636
20.600.86540.75270.75000.7931
30.360.71430.70210.67570.7073
40.500.74290.76040.70270.7535

湧く Precision は、湧くと判定して実際に湧く確率です。

閾値は、検証データ上で「湧く Precision」と「湧かない Precision」を閾値をスイープしながら比較し、バランスの良い閾値の値を(人為的に)採用しました。

Location 3 はちょっと微妙ですが、他はそれなりの精度が得られています。

実運用時に用いる気象予報データの取得

実運用時に用いる予報データは、以下より取得しました。

項目取得元補足
気象Open-MeteoLocation 1 は伏木アメダス観測所座標、Location 2, 4 は富山アメダス座標、Location 3 は魚津アメダス座標の気象予報
潮位気象庁 富山 潮位予報予測潮位を使用
波浪気象庁 富山 波浪予報有義波高・有義波周期を使用
海水温気象庁 富山湾 海水温対象日以前の最新観測値を使用

気象データは、学習時と同じ観測点の座標における予報を使っています。

実運用上は、波浪予報の取得範囲が 3 日程度であるため、予報期間も 今日・明日・明後日の 3 日までに制限しています。(ここがネック!)

海水温の変化は緩やかであるため、最新の当日の海水温データを用いて 3 日間の予報を行っています。

これでホタルイカ湧き推定に必要な気象予報データは揃いましたが、予報データを用いた場合の精度は、学習(検証)時の精度よりも当然下がるはず。どの程度下がるかは定かではありませんが、今後は予測的中率を出して明らかにしていきたいと思います。

ホタルイカ出現確率への変換

「明日はホタルイカが湧きます/湧きません」のみでは味気がありません。湧く確率が見たいですよね。

しかし、LightGBM が出す生スコアは、そのまま 「40% なら 10 回に 4 回湧く」と読めるとは限りません。そこで、isotonic regression による確率校正を行いました。以下の記事が参考になります。なかなか難しいですが、ライブラリもあるしCodexがやってくれるので理解しなくても組めました。

https://qiita.com/dai08srhg/items/eb08fc98e7149748a9d5

これで、出現確率の推定値が得られました。

実運用

3時間毎にCrontabを回して最新の気象情報を取得し、都度推論します。N100ミニPCでも推論は一瞬でした。

決定木の説明可能性という最大の利点を生かし、モデル別に重視される特徴量や、推定結果の詳細としてプラス/マイナスに効いた特徴量も表示させます。

統合モデルでは、「当日降水量」、「月齢」、「当日日照時間」、「当日夜の腫れ時間」が重視されるようです。ただし、プラスに効いた要因の「当日夜の腫れ時間」は6時間となっていて、むしろ夜に晴れている方が湧きやすいという推定結果になっていました。通説とは逆ですね。

同様に、向かい風なら岸にホタルイカが押し寄せるはずという推測に基づいた「向かい風スコア」も、逆に点数が高いとマイナスに効いていました。

課題

  • 湧く/湧かないの二値分類で、湧き量の推定が不可能
  • 波浪予報の入手範囲がボトルネックで、今日・明日・明後日の 3 日予報のみな点
  • Location 5 (新潟の糸魚川) 学習データの少なさに起因する、推定精度の検証が不能の問題
  • Location 3 (滑川、魚津) の精度が低い問題

まとめ

AIホタルイカ身投げ予想は二番煎じですが、地域別の推定ができる点で有用なのではないでしょうか。 先行研究(?)の実用精度は不明ですが、同じあの掲示板を学習データとしているようなので大きく精度は変わらないはずです。

ちなみにまだ一度もホタルイカ掬いに行ったことがありません。

おすすめ

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

Index