0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

はじめて作る予測モデルを組み込んだWebアプリ

Last updated at Posted at 2025-05-10

はじめに

はじめまして。金丸と申します。
プログラミングスクールで学んだことを活かして「ブタクサチェッカー」なるアプリを作ってみました。
「ブタクサ」と「セイタカアワダチソウ」という似ている植物を、機械学習した予測モデルで2値分類するWebアプリです。

開発言語

  • HTML5
  • CSS3
  • Python3.11.8

フレームワーク

  • Django4.2.1
  • Bootstrap5.3

ライブラリ

  • jQuery3.5.1
  • tensorflow2.12.0
  • Pillow9.5.0
  • numpy1.23.5
  • selenium4.31.0
  • webdriver-manager4.0.2

ツール

  • GoogleColaboratory
  • ColorHunt
  • diagrams.net(旧: draw.io)
  • ChatGPT

データ集め

最初の作業として、予測モデルを作成するために、Webスクレイピングを行いました。こちらの記事(【Pythonスクレイピング】Google画像検索から画像を自動保存してみた【コピペOK】)を参考にChatGPTやインストラクター様から手助けをもらい、「ブタクサ」と「セイタカアワダチソウ」の画像データを合わせて約800枚集めることができました。
参考までにコードを載せておきます。このコードは画像検索したいキーワードを入力して、そのキーワードをgoogle画像検索して集めるコードです。

import os
import requests
from selenium.webdriver import Chrome, ChromeOptions
from selenium.webdriver.chrome.service import Service
from selenium.webdriver.common.by import By
from selenium.webdriver.common.keys import Keys
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from time import sleep

# Selenium設定(ヘッドレスモード)
options = ChromeOptions()
options.add_argument("--headless")
options.add_argument("--disable-gpu")
options.add_argument("--window-size=1920,1080")
service = Service()
service.creation_flags = 0x08000000
browser = Chrome(options=options, service=service)

# Google画像検索ページにアクセス
url = "https://www.google.co.jp/imghp?hl=ja"

try:
    browser.get(url)
    # 検索ボックスが現れるまで待機
    kw_search = WebDriverWait(browser, 10).until(
        EC.presence_of_element_located((By.NAME, "q"))
    )

except Exception as e:
    print(f"検索ボックスが見つかりませんでした: {e}")
    browser.quit()
    exit()

# 検索キーワード入力
keyword = input("画像検索したいキーワードを入力してください!:")

kw_search.send_keys(keyword)
kw_search.send_keys(Keys.ENTER)

# 画像URLを取得するためのリストと取得枚数の目標
img_urls = set()  # 重複防止のためset利用
target_count = 400
scroll_pause = 2
max_attempts = 50  # ループ回数の上限

attempt = 0
thumbnail_num = 0
while thumbnail_num < target_count and attempt < max_attempts:

    attempt += 1
    # スクロール
    browser.execute_script("window.scrollTo(0, document.body.scrollHeight);")
    sleep(scroll_pause)
    
    # ページ内の「もっと見る」ボタンがあればクリックする
    try:
        # ボタンの文言やxpathは変更される可能性があるため、表示されていればクリック
        show_more = browser.find_element(By.CSS_SELECTOR, ".mye4qd")
        if show_more.is_displayed():
            show_more.click()
            sleep(scroll_pause)
    except Exception:
        # ボタンがない場合は無視
        pass

    # 現在表示されている画像要素を取得し、src属性にhttpが含まれているものをURLに追加する
    thumbnail_elements = browser.find_elements(By.XPATH, "//a[div[div[div[g-img[img]]]]]")
    thumbnail_num = len(thumbnail_elements)
    print("取得可能画像数: ", thumbnail_num)

# 画像がクリックできるようにTOPに戻る
browser.execute_script("window.scrollTo(0, 0);")
sleep(scroll_pause)

# 画像をクリックして、大きい画像のリンクを取得、蓄積
for img in thumbnail_elements:

    img.click()

    WebDriverWait(browser, 10).until(
        EC.presence_of_element_located((By.CLASS_NAME, "sFlh5c"))
    )
    img_element = browser.find_element(By.CLASS_NAME, "sFlh5c")
    src = img_element.get_attribute("src")
    if src and src.startswith("http"):
        img_urls.add(src)
    
print(f"取得済み画像URL数:{len(img_urls)}")

# 取得したURLをリストに変換し、目標枚数に切り詰める
img_urls = list(img_urls)[:target_count]

# 保存先ディレクトリ
save_dir = r"./img/" + keyword
if not os.path.exists(save_dir):
    os.makedirs(save_dir)

# 画像保存
for idx, url_img in enumerate(img_urls):
    try:
        r = requests.get(url_img, timeout=10)
        if r.status_code == 200:
            file_path = os.path.join(save_dir, "{0}_{1:0=3}.jpg".format(keyword, idx+1))
            with open(file_path, "wb") as fp:
                fp.write(r.content)
            print(f"{file_path} を保存しました。")
        else:
            print(f"ステータスコード {r.status_code} で失敗: {url_img}")
        sleep(0.1)
    except Exception as e:
        print(f"エラーが発生しました: {e}")

browser.quit()

簡単な解説(使用方法)

検索方法

# 検索キーワード入力
keyword = input("画像検索したいキーワードを入力してください!:")

kw_search.send_keys(keyword)
kw_search.send_keys(Keys.ENTER)

# 実行時(例 ブタクサ)
# PS C:\myapp\myenv> python .\scraping\scraping.py
# 画像検索したいキーワードを入力してください!:ブタクサ

この例では、コンソール画面に「ブタクサ」と入力しています。

取得したい画像枚数

target_count = 400

「target_count」に必要な画像データの枚数を指定します。
今回は、400枚欲しかったので、target_count = 400にしました。

# 保存先ディレクトリ
save_dir = r"./img/" + keyword
if not os.path.exists(save_dir):
    os.makedirs(save_dir)

# 実行時(例)
# myapp/img/ブタクサ

現在いるディレクトリに「img/keyword」ディレクトリがなければ作成する処理です。

データの前処理

データを集めることができたので、集めたデータの前処理を行いました。以下の点に気を付けて、データの選別を行いました。

人や文字が中心に映っている写真

butakusa-moji-hito.png

この例では、画面左側に確かにブタクサの写真が入っています。しかし、画面右側には関係のない「文字」や「人」が混ざり込んでしまい、その部分も学習してしまうためデータには適していません。

イラスト

butakusa-イラストや.png
イラストは

  • 「抽象化」や「デフォルメ」が強い
  • 「現実の法則」や「一貫性」が保証されない
  • 絵柄や表現の多様性が高すぎて、同じクラスでも特徴がバラバラになりやすい
    といった理由からデータセットしては不適とされることが多いです。

対象とするデータかどうか判別が難しい写真

butakusa-muzukasii.jpg
ブタクサには葉がギザギザという特徴があるのですが、この写真は上記2つの画像と比べると、葉がやや丸く雄花や雌花もないため、このようなデータは取り除いた方が無難です。

予測モデルの作成

テストデータと学習用データに振り分け出来たら、GoogleColabを利用して予測モデル作成しました。
学習済みのVGG16を転移学習させて、出力層を2つに絞りました。

  1. VGG16の全結合層以外の取得

    from tensorflow.keras.applications.vgg16 import VGG16
    vgg16_without_fc = VGG16(weights='imagenet', include_top=False, input_shape=(256, 256, 3))
    
  2. 独自の全結合層の追加

    # VGG16のSequentalモデルへの変換
    from tensorflow.keras.models import Sequential
    model = Sequential()
    for layer in vgg16_without_fc.layers:
        model.add(layer)
    
    # VGG16のパラメータ凍結
    for layer in model.layers:
        layer.trainable = False
    
  3. 独自の全結合層部分のみの学習

    # 独自の全結合層の追加
    from tensorflow.keras.layers import Flatten, Dense
    model.add(Flatten())
    model.add(Dense(256, activation='relu'))
    model.add(Dense(2, activation='softmax'))
    
    # 誤差関数、最適化方法、評価指標の設定
    model.compile(loss='binary_crossentropy',
              optimizer='adam',
              metrics=['accuracy'])
    
  • 隠れ層の活性化関数にrelu関数、出力層の活性化関数にソフトマックス関数を使用
  • 誤差関数として二値交差エントロピー誤差(Binary Cross-Entropy Error)を使用
  • 最適化手法としてAdamを、評価指標として正解率(accuracy)を指定
  • エポック数を6~12,バッチサイズを16,32,64と変更しながら学習

適切なところで学習を完了し、予測モデルを保存しました。

イメージ図
image.png

Webアプリの実装

Djangoを使い、以下の手順でWebアプリを実装していきました。

  1. ワイヤーフレーム作成
  2. フォームの作成
  3. ビューの作成
  4. テンプレートの作成

ワイヤーフレーム作成

ここまでの工程で予測モデル作成が完了しましたので、次はWebアプリのワイヤーフレーム作成に取り掛かりました。
シンプルなWebアプリであるため、デザインもシンプルなものにしました。
文字や判別機を中央に配置することを意識して、ColorHuntとdiagrams.netを使いユーザーが見やすいデザインを考えました。

フォームの作成

from django import forms

class ImageUploadForm(forms.Form):
    image = forms.ImageField(label='',
                            widget=forms.ClearableFileInput(attrs={'class': 'custom-file-input'})
    )

画像をアップロードする機能を作るため、Formクラスを継承しクラス変数「image」にフィールドを設定しています。
見た目を整えるためにフィールドオプションに「label」と「widget」を設定しています。

ビューの作成

次に、以下の順でビューの作成をしました。

  1. 必要なライブラリのインポート
  2. GETリクエストによるアクセス時の処理の実装
  3. POSTリクエストによるアクセス時の処理の実装
from django.shortcuts import render
from django.conf import settings
from io import BytesIO
from .forms import ImageUploadForm
from tensorflow.keras.models import load_model
from tensorflow.keras.preprocessing.image import load_img
from tensorflow.keras.preprocessing.image import img_to_array
import os

def predict(request):
    if request.method == 'GET':
        form = ImageUploadForm()
        return render(request, 'top.html', {'form': form})
    if request.method == 'POST':
        form = ImageUploadForm(request.POST, request.FILES)
        if form.is_valid():
            img_file = form.cleaned_data['image']
            img_file = BytesIO(img_file.read())
            img = load_img(img_file, target_size=(256, 256))
            img_array = img_to_array(img)
            img_array = img_array.reshape((1, 256, 256, 3))
            img_array = img_array/255
            model_path = os.path.join(settings.BASE_DIR, 'prediction', 'models', 'model.h5') 
            model = load_model(model_path)
            result = model.predict(img_array)
            probabilities = max(result[0].tolist())
            probabilities = round(probabilities * 100)
            if result[0][0] > result[0][1]:
                prediction = 'ブタクサ'
            else:
                prediction = 'セイタカアワダチソウ'
            img_data = request.POST.get('img_data')
            return render(request, 'top.html', {'form': form, 'prediction': prediction, 'img_data': img_data, 'probabilities': probabilities})
        else:
            form = ImageUploadForm()
            return render(request, 'top.html', {'form': form})

必要なライブラリのインポート

from django.shortcuts import render

Djangoが提供する「render」関数をインポートする記述です。「render」関数は、特定のテンプレートとデータを元にHTMLを作成します。

from .forms import ImageUploadForm

先程作成した「ImageUploadForm」クラスをインポートしています。これにより、「views.py」で「ImageUploadForm」クラスを利用できます。

from io import BytesIO

主にデータの取り扱いを担う「io」モジュールの「BytesIO」をインポートしています。アップロードされた画像ファイルを、予測モデルに適した形式へ変換するために使用します。

GETリクエストによるアクセス時の処理の実装

def predict(request):
    if request.method == 'GET':
        form = ImageUploadForm()
        return render(request, 'top.html', {'form': form})

render関数の第3引数に連想配列を指定しています。「top.html」テンプレート内で、「form」変数を活用して画像アップロードフォームを表示できます。

POSTリクエストによるアクセス時の処理の実装

if request.method == 'POST':
        form = ImageUploadForm(request.POST, request.FILES)
        if form.is_valid():
            img_file = form.cleaned_data['image']
            img_file = BytesIO(img_file.read())
            img = load_img(img_file, target_size=(256, 256))
            img_array = img_to_array(img)
            img_array = img_array.reshape((1, 256, 256, 3))
            img_array = img_array/255
            model_path = os.path.join(settings.BASE_DIR, 'prediction', 'models', 'model.h5') 
            model = load_model(model_path)
            result = model.predict(img_array)
            probabilities = max(result[0].tolist())
            probabilities = round(probabilities * 100)
form = ImageUploadForm(request.POST, request.FILES)

POSTリクエストで送信されたデータを引数として「ImageUploadForm」クラスのインスタンスを作成し、「form」変数に代入しています。「request.POST」はファイル以外の送信されたデータ、「request.FILES」は送信されたファイルを意味します。

img_file = form.cleaned_data['image']
img_file = BytesIO(img_file.read())
img = load_img(img_file, target_size=(256, 256))
img_array = img_to_array(img)
img_array = img_array.reshape((1, 256, 256, 3))
img_array = img_array/255

送信された画像データを、4次元の配列に変換し255で割ることで0から1の範囲で正規化しています。

model_path = os.path.join(settings.BASE_DIR, 'prediction', 'models', 'model.h5') 
model = load_model(model_path)
result = model.predict(img_array)
probabilities = max(result[0].tolist())
probabilities = round(probabilities * 100)

予測モデルのファイルパスを生成して、判定した予測結果を百分率に変換しています。

テンプレートの作成

<form method="post" enctype="multipart/form-data">
    {% csrf_token %}
    {{ form.as_p }}
    <button type="submit">判定</button>
  </form>

ユーザーが入力したデータを送信するためのものです。「enctype="multipart/form-data"」は、フォームにファイルアップロードが含まれる場合に記述します。
また、CSRF攻撃を防ぐために{% csrf_token %}を記述しています。formを実装する場合、formタグの内部に配置する必要があります。

終わりに

最後までお読みいただき、誠にありがとうございます。
データの集めに使ったコードや、データの前処理の考え方などが皆様の参考になれば幸いです。
この経験を活かして、次はデータベースや会員登録機能などを実装したWebアプリを開発できるように精進して参ります。

参考にした記事

【Pythonスクレイピング】Google画像検索から画像を自動保存してみた【コピペOK】

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?