コラムCOLUMNS

AIはなぜその答えを出したのか、説明可能AI(XAI)の実践ガイド

最終更新日:2026年7月25日
公開日:2026年6月1日

こんにちは。Anagraftの伊藤です。

AIモデルを業務に導入すると、予測の精度そのものより先に、判断の根拠を問われる場面が数多く出てきます。与信を断られた顧客への説明、不良品と判定した根拠の確認、経営会議での予測の信頼性の確認などが典型です。精度がどれだけ高くても、判断の根拠を示せないAIは、責任の伴う業務にはなかなか採用されません。

本コラムは、この「なぜ」に答えるための技術、説明可能AI(XAI: Explainable AI)を、考え方から実装コードまで通しで学べる実践ガイドです。テーブルデータの定番であるSHAP、画像認識のGrad-CAM系手法、予測の「自信のなさ」を測る不確実性推定、そして生成AI・LLM時代の説明可能性まで、体系的にまとめました。Pythonの実装コード付きなので、読みながら手を動かして確かめられます。

AIプロジェクトの導入が止まる理由は、精度よりも「信頼」の問題であることが多いです。モデルの中身がブラックボックスのままでは、現場の担当者は安心して業務を任せられませんし、経営層は投資判断ができません。逆に、判断根拠を適切に示せるだけで、同じモデルが受け入れられていくことも少なくありません。説明可能性は、AIを「動くもの」から「使われるもの」に変える技術だと考えており、その道具立てを考え方から実装まで通しで整理したのが本コラムです。

また、規制の面でも説明可能性は無視できなくなりました。EUのAI規則(AI Act)をはじめ、AIの透明性を求める制度的な要請は世界的に強まっており、金融や医療など説明責任の重い業界では、XAIはもはや「あれば良いもの」ではなく「なければ使えないもの」になりつつあります。

想定している読者は、次のような方々です。

  • 機械学習モデルを実務に導入していて、判断根拠の説明を求められているデータサイエンティスト・エンジニアの方
  • AIの導入を検討していて、「ブラックボックスで大丈夫か」という不安に向き合っている経営層・DX推進担当者の方
  • 与信・審査・診断など、説明責任の重い領域でAI活用を進めている方
  • SHAPやGrad-CAMという言葉は聞いたことがあるが、仕組みと使い分けを体系的に理解したい方

構成についても触れておきます。第1部の総論は、技術的な前提知識なしで読めるように書きました。実装パートは、扱うデータに応じて第2部(テーブルデータ)、第3部(画像)、第4部(不確実性)、第5部(言語・生成AI)に分かれており、各部は独立して読めるように構成していますので、関心のある部だけを参照する使い方も想定しています。要所では参考書籍もご紹介しています。

$authorName
著者伊藤 瑛志

Anagraft(アナグラフト)合同会社 代表 AXプロジェクト顧問・支援
データサイエンティスト since 2013 BCG/ALBERT(現アクセンチュア)出身

目次

序章 本コラムの構成

本コラムは5つの部と終章で構成されています。各部のテーマと、そこで答える問いは次の通りです。

テーマ答える問い
第1部 総論説明可能性の考え方・手法の分類・規制動向なぜ説明が必要か、どんな道具があるのか
第2部 テーブルデータ編解釈可能なモデル、Permutation Importance、PDP、LIME、SHAPこの予測にどの特徴量が効いたのか
第3部 画像編Grad-CAM/Grad-CAM++/Score-CAM、自動運転への応用モデルは画像のどこを見て判断したのか
第4部 不確実性編MC Dropout、ベイズニューラルネットワークこの予測はどれくらい信用してよいのか
第5部 言語・生成AI編テキスト分類の説明、Attention論争、LLMの説明可能性、RAGによる根拠提示言葉を扱うAIの根拠をどう示すのか
終章 実務ガイド手法の選び方、相手別の説明設計、よくある失敗結局、自社のプロジェクトで何をすべきか

なお、本コラムのPythonコード例は部ごとに使用ライブラリが異なります。テーブルデータ編はscikit-learn・LightGBM・lime・shap、画像編はTensorFlow(Keras)とOpenCV(opencv-python)、不確実性編はPyTorch(torch・torchvision)とPyMCを使用しています。お手元で試される際は、該当する部のライブラリをインストールしてください。

本コラム全体の前提となる考え方を先に述べておきます。「説明」とは、モデルの数式を開示することではなく、相手が意思決定できるだけの材料を、相手の言葉で提供することだと捉えています。データサイエンティストにとっての説明(特徴量の寄与)と、経営層にとっての説明(この予測に基づいて動いてよい理由)と、監査にとっての説明(プロセスの妥当性)は、それぞれ別物です。本コラムでは手法を学びながら、常に「誰に対する説明か」という視点を添えていきます。

Anagraftでは、AIプロジェクトの構想・課題設計から、データ分析・機械学習モデルの開発、AI人材の育成まで一貫したご支援を行っています。ご相談は、以下よりお問い合わせください。

お問い合わせ

第1部 総論:説明可能AIの全体像

第1部では、技術の詳細に入る前に、なぜ説明可能性が必要なのか、どんな手法の選択肢があるのか、そして規制はどこへ向かっているのかを整理します。技術的な前提知識なしで読める部です。

第1章 なぜ「説明できるAI」が必要なのか

機械学習モデルの予測精度は、この十年ほどで驚くほど向上しました。勾配ブースティングはテーブルデータの定番となり、深層学習は画像や音声、自然言語の領域で人間に匹敵する、あるいは部分的には人間を上回る性能を示すようになっています。一方で、モデルが複雑になればなるほど、「なぜその答えを出したのか」を人間が直感的に理解することは難しくなりました。数百万、数億のパラメータが絡み合って出力される予測値は、たとえ正確であっても、その理由を一言で言い当てることができません。

この問題に取り組む技術と方法論の総称が、XAI(Explainable AI:説明可能なAI)です。モデルの予測根拠や全体的な挙動を、人間が理解し検証できる形で提示することを目指します。ここで大切なのは、XAIが単なる「親切な補足機能」ではないという点です。後ほど詳しく述べますが、説明できないAIは現場で使われず、誤りに気づかれないまま運用され、ときに人を不当に扱い、そして規制上のリスクを抱えます。説明可能性は、AIを実務で使い物にするための土台そのものだと私は考えています。

「精度の高いモデルほど中身が複雑で説明しにくく、説明しやすいモデルほど精度で見劣りする」という、いわゆる精度と解釈性のトレードオフは、この分野で繰り返し語られてきた構図です。ただし実務の感覚で言えば、この構図は絶対のものではありません。トレードオフの傾きは課題やデータによって大きく異なりますし、複雑なモデルの挙動を後から解きほぐす手法も年々充実してきました。「精度を取るか、説明を取るか」という二者択一で悩む前に、まず両立の余地を探る。そのための道具箱を体系的にお渡しすることが、本ガイドの目的です。

本章では、この長いガイドの出発点として、三つの問いに答えます。第一に、そもそもなぜ説明が必要なのか。精度という分かりやすい指標があるのに、なぜそれだけでは足りないのか。第二に、説明手法にはどんな種類があり、どう整理すればよいのか。SHAPやLIMEといった名前を聞いたことがあっても、それぞれの位置づけが曖昧なままでは適切な手法を選べません。第三に、社会や規制は今、AIの説明に何を求めているのか。この三つを押さえておくと、第2章以降の個別手法の解説が、単なる技術カタログではなく「自分の課題のどこに効く道具なのか」という視点で読めるようになると思います。

精度だけではAIは使われない

高い精度が確認できたからといって、それだけで導入が決まり、現場に定着するわけではありません。検証段階では高い精度を示したモデルが、本番運用で中々使われないことは起こりがちです。原因はさまざまですが、根本をたどると「モデルが判断の理由を語れないこと」に行き着くケースも少なくないように思います。説明が必要とされる動機は、大きく四つに整理できます。現場の信頼、デバッグ、公平性、説明責任です。

予測結果だけでは信頼の溝を越えられず、判断根拠を添えた説明可能性が現場・経営・監査への橋になることを示した図

一つ目は現場の信頼です。たとえば小売業の需要予測では、発注担当者は長年の経験から、天候や曜日、地域のイベントと売れ行きの関係を把握しています。そこにAIが予測数量の数字だけを提示し、その数字が担当者の感覚と食い違った場合、根拠が何も示されなければ、担当者は自分の経験則を優先しやすくなります。予測が参照されなくなり、「AIは導入したが使われていない」という状態に陥りがちです。モデルの精度がどれほど高くても、使われなければその精度に意味はありません。

一方、同じ予測でも、気温の上昇がプラスに、前週のキャンペーン終了がマイナスに効いた、というように要因を添えて示せた場合は状況が変わります。担当者は自分の経験知と突き合わせて納得することもできますし、近隣の催事のようにモデルが取り込めていない情報を根拠に、修正を提案することもできます。つまり説明は、人とモデルが対話するためのインターフェースになります。技術的には、後の章で扱う局所的な説明手法で予測1件ごとの要因の寄与を算出し、寄与の大きい上位数件を業務用語に変換して予測値に添えて表示する、という実装がこの対話の土台になります。説明可能性は、このような形で導入の成否を左右する要素だと考えています。

現場の信頼に関してもう一点重要なのは、信頼は「高ければ高いほど良い」ものではない、という点です。根拠の見えないAIに対して現場が取る態度は二通りあります。一つは先ほどの「不信」で、モデルを無視して従来のやり方に戻るパターンです。もう一つは正反対の「過信」で、中身を確かめないままAIの出力を鵜呑みにするパターンです。過信は不信よりたちが悪いこともあります。モデルが間違えたとき、誰もそれを疑わないからです。人がシステムの出力を過度に信頼してしまう傾向は自動化バイアスと呼ばれ、航空や医療などの分野で古くから知られてきた現象です。説明可能性が目指すのは、盲目的な信頼ではなく、較正された信頼だと私は考えています。つまり、モデルが得意な場面では任せ、怪しい場面では人間が介入する。その切り分けを可能にするのが、判断根拠と自信度の提示なのです。

製造業の外観検査でも似た構図があります。画像認識モデルが「不良品」と判定したとき、ベテランの検査員が製品を見ても異常が見当たらないことがあります。このとき、モデルが画像のどこに注目して不良と判断したのかをヒートマップで示せれば、検査員は見落としていた微細な傷の存在を確認できる場合もありますし、逆に、照明の映り込みを傷と誤認していると見抜ける場合もあります。いずれの場合も、判定の根拠が見えることで検査プロセスは前に進みます。根拠が見えなければ、人とAIのどちらを信じるかという議論に決着をつける材料がなく、経験のある人間の判断が優先されてAIが使われなくなる、という状態に陥りやすくなります。

二つ目はデバッグです。説明手法の恩恵を最初に受けるのは、エンドユーザーではなく開発者自身であることが多いように思います。典型例が、いわゆるリーケージまたはデータリーク(本来予測時点では手に入らない情報が学習データに紛れ込むこと)の発見です。たとえば顧客の解約予測モデルで異常に高い精度が出たため特徴量の重要度を確認したところ、「解約手続きページの閲覧回数」が圧倒的に効いていた、というケースが典型です。これは解約を予測しているのではなく、解約という結果をほぼそのまま記録した変数を見ていただけです。重要度という説明を確認して初めて、この種の誤りに気づけます。

画像認識の分野では、さらに示唆的な事例が研究論文で報告されています。オオカミとハスキー犬を見分ける分類器が、動物の姿ではなく背景の雪に注目していた、というものです。学習データではオオカミの写真の多くに雪が写っていたため、モデルは「雪があればオオカミ」という近道の規則を学習してしまっていました。テストデータでも同じ偏りがあれば精度は高く出ますから、精度指標だけを見ていてはこの欠陥に気づけません。局所的な説明手法でモデルの注目箇所を可視化したことで、初めて問題が明らかになったのです。このような「ショートカット学習」は、本番環境でデータの分布が変わった瞬間に精度崩壊を引き起こします。説明は、モデルが正しい理由で正解しているのかを確かめる、ほとんど唯一の手段だと言えます。

三つ目は公平性です。海外の大手企業が開発していた採用支援AIが、特定の性別に不利な評価をする傾向を持っていたと報じられ、運用が取りやめられた事例はよく知られています。ここで重要なのは、性別や人種といった属性を特徴量から除外するだけでは問題が解決しない、という点です。住所、出身校、加入しているサービスといった一見中立な変数が、保護されるべき属性と相関している場合、モデルはそれらを代理変数(プロキシ)として使い、間接的に差別的な判断を再生産してしまいます。学習データに過去の人間の偏った判断が含まれていれば、モデルはその偏りを忠実に学びます。

説明手法だけで公平性が担保できるわけではありません。公平性の定義や測定にはそれ専用の方法論があります。ただ、その出発点として「モデルが実際に何に依存して判断しているのか」を見えるようにすることは不可欠です。依存関係が見えなければ、偏りの存在に気づくことも、修正することもできないからです。

データのバイアスは、公平性の問題に限りません。たとえば特定の期間や地域のデータばかりで学習していれば、モデルはその範囲の外で信頼できない予測を返します。センサーの故障や入力ミスに由来する異常値をモデルが「パターン」として学習してしまっていることもあります。大域的な説明手法でモデルの依存構造を眺めると、「なぜこの変数がこんなに効いているのか」という違和感から、データ収集や前処理の問題にさかのぼって気づけることが少なくありません。説明手法は、モデルの検査装置であると同時に、データの検査装置でもあるのです。

四つ目は説明責任です。与信の否決、保険の引き受け判断、採用選考、医療における診断支援。こうした個人の人生に影響を与えうる判断にAIが関与するとき、「AIが決めたので理由は分かりません」という回答は、顧客に対しても社会に対しても通用しません。判断の理由を問われたときに答えられる状態を保つことは、AIを使う組織の責務です。そしてこれは対外的な話だけではありません。社内の意思決定でも同じです。経営層がAIの提案を承認して施策を実行するとき、根拠を理解しないまま承認したのでは、その判断への責任を引き受けたことになりません。「なぜそう判断したのか」を人間の言葉で説明できる状態は、組織がAIの出力に責任を持つための前提条件なのです。

説明責任を具体的な業務に落とすと、次のようになります。顧客から申し込みが断られた理由の問い合わせを受けたとき、窓口の担当者が確認できるのがスコアの数値だけであれば、総合的な判断であるという趣旨の回答しかできず、顧客の不信を深めがちです。一方、その判断に効いた主な要因を局所的な説明手法で分解できていれば、どの点が基準に届かなかったのかを具体的に伝えられますし、顧客の側も、判断の元になった情報に誤りがあれば訂正を申し出ることができます。誤ったデータに基づく誤った判断を正す機会は、判断の理由が開示されて初めて生まれます。実務の面では、金融の与信のように、否決理由の上位要因をあらかじめ定義した理由コードに対応づけて通知する運用が古くから行われている分野もあり、局所的な説明手法は、この理由の割り当てを複雑な機械学習モデルでも成立させるための技術と位置づけられます。説明責任とは、組織が守りを固めるための概念であると同時に、判断される側の人が反論する権利を実質化する仕組みでもあります。この両面を押さえておくことが大切だと思います。

この四つの動機は、それぞれ独立しているようでいて、実際には深く絡み合っています。デバッグされ、偏りが点検されたモデルでなければ現場の信頼は得られませんし、信頼して使われているモデルでなければ説明責任の議論は始まりません。本ガイドで紹介する手法群は、この四つのすべてに対する共通の道具立てになります。

説明手法の分類学

XAIの分野には、SHAP、LIME、Permutation Importance、PDP、ICE、Grad-CAMなど多くの手法があります。それぞれの論文を個別に読んでも、相互の位置づけはなかなか見えてきませんが、いくつかの軸を導入すると見通しが良くなります。ここでは三つの軸で整理します。

分類に入る前に、そもそも「説明」とは誰に対する何なのか、という点を確認しておきます。同じモデルでも、説明の受け手によって求められる内容はまったく異なります。開発者が知りたいのは、モデルの誤りやデータの問題を発見するための技術的に詳細な説明です。現場の担当者が知りたいのは、目の前の一件について、自分の業務知識と突き合わせられる根拠です。顧客が求めるのは、自分への判断の理由と、必要なら異議を申し立てるための手がかりです。監査や規制当局が求めるのは、モデルの開発・検証プロセスが適切であったことを示す文書と証跡です。一つの説明ですべての受け手を満足させることはできません。この「受け手の多様性」を頭に置いておくと、これから述べる分類軸の意味がずっと掴みやすくなります。

第一の軸は、説明の範囲です。大域的説明(グローバルな説明)は、モデル全体としての傾向を対象にします。「このモデルはどの特徴量を重視しているのか」「年収が上がると予測はどう変化する傾向にあるのか」といった問いに答えるものです。モデルの全体像を把握したい開発者や、施策の方向性を判断したい経営層に向いた説明だと言えます。これに対して局所的説明(ローカルな説明)は、個々の予測の根拠を対象にします。「この申込者の審査がなぜ否決されたのか」「この画像がなぜ不良品と判定されたのか」という、一件一件への問いに答えます。顧客への説明や、現場担当者が個別案件を判断する場面で必要になるのはこちらです。

両者の違いは、具体例を並べるとはっきりします。「このモデルは全体として、勤続年数と年収を重視している」というのが大域的説明です。「この申込者については、年収は基準を満たしていたが、勤続年数の短さが大きくマイナスに働いた」というのが局所的説明です。同じモデルに対する説明でも、答えている問いがまったく違うことが分かると思います。導入プロジェクトでは、誰のどんな問いに答えたいのかを最初に確認しておかないと、作った説明機能が受け手の知りたいことと噛み合わない、ということが起こりがちです。

用語について一点補足します。この分野では「解釈可能性(interpretability)」と「説明可能性(explainability)」という二つの言葉が使われ、文献によって使い分けはさまざまです。おおまかには、モデルの構造そのものが人間に読める性質を解釈可能性、複雑なモデルの挙動を事後的な手法で人間に伝わる形にできる性質を説明可能性と呼び分けることが多いのですが、厳密な定義が定まっているわけではありません。本ガイドでは細かな用語論争には立ち入らず、「モデルの判断を人間が理解し検証できる状態を作る技術の総体」を広くXAIと呼ぶことにします。実務で大切なのは言葉の定義よりも、目の前の問いに答えられる説明が用意できているかどうかだからです。

第二の軸は、モデルへの依存性です。モデル固有(モデルスペシフィック)の手法は、特定の種類のモデルの内部構造を利用します。線形回帰の係数や決定木の分岐規則のように、モデルの構造そのものが説明になっている場合もあれば、Grad-CAMのように畳み込みニューラルネットワークの勾配情報を利用して注目領域を可視化する場合もあります。内部構造を直接見るぶん、説明の忠実度は高くなりやすい反面、適用できるモデルが限られます。

一方、モデル不可知(モデルアグノスティック)の手法は、モデルを入力と出力だけのブラックボックスとして扱います。入力を少しずつ変えながら出力の変化を観察することで、モデルがどの入力にどう反応するのかを外側から推定するのです。SHAP、LIME、Permutation Importance、PDPはいずれもこの発想に立っています。最大の利点は汎用性です。モデルを勾配ブースティングからニューラルネットワークに差し替えても、同じ説明の枠組みをそのまま使えます。モデル選定が流動的な実務のプロジェクトでは、この性質は非常にありがたいものです。

ここで一つ、忘れてはならない選択肢に触れておきます。そもそも複雑なモデルを使わず、線形回帰や浅い決定木、一般化加法モデル(GAM:特徴量ごとの効果を足し合わせる形のモデル)のような、構造自体が人間に読めるモデルを採用するという道です。これらは「解釈可能モデル」と呼ばれ、事後的な説明手法を持ち出すまでもなく、モデルそのものが説明になっています。研究者の中には、人の人生を左右する高リスクの意思決定では、ブラックボックスを後から説明するのではなく、最初から解釈可能なモデルを使うべきだと主張する立場もあります。実務でも、複雑なモデルとの精度差がわずかであれば、解釈可能モデルを選ぶほうが総合的に得だという場面は珍しくありません。精度と解釈性は常にトレードオフだと思われがちですが、まず単純なモデルで基準を作り、複雑化の利得を測ってから判断するのが健全な進め方だと思います。なお、文献ではこの違いを、モデル自体を最初から読める形にする「事前(アンテホック)の解釈可能性」と、学習済みモデルに後から手法を当てる「事後(ポストホック)の説明」として区別することもあります。本コラムの第2章以降で扱う手法の多くは後者にあたります。

第三の軸は、少し毛色が異なります。不確実性推定です。これは「なぜその答えか」ではなく「その答えにどれほど自信があるか」を扱うもので、厳密には根拠の説明とは別の概念です。しかし私は、これをXAIの実践に欠かせない一部だと考えています。というのも、信頼できるAIの条件には「分からないときに分からないと言えること」が含まれるからです。通常のニューラルネットワークは、学習データとかけ離れた入力に対しても、平然と高い確信度の予測を返すことがあります。MC Dropout(推論時にもドロップアウトを有効にして複数回予測し、そのばらつきから不確かさを見積もる手法)やベイズニューラルネットワーク(重みを一つの値ではなく確率分布として扱うモデル)は、予測に「自信の度合い」を添えることを可能にします。自信のない予測だけを人間の確認に回す、という運用の安全弁は、この軸の手法があって初めて設計できます。

不確実性には、性質の異なる二種類があることも、ここで頭出しをしておきます。一つはデータ自体に内在するばらつきに由来する不確実性で、どれだけデータを集めても消えません。サイコロの目が予測できないのと同じ種類のものです。もう一つは、モデルの知識不足に由来する不確実性で、学習データが少ない領域や、訓練時に見たことのない種類の入力で大きくなります。こちらはデータを追加すれば減らせます。実務ではこの区別が意思決定に直結します。前者が大きいなら「この問題はそもそもこれ以上は当てられない」と割り切る判断に、後者が大きいなら「このデータをもっと集めよう」という投資判断につながるからです。詳しくは不確実性推定の章で扱います。

三つの軸は、互いに独立に組み合わさります。たとえばSHAPは局所的な説明手法として設計されていますが、多数のデータに対する局所的な寄与を集計すれば、大域的な傾向の分析にも使えます。一件一件の説明と全体傾向の把握を同じ理論的枠組みで行き来できるこの性質は、実務でSHAPが広く使われている理由の一つです。逆に、PDPのような大域的手法で全体の傾向を掴んでから、気になる個別ケースをSHAPで深掘りする、という組み合わせもよく行われます。手法は一つを選んで終わりではなく、問いに応じて重ねて使うものだと捉えてください。

説明の範囲(大域・局所)とモデルへの依存性(不可知・固有)の2軸で主要なXAI手法を整理した分類マップ

以上の軸で、本ガイドで扱う代表的な手法を整理したのが次の表です。それぞれの手法は後の章で詳しく解説しますので、ここでは全体の地図として眺めてください。

手法説明の範囲モデルへの依存性主な対象と用途
SHAP局所(集約すれば大域的な分析にも使える)モデル不可知(決定木系には高速な専用アルゴリズムあり)テーブルデータ全般。個々の予測を特徴量ごとの寄与に分解する
LIME局所モデル不可知テーブル・テキスト・画像。予測の近傍を単純なモデルで近似して根拠を示す
Permutation Importance大域モデル不可知特徴量をシャッフルしたときの精度低下から、全体的な重要度を測る
PDP/ICE大域(ICEは1件ごとの曲線も描ける)モデル不可知特徴量の値と予測値の関係の「形」を可視化する
Grad-CAM系局所モデル固有(CNNなどの勾配情報を利用)画像分類・検査。モデルが注目した領域をヒートマップで示す
MC Dropout不確実性推定モデル固有(ドロップアウトを持つニューラルネット)予測の自信度を推定し、人間へのエスカレーション判断に使う
ベイズニューラルネットワーク不確実性推定モデル固有重みを分布として学習し、予測の不確かさを定量化する

手法選びの指針は、突き詰めれば一つです。「誰の、どんな問いに答えたいのか」から逆算すること。経営会議で施策の優先度を議論するなら、大域的な重要度や効果の形(Permutation Importance、PDP)が役立ちます。顧客一人ひとりに判断理由を伝えるなら、局所的な寄与の分解(SHAP、LIME)が必要です。画像検査の判定根拠を検査員に見せるならGrad-CAM系ですし、本番運用でモデルの暴走を防ぐ安全弁が欲しいなら不確実性推定の出番です。技術の目新しさから入るのではなく、問いから入る。この順序を守るだけで、XAI導入の失敗はかなり減らせると感じています。

最後に一つ注意を添えます。説明手法そのものも、モデルの挙動を近似的に写し取る道具にすぎません。相関の強い特徴量が並んでいると重要度の解釈が揺れる、近似の仕方によって説明が変わりうる、といった限界がそれぞれにあります。説明を鵜呑みにするのは、モデルを鵜呑みにするのと同じくらい危ういことです。本ガイドでは各手法の章で、使い方とあわせて「どこまで信じてよいか」の限界も必ず述べていきます。

規制と社会的要請

ここまでは、説明可能性を「AIをうまく使うための実務上の要件」として語ってきました。しかし近年、状況はもう一段階進んでいます。説明可能性は、各社の良心や工夫に委ねられたものから、規制と社会的要請によって求められるものへと変わりつつあるのです。

その象徴が、EU(欧州連合)のAI規則、いわゆるAI Actです。2024年に発効したこの規則は、AIを包括的に規律する世界で初めての本格的な法制度として広く注目されました。特徴はリスクベースアプローチにあります。AIシステムをそのリスクの大きさに応じて区分し、許容できないリスクをもたらす利用は禁止、高リスクに分類される利用には厳格な義務を課す、という構造です。雇用や与信、教育、重要インフラの管理といった、人の権利や安全に大きく関わる領域のAIが高リスクの典型例とされ、透明性の確保、技術文書や記録の整備、人間による監督といった要求が課されます。義務の適用は段階的に進む建て付けで、2024年8月の発効後、2025年2月から禁止AIに関する規定が、2025年8月から汎用AI(GPAI)モデルへの義務が適用され、高リスクAIを含む主要な規定は2026年8月以降に順次適用されます。対象となる事業者は計画的な対応が必要です。

日本企業にとっても、これは対岸の火事ではありません。AI Actは、EU域内にAIシステムやその出力を提供する域外の事業者にも適用が及びうる設計になっています。さらに、個人情報保護の分野でGDPR(EU一般データ保護規則)が事実上の世界標準となっていったのと同じように、EUの規制水準が国際的なビジネス慣行の基準になっていく可能性も指摘されています。EU市場と直接の接点がない企業でも、取引先や親会社経由で対応を求められる場面は今後増えていくだろうと私は見ています。

分野別に見ると、金融は説明への要請が最も古くからある領域です。米国では、信用供与を断る際に申込者へ主要な理由を通知する枠組みが、AI以前の時代から法制度として存在してきました。モデルが複雑になっても、この要請がなくなるわけではありません。むしろ、AIスコアリングを用いた与信では「なぜこの評価になったのか」を顧客に説明できることが、顧客保護の観点からも監督の観点からも重みを増しています。日本の金融機関でも、モデルの検証体制や説明可能性への配慮は、モデルリスク管理の一部として位置づけられるようになっています。与信判断の根拠を答えられないシステムは、精度がどうであれ、この分野では採用しにくいのが現実です。

医療も同様です。診断支援AIの最終判断を下すのは医師であり、医師が根拠を確認できないシステムは臨床の現場に受け入れられにくいという構造があります。画像診断の支援であれば、モデルがどの領域に注目して判定したのかをヒートマップで示せることが、医師の読影プロセスと噛み合うかどうかを大きく左右します。また、医療機器としての審査や品質管理の文脈でも、性能の根拠や適用限界を明確に示すことの重要性は増す一方です。人命に関わる領域ほど、「当たるが理由は不明」という道具の居場所は狭くなります。

医療で特徴的なのは、説明の受け手が医師だけではないことです。医師は患者に治療方針を説明し、同意を得る責任を負っています。AIの示唆を判断材料に使うなら、その内容を患者に伝わる言葉に翻訳できなければなりません。つまり医療AIの説明は、「モデルから医師へ」と「医師から患者へ」という二段階の伝達に耐える必要があるのです。技術的に精緻なだけの説明では、この二段目で詰まります。説明の設計は、最終的にそれを受け取る人の目線から逆算すべきだという先ほどの原則が、ここでも効いてきます。

個人データ保護の文脈でも、自動化された意思決定に対する関心は国際的に高まっています。EUのGDPRには、個人に重大な影響を及ぼす完全自動の決定に関する規定が置かれており、自動的な判断の対象となる本人の保護という考え方は、各国の制度設計や企業のプライバシーポリシーに広く影響を与えてきました。AIによる判断が個人データの処理と不可分である以上、AI規制とデータ保護規制は今後も重なり合いながら発展していくはずです。企業側から見れば、「この判断はどのデータに基づき、どんなロジックで行われたのか」を整理して答えられる体制は、両方の規制文脈で共通に効いてくる投資だと言えます。

日本国内に目を向けると、経済産業省と総務省が2024年に「AI事業者ガイドライン」を公表しています。AIの開発者、提供者、利用者という立場ごとに求められる取り組みを整理したもので、透明性やアカウンタビリティ(説明責任)が共通の指針として掲げられています。法的拘束力を持たないソフトローではありますが、取引先からの要求水準や、社会がAI活用企業に向ける期待の物差しとして機能し始めています。「ガイドラインに沿った体制になっているか」が、企業間取引の与件として問われる場面は既に出てきています。

こうした外部からの要請に応えるうえで、技術としてのXAIと同じくらい重要なのが、社内のガバナンス体制です。どのAIシステムが高リスクに該当しうるのかを棚卸しする。モデルの開発時にどんな説明手法でどんな検証を行ったかを記録する。運用開始後にモデルの挙動を監視し、想定外の偏りや精度劣化が見つかったときの対応手順を決めておく。説明手法は、こうしたプロセスの中に組み込まれて初めて、組織としての説明責任を支える道具になります。ツールを導入しただけで「わが社のAIは説明可能です」と言えるわけではない、という点は強調しておきたいところです。逆に言えば、本ガイドで扱う手法群を開発と運用の標準プロセスに織り込んでおけば、規制環境が変化しても慌てずに対応できる足腰ができます。

実務の目線で一つ付け加えます。規制対応を目的化すると、XAIへの取り組みは「監査向けの文書を揃えて終わり」になりがちです。しかし本章で見てきたとおり、説明可能性の本丸は現場と顧客の信頼を獲得し、モデルの誤りに早く気づける体制を作ることにあります。規制は守るべき最低ラインを示しているのであって、ゴールではありません。信頼されるAIを作る努力の結果として規制要件が自然に満たされている、というのが健全な姿だと思います。

本章の内容をまとめます。説明可能なAIが必要な理由は、現場の信頼、デバッグ、公平性、説明責任という四つの実務的な動機に加えて、規制と社会的要請という外部からの力によっても支えられています。そして幸いなことに、この要請に応えるための手法群は、大域と局所、モデル固有とモデル不可知、そして不確実性推定という軸の上に、かなり整然と整理できる状態まで成熟してきました。次章からは、この地図に沿って個々の手法の中身に分け入っていきます。まずは、テーブルデータの実務で出番の多い第2部の手法から試すのがおすすめです。

この章を深めたい方への参考書籍

  • 森下光之助『機械学習を解釈する技術』(技術評論社):Permutation Importance、PDP、ICE、SHAPといった本ガイドの中核手法を、理論とPython実装の両面から一歩ずつ確かめられる一冊です。数式とコードの距離が近く、手を動かしながら納得したい実務者に最適だと思います。
  • 大坪直樹ほか『XAI(説明可能なAI)』(リックテレコム):XAIの主要な考え方と手法を幅広く見渡せる入門書です。個別手法に潜る前に分野の全体像をつかんでおきたい方の、最初の一冊に向いています。

第2部 テーブルデータ編:この予測に何が効いたのか

第2部は、業務データの多くを占めるテーブルデータの説明手法です。まず解釈可能なモデルと基本手法(Permutation Importance、PDP)で土台を作り、LIME、SHAPへと進みます。

第2章 まず解釈可能なモデルから、そして基本の説明手法

この章から、テーブルデータを対象にした説明手法の各論に入ります。説明可能性の出発点は、SHAPでもLIMEでもなく、「そもそも中身が読めるモデルを使う」という選択です。線形回帰や決定木のような解釈可能なモデルは、予測の仕組みそのものが人間に読める形をしています。まずこれらで基準を作り、そのうえで、より複雑なモデルにはモデルに依存しない基本手法(Permutation ImportanceとPDP・ICE)を当てる。この順番で道具を増やしていくのが、遠回りに見えて一番確実だと思います。

実務の現場では、いきなり勾配ブースティングやニューラルネットワークを組み、後からSHAPで説明を付ける、という進め方がよくあります。それ自体が悪いわけではないのですが、シンプルなモデルとの精度差を確かめないまま複雑なモデルを選ぶと、「説明の難しさ」という追加コストを、見合うかどうか分からないまま支払うことになります。線形モデルで精度が僅かしか変わらないなら、説明責任の重い業務では線形モデルを選ぶ方が合理的、という判断は十分にあり得ます。その判断をするためにも、まず解釈可能なモデルを一度は通す価値があります。

この章で扱う内容は次の4つです。いずれもscikit-learnだけで動くコード例を付けましたので、手元のPython環境でそのまま試せます。

  • 解釈可能なモデルという選択:線形回帰の係数、ロジスティック回帰のオッズ比、決定木のルール
  • Permutation Importance:特徴量をシャッフルして性能低下を測る、モデル非依存の重要度
  • PDP・ICE:特徴量と予測の関係を「形」として可視化する部分依存プロット
  • 手法の使い分け:この章の手法と、次章以降で扱うLIME・SHAPの関係の整理

そもそも解釈可能なモデルを使うという選択

説明可能AIの文脈では、モデルは大きく2種類に分けられます。1つは、モデルの構造自体が人間に読める「本質的に解釈可能なモデル」(線形回帰、ロジスティック回帰、決定木など)。もう1つは、構造が複雑で直接は読めない「ブラックボックスモデル」(ランダムフォレスト、勾配ブースティング、ニューラルネットワークなど)に、後から説明手法を適用するアプローチです。前者は説明がモデルの仕組みそのものなので、説明と実際の挙動が食い違う心配がありません。この安心感は、後付けの説明手法にはない大きな利点です。

まず線形回帰から見ていきます。線形回帰は、予測値を各特徴量の重み付き和で表すモデルです。数式で書くと \( \hat{y} = \beta_0 + \beta_1 x_1 + \beta_2 x_2 + \cdots + \beta_p x_p \) となり、係数 \( \beta_j \) が「特徴量 \( x_j \) が1単位増えたとき、他の特徴量が変わらなければ予測値がどれだけ動くか」をそのまま表します。scikit-learn付属の糖尿病データセット(load_diabetes:患者442名、10特徴量から1年後の疾患進行度を予測する回帰問題)で確かめてみます。

import pandas as pd
from sklearn.datasets import load_diabetes
from sklearn.linear_model import LinearRegression

data = load_diabetes()
X = pd.DataFrame(data.data, columns=data.feature_names)
y = data.target

model = LinearRegression()
model.fit(X, y)

# 係数を絶対値の大きい順に表示
coef = pd.Series(model.coef_, index=X.columns)
print(coef.sort_values(key=abs, ascending=False))

実行すると、s1(血清指標の1つ)、bmi(肥満度指数)、s5などの係数が大きく出ます。ここで1つ、実務上とても大事な注意があります。係数の大きさを特徴量どうしで比較できるのは、各特徴量のスケールがそろっている場合だけです。load_diabetesは初めからスケール済みのデータが返るので比較できますが、生データを扱うときは、StandardScalerなどで標準化してから学習しないと、「単位が小さい特徴量ほど係数が大きく見える」という見かけ上の逆転が起きます。係数を説明に使うなら、標準化は事実上必須と考えてください。もう1つ、係数の解釈は常に「他の特徴量を固定したとき」という条件付きです。特徴量どうしの相関が強いと、係数が不安定になったり直感に反する符号が出たりします(多重共線性と呼ばれる現象です)。

次に、分類問題で使うロジスティック回帰です。ロジスティック回帰は、あるクラスに属する確率 \( p \) の対数オッズを線形式で表すモデルで、 \( \log \frac{p}{1-p} = \beta_0 + \beta_1 x_1 + \cdots + \beta_p x_p \) と書けます。ここで係数を指数変換した \( e^{\beta_j} \) は「オッズ比」と呼ばれ、「特徴量が1単位増えるとオッズ(そのクラスに属する見込みと属さない見込みの比)が何倍になるか」を表します。オッズ比が2なら見込みが2倍、0.5なら半分、という読み方ができるため、医療統計や与信審査の世界では昔から説明の共通言語として使われてきました。乳がん診断データセット(load_breast_cancer:腫瘍の計測値569件、30特徴量から良性・悪性を判別する二値分類)で見てみます。

import numpy as np
import pandas as pd
from sklearn.datasets import load_breast_cancer
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler

data = load_breast_cancer()
X = pd.DataFrame(data.data, columns=data.feature_names)
y = data.target  # 0が悪性、1が良性

# 標準化してから学習(係数比較の前提)
pipe = make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000))
pipe.fit(X, y)

coef = pipe.named_steps["logisticregression"].coef_[0]
odds_ratio = pd.Series(np.exp(coef), index=X.columns)
print(odds_ratio.sort_values())  # 小さいほど悪性方向に効く

標準化済みなので、ここでのオッズ比は「その特徴量が1標準偏差増えたときにオッズが何倍になるか」という意味になります。1より大きければ良性方向、1より小さければ悪性方向に効く特徴量です。「この計測値が大きいほど悪性の見込みが上がる」という説明は、医師にも監査にもそのまま通じる形をしています。

3つ目は決定木です。決定木は「もしAがX以下なら左へ、そうでなければ右へ」という分岐ルールの積み重ねで予測するモデルで、木が浅ければ、モデル全体をそのままフローチャートとして人に見せられます。scikit-learnにはルールをテキストで出力するexport_textという関数があります。

from sklearn.datasets import load_diabetes
from sklearn.tree import DecisionTreeRegressor, export_text

data = load_diabetes()
model = DecisionTreeRegressor(max_depth=3, random_state=42)
model.fit(data.data, data.target)

print(export_text(model, feature_names=list(data.feature_names)))

深さ3に制限した木なら、出力されるルールは十数行に収まり、「bmiが高く、かつs5が高い患者は進行度の予測値が高い」といった読み下しができます。業務ルールへの落とし込みや、現場担当者への説明には、この形式が一番伝わると感じる場面が多いです。ただし深さを増やすほどルールは爆発的に増え、解釈可能性は急速に失われます。解釈可能なモデルとしての決定木は、深さ3〜4程度までが実用的な目安です。

そして冒頭に述べた通り、解釈可能なモデルの最大の価値は「基準線(ベースライン)を作れること」にあります。線形モデルや浅い決定木でまず精度を測っておけば、その後に試す複雑なモデルの精度向上分が、説明の難しさという追加コストに見合うかを定量的に判断できます。経営層への報告でも、「シンプルなモデルでここまで、複雑なモデルでここまで出ます。差はこれだけなので、説明責任を優先してシンプルな方を採用します(あるいは、差が大きいので複雑なモデルに説明手法を組み合わせます)」という論理が立ちます。この判断の土台を作らずに複雑なモデルへ進むのは、もったいない省略だと思います。

Permutation Importance

ここからは、ランダムフォレストや勾配ブースティングのようなブラックボックスモデルにも使える、モデル非依存(model-agnostic)の手法に入ります。最初はPermutation Importance(並べ替え重要度)です。発想はとても素朴で、「ある特徴量の列だけをランダムにシャッフルして、モデルの性能がどれだけ落ちるかを測る」というものです。シャッフルすると、その特徴量と正解との対応関係だけが壊れます。それで性能が大きく落ちるなら、モデルはその特徴量に強く頼っていたことになりますし、ほとんど落ちないなら、その特徴量はあってもなくても予測に効いていなかったことになります。モデルの中身を一切覗かずに、入出力の振る舞いだけから重要度を測れるのがポイントです。

scikit-learnではsklearn.inspection.permutation_importanceとして実装されています。カリフォルニア住宅価格データセット(fetch_california_housing:1990年の国勢調査に基づく約20,600地区のデータで、所得や築年数など8特徴量から地区の住宅価格の中央値を予測する回帰問題)にランダムフォレストを当てはめ、木モデルが標準で持つ不純度ベースの重要度(feature_importances_)と並べて計算してみます。

import pandas as pd
from sklearn.datasets import fetch_california_housing
from sklearn.ensemble import RandomForestRegressor
from sklearn.inspection import permutation_importance
from sklearn.model_selection import train_test_split

X, y = fetch_california_housing(return_X_y=True, as_frame=True)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.25, random_state=42
)

model = RandomForestRegressor(n_estimators=100, random_state=42, n_jobs=-1)
model.fit(X_train, y_train)

# (a) 不純度ベースの重要度:学習時の分岐の貢献から計算される
imp_impurity = pd.Series(model.feature_importances_, index=X.columns)
print(imp_impurity.sort_values(ascending=False))

# (b) Permutation Importance:テストデータで性能低下を測る
result = permutation_importance(
    model, X_test, y_test, n_repeats=10, random_state=42, n_jobs=-1
)
imp_perm = pd.Series(result.importances_mean, index=X.columns)
print(imp_perm.sort_values(ascending=False))
print(pd.Series(result.importances_std, index=X.columns))  # ばらつきも確認

n_repeatsはシャッフルを繰り返す回数で、繰り返しの平均(importances_mean)と標準偏差(importances_std)が得られます。シャッフルは乱数に依存するので、平均だけでなくばらつきも併せて見るのが作法です。標準偏差が平均に対して大きい特徴量の順位は、あまり真に受けない方がよいということになります。

さて、木モデルには最初からfeature_importances_があるのに、なぜわざわざPermutation Importanceを使うのか。両者の違いは実務上かなり重要です。feature_importances_は不純度ベースの重要度と呼ばれ、学習中に各特徴量が分岐に使われてどれだけ予測のばらつきを減らしたかを集計したものです。手軽な一方で、よく知られた弱点が2つあります。

  • 取り得る値の種類が多い特徴量(連続値や高カーディナリティのカテゴリ変数)ほど分岐の候補が多く、重要度が高く出やすい偏りがあります。極端な話、予測と無関係なランダムなID列が上位に来ることさえあります
  • 学習データに対する計算なので、モデルが過学習していると、汎化に寄与していない特徴量まで重要に見えてしまいます

Permutation Importanceはこの2つの弱点を素直に回避できます。値の種類の多さに影響されにくく、そして何より、学習に使っていないテストデータ上で計算すれば「未知のデータへの予測性能に実際に効いている特徴量」を測れます。実務では、feature_importances_は学習直後に大まかな当たりを付ける用途にとどめ、報告資料に載せる重要度はテストデータでのPermutation Importanceに統一する、という使い分けをおすすめします。

ただしPermutation Importanceにも固有の注意点があります。最大のものは、相関の強い特徴量の扱いです。第一に、ある特徴量をシャッフルしても、強く相関する別の特徴量が残っていればモデルはそちらから同じ情報を得られるため、性能はあまり落ちず、本当は重要な特徴量の重要度が2つの特徴量に分散して両方とも過小評価される、ということが起きます。第二に、シャッフルは特徴量どうしの相関構造を壊すので、「築100年なのに新築並みの設備」のような現実には存在しない組み合わせのデータ点を作り出し、モデルにとって未知の領域での挙動を測ってしまう問題もあります。相関の強い特徴量群がある場合は、群ごとにまとめてシャッフルする、事前に片方を落とす、クラスタリングで代表特徴量を選ぶ、といった対処を検討してください。また、重要度が低いことは「その特徴量と目的変数が無関係」を意味しない点にも注意が必要です。測っているのはあくまで「このモデルがその特徴量に頼っているか」であって、データそのものの因果や関連ではありません。

PDP・ICE

Permutation Importanceは「どの特徴量が効いているか」という順位を教えてくれますが、「どう効いているか」までは分かりません。所得が上がるほど住宅価格の予測は上がるのか、直線的にか、途中で頭打ちになるのか。この「効き方の形」を可視化するのが、部分依存プロット(PDP: Partial Dependence Plot)です。

PDPの作り方は次の通りです。注目する特徴量(例えば所得MedInc)を、データ全体である値(例えば3.0)に強制的に置き換え、全サンプルの予測値の平均を取ります。これを値を少しずつ変えながら繰り返すと、「その特徴量の値」対「予測値の平均」という曲線が描けます。つまりPDPは、他の特徴量の分布はそのままに、注目特徴量だけを動かしたときのモデルの平均的な応答を表した図です。scikit-learnではPartialDependenceDisplayで1行で描けます。

import matplotlib.pyplot as plt
from sklearn.inspection import PartialDependenceDisplay

# 前節で学習済みのmodelとX_testをそのまま使う
features = ["MedInc", "HouseAge", "AveOccup"]
PartialDependenceDisplay.from_estimator(model, X_test, features)
plt.tight_layout()
plt.show()

カリフォルニア住宅価格の例では、MedInc(地区の所得の中央値)のPDPは右上がりで、高所得の領域で傾きが緩やかになる形が現れます。「所得が高い地区ほど住宅価格の予測は高いが、一定以上では伸びが鈍る」という、モデルが学習した関係の形を、そのまま報告資料に載せられる図として得られるわけです。特徴量の重要度の棒グラフよりも、この「形」の方が業務の議論を喚起することが多いと感じます。頭打ちの位置や急変する箇所は、ドメイン知識との突き合わせどころだからです。

ただしPDPには「平均の図である」ことに由来する落とし穴があります。例えば、サンプルの半分では右上がり、残り半分では右下がり、という異質な効果が混在している場合、平均であるPDPはほぼ水平になり、「この特徴量は効いていない」ように見えてしまいます。これを見抜くための道具がICE(Individual Conditional Expectation)です。ICEは平均を取る前の、サンプル1件1件の曲線をそのまま重ねて描きます。PartialDependenceDisplayではkind引数を変えるだけで描けます。

# ICE(個々の線)とPDP(平均線)を重ねて描く
# 全件描くと図が潰れるためsubsampleで間引く
PartialDependenceDisplay.from_estimator(
    model, X_test, ["MedInc"],
    kind="both", subsample=100, random_state=42
)
plt.show()

kind=”average”がPDPのみ、kind=”individual”がICEのみ、kind=”both”が両方の重ね描きです。ICEの線がほぼ平行にそろっていればPDPの平均線を信頼してよく、線の向きや形がばらばらなら「効き方がサンプルによって異なる」サインなので、どんなサンプルで向きが違うのかを掘り下げる価値があります(この掘り下げは、次章以降の局所的な説明手法の出番でもあります)。

もう1つ、Permutation Importanceと同根の注意点として、相関の強い特徴量の問題があります。PDPは「注目特徴量だけを動かし、他は固定」という操作をするため、特徴量間に強い相関があると、現実にはあり得ない組み合わせの領域(例えば、部屋数AveRoomsだけを極端に増やして寝室数AveBedrmsは据え置いた地区)での予測値を平均に含めてしまいます。モデルはそうした外挿領域では信頼できる根拠を持たないので、曲線の形が歪むことがあります。相関が強い場合の対処としては、データの分布に従って条件付けするALE(Accumulated Local Effects)という代替手法が知られています。scikit-learnには入っていませんが、PDPを深く使うなら名前だけでも覚えておく価値があります。実務上はまず、PDPを描く前に特徴量間の相関行列を確認し、強い相関ペアがある特徴量のPDPは慎重に読む、という習慣を付けるのがよいと思います。

なお、PartialDependenceDisplayは2つの特徴量の組(タプル)を渡すと2次元のPDPも描けます。緯度と経度のように、組み合わせで初めて意味を持つ特徴量の効き方(交互作用)を見るのに便利です。

手法の使い分け

この章では、解釈可能なモデルそのもの、Permutation Importance、PDP・ICEという3種類の道具を見てきました。いずれも「モデル全体の傾向」を説明する、大域的(グローバル)な説明が中心です。一方、実務で必ず出てくる「この顧客の審査が通らなかったのはなぜか」という問いは、個々の予測1件を説明する局所的(ローカル)な説明であり、これは次章以降で扱うLIMEやSHAPの守備範囲になります。ここで一度、両者の関係を1つの表に整理しておきます。

手法説明の範囲対象モデル得意なこと主な注意点
解釈可能なモデル(線形回帰・ロジスティック回帰・決定木)大域・局所の両方そのモデル自体仕組みごと説明でき、説明と挙動が食い違わない。精度の基準線になる表現力に限界。係数比較には標準化が前提。多重共線性で係数が不安定になる
Permutation Importance大域(重要度の順位)任意(モデル非依存)汎化性能に効く特徴量をテストデータで測れる。不純度ベースの偏りを回避相関の強い特徴量で過小評価。シャッフルが非現実的なデータ点を作る
PDP・ICE大域(ICEは個々の傾向も見える)任意(モデル非依存)特徴量の効き方の「形」が分かる。頭打ちや非線形性の発見相関が強いと外挿領域を平均に含む。PDPは異質な効果を平均で隠す(ICEで確認)
LIME(次章)局所(予測1件ごと)任意(モデル非依存)個々の予測の近傍を単純なモデルで近似して説明近傍データの作り方次第で説明が揺れる
SHAP(次章以降)局所が基本。集約すれば大域も任意(木モデルには高速な専用実装)予測値を特徴量ごとの寄与に加法分解。理論的な裏付けを持つ計算コストが高い場合がある。相関の強い特徴量では解釈に注意

使い分けの考え方を、私なりの手順としてまとめると次のようになります。

  1. まず線形モデルや浅い決定木で精度の基準線を作り、複雑なモデルとの差を測る。差が小さければ解釈可能なモデルの採用を第一候補にする
  2. 複雑なモデルを使うと決めたら、テストデータでのPermutation Importanceで「何が効いているか」の全体像を押さえる
  3. 上位の特徴量についてPDP・ICEで「どう効いているか」を確認し、ドメイン知識と矛盾しないか突き合わせる
  4. 個々の予測への説明が必要になった段階で、SHAPやLIMEなどの局所的な手法を導入する

大事なのは、これらが競合ではなく積み重ねの関係にあることです。SHAPを導入したらPDPが不要になる、という話ではありません。大域的な手法でモデルの全体像を検証し、局所的な手法で個別の判断を説明する。両方がそろって初めて、開発者・経営層・監査それぞれへの説明が成り立ちます。また、この章の手法はすべてscikit-learn標準機能だけで動くため、追加ライブラリの導入審査が厳しい企業環境でもすぐに始められるという実務上の利点もあります。まずここから着手し、必要に応じて次章以降の手法へ進むのが、負担の少ない導入順序だと思います。

Anagraftでは、AIプロジェクトの構想・課題設計から、データ分析・機械学習モデルの開発、AI人材の育成まで一貫したご支援を行っています。ご相談は、以下よりお問い合わせください。

お問い合わせ

この章を深めたい方への参考書籍

クリストフ・モルナー『Interpretable Machine Learning』。機械学習の解釈手法を体系的にまとめた定番テキストで、原著はWebで無償公開されており、日本語訳も公開されています。この章で扱った解釈可能なモデル、Permutation Importance、PDP・ICEに加え、ALEやSHAPまで、各手法の理論と長所短所が丁寧に整理されています。手法を1つ使うたびに該当章を読み返す、という辞書的な使い方ができる1冊です。

Web版はこちら

第3章 LIME、個々の予測を局所的に説明する

前章までに見てきたPermutation ImportanceやPDPは、「モデル全体として、どの特徴量がどう効いているか」を捉える大域的な説明手法でした。しかし実務で本当に問われるのは、多くの場合「この1件」についての説明です。この顧客の与信申請がなぜ否決されたのか。この患者がなぜ高リスクと判定されたのか。この製品がなぜ不良と分類されたのか。全体傾向の説明では、目の前の1件に対する問いには答えられません。

この「個々の予測に対する説明」を、モデルの種類を問わずに実現する代表的な手法がLIME(Local Interpretable Model-agnostic Explanations)です。2016年にRibeiroらが発表した論文「“Why Should I Trust You?”: Explaining the Predictions of Any Classifier」(KDD 2016)で提案され、XAIという分野が広く注目されるきっかけを作りました。名前のとおり、Local(局所的)に、Interpretable(解釈可能)な形で、Model-agnostic(モデルの種類を問わず)に説明を作る、という設計思想が名前にそのまま刻まれています。

本章ではまずLIMEの考え方を直感から数式まで丁寧に追いかけ、次にPythonのlimeライブラリを使ったテーブルデータでの実装を手を動かせる形で示します。最後に、LIMEが抱える限界と、それでもなおLIMEを選ぶべき場面を整理し、次章のSHAPへとつなぎます。

LIMEの考え方

LIMEの発想を一言で表すと、「複雑なモデルの全体を理解するのは諦めて、知りたい1点の周りだけを単純なモデルで近似する」となります。地球は球体ですが、私たちが暮らす半径数キロメートルの範囲では平面とみなして地図を描いても実用上困りません。それと同じで、深層学習や勾配ブースティングが作る複雑な決定境界も、ある1つの予測点のごく近くに限れば、線形モデルのような単純な形で十分に真似できるはずだ、というのがLIMEの根本的なアイデアです。

具体的な手順は次のとおりです。説明したい予測点を1つ選んだら、LIMEはその点の特徴量に少しずつランダムな変化を加えた「摂動サンプル」を大量に生成します。摂動(せつどう)とは、元のデータをわずかに揺らして似たようなデータを作る操作のことです。たとえば「年齢45歳、年収600万円、勤続12年」という申請者を説明したいなら、「年齢47歳、年収580万円、勤続12年」「年齢44歳、年収650万円、勤続9年」といった近傍のバリエーションを何千件も作るイメージです。

次に、その摂動サンプルの一つひとつを、説明対象であるブラックボックスモデルに入力し、予測値を得ます。ここが重要なポイントで、LIMEはモデルの内部構造を一切見ません。入力を与えたら出力が返ってくる、という関係だけを利用します。だからこそ、ニューラルネットワークでも勾配ブースティングでもアンサンブルでも、predict関数さえ呼べればどんなモデルにも適用できるのです。これがModel-agnosticの意味です。

最後に、「摂動サンプルを入力、ブラックボックスの予測値を出力」とする学習データを作り、それに対して線形回帰などの解釈しやすいモデル(代理モデル、サロゲートモデルと呼びます)を当てはめます。このとき、元の予測点に近いサンプルほど重みを大きく、遠いサンプルほど重みを小さくして学習します。近さの重み付けにはカーネル関数(距離が離れるほど滑らかに重みが減る関数)が使われます。こうして得られた線形モデルの回帰係数が、そのまま「この予測において、各特徴量がどちら向きにどれだけ効いたか」の説明になります。

論文では、この手続きが1本の最適化問題として定式化されています。説明したい点を \( x \)、ブラックボックスモデルを \( f \)、解釈可能なモデルの候補集合を \( G \) として、LIMEが求める説明 \( \xi(x) \) は次の式で表されます。

\( \xi(x) = \arg\min_{g \in G} \; L(f, g, \pi_x) + \Omega(g) \)

ここで \( L(f, g, \pi_x) \) は、近傍の重み \( \pi_x \) のもとで代理モデル \( g \) がブラックボックス \( f \) をどれだけ忠実に再現できているかを測る損失です(局所忠実度と呼ばれます)。\( \Omega(g) \) は代理モデルの複雑さへのペナルティで、たとえば「説明に使う特徴量は多くても10個まで」といった制約に相当します。つまりLIMEは、「局所的にはできるだけ本物に忠実で、かつ人間が読める程度に単純な説明」を探す最適化だと読めます。忠実さと分かりやすさはトレードオフの関係にあり、その折り合いを式の形で明示したところに、この論文の美しさがあると感じます。

もう1つ、LIMEの設計で見逃せないのが「解釈可能な表現」という考え方です。モデルが実際に受け取る特徴量と、人間に見せる説明の単位を分けて考えます。テーブルデータなら、連続値をそのまま使う代わりに「年収が400万円から600万円の範囲にある」のような区間に離散化して提示します。テキストなら単語の有無、画像ならスーパーピクセル(色や質感が似た画素のまとまり)の有無を説明の単位にします。摂動も、この解釈可能な表現の上で「単語を抜く」「画像の一部領域を灰色で塗りつぶす」といった形で行われます。説明は人間が読むものである以上、人間の認知に合わせた語彙で語るべきだ、という思想がここに表れています。

まとめると、LIMEの手順は次の4ステップです。

  1. 説明したい予測点を選ぶ
  2. その点の近傍で特徴量を摂動させたサンプルを大量に生成する
  3. 各サンプルに対するブラックボックスモデルの予測値を取得する
  4. 元の点への近さで重み付けしながら、単純な線形モデルで局所近似し、その係数を説明として提示する

Pythonでの実装

実装に移ります。LIMEは論文の著者ら自身が公開しているlimeライブラリで手軽に試せます。pipでインストールできます。

pip install lime

テーブルデータの分類問題を例にします。scikit-learn付属の乳がん診断データセット(良性か悪性かの2クラス分類、特徴量30個)でランダムフォレストを学習し、テストデータの1件についてLIMEで説明を作ってみます。テーブルデータ用のクラスはLimeTabularExplainerです。

import numpy as np
from sklearn.datasets import load_breast_cancer
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import train_test_split
from lime.lime_tabular import LimeTabularExplainer

# データの準備とモデルの学習
data = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(
    data.data, data.target, test_size=0.2, random_state=42
)
model = RandomForestClassifier(n_estimators=200, random_state=42)
model.fit(X_train, y_train)

# LIMEの説明器を作る
explainer = LimeTabularExplainer(
    training_data=X_train,
    feature_names=data.feature_names,
    class_names=data.target_names,
    mode='classification',
    discretize_continuous=True,
    random_state=42
)

# テストデータの1件を説明する
i = 0
exp = explainer.explain_instance(
    data_row=X_test[i],
    predict_fn=model.predict_proba,
    num_features=8,
    num_samples=5000
)

# 特徴量と寄与のリストを表示
for feature, weight in exp.as_list():
    print(f'{feature}: {weight:+.4f}')

コードのポイントを順に見ていきます。まずLimeTabularExplainerの初期化には、学習データそのもの(training_data)を渡します。これは摂動サンプルを作る際に、各特徴量の平均や分散、分布の情報が必要になるためです。discretize_continuous=Trueを指定すると、連続値の特徴量を四分位で区間に離散化し、説明を「worst radiusが16.8以下」のような読みやすい条件文の形で出してくれます。先ほど述べた「解釈可能な表現」がここに対応します。random_stateは摂動の乱数シードで、再現性の確保のために実務では必ず固定することをおすすめします。

説明の生成はexplain_instanceです。説明したい1行のデータ(data_row)と、モデルの予測関数を渡します。分類問題ではクラスごとの確率を返すpredict_probaを渡す点に注意してください。num_featuresは説明に使う特徴量の数で、先ほどの数式の \( \Omega(g) \) に相当します。num_samplesは生成する摂動サンプルの数で、既定値は5000です。増やすほど説明は安定しますが、そのぶんモデルの予測をたくさん呼ぶので時間がかかります。

結果はas_list()で「特徴量の条件と重みのペア」として取り出せます。重みが正ならそのクラスの予測確率を押し上げる方向に、負なら押し下げる方向に働いたことを意味します。たとえば「worst perimeterが小さい: +0.12」「mean concave pointsが小さい: +0.09」のような出力が並べば、「この検体が良性と予測されたのは、腫瘍の外周や凹点に関する値が小さかったことが主因」と読めるわけです。Jupyter環境ならexp.show_in_notebook()で棒グラフ付きの可視化がその場で表示できますし、exp.save_to_file(’explanation.html’)でHTMLファイルとして保存すれば、ノートブックを使わない関係者への共有にも使えます。

実務で使う際の細かい注意も2つ補足します。1つ目はカテゴリ変数の扱いです。性別や地域のようなカテゴリ変数が含まれる場合は、categorical_features引数に該当する列番号のリストを渡します。指定しないと数値として摂動されてしまい、「性別が0.7」のような意味をなさないサンプルが生成されてしまいます。2つ目は前処理との関係です。標準化などの前処理を挟んでいる場合、LIMEに渡すtraining_dataとdata_row、そしてpredict_fnの入出力の空間を揃える必要があります。scikit-learnのPipelineでモデルと前処理を1つにまとめ、Pipelineごとpredict_probaを渡すのが最も間違いのないやり方です。

LIMEの限界と使いどころ

LIMEは直感的で強力な手法ですが、実務投入の前に知っておくべき限界がいくつかあります。この限界を把握しないままLIMEの出力を報告資料に載せてしまうケースは実務でも起こりがちなので、順に整理します。

最大の問題は、説明が安定しないことです。LIMEの説明は乱数で生成した摂動サンプルから作られるため、乱数シードを変えて同じ予測点を説明させると、特徴量の順位や重みが変わることがあります。ひどい場合には寄与の符号が反転することさえあります。対策としては、乱数シードの固定に加えて、num_samplesを十分に増やすこと、同じ点に対して複数回説明を生成して結果のばらつきを確認すること、が挙げられます。ばらつきが大きい特徴量の寄与は、断定的に語らないのが誠実な態度だと思います。安定性を定量的に確認する簡便な方法としては、同一条件で複数回生成した説明について特徴量の順位の一致度(順位相関)を測る、寄与値の平均と標準偏差を併記する、といったやり方があります。

2つ目の問題は、「近傍」の定義に説明が依存することです。局所近似という発想は、「どこまでを局所と見なすか」を決めるカーネル幅というパラメータに支えられています。カーネル幅を狭くとれば説明はより局所的になり、広くとれば大域的な傾向に近づきます。つまり同じ点、同じモデルであっても、近傍の取り方次第で異なる説明が出てくるのです。limeライブラリは既定値として特徴量数に応じたカーネル幅を自動設定しますが、これが常に適切という保証はありません。また、摂動サンプルは特徴量ごとに独立に生成されるため、「年齢18歳で勤続30年」のような現実にはあり得ない組み合わせが近傍に混ざり、その不自然な点での予測が説明を歪めることもあります。特徴量間の相関が強いデータでは、この影響に特に注意が必要です。

3つ目として、LIMEの説明には理論的な保証がないことも挙げておきます。次章で扱うSHAPは、協力ゲーム理論のシャープレイ値に基づいており、「特徴量の寄与をすべて足すと予測値と平均予測の差に一致する」といった性質が数学的に保証されています。一方LIMEの重みは局所的な回帰係数であり、そのような一貫性は持ちません。両者の違いを表に整理します。

観点LIMESHAP
理論的基盤局所的な線形近似(理論保証はなし)シャープレイ値(公理による一意性・一貫性の保証)
説明の安定性乱数・カーネル幅に依存し、ばらつきがある厳密計算なら決定的(近似計算ではばらつきあり)
計算コスト比較的軽い(1件あたり数千回の予測呼び出し)手法によるが一般に重い(TreeSHAPは例外的に高速)
寄与の加法性保証されない寄与の合計が予測値と基準値の差に一致
説明の形式条件文と重み(直感的に読みやすい)特徴量ごとの寄与値(可視化が豊富)
適用範囲テーブル・テキスト・画像を同じ枠組みで扱えるテーブルデータが主戦場(画像・テキスト向けもあり)

こう並べると「ならば常にSHAPでよいのでは」と思われるかもしれません。実際、テーブルデータで勾配ブースティング系のモデルを使っているなら、高速なTreeSHAPが使えるため、私も第一候補はSHAPをおすすめします。それでもLIMEに出番がある場面を挙げるなら、次のようなケースです。

  • 予測APIしか触れないなど、モデルの内部に一切アクセスできず、入出力だけで説明を作る必要がある場合
  • テキストや画像も含めて、複数のデータ形式を同じ考え方・同じ語彙で説明したい場合
  • 「区間の条件文と重み」というLIMEの出力形式が、非技術者への報告にちょうどよい場合
  • 局所近似という考え方そのものを、チームでXAIを学ぶ入り口として体験したい場合

実務でLIMEを採用する際の私のおすすめは、「乱数シードを固定し、num_samplesを既定より増やし、重要な予測については複数回実行して説明の安定性を必ず確認する」という運用をセットにすることです。安定性の検証なしにLIMEの説明を意思決定の根拠として提示するのは避けるべきだと考えています。

最後に、本章の位置づけをもう一度確認します。LIMEが切り開いた「個々の予測を、モデルを問わず局所的に説明する」というアプローチは、その後のXAI研究の共通言語になりました。そして「局所的な寄与を、理論的な裏付けのある形で計算したい」という要求に応えたのが、次章で扱うSHAPです。実はSHAPの中核手法の1つであるKernel SHAPは、LIMEの枠組みでカーネルと損失関数を特別な形に選ぶとシャープレイ値が得られる、という関係にあり、両者は別々の手法というより地続きの関係にあります。LIMEの仕組みを踏まえると、SHAPが何を改善したのかが理解しやすくなります。次章で詳しく見ていきます。

この章を深めたい方への参考書籍

Pradeepta Mishra『実践XAI[説明可能なAI]』(インプレス)。LIMEやSHAPをはじめとする説明手法を、Pythonコードを動かしながら横断的に試せる実践書です。本章で扱った局所的な説明を、テーブル・テキスト・画像の各データ形式でどう適用するかまで手を広げたい方に向いています。


第4章 SHAP、寄与の分解で予測を説明する

第2章・第3章では、モデルの予測をどう解釈するかについて、それぞれ異なるアプローチによる手法を見てきました。局所的な近似に頼る手法や、特徴量の並び替えといった操作をベースにする手法は、実装のしやすさや直感的な分かりやすさが魅力である一方、算出される寄与度が満たすべき性質については、あまり厳密な保証がないケースも少なくありません。

本章で扱うSHAP(SHapley Additive exPlanations)は、その点で少し毛色が異なります。協力ゲーム理論のシャープレイ値という数学的な裏付けを持ち、各特徴量の寄与度が満たすべき性質を理論的に一意に定められるという強みがあります。木構造モデルから深層学習モデルまで幅広く適用できる汎用性の高さもあり、実務でモデルの予測根拠を説明する場面では、まず候補に挙がる手法の一つだと思います。

SHAPの理論的な一貫性の高さは、実際に使ってみると実感しやすい特長だと思います。本章では、SHAPの理論的な背景から、回帰・分類・深層学習それぞれのモデルへの適用方法、そして実装面で押さえておきたい最新動向まで、順を追って解説していきます。

機械学習モデルを解釈する指標SHAP

SHAPが提案されている論文は以下になります。

SHAPは、特定の指標そのものというよりも計算フレームワークに近く、木構造系のモデルから深層学習モデルまで、幅広いモデルに適用可能な方法です。

考え方はゲーム理論から導出されています。特徴量を「プレイヤー」とみなし、各プレイヤーが連携してゲーム(=予測)を進めるとき、そのプレイヤーがどの程度ゲームに貢献したかを、協力ゲーム理論の「シャープレイ値(Shapley value)」に基づいて算出します。

SHAPは、モデルの予測値\( f(x) \)を、各特徴量の寄与度\( \phi_i \)の足し合わせ(加法的)として説明します。

\( g(z’) = \phi_0 + \displaystyle\sum_{i=1}^{M} \phi_i z’_i \)

ここで\( \phi_0 \)は基準値(全サンプルの平均的な予測値)、\( \phi_i \)が特徴量\( i \)の寄与度、\( z’_i \)はその特徴量を使ったかどうかを表すバイナリ変数、\( M \)は特徴量の数です。各特徴量の寄与度\( \phi_i \)は、その特徴量を「加えたとき」と「加えないとき」の予測の差を、考えうるすべての特徴量の組み合わせについて重み付き平均することで求められます。

\( \phi_i = \displaystyle\sum_{S \subseteq F \setminus \{i\}} \frac{|S|!\,(|F|-|S|-1)!}{|F|!}\bigl[f_{S \cup \{i\}}(x) – f_S(x)\bigr] \)

この寄与度\( \phi_i \)は、以下の3つの性質(局所的正確性、欠損性、一貫性)を満たすものとして、ゲーム理論により一意に定まることが示されています。

  • 局所的正確性(Local accuracy):各特徴量の寄与度の合計が、実際のモデルの予測値と一致する
  • 欠損性(Missingness):使われていない特徴量の寄与度はゼロになる
  • 一貫性(Consistency):ある特徴量の影響が大きくなるようにモデルを変えたとき、その特徴量の寄与度が下がらない

このSHAPは厳密に求めようとするとかなりの計算量が必要なのですが、以下の論文では、木構造の仕組みをうまく使って効率的にSHAPを求めるアルゴリズム(TreeSHAP)を提案しています。

少し話が逸れますが、決定木やランダムフォレストなどの木構造アルゴリズムでは、特徴量の境界で切り分けた時の不純度(ジニ係数など)の減少を元にして、どの特徴量がより切り分けに効いているかをfeature_importances_で調べることができます。しかしこの指標では、評価の一貫性が保たれない場合がある(Inconsistency)ことも上記論文で述べられています。例えば、あるモデルで特徴量の影響を意図的に強めたにもかかわらず、その特徴量の重要度が下がってしまう、といった直感に反する現象が起こり得ます。

一方でSHAPを使うことで、このfeature_importances_における一貫性の問題は解消できると論文では言及されています。

加えて、特徴量どうしの相互作用効果を考慮したSHAP値(SHAP Interaction Values)も提案されています。これはSHAPと同じ考え方で2つの特徴量を使ったときの寄与度を求め、そこから各特徴量単独の主効果を切り分けることで、相互作用の大きさを評価する、というものです。

早速使ってみます。SHAPは様々な機械学習タスクに適用可能なので、回帰系タスクと分類系タスクについてそれぞれ見てみます。

回帰系モデルへの適用

SHAPは以下のライブラリから利用できます。Pythonの場合はpip install shapでインストール可能です。

まずは回帰系モデルに対して使ってみます。California住宅価格データセット(fetch_california_housing)の予測タスクをLightGBMで解いてみて、どの特徴量が効いているのかをSHAPで確認してみます。(fetch_california_housingは初回実行時にデータセットをダウンロードするため、インターネット接続が必要です。)

import numpy as np
import pandas as pd
from sklearn import datasets, model_selection, metrics
import lightgbm as lgb
import shap

# 回帰系(California住宅価格データセット)
housing = datasets.fetch_california_housing(as_frame=True)
X = housing.data
y = housing.target

train_X, valid_X, train_y, valid_y = model_selection.train_test_split(
    X, y, test_size=0.2, random_state=42)
print(train_X.shape, valid_X.shape, train_y.shape, valid_y.shape)

model = lgb.LGBMRegressor()
model.fit(train_X, train_y)

print('MSE train: %.3f, valid: %.3f' % (
    metrics.mean_squared_error(train_y, model.predict(train_X)),
    metrics.mean_squared_error(valid_y, model.predict(valid_X))
))
print('R^2 train: %.3f, valid: %.3f' % (
    metrics.r2_score(train_y, model.predict(train_X)),
    metrics.r2_score(valid_y, model.predict(valid_X))
))

続いて、学習したモデルに対してSHAPを計算します。SHAP 0.36以降は、Explainerを関数のように呼び出してExplanationオブジェクトを得るAPIが推奨されています。なお、ライブラリのバージョンによってAPIや返り値の形状(多クラス時の次元など)が変わることがあるため、利用時はインストールしたバージョンのドキュメントを確認することをおすすめします。

# 新API:Explainerを関数のように呼び出す
explainer = shap.Explainer(model)
shap_values = explainer(train_X)

# 旧API(参考):
# explainer = shap.TreeExplainer(model)
# shap_values = explainer.shap_values(train_X)

例えばある一つのサンプルについて、モデルがどの特徴量を見て予測したのかは、waterfallプロットで確認できます。

shap.plots.waterfall(shap_values[0])
1サンプルのSHAP waterfallプロット。基準値から各特徴量の寄与を積み上げて予測値に至る様子

基準値(全体の平均的な予測)から出発して、各特徴量の値が予測を上(赤)/下(青)にどれだけ動かしたかが積み上げで表示され、最終的なそのサンプルの予測値に至る様子が分かります。

多数のサンプルをまとめて俯瞰したい場合は、forceプロットを使います(サンプル数が多いと描画が重くなるため、ここでは先頭500件に絞っています)。

# JupyterではJavaScriptによるインタラクティブ表示のため初期化が必要
shap.initjs()
shap.plots.force(shap_values[:500])
先頭500サンプルのSHAP forceプロット

横軸にサンプルが並び、各サンプルについてどの特徴量がプラス・マイナスに働いたかを一覧できます。

一方で「特徴量の軸」から全体傾向を見たい場合は、beeswarmプロット(旧APIのsummary_plotに相当)が便利です。

shap.plots.beeswarm(shap_values)
SHAP beeswarmプロット。各特徴量のSHAP値の分布と特徴量値の大小

1つのドットが各サンプル、横軸がSHAP値、色が特徴量の値の大小を表します。California住宅価格データでは、一般に世帯所得(MedInc)が高いほど予測価格を押し上げる方向に働く、といった傾向が読み取れます。

プラス・マイナスを問わず「結局どの特徴量が予測に効いているのか」を見たい場合は、SHAP値の絶対値の平均を棒グラフにしたbarプロットが分かりやすいです。

shap.plots.bar(shap_values)
SHAP barプロット。特徴量の平均的な重要度ランキング

さらに、ある特徴量だけに注目し、その値とSHAP値の関係を見たい場合はscatterプロット(旧APIのdependence_plotに相当)を使います。

shap.plots.scatter(shap_values[:, "MedInc"], color=shap_values)
MedIncの値とSHAP値の関係を示すscatterプロット

横軸が対象特徴量の値、縦軸がそのSHAP値です。ドットの傾向から、その特徴量が大きくなるにつれて予測をどちらに動かすかが読み取れます。点の色には自動的に相互作用の強い別の特徴量が割り当てられ、相互作用の様子もうかがえます。

相互作用効果そのものを定量化したい場合は、Interaction SHAPを計算します(TreeExplainerで利用できます)。

explainer = shap.TreeExplainer(model)
shap_interaction_values = explainer.shap_interaction_values(train_X)

これにより、特徴量の主効果と、特徴量ペアごとの相互作用効果を切り分けて確認することができます(可視化はbeeswarmなどのプロットで行えますが、本章では計算方法の紹介に留めます)。回帰系モデルでは以上のように、特徴量の大小が予測値に対してどう影響するかを多角的に確認できることが分かりました。

分類系モデルへの適用

分類系のモデルについても、同様にどの特徴量が効いているのかを可視化できます。sklearnのあやめ(iris)の種類分類タスクをLightGBMで解いてみて、SHAPで確認します。

# 分類系(irisデータセット)
iris = datasets.load_iris(as_frame=True)
X = iris.data
y = iris.target

train_X, valid_X, train_y, valid_y = model_selection.train_test_split(
    X, y, test_size=0.2, random_state=42)
print(train_X.shape, valid_X.shape, train_y.shape, valid_y.shape)

model = lgb.LGBMClassifier()
model.fit(train_X, train_y)

print('Accuracy train: %.3f, valid: %.3f' % (
    metrics.accuracy_score(train_y, model.predict(train_X)),
    metrics.accuracy_score(valid_y, model.predict(valid_X))
))

explainer = shap.Explainer(model)
shap_values = explainer(train_X)

分類モデルでは、各クラスに対するSHAPを各特徴量について出力します。あやめのデータは3クラス分類ですので、「どの特徴量がどうだったから、そのクラスであると(あるいはないと)予測したのか」をクラスごとに見ることになります。なお、分類モデルのSHAP値は確率そのものではなく、各クラスのモデル出力スコア(ロジット)への寄与として表される点に注意してください。

新APIのExplanationオブジェクトは多クラスの場合、末尾の次元でクラスを指定できます。例えばクラス0についてのbeeswarmプロットは以下のように描けます。

# クラス0についての特徴量寄与
shap.plots.beeswarm(shap_values[:, :, 0])

# 1サンプル・クラス0のwaterfall
shap.plots.waterfall(shap_values[0, :, 0])
irisのクラス0に対するSHAP beeswarmプロット
irisの1サンプル・クラス0に対するSHAP waterfallプロット

これにより、各特徴量が「そのクラスである/ない」の判定にどう寄与しているかの傾向が分かります。あやめの分類では、花弁(petal)の長さ・幅が種類の判別に強く効くことがよく知られており、SHAPでもそうした傾向が確認できます。

深層学習モデルへの適用

深層学習モデルについても、SHAPで特徴量(や画素)の寄与度を可視化できます。SHAPには、深層学習向けにDeepExplainer(DeepLIFTの考え方をベースにしたもの)やGradientExplainer(勾配ベース)が用意されており、画像分類モデルであれば、画像のどの領域が予測に寄与したかをヒートマップとして表示できます。

本ガイドの他の章では、画像分類モデルの判断根拠を可視化するGrad-CAMという手法を紹介しています。SHAPとGrad-CAMは異なる考え方から導出されていますが、どちらも「モデルが入力のどこを見て判断したか」を明らかにするという目的は共通しています。SHAPはゲーム理論に基づき特徴量の寄与を厳密に配分する点に強みがあり、テーブルデータから画像・テキストまで統一的に扱える一方、画像に特化したGrad-CAMは計算が軽量という利点があります。目的やデータに応じて使い分けると良いでしょう。なお、SHAPはテキスト分類など自然言語処理モデルの説明にも適用が広がっており、どのトークン(単語)が予測に効いたのかを可視化する使い方は第9章で扱います。

参考文献

第3部 画像編:モデルは画像のどこを見たのか

第3部は画像認識モデルの判断根拠です。特徴マップから注目領域をヒートマップとして可視化するGrad-CAM系手法と、自動運転モデルへの応用実験を扱います。

第5章 Grad-CAM系手法、画像認識の判断根拠を可視化する

ここまで本ガイドの第2部では、テーブルデータを対象に、SHAPやLIMEといった手法で「どの特徴量が予測にどれだけ寄与したか」を定量的に示す方法を見てきました。ここから第3部・画像編に入ります。画像データを扱う深層学習モデルでは、特徴量という概念自体が人間にとって直感的ではありません。入力はピクセルの集合であり、モデルの内部で何が起きているかを言葉にするのは簡単ではないのです。そこで画像編の解釈は「どの特徴量が寄与したか」ではなく「画像のどこを見て判断したか」を可視化する、という発想に切り替わります。

本章では、その代表的な手法であるGrad-CAM、そしてその改良版であるGrad-CAM++、勾配を使わないアプローチのScore-CAMを取り上げます。深層学習は特にコンピュータビジョン(CV)分野で優れたパフォーマンスを発揮する一方、直感的でなく理解可能なコンポーネントへの分解も難しいため、解釈可能性が低くなりがちです。そこで、モデルが画像のどこに注目して予測したのかを可視化する技術が研究されてきました。本章では、TensorFlow/Kerasを使って3手法を実装し、実際の画像データセットで比較してみます。

なお、本章のコード例はTensorFlow 1系の実装をTensorFlow 2系で動作するよう書き直したものです(ロジックは同一です)。実験結果や可視化の図はTensorFlow 1系での実行結果である点にご留意ください。

深層学習の判断根拠解釈について

前述の通り、深層学習はCV分野において優れたパフォーマンスを発揮しますが、モデルの解釈を得づらいといった点があります。 したがって、モデルの判断根拠を可視化し解釈することは重要な領域の1つです。

以下は、Grad-CAMによるモデル判断根拠可視化の例です。

Grad-CAM論文より、Doctor/Nurse分類モデルの判断根拠可視化の例

判断根拠の可視化のメリットは主に以下の2つです。

  • モデルの透明性を高めることができる
  • 学習データのバイアスに気づくことができる

1つ目はこれまで記している通り、深層学習はモデルの解釈を得づらいという特徴がありますが、判断根拠の可視化により、解釈可能性の低さを改善し、モデルの透明性を向上させることができます。 これにより、モデルの予測ロジックを言語化でき、妥当性を評価できるため、実社会の責任の伴う場面にも適用しやすくなる可能性があります。

2つ目は、学習データのバイアスに気づくことができることです。 上記でご紹介した可視化例はGrad-CAMの論文から抜粋したもので、DoctorとNurseを分類するモデルの可視化例です。 左列は答えで、上の女性がNurse、下の女性がDoctorのラベルが付与されています。 中央列は、どうやら学習データに「Doctorには男性が多い/Nurseには女性が多い」というバイアスがかかっていたようで、モデルは人の顔や髪を見てどちらもNurseと予測してしまっているという様子を可視化で得ています。 右列では、性別のバイアスを学習データから取り除いて学習させた結果、人が持っている医療器具を注視するようになったということを表しています。 このように、判断根拠を可視化してみると、どうやら何か意図しない情報を使って予測しているかも?といったことに気づくことができます。

使用するデータセットと深層学習モデル

これから実装例を見ていくために、適当な深層学習モデルを用意してみます。

画像分類用のデータセットとして、以下のデータセットを利用します。

上記データセットは、以下の6クラスにラベル付けされている画像データセットです。

  • 建物(buildings)
  • 森(forest)
  • 雪山(glacier)
  • 山(mountain)
  • 海(sea)
  • ストリート(street)

各クラスについて、枚数、比率、サンプル画像を何枚か確認してみると以下のような感じです。 いずれのクラスも同じくらいの比率で含まれており、クラスごとに約2000個、合計12000個ほどの画像データが格納されているようです。

クラスごとの枚数・比率とサンプル画像の確認結果

この時点で、どうやらglacierとmountainを正しく分類するのはやや難しそうな印象を受けます。

深層学習モデルは、学習済みのResNet50のファインチューニングでモデルを作ってみます。 以下は学習部分の抜粋コードで、train/testデータセットの作成や、画像サイズ・クラス数などの定数(WHN_CLASSESBATCH_SIZEN_EPOCHS)、データ拡張の定義(datagen_traindatagen_test)は省略しています(画像は正方形にリサイズして扱う想定です)。このようにモデルを作成し、学習させてみたところ、90%ほどの精度となりました。 混同行列をプロットして確認してみると、やはりglacierとmountainは互いにやや間違えやすい傾向にありそうです。

import cv2
import numpy as np
import tensorflow as tf
from tensorflow.keras.applications import ResNet50
from tensorflow.keras.layers import Input, Dense, GlobalAveragePooling2D, BatchNormalization
from tensorflow.keras.models import Model

def build_model(w, h, n_classes):
    """Build model function.

    Args:
        w (int): Width size of image.
        h (int): Height size of image.
        n_classes (int): The number of class.

    Returns:
        Model: Model.
    """
    # Resnet
    input_tensor = Input(shape=(w, h, 3)) # To change input shape
    resnet50 = ResNet50(
        include_top=False,                # To change output shape
        weights="imagenet",               # Use pre-trained model
        input_tensor=input_tensor,        # Change input shape for this task
    )

    # fc layer
    x = GlobalAveragePooling2D()(resnet50.output)          # Add GAP for cam
    output = Dense(n_classes, activation="softmax")(x)     # Change output shape for this task

    # model
    model = Model(inputs=resnet50.input, outputs=output)

    # frozen weights
    for layer in model.layers[:-10]:
        layer.trainable = False or isinstance(layer, BatchNormalization) # If Batch Normalization layer, it should be trainable

    # compile
    model.compile(
        optimizer="adam",
        loss="categorical_crossentropy",
        metrics=["accuracy"],
    )

    return model

# Build the model
model = build_model(w=W, h=H, n_classes=N_CLASSES)

# Finetuning the model
history = model.fit(
    datagen_train.flow(
        x_train,
        y_train,
        batch_size=BATCH_SIZE,
    ),
    epochs=N_EPOCHS,
    validation_data=datagen_test.flow(
        x_test,
        y_test,
        batch_size=BATCH_SIZE,
    ),
)
Epoch 1/5
351/351 [==============================] - 70s 201ms/step - loss: 0.5062 - accuracy: 0.8201 - val_loss: 0.2515 - val_accuracy: 0.8917
Epoch 2/5
351/351 [==============================] - 48s 137ms/step - loss: 0.3234 - accuracy: 0.8817 - val_loss: 0.0313 - val_accuracy: 0.9024
Epoch 3/5
351/351 [==============================] - 49s 139ms/step - loss: 0.2801 - accuracy: 0.9011 - val_loss: 0.2366 - val_accuracy: 0.9020
Epoch 4/5
351/351 [==============================] - 48s 138ms/step - loss: 0.2494 - accuracy: 0.9080 - val_loss: 0.3192 - val_accuracy: 0.9102
Epoch 5/5
351/351 [==============================] - 49s 139ms/step - loss: 0.2238 - accuracy: 0.9169 - val_loss: 0.1859 - val_accuracy: 0.9081
混同行列のプロット

Grad-CAMの実装について

それでは、それぞれの判断根拠の可視化手法を実装し、試してみます。

まずはGrad-CAMです。 論文は以下になります。

Grad-CAMは2016年に発表されましたが、それより少し以前に発表されたCAM(Class Activation Mapping)の拡張として発表されました。 ロジックの数式は以下です。 (他のCAM手法との比較がわかりやすくなるように、こちらで表現を少し変えています)

\( L^c_{Grad\text{-}CAM} = ReLU\Bigl(\displaystyle\sum_k \alpha^c_k A^k\Bigr), \hspace{1em} \alpha^c_k = \displaystyle\frac{1}{Z}\sum_{i}\sum_{j}\frac{\partial y^c}{\partial A^k_{ij}} \)

ここで、\( A^k \)は最後の畳み込み層が出力する\( k \)番目の特徴マップ、\( y^c \)はクラス\( c \)のスコア、\( Z \)は特徴マップの画素数です。 すなわち、クラススコアの特徴マップに対する勾配を空間方向に平均したものを重み\( \alpha^c_k \)として、特徴マップの重み付き和をとった形になっています。

もととなったCAMは、一般的な畳み込み層にGlobal Average Poolingをかけて学習させた時の特徴マップの出力は、その先の全結合層の重み\( w^c_k \)がクラス分類の重要度を特徴マップ上で表すと考えられる、という考えから提案されています。

\( L^c_{CAM} = \displaystyle\sum_k w^c_k A^k \)

つまり、このままだとモデルとしてGlobal Average Poolingが必要になるのですが、それを勾配で代用できることを示し、どんなモデルアーキテクチャにもCAMのような可視化が可能だとしたのがGrad-CAMになります。 実は先ほどの深層学習モデルを準備している段階でGlobal Average Pooling層を追加しましたが、Grad-CAM自体はGAP層を前提としないため、これはなくても適用できます。

ランプ関数(ReLU)はクラスに対するマイナスの勾配を無視するためです。 意図は微妙にクリアではないですが、個人的には、勾配がマイナスに寄与している=そのクラスではない、を表すとし、そのクラスと判断した根拠の可視化からは除外して考えているだけなのかなと理解しています。

Grad-CAMの実装は以下のようになります。 (TensorFlow 1系ではK.gradientsを使っていましたが、TensorFlow 2系ではtf.GradientTapeで勾配を取得します)

def grad_cam(model, x, layer_name):
    """Grad-CAM function.

    Args:
        model (Model): Model.
        x (np.ndarray): Input.
        layer_name (str): Get layer name.

    Returns:
        tuple[int, np.ndarray]: Predicted class, heatmap of CAM.
    """
    cls = int(np.argmax(model.predict(x)))

    grad_model = Model(
        inputs=model.input,
        outputs=[model.get_layer(layer_name).output, model.output],
    )

    # Get outputs and grads
    with tf.GradientTape() as tape:
        conv_output, predictions = grad_model(x)
        y_c = predictions[:, cls]
    grads = tape.gradient(y_c, conv_output)

    output, grads_val = conv_output[0].numpy(), grads[0].numpy()

    weights = np.mean(grads_val, axis=(0, 1)) # Passing through GlobalAveragePooling

    cam = np.dot(output, weights) # multiply
    cam = np.maximum(cam, 0)      # Passing through ReLU
    if np.max(cam) > 0:
        cam /= np.max(cam)        # scale 0 to 1.0

    return cls, cam

なお、xはバッチサイズ1の入力を、layer_nameには最後の畳み込み層の名前(今回のResNet50ではconv5_block3_out)を指定する想定です。 また、本章ではsoftmax出力に対する勾配を取っていますが、Keras公式のGrad-CAM実装例のように、最終層のsoftmaxを外してlogit(softmax適用前のスコア)に対する勾配を取る実装も一般的です。

Grad-CAM++の実装について

Grad-CAM++は、2017年に、Grad-CAMの改良版として発表されました。

こちらは、特徴マップにかかる重みのようなものがあったとしたら、それはどう表現されるかを、これまでの論文で出てきた数式からガリガリと紐解いており、以下のように表現しています。

\( w^c_k = \displaystyle\sum_{i}\sum_{j}\alpha^{kc}_{ij}\cdot ReLU\Bigl(\frac{\partial y^c}{\partial A^k_{ij}}\Bigr), \hspace{1em} \alpha^{kc}_{ij} = \displaystyle\frac{\displaystyle\frac{\partial^2 y^c}{(\partial A^k_{ij})^2}}{2\displaystyle\frac{\partial^2 y^c}{(\partial A^k_{ij})^2}+\displaystyle\sum_a\sum_b A^k_{ab}\displaystyle\frac{\partial^3 y^c}{(\partial A^k_{ij})^3}} \)

勾配を空間方向に一様に平均するのではなく、画素ごとの重要度\( \alpha^{kc}_{ij} \)で重み付けする形になっています。 これにより、特徴マップの中でクラス予測に影響を与えるがその領域の大きさが大きくなかったものは、これまで捉えられていませんでしたが、捉えられるようになりました。 こちらを実装すると、以下のようになります。

def grad_cam_plus_plus(model, x, layer_name):
    """Grad-CAM++ function.

    Args:
        model (Model): Model.
        x (np.ndarray): Input.
        layer_name (str): Get layer name.

    Returns:
        tuple[int, np.ndarray]: Predicted class, heatmap of CAM.
    """
    cls = int(np.argmax(model.predict(x)))

    grad_model = Model(
        inputs=model.input,
        outputs=[model.get_layer(layer_name).output, model.output],
    )

    # Get outputs and grads
    with tf.GradientTape() as tape:
        conv_output, predictions = grad_model(x)
        y_c = predictions[:, cls]
    grads = tape.gradient(y_c, conv_output)

    conv_output = conv_output[0].numpy()
    grads_val = grads[0].numpy()
    score = float(y_c.numpy()[0])

    # first / second / third derivative
    conv_first_grad = np.exp(score) * grads_val
    conv_second_grad = np.exp(score) * grads_val ** 2
    conv_third_grad = np.exp(score) * grads_val ** 3

    # Calculate weight alpha
    global_sum = np.sum(conv_output.reshape((-1, conv_first_grad.shape[2])), axis=0)
    alpha_num = conv_second_grad
    alpha_denom = conv_second_grad * 2.0 + conv_third_grad * global_sum.reshape((1, 1, conv_first_grad.shape[2]))
    alpha_denom = np.where(alpha_denom != 0.0, alpha_denom, np.ones(alpha_denom.shape))
    alphas = alpha_num / alpha_denom

    weights = np.maximum(conv_first_grad, 0.0)
    alpha_normalization_constant = np.sum(np.sum(alphas, axis=0), axis=0)
    alphas /= alpha_normalization_constant.reshape((1, 1, conv_first_grad.shape[2]))
    deep_linearization_weights = np.sum((weights * alphas).reshape((-1, conv_first_grad.shape[2])), axis=0)

    cam = np.sum(deep_linearization_weights * conv_output, axis=2) # multiply
    cam = np.maximum(cam, 0)                                       # Passing through ReLU
    if np.max(cam) > 0:
        cam /= np.max(cam)                                         # scale 0 to 1.0

    return cls, cam

Score-CAMの実装について

Score-CAMは、2019年10月に発表された手法です。

勾配での表現は、時々入力層のわずかな小さな変化に対しても、過剰に大きな値を返してしまう問題があります。 これはGrad-CAMやGrad-CAM++においても指摘されていたことでした。

そこで、この論文では、特徴量ヒートマップを勾配を使わないで作成する方法を提案しています。

\( L^c_{Score\text{-}CAM} = ReLU\Bigl(\displaystyle\sum_k \alpha^c_k A^k\Bigr), \hspace{1em} \alpha^c_k = f\bigl(X \circ H^k\bigr)_c \)

\( X \)はインプット画像、\( H^k \)は特徴マップ\( A^k \)を入力サイズに拡大し正規化したマスク、\( f(\cdot)_c \)はマスクをかけた画像をモデルに入力した時のクラス\( c \)のスコアです。 すなわち、「特徴マップ×画像」のスコアを重みとして表現するような形をしています。 これを実装すると、以下のようになります。

def score_cam(
        model,
        x,
        layer_name,
        max_N=-1,
    ):
    """Score-CAM function.

    Args:
        model (Model): Model.
        x (np.ndarray): Input.
        layer_name (str): Get layer name.
        max_N (int): max N.

    Returns:
        tuple[int, np.ndarray]: Predicted class, heatmap of CAM.
    """
    cls = int(np.argmax(model.predict(x)))
    act_map_array = Model(inputs=model.input, outputs=model.get_layer(layer_name).output).predict(x)

    # extract effective maps
    if max_N != -1:
        act_map_std_list = [np.std(act_map_array[0, :, :, k]) for k in range(act_map_array.shape[3])]
        unsorted_max_indices = np.argpartition(-np.array(act_map_std_list), max_N)[:max_N]
        max_N_indices = unsorted_max_indices[np.argsort(-np.array(act_map_std_list)[unsorted_max_indices])]
        act_map_array = act_map_array[:, :, :, max_N_indices]

    input_shape = model.input_shape[1:]  # get input shape (height, width, channels)

    # 1. upsampled to original input size (cv2.resize dsize is (width, height))
    act_map_resized_list = [cv2.resize(act_map_array[0,:,:,k], (input_shape[1], input_shape[0]), interpolation=cv2.INTER_LINEAR) for k in range(act_map_array.shape[3])]

    # 2. normalize the raw activation value in each activation map into [0, 1]
    act_map_normalized_list = []
    for act_map_resized in act_map_resized_list:
        if np.max(act_map_resized) - np.min(act_map_resized) != 0:
            act_map_normalized = (act_map_resized - np.min(act_map_resized)) / (np.max(act_map_resized) - np.min(act_map_resized))
        else:
            act_map_normalized = act_map_resized
        act_map_normalized_list.append(act_map_normalized)

    # 3. project highlighted area in the activation map to original input space by multiplying the normalized activation map
    masked_input_list = []
    for act_map_normalized in act_map_normalized_list:
        masked_input = np.copy(x)
        for k in range(3):
            masked_input[0, :, :, k] *= act_map_normalized
        masked_input_list.append(masked_input)
    masked_input_array = np.concatenate(masked_input_list, axis=0)

    # 4. feed masked inputs into CNN model (the output is already softmax probability)
    pred_from_masked_input_array = model.predict(masked_input_array)

    # 5. define weight as the score of target class
    weights = pred_from_masked_input_array[:, cls]

    # 6. get final class discriminative localization map as linear weighted combination of all activation maps
    cam = np.dot(act_map_array[0, :, :, :], weights) # multiply
    cam = np.maximum(0, cam)                         # Passing through ReLU
    if np.max(cam) > 0:
        cam /= np.max(cam)                           # scale 0 to 1.0

    return cls, cam

各解釈手法の結果比較

各手法の実装ができましたので、各クラスの判断根拠の可視化を比較してみます。

建物(buildings)

buildingsクラスの可視化比較

モデルは建物の全体部分に注目しているような可視化結果が得られました。 Grad-CAM++とScore-CAMの方が、Grad-CAMよりも、より建物の全体を見ているような結果となりました。 Grad-CAM++とScore-CAMはそれほど大きな違いはないように見えます。

森(forest)

forestクラスの可視化比較

木の幹の部分を見つけて、森と予測しているように見えます。 特に、Grad-CAM++とScore-CAMの方が、Grad-CAMよりも顕著にその特徴を捉えているように見えます。

雪山(glacier)

glacierクラスの可視化比較

山の形を見ている?ような結果となりました。 山の色も見ているのかもしれません。 これに関しては、いずれの可視化手法も大きな違いはなさそうに見えます。

山(mountain)

mountainクラスの可視化比較

こちらも山の形を見ているような、雪山と同じような結果となりました。 山の画像にも、色的に見ると雪山の方が正しいような気もする画像が含まれており、判断に迷います。 そのくらい、差別化できるような画像要素を見つけられていないように思います。 またこちらも、いずれの可視化手法も大きな違いはなさそうです。

海(sea)

seaクラスの可視化比較

こちらは、海の表面や水平線を見ているような可視化結果となりました。 Grad-CAM++とScore-CAMの方が、Grad-CAMよりもより海全体を捉えているように見えます。

ストリート(street)

streetクラスの可視化比較

モデルは、路面およびその両側に建つ建物について見ているような結果となりました。 こちらも、Grad-CAM++とScore-CAMの方が、より全体を捉えているように見えます。

各手法の実行にかかる時間は、新しくなるにつれて長くなります(Score-CAM>Grad-CAM++>Grad-CAM)。 Grad-CAMよりもGrad-CAM++は微分計算などが加えられていて当然実行時間は長くなりますし、Score-CAMはマスク画像を複数枚推論する必要があるために実行時間がかかっているように思います。 実行時間を気にしないならば、個人的には、Grad-CAM++かScore-CAMがおすすめかと思いました。

このような手法を使って、上記の判断根拠可視化のメリットで示した通り、学習データのバイアスなどがないかを感覚的に調べたり、モデルの透明性や妥当性を示すのに使用すると良いでしょう。 可視化もある意味定性的な判断にはなってしまうのですが、やはり画像は人間もなんとなく認識している場合が多く、人間が見れば確かにこの画像はネコなんだけど、モデルはなんでそう思ったのか、をうまく言語化できなくてビジネス報告しづらい時に、これらのような表現が活用できます。

CAM系手法の広がりと実務での注意点

本章で扱った3手法の登場以降も、判断根拠の可視化(CAM系手法)は活発に発展しています。実務で使ううえで押さえておきたいポイントを整理します。

  • CAM系手法のさらなる発展: 本章で紹介した3手法の後にも、XGrad-CAM、Ablation-CAM、Eigen-CAM、LayerCAM、FullGradなど多くの派生手法が提案されています。特にHiResCAMは、Grad-CAMのヒートマップが「モデルが実際に使った根拠」を必ずしも忠実に反映しないケースがあることを指摘し、忠実性(faithfulness)を保証する代替手法として提案されました。可視化の「見栄えの良さ」と「忠実さ」は別物である、という問題意識が広がったのは大きな変化です。
  • ライブラリの充実: 現在では、これらの手法を自前で実装しなくても、ライブラリで手軽に利用できるようになりました。PyTorchではpytorch-grad-camが定番で、本章で扱った3手法を含む10種類以上のCAM系手法を、画像分類だけでなく物体検出・セグメンテーション・画像類似度などのタスクにも適用できます。TensorFlow/Kerasではtf-keras-visなどが利用できます。
  • Vision Transformer(ViT)時代の解釈手法: 画像認識の主役がCNNからViTへ広がったことに伴い、Attention Rollout(アテンションの流れを層をまたいで追跡する手法)などViT向けの解釈手法も登場しました。CAM系手法も、トークン列を2次元の特徴マップに並べ替えることでViTに適用でき、pytorch-grad-camもViTをサポートしています。CNNとViTのどちらでも、判断根拠の可視化という考え方は引き続き有効です。
  • 可視化の信頼性評価: 「もっともらしいヒートマップが、本当にモデルの判断根拠を表しているのか」を検証する研究も進みました。サニティチェック(モデルの重みをランダム化しても可視化結果が変わらないなら、その可視化はモデルを説明していない、という検証)や、重要領域を削除・挿入した時のスコア変化による定量評価(Deletion/Insertion、ROADなど)が標準的に行われるようになっています。可視化手法を業務で使う際も、こうした観点で吟味することをおすすめします。
  • 説明可能性への社会的要請: EUのAI規則(AI Act)をはじめ、AIシステムの透明性・説明可能性を求める制度的な動きが世界的に進んでいます。判断根拠の可視化は、それ単体で説明責任を果たせるものではありませんが、モデル開発時のデバッグやバイアス検出、ステークホルダーへの説明材料として、実務上の重要性はますます高まっています。

参考文献

この章を深めたい方への参考書籍

斎藤康毅『ゼロから作るDeep Learning』(オライリー・ジャパン)。外部ライブラリに頼らず、CNNを一から実装しながら仕組みを理解できる定番書です。Grad-CAMが利用している畳み込み層や勾配の意味を土台から押さえたい方におすすめします。


第6章 応用例、自動運転モデルの判断根拠を解析する

前章までは、CNNが画像分類の判断根拠をどこに置いているかを可視化するCAM系の手法(Grad-CAMなど)を中心に見てきました。本章では、その発想を少し違う文脈に応用してみます。分類問題ではなく、カメラ画像から直接ステアリング操作を出力する「End-to-End」型の自動運転モデルを題材に、モデルが画像のどこを見て運転判断をしているのかを解析します。

取り上げるのは、NVIDIAが提案した「PilotNet」と呼ばれるモデルです。車載カメラの画像を入力すると、ハンドルの切れ角を直接出力するというシンプルな構造で、認識・判断・操作を個別のモジュールに分けて作り込む従来の自動運転方式とは対照的な、End-to-Endのアプローチの草分け的な存在です。本章では、PilotNetを実際に実装して学習させ、その判断根拠を「VisualBackProp」という手法で可視化してみます。VisualBackPropは、前章までのCAM系手法と同じく、CNNの各層の特徴マップに着目して判断への寄与を可視化するという発想を土台にしており、対象が画像分類からステアリング操作の回帰予測に変わっても同じ考え方が応用できることを示す好例だと思います。

なお、本章の実験は2018年に行ったものです。その時点ではEnd-to-End方式の自動運転はまだ研究段階の話題でしたが、その後この方式は実用化の主役になりつつあります。実装や実験結果は実施時の記録をそのまま示し、章の終盤でその後の技術動向に触れます。

NVIDIAの実験にインスパイアされて

今回は、以下のNVIDIAの記事にインスパイアされた取り組みになります。

上記の記事では、NVIDIAが提案した深層学習モデルPilotNetを使った実験が紹介されています。 PilotNetは、自動運転車の運転判断をサポートするための深層学習モデルです。 車載カメラの画像を入力し、それに基づいてEnd-to-Endで運転操作(例えば、ステアリング操作)を出力するモデルです。

さらに記事では、そのようにして学習されたPilotNetが、運転操作を出力するために画像のどの部分を注視しているのかといった判断根拠を可視化する試みも行っています。

今回は、自動運転向けに収集・公開されたデータセットを使って同様の実験を試してみます。

データセットの準備

PilotNetを実装する上で、まずは自動運転のための画像とステアリング操作のデータセットを準備します。 今回の実装では、以下のUdacityが提供する自動運転車のデータセットを使用します。 データセットには、実際の道路環境での運転時の画像とセンサーデータが記録されており、学習に適していそうです。

実際にデータの中身を確認してみて、今回使うカラムに限定すると以下のようなデータセットになっています。 (なお、以降のコードは要点を抜き出した抜粋であり、import文やデータ準備の一部は紙面の都合上省略しています。)

import functools
import numpy as np
import pandas as pd
import matplotlib
import matplotlib.pyplot as plt
from PIL import Image

target_output_path = './udacity-driving-reader/legacy_data_sunny/output/'

df_camera = pd.read_csv(target_output_path + 'camera.csv')
df_steering = pd.read_csv(target_output_path + 'steering.csv')

df_camera = df_camera[df_camera['frame_id'] == 'center_camera']
df_camera['timestamp'] = pd.to_datetime(df_camera['timestamp'])
df_camera.set_index(['timestamp'], inplace=True)
df_camera.index.rename('index', inplace=True)

df_steering['timestamp'] = pd.to_datetime(df_steering['timestamp'])
df_steering.set_index(['timestamp'], inplace=True)
df_steering.index.rename('index', inplace=True)

df_merged = functools.reduce(lambda left, right: pd.merge(left, right, how='outer', left_index=True, right_index=True), [df_camera, df_steering])
df_merged.interpolate(method='time', inplace=True)
df_filtered = df_merged.loc[df_camera.index]
df_filtered.fillna(0.0, inplace=True)
df_filtered.index.rename('timestamp', inplace=True)
df_filtered = df_filtered.reset_index()
df_filtered = df_filtered[['timestamp', 'filename', 'angle', 'speed']]
df_filtered['filename'] = target_output_path + df_filtered['filename']

# 停止している画像・不自然にハンドルが切れすぎている画像を除き、正規化
df_filtered = df_filtered[10 < df_filtered['speed']]
df_filtered = df_filtered[(-0.5 <= df_filtered['angle']) & (df_filtered['angle'] <= 0.5)]
df_filtered['angle'] += 0.5
df_filtered = df_filtered.sample(frac=1)

df_filtered.head()
データセットの中身(timestamp/filename/angle/speed)の確認結果

データセットには画像パスが格納されていますので、何枚か画像を表示してみると以下のような感じです。

df = df_filtered.sample(frac=1)[:3]

fig, axs = plt.subplots(ncols=3, figsize=(15, 4))

for i, (index, row) in enumerate(df.iterrows()):
    img = Image.open(row['filename'])
    axs[i].imshow(img)
    axs[i].set_title('steering: {}'.format(row['angle']))

plt.show()
車載カメラ画像とステアリング角のサンプル3枚

PilotNetについて

冒頭で述べました通り、PilotNetは、NVIDIAが提案するディープラーニングモデルで、自動運転車の運転判断をサポートするために設計されました。 このモデルは、カメラからの入力画像を受け取り、それに基づいて運転指示(例: ハンドルの角度)を出力します。 論文は以下です。

PilotNetのアーキテクチャは、畳み込みニューラルネットワーク(CNN)をベースにしています。 このネットワークは、複数の畳み込み層、活性化関数、および全結合層から構成されています。 以下は論文から抜粋のアーキテクチャ図です。

PilotNetのアーキテクチャ図(論文より引用)

モデルの実装は以下の通りです。 この実験では深層学習ライブラリにChainerを使用しました。Chainerは2019年末に開発が終了しPyTorchへ開発リソースが移管されたため、現在から新規に取り組む場合はPyTorchやTensorFlowでの実装をおすすめしますが、ここでは実験記録としてChainer実装を残します(旧実装の参考掲載であり、現行環境での実行は想定していません)。

class PilotNet(chainer.Chain):

    def __init__(self):
        super(PilotNet, self).__init__()
        with self.init_scope():
            self.bn0 = L.BatchNormalization(3)
            self.conv1 = L.Convolution2D(3, 24, ksize=5, stride=2)
            self.conv2 = L.Convolution2D(24, 36, ksize=5, stride=2)
            self.conv3 = L.Convolution2D(36, 48, ksize=5, stride=2)
            self.conv4 = L.Convolution2D(48, 64, ksize=3)
            self.conv5 = L.Convolution2D(64, 64, ksize=3)
            self.fc6 = L.Linear(None, 100)
            self.fc7 = L.Linear(100, 50)
            self.fc8 = L.Linear(50, 10)
            self.fc9 = L.Linear(10, 1)

    def __call__(self, x, extract_feature=False):
        h0 = self.bn0(x)
        h1 = F.relu(self.conv1(h0))
        h2 = F.relu(self.conv2(h1))
        h3 = F.relu(self.conv3(h2))
        h4 = F.relu(self.conv4(h3))
        h5 = F.relu(self.conv5(h4))
        h6 = F.dropout(F.relu(self.fc6(h5)), ratio=0.1)
        h7 = F.dropout(F.relu(self.fc7(h6)), ratio=0.1)
        h8 = F.dropout(F.relu(self.fc8(h7)), ratio=0.1)
        y = self.fc9(h8)

        if extract_feature:
            return {'conv1': h1, 'conv2': h2, 'conv3': h3, 'conv4': h4, 'conv5': h5, 'fc6': h6, 'fc7': h7, 'fc8': h8, 'fc9': y}

        return y

    def get_mask(self, x):
        h0 = self.bn0(x)
        h1 = F.relu(self.conv1(h0))
        h2 = F.relu(self.conv2(h1))
        h3 = F.relu(self.conv3(h2))
        h4 = F.relu(self.conv4(h3))
        h5 = F.relu(self.conv5(h4))

        h5 = F.mean(h5, axis=1)
        h5 = F.reshape(h5, (h5.data.shape[0], 1, h5.data.shape[1], h5.data.shape[2]))
        h5 = chainer.cuda.to_cpu(h5.data)
        h4_rev = L.Deconvolution2D(1, 1, ksize=3,
                                 initialW=np.ones((1, 1, 3, 3), dtype=np.float32),
                                 initial_bias=np.zeros((1), dtype=np.float32))(h5)
        h4 = F.mean(h4, axis=1)
        h4 = F.reshape(h4, (h4.data.shape[0], 1, h4.data.shape[1], h4.data.shape[2]))
        h4 = chainer.cuda.to_cpu(h4.data)
        h3_rev = L.Deconvolution2D(1, 1, ksize=3,
                                  initialW=np.ones((1, 1, 3, 3), dtype=np.float32),
                                  initial_bias=np.zeros((1), dtype=np.float32))(h4_rev*h4)
        h3 = F.mean(h3, axis=1)
        h3 = F.reshape(h3, (h3.data.shape[0], 1, h3.data.shape[1], h3.data.shape[2]))
        h3 = chainer.cuda.to_cpu(h3.data)
        h2_rev = L.Deconvolution2D(1, 1, ksize=5, stride=2, outsize=(h2.data.shape[2], h2.data.shape[3]),
                                  initialW=np.ones((1, 1, 5, 5), dtype=np.float32),
                                  initial_bias=np.zeros((1), dtype=np.float32))(h3_rev*h3)
        h2 = F.mean(h2, axis=1)
        h2 = F.reshape(h2, (h2.data.shape[0], 1, h2.data.shape[1], h2.data.shape[2]))
        h2 = chainer.cuda.to_cpu(h2.data)
        h1_rev = L.Deconvolution2D(1, 1, ksize=5, stride=2, outsize=(h1.data.shape[2], h1.data.shape[3]),
                                  initialW=np.ones((1, 1, 5, 5), dtype=np.float32),
                                  initial_bias=np.zeros((1), dtype=np.float32))(h2_rev*h2)
        h1 = F.mean(h1, axis=1)
        h1 = F.reshape(h1, (h1.data.shape[0], 1, h1.data.shape[1], h1.data.shape[2]))
        h1 = chainer.cuda.to_cpu(h1.data)
        mask = L.Deconvolution2D(1, 1, ksize=5, stride=2, outsize=(h0.data.shape[2], h0.data.shape[3]),
                                initialW=np.ones((1, 1, 5, 5), dtype=np.float32),
                                initial_bias=np.zeros((1), dtype=np.float32))(h1_rev*h1)

        return mask

モデルのトレーニングには、先ほどの「データセットの準備」で取得したUdacityのデータセットを使用します。 したがって、入力は車載画像で出力値はステアリング(ハンドル)の角度です。 学習のコードは以下の通りです。

model = L.Classifier(PilotNet(), lossfun=F.mean_squared_error)
model.compute_accuracy = False
optimizer = chainer.optimizers.Adam(alpha=1e-4)
optimizer.setup(model)

if gpu >= 0:
    chainer.cuda.get_device(gpu).use()
    model.to_gpu(gpu)

epoch_num = 100
batch_size = 1000

train_iter = chainer.iterators.SerialIterator(train_dataset, batch_size)
test_iter = chainer.iterators.SerialIterator(valid_dataset, batch_size, repeat=False, shuffle=False)
updater = chainer.training.StandardUpdater(train_iter, optimizer, device=gpu)
trainer = chainer.training.Trainer(updater, (epoch_num, 'epoch'), out='result_pilotnet')
trainer.extend(extensions.Evaluator(test_iter, model, device=gpu))
trainer.extend(extensions.LogReport(trigger=(10, 'epoch')))
trainer.extend(extensions.LogReport())
trainer.extend(extensions.PrintReport(['epoch', 'main/loss', 'validation/main/loss', 'elapsed_time']))
trainer.extend(extensions.PlotReport(['main/loss', 'validation/main/loss'], 'epoch', file_name='loss.png'))
trainer.extend(extensions.snapshot(filename='snapshot_epoch_{.updater.epoch}.npz'), trigger=(epoch_num, 'epoch'))
trainer.run()
epoch       main/loss   validation/main/loss  elapsed_time
10          0.0309514   0.0388316             82.0616
20          0.0122537   0.00430815            149.306
30          0.00964154  0.00319876            216.603
40          0.0084587   0.00268209            284.642
50          0.00782145  0.0022793             351.935
60          0.00739421  0.00230404            419.19
70          0.00697377  0.00171783            487.325
80          0.00672234  0.00163945            554.637
90          0.00637599  0.00138362            622.032
100         0.00623202  0.00120607            690.019
学習曲線(loss.png)

問題なく学習させることができました。

Visual Backpropagationによるステアリング操作要因の可視化

NVIDIAの記事では、PilotNetの判断を明確に理解するための方法として、VisualBackPropと呼ばれる可視化手法を試しています。 名前に「Backprop」とありますが、これは通常の勾配の逆伝播そのものではなく、各畳み込み層の特徴マップをチャンネル方向に平均し、それを逆畳み込みによって入力側へ一段ずつ投影していくことで、モデルの判断に寄与した領域を浮かび上がらせる手法です。 これにより、入力画像のどの部分がモデルの判断に大きく寄与しているのかを特定することができます。

実際にやってみると以下のようになりました。

from sklearn.preprocessing import MinMaxScaler
from PIL import ImageEnhance

col_num = 5
for i, (x, y) in enumerate(zip(valid_x, valid_y)):

    if i >= 30:
        break
    if i % col_num == 0:
        fig, axs = plt.subplots(ncols=col_num, figsize=(20, 4))

    img = Image.open(x)

    x = processing_x(img)
    x = x[np.newaxis]

    mask = model.predictor.get_mask(x)
    mask = MinMaxScaler().fit_transform(mask.data.squeeze())

    w, h = img.size
    mask *= 255
    mask = Image.fromarray(mask).convert('L')
    mask = mask.resize((((w//8)*7 - (w//8)*1), ((h//8)*8 - (h//8)*5)))
    mask = Image.merge('RGB', (mask.point(lambda x: x * 0 / 255), mask.point(lambda x: x * 255 / 255), mask.point(lambda x: x * 0 / 255)))

    overlay = Image.new('RGB', (w, h))
    overlay.paste(mask, ((w//8)*1, (h//8)*5))

    blended = Image.blend(img, overlay, 0.4)
    enhancer = ImageEnhance.Brightness(blended)
    blended = enhancer.enhance(1.3)
    axs[i % col_num].imshow(blended)
    axs[i % col_num].axis('off')

plt.show()
VisualBackPropによる判断根拠の可視化結果(道路・他車・白線などがハイライト)

いくつかの画像については、他の車両、白線など、運転において重要な要素がハイライトされる傾向が確認できます。 これは、モデルがこれらの要素を重視して運転指示を出していることを示唆しています。

一方で、特定の方向や要素に焦点を当てず、画面の前方のピクセルを漠然とハイライトしているものも多く観察されました。 個人的には、これはモデルが特定の要素を強く参照して判断を下しているわけではなく、むしろ特に何も参照せずに直進を選択している(ステアリングを大きく切っていない)ことを示している可能性があるのかなと思いました。

その後のEnd-to-End自動運転の進展

この実験を行った2018年時点では、End-to-End方式(カメラ画像などのセンサー入力から運転操作までを単一のニューラルネットワークで直接出力する方式)の自動運転は、まだ研究色の強い話題でした。それから数年で、この方式は自動運転開発の主役の一つになりました。主なポイントを整理します。

  • End-to-End方式の実用化: テスラはFSD(Full Self-Driving)のバージョン12(2023〜2024年)で、認識・判断・操作をルールで個別に作り込む従来方式から、大量の走行映像から運転を直接学習するEnd-to-End方式へと大きく舵を切りました。本章のPilotNetは、まさにこのEnd-to-End方式の最も初期かつシンプルな原型にあたります。
  • 基盤モデル・生成AIとの融合: 英Wayveは、自然言語を使って運転の基盤モデルの判断を説明・対話する「LINGO」と呼ばれる取り組みを進めています。「なぜそう運転したのか」をモデル自身に言葉で語らせようという試みです。日産自動車はこのWayveのAIソフトウェアを次世代の運転支援技術「プロパイロット」(SAEレベル2の運転支援)に採用し、2027年度から市販車に搭載すると発表しており、End-to-End方式は研究から量産フェーズへ移りつつあります。
  • 説明可能性(XAI)の重要性の高まり: End-to-End方式は性能が高い反面、「なぜその操作をしたのか」がブラックボックスになりやすいという課題があります。人命に関わる自動運転では、この判断根拠の説明が安全性の担保や事故時の原因究明、社会的な受容のために不可欠です。本章で扱ったVisualBackProp(NVIDIAらが2018年のICRAで発表)のような可視化手法に加えて、近年では言語による説明や、運転シーンに対する質問応答(Visual Question Answering)など、より人間に分かりやすい形で判断根拠を提示する研究が活発になっています。
  • 可視化手法の発展: 本章のVisualBackPropは、各畳み込み層の特徴マップを逆向きに入力側へ投影して、寄与の大きい領域を浮かび上がらせる、シンプルかつ高速な手法です。特徴マップに着目して判断根拠を可視化するという発想は、前章で紹介したGrad-CAMなどの手法とも共通しています。現在ではTransformerベースのモデルにも対応した可視化・解釈手法が整備されており、CNN時代から続く「モデルが画像のどこを見ているか」という問いは、自動運転の文脈でも引き続き重要なテーマであり続けています。

ささやかな実験ではありますが、その後の業界の進展を踏まえて振り返ると、End-to-End学習とその判断根拠の可視化という2つのテーマが、現在の自動運転開発の中心に位置していることが分かります。

参考文献

この章を深めたい方への参考書籍

原田達也『画像認識』(講談社、機械学習プロフェッショナルシリーズ)。画像認識の基礎から深層学習による手法までを体系的に整理した教科書です。本章のように画像モデルを応用する際の土台となる知識を固められます。


第4部 不確実性編:この予測はどれくらい信用できるのか

第4部は、判断根拠と並ぶもう一つの「説明」である不確実性推定です。既存モデルに後付けできるMC Dropoutと、最初から確率モデルとして設計するベイズニューラルネットワークを扱います。

第7章 MC Dropout、予測の不確実性を測る

本ガイドもここまで、第2部でテーブルデータにおける特徴量の寄与を、第3部でCNNが画像のどこに着目して判断したかを、それぞれ可視化する手法を見てきました。いずれも「モデルが何を根拠に判断したか」という問いに答えるものでしたが、ここからは少し違う角度から説明可能性に迫ります。第4部・不確実性編では、判断根拠そのものではなく、「その予測にどれくらい自信を持ってよいか」を測る技術を扱います。

意思決定者にとって、モデルが根拠として何を見たかと同じくらい重要なのが、その予測がどの程度確からしいのかという情報だと思います。たとえば与信審査や異常検知のように誤判定のコストが大きい業務では、モデルの自信度が分かれば、確信度の高い予測はそのまま処理を進め、自信のない予測だけを人間が確認するという運用を設計できます。単に精度を上げることを目指すより、こうした自信度に応じた役割分担を作るほうが、現場の信頼獲得には近道になることが多いように思います。

本章で取り上げるのは、モンテカルロ・ドロップアウト(MC Dropout)と呼ばれる手法です。通常の深層学習は、予測値は出力しても「どれくらい自信があるか」までは教えてくれません。MC Dropoutは、学習時に汎化性能を保つための工夫であるDropoutを、推論時にもあえて有効にしたまま何度も予測を繰り返すという、非常にシンプルな発想でこの課題に答えます。土台になっているのは、この操作が近似的にベイズ推論になっていることを理論的に示したGal & Ghahramani(2016)の論文です。

本章では、MNIST画像分類を題材に、Dropoutを推論時にも有効にするだけで、モデルの「予測しにくさ」を定量化できることを、実際に手を動かしながら見ていきます(実装はPyTorchを使います)。

Dropoutによる近似ベイズ推論

論文は下記になります。

この論文は、Dropoutを適用して学習した深層学習が、ディープなガウス過程における近似ベイズ推論として解釈・定式化できることを理論的に示しています。

少し整理します。学習データ\( \textbf{X}, \textbf{Y} \)が与えられたとき、ニューラルネットワークの重み\( {\boldsymbol \omega} \)の事後分布\( p({\boldsymbol \omega}|\textbf{X}, \textbf{Y}) \)を直接求めるのは困難です。そこで、これを近似する分布\( q({\boldsymbol \omega}) \)を考えます。論文は、この近似分布\( q({\boldsymbol \omega}) \)からの重みのサンプリングが、Dropoutによってネットワークのユニットをランダムに0にすることと同じ意味になることを示しました。

そのうえで、出力\( \textbf{y} \)の予測分布は、Dropoutを適用したまま推論を複数回繰り返し、その平均をとることで近似できます。論文ではこれをMonte Carlo dropout(モンテカルロ・ドロップアウト、以下MC Dropout)と呼んでいます。

\( p(\textbf{y}|\textbf{x}, \textbf{X}, \textbf{Y}) \approx \displaystyle\frac{1}{T}\sum_{t=1}^{T} p(\textbf{y}|\textbf{x}, {\boldsymbol \omega}_t) \)

ここで\( T \)はサンプリング回数、\( {\boldsymbol \omega}_t \)は\( t \)回目のDropoutで得られた重みです。通常、Dropoutは推論時には無効にしますが、MC Dropoutでは推論時もDropoutを有効にしたまま\( T \)回推論する、というのがポイントです。

予測分布の不確実性(予測しにくさ)を表す指標としては、論文では分散やエントロピーの利用が提案されています。本章では、このうちエントロピーを使って、各画像の予測しにくさを定量化してみます。エントロピーは確率分布の予測しにくさを表す指標で、確率分布が一様分布に近いほど(どのラベルとも判断がつかないほど)大きくなります。

MNISTによる実証

それでは実際に、Dropoutを適用して深層学習モデルを学習し、Dropoutを適用したまま推論を繰り返して予測分布を作成してみます。論文と同様に、MNIST画像分類タスクで実験してみます。

まずは必要なライブラリの読み込みとMNISTデータの準備です。

import numpy as np
import matplotlib.pyplot as plt
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.data import DataLoader
from torchvision import datasets, transforms
from tqdm import tqdm

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')

transform = transforms.ToTensor()
train_dataset = datasets.MNIST(root='./data', train=True, download=True, transform=transform)
valid_dataset = datasets.MNIST(root='./data', train=False, download=True, transform=transform)

train_loader = DataLoader(train_dataset, batch_size=1000, shuffle=True)
valid_loader = DataLoader(valid_dataset, batch_size=1000, shuffle=False)

len(train_dataset), len(valid_dataset) # (60000, 10000)

モデルのアーキテクチャは、畳み込みを少し加えた簡単なCNNにします。全結合層のあとにDropoutを入れている点がポイントです。

class Model(nn.Module):
    def __init__(self):
        super().__init__()
        self.conv1 = nn.Conv2d(1, 16, 3)
        self.conv2 = nn.Conv2d(16, 32, 3)
        self.fc3 = nn.Linear(32 * 5 * 5, 1000)
        self.fc4 = nn.Linear(1000, 1000)
        self.fc5 = nn.Linear(1000, 10)
        self.dropout = nn.Dropout(p=0.5)

    def forward(self, x):
        h1 = F.max_pool2d(F.relu(self.conv1(x)), 2)
        h2 = F.max_pool2d(F.relu(self.conv2(h1)), 2)
        h2 = h2.flatten(1)
        h3 = self.dropout(F.relu(self.fc3(h2)))
        h4 = self.dropout(F.relu(self.fc4(h3)))
        y = self.fc5(h4)
        return y

モデルを学習させます。

model = Model().to(device)
optimizer = torch.optim.Adam(model.parameters(), lr=1e-4)
criterion = nn.CrossEntropyLoss()

epoch_num = 10
for epoch in range(epoch_num):
    model.train()
    for x, y in train_loader:
        x, y = x.to(device), y.to(device)
        optimizer.zero_grad()
        loss = criterion(model(x), y)
        loss.backward()
        optimizer.step()

    # 検証
    model.eval()
    correct = 0
    with torch.no_grad():
        for x, y in valid_loader:
            x, y = x.to(device), y.to(device)
            correct += (model(x).argmax(1) == y).sum().item()
    print(f'epoch {epoch + 1}: valid accuracy = {correct / len(valid_dataset):.4f}')

10エポックほど学習させると、検証データで98%程度の精度になります(環境や乱数により多少前後します)。

続いて、本題のMC Dropoutによる予測分布を作成します。通常、PyTorchではmodel.eval()でDropoutを無効にして推論しますが、ここでは意図的にDropoutを有効な状態(model.train())にして推論を繰り返します(今回のモデルには含まれませんが、BatchNormなどtrain/evalで挙動が変わる層がある場合は、それらはeval()のままDropout層だけを有効化する必要があります)。1回の推論で各クラスの予測確率ベクトル(softmax出力)が1つ得られ、これをサンプリング回数だけ繰り返して平均することで、「結局どのラベルにどれだけ振り分けられたのか」という予測分布を近似できます。

1枚の画像について推論を繰り返したとき、すべてのラベルに均等に振り分けられれば(一様分布に近ければ)、その画像はどのラベルか判断がつかない、つまり予測しにくい画像ということになります。逆に、予測しやすい画像は、どんなDropoutパターンでも予測ラベルが特定の値に集中します。この予測しにくさを、エントロピーで定量化します。

以下は、バリデーションデータの中からtarget_numのラベルに絞ってエントロピーを算出し、予測しやすい画像と予測しにくい画像をプロットするコードです。

def plot_entropy_examples(target_num, sampling_num=50):
    # 対象ラベルの画像だけ抽出
    valid_x = valid_dataset.data.float().unsqueeze(1) / 255.0  # (N, 1, 28, 28)
    valid_y = valid_dataset.targets
    target_x = valid_x[valid_y == target_num].to(device)

    # MC Dropout:推論時もDropoutを有効にする
    model.train()
    entropy = np.zeros(len(target_x), dtype=np.float32)
    with torch.no_grad():
        for i in tqdm(range(len(target_x))):
            x = target_x[i].unsqueeze(0)
            preds = np.zeros((sampling_num, 10), dtype=np.float32)
            for j in range(sampling_num):
                preds[j] = F.softmax(model(x), dim=1).cpu().numpy().squeeze()
            preds = preds.mean(axis=0)
            entropy[i] = np.sum(-preds * np.log(preds + 1e-12))

    target_imgs = (target_x.cpu().numpy().reshape(-1, 28, 28) * 255).astype(np.uint8)
    low_entropy_imgs = target_imgs[np.argsort(entropy)[:30]]
    high_entropy_imgs = target_imgs[np.argsort(entropy)[::-1][:30]]

    for title, imgs in [('low entropy top 30', low_entropy_imgs),
                        ('high entropy top 30', high_entropy_imgs)]:
        fig, axs = plt.subplots(ncols=10, nrows=3, figsize=(20, 5))
        for i, img in enumerate(imgs):
            axs[i // 10, i % 10].imshow(img, cmap='gray')
            axs[i // 10, i % 10].axis('off')
        plt.suptitle(title)
        plt.show()

plot_entropy_examples(target_num=0)  # 試しに0の画像で検証
数字0の、エントロピーが低い画像TOP30と高い画像TOP30

予想通りの結果になりました。バリデーションの中から0の画像で、MC Dropoutで得られた予測分布のエントロピーが低かったTOP30、高かったTOP30を表示しています。エントロピーが低いものは予測がしやすい画像なので、とても綺麗にお手本のように書かれた0が集まりました。逆にエントロピーが高いものは予測がしにくく、他のラベルと間違えやすい画像なので、形がいびつだったり汚い字が集まっています。

これを他の数字でも実行した結果が以下です。

数字1のエントロピー低/高 TOP30

1の画像では、エントロピーが低いものはただ真っ直ぐに線が引かれているだけで、間違いようがなさそうです。エントロピーが高いものは、字がかすれていたり、線が太すぎたりして予測を間違えやすい傾向にあるようです。

数字2のエントロピー低/高 TOP30

2の画像では、エントロピーが高いものはかなりひどく、人でも読めなさそうなものも見受けられます。

数字3のエントロピー低/高 TOP30

3の画像では、エントロピーが低いものはとても綺麗にバランスの取れたお手本のような3が集まりました。エントロピーが高いものは、読めないことはなさそうですが、やはりバランスが悪い字が多いです。

数字4〜9のエントロピー低/高 TOP30(複数枚)
数字4〜9のエントロピー低/高 TOP30(複数枚)
数字4〜9のエントロピー低/高 TOP30(複数枚)
数字4〜9のエントロピー低/高 TOP30(複数枚)
数字4〜9のエントロピー低/高 TOP30(複数枚)
数字4〜9のエントロピー低/高 TOP30(複数枚)

4以降も同様の傾向で、エントロピーが低いものはバランスが良く綺麗な字、高いものは崩れた字が集まります。

このように、Dropoutを入れるだけで様々な深層学習のネットワークアーキテクチャに適用でき、結果も見ていて面白いです。欠点があるとすれば、1つの入力につきサンプリング回数だけ推論を繰り返すため、予測に少し時間を要することです。また、予測がしにくいデータは教えてくれますが、「なぜ予測しにくいのか」「どうすれば間違えにくくなるのか」は、結果を見て自分で考察していく必要があります。

このような方法の応用例として、物体検出のモデルに適用し、予測確率が高そうなバウンディングボックスを重ねて可視化する、以下のような論文も出ています。

エントロピー算出に関する検証

この節では、エントロピーの計算順序に関する実装上の疑問について、実験で確かめてみます。

論文は予測の平均・分散・エントロピーといった指標には触れていますが、本章で試すような実装上の計算順序の違いまでは主題にしていません。このとき、エントロピーを使うにしても、計算の順序として例えば次の2通りが考えられます。

  • 出力ベクトル → Softmax → 平均 → エントロピー
  • 出力ベクトル → 平均 → Softmax → エントロピー

どちらも問題なさそうな気がしますが、どちらがより妥当なのかが疑問に思いました。そこで、学習・予測データやDropoutなどの乱数を固定したうえで、両方の結果を見比べてみます。

1つ目は前節と同じ、出力ベクトル → Softmax → 平均 → エントロピーのパターンです(前節のplot_entropy_examplesがこれにあたります)。MC Dropoutサンプリングごとにsoftmaxをとり、その平均からエントロピーを計算しています。

パターン1(Softmax→平均→エントロピー)の結果

2つ目は、出力ベクトル → 平均 → Softmax → エントロピーのパターンです。MC Dropoutサンプリングの出力ベクトル(softmax前)の平均をとってから、softmaxをかけてエントロピーを算出します。コードは、サンプリング部分を以下のように変更します。

# パターン2:出力ベクトルの平均をとってからSoftmax
for j in range(sampling_num):
    preds[j] = model(x).cpu().numpy().squeeze()  # softmax前の出力
mean_logits = preds.mean(axis=0)
prob = F.softmax(torch.from_numpy(mean_logits), dim=0).numpy()
entropy[i] = np.sum(-prob * np.log(prob + 1e-12))
パターン2(平均→Softmax→エントロピー)の結果

結果は、やはり完全一致はしませんが、傾向としては同じようなものになりました。

さらに、Dropoutなし(通常の推論)で 出力ベクトル → Softmax → エントロピー を計算してみると、結果は以下のようになります。

# Dropoutなし(model.eval())で1回だけ推論
model.eval()
with torch.no_grad():
    prob = F.softmax(model(x), dim=1).cpu().numpy().squeeze()
entropy[i] = np.sum(-prob * np.log(prob + 1e-12))
Dropoutなしの結果

こちらも傾向は同じになりました。ということは、例えば目的が能動学習(予測しにくいデータを優先的にラベル付けする手法)に用いるなどであれば、いずれの方法でも似たような効力が得られそうな気がします。

とはいえ、MC Dropoutサンプリングを導出することでベイズの枠組みとして考えられることは論文で理論的に定式化されていますので、サンプリングから予測分布を導出する形まで、数式的にはMC Dropoutが最も納得のいく方法だと思います。

MC Dropoutの位置づけと不確実性推定の広がり

深層学習における不確実性推定(Uncertainty Quantification)は現在も活発に研究されています。MC Dropoutの実務での位置づけを整理します。

  • MC Dropoutの立ち位置: MC Dropoutは「既存のDropout付きモデルに、推論を複数回繰り返すだけで適用できる」という手軽さから、今でも不確実性推定の入門・ベースラインとして広く使われています。追加の学習が不要で、メモリ消費も小さいことが利点です。
  • Deep Ensembles: 初期値や学習データの順序を変えて複数のモデルを学習し、その予測のばらつきから不確実性を見る「ディープ・アンサンブル」は、MC Dropoutより高い精度・較正性能を示すことが多く、不確実性推定の有力な手法として定着しました。一方で、複数モデルを学習・保持するためコストは大きく、MC Dropoutとはコストと精度のトレードオフの関係にあります。
  • Evidential Deep Learning: 近年では、1回の推論で不確実性そのものを直接出力するよう設計された「Evidential Deep Learning(証拠的深層学習)」も提案されています。サンプリングを繰り返す必要がなく高速な点が特徴で、回帰・分類の両方に拡張されています。
  • 不確実性の2分類: 不確実性は、データそのもののばらつきに由来する「偶然的不確実性(Aleatoric)」と、モデルの知識不足に由来する「認識的不確実性(Epistemic)」に分けて考えるのが一般的になりました。MC Dropoutやアンサンブルは主に後者(モデルが自信を持てない領域)を捉える手法として整理されています。
  • 実務での重要性: 自動運転や医療診断のように、予測を「どれだけ信頼してよいか」が安全性に直結する領域では、不確実性推定は欠かせない技術になっています。また、本章でも触れた能動学習(ラベル付けの優先順位付け)や、未知データ・異常データの検知などにも応用されており、AIを実務に組み込むうえでの重要なピースになっています。

手軽に試せるMC Dropoutから始めて、要求される精度やコストに応じてアンサンブルやEvidential系へ広げていく、という使い分けが現在の実務的な流れです。次章では、この不確実性推定の考え方をさらに掘り下げ、ベイズニューラルネットワークについて見ていきます。

参考文献

この章を深めたい方への参考書籍

須山敦志『ベイズ推論による機械学習入門』(講談社、機械学習スタートアップシリーズ)。MC Dropoutの背景にあるベイズ推論を、確率分布の基礎から変分推論まで丁寧に積み上げて学べる入門書です。次章のベイズニューラルネットワークの理解にもそのままつながります。


第8章 ベイズニューラルネットワーク、不確実性を最初から設計する

前章では、MC Dropoutという手法を紹介しました。学習済みのニューラルネットワークに対して、推論時にもDropoutを有効にしたまま複数回予測を行うことで近似的にベイズ推論を行い、予測のばらつきから不確実性を見積もるという方法でした。既存のモデルにほとんど手を加えずに導入できる手軽さが、MC Dropoutの大きな利点でした。

本章で扱うベイズニューラルネットワーク(BNN)は、これとはアプローチが異なります。MC Dropoutが「学習済みモデルへの後付け」だったのに対し、BNNは最初からニューラルネットワークの重みを確率分布として設計します。重みが一つの値に定まるのではなく事後分布として求まるため、予測もまた確率分布として得られます。これにより、モデルが「どれだけ自信を持ってその答えを出しているか」を、モデル構造そのものに組み込んで表現できます。

本章では、ベイズ推論の基本的な考え方からベイズニューラルネットワークの数式表現、そしてPythonの確率的プログラミングライブラリPyMCを使った実装例までを、Kaggleの毒キノコ分類データセットを題材に見ていきます。説明可能AI(XAI)の文脈で言えば、BNNは「なぜその答えを出したか」だけでなく「その答えにどれだけ確信を持っているか」まで表現できるアプローチだと位置づけられます。

ベイズ推論とベイズニューラルネットワーク

まずはベイズ推論の基本的な考え方に触れます。

ベイズ推論では、観測データの集合\( \mathcal{D} \)と未知のパラメータ\( \theta \)について、モデル\( p(\mathcal{D},\theta)=p(\mathcal{D}|\theta)p(\theta) \)を構築し、次の事後分布を求めます。

\( p(\theta|\mathcal{D})=\displaystyle\frac{p(\mathcal{D}|\theta)p(\theta)}{p(\mathcal{D})}=\displaystyle\frac{p(\mathcal{D}|\theta)p(\theta)}{\int p(\mathcal{D}|\theta)p(\theta)d\theta} \)

これを解析的または近似的に求めます。共役な事前分布を仮定すれば事後分布は解析的に求まりますが、そうでない場合は分母の\( \int p(\mathcal{D}|\theta)p(\theta)d\theta \)の計算が非常に困難になるため、代わりにマルコフ連鎖モンテカルロ法や変分推論を使って近似的に事後分布を求めます。

  • マルコフ連鎖モンテカルロ法(MCMC):\( \theta_i \sim p(\theta|\mathcal{D}) \)を大量にサンプリングし、平均や分散を求めたりプロットしたりして、分布の概形を把握します。
  • 変分推論(Variational Inference):解析しやすい分布\( q(\theta;\eta) \)を考え、\( p(\theta|\mathcal{D}) \approx q(\theta;\eta) \)と近似します。言い換えると、\( \arg\min_{\eta} KL(q(\theta;\eta) \parallel p(\theta|\mathcal{D})) \)という最小化問題を解くことになります。

これらを踏まえて、ベイズニューラルネットワークの考え方に移ります。ベイズニューラルネットワークでは、ニューラルネットワークのパラメータ(重みとバイアス)\( {\boldsymbol w} \)がある確率分布に従うと仮定し、その事後分布をベイズ推論で求めます。

\( {\boldsymbol w} \sim N({\boldsymbol 0}, {\boldsymbol I}) \)
\( p(y|{\boldsymbol x},{\boldsymbol w}) = \text{Categorical}\Bigl(\text{softmax}(f({\boldsymbol x},{\boldsymbol w}))\Bigr) \)
\( f(\cdot) : \text{ニューラルネットワークの出力} \)

ニューラルネットワークにベイズを適用する話は歴史的に古くからあり、メリット・デメリットは、ベイズ推論で一般的に言われることと同等です。

  • メリット:少ない学習データでも学習しやすい、過学習を抑制しやすい、予測を不確実性とともに表現できる
  • デメリット:計算量が多い、推論の近似や事前分布の設定に結果が依存する

確率的プログラミングライブラリについて

ベイズモデルを実装するには、確率的プログラミングライブラリを使うのが一般的です。確率変数や事前分布を宣言的に記述し、MCMCや変分推論といった推論アルゴリズムを呼び出すだけで事後分布を求められます。

かつてはEdward(計算にTensorFlowを用いる確率的プログラミングライブラリ)が知られていましたが、EdwardはEdward2を経てTensorFlow Probabilityに統合され、独立したライブラリとしての開発は終了しました。本章では、現在ベイズモデリングで広く使われているPyMCを使います。

現在、Pythonでベイズモデリングを行う主なライブラリには、PyMC、NumPyro、Pyro、TensorFlow Probability(TFP)などがあります。NumPyroやPyroはGPUを活用した高速なサンプリングに強く、PyMCもJAXバックエンドやNumPyroとの連携による高速化が進んでいます。今回使うPyMCは、Pythonらしい直感的な書き方で確率モデルを記述でき、MCMC(NUTS)や変分推論(ADVI)にも対応しているため、入門にも実務にも使いやすいライブラリです。

例えば、観測データから正規分布の平均\( \mu \)の事後分布を変分推論で求めるコードは、PyMCでは以下のように書けます。

import numpy as np
import pymc as pm

x_train = np.random.randint(10, 20, size=30)  # 10〜20の観測値が30個

with pm.Model() as model:
    mu = pm.Normal('mu', mu=0.0, sigma=10.0)      # 事前分布
    x = pm.Normal('x', mu=mu, sigma=1.0, observed=x_train)  # 尤度
    approx = pm.fit(n=10000, method='advi', random_seed=0)  # 変分推論(ADVI)

trace = approx.sample(1000)
print(trace.posterior['mu'].mean().item())  # 14.7 付近

確率変数をpm.Normalなどで宣言し、observedに観測データを渡して、pm.fit(変分推論)やpm.sample(MCMC)で事後分布を求める、という流れです。

ベイズニューラルネットワークによる毒キノコ分類モデル

それでは、実際にベイズニューラルネットワークを実装してみます。題材は、Kaggleにある毒キノコの分類用データセットです。

食用キノコか毒キノコかがラベル付けされているデータセットです。KaggleからダウンロードしたCSVをdata/に配置した前提で、mushrooms.csvを見てみると、次のようになっています。

import pandas as pd

data = pd.read_csv('./data/mushrooms.csv')
data.head()
mushrooms.csvの先頭5行。class列と各特徴量

一番左のclass列が、pなら毒(poisonous)、eなら食用(edible)です。その他、傘の形・傘の表面・傘の色・匂いなどが格納されている、面白いデータセットです。

カラムの値はすべてカテゴリ(文字列)なので、ダミー変数化します。目的変数(class)を分けて、残りを説明変数とします。今回は、毒キノコである確率を予測する二値分類として扱います。

DataFrameのNumPy変換にはto_numpy()を使います(旧APIのas_matrix()は削除されています)。

import numpy as np

# 目的変数:p(毒)を1、e(食用)を0に
y = (data['class'] == 'p').astype(int).to_numpy()

# 説明変数:class以外のカテゴリ列をダミー変数化
X = pd.get_dummies(data.drop(columns='class')).astype(np.float32).to_numpy()
print(X.shape)  # (8124, 117)

from sklearn.model_selection import train_test_split

train_x, test_x, train_y, test_y = train_test_split(
    X, y, test_size=0.2, stratify=y, random_state=0)

in_size = train_x.shape[1]

せっかくなので、まず通常の(点推定の)モデルと、ベイズニューラルネットワークの両方を実装し、実装と結果の違いを確認してみます。今回は隠れ層のないシンプルな構成にします。隠れ層がなく出力をソフトマックス(二値ならシグモイド)に通す構成は、数学的にはロジスティック回帰と同等です。

通常の(点推定の)モデル

まずは点推定のモデルです。隠れ層のないニューラルネットワーク、すなわちロジスティック回帰をscikit-learnで学習させてみます。

from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score

clf = LogisticRegression(max_iter=1000)
clf.fit(train_x, train_y)

print('train accuracy:', accuracy_score(train_y, clf.predict(train_x)))
print('test accuracy :', accuracy_score(test_y, clf.predict(test_x)))

キノコのデータは特徴が分かりやすいため、点推定のモデルでもほぼ100%に近い高い精度で分類できます(精度は環境や乱数により多少変動します)。predict_probaを使えばクラスの確率も得られますが、点推定では「重みは一つの値」として求まるため、重みそのものの不確実性(推定のばらつき)は表現されません。

ベイズニューラルネットワーク

次に、同じ構成をベイズニューラルネットワークとしてPyMCで実装します。重み\( {\boldsymbol w} \)とバイアス\( b \)に正規分布の事前分布を置き、変分推論(ADVI)で事後分布を求めます。

import pymc as pm

with pm.Model() as bnn:
    # 重みとバイアスに事前分布を置く
    w = pm.Normal('w', mu=0.0, sigma=1.0, shape=in_size)
    b = pm.Normal('b', mu=0.0, sigma=1.0)

    # 線形結合 → シグモイドで毒である確率
    logit = pm.math.dot(train_x, w) + b
    y_obs = pm.Bernoulli('y_obs', logit_p=logit, observed=train_y)

    # 変分推論(ADVI)で事後分布を近似
    approx = pm.fit(n=30000, method='advi', random_seed=0)

trace = approx.sample(500)  # 事後分布から500セットの重みをサンプリング

学習できたら、テストデータで精度を確認します。ベイズニューラルネットワークでは、重みが確率分布になっているため、同じ入力に対しても予測がばらつきます。そこで、事後分布から重みを複数サンプリングして予測を行い、その平均をとって最終的な予測とします。

# 事後分布からサンプリングした重みで予測(不確実性込み)
post_w = trace.posterior['w'].values.reshape(-1, in_size)  # (サンプル数, in_size)
post_b = trace.posterior['b'].values.reshape(-1)           # (サンプル数,)

# 各テストサンプルについて、サンプリングした重みごとに毒である確率を計算
logits = test_x @ post_w.T + post_b           # (テスト件数, サンプル数)
probs = 1.0 / (1.0 + np.exp(-logits))         # シグモイド
pred_mean = probs.mean(axis=1)                # 事後平均(毒である確率)
pred_label = (pred_mean > 0.5).astype(int)
pred_std = probs.std(axis=1)  # 事後サンプル間のばらつき(予測の不確実性)

print('test accuracy:', (pred_label == test_y).mean())

ベイズニューラルネットワークでも、点推定のモデルと同様に高い精度で分類できました(精度は環境・乱数・分割により多少変動します)。重要なのは、ベイズの場合は予測が確率分布として得られる点です。pred_meanは毒である予測確率の事後平均で、pred_std(事後サンプル間のばらつき)を見ると、モデルがどれだけ自信を持って判定しているかが分かります。例えば、pred_meanが0.5付近だったり、pred_stdが大きいサンプルは、モデルが判断に迷っているデータということになります。これが、点推定にはないベイズニューラルネットワークの強みです。

キノコのデータは特徴がはっきりしているため、序盤から精度が高く、点推定とベイズで大きな精度差は出ませんでした。ただ、データが少ない場合や、判断が難しいデータに対しては、ベイズの「不確実性を表現できる」という性質が効いてきます。本章では隠れ層のない構成で試しましたが、隠れ層を入れた本格的なベイズニューラルネットワークも、同じ考え方で実装できます(PyMC公式ギャラリーに例があります)。

なお、ベイズニューラルネットワークは計算コストが大きいため、大規模な深層学習モデルすべてに適用するのは現実的ではありません。データが限られる場合や、予測の信頼性を定量化したい場面で有力な選択肢になります。タスクの性質に応じて、通常の深層学習・MC Dropout・ベイズニューラルネットワークを使い分けるのが実務的な姿勢です。

参考文献

この章を深めたい方への参考書籍

ベイズニューラルネットワークの理論的背景をより深く学びたい方には、須山敦志『ベイズ深層学習』(講談社)をおすすめします。変分推論の仕組みからニューラルネットワークへの応用まで、本章で扱った内容を体系的に学び直せる一冊だと思います。


第5部 言語・生成AI編:言葉を扱うAIの根拠をどう示すか

第5部は、言葉を扱うAIの説明可能性です。前半(第9章)ではテキスト分類を題材に、単語の寄与という形で説明を作る方法と、Attentionをめぐる論争、LLM内部を読み解く最新研究までを扱います。後半(第10章)では生成AI・大規模言語モデル(LLM)について、従来手法がそのまま通用しない世界で実務的に何ができるのかを整理します。

第9章 テキストの説明可能性、モデルは文のどこを見たのか

ここまで、テーブルデータの説明(SHAPなど)と画像の説明(Grad-CAMなど)を見てきました。本章では3つ目の主要なデータ形式である「テキスト」を扱います。問い合わせメールの自動分類、レビューのネガポジ判定、審査業務における申請書類のテキスト評価。言葉を入力とするAIはビジネスの至るところで使われており、そこでも「なぜこの判定になったのか」という問いは避けて通れません。

テキストの説明可能性には、他のデータ形式と共通する部分と、テキストならではの事情の両方があります。共通するのは、説明の基本単位が「入力のどの部分が予測にどれだけ効いたか」であることです。テーブルなら特徴量、画像なら画素の領域、テキストなら単語(トークン)の寄与が説明の中心になります。一方でテキスト特有の事情もあります。文は長さが一定でなく、単語の意味は文脈によって変わり、そして近年の言語モデルは内部が巨大で複雑です。本章では、古典的なテキスト分類の説明から、Attention(注意機構)をめぐる論争、そしてLLM内部を読み解こうとする最新の研究までを、実行できるコードとともに整理します。

摂動ベースの説明を最小実装で理解する

テキスト分類の説明として最も直感的なのは、「その単語を取り除いたら予測はどう変わるか」を調べることです。第3章で扱ったLIMEも、テキストに適用するときはこの発想で動きます。文から単語をランダムに削った文を大量に作り、予測の変化を観察して、各単語の寄与を推定するわけです。この考え方はocclusion(オクルージョン、遮蔽)とも呼ばれ、画像の一部を隠して予測の変化を見る手法と同じ系譜にあります。

仕組みを理解するために、ライブラリに頼らず最小実装で試してみます。分かち書き済みの日本語レビュー文(ポジティブ/ネガティブの2クラス、各14文)をTF-IDFとロジスティック回帰で分類する小さなモデルを作り、テスト文から単語を1つずつ取り除いたときのポジティブ確率の変化を測ります。

# occlusion(leave-one-word-out)によるテキスト分類の説明
import matplotlib.pyplot as plt
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression

plt.rcParams["font.family"] = "Meiryo"

# 分かち書き済みの日本語レビュー文(1=ポジティブ, 0=ネガティブ)
POS = "対応 が 丁寧 で 満足 し た|担当 者 の 説明 が わかりやすく 安心 でき た|発送 が 早く 商品 も きれい で 満足|価格 が 手頃 で 品質 も 良い|サポート が 親切 で 信頼 でき る|手続き が 簡単 で 便利 だっ た|梱包 が 丁寧 で 破損 も なく 安心|返信 が 早く 対応 も 的確 だっ た|味 が 良く 家族 も 喜ん だ|予約 が スムーズ で 待ち 時間 も 短い|スタッフ が 明るく 親切 で 好印象|説明 が 丁寧 で 初めて でも 使い やすい|配送 が 予定 通り で 助かっ た|アプリ が 軽く 操作 も 快適".split("|")
NEG = "対応 が 雑 で 不満 が 残っ た|発送 が 遅く 商品 も 汚れ て い た|説明 が わかりにくく 不安 に なっ た|価格 が 高く 品質 も 悪い|サポート が 冷たく 信頼 でき ない|手続き が 複雑 で 不便 だっ た|梱包 が 雑 で 破損 が あっ た|返信 が 遅く 対応 も 的外れ だっ た|味 が 悪く 家族 も 残念 だっ た|予約 が 取りにくく 待ち 時間 が 長い|スタッフ が 暗く 不親切 で 悪印象|説明 が 不十分 で 初めて だ と 使い にくい|配送 が 遅れ て 困っ た|アプリ が 重く 操作 も 不快".split("|")
texts, y = POS + NEG, [1] * len(POS) + [0] * len(NEG)

# TF-IDF + ロジスティック回帰で分類モデルを作る
vec = TfidfVectorizer(token_pattern=r"(?u)\S+")
X = vec.fit_transform(texts)
clf = LogisticRegression(random_state=0).fit(X, y)
print(f"学習データ精度: {clf.score(X, y):.3f}")

# ポジ・ネガ両方の語を含むテスト文
test_text = "担当 者 の 対応 は 丁寧 だっ た が 発送 が 遅く 少し 不満 だ"
tokens = test_text.split()
base_prob = clf.predict_proba(vec.transform([test_text]))[0, 1]
print(f"テスト文: {test_text}")
print(f"元のポジティブ確率: {base_prob:.4f}")

# occlusion: 単語を1つずつ取り除き、ポジティブ確率の変化を見る
print("Occlusion Δp(正ならポジティブ方向):")
deltas = []
for i, word in enumerate(tokens):
    occluded = " ".join(tokens[:i] + tokens[i + 1:])
    delta = base_prob - clf.predict_proba(vec.transform([occluded]))[0, 1]
    deltas.append(delta)
    print(f"{i + 1:02d} {word}: {delta:+.4f}")

# 寄与の横棒グラフを保存
order = sorted(range(len(deltas)), key=lambda i: deltas[i])
labels = [f"{i + 1}:{tokens[i]}" for i in order]
values = [deltas[i] for i in order]
colors = ["#2a9d8f" if v >= 0 else "#e76f51" for v in values]
fig, ax = plt.subplots(figsize=(7.2, 4.8))
ax.barh(labels, values, color=colors)
ax.axvline(0, color="#333333", linewidth=0.8)
ax.set_xlabel("ポジティブ確率の変化 Δp")
ax.set_title("Occlusionによる単語寄与")
fig.savefig("occlusion_text_ja.png", dpi=150, bbox_inches="tight")

実行結果は以下のとおりです。

学習データ精度: 1.000
テスト文: 担当 者 の 対応 は 丁寧 だっ た が 発送 が 遅く 少し 不満 だ
元のポジティブ確率: 0.5039
Occlusion Δp(正ならポジティブ方向):
01 担当: +0.0144
02 者: +0.0144
03 の: +0.0144
04 対応: +0.0009
05 は: +0.0000
06 丁寧: +0.0329
07 だっ: -0.0076
08 た: -0.0103
09 が: -0.0040
10 発送: +0.0004
11 が: -0.0040
12 遅く: -0.0267
13 少し: +0.0000
14 不満: -0.0189
15 だ: +0.0012
Occlusionによる単語寄与の横棒グラフ

元のポジティブ確率は0.504で、この文がポジティブとネガティブの境界線上にあることが分かります。単語別に見ると、「丁寧」を取り除くと確率が0.033下がり(=ポジティブ方向に効いていた)、「遅く」「不満」を取り除くとそれぞれ0.027、0.019上がる(=ネガティブ方向に効いていた)。モデルがこの文を、ポジティブ要素とネガティブ要素の綱引きとして評価している様子が、単語レベルで確認できます。

もうひとつ注目してほしいのは、「担当」「者」「の」という中立的な単語がポジティブ方向に効いている点です。これは訓練データのポジティブ例に「担当 者 の 説明 が…」という文が含まれる一方、ネガティブ例にこれらの語がないという偏りを、モデルがそのまま拾っているためです。中立語への依存は本番データでの誤判定の芽になります。説明を可視化して初めて見つかるこの種の偏りこそ、第2部で述べた「デバッグとしての説明」の価値です。

実装は数十行ですが、「入力を変化させて出力の変化を見る」という摂動ベースの説明の本質はこれだけです。LIMEはこの摂動をランダムサンプリングと局所的な線形近似で体系化したもの、と捉えると見通しが良くなります。

SHAPで単語の寄与を分解する

第4章で扱ったSHAPは、テキストにもそのまま適用できます。TF-IDF+線形モデルの構成なら、各単語のTF-IDF特徴量に対するSHAP値がそのまま「単語の寄与」になります。同じデータとモデルで確認してみます。

# SHAP(LinearExplainer)によるテキスト分類の単語寄与の分解
import numpy as np
import shap
import matplotlib.pyplot as plt
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression

plt.rcParams["font.family"] = "Meiryo"

# 分かち書き済みの日本語レビュー文(occlusion版と同じデータ・同じモデル)
POS = "対応 が 丁寧 で 満足 し た|担当 者 の 説明 が わかりやすく 安心 でき た|発送 が 早く 商品 も きれい で 満足|価格 が 手頃 で 品質 も 良い|サポート が 親切 で 信頼 でき る|手続き が 簡単 で 便利 だっ た|梱包 が 丁寧 で 破損 も なく 安心|返信 が 早く 対応 も 的確 だっ た|味 が 良く 家族 も 喜ん だ|予約 が スムーズ で 待ち 時間 も 短い|スタッフ が 明るく 親切 で 好印象|説明 が 丁寧 で 初めて でも 使い やすい|配送 が 予定 通り で 助かっ た|アプリ が 軽く 操作 も 快適".split("|")
NEG = "対応 が 雑 で 不満 が 残っ た|発送 が 遅く 商品 も 汚れ て い た|説明 が わかりにくく 不安 に なっ た|価格 が 高く 品質 も 悪い|サポート が 冷たく 信頼 でき ない|手続き が 複雑 で 不便 だっ た|梱包 が 雑 で 破損 が あっ た|返信 が 遅く 対応 も 的外れ だっ た|味 が 悪く 家族 も 残念 だっ た|予約 が 取りにくく 待ち 時間 が 長い|スタッフ が 暗く 不親切 で 悪印象|説明 が 不十分 で 初めて だ と 使い にくい|配送 が 遅れ て 困っ た|アプリ が 重く 操作 も 不快".split("|")
texts, y = POS + NEG, [1] * len(POS) + [0] * len(NEG)

vec = TfidfVectorizer(token_pattern=r"(?u)\S+")
X = vec.fit_transform(texts)
clf = LogisticRegression(random_state=0).fit(X, y)
print(f"学習データ精度: {clf.score(X, y):.3f}")

test_text = "担当 者 の 対応 は 丁寧 だっ た が 発送 が 遅く 少し 不満 だ"
test_x = vec.transform([test_text])
print(f"テスト文: {test_text}")
print(f"元のポジティブ確率: {clf.predict_proba(test_x)[0, 1]:.4f}")

# TF-IDF行列を背景データにして、線形モデルの寄与をSHAP値に分解する
explainer = shap.LinearExplainer(clf, X)
values = np.asarray(explainer.shap_values(test_x))[0]

# テスト文に含まれる単語(非ゼロ特徴量)の寄与を絶対値順に表示
names = vec.get_feature_names_out()
pairs = [(names[i], values[i]) for i in test_x.nonzero()[1]]
pairs.sort(key=lambda item: abs(item[1]), reverse=True)
print("SHAP値(絶対値順):")
for word, value in pairs:
    print(f"{word}: {value:+.4f}")

# 寄与の横棒グラフを保存
plot_pairs = sorted(pairs, key=lambda item: item[1])
labels = [word for word, _ in plot_pairs]
plot_values = [value for _, value in plot_pairs]
colors = ["#2a9d8f" if v >= 0 else "#e76f51" for v in plot_values]
fig, ax = plt.subplots(figsize=(7.2, 4.8))
ax.barh(labels, plot_values, color=colors)
ax.axvline(0, color="#333333", linewidth=0.8)
ax.set_xlabel("SHAP値(ロジット寄与)")
ax.set_title("SHAPによる単語寄与")
fig.savefig("shap_text_ja.png", dpi=150, bbox_inches="tight")
学習データ精度: 1.000
テスト文: 担当 者 の 対応 は 丁寧 だっ た が 発送 が 遅く 少し 不満 だ
元のポジティブ確率: 0.5039
SHAP値(絶対値順):
丁寧: +0.1089
遅く: -0.0901
不満: -0.0646
の: +0.0548
担当: +0.0548
者: +0.0548
だっ: -0.0208
た: -0.0138
が: -0.0068
だ: +0.0064
対応: +0.0040
発送: +0.0035
SHAPによる単語寄与の横棒グラフ

occlusionの結果とSHAPの結果で、上位に来る単語(丁寧、遅く、不満、そして中立語の担当・者・の)がほぼ一致していることが分かります。手法の原理は異なりますが(摂動の実測と、シャープレイ値による理論的な寄与配分)、単純なモデルでは概ね同じ結論に落ち着きます。逆に言えば、この2つの結果が大きく食い違う場合は、モデルの非線形性が強いか、単語同士の相互作用が効いているサインであり、説明を1つの手法だけに頼らない検証の意味もここにあります。

なお、SHAPはBERTのようなTransformerベースの分類モデルにも対応しています。transformersライブラリのpipelineをそのまま shap.Explainer に渡すと、トークン単位の寄与を計算し、shap.plots.text で文中の単語を寄与に応じて色分け表示できます。公式ドキュメントに感情分類の実例があるので、深層モデルで同じことをしたい場合はそちらの書き方が出発点になります(本章の例と違い、モデルのダウンロードと相応の計算時間が必要です)。

Attentionは説明になるのか、という論争

Transformer系のモデルには、Attention(注意機構)という「入力のどこを参照しているか」を表す重みが内蔵されています。この重みを可視化すれば説明になるのではないか、という発想は自然で、実際にBertVizのようなAttention可視化ツールは広く使われてきました。モデルに元々備わっている値を見るだけなので、追加の計算が要らないのも魅力です。

ところが、この発想には有名な論争があります。2019年に発表された論文「Attention is not Explanation」(Jain and Wallace)は、Attentionの重みが勾配ベースの重要度と相関しないこと、そして予測をほとんど変えずにAttentionの分布を大きく変えられることを示し、Attentionを説明として扱うことに疑問を投げかけました。同じ年に「Attention is not not Explanation」(Wiegreffe and Pinter)という論文が反論し、検証方法の妥当性を問い直したうえで「説明になるかは使い方と定義次第」と整理しています。タイトルどおりの応酬ですが、この論争が残した教訓は実務にも重要です。すなわち、Attention可視化の単独提示を顧客や監査向けの「モデルの判断根拠」として使うのは危険だということです。Attentionは「モデルがどこを参照したか」の手がかりにはなりますが、「その参照が予測をどう動かしたか」までは保証しません。

実務での位置づけとしては、Attention可視化はデバッグや探索の道具、対外的な説明にはSHAPやocclusionのような出力への寄与を測る手法、という使い分けが安全です。第5章で述べたサニティチェック(説明がモデルの実態を反映しているかの検証)の考え方は、テキストでもそのまま有効です。

深層モデルの帰属手法と周辺ツール

深層学習ベースのNLPモデルに対する帰属(attribution、入力の各部分に寄与を割り当てること)には、摂動ベース以外に勾配ベースの手法もあります。代表格がIntegrated Gradients(統合勾配法)です。入力を「情報のない基準点(ベースライン)」から実際の入力まで徐々に変化させ、その道筋に沿って勾配を積分することで、各トークンの寄与を計算します。単純な勾配よりも飽和の問題に強く、寄与の合計が予測値の変化に一致する性質(完全性)を持つのが特徴です。PyTorchエコシステムでは、Metaが開発するCaptumがこの手法の標準的な実装を提供しており、テキスト分類モデルのトークン帰属によく使われます。

また、生成モデル(文章を出力するモデル)の帰属に特化したライブラリとしてInseqがあります。transformersの生成モデルに対して、「出力のこのトークンは、入力のどの部分に依存して生成されたのか」をIntegrated Gradientsを含む複数の手法で計算・可視化できます。翻訳や要約の品質検証、プロンプトのどの部分が出力を左右しているかの分析など、生成AIの挙動を調べる研究・開発用途で使われています。

LLMの内部を読む研究の現在地

ここまでの手法は、いずれも「入力と出力の関係」から説明を作るものでした。これに対して、モデルの内部表現そのものを解読しようとする研究分野があり、メカニスティック解釈可能性(mechanistic interpretability)と呼ばれています。LLMの内部では1つのニューロンが複数の意味を重ねて持つため、そのままでは読めません。そこで、内部の活性をスパースオートエンコーダという手法で「人間が解釈できる特徴」の組に分解し、モデルがどんな概念を内部に持っているかを調べるアプローチが2023年頃から急速に発展しました。

この分野で象徴的なのが、Anthropicが2025年3月に発表した2本の研究です。「Circuit Tracing」は、解釈可能な特徴の間のつながりを辿って、特定の入力から出力に至る計算の道筋をアトリビューショングラフとして描く方法論を提示しました。「On the Biology of a Large Language Model」は、この方法論で実際の商用モデル(Claude 3.5 Haiku)の内部を調べ、詩を書くときに行末の韻を先に決めてから行を組み立てていること、複数の言語で共通する概念表現を持っていることなど、外から観察しているだけでは分からない内部の挙動を報告しています。同年5月末にはこの解析ツール群がオープンソース化され、外部の研究者も主要なオープンモデルに対して同様の解析を試せるようになりました。

これらの研究は現時点では研究段階であり、明日の業務システムの説明責任に直接使えるものではありません。ただ、「LLMの内部は原理的に読めない」という前提が、「読み始める道具が出てきた」に変わりつつあることは、この分野の大きな転換点だと考えています。次章で扱う実務的なアプローチ(プロセスの説明責任)と、この内部解読の研究は、いずれ中間で出会うことになるはずです。

実務での使いどころ

テキストの説明可能性を実務に落とすときの考え方を整理します。

  • 問い合わせ分類・審査支援などの判別タスク:判定結果に寄与した単語の上位を業務画面に表示するだけで、担当者の確認効率と納得感は大きく変わります。TF-IDF+線形モデルなら本章の方法がそのまま使え、深層モデルでもSHAPのテキスト対応で同じ形の説明が作れます
  • 不適切な依存の検出:単語寄与を眺めると、モデルが本来注目すべきでない語に依存しているケースが見つかります。特定の地名や人名が判定を左右していれば代理変数による意図しない差別の入口になり得ますし、本章の例で見た中立語への依存のように、定型句が効いていればショートカット学習の疑いがあります。デバッグとしての説明の価値は、テキストで特に大きいと感じています
  • 生成AIの根拠提示:ユーザーへの説明はRAGによる出典提示(次章)が現実解ですが、開発・検証の段階では本章の帰属手法が「プロンプトのどこが出力を左右しているか」の分析に役立ちます

この章を深めたい方への参考書籍

山田育矢監修『大規模言語モデル入門』(技術評論社)。TransformerやBERTの仕組みから、transformersライブラリを使った分類・解析の実装までを日本語で学べる標準的な教科書です。本章で扱ったトークン単位の説明を支える、言語モデル側の基礎を固められます。


第10章 生成AI・LLMの説明可能性はどこまで可能か

ここまでの章では、主にテーブルデータ・画像・テキストを扱う「予測モデル」を対象に、SHAPやLIME、Permutation Importanceといった説明手法を見てきました。しかし2020年代のAI活用を語るうえで、大規模言語モデル(LLM:Large Language Model)を避けて通ることはできません。ChatGPTの登場以降、社内文書の検索、問い合わせ対応、文書作成支援など、LLMを組み込んだシステムは急速に業務へ浸透しています。そして導入が進むほど、「このAIの回答は信用できるのか」「なぜこの回答になったのか説明できるのか」という問いが、経営層や監査部門から必ず投げかけられます。

先に本章の結論を述べておきます。LLMの内部を人間が完全に理解できる形で説明することは、現時点の技術では困難です。これは怠慢ではなく、モデルの構造に由来する本質的な難しさです。一方で、「システムとして説明責任を果たす」ことは十分に可能です。回答の根拠を外部の文書に紐づける、思考の過程を出力させる、入出力を記録して後から検証できるようにする。こうした設計上の工夫を積み重ねることで、LLMを業務で使うために必要な水準の説明可能性は確保できます。

この章では、まずLLMの説明がなぜ難しいのかを原理から整理し、そのうえで実務で使える現実的なアプローチを紹介します。特に、検索拡張生成(RAG)を「説明可能な生成AI」の現実解として位置づけ、社内文書QAシステムでの実務パターンまで踏み込みます。最後に、本ガイドで扱ってきた従来のXAI手法とLLMの説明可能性がどういう関係にあるのかを整理して、章を閉じます。

LLMはなぜ説明が難しいのか

LLMの説明可能性を考える前に、LLMがどういう仕組みで動いているのかを簡単におさらいします。LLMは「自己回帰モデル」と呼ばれる仕組みで文章を生成します。自己回帰とは、これまでの文章(入力プロンプトと、そこまでに生成した単語列)を条件として、次の単語(正確にはトークンと呼ばれる単位)の出現確率を計算し、確率の高い候補から次の一語を選ぶ、という処理を延々と繰り返すことを指します。つまりLLMは、一文字ずつ「次に来そうな言葉」を予測し続けているだけであり、回答全体の設計図を最初に持っているわけではありません。

この確率計算を担っているのが、Transformerと呼ばれるニューラルネットワークです。近年の大規模モデルはパラメータ(学習で調整される数値)の数が数百億から数千億という規模に達しており、公開されている情報だけでも、2020年に発表されたGPT-3が約1,750億パラメータを持つことが論文で示されています。第6章で扱った深層学習モデルの説明が難しい理由として「パラメータが多く、非線形な変換が何層も重なる」ことを挙げましたが、LLMはその困難さが桁違いに増幅された存在だと考えてください。

従来のXAI手法がそのまま通用しにくい理由を、もう少し具体的に挙げます。

  • 特徴量の単位が曖昧:テーブルデータなら「年収」「勤続年数」のような意味の明確な特徴量がありますが、LLMの入力はトークンの列であり、個々のトークンの寄与を出しても人間にとって解釈しやすいとは限りません
  • 出力が単一の数値ではない:予測モデルの出力は「確率0.73」のような一点ですが、LLMの出力は数百から数千トークンの系列です。「どの入力がどの出力に効いたか」の組み合わせが爆発的に増えます
  • 計算コストが現実的でない:SHAPのように入力を何通りも変化させて出力の変化を観察する手法は、1回の推論コストが大きいLLMでは膨大な計算資源を要します
  • 生成に確率的な揺らぎがある:同じ入力でも生成のたびに出力が変わりうるため、「この入力ならこの出力」という決定的な対応関係を前提とした説明が成り立ちにくくなります

「Transformerにはアテンション(注意機構)があり、どの単語に注目したかを可視化できるのだから、それが説明になるのではないか」という考え方もあります。アテンションの重みの可視化は確かに手がかりのひとつになりますが、研究コミュニティでは「アテンションの重みがそのままモデルの判断根拠を表すとは限らない」という指摘が以前からなされています。注目度が高い単語が実際の出力に寄与しているとは限らず、また数十層にわたって何百ものアテンションが複雑に絡み合うため、特定の層の可視化だけを取り出して「これが理由です」と言うことはできないのです。

そしてもうひとつ、LLM特有の、そして実務上もっとも注意すべき問題があります。それが「もっともらしい後付け説明」の問題です。

LLMに「なぜその答えを出したのですか」と尋ねると、流暢な説明が返ってきます。一見すると、モデルが自分の思考過程を報告してくれているように見えます。しかしここに落とし穴があります。その説明もまた「次に来そうな言葉の予測」によって生成されたテキストであり、モデル内部で実際に行われた計算過程の忠実な報告である保証はどこにもないのです。人間に例えるなら、直感で答えを決めた後に、それらしい理由を後から組み立てて話しているような状態が起こりえます。

実際、この分野の研究では、モデルが出力する自己説明が実際の判断要因を反映していないケースが報告されています。たとえば、プロンプトに答えを誘導するようなバイアスを仕込むと、モデルの回答はそのバイアスに引きずられるにもかかわらず、モデル自身の説明ではバイアスの存在に一切触れず、別のもっともらしい理由を述べる、という現象が確認されています。XAIの文献では、説明が人間に納得感を与えるかという「plausibility(もっともらしさ)」と、説明がモデルの実際の計算を反映しているかという「faithfulness(忠実性)」を区別しますが、LLMの自己説明はもっともらしさは高くても忠実性は保証されない、と整理できます。

この点は経営層への報告でも誤解されやすいところです。「AIが理由を説明してくれるなら安心だ」という受け止め方は危険で、正確には「AIは理由らしきテキストを生成できるが、それが本当の理由かどうかは別問題」なのです。この区別を理解すると、LLMとの付き合い方は根本から変わってくると思います。以降の節で紹介するアプローチは、すべてこの前提の上に組み立てられています。

現実的なアプローチ

内部を完全には説明できないという前提に立ったうえで、実務で何ができるのかを整理します。現在広く使われているアプローチを4つ紹介します。いずれも「モデルの中身を透明にする」のではなく、「出力の信頼性を確認し、プロセスの説明責任を果たす」ための工夫だという点が共通しています。

1つ目は、Chain of Thought(チェーン・オブ・ソート、思考の連鎖)です。これは、最終的な答えだけを出させるのではなく、「段階的に考えてください」と指示して、結論に至るまでの中間的な推論ステップを出力させるプロンプト技法です。2022年頃から研究として広く知られるようになり、特に算数の文章題や論理的な推論を要するタスクで回答精度が向上することが報告されています。実務上のメリットは2つあります。ひとつは精度そのものの向上、もうひとつは、推論の途中経過が可視化されるため、人間がレビューする際に「どこで間違えたのか」を特定しやすくなることです。たとえば与信判断の下書きをLLMに作らせる場合、結論だけ出されるより、考慮した要素が列挙されているほうが、担当者は格段にチェックしやすくなります。

ただし、前節の議論にもあった通り、Chain of Thoughtで出力された推論ステップも、あくまで生成されたテキストです。それがモデル内部の実際の計算過程を忠実に反映している保証はありません。表示された推論と実際の判断根拠が食い違いうることは研究でも指摘されています。したがってChain of Thoughtは「本当の理由の開示」ではなく、「人間が検証可能な推論の筋道を出力させ、その筋道自体の妥当性をチェックする」ための手法だと位置づけるのが正確です。筋道が論理的に通っていれば、少なくともその筋道に沿った検証はできる。この割り切りが実務的だと思います。

2つ目は、RAG(Retrieval-Augmented Generation:検索拡張生成)です。これは、LLMが回答を生成する前に、社内文書やデータベースなどの外部知識から関連情報を検索し、その検索結果を根拠として回答を組み立てさせる仕組みです。回答の根拠が「モデルの内部知識」ではなく「特定の文書の特定の箇所」に紐づくため、出典付きの回答が可能になります。説明可能性の観点で非常に重要なアプローチなので、次節で詳しく扱います。

3つ目は、複数回生成による一貫性確認です。LLMの生成には確率的な揺らぎがあるため、同じ質問を複数回投げて回答のばらつきを観察する、という素朴ながら有効な検証ができます。研究の文脈ではSelf-Consistency(自己一貫性)と呼ばれ、複数の推論経路を生成して多数決を取ることで精度が上がることが報告されています。実務での応用としては、重要な判断に関わる出力について複数回の生成結果を比較し、回答が安定していれば確信度が高い、回答が毎回変わるようなら情報不足か曖昧な問いである可能性が高い、というシグナルとして使えます。回答のばらつき自体を「モデルの不確実性の推定値」として扱う発想です。

一貫性確認と関連して、生成の設定を明示的に管理することも挙げておきます。多くのLLMには温度(temperature)と呼ばれるパラメータがあり、値を下げるほど確率の高いトークンが選ばれやすくなって出力が安定し、上げるほど多様で創造的な出力になります。業務システムでは、正確性が求められる回答生成では温度を低めに固定する、アイデア出し支援では高めにする、といった使い分けをしたうえで、その設定値を記録に残します。「同じ質問なのに先週と回答が違う」という問い合わせは運用開始後に必ず発生しますが、生成が確率的である仕組みと設定値を最初から関係者に説明しておけば、不信感ではなく仕様の理解として受け止めてもらえます。説明可能性は回答の中身だけでなく、システムの挙動特性を利用者に正しく伝えることも含む、と捉えるのがよいと思います。

4つ目は、ガードレールとログによるプロセスの説明責任です。ガードレールとは、LLMの入出力に対して機械的なチェックを挟む仕組みの総称で、たとえば個人情報を含む入力の遮断、不適切な出力の検知、回答可能な話題の範囲制限などが含まれます。そしてログとは、いつ、誰が、どんなプロンプトを入力し、システムがどの文書を検索し、どのモデルのどのバージョンがどんな回答を返したかを記録しておくことです。プロンプトのテンプレートやモデルのバージョンも構成管理の対象に含めます。

ここにもうひとつ、人間によるレビュー体制を加えておきます。技術的な仕組みではありませんが、LLMの出力を業務に反映する前に人間が確認・承認するステップ(ヒューマン・イン・ザ・ループと呼ばれます)を挟むかどうかは、システム設計上の重要な分岐点です。第1章で見たリスクベースの考え方をここでも適用し、出力がそのまま顧客や取引先に届く用途、人の権利や財産に影響する用途では人間の承認を必須にする、社内の下書き支援のような用途では事後のサンプル確認にとどめる、といった濃淡を付けます。「AIの出力を誰がいつ確認したか」も記録に残せば、それ自体が説明責任の一部になります。

ログの整備は地味に見えますが、説明責任の観点では最重要と言ってよい要素です。第1章で見たEUのAI規則をはじめ、AIガバナンスの枠組みでは記録の保持と追跡可能性が繰り返し要求されます。問題が起きたときに「なぜその回答が出たのか」をモデル内部から説明することはできなくても、「どんな入力に対し、どの情報源を参照し、どんな設定で回答が生成されたか」を再現できれば、原因の切り分けと再発防止は可能です。説明できないものを説明できると強弁するのではなく、説明できる部分(プロセス)を確実に記録する。これが現時点での誠実な落としどころだと私は考えています。

RAGは「説明可能な生成AI」の現実解

前節で挙げたアプローチのうち、説明可能性への貢献がもっとも大きく、かつ企業導入の現場でほぼ標準構成になっているのがRAGです。本節では、RAGがなぜ説明可能性の観点で優れているのか、そしてどう設計・検証すればよいのかを掘り下げます。

RAGの処理の流れを平易に説明すると、次のようになります。ユーザーが質問を入力すると、システムはまず社内規程やマニュアルなどの文書データベースから、質問に関連する箇所を検索します。多くの場合、文書はあらかじめ数百文字程度の断片(チャンクと呼びます)に分割され、意味の近さで検索できるように数値ベクトルに変換して保存されています。検索で見つかった関連チャンクをプロンプトに埋め込み、「以下の資料に基づいて回答してください」とLLMに指示して回答を生成させます。つまりLLMの役割は「知識を思い出すこと」から「渡された資料を読んで要約・回答すること」に変わります。

この構成が説明可能性にもたらす利点は明確です。

  • 回答の根拠を「どの文書のどの箇所か」という形で具体的に示せる
  • ユーザーが原文にあたって回答の正しさを自分で確認できる
  • 根拠が古い・間違っている場合、モデルの再学習ではなく文書の更新で対処できる
  • 参照した文書がログに残るため、後からの監査・検証が可能になる

モデル内部というブラックボックスを開ける代わりに、根拠を外部化して検証可能にする。これがRAGの本質だと思います。LLMが学習データから「思い出した」知識は出所を特定できませんが、検索して渡した文書なら出所は明確です。ハルシネーション(もっともらしい嘘の生成)を完全には防げないものの、少なくとも「回答と出典を突き合わせて検証する」という手続きが成立します。

ただし、RAGを組めば自動的に説明可能になるわけではありません。設計上の勘所がいくつかあります。まず、出典を付ける設計です。プロンプトで「回答には必ず参照した資料の番号を付記すること」「資料に書かれていないことは推測で補わないこと」と明示的に指示し、回答の各記述がどのチャンクに由来するかを引用番号で示させます。システム側では、引用番号を実際の文書名・ページ・該当箇所へのリンクに変換して表示します。ユーザーがワンクリックで原文の該当箇所を開ける導線まで作って、はじめて「検証可能な回答」になります。

次に、検索結果と回答の対応の検証です。RAGには特有の失敗パターンがあり、代表的なものは3つです。第一に、検索が失敗して関連性の低いチャンクしか取れていないのに、LLMがそれらしく回答してしまうケース。第二に、正しいチャンクが渡っているのに、LLMがチャンクに書かれていない内容を付け加えてしまうケース。第三に、出典として提示された箇所に、実際にはその記述が存在しないケースです。これらを検出するために、回答が検索結果に根拠づけられているかを評価する指標(groundednessやfaithfulnessと呼ばれます)を導入し、回答文の各主張が参照チャンクから導けるかをチェックします。評価の実行には別のLLMを評価者として使う方法(LLM-as-a-Judge)が広く使われていますが、評価者もLLMである以上完璧ではないため、一定割合の人手によるサンプル検査と組み合わせるのが現実的です。

検証の運用イメージを補足します。導入前には、想定される質問と正解のペア(評価用データセット)を数十件から百件程度用意し、検索が正しいチャンクを取れているか(検索精度)と、回答が資料に忠実か(生成品質)を分けて測定します。検索と生成のどちらに問題があるかで打ち手が変わるためです。検索側の問題ならチャンクの分割サイズや検索方式の調整、生成側の問題ならプロンプトの指示強化やモデルの変更、という具合です。導入後は、ユーザーからの「役に立たなかった」フィードバックが付いた回答を定期的にレビューし、失敗パターンを分類して評価用データセットに追加していきます。この改善ループを回す前提で体制を組んでおくことが、精度と説明可能性の両方を継続的に高める鍵になります。

最後に、社内文書QAでの実務パターンです。説明可能性の観点で標準形といえる設計を整理しておきます。

  1. 回答画面に出典を常時表示する:文書名・更新日・該当箇所を回答の直下に示し、原文へのリンクを付ける。出典が表示できない回答は原則として出さない
  2. 「わからない」と言える設計にする:検索結果の関連度が閾値を下回る場合や、資料に該当情報がない場合は、無理に回答せず「該当する情報が見つかりませんでした」と返し、人間の窓口へ誘導する
  3. 回答と検索の全ログを保存する:質問、検索されたチャンク、生成された回答、ユーザーの評価(役に立った/立たなかった)をセットで記録し、定期的にレビューする
  4. 文書の鮮度管理を運用に組み込む:出典となる文書の更新日を管理し、古い規程が根拠として使われ続けないようにする。回答の正しさは検索対象文書の品質に依存するため、文書メンテナンスの責任部署を決めておく
  5. 回答の利用範囲を明示する:「本回答はAIが社内文書を基に生成したものであり、最終判断は原文と担当部署に確認してください」という位置づけを画面上に明記する

RAGの説明可能性は「モデルがなぜそう答えたか」ではなく「その回答はどの文書に基づいているか」を示すものです。問いのすり替えのように見えるかもしれませんが、実務で本当に必要なのは多くの場合こちらです。人間の担当者に問い合わせたときも、求めているのは相手の脳内の思考過程ではなく「根拠となる規程や資料」の提示です。AIにも同じ水準の説明を求めている、と整理できます。

本ガイドの手法との関係

最後に、本ガイドでここまで扱ってきたXAI手法と、LLMの説明可能性がどういう関係にあるのかを整理します。「SHAPやLIMEを覚えたのに、LLMには使えないのか」という疑問への回答でもあります。

まず、LLMの内部そのものを解釈しようとする研究は存在し、活発に進んでいます。メカニスティック解釈可能性(Mechanistic Interpretability)と呼ばれる分野で、ニューラルネットワーク内部の個々の構成要素や回路がどんな概念・機能を担っているのかを、いわばリバースエンジニアリングのように解明しようとする取り組みです。Anthropicをはじめとする研究機関がこの分野に力を入れており、モデル内部から特定の概念に対応する特徴を抽出する研究成果も発表されています。将来的にLLMの内部説明が実用レベルに到達する可能性を感じさせる、非常に重要な研究領域です。ただし現時点では、最先端の巨大モデルの挙動全体を説明できる段階にはなく、実務のシステムに組み込んで「この回答の理由はこれです」と示せる道具にはまだなっていません。発展途上の研究として注視しつつ、実務の説明責任を委ねるのは時期尚早、というのが正直な評価です。

次に、従来手法の適用可能性です。SHAPやLIMEの考え方(入力を変化させて出力への寄与を測る)は原理的にはLLMにも適用でき、入力トークンの寄与度を可視化する試みはあります。しかし前述の通り、計算コストと出力の複雑さから、生成タスクへの直接適用は実用的とは言いがたい状況です。一方で、LLMを部品として使う構成、たとえばLLMの埋め込み表現(テキストを数値ベクトルに変換したもの)を特徴量として従来型の分類モデルに入力する構成であれば、分類モデル側にはこれまでの章の手法がそのまま使えます。システムのどの部分に説明が必要かを切り分ければ、既存のXAIの知識は無駄になりません。

そのうえで、本章の議論を一枚の表に整理しました。LLMシステムの説明可能性は、モデル内部・出力・プロセスという3つのレイヤーで考えると見通しがよくなるかと思います。

レイヤー問い主な手段現時点の成熟度
モデル内部の説明モデルはなぜその計算をしたのかメカニスティック解釈可能性などの内部解析研究研究段階。実務投入は時期尚早
出力の根拠の説明その回答は何に基づいているのかRAGによる出典提示、Chain of Thought、複数回生成による一貫性確認実用段階。企業導入の標準構成
プロセスの説明どういう手続きで回答が生成・管理されているか入出力ログ、プロンプトとモデルのバージョン管理、ガードレール、人手レビュー体制実用段階。ガバナンス上は必須

レイヤーの整理ができると、ユースケースごとの判断もしやすくなります。たとえば議事録の要約やメール文面の下書きのような、人間が必ず目を通してから使う用途であれば、プロセスのレイヤー(ログと利用ルール)だけでも十分に運用できます。社内規程に関するQAのように回答の正確性が問われる用途では、出力の根拠のレイヤー(RAGによる出典提示)が必須になります。そして与信や採用のように個人への影響が大きい判断にLLMを関与させる場合は、現時点の技術では説明可能性の要求水準を満たせない可能性を直視し、LLMは情報整理の補助にとどめて判断そのものは従来型の説明可能なモデルと人間に委ねる、という構成も真剣に検討すべきだと思います。すべてをLLMで置き換える必要はなく、説明責任の重さに応じて道具を使い分ければよいのです。

この表が示す通り、実務で今すぐ担保できるのは2段目と3段目、すなわち「出力の根拠」と「プロセス」です。そして多くのユースケースでは、この2つで説明責任の要求に応えられます。監査で問われるのは「モデル内部のニューロンの挙動」ではなく、「回答の根拠を示せるか」「問題発生時に原因を追跡できるか」「リスクに応じた管理体制があるか」だからです。モデル内部の説明にこだわって導入を止めるのではなく、システムとしての説明を設計に織り込んで前に進む。これが本章の提案です。

従来の予測モデルではSHAPのような手法でモデルの振る舞いそのものに迫れたのに対し、LLMでは根拠の外部化とプロセス管理で担保する。対象によって説明の作法が変わるのは一見不便ですが、「誰に、何を、どのレベルで説明する必要があるのか」から出発するというXAIの基本姿勢は、どちらでも変わりません。第1章で述べた「説明可能性は目的ではなく手段」という原則を、LLMという新しい対象に適用した結果がこの章の整理だ、と受け止めていただければと思います。次章では、こうした説明可能性の取り組みを組織としてどう定着させるかという、運用と体制の話に進みます。

この章を深めたい方への参考書籍

岡野原大輔『大規模言語モデルは新たな知能か』(岩波書店)。LLMの能力と限界を、技術の仕組みに踏み込みながら平易な言葉で整理した一冊です。本章で述べた「なぜ説明が難しいのか」を、より大きな視点から捉え直せます。


西見公宏・吉田真吾・大嶋勇樹『LangChainとLangGraphによるRAG・AIエージェント[実践]入門』(技術評論社)。本章で現実解として位置づけたRAGを、実際にPythonで組んで動かすための実践書です。出典提示や評価の実装イメージを具体化したい方に向いています。


終章 実務でどう使うか、説明の設計ガイド

ここまで、テーブルデータのSHAPやLIME、画像のGrad-CAM系手法、予測の不確実性推定、そして生成AI・LLMの説明可能性まで、XAIの主要な道具を一通り見てきました。それぞれの章では「この手法は何を計算していて、どう実装するのか」に焦点を当ててきましたが、実務でXAIを使うときに本当に難しいのは、手法の実装そのものではありません。難しいのは、「どの手法を」「誰のために」「どのタイミングで」使うのかという設計の部分です。XAIがうまく機能しないプロジェクトの多くは、手法の選択ミスではなく、説明の設計の欠如でつまずいているのだと思います。

終章では、本ガイドの内容を実務に落とし込むための整理をします。まず、扱うデータと目的から手法を選ぶための早見表を示します。次に、説明の受け手が誰かによって説明の設計がどう変わるのかを、経営層・現場担当者・監査・顧客の4者に分けて考えます。そのうえで、XAI導入で起こりがちな失敗を整理し、最後に、本ガイド全体を貫くメッセージである「説明可能性はAI活用の信頼インフラである」という考え方に立ち返って締めくくります。

技術の詳細は各章に譲り、ここでは判断の拠りどころになる考え方だけを凝縮してお伝えします。実装フェーズに入る前の企画段階で、あるいはプロジェクトの途中で立ち止まったときに、この終章だけを読み返していただく使い方も想定しています。

手法選択の早見表

手法選びの出発点は、「どんなデータを扱うのか」と「何を知りたいのか」の2軸です。データ種別はテーブルデータ、画像、テキスト・LLMの3つに大別できます。目的は、モデル全体の傾向を掴みたいのか(大域的説明)、個別の予測1件の根拠を知りたいのか(局所的説明)、それとも予測をどこまで信用してよいかを測りたいのか(確信度・不確実性)の3つに分けられます。この2軸を掛け合わせると、本ガイドで扱った手法は次のように整理できます。

データ種別全体傾向を掴みたい個別予測の根拠を知りたい確信度を測りたい
テーブルデータ解釈可能なモデル(第2章)、Permutation Importance・PDP(第2章)、SHAP値の全体集計(第4章)SHAP(第4章)、LIME(第3章)MC Dropout(第7章)、ベイズニューラルネットワーク(第8章)
画像誤分類サンプルへのCAM適用の傾向分析(第5章)Grad-CAM・Grad-CAM++・Score-CAM(第5章)、Visual Backpropagation(第6章)MC Dropoutによる予測分布の推定(第7章)
テキスト・LLM単語寄与の集計・評価データセットに対する出力の系統的な分析(第9〜10章)occlusionやSHAPによる単語寄与、RAGによる根拠文書の提示(第9〜10章)複数回生成の一致度の確認、自己申告される確信度の限界の理解(第10章)

迷ったときの考え方を、判断の順番として書き下すと次のようになります。

  • そもそも解釈可能なモデルで精度要件を満たせないかをまず確認する(第2章)
  • 複雑なモデルを使うなら、開発段階で大域的説明によりモデルの健全性を確認する
  • 運用で個別の根拠提示が必要なら、テーブルはSHAP、画像はGrad-CAM系を第一候補にする
  • 間違いの影響が大きい業務なら、根拠の説明に加えて確信度の提示を組み込む
  • LLMを使うなら、内部の説明ではなく根拠文書の提示(RAG)で説明責任を担保する

そのうえで、この表を使うときの補足をいくつか添えておきます。

第一に、テーブルデータでは「まず解釈可能なモデルで足りないか」を必ず検討してください。第2章で述べたとおり、線形モデルや決定木、勾配ブースティングの浅い木で業務要件を満たす精度が出るなら、事後的な説明手法を持ち出すまでもなく、モデルそのものが説明になります。複雑なモデルにSHAPを重ねる構成は、それで初めて意味のある精度向上が得られる場合の選択肢です。順序としては、解釈可能なモデルを基準線として作り、複雑なモデルとの精度差を測ったうえで、その差が説明コストの増加に見合うかを判断する、という流れをおすすめします。

第二に、大域と局所は対立するものではなく、実務では両方使うのが普通です。モデルの開発段階では、Permutation ImportanceやSHAPの全体集計でモデルの挙動が業務知識と整合しているかを確認し、運用段階では、個別の判定に対してSHAPやLIMEで根拠を提示する。この二段構えが定番の形です。大域的な確認を飛ばして局所説明だけを運用に載せると、モデル自体が変な学習をしていることに気づかないまま、もっともらしい個別説明を量産してしまう危険があります。

第三に、確信度の列は他の2列と性質が異なります。SHAPやGrad-CAMが「なぜこの答えなのか」に答えるのに対し、不確実性推定は「その答えをどこまで信じてよいか」に答えます。実務では後者が意外なほど効きます。たとえば、確信度の低い予測だけを人間の確認に回す運用(第7章で触れた人間との分業設計)は、説明そのものよりも導入効果が分かりやすく、経営層の理解も得やすいと感じています。根拠の説明と確信度の提示は別の道具ですので、両方を組み合わせる前提で設計してください。

第四に、LLMの列については注意が必要です。第10章で述べたとおり、LLMの内部動作を忠実に説明する技術はまだ研究途上であり、実務で頼れるのは「根拠文書を一緒に提示する」というRAG的なアプローチが中心です。LLMが自然言語で語る「私はこう考えました」という説明は、実際の内部処理を反映している保証がなく、それらしい後付けの物語である可能性を常に疑う必要があります。この非対称性を理解しておくことが、LLM活用の説明設計では出発点になります。

誰に説明するのかで設計は変わる

序章で「説明とは、相手が意思決定できるだけの材料を、相手の言葉で提供することだ」と書きました。終章では、この考え方を具体化します。実務で説明を求めてくる相手は、大きく4者に分けられます。経営層、現場担当者、監査・規制当局、そして顧客です。同じモデル、同じ予測であっても、この4者に見せるべき「説明」はまったく別物になります。

説明の相手相手が求めているもの適した説明の形
経営層意思決定できる材料モデルが何を根拠に判断する傾向があるかの要約、期待効果とリスク、モデルが苦手な条件
現場担当者納得して使える根拠個別案件ごとの判断理由、確信度、業務画面への組み込み
監査・規制プロセスの妥当性開発・検証・運用の文書化、再現可能な記録、公平性の検証結果
顧客不利益な判断の理由平易な言葉での主要因の提示、結果を変えるために何ができるかの示唆

まず経営層です。経営層が知りたいのは、SHAP値の分布でもヒートマップでもなく、「このAIに業務を任せる判断をしてよいか」です。したがって説明の中心は、モデルが全体としてどんな要因を重視して判断しているのか(業務常識と整合しているか)、導入した場合の期待効果とリスクは何か、そしてモデルはどんな条件で間違えやすいのか、という3点になります。大域的説明の結果を業務の言葉に翻訳し、限界条件まで含めて簡潔に示すことが重要です。実務では、「このモデルはこういうケースが苦手なので、そこは人間が確認する運用にします」という限界の開示が、かえって経営層の信頼につながることが多いように思います。何でもできますという説明より、できないことが明確な説明のほうが、意思決定の材料としては上等だからです。

次に現場担当者です。与信の審査担当者、製造ラインの品質管理者、コールセンターのオペレーター。AIの判定結果を受け取って実際に動くのはこの方々であり、現場が納得しなければAIは使われずに終わります。現場向けの説明で大切なのは、個別の案件ごとに「なぜこの判定なのか」が業務画面の中で自然に見えることです。SHAPの寄与上位数件を業務用語に変換して表示する、画像検査ならGrad-CAMのヒートマップを判定結果に添える、確信度が低い案件にはその旨のフラグを立てる、といった形です。ここで手を抜いて「別のツールを開けば説明が見られます」という設計にすると、忙しい現場ではまず見てもらえません。説明は業務フローの動線上に置く、というのが鉄則だと思います。また、現場が「この判定はおかしい」と感じたときにフィードバックできる窓口を用意しておくと、説明が一方通行でなくなり、モデル改善の入口にもなります。

三番目は監査・規制当局です。この相手に対する説明は、個々の予測の根拠よりも「プロセスの妥当性」が主題になります。どんなデータで学習したのか、性能と公平性をどう検証したのか、運用中の監視はどうなっているのか、問題が起きたときに誰がどう対処するのか。これらが文書として整備され、後から検証可能な形で記録されていることが求められます。第1章で触れたEUのAI規則(AI Act)のように、高リスク用途のAIに文書化や透明性を義務付ける制度は今後も広がっていくと考えられます。技術的には、モデルの学習条件や検証結果を定型フォーマットにまとめる「モデルカード」のような文書化の実践や、判定ログとともに説明(たとえばSHAP値)を保存しておき、後から個別案件を再検証できるようにする仕組みが該当します。監査対応は後述する「後付け」が最も効かない領域なので、開発の初期から記録を残す習慣が肝心です。

最後に顧客です。与信の否決、保険の査定、採用選考など、本人に不利益が及ぶ判断をAIが支援している場合、顧客への説明は倫理的にも制度的にも避けて通れません。顧客向けの説明で重要なのは、技術的に正確であることよりも、平易で、誠実で、次の行動につながることです。「あなたの申請が承認されなかった主な理由は、直近の借入残高が基準より多いことです」のように、寄与の大きい要因を日常の言葉で伝える。可能であれば、「どの条件が変われば結果が変わり得たか」という反実仮想的な示唆(counterfactual explanation、反実仮想説明と呼ばれる考え方です)まで添えられると、顧客は次に何をすればよいかが分かります。逆に、内部の特徴量名をそのまま並べたり、「AIが総合的に判断しました」という説明にならない説明で済ませたりするのは、不信感を生むだけです。顧客向けの説明文は、データサイエンティストだけで作らず、法務やカスタマーサポートと一緒に設計することを強くおすすめします。

なお、実際のプロジェクトでは、この4者への説明が同時に必要になることも珍しくありません。たとえば与信モデルなら、経営層への導入判断の説明、審査担当者への個別根拠の提示、金融当局への検証プロセスの文書化、申込者への否決理由の通知が、すべて1つのモデルにかかってきます。それぞれの説明を場当たり的に作ると、内容の食い違いが生まれて信頼を損ないます。土台となる説明(たとえばSHAPによる寄与の分解と検証記録)を1つ整備し、そこから相手ごとに表現を変えて派生させる、という一貫した構成にしておくと、整合性が保たれ、更新の手間も減ります。

この4者の整理から見えてくるのは、XAIの手法選択(前節)と説明の設計(本節)は別のレイヤーの問題だ、ということです。SHAPを導入したからといって説明責任が果たせるわけではなく、SHAPの出力を誰向けにどう加工して届けるかまで決めて、初めて設計が完成します。プロジェクト計画の段階で、「このAIの説明の受け手は誰と誰か」を列挙し、それぞれに対する説明の形を要件として書き出しておくこと。地味ですが、これが最も効果の大きい一手だと思います。

よくある失敗

ここからは、XAIの導入で起こりがちな失敗を4つ取り上げます。いずれも技術力の不足というより、設計と見積もりの問題です。

  1. 説明の後付け:モデル完成後に説明を求められて行き詰まる
  2. SHAP値の過信:寄与を因果と読み違える
  3. 見栄えの良いヒートマップの誤解:忠実性の検証を怠る
  4. 説明コストの見積もり漏れ:計算・運用・人の負担を計画に入れ忘れる

失敗1は「説明の後付け」です。精度を追求してモデルを作り込み、いざ本番導入の稟議や監査対応の段階になって「判断根拠を説明してください」と言われ、そこから慌ててXAIを検討し始めるパターンです。この段階からでもSHAPやGrad-CAMを適用すること自体は可能ですが、問題は説明した結果の中身です。後から説明してみたら、モデルが業務的に使ってはいけない特徴量に強く依存していた、学習データの偏りをそのまま拾っていた、といった事実が判明し、モデルの作り直しに至ることも珍しくありません。説明可能性は、完成品に貼るラベルではなく、開発プロセスに組み込む検査工程です。特徴量設計の段階で「この特徴量の寄与を顧客に説明できるか」を問い、検証段階で大域的説明によるモデルの健全性チェックを回す。設計段階から説明を組み込んでいれば、手戻りは大幅に減らせます。

失敗2は「SHAP値の過信」です。第4章で強調したとおり、SHAP値は「モデルの予測値を各特徴量の寄与に分解したもの」であって、現実世界の因果関係ではありません。ところが実務では、「SHAPで年齢の寄与が大きかったので、年齢が高いことが延滞の原因です」のような因果の言葉に、いつの間にかすり替わってしまうことが少なくありません。モデルが学習しているのはあくまで相関であり、疑似相関や交絡(別の隠れた要因が両方に影響している状態)を含みます。SHAP値が示すのは「このモデルがそう判断した内訳」であって、「世界がそうなっている理由」ではありません。この区別を曖昧にしたまま説明を対外的に出すと、誤った施策判断を誘発したり、顧客への説明として不正確になったりします。説明資料には「モデルの判断根拠であり、因果関係を示すものではない」という一文を添える。それだけでも、読み手の誤解はかなり防げます。因果そのものに踏み込みたいなら、因果推論という別の枠組みが必要になります。

失敗3は「見栄えの良いヒートマップの誤解」です。Grad-CAMをはじめとする可視化手法の出力は視覚的な説得力が強く、「対象物の上が赤く光っている画像」を見せられると、人はモデルを理解した気になります。しかし第5章で議論したとおり、ヒートマップがもっともらしく見えることと、それがモデルの判断過程を忠実に反映していることは別問題です。説明手法の中には、モデルのパラメータをランダムに置き換えても出力がほとんど変わらない、つまりモデルをほぼ見ていないのではないかと指摘されたものもあり、こうした問題を検出するためのサニティチェック(説明手法自体の健全性検査)が研究されてきました。実務での対策としては、複数のCAM系手法で結果を突き合わせる、対象領域を実際にマスクして予測がどう変わるかを確かめる(削除テスト)、既知の正解がある簡単な事例で手法の挙動を確認しておく、といった検証を挟むことです。説明の説明力を疑う、という一手間が、見栄えに騙されない唯一の方法だと思います。

失敗4は「説明コストの見積もり漏れ」です。XAIはタダではありません。まず計算コストがあります。厳密なSHAP値の計算は特徴量数に対して指数的に重く、近似手法を使っても、全件・リアルタイムで説明を生成する構成では推論基盤の増強が必要になることがあります。Score-CAMのように1枚の説明に多数回の推論を要する手法もあります。計算コストへの対策としては、全件ではなく説明を求められた案件だけオンデマンドで計算する、TreeSHAPのような高速アルゴリズムが使えるモデルを選ぶ、夜間バッチで事前計算して結果だけを保存しておく、といった設計上の工夫が定石です。次に運用コストです。説明をログとして保存する仕組み、モデル更新のたびに説明の妥当性を再検証する工程、説明用の画面やレポートの保守。そして最後に人のコストです。SHAP値やヒートマップを業務の言葉に翻訳し、現場や顧客からの「この説明はどういう意味か」という問い合わせに答える人が必要です。この人のコストが最も見落とされやすく、かつ最も効いてくるように思います。プロジェクト計画の段階で、説明の生成・保存・翻訳・問い合わせ対応までを工数として積んでおくこと。「説明は要件である」と最初に宣言してしまえば、コストの議論は自然に始まります。

説明可能性はAI活用の「信頼インフラ」

最後に、本ガイド全体を貫いてきた考え方に立ち返ります。説明可能性は、AIの精度を上げる技術ではありません。AIと人間・組織・社会との間に信頼を通わせるための、いわばインフラです。道路や電気と同じで、あるときには誰も意識しませんが、なければその上に何も築けません。現場の担当者が判定結果を安心して使えるのも、経営層が投資判断を下せるのも、監査に耐える運用が組めるのも、顧客に誠実な説明ができるのも、すべてこのインフラの上での話です。精度の高いモデルでも、信頼のインフラがなければ現場に受け入れられないことがありますし、精度で一歩劣るモデルでも、根拠と限界が示されていれば業務の中で使われ続けることがあります。こうした逆転は、実務では珍しくないと感じています。

そして、序章から繰り返してきたとおり、精度と説明のバランスは技術的な妥協ではなく、課題設計の一部です。「このタスクではどこまでの精度が必要で、どこまでの説明が必要か」は、扱う業務の性質、間違えたときの影響の大きさ、説明の受け手が誰かによって決まります。レコメンドの並び順なら説明はほぼ不要かもしれませんし、与信や診断支援なら説明のない高精度モデルより、説明できる堅実なモデルのほうが正解でしょう。この問いに答えるのはアルゴリズムではなく、課題を設計する人間です。だからこそ、XAIの手法群は「モデルができた後の飾り」ではなく、課題設計の最初から手元に置いておくべき道具なのです。

本ガイドでは、その道具箱の中身を一通り並べてきました。解釈可能なモデルという原点、LIMEとSHAPによる予測の分解、Grad-CAM系による視覚的な根拠提示、不確実性推定による「自信のなさ」の定量化、そしてLLM時代の根拠提示。どれも万能ではありませんが、適材適所で組み合わせれば、「AIはなぜその答えを出したのか」という問いに、実務として十分に答えられる水準まで来ています。次にAIプロジェクトを設計する際、精度の目標値と並べて「説明の要件」を一行書き加えていただけたなら、本コラムを書いた甲斐があったと思います。最後までお読みいただき、ありがとうございました。

Anagraftでは、AIプロジェクトの構想・課題設計から、データ分析・機械学習モデルの開発、AI人材の育成まで一貫したご支援を行っています。ご相談は、以下よりお問い合わせください。

お問い合わせ

よくある質問(FAQ)

最後に、説明可能AIについてよくある質問を取り上げ、Q&A形式で私の考えをまとめます。

Q1. 精度と説明可能性はトレードオフなのですか?

「解釈可能なモデルは精度が低く、高精度なモデルはブラックボックス」と単純化して語られがちですが、実務ではもう少し穏やかな場合が多いように思います。テーブルデータでは、勾配ブースティングにSHAPを組み合わせれば、精度をほぼ犠牲にせず十分な説明が得られる場面が多くあります。逆に、データの構造が単純な問題では、ロジスティック回帰でも精度がほとんど変わらないことも珍しくありません。まず両方を試し、精度差と説明要件を並べて判断するのが健全だと思います。

Q2. SHAPとLIME、結局どちらを使えばよいですか?

迷ったらSHAPをおすすめします。理論的な一貫性があり、ライブラリの完成度も高く、大域的な傾向と個別予測の説明を同じ枠組みで扱えるからです。LIMEが有力になるのは、計算を軽くしたい場合や、テキスト・画像も同じ考え方で素早く説明したい場合です。第3章と第4章で述べたとおり、両者は思想が近いので、片方を理解すればもう片方の習得は容易です。

Q3. 説明可能性はプロジェクトのどの段階で考えるべきですか?

要件定義の段階です。終章でも触れたとおり、最も多い失敗は「モデル完成後に説明を求められて行き詰まる」というものです。誰に、何を、どの粒度で説明する必要があるのかを最初に洗い出し、それをモデルと運用の設計要件に含めてください。説明要件が厳しい業務なら、モデル選定そのものが変わります。

Q4. 判断根拠のヒートマップを顧客への説明にそのまま使えますか?

そのままの流用はおすすめしません。Grad-CAMのヒートマップやSHAPのプロットは、あくまで技術者向けの診断ツールです。顧客や現場への説明では、そこから読み取れる内容を相手の言葉に翻訳する必要があります。また第5章で触れたとおり、可視化が必ずしもモデルの計算過程を忠実に反映しない場合があることも、社外への説明では特に注意すべき点です。

Q5. 生成AIの回答に「説明」を求められたら、どうすればよいですか?

現実解はRAG(検索拡張生成)による根拠提示です。第10章で述べたとおり、LLMの内部を人間が読める形で説明する技術は発展途上ですので、「回答がどの資料に基づいているか」を出典として示すシステム設計で説明責任を担保するのが実務的です。あわせて、入出力のログを残し、プロセスとして説明できる状態を作っておくことが大切です。

Q6. 不確実性推定は、どんな業務で特に重要ですか?

予測の誤りコストが非対称な業務です。医療の見落とし、与信の誤承認、設備故障の見逃しなど、外したときの損失が大きい判断では、「自信のない予測だけ人間が確認する」という運用が効きます。第4部で扱ったMC Dropoutは既存の深層学習モデルに後付けできるので、まず試すハードルは低いはずです。

Q7. 説明可能AIの導入に、どれくらいのコストを見込むべきですか?

手法自体のコストは大きくありません。SHAPもGrad-CAMもオープンソースで、実装は本ガイドのコード程度です。本当のコストは、説明の設計と運用にあります。誰にどんな説明を出すかの合意形成、説明を表示する画面や帳票の開発、説明内容の定期的な検証。ツール導入というより、業務設計の一部として見積もることをおすすめします。

Q8. 説明可能性の検証は、どうすれば「できた」と言えますか?

2つの観点で確認することをおすすめします。1つは技術的な忠実性です。第5章で触れたサニティチェック(モデルの重みをランダム化しても説明が変わらないなら、その説明はモデルを反映していない)のように、説明がモデルの実態を映しているかを検証します。もう1つは受け手側の理解です。実際に説明を受けた現場担当者や意思決定者が、その説明をもとに正しい判断や適切な疑問を持てるようになったか。技術検証と受け手検証の両方がそろって、初めて「説明できた」と言えると思います。

Q9. AIの公平性(バイアス)の問題と、説明可能性はどう関係しますか?

説明可能性は、バイアスを発見するための最も実用的な入口です。画像データであれば、第5章のGrad-CAMのように判断根拠を可視化することで、本来注目すべきでない領域(背景など)を見て判断していないかを確認できます。テーブルデータであれば、SHAPで寄与を分解することで、性別や人種の代理変数となりうる属性(居住地域など)の寄与が不自然に大きくないかを点検できます。説明可能性は公平性そのものを保証はしませんが、公平性の監査は説明技術の上に成り立っている、という関係だと理解しています。

Q10. 小さく始めるなら、何から着手すればよいですか?

すでに運用中のモデルが1つでもあるなら、そのモデルにSHAP(テーブルデータの場合)を適用して、上位の重要特徴量と数件の個別予測の説明を眺めてみることです。半日もあれば試せて、モデルへの理解が一段深まりますし、想定外の依存が見つかることも少なくありません。そこで得た気づきを現場との会話材料にする。この小さな一歩が、説明可能性を組織に根づかせる最短距離だと感じています。

本コラムで紹介した書籍一覧

本ガイドの内容をさらに深めたい方への参考書籍をまとめました。学びたいテーマに合わせてお選びください。

  • 『機械学習を解釈する技術』(森下光之助、技術評論社):Permutation Importance・PDP・SHAPなど本ガイドの中核手法を、理論とPython実装の両面から丁寧に解説した定番書です
  • 『XAI(説明可能なAI)』(大坪直樹・中江俊博ほか、リックテレコム):XAIの背景から主要手法までを一望できる日本語の入門書。分野の全体像をつかむ最初の一冊に向いています
  • 『実践XAI[説明可能なAI]』(Pradeepta Mishra著、株式会社クイープ訳、インプレス):LIMEやSHAPをテーブル・時系列・自然言語・画像まで幅広いデータに適用するPythonコーディング集です
  • 『Interpretable Machine Learning』(Christoph Molnar、Web公開・英語):解釈手法の理論を網羅した世界標準のリファレンス。Webで無料で読み始められます(Web版はこちら
  • 『ゼロから作るDeep Learning』(斎藤康毅、オライリー・ジャパン):CNNをゼロから実装して深層学習の仕組みを体で理解できる定番書。第3部の可視化手法の土台になります
  • 『画像認識』(原田達也、講談社):画像認識の基礎から深層学習まで体系的に学べる教科書。第3部の背景知識を固めたい方に向いています
  • 『ベイズ推論による機械学習入門』(須山敦志、講談社):ベイズ推論の考え方を基礎から丁寧に積み上げる入門書。第4部の前提となる数理を学べます
  • 『ベイズ深層学習』(須山敦志、講談社):MC Dropoutやベイズニューラルネットワークの理論的背景を本格的に学べる一冊。第4部の数理を深めたい方に向いています
  • 『Pythonではじめるベイズ機械学習入門』(森賀新・木田悠歩・須山敦志、講談社):確率的プログラミングによる不確実性のモデリングを、実装を動かしながら学べる入門書です
  • 『AIガバナンス入門』(羽深宏樹、ハヤカワ新書):リスクベースの考え方から社会実装まで、AI規制とガバナンスの全体像を平易に整理した新書です。第1章の規制動向の背景が深まります
  • 『責任あるAI』(保科学世・鈴木博和、東洋経済新報社):AI倫理を企業の体制としてどう実装するかを、技術・ブランド・ガバナンス・人材の観点から解説します
  • 『大規模言語モデル入門』(山田育矢監修、技術評論社):transformersライブラリを使ってLLMの仕組みと実装を学べる標準的な教科書です。第9〜10章の技術的背景に対応します
  • 『大規模言語モデルは新たな知能か』(岡野原大輔、岩波書店):LLMの能力と限界を第一人者が平易に論じた読み物。技術者以外の方への推薦にも向く一冊です
  • 『LangChainとLangGraphによるRAG・AIエージェント[実践]入門』(西見公宏・吉田真吾・大嶋勇樹、技術評論社):第10章で紹介したRAGをPythonで実装するための実践書です

参考情報・出典