Szarny.io

There should be one-- and preferably only one --obvious way to do it.

Pythonで機械学習の初歩の初歩 - 基礎概念とクラス分類 -

はじめに

Python3とscikit-learnで機械学習のプログラミングを勉強中です.
今までに勉強したことのまとめとして,初歩的な内容ですが,クラス分類と回帰のモデルについて,そのコーディング方法についてまとめたいと思います.

大局的な理解を目標としたいので,本記事では細かい定義や数式に関しては省略しています.

実行環境

バージョン

コーディングは,Jupyter Notebook上で行っています.

Python 3.6.2
NumPy 1.13.3
matplotlib 2.0.2
IPython 6.1.0
scikit-learn 0.19.0

インポート設定

この辺をそろえておけば十分かと思います.

import sys
import numpy as np
import pandas as pd
import scipy as sp
import matplotlib
import matplotlib.pyplot as plt
%matplotlib inline
import sklearn

基本的な流れと重要概念

モデルの構築

機械学習を行うにあたっては,まず利用するモデルにデータを入力し,学習させる必要があります.
モデルをデータに沿って学習させることで,未知のデータに対しても正確な予測を返すことが期待できます.
f:id:Szarny:20171016225408p:plain:w500

モデルの評価

ところで,モデルにデータを学習させたからと言って「必ずしも未知のデータに対応できるモデルになっている」と言い切ることはできません.
つまり,適切に学習が行われたのかを評価する仕組みが必要になります.

このような場合,所持しているデータセット訓練用データテストデータに分割する手法が有用です.
すなわち,モデルを訓練用データを用いて学習させた後,訓練用データとテストデータそれぞれを用いて期待するような結果を返すかを確かめるのです.(交差検証といいます)
f:id:Szarny:20171016230105p:plain:w650

モデルの評価結果それぞれにおいて,考えられる状態を以下に示します.
f:id:Szarny:20171016231519p:plain

汎化とは

汎化とは,学習済みのモデルが,未知のデータに対して正確な予測を返すことができている状態のことをいいます.
テストデータに対して高い適合度を示しているということは,このモデルは未知のデータに対しても正確な予測をすることが可能だということが言えるでしょう.

過剰適合とは

過剰適合とは,訓練用データに対して,過度に複雑なモデルを構築してしまう状態のことを言います.
言い換えれば,モデルが訓練用データの専門バカになってしまっている状態です.
そのため,訓練用データに対しては高い適合度を示しますが,テストデータや未知のデータに対しては低い適合度を示します.
過学習とも言います

適合不足とは

適合不足とは,訓練用データに対して,過度に単純なモデルを構築してしまう状態のことを言います.
そのため,訓練用データに対しても,テストデータに対しても低い適合度を示すような状態のことを言います.
この状態になった場合は,そもそもデータ量や特徴量が足りないのではないかモデルのパラメータ設定が不適切ではないのか,ということをチェックする必要があります.

クラス分類

クラス分類の概要

クラス分類には,大きく分けて2クラス分類多クラス分類があります.

「あるデータはAクラスとBクラスどちらに属するのか」といった分類方法は2クラス分類にあたります.
一方で,「あるデータはA,B,C,...クラスのどれに属するのか」といった分類方法は多クラス分類にあたります.

これから,以下のモデルを用いて,クラス分類を実際に試してみます.

k-近傍法

k-近傍法とは,あるデータのベクトルとの距離が最も小さいk個のベクトルを基にクラスを分類するモデルです.

f:id:Szarny:20171016222939p:plain
k近傍法 - Wikipediaより

上の例は,緑のデータが赤と青のどちらに分類するかをk-近傍法で決定する様子を示しています.

k=3の時,緑のデータに近い3つのデータを見ると,赤の方が多いことが分かります.
この時,緑のデータは赤の方に属すると判断されます.
k=5の時も同じように進めると,先ほどとは異なり,緑のデータは青の方に属すると判断されます.

k-近傍法のリファレンス

モデルは,sklearn.neighbors.KNeighborsClassifierを用います.
よく用いられる代表的なメソッドは,以下の通りです.

# データXと解答yのペアを分割する
X_train, X_test, y_train, y_test = train_test_split(X, y)

# 近傍の数をnとして,モデルを生成
KNeighborsClassifier(n_neighbors=n)

# データXと解答yのペアをモデルに学習させる
KNeighborsClassifier(...).fit(X, y)

# データXと解答yのペアを与え,モデルの適合度を調査する
KNeighborsClassifier(...).score(X, y)

# データXに対する解答yを予測させる
KNeighborsClassifier(...).predict(X)

k-近傍法の実例

データセットとして,お馴染みのアイリスデータセットを用います.
詳細は,こちらを参照してください.
The Iris Dataset — scikit-learn 0.19.0 documentation


パラメータである近傍の数を変化させていったときに,適合度がどう変化するのかを調査します.

# データセットとモジュールのインポート
from sklearn.datasets import load_iris
from sklearn.model_selection import train_test_split 
from sklearn.neighbors import KNeighborsClassifier

# アイリスデータセットの特徴量と答えを抽出
iris_data = load_iris()
feature_data = iris_data.data
target_data = iris_data.target

# 訓練用データとテストデータに分割
X_train, X_test, y_train, y_test = train_test_split(feature_data, target_data, random_state=9999)

# 近傍の数を変化させながら,適合度を調査する
n_neighbors = range(1, 101)
train_score = []
test_score = []

for n_neighbor in n_neighbors:
    # 分類器の訓練
    clf = KNeighborsClassifier(n_neighbors=n_neighbor).fit(X_train, y_train)
    
    train_score.append(clf.score(X_train, y_train))
    test_score.append(clf.score(X_test, y_test))
    
plt.figure(facecolor="w")
plt.plot(n_neighbors, train_score, "-", label="train score")
plt.plot(n_neighbors, test_score, "-", label="test score")
plt.hlines(1, 0, len(n_neighbors), color="k")
plt.title("Score of KNeighborsClassifier")
plt.ylabel("Score")
plt.xlabel("n_neighbors")
plt.xlim(0, len(n_neighbors))
plt.legend(loc="lower left")
plt.grid()

f:id:Szarny:20171016234736p:plain:w600

結果を見ると,近傍の数が3から15くらいの間では,比較的優秀な適合度を示しているのに対し,それ以外では劣った適合度を示しています.

近傍の数が少ないときに,訓練用データに対しては適合度が高い一方,テストデータに対しては適合度が落ちています.
これは,過剰学習が発生したためだと考えられます.
つまり,少ない近傍しか考慮していないために,汎用的なモデルの構築に失敗している状態です.

逆に,近傍の数が多いときには,両方のデータに対して適合度が急落しています.
これは,適合不足が発生したためだと考えられます.
つまり,あまりにも多くの近傍を考慮した結果,ずさんなモデルが構築されてしまった状態です.

以上より,このデータセットに対しては,モデルのパラメータとして,近傍の数を3から15くらいに設定するのが最適であるとわかりました.

もちろん,データセットの分割方法によって変化する可能性もあります.(random_stateパラメータで変更可能です)

おわりに

機械学習の基礎概念について,k-近傍法によるクラス分類を用いてまとめました.
次回は,回帰モデルについてまとめようと思います.

単方向リストの勉強(基本情報 平成24年春期 午前問7)

はじめに

データ構造とアルゴリズムについて,再度勉強中です.
配列やFIFO, LIFOについては実際のプログラミング時にもよく利用する一方,リスト構造についてはなかなか使う機会がなく,忘れがちだったのでこの機会にまとめておきます.

単方向リスト構造について

単方向リストとは,データ部ポインタ部を持つセルのリンク配置によって構成されるデータ構造の1つです.
つまり,複数のセルをポインタによって数珠つなぎのような形にすることで,データをまとめて扱うことを可能にしたデータ構造です.
慣例的に,先頭セルを指すポインタを head末尾セルを指すポインタを tailと呼びます.

配列では,データを扱う際に添え字を利用してA[2]のようにデータにアクセスするといった「ランダムアクセス」が可能でした.
一方で,単方向リストでは, head及び tailのポインタから目的のデータまで順番にセルを辿ることで,データを扱います.

図で表すと,以下のようにになります.

f:id:Szarny:20171012222925p:plain:w700

なお, Data_nのセルに,後続セルへのポインタが設定されていないのは,それが最後のセルだからです.
プログラムで実装する際には,Nullに設定するのが一般的です.

単方向リスト構造におけるデータ操作

ここからは,基本情報で出題された問題を基に,単方向リスト構造におけるデータ操作の手順について確認していきます.

基本情報 平成24年春期 午前問7

多数のデータが単方向リスト構造で格納されている。このリスト構造には,先頭ポインタとは別に,末尾のデータを指し示す末尾ポインタがある。次の操作のうち,ポインタを参照する回数が最も多いものはどれか。

ア リストの先頭にデータを挿入する。
イ リストの先頭のデータを削除する。
ウ リストの末尾にデータを挿入する。
エ リストの末尾のデータを削除する。

リストの先頭にデータを挿入する

元々のリストと,やりたいことを図で示すと,以下のようになります.
f:id:Szarny:20171012222925p:plain:w700
f:id:Szarny:20171012224135p:plain:w700

  1. 新しいセルを作成する
  2. 新しいセルが Data_1のセルを指すようにする
  3.  headが新しいセルを指すようにする

以上の処理は, headのポインタにアクセスできれば可能です.
なぜなら,新しいセルのポインタに設定するアドレス( Data_1のアドレス)は, headに設定されていたポインタのアドレスだからです.
つまり,参照が必要なのは headだけです.

なお,2と3を入れ替えると, headが先に更新されてしまいます.そのため, Data_1のセルのアドレスが分からなくなり,設定に失敗します.

リストの先頭のデータを削除する

元々のリストと,やりたいことを図で示すと,以下のようになります.
f:id:Szarny:20171012222925p:plain:w700
f:id:Szarny:20171012224932p:plain:w700

  1.  head Data_2のセルを指すようにする

以上の処理は, head Data_1のセルにアクセスできれば可能です.
 Data_2のセルのアドレスは, Data_1のセルのポインタに設定されています.つまり, Data_1のセルにアクセスできれば必要な情報は全て揃います.
つまり,参照が必要なのは head Data_1のセルです.

リストの末尾にデータを挿入する

元々のリストと,やりたいことを図で示すと,以下のようになります.
f:id:Szarny:20171012222925p:plain:w700
f:id:Szarny:20171012230023p:plain:w700

  1. 新しいセルを作成する
  2.  Data_nのセルが新しいセルを指すようにする
  3.  tailが新しいセルを指すようにする

以上の処理は, tail Data_nのセルにアクセスできれば可能です.
新しいセルのアドレスは既知ですが, Data_nのポインタに新しいセルのアドレスを設定するには, Data_nまで到達する必要があります.
 Data_nへは tailを辿ればすぐに到達できるので,そこで設定を行い,後は tailに新しいセルのアドレスを設定すれば完了です.

なお,2と3を入れ替えると, tailが先に更新されてしまいます.そのため, Data_nのセルのアドレスを知るためには,わざわざ headから Data_nまで辿る必要が生じます.

リストの末尾のデータを削除する

元々のリストと,やりたいことを図で示すと,以下のようになります.
f:id:Szarny:20171012222925p:plain:w700
f:id:Szarny:20171012230556p:plain:w700

  1.  Data_{n-1}のアドレスを tailのポインタに設定する
  2.  Data_{n-1}のポインタをNullに設定する

この2つの処理を行うには, Data_{n-1}にアクセスする必要があります.
なぜなら, Data_{n-1}のセルのアドレスを知ることと,そのポインタを更新することの両方が必要だからです.
しかし, Data_{n-1}にアクセスするには, headから順番に辿るしか方法がありません.
つまり,データ数 nに応じた回数分,リンクをたどる必要が生じるということです.

問題のまとめ

操作に応じたリンクの参照内容をまとめると,以下のようになります.

操作 参照先
リストの先頭にデータを挿入する  head
リストの先頭のデータを削除する  head,  Data_1
リストの末尾にデータを挿入する  tail,  Data_{n}
リストの末尾のデータを削除する  head,  Data_1,  Data_2, ...,  Data_{n-2},  Data_{n-1}

以上より,最も参照回数が多くなるのは

「エ リストの末尾のデータを削除する」

です.

おわりに

単方向リスト構造について,基本情報の過去問を用いてまとめました.
いずれ言語を使った実装も行ってみようと考えています.

Webスクレイピングで画像コレクションを作成するツール

はじめに

Python3のrequests, BeautifulSoup, osモジュール等を組み合わせて,画像収集ツールを作成しました.
適当なキーワードをコマンドライン引数に指定して実行すると,関連した画像を自動的にダウンロードします.
ダウンロードされた画像は,Pythonファイルと同階層に生成されるディレクトリ内に,キーワード別に保存されます.

ソースコード

#! python3
# imageCollector.py

import requests, os, sys, webbrowser
from bs4 import BeautifulSoup

URL = "https://www.google.co.jp/search?tbm=isch&q="

def main():
    # 検索ワードの取得
    if len(sys.argv) == 1:
        print("[*] Usage: python3 {} <keyword 1> <keyword 2> ...".format(sys.argv[0]))
        sys.exit()
    else:
        keyword = " ".join(sys.argv[1:])

    # ファイル名の生成
    filename = ""
    for word in sys.argv[1:]:
        filename += (word + "_")

    # フォルダがなければ生成
    if not os.path.isdir(r"./imageCollector"):
        os.mkdir(r"./imageCollector")
    if not os.path.isdir(r"./imageCollector/" + filename[:-1]):
        os.mkdir(r"./imageCollector/" + filename[:-1])
    os.chdir(r"./imageCollector/" + filename[:-1])

    # 検索結果の取得
    res = requests.get(URL + keyword)
    res.raise_for_status()
    soup = BeautifulSoup(res.text, "html.parser")

    # 画像タグの取得
    image_tags = soup.find_all("img")

    # 画像タグ1つ1つについてダウンロード
    for num, image_tag in enumerate(image_tags, start=1):
        # 10件でストップ
        if num > 10:
            break

        print("[*] Downloading ->  {}{:02d}.jpg ...".format(filename, num), end=" ")
        res_img = requests.get(image_tag.get("src"))
        res_img.raise_for_status()

        # ファイルオープン
        with open(r"./" + filename + str(num) + ".jpg", "wb") as f_write:
            for chunk in res_img.iter_content(100000):
                f_write.write(chunk)

        print("Done")

    print("[*] Download Completed!")
    webbrowser.open(r"file:///" + os.getcwd())
    sys.exit()

if __name__ == '__main__':
    main()

実行例

>python imageCollector.py
[*] Usage: python3 imageCollector.py <keyword 1> <keyword 2> ...

>python imageCollector.py いらすとや フリー 動物
[*] Downloading ->  いらすとや_フリー_動物_01.jpg ... Done
[*] Downloading ->  いらすとや_フリー_動物_02.jpg ... Done
[*] Downloading ->  いらすとや_フリー_動物_03.jpg ... Done
[*] Downloading ->  いらすとや_フリー_動物_04.jpg ... Done
[*] Downloading ->  いらすとや_フリー_動物_05.jpg ... Done
[*] Downloading ->  いらすとや_フリー_動物_06.jpg ... Done
[*] Downloading ->  いらすとや_フリー_動物_07.jpg ... Done
[*] Downloading ->  いらすとや_フリー_動物_08.jpg ... Done
[*] Downloading ->  いらすとや_フリー_動物_09.jpg ... Done
[*] Downloading ->  いらすとや_フリー_動物_10.jpg ... Done
[*] Download Completed!

実行後は,ディレクトリが自動表示されます.
f:id:Szarny:20171010201351p:plain

参考文献

www.oreilly.co.jp