キュウリ収穫量の可視化

キュウリを植えたらキュウリと別の物ができると思うな。人は自分の植えたものを収穫するのである。
二宮尊徳(金次郎)

変わらなければ生き残れない
ドラスティックに何かを変えなければ...
そんな危機感が日々日々強くなっていきます。

ただやはりキュウリからはキュウリである。自分自身に今あるものを育てて、いかに美味しく生き生きとしたものにしていくか。こんな時だからこそしっかりと学び、またやってくる旬の時期に備えたいですね。

とキュウリキュウリ言っていますが、僕はキュウリが大好きです。浅漬け、ピリ辛、梅あえ、シンプルなもろきゅう&味噌マヨもたまりません。宮崎県のキュウリの冷汁も大好きです。

今回はキュウリでいきましょう!

f:id:KenjiU:20200504130909p:plain

キュウリの収穫量を可視化してみました。なんとPythonによるWebアプリで実現しています。

PythonでWebアプリ作成

Pythonといえば機械学習ディープラーニング用。そんな風に考えていらっしゃる方も多いかと思います。しかし、Pythonには様々なライブラリが存在しており、実はWebアプリも作成することが可能です。

では、コードを見ていきましょう。

1. ライブラリの定義

定義しているDashPython用のWebアプリを作成するためのフレームワークです。Bootstrap(HTML, CSS, JavaScript フレームワーク)やPlotly(グラフライブラリ)を使うことができます。

import dash
import dash_bootstrap_components as dbc
import dash_html_components as html
import dash_core_components as dcc
import numpy as np
import pandas as pd
import plotly.graph_objects as pgo

# Bootstrapスタイルシートをリンク
app = dash.Dash(external_stylesheets=[dbc.themes.BOOTSTRAP])

2. データの取得

キュウリの収穫量データと都道府県の緯度経度データを読み込みます。
以下のサイトのデータを使用いたしました。

きゅうり(キュウリ・胡瓜)の都道府県別生産量(収穫量)/グラフ/地図/一覧表|統計データ・ランキング|家勉キッズ

【みんなの知識 ちょっと便利帳】都道府県庁所在地 緯度経度データ - 各都市からの方位地図 - 10進数/60進数での座標・世界測地系(WGS84)

# きゅうりの都道府県別収穫量データ(CSVファイル)を読み込む
# 引用: ieben.net | きゅうりの生産量(収穫量)
# https://ieben.net/data/production-vegetables/japan-tdfk/k-kyuuri.html
ccb = pd.read_csv('cucumber_jpn.csv')

# 都道府県の緯度経度データ(Excelファイル)を読み込む
# 引用: みんなの知識 ちょっと便利帳 | 都道府県庁所在地 緯度経度データ
# https://www.benricho.org/chimei/latlng_data.html
ccb_latlng = pd.read_excel("latlng_data.xls", header=4)
ccb_latlng = ccb_latlng.drop(ccb_latlng.columns[[0,2,3,4,7]], axis=1).rename(columns={'Unnamed: 1': '都道府県'})
ccb_latlng = ccb_latlng.head(47)

3. データをグラフ表示用に加工

2で読み込んだデータを加工します。

# 収穫量分布バブルチャート用
# 「きゅうりの都道府県別収穫量データ」と「都道府県の緯度経度データ」をマージ
ccb_merge = pd.merge(ccb, ccb_latlng, on='都道府県')
# バブルチャートにカーソルをあてた時に表示されるテキストを定義
ccb_merge['notes'] = np.nan
for i in range (len(ccb_merge)):
  ccb_merge['notes'][i] = ccb_merge['都道府県'][i] + ' / ' + str(ccb_merge['収穫量'][i]) + 't' + ' / ' + str(ccb_merge['順位'][i]) + '位'

# 収穫量トップ20グラフ用
ccb_prep = ccb.sort_values("収穫量",ascending=False).head(20)
ccb_prep = ccb_prep.sort_values("収穫量")

4. 画面表示処理

各エリアのサイズや色やテキストなどを仕様に従い定義します。とてもシンプルですね。

app.layout = dbc.Container(
[
  # 画面のタイトルエリア
  dbc.Row(
  [
    dbc.Col(
    html.H1("キュウリの収穫量グラフ"),
    style = {
    "size": "30px",
    "backgroundcolor": "#fffcdb",
    "color": "#00a95f",
    "textAlign": "left",
    }
    )
  ],
  ),
  # グラフのタイトルエリア
  dbc.Row(
  [
    # 左グラフのタイトル
    dbc.Col(
    html.H4("収穫量分布"),
    width = 7,
    style = {
      "height": "100%",
      "backgroundcolor": "white",
      "textAlign": "left",
      "padding":"10px"
      },
    ),
    # 右グラフのタイトル
    dbc.Col(
    html.H4("収穫量トップ20"),
    width = 5,
    style = {
      "height": "100%",
      "backgroundcolor": "white",
      "textAlign": "left",
      "padding":"10px"
      },
    ),
  ],
  ),
  # グラフエリア
  dbc.Row(
  [
    # 左グラフ
    dbc.Col(
    dcc.Graph(
    id = 'JpnMap',
    figure = {
    'data' : [
      pgo.Scattergeo(
      lat = ccb_merge["緯度"],
      lon = ccb_merge["経度"],
      marker = dict(
        color = 'rgb(0, 169, 95)', #バブルチャートの色
        size = ccb_merge['収穫量']/1500+5, #バブルチャートのサイズ
        opacity = 0.7 #バブルチャートの透明度
        ),
      hovertext = ccb_merge['notes'], #カーソルをあてた時に表示されるテキスト
      hoverinfo = "text"
      )
      ],
    'layout' : pgo.Layout(
      width = 600,
      height = 500,
      template="plotly_dark", #ダークモード
      margin = {'b':5,'l':5,'r':5,'t':5},
      geo = dict(
        resolution = 50,
        landcolor = 'white', #陸地の色
        lataxis = dict(
          range = [25, 47], #地図表示範囲(緯度)
        ),
        lonaxis = dict(
          range = [126, 150], #地図表示範囲(経度)
        ),
      )
    )
    }
    ),
    ),
    # 右グラフ    
    dbc.Col(
    dcc.Graph(
    id = 'Pref',
    figure = {
    'data' : [ 
      pgo.Bar(
      x=ccb_prep['収穫量'],
      y=ccb_prep['都道府県'], 
      orientation='h' #横棒グラフ
      ),
      ],
    'layout' : pgo.Layout(
      width = 400,
      height = 500,
      template = "plotly_dark", #ダークモード
      margin = {'b': 5, 'l': 5, 'r': 5, 't': 5},
      xaxis_title = "収穫量",
      yaxis_title = "都道府県"
      )
      }
    ),
    ),
  ],
  ),
],
)

5. アプリ起動処理

最後にWebアプリ起動処理です。Dashはホットリロード機能を持っており、コードに変更があった場合に、自動でブラウザをリフレッシュします。

# アプリを起動
if __name__ == "__main__":
  app.run_server(debug=True)

最後に

キュウリの生育適温は20〜25℃。暖かくなり始めた5月の連休頃が植え付け時期となります。夏の生育期のキュウリは1日3cm以上も大きくなります。実家の庭で、朝小さかったキュウリが昼過ぎにはすっかり大きくなっていて、びっくりしたこともありました。ベランダ菜園始めようかしら。

AIと芸術

ついつい最新ニュースが気になってしまう今日この頃。外出自粛ムードの三連休、皆さんいかがお過ごしでしょうか。

運動不足解消のため、僕はご当地版ラジオ体操で毎回笑いながら体操してます。世間では、あの爆発的なムーブメントを巻き起こしたビリーズブートキャンプが再燃しているようです。入隊しようかしら。

家トレも良いですが、ゆったりと音楽を聞いたり、映画を観たり、本を読んだりして過ごしている方も多いかと思います。
なんとそれらの芸術的な分野までも「AI」が担う時代が来るかもしれません。

今回はクリエイティブなことが不得意と言われてきた「AI」について、最近の芸術分野での活躍ぶりをまとめてみました。

音楽

2019年の第70回NHK紅白歌合戦で「AI美空ひばり」が大いに注目を集めました。

僕は歌声を聞いたとたん思わず涙が出てしまいました。子供の頃よくテレビから聞こえてきたあの歌声がふとよみがえってきたというのと、AIでここまでできるようになったのかという衝撃からだと思います。

www.yamaha.com

ヤマハさんの「VOCALOID:AI」(ディープラーニングを使った歌声合成技術)を用いて実現しているとのこと。

学習用データの美空ひばりさんの歌声ですが、美空ひばりさんは歌声のみのレコーディングはされていなかったそうで、伴奏が含まれた歌声データから伴奏部分を取り除く作業が容易ではなかったようです。

データの前処理はとても難しくコストもかかりますが「命」と言われるくらい学習には欠かせない工程になります。

www.itmedia.co.jp

こちらのITmediaさんの記事に、学習モデルについての説明があります。

 開発したシステムでは、複数の学習モデルを組み合わせて歌声を合成する。与えられた楽譜を読み込んで、音程を決めるモデルや発音のタイミングを決めるモデルといった歌声の特徴を作るものと、それらを組み合わせてコントロールするモデルや、最終的な波形を合成するモデルなどを段階的に使う。素片接続とは大きく異なる技術だ。

歌声合成のため、このような学習モデルを複数組み合わせているとは... 驚きの技術です。

「AI」と付くと、あたかも「AI」が自分で音楽を創り出すように感じますが、実際はインプットのデータ(学習用データ)をどうするか、どのようなモデルを用意するか、どのようなアウトプットを期待するかなど「人間」の創造的作業がとても重要になってきます。

漫画

AIと人間によって制作された手塚治虫さんの新作漫画「ぱいどん」が2月27日(木)発売の「モーニング」に掲載されました。

発売直後に購入しましたが、近未来の技術と人間が描かれており、シナリオ、絵、セリフどれも自然で読みやすく続編がとても気になる内容でした。はたして、どの部分をAIが担っているのでしょうか。

robotstart.info

こちらのロボスタさんの記事によると、AIはプロット(設定とあらすじ)とキャラクター(顔)を担当しているとのこと。

  1. プロット

    今回の作品はAIと人間の協業によるもので、ストーリーとマンガの書き起こしのほとんどは人間が行っている。AIが担当したのはまずは作品のプロット(設定とあらすじ)。手塚治虫氏が描いた漫画のストーリーを学習したAIがプロットだけを提示。それを元にスタッフが検討しながらストーリーを作り上げた。

  2. キャラクター

    NVIDIAが開発している人間の顔生成のシステムを応用。膨大な人間の顔画像を元にAIが人間の顔を生成する技術(おそらくGAN)のAIモデルをこのプロジェクトに活用することを決めた。人間の顔生成システムに手塚氏の漫画のキャラクターを追加学習する「転移学習」を行うと、成果がみるみる向上。その中から「ぱいどん」のイメージにぴったりの顔を採用した。

インプット(学習用データ)として、手塚治虫氏の作品データを読み込ませたり、はたまた人間の顔も利用したり、アウトプットについても人が選定して組み合わせていくなど、AIと人間の共同作業と言えるでしょう。人間の創造的作業をいかにデータ化(数値化)して学習用データとして利用できるかが難しいところですね。

俳句

空蝉や 揺れる草木を しかと抱き

こちらは祖母が詠んだ俳句です。昔、祖母の家の額縁に飾っており今でも心に残っております。つい最近、「空蝉」とは、古語の「現人(うつしおみ)」が訛ったもので「生きている人間の世界」を表すことを知り、地に足をつけて生きていこうという思いが強くなりました。

この感性が求められる俳句の作成についても、AIが挑戦しています。

AI俳句協会さんのサイトにAIが作成した俳句が載せられています。AIがどういう意図でその季語や言葉を選んだのか想像すると何か楽しいです。人間には生みだせない意外性が素敵ですね。

気になったAI俳句を一点ご紹介。

初便り 携帯電話 閉ざすのみ

ガラケーなのかなと、微笑ましい気持ちになります。

最後に

現状のAIでは、作品を勝手に生み出すことはできません。人間が、適切なデータを学習させたり、様々な条件を指定したり、評価・調整をしたりして、作品が生み出されます。

ただ、AIは過去のデータを大量に学習することができ、色々なパターンを試すことができます。その結果、人間には生み出せない何かを生み出せたり、人間が気付けなかったことを発見することがあるかもしれません。

人間が創造的作業を行うツールの一つとして、AIを上手く活用することができれば、AIならではの面白い共同作品が生まれてくるでしょう。

OpenAI Gymで強化学習

何かを始めようと思ったとき、まずは「それは前例のあることだろうか」と調べたり、他の人の意見を聞いたりすることがあるかと思います。

過去に同じような事例があると分かった場合、仲間を見つけたような安心感がでてきますね。確かにその事例を参考にすることで行動がしやすくなり、失敗するリスクを減らすことができます。

とはいえ、前例ばかり気にするのはよくありません。前例がないと行動できないのであれば、新しいものを世に送り出すことができません。前例がないからこそ、うまくいけば成功事例として注目されるわけです。

と誰よりも慎重派ビビリ属の僕が申し上げてしまいました。ただ「AI」ならばどうでしょうか。強化学習と呼ばれる手法により、失敗を恐れることなく試行錯誤を繰り返し、成功へと導いていきます。

では今回は、AI強化学習の手法を通して、新しいことへのチャレンジのコツを学んでいきましょう。

強化学習とは

強化学習」は、赤ちゃんが失敗を繰り返しながら歩き方を習得していくのと似てます。

www.youtube.com

こちらは、僕の大好きな「物理エンジンくん」さんの強化学習シミューレーション動画です。

  • 重心が前進する
  • 頭がある高さの幅にある

を「報酬」にして学習していますね。
強化学習を理解するには、「状態」「行動」「報酬」という3つの概念が重要です。

  1. 状態: 環境が今どのような状態か
  2. 行動: その状態での実際の行動
  3. 報酬: その行動によって獲得したスコア

赤ちゃんの歩き方習得を例にすると、「強化学習」では赤ちゃんのことを「エージェント」と言います。赤ちゃん(エージェント)は、まだ歩くことができない状態(=1. 状態)で歩こうとチャレンジし(=2. 行動)、それによってどれだけ歩けたか(=3. 報酬)がより大きくなるように頑張ります。

つまり、「強化学習」におけるエージェントは、「1. 状態」において何かしらの「2. 行動」を起こし、その行動から得られる「3. 報酬」を獲得するという処理を何度も行い、報酬の合計が一番大きくなるように学習していきます。

www.youtube.com

こちらは、もう一つの強化学習動画です。シュールですね。大好きです。

OpenAI Gymとは

さて、「物理エンジンくん」ほどのシミュレーションはできませんが、OpenAI Gymというオープンソース強化学習シミュレーション用プラットフォームを利用して、強化学習を試してみます。

山登りチャレンジ

OpenAI Gymの古典的な強化学習環境であるMountainCar-v0で山登りチャレンジをしましょう。

f:id:KenjiU:20200217185336g:plain
学習初期

2つの山の間にいる車(エージェント)が、前後に勢いをつけ、坂を登ろうとします。車が右の山の頂上(位置0.5)にあるゴールまで到達できたら、1エピソード終了となります。200ステップ内にゴールまで到達できなかった場合もタイムオーバーで1エビソード終了です。車は、速度が「0」、位置は「-0.6」〜「-0.4」のランダム位置から開始します。

報酬はステップ毎に「-1」(マイナスの報酬=罰)が与えられ、例えば200ステップ内にゴールできずタイムオーバーとなった場合は、トータル報酬は「-200」になります。強化学習は、このもらえる報酬ができるだけ大きくなるように行動する方法を学んでいきます。

今回は「Q学習」という強化学習アルゴリズムを使用します。

1. ライブラリと定数の定義

定数の中の「時間割引率」とは、すぐにもらえる小さい報酬を優先するか、将来にもらえる大きい報酬を優先するかを定義します。せっかち度ですね。「0」に近いほど将来の報酬は無視されて、「1」に近いほど将来の報酬が重視されます。

import gym
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
mpl.use('Agg') # matplotlibがAnti-Grain Geometry (AGG) を使う指定

# 定数
alpha = 0.2 # 学習係数
gamma = 0.99 # 時間割引率(0〜1)
epsilon = 0.002 # ε-グリーディ法のεの確率
num_divisions = 40 # 離散化時の分割数

2. 連続値から離散値に変換

連続値とは連続して無限に流れている「時間」のようなもの。また、離散値とは「人数」のように数えることができるものです。連続値である「位置」と「速度」を離散値に変換します。

def get_state(_observation):
    env_low = env.observation_space.low # 位置と速度の最小値
    env_high = env.observation_space.high # 位置と速度の最大値
    env_dx = (env_high - env_low) / num_divisions # 分割
    position = int((_observation[0] - env_low[0])/env_dx[0]) # 位置の離散値
    velocity = int((_observation[1] - env_low[1])/env_dx[1]) # 速度の離散値
    return position, velocity

3. Q関数を更新

経験(行動、状態、次の状態、報酬)に応じてQ関数を更新します。これにより、エージェントは時間の経過とともに、最適な行動をとれるようになります。

Q関数は以下のように表します。

Q(S_{t},a_{t})←Q(S_{t},a_{t})+α[r_{t+1}+γ\max_a Q(S_{t+1},a)-Q(S_{t},a_{t})]

  • S_{t}:時間tの時の状態
  • a_{t}:時間tの時の行動
  • α:学習係数
  • r_{t+1}S_{t+1}に遷移したときに得る報酬
  • γ:時間割引率
  • max_a Q(S_{t+1},a):次ステップの価値を最大化するQ関数
def update_q_table(_q_table, _action,  _observation, _next_observation, _reward, _episode):
    next_position, next_velocity = get_state(_next_observation)
    next_max_q_value = max(_q_table[next_position][next_velocity]) # 次ステップの価値を最大化するQ関数
    position, velocity = get_state(_observation)
    q_value = _q_table[position][velocity][_action]

    _q_table[position][velocity][_action] = q_value + alpha * (_reward + gamma * next_max_q_value - q_value)

    return _q_table

4. ε-グリーディ法

「ε-グリーディ法」という手法を用いて、現在の状態に応じた行動を選択するようにします。1-εの確率で最良の行動、εの確率でランダム行動を選択します。

現状に甘んじず、さらなる最適解を求めてランダムな行動を恐れることなくとるところが素敵ですね。

def get_action(_env, _q_table, _observation, _episode):
    if np.random.uniform(0, 1) > epsilon:
        position, velocity = get_state(observation)
        _action = np.argmax(_q_table[position][velocity])
    else:
        _action = np.random.choice([0, 1, 2])
    return _action

5. 動画保存

山登りチャレンジ結果を動画として保存します。

from JSAnimation.IPython_display import display_animation
from matplotlib import animation
from IPython.display import display

def display_frames_as_gif(frames):
    plt.figure(figsize=(frames[0].shape[1]/72.0, frames[0].shape[0]/72.0), dpi=72)
    patch = plt.imshow(frames[0])
    plt.axis('off')

    def animate(i):
       patch.set_data(frames[i])

    anim = animation.FuncAnimation(plt.gcf(), animate, frames=len(frames), interval=50)
    anim.save('movie_mountain_car.mp4')
    display(display_animation(anim, default_mode='loop'))

6. 学習実行

では、5000エピソードで学習を実行してみましょう。

if __name__ == '__main__':

    # 環境の生成
    env = gym.make('MountainCar-v0')

    # 初期化
    is_episode_final = False
    q_table = np.zeros((40, 40, 3))
    observation = env.reset()
    rewards = []
    history = []
    frames = []
    fig = plt.figure()

    # 学習
    for episode in range(5000):

        total_reward = 0
        observation = env.reset()
        
        if episode == 4999:
            is_episode_final = True

        for _ in range(200):
            # ε-グリーディ法で行動選択
            action = get_action(env, q_table, observation, episode)

            # 行動により、次の状態・報酬・ゲーム終了フラグを取得
            next_observation, reward, done, _ = env.step(action)

            # Q関数を更新
            q_table = update_q_table(q_table, action, observation, next_observation, reward, episode)
            total_reward += reward

            observation = next_observation

            # 最終エピソードの画面を描画
            if is_episode_final is True:
                frames.append(env.render(mode='rgb_array'))
            
            # ゲーム終了フラグがTrueになったら1エピソード終了
            if done:
                  if episode%100 == 0:
                      # ログ出力
                      print('episode: {}, total_reward: {}'.format(episode, total_reward))
                  rewards.append(total_reward)
                  history.append(total_reward)
                  plt.plot(history)
                  break

        if is_episode_final is True:
            # グラフを保存
            fig.savefig("img.png")
            # 動画を保存
            display_frames_as_gif(frames)

7. 実行結果

f:id:KenjiU:20200217185342g:plain
5000エピソード目

5000エピソード目を実行時の動画です。最初は全く登れる気配がなかったのに、強化学習の結果、悠々と登りきっています。

f:id:KenjiU:20200217184200p:plain
トータル報酬の推移

1〜5000エピソードのトータル報酬の推移グラフです。1000エピソードぐらいまでは、トータル報酬は「-200」となっており、200ステップ内で登りきれていませんが、1000エピソード以降はエピソードを積むにつれてより少ないステップで登れるようになっています。知的ですね。

最後に

現在、弊社も新規サービス立ち上げのために試行錯誤しております。強化学習のように、実施した結果をしっかりと評価、反省して次の行動につなげ、成功したとしてもその前例にとらわれず、更なる高みへ向けて果敢にチャレンジしていきたいと思います!

今回、参考にさせていただいた記事は以下です。ありがとうございました。
OpenAI Gym 入門 - Qiita
OpenAI Gym入門 / Q学習|npaka|note

AIのメリット・デメリット

映画「AI崩壊」が公開されましたね。早速観てきました。

AIとはどういうものなのか、またAIが日常生活に深く浸透してきた場合どのような問題が発生しうるか、がよく分かるかと思います。AI vs 人間ではなく、AIを取り巻く人間を中心に描かれており、AI時代に人間がなすべきことは何なのか、を考えさせられました。大沢たかおさん演じる桐生浩介の走りにも脱帽です。

これ以上は、ネタバレになってしまいますのでやめておきます。

さて、今回は、AIのメリット・デメリットについて書きます。僕たちも、AI製品の企画・開発を日々進めていますが、一度立ち止まって再認識したいと思います。

AIのメリット

①自動化による業務効率化

AIを活用することで、単調で同じ動作が繰り返される作業について、人間が実施するよりも早く正確にこなすことが可能になります。しかも、24時間365日動き続けることが可能です。

「RPA(ロボティック・プロセス・オートメーション)」が注目を集めて久しいですね。定型化できるPC業務について、手順を登録するだけで正確に自動処理を行うことができます。効率化のプロフェッショナルとも言えます。

ただ、いざRPAを導入したものの、十分に活用しきれないケースもあるようです。例えば、残業が多い部署の業務を自動化しようとRPAを導入したのに、残業の原因である業務が、人の判断、思考に依存するもの(例:顧客との関係性によって対応を変えなければならないもの)であれば意味がありません。

自動化できる業務を正しく見極める必要があります。

②安全性の向上

危険を伴う作業や、人間が立ち入ることができない場所での作業をAIに任せることができます。福島第一原子力発電所では、事故の安定化および廃炉の推進のために、ロボットが活用されています。

また、AIにより、道路のひび割れなどインフラの劣化や、機械の故障を自動的に検知することによって、事故を事前に防ぐことができ、適切なタイミングでメンテナンスを行うことができます。

転倒などの「ふらつき検知」、モノの「置き忘れ検知」など、個人の安全性に貢献する活用法も考えられています。

③創造的機会の創出

単調な作業をAIに置き換えることによって、人間はよりクリエイティブな作業や、人との高度なコミュニケーション力が求められている仕事に集中できる環境が生まれます。ただ、単純な作業とはいえ、いかにして効率的にこなすかを考えたり、やり遂げた後の達成感があったりするなど、AIに置き換えることで幸せかというとちょっと懐疑的です。また、以下のデメリットに記載していますが雇用の減少につながる可能性があります。

AIのデメリット

①雇用の減少

定型業務などがAIに置き換わることにより、人間が今まで行っていた業務量が減少し、雇用の減少がおきるのではと大きく懸念されています。 映画「AI崩壊」でも「AIに仕事が奪われる」ことを問題視しています。

②依存性

人間がAIに依存し、何事もAIの判断に委ねることになった場合、人間の判断能力は著しく低下してしまうでしょう。AIに意見を聞くぐらいの関係性が望ましいのかもしれません。

また、人間社会とAIが密接な関係になった場合、AIの管理にトラブルが発生すると、AIシステムと紐付けされている全ての環境に影響が出ることになります。もしも会社のメイン業務がAIと連携していれば、問題が解消するまで会社が機能しなくなる可能性もあります。そのため、万が一に備えたバックアップ体制を整えるなどのリスクマネジメントが重要となります。

③責任の所在

AIが問題を起こした場合、責任の所在をどうするか、という問題があります。「AIがそう判断したから」ではすまされません。法整備も必要ですが、判断、思考内容に透明性があり十分に検証可能なAIも求められます。

まとめ

今回は、AIについてのメリット・デメリットについてまとめました。

十分に検討が必要な課題もありますが、AIによって、確実に私たちの生活は便利になるでしょう。ただ、AIは自ら新しいものを生み出せません。いかにAIを使いこなし、私たちの生活を豊かで幸せなものにしていくかは人間次第です。これからは、AIのメリット、デメリットをよく理解して、うまく共存していくことが求められます。

あっと驚くサービスを弊社から出せるように頑張ります!

株式会社ブリスウェル

ロボットがすぐそばに

先日、池袋を歩いていたところ「ロボ酒場 期間限定オープン!」という看板を見かけました。

看板の写真をよく見てみると、工場などで見かけるロボットアームがマドラーを持っています。急いでいたので立ち寄ることはできませんでしたが
調べてみると・・・
池袋駅南口の養老乃瀧「一軒め酒場」内でAIロボットがお酒を提供してくれるとのことです。

しかも、お酒を作っている最中に顧客に話しかけてきて、顧客の反応を見て「いかにして笑わすことができるか」学んでいくとのこと。その場を和ませるユーモアさをどんどん鍛えていくロボット。うだつが上がらない漫才師?の僕もその学習能力を手に入れたいです。

【世界初】養老乃瀧がAIロボット酒場をオープン「ゼロ軒めロボ酒場」を体験してきた 省人化と笑顔の演出に QBITが開発 | ロボスタ

こちらのロボスタさんの記事には以下の動画もついています。

www.youtube.com

人間らしくはないけど、なぜか感情移入してしまいますね。ロボットというと、ついつい人間的なものを想像してしまいますが、このようにシンプルなものが方が親しみが出てくるような気がします。

ということで
今回は、前フリ通りに「ロボット」について、最近の技術、活用方法などについて書いていきます。よろしくお願いいたします。

ロボットビジョン

少子高齢化に伴い、労働力人口が減少しています。そのため、産業用ロボットを導入して労働力を確保し、生産性向上を図ろうとしている工場は少なくありません。

しかし、産業用ロボットを導入するためには、ロボットがどのように動くかプログラミングする(命を吹き込む)ティーチングが必要です。扱うワークを替える時にはカスタマイズも必要となってきます。経験者の力が必要です。

こうした課題を解決するために開発が進んでいるのが「ロボットビジョン」を搭載した産業用ロボットです。ロボットビジョンとは産業用ロボットの視覚機能で、カメラに映した画像を処理し、対象物を認識・判断したり、周囲の環境を把握したりする技術です。画像処理の結果でロボットに指示を出します。

ロボットビジョンを搭載したロボットは、ティーチングをはじめとした運用コスト削減につながるだけでなく、人間のように判断しながら作業を行うことができるので、より高度な作業が実現できます。

弊社では、画像認識処理の研究に力を入れており、今年はロボットビジョンにもチャレンジします!

協働ロボット

従来の「産業用ロボット」は「人間の代わり」に工場での作業を自動化することが主な用途でした。ただ、先日、製造業様の工場を見学させていただいたのですが、その時に見たロボットのあのスピード・正確さ・パワーを見ると、人間の代わりというより人間を遥かに超えているなと感じました。

近年では、安全柵なしで人間と一緒に作業を行うことが可能な「協働ロボット」に注目が集まっています。

以前は定格出力が80Wを超えるロボットを利用する場合、柵または囲い等を設ける規制がありました。しかし、2013年12月の規制緩和により、「ロボットメーカー、ユーザーが国際標準化機構(ISO)の定める産業用ロボットの規格に準じた措置を講じる」などの条件を満たせば、80W以上のロボットと人が同じ空間で働くことが可能になりました。この規制緩和により、国内で「協働ロボット」の開発が加速しています。

協働ロボットの登場により、今までロボット導入が難しかったところでも、人と協働してさまざまな作業ができるようになりました。

ちなみに、ロボ酒場のロボットも人間と溶け込み合う協働ロボットですね!

RaaS

SaaSとは「Software as a Service(サービスとしてのソフトウェア」の略です。僕たちソフトウェア業界では馴染み深い言葉です。

さて、RaaSをご存知でしょうか。「Robotics as a Service」の略です。ロボットも「所有」ではなく「利用」の時代になってきています。

pub.nikkan.co.jp

こちらの「機械設計2020年1月別冊 [雑誌:The ROBOT イノベーション×ビジネス]」に7つのサービスが掲載されています。

一番驚いたのは

自動収穫ロボットをRaaSモデルで提供し、農家の収入を倍増させる

の自動収穫ロボットサービスの記事でした。(ぜひ読んでみてください!)
このようなサービスがあれば、高齢化が進む農家でもリスクなく導入に踏み切ることができ、大いなる働き手になってくれるでしょう。新規就農者が増えることも期待されます。

ロボットの「手」の進化

ロボットは、「眼」(カメラやセンサー)と、見たものを認識・判断する「脳」(人工知能)の進化により、人間のように自律的に動くことができるようになってきました。しかし「手」についてはまだまだ進化の余地が残っています。

ロボットがモノを掴むためには、掴むモノに応じて最適なロボットハンドが必要となります。ロボットを生産ラインに組み込む際に毎回ロボットハンドを開発するとなると、時間的、金額的にもコストが大きくなってしまいます。そのため、人の手のような「高性能」でありながら「汎用性」を持つハンドが求められています。

2020年2月12日(水)~14日(金) 開催の「第4回 ロボデックス ~ロボット 開発・活用展~」でもロボットハンドの製品が多く出展するようです。

最後に

最近のロボット技術、活用方法などについてご紹介いたしました。いかがでしたでしょうか。

近い将来、家庭内にロボットがいるのが当たり前になったり、介護施設などでは人の相手をするロボットが普及するでしょう。2020年2月から成田空港第3ターミナルで警備ロボが巡回を始めます。人とロボットが協働して作り上げていく時代が、もう直ぐそこまで来ているのかもしれません。

株式会社ブリスウェル

顔認識ソフトウェア Face API

今年で最後となる大学入試センター試験が終わりましたね。受験生のみなさまお疲れ様でした。
1日目の倫理では「人間は人工知能(AI)に仕事を奪われると思うか」についての問題が出ました。
人工知能(AI)は

  • 計算できないこと
  • 統計処理ができないこと

を不得意とします。
つまり「クリエイティブ」なことが不得意です。

来年からは、思考力・判断力・表現力を一層重視する「大学入学共通テスト」に変わります。資料・データから必要な情報を読み取る力や、読み取った情報を比較・組み合わせて課題解決力を問うことを意識した問題も出題されそうです。

将来、AIは今ある仕事の多くを代替するようになるでしょう。しかし、AIが普及することによって、新たに必要となる仕事もあります。そのような仕事に臆せず飛び込むことができるチャレンジ力も大事です。

Face API

さて、話は変わって今回は、Microsoft Azureが提供しているFace APIで顔認識をしてみます。

Face APIは、人の顔を特定するだけでなく、感情認識ができます。人間でも場合によっては読み取ることが難しい感情の状態(幸福、悲しみ、怒り等)を取得することができます。顔認証での出退勤と感情認識を組み合わせて、社員の健康状態管理をするサービスなども出始めています。

また、Face APIは、年齢、性別、眼鏡の有無、ヒゲの有無、姿勢などの分析も行えます。

顔の登録

まずは、認識をする顔の登録をしていきましょう。

1. ライブラリと定数の定義
import json
import requests
import time
import httplib2
import os

BASE_URL = 'https://japaneast.api.cognitive.microsoft.com/face/v1.0/'
SUBSCRIPTION_KEY = 'サブスクリプションキー'
GROUP_ID = 'グループID'
2. Person Groupの作成

API Reference: PersonGroup - Create

# Person Groupの作成
def createPersonGroup():
    end_point = BASE_URL + 'persongroups/' + GROUP_NAME 
    headers = {
        'Content-Type': 'application/json',
        'Ocp-Apim-Subscription-Key': SUBSCRIPTION_KEY,
    }
    body = dict()
    body["name"] = "グループ名"
    body = str(body)
    
    result = requests.put(end_point, data=body, headers=headers)
    print("Person Group:" + str(json.loads(result.text)))
3. Personの作成

API Reference: PersonGroup Person - Create

# Personの作成
def createPerson(personName):
    end_point = BASE_URL + 'persongroups/' + GROUP_NAME + '/persons'
    result = requests.post(
        end_point,
        headers = {
            'Content-Type': 'application/json',
            'Ocp-Apim-Subscription-Key': SUBSCRIPTION_KEY
        },
        json = {
            'name': personName
        }
    )
    print("Person:" + str(json.loads(result.text)))
    personId = json.loads(result.text)['personId']
    return personId
4. Faceの追加

API Reference: PersonGroup Person - Add Face

# Faceの追加
def addDataFaceToPerson(personId, folder):
    valid_images = [".jpg",".gif",".png"]
    for f in os.listdir(folder):
        ext = os.path.splitext(f)[1]
        if ext.lower() not in valid_images:
            continue
        with open(os.path.join(folder,f), 'rb') as f:
            img = f.read()
            addFaceImage(personId, img)
            f.close()

def addFaceImage(personId, img):
    end_point = BASE_URL + 'persongroups/' + GROUP_ID + '/persons/' + personId  + '/persistedFaces'
    result = requests.post(
        end_point,
        headers = {
            'Content-Type': 'application/octet-stream',
            'Ocp-Apim-Subscription-Key': SUBSCRIPTION_KEY
        },
        data=img
    )
    print("Face:" + str(json.loads(result.text)))
5. 学習の実施

API Reference: PersonGroup - Train

# 学習の実施
def trainGroup():
    end_point = BASE_URL + 'persongroups/' + GROUP_ID + '/train'
    result = requests.post(
        end_point,
        headers = {
            'Ocp-Apim-Subscription-Key': SUBSCRIPTION_KEY
        },
        json = {
            'personGroupId': GROUP_ID
        }
    )
    print("Train:" + str(result))
6. 実行

f:id:KenjiU:20200119192024p:plain

今回も弊社の取締役(陽治郎さん)の写真を使わせていただきます!
3枚程度学習すれば認識ができるようになります。

if __name__ == '__main__':
    # Person Groupの作成
    createPersonGroup()
    # Personの作成
    personId = createPerson('Yojiro')
    # Faceの追加
    addDataFaceToPerson(personId, os.path.join("data", "Yojiro"))
    time.sleep(5)
    # 学習の実施
    trainGroup()

顔の認識

次は顔の認識をしてみましょう。

1. ライブラリと定数の定義
import json
import requests
import os

BASE_URL = 'https://japaneast.api.cognitive.microsoft.com/face/v1.0/'
SUBSCRIPTION_KEY = 'サブスクリプションキー'
GROUP_ID = 'グループID'
2. 顔検出

API Reference: Face - Detect

# 顔検出
def detectFaceImage(imgPath):
    with open(imgPath, 'rb') as f:
        img = f.read()
    params = {
        'returnFaceId': 'true',
        'returnFaceLandmarks': 'false',
        'returnFaceAttributes': 'age,blur,emotion,exposure,facialHair,gender,glasses,hair,headPose,makeup,noise,occlusion,smile'
    }
    end_point = BASE_URL + "detect"
    result = requests.post(
        end_point,
        params=params,
        headers = {
            'Content-Type': 'application/octet-stream',
            'Ocp-Apim-Subscription-Key': SUBSCRIPTION_KEY
        },
        data=img
    )
    detect = json.loads(result.text)
    print("DetectFace:" + str(detect))
    return detect
3. 顔特定

API Reference: Face - Identify

# 顔特定
def identifyPerson(faceId):
    end_point = BASE_URL + 'identify'
    faceIds = [faceId]
    result = requests.post(
        end_point,
        headers = {
            'Ocp-Apim-Subscription-Key': SUBSCRIPTION_KEY
        },
        json = {
            'faceIds': faceIds,
            'personGroupId': GROUP_ID
        }
    )
    candidates = json.loads(result.text)[0]['candidates']
    return candidates
4. 名前取得

API Reference: PersonGroup Person - List

# 名前取得
def getPersonNameByPersonId(personId):
    end_point = BASE_URL + 'persongroups/' + GROUP_ID + '/persons'
    result = requests.get(
        end_point,
        headers = {
        "Ocp-Apim-Subscription-Key": SUBSCRIPTION_KEY
        },
        json = {
            'personGroupId': GROUP_ID
        }
    )
    persons = json.loads(result.text)
    for person in persons:
        if person['personId'] == personId:
            return person['name']
5. 実行

f:id:KenjiU:20200119195551j:plain
Yojiro-test.jpg

こちらの陽治郎さんの顔写真を認識してみます。

if __name__ == '__main__':
    image = os.path.join("test", "Yojiro-test.jpg")
    # 顔検出
    detect = detectFaceImage(image)
    if len(detect) > 0:
        for per in detect:
            detectedFaceId = per['faceId']
            emotion = per['faceAttributes']['emotion']
            # 顔特定
            identifiedPerson = identifyPerson(detectedFaceId)
            if len(identifiedPerson) > 0 and identifiedPerson[0]['personId']:
                personId = identifiedPerson[0]['personId']
                # 名前取得
                personName = getPersonNameByPersonId(personId)
                print("PersonName:" + str(personName))
            else:
                print("PersonName:Unknown")            
    else:
        print("Nobody")

実行結果は以下の通りです。

DetectFace:[{'faceId': 'eba5d6ab-cb19-4229-8534-dea32b55f8d9', 'faceRectangle': {'top': 45, 'left': 47, 'width': 57, 'height': 57}, 'faceAttributes': {'smile': 1.0, 'headPose': {'pitch': -2.9, 'roll': 3.4, 'yaw': -15.0}, 'gender': 'male', 'age': 34.0, 'facialHair': {'moustache': 0.1, 'beard': 0.1, 'sideburns': 0.1}, 'glasses': 'NoGlasses', 'emotion': {'anger': 0.0, 'contempt': 0.0, 'disgust': 0.0, 'fear': 0.0, 'happiness': 1.0, 'neutral': 0.0, 'sadness': 0.0, 'surprise': 0.0}, 'blur': {'blurLevel': 'low', 'value': 0.15}, 'exposure': {'exposureLevel': 'goodExposure', 'value': 0.72}, 'noise': {'noiseLevel': 'low', 'value': 0.0}, 'makeup': {'eyeMakeup': False, 'lipMakeup': False}, 'occlusion': {'foreheadOccluded': False, 'eyeOccluded': False, 'mouthOccluded': False}, 'hair': {'bald': 0.09, 'invisible': False, 'hairColor': [{'color': 'black', 'confidence': 0.99}, {'color': 'brown', 'confidence': 0.99}, {'color': 'gray', 'confidence': 0.29}, {'color': 'other', 'confidence': 0.09}, {'color': 'red', 'confidence': 0.03}, {'color': 'blond', 'confidence': 0.03}]}}}]

1個1個属性を見ていきましょう。

  1. 年齢(Age): 推定年齢
    'age': 34.0

  2. ぼかし(Blur): 顔のぼかしの程度
    'blur': {'blurLevel': 'low', 'value': 0.15}

  3. 感情(Emotion): 感情値
    'emotion': {'anger': 0.0, 'contempt': 0.0, 'disgust': 0.0, 'fear': 0.0, 'happiness': 1.0, 'neutral': 0.0, 'sadness': 0.0, 'surprise': 0.0}

  4. 露出(Exposure): 画像内の顔の露出の程度
    'exposure': {'exposureLevel': 'goodExposure', 'value': 0.72}

  5. 顔ひげ(Facial hair): 顔ひげの有無と長さ
    'facialHair': {'moustache': 0.1, 'beard': 0.1, 'sideburns': 0.1}

  6. 性別(Gender): 推定される性別
    'gender': 'male'

  7. 眼鏡(Glasses): 眼鏡があるかどうか
    'glasses': 'NoGlasses'

  8. 髪の毛(Hair): 髪質と髪色
    'hair': {'bald': 0.09, 'invisible': False, 'hairColor': [{'color': 'black', 'confidence': 0.99}, {'color': 'brown', 'confidence': 0.99}, {'color': 'gray', 'confidence': 0.29}, {'color': 'other', 'confidence': 0.09}, {'color': 'red', 'confidence': 0.03}, {'color': 'blond', 'confidence': 0.03}]}

  9. 頭部姿勢(Head pose): 3 次元空間での顔の向き
    'headPose': {'pitch': -2.9, 'roll': 3.4, 'yaw': -15.0}

  10. 化粧(Makeup): 化粧があるかどうか
    'makeup': {'eyeMakeup': False, 'lipMakeup': False}

  11. ノイズ(Noise): 顔の画像で検出された視覚ノイズ
    'noise': {'noiseLevel': 'low', 'value': 0.0}

  12. オクルージョン(Occlusion): 顔のパーツをブロックするオブジェクトがあるかどうか
    'occlusion': {'foreheadOccluded': False, 'eyeOccluded': False, 'mouthOccluded': False}

  13. 笑顔(Smile): 笑顔表現
    'smile': 1.0

笑顔100点満点ですね!

PersonName: Yojiro

顔の認識結果も「Yojiro」となり、正しく認識できています。

変装チャレンジ

ここからは、陽治郎さんすみません。陽治郎さんを色々と変装させてみます。
正しく認識できるでしょうか。

かぶりもの

f:id:KenjiU:20200119201509j:plain
Yojiro-test-1.jpg

PersonName: Yojiro

正しく認識できています。

f:id:KenjiU:20200119201520j:plain
Yojiro-test-2.jpg

PersonName: Yojiro

こちらも正しく認識できています。

ひげ

f:id:KenjiU:20200119201532j:plain
Yojiro-test-3.jpg

PersonName: Unknown

残念ながら、認識できませんでした。

'facialHair': {'moustache': 0.6, 'beard': 0.6, 'sideburns': 0.4}

顔ひげの属性値が高くなっています。

眼鏡

f:id:KenjiU:20200119203905j:plain
Yojiro-test-4.jpg

PersonName: Unknown

こちらも、認識できませんでした。

 'glasses': 'Sunglasses'

サングラスは認識しています。

ジョーカー

f:id:KenjiU:20200119201609j:plain
Yojiro-test-5.jpg

Nobody

もはや顔として認識されませんでした。

Face API の価格

Face APIにはFreeインスタンスとStandardインスタンスがあります。Freeインスタンスは無料で使えますが、トランザクション数は毎月3万回までに制限されます。Standardインスタンストランザクション数の制限はなくなりますが、トランザクション数が増えるにつれて料金が高くなります。お試しで使いたいときはFreeインスタンスを選ぶことがおすすめです。

以上です。
ありがとうございました!

株式会社ブリスウェル

Kaggle チャレンジ

正月なまりを吹っ飛ばすべく、今回はKaggleチャレンジをします。

Kaggleとは

世界中のデータサイエンティスト達がデータ分析の腕を競い合うプラットフォームです。企業や研究者が提供するデータセットに対し、適切な予測モデルを構築しその予測精度を競います。上位入賞者には賞金が出ます。
様々なデータセットが手に入り、他の競技者の解説(カーネル)を見ることができるので、データ分析の勉強にも非常に有用です。

Kaggleを始める方法

  1. Kaggleのサイトに接続してアカウントを作成します。
  2. Jupyter Notebook(Webブラウザで動作するpythonの対話環境)をインストールします。
  3. コンペに参加

これだけです!

住宅価格予測

今回は、Kaggleの回帰問題のチュートリアルである「House Prices: Advanced Regression Techniques」をやってみます。(cybozu02さんのカーネルを参考にしております)

www.kaggle.com

1. ライブラリの準備

今回使用するライブラリをインポートします。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.preprocessing import PowerTransformer
  • numpy: 数値計算を効率的に行うための拡張モジュール
  • pandas: データ解析を支援する機能を提供するライブラリ
  • matplotlib: グラフ描画ライブラリ
  • seaborn: 可視化ライブラリ
  • PowerTransformer: 0や負値を含んだ変数も対数変換ができるライブラリ
2. データの読み込み

こちらのリンクから以下のデータセットをダウンロードして読み込みます。

  • train.csv: 学習データ
  • test.csv: テストデータ
#pandasを使用してcsvデータ読み込み
train = pd.read_csv("train.csv")
test_x = pd.read_csv("test.csv")

#学習データの特徴量を出力
train.columns

f:id:KenjiU:20200114235255p:plain

学習データの特徴量は79個あります。

  • MSSubClass: 住居のタイプ
  • MSZoning: 住居のゾーン
  • LotFrontage: 隣接した道路の長さ
  • LotArea: 敷地面積
  • Street: 道路のタイプ
  • Alley: 路地のタイプ
  • LotShape: 土地の形状
  • LandContour: 土地の平坦さ
  • Utilities: ガス・電気・水の利用
  • LotConfig: 土地の構成
  • LandSlope:土地の傾斜
  • Neighborhood: 近所

等です。
実践さながらのデータでわくわくしてきますね!

3. データ分析

まずは、予測対象の変数(目的変数)である「SalesPrice(住宅価格)」について確認します。

plt.hist(train_saleprice, bins=100)
plt.show()

f:id:KenjiU:20200114235813p:plain

左に偏っており正規分布になっていません。機械学習において目的変数が正規分布に従ったほうが精度が良くなる、ということが知られているので後ほど正規分布に変換します。

次に、特徴量を確認します。

  • TotalBsmtSF: 地下室の広さ
  • 1stFlrSF: 1階の広さ
  • GrLivArea: リビングの広さ
  • GarageArea: 車庫の広さ
  • LotArea: 土地の広さ

の広さが、住宅価格に影響するのでは、と考えます。
相関関係を見てみましょう。

#地下室の広さと住宅価格の散布図を作成
data = pd.concat([train["TotalBsmtSF"],train["SalePrice"]],axis=1)
plt.figure(figsize=(20, 10))
plt.scatter(train["TotalBsmtSF"],train["SalePrice"])
plt.xlabel("TotalBsmtSF")
plt.ylabel("SalePrice")

#1階の広さと住宅価格の散布図を作成
data = pd.concat([train["1stFlrSF"],train["SalePrice"]],axis=1)
plt.figure(figsize=(20, 10))
plt.scatter(train["1stFlrSF"],train["SalePrice"])
plt.xlabel("1stFlrSF")
plt.ylabel("SalePrice")

#リビングの広さと住宅価格の散布図を作成
data = pd.concat([train["GrLivArea"],train["SalePrice"]],axis=1)
plt.figure(figsize=(20, 10))
plt.scatter(train["GrLivArea"],train["SalePrice"])
plt.xlabel("GrLivArea")
plt.ylabel("SalePrice")

#車庫の広さと住宅価格の散布図を作成
data = pd.concat([train["GarageArea"],train["SalePrice"]],axis=1)
plt.figure(figsize=(20, 10))
plt.scatter(train["GarageArea"],train["SalePrice"])
plt.xlabel("GarageArea")
plt.ylabel("SalePrice")

#土地の広さと住宅価格の散布図を作成
data = pd.concat([train["LotArea"],train["SalePrice"]],axis=1)
plt.figure(figsize=(20, 10))
plt.scatter(train["LotArea"],train["SalePrice"])
plt.xlabel("LotArea")
plt.ylabel("SalePrice")

f:id:KenjiU:20200115000607p:plain f:id:KenjiU:20200115000613p:plain f:id:KenjiU:20200115000628p:plain f:id:KenjiU:20200115000632p:plain f:id:KenjiU:20200115000647p:plain

広くなるにつれて住宅価格が上昇する傾向が見られますね。外れ値がいくつかあるので除外しておきます。

#外れ値を除外
train = train.drop(train[(train['TotalBsmtSF']>6000) & (train['SalePrice']<200000)].index)
train = train.drop(train[(train['1stFlrSF']>4000) & (train['SalePrice']<200000)].index)
train = train.drop(train[(train['GrLivArea']>4000) & (train['SalePrice']<200000)].index)
train = train.drop(train[(train['GarageArea']>1200) & (train['SalePrice']<300000)].index)
train = train.drop(train[(train['LotArea']>80000) & (train['SalePrice']<400000)].index)
4. 特徴量の作成

より良い特徴量を作成する特徴量エンジニアリングを進めていきます。

目的変数(住宅価格)を学習データから切り出します。また、学習データとテストデータをまとめて処理するため一旦マージしておきます。

#学習データを目的変数(住宅価格)とそれ以外に分ける
train_x = train.drop("SalePrice",axis=1)
train_y = train["SalePrice"]

#学習データとテストデータをマージ
all_data = pd.concat([train_x,test_x],axis=0,sort=True)

#IDのカラムは学習に不要なので別の変数に格納
train_ID = train['Id']
test_ID = test_x['Id']

all_data.drop("Id", axis = 1, inplace = True)
5. 欠損値の処理

分析に利用するデータには多くの場合、何らかの理由により記録されなかった値、欠損値が含まれます。欠損値があると統計処理ができなくなるので、削除や0埋め等が必要です。まずは欠損値がどのくらいあるのかを確認します。

#データの欠損値
all_data_na = all_data.isnull().sum()[all_data.isnull().sum()>0].sort_values(ascending=False)

#欠損値の数をグラフ化
plt.figure(figsize=(20,10))
plt.xticks(rotation='90')
sns.barplot(x=all_data_na.index, y=all_data_na)

#欠損値があるカラムをリスト化
na_col_list = all_data.isnull().sum()[all_data.isnull().sum()>0].index.tolist()
all_data[na_col_list].dtypes.sort_values()

f:id:KenjiU:20200115002401p:plain

  • PoolQC: プールの質を表す。プールがない場合にはNAとなる。
  • MiscFeature: 備え付けられている特別な設備(エレベータ等)を表す。何もない場合はNAとなる。
  • Alley: 物件にアクセスするための路地のタイプを表す。該当しない場合はNAとなる。
  • Fence: 敷地などを仕切る囲いの質を表す。囲いがない場合はNAとなる。
  • FireplaceQu: 暖炉の質を表す。暖炉がない場合はNAとなる。

が多く欠損しています。
対象物そのものが存在しない場合、欠損として扱われているようです。

  • float型の場合は「0」
  • object型の場合は「None」

で置換することにしましょう。

物件に隣接した道路の長さ(LotFrontage)については平均値で穴埋めします。

#物件に隣接した道路の長さ(LotFrontage)の欠損値を平均値で穴埋め
all_data['LotFrontage'] = all_data['LotFrontage'].fillna(all_data['LotFrontage'].median())

#欠損値が存在する、かつfloat型の場合は「0」で置換
float_list = all_data[na_col_list].dtypes[all_data[na_col_list].dtypes == "float64"].index.tolist()
all_data[float_list] = all_data[float_list].fillna(0)

#欠損値が存在する、かつobject型の場合は「None」で置換
obj_list = all_data[na_col_list].dtypes[all_data[na_col_list].dtypes == "object"].index.tolist()
all_data[obj_list] = all_data[obj_list].fillna("None")
6. 数値変数の処理
  • MSSubClass: 住宅の種類
  • YrSold: 販売年
  • MoSold: 販売月

については、数値ですが、数や量で測れない変数なので、カテゴリ変数として扱うことにします。

#カテゴリ変数に変換する
all_data['MSSubClass'] = all_data['MSSubClass'].apply(str)
all_data['YrSold'] = all_data['YrSold'].astype(str)
all_data['MoSold'] = all_data['MoSold'].astype(str)
7. 目的変数を対数変換

目的変数(住宅価格)が正規分布になっていないため、対数変換をすることで正規分布にします。

#目的変数を対数変換する
train_y = np.log1p(train_y)

#可視化
plt.figure(figsize=(20, 10))
sns.distplot(train_y)

f:id:KenjiU:20200115003749p:plain

8. 説明変数を対数変換

説明変数についても正規分布になっていない(歪度が0.5よりも大きい)ものは対数変換していきます。0や負値を含んだ変数も対数変換ができる「Yeo-Johnson変換」を使用します。

#数値の説明変数のリスト
num_feats = all_data.dtypes[all_data.dtypes != "object" ].index

#各説明変数の歪度を計算
skewed_feats = all_data[num_feats].apply(lambda x: x.skew()).sort_values(ascending = False)

#歪度の絶対値が0.5より大きい変数だけに絞る
skewed_feats_over = skewed_feats[abs(skewed_feats) > 0.5].index

#Yeo-Johnson変換
pt = PowerTransformer()
pt.fit(all_data[skewed_feats_over])

#変換後のデータで各列を置換
all_data[skewed_feats_over] = pt.transform(all_data[skewed_feats_over])
9. 特徴量の追加

新しい特徴量を追加することで、予測モデルの性能が向上することがあります。以下の特徴量を追加します。

#プールの有無(広さが0より大きい場合は有)
all_data['haspool'] = all_data['PoolArea'].apply(lambda x: 1 if x > 0 else 0)

#2階の有無(広さが0より大きい場合は有)
all_data['has2ndfloor'] = all_data['2ndFlrSF'].apply(lambda x: 1 if x > 0 else 0)

#ガレージの有無(広さが0より大きい場合は有)
all_data['hasgarage'] = all_data['GarageArea'].apply(lambda x: 1 if x > 0 else 0)

#地下室の有無(広さが0より大きい場合は有)
all_data['hasbsmt'] = all_data['TotalBsmtSF'].apply(lambda x: 1 if x > 0 else 0)

#暖炉の有無
all_data['hasfireplace'] = all_data['Fireplaces'].apply(lambda x: 1 if x > 0 else 0)
10. カテゴリ変数の処理

カテゴリ変数を、One-Hotエンコーディングにより「0」と「1」だけの数列に変換します。

#カテゴリ変数となっているカラム
cal_list = all_data.dtypes[all_data.dtypes=="object"].index.tolist()

#pandasのget_dummies関数でOne-Hotエンコーディング
all_data = pd.get_dummies(all_data,columns=cal_list)
11. データの分割

マージした学習データとテストデータを再度分割します。

#学習データとテストデータに再分割
train_x = all_data.iloc[:train_x.shape[0],:].reset_index(drop=True)
test_x = all_data.iloc[train_x.shape[0]:,:].reset_index(drop=True)
12. 予測モデルの作成&実行

勾配ブースティング(アンサンブル学習と決定木を組み合わせた手法)のライブラリ「XGBoost」でモデルを作成します。

  • max_depth: 木の深さの最大値
  • eta: 学習率を調整
  • objective: 損失関数を指定

等のハイパーパラメータについて、設定値を色々試してみたいですね。

from sklearn.ensemble import RandomForestRegressor,  GradientBoostingRegressor
from sklearn.model_selection import KFold, cross_val_score, train_test_split
from sklearn.metrics import mean_squared_error
import xgboost as xgb
from sklearn.model_selection import GridSearchCV

#データの分割
train_x, valid_x, train_y, valid_y = train_test_split(
        train_x,
        train_y,
        test_size=0.3,
        random_state=0)

#特徴量と目的変数をxgboostのデータ構造に変換する
dtrain = xgb.DMatrix(train_x, label=train_y)
dvalid = xgb.DMatrix(valid_x,label=valid_y)

#パラメータを指定して勾配ブースティング
num_round = 5000
evallist = [(dvalid, 'eval'), (dtrain, 'train')]

evals_result = {}

#パラメータ
param = {
    'max_depth': 3,
    'eta': 0.01,
    'objective': 'reg:squarederror',
}

#学習の実行
bst = xgb.train(
    param, dtrain,
    num_round,
    evallist,
    evals_result=evals_result,
)    
#学習曲線を可視化する
plt.figure(figsize=(20, 10))
train_metric = evals_result['train']['rmse']
plt.plot(train_metric, label='train rmse')
eval_metric = evals_result['eval']['rmse']
plt.plot(eval_metric, label='eval rmse')
plt.grid()
plt.legend()
plt.xlabel('rounds')
plt.ylabel('rmse')
plt.ylim(0, 0.3)
plt.show()

f:id:KenjiU:20200115012809p:plain

13. 提出用csvファイルの作成

いよいよ最後です。予測結果をcsvファイルに出力します。予測結果のSalePrice(住宅価格)は対数をとった値なので、Exponentialをかけてあげる必要があります。

#予測結果を出力
dtest = xgb.DMatrix(test_x)
my_submission = pd.DataFrame()
my_submission["Id"] = test_ID
my_submission["SalePrice"] = np.exp(bst.predict(dtest))
my_submission.to_csv('submission.csv', index=False)

f:id:KenjiU:20200115015847p:plain

1998位/5343人でした!

以下は今回参考にさせていただいたcybozu02さんのカーネルです。ありがとうございました。 www.kaggle.com