2024年2月7日水曜日

Transformers で LLM を使って日本語の文章を簡単に解析する方法

Transformers で LLM を使って日本語の文章を簡単に解析する方法

概要

Transformers は HuggingFace が提供するライブラリで公開されている LLM を元で簡単に動かすことができるツールを提供してくれています
今回は導入ということで基本の pipeline 機能を試してみました

環境

  • macOS 11.7.10
  • Python 3.11.6
    • transformers 4.37.2

インストール

今回は日本語を解析するので日本語解析に必要なライブラリも一緒にインストールできるパッケージを使います

  • pipenv install transformers["ja"]

マスク文字の予測

[MASK] としていした文章がある場合にその部分にどんな単語が入るのが正しいのかを予測してくれます
なお指定した日本語の LLM として有名な cl-tohoku さんのモデルを今回は使用します

マスク文字を予測する場合は bert-base-japanese-whole-word-masking というモデルを使用します

from transformers import AutoModel, AutoTokenizer, pipeline

model_name = "cl-tohoku/bert-base-japanese-whole-word-masking"
model = AutoModel.from_pretrained(model_name)
tokenizer = AutoTokenizer.from_pretrained(model_name)

# pipe = pipeline("fill-mask", model=model, tokenizer=tokenizer)  # なぜかmodelを渡すとKeyError: 'logits'になるので注意
pipe = pipeline("fill-mask", model=model_name, tokenizer=tokenizer)
print(pipe("吾輩は[MASK]である。"))
[{'score': 0.7356272339820862, 'token': 6040, 'token_str': '猫', 'sequence': '吾輩 は 猫 で ある 。'}, {'score': 0.08630602061748505, 'token': 2928, 'token_str': '犬', 'sequence': '吾輩 は 犬 で ある 。'}, {'score': 0.010390125215053558, 'token': 10082, 'token_str': '狼', 'sequence': '吾輩 は 狼 で ある 。'}, {'score': 0.009217966347932816, 'token': 1410, 'token_str': '人間', 'sequence': '吾輩 は 人間 で ある 。'}, {'score': 0.007725571747869253, 'token': 6259, 'token_str': '僕', 'sequence': '吾輩 は 僕 で ある 。'}]

文章のネガポジ判定

その文章がネガティブなのかポジティブなのかを判定することができます

トークナイザーは先程と同じですがモデルはネガポジ用に学習しなおした kit-nlp/bert-base-japanese-sentiment-irony を使います
先程のマスク埋めのモデルを使っても分類はできますが分類先の情報などを持っていないため LABEL_0 などと表示されてしまいます

from transformers import AutoModel, AutoTokenizer, pipeline

model_name = "kit-nlp/bert-base-japanese-sentiment-irony"
tokenizer_name = "cl-tohoku/bert-base-japanese"
model = AutoModel.from_pretrained(model_name)
tokenizer = AutoTokenizer.from_pretrained(model_name)

pipe = pipeline("sentiment-analysis", model=model_name, tokenizer=tokenizer)
print(pipe("今日は天気がよく散歩をしたので気分がいいです。"))
print(pipe("今日は天気が悪く散歩ができなかったので気分が悪いです。"))
[{'label': 'ポジティブ', 'score': 0.7811232209205627}]
[{'label': 'ネガティブ', 'score': 0.6790777444839478}]

文章生成

与えた文字列から始まる文章を生成してくれます
一昔前のスパム文章のような感じもします
トークナイザーは同じで文章生成のモデルは rinna/japanese-gpt2-small を使います

from transformers import AutoModel, AutoTokenizer, pipeline

model_name = "rinna/japanese-gpt2-small"
tokenizer_name = "cl-tohoku/bert-base-japanese"
model = AutoModel.from_pretrained(model_name)
tokenizer = AutoTokenizer.from_pretrained(model_name)

pipe = pipeline("text-generation", model=model_name, tokenizer=tokenizer)
print(pipe("こんにちは。始めまして。"))
[{'generated_text': 'こんにちは。始めまして。私は、今日が、私の最初の仕事でした。この仕事は、ある方々のご支援を受けながら、今の仕事を続けてい
  ことができました。本当にありがとうございました。私は、ここで、たくさん'}]

トラブルシューティング

文章生成で以下のようなエラーになる場合は protobuf をインストールしましょう

  • pipenv install protobuf
ImportError:
T5Converter requires the protobuf library but it was not found in your environment. Checkout the instructions on the
installation page of its repo: https://github.com/protocolbuffers/protobuf/tree/master/python#installation and follow the ones
that match your environment. Please note that you may need to restart your runtime after installation.

モデルの保存場所

基本は自動でダウンロードされキャッシュされます
キャッシュが優先されて使われるので不要であれば削除しましょう

  • /home/user/.cache/huggingface/hub/.locks/models--cl-tohoku--bert-base-japanese-whole-word-masking

最後に

Transformers の pipeline 機能を使って LLM を使った日本語の簡単な解析方法を紹介しました

pipeline には他にも機能があるので興味があれば試してみてください
ただ機能ごとに使用するモデルが変わるのでそこだけ注意してください
モデルが違うとうまく動作しないことがあります

モデルをうまく探すコツは HaggingFace にタグがあるのでそれを使うと良いかなと思います
https://huggingface.co/models?pipeline_tag=text-generation

参考サイト

2024年2月6日火曜日

OpenAI Gym 改め Gymnasium で強化学習入門

OpenAI Gym 改め Gymnasium で強化学習入門

概要

過去 に軽く強化学習に触れました再度試してみました

OpenAI Gym は Gymnasium というフォークされたライブラリとして運用されていくようです

環境

  • macOS 14.2.1
  • Python 3.11.6
  • Gymnasium 0.29.1

強化学習の概念

基本は以下です

とある環境 (主にゲーム) がありそれに対してエージェントがデータ (observation) を元に報酬 (reward) が最大となるような行動 (action) を繰り返していくのが強化学習の基本になります
なので環境とエージェントがあることが前提になるのでそういったケースでは使える学習方法になります

インストール

  • pipenv install 'gymnasium[box2d]'

box2d は Gymnasium に標準で搭載されている2Dゲームを使用するために指定します

swig が必要なのでない場合には事前にインストールしておきましょう

  • brew install swig

ルナーランダーをランダムに動かす

ゲームの名称です
簡単に言えば特定の場所に正確に着地するゲームです
特に「こういう風に動けばクリア」という条件は与えずに action_space.sample でランダムに動かしているのでゲームを見ていると適当な動きをしていることがわかります

import gymnasium as gym

# 環境(ゲーム)の作成というか呼び出し、render_mode=human は GUI 表示するオプション
env = gym.make("LunarLander-v2", render_mode="human")
# 環境の最初の状態を取得
observation, info = env.reset()

# 1000回思考
for _ in range(1000):
    # とりあえずランダムな行動を取る、action は ActType という型
    action = env.action_space.sample()
    # step で環境に対して何かしらのアクションを送る、その結果次の状態や報酬(結果良い行動だったか悪い行動だったか) を取得します
    observation, reward, terminated, truncated, info = env.step(action)
    # エージェントが終了状態になった場合はリセットして最初の状態に戻す
    if terminated or truncated:
        observation, info = env.reset()

# ゲームの終了
env.close()

学習させる

LunarLander の場合 observation は 8 次元(個) の値が返ってきます
今回はサンプルなのでこの情報を元に action の値を決定します

例えば observation 以下のような値が返ってきます

[ 0.01578617  1.3934271   0.7982909  -0.3827387  -0.01812551 -0.17971858  0.          0.        ]

今回は簡単なサンプルなのではじめの2つの値を使います
これはエージェントの現在の X, Y の値で X の値が大きくなれば右に移動し Y の値が小さくなれば下に移動していることになります

なので X の値が 0 より小さい場合は左に動かし X の値が 0 より大きければ左に動かします

import gymnasium as gym

# 環境(ゲーム)の作成というか呼び出し、render_mode=human は GUI 表示するオプション
env = gym.make("LunarLander-v2", render_mode="human")
# 環境の最初の状態を取得
observation, info = env.reset()

# 1000回思考
for _ in range(1000):
    # X の値 (0番目の値) が 0 より大きければ左へ 0 より小さければ右へ
    if observation[0] > 0:
        action = 1
    else:
        action = 3
    # step で環境に対して何かしらのアクションを送る、その結果次の状態や報酬(結果良い行動だったか悪い行動だったか) を取得します
    observation, reward, terminated, truncated, info = env.step(action)
    # エージェントが終了状態になった場合はリセットして最初の状態に戻す
    if terminated or truncated:
        observation, info = env.reset()

# ゲームの終了
env.close()

これで再度実行してみるとエージェントが旋回しながらではありますが旗と旗の間に落ちていくことがわかるかなと思います

もっと学習させる

本当は機体を安定させつつ旗と旗の間に着陸する必要があります
observation で旋回の情報も取得できるのでそれに応じてアクションをどう変更していくかが鍵になります

また reward も重要で reward は -100 から 100 の値を取ります
100 に近ければ近いほど正しい着陸状態に近いことを示しているのでこの値が 100 に近づくように action を調整することも可能です

これを応用するにはどうすればいいのか

Gymnasium を使っていれば env (ゲーム) がいろいろと用意されているので簡単に env を用意できます
これを Gymnasium なしで実際のコンシューマゲームなどに応用するにはどうすればいいのかという問題があります

一応 Gymnasium 自体に env を自作できる機能があり実装するべきインタフェースも用意されているのでこれを使うのが一番簡単にできそうです (参考gymnasium.Env)
これを使ってこの step が実行されたときには reward はどうなるのかというのを独自で実装してあげれば OK です

ただこれもあくまでもインタフェースだけでありゲームの作成自体は別途行う必要があります
gymnasium.Env を実装した上でゲームとの操作、連携部分や別途作り込みが必要になりそうです

最後に

Gymnasium で強化学習を簡単に動かしてみました
動かすこと自体は非常に簡単です

ただ応用方法を考えるとかなり実装が難しく環境 (ゲーム) の準備やつなぎこみが必要になるのでそこは更なる学習が必要になりそうです
一応公式に独自の環境を構築するチュートリアルもあったので参考サイトに掲載しておきます

参考サイト

2024年2月5日月曜日

LangChain で Azure OpenAI とチャットする

LangChain で Azure OpenAI とチャットする

概要

単純にチャットするサンプルです
openai のライブラリを使っても同じことができます

環境

  • macOS 11.7.10
  • Python 3.11.6
  • langchain 0.1.4
    • langchain-openai 0.0.5

インストール

  • pipenv install langchain-openai

サンプルコード

人間が質問する場合は HumanMessage を使いましょう

from langchain_core.messages import HumanMessage
from langchain_openai import AzureChatOpenAI


client = AzureChatOpenAI(
    api_key="xxx",
    api_version="2023-05-15",
    azure_endpoint="https://my-resource.openai.azure.com",
)

print(client.invoke([HumanMessage(content="こんにちは!")]))

最後に

簡単ですが LangChain を使った Azure OpenAI の連携方法を紹介しました
次回は Embeddings 機能を使ってみたいと思います

参考サイト

2024年2月4日日曜日

LangChain の Embddings (ベクトル化) を試す

LangChain の Embddings (ベクトル化) を試す

概要

LangChain の Embeddings を試してみました
Embeddings は簡単に言えばベクトル化 (数値化) で特定のテキストをベクトル化します
ベクトル化にはモデルが必要でそのモデルに応じてベクトル化する次元数と数値が決まります

環境

  • macOS 11.7.10
  • Python 3.11.6
  • langchain 0.1.4
    • langchain-community 0.0.17
    • sentence-transformers 2.3.1
    • faiss-cpu 1.7.4

インストール

  • pipenv install langchain-community
  • pieenv install sentence-transformers

まずは動かす

まずはやってみましょう
ベクトル化するモデルは HaggingFace にあるものを使います
その場合は HuggingFaceEmbeddings というクラスを使います
ちなみにベクトル化するためのモデルは ChatGPT や AzureOpenAI なども提供しておりそれらを使う場合は WebAPI でコールできるので手元にベクトル化用のモデルを持ってくる必要はありません

import pprint

from langchain_community.embeddings import HuggingFaceEmbeddings

# ベクトル化するテキスト
text = "これは、テストドキュメントです。\nそうですこれはテストです。"

# HuggingFaceEmbeddings を使って HuggingFace にあるモデルを使ってテキストをベクトル化する
# モデルを指定しない場合 https://huggingface.co/sentence-transformers/all-mpnet-base-v2 のモデルが使われる
# /path/to/.cache/huggingface/hub/models--sentence-transformers--all-mpnet-base-v2 に保存されている
embeddings = HuggingFaceEmbeddings()
query_result = embeddings.embed_query(text)
doc_result = embeddings.embed_documents([text])

# ベクトル化された数値の確認
# all-mpnet-base-v2 の場合次元数は768で固定、どんなテキストを渡しても同じ次元数になります(たとえ一文字でも
# query の場合は一次元、doc の場合は2次元の配列になる
print(len(query_result))
pprint.pprint(query_result)
print(len(doc_result[0]))
pprint.pprint(doc_result)

実行するとわかりますがずらーっと数字が並ぶのが確認できます
何の数字だかよくわかりませんがとりあえずベクトル化すると数字になることがわかりました

また同じテキストであれば何度実行しても同じ数値になることが確認できると思います

ベクトルを保存する

ベクトルの保存は faiss と呼ばれる facebook が提供する類似度検索ツール方式で保存します
保存はファイルでされます

他にも chromadb などファイル以外でもベクトル情報を検索、保存できるツールがあるので興味があれば調べてみてください

また今回は faiss 上での検索は cpu のみを使用するので faiss-cpu というライブラリを使っています

import pprint

from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS

# ベクトル化するテキスト
text = "これは、テストドキュメントです。\nそうですこれはテストです。"

# HuggingFaceEmbeddings を使って HuggingFace にあるモデルを使ってテキストをベクトル化する
# モデルを指定しない場合 https://huggingface.co/sentence-transformers/all-mpnet-base-v2 のモデルが使われる
# /path/to/.cache/huggingface/hub/models--sentence-transformers--all-mpnet-base-v2 に保存されている
embeddings = HuggingFaceEmbeddings()
query_result = embeddings.embed_query(text)
doc_result = embeddings.embed_documents([text])

# ベクトル化された数値の確認
# all-mpnet-base-v2 の場合次元数は768で固定、どんなテキストを渡しても同じ次元数になります(たとえ一文字でも
# query の場合は一次元、doc の場合は2次元の配列になる
print(len(query_result))
pprint.pprint(query_result)
print(len(doc_result[0]))
pprint.pprint(doc_result)

# テキストを HuggingFaceEmbeddings を使ってベクトル化し保存
# やっていることは embed_documents と同じ (はず
vectorstore = FAISS.from_texts([text], embeddings)
vectorstore.save_local("./vectorstore")

# ファイルに保存したベクトル情報の呼び出し
new_index = FAISS.load_local("./vectorstore", embeddings)

# faiss を使って類似度検索してみる
# ベクトル化した文章が少ないのでおそらくどんな単語で検索してもテキストが検索される (はず
docs = new_index.similarity_search("hawksnowlog")
pprint.pprint(docs)

せっかくなので最後に作成した faiss のインデックス情報を使って類似度検索もしています

保存に成功すると以下のようなファイルが生成されていることが確認できます

tree vectorstore/
vectorstore/
├── index.faiss
└── index.pkl

0 directories, 2 files

最後に

LangChain の Embeddings 機能を使ってみました
またベクトル化したデータを FAISS というベクトル類似度ツールを使って保存し検索も試してみました

次回は LLM を組み合わせる方法を紹介します

参考サイト

2024年2月3日土曜日

LangChain の Retrieval Chains を使って FAISS で検索した類似度情報を元に LLM で回答させるようにしてみる (create_retrieval_chain ver)

LangChain の Retrieval Chains を使って FAISS で検索した類似度情報を元に LLM で回答させるようにしてみる (create_retrieval_chain ver)

概要

LangChain には事前に作成したデータを元に LLM (ChatGPT や AzureOpenAI)を使って回答を作成することができる機能があります
今回は FAISS で作成した類似度検索結果を元に LLM を使って回答を作成する Chains を使う方法を紹介します (これを Retrieval Chain と呼びます

環境

  • macOS 11.7.10
  • Python 3.11.6
  • langchain 0.1.4
    • langchain-openai 0.0.5
    • langchain-community 0.0.17
    • sentence-transformers 2.3.1
    • faiss-cpu 1.7.4
    • beautifulsoup4 4.12.3

サンプルコード

コード中のコメントに詳しい処理の内容を記載しています
ポイントは faiss で作成したベクトル情報を Retrieval に変換し LLM にチェインしている流れになります

from langchain.chains import create_retrieval_chain
from langchain.chains.combine_documents import create_stuff_documents_chain
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain_community.document_loaders import WebBaseLoader
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_community.vectorstores import FAISS
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import AzureChatOpenAI


# AzureOpenAI を LLM として利用する、LLM には最終的に faiss で検索した結果を元に回答を作成してもらう役割
llm = AzureChatOpenAI(
    api_key="xxx",
    api_version="2023-05-15",
    azure_endpoint="https://my-resource.openai.azure.com",
)

# ベクトル化するテキストを Web 上から取得、ここはどんな情報でも OK
# 特定の回答に特化させることができるような情報を使用する
# 今回は WebBaseLoader を使っているが他にも直接テキストを与えたり PDF の情報を与えたりすることができる
loader = WebBaseLoader("https://docs.smith.langchain.com/overview")
docs = loader.load()

# HuggingFaceEmbeddings を使って HuggingFace にあるモデルを使ってテキストをベクトル化する
# ここも Embeddings する際は他の Embeddings 機能でも OK (OpenAI Embeddings など
embeddings = HuggingFaceEmbeddings()
text_splitter = RecursiveCharacterTextSplitter()
documents = text_splitter.split_documents(docs)
vector = FAISS.from_documents(documents, embeddings)

# ベクトル情報の保存と読み込み (一度保存したら二回目以降はロード部分から実行してもOK
vector.save_local("./vectorstore")
vector = FAISS.load_local("./vectorstore", embeddings)

# チャットプロンプトを作成、この {context} の部分に faiss からの結果を与えることで faiss の結果を考慮した回答を作成してもらう
prompt = ChatPromptTemplate.from_template(
    """Answer the following question based only on the provided context:

<context>
{context}
</context>

Question: {input}"""
)

# create_stuff_documents_chain でチャットプロンプトで作成した問い合わせ文言を使って LLM に問い合わせるようにする
document_chain = create_stuff_documents_chain(llm, prompt)

# document_chain を直接使う場合は以下のようにします (今回は更に faiss と連携した結果を使って LLM に問い合わせるのでコメントアウトしています
# from langchain_core.documents import Document
#
# response = document_chain.invoke(
#     {
#         "input": "how can langsmith help with testing?",
#         "context": [
#             Document(page_content="langsmith can let you visualize test results")
#         ],
#     }
# )
# print(response)

# ベクトル化情報を Retrieval に変換します https://python.langchain.com/docs/modules/data_connection/
retriever = vector.as_retriever()

# そしてチャットプロンプト+LLMのチェインに更に Retrieval をチェインすることで faiss の結果を元にチャットプロンプト -> LLM という流れで問い合わせできるようになります
retrieval_chain = create_retrieval_chain(retriever, document_chain)

# 問い合わせ、結果がちゃんと Web から取得した情報を加味した回答になっていることを確認します
response = retrieval_chain.invoke({"input": "how can langsmith help with testing?"})
print(response["answer"])

ちょっと解説

今回は 2 つの Chains を使っています

  • create_stuff_documents_chain
  • create_retrieval_chain

前者は LLM + チャットプロンプトを連携する Chains で LLM への入力にテキストを入力するのではなくテンプレート経由で生成したテキストを入力にしています

後者はテンプレート経由で生成されたテキスト + faiss の連携をしています
faiss で類似度検索結果された情報を元にテンプレートで入力テキストを作成しそれを LLM に投げています

こんな感じで Chains はローカルで実施、加工した情報を元に LLM に質問を投げるといってことができるツールになります (他にもいろいろな連携ができます

最後に

LangChain の Retrival Chain を使って faiss の結果を元に LLM に回答を作成してもらうサンプルを紹介しました
Web 上には同じような手法で RetrievalQA と呼ばれる Chain を使った手法がよく紹介されていますが RetrievalQA は Legacy Chain と呼ばれており古い手法でそのうちなくなるので今回紹介した create_retrieval_chain を使うようにしましょう

LLM をそのままにあたかも専用のモデルを作成したかのような回答を得ることができます
ファインチューニングのようにモデルを専用にビルドする必要がなくマシン側でベクトル環境を準備するだけでカスタムできるのが良い点かなと思います

ただ逆にベクトル化データベースなどの準備必要になるのでそれはデメリットでもあります

参考サイト

2024年2月2日金曜日

Python で selenium を動かすサンプルコード

Python で selenium を動かすサンプルコード

概要

現状の環境で動作するのか確認したかったのでメモ
Chrome は起動中のウィンドウを使います

環境

  • macOS 11.7.10
  • chrome 120.0.6099.216
  • chromedriver 120.0.6099.109

chromedriver

  • brew install chromedriver

インストール

  • pipenv install selenium

chrome 起動

  • "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --remote-debugging-port=9222

サンプルコード

特定のサイトの商品IDの一覧を先に取得しその後 ID を元に商品のリンク先に飛びリンク先のテキスト情報を表示する流れです

(どうしても安定しないところは sleep やリトライを入れるしかないのだろうか)

import time

from selenium import webdriver
from selenium.common.exceptions import NoSuchElementException
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By

# 既存chromeを起動
chromeoptions = Options()
chromeoptions.debugger_address = "127.0.0.1:9222"
driver = webdriver.Chrome(options=chromeoptions)


def init():
    driver.get("https://your.target.site")
    driver.find_element(By.XPATH, '//*[@id="js-list"]/div/a').click()


def init_ids() -> list:
    init()
    ids = []
    products = driver.find_elements(By.XPATH, '//*[@id="js-list"]/ul/li/div')
    for p in products:
        id = p.get_attribute("id")
        ids.append(id)
    return ids


def show_product_info(id: str):
    init()
    driver.find_element(By.XPATH, f'//div[@id="{id}"]').click()
    time.sleep(2)
    driver.find_element(By.XPATH, '//*[@id="js-product-tabs"]/div[1]/cite/a').click()
    try:
        driver.find_element(By.XPATH, '//*[@id="a_performer"]').click()
    except NoSuchElementException:
        pass
    print(driver.find_element(By.XPATH, '//*[@id="title"]').text)
    print(driver.find_element(By.XPATH, '//*[@id="performer"]').text)
    # 戻る
    driver.back()


def close():
    driver.close()


if __name__ == "__main__":
    ids = init_ids()
    for id in ids:
        show_product_info(id)
    close()

最後に

selenium は相変わらずDOMの表示タイミングなどがシビアなので安定して動かすのが大変な印象です
仕方なくブラウザからしか情報が取得できない場合を除きできる限り selenium は使わないほうがいいと思います

個人のツールとしてならありかなと思います

参考サイト

2024年2月1日木曜日

Keras で mnist の画像学習を CNN を使ってやってみる

Keras で mnist の画像学習を CNN を使ってやってみる

概要

前回 Keras + mnist のチュートリアルをやりました
今回は CNN を使った方法を紹介します
チュートリアルはこちらになります

勉強のために Conv2D のレイヤーを増やしたバージョンも紹介します

環境

  • macOS 14.2.1
  • Python 3.11.6
  • tenforflow 2.15.0
  • keras 2.15.0

サンプルコード

import numpy as np
import tensorflow as tf

mnist = tf.keras.datasets.mnist
# 60000個の学習データ
(x_train, y_train), (x_test, y_test) = mnist.load_data()
# 学習データとテストデータを3次元から4次元に変換、Conv2D を使う場合は大抵の場合4次元必要、前回 https://hawksnowlog.blogspot.com/2024/01/keras-mnist-tutorial.html Dense のみは3次元のデータのままいける
x_train = x_train.reshape((60000, 28, 28, 1))
x_test = x_test.reshape((10000, 28, 28, 1))
# ピクセル情報は0-255の範囲なので255で割ることで0-1の範囲に変換する
x_train, x_test = x_train / 255.0, x_test / 255.0

model = tf.keras.models.Sequential([
    tf.keras.layers.Conv2D(32, (3, 3), activation='relu', input_shape=(28, 28, 1)),  # 3x3 の複数のフィルタを32枚使って画像にフィルタをかけ新たな行列を作成
    tf.keras.layers.MaxPooling2D((2, 2)),  # 作成した新たな行列に対して 2x2 の領域で最大の値を取得、更にそれを新たな行列とする
    # ここで更に Conv2D + MaxPooling2D のレイヤーを増やしても OK、ただし学習時間は長くなる
    tf.keras.layers.Conv2D(64, (3, 3), activation='relu'),
    tf.keras.layers.MaxPooling2D((2, 2)),
    tf.keras.layers.Conv2D(64, (3, 3), activation='relu'),
    # ここから出力用のレイヤー
    tf.keras.layers.Flatten(),  # 3次元->1次元への変換
    tf.keras.layers.Dense(64, activation='relu'),  # 64ノードの全結合->10ノードの全結合
    tf.keras.layers.Dense(10, activation='softmax'),  # 最後はソフトマックスで10個ある出力の合計が1になるように変換する (定番
])

# モデルコンパイル
model.compile(optimizer='adam',  # 最適化関数は Adaptive moment
              loss='sparse_categorical_crossentropy',  # 損失関数は交差エントロピー誤差
              metrics=['accuracy'])  # 評価関数は正解率

# # 学習
model.fit(x_train, y_train, epochs=5)


# 評価
model.evaluate(x_test, y_test)
# 予測と結果表示
results = model.predict(x_test[:5])
# 結果はDense(10)なので10個の配列の予測値から成り立っている
# その配列10個中で一番値の大きいインデックスが予測した手書きの数字になる
# np.argmax はそれを一発でやってくれる便利メソッド
print(np.argmax(results, axis=1))  # [7 2 1 0 4] になるはず
print(y_test[:5])  # [7 2 1 0 4] こっちは答え

Conv2D のレイヤーを増やしてみる

前のコードのレイヤーを増やしてみます
最後の Conv2D(64, (3, 3), activation='relu') で (3, 3, 64) となりこれに更に conv2D でフィルタをかけると (1, 1, 128) などになり

ValueError: One of the dimensions in the output is <= 0 due to downsampling in conv2d_3. Consider increasing the input size. Received input shape [None, 1, 1, 128] which would produce output shape with a zero or negative value in a dimension.

というエラーになるので少しフィルタの次元数を調整します
具体的に (3, 3) を (2, 2) のフィルタにして次のレイヤに行く際の変換した画像の縦横の次元数を少しずつ減らすようにしています

import numpy as np
import tensorflow as tf

mnist = tf.keras.datasets.mnist
# 60000個の学習データ
(x_train, y_train), (x_test, y_test) = mnist.load_data()
# 学習データとテストデータを3次元から4次元に変換、Conv2D を使う場合は大抵の場合4次元必要、前回 https://hawksnowlog.blogspot.com/2024/01/keras-mnist-tutorial.html Dense のみは3次元のデータのままいける
x_train = x_train.reshape((60000, 28, 28, 1))
x_test = x_test.reshape((10000, 28, 28, 1))
# ピクセル情報は0-255の範囲なので255で割ることで0-1の範囲に変換する
x_train, x_test = x_train / 255.0, x_test / 255.0

model = tf.keras.models.Sequential([
    tf.keras.layers.Conv2D(32, (2, 2), activation='relu', input_shape=(28, 28, 1)),  # 3x3 の複数のフィルタを32枚使って画像にフィルタをかけ新たな行列を作成
    tf.keras.layers.MaxPooling2D((2, 2)),  # 作成した新たな行列に対して 2x2 の領域で最大の値を取得、更にそれを新たな行列とする
    # ここで更に Conv2D + MaxPooling2D のレイヤーを増やしても OK、ただし学習時間は長くなる
    # 以下では 128 個のフィルタで Conv2D するレイヤーを増やしている
    tf.keras.layers.Conv2D(64, (2, 2), activation='relu'),
    tf.keras.layers.MaxPooling2D((2, 2)),
    tf.keras.layers.Conv2D(128, (2, 2), activation='relu'),  # レイヤーを増やす場合変換した行列の縦横の次元数が途中のレイヤで (1, 1) にならないように Conv2D のフィルタの次元数を調整する必要がある
    tf.keras.layers.MaxPooling2D((2, 2)),
    tf.keras.layers.Conv2D(128, (2, 2), activation='relu'),
    # ここから出力用のレイヤー
    tf.keras.layers.Flatten(),  # 3次元->1次元への変換
    tf.keras.layers.Dense(64, activation='relu'),  # 64ノードの全結合->10ノードの全結合
    tf.keras.layers.Dense(10, activation='softmax'),  # 最後はソフトマックスで10個ある出力の合計が1になるように変換する (定番
])


# モデルコンパイル
model.compile(optimizer='adam',  # 最適化関数は Adaptive moment
              loss='sparse_categorical_crossentropy',  # 損失関数は交差エントロピー誤差
              metrics=['accuracy'])  # 評価関数は正解率

# # 学習
model.fit(x_train, y_train, epochs=5)


# 評価
model.evaluate(x_test, y_test)
# 予測と結果表示
results = model.predict(x_test[:5])
# 結果はDense(10)なので10個の配列の予測値から成り立っている
# その配列10個中で一番値の大きいインデックスが予測した手書きの数字になる
# np.argmax はそれを一発でやってくれる便利メソッド
print(np.argmax(results, axis=1))  # [7 2 1 0 4] になるはず
print(y_test[:5])  # [7 2 1 0 4] こっちは答え

model.summary()

 Layer (type)                Output Shape              Param #   
=================================================================
 conv2d (Conv2D)             (None, 27, 27, 32)        160       
                                                                 
 max_pooling2d (MaxPooling2  (None, 13, 13, 32)        0         
 D)                                                              
                                                                 
 conv2d_1 (Conv2D)           (None, 12, 12, 64)        8256      
                                                                 
 max_pooling2d_1 (MaxPoolin  (None, 6, 6, 64)          0         
 g2D)                                                            
                                                                 
 conv2d_2 (Conv2D)           (None, 5, 5, 128)         32896     
                                                                 
 max_pooling2d_2 (MaxPoolin  (None, 2, 2, 128)         0         
 g2D)                                                            
                                                                 
 conv2d_3 (Conv2D)           (None, 1, 1, 128)         65664     
                                                                 
 flatten (Flatten)           (None, 128)               0         
                                                                 
 dense (Dense)               (None, 64)                8256      
                                                                 
 dense_1 (Dense)             (None, 10)                650       
                                                                 
=================================================================
Total params: 115882 (452.66 KB)
Trainable params: 115882 (452.66 KB)
Non-trainable params: 0 (0.00 Byte)
_________________________________________________________________

最後に

Keras + CNN で画像解析を試してみました
前回はただの Dense レイヤーだけでしたが今回は CNN という方法を使って判定しています
精度はどちらも良かったのですが画像解析の主流は CNN かなと思います
単純にレイヤーを増やすだけで精度がどんどんよくなるのが摩訶不思議な感じなしますがそれが DeepLearning のすごいところです

CNN 自体の説明は以下の動画が非常に参考になりました
https://www.youtube.com/watch?v=xzzTYL90M8s

参考サイト