Briswell Tech Blog

ブリスウェルのテックブログです

システム開発プロジェクト成功の秘訣 ー 提案編 ー

東京は梅雨入りしてジメジメしてますが、いかがお過ごしでしょうか?

新型コロナの感染者もまだまだ収束していないため、ブリスウェルではリモートワークを継続しているメンバーも多いです。

さて、今日はシステム開発プロジェクト成功の秘訣と題して、書かせていただきます。

私自身、システム開発の仕事に携わって20年以上経ちますが、とても難しく奥深い仕事だと未だに感じています。

今までの経験から、システム開発を成功させるために重要だと考えていることについて、提案編と題して私の考え方をご紹介させていただきます。

ブリスウェルの主力事業はシステム開発サービスで、お客様からシステム構築の依頼を受けて、システムを開発して納品する仕事をしています。

システム開発を外部の会社に委託する場合、発注サイドと受注サイドがいて、システムの完成まで両者が協力し合い、完成後もより良いものに育てていく必要があります。

単に売って終わりというわけではなく、契約後も協力してアウトプットを創り出していく必要のあるビジネスです。

ブリスウェルのような受注者サイドとしては、

・お客様のビジネスや業務の流れ

・既存システムの仕組みやデータの流れ

・関連する外部システム

など様々な情報が不足した状態でお客様との最初のコンタクトとなることがあります。

そのような状態でもなんとか競合他社との戦いを勝ち抜き発注をいただくために色々と努力をすることになります。

受注サイドがどのような観点で提案を作成していくのかという情報は、発注サイドにとっても良い開発会社を選定する上での助けになるのではないかと思っています。

それでは具体的に見て行きましょう。

1)お客様の見えない要求を嗅ぎ分ける

f:id:yamagoochi:20200622130445j:plain
見えない要求を嗅ぎ分ける

ヒアリングやRFPを元に、過去の経験から機能一覧を推測で策定する

この能力が会社の提案力ということになると思います

全体の流れを押さえるためのQAや詳細なQAを繰り返しながら、お客様のビジネスや業務、既存システムや外部連携システムとの連携の仕組みなどを想像していきます

限られた時間で100%を把握することは不可能なので、根拠を持った上で必要だと思われる全ての機能を洗い出すことが重要です

2)要求を数字に変換する

f:id:yamagoochi:20200622130552j:plain
要求を数字に変換する

重要なのは画面数、機能数、帳票数、IF数など数字や難易度に基づいて工数を見積ることです

お客様からのハイレベルな要求を具体的な数値レベルに落とし込めることがシステム開発を請け負う上でとても大切な能力だと考えています

ふわっとした営業トークではなく、具体的に完成形をイメージできるレベルまで詳細化できることが付加価値だと思っています

また、お客様になぜそのような実装方針なのかを質問された場合に、根拠をすぐに回答できる状態であることがとても大切です

3)工数と金額を明示する

f:id:yamagoochi:20200622130623j:plain
工数と金額を明示する

開発規模から考えられるリスクを考慮した見積を算出する

ある程度機能単位での工数や費用を提示することで、価格感をお客様と共有できることになります

これはその後のスコープ変更時やリリース後の追加改修時の見積根拠が発注者と共有できるため無用な価格交渉や不信感を排除できると考えています

逆に言うと、不明確な部分が多い場合は開発会社側はある程度リスクを積むことになります

発注者側はできるだけ詳細な情報を提供して見積金額のバラツキが出にくくなるように配慮することがとても大切です

一定規模以上の案件の場合、リスク要因を把握しきれないことが多いため、要件定義終了後に開発フェーズ以降を再提案する旨を合意した方が良いと考えています

4)差別化

f:id:yamagoochi:20200622130647j:plain
差別化

提案時には自分たちが差別化できるポイントをセールストークの中心に据えることが重要です

顧客のことを深く理解することができることが伝わるととても良いですし、逆に顧客に懸念されているポイントを強みに変えられるとアピール力が高まります

例えば弊社では、あるプロジェクトの提案の際に以下の3つのポイントを差別化ポイントとして提案をしたことがあります

1 豊富な過去の経験(大規模プロジェクト実績や発注者側のプロジェクトリーダー経験がある)

 ⇒発注者の立場を理解できるという安心感

2 業界向けパッケージの活用(業界ノウハウがある)

 ⇒担当者の要求を正確に吸い上げられるという信頼感

3 オフショア開発(徹底した品質管理体制)

 ⇒ただ安いだけではなく品質もしっかりしているという安心感

お客様やプロジェクトの内容によってセールスポイントの中心をどこに持っているかは変わりますが、基本的な部分は共通していると考えています

5)プロジェクトリーダーが全てを背負えること

f:id:yamagoochi:20200622130712j:plain
プロジェクトリーダーが全てを背負えること

最終的にお客様がパートナー選定という大きな決断をする場合、やはり信頼できる人なのかどうかがとても重要だと感じています

いかなる質問にも誠実に回答する姿勢がとても大切です

金額交渉においても上司にお伺いを立てるような姿勢はできるだけ控え、オーナーシップを発揮できることを示した方が信頼感がアップすると思います

プロジェクトリーダーとしてオーナーシップを発揮できるレベルになるには相当の努力と経験と覚悟が必要です (まだまだ私自身も一人前とは言えないのですが)

そういう人が世の中に増えると良いな そのきっかけをブリスウェルで作れると良いな と思いながら経営をしています

また、業界的にはとても重要な感覚ではあるのですが、リスク回避的な態度はできるだけ見せない方が良いと思っています

とはいえ、お客様とリスクの存在を共有し、できればリスクに対する対策案を提示できると尚良いです

以上がシステム開発案件の提案を行う際に、私が大切にしていることになります

いよいよプロジェクトがスタートした後の流れ、要件定義編についても近いうちに記事を書こうと思っています

皆様のお役に少しでも立つことができたら幸いです!

音声データのノイズ除去

時間や場所にとらわれずに柔軟な働き方を実現する「Web会議」。すっかり身近になりましたね。

Web会議は、必要な時にその場ですぐに会議を開催することができるので、情報共有や意思決定を迅速に行うことができます。日程調整もしやすいので、参加者の予定を合わせやすいですね。ただ、ブレストのような自由に意見を出し合う類いの打合せは、ちょっと向いてないかなと最近感じます。座ったままPCと睨めっこ状態だと、なかなか良いアイデアも生まれてきません。これも慣れなのかしら。

ここでWeb会議あるある、です。

1. ミュートしたまま話続ける
相槌や意見を言い続けたのに、ミュートだったと気付いた時は虚しさ半端ないです。

2. 思わぬ来客
宅配便でーす!ご飯まだー?にゃあ。
日常が見えて、微笑ましいですね。

3. 雑音トラブル
カタカタカタ…ガタンゴトンガタンゴトン…どんがらがっしゃーん!
なかなか自分では気付かないことが多いです。

などなど...
どうでしょう。みなさんも思い当たるふしがあるかと思います。

さて、今回は「3. 雑音トラブル」をPythonで解決してみましょう!

ノイズ除去処理の流れ

  1. 高速フーリエ変換FFT)を用いて、ノイズデータの周波数成分を取得。あるアルゴリズムにより、ノイズと見なすしきい値を計算する。

  2. 高速フーリエ変換FFT)を用いて、ノイズを含む音声データの周波数成分を取得。1のしきい値によりノイズの部分をマスクする。

  3. 音声として復元する。

が大まかな処理の流れとなります。 pypi.org 今回は、こちらの「noisereduce」ライブラリを使用します。簡単にノイズ除去を試すことができます。

1. ライブラリの定義

import IPython
from scipy.io import wavfile
import noisereduce as nr
import soundfile as sf
from noisereduce.generate_noise import band_limited_noise
import matplotlib.pyplot as plt

2. 音声データの読み込み

data, rate = sf.read('voice.wav')
fig, ax = plt.subplots(figsize=(20,3))
ax.plot(data)

f:id:KenjiU:20200607215236p:plain
音声データ

3. ノイズデータの読み込み

noise_data, noise_rate = sf.read('noise.wav')
IPython.display.Audio(data=noise_data, rate=noise_rate)

fig, ax = plt.subplots(figsize=(20,4))
ax.plot(noise_data)

f:id:KenjiU:20200607215240p:plain
ノイズデータ

4. ノイズを含んだ音声データを生成

snr = 2 # signal to noise ratio
noise_clip = noise_data/snr
audio_clip = data + noise_clip
IPython.display.Audio(data=audio_clip, rate=noise_rate)

fig, ax = plt.subplots(figsize=(20,4))
ax.plot(audio_clip)

f:id:KenjiU:20200607215247p:plain
ノイズを含んだ音声データ

5. ノイズを除去し音声として復元

なんと以下のたったの一行でノイズ除去が実現できてしまいます。

noise_reduced = nr.reduce_noise(audio_clip=audio_clip, noise_clip=noise_clip, verbose=True)

f:id:KenjiU:20200607215254p:plain
ノイズ
f:id:KenjiU:20200607215257p:plain
ノイズと見なすしきい値を計算
f:id:KenjiU:20200607215312p:plain
入力音声データ
f:id:KenjiU:20200607215318p:plain
ノイズの部分をマスク
f:id:KenjiU:20200607215333p:plain
音声として復元

IPython.display.Audio(data=noise_reduced, rate=rate)

どうでしょう。完璧とは言えませんが、雑音トラブル無事解決!?

fig, ax = plt.subplots(figsize=(20,3))
ax.plot(noise_reduced)

f:id:KenjiU:20200607215336p:plain
ノイズ除去済み音声データ

今回は以上です!
1/fゆらぎのBGMを聞いていたら眠くなってきました( *˘ω˘)スヤァ…

Elastic Beanstalk config ファイルでどハマりしたお話

お疲れ様です。
みなさまいかがお過ごしでしょうか。

コロナウィルスにより、大変な世の中になってしまい、今後どうなっていくのか不安に思っています。

弊社は2月末よりテレワークとなりました。
一部は出社している日もあるようですが、基本的にはもうみんなの顔を忘れている頃だと思います。
私はお家時間が増えたので、FODを契約し、のだめカンタービレのドラマと映画を一気にみました。
今クラシックを聞きながら書いています。
一曲もわかりません。

本題です。
私は2 ~ 3年ほど前からElasticBeanstalk + CircleCI or GitLabCI or GitHubAction という構成を多く使用しています。
色々な構成内容をconfigファイルに記載しておき、
developとproductionをブランチによって自動で切り替えたり、環境構築を人依存しないようにしております。

近頃スタートしたプロジェクトの開発環境を構築しようと、以前のプロジェクトから色々コピーしました。
ハマりました。
今回はNode.js + nginx 環境です。

http → https のリダイレクトを nginx で設定しています。

files:
 /etc/nginx/conf.d/redirect.conf:
 mode: "000644"
 owner: root
 group: root
 content: |
# Redirect HTTP To HTTPS
 server {
 listen 81;
 rewrite ^ https://$host$request_uri permanent;
 }

こんな感じのファイルを
.ebextensions/redirect.config

として置いておけば、あとはElastic Beanstalk君がやってくれていました。

...ところが新しく作った環境だとリダイレクトされない...  

...ファイルの記載方法が悪いのか...  

...ログみても何も残っていない...  

...とりあえず、SSH Key作って接続して中身みてみよう...

あれ...
/etc/nginx/conf.d/redirect.conf
作られてねーじゃん。

なんで無視するの?嫌われた?
という壁にぶち当たりました。

色々調べていくと...
https://docs.aws.amazon.com/ja_jp/elasticbeanstalk/latest/dg/platforms-linux-extend.html

どうやら Linux
というプラットフォームでは nginx などの設定は

.platform/nginx/conf.d

にそのままの記載方法

server {
 listen 81;
 rewrite ^ https://$host$request_uri permanent;
}

これを custom.conf などに記載するように共通化されたみたいでした。

確かに元々のプロジェクト達は
Node.js running on 64bit Amazon Linux/4.14.1

今回は
Node.js 12 running on 64bit Amazon Linux 2/5.0.1

 でした。
これか!
となりました。(何かログ出してくれると助かったけど...)

AWSの進化は早いので、統一して全てのプロジェクトあげていこう!
と思いました。

他の困った方の役に少しでも立てれば幸いです。

Python × Selenium で自動化

元素周期表の覚え方「水兵リーベ僕の船...」懐かしいですね。
その元素記号の34番目は「Se」(Selenium)です。

一方、ITにおけるSelenium

  1. Webページにアクセスしてログイン

  2. 対象データを検索

  3. 必要な情報を入力して登録

というような、普段Webブラウザで行っている操作を自動化することができます。
PythonでもこのSeleniumを利用することができます。

最新ニュース記事取得

最近気になるニュースは?

就活の面接でも良く聞かれます。選んだニュースにより、その人の感性や価値観、人柄、知的好奇心が分かるので、効果的な質問だと思います。

さて、今回は最新ニュース記事の取得・収集を自動化する方法をご紹介します。(スクレイピングと言われているものです)

1時間毎にGoogleの「AI」最新ニュース記事を取得。それをSlackに投稿し、Excelに保存する。
の流れです。皆さんもサクッと試してみましょう。

1. 初期設定

pipコマンドを使って、Seleniumをインストールします。
$ pip install selenium

Google Chrome版のWebDriverをダウンロードします。
ChromeDriver - WebDriver for Chrome

2. ライブラリの定義

ここからはプログラミングです。
PythonSeleniumを使うために必要なライブラリを定義します。

  • Slackに投稿
  • Excelに保存
  • 定期実行

これらの処理のために必要なライブラリもあわせて定義します。

from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.keys import Keys

import requests
import datetime
import time
import cv2
import openpyxl
import schedule

3. WebDriverのオプション設定

Google Chrome版のWebDriverの設定です。headlessモードにすると、画面表示なしで動作します。自分のPC作業に影響せずに裏で動いてくれるので助かります。

DRIVER_PATH = './chromedriver'
options = Options()
options.add_argument('--headless')
driver = webdriver.Chrome(executable_path=DRIVER_PATH, options=options)

4. 定数の定義

検索キーを変更すれば、別のニュース記事を取得できます。

SearchKey = 'AI' #ニュースの検索キー
Token = 'XXXX-XXXX-XXXX-XXXX' #Slackで取得したトークン
Channel = 'XXXX' #SlackのチャンネルID
Excelfile = 'LatestNews.xlsx' #Excelファイル名

5. メイン処理

URL指定で最初からニュース一覧の画面に遷移させることもできますが、テンプレートとして他でも流用できるようにあえて検索キーを入力しての画面遷移としております!

def job():
  # Googleのページを開く
  driver.get("https://www.google.co.jp/")

  # 検索Boxのelementを取得
  search_box = driver.find_element_by_class_name("gLFyf")

  # 検索Boxにキーを入力
  search_box.send_keys(SearchKey)

  # Enterキーを入力
  search_box.send_keys(Keys.ENTER)

  # Google検索結果画面のニュースタブのelementを取得
  link_news = driver.find_element_by_class_name("q")

  # ニュースタブをクリック
  link_news.click()

  # 最新ニュース記事のリンクのelementを取得
  link_news_latest = driver.find_element_by_class_name("l")

  # 最新ニュース記事のリンクをクリック
  link_news_latest.click()

  # 最新ニュース記事画面の幅と高さを取得(全画面のスクショを撮るため)
  page_width = driver.execute_script('return document.body.scrollWidth')
  page_height = driver.execute_script('return document.body.scrollHeight')

  # 最新ニュース記事画面の幅と高さをセット
  driver.set_window_size(page_width, page_height)

  # 画面読み込みのため待機
  time.sleep(20)

  # 現在日時取得
  dt_now = datetime.datetime.now()
  
  # スクショのファイル名定義
  screenshot_path = "Pictures/" + str(dt_now) + ".png"

  # スクショ保存
  driver.save_screenshot(screenshot_path)

  # スクショのPNG画像を圧縮
  img = cv2.imread(screenshot_path)
  cv2.imwrite(screenshot_path, img, [cv2.IMWRITE_PNG_COMPRESSION, 9])

  # 画面タイトルを取得
  cur_url_title = driver.title

  # 画面URLを取得
  cur_url = driver.current_url

  # Slackに投稿(画面タイトル, 画面URL, スクショ)
  files = {'file': open(screenshot_path, 'rb')}
  param = {
    'token':Token,
    'channels':Channel,
    'initial_comment': SearchKey + "最新ニュース" + "\n\n" + cur_url_title + "\n\n" + cur_url,
    'title': screenshot_path
  }
  requests.post(url="https://slack.com/api/files.upload",params=param, files=files)

  # Excelの最終行に追記(時間, 画面タイトル, 画面URL, 検索キー)
  wb=openpyxl.load_workbook(Excelfile)
  sheet = wb.active
  max_row = sheet.max_row
   
  list = [dt_now, cur_url_title, cur_url, SearchKey]
  for index, item in enumerate(list):
    sheet.cell(row=max_row+1,column=index+1).value = item
  wb.save(Excelfile)

6. 定期実行処理

scheduleライブラリを使用すると手軽にジョブの設定ができます。

# 3分毎に実行
# schedule.every(3).minutes.do(job)
# 1時間毎に実行
schedule.every().hour.do(job)

while True:
  schedule.run_pending()
  time.sleep(1)

以上です!シンプルですね。

最後に

今回は、最新ニュース記事取得(スクレイピング)を例としてご紹介しましたが、WebシステムのUIテスト自動化にも使えます。画面遷移や、項目が多い入力フォームの検証にはもってこいです。単純な繰り返し作業はSeleniumにお任せしちゃいましょう!

株式会社ブリスウェル

キュウリ収穫量の可視化

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

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

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

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

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

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インスタンスを選ぶことがおすすめです。

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

株式会社ブリスウェル