たつぷりの調査報告書

博士後期課程(理学)の学生が趣味でUnityやBlenderで遊ぶブログです。素人が独学で勉強した際の忘備録です。

分類器で機械学習入門(その1)- データの準備と可視化

どうもたつぷりです。こんにちは。最近、個人的な趣味と研究上の必要によって、機械学習の門をたたいてみました。本当に何も知らないので公式のチュートリアルなどから触っていっています。

さて今回の記事は、この世には既に腐るほど存在している、CIFAR10の画像を分類する画像分類器を作成するチュートリアルの解説を行います。要は自分への備忘録です。

目的と目標

自分がチュートリアルをこなす際、知らなかったり分からなかったことをまとめながらやっていたので、その内容をブログにもまとめる。具体的には以下のURLのpytorchの公式チュートリアルの画像分類器の作成を、いろいろと何をやっているのかを解説付きでまとめた。

pytorch.org

今の自分の理解の範囲では正しいと思うことを書いているが、今後理解が進んでより深い理解をしたら修正を行う可能性がある。また現段階の勉強不足で分からないことは今後別のノートでまとめる。

学習データの準備

まず学習に必要なデータセットを準備する。

前準備

torchvision.datasetsを用いることで、有名なデータセットを簡単に取り込むことができる。また、 torch.utils.data.DataLoader を使うと画像のデータセットを作る、例えばミニバッチに分けるなどの操作をするのに便利である。

ここではCIFAR10のデータを用いる。各画像データの次元は3x32x32(色x高さx幅)である。

まずデータの準備に必要なモジュールを導入する。

import torch
import torchvision
import torchvision.transforms as transforms

ここで、torchvision.transformsは画像の成型を行うためのライブラリと考えてよい。

pytorch.org

今回は次のようなデータの成型を行う。

transform = transforms.Compose(
    [transforms.ToTensor(),
     transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])

この際、transform.Composeは複数の変換をまとめて適用するためのものである。ここでは、

  • transforms.ToTensor()
  • transforms.Normalize()

をまとめて適用している。

transform.ToTensorは画像データをpytorchで扱えるテンソル型の量に変換する。

transform.Normalizeテンソル量を正規化する。この操作に関してはあまり理解できていないが、CNNなどで学習するときに効率が良くなることがあるなどの説明を見かけた。公式レファレンスによると、具体的には下のような操作を行っている。

Given mean: (mean[1],...,mean[n]) and std: (std[1],..,std[n]) for n channels, this transform will normalize each channel of the input torch.*Tensor i.e., output[channel] = (input[channel] - mean[channel]) / std[channel]

この式から考えると、例えば今の場合はRGBの3チャンネルが存在しているので。

Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))

と書くと、最初の(0.5, 0.5, 0.5)はrgbに対して正規化する際の平均値、後の(0.5, 0.5, 0.5)はrgbに対して正規化する際の標準偏差を与えている。よって

output_R = (input_R-0.5)/0.5

を与えていることになる。後で用いるように、inputを再構成するにはRGBそれぞれに対して

input_R = output_R * 0.5 + 0.5

をすればよい。

データのロード

実際に、以下のようにしてデータの準備をする。torchvision.datasets.CIFAR10()はCIFAR10のデータを読み込むのに用いる。引数に様々なオプションを指定する。

例えば今回筆者はデータのダウンロードも行ったので、download=Trueを指定した。CIFAR10ではあらかじめ教師データと、テストデータが準備されている。それらの指定をtrainで行う。上述のtransformを用いてデータの成型方法に関しての指定も行っている。

これによってtrainsetには教師用データの情報が格納された。ここで機械学習は各ステップでミニバッチと呼ばれる小さな部分集合ごとに行われる。ミニバッチに含まれる画像データの数をbatch_sizeで指定する。ここでは4にしているので学習の各ステップは4枚の画像に対して行われることになる。

trainset = torchvision.datasets.CIFAR10(root='./data', train=True,
                                        download=True, transform=transform)

trainloader = torch.utils.data.DataLoader(trainset, batch_size=4,
                                          shuffle=True, num_workers=0)

testset = torchvision.datasets.CIFAR10(root='./data', train=False,
                                       download=True, transform=transform)

testloader = torch.utils.data.DataLoader(testset, batch_size=4,
                                         shuffle=False, num_workers=0)

classes = ('plane', 'car', 'bird', 'cat',
           'deer', 'dog', 'frog', 'horse', 'ship', 'truck')

後で改めて述べるが、CIFAR10のデータは、画像データとその画像が何の画像かを表すラベルが含まれている。ラベルはこの場合だと0-9の10個の数字からなり、それぞれに犬や猫などが対応する。この数字と名前の対応がclassesで与えられている。

可視化

基本的にここで行うことは、

  • 正規化したデータを逆変換で元に戻す
  • テンソル量をNumpy arrayに変換しmatplotlibで可視化する

のである。まずこれに必要なモジュールを導入する。

import matplotlib.pyplot as plt
import numpy as np

データの構造の確認

実際に画像を出力する前に、各量のデータの構造を確認しておく。

dataiter = iter(trainloader)
images, labels = dataiter.next()

print(len(dataiter))
print(12500*4)

print(labels.size(),labels)
print(images.size())
print(images.shape)

上の入力に対して以下の出力が得られた。

50000
torch.Size([4]) tensor([5, 4, 5, 9])
torch.Size([4, 3, 32, 32])
torch.Size([4, 3, 32, 32])

この出力を理解する。

dataiter = iter(trainloader)でDataloader型のオブジェクトであるdataloaderをイテレータに変換している。イテレータに変換すると、next()を用いて各要素を順番に呼び出すことができるようになる。今回は省略するがこれらのメソッドは__iter__()で定義されるはずなので具体的に何をしているか見たければ、Dataloaderクラスのソースの該当箇所を見れば分かるはずである。

実際、詳しく見なくともこの後に続く文、

images, labels = dataiter.next()

を見ると、1つ目の返り値が画像データの情報で、2つ目の返り値が各データのラベルの情報であることが分かる。ラベルは数字で返ってきているが、これは既に定義したclasses = ('plane', 'car', 'bird', 'cat','deer', 'dog', 'frog', 'horse', 'ship', 'truck')と対応させてラベルを読み取ることができるようになっている。

とりあえず、このイテレータが何要素あるのか見てみると、 print(len(dataiter)) = 12500 であった。今回はバッチ数を4にしているで、各要素には4つの画像データが入っていることになる。つまり、print(12500*4) = 50000が読み込まれた全画像の数であることが分かる。

さて、この数字は何なのであろうか?以下のURLにCIFAR10についての基本的なことがまとめられているので見てみる。これによると、CIFAR10は全部で60000個の画像からなっており、そのうち50000個をTrainingデータ、10000個をTestデータとして用意している。今回ロードしたのは教師データなので、確かにこの数に一致している。

CIFAR-10 and CIFAR-100 datasets

次に、labelsのサイズと、実際の値を読み取った。 print(labels.size(),labels)に対してtorch.Size([4]) tensor([8, 2, 2, 5])が返ってきている。これはlabelが4つの量からなっており、その値は[8,2,2,5]であることを言っている。たしかにバッチ数と同じ数のラベルが返ってきており、この場合は「船、鳥、鳥、犬」を表していることになる。

同様に、imagesの構造も確認する。ちなみに、テンソルの構造はsize()shapeどちらでも見ることができる。

print(images.size())
print(images.shape)

これらに対しての返り値はtorch.Size([4, 3, 32, 32])であった。これは確かに、4つの画像からなり、各画像が3*32*32のサイズを持っていることを表しており、そのとおりである。

画像の可視化

実際にこれらを出力するには以下のようにすれば良い。まず、可視化を実現する関数を準備する。

def imshow(img):
    img = img / 2 + 0.5     # unnormalize
    npimg = img.numpy()
    plt.imshow(np.transpose(npimg, (1, 2, 0)))
    plt.show()

この関数内で上述した、データ準備する際施した正規化の逆変換を行い、numpy arrayに変換したあとmatplotlibで出力する一連の流れを実装したものである。ここで、transposeに関しては後で改めて説明する。

以下で実際に画像を出力する。

# show images
imshow(torchvision.utils.make_grid(images))
# print labels
print(' '.join('%5s' % classes[labels[j]] for j in range(4)))

f:id:Tatsupuri:20201124234728p:plain

このようにデータを可視化することができた。しかしここで上のコードを見ると、可視化されているのはimagesではなくてtorchvision.utils.make_grid(images)であることに注意する。

上で既に説明したように今回はサイズ4のミニバッチを用いているので、imagesの次元は4*3*32*32であり、画像データの次元C*H*Wと一致しない。そこでtorchvision.utils.make_gridを用いて4つの画像を1つの画像に結合する必要があるのだ。

実際以下のURL見るとわかるが、torchvision.utils.make_grid(images)は、(B,C,H,W)の構造をもつデータからバッチ数分ある画像を結像して一つの(C,H,W)の構造を持つデータに変換することを行う。

torchvision.utils — PyTorch 1.7.0 documentation

この際様々なパラメータを与えることができて、例えば結合するときの画像間の間隔などを変えることができる。上の公式レファレンスか下のURL

画像をただ並べたいときに使えるTorchVision | Shikoan's ML Blog

も参考になる。

ちなみに上の出力だと、4*3*32*32だったのを、3*36*138に変換している。これは、次のように理解できる。まず上下に、2ピクセルずつのマージンを加えているので、32から36になっており、横方向には、2ピクセルのマージンが5つ入っている。なので、32 * 4 + 2 * 5 = 138であり確かに一致している。

また、torchvision.utils.save_imageを用いれば結合された画像を保存することができる。

ここで先ほどimshow関数の中の以下の処理についても補足しておく。

plt.imshow(np.transpose(npimg, (1, 2, 0)))

NumpyのTranspose関数は多次元配列の軸の入れかえを行う関数である。この場合は、「もともと(0,1,2)の構造を持っていたものを(1,2,0)に変えよ」ということなので、(C,H,W) -> (H,W,C) の構造に変換したということである。これで正しくmatplotlibで出力することができる。

まとめ

今回は、公式チュートリアルのCIFAR10の分類器を作るプロジェクトにおいて、データの準備と可視化に関してフォーカスしてまとめた。これで学習の準備が整ったが、長くなってきたので次の記事で実際にモデルを構築し学習を行う。

Windowsで機械学習に入門する前の準備

こんにちは。たつぷりです。今までMacbook Airがメインのマシンだったのですが、ゲーム用(作るためにも、プレイするためにも)に最近Windowsのマシンを購入しました。GTX 1650Tiがのっています。いままでWindowsの環境で作業したことがなかったので、備忘録としてこの記事に残しておくことにします。

思想の違いに応じて準備には差があると思いますが、少なくとも自分はこの準備でpytorchのチュートリアルはできたので良しとします。今後必要に応じて加筆の可能性はあります。

Pythonの導入

PythonWindowsに導入する。筆者はまず以下のサイトから現時点で(安定版として)最新版のバージョン3.9.0をダウンロードした。その後、インストーラーに従ってインストールした。

www.python.org

インストーラーで「Pathを追加する」旨のオプションがあるが、後述するようにWindowsではバージョンの管理はPython Launcherを用いて行われるので特に必須ではないらしい。

機械学習用のライブラリとしてPytorchを使うことにする。しかし上ではバージョン3.9.0を導入しているが、実は現時点(2020年11月)ではpytorchはpython3.9シリーズに対応していない。ライブラリの対応バージョンをあらかじめ確認するのは非常に大事であることを学んだ。

そこで上に加えて新しくバージョン3.7.8を導入した。このバージョンを選んだ理由は単純にMacで使っていたから安心感があるというだけである。

バージョンの切り替え

さてこの段階で、この環境にはバージョン3.9と3.7が共存するようになった。とりあえずコマンドプロンプトからpythonを実行するとバージョン3.9が呼び出された。今回pytorchを用いる都合からバージョン3.7を使いたいのでバージョンを切り替える方法を調べた。

このセクションの内容はここを参考にして行った。 Pythonの複数バージョンの扱い方(Windowsの場合) | ガンマソフト株式会社

Python Launcherによるバージョン管理

Windowsの場合、pythonはマイナーバージョンごとにディレクトリが分けられ保存されている。環境のすべてのバージョンを確認するには以下のコマンドを実行すればよい。

C:\Users\hoge>py --list 
Installed Pythons found by py Launcher for Windows 
 -3.9-64 * 
 -3.7-64 

ちなみに、以下のオプションでコマンドたたくと、各バージョンのpythonのパスを調べることができる。

py --list-paths 

さてWindowspythonPython Launcher(py.exe)というアプリケーションで管理されている。これはpyで実行することができる。Python Launcherはデフォルトでは導入されているバージョンのうち、最も新しいバージョンのPythonに紐づけるらしい。任意のバージョンのPythonに対してプログラムを実行したいときは

py -(version)

を用いる。例えば、py -3.7 とたたくと、Ver3.7のpythonが実行される。また、py -(version) -m をコマンドの頭につけることで、該当のバージョンのpythonに紐づけることができる。

例えばpipもそのままたたくと、Python Launcherによって一番新しいバージョンのものに自動的に紐づくが、

py -3.7 -m  pip install hoge

とすることでバージョン3.7に紐づくパッケージの管理をすることができる。

Jupyter Labの導入

では実際にJupyter Labを導入する。今回は一貫してバージョン3.7に紐づけることにする。公式レファレンスを参照して

py -3.7 -m pip install jupyterlab

実行することで導入できる。

jupyterlab.readthedocs.io

Jupyter Labの起動に関しても全く同様で、

py -3.7 -m jupyterlab

とたたくと、3.7をカーネルとしてJupyterを起動することができる。

CUDAの導入

次にGPUを使えるようにするための設定を行う。GTX 1650Tiに対しての設定を行った。

まずNVIDIA社製のGPUを利用するには、

  • NVIDIAグラボドライバーの導入
  • CUDAの導入

が必要とのことである。

ドライバーは以下のリンクから該当のGPUを選択。オプションに関して、ドライバーはGameReadyとStudioの2種類がある。調べてみると、GameReadyは最新版で、Studioは安定板という違いのようだ。自分は最新のゲームをプレイする可能性があるので、とりあえずGameReadyを選択してみた。

www.nvidia.co.jp

CUDAは以下のリンクから導入した。今回は現時点で最新版のVer11.1を導入した。

developer.nvidia.com

CUDAが導入されたどうかは、導入されたCUDAのVersionを調べる以下のコマンドで確認することができる。

C:\Users\hoge>nvcc -V 
nvcc: NVIDIA (R) Cuda compiler driver 
Copyright (c) 2005-2020 NVIDIA Corporation 
Built on Mon_Oct_12_20:54:10_Pacific_Daylight_Time_2020 
Cuda compilation tools, <span style="color: #ff0000">release 11.1</span>, V11.1.105 
Build cuda_11.1.relgpu_drvr455TC455_06.29190527_0

たしかに、バージョン11.1が導入されていることが確認できた。

pytorchの導入

以下、Numpyなどの最低限のライブラリは既に導入していることを仮定している。 pytorchをpipを用いて導入する。しかしそのオプションは環境に応じて異なるので注意する。

pytorch.org

から使っている環境のバージョンに合わせて、pipコマンドを出力してもらう。今回の場合は

pip install torch===1.7.0+cu110 torchvision===0.8.1+cu110 torchaudio===0.7.0 -f https://download.pytorch.org/whl/torch_stable.html

であった。

さて上述のとおり、このコマンドをそのままたたくとPython Lancherによって最新バージョンの3.9に紐づいてしまうので注意する。今回は3.7に導入したいので以下のコマンドをたたく。

py -3.7 -m pip install torch===1.7.0+cu110 torchvision===0.8.1+cu110 torchaudio===0.7.0 -f https://download.pytorch.org/whl/torch_stable.html

とすることで正常にインストールすることができた。

確認しておくと、

C:\Users\hoge>py -3.7 -m pip list 
Package             Version 
------------------- -----------
torch               1.7.0+cu110

が入っていることが確認できた。

以上でPytorchのチュートリアルをこなす準備はできた。

参考資料

  1. Python Releases for Windows | Python.org
  2. Pythonの複数バージョンの扱い方(Windowsの場合) | ガンマソフト株式会社
  3. WindowsユーザーはPythonランチャーの存在を意識しましょう | ガンマソフト株式会社
  4. NVIDIA Driverってどれをダウンロードすればいいの? - Qiita

Google画像検索から画像を収集する方法

どうもこんにちは、たつぷりです。最近少し忙しかったので久しぶりの更新です。最近、大学院で開かれていた機械学習素粒子物理への応用に関してのスクールに参加しました。その関連で機械学習に興味を持ったのでしばらくその関連で何かできないかなあと思っている今日この頃です。

今日は自分で何か学習をさせる時にトレーニングデータを収集する一つの方法をまとめることにしました。

目的

PytorchでDCGANのtutrialをやってみたので、復習のために自分でも同様のことをやってみようと思った。 tutrialでは、セレブの画像からFakeセレブを生成していた。そこで自分でオリジナルの学習用の画像を用意する方法をまとめておく。

似た記事は腐るほどある気もするが、自分の文脈で作成したコードが自分が後にフォローできるレベルで解説付きで示した。

目標

自分が好きなものでやったほうが楽しいという理由で、今回はバイオハザード風のFake画像を作ることを最終的な目標にする。 そこで、具体的な目標は以下である。

実装

モジュールと各種設定

最初にモジュールのインポートと、保存先のパスの設定などを行う。

import os 
from urllib import request
import requests 
from pathlib import Path 
from bs4 import BeautifulSoup 
from IPython.display import HTML, display,clear_output

#インポート先の設定 
output_folder = Path('data/re2') 
output_folder.mkdir(exist_ok=True) 
output_filename = "re2" 

header={'User-Agent':"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.134 Safari/537.36"} 

query変数にGoogle検索の検索ワードを代入する。

query = "resident+evil+re2+NEST"

Beautiful Soupを用いたスクレイピング

今回はBeautiful Soupを用いてスクレイピングを行う。 Beautiful Soupはhtmlのパースを行うためのライブラリの一つである。 公式ドキュメントは下。

www.crummy.com

以下で行うのは、まずURLで指定されるページをHTMLでurllib.request で取得し、それをBeaufulSoup() でパースするという流れである。 パースとは、HTMLのようなテキストをXMLのような構造を持つデータに変換することと考えてよい。それを実行するプログラムがパーサである。 BeautifulSoup() の引数は、解析するHTMLと用いるパーサである。

#最終的に以下のリストに画像のURLの一覧を保存する。
linklist = []

#検索ワードから構成したURLで指定されるページをHTMLで取得し、BeautifulSoupでパースする。
url = "https://www.google.com/search?q=" + query + "&source=lnms&tbm=isch" 
html = request.urlopen(request.Request(url,headers=header))
soup = BeautifulSoup(html, 'lxml') 

画像のURL一覧を取得

これで、soup オブジェクトからHTMLのタグやその属性にアクセスすることができる。 以下ではimg タグを取得する。指定のタグを取得するには、find_all() メソッドを用いれば良い。実際に以下のimg_list を出力すると、

<img alt="hoge1" class="hoge2" src="hige3">
<img alt="hoge4" class="hoge5" src="hige6">
<img alt="hoge7" class="hoge8" src="hige9">

のようなリストが得らていることが分かる。

次に、これらのimgタグから、画像のソースURLを取得する。画像のソースURLは、data-src アトリビュートに格納されているのでこれを取得すればよい。具体的には

の手順を行えば良い。

#soupオブジェクトからHTMLのタグやその属性にアクセスすることができる。

#imgタグのリストを取得する。
img_list = soup.find_all("img")  

#imgのリストから画像のソースURLを取得する。
for tag in img_list:   
    if tag.has_attr('data-src'):  
        linklist.append(tag.attrs['data-src'])  

出力

以上で、検索によって得られた画像のURLのリストを得ることができた。 ここで一旦、出力の準備をする。ここでは、入力した検索ワード、取得できた画像の数、ダウンロードの進捗を表すプログレスバーなどを用意した。

ただしプログレスバーに関しては、以下の関数をどこかで定義しておく必要がある。

def progress_bar(count, total, message=''): 
    return HTML(""" 
        <progress  
            value='{count}' 
            max='{total}', 
            style='width: 30%' 
        > 
            {count} 
        </progress> {frac}% {message} 
    """.format(count=count,total=total,frac=int(float(count)/float(total)*100.),message=" "+message))

次に、一つの検索ワードだけでは十分な画像が取得できない可能性もあるので、複数の検索ワードで画像を収集する場合を想定して、ファイルの名前の付け方を少し工夫しておく。尚、後述するがこの方法では画像の重複を避けることができない。

このプロジェクトでは基本的にはファイルの名前は、hoge-0.jpg,hoge-1.jpg...    のように番号をつけて保存していくことにする。そこで保存するディレクトリから既に存在しているファイルの数を取得して、その番号からファイル名につける番号を始めれば、既にあるファイルを上書きすることはなくなる。

ディレクトリからファイルの総数を取得する方法は以下を参考にした。

Python - ファイル・ディレクトリの一覧を取得する | murashun.jp

尚ここではそうしなかったが、この段階でURLに対応した名前を付けておけば同じURLからくる画像の重複は防ぐことができそうである。

#アウトプット部分

#検索ワードやデータ数、プログレスバーを表示する。
print("Query is "+ query)  
print("Number of image:" + str(len(linklist)))  
progress = display(progress_bar(0,len(linklist)),display_id=True)  

#画像を保存するディレクトリのファイルの数を取得
files = os.listdir(output_folder)  
file = [f for f in files if os.path.isfile(os.path.join(output_folder, f))]  
index = len(file) 

データの保存

linklistに画像のURLがリストとして格納されているので、各URLに対して画像のダウンロードを行う。 上述のルールでファイル名を付けていき、requests でデータを取得しopen(save_path, 'wb').write(image.content)  でデータを保存していく。

for img in linklist: 
    filename = output_filename + "-" + str(index)+".jpg"   
    save_path = output_folder.joinpath(filename)   

    message= filename + ' is importing.'   
    progress.update(progress_bar(index-len(file)+1 ,len(linklist),message=message))   


    #画像ファイルのURLからデータをダウンロード  
    try:  
        #URLからデータを取得
        image = requests.get(img)

        #データを保存
        open(save_path, 'wb').write(image.content)   

    except ValueError:
        print("Error")    

    index += 1

プログラムの実行

query に、検索したいキーワードを+ で結合して代入する。 その後、実行すると該当する画像のダウンロードが始まる。

出力のプログレスバーが100%になったらダウンロード完了である。

f:id:Tatsupuri:20201123105833p:plain

各検索キーワードに応じて、ヒットする画像の数が少なかったりすることがある。この場合は検索キーワードを色々変えて画像を取得していく。 上で既に述べたように、このプログラムではディレクトリの中のファイル数をカウントして、ナンバリングをそこから始めるようにしているので、関連する検索ワードを変えていけばどんどん画像を取得することができる。

例えば、自分の場合は

resident+evil+re2+zombie
resident+evil+re3+zombie
biohazard+re2

など、で検索をかけるとすぐに500枚くらいたまった。

結果

保存先のデータを見てみると、以下のようにそれっぽい画像がインポートされている。

f:id:Tatsupuri:20201123105836p:plain

ここですぐに問題に気が付く。これは当然予想できる問題であるが、重複する画像が存在していることである。

今回の範囲では大した量ではないので、重複している画像は手で削除した。

課題

上で述べた通り、このままでは重複データがかなり存在ことがある。これは複数のキーワードで検索しているのでの当然のことである。上では手で消すということをしたが、その都度手で削除するのは大変時間の無駄である。

上で既に述べたようにURLと一対一対応になるようにファイル名を付けておけば同じURLからくる重複は防ぐことができる。しかし拾い画を張り付けたりしているケース(?)もあって違うURLでも同じ画像があったりすることもある。

そこでせっかくなので練習として、画像の重複を判定するモデルを作成したいと思う。近いうちにその記事を書く予定である。

またもう一つの問題は今の段階では画像の拡張子をあまり意識していない点である。とりあえずJPGで保存しているが、必ずしもそうでないはずである。拡張子を取得してそれに応じてファイル名を変える工夫する必要がある。

参考にしたもの

  1. Pythonのスクレイピングで、いらすとやの画像を一気にダウンロード | ハシカケ-実現したいことから学べるプログラミングサイト
  2. Googleの画像検索APIを使って画像を大量に収集する - Qiita
  3. BeautifulSoup のエラー "Couldn't find a tree builder" の原因と対処法 - Qiita

スクリプト全文

今回作成したスクリプトの全文を以下に書いておく。これらは各ブロックをJupyterNotebookに貼り付けて動かすことを想定して作成した。

import os 
from urllib import request
#import time 
#import re 
#import requests 
from pathlib import Path 
from bs4 import BeautifulSoup 
from IPython.display import HTML, display,clear_output
#インポート先の設定 
output_folder = Path('data/re2') 
output_folder.mkdir(exist_ok=True) 
output_filename = "re2" 
header={'User-Agent':"Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/43.0.2357.134 Safari/537.36"}
def progress_bar(count, total, message=''):
    return HTML("""
        <progress 
            value='{count}'
            max='{total}',
            style='width: 30%'
        >
            {count}
        </progress> {frac}% {message}
    """.format(count=count,total=total,frac=int(float(count)/float(total)*100.),message=" "+message))
query = "resident+evil+re2+NEST"
#最終的に以下のリストに画像のURLの一覧を保存する。
linklist = []
#検索ワードから構成したURLで指定されるページをHTMLで取得し、BeautifulSoupでパースする。
url = "https://www.google.com/search?q=" + query + "&source=lnms&tbm=isch" 
html = request.urlopen(request.Request(url,headers=header))
soup = BeautifulSoup(html, 'lxml')

#soupオブジェクトからHTMLのタグやその属性にアクセスすることができる。
#imgタグのリストを取得する。
img_list = soup.find_all("img")  
#imgのリストから画像のソースURLを取得する。
for tag in img_list:   
    if tag.has_attr('data-src'):  
        linklist.append(tag.attrs['data-src'])

#アウトプット部分
#検索ワードやデータ数、プログレスバーを表示する。
print("Query is "+ query)  
print("Number of image:" + str(len(linklist)))  
progress = display(progress_bar(0,len(linklist)),display_id=True)  
#画像を保存するディレクトリのファイルの数を取得
files = os.listdir(output_folder)  
file = [f for f in files if os.path.isfile(os.path.join(output_folder, f))]  
index = len(file)

for img in linklist:
    filename = output_filename + "-" + str(index)+".jpg"  
    save_path = output_folder.joinpath(filename)  

    message= filename + ' is importing.'  
    progress.update(progress_bar(index-len(file)+1 ,len(linklist),message=message))  


    #画像ファイルのURLからデータをダウンロード 
    try: 
        #URLからデータを取得
        image = requests.get(img)

        #データを保存
        open(save_path, 'wb').write(image.content)  

    except ValueError:
        print("Error")   

    index += 1