[Python3] scikit-learnによる機械学習3: 自動車の情報から価格を予測する

前提

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

要素 バージョン
debian 8.6
python 3.6.2

scikit-learnによる機械学習シリーズ

機械学習も数学もPythonもよくわからなくても機械学習はデキるシリーズ

概要

scikit-learnによる機械学習がなんたるかがほんの少しわかってきたので、足し算などという実用性皆無な例でなく、もう少し実用的な使い方をしてみる。

今回は、自動車の各種情報と、その自動車の価格を学習させ、新たに情報を与えると価格を予測してくれるスクリプトを作成する。なお、学習データにはMicrosoftのクラウド機械学習サービスである、Azure Maschine Learningより、そのチュートリアルで使用されているサンプルデータを拝借する。

学習用データについて

今回使用する学習データは以下のようなCSVファイル。

画像をだとわかりづらいが、以下の列で構成されている。列の内容は列名から概ね察せるが、最終列にある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

学習用データはいつもどおりPandasを用いてDataFrameに展開する

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

無効な行/列の削除

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

まず、’normalized-losses'(2列目)は、列全体で”?”が多いので、思い切ってこの列自体を削除する。列についではdel文で削除できるので削除してしまう。

del df['normalized-losses']

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

“?”が存在する列は’num-of-doors’, ‘price’, ‘bore’, ‘stroke’, ‘horsepower’, ‘peak-rpm’の6列なので、これらのいずれの列にも”?”が含まれていない行のみ取り出して再代入するという処理を行う。PandasのDataFrameでは、行の抽出に、列を対象とした条件式を指定することができるので、”?”が含まれている行を排除できる。

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

おそらくPandasを使ってもっとスマートに同じことができると思うので、良い方法があったらご教授お願いします。

文字列を数値化する

今回も前回までと同様、scikit-learnの学習モデルの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
(以下略)

わざわざ変換する列名を配列でツラツラ書かなくてももっとスマートに書ける方法がありそうです。こちらも良い方法ありましたらご教授お願いします。

学習用データとテストデータの分割

ここは前回一緒なのでコードだけ載せて割愛

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 = RandomForestClassifier()
clf.fit(train_data, train_label)

前回までとの違いは、clf変数にどの学習アルゴリズムのオブジェクトを代入するかの違いだけである。どの学習アルゴリズムについても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)

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

$ python cars.py
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))

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

$ python cars.py
86.6734693877551
$ python cars.py
87.46938775510205
$ python cars.py
88.59183673469387
$ python cars.py
86.55102040816327
$ python cars.py
86.04081632653062

さらに、アルゴリズムをランダムフォレストから、線形回帰に変更して再評価してみる。変数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に様々な機能があるが、今回は意図的に自前で計算するようにして感覚を掴んだ。

ソースコード

Github

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

# CSVファイルを読み込む
df = pd.read_csv('cars.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 = RandomForestClassifier()
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))

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です