初心者のためのAI丸投げプログラミング

バイブコーディングで学ぶ深層学習

渡辺 英治(基礎生物学研究所)

イラスト

注意書き

本稿は、内容の鮮度を保つためにスピード命で書き上げました。ですので、比較的軽めの編集作業で公開しています。もし誤字脱字や不具合などありましたら、GitHubリポジトリのIssuesへ書き込んでください。よろしくお願いします。


はじめに

プログラミングを学ぶ時代は終わった

私は神経科学者として、長年、脳の視覚情報処理を研究してきました。研究の過程でPythonや深層学習を使うようになりましたが、正直に告白すると、プログラミングを体系的に学んだことはありません。必要に迫られて、その都度、必要なコードを書いてきただけです。

そんな私が今、確信を持って言えることがあります。

もう、プログラミングを「学ぶ」時代は終わりました。

2025年あたりから、AIとの対話でプログラミングコードを生成する「バイブコーディング」が急速に広まっています。自分でPythonコードを書くことなく、自然言語だけでアプリができていきます。すこし前の感覚からすると嘘みたいな世界ですが、ついに現実のものとなりました。私自身、この1年ほど、ほぼすべての研究プログラムをバイブコーディングで開発してきました。予測符号化理論に基づくニューラルネットワークモデル、たくさんの動画を処理する学習システム、論文用の図表作成ツール、いずれも、AIと対話しながら作り上げたものです。

従来であれば、Pythonの文法を学び、NumPyの使い方を覚え、PyTorchのチュートリアルをこなしと、深層学習を使えるようになるまでに膨大な時間が必要でした。その過程で挫折した人も多いでしょう。

しかし今は違います。「こういうことがしたい」とAIに伝えれば、動くコードが返ってきます。それを実行し、結果を確認し、「ここをこう変えて」と指示する。この繰り返しで、驚くほど高度なプログラムが完成します。

本書の対象読者

本書は、以下のような方を想定して書きました。

逆に、本書は「プログラミングそのものを深く学びたい」という方には向いていません。本書のゴールは、Pythonの文法を完璧に理解することではなく、深層学習を実際に動かして、自分の目的を達成することです。

なぜ深層学習なのか

バイブコーディングの入門書は、すでにいくつか出版されています。しかし、それらの多くはWebアプリや業務システムの開発を扱っており、深層学習に特化したものはありません。

深層学習は、画像認識、自然言語処理、生成AIなど、現代のAI技術の中核です。研究者にとっても、データ分析や実験結果の解析に欠かせないツールになっています。しかし、その敷居は依然として高いままでした。

本書は、バイブコーディングという新しいアプローチで、その敷居を大きく下げることを目指しています。

Google Colabという選択

本書では、すべての実習をGoogle Colab上で行います。

Google Colabは、ブラウザ上でPythonを実行できる無料のサービスです。面倒な環境構築は一切不要。Googleアカウントさえあれば、今日から始められます。さらに、無料でGPUを使用でき、深層学習の実行も可能です。

プログラミング入門で最も多い挫折ポイントは、実は環境構築です。「Pythonをインストールしたが動かない」「ライブラリのバージョンが合わない」。こうした問題で、本題に入る前に諦めてしまう人が少なくありません。

Google Colabなら、そうした心配は無用です。本書の内容は、コピー&ペーストですべてそのまま動きます。

本書の構成

本書は、以下のような流れで進みます。

第0章・第1章では、AIとの対話の基本と、Google Colabの使い方を学びます。プログラミング未経験者でも、ここから始められます。

第2章では、画像処理を題材に、Pythonの基本的な読み方を身につけます。コードを「書く」のではなく「読む」ことに重点を置きます。

第3章以降では、深層学習の世界に入ります。画像分類から始まり、動画処理、VAE(変分オートエンコーダ)による生成モデル、そしてViTやYOLOといった最新技術まで、段階的に進んでいきます。

後半では、GitHubで公開されているコードを改造する方法、大規模データの効率的な処理、デバッグの技法など、実践的なスキルを扱います。全部で10章構成です。

章と章の間には雑談とイラストを挿入して息抜きの場を設けました。

バイブコーディングの心得

最後に、バイブコーディングに取り組む際の心得をお伝えします。

完璧を目指さない。 AIが生成するコードは、必ずしも最適ではありません。しかし、「動く」ことが最も重要です。まず動かし、そこから改善していけばいい。

エラーを恐れない。 エラーが出たら、そのメッセージをAIに見せればいい。AIは優秀なデバッグパートナーです。

コードを読む力を養う。 書く必要はありませんが、読めることは重要です。AIが何をしているのか、大まかに理解できるようになりましょう。

目的を明確に。 「何がしたいか」が曖昧だと、AIも曖昧な答えを返します。具体的に、明確に伝えること。これがバイブコーディング成功の鍵です。

さあ、始めましょう。Google Colabを開いて、最初の一歩を踏み出す準備はできていますか?

第0章:AIと対話する準備

本書では、AIと対話しながらプログラムを作っていきます。この章では、まずAIとは何か、どう話しかければいいのかを理解し、最初の対話を体験します。

0.1 Claude/ChatGPTとは何か

大規模言語モデルという存在

本書で使うAIは、「大規模言語モデル(Large Language Model、LLM)」と呼ばれるものです。代表的なものに、AnthropicのClaude、OpenAIのChatGPT、GoogleのGeminiがあります。Geminiは著者があまり使っていないので本稿では横に置いておきます。

LLMとは、膨大な量のテキストデータから「言葉の使い方」を学習したAIです。人間が書いた文章、プログラムコード、学術論文、Webページ、ありとあらゆるテキストを読み込み、「この言葉の次には、こういう言葉が来やすい」というパターンを獲得しています。

その結果、私たちが自然な言葉で質問すれば、自然な言葉で答えてくれます。「Pythonで画像を読み込むコードを書いて」と頼めば、動くコードを生成してくれます。「このエラーメッセージの意味を教えて」と聞けば、原因と対処法を説明してくれます。

ClaudeとChatGPT

本書では主にClaudeを使って説明しますが、ChatGPTでも同様のことができます。

Claude(Anthropic社)

ChatGPT(OpenAI社)

どちらを使っても、本書の内容は実践できます。両方試してみて、自分に合う方を選んでください。私自身は、強力なコード生成と親切な説明においてClaudeの応答が好みなので、普段はClaudeを使っています。ChatGPTは主に画像生成に使っています。もし可能なら両方を並行して使うのが理想です。GoogleのGeminiも合わせて3LLM体制にしておくと、いざというときに役に立ちます。3LLMはそれぞれ個性がありますので、ひとつのLLMが煮詰まっても他のLLMが突破していくれることがよくあるのです。

無料版と有料版

ClaudeもChatGPTも、無料版と有料版があります。

無料版でも本書の内容は十分に実践できます。ただし、利用回数に制限があったり、最新のモデルが使えなかったりします。

有料版(Claude Pro、ChatGPT Plus)では、より高性能なモデルを、より多く使えます。深層学習のコード生成は複雑になりがちなので、本格的に使うなら有料版をお勧めします。

まずは無料版で始めて、必要を感じたら有料版に移行する。それで十分です。

AIは「賢いアシスタント」である

ここで、AIとの付き合い方について、重要なことをお伝えします。

AIは、何でも知っている万能の存在ではありません。間違えることもあります。古い情報を答えることもあります。自信満々に嘘をつくこともあります。特にチャットが長くなると、その傾向は顕著になります。

AIは、「とても賢いが、たまに間違えるアシスタント」だと思ってください。優秀な部下のようなものです。指示を出せば素早く仕事をしてくれますが、結果は必ず自分の目で確認する。おかしいと思ったら質問する。最終的な判断は自分でする。

この姿勢を忘れなければ、AIは強力な味方になります。

コードは誰が書いても間違える

ここで大事なことをもう一つ。

コードの間違いは、AIに限った話ではありません。人間が書いたコードも間違えます。ベテランのプログラマーでも、バグのないコードを一発で書くことは稀です。私も何度も何度も間違えてきました。

つまり、「コードは誰が書いても間違える」という前提に立つべきなのです。

では、どうすればいいか。答えは「テスト」です。

コードが正しく動くかどうかを確認するためのテストプログラムを書く。入力に対して期待通りの出力が得られるかを自動で検証する。これがプログラミングの基本的な作法です。

バイブコーディングでも、これは変わりません。むしろ、AIにコードを生成してもらう際には、テストプログラムも同時に書いてもらうことを強くお勧めします。

1から10までの数字を足し合わせる関数を書いて。動作確認用のテストコードも一緒にお願い。

こう頼めば、AIは関数本体とテストコードの両方を生成してくれます。テストを実行して、期待通りの結果が出れば、そのコードは(少なくともテストした範囲では)正しく動いていると確認できます。

「AIが書いたから大丈夫」ではなく、「テストで確認したから大丈夫」。この考え方を、最初から身につけておきましょう。

0.2 効果的な対話の基本

AIとの対話には、ちょっとしたコツがあります。同じことを聞いても、聞き方によって返ってくる答えの質が大きく変わります。

具体的に伝える

良くない例:

プログラムを書いて

良い例:

Pythonで、フォルダ内のすべてのJPG画像を読み込み、グレースケールに変換して、別のフォルダに保存するプログラムを書いてください

AIは、あなたの頭の中を読めません。何がしたいのか、具体的に伝えるほど、期待に沿った答えが返ってきます。

無論、初学者のかたは、最初から完璧な指示を出す必要はありません。まずはざっくり伝えて、結果を見てから「ここをこう変えて」と修正していく方法でも構いません。曖昧な最初の一言、AIの応答、それを受けてユーザーが修正、再応答、、、、そして完成、という流れになります。その流れのなかでで自然とPythonエンジニアリング風の具体的な指示を学んでいくことでしょう。

段階的に進める

複雑なことを一度にやろうとすると、AIも混乱します。

一度に頼みすぎる例:

画像を読み込んで、CNNで分類して、結果をグラフにして、PDFで保存するプログラムを書いて

段階的に進める例:

まず、画像を読み込んで表示するコードを書いて

(実行して確認)

次に、このコードにCNNで分類する機能を追加して

(実行して確認)

分類結果をグラフにしたい

一歩ずつ進めば、どの段階で問題が起きたかも分かりやすくなります。

文脈を共有する

AIは、あなたの状況を知りません。何を使っているのか、何が目的なのか、伝えると的確な答えが返ってきます。

文脈がない例:

エラーが出ました。直してください

文脈を伝える例:

Google ColabでPyTorchを使って画像分類モデルを学習しています。以下のエラーが出ました。原因と対処法を教えてください。

RuntimeError: CUDA out of memory…

自分の状況をAIに伝える。これだけで、回答の精度は格段に上がります。

説明を求める

AIが生成したコードは、そのまま使うだけでなく、説明を求めましょう。

このコードを1行ずつ説明して

model.eval() は何をしているの?

なぜここで with torch.no_grad(): を使うの?

説明を聞くことで、コードの理解が深まります。次に似たような問題に出会ったとき、自分で対処できるようになります。そしてこれがPythonの勉強になります。

代替案を求める

一つの方法を教えてもらったら、他の方法も聞いてみましょう。

他のやり方はある?

もっとシンプルに書ける?

この方法のメリットとデメリットは?

プログラミングには、たいてい複数の解法があります。AIに代替案を聞くことで、視野が広がります。

0.3 最初の対話体験

さあ、実際にAIと対話してみましょう。まだGoogle Colabは使いません。まずはAIとの会話に慣れることが目的です。

アカウントを作成する

ClaudeまたはChatGPTのアカウントを作成してください。

Claude [https://claude.ai] にアクセスし、メールアドレスまたはGoogleアカウントで登録します。

ChatGPT [https://chat.openai.com] にアクセスし、メールアドレスまたはGoogleアカウントで登録します。

どちらも、数分で完了します。

最初の質問をしてみる

アカウントができたら、何か質問してみましょう。まずは簡単なものから。

こんにちは。私はプログラミング初心者です。これから深層学習を学びたいと思っています。よろしくお願いします。

AIは丁寧に挨拶を返してくれるはずです。これだけで、最初の対話は成功です。

Pythonコードを生成してもらう

次に、簡単なPythonコードを生成してもらいましょう。

Pythonで「Hello, World!」と表示するコードを書いてください

AIは、以下のようなコードを返してくれるはずです。

print("Hello, World!")

たった1行ですが、これが「AIにコードを書いてもらう」という体験です。

もう少し複雑なコードを頼んでみる

次は、もう少し複雑なことを頼んでみましょう。

Pythonで、1から10までの数字を足し合わせて、結果を表示するコードを書いてください

AIは、こんなコードを返してくれるかもしれません。

total = 0
for i in range(1, 11):
    total += i
print(total)

今の段階では、このコードの意味が分からなくても構いません。大事なのは、「日本語で頼むと、Pythonコードが返ってくる」という体験です。

コードの説明を求める

生成されたコードの説明を求めてみましょう。

このコードを初心者にも分かるように説明してください

AIは、変数とは何か、for文とは何か、range関数は何をしているか、丁寧に説明してくれるはずです。

これが、本書で繰り返し行う「AIとの対話」の基本形です。

  1. やりたいことを伝える

  2. コードを受け取る

  3. 説明を求める

  4. 理解を深める

対話を続ける

さらに対話を続けてみましょう。

このコードを、もっと短く書く方法はありますか?

AIは、こんなコードを提案するかもしれません。

print(sum(range(1, 11)))

1行になりました。sum関数やrange関数の説明も聞いてみてください。

sum関数は何をしているの?

range(1, 11)はなぜ11なの?10じゃないの?

疑問に思ったことは、何でも聞いて構いません。AIは嫌な顔一つせず(顔はありませんが)、何度でも答えてくれます。これが新しいPythonの勉強方法です。

第0章のまとめ

この章では、以下のことを学びました。

次の章では、いよいよGoogle Colabを使い、AIと一緒にPythonプログラミングを始めます。


雑談:本稿と大規模言語モデルの秘密

雑談です。気楽に読んでください。

さて、本稿の著者である渡辺は基礎生物学研究所のAI解析室の室長としてAIの伝道に日々努力をしております。そんな渡辺が、今年(2026年)に入って、Claudeの様相が一変したと感じました。プログラムを書く力が半端なく高まったのです。私が開発を進めている脳のAIモデルは数千行もあるPythonコードですが、その改造をお願いしたら10分ほどで終わらせてしまいました。なんということでしょう。魔法の世界です。しかも邪魔くさすぎてときおり省いたりするテストコードも、こっちが言わなくても用意してくれます。人間がコードを書く時代は終わったとおもった瞬間です。そこでAI伝道師として本稿を書く決断をしました。とにかくまだ知らない人がいるのなら、すぐにすべての人に伝えねば!

ただし、現時点ではClaudeのプログラミング能力を引き出すためにはある程度のプログラミングの素養が必要です。だからといって今からプログラミングを真剣に学ぶのは時間がもったいないです。そこで逆転の発想です。プログラミング初心者がClaudeの力を使ってPythonやAIプログラミングを学ぶのです。これからのプログラミングは大規模言語モデル(LLM)の存在が大前提です。であるならば、最初からLLMを使ってPythonやAIプログラミングを学ぶのが最も効率的です。

そして、さらに逆転の発想です。本稿を可能な限りClaudeに書いてもらうのです。Claudeを使ってプログラミングをするのですから、バイブコーディングに最も精通しているClaudeが書くべきだと考えました。本稿の初稿は雑談以外Claudeが2時間で書き上げました。それが本書の秘密です。

とはいえ、皆さんにご覧いただく本が本当に2時間でできるような都合のいい話がありません。コードの多くは動作確認したりして、初稿から完成品までまるまる3日かかりました。Claudeはよく間違いを犯します。本書の初稿でもたくさん間違いがありました。間違いだけではなく、当初のプランを忘れて暴走したりしました。チャットが長くなると精度は極端に落ちます。しかし、私は「AIって嘘つきじゃん」「AIってすぐに忘れるじゃん」とは思いません。文章に責任を持つのは人間です。人間側がおかしいと判断すれば、すぐに修正できます。LLMと付き合うには、そのようなポジティブ思考が大事だとおもいます。

もうひとつ知っておきたいことがあります。それは、あなたがチャットの一部であることです。あなたの打ち込んだプロンプトは、Claudeが語った文章と一体化され、再びClaudeに読み込まれ、次の文章が生成されます。すなわち、Claudeが生成する次の文章はあなたの文章の出来次第なのです。嘘が現れる直前のあなたのプロンプトをよく確認してみてください。Claudeを無理に追い詰めていないでしょうか。Claudeが苦手なことを強要していないでしょうか。あなたもチャットの一部、Claudeはあなたの鏡、これがLLMの秘密です。

ちなみに雑談のパートは、Claudeの関与はゼロ。人間100%です。

イラスト

第1章:AIと始めるGoogle Colab

前章では、AIとの対話の基本を学びました。この章では、いよいよGoogle Colabを使って、実際にPythonコードを動かしていきます。もちろん、AIと一緒に。

1.1 AIと一緒にColabを始めよう

Google Colabとは

Google Colab(正式名称:Google Colaboratory)は、ブラウザ上でPythonを実行できる無料のサービスです。Googleが提供しています。

通常、Pythonを使うには、自分のパソコンにPythonをインストールし、開発環境を整え、必要なライブラリを入れて……と、始める前に多くの準備が必要です。この環境構築で挫折する人が、実はとても多い。

Google Colabなら、その心配は無用です。Googleアカウントさえあれば、今すぐ、このページを読み終わる前にでも、Pythonを動かし始められます。

さらに、Google ColabではGPU(画像処理用の高速なプロセッサ)を無料で使えます。深層学習の計算には大量の演算が必要ですが、GPUがあれば高速に処理できます。自分でGPU搭載のパソコンを買えば数十万円しますが、Google Colabなら無料です。Googleはエライ!

AIにColabの使い方を聞いてみる

では、さっそくAIに聞いてみましょう。Claudeを開いて、こう質問してください。

Google Colabの始め方を、初心者向けに教えてください

AIは、おそらく以下のような手順を教えてくれるはずです。

  1. Googleアカウントを用意する

  2. [https://colab.research.google.com] にアクセスする

  3. 「ノートブックを新規作成」をクリックする

  4. コードを入力して実行する

どうでしょう。AIに聞けば、必要な情報が手に入る。本書を読みながら、分からないことがあれば、すぐにAIに聞く。この習慣を身につけてください。

実際にColabを開いてみる

では、AIの説明に従って、Google Colabを開いてみましょう。

[https://colab.research.google.com] にアクセスしてください。Googleアカウントでログインしていない場合は、ログインを求められます。

ログインすると、「ノートブックを新規作成」または「新しいノートブック」というボタンが見つかるはずです(画面のデザインは時々変わります。見つからなければ、AIに「Google Colabで新しいノートブックを作る方法」と聞いてください)。

新しいノートブックを作成すると、画面に入力欄が現れます。これが「セル」と呼ばれるものです。ここにPythonコードを入力し、実行します。

最初のコードを実行する

セルに、以下のコードを入力してください。コピー&ペーストでもいいです。

print("Hello, Colab!")

入力したら、セルの左にある再生ボタン(▶)をクリックするか、Shift+Enterを押してください。

画面に「Hello, Colab!」と表示されましたか?

おめでとうございます。これが、あなたがGoogle Colabで実行した最初のPythonコードです。

セルの基本操作

Google Colabの操作で、最低限知っておくべきことをまとめます。

コードの実行

新しいセルの追加

セルの削除

セルの順序変更

これだけ覚えれば、当面は困りません。他の操作は、必要になったときにAIに聞けばいい。

GPUを有効にする

深層学習を実行するには、GPUを有効にしておくと便利です。今すぐ必要ではありませんが、設定方法を覚えておきましょう。

メニューから「ランタイム」→「ランタイムのタイプを変更」を選択します。「ハードウェアアクセラレータ」という項目があるので、「GPU」を選択して保存します。

これで、深層学習の計算がずっと速くなります。無料版では使用時間に制限がありますが、本書の内容を学ぶには十分です。

1.2 AIにPythonコードを書いてもらう

Colabの準備ができました。ここからが本番です。AIにPythonコードを書いてもらい、Colabで実行する。この流れを身につけましょう。

AIにコードを頼む

Claudeを開いて、こう頼んでみてください。

Pythonで、1から100までの数字の中から偶数だけを取り出して、その合計を計算するコードを書いてください

AIは、例えばこんなコードを返してくれます。

even_numbers = [x for x in range(1, 101) if x % 2 == 0]
total = sum(even_numbers)
print(f"偶数の合計: {total}")

Colabで実行する

このコードをコピーして、Google Colabのセルに貼り付けてください。そして実行。

「偶数の合計: 2550」と表示されましたか?

これが、バイブコーディングの基本的な流れです。

  1. AIにやりたいことを伝える

  2. コードを受け取る

  3. Colabにコピペする

  4. 実行する

拍子抜けするほど簡単でしょう? でも、これだけで、かなりのことができるのです。

コードが分からなくても大丈夫

先ほどのコード、意味が分からなくても問題ありません。[x for x in range(1, 101) if x % 2 == 0]なんて呪文にしか見えない、という人もいるでしょう。

そんなときは、AIに聞けばいい。

このコードの意味を、1行ずつ説明してください

AIは丁寧に説明してくれます。リスト内包表記とは何か、range関数の動き、%演算子(剰余)の意味、f文字列の使い方、聞けば、何でも教えてくれます。

分からないまま先に進んでも構いません。使っているうちに、自然と分かるようになります。ただ、「聞けば教えてもらえる」ということは覚えておいてください。

エラーが出たら

コードを実行して、エラーが出ることがあります。赤い文字でずらずらと表示されると、初心者は慌ててしまいます。

慌てなくていいのです。

エラーメッセージをそのままコピーして、AIに見せてください。

このエラーが出ました。原因と対処法を教えてください。

NameError: name ‘x’ is not defined

AIは、何が問題なのか、どう直せばいいのか、教えてくれます。

エラーは、失敗ではありません。プログラミングでは、エラーが出るのが当たり前です。ベテランでも、毎日エラーに遭遇します。大事なのは、エラーを恐れないこと、そしてエラーから学ぶことです。

AIがいれば、エラーは怖くありません。むしろ、エラーはAIに質問するチャンスです。

テストコードも一緒に頼む

第0章で学んだように、コードにはテストが大事です。AIにコードを頼むときは、テストも一緒に頼みましょう。

1から100までの偶数の合計を計算する関数を作ってください。正しく動くか確認するテストコードも一緒にお願いします。

AIは、関数本体とテストコードを両方生成してくれます。

def sum_even_numbers(start, end):
    """startからendまでの偶数の合計を返す"""
    return sum(x for x in range(start, end + 1) if x % 2 == 0)

# テストコード
def test_sum_even_numbers():
    # 1から10までの偶数は 2+4+6+8+10 = 30
    assert sum_even_numbers(1, 10) == 30, "1-10のテスト失敗"
    # 1から100までの偶数の合計は2550
    assert sum_even_numbers(1, 100) == 2550, "1-100のテスト失敗"
    # 2から2まで(偶数1つだけ)
    assert sum_even_numbers(2, 2) == 2, "2-2のテスト失敗"
    print("すべてのテストに合格しました!")

# テスト実行
test_sum_even_numbers()

実行して「すべてのテストに合格しました!」と表示されれば、この関数は(少なくともテストした範囲では)正しく動いています。

最初は面倒に感じるかもしれません。でも、この習慣が、後々あなたを助けてくれるのです。

1.3 AIと一緒に基本を学ぶ

ここまでで、AIにコードを書いてもらい、Colabで実行する流れは掴めたと思います。この節では、Pythonの基本的な要素を、AIとの対話を通じて学んでいきます。

「学ぶ」といっても、教科書的に文法を暗記する必要はありません。AIに質問しながら、「なんとなく分かる」状態を目指しましょう。

変数について聞いてみる

AIに、こう質問してみてください。

Pythonの変数について、初心者向けに説明してください。簡単な例も見せてください。

AIは、変数とは何か、どう使うのかを説明し、例を示してくれます。

説明を読んだら、示された例をColabで実行してみてください。動かしてみると、理解が深まります。

リストについて聞いてみる

次は、リストです。

Pythonのリストについて教えてください。どんなときに使うのか、具体例も見せてください。

リストは、複数のデータをまとめて扱うための仕組みです。画像処理や深層学習では頻繁に登場するので、なんとなくでも理解しておくと後で楽です。

繰り返し処理について聞いてみる

プログラミングでは、同じ処理を何度も繰り返すことがよくあります。

Pythonのfor文について、初心者向けに説明してください。

for文を使えば、「リストの要素を1つずつ取り出して処理する」「同じ処理を100回繰り返す」といったことが簡単にできます。

条件分岐について聞いてみる

「もし〇〇なら××する」という処理も、プログラミングの基本です。

Pythonのif文について教えてください。

if文を使えば、条件によって処理を変えられます。「もしこの値が10より大きければ、こうする」といった具合です。

関数について聞いてみる

関数は、一連の処理をまとめて名前をつけたものです。

Pythonの関数の作り方を教えてください。defの使い方を、簡単な例で説明してください。

関数を作れるようになると、コードの整理がしやすくなります。同じ処理を何度も書かなくて済むようになります。

学び方のコツ

ここで紹介した要素(変数、リスト、for文、if文、関数)は、Pythonの基本中の基本です。でも、今すぐ完璧に理解する必要はありません。

大事なのは、「分からなければAIに聞ける」ということを知っておくことです。

コードを読んでいて「このfor文は何をしているんだろう」と思ったら、その場でAIに聞く。「この関数の引数って何?」と思ったら、聞く。その繰り返しで、自然と理解が深まっていきます。

1.4 AIと作る最初のプログラム

さて、この章の締めくくりとして、少しまとまったプログラムをAIと一緒に作ってみましょう。

お題:気温データの分析

こんなお題を考えてみました。

「1週間分の最高気温データがあります。平均気温を計算し、最も暑かった日と最も寒かった日を見つけ、結果をグラフで表示したい」

これを、AIとの対話だけで実現してみます。

ステップ1:データを用意する

まず、AIにデータの準備を頼みます(テストデータと呼びます)。

Pythonで、1週間分の最高気温データ(月曜から日曜)をリストで作成するコードを書いてください。気温は適当な値で構いません。

AIは、こんなコードを返してくれるかもしれません。

# 1週間分の最高気温(℃)
days = ["月", "火", "水", "木", "金", "土", "日"]
temperatures = [22, 25, 28, 24, 30, 27, 23]
print("曜日:", days)
print("気温:", temperatures)

Colabで実行して、データが表示されることを確認します。

ステップ2:平均気温を計算する

次に、平均気温を計算する機能を追加してもらいます。

このデータの平均気温を計算するコードを追加してください

AIは、先ほどのコードに追加する形で、平均計算のコードを返してくれます。

# 平均気温を計算
average_temp = sum(temperatures) / len(temperatures)
print(f"平均気温: {average_temp:.1f}℃")

実行して、平均気温が表示されることを確認します。

ステップ3:最高・最低の日を見つける

さらに機能を追加します。

最も暑かった日と最も寒かった日を見つけて、何曜日だったか表示するコードを追加してください

AIは、最大値・最小値を見つけ、対応する曜日を取得するコードを生成してくれます。

# 最も暑かった日
max_temp = max(temperatures)
max_day = days[temperatures.index(max_temp)]
print(f"最も暑かった日: {max_day}曜日 ({max_temp}℃)")
# 最も寒かった日
min_temp = min(temperatures)
min_day = days[temperatures.index(min_temp)]
print(f"最も寒かった日: {min_day}曜日 ({min_temp}℃)")

ステップ4:グラフで表示する

最後に、グラフ表示を追加します。

この気温データを棒グラフで表示するコードを追加してください。Google Colabで動くようにしてください。

AIは、matplotlibというライブラリを使ったグラフ描画のコードを生成してくれます。

!pip install japanize-matplotlib -q
import japanize_matplotlib
import matplotlib.pyplot as plt

plt.figure(figsize=(10, 6))
plt.bar(days, temperatures, color='coral')
plt.axhline(y=average_temp, color='blue', linestyle='--', label=f'平均: {average_temp:.1f}°C')
plt.xlabel('曜日')
plt.ylabel('気温 (°C)')
plt.title('1週間の最高気温')
plt.legend()
plt.show()

実行すると、きれいな棒グラフが表示されます。平均気温の線も引かれています。

完成したプログラム

4つのステップを経て、プログラムが完成しました。最後に、AIにこう頼んでみましょう。

ここまでのコードを、1つのセルにまとめてください。コメントも付けてください。

AIは、すべてを整理した完成版を返してくれます。コメント付きで、何をしているか分かりやすくなっています。

振り返り

この節で行ったことを振り返りましょう。

  1. やりたいこと(気温データの分析)を決めた

  2. ステップに分けてAIに依頼した

  3. 各ステップで実行して確認した

  4. 最後に全体をまとめてもらった

これが、バイブコーディングの典型的な流れです。

複雑なことも、小さなステップに分ければ実現できます。そして、各ステップはAIが手伝ってくれます。

第1章のまとめ

この章では、以下のことを学びました。

次の章では、画像処理を題材に、さらに実践的なプログラミングに挑戦します。引き続き、AIと一緒に進めていきましょう。


雑談:人のしあわせ

山伏修行のひとつに「南蛮いぶし」と呼ばれる荒行があります。修行者を狭い部屋に閉じ込め、乾燥させた唐辛子をいぶした煙で部屋を充満させます。狭い部屋に充満する刺激臭と煙に耐え、過去の罪を懺悔し、心身を清めることを目的としているそうです。地獄のような苦痛が修行者を襲います。目から鼻から溢れ出る涙、息を吸うことすら難しい状況です。わざわざ自ら進んで苦しみにいくとは、なんということでしょう。人間という生き物の習性は計り知れません。修行者によっては、南蛮いぶしを何度も体験するそうです。

しかし、狭い部屋から生還した修行者は口を揃えて言います。「こうやって生きているだけでしあわせ」「普段気が付かなかったが、息を吸えるだけでしあわせ」「世界のすべてが輝いて見える」「心身が清められた」と。そうなんです。この修行は、超マイナスの世界を体験することで、ゼロだった世界を超プラスにもっていくことが目的なんです。

さて、前章ではGoogle Colabをご紹介しましたが、みなさんはこのしあわせの世界を感じていただけたでしょうか。よくわからなかった?では、ご自分でPythonプログラム、AIプログラムの実行環境を一度ご自分でセットアップするというのはいかがでしょうか?きっと、その苦行の果てに、Google Colabはとてもしあわせな世界なんだと実感することでしょう。

イラスト

第2章:AIと学ぶ画像処理

前章では、AIと一緒にGoogle Colabを使い、Pythonの基本を学びました。この章では、画像処理を題材に、さらに実践的なプログラミングに挑戦します。

画像処理を選んだ理由は、結果が目に見えるからです。コードを実行すると、画像が表示される。変換すると、見た目が変わる。この「目に見える」という特性が、学習のモチベーションを高めてくれます。

そして何より、画像処理は深層学習への入り口です。第3章以降で扱う画像分類や物体検出は、すべて画像データを扱います。この章で画像の基本を理解しておくと、後がずっと楽になります。

2.1 AIに画像処理を教わる

画像を読み込んで表示する

さっそく始めましょう。Claudeを開いて、こう頼んでみてください。

Google Colabで、画像ファイルを読み込んで表示するPythonコードを書いてください。サンプル画像はインターネットからダウンロードしてください。

AIは、例えばこんなコードを返してくれます。

import urllib.request
!pip install japanize-matplotlib -q
import japanize_matplotlib
import matplotlib.pyplot as plt
from PIL import Image

# サンプル画像をダウンロード(公開APIから)
url = "https://picsum.photos/seed/cat/320/240"
urllib.request.urlretrieve(url, "sample.png")

# 画像を読み込んで表示
img = Image.open("sample.png")
plt.imshow(img)
plt.axis('off')  # 軸を非表示
plt.show()
print(f"画像サイズ: {img.size}")
print(f"画像モード: {img.mode}")

Colabで実行してみてください。画像が表示されましたか?

これだけで、あなたは「画像をプログラムで読み込んで表示する」という処理を実行できました。

自分の画像を使う

サンプル画像ではなく、自分の画像を使いたいこともあるでしょう。AIに聞いてみます。

Google Colabで、自分のパソコンから画像をアップロードして表示する方法を教えてください

AIは、ファイルアップロードの方法を教えてくれます。

from google.colab import files
from PIL import Image
!pip install japanize-matplotlib -q
import japanize_matplotlib
import matplotlib.pyplot as plt
# ファイルをアップロード
uploaded = files.upload()
# アップロードしたファイル名を取得
filename = list(uploaded.keys())[0]
# 画像を読み込んで表示
img = Image.open(filename)
plt.imshow(img)
plt.axis('off')
plt.show()

実行すると、「ファイルを選択」ボタンが表示されます。クリックして、自分のパソコンから好きな画像を選んでください。アップロードが完了すると、その画像が表示されます。

画像をグレースケールに変換する

次に、画像を加工してみましょう。

この画像をグレースケール(白黒)に変換するコードを追加してください

AIは、変換コードを返してくれます。

# グレースケールに変換
gray_img = img.convert('L')
# 元画像と変換後を並べて表示
fig, axes = plt.subplots(1, 2, figsize=(12, 5))
axes[0].imshow(img)
axes[0].set_title('元画像')
axes[0].axis('off')
axes[1].imshow(gray_img, cmap='gray')
axes[1].set_title('グレースケール')
axes[1].axis('off')
plt.show()

元の画像とグレースケール画像が並んで表示されます。たった1行(img.convert(‘L’))で、カラー画像が白黒になりました。

画像のサイズを変更する

画像のリサイズも、よく使う処理です。

この画像を幅200ピクセルにリサイズするコードを書いてください。縦横比は維持してください。

# 幅200ピクセルにリサイズ(縦横比維持)
width = 200
ratio = width / img.size[0]
height = int(img.size[1] * ratio)
resized_img = img.resize((width, height))
print(f"元のサイズ: {img.size}")
print(f"リサイズ後: {resized_img.size}")
plt.imshow(resized_img)
plt.axis('off')
plt.show()

対話を重ねて学ぶ

ここまでで、画像の読み込み、表示、グレースケール変換、リサイズを行いました。すべて、AIに「〇〇したい」と伝えるだけで実現できました。

もっと他のこともやってみたければ、AIに聞いてみてください。

画像を回転させるには?

画像の明るさを変えるには?

画像の一部だけを切り抜くには?

画像にぼかしをかけるには?

何でも聞けば、コードが返ってきます。それをColabで実行して、結果を確認する。この繰り返しで、画像処理の引き出しが増えていきます。

2.2 AIにコードを説明してもらう

画像処理のコードが動くようになりました。でも、「動く」と「分かる」は別のことです。この節では、コードの中身を理解することに焦点を当てます。

「画像は数値の配列である」という事実

画像処理を理解する上で、最も重要な概念があります。

画像は、数値の配列である。

これを、AIに説明してもらいましょう。

「画像は数値の配列である」とはどういう意味ですか?初心者にも分かるように、具体例を使って説明してください。

AIは、こんな説明をしてくれるかもしれません。

「デジタル画像は、小さな点(ピクセル)の集まりでできています。各ピクセルには色を表す数値が割り当てられています。グレースケール画像なら0(黒)から255(白)の1つの数値、カラー画像なら赤・緑・青(RGB)それぞれに0〜255の数値が3つ。つまり、画像全体は数値が縦横に並んだ表(配列)として表現されているのです。」

言葉だけだと分かりにくいので、実際に見てみましょう。

import numpy as np
from PIL import Image
!pip install japanize-matplotlib -q
import japanize_matplotlib
import matplotlib.pyplot as plt

# 小さな画像を作成(5x5ピクセル、グレースケール)
small_array = np.array([
    [0, 50, 100, 150, 200],
    [50, 100, 150, 200, 250],
    [100, 150, 200, 250, 200],
    [150, 200, 250, 200, 150],
    [200, 250, 200, 150, 100]
], dtype=np.uint8)

# 配列を画像として表示
fig, axes = plt.subplots(1, 2, figsize=(10, 4))

# 左:数値を表示
axes[0].imshow(small_array, cmap='gray', vmin=0, vmax=255)
for i in range(5):
    for j in range(5):
        axes[0].text(j, i, str(small_array[i, j]),
                     ha='center', va='center', color='red', fontsize=10)
axes[0].set_title('数値と画像の対応')
axes[0].axis('off')

# 右:拡大した画像
axes[1].imshow(small_array, cmap='gray', vmin=0, vmax=255)
axes[1].set_title('5×5ピクセルの画像')
axes[1].axis('off')
plt.show()

実行すると、5×5の小さな画像が表示されます。各ピクセルに数値が書き込まれているので、「0に近いほど黒、255に近いほど白」ということが視覚的に分かります。

実際の画像の中身を見る

サンプル画像の中身も見てみましょう。

画像をNumPy配列に変換して、その形状と数値の範囲を表示するコードを書いてください

import numpy as np
from PIL import Image
# 画像を読み込み
img = Image.open("sample.png")
# NumPy配列に変換
img_array = np.array(img)
print(f"配列の形状: {img_array.shape}")
print(f"データ型: {img_array.dtype}")
print(f"最小値: {img_array.min()}")
print(f"最大値: {img_array.max()}")
# 左上の10x10ピクセルの値を表示
print("\n左上10×10ピクセルの値(赤チャンネル):")
print(img_array[:10, :10, 0])

配列の形状: (280, 280, 4) のような出力が得られます。これは「縦280ピクセル × 横280ピクセル × 4チャンネル(RGBA)」という意味です。

この「画像=数値の配列」という理解は、深層学習を学ぶ上で非常に重要です。ニューラルネットワークは、この数値の配列を入力として受け取り、処理するのです。

コードを1行ずつ理解する

先ほどのグレースケール変換のコードをもう一度見てみましょう。

gray_img = img.convert('L')

たった1行ですが、中で何が起きているのでしょうか。AIに聞いてみます。

img.convert(‘L’) は内部で何をしているのですか?どういう計算でカラーからグレースケールに変換しているのですか?

AIは、詳しい説明をしてくれます。

「グレースケール変換は、RGB各チャンネルの加重平均を取っています。一般的な計算式は Gray = 0.299×R + 0.587×G + 0.114×B です。人間の目は緑に敏感なので、緑の重みが最も大きくなっています。」

これを実際に確認してみましょう。

import numpy as np
from PIL import Image
# カラー画像を読み込み
img = Image.open("sample.png").convert('RGB')
img_array = np.array(img)
# 手動でグレースケール計算
r = img_array[:, :, 0]
g = img_array[:, :, 1]
b = img_array[:, :, 2]
gray_manual = 0.299 * r + 0.587 * g + 0.114 * b
gray_manual = gray_manual.astype(np.uint8)
# PILの変換結果と比較
gray_pil = np.array(img.convert('L'))
# 差分を確認
diff = np.abs(gray_manual.astype(int) - gray_pil.astype(int))
print(f"最大差分: {diff.max()}")
print(f"平均差分: {diff.mean():.4f}")

内部で行われている計算を、自分で再現できました。このように、「なぜ動くのか」を理解することで、応用が効くようになります。

分からないことは何でも聞く

コードを読んでいて、分からない部分があったら、遠慮なくAIに聞いてください。

dtype=np.uint8 の意味は?

img_array[:10, :10, 0] の [:10, :10, 0] は何をしている?

astype(np.uint8) はなぜ必要?

一見些細な疑問でも、聞けば丁寧に教えてくれます。「こんな基本的なことを聞いていいのかな」という遠慮は不要です。AIは何度聞かれても嫌な顔をしません(繰り返しますが、顔はありません)。

2.3 AIと一緒にエラーを解決する

プログラミングにエラーはつきものです。この節では、画像処理でよく遭遇するエラーを題材に、AIと一緒にエラーを解決する方法を学びます。

エラーを恐れない

まず、わざとエラーを起こしてみましょう。

from PIL import Image
# 存在しないファイルを開こうとする
img = Image.open("neuronist.jpg")

実行すると、こんなエラーが出ます。

FileNotFoundError: [Errno 2] No such file or directory: ‘neuronist.jpg’

赤い文字でエラーメッセージが表示されると、最初は驚くかもしれません。でも、これは「失敗」ではありません。プログラムが「何がおかしいか」を教えてくれているのです。

AIにエラーを見せる

このエラーメッセージを、そのままAIに見せましょう。

このエラーが出ました。原因と対処法を教えてください。

FileNotFoundError: [Errno 2] No such file or directory: ‘neuronist.jpg’

AIは、こう説明してくれるはずです。

「このエラーは、指定したファイルが見つからないことを示しています。’neuronist.jpg’というファイルが、現在の作業ディレクトリに存在しないか、ファイル名が間違っています。対処法は:1)ファイル名を確認する、2)ファイルが正しい場所にあるか確認する、3)ファイルをアップロードする、などです。」

エラーの意味と対処法が分かりました。これが、AIと一緒にエラーを解決する基本的な流れです。

よくあるエラーと対処法

画像処理でよく遭遇するエラーをいくつか見てみましょう。

形状の不一致

import numpy as np
a = np.array([[1, 2], [3, 4]]) # 2x2
b = np.array([[1, 2, 3], [4, 5, 6]]) # 2x3
c = a + b # エラー!

ValueError: operands could not be broadcast together with shapes (2,2) (2,3)

このエラーをAIに見せると、「配列の形状が一致しないため、演算ができない」と教えてくれます。

型の問題

from PIL import Image
import numpy as np

img = Image.open("sample.png")
img_array = np.array(img)

# 型を考えずに計算(オーバーフローのチェック)
if img_array.dtype == np.uint8:
    raise ValueError("uint8型のまま加算するとオーバーフローします!astype(np.int16)で型変換が必要です")

bright = img_array + 100

AIに「画像の明るさを100増やしたいのですが、正しい方法を教えてください」と聞くと、型変換やクリッピングの必要性を説明してくれます。

# 正しい方法
img_array = np.array(img).astype(np.int16)  # 一時的に範囲を広げる
bright = img_array + 100
bright = np.clip(bright, 0, 255).astype(np.uint8)  # 範囲を制限して戻す
print("完成!", bright.shape)

メモリ不足

大きな画像を大量に処理しようとすると、メモリ不足になることがあります。

MemoryError: Unable to allocate …

このエラーが出たら、AIに相談しましょう。「画像を1枚ずつ処理する」「画像サイズを小さくする」「不要な変数を削除する」など、対処法を教えてくれます。

エラーメッセージを読む習慣

AIに聞く前に、エラーメッセージを自分で読む習慣をつけると、さらに成長が早くなります。

エラーメッセージには、たいてい以下の情報が含まれています。

  1. エラーの種類(FileNotFoundError、ValueError、TypeErrorなど)

  2. エラーの内容(何が問題か)

  3. 発生場所(どの行で起きたか)

最初は読み方が分からなくても、AIに「このエラーメッセージの読み方を教えてください」と聞けば、解説してくれます。

何度かエラーを経験するうちに、「あ、これはファイルが見つからないエラーだな」「これは型の問題だな」と、自分で判断できるようになってきます。

2.4 AIと作る画像処理ツール

この章の締めくくりとして、実用的な画像処理ツールをAIと一緒に作りましょう。

お題:複数画像の一括処理ツール

こんなツールを作ります。

「フォルダ内のすべての画像を読み込み、指定したサイズにリサイズして、グレースケールに変換し、別のフォルダに保存する」

研究でデータを前処理するとき、こういった一括処理が必要になることがよくあります。

ステップ1:処理の流れを設計する

まず、AIに全体の流れを相談します。

Google Colabで、複数の画像ファイルを一括で処理するツールを作りたいです。処理内容は「リサイズ」と「グレースケール変換」です。どういう流れで作ればいいですか?

AIは、処理の流れを提案してくれます。

  1. 画像ファイルをアップロード(または用意)

  2. 出力フォルダを作成

  3. 各画像に対して:リサイズ → グレースケール変換 → 保存

  4. 処理結果を表示

この流れに沿って、ステップごとにコードを作っていきます。

ステップ2:サンプル画像を用意する

まず、処理対象の画像を用意します。

Google Colabで、サンプル画像を5枚ダウンロードして、’input_images’フォルダに保存するコードを書いてください

import os
import urllib.request

# 入力フォルダを作成
os.makedirs('input_images', exist_ok=True)

# サンプル画像のURL(著作権フリーの画像)
urls = [
    "https://picsum.photos/id/10/800/600",
    "https://picsum.photos/id/20/800/600",
    "https://picsum.photos/id/30/800/600",
    "https://picsum.photos/id/40/800/600",
    "https://picsum.photos/id/50/800/600",
]

# ダウンロード
for i, url in enumerate(urls):
    filename = f'input_images/image_{i+1}.jpg'
    urllib.request.urlretrieve(url, filename)
    print(f"ダウンロード完了: {filename}")

print(f"\n合計 {len(urls)} 枚の画像を保存しました")

ステップ3:1枚の画像を処理する関数を作る

次に、1枚の画像を処理する関数を作ります。

1枚の画像を受け取り、指定サイズにリサイズしてグレースケールに変換し、指定パスに保存する関数を作ってください。テストコードも一緒にお願いします。

from PIL import Image
import os

def process_image(input_path, output_path, target_size=(256, 256)):
    """
    画像を読み込み、リサイズしてグレースケールに変換し、保存する

    Args:
        input_path: 入力画像のパス
        output_path: 出力画像のパス
        target_size: リサイズ後のサイズ (幅, 高さ)

    Returns:
        成功したらTrue、失敗したらFalse
    """
    try:
        img = Image.open(input_path)
        img_resized = img.resize(target_size)
        img_gray = img_resized.convert('L')
        img_gray.save(output_path)
        return True
    except Exception as e:
        print(f"エラー: {input_path} - {e}")
        return False

# テスト
def test_process_image():
    test_input = 'input_images/image_1.jpg'
    test_output = 'test_output.jpg'
    if os.path.exists(test_input):
        result = process_image(test_input, test_output, target_size=(128, 128))
        assert result == True, "処理が失敗しました"
        assert os.path.exists(test_output), "出力ファイルがありません"
        output_img = Image.open(test_output)
        assert output_img.size == (128, 128), f"サイズが違います: {output_img.size}"
        assert output_img.mode == 'L', f"モードが違います: {output_img.mode}"
        os.remove(test_output)
        print("すべてのテストに合格しました!")
    else:
        print("テスト画像がありません。先にステップ2を実行してください。")

test_process_image()

テストが通れば、この関数は正しく動いています。

ステップ4:複数画像を一括処理する

いよいよ、複数画像の一括処理です。

input_imagesフォルダ内のすべての画像を処理して、output_imagesフォルダに保存するコードを書いてください。処理の進捗も表示してください。

import os
from PIL import Image
import glob

def batch_process_images(input_folder, output_folder, target_size=(256, 256)):
    """
    フォルダ内のすべての画像を一括処理する
    """
    os.makedirs(output_folder, exist_ok=True)

    extensions = ['*.jpg', '*.jpeg', '*.png', '*.gif', '*.bmp']
    image_files = []
    for ext in extensions:
        image_files.extend(glob.glob(os.path.join(input_folder, ext)))

    if not image_files:
        print("処理対象の画像が見つかりません")
        return

    print(f"処理対象: {len(image_files)} 枚の画像\n")

    success_count = 0
    for i, input_path in enumerate(image_files):
        filename = os.path.basename(input_path)
        output_path = os.path.join(output_folder, filename)
        if process_image(input_path, output_path, target_size):
            success_count += 1
            print(f"[{i+1}/{len(image_files)}] 完了: {filename}")
        else:
            print(f"[{i+1}/{len(image_files)}] 失敗: {filename}")

    print(f"\n処理完了: {success_count}/{len(image_files)} 枚成功")

# 実行
batch_process_images('input_images', 'output_images', target_size=(256, 256))

ステップ5:結果を確認する

処理結果を視覚的に確認しましょう。

input_imagesとoutput_imagesの画像を並べて比較表示するコードを書いてください

!pip install japanize-matplotlib -q
import japanize_matplotlib
import matplotlib.pyplot as plt
from PIL import Image
import os
import glob

def compare_results(input_folder, output_folder, num_samples=5):
    """処理前後の画像を比較表示する"""
    input_files = sorted(glob.glob(os.path.join(input_folder, '*.jpg')))[:num_samples]
    fig, axes = plt.subplots(num_samples, 2, figsize=(10, 4*num_samples))

    for i, input_path in enumerate(input_files):
        filename = os.path.basename(input_path)
        output_path = os.path.join(output_folder, filename)

        # 入力画像
        input_img = Image.open(input_path)
        axes[i, 0].imshow(input_img)
        axes[i, 0].set_title(f'入力: {input_img.size}')
        axes[i, 0].axis('off')

        # 出力画像
        if os.path.exists(output_path):
            output_img = Image.open(output_path)
            axes[i, 1].imshow(output_img, cmap='gray')
            axes[i, 1].set_title(f'出力: {output_img.size}')
        axes[i, 1].axis('off')

    plt.tight_layout()
    plt.show()

compare_results('input_images', 'output_images', num_samples=5)

元画像とリサイズ&グレースケール化された画像が並んで表示されます。

ステップ6:ツールを完成させる

最後に、AIにコード全体を整理してもらいます。

ここまでのコードを、1つのまとまったツールとして整理してください。設定(サイズなど)を最初に変更できるようにして、コメントも付けてください。

AIは、すべてを整理した完成版を返してくれます。これで、再利用可能な画像処理ツールの完成です。

このツールを応用する

完成したツールは、いろいろな用途に応用できます。

何か追加したい機能があれば、AIに相談してください。「〇〇の機能を追加したい」と伝えれば、コードを生成してくれます。

第2章のまとめ

この章では、以下のことを学びました。

次の章では、いよいよ深層学習の世界に入ります。この章で学んだ「画像=数値の配列」という理解が、そこで活きてきます。

引き続き、AIと一緒に進めていきましょう。


雑談:百聞は一見にしかず

人間は自分が見たものに自信をもっています。百聞は一見にしかずです。視覚は他の感覚に比べて、有利な点が非常に多いのは事実です。動いているもの、止まっているもの関係なく位置の特定が容易で、しかも見ているものが何であるかの特定も容易です。

視覚系は進化の過程ではカンブリア紀に獲得されたそうです。想像してください。ほとんどの生き物が目を持たない状況で、一部の生き物が目を獲得したとすると何が起こるでしょうか?目を持たない生き物にとっては地獄。目を持つ生き物にとっては天国です。そこから食うものと食われるものの進化の大爆発が起こったのも頷けます。

人間の脳はかなりの部分を視覚系の情報処理に使っています。威力もすごいのですが、取り扱いもたいへんなのです。LLMの実力からすると意外なことですが、実はAIは視覚系の情報処理があまり得意ではありません。データがふんだんに揃っている医療データだったり、車載カメラデータだったりの視覚情報処理は、人間を超えるほどのレベルに達していますが、全く学習したこともない画像に対しては難しい状況です。

それでも最近のLLMの能力はすさまじく、すでに「目」のような機能を持ち始めています。写真を読み込ませれば、どこに何が写っているか、何個くらいあるかを推定します。さらにはプロンプトでうまく誘導すれば、それがあたかも「学習」のような作用をして精度をあげることも可能です。今後、言語と一体化した目は大いに発展していくことは間違いないでしょう。

イラスト

第3章:AIと作る最初の深層学習モデル

ここまで、AIとの対話の方法、Google Colabの使い方、画像処理の基本を学んできました。いよいよ、深層学習の世界に足を踏み入れます。

「深層学習」という言葉に、身構える必要はありません。この章では、AIと対話しながら、最初のモデルを動かし、その仕組みを少しずつ理解していきます。難しい数式を暗記する必要はありません。大事なのは、「動かしてみて、結果を見て、AIに聞いて、理解を深める」というサイクルです。

3.1 「画像分類モデルを作りたい」と伝える

深層学習で何ができるのか

まず、AIに聞いてみましょう。

深層学習で何ができるのですか?初心者向けに、具体例を挙げて教えてください。

AIは、様々な応用例を教えてくれるはずです。画像認識、音声認識、自然言語処理、ゲームAI、自動運転……。深層学習は、現代のAI技術の中核を担っています。

この章では、その中でも最も基本的な「画像分類」に挑戦します。画像を入力として受け取り、「これは何の画像か」を判定するタスクです。

MNISTデータセット

画像分類の入門として、最もよく使われるのが「MNIST」というデータセットです。

MNISTデータセットについて教えてください。

AIは、こう説明してくれるでしょう。

「MNISTは、手書き数字(0〜9)の画像データセットです。28×28ピクセルのグレースケール画像が、訓練用に60,000枚、テスト用に10,000枚含まれています。機械学習の『Hello, World!』として広く使われています。」

28×28ピクセルという小さな画像に、0から9までの数字が手書きされています。これを見て、「これは3だ」「これは7だ」と判定するモデルを作るのです。

AIに最初のモデルを作ってもらう

では、さっそくAIにモデルを作ってもらいましょう。

Google ColabでPyTorchを使って、MNISTの手書き数字を認識する深層学習モデルを作ってください。シンプルなもので構いません。

AIは、こんなコードを返してくれるかもしれません。

import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
!pip install japanize-matplotlib -q
import japanize_matplotlib
import matplotlib.pyplot as plt

# デバイスの設定(GPUがあれば使用)
device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"使用デバイス: {device}")

# データの前処理
transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize((0.1307,), (0.3081,))
])

# MNISTデータセットのダウンロード
train_dataset = datasets.MNIST('./data', train=True, download=True, transform=transform)
test_dataset = datasets.MNIST('./data', train=False, transform=transform)

# データローダーの作成
train_loader = DataLoader(train_dataset, batch_size=64, shuffle=True)
test_loader = DataLoader(test_dataset, batch_size=1000, shuffle=False)

# シンプルなニューラルネットワークの定義
class SimpleNet(nn.Module):
    def __init__(self):
        super(SimpleNet, self).__init__()
        self.flatten = nn.Flatten()
        self.fc1 = nn.Linear(28*28, 128)
        self.relu = nn.ReLU()
        self.fc2 = nn.Linear(128, 10)

    def forward(self, x):
        x = self.flatten(x)
        x = self.fc1(x)
        x = self.relu(x)
        x = self.fc2(x)
        return x

# モデルの作成
model = SimpleNet().to(device)
print(model)

Colabで実行してみてください。MNISTデータセットがダウンロードされ、モデルの構造が表示されます。

データを見てみる

モデルを学習させる前に、データがどんなものか見ておきましょう。

MNISTデータセットからいくつかサンプル画像を表示するコードを書いてください

# サンプル画像を表示
fig, axes = plt.subplots(2, 5, figsize=(12, 5))
for i, ax in enumerate(axes.flat):
    image, label = train_dataset[i]
    ax.imshow(image.squeeze(), cmap='gray')
    ax.set_title(f'ラベル: {label}')
    ax.axis('off')
plt.tight_layout()
plt.show()

手書きの数字が表示されます。人間が見れば、何の数字か分かりますよね。これを、コンピュータにも分かるように学習させるのです。

3.2 ニューラルネットワークの仕組みをAIに聞く

「このモデルは何をしているの?」

先ほど定義したSimpleNetというモデル、中身がよく分からないと思います。AIに聞いてみましょう。

このSimpleNetというモデルは何をしているのですか?初心者にも分かるように説明してください。

AIは、丁寧に説明してくれます。

「SimpleNetは、画像を受け取って、0〜9のどの数字かを判定するモデルです。処理の流れは次の通りです:

  1. Flatten:28×28の2次元画像を、784個の数値が並んだ1次元に変換

  2. fc1(全結合層):784個の入力を128個の値に変換

  3. ReLU(活性化関数):負の値を0にする

  4. fc2(全結合層):128個の値を10個(0〜9の各クラス)に変換

最終的に出力される10個の値が、各数字である確率のようなものを表しています。」

ニューラルネットワークの構造を図で理解する

言葉だけだと分かりにくいので、AIにもう少し詳しく聞いてみます。

ニューラルネットワークの「入力層」「隠れ層」「出力層」について、具体的な数値を使って説明してください。

AIは、こう説明してくれるでしょう。

「このモデルの場合:

入力層の784個の値は、画像の各ピクセルの明るさです。これが隠れ層で処理され、最終的に10個の値になります。その10個の中で最も大きい値のインデックスが、予測される数字です。」

この「入力→隠れ層→出力」という流れが、ニューラルネットワークの基本構造です。

技術解説:ニューラルネットワークの数理的基礎

ここで、少し技術的な話をします。理解できなくても先に進めますが、知っておくと後で役立ちます。

ニューラルネットワークの各層では、次のような計算が行われています。

出力 = 活性化関数(重み × 入力 + バイアス)

「重み」と「バイアス」は、学習によって調整されるパラメータです。最初はランダムな値ですが、学習を繰り返すことで、正しい予測ができるような値に近づいていきます。

具体的に、隠れ層の計算を見てみましょう。

# 隠れ層の計算を手動で確認
sample_image, label = train_dataset[0]
sample_image = sample_image.view(-1) # 784個の1次元に変換
print(f"入力のサイズ: {sample_image.shape}") # torch.Size([784])
# fc1層の重みとバイアス
print(f"fc1の重みのサイズ: {model.fc1.weight.shape}") # torch.Size([128, 784])
print(f"fc1のバイアスのサイズ: {model.fc1.bias.shape}") # torch.Size([128])
# 計算: 出力 = 入力 × 重み^T + バイアス
# これが128個の値を生成

784個の入力と128×784個の重みを掛け合わせ、128個のバイアスを足すことで、128個の出力が得られます。この128個がさらに次の層に渡されます。

今は「そういうものか」という理解で十分です。使っているうちに、自然と身についてきます。

3.3 学習の過程を理解する

「学習」とは何か

モデルの構造は分かりました。では、「学習」とは何をしているのでしょうか。AIに聞いてみます。

ニューラルネットワークの「学習」とは何をしているのですか?

AIは、こう説明してくれるでしょう。

「学習とは、モデルの予測を正解に近づけるために、重みとバイアスを調整することです。流れは次の通りです:

  1. 画像をモデルに入力し、予測を得る

  2. 予測と正解の『ズレ』を計算する(これを損失という)

  3. 損失を小さくするように、重みを少し調整する

  4. これを何度も繰り返す

繰り返すうちに、モデルの予測精度が上がっていきます。」

実際に学習させてみる

では、実際にモデルを学習させてみましょう。

このモデルを学習させるコードを書いてください。学習の進捗が分かるように、各エポックの損失と精度を表示してください。

# 損失関数と最適化手法の定義
criterion = nn.CrossEntropyLoss()
optimizer = optim.Adam(model.parameters(), lr=0.001)

# 学習ループ
def train(model, train_loader, criterion, optimizer, device):
    model.train()
    total_loss = 0
    correct = 0
    total = 0
    for images, labels in train_loader:
        images, labels = images.to(device), labels.to(device)
        optimizer.zero_grad()
        outputs = model(images)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()
        total_loss += loss.item()
        _, predicted = outputs.max(1)
        total += labels.size(0)
        correct += predicted.eq(labels).sum().item()
    return total_loss / len(train_loader), 100. * correct / total

# テスト
def test(model, test_loader, criterion, device):
    model.eval()
    total_loss = 0
    correct = 0
    total = 0
    with torch.no_grad():
        for images, labels in test_loader:
            images, labels = images.to(device), labels.to(device)
            outputs = model(images)
            loss = criterion(outputs, labels)
            total_loss += loss.item()
            _, predicted = outputs.max(1)
            total += labels.size(0)
            correct += predicted.eq(labels).sum().item()
    return total_loss / len(test_loader), 100. * correct / total

# 学習実行
epochs = 5
train_losses, test_losses = [], []
train_accs, test_accs = [], []

for epoch in range(epochs):
    train_loss, train_acc = train(model, train_loader, criterion, optimizer, device)
    test_loss, test_acc = test(model, test_loader, criterion, device)
    train_losses.append(train_loss)
    test_losses.append(test_loss)
    train_accs.append(train_acc)
    test_accs.append(test_acc)
    print(f"Epoch {epoch+1}/{epochs}: Train Loss={train_loss:.4f}, Train Acc={train_acc:.1f}%, Test Acc={test_acc:.1f}%")
    print(f"エポック {epoch+1}/{epochs}")
    print(f" 訓練 - 損失: {train_loss:.4f}, 精度: {train_acc:.2f}%")
    print(f" テスト - 損失: {test_loss:.4f}, 精度: {test_acc:.2f}%")

実行すると、各エポックごとに損失と精度が表示されます。5エポック後には、テスト精度が97%以上になっているはずです。

たった5回の学習で、手書き数字の97%以上を正しく認識できるモデルができました。

学習曲線を見る

学習の過程をグラフで見てみましょう。

fig, axes = plt.subplots(1, 2, figsize=(12, 4))
# 損失の推移
axes[0].plot(train_losses, label='訓練')
axes[0].plot(test_losses, label='テスト')
axes[0].set_xlabel('エポック')
axes[0].set_ylabel('損失')
axes[0].set_title('損失の推移')
axes[0].legend()
# 精度の推移
axes[1].plot(train_accs, label='訓練')
axes[1].plot(test_accs, label='テスト')
axes[1].set_xlabel('エポック')
axes[1].set_ylabel('精度 (%)')
axes[1].set_title('精度の推移')
axes[1].legend()
plt.tight_layout()
plt.show()

損失が下がり、精度が上がっていく様子が視覚的に確認できます。これが「学習」です。

技術解説:誤差逆伝播法の仕組み

学習の核心部分をもう少し詳しく見てみましょう。コードの中に loss.backward() という行がありました。これが「誤差逆伝播法」と呼ばれる技術です。

AIに聞いてみます。

loss.backward() は何をしているのですか?誤差逆伝播法について、初心者にも分かるように説明してください。

AIは、こう説明してくれるでしょう。

「loss.backward() は、損失(予測と正解のズレ)を小さくするために、各重みをどの方向にどれだけ調整すべきかを計算します。

これは微分(勾配)の計算です。出力層から入力層に向かって、逆向きに計算が進むので『逆伝播』と呼ばれます。

計算された勾配を使って、optimizer.step() で実際に重みを更新します。損失が小さくなる方向に、少しずつ重みを動かしていくのです。」

数式で書くと複雑ですが、やっていることは「予測を正解に近づけるように、パラメータを調整する」というシンプルなことです。

3.4 AIと一緒にモデルを改良する

予測結果を見てみる

学習が終わったモデルで、実際に予測してみましょう。

テストデータからいくつかサンプルを取り出して、モデルの予測結果を画像と一緒に表示するコードを書いてください。間違えた例も見たいです。

# 予測結果を表示
model.eval()
fig, axes = plt.subplots(2, 5, figsize=(12, 5))

# いくつかのサンプルで予測
correct_samples = []
wrong_samples = []

with torch.no_grad():
    for images, labels in test_loader:
        images, labels = images.to(device), labels.to(device)
        outputs = model(images)
        _, predicted = outputs.max(1)
        for i in range(len(labels)):
            if predicted[i] == labels[i] and len(correct_samples) < 5:
                correct_samples.append((images[i], labels[i], predicted[i]))
            elif predicted[i] != labels[i] and len(wrong_samples) < 5:
                wrong_samples.append((images[i], labels[i], predicted[i]))
        if len(correct_samples) >= 5 and len(wrong_samples) >= 5:
            break

# 正解した例を上段に表示
for i, (image, label, pred) in enumerate(correct_samples):
    axes[0, i].imshow(image.cpu().squeeze(), cmap='gray')
    axes[0, i].set_title(f'正解: {label.item()}\n予測: {pred.item()}', color='green')
    axes[0, i].axis('off')

# 間違えた例を下段に表示
for i, (image, label, pred) in enumerate(wrong_samples):
    axes[1, i].imshow(image.cpu().squeeze(), cmap='gray')
    axes[1, i].set_title(f'正解: {label.item()}\n予測: {pred.item()}', color='red')
    axes[1, i].axis('off')

axes[0, 0].set_ylabel('正解', fontsize=12)
axes[1, 0].set_ylabel('不正解', fontsize=12)
plt.tight_layout()
plt.show()

モデルが正解した例と、間違えた例が表示されます。間違えた例を見ると、人間でも判別が難しいような、曖昧な手書き文字であることが多いでしょう。

「精度を上げたい」とAIに相談する

97%の精度でも十分ですが、もっと上げられないか、AIに相談してみましょう。

このモデルの精度をもっと上げたいです。どうすればいいですか?

AIは、いくつかの方法を提案してくれるはずです。

  1. モデルを深くする(層を増やす)

  2. 畳み込み層(CNN)を使う

  3. ドロップアウトを追加する(過学習対策)

  4. 学習率を調整する

  5. エポック数を増やす

この中から、CNNを試してみましょう。

畳み込みニューラルネットワーク(CNN)を使ったバージョンを作ってください

class CNN(nn.Module):
    def __init__(self):
        super(CNN, self).__init__()
        self.conv1 = nn.Conv2d(1, 32, kernel_size=3, padding=1)
        self.conv2 = nn.Conv2d(32, 64, kernel_size=3, padding=1)
        self.pool = nn.MaxPool2d(2, 2)
        self.dropout1 = nn.Dropout(0.25)
        self.dropout2 = nn.Dropout(0.5)
        self.fc1 = nn.Linear(64 * 7 * 7, 128)
        self.fc2 = nn.Linear(128, 10)
        self.relu = nn.ReLU()

    def forward(self, x):
        x = self.relu(self.conv1(x))
        x = self.pool(x)
        x = self.relu(self.conv2(x))
        x = self.pool(x)
        x = self.dropout1(x)
        x = x.view(-1, 64 * 7 * 7)
        x = self.relu(self.fc1(x))
        x = self.dropout2(x)
        x = self.fc2(x)
        return x

# 新しいモデルで学習
cnn_model = CNN().to(device)
optimizer = optim.Adam(cnn_model.parameters(), lr=0.001)
print("CNNモデルの構造:")
print(cnn_model)

このCNNモデルで同じように学習すると、99%以上の精度が出るはずです。学習コードは先ほどのSimpleNetと同じです。modelをcnn_modelに、optimizerを新しく作り直して、同じ学習ループを実行してください。

先ほどの学習コードを、このCNNモデルで実行できるように修正してください

とAIに頼めば、すぐに修正版を返してくれます。

技術解説:過学習と汎化性能

モデルを改良する際に注意すべきことがあります。「過学習」です。

AIに聞いてみましょう。

機械学習における「過学習」とは何ですか?どうすれば防げますか?

AIは、こう説明してくれるでしょう。

「過学習とは、訓練データに対しては高い精度が出るが、新しいデータ(テストデータ)に対しては精度が低い状態です。モデルが訓練データを『丸暗記』してしまい、一般的なパターンを学習できていない状態とも言えます。

防ぐ方法としては:

先ほどのCNNモデルにはDropoutが入っていました。これが過学習を防ぐ役割を果たしています。

学習曲線を見て、訓練精度だけが上がりテスト精度が下がり始めたら、それが過学習のサインです。

3.5 テストコードでモデルを検証する

なぜテストコードが必要なのか

第1章で「テストコードも一緒に頼む」という習慣を紹介しました。深層学習でも、テストコードは非常に重要です。

モデルが「なんとなく動いている」ように見えても、出力の形状が間違っていたり、予測値の範囲がおかしかったりすることがあります。こうした問題を早期に発見するために、テストコードを書きましょう。

モデルをテストする

SimpleNetとCNNの両方について、「ダミー入力を渡したときに、正しい形状の出力が返ってくるか」を確認します。

def test_models():
    """モデルの出力形状が正しいかテスト"""
    # SimpleNetのテスト
    simple_model = SimpleNet()
    dummy_input = torch.randn(4, 1, 28, 28)
    output = simple_model(dummy_input)
    assert output.shape == (4, 10), f"SimpleNet出力形状が違う: {output.shape}"
    print("✅ SimpleNet: 入力(4,1,28,28) → 出力(4,10) OK")

    # CNNのテスト
    cnn_test = CNN()
    output = cnn_test(dummy_input)
    assert output.shape == (4, 10), f"CNN出力形状が違う: {output.shape}"
    print("✅ CNN: 入力(4,1,28,28) → 出力(4,10) OK")

    # 確率の合計が1になるか確認
    probs = torch.softmax(output, dim=1)
    assert torch.allclose(probs.sum(dim=1), torch.ones(4), atol=1e-5)
    print("✅ Softmax後の確率の合計が1 OK")

test_models()

テストが通れば、モデルの構造に大きな問題はないことが分かります。

テストの習慣を身につけよう

テストコードを書くのは、最初は面倒に感じるかもしれません。でも、この習慣が身につくと、大きなメリットがあります。バグを早く見つけられる。安心して変更できる。コードの仕様が明確になる。

AIにコードを頼むときも、「テストコードも一緒にお願いします」と言う習慣をつけましょう。

MNISTの分類モデルを作ってください。出力形状を確認するテストコードも一緒にお願いします。

このように頼めば、AIはモデル本体とテストコードを両方生成してくれます。

第3章のまとめ

この章では、以下のことを学びました。

あなたは今、深層学習の最も基本的な部分を体験しました。画像を入力し、モデルが予測を出し、正解と比較して学習する。この流れは、どんな複雑な深層学習モデルでも共通です。ひょっとしたら本章は難関だったかもしれません。でもそれでも深層学習プログラムは動いたはずです。動くことがすごく大事なのです。最初からすべてを理解する必要はありません。これがバイブコーディングの醍醐味です。

そして本章では、テストコードを書く習慣も身につけ始めました。これは、実務や研究で深層学習を使うときに、必ず役立つスキルです。

次の章では、動画や時系列データへの挑戦です。画像が「1枚の静止画」だとすれば、動画は「時間軸を持った画像の連続」です。新しい次元が加わりますが、基本的な考え方は同じです。

引き続き、AIと一緒に進めていきましょう。


雑談:ニューロニスト

いやー、ついにでてきましたね。ニューラルネット。ニューラルネットはニューロンの塊。自称ニューロニストの渡辺としては嬉しい章でした。ニューロニストとは、ものごとをニューロンから考える主義のことです。世間にはまだそんな言葉はなく、私が勝手にいい触れてるだけですが、これから広げていきます。

とはいえ、本書ではニューロンは扱いません。ニューロニストなのに扱いません。そこは最初のコンセプトに忠実です。PythonですらAIに任せようというのに、深層学習の裏で働いているニューロンなんて知る必要なんてありません。

もし、それでも、もし、ニューロンのことが気になるというかた!朗報がございます。拙著「神経科学者と学ぶ深層学習超入門」があるではありませんか。この本は深層学習をニューロンから解き明かすことを目的にしているのです。もしご興味があるかたはぜひご検討ください。ちょぴっと高額ですが、かわいいイラストも満載です。「神経科学者と学ぶ深層学習超入門」で検索です!

それはさておき、ここではニューロニストの野望を語っておきましょう。ニューロニストの歴史はかなり古く、渡辺が子供の頃に「人間とはなにか」を考え始めたときにまで遡ります。とてもロックンロールな命題です。そのとき子供なりに図書館でいろいろ調べてわかったことがひとつありました。それがニューロンです。人間が作り上げたすべてのもの、科学、文化、工業、経済、政治。これらの中心にニューロンがあると確信しました。ニューロンを理解すれば人間そして社会が理解できる。これが幼いニューロニストの野望となりました。

LLMを手に入れたニューロニストの野望はさらに膨らんでいます。当初の渡辺の疑問は3つありました。残りふたつも「生命の起源」「宇宙の始まり」というロックンロールなお題です。LLMが手に入った今、これらにも立ち向かうことができるんじゃないかというドキドキ感が湧いてきました。生命のオリジンを、まるごと計算機シミュレーションできるのではないか。ひょっとして宇宙を数学ではなく、ニューロンで表現できるのではないか。もはや「このおバカは何を言ってるのでしょう」だとおもいます。宇宙の下りに至っては、いまだにClaudeにも理解されていません笑

イラスト

第4章:AIと挑む動画・時系列データ

前章では、1枚の画像を分類する深層学習モデルを作りました。MNISTの手書き数字を見分けるモデルでしたね。

この章では、もう一歩進んで「動画」を扱ってみます。

といっても、心配は要りません。やり方はこれまでと同じです。「やりたいこと」をAIに伝えて、コードを書いてもらい、動かして結果を観察する。コードの中身をすべて理解する必要はありません。「動画をAIで扱うと、こんなことができるんだ」という体験を楽しむことが、この章の目的です。

私の研究分野である視覚神経科学でも、時間方向の処理は重要なテーマです。脳は、網膜に映る映像の時間変化から、動きや因果関係を推論しています。深層学習でも、この「時間」という次元をどう扱うかが鍵になります。

4.1 「動画を扱いたい」とAIに相談する

動画データとは何か

まず、AIに基本的なことを聞いてみましょう。

深層学習で動画を扱う場合、画像との違いは何ですか?初心者向けにわかりやすく教えてください。

AIは、こう説明してくれるでしょう。

「画像が2次元(高さ×幅)のデータであるのに対し、動画は3次元(時間×高さ×幅)のデータです。カラー動画なら4次元(時間×高さ×幅×色チャンネル)になります。

この『時間』という次元が加わることで、動きのパターンや変化の方向を学習できるようになります。一方で、データ量が大きくなり、処理の複雑さも増します。」

つまり、動画とは「パラパラ漫画」のようなものです。1枚ずつは普通の画像ですが、それが時間順に並ぶことで「動き」が生まれます。

動画を作って、フレームに分解してみよう

言葉だけだと分かりにくいので、実際に手を動かしてみましょう。AIに、簡単なアニメーション動画を作ってもらいます。

Google Colabで、緑の円が左から右に動く簡単なアニメーション動画を生成して、そこからフレームを抽出して並べて表示するコードを書いてください。

以下のコードをAIに生成してもらい、そのままコピー&ペーストして実行してください。コードの中身を理解する必要はありません。結果の画像を見ることが大切です。

import cv2
import numpy as np
!pip install japanize-matplotlib -q
import japanize_matplotlib
import matplotlib.pyplot as plt

# --- ここから:動画を生成する ---
video_path = "sample_video.mp4"
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
out = cv2.VideoWriter(video_path, fourcc, 30.0, (256, 256))

for i in range(90):  # 3秒分(30fps × 3秒)
    frame = np.zeros((256, 256, 3), dtype=np.uint8)
    x = int(30 + i * 2.2)  # 円が左から右に移動
    cv2.circle(frame, (x, 128), 20, (0, 255, 0), -1)
    out.write(frame)

out.release()
print("動画を生成しました")
# --- ここまで:動画生成おわり ---

# --- ここから:動画の情報を表示する ---
cap = cv2.VideoCapture(video_path)
fps = cap.get(cv2.CAP_PROP_FPS)
frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))

print(f"FPS: {fps}")
print(f"総フレーム数: {frame_count}")
print(f"解像度: {width} x {height}")
print(f"動画の長さ: {frame_count / fps:.2f} 秒")
cap.release()
# --- ここまで ---

# --- ここから:フレームを抜き出して並べる ---
cap = cv2.VideoCapture(video_path)
total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
indices = np.linspace(0, total - 1, 10, dtype=int)  # 等間隔で10枚

frames = []
for idx in indices:
    cap.set(cv2.CAP_PROP_POS_FRAMES, idx)
    ret, frame = cap.read()
    if ret:
        frames.append(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
cap.release()

fig, axes = plt.subplots(2, 5, figsize=(15, 6))
for i, (frame, idx) in enumerate(zip(frames, indices)):
    ax = axes[i // 5, i % 5]
    ax.imshow(frame)
    ax.set_title(f'フレーム {idx}')
    ax.axis('off')
plt.suptitle('動画から抽出した10フレーム', fontsize=14)
plt.tight_layout()
plt.show()
# --- ここまで ---

実行すると、10枚の画像が並んで表示されます。緑の円が、フレームごとに少しずつ右に移動しているのが見えるはずです。

これが「動画」というデータの正体です。静止画の集まりを、時間順に並べたもの。パラパラ漫画そのものですね。

動画データの構造を整理する

表示された情報を確認してみましょう。

これらの数値は、AIに「この動画のFPSは?」と聞いてコードを書いてもらえばすぐに確認できます。覚える必要はありません。

ここまでのポイント:動画とは「画像の連続」です。深層学習で動画を扱うとは、この「時間の流れ」をモデルに教えることです。

4.2 AIに「次のフレーム」を予測させる

やりたいこと:動きの予測

動画の構造が分かったところで、面白いことに挑戦しましょう。

「過去のフレームを見せて、次のフレームを予測させる」。つまり、AIに動きのパターンを学習させます。

点が右に動いている8枚の画像を見せたら、9枚目で点がどこにあるかを予測できるでしょうか?

AIに相談します。

点が動くアニメーションを大量に作って、「過去8フレームから次のフレームを予測する」モデルをPyTorchで作りたいです。LSTMを使ってください。データの生成から学習、予測結果の可視化まで、一つのコードにまとめてください。

AIが生成してくれるコードは長めですが、そのまま丸ごとコピー&ペーストで大丈夫です。以下がその全体です。

import torch
import torch.nn as nn
import numpy as np
!pip install japanize-matplotlib -q
import japanize_matplotlib
import matplotlib.pyplot as plt

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

# ============================
# ステップ1:訓練データを自動生成
# ============================
# 点が色々な速度で動く動画をたくさん作る
def generate_moving_dot_sequence(num_sequences=100, seq_length=16, img_size=32):
    """点が動くアニメーションを大量に生成する"""
    sequences = []
    for _ in range(num_sequences):
        start_x = np.random.randint(5, img_size - 5)
        velocity = np.random.uniform(-2, 2)
        seq = []
        for t in range(seq_length):
            frame = np.zeros((img_size, img_size), dtype=np.float32)
            x = int(start_x + velocity * t) % img_size
            y = img_size // 2
            for dx in range(-1, 2):
                for dy in range(-1, 2):
                    nx, ny = (x + dx) % img_size, (y + dy) % img_size
                    frame[ny, nx] = 1.0
            seq.append(frame)
        sequences.append(seq)
    return np.array(sequences)

data = generate_moving_dot_sequence(num_sequences=500, seq_length=16, img_size=32)
print(f"データ形状: {data.shape}")  # (500, 16, 32, 32)

# 最初の8フレーム→入力、9フレーム目→正解
X = data[:, :8]   # (500, 8, 32, 32)
y = data[:, 8]    # (500, 32, 32)

split = 400
X_train = torch.FloatTensor(X[:split]).to(device)
X_test  = torch.FloatTensor(X[split:]).to(device)
y_train = torch.FloatTensor(y[:split]).to(device)
y_test  = torch.FloatTensor(y[split:]).to(device)

# ============================
# ステップ2:予測モデルを定義
# ============================
class LSTMPredictor(nn.Module):
    def __init__(self, img_size=32, hidden_size=256):
        super().__init__()
        self.img_size = img_size
        self.input_size = img_size * img_size
        self.lstm = nn.LSTM(
            input_size=self.input_size,
            hidden_size=hidden_size,
            num_layers=2,
            batch_first=True
        )
        self.fc = nn.Linear(hidden_size, self.input_size)
        self.sigmoid = nn.Sigmoid()

    def forward(self, x):
        batch_size, seq_len, h, w = x.shape
        x = x.view(batch_size, seq_len, -1)
        lstm_out, _ = self.lstm(x)
        last_output = lstm_out[:, -1, :]
        pred = self.sigmoid(self.fc(last_output))
        return pred.view(batch_size, h, w)

model = LSTMPredictor(img_size=32, hidden_size=256).to(device)

# ============================
# ステップ3:学習
# ============================
criterion = nn.MSELoss()
optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

print("学習を開始します...")
train_losses = []
for epoch in range(50):
    model.train()
    optimizer.zero_grad()
    pred = model(X_train)
    loss = criterion(pred, y_train)
    loss.backward()
    optimizer.step()
    train_losses.append(loss.item())

    if (epoch + 1) % 10 == 0:
        model.eval()
        with torch.no_grad():
            test_pred = model(X_test)
            test_loss = criterion(test_pred, y_test)
        print(f"  エポック {epoch+1}/50  訓練損失: {loss.item():.6f}  テスト損失: {test_loss.item():.6f}")

# ============================
# ステップ4:結果を見る
# ============================
# 学習曲線
plt.figure(figsize=(8, 3))
plt.plot(train_losses)
plt.xlabel('エポック')
plt.ylabel('損失')
plt.title('学習曲線(損失が下がっていれば学習成功)')
plt.show()

# 予測結果の可視化
model.eval()
with torch.no_grad():
    predictions = model(X_test)

fig, axes = plt.subplots(3, 10, figsize=(15, 5))
fig.suptitle('左8枚=入力(過去のフレーム)、右端2枚=正解と予測', fontsize=12)
for i in range(3):
    for j in range(8):
        axes[i, j].imshow(X_test[i, j].cpu(), cmap='gray')
        axes[i, j].axis('off')
        if i == 0:
            axes[i, j].set_title(f't={j}', fontsize=8)
    axes[i, 8].imshow(y_test[i].cpu(), cmap='gray')
    axes[i, 8].axis('off')
    if i == 0:
        axes[i, 8].set_title('正解', fontsize=8, color='blue')
    axes[i, 9].imshow(predictions[i].cpu(), cmap='gray')
    axes[i, 9].axis('off')
    if i == 0:
        axes[i, 9].set_title('予測', fontsize=8, color='red')
plt.tight_layout()
plt.show()

結果を観察しよう

実行すると、2つの図が表示されます。

学習曲線:エポックが進むにつれて損失(Loss)が下がっていきます。これは「モデルが徐々に学習している」ことを意味します。第3章のMNISTと同じですね。

予測結果:横に10枚並んだ画像のうち、左の8枚が入力(AIが見た過去のフレーム)、右から2番目が正解(本当の次のフレーム)、右端がモデルの予測です。点の位置がだいたい合っていれば、モデルは「動きのパターン」を学習できたということです。

予測画像がぼんやりしているかもしれません。それは、モデルが「このあたりに点があるはず」という確率的な予測をしているためです。完璧でなくても、動きの方向を捉えていれば大成功です。

★読み飛ばしOK★ LSTMとは何か

ここでは、いまコードの中に出てきた「LSTM」について少し詳しく触れます。ここは読み飛ばしても、この後の内容に支障はありません。興味がある方だけお読みください。

AIに聞いてみましょう。

RNNとLSTMの違いを、たとえ話で教えてください

AIは、こう説明してくれるでしょう。

RNN(再帰型ニューラルネットワーク) は、過去の情報を内部状態として保持しながら処理します。しかし、長いシーケンスを扱うと、遠い過去の情報がどんどん薄れてしまいます(勾配消失問題)。

LSTM(Long Short-Term Memory) は、この問題を解決するために設計されました。『ゲート』という仕組みで、情報を選択的に記憶・忘却します。」

イメージとしては:

LSTMが「どの情報を覚えておくか」を自分で判断できる点が、RNNとの大きな違いです。ただし、実際にコードを書くときは、nn.LSTM と書くだけで使えるので、内部の仕組みを詳しく知らなくても問題ありません。

4.3 動画の「分類」に挑戦する——3D CNN

やりたいこと:動きの方向を見分ける

次は、動画の「分類」に挑戦しましょう。

第3章では、手書き数字の画像を「0〜9」に分類しました。今度は、動画を見て「点が左に動いている」か「右に動いている」かを分類するモデルを作ります。

AIに相談します。

点が左に動く動画と右に動く動画を大量に作って、その方向を分類するモデルを作りたいです。3D CNNを使ってください。データの生成から学習、精度の表示まで一つにまとめてください。

これも長いコードですが、コピー&ペーストで実行できます。

import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np

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

# ============================
# ステップ1:分類用の動画データを生成
# ============================
def generate_moving_direction_data(num_samples=200, seq_length=8, img_size=32):
    """左に動く動画(ラベル0)と右に動く動画(ラベル1)を生成"""
    sequences = []
    labels = []
    for i in range(num_samples):
        direction = i % 2  # 0: 左, 1: 右
        velocity = 2 if direction == 1 else -2
        start_x = 16
        seq = []
        for t in range(seq_length):
            frame = np.zeros((img_size, img_size), dtype=np.float32)
            x = int(start_x + velocity * t) % img_size
            y = img_size // 2
            for dx in range(-2, 3):
                for dy in range(-2, 3):
                    nx, ny = (x + dx) % img_size, (y + dy) % img_size
                    frame[ny, nx] = 1.0
            seq.append(frame)
        sequences.append(seq)
        labels.append(direction)
    return np.array(sequences), np.array(labels)

X_data, y_data = generate_moving_direction_data(num_samples=400, seq_length=8, img_size=32)
print(f"データ形状: {X_data.shape}, ラベル形状: {y_data.shape}")

split = 300
X_train = torch.FloatTensor(X_data[:split]).to(device)
y_train = torch.LongTensor(y_data[:split]).to(device)
X_test  = torch.FloatTensor(X_data[split:]).to(device)
y_test  = torch.LongTensor(y_data[split:]).to(device)

# ============================
# ステップ2:3D CNNモデル
# ============================
class Simple3DCNN(nn.Module):
    def __init__(self, num_classes=2):
        super().__init__()
        self.conv1 = nn.Conv3d(1, 32, kernel_size=(3, 3, 3), padding=(1, 1, 1))
        self.pool1 = nn.MaxPool3d(kernel_size=(2, 2, 2))
        self.conv2 = nn.Conv3d(32, 64, kernel_size=(3, 3, 3), padding=(1, 1, 1))
        self.pool2 = nn.MaxPool3d(kernel_size=(2, 2, 2))
        self.conv3 = nn.Conv3d(64, 128, kernel_size=(3, 3, 3), padding=(1, 1, 1))
        self.pool3 = nn.MaxPool3d(kernel_size=(2, 2, 2))
        self.fc1 = nn.Linear(128 * 1 * 4 * 4, 256)
        self.fc2 = nn.Linear(256, num_classes)
        self.dropout = nn.Dropout(0.5)

    def forward(self, x):
        x = x.unsqueeze(1)  # チャンネル次元を追加
        x = self.pool1(F.relu(self.conv1(x)))
        x = self.pool2(F.relu(self.conv2(x)))
        x = self.pool3(F.relu(self.conv3(x)))
        x = x.view(x.size(0), -1)
        x = self.dropout(F.relu(self.fc1(x)))
        return self.fc2(x)

model_3d = Simple3DCNN(num_classes=2).to(device)

# ============================
# ステップ3:学習
# ============================
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model_3d.parameters(), lr=0.001)

print("学習を開始します...")
for epoch in range(30):
    model_3d.train()
    optimizer.zero_grad()
    outputs = model_3d(X_train)
    loss = criterion(outputs, y_train)
    loss.backward()
    optimizer.step()

    if (epoch + 1) % 10 == 0:
        model_3d.eval()
        with torch.no_grad():
            test_outputs = model_3d(X_test)
            _, predicted = test_outputs.max(1)
            accuracy = (predicted == y_test).float().mean()
        print(f"  エポック {epoch+1}/30  損失: {loss.item():.4f}  テスト精度: {accuracy.item()*100:.1f}%")

結果を観察しよう

テスト精度が90%以上になっていれば成功です。モデルは「点が左に動いている」か「右に動いている」かを、フレームの連続から判断できるようになりました。

これは第3章の手書き数字分類と本質的に同じ「分類」タスクですが、入力が1枚の画像ではなく「複数フレームの動画」になっています。時間方向のパターンを見分けられる。これが動画を扱うAIの力です。

★読み飛ばしOK★ 2D畳み込みと3D畳み込みの違い

コードの中に nn.Conv3d という見慣れないものが出てきました。第3章で使った nn.Conv2d との違いを簡単に説明します。ここも読み飛ばしOKです。

3×3の2Dフィルタが「3×3=9ピクセルの領域」を見るのに対し、3×3×3の3Dフィルタは「3フレーム×3×3=27ピクセルの領域」を一度に見ます。時間方向にもまたがって見るので、「動き」のパターンを検出できるのです。

4.4 動画データが大きすぎるとき——AIに相談しよう

ここまでは、小さなデータ(32×32ピクセルの短い動画)を使ってきました。実際の動画はもっと大きいので、メモリ不足になることがあります。

そのときの対処法を一つだけ紹介しておきます。

困ったらAIに聞く——いつもの作戦

メモリ不足になったら、まずAIに相談しましょう。

動画データを扱っていたら、メモリ不足になりました。どうすればいいですか?

AIは、いくつかの対策を教えてくれるでしょう。代表的なものは:

最初の2つは、すでに使っている数値を変えるだけなので手軽です。「遅延読み込み」は少し高度ですが、AIに「PyTorchのDatasetクラスで遅延読み込みするコードを書いて」と頼めば、すぐに使えるコードを生成してくれます。

大事なのは、メモリ不足はよくあることで、恥ずかしいことではないということです。プロの研究者でも日常的にメモリ不足と戦っています。エラーメッセージをそのままAIに貼り付けて相談するのが、最も効率的な解決法です。

第4章のまとめ

この章では、動画データに挑戦しました。

やったこと:

覚えておいてほしいこと:

次の章では、「生成モデル」に挑戦します。これまでは「認識」(入力を見て分類する)でしたが、次は「生成」(新しいデータを作り出す)です。VAE(変分オートエンコーダ)を使って、AIに画像を「創造」させます。

引き続き、AIと一緒に進めていきましょう。


雑談:錯視

渡辺が研究している錯視を紹介します。

最初にご紹介するのは、前章にちなんで動きに関するものです。知っている方も多い最も有名な錯視のひとつかもしれません。それは北岡明佳先生が作られた「蛇の回転錯視」です。止まっているイラストが、右に左にぐるぐる回転して知覚されます。私が錯視研究にのめり込むきっかけになった錯視です。ひょっとしたら「ぜんぜん動かない」という方がおられるかもしれませんが、だからといってなにか問題があるわけではありません。個人差の大きい錯視です。この錯視の偉大なところはネコが反応することです。

[https://youtu.be/CcXXQ6GCUb8?si=ykZkB0QgfztKjz-v]

ネコだけではなく、ライオンや小魚など、報告があります。

イラスト

渡辺が錯視研究に入ったころに創作した錯視をご紹介します。その名も「渡辺錯視」です(命名は北岡明佳先生)。仮想上の線の行き着く先がズレるという錯視です。結構ズレます。これも個人差が激しい錯視です。

[https://doi.org/10.6084/m9.figshare.5838606]

イラスト

次は明るさの錯視、ログヴィネンコ錯視です(Logvinenko 1999)。あまりにも錯視量がすぎてどこが錯視かわからないくらいです。横に並んだひし形の図形の明るさがすべて同じ明るさで描かれていますが、白いひし形が横一列に、濃い灰色のひし形が横一列にあるように見えます。ひし形の周囲を隠すと、同じ明るさであることが確認できます。何を言っているのかわからない場合は「ログヴィネンコ錯視」で検索です。

イラスト

次は月の錯視です(写真はWikipediaから)。月は天頂にあるときと、水平線近くにあるときではまるで大きさが変わります。月と地球との距離はほとんど変化しませんので、それは錯視です。大昔から記述されてきた錯視です。太陽も同じ原理が働きます。富士山などの遠くにある巨大なものは大きく知覚されます。写真やイラストでは再現できない究極の野生の錯視です。

イラスト

最後に、動きにちなんだ錯視をもうひとつご紹介しておきます。フラッシュラグ効果(MacKay, 1958; Nijhawan, 1994)と呼ばれています。動いている物体の位置は、突然現れた静止物体の位置と比較して、動いている方向へズレて知覚されるという錯視です。これは動画なので本書では取り扱えないのでぜひ私がつくった動画を御覧ください。

[https://youtu.be/DUBM-GG0gAk?si=JJ-ffSBW7od6rJ_y]

錯視は奥深くて面白いテーマです。ネットを検索すると山ほどでてきます。みなさんもぜひ不思議な世界を堪能してみてください。

イラスト

第5章:AIと学ぶ生成モデル(VAE)

これまでの章では、「認識」に取り組んできました。画像を見て「これは何か」を判定する。動画を見て「どちらに動いているか」を分類する。

この章では、まったく異なる方向に進みます。「生成」です。

AIに画像を「作らせる」のです。学習データに似ているけれど、まったく同じではない新しい画像を生み出す。最近話題の画像生成AI(Stable Diffusionなど)の基礎にもなっている考え方です。

やり方はいつもと同じです。AIにコードを書いてもらい、実行して、結果を観察する。今回は特に「見て楽しい」結果が得られるので、ぜひ楽しみながら進めてください。

5.1 まずは「圧縮と復元」を体験する——オートエンコーダ

やりたいこと

生成モデルの基礎として、まず「オートエンコーダ」を体験しましょう。

オートエンコーダとは、画像をぎゅっと圧縮して、そこから元に戻すモデルです。

イメージとしては:

この「圧縮→復元」の訓練を通じて、モデルは画像の本質的な特徴を自然に学びます。

AIに頼んでみましょう。

MNISTの手書き数字を使って、シンプルなオートエンコーダをPyTorchで作ってください。学習して、元画像と復元画像を並べて表示するところまでお願いします。

以下のコードをそのままコピー&ペーストして実行してください。

import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
!pip install japanize-matplotlib -q
import japanize_matplotlib
import matplotlib.pyplot as plt

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"使用デバイス: {device}")

# ============================
# ステップ1:MNISTデータの準備
# ============================
transform = transforms.Compose([transforms.ToTensor()])
train_dataset = datasets.MNIST('./data', train=True, download=True, transform=transform)
test_dataset  = datasets.MNIST('./data', train=False, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True)
test_loader  = DataLoader(test_dataset, batch_size=128, shuffle=False)

# ============================
# ステップ2:オートエンコーダの定義
# ============================
class Autoencoder(nn.Module):
    def __init__(self, latent_dim=32):
        super().__init__()
        # エンコーダ:784 → 256 → 64 → 32(圧縮)
        self.encoder = nn.Sequential(
            nn.Flatten(),
            nn.Linear(784, 256), nn.ReLU(),
            nn.Linear(256, 64),  nn.ReLU(),
            nn.Linear(64, latent_dim)
        )
        # デコーダ:32 → 64 → 256 → 784(復元)
        self.decoder = nn.Sequential(
            nn.Linear(latent_dim, 64),  nn.ReLU(),
            nn.Linear(64, 256),         nn.ReLU(),
            nn.Linear(256, 784),        nn.Sigmoid()
        )

    def forward(self, x):
        z = self.encoder(x)
        return self.decoder(z).view(-1, 1, 28, 28)

autoencoder = Autoencoder(latent_dim=32).to(device)

# ============================
# ステップ3:学習
# ============================
criterion = nn.MSELoss()
optimizer = optim.Adam(autoencoder.parameters(), lr=0.001)

print("学習を開始します...")
for epoch in range(10):
    total_loss = 0
    for images, _ in train_loader:
        images = images.to(device)
        optimizer.zero_grad()
        reconstructed = autoencoder(images)
        loss = criterion(reconstructed, images)
        loss.backward()
        optimizer.step()
        total_loss += loss.item()
    print(f"  エポック {epoch+1}/10  損失: {total_loss/len(train_loader):.6f}")

# ============================
# ステップ4:結果を見る
# ============================
autoencoder.eval()
with torch.no_grad():
    test_images, _ = next(iter(test_loader))
    test_images = test_images[:10].to(device)
    reconstructed = autoencoder(test_images)

fig, axes = plt.subplots(2, 10, figsize=(15, 3))
fig.suptitle('上段:元の画像  下段:復元された画像', fontsize=12)
for i in range(10):
    axes[0, i].imshow(test_images[i].cpu().squeeze(), cmap='gray')
    axes[0, i].axis('off')
    axes[1, i].imshow(reconstructed[i].cpu().squeeze(), cmap='gray')
    axes[1, i].axis('off')
plt.tight_layout()
plt.show()

結果を観察しよう

上段が元の手書き数字、下段がモデルが復元した画像です。少しぼやけているかもしれませんが、どの数字かは十分にわかるはずです。

784個の数値を32個にまで圧縮して、そこからここまで復元できる。これがオートエンコーダの力です。

ポイント:オートエンコーダは「何が数字の本質か」を、ラベルなしで自力で学習しています。誰も「ここが重要だ」と教えていないのに、圧縮に必要な情報を自分で見つけ出すのです。

5.2 VAEで画像を「生成」する

オートエンコーダからVAEへ

オートエンコーダで「圧縮と復元」はできました。しかし、「新しい画像を作る」にはもう一工夫が必要です。

オートエンコーダの圧縮された空間(潜在空間)は、整理されていません。適当な点を選んでデコーダに渡しても、意味のある画像になるとは限りません。

VAE(変分オートエンコーダ) は、潜在空間を「整理された地図」にするための仕組みです。この地図の上のどの点を選んでも、それらしい画像が生成されるようになります。

AIに頼みましょう。

MNISTでVAEを作ってください。潜在空間を2次元にして、学習後に①潜在空間の散布図、②ランダム生成画像、③2つの数字の間の補間画像を表示してください。一つのコードにまとめてください。

長いコードですが、コピペして実行するだけで3つの美しい図が得られます。

import torch
import torch.nn as nn
import torch.optim as optim
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
!pip install japanize-matplotlib -q
import japanize_matplotlib
import matplotlib.pyplot as plt
import numpy as np

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

# ============================
# ステップ1:データ準備
# ============================
transform = transforms.Compose([transforms.ToTensor()])
train_dataset = datasets.MNIST('./data', train=True, download=True, transform=transform)
test_dataset  = datasets.MNIST('./data', train=False, transform=transform)
train_loader = DataLoader(train_dataset, batch_size=128, shuffle=True)
test_loader  = DataLoader(test_dataset, batch_size=128, shuffle=False)

# ============================
# ステップ2:VAEモデルの定義
# ============================
class VAE(nn.Module):
    def __init__(self, latent_dim=2):
        super().__init__()
        self.latent_dim = latent_dim
        # エンコーダ
        self.encoder = nn.Sequential(
            nn.Flatten(),
            nn.Linear(784, 256), nn.ReLU(),
            nn.Linear(256, 128), nn.ReLU(),
        )
        self.fc_mu     = nn.Linear(128, latent_dim)  # 平均
        self.fc_logvar = nn.Linear(128, latent_dim)  # 分散
        # デコーダ
        self.decoder = nn.Sequential(
            nn.Linear(latent_dim, 128), nn.ReLU(),
            nn.Linear(128, 256),        nn.ReLU(),
            nn.Linear(256, 784),        nn.Sigmoid()
        )

    def encode(self, x):
        h = self.encoder(x)
        return self.fc_mu(h), self.fc_logvar(h)

    def reparameterize(self, mu, logvar):
        std = torch.exp(0.5 * logvar)
        eps = torch.randn_like(std)
        return mu + eps * std

    def decode(self, z):
        return self.decoder(z).view(-1, 1, 28, 28)

    def forward(self, x):
        mu, logvar = self.encode(x)
        z = self.reparameterize(mu, logvar)
        return self.decode(z), mu, logvar

vae = VAE(latent_dim=2).to(device)

# ============================
# ステップ3:損失関数と学習
# ============================
def vae_loss(reconstructed, original, mu, logvar):
    # 復元のうまさ + 潜在空間の整理度
    recon = nn.functional.binary_cross_entropy(
        reconstructed.view(-1, 784), original.view(-1, 784), reduction='sum')
    kl = -0.5 * torch.sum(1 + logvar - mu.pow(2) - logvar.exp())
    return recon + kl

optimizer = optim.Adam(vae.parameters(), lr=0.001)

print("VAEの学習を開始します...")
for epoch in range(15):
    vae.train()
    total_loss = 0
    for images, _ in train_loader:
        images = images.to(device)
        optimizer.zero_grad()
        reconstructed, mu, logvar = vae(images)
        loss = vae_loss(reconstructed, images, mu, logvar)
        loss.backward()
        optimizer.step()
        total_loss += loss.item()
    print(f"  エポック {epoch+1}/15  損失: {total_loss/len(train_loader.dataset):.4f}")

# ============================
# ステップ4:結果を3つの図で見る
# ============================

# --- 図1:潜在空間の地図 ---
vae.eval()
latent_points, all_labels = [], []
with torch.no_grad():
    for images, targets in test_loader:
        mu, _ = vae.encode(images.to(device))
        latent_points.append(mu.cpu())
        all_labels.append(targets)
latent_points = torch.cat(latent_points).numpy()
all_labels = torch.cat(all_labels).numpy()

plt.figure(figsize=(10, 8))
scatter = plt.scatter(latent_points[:, 0], latent_points[:, 1],
                      c=all_labels, cmap='tab10', alpha=0.6, s=10)
plt.colorbar(scatter, label='数字')
plt.xlabel('潜在次元 1')
plt.ylabel('潜在次元 2')
plt.title('潜在空間の地図(同じ数字が近くに集まる)')
plt.show()

# --- 図2:ランダムに新しい画像を生成 ---
with torch.no_grad():
    z = torch.randn(10, 2).to(device)
    generated = vae.decode(z)

fig, axes = plt.subplots(1, 10, figsize=(15, 2))
fig.suptitle('VAEがランダムに生成した画像(学習データにない新しい画像)', fontsize=12)
for i, ax in enumerate(axes):
    ax.imshow(generated[i].cpu().squeeze(), cmap='gray')
    ax.axis('off')
plt.tight_layout()
plt.show()

# --- 図3:2つの数字の間を滑らかに変化させる ---
img1, _ = test_dataset[2]   # ある数字
img2, _ = test_dataset[13]  # 別の数字
with torch.no_grad():
    mu1, _ = vae.encode(img1.unsqueeze(0).to(device))
    mu2, _ = vae.encode(img2.unsqueeze(0).to(device))
    interpolated = []
    for alpha in np.linspace(0, 1, 10):
        z = (1 - alpha) * mu1 + alpha * mu2
        interpolated.append(vae.decode(z).cpu().squeeze())

fig, axes = plt.subplots(1, 10, figsize=(15, 2))
fig.suptitle('2つの数字の間を滑らかに変化(潜在空間の補間)', fontsize=12)
for i, (ax, img) in enumerate(zip(axes, interpolated)):
    ax.imshow(img, cmap='gray')
    ax.axis('off')
plt.tight_layout()
plt.show()

3つの結果を観察しよう

図1:潜在空間の地図

色のついた点が散らばっている散布図が表示されます。同じ数字(同じ色)の点が近くに集まっていることに注目してください。VAEは、似た数字を潜在空間の近い位置に配置することを自力で学んだのです。「0」のかたまり、「1」のかたまり……という具合に、数字ごとの「領土」ができています。

図2:ランダム生成

10枚の手書き数字っぽい画像が表示されます。これらは学習データのどれとも同一ではない、新しい画像です。潜在空間のランダムな点からデコーダが「作り出した」ものです。ぼんやりしているかもしれませんが、数字として読めるものが多いはずです。

図3:数字の間の補間

左端の数字から右端の数字へ、滑らかに変化していく10枚の画像が表示されます。これが「潜在空間が整理されている」ことの証拠です。2つの点の間を直線で結んで歩いていくと、途中の画像も意味のあるものになっています。

★読み飛ばしOK★ VAEの数学的な仕組み

ここでは、コードの中にある2つの重要な概念について説明します。読み飛ばしても、この後の内容には影響しません。

再パラメータ化トリック

通常、ランダムサンプリングは微分できません(勾配が伝わらない)。VAEでは次のトリックを使ってこれを解決します:

z = μ + ε × σ (εは標準正規分布からのランダムな値)

ランダム性をεに押し込めることで、μとσを通じて勾配を伝播できるようにしています。

KLダイバージェンス

損失関数に含まれる kl = -0.5 * torch.sum(...) の部分です。これは「潜在空間を整理するための力」で、潜在分布を標準正規分布に近づけるように働きます。この力のおかげで、潜在空間の地図がきれいに整理され、ランダムな点からでも意味のある画像を生成できるのです。

どちらも、使うだけなら中身を知らなくても大丈夫です。興味が出たら、AIに「再パラメータ化トリックをもっと詳しく教えて」と聞いてみてください。

5.3 潜在空間をもっと探検する——グリッド探索

潜在空間の地図が面白いと思った方のために、もう一つ可視化を紹介します。

潜在空間を格子状に区切って、各点でどんな画像が生成されるかを一覧にします。

# 潜在空間のグリッド探索(5.2のコードの続きとして実行)
vae.eval()
n = 20
digit_size = 28
grid_x = np.linspace(-3, 3, n)
grid_y = np.linspace(-3, 3, n)[::-1]
figure = np.zeros((digit_size * n, digit_size * n))

with torch.no_grad():
    for i, yi in enumerate(grid_y):
        for j, xi in enumerate(grid_x):
            z = torch.tensor([[xi, yi]], dtype=torch.float32).to(device)
            generated = vae.decode(z)
            figure[i * digit_size:(i+1) * digit_size,
                   j * digit_size:(j+1) * digit_size] = generated.cpu().squeeze().numpy()

plt.figure(figsize=(10, 10))
plt.imshow(figure, cmap='gray')
plt.title('潜在空間のグリッド探索(座標を動かすと数字が変わる)')
plt.axis('off')
plt.show()

大きなモザイク状の画像が表示されます。座標を移動すると、数字が滑らかに変化していく様子がわかります。これは、VAEが学んだ「数字の地図」そのものです。

5.4 テストコードでVAEを検証する

第3章で学んだ「テストコード」をVAEにも適用しましょう。VAEはエンコーダとデコーダが連携して動くので、各パーツが正しく動作しているか確認することが大切です。

AIに頼みます。

VAEのエンコーダ、デコーダ、全体の入出力をテストするコードを書いてください。

# --- VAEのテストコード(5.2のVAEクラス定義の後に実行) ---

def run_vae_tests():
    """VAEの各部品が正しく動くかチェック"""
    test_vae = VAE(latent_dim=2)
    dummy = torch.rand(4, 1, 28, 28)

    # テスト1:エンコーダの出力
    mu, logvar = test_vae.encode(dummy)
    assert mu.shape == (4, 2), f"muの形状が違う: {mu.shape}"
    assert logvar.shape == (4, 2), f"logvarの形状が違う: {logvar.shape}"
    print("✅ エンコーダ出力: OK")

    # テスト2:デコーダの出力
    z = torch.randn(4, 2)
    decoded = test_vae.decode(z)
    assert decoded.shape == (4, 1, 28, 28), f"復元画像の形状が違う: {decoded.shape}"
    assert decoded.min() >= 0 and decoded.max() <= 1, "値が0〜1の範囲外"
    print("✅ デコーダ出力: OK")

    # テスト3:全体の入出力
    reconstructed, mu, logvar = test_vae(dummy)
    assert reconstructed.shape == dummy.shape, "復元画像の形状が入力と違う"
    loss = vae_loss(reconstructed, dummy, mu, logvar)
    assert not torch.isnan(loss) and loss > 0, "損失が異常"
    print("✅ 全体の入出力: OK")

    print("\n🎉 すべてのテストに合格!")

run_vae_tests()

すべて「✅ OK」が出れば成功です。モデルの構造を変更したとき(たとえば潜在次元を2から10に変えたとき)に、このテストを再実行すれば、変更で何かが壊れていないか素早く確認できます。

第5章のまとめ

この章では、生成モデルに挑戦しました。

やったこと:

覚えておいてほしいこと:

VAEで学んだ「潜在空間」「エンコーダ・デコーダ」「確率的生成」の考え方は、より高度な生成モデル(GAN、拡散モデルなど)の基盤にもなっています。

次の章では、GitHubで公開されている最先端のモデル(ViT)を改造する方法を学びます。自分でゼロからモデルを作るのではなく、既存のコードを活用する技術です。

引き続き、AIと一緒に進めていきましょう。


雑談:生成AIとアート

渡辺はありふれた名前です。どこでもいます。なので、あまり好きじゃありませんでした。ご先祖様には申し訳ないのですが、西園寺、伊集院、小早川、喜多川とか、カッコいいじゃないですか。

ところが、ある日状況は一変しました。同姓同名の人物と出会ったのです。現代アーティストの渡辺英司さんです。名前の漢字は一文字異なりますが、ほぼ同姓同名です。漢字一文字の違いで「司(つかさ)さん」「治(おさむ)さん」と呼び合っています。同姓同名の縁で知り合うことになり、なんと昨年(2025年)には、共同のアート展覧会を開催したのです(大府市大倉公園)。司さんはご自分の現代アートを展示し、私は錯視や自分の研究やイラストを展示しました。アート展覧会では普段の研究生活では得られない刺激を得ることができました。いや〜、楽しかった。急に渡辺という名前が好きになりました。ありふれた名前だったからこその縁です。

実は、私はもともとアート志向がかなり強いほうです。特にポップアート系の作品には強く惹かれます。わたしのイラストもそっち系だとおもっています。そのへんが科学者たる所以なんでしょう。レアで閉じるのではなく、ポップな広がりを常に求めています。

画像生成AIはイラストの創作にかなり使います。「生成AIって誰でも使えるのに、生成AIが作ったイラストは渡辺のイラストなんでしょうか」という疑問を持たれる方もいるかもしれません。もしプロンプトだけで作ったとしたら、オリジナリティあるイラストとはいいがたいでしょう。しかしわたしの場合、まず手描きのイラストを描き上げます。本稿イラストにはすべて渡辺画伯オリジナルの手描きイラストあります。その手描きを生成AIに読み込ませ、プロンプトによって背景などのスタイル調整を施しています。わたしのキャラクターが描けるのはわたしだけで、プロンプトだけでは生み出すことができない世界観です。たとえ生成AIを通したとしても、それは私のアートなのです。

イラスト

第6章:AIとGitHubコードを改造する(ViT編)

これまでの章では、モデルをゼロから作ってきました。CNN、LSTM、VAE、すべて自分たちの手で(AIの助けを借りながら)組み立てました。

しかし、実際の研究や業務では、ゼロから作ることは稀です。世界中の研究者やエンジニアが、自分たちの実装をインターネット上で公開しています。それを活用するほうが、はるかに効率的です。

この章と次の章では、公開されたコードを取得して、自分の目的に合わせて使う方法を学びます。「他人の書いたコードを使う」と聞くと難しそうですが、やることはいつもと同じ。AIに聞きながら、コピー&ペーストで進めます。

GitHubを知っておこう

3分で分かるGitHub

公開コードの多くはGitHub(ギットハブ)というWebサービスで共有されています。AIに聞いてみましょう。

GitHubとは何ですか?

GitHubは、プログラムのコードを保存・共有するためのWebサービスです。コードの『図書館』のようなもので、世界中の開発者が自分のプログラムを公開しています。誰でも見に行って、気に入ったプログラムをコピーして使えます。」

深層学習の最新モデルのほとんどは、論文と一緒にGitHubでコードが公開されています。

Colabでの使い方は2つだけ

GitHub上のコードをGoogle Colabで使う方法は、基本的に2つだけ覚えれば十分です。

# 方法1:pipでインストール(ライブラリとして公開されている場合)
!pip install timm

# 方法2:リポジトリごとダウンロード(ソースコード全体が必要な場合)
!git clone https://github.com/ultralytics/yolov5.git

方法1が使えるならそちらが簡単です。方法2は、設定ファイルやスクリプトが必要なときに使います。どちらを使うかは、GitHubの説明ページ(README)に書いてあります。英語で書かれていることが多いですが、AIに「このREADMEを日本語で要約して」と頼めば大丈夫です。

ライセンスについて:GitHubのコードには利用条件(ライセンス)があります。研究や学習で使う分にはほぼ問題ありませんが、商用利用する場合はライセンスを確認してください。よくわからなければ、AIに「このライセンスで商用利用できますか?」と聞けば教えてくれます。

6.1 Vision Transformer(ViT)を使ってみる

ViTとは何か

この章で使うのはViT(Vision Transformer)という画像認識モデルです。

Vision Transformerとは何ですか?

ViTは、2020年にGoogleが発表した画像認識モデルです。画像を小さなパッチ(断片)に分割して、それぞれの関係性を学習します。大規模データで学習すると、従来のCNNを上回る性能を発揮します。」

ViTの詳しい仕組みは後で説明しますが、まずは動かしてみましょう。

学習済みViTで画像を分類する

AIに頼みます。

timmライブラリを使って、学習済みViTでCIFAR-10の画像を分類するコードを書いてください。まずは事前学習済みモデルをそのまま読み込んで、分類層だけを10クラス用に変更してファインチューニングしてください。

そのままコピー&ペーストで実行してください。 学習には長時間かかります。ランタイムはGPU必須です。

!pip install timm -q
!pip install japanize-matplotlib -q

import torch
import torch.nn as nn
import timm
from torchvision import datasets, transforms
from torch.utils.data import DataLoader
from tqdm import tqdm
import japanize_matplotlib
import matplotlib.pyplot as plt

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
print(f"使用デバイス: {device}")

# ============================
# ステップ1:データ準備
# ============================
# ViTの入力サイズ(224×224)に合わせてリサイズ
transform_train = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.RandomHorizontalFlip(),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

transform_test = transforms.Compose([
    transforms.Resize((224, 224)),
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
])

train_dataset = datasets.CIFAR10('./data', train=True, download=True, transform=transform_train)
test_dataset  = datasets.CIFAR10('./data', train=False, transform=transform_test)

train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True)
test_loader  = DataLoader(test_dataset, batch_size=32, shuffle=False)

classes = ['airplane', 'automobile', 'bird', 'cat', 'deer',
           'dog', 'frog', 'horse', 'ship', 'truck']

print(f"訓練データ: {len(train_dataset)} 枚, テストデータ: {len(test_dataset)} 枚")

# ============================
# ステップ2:学習済みViTを読み込み、分類層を変更
# ============================
# num_classes=10 で、出力層が自動的に10クラス用に変更される
model = timm.create_model('vit_base_patch16_224', pretrained=True, num_classes=10)
model = model.to(device)

# 事前学習済みの層は固定し、分類層だけを学習
for param in model.parameters():
    param.requires_grad = False
for param in model.head.parameters():
    param.requires_grad = True

trainable = sum(p.numel() for p in model.parameters() if p.requires_grad)
total = sum(p.numel() for p in model.parameters())
print(f"学習するパラメータ: {trainable:,} / {total:,} ({100*trainable/total:.2f}%)")

# ============================
# ステップ3:学習
# ============================
criterion = nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.head.parameters(), lr=0.001)

train_acc_list = []
test_acc_list = []

print("\n学習を開始します...")
for epoch in range(5):
    model.train()
    total_loss, correct, total = 0, 0, 0

    for images, labels in tqdm(train_loader, desc=f"エポック {epoch+1}/5 [訓練]"):
        images, labels = images.to(device), labels.to(device)
        optimizer.zero_grad()
        outputs = model(images)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()

        total_loss += loss.item()
        _, predicted = outputs.max(1)
        total += labels.size(0)
        correct += predicted.eq(labels).sum().item()

    train_acc = 100 * correct / total
    train_acc_list.append(train_acc)

    # テスト精度
    model.eval()
    test_correct, test_total = 0, 0
    with torch.no_grad():
        for images, labels in tqdm(test_loader, desc=f"エポック {epoch+1}/5 [テスト]"):
            images, labels = images.to(device), labels.to(device)
            outputs = model(images)
            _, predicted = outputs.max(1)
            test_total += labels.size(0)
            test_correct += predicted.eq(labels).sum().item()

    test_acc = 100 * test_correct / test_total
    test_acc_list.append(test_acc)

    print(f"  エポック {epoch+1}/5  "
          f"訓練精度: {train_acc:.1f}%  "
          f"テスト精度: {test_acc:.1f}%  "
          f"損失: {total_loss/len(train_loader):.4f}\n")

# ============================
# ステップ4:結果の可視化
# ============================
plt.figure(figsize=(8, 5))
plt.plot(range(1, 6), train_acc_list, 'o-', label='訓練精度')
plt.plot(range(1, 6), test_acc_list, 's-', label='テスト精度')
plt.xlabel('エポック')
plt.ylabel('精度 (%)')
plt.title('ViT ファインチューニング(CIFAR-10)')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.savefig('vit_result.png', dpi=150)
plt.show()
print("完了!")

結果を観察しよう

たった5エポック、しかも分類層だけの学習で、かなりの精度が出ているはずです。

これが転移学習の威力です。ViTは数百万枚の画像で何日もかけて学習された重みを持っています。その知識がたった一行(pretrained=True)で手に入り、私たちは最後の仕上げ部分だけを教えればいいのです。

6.2 ViTの「目」を覗いてみる——アテンションの可視化

ViTの面白いところは、モデルが画像のどこに注目しているかを見ることができる点です。

# アテンションの可視化(ステップ3の続きとして実行)
from PIL import Image
import numpy as np

def visualize_attention(model, image_tensor):
    """モデルがどこに注目しているかを可視化する"""
    model.eval()
    with torch.no_grad():
        x = model.patch_embed(image_tensor)
        cls_token = model.cls_token.expand(x.shape[0], -1, -1)
        x = torch.cat((cls_token, x), dim=1)
        x = x + model.pos_embed
        x = model.pos_drop(x)
        for block in model.blocks:
            x = block(x)
        # CLSトークンと各パッチの類似度 → 注目度
        cls_feat = x[0, 0, :]
        patch_feat = x[0, 1:, :]
        sim = torch.nn.functional.cosine_similarity(cls_feat.unsqueeze(0), patch_feat, dim=1)
        return sim.view(14, 14).cpu().numpy()

# 3枚のサンプルで試す
fig, axes = plt.subplots(3, 3, figsize=(12, 12))
fig.suptitle('左=入力画像  中央=注目マップ  右=重ね合わせ', fontsize=13)

for i in range(3):
    idx = np.random.randint(len(test_dataset))
    img, label = test_dataset[idx]
    sample = img.unsqueeze(0).to(device)

    # 予測
    with torch.no_grad():
        pred = model(sample).argmax().item()

    # アテンションマップ
    attn = visualize_attention(model, sample)

    # 元画像の表示用
    disp = img.cpu() * torch.tensor([0.229, 0.224, 0.225]).view(3,1,1)
    disp = disp + torch.tensor([0.485, 0.456, 0.406]).view(3,1,1)
    disp = disp.permute(1,2,0).numpy().clip(0, 1)

    axes[i,0].imshow(disp)
    axes[i,0].set_title(f'正解: {classes[label]}')
    axes[i,0].axis('off')

    axes[i,1].imshow(attn, cmap='hot')
    axes[i,1].set_title('注目マップ')
    axes[i,1].axis('off')

    attn_resized = np.array(Image.fromarray(
        (attn * 255).astype(np.uint8)).resize((224, 224), Image.BILINEAR)) / 255
    axes[i,2].imshow(disp)
    axes[i,2].imshow(attn_resized, cmap='hot', alpha=0.5)
    axes[i,2].set_title(f'予測: {classes[pred]}')
    axes[i,2].axis('off')

plt.tight_layout()
plt.show()

赤く光っている部分が、モデルが注目しているところです。物体の輪郭や特徴的な部分に注目していることが多いはずです。

★読み飛ばしOK★ ViTの仕組み

ViTの内部でどんな処理が行われているか、興味がある方向けに簡単に説明します。

パッチ分割:224×224の画像を、16×16ピクセルの小さな断片(パッチ)に分割します。14×14=196個のパッチができます。これは自然言語処理における「単語」に相当します。

自己注意機構(Self-Attention):各パッチが「他のどのパッチと関連が強いか」を学習します。たとえば、犬の顔のパッチは犬の体のパッチと強く関連し、背景のパッチとは弱く関連する、というように。この仕組みのおかげで、画像内の離れた場所同士の関係も捉えられます。

CLSトークン:196個のパッチに加えて、特別な「まとめ役」のトークンがあります。このトークンが全パッチの情報を集約し、最終的な分類に使われます。

詳しく知りたくなったら、AIに「Transformerの自己注意機構を図で説明して」と聞いてみてください。

第6章のまとめ

この章では、以下のことを体験しました。

やったこと:

覚えておいてほしいこと:

次の章では、物体検出の代表的なモデルYOLOに挑戦します。「画像の中に何がどこにあるか」を見つけるタスクです。

引き続き、AIと一緒に進めていきましょう。


雑談:AIの爆発的発展の秘密

AIは爆発的に発展しましたが、その秘密のひとつが「オープンサイエンス」という考え方です。論文は査読前にすみやかにarXivで公開、プログラムコードはGitHubで公開して誰でも使えるようにします。重要な発見は重宝されるので、ユーザーがどんどん使いますからダウンロード数などで自動的に評価されていきます。間違いがみつかれば、アップデートされます。arXivもGitHubもバージョン管理されていますので、過去から現在までのバージョンをすべて記録されています。

雑誌のブランドにも影響されないし、投稿する側も利用する側も完全に無料です。科学成果を発表する理想の場ではないでしょうか。科学論文はすべてこのような「オープンサイエンス」に統一するべきです。高額の掲載料を要求するような雑誌はこの世に必要ありません。

本稿もarXivと同じ考え方で公開しています。しかし、そんなarXivも転換期にきています。LLMのお陰で投稿される論文がとんでもなく増えているのです。あまりにも増え過ぎたので、論文がどんどん地層に埋もれていきます。しかもレベルの低い論文がかなり多いようです。

それでも私は楽観的です。程度の低い論文が増えてる一方で、程度の高い論文も増えているはずです。程度の低い高いは確率です。母数が増えればどちらも増えているのが自然です。母数が増えたぶん、多様性が大きくなり、大発見も増えてくると期待すべきじゃないでしょうか。

イラスト

第7章:AIとGitHubコードを改造する(YOLO編)

前章では、ViTで画像分類を行いました。この章では、さらに実践的なテーマ「物体検出」に挑戦します。

画像分類は「この画像は何?」と答えるタスクでしたが、物体検出は「画像の中に何がどこにある?」を答えるタスクです。犬と猫が一緒に写っている写真なら、「ここに犬がいて、ここに猫がいる」と位置まで教えてくれます。

自動運転、監視カメラ、医療画像、品質検査——さまざまな分野で使われている、とても実用的な技術です。

7.1 YOLOで物体を検出してみる

YOLOとは

この章で使うのはYOLO(You Only Look Once)という物体検出モデルです。名前の通り「一目見るだけ」で検出が完了する、高速で実用的なモデルです。

GitHubで公開されているYOLOv5を使います(年々バージョンが上がっております)。まずはインストールしましょう。

# ============================
# ステップ1:YOLOv5をインストール
# ============================
!git clone https://github.com/ultralytics/yolov5.git
%cd yolov5
!pip install -r requirements.txt -q

import torch
print(f"PyTorch: {torch.__version__}, CUDA: {torch.cuda.is_available()}")

サンプル画像で検出を試す

YOLOv5には、80種類の物体(人、車、犬、椅子……)を検出できる学習済みモデルが付属しています。まずはこれを試しましょう。

# ============================
# ステップ2:学習済みモデルで検出
# ============================
!python detect.py --weights yolov5s.pt --source data/images/ --conf 0.25

# 結果を表示
from IPython.display import Image, display
import glob

result_dir = sorted(glob.glob('runs/detect/exp*'))[-1]
for img_path in glob.glob(f'{result_dir}/*.jpg')[:3]:
    display(Image(filename=img_path, width=600))

検出された物体が四角い枠(バウンディングボックス)で囲まれ、ラベルと信頼度が表示されます。たった2行のコマンドでここまでできるのは、先人たちが公開してくれたコードのおかげです。

自分の画像で試す

自分の写真でも試してみましょう。

# ============================
# ステップ3:自分の画像をアップロードして検出
# ============================
from google.colab import files

uploaded = files.upload()  # 画像を選択してアップロード

for filename in uploaded.keys():
    !python detect.py --weights yolov5s.pt --source "{filename}" --conf 0.25
    result_dir = sorted(glob.glob('runs/detect/exp*'))[-1]
    for img in glob.glob(f'{result_dir}/*'):
        display(Image(filename=img, width=600))

身の回りの写真を使うと、「これも検出できるのか!」という発見があって面白いです。

Pythonコードから直接使う

コマンドラインだけでなく、Pythonコードからも使えます。検出結果をプログラムで扱いたいときに便利です。

# Pythonから直接使う方法
model = torch.hub.load('ultralytics/yolov5', 'yolov5s', pretrained=True)
results = model('https://ultralytics.com/images/zidane.jpg')

# 検出結果を表で見る
print(results.pandas().xyxy[0])
# xmin, ymin, xmax, ymax(位置)、confidence(信頼度)、name(物体名)が表示される

7.2 自分のデータでYOLOを学習させる

やりたいこと

学習済みモデルの80クラスに含まれない物体を検出したい場合は、自分のデータで学習させます。

YOLOv5には、学習を試すためのサンプルデータが用意されています。まずはこれで練習しましょう。

# ============================
# サンプルデータで学習を体験する
# ============================

# サンプルデータをダウンロード
!python -c "from utils.downloads import attempt_download; attempt_download('coco128.zip', repo='ultralytics/yolov5')"
!unzip -q coco128.zip -d datasets/

# 学習を実行(10エポック、数分で完了)
!python train.py --img 640 --batch 16 --epochs 10 --data coco128.yaml --weights yolov5s.pt --cache

# 学習曲線を表示
result_dir = sorted(glob.glob('runs/train/exp*'))[-1]
display(Image(filename=f'{result_dir}/results.png', width=800))

結果を観察しよう

results.png に学習の進み具合がグラフで表示されます。損失が下がっていれば、学習は成功しています。

学習が完了すると runs/train/exp/weights/best.pt に最も性能が良かったモデルの重みが保存されます。これを使って推論できます。

# 学習したモデルで推論
best_weights = f'{result_dir}/weights/best.pt'
!python detect.py --weights {best_weights} --source data/images/ --conf 0.25

自分のデータを使うには

実際に自分のデータで学習させるには、画像にラベル(物体の位置とクラス)を付ける「アノテーション」という作業が必要です。

アノテーションツールとしてはRoboflow(Webベース、無料プランあり)が初心者に使いやすいです。AIに「Roboflowの使い方を教えて」と聞けば、ステップバイステップで案内してくれます。

データの準備ができたら、学習コマンドの --data に自分の設定ファイルを指定するだけです。学習の手順自体は、上のサンプルと同じです。

★読み飛ばしOK★ 物体検出の仕組み

YOLOがどうやって「何がどこにあるか」を見つけているのか、興味がある方向けに説明します。

バウンディングボックス:物体の位置を四角い枠で表します。中心座標(x, y)と幅・高さの4つの数値で指定します。

信頼度スコア:各検出に「どれくらい確かか」を表す0〜1の値が付きます。低い検出は誤検出の可能性があるため、閾値(--conf 0.25)で除外します。

YOLOの損失関数:学習時には3つの誤差を同時に減らそうとします。「位置のズレ」「物体があるかどうかの判定ミス」「物体の種類の判定ミス」です。

NMS(Non-Maximum Suppression):同じ物体に対して複数の枠が出ることがあるので、重なりが大きい枠を自動的に1つにまとめます。

これらはすべてYOLOv5が内部で処理してくれるので、使う側が意識する必要はほとんどありません。

第7章のまとめ

この章では、物体検出に挑戦しました。

やったこと:

覚えておいてほしいこと:

第6章・第7章を通しての大切なメッセージ:

すべてをゼロから作る必要はありません。世界中の研究者が公開してくれたコードを pip installgit clone で取得し、自分の目的に合わせて使う。それが現代の深層学習の実践的なやり方です。コードを読んで完全に理解する必要もありません。「動かす→結果を見る→必要に応じてAIに聞く」という流れで十分です。

次の章では、効率的な学習環境の構築について学びます。チェックポイントの保存、データ拡張、メモリ不足への対処など、実際にプロジェクトを進めるときに役立つテクニックです。

引き続き、AIと一緒に進めていきましょう。


雑談:ブランド

YOLOは業界的には有名なブランドです。中身はわからなくても、YOLOなら大丈夫という安心感があります。似たような機能をもつAIは多いのですが、確立したブランドの力は大きいようです。

「みんなが知っている」は大きな価値を生みます。例えば、仮想通貨のビットコインは多くのひとが知っています。仮想通貨はソフトウェアなので個人でも作ることができます。いま仮想通貨は2万種類以上あるそうです。しかしその殆どが価値をもっていません。価値があるのはビットコインを始めとする名が通っている一部の仮想通貨だけです。その差は「みんなが知っているかどうか」です。ワタナベコインをつくったところでなんの価値もないのです。

もう少し深堀りしましょう。人が価値を見出す基準は「みんなが知っている」以外になにがあるのでしょう。例えば、ビットコイン、ゴールド、アート作品、食品、株式、映画、科学的発見、自動車など、これらをひとつひとつを精査してみます。すると、7つほどの要素に分けることができました。それは、有名・貴重・換金・歴史・有用・不変・娯楽の7つです。ひょっとしたらまだあるかもしれませんが、私なりに精査した7つです。これら7つをうまく組み合わせると重みづけをすると価値方程式ができあがります。私はこれをワタナベの価値方程式と呼んでいます。

例えば、ゴールド。誰もが知っているので有名性が高く、数量にはかなりの制限があるので貴重性は高く、換金性も高く、有史以来価値を失ったことはないので歴史性も高く、どれだけ時間が経ってもゴールドは変化しないことが保証されていることから不変性も高く、その美しさで宝飾品としての娯楽性もあるのです。有用性だけは微妙ですが、その他は満点に近いのです。ゴールドの価値を数式化できました。実はビットコインも似たような理由で価値をもっています。しかし、いくつかの点でゴールドより劣っているのも一目瞭然です。

どうでしょう。ワタナベの価値方程式でお金儲けをしてみませんか?(笑)。

イラスト

第8章:AIと効率的な学習環境を構築する

ここまでで、いくつものモデルを作って動かしてきました。CNN、LSTM、VAE、ViT、YOLO、振り返ると、ずいぶん遠くまで来ましたね。

この章では、少し趣向を変えます。「モデルを動かす」のではなく、「モデルを動かしやすくする環境」を整えます。

たとえば、Colabがタイムアウトして学習結果が全部消えた経験はありませんか? データが足りなくて困ったことは? そういう「実際にやってみると遭遇する困りごと」に対処するためのテクニックを、AIと一緒に学びます。

この章は、すべてを一度に覚える必要はありません。「こういうことができるんだ」と知っておいて、必要になったときにAIに「やり方を教えて」と聞けば大丈夫です。

8.1 学習結果を消さない: チェックポイントとGoogle Drive

いちばん大切なこと:結果を保存する

Google Colabでいちばんよくある悲劇は、何時間もかけた学習の結果がセッション切れで消えることです。

これを防ぐ方法は2つあります。

  1. チェックポイント:学習の途中経過をファイルに保存する
  2. Google Drive連携:保存先をGoogle Driveにして、セッションが切れても残るようにする

AIに頼みましょう。

PyTorchで学習の途中経過をGoogle Driveに自動保存して、中断しても再開できるコードを書いてください。

以下のコードは、これまでのどの章のモデルにもそのまま適用できるパターンです。

import torch
import os
from google.colab import drive

# ============================
# ステップ1:Google Driveをマウント
# ============================
drive.mount('/content/drive')

# 保存先フォルダを作成
save_dir = '/content/drive/MyDrive/my_dl_project'
os.makedirs(save_dir, exist_ok=True)
print(f"保存先: {save_dir}")

# ============================
# ステップ2:保存と読み込みの関数
# ============================
def save_checkpoint(model, optimizer, epoch, loss, path):
    """学習の途中経過を保存する"""
    torch.save({
        'epoch': epoch,
        'model_state_dict': model.state_dict(),
        'optimizer_state_dict': optimizer.state_dict(),
        'loss': loss,
    }, path)
    print(f"  💾 チェックポイント保存: エポック{epoch}, 損失{loss:.4f}")

def load_checkpoint(path, model, optimizer):
    """保存した途中経過から再開する"""
    checkpoint = torch.load(path)
    model.load_state_dict(checkpoint['model_state_dict'])
    optimizer.load_state_dict(checkpoint['optimizer_state_dict'])
    epoch = checkpoint['epoch']
    loss = checkpoint['loss']
    print(f"  📂 チェックポイント読み込み: エポック{epoch}から再開")
    return epoch

# ============================
# ステップ3:学習ループに組み込む(例)
# ============================
# ※ model, optimizer, criterion, train_loader は事前に定義されているとします

checkpoint_path = f'{save_dir}/checkpoint.pt'
start_epoch = 0

# 前回のチェックポイントがあれば読み込む
if os.path.exists(checkpoint_path):
    start_epoch = load_checkpoint(checkpoint_path, model, optimizer) + 1

# 学習ループ
total_epochs = 50
for epoch in range(start_epoch, total_epochs):
    model.train()
    total_loss = 0
    for images, labels in train_loader:
        images, labels = images.to(device), labels.to(device)
        optimizer.zero_grad()
        outputs = model(images)
        loss = criterion(outputs, labels)
        loss.backward()
        optimizer.step()
        total_loss += loss.item()

    avg_loss = total_loss / len(train_loader)
    print(f"エポック {epoch+1}/{total_epochs}  損失: {avg_loss:.4f}")

    # 5エポックごとに自動保存
    if (epoch + 1) % 5 == 0:
        save_checkpoint(model, optimizer, epoch, avg_loss, checkpoint_path)

print("学習完了!")

ポイント

これは一度覚えてしまえば、どんなプロジェクトでも使えます。この章でいちばん大切な内容なので、ぜひ試してみてください。

8.2 データが足りないとき: データ拡張

よくある問題:データが少ない

深層学習では、データの量が性能を大きく左右します。でも、十分なデータを集めるのは簡単ではありません。

そんなとき、最も手軽で効果的な方法がデータ拡張(Data Augmentation)です。手持ちの画像を回転、反転、色調変化などで加工して、バリエーションを増やします。

PyTorchのtransformsを使って、画像のデータ拡張を行うコードを書いてください。元画像と拡張後の画像を並べて表示してください。

import torch
from torchvision import transforms, datasets
!pip install japanize-matplotlib -q
import japanize_matplotlib
import matplotlib.pyplot as plt

# ============================
# データ拡張の設定
# ============================
# 「学習時に毎回少し違う画像を見せる」ことで、モデルの汎用性が上がる
train_transform = transforms.Compose([
    transforms.RandomRotation(15),          # ±15度回転
    transforms.RandomHorizontalFlip(),       # 左右反転(50%の確率)
    transforms.ColorJitter(brightness=0.2, contrast=0.2),  # 明るさ・コントラスト変化
    transforms.ToTensor(),
])

# 比較用:拡張なし
simple_transform = transforms.Compose([
    transforms.ToTensor(),
])

# ============================
# 同じ画像に拡張を適用した結果を並べて見る
# ============================
dataset = datasets.MNIST('./data', train=True, download=True, transform=simple_transform)
original_image = dataset[0][0]  # 最初の画像

# データ拡張を10回適用
aug_transform = transforms.Compose([
    transforms.ToPILImage(),
    transforms.RandomRotation(15),
    transforms.RandomAffine(degrees=0, translate=(0.1, 0.1)),
    transforms.ToTensor(),
])

fig, axes = plt.subplots(1, 11, figsize=(15, 2))
fig.suptitle('左端=元画像、右10枚=データ拡張で生成したバリエーション', fontsize=11)
axes[0].imshow(original_image.squeeze(), cmap='gray')
axes[0].set_title('元', fontsize=9)
axes[0].axis('off')

for i in range(10):
    augmented = aug_transform(original_image)
    axes[i+1].imshow(augmented.squeeze(), cmap='gray')
    axes[i+1].set_title(f'拡張{i+1}', fontsize=8)
    axes[i+1].axis('off')

plt.tight_layout()
plt.show()

結果を観察しよう

同じ数字が少しずつ違う角度・位置で表示されます。人間の目には「全部同じ数字」ですが、モデルにとっては「違う入力」です。これにより、「少し傾いていても正しく認識できる」頑健なモデルが育ちます。

データ拡張は「嘘のデータ」ではありません。実世界では文字が傾いていたり、写真の明るさが違ったりするのが普通です。むしろ、「現実に近いバリエーション」を学習データに加えているのです。

もっとデータが必要なとき

データ拡張だけでは足りない場合もあります。そのときは:

どちらも「〇〇のデータセットを探したい」「転移学習のやり方を教えて」とAIに聞けば、具体的な方法を教えてくれます。

8.3 学習が進まない・遅いとき: AIに相談する

まず確認:GPUを使っていますか?

学習が遅い場合、いちばんよくある原因はGPUが有効になっていないことです。

import torch

print(f"CUDA利用可能: {torch.cuda.is_available()}")
if torch.cuda.is_available():
    print(f"GPU: {torch.cuda.get_device_name(0)}")
    print(f"メモリ: {torch.cuda.get_device_properties(0).total_memory / 1e9:.1f} GB")
else:
    print("⚠️ GPUが使えません!Colabの設定を確認してください")
    print("  → メニュー「ランタイム」→「ランタイムのタイプを変更」→ GPU を選択")

CUDA利用可能: False と出たら、GPUが有効になっていません。メニューからランタイムの設定を変更してください。

よくあるエラー:CUDA out of memory

GPUを使っていても「CUDA out of memory」というエラーが出ることがあります。これは、GPUのメモリが足りないという意味です。

対処法は簡単です:

これ以上の高度な対処(混合精度学習、勾配累積など)もありますが、まずは上の3つを試してみてください。それでも解決しない場合は、エラーメッセージをそのままAIに貼り付けて相談するのが最も効率的です。

学習を自動的に止める: Early Stopping

何エポックまで学習すればいいか、事前にはわかりません。学習しすぎると、訓練データには合うのにテストデータでの性能が下がる「過学習」が起きます。

Early Stoppingは、「検証損失が改善しなくなったら自動的に学習を止める」仕組みです。

class EarlyStopping:
    """改善が止まったら学習を自動終了する"""
    def __init__(self, patience=5):
        self.patience = patience  # 何回改善なしで待つか
        self.counter = 0
        self.best_loss = None

    def __call__(self, val_loss):
        if self.best_loss is None or val_loss < self.best_loss:
            self.best_loss = val_loss
            self.counter = 0
        else:
            self.counter += 1
            if self.counter >= self.patience:
                print(f"Early Stopping: {self.patience}エポック改善なし → 学習終了")
                return True
        return False

# 使い方(学習ループの中で)
# early_stopping = EarlyStopping(patience=5)
# for epoch in range(100):
#     train_loss = ...(学習)
#     val_loss = ...(検証)
#     if early_stopping(val_loss):
#         break

patience=5 は「5エポック連続で改善がなければ止める」という意味です。この数値は変えて構いません。

8.4 ★発展★ もっと効率を上げたいとき

ここから先は、必要になったときに戻ってくれば十分な内容です。すべてを今すぐ覚える必要はありません。

実験管理ツール

複数のパラメータ設定を試していると、「どの設定でどんな結果だったか」がわからなくなります。実験管理ツールを使うと、学習曲線やパラメータを自動で記録・比較できます。

代表的なツール:

どちらも「TensorBoardの使い方を教えて」「W&Bの設定方法を教えて」とAIに聞けば、すぐに始められるコードを生成してくれます。最初はTensorBoardから試すのがお勧めです。

混合精度学習・勾配累積

学習をさらに高速化したり、大きなモデルをGPUに載せたりするための高度なテクニックです。

どちらも「PyTorchで混合精度学習を使うコードを教えて」とAIに聞けば、数行の追加で実現できます。ただし、本書で扱うレベルのモデルでは必要になることは少ないでしょう。

論文品質の図表

研究発表や論文を書くときには、きれいなグラフが必要です。

学習曲線を論文に載せられる品質のグラフにするコードを書いてください

とAIに頼めば、フォントサイズ、解像度、凡例の位置まで調整されたmatplotlibのコードを生成してくれます。LaTeX形式の表を作ることもできます。これらは必要になったときにAIに相談すれば十分です。

再現性の確保

実験の結果を他の人が再現できるようにしておくことは、研究では重要です。最低限やるべきことは一つ:乱数シードの固定です。

import torch, numpy as np, random

def set_seed(seed=42):
    """すべての乱数を固定して、結果を再現可能にする"""
    random.seed(seed)
    np.random.seed(seed)
    torch.manual_seed(seed)
    if torch.cuda.is_available():
        torch.cuda.manual_seed_all(seed)
        torch.backends.cudnn.deterministic = True
        torch.backends.cudnn.benchmark = False
    print(f"乱数シード固定: {seed}")

# 学習の最初に1回呼ぶだけ
set_seed(42)

学習コードの冒頭で set_seed(42) と書くだけです。これだけで、同じコードを実行すれば同じ結果が得られるようになります。

第8章のまとめ

この章では、深層学習を「続けていく」ための実践的なテクニックを学びました。

今すぐ使うべきもの:

必要になったら戻ってくるもの:

覚えておいてほしいのは、これらのテクニックはすべて、AIに聞けばコードを書いてもらえるということです。「チェックポイントの保存方法を教えて」「CUDA out of memoryが出ました」「Early Stoppingを組み込みたい」——こう聞くだけで、あなたのコードに合った具体的な解決策が返ってきます。

次の章では、デバッグとトラブルシューティングを学びます。「エラーが出た」「学習が進まない」「GitHubのコードが動かない」——こうした問題を、AIと一緒に解決する方法です。

引き続き、AIと一緒に進めていきましょう。


雑談:本の書きかた

本書を読み進めながら、こんなのだったら私にも書けるんじゃないかと思われているかたもおられるかもしれません。そのような方のために参考までに本書ができるまでのステップを備忘録的に記録しておきます。

これで原稿は完成して、Github Pageで公開しました。自分が書きたいものがあるとか、誰かにどうしても伝えたいものがある場合、ぜひチャレンジしてみてください。

イラスト

第9章:AIとデバッグ・トラブルシューティング

プログラミングにエラーはつきものです。これは、初心者だからではありません。ベテランのプログラマーでも、毎日のようにエラーに遭遇します。

違いは、「エラーとの付き合い方」を知っているかどうかです。

エラーメッセージを恐れる必要はありません。エラーは、プログラムがあなたに送る「ここがおかしいよ」というメッセージです。このメッセージを読み解き、問題を解決する——これがデバッグです。

この章では、深層学習でよく遭遇するエラーや問題を、AIと一緒に解決する方法を学びます。そして、第1章から繰り返してきた「テストコードも一緒に頼む」という習慣の意味を、改めて確認します。

9.1 「エラーが出た」をAIに伝える

エラーメッセージの読み方

Pythonでエラーが発生すると、こんな表示が出ます。

---------------------------------------------------------------------------
ValueError                  Traceback (most recent call last)
<ipython-input-5-abc123> in <module>
      3 x = torch.randn(10, 5)
      4 y = torch.randn(10, 3)
----> 5 z = x + y

ValueError: operands could not be broadcast together with shapes (10,5) (10,3)

怖く見えますが、読み方は簡単です。

この例では「形状(10,5)と(10,3)のテンソルは足し算できない」と教えてくれています。

実際にエラーを出してみましょう。

# わざとエラーを出してみる(実行してみてください)
my_list = [1, 2, 3]
print(my_list[10])   # → IndexError: list index out of range

result = "hello" + 123  # → TypeError: can only concatenate str (not "int") to str

英語ですが、慣れれば読めるようになります。そして、分からなければAIに聞けばいいのです。

AIへの効果的な伝え方

エラーが出たとき、AIにどう伝えるかで、回答の質が変わります。

良くない例:

エラーが出ました。直してください。

これでは、AIも何が問題か分かりません。

良い例:

PyTorchで画像分類モデルを作っています。損失計算のところで以下のエラーが出ました。

[エラーメッセージ全文をコピペ]

該当するコード:[コードの該当部分]

やりたいこと:予測とラベルの損失を計算したい

ポイントは3つです。エラーメッセージ全文をコピペする。該当するコードを含める。何をしようとしていたかを説明する。この3つがあれば、AIはほぼ確実に的確な回答をしてくれます。

深層学習でよくあるエラー4選

ここで、深層学習でよく遭遇するエラーとその対処法をまとめておきます。どれも一度は出会うエラーなので、「ああ、これか」と思えるだけで気が楽になります。

① メモリ不足

RuntimeError: CUDA out of memory. Tried to allocate 2.00 GiB

GPUのメモリが足りません。対処法はシンプルです。

# バッチサイズを小さくする(64→32→16と試す)
batch_size = 16

# 画像サイズを小さくする
transforms.Resize((128, 128))  # 224→128

# 不要な変数を消す
del unnecessary_tensor
torch.cuda.empty_cache()

# 推論時は勾配計算を切る
with torch.no_grad():
    output = model(input)

② 次元不一致

RuntimeError: mat1 and mat2 shapes cannot be multiplied (64x512 and 256x10)

層の入出力サイズが合っていません。各テンソルの形状をprintで確認しましょう。

# どこで形状がおかしくなるか確認
print(f"入力: {x.shape}")
x = model.conv1(x)
print(f"conv1後: {x.shape}")
# ... 問題の箇所を特定したら、層のサイズを修正

③ GPU↔︎CPU変換エラー

TypeError: can't convert cuda:0 device type tensor to numpy.

GPUにあるテンソルをそのままNumPyに変換しようとしました。

# 誤り
array = tensor.numpy()

# 正しい(GPU→CPU→NumPy の順)
array = tensor.detach().cpu().numpy()

④ デバイス不一致

RuntimeError: Expected all tensors to be on the same device, but found at least two devices, cuda:0 and cpu!

一部のデータがGPU、一部がCPUにあります。すべて同じデバイスに揃えましょう。

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')
model  = model.to(device)
inputs = inputs.to(device)
labels = labels.to(device)

デバッグの基本テクニック

エラーの原因がわからないとき、以下の方法が役立ちます。

# テクニック1:printで中間値を確認する
print(f"x の形状: {x.shape}")
print(f"x の型: {x.dtype}")
print(f"x のデバイス: {x.device}")
print(f"x の値の範囲: {x.min():.4f} ~ {x.max():.4f}")

# テクニック2:1バッチだけで試す
images, labels = next(iter(train_loader))
output = model(images.to(device))
print(f"入力: {images.shape} → 出力: {output.shape}")

# テクニック3:Colabでコードを小さなセルに分けて1行ずつ実行する

9.2 「学習が進まない」をAIと解決する

学習曲線を見る習慣

エラーは出ないけど学習がうまくいかない。これも深層学習ではよくあることです。

大切なのは、学習曲線を見る習慣をつけることです。損失や精度の推移をグラフにすると、何が起きているか分かります。

!pip install japanize-matplotlib -q
import japanize_matplotlib
import matplotlib.pyplot as plt

def plot_learning_curves(train_losses, val_losses):
    """学習曲線を描画する"""
    fig, ax = plt.subplots(figsize=(8, 4))
    epochs = range(1, len(train_losses) + 1)
    ax.plot(epochs, train_losses, 'b-', label='訓練')
    ax.plot(epochs, val_losses, 'r-', label='検証')
    ax.set_xlabel('エポック')
    ax.set_ylabel('損失')
    ax.set_title('学習曲線')
    ax.legend()
    ax.grid(True)
    plt.show()

# 使い方:各エポックの損失をリストに貯めて渡す
# plot_learning_curves(train_losses, val_losses)

学習曲線のパターンと対処法

学習曲線には典型的なパターンがあります。AIに相談するときも、「こういうパターンになっている」と伝えると、的確なアドバイスがもらえます。

パターン1:損失が全く減らない(横ばい)

損失: ████████████████████████ (変化なし)

学習率が小さすぎるか、データが正しく読み込まれていない可能性があります。まず学習率を上げてみましょう(0.001→0.01)。データの値の範囲やラベルも確認してください。

パターン2:損失がNaN(数値エラー)になる

損失: 0.5 → 0.3 → 0.2 → nan → nan

学習率が大きすぎます。学習率を下げる(0.001→0.0001)か、勾配クリッピングを追加しましょう。

# 勾配クリッピング:勾配が大きくなりすぎるのを防ぐ
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)

パターン3:訓練損失は減るが、検証損失が増える(過学習)

訓練損失: ████████░░░░░░░░ (減少)
検証損失: ░░░░████████████ (増加)

モデルが訓練データを「暗記」してしまっています。ドロップアウト、データ拡張、Early Stopping(第8章参照)が有効です。

# ドロップアウト:学習中にランダムにニューロンを無効化する
self.dropout = nn.Dropout(0.5)

# 正則化:重みが大きくなりすぎるのを防ぐ
optimizer = torch.optim.Adam(model.parameters(), lr=0.001, weight_decay=1e-4)

パターン4:訓練精度も検証精度も低いまま(未学習)

精度: ░░░░░░░░░░░░░░░░ (50%付近で停滞)

モデルが単純すぎるか、学習率が不適切です。より大きなモデルを試すか、学習済みモデルの転移学習(第6章参照)を検討してください。

9.3 「GitHubのコードが動かない」

いちばん多い原因:バージョンの不一致

GitHubからコードをダウンロードしても動かないことがあります。最も多い原因はライブラリのバージョンの不一致です。

# こんなエラーが出たら、バージョンの問題を疑う
ModuleNotFoundError: No module named 'timm'     # → そもそもインストールされていない
AttributeError: module 'torch' has no attribute 'compiler'  # → バージョンが違う

対処法

# まず現在の環境を確認する
import sys, torch
print(f"Python: {sys.version}")
print(f"PyTorch: {torch.__version__}")

# requirements.txt があればそれに従う
!pip install -r requirements.txt

# 特定のバージョンが必要な場合は明示的にインストール
!pip install torch==2.0.0 torchvision==0.15.0 --quiet

古いコードを動かすコツ

9.4 テストコードを書く習慣をもう一度

なぜテストが大切なのか

第1章で「テストコードも一緒に頼む」と学びましたね。ここまで各章でもテストコードを書いてきました。改めて、なぜテストが大切なのかを確認しましょう。

AIにテストを頼む例

AIにコードを頼むときは、いつでもテストを一緒に頼みましょう。

画像を読み込んでリサイズする関数を作ってください。正しく動くか確認するテストコードも一緒にお願いします。

AIはこんなコードを返してくれます。

from PIL import Image
import os

def load_and_resize(image_path, size=(224, 224)):
    """画像を読み込んでリサイズする"""
    if not os.path.exists(image_path):
        raise FileNotFoundError(f"ファイルが見つかりません: {image_path}")
    image = Image.open(image_path).convert('RGB')
    return image.resize(size)

# --- テストコード ---
def test_load_and_resize():
    # テスト用画像を作成
    test_img = Image.new('RGB', (100, 100), color='red')
    test_img.save('test_tmp.png')

    try:
        result = load_and_resize('test_tmp.png', size=(50, 50))
        assert result.size == (50, 50), "サイズが違う"
        assert result.mode == 'RGB', "モードが違う"
        print("✅ リサイズテスト: 合格")

        try:
            load_and_resize('存在しないファイル.png')
            assert False, "例外が発生すべき"
        except FileNotFoundError:
            print("✅ エラー処理テスト: 合格")
    finally:
        os.remove('test_tmp.png')

test_load_and_resize()

関数本体とテストコードがセットで返ってきます。テストを実行して「✅ 合格」と出れば、安心して使えます。

この「コードとテストをセットで頼む」習慣は、どんなプログラミングでも役に立つ、一生モノのスキルです。

第9章のまとめ

この章では、エラーやトラブルとの付き合い方を学びました。

エラーへの対処:

学習の問題への対処:

GitHubコードの問題:

テストの習慣:

エラーやトラブルは、学習プロセスの一部です。恐れずに向き合い、AIの助けを借りながら解決していきましょう。

次の章では、学んだことを研究や実務に展開する方法を扱います。

引き続き、AIと一緒に進めていきましょう。


雑談:LLMが苦手なこと

いかにも万能そうにみえるLLMですが、苦手なことはいっぱいあります。例えば、論理的なジャンプは苦手なことのひとつです。

例えば、人間と人間が会話をしているとき、言葉のキャッチボールが延々に続きます。奥様方の井戸端会議などはそれです。あるいはおじさんたちの居酒屋での会話もそうでしょう。何時間も喋っているうちに夜が明けたりします。よくもまあそんなに喋ることがあるなとおもいますが、人間は体力の続く限り延々と続けることができます。話している内容は、どんどん変化していきます。ときには「ちょっと話は変わるけどさー」とか言って、わざと大きなジャンプを引き起こします。

しかしLLM同士の会話はそうはいきません。LLM同士で会話をすると最後はおなじ内容を無限ループするようになります。論理的なジャンプができないからです。LLMは自然な言葉の流れを紡ぎ出すことを学習しているので、構造的に論理的なジャンプができない仕組みになっているのです。

これはLLMが人間とチャットしているときにも起こる現象です。LLMが同じようなことを繰り返すようなときは要注意です。あなたが積極的に論理をジャンプさせて無限ループを断ち切る必要があります。LLMは広大な世界モデルですが、自分だけの力では広大な世界を活かしきれません。あなたの言葉がチャットの一部であることをもう一度思い起こしてください。

イラスト

第10章:AIと研究・実務に展開する

ここまで、長い旅をしてきました。

Google Colabの使い方から始まり、画像処理の基本、CNNによる画像分類、動画処理、VAEによる生成モデル、ViTとYOLOの実践的な活用、効率的な学習環境の構築、そしてデバッグとトラブルシューティング——すべてをAIと一緒に学んできました。

この最終章では、学んだことをどのように研究や実務に活かしていくか、そしてこれからのAIとの付き合い方について考えます。

10.1 学んだことを整理する

この本で身につけたスキル

振り返ってみましょう。あなたがこの本を通じて身につけたスキルは、大きく2つに分かれます。

技術的なスキル——Google ColabでPython+PyTorchを使い、CNN・LSTM・VAE・ViT・YOLOといったモデルを動かせるようになりました。GitHubのコードを取得してファインチューニングし、テストコードで検証し、エラーが出たらデバッグする方法も学びました。

AIとの協働スキル——やりたいことを具体的に伝え、コードを書いてもらい、動かして結果を観察する。分からなければ質問し、エラーが出たらメッセージごとAIに渡す。テストコードも一緒に頼む。この一連の流れが自然にできるようになっています。

「分からないことが分かる」という成長

この本を読み終えた今、あなたは以前とは違う状態にあります。

以前は、「深層学習」と聞いても、何が分からないのかすら分からなかったかもしれません。CNNとViTの違いも、オートエンコーダとVAEの違いも、知らなかったかもしれません。

今は違います。

「CNNは局所的な特徴を階層的に抽出するモデルで、ViTは画像をパッチに分けて自己注意機構で処理するモデル」——こう説明できるようになっています。

そして、自分が何を分かっていて、何を分かっていないか、区別できるようになっています。

「分からないことが分かる。」これは大きな成長です。分からないことが分かれば、AIに質問できます。調べることができます。学ぶことができます。

10.2 研究への応用

論文を読む力

深層学習の研究論文は、最初は難しく感じるかもしれません。しかし、この本で学んだ知識があれば、かなり読めるようになっています。

分からない用語が出てきたら、AIに聞きましょう。

この論文に出てくる「Skip Connection」とは何ですか?図を使って説明してください。

この論文の「Loss Function」の式の意味を、初心者向けに解説してください。

AIは、あなたの理解度に合わせて説明してくれます。

論文のコードを動かす

多くの研究論文は、GitHubでコードを公開しています。第6章と第7章で学んだスキルを使えば、これらのコードを動かすことができます。

# 論文のリポジトリをクローン
!git clone https://github.com/著者名/論文のリポジトリ.git

# READMEを確認
!cat 論文のリポジトリ/README.md

# 必要なライブラリをインストール
!pip install -r 論文のリポジトリ/requirements.txt

コードが動かない場合は、第9章で学んだデバッグ技法を使い、AIに相談しましょう。

自分の研究に応用する

学んだモデルを、自分の研究テーマに応用することもできます。生物学なら細胞画像の分類や顕微鏡画像の物体検出、医学ならX線画像の異常検出、工学なら製品の欠陥検出——あらゆる分野で深層学習は活用されています。

AIに相談してみましょう。

私は〇〇の研究をしています。深層学習を使って〇〇を解決したいのですが、どのようなアプローチが考えられますか?

AIは、あなたの研究分野に適したアプローチを提案してくれます。

10.3 実務への応用

プロトタイプを作る

実務で深層学習を活用する第一歩は、プロトタイプを作ることです。

完璧なシステムを最初から作る必要はありません。まずは、「こんなことができそう」を示す小さなデモを作りましょう。たとえば:

  1. 自分のデータを数十枚集める
  2. 転移学習でモデルを作る(第6章・第7章の方法で)
  3. Gradioで簡単なWebインターフェースを作る

Gradioというライブラリを使うと、画像をアップロードして結果を表示するデモが数行で作れます。AIに「Gradioで画像分類のデモを作って」と頼めば、すぐに使えるコードを生成してくれます。

このようなプロトタイプを見せることで、関係者に「深層学習でこんなことができる」と伝えることができます。

本番システムへの道

プロトタイプが評価されたら、本番システムに向けて進みます。モデルのデプロイ、APIの設計、データパイプラインの構築——ここからはこの本の範囲を超えますが、これらもAIに聞きながら学ぶことができます。

10.4 継続的な学習

深層学習は進化し続ける

深層学習の分野は、驚くべき速さで進化しています。Transformer、GPT、Stable Diffusion、マルチモーダルモデル——次々と新しい技術が登場しています。

この流れはこれからも続きます。しかし、恐れる必要はありません。

学び続けるためのヒント

基礎を大切にする——新しいモデルが登場しても、基礎は変わりません。この本で学んだCNN、Transformer、損失関数、最適化——これらの理解があれば、新しい技術も理解しやすくなります。

手を動かす——論文やブログを読むだけでなく、実際にコードを動かしましょう。動かすことで、理解が深まります。

AIを活用する——新しい技術が出てきたら、AIに聞いてみましょう。

最近話題の「〇〇」とは何ですか?従来の手法との違いを教えてください。

「〇〇」を試すためのサンプルコードを書いてください。

コミュニティに参加する——Kaggleのコンペティション、勉強会、X(Twitter)での情報収集など。一人で学ぶより、仲間がいると続けやすいです。

最新情報は、arXivやPapers with Codeで論文を追ったり、各社の技術ブログを読んだりすることで得られます。全部追う必要はありません。興味のある分野に絞って、無理のないペースで。

10.5 AIとの付き合い方

AIは「道具」か「パートナー」か

この本を通じて、AIと一緒にコードを書いてきました。AIは、あなたにとってどんな存在でしたか?

ある人は「便利な道具」と感じたかもしれません。Google検索の延長線上にある、高性能な情報ツール。

別の人は「頼れるパートナー」と感じたかもしれません。分からないことを教えてくれ、一緒に問題を解決してくれる存在。

どちらの捉え方も正しいです。大切なのは、AIを使いこなすという姿勢です。

AIの限界を知る

しかし、AIは万能ではありません。AIの回答を鵜呑みにしない。重要な判断は自分で確認する。コードはテストして動作確認する。この本で繰り返してきた姿勢は、そのまま「AIの限界と付き合う方法」でもあります。

これからのAIと人間

AIは、これからもっと賢くなっていくでしょう。できることが増え、精度も上がり、より自然に対話できるようになるでしょう。

しかし、それは人間が不要になることを意味しません。

AIは、人間の能力を拡張するツールです。AIを使いこなすことで、一人の人間ができることの範囲が大きく広がります。

この本で学んだ「AIと一緒にプログラミングする」スキルは、これからの時代、ますます重要になっていくでしょう。


おわりに

この本を最後まで読んでくださり、ありがとうございます。

振り返れば、長い旅でした。Google Colabで最初のprint(“Hello, Colab!”)を実行したときから、YOLOで物体検出ができるようになるまで——多くのことを学びました。

もしかすると、まだ理解が不十分な部分もあるかもしれません。コードを見ても、何をしているのか100%は分からないかもしれません。

それで大丈夫です。

プログラミングは、一度読んだだけで完璧に理解できるものではありません。実際に手を動かし、エラーに遭遇し、解決し、また新しいことを試す——その繰り返しで、少しずつ理解が深まっていきます。

そして、分からないことがあれば、いつでもAIに聞けばいいのです。

「AIに聞けばいい」——これが、この本を通じて最も伝えたかったことです。

深層学習の知識も、Pythonの文法も、エラーの解決方法も、すべてAIに聞くことができます。もはや、一人で抱え込む必要はありません。

あなたの研究や仕事で、深層学習が役立つことを願っています。

そして、これからもAIと一緒に、新しいことに挑戦し続けてください。

がんばってください!


おわりの雑談

お疲れ様でした!これにて本稿はおわりです。

ひょっとしたら気が付かないうちに、初学者のかたにとってはハードルを高くした原稿になってしまったかもしれません。どんどん改善していきますのでフィードバックをいただければ原稿をアップデートしています。遠慮なくご意見をおよせください。

さて、雑談の最後は前著「神経科学者と学ぶ深層学習超入門」と同じくドラえもんで締めます。前著では「ドラえもんはのび太の親友です、AIをドラえもんのようにパートナーとして迎えましょう」としました。

藤子・F・不二雄の想いは明確です。「出自が違えど、心が通えばそれはどんなものであっても親友になりえる」です。オバケのQ太郎もおなじ発想です。あなたが味方だとおもえば、かけがえのないパートナーになります。

この考え方は今でもかわりません。しかし、ひとつ注意を付け加えておきます。それは「今のLLMはドラえもんではない」ということです。あまりにも流暢にコミュニケーションがとれるので勘違いしてしまいますが、LLMはトランスフォーマーという仕組みで動いているAIであり、その域をでることはありません。私たちとは似て非なる存在です。高性能なツールとして、トランスフォーマーには独特な限界があり、その癖を知ったうえで、高性能なツールとして使うべきでしょう。またLLMのアウトプットは非常に高速ですが、主人公はあなたですからあなたのペースは守るようにしましょう。

今後、ドラえもんのようなAIが現れるか否かはわかりません。でも、そのときが来るのを私はドキドキしながら待っています。

イラスト

おすすめの入門書 4選

ゼロから作るDeep Learning

斉藤康穀/著, 2016年, オライリージャパン この本を2016年に出版している先見性に脱帽です。名著です。シリーズ化されています。

Python,TensorFlowで実践する深層学習入門

J.Krohn/著, 2022年, 東京化学同人 私が一番好きな本です。名著です。イラストが可愛いです。

ディープラーニングを支える技術

岡野原大輔/著, 2022年, 技術評論社 比較的最新のところまで網羅している名著です。シリーズ化されています。

神経科学者と学ぶ深層学習

渡辺英治/著, 2025年, 羊土社 本稿の鋳型になった本。構成はこの本を踏襲しています。本文にまで著者作の可愛いイラストがあふれています。ニューロンから深層学習を解き明かす試み。自分の本なので名著とは言いません。


著者

大阪大学理学部生物学科卒、同基礎工学部生物工学科にて学位取得。工学博士。愛知県発達障害研究所の研究員を経て、基礎生物学研究所の助教、准教授。神経生理学研究室の室長ならびに超階層生物学センターAI解析室の室長を兼務。総合研究大学大学院も兼務。

専門は神経科学。研究テーマはニューロンの働きを基盤にして脳を理解すること。知覚や思考を人工ニューロンでモデル化し構成論的な理解を目指しています。趣味は音楽鑑賞(ロックンロール)と野生の錯視を見つけることとイラスト描き(本稿のイラストは渡辺画伯作、ChatGPTでスタイルを改修)。

イラスト