Briswell Tech Blog

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

SaaS・サブスクサービス開発が増えています

f:id:brix:20200629204044j:plain
Photo by Tai's Captures on Unsplash

今、SaaS・サブスクサービス開発が増えています。

背景はほとんど言い尽くされていると思いますが、システム開発側の観点を書いておきたいと思います。

①新型コロナウィルスによる働き方への影響

新型コロナウィルスによって世界的にも日本でも(特に東京でも)デスクワーク職種のリモートワーク化が急に進んでいます。 日本では緊急事態宣言が終わったあとリモートワーク解除になった会社もあるようですが、そもそもWork From Anywhereがデフォルトになった会社もたくさんあります。

わかりやすい所で言えば株式市場へのインパクトは絶大で、医療製薬創薬などのコロナに直接関係する業界と並んで、リモート関連企業、オンライン関連企業、SaaS関連企業と括れるようなソフトウェア企業への期待があがっています。

市場でなくても、Zoom、MicrosoftのTeams、Slack、Docusignのような企業・サービスは、ほとんど毎日のようにどこかで名前が上がってるような気がします。

②ビジネスモデルへのインパク

ブリスウェルの本業ではないが、SaaS製品の導入に関するアドバイスを求められることが文字通り激増しています。 (うちは代理店でもないので客観的な意見を言っていると思います。)

それに伴って、我々のB2Bソフトウェア受託開発事業(顧客企業向けのソフトウェア開発事業)に対する問い合わせ内容もかなり大きく変化してきました。

例えば、クライアント企業内で長年滞っていたようなSaaS事業が動き出したり、オフラインの伝統的なコンサルサービスを畳んでオンラインのコンサル、ナレッジシェアサービスを始めるなどと言う話もあったりします。

もちろんチャージの方法も、売り切り・都度課金から、サブスク・継続課金・前払いを想定した話に変わってきています。 肌感で言うと、今までオンラインサブスクでの収益に無頓着だった伝統的な企業が、より急速に頭を切り替えてチャレンジされてるように見えます。 一方で、もともとサブスク型のビジネスを運営していた企業は、やっと波が来たと言う会社もいらっしゃったり、周りも始めちゃったので更に差別化が必要なんて会社もあったりです。

③ソフトウェア開発へのインパク

旧来の保守・メンテナンスから、やっと文字通りのDevOpsになるチャンスな気がします。

旧来の保守と今のDevOpsとの大きな違いは、ざっくり言えば「継続的にサービスの向上を図ると言う前提」にあると思います。

ブリスウェルが開発しているシステム、サービスは数年前からかなりメンテナビリティ重視だったのですが、ここへきてクライアント企業からも当領域への細かな要望が出てくるようになったのかなと。

アプリケーションアーキテクチャへ理解・要望がどんどん細かくなり、大まかに行ってマイクロサービス化、疎結合化の流れが加速していると思います。

ブリスウェルのような顧客向けのソフトウェア開発業は、アプリケーションアーキテクチャに関して弊社側から提案し、クライアント側が合意してくれると言うようなステップをとることが多いのですが、

足元数ヶ月ではもう1歩進んでクライアントがビジネスを構想する段階で前もってそういった考え方が共通認識となっていることが多いように思っています。

本題に入る前に既にかなり長くなってしまったので、技術的な詳細は次の機会に譲ろうかと。。。

なお今回のブログは、すべて音声入力で書いてみたのですが、かなり多くの口語が入ってしまうので、この辺もテクノロジーで文語調に直してくれたらいいなと。

コロナ時代のサブスクサービス開発はこちらへ。https://www.briswell.com/news/remote-online/

以下、DeepL

ーーー

Translated by DeepL

Right now, SaaS and subscription development is on the rise.

I think most of the background has been said, but I wanted to write about the perspective of the system development side of things I think.

(1) Impact of the new coronavirus on the way we work The new coronavirus has led to a worldwide increase in the number of people with remote, desk-bound jobs, both in Japan (and especially in Tokyo) and around the world. The shift to remote work is progressing rapidly. In Japan, there are companies that have lifted the restrictions on remote work after the declaration of the state of emergency. There are many companies that defaulted on Work From Anywhere in the first place.

To put it in plain English, the impact on the stock market has been tremendous, directly into the corona of medical and pharmaceutical drug discovery, etc. Along with related industries, you can group them together as remote stocks, online stocks, and SaaS stocks. The stock prices of software companies like

For example, Zoom and Docusign seem to be mentioned almost every day. I feel like I'm a good candidate.

(2) Impact on the business model. Although it's not Briswell's core business, I've been asked to advise on the implementation of SaaS products It's literally increased dramatically. (We're not an agency, so I think I'm being objective.)

With that, our B2B software contract development business (software development business for clients The nature of the inquiries we have received from clients (for example) has changed quite significantly.

For example, we've seen SaaS projects that have been stalled for years within our clients start to take off, and we've seen offline to start an online consulting and knowledge-sharing service by folding the traditional consulting services of And so on.

And, of course, the method of charging, from selling out and charging at a time, to assuming subscriptions, ongoing charges, and payment in advance. The story is changing. In a skin-deep sense, traditional companies that have been indifferent to online subs revenue are now more rapidly It looks like they're changing their minds and taking on the challenge. On the other hand, some companies that originally operated a subscribed business say they're finally seeing a wave of There are some companies that need to differentiate themselves further from others because they have started to do so too.

3) Impact on software development I feel like this is an opportunity to finally go from old-school maintenance and upkeep to literal DevOps.

The big difference between old-school maintenance and today's DevOps is that, to put it crudely, "We're going to continuously improve our service I think it's in the "assumption that we will figure it out".

The systems and services that Briswell develops have been quite maintenance oriented for several years now, but I think we are starting to receive more detailed requests for this area from our clients.

The understanding and demands for application architecture are becoming more and more detailed, and I think the trend toward microservices and loosely coupled systems is accelerating.

However, in recent months, we've seen a lot of clients go one step further and come to a common understanding in advance when they start to think about their business.

I've already gone on quite long before I get to the main topic, so I'll leave the technical details for the next time. .

In addition, I tried to write this blog entirely in voice input, but it contains quite a lot of spoken words. So I'm hoping that technology will fix this area to a literary tone as well.

To see the development of subscription in the Corona era, go to https://www.briswell.com/news/remote-online/

Here's a link to DeepL

Translated with www.DeepL.com/Translator (free version)

Pythonで江戸パワポ作成

江戸の古地図

夜な夜な江戸の古地図をながめるのが最近の和みです。

ふと、古地図で移転前の弊社オフィス(@恵比寿)周辺を眺めていると、ドラマの撮影でよく使われる「タコ公園」前を流れる渋谷川は、当時も同じように流れていました。

渋谷川を調べてみると

春の小川は さらさら行くよ〜♪

この歌のモデルになっているらしい。その昔、歌のように川の水はとてもきれいで、蛍もいたようです。

また、タコ公園周辺には、大きなお屋敷(森越中赤穂藩下屋敷)もありました。恵比寿駅東口の五差路も当時から存在しています。何かワクワクしませんか?

江戸庶民の生活

さて、江戸時代の地図に想いを寄せていると、その当時の生活はどのようなものであったのか気になってきました。

www.php.co.jp

こちらの本を今読んでいるのですが、江戸庶民の生活模様が、収入やモノの値段からうかがい知れてとても興味深いです。宵越しの銭を持たず物事に執着しないのが「江戸っ子」の美学と言われています。隣近所が一つの家族のようにワイガヤで楽しく助け合いながら暮らしていたのでしょう。当時の情景が目に浮かんできます。

江戸パワポ

と、ここでテックブログなので... 一つPythonネタをご紹介します。

下図のように、Microsoft PowerPointのテンプレファイルに、新しいスライドを追加し

  1. タイトルテキスト
  2. 円グラフ
  3. テキストボックス

を挿入することができます。

f:id:KenjiU:20200628204157p:plain

データ引用:
中江 克己(2003/8/1) | お江戸の意外な「モノ」の値段 物価から見える江戸っ子の生活模様(PHP文庫) | 位置No.176/2899

1. ライブラリの定義
from pptx import Presentation
from pptx.chart.data import ChartData
from pptx.enum.chart import XL_CHART_TYPE, XL_LEGEND_POSITION, XL_LABEL_POSITION
from pptx.util import Cm, Pt

以下の「python-pptx」ライブラリを使用します。 python-pptx.readthedocs.io

2. テンプレとするパワポを指定
prs = Presentation('edo_expenses_template.pptx')
3. 空のスライドの挿入
title_only_slide_layout = prs.slide_layouts[1]
slide = prs.slides.add_slide(title_only_slide_layout)
shapes = slide.shapes
4. タイトルテキストの挿入
shapes.title.text = '江戸時代のある大工さんの家計'
5. 表の挿入
# 表のデータを定義
name_objects = ['支出項目','匁']
name_expenses = ['家賃', '飯米', '塩・味噌・油・薪炭', '道具・家具', '衣服費', '音信・祭礼・布施']
val_expenses = [120, 354, 700, 120, 120, 100]

# 表の行数と列数
rows = 7
cols = 2

# 表を挿入する位置
top_table = Cm(6)
left_table = Cm(3)

# 表の幅と高さ
width_table = Cm(14)
height_table = Cm(7)
table = shapes.add_table(rows, cols, left_table, top_table, width_table, height_table).table

# 表のヘッダーの定義
table.cell(0, 0).text = name_objects[0]
table.cell(0, 1).text = name_objects[1]

# 表のボディの定義
for index in range(len(name_expenses)):
  table.cell(index+1, 0).text = name_expenses[index]
  table.cell(index+1, 1).text = str(val_expenses[index])
6. 円グラフの挿入
# グラフを挿入する位置
top_graph = Cm(14)
left_graph = Cm(5)

# グラフの幅と高さ
width_graph = Cm(21)
height_graph = Cm(12)

# グラフのデータを定義
total = sum(val_expenses)
def calc_double(n):
    return n / total
val_expenses_percent = list(map(calc_double, val_expenses))

chart_data = ChartData()
chart_data.categories = name_expenses
chart_data.add_series('Pie', val_expenses_percent)

graphic_frame = slide.shapes.add_chart(
    XL_CHART_TYPE.PIE, top_graph, left_graph, width_graph, height_graph, chart_data
    )
chart = graphic_frame.chart

# グラフのプロパティを定義
chart.has_legend = True
chart.legend.position = XL_LEGEND_POSITION.BOTTOM
chart.legend.include_in_layout = False
chart.plots[0].has_data_labels = True
data_labels = chart.plots[0].data_labels
data_labels.number_format = '0%'
data_labels.position = XL_LABEL_POSITION.OUTSIDE_END
7. テキストボックスの挿入
# テキストの定義
text = '※銀一匁は現代で約1,250円です。'

# テキストボックスを挿入する位置
top_text = Cm(14)
left_text = Cm(3)

# テキストボックスの幅と高さ
width_text = Cm(5)
height_text = Cm(2)

# フォントサイズ
text_font = 20

# テキストボックスの定義
text_box = slide.shapes.add_textbox(left_text, top_text, width_text, height_text)
text_box.text = text
text_box.text_frame.add_paragraph().font.size = Pt(text_font)
8. ファイル保存
prs.save('edo_expenses.pptx')

以上です。
AI学習モデルの予測精度やデータ分析の報告書作成などにも利用できそうです。

株式会社ブリスウェル

行き先掲示板IoT化計画(仮)

こんにちは大澤です. こちらの記事(クラスタリング勉強会 - Briswell Tech Blog)でAI勉強会を実施した大澤です.

  • 特技はマジック(クロースアップ)

  • 好きなものは紅茶とチョコレート(チョコレートスペシャリストの資格保有)

  • 苦手なものは犬・猫

  • 最近の趣味はカリグラフィの動画を見ること

・・・

さて,新型コロナウィルスの騒ぎがあり,ブリスウェルでは3月からリモートワークを実施しています.7月からは様子をみながら徐々に出社(時差出勤)の方向性になっていますが,引き続きリモートのメンバーもいます. 通勤時間が0になるという最大のメリットを享受していましたが,4ヶ月も続くと不便な点も出てきます.

例えばリモートだと相手の状態がわかりません.

出社してれば「今MTG中かな」「もう帰っちゃったかな?」というのが分かります.(わからないときもあります)

昼休憩に入るときも,リモート時はチャットで休憩に入る旨を伝えています.

なんとなぁく不便だなぁと感じていたので,ブリスウェルのIoT担当として何かソリューションを考えようと思い立ちました. これはつまり,古き良き「行き先掲示板」をIoT化してあげたら良いのかも知れません.とするとこんなものはいかがでしょう.

f:id:kyoshi0000:20200627202610p:plain

ステータス管理ボックスと名付けました.決してふざけているわけではありません.

説明しますと,これは手のひらサイズの箱(or ブロック)のデバイスです.例えば「出社」の面を上にするとその人のステータスは「出社」となり,「MTG」の面を上にするとステータスが「MTG」となります. このデバイスを各メンバーが持ち,それぞれのステータスデータをサーバに送ってあげることで,各メンバーのステータスを確認できるじゃないか!というわけです. これを実現するためには,どの面が上になっているかを知る必要があります.これは加速度センサをデバイスに仕込んであげればできそうです. また,データをサーバーに投げるので,ネットに繋いであげる必要もあります.

なんとなくできる気がしてきたので,早速作ってみました.

【用意したもの】

  • 押し入れに眠っていたWiFiモジュール「DSP-WROOM-02」(ESPr Developer)
  • 早速買った3軸デジタル加速度センサモジュール「ADXL345」

ESP-WROOM-02arduino IDEを使用して開発できるので非常に楽です.

また,加速度センサはアナログ出力ではなく,デジタル出力でないといけません. (ESP-WROOM-02にはアナログ入力ポートが1つしか無いので,3軸のアナログデータを取得するのが難しいためです)

【作ってみる】

こんな感じで接続して,arduinoIDEでプログラムを書き込んであげると… f:id:kyoshi0000:20200627204941j:plain

なんと加速度が取得できます.X軸,Y軸の値は小さいですが,Z軸の値が大きく出ていますね.つまりデバイスは上向きであることがわかります. f:id:kyoshi0000:20200627205049p:plain

次にESPをWiFiに接続してセンサデータをサーバに投げます. PHPで受け手側のプログラムをなんとなく書き,ローカルサーバを立ち上げ,センサデータを投げるプログラムをESPに書き込みます. ブラウザを立ち上げて,作ったデバイスの向きを色々変えてみます.すると… f:id:kyoshi0000:20200627205433p:plain

出社!



この向きだと… f:id:kyoshi0000:20200627205533j:plain

f:id:kyoshi0000:20200627205742p:plain

お昼!  …ん??

f:id:kyoshi0000:20200627205840p:plain

うーん,向きに応じて異なるデータを送ることはできていますが,表示の仕方に工夫が必要ですね…この辺りは他のメンバーの方が得意そうです.

表示はどうにかしたいですが,やりたいことはできました. こういうのは未完成でも基本的な機能ができた時点で人目につく場に出してフィードバックをもらうべきだと偉い人に教わりました. いかがでしょうか.

ステータスを管理してメンバーで共有するだけならそういうWEBサービスを使えばいいだけですが,リアルの世界の動作と紐付けられるのはIoTの強みだと思います.

なかなか外出できない土日を有意義に過ごすことができました.

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

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

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

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

私自身、システム開発の仕事に携わって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