Python
機械学習
scikit-learn

数学も英語もPythonもわからないけど機械学習に触れたかった(後編)

概要

数学も英語もPythonもよくわからないし、機械学習とは一生縁がないのではと思っていた最中、突如仕事で必要になって必至に勉強したときのお話(後編)

前編はこちら
数学も英語もPythonもわからないけど機械学習に触れたかった(前編)

当時ブログに起こしたものをQiitaで書き直したのと、何もわからないド素人が頑張ったのでツッコミどころ満載かも。

前提

以下の環境で実装、動作確認済み

$ python --version
Python 3.5.2
$ pip list
ascii (3.6)
numpy (1.14.3)
pandas (0.22.0)
Pillow (5.1.0)
pip (9.0.1)
python-dateutil (2.7.2)
pytz (2018.4)
scikit-learn (0.19.1)
scipy (1.0.1)
setuptools (20.7.0)
six (1.11.0)
urllib3 (1.22)
wheel (0.29.0)

少しだけ実用的な機械学習を体験する

前編では、足し算を学習させるというクソの役にも立たない、幼稚園児でもできることをやらせて、それはそれで満足した。

とはいえ、やはり機械学習を勉強するからには、少なからず実用的っぽいことも体験したい。

ということで、Microsoftが提供する、なんかブラウザベースで機械学習ができるAzure Maschine Learningのチュートリアルに登場した、自動車の価格予想を、Python+scikit-learnでやってみることに。

今回の目的をザックリと書くと以下の通り

  1. 自動車に関する各種情報(性能など)と、その自動車の価格の関係を学習させる
  2. 各種情報のみを与えて、その価格を予測できるようにする
  3. その機械学習の精度を評価する

学習用データについて

学習用データは、前述の通りAzure Maschine Learningのチュートリアル内に登場する以下のようなCSVファイルを拝借する。

data.png

画像をだとわかりづらいが、以下の列で構成されている。列の内容は列名から概ね察せるが、最終列にあるprice列の値(自動車の価格)を、他の列の情報を元に予測するということがわかれば問題ない。

  • symboling
  • normalized-losses
  • make
  • fuel-type
  • aspiration
  • num-of-doors
  • body-style
  • drive-wheels
  • engine-location
  • wheel-base
  • length
  • width
  • height
  • curb-weight
  • engine-type
  • num-of-cylinders
  • engine-size
  • fuel-system
  • bore
  • stroke
  • compression-ratio
  • horsepower
  • peak-rpm
  • city-mpg
  • highway-mpg
  • price

CSVファイルを読み込む

前述のCSVファイル(data.csv)をスクリプトと同ディレクトリに配置した場合、以下のようにPandasのデータフレームに展開することができる。

df = pd.read_csv('data.csv')

無効な行/列の削除

CSVのスクリーンショットを見てもらうとわかるが、 全体的に値が”?”になっているセルが散らばっている。これは情報が無いという意味を表していると思うので、それらを取り除いていく。

まず、normalized-losses(2列目)は、列全体で”?”が多いので、思い切ってこの列自体を削除する。列についではdel文で削除できるので削除してしまう。Pandasのデータフレームは列名を指定することでその列全体を取得できるみたい。

del df['normalized-losses']

次に、他の"?"についてだが、それらについては一部の行のみ"?"になっているだけなので、列全体の削除はせず、"?"が一つでも含まれている行を削除するという方針にする。

"?"が存在する列はnum-of-doors, price, bore, stroke, horsepower, peak-rpmの6列なので、これらのいずれの列にも"?"が含まれていない行のみ取り出して再代入するという処理を行う。

なんとデータフレームでは以下のようにすれば、条件を指定して行をフィルタリング出来る。すごすぎる。

for col in ['num-of-doors', 'price', 'bore', 'stroke', 'horsepower', 'peak-rpm']:
 df = df[df[col] != '?']

でも多分こんなまどろっこしいやり方せずともなんとかなりそう。

車の情報と、価格を分ける

データフレームのうち、price列が価格(=予測する数値)で、それ以外の列が車の情報(=予測の元になる情報)になっているので、それを分割する。

データフレームだと列名を指定して列を取得できるので、price列全体を取得してlabelに代入。データフレーム全体からprice列を除いたものをdataに代入する。

label = df['price']
data  = df.drop('price', axis=1)

文字列を数値化する

今回も前編と同様、fitメソッドを用いて学習を行うが、fitメソッドはfloat型の値しか受け付けないみたい。今回のCSVには文字列も多く含んでいるので、それを数値化する必要がある。

そこで今回は、scikit-learnのLabelEncoderクラスを用いて各文字列を数値に置き換えることで、学習可能のデータに変換する。

変換が必要な列は、make, fuel-type, aspiration, num-of-doors, body-style, drive-wheels, engine-location, engine-type, num-of-cylinders, fuel-systemの10列なので、それぞれに対して、以下のようにLabelEncoderオブジェクトのfit_transformメソッドを適用することで文字列を数値に変換できる。

le = preprocessing.LabelEncoder()
for col in ['make', 'fuel-type', 'aspiration', 'num-of-doors', 'body-style', 'drive-wheels', 'engine-location', 'engine-type', 'num-of-cylinders', 'fuel-system']:
  data[col] = le.fit_transform(data[col])

例として、body-style列の、変換前後を出力すると以下のようになり、文字列が数値に変換されていることがわかる

変換前

0      convertible
1      convertible
2        hatchback
3            sedan
4            sedan
5            sedan
6            sedan
7            wagon
8            sedan
(以下略)

変換後

0      0
1      0
2      2
3      3
4      3
5      3
6      3
7      4
8      3
(以下略)

わざわざ変換する列名を配列でツラツラ書かなくてももっとスマートに書ける方法がありそう。いやきっとある。

学習用データと評価用データの分割

次に、学習用データの一部をテストデータとして抜き出す処理をする。

CSVファイルには約40行ぐらいのデータが含まれているが、このうち30行程度を学習用データ、残りの10行程度を評価用データにする。

生成した学習モデルを使って、評価用データから金額を予測し、それが実際の金額にどれほど近づいているかで学習モデルの精度を評価できるようにする。

データの分割には、train_test_split関数を使用する。datalabelを指定してあげると、学習用と評価用それぞれのdataとlabelを返してくれる。

train_data, test_data, train_label, test_label = train_test_split(data, label)

ランダムフォレストで学習させる

前編では線形回帰アルゴリズムを用いて足し算の学習を行わせたが、今回は試しに別の学習アルゴリズムを使ってみる。どのアルゴリズムをどのような場合に使えば良いかはハッキリ言って全然わかってないが、とりあえず線形回帰とは異なるアルゴリズムとして、ランダムフォレストとやらを使ってみることにする。

ランダムフォレスト(英: random forest, randomized trees)は、2001年に Leo Breiman によって提案された[1]機械学習のアルゴリズムであり、分類、回帰、クラスタリングに用いられる。決定木を弱学習器とする集団学習アルゴリズムであり、この名称は、ランダムサンプリングされたトレーニングデータによって学習した多数の決定木を使用することによる。対象によっては、同じく集団学習を用いるブースティングよりも有効とされる。(Wikipedia引用)

さっぱりわからん。が、scikit-learnではどの学習アルゴリズムを使おうと、その使い方が共通化されているので、線形回帰の時とほとんど同じコードで動かすことができる。

必要なモジュールをインポートした状態で以下のようにすることランダムフォレストで学習を実行できる。

clf = RandomForestRegressor()
clf.fit(train_data, train_label)

学習アルゴリズムのオブジェクトを代入してしまえば、どの学習アルゴリズムについてもfitメソッドを使うことができるので、前編と同じように利用することができる。

評価する

学習用データと評価用データに分割した際の、評価用データの方を用いて学習モデルの評価を行う。これまで通り、predictメソッドを用いて各自動車の価格を予想する。

predict = clf.predict(test_data)

とりあえず予測した結果と実際の価格を並べて出力してみる。

for i in range(len(test_label)):
  p = int(predict[i])
  t = int(test_label.iloc[i])
  print(p, t)

どうだろうか、かなり近い価格を出せているのではないだろうか。(予測値 正解値)

7898 6918
5572 5572
12764 12964
10198 11694
7299 8249
8949 9549
17450 15250
7198 8358
23875 17710
10245 8495
8238 8058
6338 6488
31600 28248
11199 8449
10295 6785
34028 32528
15985 12940
7299 6849
(以下略)

ただこれだと、どのぐらいの精度が出てるかわかりづらいので、正解に何%近いかを計算して出力するように修正する

for i in range(len(test_label)):
  p = int(predict[i])
  t = int(test_label.iloc[i])
  rate = int(min(t, p) / max(t, p) * 100)
  print(rate)

それなりの精度であることがわかる

70
93
79
87
75
97
91
84
90
97
88
89
95
90
100

さらに、個々の精度の平均値を計算し、全体の精度を出力するようにする

for i in range(len(test_label)):
  t = int(test_label.iloc[i])
  p = int(predict[i])
  rate_sum += int(min(t, p) / max(t, p) * 100)
print(rate_sum / len(test_label))

学習データとテストデータの組み合わせによってムラがあるので、何度か実行して確認してみると、概ね90%前後の精度が得られた

$ python car.py
90.16326530612245
$ python car.py
89.46938775510205
$ python car.py
88.71428571428571
$ python car.py
90.3265306122449

さらに、アルゴリズムをランダムフォレストから、線形回帰に変更して再評価してみる。変数clfに代入する学習モデルのオブジェクトを、ランダムフォレストから線形回帰に変更するだけで良い

clf = linear_model.LinearRegression()

同じように何度か実行してみると、83%程度と、ランダムフォレストより微妙に精度が下がり、アルゴリズムによって結果が変わることを実感した。

$ python cars.py
82.40816326530613
$ python cars.py
83.79591836734694
$ python cars.py
83.53061224489795
$ python cars.py
85.65306122448979
$ python cars.py
82.46938775510205

なお、学習モデルの評価方法についても、scikit-learnに様々な機能があるようだが、今回は意図的に自前で計算するようにして感覚を掴んだ。

ソースコード/実行結果

最後にソースコード全文

#
# 車の製品情報と価格を学習し、価格を予測させる
#
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestRegressor
from sklearn import preprocessing

# CSVファイルを読み込む
df = pd.read_csv('data.csv')

# 学習に不要な行、列を削除
del df['normalized-losses']
for col in ['num-of-doors', 'price', 'bore', 'stroke', 'horsepower', 'peak-rpm']:
 df = df[df[col] != '?']

# 説明変数列と目的変数列に分割
label = df['price']
data  = df.drop('price', axis=1)

# 文字列データを数値化
le = preprocessing.LabelEncoder()
for col in ['make', 'fuel-type', 'aspiration', 'num-of-doors', 'body-style', 'drive-wheels', 'engine-location', 'engine-type', 'num-of-cylinders', 'fuel-system']:
  data[col] = le.fit_transform(data[col])

# 学習用データとテストデータにわける
train_data, test_data, train_label, test_label = train_test_split(data, label)

# 学習する
clf = RandomForestRegressor()
clf.fit(train_data, train_label)

# 評価する
predict = clf.predict(test_data)
rate_sum = 0

for i in range(len(test_label)):
  t = int(test_label.iloc[i])
  p = int(predict[i])
  rate_sum += int(min(t, p) / max(t, p) * 100)
print(rate_sum / len(test_label))

実行結果

$ python car.py
90.16326530612245
$ python car.py
89.46938775510205
$ python car.py
88.71428571428571
$ python car.py
90.3265306122449

終わりに

普段はWeb系ばかり(PHP/Ruby)をやっているので、本当にPython含む機械学習に縁がなかったが、いざやってみると新鮮な事ばかりで楽しかった。学習アルゴリズムがどんな仕組みなのかは全然わからなかったし、Pandasも全然使いこなせてなかったが、何となく機械学習はこういう事をしてるというイメージは掴めたので、視野は少なからず広まったと思う。

こうやって今後も専門でない分野の技術を少しずつ取り入れて、より興味が持てれば深みにハマっていくようにしたい。