Aidemy Tech Blog

機械学習・ディープラーニング関連技術の活用事例や実装方法をまとめる、株式会社アイデミーの技術ブログです。

クラウドワークス副社長が語る【AI時代に持つべき危機感】

こんにちは、Aidemyにてエンジニアをしています小川です。

https://openaim.link/img/thumb/ai.jpg

 

突然ですが、トフラー「第三の波」(※1)という書籍をご存知でしょうか?

 

人間の歴史を紐解くと、

大きく分けて2つの大きな波「革命」が起きたと言われています。

 

第一の波🌊

狩猟時代から農業時代となり定住と食の安定による富の蓄積もたらし加速的文化の発展をもたらした「農業革命」

 

第二の波🌊

蒸気など自然エネルギーを力に変換する技術の開発により、圧倒的生産力のある機械が生まれさらに加速度が増した「産業革命」

 

そして今、第三の波「情報革命」が起きようとしています。

 

 

2015年12月

野村総合研究所がイギリス・オックスフォード大学と行った共同研究によれば、

 

「国内601種類の職業について、それぞれ人工知能やロボット等で代替される確率を試算した結果、10~20年後に日本の労働人口の約49%が就いている職業において、それらに代替することが可能との推計結果が得られた」

 

(出所:野村総合研究所

「国内601種の職業ごとのコンピューター技術による代替確率の試算」)

 

 

なにかとAIの話題が多くなり、確実に避けては通れない革命の波。

そんな時代の中、AIに仕事が取られるかもしれないと危機感を募らせている人も多いのではないでしょうか。

 

今後どのような時代になり、どのような立ち回りをすべきなのか、

クラウドワークスのCOO "成田修造" さんにインタビューをしてきました。

 

f:id:ogajundayo:20170909161301p:plain

 

成田修造さんの経歴(https://crowdworks.co.jp/ir/officers より抜粋)

慶應義塾大学在学中よりアスタミューゼ株式会社に参画。
オープンイノベーション支援サービス「astamuse」の事業企画を手掛ける他、
大手人材紹介会社との提携事業を立ち上げ、サイトディレクション、
Webマーケティングなどを担当。
その後、株式会社アトコレを設立し、代表取締役社長に就任。
アート作品の解説まとめサイト「atokore」の立ち上げやiPhoneアプリ開発などを行う。
2012年より株式会社クラウドワークスに参画、学生でありながらインターン生から執行役員に就任
2014年8月、同取締役に就任。
2015年4月、同取締役副社長に就任

 

 

輝かしき経歴すぎて目が開けられないですね、、、

 

成田さんは、持つべき危機感は変化に置いてかれる危機感であり、変化に対応するにはソフトウェア、IT、AI分野の勉強は避けて通れないという。

 

〜以下インタビュー〜↓↓↓

 

 

 

  f:id:ogajundayo:20170909154538p:plain

── 一般的に人工知能AIは万能だと思われていて、人の仕事が変わると言われていますが、AIについてどういうものだと考えていますか?

 

 

f:id:ogajundayo:20170909153834p:plain 

成田さん:

現状だと、意外とAIのできることは限られている認識です。

事務的な処理など“特定の目的に沿うもの”は得意分野であると考えていて、

すぐに置き換わると考えています。例えばレジ、運転、倉庫整理など。

深層学習でも、画像識別分野においてはブレークスルーが起きていますが、

それ以外にはまだまだこれからですし、

“今人が考えてレベルの複雑な作業を要する知能分野の仕事”では、

人間と機械が代替する未来は、すぐ来るとは考え難いですね。

 

 

  f:id:ogajundayo:20170909154538p:plain

── 具体的にはどれくらいだと考えていますか?

 

 

f:id:ogajundayo:20170909153834p:plain 

成田さん:

具体的なところはわからないですが、50〜100年タームで本格的に入れ替わるとか、それくらいで捉えていいますね。

2045年にシンギュラリティが来ると言われていますが、その頃にようやく変化が出て来る。それくらいの時間感覚で捉えています。

 

 

  f:id:ogajundayo:20170909154538p:plain

── まだ先の未来でそこまで危機感は感じる必要はないということですか?

 

f:id:ogajundayo:20170909153834p:plain 

成田さん:

いえ、危機感は持たないとまずいと思います。

たしかに現状AIのできることは限られていますが、

その制限の中で、技術はここ数年で非常に早く進化しています。

その変化は極めて早いので、その変化についていかないといけない。

人間以外がやった方が良い分野を見極め、どうAIを活用するかを考えないといけないと思ってます。

 

 

  f:id:ogajundayo:20170909154538p:plain

── なるほど。入れ替わることに対する危機感というよりは、進化していく技術をどう取り入れるかという点において危機感があるということですね。

どのように取り入れるかはAIのこと自体を知らないとできないような気がしますが、そのような学びたいニーズ・危機感みたいなものはありますか?

 

 

f:id:ogajundayo:20170909153834p:plain 

成田さん:

そうですね。あります。経営者としてもAI、というかその技術である機械学習などの学習は必須だと思っています。

多くの人が危機感を持っているかわからないけど、僕は持っています。

 

  f:id:ogajundayo:20170909154538p:plain

── 経営者として、ですか。そんな中、成田さんは具体的にどのような行動をしていますか?

 

 

f:id:ogajundayo:20170909153834p:plain 

成田さん:

そうですね。勉強しています。

AI分野のホットトピックとして機械学習があり、深層学習があり、強化学習があり、それらの数学的な裏付けやロジックを勉強することで、それぞれの手法の何が得意で何が苦手なのかを学んでいます。
具体的には、本読んだり、セミナー行ったり、エンジニアに聞いたり、本質を理解し、どう社会やビジネスを変えうるのかを自分で考えられるようにと思ってます。

 

  f:id:ogajundayo:20170909154538p:plain

── Aidemyの勉強会にも参加してくださっていましたね。

今の話は、経営者として必要なこととしてご自身で行動なさっているという話でしたが、経営以外の業務に携わる人についてはどう考え、どう行動をするべきだと思いますか?

 

 

f:id:ogajundayo:20170909153834p:plain 

成田さん:

AIのできることを考えると、例えばマネジメントのような、複数の情報源を元に不確実なリスクとリターンを考え、意思決定をする、みたいな仕事。単純な数値化やフローにしにくい仕事はまだ入れ替わらないと思います。

一方で単純な事務みたいな仕事は普通に考えたら、急速に置き換わると思います。

 

そうなって来ると、別の仕事重要度が増して来るはず。

形式的に仕事をしていることがなくなっていき、コミュニケーションやエモーショナルな仕事が増えていく。短期的にはそういう仕事の重要度が高まる気がします。

 

あと、どんな仕事や産業であっても、IT・AI化の波は避けられないので、ソフトウェアの仕事の重要度は高まる一方ですよね。ソフトウェアの知識を学んだ上でそれを開発するのか、売る人になるのか、得意は人によって異なりますが、現代においてソフトウェア分野を学ばないという選択肢はないでしょうね。

 

  f:id:ogajundayo:20170909154538p:plain

── 避けられないIT化の中で生きていくには、勉強は必須と言っても過言ではないということですね。

このような危機感を成田さん自身感じて、行動に移していると思いますが、周りを見ていて他の人はどうですか?危機感自体は感じていますか?

 

 

f:id:ogajundayo:20170909153834p:plain  

成田さん:

クラウドワークスはIT系の会社ですから、比較的危機感はある方だと思いますが、

問題は、大手企業はどうなのかってところですよね。

優秀な人がITやソフトウェアの世界にまだ流れ込んでいない。

IT化についていかないと日本がまずいですよね。

 

 

  f:id:ogajundayo:20170909154538p:plain

── 日本の就職人気ランキングを見ても、10年前と同じですね。

少し話変わりますが、このIT化の波を受けて、クラウドワークスのクライアントのニーズはどのように変化していますか?

 

 

f:id:ogajundayo:20170909153834p:plain 

成田さん:

クラウドワークスとしても一般論としてもハイエンドなエンジニアニーズはすごく高まっていますね。

 

  f:id:ogajundayo:20170909154538p:plain

── エンジニアの中でもどういった言語の技術者のニーズが高待っているなどありますか?

 

 

f:id:ogajundayo:20170909153834p:plain 

成田さん:

クラウドワークスでは様々ですね。一般論ではデータ分析の分野でPythonの技術者のニーズが高まっていますね。

あとは、データを使う大規模なシステム開発では、

PHPや Ruby on Rails はニーズが少なくなってきている一方で、

ScalaやJavaのニーズが高まる匂いはしますね。

あとは、データが王者の時代なので、データそのものを集める仕事が増えてきていますね。

 

  f:id:ogajundayo:20170909154538p:plain

── DB設計のような仕事ということですか?

 

 

f:id:ogajundayo:20170909153834p:plain 

成田さん:

いやもっと誰でもできるような教師データを集める仕事です。例えば、翻訳機能の学習データのために翻訳データを作ったり集めたりする仕事。

機械学習でもディープラーニングでも、正解不正解の教師データを始めは集めないといけないからね。そういったデータを集める人のニーズはアナリストと比例して増えていますね。

 

  f:id:ogajundayo:20170909154538p:plain

── なるほど。ありがとうございます。

この時代が進んで、遅かれ早かれAIが人の仕事を奪うようになると、生産性が変わらず、人が余るという事態に陥り、ベーシックインカムという制度になるような気が個人的にはするのですがどう思いますか?

 

 

f:id:ogajundayo:20170909153834p:plain 

成田さん:

論理的に考えたら、ベーシックインカムのような整備があった方がいいと思います。ただ、そこには倫理や哲学的な話も入って来るので、相当ドラスティックにやる人が必要だし、できるかはわからないです。

 

 

  f:id:ogajundayo:20170909154538p:plain

── 論理的に考えたらとはどういうことでしょう?

 

 

f:id:ogajundayo:20170909153834p:plain 
成田さん:

AIによって、不要になる仕事が必ず出てきます。そうすると人は暇になっていく。これがマクロのトレンドだと思うんです。

その人を企業が養うために雇うか、国が養うべきかという話になります。

自分は後者、つまりベーシックインカムがいいと思っています。

何故ならば、企業がお金をつかい、その人たちも貢献しない時間を使うよりは、その人たちがその時間でお金を使った方が良いし、企業もその人たちを養わない分、余ったお金を次のテクノロジーに使えます。

ただそこにはそれでいのかという倫理的問題がありますが。だから難しいです。

人間は高度化しすぎていますよね。

もっと動物的にローカルな環境で生きていってもいいと思います。

 

 

  f:id:ogajundayo:20170909154538p:plain

── 昼からお酒みたいなイメージですか?

 

 

f:id:ogajundayo:20170909153834p:plain  

成田さん:

そうですね。

時代が進むと、一部の人が富を稼いで、ローカルな人に分配するようになる。

現にアメリカや中国を見ていると、そういう歴史になっています。

そうなってくるともっとローカルな環境での生活になります。

 

 

  f:id:ogajundayo:20170909154538p:plain

── 働くために生きるというより、生きるために生きるみたいになると。

 

 

f:id:ogajundayo:20170909153834p:plain 

成田さん:

そうですね。

それが問題かどうかは倫理や哲学の問題で答えを出すのは難しいです。

その流れはどっかで変化するかもしれないですが、IT・AIによって加速する確率はかなり高いというのが自分の思っていることですね。

 

それでも、社会に対して何かしたいという、ある種謎の野望を持つ人は一定数いて、そういう人たちは、このIT・AIの加速的進化を踏まえるべきで、

必然的にソフトウェアの知識がないのはありえない。

知識を入れた後、エンジニアなのか、マネージャーなのか、売る人なのか

を選ぶみたいな話になって来ると思います。

 

 

  f:id:ogajundayo:20170909154538p:plain

── 最後まとめてくださってありがとうございます。

 

 

 

 

経営者として、加速するAIの世界で、ITをどう活かすかに危機感を抱き、自ら積極的に勉強をしているという成田さん。

AIが仕事を奪う世界はすぐではないが、いずれ来る。

その中で、どう考えどう生きるのか? 考えるきっかけになればと思います。

 

改めて成田さんありがとうございました。

 

 

危機感を感じている人必見!

人工知能エンジニアを育てるオンラインゼミ「Aidemy

http://aidemy.net/

f:id:ogajundayo:20170908202719p:plain

個別指導のオンライン人工知能ゼミ

AIのエキスパートになる8週間の集中ゼミ

------------

※1 

第三の波 (トフラー) - Wikipedia

 

花火大会におけるTwitter民の感情分析

2017年7月29日に行われた2017年度隅田川花火大会. 7月27日午前9時から花火大会翌日の30日午前9時までの隅田川花火大会に関するツイートの時系列での感情分析結果はこんな感じでした.

f:id:ken787:20170810073024p:plain

ポジティブなツイートほど1に近く,ネガティブなツイートほど-1に近づきます.

なかなかネガティブですね!

以下で分析方法を解説します!

目次

雨の中開催された隅田川花火大会

早速ですが花火大会当日前後の天気を見てみましょう.Pythonを使って気象庁のホームページからデータを引っ張ってきました. 以下のサイトを参考にしています.

qiita.com

import pandas as pd

dates = [27,28,29]
df_weather = pd.DataFrame()

for date in dates:
    url = 'http://www.data.jma.go.jp/obd/stats/etrn/view/daily_s1.php?prec_no=44&block_no=47662&year=2017&month=7&day='+str(date)+'&view='
    df = pd.read_html(url)[0].dropna().iloc[date-1,[19,20]].transpose()
    df.index = ['昼(06:00-18:00)', '夜(18:00-翌日06:00)']
    df_weather = pd.concat([df_weather, df],axis=1)
df_weather.columns = dates
df_weather[::-1]
27 28 29
昼(06:00-18:00) 曇時々晴 曇後一時雨、雷を伴う
夜(18:00-翌日06:00) 曇時々雨 雨後一時曇、雷を伴う

花火大会前々日から不安定な天気,そして当日はバッチリと雨が降っていたようですね.それでももちろん花火大会は(強風がない限り)開催されます. 花火大会当日の東京の天気予報が出た頃からTwitter上では様々な発言が飛び交いました.想像に難くないと思いますが概ね以下のようなものです.

  1. 天候不良による花火大会の中止を危惧する声
  2. 天候不良による花火大会の中止を期待する声
  3. 雨天時の開催の是非に関するお知らせ
  4. 脳内妄想ツイート

理系単科大学生である僕のタイムライン上では2番と4番が多かったですね.そもそも触れてない人が殆どでしたが. それでは,隅田川花火大会前々日から当日までのツイッター民の感情はどのように推移したのでしょうか? 花火大会に関するツイートをしたアカウントの一つ一つを見て行くのはTwitter APIを用いなければならず,その仕様上時間がかかりすぎるためTwitter上の全体的な傾向のみを分析することにします.

実行環境

  • conda 4.3.23
  • python 3.6.0
  • jupyter notebook 4.3.1 その他,numpypandasなどのパッケージ,および後に特筆するGetOldTweets-pythonを用いました.最後のコレは上記に環境での使用に当たって注意が必要です.

Twitterからツイートを検索し取得する

Twitter社は,プログラムを用いてTwitter上のデータを収集するためのAPIを公開しています.一回に取得できるツイート数に制限があったり短時間に何回も取得を行えなかったりと使い勝手がよくないと言われています.(こちらがデータをタダで使おうとしてるからですが.)

またこのAPIの最も致命的な点は7日前までのツイートしか取得できないのです. 8月5日を過ぎてしまったためTwitter APIを用いて隅田川花火大会前後のツイートを収集することができません!

そこでHTTPリクエストを用いてツイートを取得するGetOldTweets-pythonを用いました.参考にしたサイト及びGetOldTweets-pythonのgitリポジトリは以下の通りです.

stackoverflow.com

github.com

なお,このGetOldTweets-pythonはpython3で廃止となった(厳密にはurllibパッケージに統合された)urllib2を用いていますので,python3.6.0の本環境で使用するためにurllib2を使用している部分を書き換えて使用しました.python3以前なら問題ないと思います(検証はしてません).GitHubからクローンしたgot3フォルダを作業ディレクトリに持って来てimportすると使用できます.

それでは,2017-7-27から2017-7-29までの隅田川花火大会に関するツイートを収集しましょう!

以下のメソッドを活用してクエリを作成します.

  • setUsername (str): An optional specific username from a twitter account. Without “@”.
  • setSince (str. “yyyy-mm-dd”): A lower bound date to restrict search.
  • setUntil (str. “yyyy-mm-dd”): An upper bound date to restrist search.
  • setQuerySearch (str): A query text to be matched.
  • setTopTweets (bool): If True only the Top Tweets will be retrieved.
  • setNear(str): A reference location area from where tweets were generated.
  • setWithin (str): A distance radius from “near” location (e.g. 15mi).
  • setMaxTweets (int): The maximum number of tweets to be retrieved. If this number is >- >- unsetted or lower than 1 all possible tweets will be retrieved.

このライブラリの一日は朝9時始まりらしいので指定期間は2017-7-27から2017-7-30,検索文字列は"#隅田川花火大会 OR 隅田川花火大会 OR (隅田川 AND 花火) OR (隅田川 AND 花火大会)“と指定します.英語では,語句の区切りに空白文字を使用しますが日本語では使用しないため文字列検索がどういった挙動を示すかわかりません.そのため大事を取って広めに指定しました.条件に合致するすべてのツイートを集めるためsetMaxTweets()は無指定で行きました.

以下スクリプトです.

import got3

since = '2017-07-27'
until = '2017-07-30'
q = '#隅田川花火大会 OR 隅田川花火大会 OR (隅田川 AND 花火) OR (隅田川 AND 花火大会)'

tweetCriteria = got3.manager.TweetCriteria().setSince(since).setUntil(until).setQuerySearch(q)
tweets = got3.manager.TweetManager.getTweets(tweetCriteria)

リクエストが処理されるまで2時間ほどかかりました.せっかく取得したものを失うのが怖いので早急にcsvファイルに退避させます.1日ごとに抜き出した方がいいかもしれません.僕は1時間半越えたあたりまで処理したところを2回失敗してます(泣

f = open('tweet_data.csv', 'w')
for tweet in tweets:
    tweet_list = [str(tweet.id), str(tweet.text.replace(',','')), str(tweet.date)]
    f.write(','.join(tweet_list) + '\n')
f.close()

pandas.DataFrame()にデータを詰めて処理していきます.

import pandas as pd
df_tweets = pd.read_csv('tweet_data.csv', names=['id', 'text', 'date'], index_col='date')

ここでdf_tweets.head()にて冒頭を確認したかったのですがたった5ツイートの中に検閲対象のものがありました.

投稿時間について降順に格納されていました.またもしかしたらと思ってidも抽出しましたが,いらない気もするので消します.また,indexになっている投稿時間をdatatime型に変換し,昇順にします.

df_tweets.index = pd.to_datetime(df_tweets.index)
df_tweets = df_tweets[['text']].sort_index(ascending=True)

これで,7月27日から7月29日までの隅田川花火大会に関するツイートが時系列順に揃いました!

引き続き,感情分析を行う準備をしましょう.

ツイートの形態素解析及び印象の評価

取得したツイートはただの文字の集合であるため扱いづらいです.

そこで,ツイートを形態素解析して単語ごとに分け,基本形表記に変換します.そして得られた単語一つ一つの「ポジティブさ」または「ネガティブさ」を評価します. 形態素解析にはMeCabを使い,単語の「ポジティブさ」の評価はPN Tableという辞書を用いました.PN Tableは単語の印象が-1から+1の実数で表された辞書で,+1に近いほど「ポジティブな」印象,逆に-1に近いほど「ネガティブな」印象を持つとしています.

こういった「ポジティブ」,「ネガティブ」といった性質を極性といい,極性の値をPN値と呼びます.

文章の感情分析には以下のサイトを参考にしました.また,リンク先のPN Tableファイルの文字コードはUTF-8ではないのでnkfコマンドなどを用いてutf-8に変換しておくことをお勧めします.

www.statsbeginner.net

必要な処理は以下です

  • テキスト(文章)を形態素解析し,単語の集合を得る.
  • 単語ごとのPN値を求める.
  • 各ツイートの平均のPN値を求める.

上記のサイトを参考に実装しました.

import pandas as pd
import numpy  as np
import MeCab

# 辞書のデータフレームを作成.パスは適宜設定してください.
pn_df = pd.read_csv('./pn_ja.dic', sep=':', encoding='utf-8', names=('Word','Reading','POS', 'PN'))
# 各列の分割
word_list = list(pn_df['Word'])
pn_list   = list(pn_df['PN'])
pn_dict   = dict(zip(word_list, pn_list))

# MeCabインスタンスの作成.引数を無指定にするとIPA辞書になります.
m = MeCab.Tagger('')

# テキストを形態素解析し辞書のリストを返す関数
def get_diclist(text):
    parsed = m.parse(text)      # 形態素解析結果(改行を含む文字列として得られる)
    lines = parsed.split('\n')  # 解析結果を1行(1語)ごとに分けてリストにする
    lines = lines[0:-2]         # 後ろ2行は不要なので削除
    diclist = []
    for word in lines:
        l = re.split('\t|,',word)  # 各行はタブとカンマで区切られてるので
        d = {'Surface':l[0], 'POS1':l[1], 'POS2':l[2], 'BaseForm':l[7]}
        diclist.append(d)
    return(diclist)

# 形態素解析結果の単語ごとのdictデータにPN値を追加する関数
def add_pnvalue(diclist_old):
    diclist_new = []
    for word in diclist_old:
        base = word['BaseForm']        # 個々の辞書から基本形を取得
        if base in pn_dict:
            pn = float(pn_dict[base]) 
        else:
            pn = 'notfound'            # その語がPN Tableになかった場合
        word['PN'] = pn
        diclist_new.append(word)
    return(diclist_new)

# 各ツイートのPN平均値を求める
def get_mean(dictlist):
    pn_list = []
    for word in dictlist:
        pn = word['PN']
        if pn!='notfound':
            pn_list.append(pn)
    if len(pn_list)>0:
        pnmean = np.mean(pn_list)
    else:
        pnmean=0
    return pnmean

では,さきほど抽出したデータ一件一件に対して処理を適用していきましょう.PN Tableに含まれない語句は考慮の対象外とし,PN Tableに含まれる語句が一つもない場合,そのツイートのPN値は0となります.

means_list = []
for tweet in df_tweets['text']:
    dl_old = get_diclist(tweet)
    dl_new = add_pnvalue(dl_old)
    pnmean = get_mean(dl_new)
    means_list.append(pnmean)

えられたPN値の平均値のリストを対応するツイートと統合します.

df_tweets['pn'] = means_list

ツイートの感情の可視化

この三日間(7/27-29)の隅田川花火大会に関するツイートのPN値の平均を出してみます.

df_tweets['pn'].mean()
-0.49401309926840586

取り得る値は-1から1までの実数値なのですが,少々低過ぎやしないですかね?

しかし,これはあくまでも単語の印象を元に算出した値です. そもそも言語が崩壊しているようなツイート,広告ツイートなど予め除外した方がいいツイートはたくさんあるでしょうし,ネットスラングに対する辞書の不完全さも正確な評価が難しい理由の一つでしょう.

また,ツイート数も日によって偏りがあります.

print('三日間全てのツイート:',len(df_tweets))
print('7月29日のツイート:',df_tweets['2017-7-29']['text'].count())
三日間全てのツイート: 81979
7月29日のツイート: 72656

最後に,ツイートの極性の時系列での推移がわかるようにデータを可視化しましょう! 最初に日付はpandasのTimeStampとして格納していたので楽ですね!

import matplotlib.pyplot as plt
%matplotlib inline   # jupyter notebookでmatplotlibを使うためのおまじない

x = df_tweets.index
y = df_tweets.pn
plt.plot(x,y)
plt.grid(True)

f:id:ken787:20170810073024p:plain

こうやって見るとなんか,うーん...

ネガティブなツイートが終始多いですね,特に前日にかけてのが.これ,雨が降ったことに対する「ざまぁ!!」も負の印象として扱われているのでしょう.

日頃のタイムライン上でも,ポジティブな内容のツイートって少ないのではないでしょうか?

最後までご覧頂き,ありがとうございました!

自分で強くなるAI「DQN」で3色オセロ「トリコロール」の学習に挑戦

どーも! まじすけです🎉
今回は最近話題の強化学習、DQNに挑戦してみました。

以前「AINOW」というAIのキュレーションメディアにてDQNについての記事を書いたので、よろしければ見てみてくださいm(_ _)m
ainow.ai

今回はこのDQNを使って3色オセロのトリコロールを学習しました!

トリコロールとは?

f:id:majisuke:20170805195611p:plain
トリコロールは青・赤・白の3色で行うボードゲームです。

青と赤それぞれの裏面が白になっています。

f:id:majisuke:20170805195628p:plain
ひっくり返るルールはオセロと同じで、縦・横・斜めで自分の色のコマ同士で挟むことでひっくり返ります。

ただ記憶力が高くなければ白がひっくり返った時に何色になるかを覚えることができません。特に本プログラムでは初期配置の白コマの裏面はランダムになっています。なので、最初の時点ですでに運ゲー要素があります。

DQNの学習

今回はこちらのサイトを参考に実装しました。
機械学習の理論を理解せずに tensorflow で オセロ AI を作ってみた 〜導入編〜 - Qiita

まずはオセロとDQNのプログラムをこちらからダウンロードしてみてください。

$ git clone https://github.com/sasaco/tf-dqn-reversi

DQNはAI(エージェント)が状況を把握して行動します。行動に対する報酬"Q値"をエージェントに与えることで、より適した行動を取れるようになります。初めはランダムに行動した結果を保存しますが、勝った時に得られる報酬を参考にそれぞれの一手に対するQ値が定まります。

本プログラムではDQNのエージェント同士を戦わせて学習していきます。
まずは黒ターンと白ターンのAIを用意します

#train.py
#もろもろインポート
import copy
from Reversi import Reversi
from dqn_agent import DQNAgent

#オセロ開始の合図
if __name__ == "__main__":
    # 繰り返しの学習回数の設定
    n_epochs = 100
    # オセロの環境を構築
    env = Reversi()
    # playerID    
    playerID = [env.Black, env.White, env.Black]
    players = []
    # player[0]= 黒のターン
    players.append(DQNAgent(env.enable_actions, env.name, env.screen_n_rows, env.screen_n_cols))
    # player[1]= 白のターン
    players.append(DQNAgent(env.enable_actions, env.name, env.screen_n_rows, env.screen_n_cols))

ここから学習の記述になります。コマを置く、勝敗が決まる、というオセロの一連の流れをn_epochs回リピートして行動のモデルを保存します。
後にAIと戦う際に学習したAIが後攻になるので、後攻の勝敗の結果のみ保存します。

#train.py
    for e in range(n_epochs):
        env.reset() # 最初は初期化
        terminal = False
        while terminal == False: # 1エピソードが終わるまでループ
            for i in range(0, len(players)): 
                state = env.screen
                targets = env.get_enables(playerID[i])
                if len(targets) > 0: # どこかに置く場所がある場合
                    for tr in targets: #すべての手をトレーニングする
                        tmp = copy.deepcopy(env)
                        tmp.update(tr, playerID[i])
                        #終了判定
                        win = tmp.winner()
                        end = tmp.isEnd()
                        #次の状態
                        state_X = tmp.screen
                        target_X = tmp.get_enables(playerID[i+1])
                        if len(target_X) == 0:
                            target_X = tmp.get_enables(playerID[i])
                        # 両者トレーニング
                        for j in range(0, len(players)):
                            reword = 0
                            if end == True:
                                if win == playerID[j]:
                                    # 勝ったら報酬1を得る
                                    reword = 1
                            # 勝敗を保存する    
                            players[j].store_experience(state, targets, tr, reword, state_X, target_X, end)
                            players[j].experience_replay()

                    # 行動を選択  
                    action = players[i].select_action(state, targets, players[i].exploration)
                    # 行動を実行
                    env.update(action, playerID[i])
                    # ログの記述
                    loss = players[i].current_loss
                    Q_max, Q_action = players[i].select_enable_action(state, targets)
                    print("player:{:1d} | pos:{:2d} | LOSS: {:.4f} | Q_MAX: {:.4f}".format(
                             playerID[i], action, loss, Q_max))

                # 行動を実行した結果
                terminal = env.isEnd()     
        # 試合ごとの勝敗を記述               
        w = env.winner()                    
        print("EPOCH: {:03d}/{:03d} | WIN: player{:1d}".format(
                         e, n_epochs, w))
    # 後攻のplayer2のデータのみを保存する。
    players[1].save_model()

f:id:majisuke:20170806113353p:plain
実行するとこんな感じにどんどん学習していきます。(結構時間かかります)

DQNとの戦いの実装

次は実際に学習したAIと戦うプログラムです。コマンドライン上でコマンドを書きながらAIとオセロをします。argparseを使用することで、保存しているモデルをエージェントに追加します。

#FightWithAI.py
#もろもろインポート
import argparse
from Reversi import Reversi
from dqn_agent import DQNAgent

if __name__ == "__main__":
    # argparseでコマンドラインの引数を解釈
    parser = argparse.ArgumentParser()
    parser.add_argument("-m", "--model_path")
    parser.add_argument("-s", "--save", dest="save", action="store_true")
    parser.set_defaults(save=False)
    args = parser.parse_args()
    # オセロ環境を構築してmodelsのフォルダから学習したモデルをロードする
    env = Reversi()
    agent = DQNAgent(env.enable_actions, env.name, env.screen_n_rows, env.screen_n_cols)
    agent.load_model(args.model_path)

これ以下のほとんどのコードがオセロの実行プログラムReversi.pyと記述が同じなので、AIの実装部分のみ紹介します。保存されている学習結果を参考に次の手を決めています。

#FightWithAI.py
 print("*** AIターン● ***")
        env.print_screen()
        enables = env.get_enables(2)
        if len(enables) > 0:
            # 学習結果の中で最も報酬が高い一手を選択
            qvalue, action_t = agent.select_enable_action(env.screen, enables) 
            print('>>>  {:}'.format(action_t))              
            env.update(action_t, 2)
        else:
            print("パス")

f:id:majisuke:20170806113536p:plain
実行すると、このようにAIと戦えます。(画像はトリコロールでの実行結果)

オセロをトリコロールに書き換え

最後にオセロのプログラムReversi.pyをトリコロールに書き換えます。

まずは初期のマスが8*8と大きく時間がかかってしまうので、5*5に変えます。
そしてトリコロール独特のコマである「黒の裏」と「白の裏」を作っておきます。

import os
import numpy as np
import random # 初期の白コマにランダムを付与するため

class Reversi:
    def __init__(self):
        # parameters
        self.name = os.path.splitext(os.path.basename(__file__))[0]
        self.Blank = 0
        self.Black = 1
        self.White = 2
        self.Blackrev = 3 # 黒の裏のコマを追加
        self.Whiterev = 4 # 白の裏のコマを追加
        # 学習時間を早めるために5*5の25マスでプレイ #
        self.screen_n_rows = 5 
        self.screen_n_cols = 5 
        self.enable_actions = np.arange(self.screen_n_rows*self.screen_n_cols)
        # variables
        self.reset()

初期のコマの配置の際、センターのコマの色をランダムで決めます。
20~25行目

    def reset(self):
        """ 盤面の初期化 """
        # reset ball position
        self.screen = np.zeros((self.screen_n_rows, self.screen_n_cols))
        self.set_cells(7, self.White)
        self.set_cells(11, self.Black)
        self.set_cells(17, self.Black)
        self.set_cells(13, self.White)
        self.set_cells(12, random.randint(3,4)) # 初期の白コマはランダムで決まる

黒と白以外のコマは「裏」と表記することにします。
40~50行目

    def print_screen(self):
        """ 盤面の出力 """
        i = 0
        for r in range(self.screen_n_rows):
            s1 = ''
            for c in range(self.screen_n_cols):
                s2 = ''
                if self.screen[r][c] == self.Blank:
                    s2 = '{0:2d}'.format(self.enable_actions[i])
                elif self.screen[r][c] == self.Black:
                    s2 = '●'
                elif self.screen[r][c] == self.White:
                    s2 = '○'
                else:
                    s2 = '裏' # 出力が白黒なので、ひっくり返った時は「裏」にする
                s1 = s1 + ' ' + s2
                i += 1
            print(s1)

マスの数を変えたので、コマをおける場所の定義も変更します。
60~80行目

    def put_piece(self, action, color, puton=True):
        
        """ ---------------------------------------------------------"""

        t, x, y, l = 0, action%5, action//5, [] # 盤面の変更にともないコマの動きを変更
        for di, fi in zip([-1, 0, 1], [x, 4, 4-x]):
            for dj, fj in zip([-5, 0, 5], [y, 4, 4-y]):

裏に返す処理の記述です。黒と白だけであれば塗り替えるだけで良いのですが、反転するという動作に変更します。
110~120行目

if puton:
            """ ひっくり返す石を場合分けする """
            for i in l:
                if self.get_cells(i) == 3:
                    self.set_cells(i, 1)
                elif self.get_cells(i) ==4:
                    self.set_cells(i, 2)
                elif self.get_cells(i) ==1:
                    self.set_cells(i, 3)
                elif self.get_cells(i) ==2:
                    self.set_cells(i, 4)

これでトリコロールに書き換え完了です。

実戦!!

それでは学習数100回のAIと戦ってみました!

f:id:majisuke:20170806113717p:plain

負けちゃいましたね...

もうちょっと自分の腕を磨いて再チャレンジします。

メンヘラ炸裂!? 西野カナの歌詞から感情の時系列データを抽出してみた

f:id:majisuke:20170805151412j:plain
どーも! まじすけです✨
今回は以下のリンクを参考に、pythonで曲の歌詞から曲中の感情の動きを可視化してみました。
www.statsbeginner.net

「会いたくて 会いたくて」を初め、女性に圧倒的人気を誇る西野カナさん。
よくメンヘラの代名詞とも言われる彼女の曲を可視化してみたら面白いんじゃないかな?ということでやってみました。

f:id:majisuke:20170809141353p:plain

ちなみに「会いたくて 会いたくて」はこんな感じに出ます。(浮き沈みが激しい...)

準備

用意するものはこちら!

  • 西野カナの曲の歌詞
  • python3系
  • MeCab
  • PN Table

日本語の形態素解析MeCabを使うので、使ったことがない人は以下のリンクを参考にしてください。
qiita.com

続いて日本語の感情のネガポジ判定をしてくれる辞書PN Tableをダウンロードしてください。
Macで使う方は辞書が文字化けしてしまうためUTF-8に変換しなければいけません。以下のリンクを参考にファイルをUTF-8形式に変換してください。
kawatama.net

実装

それでは実装に移ります✌︎('ω'✌︎ ) 今回は西野カナさんの「if」を使ってみました!

#まずはもろもろインポート
import re
import csv
import time
import pandas as pd
import matplotlib.pyplot as plt
import MeCab
import random
%matplotlib inline
# 歌詞の読み込み
title = 'nishino'
f = open(title+'.txt')
lyrics_df = f.read()  # ファイル終端まで全て読んだデータを返す
f.close()
# MeCabインスタンス作成
m = MeCab.Tagger('')

このとき歌詞のテキストファイルはプログラムと同じフォルダに入れてください。*歌詞のテキストファイルに関して後で読み取れない部分を省くので、改行などがあっても構いません。
MeCabが上手くいってるかチェックしてみましょう。

print(m.parse(lyrics_df))

f:id:majisuke:20170805144009p:plain

こんな感じに出ればひとまずokです!
続いてネガポジ判定の辞書を読み込みます。

# PN_TABLE辞書の読み込み
pn_df = pd.read_csv('pn_ja.dic',\
                    sep=':',
                    encoding='utf-8',
                    names=('Word','Reading','POS', 'PN')
                   )

次に2つの関数をつくります。
一つはMeCabが解析したあとにでてくる余分な2行を削除して、必要な情報だけを残します。
もう一つはMeCabから作った新しいデータにPN_TABLEの情報を付与します。さらにPN_TABLEに入っていない文字列を削除します。

# テキストを形態素解析して辞書のリストを返す関数
def get_diclist(lyrics):
    parsed = m.parse(lyrics)      # 形態素解析結果(改行を含む文字列として得られる)
    lines = parsed.split('\n')  # 解析結果を1行(1語)ごとに分けてリストにする
    lines = lines[0:-2]         # 後ろ2行は不要なので削除
    diclist = []
    for word in lines:
        l = re.split('\t|,',word)  # 各行はタブとカンマで区切られてるので
        d = {'Surface':l[0], 'POS1':l[1], 'POS2':l[2], 'BaseForm':l[7]}
        diclist.append(d)
    return(diclist)

# PN Tableをデータフレームからdict型に変換しておく
word_list = list(pn_df['Word'])
pn_list = list(pn_df['PN'])  # 中身の型はnumpy.float64
pn_dict = dict(zip(word_list, pn_list))

# 形態素解析結果の単語ごとdictデータにPN値を追加する関数
def add_pnvalue(diclist_old):
    diclist_new = []
    for word in diclist_old:
        base = word['BaseForm']        # 個々の辞書から基本形を取得
        if base in pn_dict:
            pn = float(pn_dict[base]) # pnの中身があれば追加
        else:
            pn = 'notfound' # 中身がなければnotfound
        word['PN'] = pn
        diclist_new.append(word)
    pn_list = []
    textlist = []
    for word in diclist_new:
        pn = word['PN']
        text = word['BaseForm']
        if pn != 'notfound': # notfoundじゃなかったら追加
            pn_list.append(pn)
            textlist.append(text)
    return(pn_list, textlist)

最後にそれぞれの関数を実行し、pandasのDataFrameを使って文字とそのPN値を対応させたデータを作ります。

dl_old = get_diclist(lyrics_df)
pn_list, text_list= add_pnvalue(dl_old)
    
# 本文、PN値を格納したデータフレームを作成
aura_df = pd.DataFrame({'text':text_list,
                        'PN':pn_list,
                       },
                       columns=['text', 'PN']
                      )

結果

ここでデータを可視化してみましょう。

plt.plot(pn_list)
plt.title(title)
plt.show

f:id:majisuke:20170805144858p:plain

こんな感じにプロットできました。かなりマイナスの要素が強い曲でしたね。
歌詞というのは曲の中で時間とともに流れていくので、"感情の時系列データ"を抽出したとも言えるかもしれませんね。

MySQLをjupyter notebookのkernelに入れる方法

f:id:matsume_goods:20170808213817p:plain

目次

  1. いきさつ
  2. 前準備
  3. 実践

方法だけ知りたければ3番までスキップしてください。

1.いきさつ: MySQL を jupter notebook で動かしたい

pythonで機械学習を勉強する際にデータベースの勉強が必要になりまして、sqlをjupyter notebook上で動かす必要が出てきました。
そこでjupyter notebookのkernelをいじっていたのですが、少し躓いてしまったので備忘録程度にjupyterへのMySQLの入れ方をまとめておきます。
なお、実行環境はubuntu16.04です。

注意
この記事はMySQLをjupyter notebookのkernelに入れることを目的としていますが、pythonでデータベースを扱う際にはMySQL-pythonをinstallして、
pythonのカーネルでMySQLdbをimport
したほうが実用的です。MySQL-pythonについては下記のサイトを参考にしてください。

MySQLを操作する | Make.

MySQL Python tutorial - programming MySQL in Python

2. 前準備: mysql-serverをインストール

そもそもmysqlサーバーのユーザー設定が無いと意味がないので、ターミナルで

sudo apt-get install mysql-server

を実行。当然、元から入っている人はスルーです。
また、この際にpasswordの設定画面が出るので、設定しましょう。
詳しくは下記の記事を参考に。

Ubuntu で MySQL - Qiita

3. 実践: MySQLをjupyter notebookのkernelに入れる

github.com

ここから実際にインストールしていきます。
いろいろ悩みまして、ipython用のパッケージでもjupyterとの互換性があるということなので上記のパッケージを使いました。
基本は上記の"README"を参考にしましたが、適宜"ipython"の部分を"jupyter"に読み替えていきます。

基本やることは、

  1. ipython_mysql_kernelのインストール
  2. Configのjsonファイルを作成
  3. kernelのjsonファイルを作成、jupyterのkernelにファイルを追加

この3フェーズに別れます。

1. ipython_mysql_kernelのインストール

ターミナルで

sudo pip install git+https://github.com/mmisono/ipython_mysql_kernel

を実行します。

2. Configのjsonファイルを作成

ここで少し躓きました。というのも、ここを飛ばしてしまってもjupyter notebookでmysqlが選択できるようにはなるのですが、mysqlサーバーへのログインがうまく行かないためdead kernelと表示されてしまったからです。

f:id:matsume_goods:20170808213240p:plain

画像1: ログインできず、死んだことにされるkernel。ターミナルの方のログを見るとログインでうまく行っていないことが確認できる。

ということで、先ほど作ったユーザー設定をまとめたConfigファイルを作ります。
ホームディレクトリに
.ipython
というフォルダがあるので、そのフォルダに
mysql_config.json
というファイルを作り、内容を

{
    "user"     : "root",
    "port"     : "3306",
    "host"     : "127.0.0.1",
    "charset"  : "utf8",
    "password" : "(設定したpassword)"
}

mysql_config.json

とします。
これでjupyterによるログイン時にこのユーザー設定が優先されるようになります。

3. kernelのjsonファイルを作成、jupyterのkernelにファイルを追加

今度は現在のディレクトリ(どこでも良い)に
mysql
というフォルダを作成し、そのフォルダに
kernel.json
を作り、内容を

{
    "argv": [
        "python", 
        "-m",
        "ipython_mysql_kernel",
        "-f",
        "{connection_file}"
    ],
    "display_name": "mysql",
    "language": "mysql"
}

kernel.json

とします。そうしたらターミナルからフォルダmysqlがあるディレクトリで

sudo jupyter kernelspec install mysql

を実行します。

usr/local/share/jupyter/kernels

mysqlがあればkernelの追加は完了です。

jupyter notebookのkrenelが動くか確認

f:id:matsume_goods:20170808213315p:plain

画像2: jupyter notebookのkernelにMySQLが入っているかを確認する。

うまく行っていれば上記のようにmysqlを選択できるので、適当にいじってみましょう。


以上になります。
今回の躓きはjupyterに執着しすぎていてipythonの互換性を見落としていたことを起因としたものでした。そのうちjupyterのSQLkernelを作ったほうが良いのだろうか・・・。

【音声認識 超入門】 固定長音声データの分類

概要

以前から音声認識には興味があったので, その第一歩として, Yes と No の固定長の音声を機械学習を用いて分類しました. (どっちも2秒)
今回作成したソースコードはgithub上げときました.

環境

macOS, python3 (anaconda)

必要なものをインストールする.

portaudioとpyaudioをインストールする.

brew install portaudio
pip install pyaudio

他のOSの場合など詳しくはここを見てね.

soundfileをインストールする.

pip install soundfile

データを作る

今回は自分の声を分類する分類器を作りたいから, 録音するプログラムから作りました.
録音するプログラムにimportしているrecorderのソースコードもgithubに上げたから気になったら見てください.
下のプログラムで連続して録音しました. (YesとNo, 20回ずつ)

import sys
import pyaudio
import wave

import recorder

cnt = 0
print('input prefix of output filename:')
fname = input()
print('input number to start with:')
cnt = int(input())
rec = recorder.WaveRecorder()
while True:
    print('Press enter to start recoding. Type end to finish recording.')
    if input() == 'end':
        break
    rec.record('{}_{}.wav'.format(fname, cnt))
cnt += 1

モデルの学習

分類器は, Random Forest, Gradient Boosting, Support Vector, KNNの4つを試しました. まず, それぞれのwaveデータから一次元データを作成し, くっつける.

n_datas = 20
X = []
y = []
for i in range(n_datas):
    no, _ = sf.read('no_{}.wav'.format(i))
    yes, _ = sf.read('yes_{}.wav'.format(i))
    no = no[:, 0] #2chで録音したので片方だけ取ってくる
    yes = yes[:, 0]
    X.append(no)
    X.append(yes)
    y.append(0) #Noはクラス0
    y.append(1) #Yesはクラス1

X = np.array(X)
y = np.array(y)

次に説明変数をFFTの絶対値と位相にする.

X_fft = np.array([np.fft.fft(x) for x in X])
X_fft = np.fft.fft(X)
X = np.array([np.hstack((x.real**2+x.imag**2, np.arctan2(x.real, x.imag))) for x in X_fft])

そしてcross_val_scoreで5分割して交差検証, 正答率を表示.

#ここをGradientBoostingClassifier(), SVC()やKNeighborsClassifier()に変える
clf = RandomForestClassifier()
scores = cross_val_score(clf, X, y, cv=5)
print('score:{:.3f} (+/-{:.3f})'.format(scores.mean(), scores.std()*2))

結果

Random Forest の結果は0.975 (+/-0.100), Gradient Boostingは1.000 (+/-0.000), SVCは0.700 (+/-0.339), KNNは0.675 (+/-0.436)でした. この結果から, Gradient Boostingが最も良いと考えられます.

データ数がそれぞれ20個ずつしか無いのに思ったより精度が高かったです.
この理由は,
・ 分類クラスが2クラスだけ
・ 雑音がほぼなかった ・ trainもtestも自分の声しか入っていないので周波数とか似てる
だと思います.

実行するとわかると思いますが, Gradient Boostingは3手法の中では1番学習に時間がかかっています. 学習時, 全ての弱分類器が独立でないBoostingという手法を使っているのが原因だと考えられます. ただ, 学習には時間がかかっても, 実際に使うときには短時間で計算できるため, これを使っても問題はないです.

遊び

実際に学習したモデルで, 新たに録音した音声を分類させてみる.

#全データで学習
clf.fit(X, y)

rec = recorder.WaveRecorder()
yesno = ['No', 'Yes']
while True:
    print('Press enter to start recording. Type end to finish recording.')
    if input() == 'end':
        break

    rec.record('output.wav')
    wav, _ = sf.read('output.wav')
    wav = np.array(wav[:, 0])
    wf = np.fft.fft(wav)
    wav = np.hstack((wf.real**2+wf.imag**2, np.arctan2(wf.real, wf.imag)))
    pred = clf.predict(np.array([wav]))
    print(yesno[int(pred)])

実行すると次のようになった.

* recording
* done recording
Yes
Press enter to start recording. Type end to finish recording.

* recording
* done recording
Yes
Press enter to start recording. Type end to finish recording.

* recording
* done recording
No
Press enter to start recording. Type end to finish recording.

* recording
* done recording
No
Press enter to start recording. Type end to finish recording.

* recording
* done recording
Yes
Press enter to start recording. Type end to finish recording.

* recording
* done recording
No
Press enter to start recording. Type end to finish recording.
end

Yes, Yes, No, No, Yes, Noの順に発声したので全て正しく分類されていました. この程度のデータ数で自分の声だけならこれだけちゃんとできるのであれば, 家電とか声で操作するの一から作ってもそこまで難しくないかも!?

【教師あり学習】怠惰で強力なアルゴリズム!?k-NN【分類】

k-NNとは?

突然ですが、皆さんは巡回セールスマン問題(TSP)は知っていますか。
最短経路問題の一つですが、その経路決定アルゴリズムの一つに最近傍法というのがあります。
これは現在いる地点から一番近い地点へと経路を決定するアルゴリズムですが、機械学習にも似たようなアルゴリズムが存在します。
それがk近傍法、k-NNと略されるアルゴリズムです。

教師あり学習

k-NNは教師あり学習のアルゴリズムの一つです。
さて、教師あり学習とはなんでしょうか。
教師あり学習は主に未知のデータや将来のデータを予測できるように既知のデータを用いて学習をする機械学習の種類の一つです。
既知のデータから学習するためそのデータを教師に見立てて教師データと呼ぶことにします。
教師ありデータでは予測したいデータを目標変数、その予測に使うデータを説明変数と言います。
目標変数と説明変数の間になにか関係があって、それを数式、あるいは関数として表せないかという問題を扱う分野です。 他にも教師なし学習や強化学習が機械学習の主な種類としてあげられます。

分類と回帰

教師あり学習には大きく分けて二つの分野が存在します。
分類回帰です。 違いは目標変数が離散値であるか連続値であるか、ということです。

目標変数が離散値をとる場合、教師あり学習で予測したいものは入力したデータがどの値に分類されるかということです。
よって目標変数が離散値である場合を分類(Classification)と言います。

目標変数が連続値をとる場合、目標変数と説明変数との間には数学的関係が存在するのではないか、私たちの多くはそう考えることでしょう。
回帰(Regression)はそうした数学的関数を識別関数として探索し、その識別関数からデータを予測しようとすることを指します。

“怠惰"学習

k-NNは怠惰学習(lazy learner)という学習アルゴリズムの代表例です。
怠惰というのは教師データから識別関数、つまり入力されるデータを直接分類する関数を学習しないということです。
ではどのように分類を行っているのかというと、入力されたデータと類似している上位k個の教師データを参照し、そのデータグループの中で一番多く分類されているクラスに分類するという方式を取っています。
機械学習のアルゴリズムですが教師データを丸暗記するような挙動をとる特徴的なアルゴリズムなんです。

Pythonによるk-NN分類器実装

k-NNを使った分類予測をPythonを使って実装してみます。
機械学習ではscikit-learnというPythonのモジュールが便利です。
まずはプログラムの全容から。

import requests
import io
import pandas as pd

from sklearn import preprocessing
from sklearn.model_selection import train_test_split

from sklearn.neighbors import  KNeighborsClassifier

import matplotlib.pyplot as plt

vote_data_url = "https://archive.ics.uci.edu/ml/machine-learning-databases/voting-records/house-votes-84.data"
s = requests.get(vote_data_url).content
vote_data = pd.read_csv(io.StringIO(s.decode('utf-8')),header=None)
vote_data.columns = ['Class Name',
                     'handicapped-infants',
                     'water-project-cost-sharing',
                     'adoption-of-the-budget-resolution',
                     'physician-fee-freeze',
                     'el-salvador-aid',
                     'religious-groups-in-schools',
                     'anti-satellite-test-ban',
                     'aid-to-nicaraguan-contras',
                     'mx-missile',
                     'immigration',
                     'synfuels-corporation-cutback',
                     'education-spending',
                     'superfund-right-to-sue',
                     'crime',
                     'duty-free-exports',
                     'export-administration-act-south-africa']

label_encode = preprocessing.LabelEncoder()
vote_data_encode = vote_data.apply(lambda x: label_encode.fit_transform(x))
X = vote_data_encode.drop('Class Name', axis=1)
Y = vote_data_encode['Class Name']
X_train, X_test, y_train, y_test = train_test_split(X,Y, random_state=50)

training_accuracy = []
test_accuracy = []

neighbors_settings = [i for i in range(1, 30)]
for n_neighbors in neighbors_settings:
    clf = KNeighborsClassifier(n_neighbors = n_neighbors)
    clf.fit(X_train, y_train)
    training_accuracy.append(clf.score(X_train, y_train) * 100)
    test_accuracy.append(clf.score(X_test, y_test) * 100)

plt.plot(neighbors_settings, training_accuracy,label='教師用データの正解率')
plt.plot(neighbors_settings, test_accuracy,label='テスト用データの正解率')
plt.ylabel('正解率')
plt.xlabel('分類に使ったデータの数')
plt.legend()
plt.show()

モジュールの読み込み

import requests
import io
import pandas as pd

from sklearn import preprocessing
from sklearn.model_selection import train_test_split

from sklearn.neighbors import  KNeighborsClassifier

import matplotlib.pyplot as plt

今回のプログラムで使うデータはオンラインに公開されているデータを使うため取得するために標準モジュールのrequestsとioを使っています。
pandasは機械学習に使うデータとして扱いやすくするよう加工するために用います。
sklearnはscikit-learnのことで機械学習のためのモジュールです。
このモジュールから様々なアルゴリズムを読み込むことができますが今回はKNNのみです。
最後にグラフを表示させるためにmatplotlibを読み込んでいます。

データの読み込みと前処理

# dataの参照URL
vote_data_url = "https://archive.ics.uci.edu/ml/machine-learning-databases/voting-records/house-votes-84.data"
# data取得
s = requests.get(vote_data_url).content
# dataを扱いやすいように加工
vote_data = pd.read_csv(io.StringIO(s.decode('utf-8')),header=None)
# dataには名前が付いてないので自分でつける
vote_data.columns = ['Class Name',
                     'handicapped-infants',
                     'water-project-cost-sharing',
                     'adoption-of-the-budget-resolution',
                     'physician-fee-freeze',
                     'el-salvador-aid',
                     'religious-groups-in-schools',
                     'anti-satellite-test-ban',
                     'aid-to-nicaraguan-contras',
                     'mx-missile',
                     'immigration',
                     'synfuels-corporation-cutback',
                     'education-spending',
                     'superfund-right-to-sue',
                     'crime',
                     'duty-free-exports',
                     'export-administration-act-south-africa']

# データの中身は文字列なので数字に変更
label_encode = preprocessing.LabelEncoder()
vote_data_encode = vote_data.apply(lambda x: label_encode.fit_transform(x))
# 説明変数
X = vote_data_encode.drop('Class Name', axis=1)
# 目標変数
Y = vote_data_encode['Class Name']
# 教師データとテストデータに分ける
X_train, X_test, y_train, y_test = train_test_split(X,Y, random_state=50)

今回のデータはUCI Machine Learning RepositoryにあるCongressional Voting Records datasetを用いました。
これはアメリカ合衆国下院議会において議員が共和党員か民主党員かという立場と議員が政策に対してどのように投票したかということをまとめたデータセットです。

詳しくは以下のリンクを参照してください。 UCI Machine Learning Repository: Congressional Voting Records Data Set

取得してきたデータセットはClass Nameと名付けた項は'democrat'(民主党員)、'republican'(共和党員)のどちらかが、それ以外の項は'y'(yes)、'n'(no)、そして'?‘のどれかが入っています。
このままだと使いづらいので('democrat’, ‘republican’) = (0, 1)、(‘?’, ‘n’, ‘y’) = (0, 1, 2)というように数字に変換します。

今回のデータでは分類したい目標はClass Name、そのために使うデータはそれ以外として分類を行うことにします。
つまり、分類を予測したいものは民主党員であるか、共和党員であるかということで、そのために使うデータは教育や保険などの13の政策に対して議員が賛成、反対のどちらに票を投じたのかという投票記録です。
本当はここでデータの選択をするべきですが、入門なのでそこは気にせずやっていきます。

scikit-learnのtrain_test_split関数を使って教師データとテストデータに分割します。
こうして得られたX_trainとy_trainを使って学習をします。

k-NNによる分類と正解率の表示

# 教師データに対する予測の正解率を格納
training_accuracy = []
# テストデータに対する予測の正解率を格納
test_accuracy = []

# kの値を格納(グラフのプロットに用いるためリストを用意)
neighbors_settings = [i for i in range(1, 30)]
for n_neighbors in neighbors_settings:
    # KNN分類器を作成(k=n_neighbors)
    clf = KNeighborsClassifier(n_neighbors = n_neighbors)
    # 教師データを用いて学習
    clf.fit(X_train, y_train)
    # k=n_neighborsのときのKNNの教師データに対する予測の正解率をリストに送る
    training_accuracy.append(clf.score(X_train, y_train) * 100)
    # k=n_neighborsのときのKNNのテストデータに対する予測の正解率をリストに送る
    test_accuracy.append(clf.score(X_test, y_test) * 100)

# 以下、グラフ表示のための処理
plt.plot(neighbors_settings, training_accuracy,label='教師用データの正解率')
plt.plot(neighbors_settings, test_accuracy,label='テスト用データの正解率')
plt.ylabel('正解率')
plt.xlabel('分類に使ったデータの数')
plt.legend()
plt.show()

k-NN分類器の実装そのものはとてもシンプルです。
clf = KNeighborsClassifier() たったこれだけです。

自分で実装する場合はそれなりに数学の知識が必要になりますが、とにかくこれで分類器が作成できました。
続いてfitメソッドを用いて分類器に学習させます。
これで分類器がどのようなデータをどう分類すればいいのか学習します。

あとはデータを使って予測をさせるだけです。
今回は正解率をグラフにプロットするためにscoreメソッドを用いていますが、分類の予測結果が欲しい場合はpridictメソッドを使います。

実行結果

f:id:trickortreatcat338:20170729181526p:plain

画像を見てみると正解率にブレはあるものの、おおよそkの値が3か4で一番正解率が高く、kの値が大きくなるに従って正解率が下がっているのがわかります。
k-NNにおいては、kの値が大きくなるとデータの類似度がそんなに高くないデータも類似データとして識別処理に使われることになります。
その結果として、分類クラスの境界線が曖昧になってしまうという問題が発生します。
kの値は参照するデータなどにより変えるのが一般的です。

また、正解率がおおよそ90%を超えているのも画像から読み取れますね。
かなり直感的なように思えるこのアルゴリズムですが、機械学習として強力なアルゴリズムであることが分かっていただけたでしょうか?

参考文献

今回記事を作成するにあたり、以下の本を参考にさせていただきました。
Amazon CAPTCHA