Ledge Tech Blog

We're the data scientists and AI engineers behind Ledge.

Azure Speech to Textを使って面倒な文字起こしの作業時間を90%以上削減した話

こんにちは。レッジでデータサイエンティストをしている今村です。

今回は社内における業務効率化の取り組みの1つとして、音声文字起こしツールの開発秘話について紹介します。

音声文字起こしツールを活用することで、6時間程度かかっていた文字起こし作業が30分程度まで(90%以上)短縮することができました。AIを活用した業務改善のケースとして、開発背景からアプリケーションの構築、実際の作業に適用させた結果などを説明していきます。

記事の概要

本記事では下記のような流れで紹介していきます。

  • そもそもなぜ音声による文字起こしが必要だったのか?

  • 使用したサービス

  • アプリケーション開発

  • 周りの評価はどうだったのか

そもそもなぜ音声による文字起こしが必要だったのか?

なぜ音声による文字起こしが必要だったのかといいますと、取材やインタビューの文字起こしが非常に大変だったからです。

いきなり取材やインタビューというキーワードが出てきましたが、

実は弊社は国内最大級のPV数を誇るAI特化型メディア Ledge.ai を運営している会社です。

Ledgeai

Ledge.aiでは国内外のAIに関する様々な記事が掲載しており、最新技術の紹介やトレンドを捉えた記事など日々更新しています。

記事を執筆する上で欠かせないのが取材やインタビューです。執筆のために取材時の音声を聞きながら、どういった会話をしていたのかを文字で起こし、その情報を元に記事を執筆していきます。

実はこの文字起こし、想像以上に非常に時間がかかります。音声を聞きながら、文字を書いていくので録音時間が長いほど時間がかかってしまいます。1時間の取材音声の場合、文字起こしに5~6時間かかってしまうこともあるそうです。

一方で、AIを始めとする最先端技術を扱うメディアとして情報の鮮度は非常に重要です。日々技術がアップデートされる業界であるため、取材した内容はできるだけ早く展開していくことが世の中的にも望まれていることと思います。 そのため、取材した情報をいかに早く公開できるかどうかが勝負となってきます。

つまり、取材した内容をできるだけ早く世の中に展開していく必要がある。でも、文字起こしが作業のボトルネックになってしまっているという現状があります。

そこで、

「音声ファイルから自動で文字起こし」できればいいのに

というニーズが生まれました。

もちろん、このニーズは別に新しいものでもなく、世の中には文字起こしに関するサービスが多数存在します。しかし、比較的高価なものであったり、従量課金体系のパターンが多く、気軽に手を出すことができない。

それだったら自分たちで作ってしまおう

と思い立ち、開発が始まりました。

使用したサービス

今回のアプリケーションでは下記のサービスを使用しました。

音声認識

自動文字起こしのコア技術となる音声認識には、Microsoft社が提供するクラウドサービスAzureにあるAzure Cognitive Serviceを使用しました。

Azure Cognitive Serviceとは、クラウドベースで展開されるAI技術のAPIサービス群の総称であり、以下の5つのカテゴリで構成されています。

  • Azure Cognitive Serviceが展開するカテゴリ群
    • 視覚
    • 音声
    • 言語
    • 意思決定
    • 検索

Azure Cognitive Serviceの概要については こちら を参照してください。

今回は音声のカテゴリに該当するSpeech APISpeech to Textを用いて開発を行いました。Speech APIでは音声に関わる様々なAPIが用意されており、機能別に下記のような項目があります。

Webアプリケーションフレームワーク

アプリケーションの構築にはPythonのライブラリであるStreamlitを使用しました。

着想から素早く形にしたかったので、

以上の特徴を持つStreamlitを採用しています。

Streamlitについては、弊ブログの過去記事 、もしくは 公式ドキュメント を是非チェックしてみてください。

通知

APIを使ったとしても、文字起こしには時間がかかってしまいます。それまで画面を開いたまま辛抱強く待つのは本末転倒です。

本アプリケーションではSlackと連携を行い、文字起こしが完了したタイミングでSlackに通知する仕組みを構築しました。

特定の人をメンションしたり、文字起こししたファイルをSlackの特定のチャンネルにアップロードできます。

アプリケーション開発

さて、ここからは開発背景について紹介していきます。

早速ですが、記事執筆時点のWebアプリはこちらになります!

アプリキャプチャ

非常にシンプルですね。別にいいんです。あくまで社内の業務効率化が目的ですから。

本アプリケーションの構成は以下の通りです。

アプリキャプチャ

処理の流れは下記の通りです。

  • 音声データ入力
  • WAV変換
    • 音声ファイルの場合は、APIが読み込める.wavに変換
  • 動画から音声抽出
    • 動画ファイルの場合は、動画から音声を抽出した上で.wavに変換
  • 音声文字起こし
    • .wavをSpeech to Text APIに読み込み、音声から起こしたテキストを取得
  • 通知・テキスト共有
    • APIから得られたテキストをSlackに自動で投稿する

下記より順に詳細を説明していきます。

音声データ入力

データ入力はStreamlitのfile_uploader()メソッドを使用しています。

file_uploader()を使用することで1行でアップロード機能を付与できます。 なお、読み込んだファイルはBytesIO形式となります。

詳しくは file_uploader() - 公式ドキュメント を参照してください。

import streamlit as st

uploaded_file = st.file_uploader('こちらからファイルを読み込み')
print(type(uploaded_file))  # BytesIO

WAV変換

AzureのSpeech to Text APIにおいて、オーディオファイルの規定形式は.wavです。

そのため、音声認識を実行する前処理としてWAV変換を行います。

WAVの変換は pydub というライブラリを使用することで簡単に実現できます。

以下のコマンドでインストールできます。

pip install pydub

例えば、.mp3.wavに変換するサンプルコードは下記の通りです。

from pydub import AudioSegment

audio_file = 'YOUR_SONG.mp3'
output_path = 'YOUR_SONG.wav'

audio = AudioSegment.from_mp3(audio_file)
audio.export(output_path, format='wav')

今回のアプリケーションでは、Streamlitのfile_uploader()によって音声ファイルはBytesIO形式で読み込まれます。

BytesIO形式のファイルはAudioSegment.from_file()にて読み取ることができます。

まとめると、全体を通して下記のように実装できます。

from pydub import AudioSegment
import streamlit as st

byte_file = st.file_uploader('こちらからファイルを読み込み')
audio = AudioSegment.from_file(byte_file)
audio.export(output_path, format='wav')

動画から音声抽出

動画ファイル(.mp4)

動画やYouTubeから文字起こしがしたい!という要望がありましたので、動画から音声を抽出してみました。

実は、.mp4のような動画ファイルについては、BytesIO形式に変換することで、上のWAV変換の処理がそのまま使えます。

そのため、動画に対しては上に記載したコードで対応ができてしまうので、特別な処理は不要です。

YouTube

YouTube動画については youtube-dl というライブラリを用いています。

このライブラリを使用することでYouTubeのURLから動画をダウンロードできます。

なお、このライブラリを使用する場合は、事前に ffmpeg をインストールする必要があります。

以下のコマンドでインストールできます。

pip install youtube-dl

今回は音声のみの抽出が必要なので、YouTubeのURLから該当の動画の音声を抽出していきます。

import youtube_dl
from pydub import AudioSegment

ydl_opts = {
    'format': 'bestaudio/best',
    'outtmpl':  output_file_path + '.%(ext)s',   # 出力先パス
    'postprocessors': [
        {'key': 'FFmpegExtractAudio',
         'preferredcodec': 'mp3',                # 出力ファイル形式
         'preferredquality': '192'},             # 出力ファイルの品質
        {'key': 'FFmpegMetadata'},
    ],
}
url = "<YouTube URL>"

ydl = youtube_dl.YoutubeDL(ydl_opts)

# 指定したパスに音声ファイルが格納される
_ = ydl.extract_info(url, download=True)

# 格納された音声ファイルをpydubで読み込む
audio = AudioSegment.from_mp3(output_file_path + '.mp3')

読み込んだ音声ファイルは上述したWAV変換の処理を行います。

音声文字起こし

いよいよAPIを使用した音声から文字起こしを行う部分の実装になります。

まず初めにAPIを使用するためのSpeech SDKをインストールします。

下記のコマンドを実行してインストールしておきます。

pip install azure-cognitiveservices-speech

文字起こしアプリケーションの実装コードは下記の通りです。

import time
import azure.cognitiveservices.speech as speechsdk

def recognize_audio(output, speech_key, service_region, filename, recognize_time=100):
    """
    wav形式のデータから文字を起こす関数
    ---------------------------
    Parameters
    output: str
        音声から起こしたテキスト(再帰的に取得する)
    speech_key: str
        Azure Speech SDKのキー
    service_region: str
         Azure Speech SDKのリージョン名
    filename: str
        音声ファイルのパス
    recognize_time: int
        音声認識にかける時間(秒)
    """
    # Speech to Text 設定周り
    speech_config = speechsdk.SpeechConfig(subscription=speech_key, region=service_region)

    # 認識器の設定
    audio_input = speechsdk.AudioConfig(filename=filename)
    speech_recognizer = speechsdk.SpeechRecognizer(speech_config=speech_config, audio_config=audio_input)

    def recognized(evt):
        nonlocal output
        output += evt.result.text
        
    # 音声認識の実行
    # recognize_timeの時間、継続して文字起こしを行う
    speech_recognizer.recognized.connect(recognized)
    speech_recognizer.start_continuous_recognition()
    time.sleep(recognize_time)

    return output


filename = "<WAV File Path>"
speech_key = "<Azure Speech SDK Subscription Key>"
service_region = "<Azure Speech SDK Region Name>"
output = ""

output = recognize_audio(output, speech_key, service_region, filename, recognize_time=100)

この辺りはAzureのSpeech SDKの書き方に合わせて書いていきます。

具体的な内容は 公式ドキュメント を参照してください。

今回の記事では上で紹介した実装コードについて解説します。

まずは、Speech SDKを使用してAPIを使うために音声構成SpeechConfigを作成します。

# 音声構成の作成
speech_config = speechsdk.SpeechConfig(
  subscription="<paste-your-subscription-key>", 
  region="<paste-your-region>")

続いて、音声ファイルの入力と音声認識器を作成します。

音声ファイルは.wav形式の音声ファイルパスを指定します。

# 音声ファイルの入力
filename = "<WAV File Path>"
audio_input = speechsdk.AudioConfig(filename=filename)
# 音声認識器の作成
speech_recognizer = speechsdk.SpeechRecognizer(speech_config=speech_config, audio_config=audio_input)

最後に指定した秒数で継続的に音声認識を行います。

def recognized(evt):
    """
    APIで文字起こしされたテキストを更新する関数
    関数のスコープ外にある変数"output"に文字起こしされたテキストを更新する
    """
    # nonlocalを使用することで関数のスコープ外である"output"を更新できるように宣言
    nonlocal output
    # evt.result.textに文字起こしされたテキストが存在する
    # 変数outputにテキストを追記していく
    output += evt.result.text
    
# 音声認識の実行
speech_recognizer.recognized.connect(recognized)
# 音声認識の開始
speech_recognizer.start_continuous_recognition()
# 指定した秒数待機する
time.sleep(recognize_time)

継続的な音声認識を行うためには、音声認識の状態を管理しておく必要があります。

今回は関数recognized(evt)を定義し、文字起こしされたテキストをrecognize_timeで指定した秒数の間実行しています。

より詳細な内容については、継続的認識に関する 公式ドキュメント を参照してください。

以上が音声文字起こしアプリケーションの根幹となる音声認識の処理となります。

最終的なアウトプットは変数outputに格納されます。

通知・テキスト共有

取得したテキストは文字起こしをしたタイミングでSlackに自動投稿する仕組みを構築しました。

弊社インターンの大熊による協力で実現できました。

import time
import slackweb
from slack import WebClient

def slack_send_notification(webhook_url, message):
    """
    文字おこしの完了をslackに通知する関数
    ---------------------------
    Parameters
    webhook_url: str
        Slack環境変数(webhook_url)
    message: str
        Slackに送りたいメッセージ
    """
    slack = slackweb.Slack(url=webhook_url)
    slack.notify(text=message)

def slack_send_content(OAuth_Token, channel_id, file_name):
    """
    文字おこしの内容をスレッドに返信する関数
    ---------------------------
    Parameters
    OAuth_Token: str
        Slack環境変数(OAuth_Token)
    channel_id: int
        Slack環境変数(Channel_id)
    file_name: str
        Slackに送信するファイルパス
    """
    # clientの認証
    client = WebClient(token=OAuth_Token)
    # チャンネルの履歴を取得
    result = client.conversations_history(channel=channel_id)
    # 最新のメッセージのタイムスタンプを取得
    ts = result["messages"][0].get('ts')
  
    client.files_upload(
      channels=channel_id,
      thread_ts=str(ts),
      initial_comment="議事録だよ",
      file=file_name,
      reply_broadcast = False
    )

# 文字起こしされたテキストをoutput.txtとして保存
with open('output.txt', 'w') as f:
    f.write(output)

# 環境変数
slack_mention_id = "<mention_id>"
slack_webhook_url = "<webhook url>"
slack_channel_id = "<channel id>"
slack_OAuth_Token = "<OAuth Token>"

# Slackでの通知と文字起こしのテキストファイルの送信
message = "<@" + str(slack_mention_id) + "> 文字おこし完了したよ"
slack_send_notification(webhook_url=slack_webhook_url, message=message)
time.sleep(2)
slack_send_content(OAuth_Token=slack_OAuth_Token, channel_id=slack_channel_id, file_name="output.txt")

SlackAPIを用いて文字起こししたテキストファイルをアップロードします。

メンション先を指定する<mention_id>は、Streamlitのインターフェイス上でユーザーが選択した項目に応じて自動で取得するようにしています。

import streamlit as st
from slack import WebClient

def slack_get_users(OAuth_Token):
    """
    OAuth_Tokenの情報からユーザーIDとユーザー名を取得する関数
    ---------------------------
    Parameters
    OAuth_Token: str
        Slack環境変数(OAuth_Token)
    """
    user_ids = ['None']
    user_names = ['None']
    client = WebClient(token=OAuth_Token)
    result = client.users_list()
    for member in result["members"]:
      member_id = member.get('id')
      member_name = member['profile'].get('display_name')
      user_ids.append(member_id)
      user_names.append(member_name)
  
    user_ids_dic = dict(zip(user_names,user_ids))
    user_names = sorted([name for name in user_names if name!='' and name!='None'], key=str.lower)
    user_names.insert(0, 'None')
  
    return user_ids_dic, tuple(user_names)

slack_OAuth_Token = "<OAuth Token>"

# OAuth TokenからSlackのユーザー名とIDを取得
user_ids_dic, user_names = slack_get_users(OAuth_Token=slack_OAuth_Token)
# Streamlitによるプルダウン選択メニューを設定
user_name = st.selectbox(
    '議事録完成時にメンションするメンバーを選択',
    user_names)
# メンション対象のユーザーIDを取得
slack_mention_id = user_ids_dic[user_name]

周りの評価はどうだったのか

当たり前の話ですが、業務改善ツールは現場が使ってくれないと意味がありません。

せっかく業務改善ツールを作ったとしても、使いにくかったり思ったような形ではないと「作って終わり」になってしまい、使用者開発者どちらもマイナスとなってしまいます。

そこで実際に使ってくれているLedge.ai編集部の方に評価してもらいました!

『Ledge.ai編集部に来てから、AIを導入している企業や著名人などに、インタビュー取材をする機会が多くなりました。 文字起こしに消耗される日々、つらい……。と思っていたときに、今村さんが文字起こしアプリのプロジェクトを手がけていることを知りました。

同アプリを使っていくなかで、

「ほど良い箇所で自動で改行してくれるシステムがほしい!」

「動画データからも(自動で音声を抽出して)文字起こしがしたい!」

「いっそのこと、YouTubeのURLを入力するだけで、文字起こしできるようにしたい!」

など、フィードバックをしたら、そのたびにすぐに要望どおりにアップデートしてくれました。 自分で文字起こししていたときとは"執筆"といった感覚でしたが、現在では同アプリが文字起こししてくれた文章を"編集"するだけになり、とんでもない業務効率化ができたと感じています。 今となっては、文字起こしアプリなしの取材記事の執筆は考えられません!!』


『文字を起こしただけでは褒められない世の中だからこそ、求められるアプリでした。 文字起こしを自動化しているとほかの会社の人に話すと、「レッジさんすげー!」「超うらやましい!」と言われます。鼻が高いです。

実際、メディアに携わる人以外でも、文字起こし作業が苦痛だと認識している人はこの世に多く存在しています。 それこそ、いろんな会社で文字起こしが必要な局面を強いられると思うのですが、強いられた方は死ぬほど苦痛だと思うんです。 何よりも時間がかかる……。1時間の音声データを文字起こしすると、5,6時間は最低でも必要になります。 でも、5,6時間かけて文字起こしをしたからって褒められるわけではないんです。

文字起こしは、文字を起こしをしたうえで、何かを作ったり、考えたりするための準備作業ですよね。 この準備作業に膨大な時間をかけなければいけないって、冷静になって考えると「おかしい」ですよね。 いや、冷静にならなくてもわかるレベルでおかしい。 本当に必要なのは、文字起こしにかける時間を確保するのではなくて、文字起こしをしたうえで何を作るかを考える時間です。

これらの課題を今村さんたちが秒速で解決してくれました。今村さんたちが作ってくれた文字起こしアプリなら、音声や動画データを選択するだけで、勝手に文字を起こしてくれます。 文字起こしアプリによる文字起こしの精度は、人間が起こすレベルまでとは言わないものの、文字化されたテキストデータをもとに編集することを考えれば必要十分な内容です。 時間換算をするのであれば、1時間の取材に対して、従来は文字起こしに6時間、編集作業には6時間かかっていたのですが、 文字起こしアプリを使うことで、文字起こしの時間が30分程度まで短縮され、編集作業を込みでも6時間ほどで記事が完成できるようになりました。 しかも、文字起こしアプリが文字を起こしている間の時間は、別の作業ができるというのもうれしいポイント。

Ledge.aiが急成長を遂げた背景にも、この文字起こしアプリによる作業時間の短縮が間違いなくあります。編集部側の要望にも即座に応えてくれるため、使い勝手はバツグン。 文字起こしアプリによって、いろんな記事を作りやすくなりました。 改めて振り返ると、潜在的な課題をAIで解決するという綺麗な事例だと思います。恵まれているな……俺たち……。』

こんなにもエモーショナルに書いてもらいました。ベタ褒めいただき大変恐縮です。。

業務改善アプリケーションとして、今回の自動文字起こしツールは成功できたと思います。

特に、文字起こしに6時間かかっていたのが30分程度まで短縮でき、作業工数の大幅な削減が実現できました。

とはいえ、まだまだ改善すべき点が多々ある荒削りなツールなので、この評価で満足せず、社員の声を聞きながらアップデートを繰り返していきます。

最後に

今回はAzure Cognitive ServiceのAPIを用いた音声による自動文字起こしアプリケーション開発の紹介を行いました。

Azure Cognitive Serviceを使用することで、音声やテキストなどの業務改善に関する高度なAI技術を簡単に実装することができます。 特に、自前のツールにAI技術を組み込む場合は最適解になると感じました。

しかし、パッケージされて便利な分、モデルの再学習はやや大変です。 学習する仕組みは整っているものの、音声データと人間によるアノテーションが必要であるため、学習データの準備に工数がかかってしまいます。 今回使用したAzureのSpeech APIにおけるモデル学習の手順は こちら にまとめられています。

レッジでは今回のように、クライアント業務の課題感に対してAIなどの適切な技術で業務プロセスを改善していくための議論が日々行われております。もちろん議論や提案だけでなく、実際に手を動かし実装しながらクライアントの課題に向き合っています。

AIのトレンドを追うレッジだからこそ、AIでできることを業務改善などといった様々な形で実現し、そこで得られた経験を積極的に発信していきたいと思います。

参考文献

GCP Cloud Functionsを使用して新チャンネル作成通知Slack Appを作る方法

こんにちは。レッジインターン生の大熊です。 今回はSlackで誰かが新しいチャンネルを作成したときに通知するAppを、Google Cloud Functions(以下Cloud Functions)を通じて作成する方法をご紹介します。

弊社ではSlack上で誰かが新しいチャンネルを作成すると、「新しいチャンネルの名前」と「チャンネル作成者」をパブリックのチャンネルに通知するようにしています。新しいチャンネルができたことを可視化できると、組織の中でどんな動きがあるのかを把握しやすくなります。

手順としてもそこまで複雑でないため、是非参考にしてみてください。

大まかな手順

今回はSlackに新しいチャンネルが作成された時、以下のように通知するようにします。

今回はチャンネル作成をキャッチしてからSlackへ通知するまでを、大きく分けて以下の4つのステップで説明します。

  1. Slack appの作成と設定
  2. Cloud Functions上で実行する.pyの作成
  3. Cloud FunctionsとGithub Actionsの設定
  4. SlackのEvent Subscriptionsの設定

1. Slack appの作成と設定

1-1. Appの作成

まず、チャンネル内で通知するappの作成を行います。Slack appのページの右上にある「Create New App」をクリックし、Appの名前とワークスペース決定してAppの作成を行います。

1-2. Scopeの設定

チャンネルへ通知するために、Appに権限を渡す必要があります。「OAuth & Permissions」の「Scopes」からincoming-webhookchat:writeとScopeに追加します。

Scopeを変更すると「Please reinstall your app for these changes to take effect」と言われるので、「Reinstall to Workspace」をクリックし、通知を送りたいチャンネルを選択してインストールします。

(この時、「権限がありません」のような表記が出てインストールできない場合があります。その際は「App Home」の「Your App’s Presence in Slack」の中身を設定することで解消されることがあります。)

1-3. Webhook URLの取得

Slack側にPOSTリクエストをするために必要なURLを取得します。「Incoming Webhooks」の「Webhook URL」からURLをコピーしてどこかにメモしておきます。

Appの設定は一旦ここまででOKです。また後でCloud functions側の設定を行うときに連携するための設定を行います。

2. Cloud Functions上で実行する.pyの作成

Slack通知を行う関数を記入する.pyファイルを作成します。この時、ファイル名は「main.py」である必要があります。

main.pyに以下のように記入して、保存します。

import os
import requests

url = os.getenv('WEBHOOK_URL')

def get_slack_event(request):
    request_json = request.get_json()

    if 'challenge' in request_json:
        return request_json.get("challenge")

    if "event" in request_json:
        request_json = request.get_json()

        channel_name = request_json['event']['channel'].get('name') # チャンネル名
        channel_id = request_json['event']['channel'].get('id') # チャンネルID
        user_id = request_json['event']['channel'].get('creator') # ユーザーID

        payload = '{"text":"New public channel :point_right: <#' + str(channel_id) + '|' + str(channel_name) + '> \n Created by <@' + str(user_id) + '> "}'

        headers = {
            'Content-Type': 'application/json'
        }

        response = requests.request("POST", url, headers=headers, data=payload)

        return print(response.text.encode('utf8'))

if 'challenge' in request_json:の箇所は、Slack AppにエンドポイントのURLを認証させる為のChallenge認証を行っています。送られたchallengeの値をそのまま送り返せばOKです。

次にif "event" in request_json:の箇所でSlackへ通知するための処理を書いていきます。今回の仕様では、「チャンネル名」と「作成ユーザー」を表示したいので、「チャンネル名」、「そのチャンネルのID」、「チャンネルを作成したユーザーのID」の3つをJSONから取得します。Slack app経由でリンクやメンションを行う際には、チャンネルやユーザーのIDを<#channel_id>、<@user_id>の形式で記述する必要があります。またSlack上での表記をカスタムしたいときは各IDの後ろにパイプ(|)を置き、それに続く形で記入します。(公式HP

3. Cloud FunctionsとGithubの設定

次に、Slackからのリクエストを受け取り、それに応じて通知を送り返すためにCloud Functionsの設定を行います。

また今回はGitHub actionsからCloud Functionsへデプロイする方法で行います。

3-1. ローカルでの設定

masterへpushしたときにデプロイを実行するような設定をします。まず、以下のようなディレクトリを準備します。

work
├── .github
│   └── workflows
│       └── function.yml
└── main.py

次に、function.ymlの中身を以下のように記述します。

name: Build and Deploy to Cloud Run

on:
  push:
    branches:
      - master

env:
  PROJECT_ID: ${{ secrets.RUN_PROJECT }}

jobs:
  setup-build-deploy:
    name: Setup, Build, and Deploy
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@v2

      # Setup gcloud CLI
      - uses: GoogleCloudPlatform/github-actions/setup-gcloud@master
        with:
          version: "290.0.1"
          service_account_key: ${{ secrets.RUN_SA_KEY }}
          project_id: ${{ secrets.RUN_PROJECT }}

      - name: Deploy
        run: |-
          gcloud functions xxxx

gcloud functions xxxxにはgcloudコマンドでデプロイするときのコマンドに設定します。gcloudコマンドの使い方は公式のリファレンスをご参照ください。

またデプロイのコマンドの中に、ランタイム環境変数として、先ほどメモしたWebhook URLを指定します。

3-2. サービスアカウントの作成・権限の付与・鍵の取得

Github Actions上でgcloudコマンドを実行するためにはGCPのサービスアカウントが必要になります。

まずgcloudコマンドで以下のように実行しアカウントを作成します。

gcloud iam service-accounts create slack-channel-created-notification \
--description="GitHubAction" \
--display-name="SLACK CHANNEL CREATED NOTIFICATION"
  • slack-channel-created-notification:アカウント名になるのでやることに応じて適宜設定。
  • descriptionとdisplay-nameは必要に応じて適宜内容を書きます。

アカウントを作成したら、次にロールを追加します。roles/cloudfunctions.developerroles/iam.serviceAccountUserを以下のコマンドで追加します。

gcloud projects add-iam-policy-binding (プロジェクトID) \
--member="serviceAccount:slack-channel-created-notification@(プロジェクトID).iam.gserviceaccount.com" \
--role="roles/cloudfunctions.developer"
gcloud iam service-accounts add-iam-policy-binding \
(プロジェクトID)@appspot.gserviceaccount.com \
--member='serviceAccount:slack-channel-created-notification@(プロジェクトID).iam.gserviceaccount.com' \
--role=roles/iam.serviceAccountUser

(プロジェクトID)にはプロジェクト名を記入します。すなわちmemberの箇所は、{アカウント名}@{プロジェクト名}.iam.gserviceaccount.comのようにして記述します。

最後に、GithubのSecretsに登録するためのjson鍵を取得する。以下のコマンドで取得できます。

gcloud iam service-accounts keys create ~/key.json --iam-account slack-channel-created-notification@(プロジェクト名).iam.gserviceaccount.com

3-3. GithubのSecretsにプロジェクト名と鍵を登録

以下のようにsettings -> secretsの画面で、プロジェクトIDと鍵を登録します。NameとValueは以下のように設定します。

  • RUN_PROJECT:プロジェクトID
  • RUN_SA_KEY:前節で取得したjson

これで大まかな設定は完了です。ここでデプロイしておきます。

4. SlackのEvent Subscriptionsの設定

チャンネル作成時をトリガーとしてCloud Functionsにデータを送るために、Event Subscriptionsを設定します。

まずCloud Functionsの管理画面の中で、先にデプロイした関数の編集画面にいき、トリガーURLを取得します。

次にSlack Appの設定画面に戻り、「Event Subscriptions」で「Enable Events」を「On」にし、「Request URL」に先ほど取得したトリガーURLを記述します。

この時、「Varified」と表示されればOKです。

完成

以上で、新しいチャンネルを作成するとSlackに通知が行くようになるはずです。

視認性担保のためにアイコン等は適宜変えると良いと思います(Slack Appの設定画面の「Basic Infomation」の下の方に設定するところがあります)。

まとめ

今回はSlackで誰かが新しいチャンネルを作成したときに通知するAppを、Cloud Functionsを通じて作成する方法をご紹介しました。

通知されるようにすると、新しいチャンネルが意外と日々作成されていることに気づきました。そして全体の動きがわかることで、「あの部署の人は何やっているんだろう」と思っていたのが、出社せずとも「今こういうプロジェクトに取り組んでいるのか!」ということがすぐにわかるようになりました。そのため、テレワーク下での組織の動きの把握には持ってこいの機能ではないかなと感じています。

参考資料

【Tableau】 Workout Wedesday 2021W2を解いてみた

こんにちは。レッジインターン生の大熊です。 第2回目はWorkout Wednesday2021 Week2について解説していきます。 Workout WednesdayはTableauのコミュニティ活動の1つであり、お題にあるVizを再現することでTableauのスキルを磨くことを目的としています。 以下のページから今回のチャレンジへアクセスできます。

2021W2のページ:http://www.workout-wednesday.com/2021w02tab/

2021W2の課題

今回のお題は「Can you build a Customer Lifetime Value Matrix?」というもので、以下のコホート図のようなダッシュボードを作成するのが目標です。コホートと異なるのは、セルの中身が顧客生涯価値(CLTV)となっており、表の横方向に累積された売上を、顧客数で除して算出されている点です。そしてポイントは、値が存在しないはずの表の右下三角を空白にする点です。

またその他条件は以下のようなものです。公式ページにあるものを引用しています。

  1. ダッシュボードのサイズ:1400px x 1000px。
  2. シート数は自由。
  3. 顧客生涯価値マトリックスを作成する。
    1. 各顧客は、取得四半期(第1四半期)ごとに分類される。
    2. 初購買日からの四半期単位での顧客一人当たりの生涯価値を計算する
  4. ツールチップとフォーマットを一致させる。
    1. カラーパレットはこちらの「PuBuGn」を使用する。
    2. CLVが表示されていないマークの背景は白。
  5. 応用
    1. 2019年第2四半期と2018年第3四半期の両方の空白を埋める。

顧客一人あたりのLTVの算出はLOD計算を使用すれば簡単に算出できそうです。詰まる点としては「いかにして右下を空けるか」という部分かと思われますが、今回はINDEX()を使用して(y軸方向のINDEX)>(x軸方向のINDEX)が保持されるように表現してみます。

このようにすると、各四半期の最後の四半期で、直近の四半期からの増減が0であっても値を表示してくれます。ペインの中の値の増減に着目するやり方もありますが、この場合四半期間での増減が0であると上手く表示されない場合があります。データの特性ともくてきに応じて使い分けたいところです。

以下、具体的に手を動かしながら作成していきましょう。なお、手順がわかりやすいよう各計算フィールドに番号を振っている関係で、お題の表記とは若干異なりますのでご了承ください。

さっそく作成してみる

1.データの読み込みと整形

ダウンロードしたデータをTableauに読み込んだら、作業しやすくするために以下のようにいくつか設定をします。

  • 「Customer Name」,「Order Date」,「Sales」以外は非表示にする。
  • (もし「Sales」で$により文字列と認識されてしまう場合、)「Sales」を右クリック→変換→分割で数値だけの列を作成し、列名を「Sales_rm$」とする。

2.四半期毎の初購買顧客数と、各購買と初購買四半期の期間幅を求める

今回求められるビューは、➀「各四半期が最初の購買日であった顧客が何人」で、➁「当該四半期が最初の購買日であった顧客がt四半期後までに平均いくら支出したか」という2点を表したものです。

そのため、まずは「各顧客の初購買日の四半期」を求めます。これは「各顧客の初購買日を特定」→「その初購買日を四半期に変換」の流れで求められ、以下のようにLODを使用することで求まります。

1.AQUISITION QUARTERS

DATETRUNC('quarter', {FIXED [Customer Name]:MIN([Order Date])})

次に、各顧客のその後の購買日が、初購買日から四半期単位でどれだけ離れているかを求めます。DATEDIFF()を使用した以下の計算フィールドを作成します。

2.QUARTER SINCE BIRTH

DATEDIFF('quarter', [1.AQUISITION QUARTERS], [Order Date])

さらに各四半期において、その四半期が初購買日であった顧客のユニーク数を求めます。先ほど「1.AQUISITION QUARTERS」で各顧客の初購買四半期を求めているので、これをその四半期ごとで集計し、顧客のユニーク数をCOUNTED()で求めればOKです。計算フィールドの中身は以下の通りです。

3.CUSTOMERS

{ FIXED [1.AQUISITION QUARTERS]:COUNTD([Customer Name])}

ここまでで作成したもので、ビューを作成してみます。下図のように配置します。

いい感じにできてきました。後はラベルであるCLTVをどう作るかです。

3.CLTVを求める

CLTVは比較的簡単に求まります。各四半期(AQUISITION QUARTER)における総LTVは、先ほどの図ではSalesを横に累積していくことで求まります。例えば2016Q1が初購買四半期の顧客の1四半期後(つまりQUARTER SINCE BIRTHが1)の総LTVは、初購買四半期におけるSalesの合計(つまり2016Q1の行かつQUARTER SINCE BIRTHが0の列のセル)と、初購買四半期の翌四半期におけるSalesの合計(つまり2016Q1の行かつQUARTER SINCE BIRTHが1の列のセル)を足し合わせた値になります。

後は総LTVを各CUSTOMERSの値で割ってあげれば良いです。

以下のように計算フィールドを作成します。

4.RUNNING SUM

RUNNING_SUM(SUM([Sales_rm$]))

5.CLTV

[4.RUNNING SUM]/WINDOW_AVG(MAX([3.CUSTOMERS]))

それではマークを「四角」にし、これらを以下のように配置して確認します。

左上のほうを見てみると、お題の数値と同じことがわかります。しかし、右下が空白にならず、累積された値が表示されてしまいます。

したがって、次は右下の不要な箇所をINDEX()を使用して除外していきます。

4.INDEX()を使用して左上のみを保持

左上のみ保持するということは、座標平面のように考えると、対角線を引いたときにy>xとなる領域を保持することと同じです。この発想を利用して、マトリックスの左下を基点として各セルにx,y軸方向のINDEX()の値を付与し、y>xとなるINDEX()を持つセルのみを保持することでお題のレイアウトを表現できそうです。

したがって、まず横方向用と縦方向用のINDEX()をそれぞれ用意します。

6.INDEX X

INDEX()

7.INDEX Y

INDEX()

次にy>xとなるような計算フィールドを作成します。

8.KEEP INDEX

[6.INDEX X]<=[7.INDEX Y]

これで空白を作成する準備ができたので、「8.KEEP INDEX」をフィルターに入れ、「真」にチェックを入れ、OKをクリックします。

「8.KEEP INDEX」は「6.INDEX X」と「7.INDEX Y」の2つの表計算を使用したフィールドなので、この2つの表計算がどのような規則で計算するかを指定する必要があります。フィルターに入れた「8.KEEP INDEX」を右クリックし「表計算の編集」を選択して、それぞれ以下のように設定を行います。

再度フィルターの編集を行い、「真」のみにチェックを入れれば、以下のようになるはずです。

これでお題とほぼ同じのができました!あとは細かいレイアウトを整えるだけです。

なお、なぜこのようにフィルターをかけると上手いこといくか理解が難しい場合は、フィルターをかける前に「6.INDEX X」と「7.INDEX Y」を「ラベル」にいれ、それぞれに対し表計算の編集を同様に行うと、y>xの関係性がわかりやすくなります。

5.レイアウトの調整

ここでは、わかりにくい以下の3か所を解説します。

  • 「2016 Q1」→「Q1 2016」のように年と四半期の表示順序を入れ替える。
  • 表の値に「$」を表示する。
  • 指定の色を使用する。

まず年と四半期の表示順序は、行シェルフの「1.AQUISITION QUARTERS」を右クリック→書式設定→ヘッダーの日付→カスタムに「"Q"q yyyy」と入力すればOKです。

次に「$」の表示は、「5.CLTV」に既定で表示されるように設定します。左のサイドバーの「5.CLTV」を右クリック→既定のプロパティ→数値形式→数値(カスタム)でプレフィックスに「$」を入力をすればOKです。

以下のGIFでも操作を確認できます。

最後に指定された色を設定します。今回のお題ではこちらのページからPurple/Blue/Green(PuBuGn)の色を選択します。

デフォルトのインストールパス指定でTableauをダウンロードした場合、ドキュメント配下にある「My Tableau Repository」フォルダの中にPreferences.tpsというファイル名でファイルが存在していると思います。何のエディタでも良いのでPreferences.tpsを開き、以下のコードをの間に挿入し、保存します。

<preferences>
    <color-palette name="CB_PuBuGn" type="ordered-sequential">
        <color>#fff7fb</color>
        <color>#ece2f0</color>
        <color>#d0d1e6</color>
        <color>#a6bddb</color>
        <color>#67a9cf</color>
        <color>#3690c0</color>
        <color>#02818a</color>
        <color>#016c59</color>
        <color>#014636</color>
    </color-palette>
</preferences>

この後、Tableauを1回閉じ、もう一度起動させることでカラーパレットが追加されます。

6.完成

細かい調整をすると、最終的に以下のようなビューができ上がります(ツールヒントの設定を忘れないようにしてください)。なお、手順がわかりやすいように各計算フィールドの番号を振ってあるため、お題とは若干表記が異なってしまっています。ご了承ください。

まとめ

本記事ではWorkout Wedesday 2021W2を解くプロセスについて解説しました。Tableauにおいて欠損値の扱いは表現方法の幅を増やすのに非常に重要なスキルです。空白の表現方法はいくつかやり方があるので、余裕のある方は是非他の表現方法でもトライしてみてください。

【Tableau】 Workout Wedesday 2021W1を解いてみた

こんにちは。レッジインターン生の大熊です。 今回から不定期ですが、Workout Wednesdayの解説を紹介していきます。 Workout WednesdayはTableauのコミュニティ活動の1つであり、お題にあるVizを再現することでTableauのスキルを磨くことを目的としています。

記念すべき第1回目はWorkout Wednesday2021 Week1について解説していきます。

2021W1のページ:http://www.workout-wednesday.com/wow2021w1tab/

2021W1の課題

今回のお題は「Can You Find the Variance Along the Line?」というもので、以下のようなダッシュボードを作成するのが目標です。課題の大筋としては、基準としたい年を選択し、その基準年の値と、「最も古い値」、「最も新しい値」、「基準の直近の値」の3つの比較指標との差分(%)を示せるか、というものです。

またその他条件は以下のようなものです。公式ページにあるものを引用しています。

  1. LOD計算は使用不可
  2. 数値形式をパーセントに変換するための計算フィールドは使用不可。
  3. 計算フィールドにハードコードされた年がない(値は動的に計算する)
  4. ビュー数は1に収める
  5. ツールチップ、色、書式設定を一致させる
    1. 選択された年マークの色は#F0007B
    2. 矢印のユニコードは▲ U+25B2, ▼ U+25BC
    3. ダッシュボードのサイズ:1000px x 800px)
  6. 米国の食糧不安率の年別推移を示す折れ線グラフを作成し、ユーザーが選択した年と比較年を反映した丸印を付ける。
  7. ユーザーがどの年に注目したいか、どのようなタイプの比較をしたいかを切り替えられる機能を構築する。
    1. データセットの中で最も新しい年(今回の場合は2019年)
    2. データセットの最初の年(今回の場合は1995年
    3. 前年(=選択年-1)

作成の方針としては、いかにして基準年の値を保持しながら「最も古い値」、「最も新しい値」、「基準の直近の値」の値を取得するかが重要そうなので、上手く表計算で取ってくる必要がありそうです。最も古いと最も新しいはFIRST()とLAST()で取得でき、直近の値は(基準年)-1かLOOKUP()で取得できそうです。今回のお題は比較的易しめかと思われます。

以下、具体的に手を動かしながら作成していきましょう。

さっそく作成してみる

1.データの読み込みと整形

ダウンロードしたデータをTableauに読み込んだら、作業しやすいようにいくつか設定をします。なお、元のExcelのカラムは階層的に作成されているので、そのままExcelファイルを読み込むと上手く読み込めません。データインタープリターを使用すると上手く読み込んでくれます。

  • 「Year」のデータ型を「日付」にする
  • 「Very low food security Percent of households」カラムを非表示にする。
  • 「Food insecurity (includes low and very low food security)」のカラム名を「value」に変更する

2.パラメータの作成

基準となる年を選択するパラメーターと、比較する指標を選択するためのパラメータを以下のように作成します。

なお「1.対象年」のリストは日付でも作成できますが、計算フィールドでいちいち年を抽出するのが手間かかるので、初めから整数で作成しています。まずデータ型を整数の状態にし、「範囲」で1995~2020としたのちに「リスト」に切り替えると一括して年のリストを作成できます。

3.「対象年の値」、「比較年の値」、「対象年と比較年の増減率」の作成

それぞれ以下の方針で作成できます。

  • 対象年の値:「Year」と「1.対象年」のパラメータを使用して、両者が一致するときにvalueを返すようにします。
  • 比較年の値:まず「2.比較年」に該当する年を特定するフラグを作成します。そしてそのフラグがTrueを返す年でvalueを返すようにします。
  • 増減率:(対象年の値-比較年の値)/比較年で算出できます。

順に計算フィールドを作成していきます。

3.対象年のvalue

IIF(MAX(YEAR([Year]))=[1.対象年], SUM([value]),NULL)

4.比較年フラグ

CASE [2.比較年]
WHEN 'First Year' THEN IIF(FIRST()=0,True,False)
WHEN 'Most Recent Year' THEN IIF(LAST()=0,True,False)
WHEN 'Previous Year' THEN ATTR(YEAR([Year]))=[1.対象年]-1
END

WOWのページにはヒントとしてLOOKUP()の使用を記述していますが、今回はFIRST()とLAST()だけを使用して、直近年の特定は、(パラメータの値)-1で特定しています。ただし今回の方法だと、年に抜けがあった場合に対応できません。このパターンに対応したい場合はLOOKUP()を使用します。

5.比較年のvalue

IIF([4.比較年フラグ],SUM([value]),NULL)

6.対象年と比較年の増減率

WINDOW_MAX([3.対象年のvalue])/WINDOW_MAX([5.比較年のvalue]) - 1

4.点を描画するための計算フィールドを作成

以下の計算フィールドを作成します。

7.点

IF ATTR(YEAR([Year]))=[1.対象年] or [4.比較年フラグ]
THEN SUM([value])
ELSE NULL
END

これによって対象年と比較年のvalueのみが保持されたフィールドができます。

5.ビューの作成

まずピルを以下のように配置します。なお配置したときに出るNULLの存在を教えてくれるインジケーターは非表示にしています。

次に以下の操作を行い、折れ線グラフを完成させます。

  • 行シェルフの「7.点」を右クリックして二重軸にする
  • 軸の同期をする。
  • 右側のヘッダーを非表示にする。
  • 「7.点」のマークで、IF [4.比較年フラグ] THEN 1 ELSE 0 ENDを作成し色に入れる。(カラーパレットをいじらなくても指定色#F0007Bへ変更できるようにするため)
  • 各種、色を変更する。

その後、軸の見た目を整えます。わかりにくいのは以下の2点でしょうか。

  • 横軸の表示を「1995年」→「1995」みたいにする:横軸の書式設定のスケールにおいてカスタムを選択し、「yyyy」と入力する
  • 縦軸の単位を%表記にする:「value」を右クリックして出てくる既定のプロパティ内の「数値形式」→「数値(カスタム)」で小数点を0桁、サフィックスに「%」を入力する

そのほかに関しても以下のGIFで操作を確認できます。

次に、ビューのタイトルを変更します。以下のようにタイトルに記入します。

この時、「6.対象年と比較年の増減率」はプラスなら▲、マイナスなら▼を先頭につける必要があります。「6.対象年と比較年の増減率」を右クリックして出てくる既定のプロパティ内の「数値形式」→「カスタム」において「▲ 0%; ▼ 0%; "N/C "」と入力することで表示できます。

最後に、ツールヒントの中身を整えます。ツールヒントを以下のように設定します。

これで必要なビューの作成は終えました。

6.ダッシュボードの作成

シートが1枚しかないのでそのまま載せればOKです。ポイントは2種類のパラメータを浮動にして、小さくした状態で配置します。パラメータを小さくするとボタンだけっぽくなります(キャプチャでは上手く表示されないですが、実際の画面ではボタンのように表示されます)。

以上で完成です。

まとめ

本記事ではWorkout Wedesday 2021W1を解くプロセスについて解説しました。今回の出題者のCandra McRaeさんのコメントや課題の条件から、LOD計算に頼らず表計算でシンプルに表すことの良さを伝える意図の課題であったのではないでしょうか。

TableauのLODや表計算は複雑ですが、使いこなすことで表現の幅が広がるので、基本は押さえておきたいところですね。

「TableauでExcelっぽい棒グラフを作成してくれないか」と上司に言われたときの2つの対処法

こんにちは。レッジインターン生の大熊です。今回はExcelっぽい幅の狭い棒グラフをTableauで作成する方法をご紹介します。 「ExcelっぽいグラフをTableauで作成してくれないか」という声を稀に聞きます。しかしTableauのデフォルトの機能で棒グラフを作成すると、幅が均等な状態で作成されるので、塊に応じてスペースをちょっと開けたい場合にはひと工夫必要です。

そして、工夫の仕方は大きく分けて以下の2種類あります。

  • データに階層構造がない場合(e.g.売上と利益の比較)
  • データに階層構造がある場合(e.g.同月における各年の比較)

本記事ではこの2種類のExcelっぽい棒グラフの作り方を紹介します。

1.データが階層構造にない場合(e.g.売上と利益の比較)

データが階層構造にないとは、下の左図のデータから、右図のように作成したい場合のことを指しています。この場合は簡単に作成できます。

今回は上図を目標にします。つまり各月における売上と利益を棒グラフで表現し、月ごとに隙間を少し空けるやり方を紹介します。

1-1.通常の棒グラフを作成する

今回は売上と利益はそれぞれ別の列に格納されているので、メジャーバリューを使用して棒グラフを作成します。

1-2.メジャーバリューに0を2つ追加する

メジャーバリューの枠の中をダブルクリックすると計算式が打てるようになるので、そこに「0」と入力したものを2つ作成します。 そして2つの0上端と下端に置き直します。

すると、ビューには値が0の棒グラフが作成され、スペースが生まれます。

1-3.レイアウトの調整

スペースが作れたので、ほぼ完成です。後は好みに合わせてレイアウトを調整するだけです。 今回は以下のように、月のラベルを下に持ってきてみました。

Tabelauの標準の機能ではラベルを直接下部に持ってこれないので、以下の調整を行いました。

  • 列の各ヘッダー、フィールドラベルを非表示にする
  • 行シェルフに「-WINDOW_MAX(SUM(売上))*0.05」を入力し、二重軸&軸の同期を行う。(この時表計算は「表(横)」)
  • メジャーバリューのマークは棒グラフ、WINDOW_MAXのマークは円にし色の不透明度を0%にする
  • WINDOW_MAXのマークラベルに「月(オーダー日)」を追加し、ラベルの水平方向の配置を左にする
  • メジャーネームの並び変えで、WINDOW_MAXの項目を利益の横に持ってくる
  • 行のゼロラインと0の棒グラフの色を統一する

2.データが階層構造にある場合(e.g.同月における各年の比較)

階層構造とは年>月>日のように階層があり、その階層でグルーピングしたいときです。別の表現をすると、ディメンションを2つ使って棒グラフを作るときです。 本稿では以下の図を目標にして作成します。

なお本稿では年×月の粒度で行いますが、エッセンスを理解すれば別の種類のディメンションの組み合わせでも、同様の棒グラフを作成できます。 また、このグラフはWorkout Wednesdayの2020W23のチャレンジと同様ですので、データやワークブックはこちらからダウンロードできます。

作成のイメージ

作成のイメージとしては、各棒グラフを置く箇所を日付に置き換えて表示するイメージです。例えば2018年1月のデータは2020年1月3日、2019年1月のデータは2020年1月8日とラベリングし、横軸を日単位にして、棒グラフを描画します。1月のデータは1月付近に、2月のデータは2月の付近にラベリングすることで、月ごとにまとまった棒グラフを作成できます。

2-1.スペースの大きさを決めるパラメータを作成する

浮動小数点数で許容値が「すべて」のパラメータを作成します。 これが各棒をプロットする間隔を調整します。

2-2.計算フィールドを作成する

今回作成する計算フィールドは1つのみです。以下の計算フィールドを作成します。

//Calculation1
DATEADD("day"
        , 2 + INT([Bar Width] + 1) * (YEAR([Order Date]) - ({MAX(YEAR([Order Date]))}-1))
        , MAKEDATE(2020, DATEPART("month", [Order Date]), 1)
)

これによって、1月のデータは2020年1月付近に、2020年2月のデータは2月の付近にラベリングされます。具体的にいうと、2016年から2019年までデータがあり、パラメータが4のとき以下のように割り振られます。

  • 2016-04-13 → 2020-03-24
  • 2016-11-03 → 2020-10-24
  • 2017-04-13 → 2020-03-29
  • 2017-11-03 → 2020-10-29
  • 2018-04-13 → 2020-04-03
  • 2018-11-03 → 2020-11-03
  • 2019-04-13 → 2020-04-08
  • 2019-11-03 → 2020-11-08

({MAX(YEAR([Order Date]))}-1)が2019-1=2018となるので2018年を基点にしてみると式が理解しやすいです。イメージとしては、各日付が2020年の同月の初日に割り振られ、年に応じて日付が足し引きされています。そして同月内の年で比較すると、間がパラメータで指定した4日分空いていることがわかります。

2-3.配置する

列シェルフにCalculation1を、行シェルフに売上を置き、年(オーダー日)を色に配置します。 そして列シェルフにあるCalculation1を右クリックし「正確な日付」に変更します。 最後にビュー内の横軸を右クリックし、書式設定→軸と選択。次にスケール内の日付で「カスタム」を選択し、「MMM」と入力すれば完成です。 下にGIFとして一連の動作をまとめたので参考にしてみてください。

まとめ

今回はTableauでExcelっぽい棒グラフを作る方法について2つご紹介しました。

グラフを作成する時は「何を明らかにしたいか」という点を明確にしておくことが重要です。今回の2番目のグラフ場合、「同月における各年の比較」を見るのには向いているかもしれませんが、推移を追うことは難しそうです。

また、「同月における各年の比較」を見るためのベストプラクティスがExcelっぽい棒グラフかと言われたら、議論の余地がありそうです。その場の状況に応じて最適なグラフを選択することは常に念頭に置いておきたいです。

合成データがモデル構築をよりオープンにする〜MLタスクでのSDVによる合成データの有効性を検証する

こんにちは。レッジのデータサイエンティストの松本です。

レッジでは、クライアント先に常駐してデータ・ドリブンな課題解決に取り組んだり、ダイナミックプライシングやNLP周りのアルゴリズムを受託開発したり、クライアント先へのBI導入の推進など、幅広くデータ利活用に関わる業務に取り組んでいます。

さて、今回はSDV(Synthetic Data Vault)による合成データで学習した機械学習モデルの有効性を検証します。

後述するように、個人情報を匿名化する手法であるk-匿名化や差分プライバシー基準の担保は実データの構造や特性を歪めます。 したがって、これらの匿名化技術を用いてデータサイエンス系のコンペ等でデータを提供してモデル構築の知見を得られたとしても、そのモデルを実データに適用すると精度が悪化してしまう可能性があります。

このようなデータ匿名化の課題を克服する手法がデータの合成です。 合成したデータで学習した機械学習モデルが実データで学習したモデルと同等の精度を担保できるのであれば、今以上に企業や政府機関でのデータ活用の敷居が低くなる でしょう。

本稿ではまず合成データについて説明した後に、合成データを生成できるSDVライブラリについて説明します。そして、最後に合成データで学習した機械学習モデルの有効性を検証します。

合成データとは

以下の要件を満たしたデータを合成データと呼びます。

  • 元のデータと統計的にある程度似ていること
    ex)平均、分散といった要約統計量が元のデータと似ている
  • 元のデータと形式的、構造的に似ていること
    ex)テーブルの主キー、外部キーが元データと似ている

この2つの要件を満たす合成データを生成するためには、元のデータと同一の形式や構造を保持したうえで統計モデルを構築する必要があります。

後述するSDVは、元のデータをモデリングし、そのモデルから合成データを生成(サンプリング)できるライブラリです。

なぜ合成データが必要か

企業がデータを収集・共有・活用する場合、データを保護したりプライバシーを遵守するためにk-匿名化差分プライバシー基準の担保など、データを何らかの方法で匿名化する必要があります。

しかし、上記のデータの匿名化は本来のデータの構造や特性を歪めるため、匿名化したデータで学習した機械学習モデルを実データに適用すると悪影響を及ぼす可能性があります。

このような データの匿名化に関する課題を克服する手法がデータの合成です。

データを合成することによって、そのままでは公開できないようなデータセットでも個人情報を明らかにすることなく、データセットの属性間の特性や関係を維持できます。 合成したデータには個人の情報が含まれていないため、共有も容易になります。

また、理論的には、合成したデータで機械学習モデルを学習させても、最終的には実データで学習させたモデルと同等の精度を得ることが可能です。*1

SDVとは

Synthetic Data Vault (SDV) は、単一のテーブルデー*2やリレーションを有する複数のテーブルデー*3、時系列データセットを学習し、元のデータセットと同一のデータ型・統計的特性を持つデータを合成できるライブラリです。

今回は単一のテーブルデータとして、分類タスクの入門でお馴染みのirisデータセットを利用します。 SDVではテーブルデータをGaussianCopura Model(正規コピュラモデル)*4に基づいてモデリングし、データを合成していきます。 SDVで利用できるモデルはGaussianCopura Modelの他にも、CTGAN ModelCopulaGAN Modelがあります。

また、時系列データ、リレーションを有する複数テーブルのデータセットに対するSDVライブラリの使用方法については公式のチュートリアルこちらで説明されているので、興味があればご参照ください。

合成データで学習した機械学習モデルの有効性を検証する方法

SDVにより合成したデータで学習したモデルと、実データで学習したモデルの精度を比較します。 データ分割での意図せぬ特徴量やターゲット変数の偏りを防ぐするため、以下のK分割交差検証を行います。

  1. データセット全体を5分割し、1/5をテストデータ、4/5を学習データとする
  2. 4/5の学習データでSDVモデル(GaussianCopura Model)を学習させ、学習データと同数のデータを合成する
  3. 合成データでロジスティック回帰モデルを学習させる
  4. 学習データでロジスティック回帰モデルを学習させる
  5. テストデータに対しての2モデルの精度を評価する

上記ステップにおける2~5を5分割繰り返し、平均値を算出します。

また、合成データにおけるモデル改善と実データにおけるモデル改善の対応を確かめるため、ロジスティック回帰モデルの正則化パラメータを複数設定します。

検証

今回はローカルのDockerコンテナ上で検証しました。 以下で紹介するコードはGitHubで確認できます。

まず、次のコマンドでsdvライブラリをインストールします。

!pip install sdv

sdvライブラリをインストール後、いったん、データセット全体をSDVで学習して合成したデータと、元のデータの分布を比較してみます。 なお、検証ではscikit-learnのirisデータセットを使用します。 以下はデータセット全体をSDVで学習し、学習データと同数のデータを合成するコードです。

from sdv.tabular import GaussianCopula

from sklearn.datasets import load_iris


# 変数定義
field_transformers = {
    'sepal length (cm)': 'float',
    'sepal width (cm)': 'float',
    'petal length (cm)': 'float',
    'petal width (cm)': 'float',
    'target': 'categorical'
}
gc = GaussianCopula(field_transformers=field_transformers)

# irisデータセットをロード
iris = load_iris()
# 特徴量
data = iris.get('data')
# ターゲット変数
target = iris.get('target')

# 特徴量とターゲット変数をまとめてDataFrameに変換
data_tmp = pd.concat([pd.DataFrame(data, columns=iris.get('feature_names')), pd.DataFrame(target, columns=['target'])], axis=1)
# GaussianCopulaでモデリング
gc.fit(data_tmp)

# 合成するデータ数
N = len(data_tmp)
# GaussianCopulaからデータを合成
sampled = gc.sample(N)

元のデータのペアプロットを確認します。

# 元のデータのペアプロット
g = sns.pairplot(data=data_tmp, hue='target')
g.fig.suptitle('original Data', y=1.02)

次に、合成データのペアプロットを確認します。

# 合成データのペアプロット
g = sns.pairplot(data=sampled, hue='target')
g.fig.suptitle('generated Data by SVD', y=1.02)

元のデータと比較して、合成データでは特に2変数間の分布についてターゲット変数が綺麗に分離していないように見えますが、これはSVDによるデータ生成過程に起因しています。

SVDでは、元のデータにおける各特徴量の分布パラメータ*5と特徴量間の共分散をもとにデータをサンプリングします。

そのため、少量ではあるものの確率分布の裾野に存在するデータも生成されるため、元のデータほどターゲット変数が綺麗に分離しません。*6

ただし、ターゲット変数間の各特徴量の大小関係は合成データにおいても元データとほぼ同じであることがわかります。

次に、「合成データで学習した機械学習モデルの有効性を検証する方法」で記載した交差検証を実施します。

from sklearn.linear_model import LogisticRegression

from sklearn.model_selection import StratifiedKFold


# 層化5分割
skf = StratifiedKFold(n_splits=5, shuffle=True)
syn_scores = list()
origin_scores = list()

# ロジスティック回帰のパラメータグリッド
C = [1e-4, 1e-3, 1e-2, 1e-1, 1]
for train_idx, val_idx in skf.split(data, target):
    # 学習データ
    X_train, X_test = data[train_idx], data[val_idx]
    # テストデータ
    y_train, y_test = target[train_idx], target[val_idx]
    # 学習データの特徴量とターゲット変数を結合
    train_tmp = pd.concat([pd.DataFrame(X_train, columns=iris.get('feature_names')), pd.DataFrame(y_train, columns=['target'])], axis=1)
    # GaussianCopulaでモデリング
    gc.fit(train_tmp)
    # 学習データと同数をサンプリング
    sample_gc = gc.sample(len(train_tmp))
    syn_score = list()
    ori_score = list()
    for c in C:
        # 合成データに対するロジスティック回帰モデルを学習させる
        lr_syn = LogisticRegression(C=c)
        lr_syn.fit(sample_gc[iris.get('feature_names')], sample_gc['target'])
        # 元のデータに対するロジスティック回帰モデルを学習させる
        lr_ori = LogisticRegression(C=c)
        lr_ori.fit(X_train, y_train)
        syn_score.append(lr_syn.score(X_test, y_test))
        ori_score.append(lr_ori.score(X_test, y_test))

    syn_scores.append(syn_score)
    origin_scores.append(ori_score)

交差検証で算出した、各正則化パラメータにおける精度の平均値を比較します。

import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np
sns.set()

fig = plt.figure()
ax = fig.add_subplot(1, 1, 1)
ax.plot([str(i) for i in C], np.mean(np.array(syn_scores), axis=0), label='svd')
ax.plot([str(i) for i in C], np.mean(np.array(origin_scores), axis=0), label='original')
ax.legend()
plt.show()

オレンジ色は元の学習データで学習したモデル、青色はSDVで合成したデータで学習したモデルです。 どちらのモデルでも、正則化パラメータが増加すれば精度の向上しているため、この正則化パラメータのグリッドにおいては精度と比例関係にあります。

これは、 合成データ上での最適なパラメータが実データにおける最適なパラメータに対応する ことを意味しています。 テストデータに対する精度はSDVで合成したデータで学習したモデルの方が低いですが、ハイパーパラメータの選定においては合成データを利用しても問題がないことがわかりました。

最後に

今回はサンプルデータ(iris)をもとに各特徴量やターゲット変数のデータを合成しましたが、SDVではカテゴリ変数を匿名化してデータを合成することも可能です。

今後このようなデータ合成に関する研究が発展し、データ合成のモデリング精度が向上していくことで、もっと機械学習の活用がオープンになっていくはずです。

昨今、事業会社でデータサイエンティストやデータエンジニアを雇って内製化しようする動きをちらほら聞きますが、データの匿名化が容易にできれば外部の知見を取り入れるチャンスが広がります。

今後は企業のデータもそうですが、政府機関のデータセットも活発に公開されて、どんどんオープンになっていけばいいなと思います。

参考資料

*1:あくまで理論上のお話です。今回説明したSDVライブラリではまだまだ合成データで学習したモデルと実データで学習したモデルの精度に差があります。

*2:Kaggle等のコンペで用いられるテーブルデータ(pandas.DataFrameとして保持できる表データ)のイメージです。

*3:ER図で記述できる、親子関係などを有する複数のテーブルのことです。

*4:多変量分布を周辺分布と分布間の依存構造に分離して表現した関数のこと。

*5:例えば切断正規分布、一様分布、ベータ分布、指数分布のパラメータ。

*6:元のデータが綺麗に分離している場合は特に。

「Pythonで動かして学ぶ!深層学習の教科書」を読んでPythonの勉強をしてみた

こんにちは。初めまして。レッジのインターン生の大見川です。 レッジでは、 データサイエンス部門に所属していますが、 Ledge.ai でも記事を書いているので良ければご覧ください。 今回の記事では、「Python機械学習の実装をしてみたい!」という方に向けて、初学者の1冊目におすすめな本を紹介します。 Pythonの勉強を始める前はプログラミングの経験は全くなく、HTMLもわからないようなレベルでした。そのような私でもこの本をベースに勉強を進めていくことで、ある程度コードが書けるようになったのできっと初学者のみなさんにとっても参考になると思います。

1冊目におすすめの本

私が最初に勉強していた本は「Pythonで動かして学ぶ!深層学習の教科書」です。正直、図書館で見つけて「これならわかりやすそうだし面白そうだなー」くらいの感覚で選びましたが、今考えても1冊目にはおすすめです。ここで注意してほしいことは、その本で「最終的に何をできるようになる本なのか」を確認して、自分が面白そうだと思う本を選んだ方がいいということです。なぜなら、プログラミングを勉強する上で(特に独学だと)一番大事なのは根気よく続けることだからです。この本は最終的に画像認識ができるようになることが目標ですが、他にも自然言語処理やデータ分析などPythonで出来る事はたくさんあります。その中で1番面白そうだと思うテーマを扱っている教材を選んだほうがいいと思います。

全体的な流れ

この本の大まかな流れは以下になります。

  1. 環境構築

  2. 機械学習の説明

  3. Pythonの文法

  4. 有名なライブラリの紹介

  5. 機械学習の実装

  6. 深層学習の実装

  7. CNNの実装

  8. データの水増し・転移学習

ここからはこの順番にそって説明していきます。

環境構築

プログラミングの勉強をする時に、まず環境構築をする必要があります。この本ではAnacondaというパッケージが紹介されています。これを使うと必要なライブラリが簡単にインストールできるようになり便利です。Anacondaのインストールから仮想環境の構築まで丁寧に説明されているので、説明されている通りに環境構築をしていきましょう。

機械学習の説明

次は「そもそも機械学習とは何か?」という説明から、よく使われる手法などの説明が書かれています。コードを書くことはありませんが、機械学習までの流れや、精度を上げるための技術など実装するときにも必要となる知識なので、しっかり読んで理解していきましょう。この記事では実際に実装するところで詳しく書きます。

Pythonの文法

いよいよ実際にPythonのコードを書いていきます。 まずはPythonの基礎ということで変数、型、if文の説明となります。本書ではそれぞれの項目が説明された後や、章の最後に問題が書かれているので、解いてみてください。読んで理解しても実際に手を動かすと分からないところが出てくると思うので、その度に説明部分を読み返すことが大事だと思います。

次はリスト型、辞書型、while文、for文の説明です。本に書かれている内容を少しずつ変えてどのように出力されるかを予想してみたり、問題の設定を変えて解いてみたりすると、より理解が深まると思います。

文法の最後は関数です。この章ではPythonにもともと組み込まれている関数やメソッドから、自分で関数を定義するところまで学びます。ここで注意してほしいことは、すべてを覚える必要はないということです。例えば、[1,3,5,2,4]というリストにsortというメソッドを用いると[1,2,3,4,5]というように並び替えることができますが、ここでは「なんか並び替えるメソッドあったなー」くらいの記憶で十分です。メソッドや組み込み関数は本で紹介されていないものもたくさんあり、全てを覚えていたらきりがないです。今回の例で言えば「Python 並び替え」のように調べれば色々な記事が出てくるので、検索してみましょう。実際に業務をする上でも忘れてしまったら、その度に調べれば良いので、どんどん進めていきましょう。

有名なライブラリの紹介

ここではPythonでよく用いられるライブラリを紹介していきます。ライブラリとは外部から読み込むPythonのコードの塊です。わかりにくいと思うので以下の図をご覧ください。今回はベクトル・行列の計算に特化したライブラリであるNumPyを表しています。

numpy

NumPyのrandomというモジュールの中randintという関数があるという感じです。この本ではNumPy、Pandas、matplotlibについて説明されています。

※ここから先は高校〜大学1年程度の数学の知識が必要な場面が出てきます。私は理系なのでそこで困ることはなかったのですが、「プログラミング以前に数学的にわからない」という状況になったら数学の勉強もするしかないです。「高校~大学1年程度の数学」といっても、その範囲の数学が完璧である必要は全くないので、プログラミングの勉強をしながら知らない話が出てきたら調べて勉強する程度で大丈夫です。

NumPy

Numpyの章では主にndarray配列を使って計算をしていきます。このあたりから少しづつ複雑になり、エラーが出てしまうこともあると思います。エラーが出たらそのエラー文をそのままコピペして検索してみましょう。多くのエラーは過去に同じ体験をした人が解決方法を書いてくれています。先ほどから「分からなかったり、忘れてしまったら検索」といっていますが、欲しい「情報を検索できる」というのも重要な能力です。 NumPyは画像処理だけでなくPythonを使う開発であれば、大体使われると思うので、何をやっているのかはしっかり理解していきましょう。

Pandas

この章ではPandasについての説明がされています。Pandasとは一般的なデータベースにて行われる操作が実行でき、数値以外にも氏名や住所といった文字列データも簡単に扱うことができるライブラリです。なのでデータ分析をする際には欠かせないライブラリとなります。

matplotlib

この章ではデータの可視化をすることができるmatplotlibというライブラリの説明がされています。データ分析をする上で可視化することは非常に有効な手段の1つであるので、こちらもデータ分析には欠かすことのできないライブラリとなります。

機械学習とは

ここまでで基礎的な知識は揃ったので、ようやく機械学習の実装に入っていきます。機械学習といっても大きく以下の3種類に分けることができます。

蓄積されたデータを元に新しいデータや未来のデータ予測、あるいは分類を行う。株価予測や画像認識が当てはまる。

蓄積されたデータの構造や関係性を見出すことを意味する。 小売店の顧客の傾向分析などで用いられる。

報酬や環境などを設定することで学習時に収益の最大化を図るような行動を学習する。 囲碁などの対戦型AIで用いられることが多い。

machinelearning

(引用元:機械学習をどこよりもわかりやすく解説! 教師ありなし学習・強化学習だけでなく5つのアルゴリズムも完全理解!

ここでは画像認識を学ぶので、教師あり学習にあたります。 また、機械学習を実装する流れは以下のようになります。

  1. データを集める
  2. データクレンジング
  3. 機械学習手法でデータを学習
  4. テストデータで性能をテスト
  5. 機械学習モデルをWebなどに実装

2のデータクレンジングとは、データをモデルが学習しやすい形に加工する作業です。本書ではモデルを作成し、学習させるところをメインに扱っていますが、実際には重要な部分なのでしっかり理解してください。

また、深層学習というのは機械学習の手法の1つであり、上で述べた3のモデルを作るという場面で登場します。この本では、まず、深層学習ではないモデルで上の流れを1通り紹介して、最後にCNNという画像処理によく使われる深層学習を紹介されています。

機械学習の実装

この章ではまず、教師学習で用いられる機械学習のモデルが数種類紹介されています。具体的にはロジスティック回帰、SVM、決定木、ランダムフォレスト、k-NN等が紹介されています。これらのモデルはそれぞれ特有のハイパーパラメータを持っているので、それらの調整(チューニング)をするところまで学ぶことができます。

深層学習の実装

ここではMNISTという手書き文字のデータセットを用いて、簡単な深層学習の実装をします。この章を最後まで読み終えると、手書き数字画像データから数字を判別できるコードを書けるようになります。具体的にはディープニューラルネットワーク(DNN)という、層が何層にも重なったモデルを作り、DNNのハイパーパラメータをチューニングします。この流れはこれまでのDNN以外の機械学習の流れと同じです。

mnist (引用元:初心者のための畳み込みニューラルネットワーク(MNISTデータセット + Kerasを使ってCNNを構築)

CNNの実装

ついに最後の、画像認識でよく用いられるCNNを実装する章です。画像認識とは、画像や映像に映る文字や顔などいった「モノ」や「特徴」を検出する技術です。具体的には、画像の分類やモノの位置の推定など様々な認識技術が挙げられます。CNNとは畳み込みニューラルネットワークのことでConvolutional Neural Networkの略となります。

CNNは畳み込み層プーリング層と呼ばれる層をいくつも重ねていくことで形成されます。 畳み込み層では、入力データの一部分に注目してその部分画像の特徴を調べます。例えば顔認識をする場合は、適切に学習が進むと、入力層に近い畳み込み層では線や点といった低次元な概念の特徴に、出力層に近い層では目や鼻といった高次元な概念の特徴に注目するようになります。 プーリング層は畳み込み層の出力を縮約しデータの量を削減する層と言えます。畳み込み層での畳み込みを行うと、同じような特徴が近くにあったり、うまく特徴を見つけることができない場所が出てきたりして、畳み込み層からの出力には無駄があります。プーリング層ではそのようなデータの無駄を削減し、情報の損失を押さえながらデータを圧縮します。一方、細かい位置情報などは失われてしまいますが、逆にこれで元の画像の平行移動などの影響を受けにくくなります。例えば、手書き文字認識を行う場合、数字の位置情報はあまり重要ではないのでそれを削除し、位置の変化に強いモデルとなります。

これらの層はKerasTensorFlowというライブラリを使うと簡単に実装することができます。ここでは先ほども登場したMNISTに加え、10種類のカラー画像のデータセットであるCIFAR10を使って、CNNの実装をしていきます。

cifar10 (引用元:[ 人工知能に関する断創録 ] )

データの水増し・転移学習

画像認識では、画像データとそのラベルの組み合わせが大量に必要になります。しかし、モデルを学習させるのに十分な量のデータセットを揃えることができないことは多々あります。そこで、画像の水増しというテクニックを使って、持っているデータの量を増やすことができます。具体的には、画像を反転・ずらし・色を変えるなどして、新たなデータを作ります。 この作業もKerasのImageDataGeneratorを使うと簡単に実装することができます。ImageDataGeneratorには多くの引数が存在し、それらの値を調節することで、画像を水増しすることができます。引数についての詳しい説明はこちら

imagedatagenerator (引用元:[ Wild Data Chase -データを巡る冒険- ] )

また、大規模なニューラルネットワークを学習させるには、膨大なデータと時間が必要となります。そこで、「大量のデータですでに学習され公開されているモデルを使って新たなモデルを作ろう」というものが転移学習です。KerasではImageNetで学習済みの画像分類モデルとその重みをダウンロードして、使用します。公開されているモデルは数種類ありますが、ここではVGG16というモデルを使います。VGG16は1000クラスの分類モデルなので、出力ユニットは1000個ありますが、最後の全結合層は使わずに途中までの層を特徴抽出のために使用することで転移学習に用いることができます。

vgg16 (引用元:[ 初心者のためのAI人工知能テクノロジーブログ ] )

最後に

今回は、どういう風に勉強したかも交えながら、初めて読んだ本の内容を紹介してみました。前半は基礎的な話でPythonを使う際には必ず必要になる知識だと思います。後半部分は画像認識の話でしたが、ぜひ、これらの技術を使って何かアプリを作ったりしてアウトプットしてみてください。 また、他にも自然言語処理や時系列データの分析などPythonできることはたくさんあるのでそれらを学んでみるのもいいと思います。

書籍情報

Pythonで動かして学ぶ! あたらしい深層学習の教科書 機械学習の基本から深層学習まで」

著者:株式会社アイデミー  石川 聡彦.
発売日:2018年10月22日.
出版社:翔泳社.
価格:3,200円 (税抜).