Snowflake Cortex AIを活用して画像分類をやってみた with Cortex Code

本記事は 春のスキルアップ応援フェア2026 4/24付の記事です

こんにちは、ひるたんぬです。

最近料理にハマっており、仕事終わりは可能な限り自分で料理を作ろうと心がけています。
そんな私はWebサイトに数多あるレシピのお世話になっているのですが、材料欄によく「少々」「ひとつまみ」といった記載を見かけます。これら二つを使い分けているレシピもあり、分量に差はあるのか気になったので調べました。

【少々のはかり方】親指と人差し指の2本で軽くつまむ。
【ひとつまみのはかり方】親指、人差し指、中指の3本の指で軽くつまむ。
引用:デリッシュキッチン「料理の基本!少々・ひとつまみのはかり方

明確な違いがあったのですね。。。今まで「どっちも同じだろう」と思い投入していたので改めようと思います。

さて、今回は「春のスキルアップ応援フェア」ということでしたので、普段はAWSを中心に触っておりSnowflakeミリしらな私が、SnowflakeのCortex AIを活用して非構造化データの画像を検索できるようになるまでの道のりを記したものです。
長めの投稿となりますが、飛ばしながらでも最後まで温かく見守っていただけますと幸いです。

きっかけ

先日、社内勉強会にてSnowflakeについて知る機会がありました。
その中ではデータ活用の重要性を起点にSnowflakeの概要から強み、実際の導入事例までを座学で学んだほか、実際にSnowflakeの社員様にご登壇いただきサービスのデモを見せていただきました。

それまでは、「Snowflakeってデータ分析や活用ができる、なんかすごいツールが集まったもの」程度の認識だったのですが、デモの中で、「画像のような非構造化データまで分析ができる」ことを聞いたときに、これはすごい…!と思い、試すまでに至りました。
なお、その中でご紹介いただいたブログ記事はこちらです。

また、上記ブログで紹介されている元の記事も参考にさせていただきました。

本記事でも、上記ブログ記事に沿って実際に手を動かしました。

Snowflake Cortex AIとは?

公式サイトの文章が簡潔で分かりやすかったので、引用させていただきます。

データの存在する場所でAIを活用して、会話、ドキュメント、画像をインテリジェントなインサイトに変換できます。業界をリードするLLMにSQLで直接、またはAPIを介して大規模にアクセスし、マルチモーダルデータの分析とエージェントの構築を、すべてSnowflakeのセキュアな境界内で実行できます。 
引用:Snowflake「Cortex AI

ポイントとしては、以下が挙げられます。

  • 会話、ドキュメント、画像をインテリジェントなインサイトに変換
    → 非構造化データの中でも、そもそも文字データですらない画像からも情報抽出が可能
    → 構造化データ・非構造化データを一箇所に集約可能
  • Snowflakeのセキュアな境界内で実行
    → 大切なデータも安全に処理ができる

画像データを分類してみよう!

下準備

今回私は初めてSnowflakeを触るので、アカウント開設から始めました。
Snowflakeでは30日間(もしくは$400到達のどちらか早い方)の無料お試しが利用できるので、今回は学習目的でこちらを使わせていただきました。
アカウント開設は、メールアドレスなどの必要事項を数点入力するのみで完了し、ものの5分程度でアカウントが発行されました。

また、参照元のブログ記事では車載カメラで撮影されたデータセットを用いておりましたが、折角なので違うデータセットで試すこととします。今回は飛行機の画像が含まれているデータセット¹を用意し利用しました。
¹ Kaggle「Commercial Aircraft Dataset

学習目的で任意のデータセットを利用する場合、権利関係にご注意ください。
今回のデータセットは「CC0」であり、クレジットを表記することなく利用が可能です。
参考:Creative Commons「CC0

データ格納

Snowflakeでは「ステージ」という概念があります。これは、簡単に言えばデータを格納する場所です。
ステージ上にデータを保管し、Snowflakeテーブルとデータのやり取りをする、中継地点という考え方もできます。

ステージは、Snowflake Stageという「内部ステージ」と、パブリッククラウド(Amazon S3・Google Cloud Storage・Azure Blob Storage)上の「外部ステージ」に分けることができます。
更にこの「内部ステージ」は以下の3つに分類することができます。

  • ユーザーステージ【Snowflake管理ステージ】
    → 各ユーザーにデフォルトで割り当てられているステージ。ファイルに対するアクセスが一人(=そのユーザー自身)である場合に選択。
  • テーブルステージ【Snowflake管理ステージ】
    → 各テーブルごとに自動で作成されるステージ。ファイルに対するアクセスが一つのテーブルである場合に選択。
  • 名前付きステージ
    → 上記の制約が存在しないステージ。自由に作成が可能。外部ステージはここに位置する。

Snowflake上のドキュメントには、上記内部ステージの選択方針として以下のように述べられていました。

自分だけがロードするデータファイル、または単一のテーブルにのみロードするデータファイルをステージする場合は、ユーザーステージまたはデータをロードするテーブルのステージのいずれかを使用することをお勧めします。
名前付きステージはオプションですが、複数のユーザーやテーブルが関係する可能性のある通常のデータロードを計画する場合は、使用を推奨します。
引用:Snowflake Documentation「ローカルファイルに対する内部ステージの選択

ステージの取り扱いについてはなんとなく理解することができました。

今回の処理は、ユーザーステージやテーブルステージで実施することはできません。必ず名前付きステージを作成して作業を行ってください。
参考:Snowflake Documentation「AI_CLASSIFY – Limitations」「AI_FILTER – Limitations

ステージの作成

ここからは実際にSnowflake上のリソースを操作していきます。
SnowflakeはコンソールのGUIでも操作はできますが、SQLでもできるため、今回はできる限りSQLを使って作業していきたいと思います。
SQLのコマンド実行環境はSnowflake上のWorkSpaceを使用します。

まず、Snowflake内部に名前付きステージを作成します。
ステージ名は「AIRCRAFT_IMAGE_STAGE」としました。
なお、このタイミングでデータベースも併せて作成します。

CREATE DATABASE AIRCRAFT_DB;
CREATE OR REPLACE STAGE AIRCRAFT_DB.PUBLIC.AIRCRAFT_IMAGE_STAGE
  ENCRYPTION = (TYPE = 'SNOWFLAKE_SSE');

ステージの作成時には、暗号化オプションを忘れずにつけるようにしてください。

デフォルトのクライアントサイドでの暗号化では、後述するAI_CLASSIFY、AI_FILTERなどの処理を行うことができません。

実際に作成できたかも確認します。

SHOW DATABASES STARTS WITH 'AIRCRAFT_DB';
SHOW STAGES STARTS WITH 'AIRCRAFT_IMAGE_STAGE';

それぞれ作成されていることが確認できました。

画像のアップロード

取得した画像を先程作成したステージに格納します。
WorkSpace上にImagesフォルダを作成、フォルダ内に画像ファイルを準備し、ステージにアップロードします。

ファイルのアップロードを一度に行うとフリーズしてしまったので、数回に分けて行いました。
私の環境では1,000枚程度であれば一度に作業できました。
ちょっと動作を確かめたい場合は、全部アップロードしなくても良かったですね…後述しますが、すべての画像のアップロードは、おすすめしません。

今回はWorkSpaceで作業してみたかったため、画像をWorkSpaceにアップロードする必要がありましたが、Snowflake CLIを利用することで、ローカル環境から直接ステージにファイルを転送することも可能です。

WorkSpaceへのアップロードには、ブログ記事を参考に以下のコマンドを用いました。

PUT file:///snow://workspace/USER$.PUBLIC.DEFAULT$/versions/head/Images/*.jpg @AIRCRAFT_IMAGE_STAGE;

しかし、上記コマンドで試したところ以下のようなエラーが表示されました。

Unsupported feature ‘unsupported_requested_format:snowflake’.

ここでコンソール上には「💠Fix」の文字が。。これをクリックすると、Cortex Codeによる問題解決が始まりました。

Cortex Codeによると、

Replaced PUT with COPY FILES INTO. The PUT command isn’t supported in Snowsight  COPY FILES INTO is the workspace-compatible way to upload files from a workspace to a stage. Also changed versions/head to versions/live (the correct workspace path).

とあり、PUTコマンドはSnowSight²ではサポートされていない、代わりにCOPY FILES INTOコマンドを使うように案内されていました。
今回はこちらに従い修正を受け入れると、問題なく実行できるようになりました。
² SnowSightとは、PythonやSQLでSnowflake上のデータを操作するWebインターフェースのことです。

修正後のコードはこちらです。

COPY FILES INTO @AIRCRAFT_IMAGE_STAGE
FROM 'snow://workspace/USER$.PUBLIC.DEFAULT$/versions/live/Images/'
PATTERN = '.*\.jpg';

なお、PUTコマンドが使えないことについては、公式ドキュメントにも記載がありました。

コマンドは、いずれのSnowflakeウェブインターフェイスの Worksheetsページからも実行できません。
引用:Snowflake Documentation「PUT – 使用上の注意

また、今回はWorkSpaceで作業してみたかったため、画像をWorkSpaceにアップロードする必要がありましたが、Snowflake CLIを利用することで、ローカル環境から直接内部ステージにファイルを転送することも可能です。

外部ステージへのファイル転送は、それぞれのサービスで用意されているコマンドやコンソールを使用します。

一通りのアップロードを終えたら、無事にファイルが格納されていることを確認します。

SELECT * FROM DIRECTORY(@AIRCRAFT_DB.PUBLIC.AIRCRAFT_IMAGE_STAGE);

しかし、ここでもエラー…

DIRECTORY not enabled for the stage AIRCRAFT_IMAGE_STAGE

日本語約すると、作成したステージでは、DIRECTORYが有効化されていないとのことでした。そもそもこの「DIRECTORY」というものがどういうものか分からなかったので、その理解から始めます。

ディレクトリテーブルは、ステージで複数層になった暗黙のオブジェクトで(独立したデータベースオブジェクトではない)、ステージ内のデータファイルに関するファイルレベルのメタデータを格納するため、概念的には外部テーブルに似ています。
引用:Snowflake Documentation「ディレクトリテーブル

ステージ内に格納したファイルのメタデータを格納するテーブル、と理解しました。
ディレクトリテーブルはステージの作成時に作ることもできますが、今回は既にステージを作っていたので、ステージのプロパティ変更コマンド(ALTER STAGE)を使用します。
ディレクトリ作成後は、メタデータ更新のため、手動でリフレッシュコマンドを実行します。

ALTER STAGE AIRCRAFT_DB.PUBLIC.AIRCRAFT_IMAGE_STAGE SET DIRECTORY = (ENABLE = TRUE);
ALTER STAGE AIRCRAFT_DB.PUBLIC.AIRCRAFT_IMAGE_STAGE REFRESH;

上記が完了したら、改めて確認をします。

SELECT * FROM DIRECTORY(@AIRCRAFT_DB.PUBLIC.AIRCRAFT_IMAGE_STAGE);

無事にアップロードできていることを確認できました!

上記の解決にもCortex Codeの力をお借りしました。

画像分類項目の検討

いよいよここからはCortex AIを用いた画像分析に入ります。
今回はブログでも紹介されていた関数のうち、AI_FILTER関数とAI_CLASSIFY関数を使います。

  • AI_FILTER関数 
    • ランディングギアは出ているか
      ※ 車輪のことです。
    • 四発ジェット機か
      ※ ジャンボジェットのようにエンジンが4発の航空機のことです。
    • 着陸しているか
    • 日本航空(JAL)、もしくは全日本空輸(ANA)の航空機か
  • AI_CLASSIFY関数
    • 各画像の天候のラベル付け
      • 晴れ
      • 曇り
    • 航空機が所属するアライアンスのラベル付け
      • Oneworld
      • Star Alliance
      • SkyTeam
      • 所属なし

紹介されていたブログの項目を参考に王道からチャレンジングな項目まで設定しました。ブログ記事ではAI_CLASSIFY関数でトヨタ車の検出ができていたようだったので、AI_FILTER関数で同じようなことができるか検証します。また、そこから少し踏み込み、航空会社を判別したうえで、その航空会社が所属するアライアンスを分類できるかを試します。

いよいよ実践!

すべての準備が整ったので、実際に分類をしていきます。

実践①:AI_FILTER

AI_FILTER関数は、自然言語によるプロンプト入力の結果をブール値で返す関数です。テキストはもちろんですが、今回実験するような画像に対する処理も対応しています。

ランディングギアの判別
SELECT 
  RELATIVE_PATH AS FILE_NAME,
  AI_FILTER(
    'この画像の航空機は、ランディングギアは出ていますか?', 
    TO_FILE('@AIRCRAFT_DB.PUBLIC.AIRCRAFT_IMAGE_STAGE', RELATIVE_PATH)
  ) AS SHOW_LANDINGGEAR
FROM DIRECTORY(@AIRCRAFT_DB.PUBLIC.AIRCRAFT_IMAGE_STAGE);

TRUEと判別されたのは6,070枚でした。いくつかピックアップしてみます。

様々な角度の画像がありますが、どれもランディングギアが出ていることが分かります。

逆に、FALSE(ランディングギアが出ていない)と判別された結果も、いくつかピックアップしてみます。

こちらもランディングギアが出ていないものが多かったです。
ただ、一部(上図右下)ではちょうどしまっているところなども、FALSEと検出されていました。判断に迷うところですね。。

四発ジェット機の判別

SQL文は自然言語での指示部分、及び結果を整理するインデックス名以外は変更がありません。

SELECT 
  RELATIVE_PATH AS FILE_NAME,
  AI_FILTER(
    'この画像の航空機は、四発ジェット機ですか?', 
    TO_FILE('@AIRCRAFT_DB.PUBLIC.AIRCRAFT_IMAGE_STAGE', RELATIVE_PATH)
  ) AS IS_QUADJET
FROM DIRECTORY(@AIRCRAFT_DB.PUBLIC.AIRCRAFT_IMAGE_STAGE);

TRUEと判別されたのは679枚でした。いくつかピックアップしてみます。

こちらもどれも四発ジェット機ですね。上図右下のANAは画像全体がかなり霞んでいますが、正しく判別できています。

着陸しているかの判別
SELECT 
  RELATIVE_PATH AS FILE_NAME,
  AI_FILTER(
    'この画像の航空機は、着陸していますか?', 
    TO_FILE('@AIRCRAFT_DB.PUBLIC.AIRCRAFT_IMAGE_STAGE', RELATIVE_PATH)
  ) AS IS_LANDING
FROM DIRECTORY(@AIRCRAFT_DB.PUBLIC.AIRCRAFT_IMAGE_STAGE);

TRUEと判別されたのは681枚でした。いくつかピックアップしてみます。

どの航空機も着陸した状態です。

JALまたはANAの航空機の判別
SELECT 
  RELATIVE_PATH AS FILE_NAME,
  AI_FILTER(
    'この画像の航空機は、JALもしくはANAの航空機ですか?', 
    TO_FILE('@AIRCRAFT_DB.PUBLIC.AIRCRAFT_IMAGE_STAGE', RELATIVE_PATH)
  ) AS IS_JAL_ANA
FROM DIRECTORY(@AIRCRAFT_DB.PUBLIC.AIRCRAFT_IMAGE_STAGE);

TRUEと判別されたのは84枚でした。いくつかピックアップしてみます。

しっかり分類されていましたね!
Oneworld塗装やStar Alliance塗装の機体まで分類できている点、JAL Expressという、かつて存在したJALグループの便まで検出できている点は驚きでした。
JALのロゴ、懐かしいですね。。

実践②:AI_CLASSIFY

AI_CLASSIFY関数は、指定されたカテゴリに分類をする関数です。こちらの関数もテキスト・画像両方に対応しています。

天候のラベル付け
SELECT 
  RELATIVE_PATH AS FILE_NAME,
  AI_CLASSIFY(
    TO_FILE('@AIRCRAFT_DB.PUBLIC.AIRCRAFT_IMAGE_STAGE', RELATIVE_PATH),
    ['晴れ', '曇り', '雨']
  ):labels[0]::STRING AS WEATHER_CATEGORY
FROM DIRECTORY(@AIRCRAFT_DB.PUBLIC.AIRCRAFT_IMAGE_STAGE);

晴れが5,013枚、曇りが1,491枚、雨が33枚でした。

▼ 晴れ

▼ 曇り

▼ 雨

AI_FILTERと異なり、はい・いいえの二択で判断できないので中々評価が難しいところではありますが、明らかに間違えている、と言えるものは無さそうでした。
「曇り」画像の左下は人間だと「晴れ」と答えそうですが、気象庁における天候の定義に基づくと誤りでは無さそうです。すごいなぁ…と感心してしまいました。

雲量が0から1のときは「快晴」、2から8のときは「晴れ」、9から10のときは「くもり」としています。
引用:気象庁 はれるんライブラリー「雲の量と天気の「快晴」「晴れ」「くもり」の関係は?

アライアンスのラベル付け

いよいよ、難関と思われる問題です。
SQL文はラベルカテゴリとインデックス名が変わる程度で、大きな違いはありません。

SELECT 
  RELATIVE_PATH AS FILE_NAME,
  AI_CLASSIFY(
    TO_FILE('@AIRCRAFT_DB.PUBLIC.AIRCRAFT_IMAGE_STAGE', RELATIVE_PATH),
    ['Oneworld', 'Star Alliance', 'SkyTeam', '所属なし']
  ):labels[0]::STRING AS ALLIANCE_CATEGORY
FROM DIRECTORY(@AIRCRAFT_DB.PUBLIC.AIRCRAFT_IMAGE_STAGE);

Oneworldが507枚、Star Allianceが1,218枚、SkyTeamが659枚、所属なしが4,153枚でした。

▼ Oneworld

  • マレーシア航空:○
  • イベリア航空:○
  • (旧)メキシカーナ航空:○
    ※ (旧)メキシカーナ航空は運行停止
  • 日本航空:○
  • フィンエアー:○
  • アメリカン・イーグル(アメリカン航空グループ):○
    ※ アメリカン・イーグルはアフィリエイトメンバーです。

▼ Star Alliance

  • ルフトハンザドイツ航空:○
  • ユナイテッド・エクスプレス(ユナイテッド航空グループ):○
    ※ ユナイテッド・エクスプレスはアフィリエイトメンバーです。
  • オーストリア航空:○
  • LOTポーランド航空:○
  • 全日本空輸:○
  • TAPポルトガル航空:○

▼ SkyTeam

  • デルタ航空:○
  • KLMオランダ航空:○
  • エールフランス:○
  • デルタ・コネクション(デルタ航空グループ):○
    ※ デルタ・コネクションはアフィリエイトメンバーです。
  • 中国東方航空:○
  • アルゼンチン航空:○

…という訳で、なんと抽出したものはすべて合っていました。。びっくりです。単純に航空会社を認識するだけでなく、その先のアライアンスまで認識して分類できる能力はありそうですね。

結果の整理

それぞれのSQL文の実行結果は画面上にテーブル形式で出力されるほか、CSV形式でのダウンロードも可能です。

これでももちろんいいのですが、データ活用という観点からすると、これらの結果をSnowflake上で活用できる形で管理したいと思います。この場合には、テーブルを用意して結果を保存します。

しかし、私はSQL文の中にTABLEの作成を入れ忘れていました…
再実行、、、が頭によぎりましたが、ここで朗報です。
Snowflakeではクエリの実行結果が24時間保存されます。

まさに、今の私のためだけに作られたような機能…!早速この「RESULT_SCAN」を使ってテーブルを作成していきます。

ここで必要になるのが、クエリ実行結果のIDです。
これもSQLで検索することが可能ですが、今回はWorkSpace上ですぐ特定できたため、そちらの方法を採用しました。

実際のSQL文も併せて表示されるため、迷うことは無さそうです。
これを使ってテーブル作成のSQL文を作成し、実行します。
CREATE OR REPLACE TABLE AIRCRAFT_DB.PUBLIC.RESULT_LANDINGGEAR AS
  SELECT * FROM TABLE(RESULT_SCAN('00c11e3b-1234-8aba-5678-231a00012345'));
これをクエリの数繰り返し、計6つのテーブルを作成することができました。
最終的にこれを一つのテーブルにまとめます。
今回はこの方法をCortex Codeに聞いてお願いしました。
抽象的な表現かつ日本語ですが、うまく動いてくれるのでしょうか…
Cortex Codeは、今までのSQLの中身を確認したあと、それぞれの結果のテーブルを確認し、SQL文を生成してくれました。
生成内容を確認するに、きちんと指定名でのテーブルを作成し、その中にすべての結果をLEFT JOIN関数を用いて結合させていることがわかります。
問題はなさそうだったので、この結果を受け入れます。
するとCortex Codeではコードの実行と検証が実施され、問題ない旨回答をもらいました。検証まで一連の流れで行ってくれるのはありがたいですね。
最終的な回答文も日本語でした。

おまけ:分類に失敗したと思われる画像

さて、ここまででこの機能のすごさは少しでも感じていただけたでしょうか?
一方、AIに絶対はない、と言われているのが今日です。ここでも実際の分類結果から、あり得ないシチュエーションを設定し、そこに分類された画像を見てみようと思います。

パターン①:JAL or ANA × SkyTeam

上記の結果にもありますが、JALはOneworld、ANAはStar Allianceなので、このパターンの結果は存在し得ないはずです。しかし、この結果を示すものが一件だけありました。

ロゴを見ればJALと一目で分かりますが、尾翼の緑色が特殊ですね。これにより結果が揺らいでしまったのでしょうか…

パターン②:ランディングギアが出ていない × 着陸状態

飛行機では着陸時にランディングギアを出します。
上記の状態は通常の着陸では起こらないはずですので、このパターンは存在し得ないはずです。しかし、この結果を示すものが35件ありました。

実際の画像をすべて確認すると、すべて着陸状態で、かつランディングギアは出ている状態でした。
つまり、ランディングギアの判別が誤っているということになります。

一方、正常に判別できている結果と比べると、ランディングギアが主翼や影で隠れて見えない(見えにくい)画像が多い…?と感じました。この点を表現としてプロンプトに加えることで、精度改善につながるのでは、と考えています。

この改善に挑戦しようとしたところで、一通のメールが…

6,000枚の画像を無計画に分析したからですね。。2日間で$400を使い切ってしまったようです。
プロンプトによる精度改善の検証は、今後の課題にしたいと思います。
皆様が同じように試される場合は、事前にサイズを圧縮する・枚数を減らすなど工夫をされることを心よりおすすめいたします…

おわりに

今回はSnowflakeのAIサービス「Cortex AI」を活用して、非構造データの代表格である画像データの分類に挑戦してみました。

SQLに触れることが3年前の新人研修以来だった私にとって、不安なことが多かったのですが、ほんの数行書くだけ、しかもパッと見ただけで処理内容が分かるような記述に感動しました。
また、今回の学習では数多くのエラーに遭遇しましたが、そのたびにCortex Codeが助け舟を的確に出してくれ、効率的に学習を進めることができました。
もちろん、公式ドキュメントでの裏取りも行ったので、今まで以上に深く物事が学べている印象です。

ワタシハ スノーフレーク チョットデキルに一歩でも近づけたのでしょうか…

タイトルとURLをコピーしました