はじめに
はじめまして。金丸と申します。
プログラミングスクールで学んだことを活かして「ブタクサチェッカー」なるアプリを作ってみました。
「ブタクサ」と「セイタカアワダチソウ」という似ている植物を、機械学習した予測モデルで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」ディレクトリがなければ作成する処理です。
データの前処理
データを集めることができたので、集めたデータの前処理を行いました。以下の点に気を付けて、データの選別を行いました。
- 人や文字が中心に映っている写真
- イラスト
-
対象とするデータかどうか判別が難しい写真
データの前処理に苦労したので、一つずつ解説していきます。
人や文字が中心に映っている写真
この例では、画面左側に確かにブタクサの写真が入っています。しかし、画面右側には関係のない「文字」や「人」が混ざり込んでしまい、その部分も学習してしまうためデータには適していません。
イラスト
- 「抽象化」や「デフォルメ」が強い
- 「現実の法則」や「一貫性」が保証されない
- 絵柄や表現の多様性が高すぎて、同じクラスでも特徴がバラバラになりやすい
といった理由からデータセットしては不適とされることが多いです。
対象とするデータかどうか判別が難しい写真

ブタクサには葉がギザギザという特徴があるのですが、この写真は上記2つの画像と比べると、葉がやや丸く雄花や雌花もないため、このようなデータは取り除いた方が無難です。
予測モデルの作成
テストデータと学習用データに振り分け出来たら、GoogleColabを利用して予測モデル作成しました。
学習済みのVGG16を転移学習させて、出力層を2つに絞りました。
-
VGG16の全結合層以外の取得
from tensorflow.keras.applications.vgg16 import VGG16 vgg16_without_fc = VGG16(weights='imagenet', include_top=False, input_shape=(256, 256, 3)) -
独自の全結合層の追加
# 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 -
独自の全結合層部分のみの学習
# 独自の全結合層の追加 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と変更しながら学習
適切なところで学習を完了し、予測モデルを保存しました。
Webアプリの実装
Djangoを使い、以下の手順でWebアプリを実装していきました。
ワイヤーフレーム作成
ここまでの工程で予測モデル作成が完了しましたので、次は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」を設定しています。
ビューの作成
次に、以下の順でビューの作成をしました。
- 必要なライブラリのインポート
- GETリクエストによるアクセス時の処理の実装
- 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アプリを開発できるように精進して参ります。


