【期間限定・無料公開】『Pythonによるはじめての機械学習プログラミング』ー現場で必要な基礎知識がわかる【40ページ】

こんにちは!

@tatsushimです。
共著で書かせていただいた、先月4/23に発売の『Pythonによるはじめての機械学習プログラミング[現場で必要な基礎知識がわかる]』(表紙がジェンガなので、「ジェンガ本」と呼ばれたりしています)の一部を無料公開しようと思います。

※ 編集の方には了承いただいております
※ noteに転記した関係で一部書籍版とは微妙に編集されている箇所もあります
※ 一定期間経ちましたら有料noteにする予定です

なぜ公開をするのか?

公開する3.1章と3.2章は私が執筆した箇所で、書籍では約40ページ分の内容になります。
Pythonで機械学習にふれて、できることを体感してもらうこと」がメインの内容となっており、コンセプトを伝えて多くの方にこの本を手にとってもらいたいと考え、公開するに至りました。

・機械学習はどんな課題を解決することが得意なのか?
・そのためにはどんなステップがあるのか?
・それぞれのステップでどのような知識が必要なのか?

といった全体像を解説しておりますので、ぜひご覧になっていただけますと幸いです!

読んでいただいた方々の声

実際に本を読んでいただいた方々からの書評をご紹介します。

BASE株式会社 CTO えふしんさん

全部を読んだからと言って、決して本書一冊でプロになれるわけじゃないよ、でも、実戦で必要なことはしっかり書いてあるよということを学ぶことができるだろう。そして、この先に進みたければという人に読むべき書籍や論文のポインタが示してある。等身大で誇張も矮小もせず機械学習の入り口について語っている良書だと思う。

サークルアラウンド株式会社 CEO 佐藤さん

とりあえずこれまで「機械学習難しそうだしよくわからない言葉いっぱい出てきそうだからなぁ」と敬遠していたシステム開発者だったらこの"ジェンガ本"は一度目を通してみて損がない一冊だと思います。弊社トレーニングで機械学習コース作るときは教材にしたいです。

そもそもどんな本なのかは前回のエントリーにまとめさせていただきましたので、良かったらそちらもご覧になってみてください!

以下、本文になります。なお、本文のサンプルコードはGitHubで公開しております。

3章 scikit-learnではじめる機械学習

3.1 機械学習に取り組むための準備
3.1.1 機械学習とは

Googleをはじめとする、多くのテクノロジーカンパニーは機械学習を使用している、ということを耳にしたことのある方は多いでしょう。では、機械学習とはどのようなものなのでしょうか? 機械学習とはその名が表す通り、コンピュータ(機械)がデータから学習した結果、そこから規則など(数学的モデル)を見つけ出すことです(図3.1)。

図3.1 機械学習の概念図

3.1.2 機械学習を使うメリット

機械が見つけ出した規則を用いることで、新たなデータに対して予測や分類ができます。機械学習を使うメリットには次が挙げられます。

● 人間が目視で発見することが難しい数学的モデルの発見・構築(図3.2)
● 単純なルールで決めた方法よりも、高精度な予測や分類の実現(図3.3)
● 上記を利用した予測や分類の自動化によるコスト削減

機械学習の具体的な事例として、図3.2、図3.3のような事例が挙げられます。

図3.2 人間が目視で発見することが難しい数学モデルの発見の例

図3.3 Q&Aサイトにおいて単語のみの単純なルールで決めた方法よりも、機械学習を使って高精度な分類をしている例

機械学習は人間では処理できない量のデータを短時間で処理し、現実社会における、情報のレコメンドや分類といった課題に利用されています。

3.1.3 機械学習を使うデメリット

機械学習を使うデメリットにもふれておきます。まず、機械学習を使うコストが大きいことが挙げられます。実際に機械学習を使うまでには、次のようなコストがかかります。

● データの準備
● システムの実装
● 本番システムへの組み込み

筆者が経験した現場では、機械学習を使うためにかかったコストより、人手でやった方が効果が高いことがありました。また、多くのコストを払って作った機械学習のシステムが期待したほどの効果を出せず、本番システムでの使用に耐えないということもあります。つまり、機械学習を用いずに問題が解決できるのであれば、それに越したことはありません。

3.1.4 機械学習を用いるかの判断

機械学習を用いるかを適切に判断するには、目的の確認が大事です。目的によっては機械学習を使う必要がない場合もあります。
Googleのリサーチ・サイエンティストであるMartin Zinkevich 氏は次のように述べています注1(注1 http://martin.zinkevich.org/rules_of_ml/rules_of_ml.pdf)。

Don’t be afraid to launch a product without machine learning.
訳:機械学習なしでプロダクトを出すことを恐れるな。

例えば、2章で紹介したPandasを用いてデータを分析する過程で、一定のルールや法則性を発見できたとします。そのルールを用いて予測や分類を行うことで、機械学習を用いるよりも効果的に予測や分類を実現できるかもしれません。また、データ量が多くない場合には人手で対応することも1 つの方法です。

筆者はSNSサイトの運営に携わった経験があります。その中でわかりやすい例がありますので、その必要性について考えてみましょう。
例として「SNSへ投稿された内容が、スパムであるかそうでないか」を分類したいというニーズがあるとします。SNSにおいて、スパムのようなガイドラインに反する投稿がされることは、サービスを運営する上ではとてもクリティカルな問題です。しかし、サービス開始初期で1 日に投稿される数が全投稿を合わせて数十投稿であれば、実は機械学習を導入するよりも目視で確認した方が正確に判断でき、すぐに取り組めます。逆にサービスの規模が十分に大きく、目視での確認では捌ききれない量であれば、機械学習を導入する必要性があるかもしれません(図3.4)。

図3.4 「SNSへ投稿された内容が、スパムであるかそうでないか」を分類する際、機械学習を用いるかの判断基準

もう1つ、別の事例をみてみましょう。SNSサイトにおいて、コンテンツ推薦のためにユーザの属性を特定したいとします。例えば、ユーザが20代女性であれば、20代女性に対してよく読まれているコンテンツを提供することでSNSサイトの利用時間を伸ばすことができます。ユーザの過去の投稿や閲覧履歴のデータを用いて機械学習を行えば、ユーザの属性を特定できるかもしれません。しかし、前述の通り機械学習のシステム構築や導入はとても大変ですし、十分な効果が出せない可能性があります。また、ユーザが本当は20代女性にもかかわらず、40代男性として機械が判断してしまい、40代男性向けのコンテンツを誤って推薦することで、逆にユーザのSNSサイト利用時間が減ってしまうといったおそれもあります(図3.5)。

図3.5 誤った情報推薦

もしそういったコンテンツ推薦のミスが許容できないのであれば、SNSサイト内でキャンペーンを行い、ユーザに年齢や性別といった、自分の属性を入力してもらうことで、機械学習を用いずにユーザ属性を特定するという手法あります(図3.6)。ユーザ自身が属性を入力することで、誤りのないユーザ属性を得ることができます。一方でこの方法の欠点は、キャンペーンに参加したユーザのみしかユーザ属性を特定できない点です。ユーザ属性を入力していないユーザに対してもコンテンツの推薦を行いたい場合には、機械学習の力が必要になるかもしれません。

図3.6 ユーザに自ら情報を入力してもらい、情報推薦に使うステップ

このように、本当に機械学習が必要かどうかはそのときの状況に依存します。まずは目的を確認して、本当に機械学習が必要かを精査しましょう

3.2 scikit-learnによる機械学習の基本

3.2.1 scikit-learnとは

昨今では、オープンソースの機械学習ライブラリの開発・活用が進んだことで、これまでの章で用いてきたPythonで、機械学習をシンプルに実装できます。その中でもscikit-learnは機械学習のためのオープンソースのメジャーなPythonライブラリです。多くの機械学習のためのアルゴリズムがサポートされており、手厚いドキュメントもあります。

なお、本章では実行するプログラムの関係上から、前章で利用したJupyter NotebookではなくCLI(Command Line Interface)上でのプログラム実行を前提としています。

3.2.2 教師あり学習と教師なし学習

広く用いられている機械学習の方法として、「教師あり学習」と「教師なし学習」という2 種類の機械学習の方法があります。教師あり学習とは人間があらかじめ正解データを用意しておき、それを元にプログラム(機械)が学習を行う方法です。この正解データのことを教師データと呼びます。また「正解」と「不正解」自体をラベルと呼び、それらを付与することをラベル付けと呼びます。

「教師あり学習」は未知のデータに対する「予測」に使われることが多いです(図3.7)。

図3.7 教師あり学習の例

例えば、何かが写った画像が入力された際に「犬であるかどうか」を予測する際に使われます。また、「犬である」ことと「犬でない」ことがラベルで、画像へそれらを付与することをラベル付けです。これらの教師データからプログラム(機械)が学習を行い、新たに入力された画像に対して「犬である」か「犬でない」かを予測します。

このように教師あり学習で解くことのできる課題は以下のようなものが挙げられます。

● 受信したメールがスパムメールかを予測
● スマートフォンで写真を撮る際に、カメラに写った顔が笑顔かを予測
● ニュース記事のカテゴリを予測

一方で、教師なし学習とはプログラム(機械)自身が入力されたデータからそのデータの属性や構造を見つけ出す学習方法です。代表的な手法として「クラスタリング(clustering)」という手法があります。

例えば、図3.8の左にはさまざまなデータが混在しています。これらのデータを図3.8の右ように、データの特徴からグループ化する方法がクラスタリングです。「クラスタ(cluster)」とは、「集団」や「群れ」の意味で、似たものがたくさん集まっている様子を表します。  

図3.8 教師なし学習の例

クラスタリングの具体的な応用例としてはマーケティングにおけるターゲティング・ダイレクトメールが挙げられます(図3.9)。

図3.9 教師なし学習の例

ダイレクトメールを送る場合には、その人の趣味・関心に合った内容のメールを送ることで効果の最大化が見込めます。例えば、顧客の今までの購買データからクラスタリングを行うことで、顧客をいくつかのグループに分け、そのグループに合ったダイレクトメールを送ることができます。

このように、クラスタリングは正解がわからないデータに対して、どのような法則があるかを理解しやすい形にできる一方で、グループ化された集団に対しての解釈は分析者に委ねられます。したがって、応用先となるサービスやビジネスの背景知識を理解した上での意味付けがとても重要です。

筆者の経験では、あらかじめ正解が定められているため「教師あり学習」の方が実サービスに組み込むハードルが低いです。そこで、本章ではscikitlearnで「教師あり学習」に挑戦します。

3.2.3 教師あり学習における課題の取り組み方

ここでは、教師あり学習において、どのように課題に取り組むかを解説します。一般的に、図3.10の4つのステップで課題に取り組みます。

図3.10 教師あり学習における課題へ取り組むステップ

1. 教師データを準備する

教師データは特徴量と呼ばれる、分析データの特徴を定量的に表現した値を持ちます。例えば、健康診断の結果から性別を予測するという課題があるとします。教師データとなる健康診断の結果データには体重や身長といったデータが含まれているとき、体重や身長を特徴量と呼びます。このとき、「教師データを準備する」とは「特徴量」と「ラベル(ここでは性別)」がセットになったデータの準備を意味します。

2. 教師データを整形する

準備されたデータに対して、機械が学習をするためにデータを整形する必要があります。筆者の経験上、現場で取り扱うデータはそのまま機械学習に利用できない形で保存されていることが多いです。機械が学習を正しく行うため、もしくは精度を上げるためにデータの前処理を行うことが求められます。なお、前処理については2章で解説していますので、本章での詳しい解説は省略します。

3. 教師データから学習を行う

将来、ラベル(性別)がないデータが入ってきても、正しくラベル(性別)を予測するために、特徴量とラベルを含む教師データから学習を行います。学習の際には機械学習のためのアルゴリズムを選択し、学習させます。教師あり学習に利用できる代表的なアルゴリズムには、次が挙げられます。

 ● ニューラルネットワーク
 ● SVM(support vector machine)
 ● 決定木
 ● ランダムフォレスト

アルゴリズムそれぞれの詳細な理論はここでは深くふれませんが、理解を深めたい場合には次の書籍や「おわりに」で挙げる参考書籍などを参考にしてください。

この学習のためのアルゴリズムを「学習器」と呼びます(もしくはあとに分類に使用するために「分類器」と呼ばれることもあります)。

4. 未知のデータに対して予測を行う

学習済みのアルゴリズムに対して、ラベルが未知であるが特徴量(体重や身長)を持ったデータを入力し、出力としてラベル(性別)を予測します。この予測が機械学習によってはじき出された予測結果です。では、以降で実際のデータに対して手を動かしながら、教師あり学習のステップを体感してみましょう。

3.2.4 データの準備

前項で言及したように、「教師あり学習」を行うためには、教師データが必要です。scikit-learnはさまざまなデータセットを簡単に扱えるような機能が実装されています。簡単に扱えるデータの1 つであるワインのデータを用いて、分類に挑戦してみましょう。まずはワインのデータをインポートしてデータの概要を確認します。
(ch3/show_wine_descr.py)

from sklearn.datasets import load_wine

# ワインのデータをインポート
data = load_wine()

# データの概要を表示
print(data.DESCR)

# データの概要を表示
print(data.DESCR)​

結果、以下のような出力が得られました(抜粋)。
(実行結果)

Data Set Characteristics:
   :Number of Instances: 178 (50 in each of three classes) # 178のデータ
   :Number of Attributes: 13 numeric, predictive attributes and the class
   :Attribute Information: # 13の特徴量
               - 1) Alcohol
               - 2) Malic acid
               - 3) Ash
               - 4) Alcalinity of ash
               - 5) Magnesium
               - 6) Total phenols
               - 7) Flavanoids
               - 8) Nonflavanoid phenols
               - 9) Proanthocyanins
               - 10)Color intensity
               - 11)Hue
               - 12)OD280/OD315 of diluted wines
               - 13)Proline
               - class: # 3つの分類が存在している
               - class_0
               - class_1
               - class_2

次のURL から同様の内容を確認できます。

データの数は178で、各データは13の特徴量を持つことがわかります。このデータには、3人の異なる生産者が生産したワインの特徴量が含まれています。生産者が違うために、ワインごとに含まれる化学成分に傾向があるというわけです。特徴量の名前を日本語で翻訳したものを表3.1にまとめます。

表3.1 wineデータに含まれる特徴量の名前

データの中身を見ていきましょう。以下を実行してみください。
(ch3/show_wine_data.py)

import pandas as pd
from sklearn.datasets import load_wine

# ワインのデータをインポート
data = load_wine()

# Pandasを用いて特徴量とカラム名を取り出す
data_x = pd.DataFrame(data=data.data,columns=data.feature_names)

# データが持つ特徴量を上から5行表示
print (data_x.head())

# Pandasを用いてラベルを取り出す
data_y = pd.DataFrame(data=data.target)

# カラム名が「0」となっており分かりづらいので、「class」に変更
data_y = data_y.rename(columns={0: 'class'})

# データに割り振られたラベルを上から5行表示
print (data_y.head())

data_x.head()とdata_y.head()はそれぞれのデータの上から5行を表示しています。以下のような結果が得られました。
(実行結果)

alcohol malic_acid ash alcalinity_of_ash magnesium total_phenols /
0 14.23 1.71 2.43 15.6 127.0 2.80
1 13.20 1.78 2.14 11.2 100.0 2.65
2 13.16 2.36 2.67 18.6 101.0 2.80
3 14.37 1.95 2.50 16.8 113.0 3.85
4 13.24 2.59 2.87 21.0 118.0 2.80

flavanoids nonflavanoid_phenols proanthocyanins color_intensity hue /
0 3.06 0.28 2.29 5.64 1.04
1 2.76 0.26 1.28 4.38 1.05
2 3.24 0.30 2.81 5.68 1.03
3 3.49 0.24 2.18 7.80 0.86
4 2.69 0.39 1.82 4.32 1.04

od280/od315_of_diluted_wines proline
0 3.92 1065.0
1 3.40 1050.0
2 3.17 1185.0
3 3.45 1480.0
4 2.93 735.0
class
0 0
1 0
2 0
3 0
4 0

13種類の化学的な要素に関する数値データが含まれていることがわかります(一番左の0から4の数字は行数です)。各ワインの特徴を表すデータという意味で、このデータを「特徴量」と呼びます。3段落目に「class」というカラムがあります。分類は英語で「class」といいます。ここでいう「class(分類)」とは「ワインの生産者」です。それぞれの生産者にIDとして0,1,2 を割り振って表現しています。つまりこの5行は「0」というclassに分類されているので、ワインの生産者IDが0の人が生産したワインということになります。データセット全体では他にも「1」や「2」というclass の値を持った、別のワインの生産者によって作られたデータを含んでおり、教師データとして3人のうちどの生産者が生産したワインなのかを表しています。この0、1、2 がワインデータのラベルになります。
アルコールや鉄分といった13の化学成分のデータを入力データとして、3つのclass(分類)=「3人のうちどの生産者が生産したワインか」に、機械学習を用いて正しく分類することに挑戦します(図3.11)。

図3.11 ワインを機械学習を用いて分類する

3.2.5 機械学習でデータを分類する

それでは、機械学習のモデルの実装をしてみましょう。代表的な機械学習のためのアルゴリズムの中でも、今回は分類結果の解釈がしやすい決定木というアルゴリズムを用いて分類してみます。決定木は生成したルールを用いて段階的にデータを分割していき、木構造の分析結果を出力できるアルゴリズムです。例えば、図3.12のように、人が家を出るときに傘を持つかどうかを予測する場合、天気という基準でデータを分割したあとに、紫外線や湿度で分割する…といった具合です。

図3.12 決定木の例

このような特性から、分類結果の解釈が容易です。

先ほどのデータを用いてワインの特徵が生産者によって異なると仮定して、その成分から3 人のうちどの生産者によってつくられたワインなのかを予測する課題に取り組んでみましょう。予測するために必要なコードはとてもシンプルです。以下にコードを示します注2(注2 なお、本書ではプログラミング言語の仕様については細かくふれません。Python自体に対しての理解を深めたい場合には次の参考書籍などを参考にしてください。辻真吾 著「Pythonスタートブック[増補改訂版]」(技術評論社、2018年)https://gihyo.jp/book/2018/978-4-7741-9643-5)。
(ch3/tree.py)

from sklearn import tree
from sklearn.datasets import load_wine
from sklearn.model_selection import train_test_split

# ワインのデータをインポート
wine = load_wine()

# 特徴量とラベルデータを取り出す
data = wine.data
target = wine.target

# データを分割
X_train, X_test, Y_train, Y_test = train_test_split(data, target, test_
size=0.2, random_state=0)

# 決定木をインスタンス化
clf = tree.DecisionTreeClassifier()

# 学習データから決定木が学習
clf = clf.fit(X_train, Y_train)

# 正解率を表示
print (clf.score(X_test, Y_test))

5行目から、解説をしていきます。前節でも出てきたload_wine()でデータを取得し、特徴量とラベルデータを取り出しています。wine.dataに特徴量が含まれており、wine.targetにはラベルデータが含まれています。

# ワインのデータをインポート
wine = load_wine()
# 特徴量とラベルデータを取り出す
data = wine.data
target = wine.target

機械学習の予測した結果を評価する際には、学習用とは別にテスト用のデータを準備した上で、そのテスト用のデータに対する予測結果から評価します。なぜなら、データセットのすべてを使って学習して、その後同じデータでテストを行うことは、テストの答えを元に機械が学習してしまうので、未知のデータに対する予測結果とはいえないからです。それを避けるために、train_test_split関数を使ってデータを分割しています。train_test_split関数はデータをランダムに、好きな割合で分割できる便利な関数です。ここでは、引数のtest_sizeに0.2を指定することで、2割をテストデータとし、残りの8割のデータを学習用のデータとして、データを分割しています。

 また、この関数は実行するたびに学習データとテストデータの中身がランダムに変わります。そのため、結果の再現ができません。本書では同一のデータ分割の条件で実行してもらいたいので、random_stateという引数に特定の数値(ここでは0)を指定することで、実行ごとに学習データとテストデータの中身がランダムに変わることを防いでいます。

# データを分割
train_test_split(data, target, test_size=0.2, random_state=0)

 次に、決定木をインスタンス化しています。clfという変数名はclassifier(分類器)という単語の略です。scikit-learnのドキュメントなどではよく見られるので、覚えておくと良いでしょう。

# 決定木をインスタンス化
clf = tree.DecisionTreeClassifier()

 学習データから、決定木に学習をさせています。

# 学習データから決定木が学習
clf = clf.fit(X_train, Y_train)

最後に、テストデータを引数に渡し、テストデータが入力された際の正解率(Accuracy)を出しています。

# 正解率を表示
print(clf.score(X_test, Y_test))

正解率は予測結果の評価値として一般的に使われる値で、定義は以下になります。

正解率 = 正解した数÷ 予測した全データ数

筆者の手元の環境では、0.972という高い正解率が出力されました(ここでは結果が違っていても問題ありません)。

3.2.6 どんな特徴量が分類に貢献しているのか?

決定木アルゴリズムは、どんな特徴量が分類に対して貢献しているかをわかりやすく視覚化できます。以下のプログラムを実行してください。
(ch3/visualize.py)

import pydot
from sklearn import tree
from sklearn.datasets import load_wine
from sklearn.model_selection import train_test_split

# ワインのデータをインポート
wine = load_wine()

# 特徴量とラベルデータを取り出す
data = wine.data
target = wine.target

# データを分割
X_train, X_test, Y_train, Y_test = train_test_split(data, target, test_
size=0.2, random_state=0)

# 決定木をインスタンス化
clf = tree.DecisionTreeClassifier()

# 学習データから、決定木に学習をさせる
clf = clf.fit(X_train, Y_train)

# 正解率を表示
print (clf.score(X_test, Y_test))

# ❶DOT言語でグラフを表現した、tree.dotを生成
tree.export_graphviz(
 clf,                      # 決定木インスタンス
 feature_names=wine.feature_names,  # 特徴量の名前
 class_names=wine.target_names,     # 分類先の名前
 filled=True,                       # 最も多数を占める分類先ごとに色分け
 rounded=True,                      # 各ノードのボックスの角を丸くし、
Helveticaフォントで見やすく
 out_file='tree.dot',               # 生成されるファイル名を指定
)

# ❷tree.dotを視覚化して、画像で出力
(graph, ) = pydot.graph_from_dot_file('tree.dot')
graph.write_png('tree.png')

1 行目でimport pydotを行っている以外は、24行目までtree.pyと同じです。❶で示す25行目から解説をしていきます。tree.export_graphvizは決定木インスタンスを第一引数にとります。以下、それぞれの引数について説明していきます。

 feature_namesは各特徴量の名前で、class_namesは分類先の名前になります。load_wine()で取得したwineにはそれぞれfeature_names、target_namesで特徴量の名前と分類先の名前へアクセスできます(後者がclass_namesでないことに注意してください)。filled にTrueを指定することで、最も多数を占める分類先ごとに色分けをしてくれます。rounded にTrueを指定することで各ノードのボックスの角を丸くし、Times-Romanの代わりにHelveticaフォントが使用されることで見やすくなります。最後に、out_fileは視覚化された結果、生成されるファイル名を指定します。

# ❶DOT言語でグラフを表現した、tree.dotを生成
tree.export_graphviz(
 clf,                               # 決定木インスタンス
 feature_names=wine.feature_names,  # 特徴量の名前
 class_names=wine.target_names,     # 分類先の名前
 filled=True,                       # 最も多数を占める分類先ごとに色分け
 rounded=True,                      # 各ノードのボックスの角を丸くし、
Helveticaフォントで見やすく
 out_file='tree.dot',               # 生成されるファイル名を指定
)

この.dotという拡張子は見慣れないかもしれませんが、DOT言語で書かれたデータのファイルに拡張子として使用されます。次のコマンドで生成されたtree.dotの中身を見てみましょう。

cat tree.dot

以下のようなテキストが表示されます。
(実行結果)

digraph Tree {
node [shape=box, style="filled, rounded", color="black", fontname=helvetica] ;
edge [fontname=helvetica] ;
0 [label="color_intensity <= 3.46\ngini = 0.662\nsamples = 142\nvalue =
[45, 55, 42]\nclass = class_1", fillcolor="#39e5811a"] ;
1 [label="gini = 0.0\nsamples = 46\nvalue = [0, 46, 0]\nclass = class_1",
fillcolor="#39e581ff"] ;
0 -> 1 [labeldistance=2.5, labelangle=45, headlabel="True"] ;
2 [label="flavanoids <= 2.11\ngini = 0.58\nsamples = 96\nvalue = [45, 9,
42]\nclass = class_0", fillcolor="#e581390e"] ;
0 -> 2 [labeldistance=2.5, labelangle=-45, headlabel="False"] ;
3 [label="hue <= 0.97\ngini = 0.245\nsamples = 49\nvalue = [0, 7, 42]\
nclass = class_2", fillcolor="#8139e5d4"] ;
2 -> 3 ;
4 [label="flavanoids <= 1.58\ngini = 0.045\nsamples = 43\nvalue = [0, 1,
42]\nclass = class_2", fillcolor="#8139e5f9"] ;
3 -> 4 ;
5 [label="gini = 0.0\nsamples = 42\nvalue = [0, 0, 42]\nclass = class_2",
fillcolor="#8139e5ff"] ;
4 -> 5 ;
6 [label="gini = 0.0\nsamples = 1\nvalue = [0, 1, 0]\nclass = class_1",
fillcolor="#39e581ff"] ;
4 -> 6 ;
7 [label="gini = 0.0\nsamples = 6\nvalue = [0, 6, 0]\nclass = class_1",
fillcolor="#39e581ff"] ;
3 -> 7 ;
8 [label="alcohol <= 12.785\ngini = 0.081\nsamples = 47\nvalue = [45, 2,
0]\nclass = class_0", fillcolor="#e58139f4"] ;
2 -> 8 ;
9 [label="gini = 0.0\nsamples = 2\nvalue = [0, 2, 0]\nclass = class_1",
fillcolor="#39e581ff"] ;
8 -> 9 ;
10 [label="gini = 0.0\nsamples = 45\nvalue = [45, 0, 0]\nclass = class_0",
fillcolor="#e58139ff"] ;
8 -> 10 ;
}

 DOT言語は、プレーンテキストを用いてデータ構造としてのグラフを表現するための言語です。したがってDOT言語としてこのテキストでグラフを表現しているのですが、人の目でみたときによりわかりやすくするために、このグラフを図にしましょう。❷で示すvisualize.pyの末尾2行を見てください。

 図にして見やすい形にするためにpydot.graph_from_dot_file('tree.dot') はdotファイルをグラフにしています。返り値として、複数のグラフを含むlistを想定しているので、1 つのグラフを取り出すために(graph,) という書き方で変数に代入しています。

 最後に、graph.write_png('tree.png') でグラフをpngファイルにして書き出しています。

# ❷tree.dotを視覚化して、画像で出力
(graph, ) = pydot.graph_from_dot_file('tree.dot')
graph.write_png('tree.png')

 visualize.pyを実行することで、実行ファイルが含まれているフォルダと同じフォルダにtree.pngが生成されます。tree.pngを開いてみると、図3.13のように、分類に用いられた特徴量とそのルールが上から判断順に視覚化されます

図3.13 tree.png

決定木アルゴリズムは、特定の特徴量のしきい値を使って段階的にデータの分割を行います。上から下に順に徐々に分割を行っていることがこの図から読み取れます。上から順に説明をしていきます。まずは一番上のボックスの中身を1 行ずつ見ていきましょう。

color_intensity <= 3.46

 色彩の強度(color_intensity)が3.46以下かで分割が行われているようです。

 gini = 0.662

 このginiはジニ係数注3(注3 ジニ係数についてもう少し詳しく学びたい方は次のURLを参照してみてください。「データからの知識発見('12) 第10 章決定木」http://www.is.ouj.ac.jp/lec/data/C10/T10.pdf)を表しています。ジニ係数とは純粋性を測定するための指標の1つです。係数が低いほど分割後のデータの分類がうまくいっていることを表します。

 samples = 142

 samplesは学習データ全体の件数を表しています。load_wine()で読み込まれた全データは178でしたが、そのうち8 割を学習データに、2 割をテストデータにしたため、学習データは142件となっています。

 value = [45, 55, 42]

 valueは左からclass_0, class_1, class_2の分類先に所属するデータの数を表しています。

 class = class_1

 最も多数を占めるデータの分類先を示しています。次に、1つ目のボックスから出た分岐の矢印とその先のボックスを見てみましょう。色彩の強度(color_intensity)が3.46以下であることに対してTrue(左斜め下)の場合を見ていきます。ボックスの中身を見るとvalue = [0, 46, 0] という値があります。ここから、46件のデータは、class_1に分類できることがわかります。

 次にFalse(右斜め下)の矢印の先のボックスを見るとvalue = [45, 9,42] となっており、まだ複数の分類先が存在していることがわかります。このボックスの下以降行は矢印にTrueとFalseの記載がありませんが、1つ目の分岐にならって、左斜め下に進む矢印がTrue、右斜め下に進む矢印がFalseになります。

 矢印をたどっていきましょう。次はフラボノイド(flavanoids)の含有量が2.11 以下というしきい値で分岐するようです。2 段目右のボックスからTrue(左斜め下)に進むと、value = [0, 7, 42] と分類されています。データの割合としては、class_2が多数を占めています。

 2 段目右のボックスからFalse(右斜め下)に進むと、value = [45, 2, 0] と分類されています。データの割合としては、class_0が多数を占めています。

 この時点(3段目)で、多くのデータは3つの分類先に正しく分類されつつあることが上記からわかります。「色彩の強度(color_intensity)」と「フラボノイド(flavanoids)の含有量」という2 つの特徴量だけで、今回のデータの場合、ほとんどのワインは誰が生産したワインかわかる(正しいクラスに分類できる)ということはとても興味深いです。

 このように、「どんな特徴量が分類に寄与しているか?」を考察することで、新しい気付きを得ることができます。例えば、もし仮にclass_1のワインがよく売れるワインであるということであれば、色彩の強度を特定のしきい値以下にコントロールすることが売れるワインをつくる秘訣であるといえるかもしれません。また、色彩の強度に関連するデータを取り、特徴量へ追加することで分類の精度がより増すかもしれません。

 ビジネス的な視点や分類精度の向上という観点から、「どんな特徴量が分類に寄与しているか?」を考察することはとても大事であることがわかります。

 4 段目以降は前述のような分岐を重ねて、最終的には学習データすべての分類を正しく行えるようなアルゴリズムとなっています。このアルゴリズムを、予測結果を評価するためにあらかじめとっておいた、未知のデータに対して適用したところ、0.972という正解率となったのがtree.pyの実行結果でした。

3.2.7 別のアルゴリズムを試してみる

先ほどは決定木というアルゴリズムでワインのデータを分類しました。別のアルゴリズムも試してみましょう。

scikit-learnは、3.2.3 項でもふれたSVM、ニューラルネットワーク、ランダムフォレストといった機械学習のためのアルゴリズムをサポートしています。ここでは、決定木と関係の深いランダムフォレストを取り上げます。ランダムフォレストはアンサンブル学習アルゴリズムの1つです(図3.14)。アンサンブル学習とは、簡単にいえば多数決をとる方法です。複数の決定木の分類結果を集めて多数決をとることで、汎化能力(学習データだけに対してだけでなく、未知の新たなデータに対して正しく予測できる能力)を向上させて最終的な結果を予測します。

図3.14 ランダムフォレストの概念図

ランダムフォレストを用いて分類するための実装コードを見てみましょう。
(ch3/forest.py)

from sklearn import ensemble
from sklearn.datasets import load_wine
from sklearn.model_selection import train_test_split

# ワインのデータをインポート
wine = load_wine()

# 特徴量とラベルデータを取り出す
data = wine.data
target = wine.target

# データを分割
X_train, X_test, Y_train, Y_test = train_test_split(data, target, test_
size=0.2, random_state=0)

# ❶ランダムフォレストをインスタンス化
clf = ensemble.RandomForestClassifier(n_estimators=100, random_state=1)

# ❷学習データからランダムフォレストが学習
clf = clf.fit(X_train, Y_train)

# 正解率を表示
print (clf.score(X_test, Y_test))

実は決定木で予測を行ったコード(tree.py)と比べたとき、コメント以外での変更箇所は1行目と16行目だけです。それぞれのコードを見てみましょう。

 1行目はライブラリのインポートを変更しています。これは先ほど紹介したアンサンブル学習のライブラリをインポートしています。

from sklearn import ensemble

 ❶で示す16行目はランダムフォレストをインスタンス化しています。n_estimators は決定木の数です。一般的にはこの数を増やすと汎化能力が上がると言われています。しかし、計算量が増えるためn_estimatorsの数を増やせば実行時間は長くなります。

 決定木の実装でも出てきましたが、random_stateは同一のデータ分割の条件で行いたいという場合に、random_stateに特定の数値を指定することで、その条件を再現できます。 

# ❶ランダムフォレストをインスタンス化
clf = ensemble.RandomForestClassifier(n_estimators=100, random_state=1)

このように、機械学習のアルゴリズムを変更しても、❷で示す19行目のclf.fit(X_train, Y_train) といった学習のための実行方法は変わりません。各機械学習のアルゴリズムのインターフェースが変わらないために、容易にアルゴリズムを変更して実験できることがscikit-learnの良い特徴です。

 筆者の環境では、forest.pyを実行した結果、1.0という正解率を得ることができました。

3.2.8 機械学習での予測結果に関する評価方法

適合率と再現率

今まではclf.score(X_test, Y_test)という関数で正解率(Accuracy)を基準に分類の精度を評価してきました。しかし、実社会では必ずしも正解率だけで評価できないことがあります。

 例えば、機械工場での部品の製造工程の中で不良品を見つけるという課題があったとします。ここに100個の部品があり、99個の良品と1個の不良品があります。この良品と不良品を、部品の重さなどの特徴量から機械学習を用いて分類します。このとき分類器Aと分類器Bについて、課題に適した分類器はどちらの分類器なのかを考えてみましょう。分類器Aは100個の部品をすべて「良品である」と判定します。結果、正解率は99%です。

 正解99個÷全体100個= 正解率0.99(99%)

 一見すると正解率99%はとても優秀な分類結果に見えます。しかし、不良品として予測される部品が1 つもなく、もともとの目的である「不良品を見つける」という課題に対してはまったく役に立っていないことがわかります(図3.15)。

図3.15 適合率(Precision)と再現率(Recall)

 100個のうち10個を「不良品である」と判定し、その10個の中には不良品を含めることのできる別の分類器Bが存在するとします。不良品と判定した10個のうちに不良品を含めることができたとき、この分類器Bの正解率は91%です(図3.16)。

 正解91(90 個の良品+1 個の不良品) 個÷全体100 個= 正解率0.91(91%)

「正解率」だけで見ると分類器Aのほうが分類器Bのよりも優秀です。しかし、分類器Bはもともとの「不良品を見つける」という目的を達成していていることがわかります。

図3.16 適合率(Precision)と再現率(Recall)

 製造工程の中で元々は目視で100個の部品を確認していたとすれば、分類器Bを用いることで目視で確認すべき部品数は10個になり、90%のコストカットが実現できるかもしれません。

 このように、現実世界では分類によって取り出したいクラス(グループ)は目的によってさまざまであることがわかります。

 そこで、機械学習での予測結果を評価する際に確認すべき2 つの指標を紹介します。それが適合率(Precision)再現率(Recall)です。適合率と再現率はクラスごとに値を出すことができます。分類器B の適合率と再現率を出してみましょう。

適合率は、予測結果のうち、どの程度正解したかの割合を表す指標です。

 分類器B の良品の適合率:90 個を「良品」と予測。
そのうち90 個が良品
 90 ÷ 90 = 1.0(100%)

 分類器B の不良品の適合率:10 個を「不良品」と予測。
そのうち1 つが不良品 1 ÷ 10 = 0.1(10%)
 1 ÷ 10 = 0.1(10%)

再現率は、正解した予測結果が実際の正解をどの程度を網羅しているか割合を表す指標です。

 分類器Bの良品の再現率:正解した予測結果は90個。
実際の正解(良品数)は99個
 90 ÷ 99 = 0.909(90.9%)

 分類器Bの不良品の再現率:正解した予測結果は1個。
実際の正解(不良品)は1個
 1 ÷ 1 = 1.0(100%)

 今回の例では、不良品の再現率を1 にすること第一目的とし、その上で不良品の適合率を上げるようなことができれば、不良品を見つけながらも、うまく目視のコストを下げることにつながります。

適合率と再現率をコードで確認する

 実際のコードとワインデータで適合率と再現率を確認してみましょう。以下のコードで確認できます。
(ch3/evaluation.py)

from sklearn import tree
from sklearn.datasets import load_wine
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report

wine = load_wine()
data = wine.data
target = wine.target
X_train, X_test, Y_train, Y_test = train_test_split(data, target, test_
size=0.2, random_state=0)
clf = tree.DecisionTreeClassifier()
clf = clf.fit(X_train, Y_train)

# ❶テストデータのラベルを予測
Y_pred = clf.predict(X_test)

# ❷各クラスの適合率と再現率を表示
print (classification_report(Y_test, Y_pred, target_names=wine.target_names))

 tree.pyから以下の3行を追記しました。これらを1行ずつ説明していきます。

 scikit-learnには各クラスの適合率と再現率を簡単に確認できるclassification_reportが準備されており、4行目でimportしています。

from sklearn.metrics import classification_report

 予測結果を出すには❶で示すclf.predict(X_test)を実行します。特徴量
を持った予測したいデータを引数に渡しpredictでラベルを予測します。

# ❶テストデータのラベルを予測
Y_pred = clf.predict(X_test)

 最後に❷のようにclassification_report(Y_test, Y_pred, target_names=wine.target_names)を実行します。第一引数に正解のラベル、第二引数に予測結果、target_namesにクラス名を渡すことで、各クラスの適合率と再現率を出力します。

# ❷各クラスの適合率と再現率を表示
print (classification_report(Y_test, Y_pred, target_names=wine.target_names))

 evaluation.pyを実行すると、以下の結果が出力されます。
(実行結果)

              precision    recall   f1-score   support

    class_0      0.93      1.00       0.97        14
    class_1      1.00      0.94       0.97        16
    class_2      1.00      1.00       1.00         6

  micro avg      0.97      0.97       0.97        36
  macro avg      0.98      0.98       0.98        36
weighted avg      0.97      0.97       0.97        36

 precisionとrecallの列を見てみましょう。precision(適合率)を見るとclass_0が0.93となっており、他のクラスは1.00です。適合率は予測結果のうち、どの程度正解したかの割合を表す指標なので、予測結果がclass_0以外である予測結果はすべて正解だったようです。「class_1のワインだけを取り出したい。ただし、他のワインを混ぜないでほしい。」というニーズは満たせそうです。一方で、class_0の適合率は0.93なので、分類器が「class_0と分類したが、そうでないクラス」のワインが存在することになります。したがってこの分類器では「class_0のワインだけを取り出したい。ただし、他のワインを混ぜないでほしい。」というニーズは、「class_0のワインだと予測した結果」に別のクラスのワインが入ってしまうために満たせません。

 recall(再現率)を見るとclass_1が0.94となっており、他のクラスは1.00です。再現率は、正解した予測結果が実際の正解をどの程度を網羅しているか割合を表す指標なので、実際の正解がclass_1 以外の予測結果はすべて網羅されているようです。例えば、「間違ったワインが入っていても多少の誤りは許容できるので、class_0の生産者がつくったワインすべてを洗い出したい」というニーズは満たせそうです。一方で「間違ったワインが入っていても多少の誤りは許容できるので、class_1 の生産者がつくったワインすべてを洗い出したい」というニーズは満たせません。class_1の生産者がつくったワインがclass_0に分類されていることがあるからです。

 ちなみに、class_2は適合率と再現率がどちらも1.00になっているために「他のワインを混ぜずに、全体からclass_2のワインだけをすべて取り出したい」というニーズを満たすことができます。

 繰り返しになりますが、「機械学習によってどんな課題を解決したいか」によって追うべき指標が変わり、最適な方法も変わってきます

 実際に利用するシーンでは目的を確認しながら、適合率や再現率に注目して追うべき指標を判断しましょう。

3.2.9 ここまでのまとめ

 機械学習とは何かという理解を深めながら、scikit-learnを用いて、機械学習でデータを分類することに挑戦しました。たった数行のコードで機械学習を実装でき、さまざまなアルゴリズムを簡単に利用できるscikit-learnは、Pythonを用いた機械学習の分野でデファクトスタンダードになりつつあります。手を動かして少しずつ慣れていきましょう。

おわりに

以上が、本文になります。一人でも多くの方がPythonで機械学習にふれて、できることを体感していただけたのであれば幸いです。@tasushimがお送りしました。


この記事が気に入ったら、サポートをしてみませんか?気軽にクリエイターを支援できます。

「スキ」ありがとうございます!twitterでもいろいろ発信してます!
66

たつしむ

#エンジニア 系記事まとめ

noteに投稿されたエンジニア系の記事のまとめ。コーディングTIPSよりは、考察や意見などを中心に。
1つのマガジンに含まれています
コメントを投稿するには、 ログイン または 会員登録 をする必要があります。