株式会社RevCommのブログ - TECH PLAY

TECH PLAY

株式会社RevComm

株式会社RevComm の技術ブログ

185

Research Engineerの大野です。 新しいスライドを公開したのでご紹介します。 speakerdeck.com スライドの概要 ここでは、機械学習モデルの学習の際のメモリ要件を下げることができる、Low-Rank Adaptation (LoRA) の評価を行いました。 言語モデルの大きさが加速度的に増えており、それに伴いハードウェアの要件が大きくなっています。第1回LLM勉強会の資料の図を下記に引用し、近年に作成された言語モデルのパラメータ数を示します。 第1回LLM勉強会の資料の図 評価では、対話要約のデータセットである[Gliwa 19]のSAMSumコーパス(コーパスへのリンク)を使用し、ファインチューニングに必要な時間とGPUメモリを計測しました。また、ROUGEスコアを使用してそのモデルの性能を評価しました。実験では、NVIDIA A10G Tensor Core GPUを持つAWS EC2のg5.xlargeを使用しました。 スライドの結論 評価の結果、LoRAを使用することで、ファインチューニングに必要なメモリが低減することを確認しました。また、同じデータ量を使用した場合には、大きなモデルが小さなモデルと比べて性能が高いことも確認しています。このことは[Kaplan 20]でも示されています。 [Kaplan 20]の図 LoRAの関連研究はこのスライドの中で扱っていません。[Lialin 23]や[Hu 23]が関連研究をまとめていますので、興味がある方がいらっしゃいましたらご確認ください。これらに載っていない関連研究では、[Zhang 23]や[Dettmers 23]が注目を集めています。 引用 [Dettmers 23] Dettmers, T., Pagnoni, A., Holtzman, A., and Zettlemoyer, L.: QLoRA: Efficient Finetuning of Quantized LLMs (2023) [Gliwa 19] Gliwa, B., Mochol, I., Biesek, M., and Wawer, A.: SAMSum Corpus: A Human-annotated Dialogue Dataset for Abstractive Summarization, in Proceedings of the 2nd Workshop on New Frontiers in Summarization, pp. 70–79, Hong Kong, China (2019), Association for Computational Linguistics [Hu 23] Hu, Z., Lan, Y., Wang, L., Xu, W., Lim, E.-P., Lee, R. K.-W., Bing, L., and Poria, S.: LLM-Adapters: An Adapter Family for Parameter-Efficient Fine-Tuning of Large Language Models, arXiv preprint arXiv:2304.01933 (2023) [Kaplan 20] Kaplan, J., McCandlish, S., Henighan, T., Brown, T. B., Chess, B., Child, R., Gray, S., Radford, A., Wu, J., and Amodei, D.: Scaling Laws for Neural Language Models (2020) [Lialin 23] Lialin, V., Deshpande, V., and Rumshisky, A.: Scaling Down to Scale Up: A Guide to Parameter-Efficient Fine-Tuning (2023) [Zhang 23] Zhang, R., Han, J., Zhou, A., Hu, X., Yan, S., Lu, P., Li, H., Gao, P., and Qiao, Y.: LLaMA-Adapter: Efficient Fine-tuning of Language Models with Zero-init At- tention (2023)
2023年7月5日(水)12:00 より、RevComm主催の技術勉強会を開催します。 revcomm.connpass.com イベント内容 近年、PythonのWebフレームワークとしてFastAPIが注目されています。 一方で、Djangoもバージョンアップを重ね、より使い勝手の良いものに。 そんな2つのWebフレームワークをRevCommは使い分けています。 RevComm はPythonを得意とする開発者が多く在籍するスタートアップ。 AIを活用した音声コミュニケーションのイノベーションに取り組んでおり、 米国の「Forbes AI 50 2023」にもアジアで唯一選出されています。 レブコム、米国の「Forbes AI 50 2023」に選出 アジアで唯一、最も有望なAI活用企業として表彰 今回のイベントでは、RevCommの開発者がDjangoに感じている魅力と今注目して取り組んでいることを題材に、質問をどんどん取り入れながら、フランクにディスカッションしていきます。 日々課題に取り組んで得た知見は、社外の方にもきっと役に立つはずです。 参加登録はこちら 登壇者 近藤 智哉(こんどう もとや)Backend Engineer tech.revcomm.co.jp 小門 照太(こかど しょうた)Backend Engineer tech.revcomm.co.jp モデレーター 小山 功二(こやま こうじ)Frontend Engineer / Dev Relチーム所属 参加登録 参加登録は、connpassにて受け付けております 。奮ってご参加ください。
はじめに こんにちは。RevCommでCorporate Engineeringチームで活動している川添です。 今回のブログでは、オンライン申し込みからお客様専用の環境構築までの一連の業務プロセスを自動化するテクニックについて解説していきたいと思います。業務効率化や自動化に興味がある方はぜひ参考にしてみてください。 自己紹介 まず自己紹介をしておきますと、私は2020年7月にRevCommに入社して、主にCorporate Engineering領域で活動しつつ、フルスタックチームとしても社内チーム横断プロジェクトの管理などをしております。 これまでの経歴としては、ITコンサルティングの会社で基幹システム導入など、いわゆるSIer業務のようなことを行い、業務プロセスとシステムの整理や設計などをやってきました。今回はそのような経験も活かして、どのようなシステムを構築したかをお話ししたいと思います。 1. プロジェクト背景 これまで、弊社ではオンラインで契約やお客様用のテナント発行までを完結することができるものはありませんでした。そのため、お客様がMiiTelを利用するためには、必ず弊社のメンバーと商談や打ち合わせをする必要があり、気軽に製品を試したいというニーズにお答えすることができていない部分がありました。 そこで、今回 MiiTel Meetings (旧称: MiiTel for Zoom )の名称変更に伴う無償提供キャンペーンに合わせて、顧客満足度向上や作業効率化を目指し、オンライン申し込みのシステムを開発するプロジェクトが発足しました。 2. 業務プロセスを整理する まず、どの部分を自動化すべきかを見極めるために、現行の業務プロセスをしっかりと理解することが重要です。業務プロセスを整理し、それぞれのステップがどのように影響しているかを把握する必要があります。この段階で各チームとの協力が必要となります。各部門の意見を収集し、業務プロセスを再設計することが求められます。 私はこの段階で業務フロー図を必ず作成するようにしています。 言葉でのやり取りは曖昧なもので、同じ会社のメンバーでもそれまでの経験や、所属しているチームによって言葉の定義が違ったり、イメージしているものが異なったりします。そのため、業務フロー図を書いて、認識齟齬をなくし抜け落ちているプロセスがないかを洗い出します。 この部分の整理が甘いと、後の開発のプロセスで手戻りが多くなってしまうので、丁寧に進めることをおすすめします。 イメージ図: オンライン申込み業務フローチャートサンプル 2-1. システム化するポイント・しないポイントの整理 業務フロー図を作成している中で、システム化(自動化)するポイントとしないポイントの整理も行います。オペレーションを自動化しない場合は、社内のどこかの部署にオペレーションを担当していただくことになります。 システム化すべき部分を見極めるのは難しいですが、「実装工数」・「確証性」・「納期」などがポイントになってくると思います。 「実装工数」や「納期」は言葉の通りです。「確証性」は、そのオペレーションが今後変更される可能性(逆に言えば変更されない可能性)がどれだけあるかを意味して書いています。オペレーションのやり方が今後ガラッと変わったり、細かくても何度も変わる可能性があれば、自動化しないほうがいいでしょう。 逆にオペレーションが変わらない可能性が高ければ、自動化してしまったほうがより効率的な仕組みを作ることができます。 3. アジャイル的なプロトタイプ開発 プロセスの整理ができたら、次にプロトタイプを作成しましょう。プロトタイプを作成する目的は、システムの全体像を把握し、各チームとの調整をスムーズに進めることにあります。アジャイル開発手法を取り入れることで、段階的にシステムを改善・拡張しながら、各チームと連携してより良いシステムを目指します。 見落としがないように進めたとしても、作ってみると意外と抜けているポイントがあったり、「実際やってみるとこうしたほうがいい」ということが発生しがちです。そのため、納期ギリギリに初めて動くものができましたというやり方より、小さく作って認識合わせを細かくするほうがいいと思います。 実際に今回のプロジェクトでも、解約のフローに関してのプロセスの見直しが起きました。解約を受け付ける際のデータの流れに各チームでの認識齟齬があり、プロトタイプ実装中に問題が発覚し再検討・再実装が必要となった部分がありました。 業務プロセスに関わるものは、部署やチーム横断的に多くの人が関わることが多いです。そのため、ボールの取りこぼしというのは絶対に発生するもの、くらいの認識で進めるといいかもしれません。 私の場合は、プロセスの見直しがある前提で、見直すべき箇所をいち早く見つけるためにプロトタイプでの実装をよく使います。 4. 全体のシステム構成 前置きが長くなってしまいましたが、ここからが実際のシステムの話です。実際にどのようなものをどのような技術で構成したかをお話したいと思います。 まず具体的な機能を紹介します。当システムではお客様が専用フォームに必要事項を入力するだけで、独自の環境が自動的に構築されるとともに、MiiTel Meetingsを使うためのセットアップも自動的に実行されます。 さらに、お客様が当システムを利用するにあたって、事前に利用規約に同意することが必須となっております。利用規約ページでお客様により同意操作が行われた際に、お客様のアカウント情報が自動的にメールで送信される機能も備えています。 以上により、お客様はオンライン上のフォーム入力だけでMiiTel Meetingsの利用を開始できます。 続いて、上記のシステムを構築するために用いた技術スタックを紹介します。システム全体の俯瞰図は以下のとおりです。 オンライン申込みシステム俯瞰図 4.1 インフラ インフラは全てAWS上で、後述のとおりサーバーレスアーキテクチャとなっています。常時アクセスされるものではないので、必要なときに必要なだけ稼働する仕組みにしています。 IaCはTerraform でベース部分は構築しており、バックエンドのAPIの管理はServerless Framework を活用しています。 4.2 フロントエンド 社内でも広く使われているReactを用いました。(create-react-app でプロジェクト作成しています) オンライン申込みのフォームはWordPressで構築されているプロダクトLPに置くので、最終的にはビルドしたファイルをサーバに置くようにGitHub ActionsでCI/CDを組んでいます。 また、ページがサブディレクトリ配下でも正しく動くように、以下の設定をいれています。 package.json { " name ": " revcomm-online-application-front ", " version ": " 0.1.0 ", " private ": false , " homepage ": " https://miitel.com/jp/cpform/meetings/ ", … } 実装においては、使いやすく、わかりやすいUI/UXを心がけるようにしました。ページのデザインは社内のデザイナーが担当したものになりますが、使いやすさの観点でいくつか工夫をしています。 例えば、入力ミスがあった場合にはわかりやすいメッセージを出したり、メッセージの箇所までスクロールで遷移させたりしてお客様が気づきやすいように作っています。また、登録処理に時間がかかる場合は途中で離脱してしまわないよう、処理の進捗状況を画面に表示させてお客様がどれくらい待てばよいのかを知らせています。 後述しますがバックエンドはAWS Lambdaで実装しています。そのため、立ち上げがスムーズになるように、ページにアクセスされたときにヘルスチェックリクエストを送ってあらかじめバックエンドを立ち上げておく工夫も行っています。 const TopPage = ( props ) => { // コンポーネント初期化時にバックエンドを立ち上げておく useEffect (() => { (async () => { await APIClient. get( "/api/health" ); } )(); } , [] ); } 4.3 バックエンド バックエンドはAWSのサービスを利用しています。API GatewayとAWS Lambda上に、PythonのFast APIというWebフレームワークを用いて構築しています。 データベースは社内の既存システムを利用しているため、このシステム自体に対するデータベースはありません。 そのため、軽量かつバリデーションも簡単でありながらしっかりと行えるFast APIを選択しました。 バックエンドの実装で特に気をつけたポイントは、社内では「Adminエンドポイント」と呼んでいるAPIを実装しておいたことです。 これは、お客様が処理の途中で離脱してしまった場合や、社内オペレーションミスなどで特殊な作業をする必要があった場合に、データの整合性を後で取る運用のためのエンドポイントです。 あらかじめ作っておくことで、安心感も違いますし、柔軟な対応ができています。 4.4 バッチ 申込時に途中離脱が発生したかどうかをチェックしたり、解約の意思があった際に自動的にお客様の環境を停止したりする処理には、AWS Batchを利用しています。 そこまで重たい処理でもないのですが、一部処理に時間がかかる可能性があったり、処理件数が増えてきた場合に備えて初めからAWS Batchを使って実装しています。 4.5 監視 弊社ではDatadogをログやパフォーマンスの監視に用いているので、このサービスにおいてもそれを組み込んでいます。アラートを設定しておいて、何か問題があったときに検知できるようにしています。 お客様はこういった申込みシステムで、何かあったら問い合わせなどはせず離脱して終わってしまうことが多いので、何かあったときにシステムが検知できるかどうかは非常に重要です。 5. 結果と展望 プロセス整理から始まってリリースを行い、問題なくテナント発行がされる仕組みが構築できました。実際にお客様からの申し込みもあり、社内の対応を効率的に行うこともできています。 今回は、MiiTel Meetingsの無償キャンペーンのみの対応ですが、今後はより幅を広げて、より多くのお客様にRevCommのプロダクトを体験してもらうことができる仕組みを作りたいと考えています。
こんにちは、RevCommでMiiTelの音声解析機能に関する研究開発を担当している石塚です。 石塚賢吉(いしづか けんきち) プリンシパルリサーチエンジニア。筑波大学大学院博士後期課程卒業。博士(工学)。日本HP株式会社にて通信事業者向けのシステム開発、株式会社ドワンゴで全文検索システムの開発などに従事。2019年12月、株式会社RevComm入社。音声認識、音声感情認識、全文検索システムの研究開発を行なっている。 → 過去記事一覧 2023年1月に開催された国際会議 IEEE Workshop on Spoken Language and Technology (SLT) 2022 で発表された E-Branchformer: Branchformer with Enhanced Merging for Speech Recognition (Kim et al., 2023) *1 という論文で、音声認識タスクで高い性能を発揮する E-Branchformer という新しい深層学習モデルが提案されました。論文中では英語の音声コーパスを用いて音声認識精度が評価されていますが、日本語についての評価は行われていません。 End-to-end音声処理ツールキット ESPnet のversion 202301からこのE-Branchformerが利用可能となったので、本記事では日本語の音声コーパスである 日本語話し言葉コーパス (Corpus of Spontaneous Japanese; CSJ) (Maekawa, 2003) *2 でモデル構築および音声認識の精度・スピードの評価を行ってみます。 本記事の内容は以下のとおりです。 E-Branchformerとは 日本語音声コーパスCSJでの利用方法 得られたモデルの音声認識の精度とスピード モデルの学習 音声認識精度の評価 音声認識スピードの評価 まとめ E-Branchformerとは 2020年のINTERSPEECH で発表された Conformer: Convolution-augmented Transformer for Speech Recognition (Gulati et al., 2020) *3 において、Transformerとconvolutional neural network(CNN; 畳み込みニューラルネットワーク)を組み合わせた深層学習モデルである Conformer が提案されました。Conformerでは、TransformerのMulti-head self attentionでグローバルなコンテキスト情報を捉え、CNNでローカルなコンテキスト情報を捉えます。Conformerをエンコーダーに使用した音声認識モデルは、Transformerの音声認識モデルよりも高い音声認識精度を発揮し、発表時点におけるstate of the art(SOTA; 最高性能のモデル)でした。 ConformerのEncoderの構成 このConformerに触発され、 2022年のInternational Conference on Machine Learning (ICML) で発表された Branchformer: Parallel MLP-Attention Architectures to Capture Local and Global Context for Speech Recognition and Understanding (Peng et al., 2022) *4 において、並列に分岐する2つのブランチで情報を捉える Branchformer が提案されました。上記の図のように、ConformerではMulti-head self attentionモジュールの後にCNNで構成されるConvolutionモジュールが続く形となっており、グローバルなコンテキスト情報とローカルなコンテキスト情報を 逐次的に 扱っていました。一方で、Branchformerでは下記の図のように、1つ目のグローバル抽出ブランチでMulti-head self attentionによりグローバルなコンテキスト情報を捉え、もう1つのローカル抽出ブランチでconvolutional spatial gating unitを利用してローカルなコンテキスト情報を捉えます。そして、後段で 各ブランチの出力をマージ することにより、音声認識タスクでConformerと同等の性能を達成しています。なお、Branchformerのマージモジュールでは、 をグローバル抽出ブランチによる出力、 をローカル抽出ブランチによる出力としたとき、 と を連結し、線形射影で次元を削減します。 ここで、 は線形射影の学習可能な重みを表します。 BranchformerのEncoderの構成 そして、 E-Branchformer はBranchformerのマージ処理部分を改良したものとして提案されました (Kim et al., 2023)。Branchformerでは、2つのブランチの出力は点別かつ線形に結合されていました。E-Branchformerでは局所的な情報の統合を強化するために、マージモジュールにdepth-wise convolusionによる深さ方向の畳み込みを導入し、2つのブランチからの情報を結合する際に ローカルな情報とグローバルな情報を逐次的かつ並列的に組み合わせる ことで、Branchformerを強化しています。 をグローバル抽出ブランチによる出力、 をローカル抽出ブランチによる出力としたとき、E-branchformerではこれらを下記のようにマージします。 ここで、 はdepth-wise convolusion、 は線形射影の学習可能な重みを表します。また、E-Branchformerでは、TransformerやConformerにならい、グローバル抽出ブランチ・ローカル抽出ブランチ・マージモジュールを挟み込む形でfeed forward network (FFN) のモジュールが追加されています。E-Branchformerのエンコーダーの構成は、上記Branchformerのエンコーダーの構成の図のBranchformer blockを下記のE-Branchformer blockに差し替えたものになります。 E-Branchformer blockの構成 なお、E-Branchformerの論文中では、 LibriSpeech (Panayotov et al., 2015) *5 という英語の音声コーパスを利用して音声認識モデルを構築し、音声認識精度の確認が行われています。以下では、日本語の音声コーパスCSJでE-Branchformerの音声認識モデルを構築し、音声認識精度とスピードをConformerの音声認識モデルと比較してみます。 日本語音声コーパスCSJでの利用方法 E-Branchformerは ESPNetのv.202301 から利用できる状態になっていますが、日本語の音声コーパスに対応したレシピはまだ存在しない状態です。そこで、 英語の音声コーパスであるLibriSpeechのレシピの中にあるE-Branchformerの設定 を、日本語音声コーパスCSJのレシピにコピーして利用します。 ESPNetのv.202301の学習環境とCSJのセットアップが完了した状態から、E-Branchformerを用いた音声認識モデルを構築する手順は下記のとおりです。 LibriSpeechのレシピの中にあるE-Branchformerの設定ファイル train_asr_e_branchformer.yaml を CSJレシピのconfディレクトリ の配下にコピーする CSJレシピの学習スクリプトの学習設定ファイルの参照先 をasr_config=conf/train_asr_e_branchformer.yaml にする run.sh を実行する 得られたモデルの音声認識の精度とスピード モデルの学習 本実験では、NVIDIA V100を4つ搭載した ABCI のrt_Fノードで音声認識モデルの学習を行います。デフォルトの学習設定のバッチサイズではout of memory (OOM) が発生してしまうので、batch_binsの設定を1/8 (140,000,000 / 8 = 17,500,000) にしました。また、言語モデルは使わないこととします。 CSJの学習データを用いてデフォルトの80エポックまで音声認識モデルの学習を行ったところ、学習曲線は下記のようになりました。なお、音声認識モデルの学習には約130時間かかりました。 音声認識精度の評価 上記で学習したE-Branchformerの音声認識モデルを用いて、言語モデルなしでCSJのテストセットを音声認識したときのCharacter Error Rate(CER; 文字誤り率)と、 学習済のConformerのモデル を用いて、同じく言語モデルなしでCSJのテストセットを音声認識したときのCERを下記の表に示します (%)。表を見ると、 E-BranchformerのほうがConformerよりCERが0.3から0.7ポイント低く、音声認識精度がよい ことがわかります。 テストセット E-Branchformer Conformer eval1 3.8 4.5 eval2 2.9 3.2 eval3 3.2 3.5 音声認識スピードの評価 次に、NVIDIA A10を搭載するAWSのg5.xlargeインスタンスを用いて、CSJのeval1からeval3のテストセットをGPUで音声認識するときのスピードを下記のReal Time Factor (RTF) の指標で確認しました。 なお、batch_sizeは1、beam_sizeは20としました。E-BranchformerとConformerの音声認識モデルでCSJのテストセットを音声認識したときのRTFを下記の表に記載します。表から分かるとおり、 認識スピードはConformerよりE-Branchformerのほうが少し遅い 結果となりました。 E-Branchformer Conformer 0.297 0.268 まとめ 本記事では、E-Branchformerについて簡単に紹介し、日本語の音声コーパスCSJでE-Branchformerの音声認識モデルを構築し、認識精度および認識スピードについてConformerの音声認識モデルと比較しました。結果として、E-Branchformerモデルの方が、Conformerモデルより高い精度で音声認識ができることがわかりました。一方で、今回構築したE-Branchformerの音声認識モデルは、Conformerの音声認識モデルよりも音声認識処理に少し時間がかかりました。 なお、今回の実験では、E-BranchformerをAttentionベースのEncoder-Decoder音声認識モデルのエンコーダーに適用しましたが、E-Branchformerはconnectionist temporal classification (CTC) やTransducerなどのリアルタイム処理に適した高速な音声認識モデルに適用することもできるようです。認識スピードを重視する場合は、CTCやTransducerとの組み合わせを試してみるとよいかもしれません。 *1 : Kim, K., Wu, F., Peng, Y., Pan, J., Sridhar, P., Han, K. J., & Watanabe, S. (2023). E-Branchformer: Branchformer with Enhanced Merging for Speech Recognition. Proc. IEEE Spoken Language Technology Workshop (SLT) , 84–91. *2 : Maekawa, K. (2003). Corpus of Spontaneous Japanese: Its Design and Evaluation. Proc. ISCA/IEEE Workshop on Spontaneous Speech Processing and Recognition (SSPR) , 7–12. *3 : Gulati, A., Qin, J., Chiu, C.-C., Parmar, N., Zhang, Y., Yu, J., Han, W., Wang, S., Zhang, Z., Wu, Y., & Pang, R. (2020). Conformer: Convolution-Augmented Transformer for Speech Recognition. Proc. INTERSPEECH , 5036–5040. *4 : Peng, Y., Dalmia, S., Lane, I., & Watanabe, S. (2022). Branchformer: Parallel MLP-Attention Architectures to Capture Local and Global Context for Speech Recognition and Understanding. Proc. International Conference on Machine Learning (ICML) . *5 : Panayotov, V., Chen, G., Povey, D., & Khudanpur, S. (2015). LibriSpeech: An ASR Corpus Based on Public Domain Audio Books. Proc. IEEE International Conference on Acoustics, Speech and Signal Processing (ICASSP) , 5206–5210.
はじめに こんにちは。RevCommでPBX (電話交換システム) サーバーの開発等を担当している宮崎です。 RevCommは、電話やオンライン通話による営業活動や顧客応対を支援するビジネス向け通話アプリケーションのMiITelを提供しています。PBXはMiiTelの通話機能を担う重要な技術の一つです。 このたび、ソフトウェアPBXとして世界的に広く活用されているオープンソース「Asterisk」の世界最大のカンファレンスであるAstriCon 2023に参加してきました。 本記事では、AstriCon 2023のイベントや発表の概要、イベント参加を通して知ったことを共有します。 AstriConについて AstriConはSangoma Technologies社の主催するオープンソースのPBX製品「Asterisk」に関する世界最大のユーザーカンファレンスです。 2004年の初回開催以来、ほぼ毎年アメリカの都市で開催されており、1000名近くが参加した年もあったようです。 新型コロナウイルスの影響で2020年、2021年はオンライン開催、2022年は中止となっており、今回は4年ぶりに現地での開催となりました。 開催地: Broward County Convention Center, Fort Lauderdale, Florida 開催期間: Feb 14-16, 2023 参加者: 50名程度 参加者は想像していたより少なく、ベテランの方が多い印象でした。 また、我々以外に日本人の参加者はいないようでした。 会場の様子 初日はスポンサー企業限定のレセプションパーティで、我々は2、3日目に行われた講演形式の発表を聴講しました。 発表の中で特に興味深かった内容をピックアップして紹介します。 Asteriskの最新情報について Sangoma Technologies社のAsteriskプロジェクトリーダーであるJoshua C. Colp (JColp) 氏からAsteriskの最新情報に関する紹介がありました。内容をいくつか紹介します。 E911 (緊急通報) 対応 米国では、E911 (Enhanced 911) と呼ばれる緊急通報サービスがあります。 これは、緊急通報時に自動的に発信元の位置情報 (通常は住所) を緊急通報センター (PSAP: Public Safety Answering Point) に提供する機能です。 AsteriskはE911に対応するため、res_geolocation, res_pjsip_geolocationモジュールを実装しました。 これらによりSIP INVITE messageのSDPに含まれる、位置情報を示すPIDF形式 (XML形式) のデータを追加したり読み取ったりすることができるようになりました。また、これらのデータはMulti-partで送信するとUDPのパケットサイズを超える場合があり、TCPかTLSで送る必要があるとのことでした。 参加者から、実際に使えるのか?という質問があがりましたが、現在のところ1キャリアで試したのみで、本格的な活用はこれから広がっていくものと思われます。 音声認識専用プロトコル (res_aeap) これまでAsteriskを外部の音声認識エンジンと連携させる場合は、各サードパーティごとに用意されたモジュールを使用する必要があり、対応していない音声認識エンジンと連携したい場合は、出力される録音データを外部のアプリケーションでキャプチャして送信する方法しかありませんでした。 今回発表されたAEAP (Asterisk External Application Protocol) は、WebSocketを経由して外部アプリケーションに音声情報を簡単に連携することができます。 サンプルコードも公開されており、簡単にリアルタイムの音声文字起こし (Speech To Text) を実装することができたので、今回のAstriCon参加レポートを社内発表する際、res_aeapを用いて実装した簡単なデモを作成して共有しました。その内容については後述します。 その他アップデート そのほかAsteriskに関しては、ソースコード管理をGerritからGitHubに移動予定であることや、Asterisk用のテストツールであるTest SuiteをPython2 から Python3に書き直したことなどが発表されていました。FreePBX (Asteriskに管理画面がついた商用製品) に関するアップデート等も紹介されていました。 最後に、JColp氏から会場の参加者に利用しているAsteriskのバージョンを質問する場がありました。意外にも、サポート外のバージョンを使っている参加者や、サポートされているかわからない人がたくさんいるということが印象的でした。 サポートされているバージョン (v18, v20) を使っている人:数人 Security fix only (v16, v19) を使っている人:数人 サポートされていないバージョンを使っている人:たくさん サポートされているかわからない人:たくさん Asteriskの活用例 Asteriskの各社における活用例を紹介する内容が多く発表されていました。 業務における利用シーンや、利用している機能・アーキテクチャなどを紹介しており、自社サービスに組み込んでいるケースや、社内利用しているケースもありました。 面白いと感じた内容をいくつか紹介します。 社内業務での活用例 大手量販店チェーンのTARGET社では、店舗の従業員が使うモバイルアプリのバックエンドにAsteriskを採用しているとのことです。 以前は顧客から電話が入った際に従業員をWalkie-Talkie (トランシーバー) で呼び出し、事務所の電話を取らせるというオペレーションでしたが、これを内製のモバイルアプリとハードフォン宛に転送するように改善しました。TARGET社は小売業者でありながらTech CompanyとしてもさまざまなOSSの公開などの活動をしているようです。 金融情報サービス大手のBloomberg社も、社内向けに開発したAsteriskのサービスについて紹介していました。Bloombergでは頻繁に電話を使うこともあり、Asteriskベースの電話システムをバージョンアップを繰り返しながら10年くらい運用しているとのことでした。 ターミナルライクなUIが好まれるため、Slackのようなチャットアプリを社内を独自開発し、アプリ内でクリックtoコールもできるようにしているとのことです。電話代の削減のため、UKからUSに国際電話をするようなケースでは、USのAsteriskを経由させる運用をしています。Asteriskを選んでいる理由は安定しており、標準的なSIPを利用できるからとのことでした。 Asterisk - Kubernetes Kubernetes (K8s)によるAsteriskのクラスタ化に関するトピックが2件ありました。 Stratus Talk社の方による発表では、Stratus TalkというクラウドPBXをEC2のK8sで構成して運用しているとのことでした。単一のノードにおける同時通話を負荷テストすると100コールくらいで音声に途切れが出始めたので、100コールを目安にスケールアップしているとのことです。クラスタはRancher で管理し、Prometheusで監視しているとのことでした。Extension (内線番号) を持たせず、個人宛の通話は全てDID (Direct Inward Dialing) によって実現しているという話でした。 引用: https://github.com/voxoco/k8s voxo社のCTOであるJoe氏もK8sによるAsteriskのクラスタ化に関するトピックで発表していました。voxo社ではKamailioをフロントに置いたk8sの構成を組んだプラットフォームをOSSで公開しています。構成上のポイントとして、共通して使う情報をGlobal KVSに置く点、HPA (Horizontal Pod Autoscaler) を使ったスケールダウン、スケールアップのポリシー, サイジングに関してはCPUに気を配る点、4コアのCompute Optimizeのインスタンスをつかっており、200chを目安にオートスケーリングを入れている点を紹介していました。 Natural Language Intent Based IVR localsplash社のCEOであるDavid氏は、IVRで顧客の発話した内容から適切な呼び出し先に接続するサービスを提供しています。 David氏によるとIVRは、ボタン入力のみを受け付けるレガシーなIVRから、単語の音声入力を受け付ける少し賢いIVR、そしてNatural Language Intent Based IVRと言われる顧客の話した内容から意図を汲み取り、適切な呼び出し先に接続する賢いIVRへと変遷してきたといいます。 Localsplash社は顧客ごとにAsteriskかVicidialのPBXサーバーを提供しており、Spreadsheetにインテント(意図)とキーワードを登録すると、Dialplanが自動的に作られる仕組みを開発しているとのことです。 Natural Language Intent Based IVRの導入後に直面した課題として、IVRの自動応答に対して実際に電話をかけてきた人の44%が何も喋らかったということをあげていました。 ”すみません、よく聞こえませんでした” などといった音声をさらに流すことでその後90%の人が喋るようになったそうです。 機械の音声に対して会話するという体験に慣れていないユーザーが多いので、「例えば、”住所変更がしたい” というようにおっしゃってください」など、どのように話せばいいかを例示するなどの工夫が重要であるとおっしゃっていました。 AstriConのイベントについては以上です。 AEAP を活用した ChatGPT 自動応答電話機能 AstriConの参加レポートを社内で発表した際に、Asteriskの新機能として紹介されていた音声認識専用プロトコル (res_aeap) を利用して自動応対ボットを開発し、そのデモを行いました。 デモの概要 このデモでは発信者の音声入力をres_aeapによるWebsocketでの音声データ送信でテキストに変換後、ChatGPTのAPIにリクエストを投げ、レスポンスのテキストを音声に変換して再生することで、ChatGPTと音声で会話する体験を提供します。 下記のような処理をおこなっています。 通話中に受け取った音声データを、AsteriskがWebsocketクライアントとなりサーバー (図中の aeap-speech-to-text) に送信する。 サーバーは、受け取ったデータをGoogle Cloud Speech APIに送信し、音声認識結果のテキストデータを受け取る。 受け取ったテキストを引数にしてpython3で実装したスクリプトchatgpt.pyを実行する chatgpt.py内で引数で受け取ったテキストをChatGPTのAPIに投げ、レスポンスのテキストを受け取る 受け取ったテキストを自社開発のText-To-Speech API (TTS API) に送り、テキストを音声に変換して受け取る 受け取った音声を再生する res_aeapによるSpeech APIへの音声送信 上記のデモにおけるres_aeapを使用した箇所について紹介します。 /etc/asterisk/aeap.conf [my-speech-to-text] type=client codecs=!all,ulaw url=ws://127.0.0.1:9099 protocol=speech_to_text @language=ja-JP Asteriskのaeap.confにmy-speech-to-textというSpeech engineを定義しています。 /etc/asterisk/extensions.conf [speech_to_text] exten=>550,1,log(NOTICE, 'Start IVR' ) same=> n,Wait(1.5) same=> n,SpeechCreate(my-speech-to-text) same=> n,SpeechStart() same=> n,Playback(please-wait) same=> n,Set(SPOKEN_TEXT=${SPEECH_TEXT(0)}) same=> n,log(NOTICE,${SPOKEN_TEXT}) same=> n,System(python3 /root/chatgpt/chatgpt.py /tmp/rec_${UNIQUEID}.wav ${SPOKEN_TEXT}) same=> n,SpeechDestroy() same=> n,Playback(/tmp/rec_${UNIQUEID}) same=> n,Return() extensions.conf の定義では、 Speech Recognition API の SpeechCreate() に先ほど定義したmy-speech-to-textを指定しています。 これにより、WebSocketクライアントはmy-speech-to-textに指定された URL への接続を試行します。 my-speech-to-textに指定した ws://127.0.0.1:9099 をListenするWebSocketサーバを、サンプルコードとして公開されている こちらのアプリケーション を利用して起動します。 このWebSocketサーバを経由してGoogle Cloud Speech APIに音声データを送信し、テキストデータを受け取ります。 実際に社内発表の中でデモを行った様子を公開します。   このように外部アプリケーションへの音声データの連携を簡単に実装することができました。 まとめ 今回はAstriCon2023への参加を通じて得た情報を紹介しました。 現地の肌感としては、思ったより参加人数が少なく、参加者の年齢層が比較的高かった事もありVoIP系の技術者は国内外問わずあまり増えていないのかな?という印象を受けました。現在、音声認識技術や対話型AIなどVoIP技術との相性が良さそうな技術への注目度が高まってきているので、今後VoIP系技術者の需要も高まっていくのではないでしょうか。 参考資料: http://lists.digium.com/pipermail/asterisk-users/2004-April/036295.html https://www.itexpo.com/east/astricon.aspx RevCommでは音声サーバエンジニアを募集しています。興味を持っていただけた方は、ぜひともご応募ください! hrmos.co
はじめに 対話要約手法の概要 抽出的要約 抽象的要約 その他 近年の研究 Pegasus DialogLM ChatGPT/GPT3 まとめ 引用 はじめに この記事は『 対話要約研究の最前線 前編 〜データセットと評価指標の紹介〜 』の続きです。 RevCommは電話営業や顧客応対の通話を支援するAI搭載型のIP電話「MiiTel」を提供しています。 この製品は、通話の文字起こしを保存する機能を備えており、RevCommは数千時間の対話データに接しています。 この対話データに対する支援の1つとして対話要約が考えられます。対話要約とは、入力された対話から、その主要な概念を含む、より短い文書(要約)を自動的に作成することです。 ユーザは、要約を作成する手間が省けたり、あるいは要約を読むことで対話の概要をより早く理解できるなどの利点があります。 本記事では、はじめに対話要約の手法の概要を書き次に近年の研究をいくつかご紹介します。 対話要約手法の概要 ここでは対話要約の手法の概要を説明します。今回の記事の趣旨は、最近の研究をいくつか紹介することなので、こちらについては簡単な説明にとどめます。 要約手法の分類方法は何種類かありますが、ここでは抽出的要約と抽象的要約の2つの分類方法に従います。要約を制限するためのクエリや、対話状態の管理方法についてはここでは触れません。また、要約手法ではありませんが、与えられた対話の概要を得るために用いられる手法を紹介します。 抽出的要約 入力文書の一部を切り出して要約文を作成する方法です。この手法における重要な課題は、入力文書のどの部分を切り出すのかを検討することです。例えば、重要度の高い文を選んだり、特定のトピックの文を選んだりする方法があります。 [Song 20]の図を引用し、抽出的対話の例を示します。ここでは、患者の発言のうち特定のトピックに関連するものを抜粋することで対話の要約を作成しています。要約であるSUM1は、対話のうち4番目と7番目の発話をくっつけたものです。 [Song 20]の図 抽象的要約 抽象的要約は、入力文書の主要な概念を含む文書を新しく生成する方法です。機械翻訳などを含むテキスト生成のタスクの一部といえます。 下記に[Chen 21]の図を引用して、抽象的要約の例を示します。要約対象の対話では顔文字や絵文字が多用されていますが、正解として定義される要約文ではそれらは使用されていません。抽出的手法とは異なり、要約文は入力の文書を単に抽出したものではなく、要約手法が生成したものであることがわかります。 [Chen 21]の図 その他 情報抽出の手法が与えられた文書の概要を得る方法として使用されていたのでご紹介します。この方法は、事前に要約のためのテンプレートを用意して、情報抽出を使用してその必要箇所を埋めていくことで要約を作成する手法であるとみなせます。 下記の2つの研究では、どちらも医者と患者の対話をまとめるために情報抽出の手法が使用されていました。この手法を適用することで、医師が対話録を読む手間を削減することができます。 [Kannan 18]では、患者の症状を得るために情報抽出を適用しました。下記に[Kannan 18]の図を引用します。固有表現抽出を用いたシンプルなベースラインでは約20%の症状しか検出できないことが、この研究のモチベーションとなっています。 [Kannan 18]の図 [Zhang 20]では症状だけでなく、検査や手術などの情報も対象にしています。ここでは、LSTMを使ったDeep Matching Modelsと呼ばれる手法を提案しています。手法への入力の単位としてwindowレベルとdialogueレベルがあり、dialogueレベルの場合には適合率が97%以上で情報を獲得できたと報告されています。 [Zhang 20]の図 近年の研究 以下では、対話要約に関する近年の研究を3つ紹介します。2つは論文の内容であり、1つはRevComm内で評価したものです。 Pegasus Pegasusは[Zhang 19]によって提案された文書要約モデルです。最近の研究を紹介する文脈で2019年の研究を紹介することに違和感があるかもしれません。ですが、PegasusはGoogleの製品に使用されており、昨今の文書要約モデルの代表のひとつとして選びました。 2022年11月のGoogleブログへの投稿 では、Google Chatに要約機能が追加され、そこにPegasusを使用していることが報告されています。また、 2022年3月のGoogleブログへの投稿 では、Google Documentに対する要約作成機能が紹介されており、Pegasusが触れられています。なお、Pegasusを拡張し、より大規模な文書を対象にした Pegasus-X が2022年に公開されていますが、ここではあくまでもPegasusを対象に説明します。 Pegasusは事前学習を工夫しており、その観点ではMasked Language Model (MLM) の発展といえます。MLMは文書の一部の単語を隠し、その隠された部分を推測することで事前学習を行います。この一部を隠した部分をマスクと呼びます。 Pegasusは単語のマスクだけではなく、文のマスクを作成し、それらを推測します。下記にPegasusの概要を示す図を論文から引用します。[MASK1]が文のマスクを示し、[MASK2]が単語のマスクを示します。Pegasusは事前学習において[MASK1]と[MASK2]を推測します。この文のマスクを推定することを、論文中はGap Sentences Generation (GSG) と呼んでいます。 [Zhang 19]の図1 著者らは、文のマスクの選び方について、数種類の方法を比較しています。縦軸は文書要約における性能を示し、ランダムにマスクを選んだ場合を1.0としています。横軸は実験に用いたデータセットです。"MLM Solely” の棒がMLM(単語のマスクのみ)の性能であり、これと他の色を比較すると、文のマスクが性能の向上に寄与していることが分かります。 [Zhang 19]の図2 性能評価の実験では、パラメーターのサイズに応じてBASEとLARGEの2種類のモデルが比較されています。BASEは事前学習の対象として単語と文の両方を推測していますが、LARGEでは文のみを推測にしています。 [Zhang 19]の図3 DialogLM [Zhong 22]は、対話要約モデルDialogLMを提案しています。DialogLMは、この論文の著者がMicrosoftのインターンシップの際に行った研究であり、 コード がMicrosoftのGitHubアカウントから提供されています。 DialogLMでは、先に紹介したPegasusと同様に事前学習が工夫されています。具体的にはWindow-based Denoisingと呼ばれる手法であり、5種類のノイズを対話文に入れて事前学習を行います。ここでの事前学習は、文書の一部の単語を隠し、その隠された部分を推測することです。 下図において、Windowがノイズを入れる前の対話文、Noisy Windowがノイズを入れた対話文です。[MASK]は隠された単語を示しています。このNoisy Windowの部分が本論文で工夫されている点です。 下記にこの論文の図を引用して、この工夫についてもう少し詳しく説明します。この論文では、事前学習において対話に入れるノイズとして、5種類のノイズが試されています。 [Zhong 22]の図1 それぞれのノイズの内容を下記に説明します。 Speaker Maskでは、話者の部分が隠されます。そして、その隠された話者を言語モデルに推定させます。 Turn Splittingでは、1つの長い発話が複数の発話に分割されます。そして、最初の発話の話者をそのまま残されますが、分割された2つ目以降の発話の話者を言語モデルに推定させます。 Turn Merginでは、複数の発話が1つの発話にまとめられます。最初の発話の話者はそのまま残されますが、それ以降の発話の話者は削除されます。 Text Infillingでは、発話内の単語が隠されます。 Turn Permutationでは、発話の順序が変更されます。 下図は論文から引用した実験結果です。この表で示されているのはForeverDreamingとTVMegaSiteというデータセットを使った場合の結果ですが、他にAMIとICSIというデータセットを使った結果も論文には示されています。ベースラインとして、LongformerやBARTなどが使用されており、提案手法はそれよりも性能が良かったことを示しています。 [Zhong 22]の図2 ChatGPT/GPT3 OpenAIが提供しているChatGPTとGPT3は要約に特化したサービスではありませんが、これを使って要約を行うことができます。そこで、ChatGPT/GPT3を用いた要約の性能について報告します。なお、ChatGPTやGPT3の概要については割愛します。 本記事では、性能を測るためのデータセットとしてSAMSumコーパスを用いました。SAMSumコーパスは、チャットでの短い対話から作成された対話要約のためのデータセットです。このコーパスは広く使用されており、評価結果をさまざまな研究と比較できます。さらに、学習済みの対話要約モデルが多く公開されています。 GPT3の評価では、OpenAIが提供するAPIから「text-davinci-003」モデルを選択しました。ChatGPTの評価では、「gpt-3.5-turbo-0301」モデルを選択しました。 ベースラインとしては、3つの学習済みモデルを使用しました。1つ目は philschmid/bart-large-cnn-samsum (以下、BART-large)で、これはBARTをCNN Daily Mailコーパスと SAMSumコーパスの2つの要約データセットでファインチューニングしたものです。2つ目は philschmid/flan-t5-base-samsum (以下、Flan-T5-base)で、これは flan-t5-base をSAMSumコーパスでファインチューニングしたものです。最後は jaynlp/t5-large-samsum (T5-large)で、最近の対話要約に関する論文の成果物のひとつです。 さらに、 UL2 と Pegasus の2つのモデルのスコアを参照しました。 UL2は2022年後半に提案されたモデルであり、そのモデルがSAMSumコーパスで最先端のスコアを達成したと主張されています。 Pegasus は、Googleが提供しているサービスの要約機能で使用されているモデルであり、最も洗練された要約モデルのひとつです。 評価指標としてBLEUとROUGEの2種類を使用しました。結果を下記の表に示します。 モデル BLEU ROUGE1 ROUGE2 ROUGEL ChatGPT 0.093 0.406 0.165 0.318 GPT3 0.111 0.423 0.171 0.340 BART-large 0.123 0.403 0.203 0.312 Flan-T5-large 0.212 0.516 0.274 0.431 T5-large 0.200 0.506 0.262 0.418 UL2 - - 0.296 - Pegasus - 0.523 0.283 0.4783 GPT3はChatGPTよりも高いスコアを獲得しました。そして、ファインチューニングされたモデル (BART-large, Flan-T5-base, T5-large) はGPT3よりも優れていることがわかります。この結果は、 2023年2月に発表された論文 で報告されている内容と一致します。ただし、この論文では実験の際に40GB程度の大きなモデルを使用しているため、小さなモデル(例えば、T5-largeは3GB程度です)がGPT3およびChatGPTの性能を上回るという事実は、この実験で初めて明らかになりました。 Flan-T5-largeはGPT3/ChatGPTと比較して小さなモデルですが、GPT3/ChatGPTよりも高いスコアを獲得しました。この事実は、非常に大規模なモデルを使用せずに、高精度な要約モデルを開発できることを示唆しています。ただし、開発にChatGPTのような非常に大きなモデルが不要であるというわけではありません。ChatGPTを使用することでデータ拡張や教師データの作成などを行い、開発をスピードアップすることが可能だと思われます。 BART-largeは、ファインチューニングされたモデルの中では最低の性能でした。 BARTは要約タスクを含むテキスト生成タスクのベースラインとして頻繁に使用されており、一般に性能が低いわけではありません。 GPT3とChatGPTのスコアが低かった原因を分析するために、スコアが低かった要約結果の中身を確認しました。その結果、スコアが低かったにもかかわらず、対話の重要な部分はしっかりまとまっていることが確認されました。ただし、その表現は正解として定義された要約文を言い換えたものであり、単語でのマッチングをもとに評価を行うやBLEUやROUGEでは不正解と見なされていました。これはやBLEUやROUGEの弱点であり、この問題に対処するために近年いくつかのスコアが提案されています。詳しくは前編記事をご覧ください。 まとめ 本記事では、対話要約の最近の研究としてPegasus、 DialogLM、そしてRevComm内でGPT3/ChatGPTを評価した結果を紹介しました。 GPT3/ChatGPTの評価では、SAMSumコーパスを使用してGPT3とChatGPTの対話要約性能を評価しました。 GPT3はChatGPTよりも高いBLEU/ROUGEスコアを獲得しましたが、BARTやFlan-T5などをSAMSumコーパスでファインチューニングしたモデルはGPT3よりもさらに高いスコアを獲得しました。しかし、GPT3/ChatGPTによる要約結果は必ずしも悪いものではなく、むしろBLEUやROUGEといったスコアに改良が必要であることが示唆されました。 引用 [Chen 21] Chen, Y., Liu, Y., and Zhang, Y.: DialogSum Challenge: Summarizing Real-Life Scenario Dialogues, in Proceedings of the 14th International Conference on Natural Language Generation, pp. 308–313, Aberdeen, Scotland, UK (2021), Association for Computational Linguistics [Kannan 18] Kannan, A., Chen, K., Jaunzeikare, D., and Ra- jkomar, A. R.: Semi-supervised learning for information extraction from dialogue (2018) [Song 20] Song, Y., Tian, Y., Wang, N., and Xia, F.: Summarizing Medical Conversations via Identifying Important Utterances, in Proceedings of the 28th International Conference on Computational Linguistics, pp. 717–729, Barcelona, Spain (Online) (2020), International Committee on Computational Linguistics [Zhang 20] Zhang, Y., Jiang, Z., Zhang, T., Liu, S., Cao, J., Liu, K., Liu, S., and Zhao, J.: MIE: A Medical Information Extractor towards Medical Dialogues, in Proceedings of the 58th Annual Meeting of the Association for Computational Linguistics, pp. 6460–6469, Online (2020), Association for Computational Linguistics [Zhang 19] Zhang, J., Zhao, Y., Saleh, M., and Liu, P. J.: PEGASUS: Pre-training with Extracted Gap-sentences for Abstractive Summarization (2019) [Zhong 22] Zhong, M., Liu, Y., Xu, Y., Zhu, C., and Zeng, M.: Dialoglm: Pre-trained model for long dialogue understanding and summarization, in Proceedings of the AAAI Conference on Artificial Intelligence, Vol. 36, pp. 11765–11773 (2022)
2023年5月19-20日に開催されるスクラムフェス新潟にRevCommのQAエンジニアの大野泰代が登壇します。 トークタイトルは Whole-Team Approach (WhTA) for babies です。 開催概要 名称:スクラムフェス新潟 開催日時:2023年5月19-20日 開催場所:NINNO3(新潟市)、オンライン 主催:Scrum Fest Niigata実行委員会 イベントの詳細、お申し込みはこちら イベント内容 スクラムフェス新潟はアジャイルコミュニティの祭典です。 アジャイルやテストのエキスパートと繋がりを持ちましょう。この祭典は初心者からエキスパートまで様々な参加者が集い、学び、楽しむことができます。 参加者同士でアジャイルやテストのプラクティスについての知識やパッションをシェアするだけでなく、ここで出会ったエキスパートに困りごとを相談することもできます。 イベントサイトより引用 登壇情報 タイトル: Whole-Team Approach (WhTA) for babies 日時: 5月20日(土) 午後 5:00-5:45 confengine.com 登壇者紹介 大野泰代、RevComm QAチーム QAエンジニア 筑波大学人間学類卒業(社会心理学専攻)。CRC総研、フューチャーシステムコンサルティングなどのSIerで、DBAやPLなどを経験後、2010年頃より、QAとして、各種システム開発に従事。 2022年5月よりRevCommに参画。QMファンネルでいうところの「スプリットTE~インプロセスTE~TEコーチ」や「スプリットQA~インプロセスQA~QAコーチ」として活動中。 また、Sig-SQAで社外コミュニティ活動も行っている。 最後に RevCommは電話営業や顧客応対を可視化する音声解析AI搭載型のクラウドIP電話「MiiTel(ミーテル)」を開発しています。 MiiTelは電話サービスであり、高い信頼性が求められます。一方で、まだまだ機能の追加や発展も著しい側面もあります。お客様に MiiTel を安心して使っていただくため、品質向上のための取り組みは重要度を増すばかりです。 日々お客様が安心して使えるよう、品質保証への取り組みをさらに強化するための仲間を募集しています。MiiTelの開発や品質保証に興味をお持ちの方は、ぜひ当社の採用サイトをご覧ください。 www.revcomm.co.jp
はじめに Hello World! RevCommのバックエンドエンジニアの矢島です。 今回はMiiTelのアーキテクチャや技術スタックと開発・運用体制についてご紹介します。 MiiTelってなに? アーキテクチャや技術スタックの話題に入る前に、簡単にMiiTelについて紹介します。 MiiTelはAI搭載型クラウドIP電話です。 電話業務の可視化・分析を行うことで、セルフコーチングの機会を提供し、教育コストの削減・業績向上を実現するSaaSとして、電話営業やコールセンター、カスタマーサクセスなどの現場に導入いただいています。 MiiTelはそのサービスの性質上、特に平日の日中は業務のメインアプリケーションとして活用されることが多く、数秒でもサービス停止が発生するとお客様の業務にダイレクトに影響してしまいます。そのようなことが起きないように、適切な技術選定と設計を行わなければいけません。 MiiTelは大きく以下の3つのコンポーネントに分けることができます。 電話機能を提供するPBX 電話の音声データの話し方解析・言語解析を行う機械学習モデル 通話データを管理・可視化するWebアプリケーション RevCommではこれらのコンポーネントをすべて内製しています。 この記事では全体的なアーキテクチャや開発・運用体制に触れつつ、主にお客様に常に利用されているアプリケーションに焦点を当て、MiiTelを支える技術スタックとその変遷を紹介していきます。 ※当記事に記載された情報は公開時点(2023年5月)のものになります。 MiiTel アプリケーションアーキテクチャと技術スタック MiiTelには、管理者がユーザーを管理・電話の応答ルールの設定などを行うMiiTel Adminと、ユーザーが電話・通話履歴の管理・解析結果の確認などを行うMiiTel Analyticsがあります。 認証基盤アプリケーションは今後のサービス展開でも共通化できるよう、MiiTelアプリケーションから切り出して開発を行っています。 バックエンドの言語はPythonを採用しています。機械学習や音声認識分野でPythonが多く使われていることや、グローバル進出時に海外のエンジニア採用に有利であろうという観点から採用しました。 WebフレームワークにはDjangoを採用しており、機能単位にアプリケーションを分割して開発しています。 MiiTel はZoomとも連携しており、Web会議を自動で取り込んで解析し、会議の録画を解析結果とともに確認・管理できます。 MiiTelインフラアーキテクチャと技術スタック 要件 平日の日中に集中するアクセスを低レイテンシーで処理する 高い可用性を実現する 機能実装に集中できるインフラ構成を実現する 冒頭にも記載した通り、MiiTelはサービスの性質上、平日の日中にアクセスが集中します。そして数秒のサービス停止であっても大きな影響を及ぼします。 そのような柔軟なスケーリングと高い可用性を、なるべくリソースの管理に人員を割くことなく実現するためにさまざまなAWSサービスを活用しています。  CloudFrontとLambdaでのキャッシュ・アクセス制御をすることでバックエンドアプリケーションへの負荷を削減 Application Load Balancerでのヘルスチェックと負荷分散 ECSでコンテナ化されたアプリケーションをデプロイ Fargateでのマネジメント不要なスケーリング 負荷分散の観点からECSサービスを役割によって分割 OpenSearchで膨大なデータを横断して検索する機能の実現 SQSで音声解析システムとの連携を疎結合に実現 もちろん初めからこのようなアーキテクチャになっていたわけではありません。現在の構成に至るまで改善が繰り返されてきました。 そこで、MiiTelアプリケーションバックエンドの核を実行する赤枠の部分について、過去からの変遷を踏まえながら詳細を紹介します。 MiiTelアプリケーションバックエンドのインフラアーキテクチャの変遷 EC2の時代 創業から1年程度は、EC2のみを使ったシンプルな構成でした。 この画像は2017年11月のSlackでのやりとりです。 当時はスケーリングも考慮しつつ、スピードを意識した開発をしていた様子がうかがえます。 ECSの導入 ユーザー数が急速に増えていくなかで、スケーリングの課題が発生しました。 都度EC2インスタンスを管理しするのは運用に工数がかかり、スケールできないリスクが高まります。 そこで、以下のような改修を行いました。 アプリケーション環境をEC2からFargateに変更する ECSのAuto Scaling を利用し、実行するタスク数を自動的に増減させる この改修により急激なリクエストの増加にも耐えられるようになりました。 ECSサービスを役割で分割 ECSでタスク数をオートスケーリングするようになったことで、スケーリングは容易になりました。 しかし当時は、すべてのバックエンドへのリクエストを同一のサービスで処理していました。 それにより、サービス成長によりデータ量が多くなるにつれて、リクエストの中でも高負荷のものが他のリクエストのレイテンシーに影響を及ぼすようになってきました。 そこで、以下のような改修を行い、現在の構成になりました。 リクエストの内容によってCloudfrontのオリジンとビヘイビアを使い、異なるロードバランサーにリクエストを振り分ける それぞれのロードバランサーのターゲットを異なるECSサービスにする この改修により、高負荷なリクエストが増えても他のリクエストのレイテンシーに影響させないようにすることができるようになりました。 加えて、RevCommのTerraformを利用した現在のインフラ構築・運用の基盤ができたのもこの頃です。 次は、RevCommのインフラの構築・運用方法をご紹介します。 インフラ構築・運用方法 RevCommのインフラ構築・運用はヒューマンエラーを最小限にしつつ、よりスピーディーなリリースを実現するための工夫がなされています。 スピーディーなリリース 基本的に機能の開発者がそれに必要なリソースをつくる 職種によらずInfrastructure as Code (IaC) でリソース構築を行うことを推奨している RevCommにはインフラチームが存在しますが、アプリケーションのAWSリソースの構築には原則関わりません。アプリケーションの機能に必要なAWSリソースはアプリケーション開発者が構築・運用に責任を持ちます *1 。 このように機能開発からリソース構築まで一気通貫で行うことで、インフラチームのAWSリソース構築待ちといった時間が削減できています。 また私が入社して驚いたのが、フロントエンドエンジニアでもAWSリソースを構築・運用できる人がいることです。 RevCommには、在籍しているメンバーが市場価値の高いエンジニアになってほしいという方針があり、それが体現されているなと思いました。 多くのメンバーが担当できる技術領域を広げられる構築・運用体制になっていることで個人の成長に繋がっています。組織としても、属人化による運用コストの増加を防ぎ、スピーディーなリリースを実現できます。 ヒューマンエラーの最小化 RevCommでは、IaCのツールとしてTerraformとServerless Frameworkなどを利用しています。 以下の運用体制を敷いてヒューマンエラーを最小化するようにしています。 GitHub Actionsで、terraform fmt, validate, planを自動実行し結果を出力 特定のリソースはGitHub Actionsで applyを自動化 AWSアカウントに紐づくIAM roleで開発者が直接applyできるリソースを制御 実際の開発フローは以下のようになっています。 今後のMiiTelインフラアーキテクチャの展望 MiiTelを支える技術スタックやアーキテクチャとその変遷、開発・運用体制を紹介しました。 MiiTelのアプリケーションやインフラにおいては、今回ご紹介した以外においても様々な改善が繰り返されてきました。 そして今やMiiTelは日本だけでなくインドネシアでも提供されており、今後は北米進出も目指しています。 今後の更なるサービス成長や世界展開を視野に、インフラアーキテクチャにおいてはリージョンをフル活用して可用性と耐障害性を高められる構成にアップデートするPoCを実施しています。 まとめ 今回は、MiiTelを支える技術スタックと開発・運用体制というテーマで、記事を書きました。 2022年7月に入社してから、日々アーキテクチャやシーケンスのキャッチアップをしながら開発に奮闘している私としても、全体を俯瞰するいい機会となりました。 この記事をご覧いただいて、少しでもRevCommに興味を持っていただけたら、ぜひ採用ページもご覧ください。 www.revcomm.co.jp *1 : インフラチームは、インフラの全体最適化や、VoIPのためのインフラ構築・運用、セキュリティの強化などを行っています。
2023年4月28日(金)19:00 より、 Forbes AI 50 2023にアジアで唯一選出されたばかり の、 弊社の開発組織に焦点を当てたオンライン説明会 を開催します。 revcomm.connpass.com イベント内容 RevCommのエンジニア組織はどうなっているの?プロジェクトはどのように進めているの? Forbes AI 50 2023にアジアで唯一選出された と聞いて初めて知った会社だけど、どんな開発体制なの?…などなど、少しでも気になったことはありませんか? 本イベントでは、CTO・平村健勝をはじめとする弊社エンジニアが組織や開発体制について直接説明させていただき、皆様の疑問にお答えします。ぜひお気軽にご参加ください。 参加登録はこちら 登壇者 平村 健勝(ひらむら たけかつ)執行役員 CTO tech.revcomm.co.jp 瀬里 俊行(せり としゆき)執行役員 シニアエンジニアリングマネージャー tech.revcomm.co.jp 大谷 紗良(おおたに さら)Software Engineer, Backend tech.revcomm.co.jp 川添 貴之(かわぞえ たかゆき)Engineering Manager tech.revcomm.co.jp モデレーター 小幡 倫美(おばた ともみ)HR 参加登録 参加登録は、connpassにて受け付けております 。奮ってご参加ください。
はじめに バックエンドエンジニアの小門 照太です。 RevCommの主要製品であるAI搭載型IP電話「MiiTel」において、バックエンドAPIフレームワークの一つとしてDjangoを利用しています。 さて、4/3(月) にDjango 4.2がリリースされました。 Django 4.2 released これはLTS(Long-Term Support)バージョンであり、3.2 LTSから2年ぶりのリリースです。 サービスの継続開発と運用においてフレームワークのバージョンアップに追従することは、機能の追加や強化そしてセキュリティ面で重要です。 この度RevCommで運用しているサービスにおいてDjango 4.2へアップグレード(本番環境へ適用)しました。 今回の対応で実施した手順や必要となったソースコード修正の内容をご紹介します。 事前準備 本対応に際して事前にベータ版を使って検証を行いました。 Django 4.1から4.2正式リリースまでの間に下記のバージョンがリリースされており、事前検証や準備をするための期間が設けられています。 4.2a1 4.2b1 4.2rc1 本対応では検証バージョンに 4.2b1 を利用しました。検証用にブランチを作成し、その中でバージョンをアップグレードしてアプリケーションを起動させました。 いくつかエラーが出たため、修正した内容を後述します。 修正内容 django.conf.urls.url django.conf.urls.url() 関数がver4で廃止になりました。 例えば urls.py において下記のような修正です。 - from django.conf.urls import url + from django.urls import path urlpatterns = [ - url(r"~~~", ...) + path(r"~~~", ...) ] なお django.conf.urls.url() はver3.1から非推奨(Deprecated)とされています。 django.urls functions for use in URLconfs Deprecated since version 3.1: Alias of django.urls.re_path() for backwards compatibility. また urls() から path() への修正後に警告が出るようになりました。 $ python manage.py check System check identified some issues: WARNINGS: ?: ( 2_0.W001 ) Your URL pattern ' ^path/to/view$ ' has a route that contains ' (?P< ' , begins with a ' ^ ' , or ends with a ' $ ' . This was likely an oversight when migrating to django.urls.path () . System check identified 1 issue ( 0 silenced ) . これは path() のURLパターンに ^ や $ といった正規表現を含んでいたことが原因でした。 URLパターンに正規表現が必要な場合のためには django.urls.re_path() 関数が用意されています。 URLパターンから ^ と $ を削除して警告を解消しました。 urlpattenrs = [ - path(“^path/to/view/$”, …), + path(“path/to/view/”, …), ] django.utils.translation.ugettext_lazy 上記と同様に Deprecated な関数への対応です。 django.utils.translation.ugettext_lazt から gettext_lazy を使用するようにします。 - from django.utils.translation import ugettext_lazy as _ + from django.utils.translation import gettext_lazy as _ こちらもDjango 3.0のRelease noteに非推奨となる旨が記載されています。 Django 3.0 release notes django.utils.translation.ugettext(), ugettext_lazy(), ugettext_noop(), ungettext(), and ungettext_lazy() are deprecated in favor of the functions that they’re aliases for: Djangoドキュメントにおいて、関数/メソッドの deprecation はマイナーバージョンも含めた各Release noteで確認することができます。 しかし、メジャーバージョン毎の大まかなアップデートをRelease noteで俯瞰することは難しいです。 全てのバージョン毎の Deprecated あるいは廃止される関数/メソッドの情報を知るにはRelease noteではなく「 Django Deprecation Timeline 」を確認すると良いでしょう。 Psycopg 3 PostgreSQL 向けのデータベースドライバーである psycopg ver3 がサポートされました。 これは Django 4.2 リリースノート 「What’s new in Django 4.2」の1番目に記述されています。 Psycopg ver3 では 非同期操作のサポート を含む機能強化が行われています。 Differences from psycopg2 従来はPsycopg ver2でしたが、将来的にver2はDeprecatedとなるため今回で対応しました。 ドキュメントの手順に従ってPsycopg ver2からver3にアップグレードします。 ※パッケージマネージャーとしてPoetryを使用している例 $ poetry remove psycopg2-binary $ poetry add " psycopg[binary,pool] " また設定モジュール(settings.py)の記述もDjangoドキュメントに従い修正します。 Settings | Django documentation | Django DATABASES = { "default": { - "ENGINE": "django.db.backends.postgresql_psycopg2", + "ENGINE": "django.db.backends.postgresql", ... 動作確認 上述までの修正手順でローカルでエラーが出ないこと、およびCIの成功を以って検証環境への反映を行いました。 $ python manage.py check System check identified no issues ( 0 silenced ) . $ python manage.py test ... ---------------------------------------------------------------------- Ran 435 tests in 19 .712s OK Destroying test database for alias ' default ' ( ' myapp ' ) ... そして検証環境で一定期間稼働させて不具合が顕在化しないことを確認して、最終的なリリース判定として本番環境へ反映しました。 Django 4.2LTSが4/3(月)にリリースされて1週間ほどで本番環境にアップグレード適用を完了させました。 今回の迅速な対応を可能にした要素として下記の点が挙げられます。 Djangoのリリーススケジュールをウォッチして計画に組み込んでいた 事前準備としてベータ版を適用して検証を行った サービスの正常性を担保するためのCIを整備していた 今後に向けて 以上までの内容に加えてDjango ver4のアップデートに伴う修正の余地はまだまだあります。 今後対応していきたい箇所を合わせて例示してみます。 非同期サポートの強化 Django 4.1でORMによるクエリ実行に非同期サポートが導入されました。 Asynchronous queries SQLクエリを発行する全ての QuerySet メソッドの接頭辞に a を付けたものが追加されています。 async for author in Author.objects.filter(name__startswith= "A" ): # afirst(), not first() book = await author.books.afirst() データベースに対する操作はIOバウンドであるため、ノンブロッキング処理に切り替える効果が大きいはずですので、検証しつつ徐々に導入していきたいと考えています。 Redis用バックエンドクラスの提供 Django 4.0より、キャッシュ用途でRedisを利用する際のバックエンドクラスをDjangoがネイティブで提供するようになりました。 Django 4.0 release notes Redis cache backend The new django.core.cache.backends.redis.RedisCache cache backend provides built-in support for caching with Redis. これにより、DjangoとRedisを統合するための django-redis を利用する必要がなくなります。 CACHES = { "default" : { "BACKEND" : "django.core.cache.backends.redis.RedisCache" , "LOCATION" : "redis://127.0.0.1:6379" , } } まとめ Django 4.2LTSのリリースに伴いアップグレードを実施した事例をご紹介しました。 アップグレードして終わりではなく、「今後に向けて」のように対応の余地はまだまだたくさんあるので運用を継続していくことが最も重要であると考えています。 RevCommでは一緒に働いてくださるエンジニアを募集しています。 「コミュニケーションを再発明し人が人を想う社会を創る」というミッションの元、プロダクトの安定稼働を支えるために様々な取り組みをしていますので、ぜひご応募ください。 hrmos.co
1. はじめに こんにちは、RevComm Researchでリサーチエンジニアとして働いている髙瀬です。 2023年1月上旬にUltralytics社からYOLOv8が公開されました。 今回はYOLOv8について、v5との変更点や動かし方を紹介していこうと思います。 2. YOLOとは 非常に有名な物体検出手法なので、ご存知の方も多いと思います。 YOLOは、CVPR2016でJoseph Redmon氏らが発表した You Only Look Once: Unified, Real-Time Object Detection という論文で提案された物体検出手法です。 YOLOという名称はYou Only Look Once(見るのは一度だけ)の略称です。 YOLOは当時の物体検出手法で課題となっていた処理速度の遅さを、物体の検出と識別を同時に行うことで改善しました。 このため、推論速度が非常に高速でリアルタイム物体検出の先駆けにもなっています。 今日に至るまでに様々な物体検出手法が登場していますが、その中でも物体検出に興味を持ち始めた方、これから学ぼうとしている方には是非一度はチェックしてもらいたい手法の一つです。 本記事では詳細な説明は割愛しますが、日本語のわかりやすい解説記事も豊富ですので調べてみてはいかがでしょうか。 3. YOLOv8 YOLOv8は、YOLOv5の公開元であるUltralytics社が公開したYOLOの最新バージョンのモデルです。 大規模データセットでの学習はもちろんのこと、object detection, segmentation, classificationタスクで利用可能であり、CPU, GPUを始めとしたさまざまなハードウェアでの実行が可能になっています。 YOLOv8では、新しいbackboneや損失関数、anchor-free detection headの導入などの変更が加えられているだけでなく、過去のバージョンのYOLOをサポートし異なるバージョン間の切り替えや性能比較を容易にするといった機能を備えている点も大きな特徴と言えます。 現在、リポジトリを確認するとv3, v5, v8のconfigが用意されています。 なお、本記事執筆時点ではYOLOv8の論文は未公開のため、公式からの続報を待ちたいと思います。 4. YOLOv8のモデルサイズ YOLOv8にはモデルサイズが異なるn, s, m, l, xの5パターンのprerained modelが用意されています。 下記のパラメータ数とCOCO mAP(精度)に着目するとYOLOv5から精度がかなり向上していることがわかります。 特に大きいモデルサイズであるl, xはパラメータ数を削減しつつ精度が向上しています。[ 引用 ] 各モデルの精度は下記のようになっています。[ 引用 ] 5. YOLOv5とYOLOv8の変更点 YOLOv8になったことでYOLOv5から構成がいくつか変更されていますが、現時点で公開されているモデルの構成から読み取れる大きな変更は2点です。 C2f layerの導入 Decoupled head導入とobjectness branchの削除 この他にも一部のconv module削除、kernel sizeの変更など細かな変更が加えられています。 公式ではありませんが、 RangeKing 氏が公開している YOLOv8 detection modelアーキテクチャ の図が参考になります。 RangeKing氏によるYOLOv8 detection modelアーキテクチャ[ 引用 ] 6. YOLOv8の環境構築と推論実行 本章では、YOLOv8をインストールして実際に触れていこうと思います。 今回はpretrained modelを使ってM1 MacBook ProのCPU環境でどの程度動くのか気になったので検証してみます。 <筆者の環境> MacBook Pro (13-inch, M1, 2020) 16GB Mac OS Monterey YOLOv8のインストールは、下記のコマンドでできます。 pip install ultralytics インストールが完了したところで、実際に動かしてみます。 今回はPythonスクリプトで推論してみます。 画像に対しての推論 まずは画像を入力してみます。 本記事では 公式からダウンロードできる、バスと人が映った画像 を使います。 画像サイズはwidth=810, height=1080のRGB画像です。 from ultralytics import YOLO # load pretrained model. model: YOLO = YOLO(model= "yolov8n.pt" ) # inference # save flgをTrueにすることで推論結果を描画した画像を保存できる。 result: list = model.predict( "https://ultralytics.com/images/bus.jpg" , save= True ) YOLOv8nの推論結果の画像 動画に対しての推論 動画に対しても推論を行ってみました。 画像に対しての推論ではYOLOv8nを使っていたので、動画ではYOLOv8xを使ってみようと思います。 画像の推論時とインターフェースは変わらず、動画ファイルのパスを引数に渡します。 from ultralytics import YOLO # load pretrained model. model: YOLO = YOLO(model= "yolov8x.pt" ) # <figure class="figure-image figure-image-fotolife" title="YOLOv8xの推論結果のgif">[f:id:yasaka_uta:20230327173819g:plain]<figcaption>YOLOv8xの推論結果のgif</figcaption></figure>inference # save flgをTrueにすることで推論結果を描画した動画が保存される。 result: list = model.predict( "MOT17-14-FRCNN-raw.mp4" , save= True ) 今回、 MOT Challenge MOT17のTest set の動画を利用します。 なお、MOT17の動画ファイルはWebM形式なので、事前にMP4形式に変換を行っています。 変換した動画は30秒、フレームレートは30なので、900枚の画像に対して推論が行われる形になります。 YOLOv8xの推論結果のgif 補足 1. サポートされている動画像のファイル形式 YOLOv8でサポートしている画像、動画のファイル形式は以下のようになっています。 画像: "bmp", "dng", "jpeg", "jpg", "mpo", "png", "tif", "tiff", "webp", "pfm" 動画: "asf", "avi", "gif", "m4v", "mkv", "mov", "mp4", "mpeg", "mpg", "ts", "wmv" サポートされているファイル形式のコード上での定義はこちら 2. 検出結果のテキスト出力 推論を行っている時に検出結果をテキストとして保存したいことがあると思いますが、下記のように引数を設定すれば可能になります。 # 検出結果を描画した画像と検出結果をテキストに出力したいとき model.predict( "https://ultralytics.com/images/bus.jpg" , save= True , save_txt= True ) # 検出結果+各物体のconfidenceをテキストに出力したいとき model.predict( "https://ultralytics.com/images/bus.jpg" , save_txt= True , save_conf= True ) Prediction関数に設定できる引数はほかにも用意されているので、 公式ドキュメント を参照ください。 3. modelへの入力方法の補足 動作確認で動画像のパスを指定していますが、以下のようにlist形式で複数の動画像のパスを渡すこともできます。 source_list: list = [ "./sample1.jpg" , "./sample2.jpg" ] result: list = model.predict(source_list, save= True ) 7. 各モデルの推論結果を俯瞰してみる 前章で環境構築し推論できることが確認できたので、この環境を使ってYOLOv8のsからlのモデルで推論してみようと思います。 また、結果の比較としてYOLOv5だけでなく、ここ1年以内に出ているYOLOv6およびYOLOv7の出力結果も一緒に比較してみます。 なお、今回は各モデルの精度比較ではなく、モデルごとの出力を俯瞰するかたちで比較したいと思います。 YOLOv5, YOLOv6, YOLOv7についての解説は割愛しますが、気になった方は調べてみてください。※YOLOv6のモデルはYOLOv6 (v3.0) がYOLOv8とほぼ同時に公開されているため、YOLOv6 (v3.0)を使っています 対象とする画像はYOLOv8のサンプル画像を利用しています。 各モデルの検出結果を描画した画像と検出物体の情報、推論速度をまとめてみました。 YOLOv8l, xではYOLOv5l, xで検出できていなかった画像右上のbicycleを検出できていますね。 YOLOv8の検出物体のconfidenceをみていくとYOLOv5よりも基本的に向上していることがわかります。 YOLOv7が若干遅い印象はあるものの、どのモデルもかなり高速ですね。 YOLOv8 YOLOv5 YOLOv6(v3.0) YOLOv7 8. まとめ 本記事では、YOLOv8の概要・YOLOv5との差分・簡単な使い方を紹介しました。 YOLOv8を使ってみた所感としては、 pip install可能で導入が容易、かつ使いやすく整理されたインターフェース onnx, torchscriptなどへの変換が非常に簡単 以前のYOLOv5も使いやすかったですが、さらに利便性が向上した印象を持ちました。 モデルのexportについては今回触れていませんが、 export形式 が豊富で簡単に実行できるのは開発者にとってうれしいですね。 9. 参考文献 https://github.com/ultralytics/ultralytics https://docs.ultralytics.com/ https://github.com/meituan/YOLOv6 https://github.com/WongKinYiu/yolov7 https://github.com/ultralytics/yolov5 https://github.com/ultralytics/ultralytics/issues/189 https://motchallenge.net/data/MOT17/ https://blog.roboflow.com/whats-new-in-yolov8/
1. はじめに こんにちは、RevComm でバックエンドエンジニアとしてプロダクトの開発に携わっている池畠です。 2022 年 12 月下旬に弊社から MiITel の新機能として Outgoing Webhook をリリースしました。今回は弊社における Outgoing Webhook を活用した業務の効率化事例について、Python によるマイクロサービスを実装しながら紹介していこうと思います。 2. Outgoing Webhook MiiTel の Outgoing Webhook は、応対履歴の音声解析完了時に、設定した URL (Webhook 送信先サーバー) に HTTP リクエストを送信する機能です。開発者は、Webhook を利用して外部システムへ MiiTel 応対履歴情報を連携できます。 詳しい設定方法・連携内容は サポートページ をご参照ください。 3. 運用に向けた課題 弊社のカスタマーサポートではお客様からのお電話での対応に MiiTel を活用しております。MiiTel には既に Salesforce や kintone, HubSpot といった外部の CRM に応対履歴情報の連携機能は備わっていますが、問い合わせ内容のチケット管理には Asana を活用しているため、手動で Asana タスクを作成して応対履歴内容を転記するひと手間が要りました。 そこで、Outgoing Webhook によって MiiTel 応対履歴情報から自動で Asana タスクを作成するマイクロサービスを構築し、チケット作成を自動化するような業務改善の事例をご紹介します。 4. Asana 側の設定 Asana にタスクを自動作成するためには、事前に以下の項目を取得し控えておく必要があります。本章では、それらの確認方法を紹介します。 API token Workspace id Project id API token まずは Asana の デベロッパーコンソール にアクセスしてログインします。 画面下部の「トークンを新規作成」をクリックし、「API 利用規約に同意しますにチェック」を入れ、トークン名を入力します。 入力が完了したらトークンを作成ボタンを押下すると、トークンをコピーのモーダルが出現するので「コピー」ボタンを押下してすぐに参照できるように控えておきます。 「必ずここでこのアクセストークンをコピーしてください。二度と表示されません。」という表示もありますので紛失しないようにしましょう。「完了」を押下して API token の取得は完了です。 Workspace id まずは Asana のトップページにアクセスします。 ご自身のアイコンをクリックすると「所属先組織について...」が選択できるのでこれを押下すると別ウィンドウに遷移します。 そのときの URL を確認すると https://app.asana.com/admin/{数字}/overview のようになっており、 admin と overview に挟まれている数字が Workspace id に相当するのでこれを控えておきましょう。 Project id 本記事ではデモとして新規に Asana プロジェクトを作成してみます。 まずは Asana のトップページにアクセスします。 「プロジェクトを作成」をクリックし、「テンプレートを使用」を選んでいきます。 今回は、「Support チームへの機能要望のお問い合わせを Developer チームが確認しやすいようなチケット化をする」というユースケースを想定して、「IT リクエスト」のテンプレートを選択していきます。「テンプレートを使用」をクリック。 プロジェクトの詳細を追加していきます。プロジェクト名を記入し、チームを選択して「プロジェクトを作成」ボタンを押下します。 プロジェクトの作成が完了すると https://app.asana.com/0/{数字}/board という URL に遷移します。0 と board に挟まれている数字が Project id に相当するのでこれを控えておきましょう。 5.Chalice によるマイクロサービス構築 本章では、Python の AWS マイクロサービス構築用バックエンドフレームワークである Chalice を活用して Outgoing Webhook の受け口と Asana API を実行する Lambda を作成していきます。 筆者の環境 MacBook Pro (13-inch, M1, 2020) Python 3.10.2 Mac OS Ventura 導入 Chalice, Asana の Python 用モジュールのインストールは、下記のコマンドでできます。 pip install chalice asana 続いて Chalice の新規プロジェクトを作成します。ディレクトリを移動し、Lambda へのデプロイ用に pip freeze をしてファイルに出力させておきます。 chalice new-project helloworld cd helloworld pip freeze > requirements.txt 認証周辺の実装 本機能における「認証」とは初回リクエストで "challenge" キーの値をサーバー側でレスポンスするということになります。その部分を実装していきます。 現在のディレクトリに app.py というファイルがありますが、これがアプリケーションの本体になります。まず、例として認証周りの処理を下記のように記述していきます。 import json import os import asana from chalice import Chalice, ForbiddenError, Response app = Chalice(app_name= 'helloworld' ) @ app.route ( "/" , methods=[ "POST" ]) def index (): request = app.current_request body = request.json_body headers = request.headers # コメント 1 if headers.get( "X-MiiTel-Outgoing-Webhook-Key" ) != "OUTGOING_WEBHOOK_KEY" : raise ForbiddenError( "Invalid header key" ) if "challenge" in body.keys(): return Response( body=body[ "challenge" ], status_code= 200 , headers={ "Content-Type" : "text/plain" }, ) まずは Outgoing Webhook で設定予定の追加ヘッダーに該当する箇所を見ていきます。コメント 1 の箇所におきまして、”OUTGOING_WEBHOOK_KEY” という値があるかを確認しています。ハードコードは良くないので実運用においては環境変数にするか、AWS Systems Manager (SSM) や AWS Secrets Manager に格納した値を参照するなどしておく必要があります。存在しなければ 403 エラーに該当する FobiddenError を raise してその原因を軽く引数の文字列に記しておきます。次にポイントとなるのが、body 内に ”challenge” というキーが存在するかを確認するということです。初回リクエストではこの ”challenge” キーの値をそのままレスポンスで返す必要があるので早めに return しておきます。 応対履歴の整形 次は応対履歴から Asana タスクを作成する処理を以下のコードでしていきます。Outgoing Webhook ペイロードの形式に関してはサポートページにも記載がありますので詳しくはこちらを参照ください。 # コメント 2 success_response = Response( body= "Success" , status_code= 200 , headers={ "Content-Type" : "text/plain" } ) call_type = body[ "call" ][ "details" ][ 0 ][ "call_type" ] # コメント 3 if call_type not in ( "INCOMING_CALL" , "OUTGOING_TRANSFER" , "QUEUEING_CALL" ): return success_response comments = body[ "call" ][ "details" ][ 0 ][ "comments" ] # コメント 4 if not comments: return success_response sorted_comments = sorted (comments, key= lambda x: x[ "created_at" ]) first_comment = sorted_comments[ 0 ][ "value" ] second_comment = sorted_comments[ 1 ][ "value" ] if len (sorted_comments) > 1 else "" tags = [ t[ "value" ] for t in list ( filter ( lambda x: x[ "value" ] in ( "[OW] 緊急度: 高" , "[OW] 緊急度: 中" , "[OW] 緊急度: 低" , "[OW] 要望度: 高" , "[OW] 要望度: 中" , "[OW] 要望度: 低" , ), body[ "call" ][ "details" ][ 0 ][ "tags" ], ) ) ] # コメント 5 if not tags: return success_response sorted_tags = sorted (tags) first_tag = sorted_tags[ 0 ] second_tag = sorted_tags[ 1 ] if len (sorted_tags) > 1 else "" participants = { p[ "from_to" ]: p[ "company_name" ] for p in body[ "call" ][ "details" ][ 0 ][ "participants" ] } company_name = participants[ "FROM" ] speech_recognition_summary = body[ "call" ][ "details" ][ 0 ][ "speech_recognition" ][ "raw" ] url = f 'https://{body["call"]["tenant_code"]}.miitel.jp/app/calls/{body["call"]["details"][0]["id"]}' priority_and_urgency = { "[OW] 緊急度: 高" : "高" , "[OW] 緊急度: 中" : "中" , "[OW] 緊急度: 低" : "下" , "[OW] 要望度: 高" : "高" , "[OW] 要望度: 中" : "中" , "[OW] 要望度: 低" : "下" , } urgency = priority_and_urgency.get(first_tag, "" ) priority = priority_and_urgency.get(second_tag, "" ) note_template = """▼顧客が言っている要望詳細(もしあれば録画・録音): 企業名: {company_name} 応対履歴: {url} <原文ママ> {speech_recognition_summary} ▼なぜそう言っているか?どのような顧客か?解釈を記入ください: {second_comment} ▼要望度合い(高・中・下):{priority} ▼緊急性(高・中・下):{urgency}""" notes = note_template.format( company_name=company_name, url=url, speech_recognition_summary=speech_recognition_summary, second_comment=second_comment, priority=priority, urgency=urgency, ) コメント 2 の箇所では返却用のレスポンスオブジェクトを変数化しています。 Asana への連携する・しない/成功・失敗に関わらずこの値を返していきます。 お客様から受けた問い合わせを連携する想定なのでコメント 3 の箇所の分岐処理では着信系以外の応対種別を省いています。 コメント 4 の箇所の分岐では通話中のメモ用にとったコメントが存在しなければ連携なしとしています。 同じくコメント 5 の箇所の分岐では緊急度や優先度の応対メモがなかった場合にも連携なしとします。 その他取引先会社名・音声認識結果の要約・応対履歴 URL を変数に格納し、Asana の notes に挿入するテンプレートに埋め込みます。 Asana へのリクエスト 最後に下記のようになります。 try : client = asana.Client.access_token(asana_token) client.tasks.create_task( { "workspace" : "workspace_id" , "projects" : [ "project_id" , ], "name" : first_comment, "notes" : notes, }, opt_pretty= True , ) except : pass return success_response asana モジュールにて asana_token から access_token のオブジェクトを取得します。 これまでの処理で得られた workspace_id, project_id, first_comment, notes を引数に task を作成する関数を実行します。 例外処理はエラー確認用に出力しておくと良いですが今回は pass にて割愛します。最後に 200 レスポンスを返却することで実装は完了です。 デプロイ Chalice のデプロイは下記コマンドからできます。 chalice deploy 処理が終了すると api gateway の URL が発行されるのでそれを控えておきましょう。 6. MiiTel 側の設定 前章で Asana タスク自動作成を行うマイクロサービスを構築し、curl による疎通が確認できたので、発行された URL を MiiTel 側に設定していきます。 サポートページの引用になりますが、以下のように設定画面にアクセスします。 管理者権限があるユーザーで MiiTel Admin にログインします。 (MiiTel Admin にアクセスするには、MiiTel Analytics 画面右上の歯車アイコンをクリックします) [外部連携] > [Outgoing Webhook] を選択します。 [連携設定を追加] をクリックします。 各項目を設定していきますが今回のケースでは添付画像のような設定にしていきたいと思います。 設定時のポイントとしては以下になります。 追加ヘッダーを設定する リクエストの軽量化のため、音声認識結果のフレーズとキーワードをペイロードから除外する [保存] をクリックして設定は完了です。 7.着信〜音声認識〜タスク作成まで MiiTel Phone を起動した状態で着信通話をしてみます。まず、UI をワイドモードに変更し、「電話に出る」を押下すると通話情報が確認できます。 続いて「コメント」タブに移動し、Asana タスクのタイトルを記入して保存します。 次に「応対メモ」から緊急度・要望度タグを選択して保存しておきます。 通話が終了したときに応対メモとしてチェックした内容が正しいかを確認して保存、コメントも同様に保存を再度押下します。 作成された応対履歴に移動し、コメントからこの応対における補足内容を記入します。 音声解析が完了すると IT リクエストプロジェクトに Asana タスクが作成され、記入内容・音声認識結果が反映されていることが確認できました。 8.まとめ 本記事では、Outgoing Webhook の受け口として Chalice でマイクロサービスを構築し、Asana タスクを作成する実装と手順を紹介しました。 音声認識結果の要約が見れることによって、 MiiTel の応対履歴を閲覧せずとも要望背景の把握に活用することができそうですね。 お客様のユースケースに応じて自由にアレンジが可能な Outgoing Webhook をぜひご活用ください。
はじめに RevComm の小門です。 普段はバックエンドエンジニアとしてプロダクトの開発に携わっています。 2022年12月まではインフラエンジニアとして全社横断的なシステムのセキュリティ強化対応を担当していました。 今回は、セキュリティ強化の一環として「プラットフォーム診断」およびそれに付随したコンテナイメージのセキュリティ改善対応について紹介します。 プラットフォーム診断 プラットフォーム診断とは OS やミドルウェアに既知の脆弱性が潜んでいないか洗い出すことを指します。 プラットフォーム診断として脆弱性を検知するにはスキャンツールが必要となります。 例えば OSS の脆弱性ツールとして下記のものがあります。 サーバー用 Vuls, OpenSCAP, OpenVAS コンテナ用 Trivy, Clair RevComm では主に AWS を使用してサービス提供しており、AWS のセキュリティサービスに Amazon Inspector というものがあります。 Inspector を使うと EC2、ECR を対象にした脆弱性スキャンを実現できます。 結論として、下記のポイントからプラットフォーム診断のツールとして Amazon Inspector を採用しました。 機能要件を満たしている点 仮想マシン(Amazon EC2)とコンテナ(Amazon ECR)のスキャンに対応している マネージドである点 AWS の他サービスと連携して自動スキャンできる 運用に向けた課題 RevComm では Amazon ECS を利用して提供している Web アプリケーションのサービスがあります。 Inspector を利用するとスキャンが自動で実行されるため便利ですが、ECR イメージのスキャンに関してはデフォルトの機能だけでは運用を開始できない課題がありました。 1点目は、棚卸しすべきコンテナイメージ(ECR)の対象を絞り込むことが難しいことです。 サービスのリリースに伴って新しいコンテナイメージを ECR に保存し、タスク定義で指定するイメージも更新します。 Inspector の機能でリポジトリやタグなどの条件で ECR のイメージを絞り込むことはできますが、リリースの度に使用するイメージが変化する場合は特定が難しくなります。 例えば、ソースコードの git ハッシュ値をコンテナイメージのタグでバージョニングしていくケースが考えられます。 参考: Best Practices - Running your application with Amazon ECS - Amazon Elastic Container Service As a best practice, tag container images with a unique tag for each build. We recommend that you tag your images using the git SHA for the git commit that was used to build the image. また、開発と本番環境間で同じコンテナイメージを使用する方法として AWS アカウント間で ECR イメージを共有することができます(Resource based permission )。 これは複数の環境で同一コンテナイメージからアプリケーションを実行するのに便利ですが、この場合デフォルトだと Inspector で別アカウントの ECR イメージに関する情報が取得できません。 そのため、複数 AWS アカウント上の ECR イメージのスキャン結果を集約管理する必要があります。これが2点目です。 以上より、ECS サービスで使用するサービス提供のために実際に使用されるコンテナイメージのタグを適切に特定し、ECR リポジトリもアカウント跨ぎである可能性を考慮したツール整備と運用を検討しました。 実現した構成 上記の課題を解決するアプローチとして、本番環境で実稼働している ECS タスクの情報を取得し集計すべきコンテナイメージタグを抽出するようにしました。 具体的な構成イメージと処理概要を示します。 AWS Organizations の機能と連携して「Inspector 管理アカウント」を設定する。 …Inspector 管理アカウントでは全アカウントにまたがった Inspector スキャン結果を取得できる 本番環境で稼働している ECS タスクの情報から、実際に使用中のコンテナイメージを特定する 対象のコンテナイメージに関する脆弱性情報を Inspector 管理アカウント上でフィルタリング抽出して集計する ポイントは 2. において本番環境で稼働中の ECS サービスおよび ECS タスク定義から集計すべきコンテナイメージを絞り込んでいる点です。 下記の API を組み合わせて集計するスクリプトを実装しました。 ecs list-clusters ecs describe-services ecs describe-task-definition これにより、プロダクトとして稼働するサービスの実態を必要十分に集計することを実現しました。 コンテナイメージ改善 前述したプラットフォーム診断と合わせて、プロダクトサービスを提供するコンテナアプリケーションの脆弱性を改善する対応を実施しました。 具体的には、コンテナイメージが軽量な環境となるように Dockerfile を修正しました。 RevComm ではバックエンド言語として主に Python を使用しているため、Python を例にポイントとなる点を紹介していきます。 ベースイメージに slim イメージを使う 本番用のアプリケーション環境として使用する場合、ベースイメージとして slim イメージを使用します。 slim イメージと非 slim イメージ(例えば python:3 や python:3.11 )の大きな違いはビルトツールや dev パッケージの有無です。 $ docker run -ti --rm python:3. 11 . 1 apt list --installed | grep " ^lib.*-dev " | wc -l 84 $ docker run -ti --rm python:3. 11 .1-slim apt list --installed | grep " ^lib.*-dev " | wc -l 0 上記のように slim イメージには libxxx-dev といった dev パッケージが一切含まれていないことが分かります。 2 つのイメージを Amazon ECR の拡張スキャン機能でスキャンした結果を比較してみます。 ※スキャン結果は 2023/2/2 時点のもの python:3.11.1 python:3.11.1-slim(※脆弱性の検出なし) 不要なパッケージをインストールしない セキュアなコンテナイメージを作り上げるためには不要なパッケージをインストールしないことが重要です。 結果的にイメージが軽量になり、可搬性も向上します。 例えば前述の slim イメージを使うと gcc が使えませんが、Dockerfile の中で gcc をインストールしてしまうのは良い方法ではありません。 FROM python:3.11.1-slim RUN apt-get update \ && apt-get -y install gcc # <= ★No good ビルド用に gcc が必要な場合があっても、実行時に gcc は(ほとんどの場合)不要だからです。 マルチステージビルド アプリケーションに必要なビルド処理と軽量かつセキュアな実行環境を両立させるために有用なのがマルチステージビルドです。 ※本記事ではマルチステージビルド自体の解説は割愛します。 Multi-stage builds | Docker Documentation マルチステージビルドを使った Python 用の Dockerfile の例を紹介します。 サンプルとして、インストール時に C コンパイラが必要な uWSGI(WSGI 規格のアプリケーションサーバー)を slim イメージで利用するケースを考えてみます。 # -- stage: builder FROM python:3.11.1 AS builder RUN pip3 install --no-cache-dir uwsgi # -- stage: runner FROM python:3.11.1-slim AS runner RUN apt-get update \ && apt-get -y upgrade \ && apt-get install -y \ # for uWSGI libxml2 COPY --from=builder /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/site-packages COPY --from=builder /usr/local/bin /usr/local/bin # uWSGI を使用して WSGI アプリケーションを起動できることを確認 COPY main.py ./ CMD [ "uwsgi" , "--http" , ":8000" , "--wsgi-file" , "main.py" ] EXPOSE 8000 1つの Dockerfile の中で builder と runner 、2つの「ステージ」を定義しています。 builder ステージ:uwsgi ライブラリをビルド・インストールする runner ステージ:インストール成果物である site-packages 配下をコピーすることで使用できるようにする main.py は HTTP レスポンスを返すための最小限のコードです。 def application (env, start_response): start_response( '200 OK' , [( 'Content-type' , 'text/plain; charset=utf-8' )]) return [b 'Hello World' ] 上記 Dockerfile をビルド・起動してレスポンスが受け取れることを確認してみます。 $ # ビルド対象ステージを --target で指定する $ docker build . --target runner -t multistage-test $ docker run -d --rm \ --name multistage-test \ -p 8000:8000 \ multistage-test $ # アクセス確認 $ curl localhost:8000 Hello World $ docker kill multistage-test 本記事で紹介した Dockerfile の改善例については SpeakerDeck のスライドでより詳しく解説していますので、ぜひご覧ください。 まとめ Amazon Inspector を使ったプラットフォーム診断の実践例と、コンテナイメージ改善の取り組みについて紹介しました。 コンテナイメージには必要最小限のパッケージ、ライブラリのみインストールするようにすることで軽量かつセキュアな環境を作成することができます。 また、新しい脆弱性は日々報告されていくため、一度作ったイメージは定期的に最新化する運用も重要です。 RevComm は電話営業や顧客対応を可視化する音声解析 AI 搭載型のクラウド IP 電話 MiiTel (ミーテル) を提供しています。 今後もお客様に安心して MiiTel をご利用頂けるよう、引き続きプロダクトのセキュリティ向上に取り組んでまいります。 また、エンジニアを積極採用中ですので、興味をお持ち頂けましたらぜひともご応募ください。 hrmos.co
こんにちは、RevComm テックブログ運営担当の小山と申します。この度、社内で共有された技術スライドの一部を公開していくことにしました。 RevComm のエンジニアの情報発信は、今までこのテックブログが中心でした。しかし、社内には技術情報をまとめたスライドが多数あります。中には社内外を問わず広く参考になるような内容も共有されており、技術資料として公開することになりました。 今回はスライドを公開しようとした背景と、最初に公開するスライドの紹介を記事にします。 「どんなスライドが公開されているのかをまず知りたい!」という方は 今回公開する2つのスライドの紹介 をご覧ください! RevComm は社内での技術共有が盛ん 私が RevComm に入社したのは 2022 年の 1 月。そのときにはエンジニアの情報発信の文化はこれから作っていくのだろうなという印象でした。入社前は情報不足を感じていましたが、機会があれば自分で盛り上げていくのもありだろうなという気持ちで入社しました。 実際に入社してみると、社内では技術共有が盛んに行われていました。 Tech Talk(社内勉強会) は有志による発表がほぼ毎週行われており、そのほかにも業務に関わる技術共有の場が必要に応じて作られていました。 様々なジャンルのテーマで開催されている Tech Talk 社内の情報共有の代表例として Tech Talk について説明します。 RevComm では、毎週 30分間 Tech Talk が開催されています。Tech Talk は有志で運営されており、自主的に手をあげたり、発表して欲しい人にリクエストしたりという形で発表者が決まっています。ジャンルは自由で、より良いコードの書き方や、テスト、プロジェクトマネジメントなど様々です。個々人の経験や学習から得た知見を全体へ共有する場になっています。 直近の Tech Talk のタイトルは以下のようなものがあります。 RevComm 版 VSCode オススメプラグイン - 2022 データガバナンス入門 初心者プログラマーにこそ勧めたい TDD AWS 認定試験(アソシエイト)は業務に活きるのか、更に活かすには ゲームボーイを作ろう 2022 年には 39 回開催されており、発表に使われたスライドが社内資料として蓄積されています。 なぜスライドを公開するのか? Tech Talkを始めとして、社内では多数のスライドが作られています。これまでを振り返ると、ドキュメントよりもスライドで技術共有というケースの方が多いかもしれません。そんな RevComm の業務風景を公開して、自社を知ってもらいたいという気持ちが、技術スライドの公開を考えたきっかけです。あえてブログにまとめ直すよりも、そのまま公開した方が RevComm の情報共有のあり方が伝わると考えています。 Tech ブログを 2022 年の 2 月に始めて、以前よりは RevComm の技術情報を社外に伝えられるようになりました。しかし、十分とはまだ言えません。普段作っているスライドを通じて、RevComm を認知していただくきっかけや、より深く知っていただきたいと思っています。 また、スライドを通じて RevComm の幅広い業務領域を感じてもらえるきっかけになり得ると考えています。特にバックエンドチームはTech Talk などでも技術共有をよくしており、今回紹介するスライドもバックエンドチームメンバーの発表資料になります。その他のチームでも技術共有は盛んで、例えば私の所属するフロントエンドチームの優秀なエンジニアが、開発者体験を向上させるための取り組みを発表しています。 そういった技術共有に使うスライドは、日々の課題解決や工夫が詰まったものです。テーマによりますが、社内だけではなくどこかの誰かの学びの機会にもなりえるものと考えています。 今回公開する2つのスライドの紹介 よりよい Dockerfile を考える 〜 Python の実例とともに 〜 speakerdeck.com Python の Docker ファイルのより良い書き方を知ることができるスライドです。題名には Python とついていますが、Python 以外の言語でも参考にできそうですね。開発利便性・可搬性・セキュリティの観点で、最適化するポイントがわかるスライドとなっています。 Tech Talk での発表後、他のチームでも導入されて大きな改善をすることができたという実績があります。イメージサイズが 395MB から 94MB になり、脆弱性を無くすこともできました。 Introduction to Dependency Injection 「DI」の整理とそのメリット speakerdeck.com 依存性の注入(Dependency Injection)と依存性の逆転(Dependency Inversion)の関係が図解されているスライドです。プロジェクトの実コードを交えて解説がされています。どういったケースで使うと良いか、注意点があるかということもわかりやすいものとなっています。 終わりに 社内のコミュニケーション・技術共有の手段としてスライドが作られてきました。今後、社内で共有されたスライドを、このブログでもいくつか紹介していく予定です。 ブログ以外にも、技術スライドの公開によって、学びの機会や、RevComm に興味を持っていただけるきっかけになると嬉しいです。 RevComm は開発者やデザイナーの採用に力を入れています。情報発信をしていきたいという意欲のある方も大歓迎です!是非とも仲間になっていただけると嬉しいです! hrmos.co
本記事の著者はResearch Engineerの大野です。最近は、 ホロウナイト というゲームをやっていましたが、もう少しでクリアというところで敵が倒せず諦めました。 はじめに RevCommは電話営業や顧客応対の通話を支援するAI搭載型のIP電話「MiiTel」を提供しています。 この製品は、通話の文字起こしを保存する機能を備えており、RevCommは数千時間の対話データに接しています。 この対話データに対する支援の1つとして対話要約が考えられます。対話要約とは、入力された対話から、その主要な概念を含むより短い文書(要約)を自動的に作成することです。 ユーザは、要約を作成する手間が省けたり、あるいは要約を読むことで対話の概要をより早く理解できるなどの利点があります。 これから前編と後編の2回に分けて、対話要約に関する記事を書きます。今回の記事では、はじめにいくつかの対話要約のデータセットをご紹介します。 対話要約は、メール・チャット・エージェントと顧客間の対話など、複数のドメインで使用されます。そして、ドメインによって課題やゴールが異なります。そこでデータセットをいくつかご紹介することで、各ドメインの抱える課題やゴールを概観していきます。 また、本記事では要約システムの評価指標についてもご紹介します。長らく単語のオーバーラップに基づく指標が使用されていましたが、他の内容に基づく評価指標が最近になって提案されています。 評価指標を概観することは、要約システムを構築する際の課題を考える良い材料になると考えています。 はじめに データセット オープンドメイン対話要約 チャット 日常会話 TV番組・インタビュー タスク指向の対話要約 会議 メール 医療 カスタマーサービス 評価手法 ROUGE BLEU chrF BERTScore FEQA FactSumm まとめ 参考文献 データセット [Jia 22] は対話要約を2つに分類しました。1つはオープンドメイン対話要約で、もう1つはタスク指向対話要約です。下記に[Jia 22]の図を引用します。 オープンドメイン対話要約は、日常生活やドラマにおける対話、あるいはオンラインフォーラムを対象にした要約です。物語の大筋を獲得したり、与えられたテーマに対する意見をまとめることに焦点を当てます。この記事では、オープンドメイン対話要約のデータセットとして、「チャット」「日常会話」「TV番組・インタビュー」の3つのカテゴリを取り上げます。 タスク指向の対話要約は、主に顧客とサービスプロバイダに所属するエージェントとの間の会話を対象にします。顧客は特定の課題を持っており、それを解消するためにエージェントと対話をします。この記事では、タスク指向の対話要約のデータセットとして、「会議」「メール」「医療」「カスタマーサービス」の4つのカテゴリを取り上げます。 オープンドメイン対話要約 チャット [Gliwa 19]は対話要約のためのデータセットである SAMSum (Samsung Abstractive Messenger Summarization)を作成しました。データセットの名前が示すように、著者はSamsungに所属しています。 データセットはオンラインチャットと要約のペアを約1万6000件含んでいます。[Zhao 21]によると、ターン数は平均して9.9回です。オンラインチャットの性質上、データセットが絵文字・顔文字あるいは略語を含んでいます。下記に[Chen 21]の図を引用して、このデータセットに含まれる対話の例を示します。 日常会話 [Chen 21]は対話要約のためのデータセットDialogSumを作成しました。このデータセットは、学校・ビジネス・レジャーなどの、日常生活における様々なトピックに関連した対話を約1万3000件含みます。 DialogSumは主に下記の3つのデータセットから作成されました。これらは対話要約のためのデータセットではありません。例えば、[Sun 19]のDREAMはdialogue understandingというタスクのデータセットです。 [Li 17]のDailydialog [Sun 19]のDREAM [Cui 20]のMuTual 下記に[Chen 21]の図を引用して、DialogSumに含まれる対話の例を示します。これはビジネス上の対話です。 [Mehnaz 21]は、英語とヒンディー語混じりの対話要約のデータセットである、 GupShup を作成しました。このデータセットは、[Gliwa 19]によって作成されたSAMSumをもとに作成されました。 このデータセットはコードスイッチングと呼ばれる、会話中に話し手が異なる言語を切り替えて話す現象に着目しています。論文には、「会話エージェントやチャットプラットフォームの普及に伴い、世界中の多くの多言語コミュニティにおいて、コードスイッチングは文字による会話に不可欠なものとなっています」と書かれています。 下記に[Mehnaz 21]の図を引用します。青い部分が英語で、紫の部分がヒンディー語です。この例では英語での要約だけが示されていますが、データセットは英語とヒンディー語混じりの要約も含みます。 TV番組・インタビュー [Chen 22]はTV番組の対話要約データセットである SummScreen を作成しました。このデータセットは、次の2つのステップにより作成されました。まず、 TVMegaSite と ForeverDreaming から、TV番組のトランスクリプトを獲得します。次に、そのトランスクリプトの要約として、 Wikipedia と TVmaze から番組概要を獲得しました。 下記に[Chen 22]の図を引用し、SummScreenに含まれる対話の例を示します。青い枠にあるSheldonとLeonardの会話は、物語の大筋に関連が薄いため要約文には出てきません。 [Zhu 21]は米国公共ラジオ放送(National Public Radio; NPR)とCNNで行われたインタビューから、 MediaSum を作成しました。ここでは、一般に公開されているインタビューの原稿を対話文とし、NPRとCNNに掲載されたインタビューの説明を要約文としました。 タスク指向の対話要約 会議 [Carletta 05]は 会議のマルチモーダルなデータセットである AMI meeting corpus を作成しました。架空のデザインチームが、リモコンのデザインを話し合うために行ったミーティングが、この データセット に含まれます。また、視線の方向やホワイトボードの情報などの対話以外の情報も含まれています。このデータセットを日本語に翻訳した データセット も存在します。 [Zhong 21]は QMSum を提案しました。これは、クエリに基づく要約タスクのためのデータセットであり、232件の会議に関連する1808件のクエリと要約の組から構成されています。[Janin 03]による ICSI Meeting Corpus と[Carletta 05]によるAMI meeting corpusと議会のミーティングから作成されました。 クエリとは、システムが出力する要約に対する制限です。クエリの例として「Summarize the discussion about remote control style and use cases.」や「Summarize Project Manager's opinion towards remote control style and use cases.」が挙げられます。 オンラインチャットなどのこれまでご紹介したデータセットと比較して、会議はその文量が多く、また多様なトピックを含みます。そのため、会議から短い要約を作成することは難しいことと、参加者によって知りたい内容が異なることに[Zhong 21]は着目しました。その結果、与えられたクエリに応じて要約を作成する対話要約システムのためのデータセットを作成しました。 下記に[Zhong 21]の図を引用し、クエリに基づく要約の例を示します。3つのクエリが入力され、各々に対して対話要約システムが要約を作成します。 メール [Zhang 21a] は、2549件の電子メールスレッドからなるデータセットである EmailSum を作成しました。各々のスレッドは3件から10件のメールを含みます。また、30単語以下の短い要約と100単語以下の長い要約が、各スレッドに付与されています。 このデータセットは下記の3つのメールのデータセットから作成されました。これらにはスレッド構成が整理されていませんでした。そのため、スレッド構成を整理して、要約を作成することが[Zhang 21a]の貢献と言えます。 [Klimt 04]が作成したEnron [Craswell 06]が作成したW3C Avocado Research Email Collection 下記に[Zhang 21a]の図を引用し、短い要約と長い要約の例を示します。 医療 [Song 20]は公開フォーラムで医者と対話できるプラットフォームである Chunyu-Doctor から対話要約のための データセット を作成しました。フォーラム上の対話に対話要約を適用する目的を、医師の手間を削減するためとしています。 [Song 20]の図を引用し、対話の例を示します。ここでは、患者の発言のうち特定のトピックに関連するものを抜粋しています。 カスタマーサービス カスタマーサービスは、企業に所属するエージェントが顧客に対して何らかのサービスを行うことです。対話要約を適用する理由の1つには、エージェントの負担軽減があります。[Liu 19]は、滴滴出行(DiDi)で働く30万のエージェントと顧客の対話に対して、対話要約を適用した論文です。この論文のモチベーションの項では「要約の作成にはエージェントの時間の約25%が費やされています。DiDiには数千人のエージェントがいます。そのため、要約の自動生成は膨大な人的資源を節約することができます。」と書かれています。 [Lin 21]は中国語で書かれた対話要約のデータセットである CSDS (Customer Service domain Dialogue Summarization)を作成しました。これは[Chen 20]が作成した対話データセットJDDCに要約を付け加えることで作成されています。JDDCは、中国のe-コマース最大手である京東商城(JD)のスタッフと顧客間のプリセールスとアフターセールスの対話から構成されています。 下記に[Lin 21]の図を引用します。注文した商品の配送に関して、スタッフと顧客が話し合っており、これに対して3種類の要約が付与されています。 [Zhao 21]は、5つのドメイン(レストラン、ホテル、アトラクション、タクシー、電車)における、約1万の対話とその要約を含んだデータセットTODSumを作成しました。この論文の著者の所属は3つに別れており、北京郵電大学と美団(Meituan)と中国移動(China Mobile)です。美団はe-コマースプラットフォームである美団と、口コミサイトである大衆点評を運営しています。中国移動は世界最大の携帯電話事業者です。 下記に[Zhao 21]の図を引用し、TODSumに含まれる対話の例を示します。 阿里巴巴(Alibaba)に所属する[Zou 21a]は淘寶(Taobao)で行われた顧客とエージェントのチャットログ109万件を使用して、対話要約に取り組みました。この研究で使用された 実装とデータセット が公開されています。この論文の著者は、[Zou 21b]において、阿里巴巴のコールセンターで行われた顧客とエージェントの対話に対して、対話要約を適用したことを報告しています。ただし、こちらはデータセットが公開されていません。 評価手法 要約の手法を評価するための評価指標をいくつか紹介します。これまでは、正解と定義された要約文とシステムが出力した要約文の類似度を計算し、その類似度が高ければ良い要約文と判断していました。近年になって、質問応答システムを使用するなどの、いくつかの評価指標が提案されています。ここでは数式なしで説明しようと試みており、いくつかの評価指標には数式を示していません。 ROUGE ROUGEは最も広く使われる評価指標の1つで、この指標を提案した[Liu 04]の引用件数は9000件を超えています。 ROUGEは2つの文書に共通する単語数に着目して、2つの文書の類似度を計算します。いくつかの派生があり、ROUGE-1はuni-gram(単語)を使用し、ROUGE-2はbi-gram(連続する2単語)を使用します。Huggingface社のライブラリであるevaluateに 実装 があります。 BLEU [Papineni 02]によるBLEUは共通する単語n-gramの数に着目し、類似度を計算します。nはパラメタであり、1から4がよく使用されます。言い換えれば、単語だけでなく、連続する2単語・3単語・4単語に着目します。また、システムが出力した要約文が、正解と定義された要約文よりも短い時にペナルティを与えるように工夫されています。Huggingface社のライブラリであるevaluateに 実装 があります。 下記に計算式を示します。ここでは、システムが出力した要約文の数と、正解と定義された要約文の数が共に1である時の計算式を示しています。BPはシステムが出力した要約文が正解と定義された要約文よりも短い時のペナルティです。また は与えられた文の単語n-gramを返す関数です。pnは2つの文に存在する単語n-gramのうち、共通する単語n-gramの割合を表します。N=4がよく使用され、1-gramから4-gramに基づきBLEUが計算されます。 chrF [Popovi ́c 15]によるchrFは、上記のROUGE・Bleuと異なり、単語ではなく文字n-gramに着目します。はじめに文字n-gramの集合を、正解と定義された要約文と、システムが出力した要約文のそれぞれから獲得します。次にそれらの 適合率と再現率を求め、最後にF値を計算します。 Huggingface社のライブラリであるevaluateに実装があります。n=6が論文で使用されており、また 実装 でもデフォルト値はn=6となっています。 BERTScore これまでにご紹介した指標では、2つの単語を比較する際に完全一致するかを確認しており、同義語や類義語を扱えませんでした。[Zhang* 20a]によるBERTScoreは言語モデルを使用してこの問題に取り組みます。 はじめに言語モデルを用いて、正解と定義された要約文の各トークンをベクトルに変換したものをxとします。次に、システムが出力した要約文の各トークンをベクトルに変換したものをyとします(論文中ではx^でしたが、今回記事を書いた時に読みにくいと感じたのでyに変更しています)。 下記に計算式を示します。 再現率と適合率を計算した後に、その調和平均を求めます。 数式よりも図を使った説明の方が直感的で分かりやすいかもしれません。下記に[Zhang* 20a]の図を引用し、BERTScoreの概要を示します。Contextual Embeddingの部分に置かれたキャラクタはセサミストリートに登場する Bert です。Importance Weightingの部分は上の手順では説明を省略した部分です。基本的にはBERTScoreは各トークンに重みを付けることはしませんが、idfなどを使用して重み付けすることもできます。 また、[Zhao 19]の図を示します。こちらの方がBERTScoreの概要が理解しやすいかもしれません。青色の「System」がシステムが出力した要約文であり、オレンジ色の「Ref」が正解と定義された要約文です。BERTScoreでは、guyとmanあるいはboatとcanoeなどの類似した語を扱うことができ、単純に共通な単語数を数えるよりもより正確な評価が期待できます。 FEQA [Durmus 20]のFEQAは質問応答システムを利用する指標であり、factual consistencyという観点で要約を評価します。言い換えれば、システムが生成した要約文の内容が、要約前の文書の内容にどれくらい近いかを評価しています。下記に、[Durmus 20]の図を引用し、factual consistencyが欠けた要約の例を示します。ここでは、要約前の文書に「born」と書かれていますが、要約文は「died」と書きました。この単語の違いは内容に大きく影響を与えます。 FEQAは3つのステップから構成されています。はじめにシステムが出力した要約文のあるフレーズを[MASK]に置換します。次に[MASK]が答えになるような質問を生成します。最後に質問と要約前の文書を質問応答システムに入力し、置換前のフレーズが答えとして出力されるか確認します。 [Durmus 20]の図を下記に引用し、この指標の計算例を示します。「The home was built for inspection.」という文がシステムが生成した要約文です。ここから、「Q1: What was the home built for?」「Q2: What was built for inspection?」という 2つの質問が作成されました。これらの質問に対する質問応答システムの出力は「A1’: former australian prime minister malcolm fraser and his wife」「A2’: the home」であり、片方は期待されたものと異なります。 各ステップの実装についても簡単に紹介します。はじめのステップでは固有表現抽出や句構造解析器(constituency parser)を使用して[MASK]に置換する箇所を探します。次のステップでは、 QA2Dデータセット で訓練したseq2seqモデルが使用されます。最後のステップでは、 SQuAD (Stanford Question Answering Dataset)でファインチューニングしたBERTを使用します。 実装 は公開されています。 FactSumm FactSummはトリプル抽出を利用した指標であり、factual consistencyを評価します。トリプルとは、2つのオブジェクトとそれらの関係の3つから構成されたデータの表現方法の一種です。FactSummは、要約文と要約前の文書の各々にトリプル抽出を適用して、その結果を比較することで評価を行います。FactSummは論文として公開されておらず、 GitHub で文書と実装が公開されています。 下記にFactSummのGitHubレポジトリから図を引用し、その処理の概要を示します。要約前の文書からは、(Inception, is, science fiction film)というトリプルが抽出され、要約文からは(Inception, is, action film)というトリプルが抽出されました。この2つは異なるので、要約文はfactual consistencyが欠けていると言えます。 まとめ この記事では下記のことを行いました。後編では、最近の対話要約手法について書く予定です。 オープンドメイン対話要約のデータセットとして、「チャット」「日常会話」「TV番組・インタビュー」の3つのカテゴリを取り上げました。 タスク指向の対話要約のデータセットとして、「会議」「メール」「医療」「カスタマーサービス」の4つのカテゴリを取り上げました。 ROUGE・Bleu・chrF・BERTScore・FEQA・FactSummを取り上げました。FEQA・FactSummは、factual consistencyを評価する指標であり、これまで用いられてきた単語のオーバーラップに基づく指標とは大きく異なります。 参考文献 [Carletta 05] Carletta, J., Ashby, S., Bourban, S., Flynn, M., Guillemot, M., Hain, T., Kadlec, J., Karaiskos, V., Kraaij, W., Kronenthal, M., Lathoud, G., Lincoln, M., Lisowska, A., McCowan, I., Post, W., Reidsma, D., and Wellner, P.: The AMI Meeting Corpus: A PreAnnouncement, in Proceedings of the Second International Conference on Machine Learning for Multimodal Interaction, MLMI’05, pp. 28–39, Berlin, Heidelberg (2005), Springer-Verlag [Chen 20] Chen, M., Liu, R., Shen, L., Yuan, S., Zhou, J., Wu, Y., He, X., and Zhou, B.: The JDDC Corpus: A LargeScale Multi-Turn Chinese Dialogue Dataset for E-commerce Customer Service, in Proceedings of the Twelfth Language Resources and Evaluation Conference, pp. 459–466, Marseille, France (2020), European Language Resources Association [Chen 21] Chen, Y., Liu, Y., and Zhang, Y.: DialogSum Challenge: Summarizing Real-Life Scenario Dialogues, in Proceedings of the 14th International Conference on Natural Language Generation, pp. 308–313, Aberdeen, Scotland, UK (2021), Association for Computational Linguistics [Chen 22] Chen, M., Chu, Z., Wiseman, S., and Gimpel, K.: SummScreen: A Dataset for Abstractive Screenplay Summarization, in Proceedings of the 60th Annual Meeting of the Association for Computational Linguistics (Volume 1: Long Papers), pp. 8602–8615, Dublin, Ireland (2022), Association for Computational Linguistics [Craswell06] Craswell, N., Vries, A., and Soboroff, I.: Overview of the TREC-2005 Enterprise Track, Text Retrieval Conference (TREC), , USA (2006) [Cui 20] Cui, L., Wu, Y., Liu, S., Zhang, Y., and Zhou, M.: MuTual: A Dataset for Multi-Turn Dialogue Reasoning, in Proceedings of the 58th Annual Meeting of the Association for Computational Linguistics, pp. 1406–1416, Online (2020), Association for Computational Linguistics [Durmus 20] Durmus, E., He, H., and Diab, M.: FEQA: A Question Answering Evaluation Framework for Faithfulness Assessment in Abstractive Summarization, in Proceedings of the 58th Annual Meeting of the Association for Computational Linguistics, pp. 5055–5070, Online (2020), Association for Computational Linguistics [Feigenblat 21] Feigenblat, G., Gunasekara, C., Sznajder, B., Joshi, S., Konopnicki, D., and Aharonov, R.: TWEETSUMM A Dialog Summarization Dataset for Customer Service, in Findings of the Association for Computational Linguistics: EMNLP 2021, pp. 245–260, Punta Cana, Dominican Republic (2021), Association for Computational Linguistics [Gliwa 19] Gliwa, B., Mochol, I., Biesek, M., and Wawer, A.: SAMSum Corpus: A Human-annotated Dialogue Dataset for Abstractive Summarization, in Proceedings of the 2nd Workshop on New Frontiers in Summarization, pp. 70–79, Hong Kong, China (2019), Association for Computational Linguistics [Janin 03] Janin, A., Baron, D., Edwards, J., Ellis, D., Gelbart, D., Morgan, N., Peskin, B., Pfau, T., Shriberg, E., Stolcke, A., and Wooters, C.: The ICSI Meeting Corpus, in 2003 IEEE International Conference on Acoustics, Speech, and Signal Processing, 2003. Proceedings. (ICASSP ’03)., Vol. 1, pp. I–I (2003) [Jia 22] Jia, Q., Ren, S., Liu, Y., and Zhu, K. Q.: Taxonomy of Abstractive Dialogue Summarization: Scenarios, Approaches and Future Directions (2022) [Klimt 04] Klimt, B. and Yang, Y.: The Enron Corpus: A New Dataset for Email Classification Research, in Proceedings of the 15th European Conference on Machine Learning, ECML’04, pp. 217–226, Berlin, Heidelberg (2004), SpringerVerlag [Li 17] Li, Y., Su, H., Shen, X., Li, W., Cao, Z., and Niu, S.: DailyDialog: A Manually Labelled Multi-turn Dialogue Dataset, in Proceedings of the Eighth International Joint Conference on Natural Language Processing (Volume 1: Long Papers), pp. 986–995, Taipei, Taiwan (2017), Asian Federation of Natural Language Processing [Lin 04] Lin, C.-Y.: ROUGE: A Package for Automatic Evaluation of Summaries, in Text Summarization Branches Out, pp. 74–81, Barcelona, Spain (2004), Association for Computational Linguistics [Lin 21] Lin, H., Ma, L., Zhu, J., Xiang, L., Zhou, Y., Zhang, J., and Zong, C.: CSDS: A Fine-Grained Chinese Dataset for Customer Service Dialogue Summarization, in Proceedings of the 2021 Conference on Empirical Methods in Natural Language Processing, pp. 4436–4451, Online and Punta Cana, Dominican Republic (2021), Association for Computational Linguistics [Liu 19] Liu, C., Wang, P., Xu, J., Li, Z., and Ye, J.: Automatic dialogue summary generation for customer service, in Proceedings of the 25th ACM SIGKDD International Conference on Knowledge Discovery & Data Mining, pp. 1957– 1965 (2019) [Mehnaz 21] Mehnaz, L., Mahata, D., Gosangi, R., Gunturi, U. S., Jain, R., Gupta, G., Kumar, A., Lee, I. G., Acharya, A., and Shah, R. R.: GupShup: Summarizing Open-Domain Code-Switched Conversations, in Proceedings of the 2021 Conference on Empirical Methods in Natural Language Processing, pp. 6177–6192, Online and Punta Cana, Dominican Republic (2021), Association for Computational Linguistics [Papineni 02] Papineni, K., Roukos, S., Ward, T., and Zhu, W.-J.: Bleu: a Method for Automatic Evaluation of Machine Translation, in Proceedings of the 40th Annual Meeting of the Association for Computational Linguistics, pp. 311–318, Philadelphia, Pennsylvania, USA (2002), Association for Computational Linguistics [Popovi ́c 15] Popovi ́c, M.: chrF: character n-gram F-score for automatic MT evaluation, in Proceedings of the Tenth Workshop on Statistical Machine Translation, pp. 392–395, Lisbon, Portugal (2015), Association for Computational Linguistics [Song 20] Song, Y., Tian, Y., Wang, N., and Xia, F.: Summarizing Medical Conversations via Identifying Important Utterances, in Proceedings of the 28th International Conference on Computational Linguistics, pp. 717–729, Barcelona, Spain (Online) (2020), International Committee on Computational Linguistics [Sun 19] Sun, K., Yu, D., Chen, J., Yu, D., Choi, Y., and Cardie, C.: DREAM: A Challenge Data Set and Models for Dialogue-Based Reading Comprehension, Transactions of the Association for Computational Linguistics, Vol. 7, pp. 217–231 (2019) [Zhang 20a] Zhang , T., Kishore , V., Wu , F., Weinberger, K. Q., and Artzi, Y.: BERTScore: Evaluating Text Generation with BERT, in International Conference on Learning Representations (2020) [Zhang 20b] Zhang, Y., Jiang, Z., Zhang, T., Liu, S., Cao, J., Liu, K., Liu, S., and Zhao, J.: MIE: A Medical Information Extractor towards Medical Dialogues, in Proceedings of the 58th Annual Meeting of the Association for Computational Linguistics, pp. 6460–6469, Online (2020), Association for Computational Linguistics [Zhang 20c] Zhang, Y., Sun, S., Galley, M., Chen, Y.-C., Brockett, C., Gao, X., Gao, J., Liu, J., and Dolan, B.: DialoGPT: Large-Scale Generative Pre-training for Conversational Response Generation, in ACL, system demonstration (2020) [Zhang 21a] Zhang, S., Celikyilmaz, A., Gao, J., and Bansal, M.: EmailSum: Abstractive Email Thread Summarization, in Proceedings of the 59th Annual Meeting of the Association for Computational Linguistics and the 11th International Joint Conference on Natural Language Processing (Volume 1: Long Papers), pp. 6895–6909, Online (2021), Association for Computational Linguistics [Zhang 21b] Zhang, Y., Ni, A., Yu, T., Zhang, R., Zhu, C., Deb, B., Celikyilmaz, A., Awadallah, A. H., and Radev, D.: An Exploratory Study on Long Dialogue Summarization: What Works and What’s Next, in Empirical Methods in Natural Language Processing (EMNLP) 2021 (2021) [Zhao 19] Zhao, W., Peyrard, M., Liu, F., Gao, Y., Meyer, C. M., and Eger, S.: MoverScore: Text Generation Evaluating with Contextualized Embeddings and Earth Mover Distance, in Proceedings of the 2019 Conference on Empirical Methods in Natural Language Processing and the 9th International Joint Conference on Natural Language Processing (EMNLP-IJCNLP), pp. 563–578, Hong Kong, China (2019), Association for Computational Linguistics [Zhao 21] Zhao, L., Zheng, F., He, K., Zeng, W., Lei, Y., Jiang, H., Wu, W., Xu, W., Guo, J., and Meng, F.: TODSum: Task-Oriented Dialogue Summarization with State Tracking (2021) [Zhong 21] Zhong, M., Yin, D., Yu, T., Zaidi, A., Mutuma, M., Jha, R., Awadallah, A. H., Celikyilmaz, A., Liu, Y., Qiu, X., and Radev, D.: QMSum: A New Benchmark for Query-based Multi-domain Meeting Summarization, in Proceedings of the 2021 Conference of the North American Chapter of the Association for Computational Linguistics: Human Language Technologies, pp. 5905–5921, Online (2021), Association for Computational Linguistics [Zhu 21] Zhu, C., Liu, Y., Mei, J., and Zeng, M.: MediaSum: A Large-scale Media Interview Dataset for Dialogue Summarization, in Proceedings of the 2021 Conference of the North American Chapter of the Association for Computational Linguistics: Human Language Technologies, pp. 5927–5934, Online (2021), Association for Computational Linguistics [Zou 21a] Zou, Y., Lin, J., Zhao, L., Kang, Y., Jiang, Z., Sun, C., Zhang, Q., Huang, X., and Liu, X.: Unsupervised summarization for chat logs with topic-oriented ranking and context-aware auto-encoders, in Proceedings of the AAAI Conference on Artificial Intelligence, Vol. 35, pp. 14674– 14682 (2021) [Zou 21b] Zou, Y., Zhao, L., Kang, Y., Lin, J., Peng, M., Jiang, Z., Sun, C., Zhang, Q., Huang, X., and Liu, X.: Topic-oriented spoken dialogue summarization for customer service with saliency-aware topic modeling, in Proceedings of the AAAI Conference on Artificial Intelligence, Vol. 35, pp. 14665–14673 (2021)
この記事は RevComm Advent Calendar 2022 の 25 日目の記事です。 はじめに  株式会社 RevComm 執行役員 CTO の平村 健勝 (@hiratake55) です。2022 年は、AI 搭載 IP 電話 MiiTel の機能改善から大規模なシステム移行、セキュリティ面での改善、さらにインドネシアをはじめとする海外事業の拡大、AI 搭載オンライン商談解析ツール MiiTel for Zoom の正式リリースなど、社会やビジネスにおけるさまざまな課題を解決する製品をリリース・改良してまいりました。  この記事では、このような急速な発展を遂げるために、技術的な挑戦だけでなく、私やほかのエンジニアリングマネージャー陣が実践している、あまり触れられないユニークな工夫について紹介したいと思います。 エンジニア出身ではないCEOの良いところ  すべての会社は「 エンジニア出身の CEO がいる会社 」「 エンジニア出身ではない CEO がいる会社 」の 2 つのタイプに分類されると思います。中には、エンジニア出身ではないものの一定のシステムの知識がある CEO もいるでしょう。そして、弊社 CEO の會田は、大学も文系学部、三菱商事の出身で、エンジニア出身ではなく、後者のタイプです。  開発部門にとって「エンジニア出身ではない CEO」は、一見 デメリットが多い と思われがちですが、必ずしもそうではなく、次の表のようにそれぞれによい点があります。 エンジニア出身の CEO のよいところ エンジニア出身ではない CEO のよいところ(一例) システム構成やシステムの特徴、開発の方法論などを時間をかけて説明する必要がない セキュリティやシステム可用性、保守性など、非機能面でのリスクアセスメントや経営判断ができる 収益性を見積もりにくい R&D などの活動に対して理解が得やすい マーケティングの知識や営業力があるので、事業や案件の拡大が高速 技術的実現性を度外視した、突拍子もないアイデアが出てくる ファイナンス(P/L, B/S, C/F など)を意識した意思決定ができる 他社のエグゼクティブ層との広く深い人脈がある  このため、エンジニア出身ではないCEOの会社においては、 工夫が求められるところは工夫する ことにより、エンジニア出身の CEO の会社には難しいことを 圧倒的にスピーディーに進めることができる と考えています。  これらを踏まえ、エンジニア出身ではない CEO の会社のよいところを最大限生かしながら、 エンジニア出身の CEO の会社のよいところを上回るような組織 を目指して、我々が実践していてうまく機能している方策を 5 つ紹介します。 方策 1: 開発スケジュールや開発方針の説明に比喩を用いる  前例がある開発タスクはある程度正確にスケジュールを見積もることができますが、RevComm では社内どころか世界的に前例がない取り組みであっても、それがユーザーや社会に必要とされているならばむしろ積極的に取り組もうという文化があります。 このため、 工数の見積もりが難しかったり 、 そもそも実現できるのかどうかも不明な状態 から着手することがあります。  このため、予定より遅延したり早期にリリースできてしまうこともしばしばあります。このような状況を説明するには、 建設工事 を例に出すと、スムーズに理解を得ることが多いです。  例えば、すでに何棟も実績がある戸建住宅の建築工事はある程度正確に工事完了の時期を見積もることができるでしょう。しかし、例えば高速道路や地下鉄のような土木工事で、まだトンネルを掘ったことのない場所にトンネルを作るのは、工事の途中で何が起こるかわかりません。快調に進めば予定より早く完了するでしょうし、工事中に想定していなかった硬い地盤や軟弱な地盤が見つかると、数か月や数年単位で遅れが発生してしまいます。 このように、想像しづらいシステム開発の工程を イメージしやすい現実的な比喩 を用いて説明することで、手触り感のある説明にできることが多いです。 方策 2: エンジニアにはそれぞれの得意分野があることを伝える  同様に比喩を用いるケースとして、それぞれのエンジニアに専門分野があることについては、高い専門性を持ったスペシャリストが活躍する 医療現場 に例えるとスムーズに理解を得られることが多いです。  創業間もないシード期には、フロントエンドから、モバイルアプリ、サーバサイド、インフラ、デザインや機械学習に至るまで、幅広い領域をカバーできるフルスタックエンジニアが活躍できることが多くあります。つまりこれは個々の病気に対する専門性は高くないものの幅広い知識を持った " かかりつけ医 " のようなタイプのドクターです。  しかし、組織の規模が大きくなると、高い技術や経験、ナレッジが求められるため、役割が分担されていきます。これは総合病院や大学病院のように、内科、外科、眼科などそれぞれの専門性が高く発揮できる組織に分けられていることに近いです。一方で、フルスタックエンジニアも救急救命医のように、分野横断的な課題の解決に高く貢献できます。  例えば、優先度を高めて対応しないといけない急な ToDo が生まれた際、エンジニア出身ではない CEO は「 エンジニアは全員今の仕事を停めて優先度の高い ToDo に着手すること 」といった指示を出してしまいがちです。例えば、サーバサイドアプリケーションの不具合改修をモバイルアプリエンジニアが担当することは、 外科のドクターが内科の病気を診察する ような状況であり専門領域が違います。 各メンバーが得意な分野で活躍できると、効率的に課題を解決したり早期に目標を達成することができます。このように、役割分担についても納得感があり透明性の高い形で説明することを心がけています。  また、開発プロセスにおいて、フロントエンドエンジニアとサーバサイドエンジニアのように役割分担して開発することを「カレー作り」に例えて説明しました。 下記の図は全社オフサイトミーティングでも説明したスライドの抜粋です。  役割分担が進んでも、企画構想から設計、開発、QA、リリースに至る全体のプロセスを俯瞰することで、技術的に高いレベルの取り組みを高速に進めることの重要性を伝えました。 方策 3: 全メンバーが共通の目標を意識する  これは、CEO に対して行っている取り組みではなく、メンバー全員に対して心がけるようにしている内容です。  製品開発を進めるにあたって、CEO や事業責任者(プロダクトオーナー)が一方的に指示し、開発やデザイン、マーケティング、セールスを行う形の組織もあると思います。しかし、RevComm ではそのような開発方法はそぐわないと判断し、創業初期からエンジニアやデザイナーだけでなく、セールスやカスタマーサクセス、カスタマーサポートのようなプロダクト・ビジネス部門に加えて、コーポレート部門(管理部門)のメンバーも一体となり、 全員が主導で共通の目標を達成することを意識しています 。 これにより、短期間で高い収益、高い満足度が得られるような新しいアイデアを提案したり、トレンドに合わせて製品をアップデートしたり、今後増えるであろうお客様のニーズを先んじて実装することにつながり、 競争力の高い製品をマーケットにいち早くリリースすることができる と考えています。 方策 4: 相手の立場に立って、相手に伝わる言葉を選ぶ  リファクタリングや長期間運用してきたシステムのリニューアルなどのように、収益性が見通しづらい、いわゆる " 守りの開発 " や、避けてほしいこと、お願いしたいことも、言葉を選んで言い換えることで、その重要性をスムーズに理解してもらえることが多くあります。 以下にその一例を挙げます。 望ましくない伝え方 望ましい伝え方 開発しにくくなったので、追加開発は今後半年間ストップして全面的に作り直します 今後企画されている機能を現状のアプリに組み込むとなると、3 か月程度あればできます。 しかし、半年かけて作り直すと、1 か月程度で済むようになります。さらに、今後他の追加開発も短期間で完了でき、プロダクトアップデートのスピードも速くなるメリットがあります 現在商談中の大型案件では、想定されていない規模のユーザーが一度に増えるので止めてほしいです パフォーマンステストを十分にできていないため、導入後トラブルが発生してお客様にご迷惑をおかけする可能性があります 商談中の会社は我々にとっても大事なお客様なので、受注後に解約なされると心象が悪くなってしまい、今後再提案もしづらくなると思うので、1 か月ほど時間をください。その間に、安心して利用できるための準備が整う可能性があります 現在商談中のお客様から寄せられている要望を期間内に すべて 満たすようにリリースすることは不可能です 開発チームのリソースにも限りがあるので、できるだけお客様の期待に多く応えるためにも、重要度の高いものから進めたいので、必須のものを教えてください セキュリティレベルを向上させるため、製品の導入に数百万円の投資が必要です インシデントの発生確率はだいたいこのくらいで、仮に発生した場合、弁護士費用や失った収益、信用失墜などを考慮すると、大体数千万円〜数億円単位の損失が発生します 同様のセキュリティ事故を起こした事例としては○○社があり、このようなリスクを回避できるため、導入を進めたいです 方策 5: 説明すべき事項を整理する  プログラミング言語、フレームワーク、クラウドサービスのような技術選定、開発フローなどに関しては、 あえて CEO に説明していません (もし質問があればもちろん納得いくまで説明しています)。  なぜならば、 技術選定や開発方針については完全に CTO に権限移譲されている からです。エンジニア出身でない CEO にとって専門範囲外の質問や相談を受けても、適切な判断をすることは簡単ではなく、そのような相談がなされても困るでしょう。  仮に誤った判断をしてしまったら、CEO ではなく CTO が責任を持ってリカバリする方針にしているので、CEO にとっても本来やるべきことに集中して安心して気持ちよくビジネスの拡大を推進することができます。 まとめ  RevCommでは " コミュニケーションを再発明し、人が人を想う社会を創る " というミッションを達成するために、上記のような方策を通して、サービスをご利用いただいているお客様の期待に応えることはもちろん、エンジニアやデザイナーが気持ちよく働ける組織を作り、さまざまな工夫やアイデアを組み合わせて目標に向かって取り組んでいます。  ご興味を持った方は、採用中のポジションの詳細を、 RevComm 採用情報 で公開していますので、ぜひご覧ください。 hrmos.co
この記事は RevComm Advent Calendar 2022 の 24 日目の記事です。 はじめに クリアすべき 4 つのステップ 1. 個人のモチベーション向上 Will Can Need まとめ 2. パフォーマンス向上 条件 1: 強い意志 条件 2: リラックス 条件 3: 手順の明確なイメージ 条件 4: フィードバック 条件 5: ちょっとした混乱(緊張感) まとめ 3.企業の業績向上 まとめ 4. 社会への価値創造   まとめ 最後に はじめに 2022 年 10 月に RevComm にシニアエンジニアリングマネージャーとして入社した樋沼です。前職ではエンジニア組織と兼務でビジネス組織の責任者をしていました。 エンジニア組織を内側と外側から見てきましたが、「エンジニアがイキイキしていないと IT 企業は成長しない」という当たり前のことを改めて痛感しています。そして、企業のミッションを実現させるために「個人の意志」と「企業の理念」をつなぐには何が重要なのか、ようやくわかってきました。 本日は、両者をつなぐためにエンジニアリングマネージャーとしてできることについて書きます。これまで私が経験したことの棚卸しをし、これから RevComm で取り組んでいきたい内容をまとめます。 クリアすべき 4 つのステップ 最初に「個人の意志」と「企業の理念」の間をつなぐための、私が考える 4 つのステップを紹介します。 4 つのステップ これらは段階的にクリアしていくステップになりますが、離脱ポイントも多くあります。例えば個人のモチベーションが高まっても、集中できる環境が整わなければパフォーマンスが発揮できないどころか、モチベーション低下に陥ります。 また、全エンジニアのパフォーマンスが最高であったとしても、ビジネスに直結する開発案件を最適に割り振っていなければ、企業の業績が上がらないという結末もありえます。 後半の「3. 企業の業績向上」や「4. 社会への価値創造」は、エンジニアリングマネージャーが直接影響を及ぼすことは困難な領域ですが、ビジネス視点や経営視点を意識することで最後まで到達できると考えています。 1. 個人のモチベーション向上 まずは教科書的な話になってしまいますが、個人の「やりたいこと = Will」と「できること = Can」を確認し、「求められていること = Need」と突き合わせます。 Will 仕事に対する考え方や譲れない価値観は個人ごとに千差万別で、それは他人に否定されずに尊重されるべきコアなものです。最初にこの Will を丁寧にヒアリングして正しく理解することに努めます。 一括りにエンジニアといっても「技術を手段としてビジネスに貢献したい」や「技術の習得を追求したい」などタイプはさまざまです。RevComm では、等級制度のグレードを段階的に上がっていく過程で、エンジニアはマネジメント (M) かプロフェッショナル (P) でキャリアパスを選択することになります。 Can 次に「これまで何をやってきたか」「これから伸ばしたい強みは何か」を確認します。言語化できる顕在化した強みもありますが、ヒアリングの途中で本人が気付いたり、まだ潜在的である強みをマネージャーが感じ取ることもあります。 RevComm のエンジニアは、各プロジェクトに参画しつつ、職能(フロントエンド/バックエンド/インフラなど)チームにも所属する「マトリクス組織」で業務を行います。自分が伸ばしたい強みを磨くために、マネージャーに相談して新しいチャレンジに取り組むケースもあります。 Need 個人の Will と Can を踏まえた上で、ビジネスサイドからのタスク (Need) を割り振ることになります。ただ、チームがどんなメンバー構成であっても、タスク自体は基本的には変わりません。 ここでマネージャーは、個人の Will や Can に対する意味づけを添えてタスクを渡すことが重要になります。タスクの背景/目的/期待値/得られる経験値など、相手に合わせてタスクの渡し方を工夫することで、個人のモチベーション向上につなげることができます。 まとめ やりたいことを周囲に話すと、誰かがそれを聞いて話が組織内を伝わり、巡り巡って自分の元に機会が訪れることがよくあります。たった数か月でも、RevComm 内で何人もの Will が伝わって実現されていくのを見ているので、改めて“意志の芽”を大切にしていきたいと思います。 2. パフォーマンス向上 エンジニアが高いモチベーションを維持したまま、期間中のパフォーマンスを高めるには、集中できる業務環境が重要になります。その際に、私は「フロー (Flow)」と呼ばれる心理状態に着目しています。 フローとは:時間を忘れるほど目の前のことに夢中になって、集中している状態 ※世羅侑未『3 倍のパフォーマンスを実現するフロー状態 魔法の集中術』(総合法令出版、2019年)より フローな状態になるための条件は以下の 5 つあると言われています。(諸説あり) 条件 1: 強い意志 大前提として、個人がそのタスクに取り組む意志が必要です。前述の「個人の意志」を理解し、そのタスクの目的や自分の成長にとっての意味合いを納得している状態を整えます。 条件 2: リラックス 業務に集中して取り組む上で、力まず平常心を保てる状態が必要です。まず、会社として働きやすい環境を提供できるかが重要ですが、RevComm ではフルリモート/フルフレックス制を採用しています。また、ミーティングは極力短い時間で終わらせるようにするなどで、業務に集中できる時間を確保できるようにしています。 マネージャーとしては、1 on 1 や懇親会で何でも話せる機会をつくり、心理的安全性が構築できているか、本音を話してくれているか声色や表情にも注意を払います。 条件 3: 手順の明確なイメージ どこから手をつけていいかわからないタスクを渡されると頭がスタックしてしまうものです。頭の中である程度の業務フローが展開できれば、スムーズに取り組むことができます。 タスクの難易度は本人のスキルより少し高いハードル設定が望ましいため、どこまで問題を解きほぐした状態で相手に渡すか、マネージャーの見極めがポイントになります。 条件 4: フィードバック 業務に取り組み始めたら、その方向性やスピード感で問題ないか本人が随時確認できることが必要です。 定量的には、作業の進捗率が可視化されたり、顧客の利用状況など KPI がエンジニアでもすぐに確認できる環境を整えることができます。また、定性的には、1 on 1 で順調であることや改善点を伝えることで、安心して業務できる状態を維持します。ただ、マネージャーが改善点を伝える際は、答えを伝えるよりも本人に気付きを促すほうが効果的です。自分の口で対策を言語化できるように、壁打ちの相手になるのが理想的だと思います。 条件 5: ちょっとした混乱(緊張感) 条件 2 のリラックスは必要ですが、度が過ぎると緊張感のない惰性の作業時間になってしまいます。組織の中には「ちょっとした緊張感」があることが望ましいと考えています。 そのため、正論や理想論など思ったことは立場を越えて主張するように投げかけています。私もチーム内からの突き上げを受けて、自分のリミッターを外して行動できるように成長できたという体験があります。組織内で相互にプレッシャーをかけられる関係性が、ちょっとした混乱=緊張感をもたらします。 まとめ パフォーマンスを向上させる上で権限移譲は重要な要素です。RevComm では、マネージャーはタスクの背景 (Why) と開発要件 (What) を明示しますが、実装方法 (How) はチームやエンジニアに一任されます。そして工数見積もりの結果、目標期日 (When) が要件と合わない場合は、優先順位の組み換えをビジネスサイドと調整したり、人員の増強などを全力で後方支援しています。 3.企業の業績向上 個人のパフォーマンスが最大限に発揮されたら、企業の業績に連動されるべきです。ただ、それは業績につながる業務がタスクとして割り振られ、適切な目標値を達成していることが前提となります。 企業の業績はPdM (Product Manager) や PMM (Product Marketing Manager) などビジネスサイドが担うため、エンジニア組織のマネージャーは基本的に責任の範疇外です。ただ、PdM や PMM に対して企業の売上予算がどう因数分解されビジネスサイドがどんな KPI として担っているか、また各開発タスクはどの KPI を期待されているかを確認することはできます。 PM (Project Manager)を含めたエンジニアリングマネージャーは、期初に行われるキックオフなどで共有される目標や KPI を確認するだけでなく、期中も PdM と密にコミュニケーションをとって、エンジニアの OUTPUT が業績につながることを意識する必要があります。 ちなみに、「戦略」と「戦術」のよしあしの組み合わせで、絶対に回避しなければならないパターンはどれだと思いますか? 最もよい状態は A であるのは間違いないですが、最も悪い状態は D でなく C になります。「よい戦略は正しい方向に動く」「よい戦術は遠くに動く」と捉えると、適切でない方向に遠く動いた分だけコストもかかり修正範囲も大きくなります。 まとめ エンジニアリングマネージャーは貴重なエンジニアの工数に責任を持っている立場です。ビジネス面を任せきりにするのでなく、戦略や戦況を読み取り PdM/PMM と議論できるビジネススキルも習得することが重要だと考えます。 4. 社会への価値創造   会社の業績が向上したらその規模の分だけ、社会への価値創造のインパクトが高まります。エンジニアリングマネージャーができることは、企業理念や経営の描くビジョンを自分の言葉で言語化して、相手の価値観に合わせて伝えることだと思います。 RevComm では『コミュニケーションを再発明し、人が人を想う社会を創る』という理念を掲げています。私は、企業活動におけるコミュニケーション不足やコミュニケーションロスを、定例会議、1 on 1、入社面接、カスタマーサポート、ユーザーヒアリングなど様々な場面で実感してきました。これらの内容が解析され相互の意思疎通が改善できたら、大幅な生産性の向上につながると確信して RevComm への入社を決めました。 また、『人が人を想う社会を創る』という表現も、“企業が社員により最適な環境を提供する”や“企業が顧客により便利なサービスを提供する”など、さまざまなシーンに広げて世界観を描くことができます。 時にはマネージャーも自分自身の Will と企業の理念を重ねてみる機会を持つのがよいと思います。 まとめ 企業の MVV (Mission/Vision/Value) は、日常の業務の中では忘れられがちです。ただ、壁にぶつかったときや企業内で対立軸が生じたときに、一度立ち止まって「この会社の存在意義は何か?」「自分は何をやり遂げたくてこの会社にいるのか?」を見つめ直すと解決策が見えるものです。 そんな時にマネージャーが「企業の理念」が実現したいビジョンを自分の言葉で具現化して、「個人の意志」の延長線上に重ねて示せれば、価値創造につながる推進力が生まれるはずです。 最後に エンジニアリングマネージャーは、エンジニアが持つ能力を最大限に発揮させ、それを業績や企業理念の実現につなげる重要な役割を担っています。偉そうなことを語ってきましたが、私も自分で書いた内容を実行するように精進していきたいと思います。 この記事をきっかけに RevComm の企業理念に共感してくださった方がいらっしゃいましたら、ぜひご応募ください。 www.revcomm.co.jp
この記事は RevComm Advent Calendar 2022 の 22 日目の記事です。 はじめに こんにちは、バックエンドエンジニアのまつどしんたろうです。 私は最近 Rust で Slackbot を作っています。 そのBotは、 @Slack名 ++ と投稿すると、 Thank you @Slack名 (counter: 1) と返答してくれます。 また、もう一度 @Slack名 ++ と投稿すると、 Thank you @Slack名 (counter: 2) となり、感謝された数だけ counter が増えていきます。 この Bot は弊社の制度である 15% ルールを使って開発しました。 15% ルールとは、業務外の活動への積極的な関与を推奨するために、業務時間のうち 15% はコア業務外の活動に取り組んで良いというルールです。 このルールのおかげで、日々、技術力の幅を広げることができています。 今回は、Slackbot にオウム返しをしてもらうところまでを記事にしていきます。 モチベーション 世のため人のために行動するというカルチャーをさらに根付かせたい フルリモートの環境でも感謝の気持ちを気軽に表現できるようにしたい という思いから開発しました。 インスパイア この Bot は、PyConJP Staff 用の Slack に導入されていて、感動しぜひ社内にも取り入れたいと思い開発をはじめました。 PyConJP 2022 でも紹介されているのでぜひご覧になってみてください。 https://2022.pycon.jp/timetable?id=ELUNPR 構成 業務では主に Python を使っていますが、触ってみたいという理由で Rust を採用しています。 今回はお試しのため、EC2 に環境を構築します。 EC2 (Amazon Linux) Rust Axum 今回、出来上がるコードはこちらです。 Cargo.toml [package] name = "slackbot" version = "0.1.0" edition = "2021" publish = false [dependencies] axum = "0.6.1" tokio = { version = "1.0", features = ["full"] } serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } tracing = "0.1" tracing-subscriber = { version = "0.3", features = [ "std", "env-filter" ] } dotenv = "0.15.0" reqwest = { version = "0.11", features = ["json"] } .env VERIFICATION_TOKEN= BOT_USER= BOT_USER_OAUTH_TOKEN= main.rs use axum :: { http :: StatusCode, routing :: {get, post}, Router, }; use std :: net :: SocketAddr; use dotenv :: dotenv; mod slackbot ; use crate :: slackbot :: slackbot; #[tokio::main] async fn main () { init ().await; let app = Router :: new () . route ( "/" , get (handler)) . route ( "/slackbot" , post (slackbot)); let addr = SocketAddr :: from (([ 127 , 0 , 0 , 1 ], 8080 )); tracing :: debug! ( "listening on {}" , addr); axum :: Server :: bind ( & addr) . serve (app. into_make_service ()) .await . unwrap (); } async fn init () { tracing_subscriber :: fmt () . with_max_level ( tracing :: Level :: DEBUG) . init (); dotenv (). ok (); } async fn handler () -> (StatusCode, String ) { ( StatusCode :: OK, String :: from ( "Hello, World!" )) } slackbot.rs use std :: env; use axum :: { http :: StatusCode, Json}; use serde :: {Deserialize, Serialize}; #[derive( Debug , Deserialize)] pub struct Request { token: String , challenge: Option < String > , event: Option < Event > , } impl Request { fn is_initialize ( & self ) -> bool { self .challenge. is_some () } } #[derive( Debug , Deserialize)] struct Event { channel: String , user: String , text: String , thread_ts: Option < String > , } impl Event { fn from_bot ( & self , bot_user: String ) -> bool { self .user == bot_user } } #[derive( Debug , Serialize)] struct PostBody { text: String , channel: String , thread_ts: Option < String > , } #[derive( Debug , Serialize)] pub struct Response { ok: bool , challenge: Option < String > , } pub async fn slackbot ( Json (req): Json < Request > ) -> (StatusCode, Json < Response > ) { tracing :: info! ( "slackbot" ); // Validate let verification_token = env :: var ( "VERIFICATION_TOKEN" ). expect ( "VERIFICATION_TOKEN must be set" ); if req.token != verification_token { tracing :: warn! ( "AuthenticationFailed, token: {}" , req.token); let res = Json (Response { ok: false , challenge: None , }); return ( StatusCode :: BAD_REQUEST, res); } // Verify from Slack if req. is_initialize () { let res = Json (Response { ok: true , challenge: req.challenge, }); return ( StatusCode :: OK, res); } // Validate match & req.event { Some (event) => { let bot_user = env :: var ( "BOT_USER" ). expect ( "BOT_USER must be set" ); if event. from_bot (bot_user) { tracing :: debug! ( "From happy bot" ); let res = Json (Response { ok: false , challenge: None , }); return ( StatusCode :: BAD_REQUEST, res); } } None => { let res = Json (Response { ok: false , challenge: None , }); return ( StatusCode :: BAD_REQUEST, res); } } // Execute let event = req.event. unwrap (); let post_body = PostBody { text: event.text, channel: event.channel, thread_ts: event.thread_ts. map ( | x | x), }; post_request (post_body).await; let res = Json (Response { ok: true , challenge: None , }); return ( StatusCode :: OK, res); } async fn post_request (post_body: PostBody) -> reqwest :: Result < () > { tracing :: info! ( "slackbot__post_request" ); let url = "https://slack.com/api/chat.postMessage" ; let bot_user_oauth_token = env :: var ( "BOT_USER_OAUTH_TOKEN" ). expect ( "BOT_USER_OAUTH_TOKEN must be set" ); let client = reqwest :: Client :: new (); let response = client . post (url) . header ( reqwest :: header :: AUTHORIZATION, format! ( "Bearer {}" , bot_user_oauth_token), ) . json ( & post_body) . send () .await ? ; Ok (()) } 準備① Slack API の App 作成 1/4 App 作成 Slack api の Your Apps ( https://api.slack.com/apps?new_app=1 ) の Create New App から App を作成します。 今回は From scratch から作成しました。 2/4 Token 取得 取得した Token は .env ファイルに記載します。 Basic Information ( https://api.slack.com/apps? ) > App Credentials の Verification Token を取得します。 OAuth & Permissions ( https://api.slack.com/apps/XXXXXXXXXXXX/oauth? ) の Reinstall to Workspace からチャンネルにインストールします。 Bot User OAuth Token が現れるので取得します。 3/4 権限の設定 OAuth & Permissions ( https://api.slack.com/apps/XXXXXXXXXXXX/oauth? ) の Scope Bot Token Scopes と EventSubscriptions ( https://api.slack.com/apps/XXXXXXXXXXXX/event-subscriptions? ) の Subscribe to bot events に 必要な権限を追加します。 4/4 Slack チャンネルへの追加 導入したい Slack チャンネルに行き、Slack チャンネル名をクリックし、インテグレーションからアプリを追加します。 実装① Rust のインストールと Rust で POST を受け取れるように設定 1/5 Rust をインストール rustup を使ってインストールします。詳細は 公式ドキュメント を参照してください。 2/5 Axum の Hello world Axum は Rust の Web フレームワークのひとつです。Axum が豊富に example を提供してくれているので、これを元にして Bot の開発をしていきます。 まずは、最小構成で Hello World をやってみます。 https://github.com/tokio-rs/axum/tree/main/examples/hello-world をclone Cargo.toml の axum を axum = "0.6.1" に変更 port を 8080 に変更 (Optional) REST API にしたいので hundler を修正 (Optional) cargo run でサーバーを立ち上げます curl localhost:8080 を叩くと、 Hello, World! が返ってきます main.rs - use axum::{response::Html, routing::get, Router}; + use axum::{http::StatusCode, routing::get, Router}; use std::net::SocketAddr; #[tokio::main] async fn main() { let app = Router::new() .route("/", get(handler)) - let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); + let addr = SocketAddr::from(([127, 0, 0, 1], 8080)); println!("listening on {}", addr); axum::Server::bind(&addr) .serve(app.into_make_service()) .await .unwrap(); } - async fn handler() -> Html<&'static str> { - Html("<h1>Hello, World!</h1>") + async fn handler() -> (StatusCode, String) { + (StatusCode::OK, String::from("Hello, World!")) } 3/5 challenge を返す Slack API に URL を登録する際に、有効な URL だと Slack に知らせる必要があります。 POST で verification_token と challenge というランダムな値が送られてくるので、そのまま challenge を返してあげることで有効だと判断されます。 serde クレート *1 を追加します。serde はシリアライズ/デシリアライズをしてくれるクレートです。 別ファイルでメソッドを追加します。 slackbot.rs use axum :: { http :: StatusCode, Json}; use serde :: {Deserialize, Serialize}; #[derive( Debug , Deserialize)] pub struct Request { token: String , challenge: Option < String > , } #[derive( Debug , Serialize)] pub struct Response { ok: bool , challenge: Option < String > , } pub async fn slackbot ( Json (req): Json < Request > ) -> (StatusCode, Json < Response > ) { let res = Json (Response { ok: true , challenge: req.challenge, }); ( StatusCode :: OK, res) } 3. main.rs に新しく POST を受け取れる Route を追加します。 main.rs - use axum::{http::StatusCode, routing::get, Router}; + use axum::{ + http::StatusCode + routing::{get, post}, + Router, + }; use std::net::SocketAddr; + mod slackbot; + use crate::slackbot::slackbot; #[tokio::main] async fn main() { let app = Router::new() .route("/", get(handler)) + .route("/slackbot", post(slackbot)); let addr = SocketAddr::from(([127, 0, 0, 1], 8080)); 4/5 .env を導入する 「準備① 2/4 Token 取得」で取得できた値 .env ファイルに設定します。読み取りには dotenv クレートを使います。 main.rs use std::net::SocketAddr; + use dotenv::dotenv; mod slackbot; use crate::slackbot::slackbot; (中略) async fn init() { tracing_subscriber::fmt() .with_max_level(tracing::Level::DEBUG) .init(); + dotenv().ok(); } 5/5 tokenでバリデートする。 「4/5 .env を導入する」で設定した値でバリデートします。 slackbot.rs + use std::env; + use axum::{http::StatusCode, Json}; use serde::{Deserialize, Serialize}; #[derive(Debug, Deserialize)] pub struct Request { token: String, challenge: Option<String>, } #[derive(Debug, Serialize)] pub struct Response { ok: bool, challenge: Option<String>, } pub async fn slackbot(Json(req): Json<Request>) -> (StatusCode, Json<Response>) { + // Validate + let verification_token = + env::var("VERIFICATION_TOKEN").expect("VERIFICATION_TOKEN must be set"); + + if req.token != verification_token { + tracing::warn!("AuthenticationFailed, token: {}", req.token); + let res = Json(Response { + ok: false, + challenge: None, + }); + return (StatusCode::BAD_REQUEST, res); + } let res = Json(Response { ok: true, challenge: req.challenge, }); (StatusCode::OK, res) } 準備② URL の登録 EC2 で適当なインスタンスを立ち上げて、SSH と HTTP で通信ができるように設定します。 EventSubscriptions ( https://api.slack.com/apps/XXXXXXXXXXXX/event-subscriptions? ) で、上記で準備した EC2 の URL (例: http://ec2-12-345-678-90.ap-northeast-1.compute.amazonaws.com/slackbot) を RequestURL の欄に入力します。 Slack API から送られてきた challenge を返せていれば Verified になります。 実装② Bot から Slack に投稿する 1/2 Slack からの入力を受け取る デシリアライズ Event を追記します。 slackbot.rs #[derive(Debug, Deserialize)] pub struct Request { token: String, challenge: Option<String>, + event: Option<Event>, } + #[derive(Debug, Deserialize)] + struct Event { + channel: String, + user: String, + text: String, + thread_ts: Option<String>, + } 2/2 Bot からオウム返しする reqwest クレートで Slack に 投稿します。 Botからの投稿に返答してしまうと無限ループになってしまうため、無視します。 slackbot.rs #[derive(Debug, Deserialize)] struct Event { channel: String, user: String, text: String, thread_ts: Option<String>, } + impl Event { + fn from_bot(&self, bot_user: String) -> bool { + self.user == bot_user + } + } (中略) // Validate let verification_token = env::var("VERIFICATION_TOKEN").expect("VERIFICATION_TOKEN must be set"); if req.token != verification_token { tracing::warn!("AuthenticationFailed, token: {}", req.token); let res = Json(Response { ok: false, challenge: None, }); return (StatusCode::BAD_REQUEST, res); } + // Validate + match &req.event { + Some(event) => { + let bot_user = env::var("BOT_USER").expect("BOT_USER must be set"); + if event.from_bot(bot_user) { + tracing::debug!("From happy bot"); + let res = Json(Response { + ok: false, + challenge: None, + }); + return (StatusCode::BAD_REQUEST, res); + } + } + None => { + let res = Json(Response { + ok: false, + challenge: None, + }); + return (StatusCode::BAD_REQUEST, res); + } + } ここでデバッグ用途に tracing クレートを追加しました。 2. リクエストを challenge のあるなしで分岐させます。 slackbot.rs #[derive(Debug, Deserialize)] pub struct Request { token: String, challenge: Option<String>, event: Option<Event>, } + impl Request { + fn is_initialize(&self) -> bool { + self.challenge.is_some() + } + } (中略) // Validate let verification_token = env::var("VERIFICATION_TOKEN").expect("VERIFICATION_TOKEN must be set"); if req.token != verification_token { tracing::warn!("AuthenticationFailed, token: {}", req.token); let res = Json(Response { ok: false, challenge: None, }); return (StatusCode::BAD_REQUEST, res); } + // Verify from Slack + if req.is_initialize() { + let res = Json(Response { + ok: true, + challenge: req.challenge, + }); + return (StatusCode::OK, res); + } // Validate match &req.event { Some(event) => { let bot_user = env::var("BOT_USER").expect("BOT_USER must be set"); if event.from_bot(bot_user) { tracing::debug!("From happy bot"); let res = Json(Response { ok: false, challenge: None, }); return (StatusCode::BAD_REQUEST, res); } } None => { let res = Json(Response { ok: false, challenge: None, }); return (StatusCode::BAD_REQUEST, res); } } + let res = Json(Response { + ok: true, + challenge: None, + }); + return (StatusCode::OK, res); 3. Slack への POST リクエストを追加します。 slackbot.rs impl Event { fn from_bot(&self, bot_user: String) -> bool { self.user == bot_user } } + #[derive(Debug, Serialize)] + struct PostBody { + text: String, + channel: String, + thread_ts: Option<String>, + } #[derive(Debug, Serialize)] pub struct Response { ok: bool, challenge: Option<String>, } (中略) + // Execute + let event = req.event.unwrap(); + + let post_body = PostBody { + text: event.text, + channel: event.channel, + thread_ts: event.thread_ts.map(|x| x), + }; post_request(post_body).await; let res = Json(Response { ok: true, challenge: None, }); return (StatusCode::OK, res); } + async fn post_request(post_body: PostBody) -> reqwest::Result<()> { + tracing::info!("slackbot__post_request"); + + let url = "https://slack.com/api/chat.postMessage"; + let bot_user_oauth_token = + env::var("BOT_USER_OAUTH_TOKEN").expect("BOT_USER_OAUTH_TOKEN must be set"); + + let client = reqwest::Client::new(); + let response = client + .post(url) + .header( + reqwest::header::AUTHORIZATION, + format!("Bearer {}", bot_user_oauth_token), + ) + .json(&post_body) + .send() + .await?; + + Ok(()) + } できたコードを EC2 にデプロイして、実際に Slack で入力をすると、オウム返しができるようになりました。設定したチャンネルで何か投稿してみてください。 まとめ そのうち以下のようなテーマも学習していきたいと考えています。 ファイル分割 ORM の導入 エラーハンドリング また、今後実運用を見据えての検証として、 test 導入 linter formatter 導入 などを考えております。 面白かったところ 所有権 Option、Some 構造体、型 はまったところ ファイル、ワークスペース分割のルールが一番はまりました。 面白かったところとはまったところは紙一重で、Python とは書き方が違うところかなと思います。 上記以外にも細かいつまずきが多かったですが、とても楽しく学ぶことができました。 Python は書きやすさを重視した言語だと言うことを改めて実感しました。普段あまり考えずに書いていたところを見直す良い機会となりました。 *1 : Rust では Python のパッケージに相当するものをクレートと呼びます。
この記事は、 RevComm Advent Calender 21 日目の記事です。 RevComm の宇佐美です。認証基盤開発チームで開発および Project Manager を担当しています。 OpenID Connect (OIDC) を利用して Cognito user pool と外部の Identity Provider (IdP) の連携を行う方法について調べる機会があったので、まとめてみました。 IdP には Azure AD (Microsoft が提供するクラウドベースの ID およびアクセス管理サービス)を使います。一部 Azure AD 特有の要件がありますが、基本的には OIDC での連携はほぼ同じ流れでできると思います。 この記事について この記事のゴール Cognito user pool と外部 IdP を OIDC で連携して、IdP へのサインインで Cognito user pool の既存のユーザーとして Cognito から各種 token を払い出す方法をまとめます。 前提知識 OAuth 2.0, OIDC の概要 Cognito の基本的な仕様 Azure AD の設定方法 AWS CLI の一般的な知識 注意事項 この記事で記載している ID 連携の手順はあくまで動作するために必要最低限のものであり、本番環境での運用を想定したものではありません。 実務では、CSRF や Open redirect などの OAuth 2.0 / OIDC における既知の脆弱性に対して対策を行う *1 ことが必要で、Authorization code flow における PKCE (Proof Key for Code Exchange) などのベストプラクティスに対応することも推奨されます。 本文 外部 IdP での ID 連携を行うことの利点 これから詳細に説明していくとおり、連携形式を問わず IdP との ID 連携を行うためにはそれなりの手間がかかりますが、連携を行うことによるメリットはどこにあるのでしょうか。 ID 連携を行う大きな利点としては以下のような点が挙げられます。 ユーザー側の利点 ユーザー情報の統一管理 ユーザーを IdP で一元的に管理できるため、複数のサービスごとに ID やパスワード、MFA デバイスなどの認証情報を管理する必要がなくなります。IdP でサインイン状態が継続していればそれぞれのサービスに都度サインインする必要がないので、UX も向上します。 セキュリティ向上 認証情報をサービスごとに管理していると、どうしてもパスワードが使いまわされたり、サービス提供者側のセキュリティインシデントなどによる漏洩が起きたりするリスクがあります。IdP でのサインインに統一しておけば、これらのリスクを軽減することができます。 認証手段 IdP 側で MFA や FIDO などの認証手段を提供している場合、サインインしたい対象のサービスが対応していなくてもこれらを利用することができます。 アプリケーション提供側の利点 認証情報管理の委譲 アプリケーション提供者としても、パスワードなどの認証情報を管理することは極力避けたいものです *2 。データベースに保管する際の不可逆なハッシュ化やログのマスキングなどの基本的な対策に加え、アプリケーション内で認証情報を扱うあらゆるところで特別な配慮が必要になります。IdP に認証情報の管理を委譲してしまえば、これらのうち一部はアプリケーション内で管理する必要がなくなります。 Cognito user poolの概要 Cognito は AWS が提供するウェブおよびモバイルアプリの認証、承認、およびユーザー管理サービスです。 Cognito user pool は、Cognito 内でユーザーを管理するためのディレクトリに相当するもので、ユーザーのサインアップおよびサインイン、IdP 経由でのサインイン、簡易的な Web UI (Hosted UI) などの機能を提供しています。 Cognito user pool にユーザー ID とパスワードでサインインすると、各種 token が払い出されます。 Cognito には Identity pool というものもあって紛らわしいですが、これは本記事で実現しようとする IdP 経由での user pool へのサインインとは異なる機能なので注意が必要です。 OpenID Connect を使った ID 連携の概要 OpenID Connect (OIDC) は、OAuth 2.0 をベースとして構築された認証プロトコルです。つまり OIDC は OAuth 2.0 の拡張として位置づけられますが、OAuth 2.0 が認可 (authorization) のプロトコルであるのに対して、OIDC は認証 (authentication) のためのプロトコルです。 平たく言うと、OAuth 2.0 はあるユーザーがどのリソースにアクセスできるかを制御するための仕様であるのに対し、OIDC ではこれに加えてそのユーザーが誰であるかという認証情報の提供についても規定しています。 そのため、今回のように既存の Web アプリケーションにおけるユーザー管理機能と統合して、ID 連携を行うことができます。具体的には、以下のような流れで認証を行うことになります。 Web アプリケーションから IdP に認証リクエスト ブラウザ上で認証画面にリダイレクト ユーザーが IdP の認証情報を入力 IdP で認証成功 token 払い出し ID token をデコードしてユーザー情報を取得 ユーザー情報を使って Web アプリケーションにサインイン この流れは非常にシンプルに書いたもので、実際にはセキュリティ的な対応など途中でさまざまな処理が入りますが、大枠としてはこのような流れになります。 Azure AD App のセットアップ それでは実際に Azure AD を IdP として、Cognito user pool との連携を行う流れを見ていきます( Microsoft の公式ドキュメント )。 なお、各サービスの画面キャプチャーは 記事作成時点の 2022 年 12 月のものです。 事前に必要なものは以下のリソースです。 Cognito user pool User pool の App Client User pool のユーザー Azure AD のテナント(ディレクトリ) まずは Azure の Portal にサインインして、ホーム画面から以下のように進みます。 Manage Azure Active Directory App registrations + New registration App 登録画面 ここでは App の Name など最低限の情報のみ入力します。 Redirect URI は認証リクエストの redirect_uri として指定できる URI です。一旦 http://localhost だけで問題ありません。 Register を押して App が作成されたら、その App を選択して詳細画面を見てみましょう。 ここで必要となるのは Application (client) ID なので、値をコピーしておきます。 作成した App 画面上部の Endpoints を選択すると、OIDC や SAML での連携に必要となる各種エンドポイントをまとめて確認できます。この中の OpenID Connect metadata document に、OIDC で利用するエンドポイントの情報があります。 次に、Client secret を作成します。 左ペインから Certificates & secrets を選択して、+ New client secret から作成します。 作成された Client secret のうち、value を使用するので忘れずにコピーしておきましょう。一度ページ遷移すると、この後は value が表示されませんので注意です。 次に API 権限の設定を行います。 同じく左ペインから API permissions を選択し、+ Add a permission から権限追加ができます。Microsoft Graph の Delegated permissions から、以下の項目にチェックを入れます。 email openid User.Read これで Azure AD 側での準備はひとまず終わりです。 Cognito user pool のセットアップ ここからは Cognito 側の設定に移ります。 まず、Cognito user pool を作成します( AWS の公式ドキュメント )。設定は一般的なもので問題ありませんが、Cognito user pool sign-in options は Email を指定し、 Required attributes に name を追加しておきます。 User pool ができたら、Azure AD に存在するユーザーの Email でユーザーを作成しておきます。 次に App integrations から App client を作成します。 Hosted UI を有効化し、App client の App type は Public client にしておきます。 Allowed callback URLs には http://localhost を設定します。これは Cognito での認証リクエストが成功した後、Authorization code を受け取るエンドポイントになります。 Authentication flows の USER_PASSWORD_AUTH も有効化して、OpenID Connect scopes には Email OpenID Profile を設定します。 App client ができたら、作成したユーザーで Hosted UI からサインインができることを確認しておきましょう。サインインが成功するとパスワード変更を促されるので、ここで新しいパスワードを設定します。 Hosted UI を起動してサインイン パスワード変更が終わると、Callback URL に指定した localhost にリダイレクトされるはずです。 あわせて AWS CLI からも認証できることを確認します。 aws cognito-idp initiate-auth \ --auth-flow USER_PASSWORD_AUTH \ --auth-parameters USERNAME = < your-user-name > , PASSWORD = < your-password > \ --client-id < app-client-id > client-id に指定するのは、上記で作成した App client の Client ID です。 うまくいけば、レスポンスの AuthenticationResult の中に AccessToken や IdToken 、 RefreshToken が入っているのが確認できるはずです。 これで、Cognito の Email とパスワードでサインインできることを確認できました。 Azure AD と Cognito user poolの連携 ここからは用意した Cognito user pool のユーザーで Azure AD 経由のサインインを試してみます。ゴールは、Cognito の Email とパスワードを使用せずに Azure AD の認証を行うことで Cognito から取得したのと同じ token が得られることです。 まず、Cognito user pool の Sign-in experience タブで Federated identity provider sign-in から Add identity provider を選択します。 Identity provider の追加 Identity provider の選択肢は OpenID Connect (OIDC) を選び、Set up OpenID Connect federation with this user pool の各項目にはそれぞれ以下の値を入れます。 Provider name 名称。Hosted UI のボタンに表示されます。 Client ID Azure AD で作成した App の Client ID Client secret Azure AD で作成した App の Client secret (value) Authorized scopes email profile openid Identifiers 一意の ID(任意) Issuer URL Azure AD で作成した App の OpenID Connect metadata document リンク先 issuer の値 Map attributes between your OpenID Connect provider and your user pool name: name email: preferred_username これで Cognito user poolでの IdP 作成は完了です。 次に、この IdP を App client から利用できるようにするため、App integration タブから作成した App client を選択します。 Hosted UI の Edit を押下し、Identity providers の項目に先ほど作成した Identity Provider を追加します。 作成した IdP にチェックを追加 Save してから再度 Hosted UI を開いてみると、追加した Identity provider のボタンが表示されているはずです。これで Cognito と Azure AD の OIDC 連携はとりあえずできている状態になります。 Sign in with your corporate ID に追加した IdP が表示される ここまで来たら、一旦 Azure AD のポータルに戻って App の設定を追加します。 左ペインから Authentication を選択して、Redirect URIs に以下の URL を追加します。 https://<your-cognito-domain>/oauth2/idpresponse ドメインは Hosted UI のドメインと同一です。 このエンドポイントでは以下のようなことが行われます *3 。 IdP での認証が終わった後に Authorization code を受け取る IdP に token request を送信 受け取った Access token で IdP の Userinfo エンドポイントにリクエスト Userinfo のレスポンスを元に Cognito user pool のユーザーにマッピング あらためて Hosted UI を開き、IdP 経由でサインインしてみましょう。うまくいけば、 http://localhost/?code=<cognito-authorization-code> のようにリダイレクトされて、Cognito の Authorization code が取得できるはずです。 この Code を使って、Cognito に以下の token request を送ると Cognito 発行の token が得られます *4 。 curl --location --request POST ' https://<your-cognito-domain>/oauth2/token ' \ --header ' Content-Type: application/x-www-form-urlencoded ' \ --data-urlencode ' code=<cognito-authorization-code> \ --data-urlencode ' redirect_uri =http://localhost \ --data-urlencode ' client_id=<cognito-client-id> ' \ --data-urlencode ' grant_type=authorization_code ' --data-urlencode ' scope=openid email profile ' 得られた token のうち、 id_token を jwt.io などでデコードしてみましょう。 Cognito の通常の Claim に加えて、 identities という Claim に IdP の情報が入っているのがわかります。 Azure AD でのシングルサインオン これで OIDC での ID 連携は完成、と言いたいところですが、前段で Cognito の Authorization code が適切に取得できている場合、User pool に新しいユーザーが作成されているはずです。 Confirmation status が External provider となっていて、外部 IdP によって作成されたユーザーということがわかります。 同じメールアドレスで新しいユーザーができてしまう この状態だと、既存のユーザーと IdP 経由で作成されたユーザーが同じ Email アドレスなのに別ユーザーと認識されてしまいます。これでは、この記事のゴールだったはずの「既存の Cognito user pool のユーザーとして Azure AD ユーザーでサインインする」という点が実現できていません。 既存ユーザーとして IdP でのサインインを行うためには、AWS CLI で admin-link-provider-for-user という API を使います *5 。 事前に Azure AD から ID token を取得しておく必要がありますので、 ドキュメントに沿って取得しておきましょう 。 ID token を取得できたらこれをデコードします。External provider で作成されたユーザーは一度削除してから、以下を実行します。 aws cognito-idp admin-link-provider-for-user \ --user-pool-id < your-user-pool-id > \ --destination-user ProviderName =Cognito, ProviderAttributeValue = < cognito-user-name > \ --source-user ProviderName = < idp-name > , ProviderAttributeName =Cognito_Subject, ProviderAttributeValue = < user-sub-for-idp > cognito-user-name Cognito での User name idp-name Cognito の Identity provider として登録している IdP の name user-sub-for-idp IdP でのユーザーの識別子(ID token の sub) 紐付けが成功すると、Cognito 内で User pool のユーザーと IdP のユーザーが同一だと認識してくれるので、IdP 経由のサインインで既存ユーザーとしての token を払い出してくれます。 なお、一度紐付けされたユーザーを再度紐付けしようとすると、すでに同一のユーザーとしてみなされているため、 InvalidParameterException が発生します。紐付けを解除するためには、 admin-disable-user を使います *6 。 紐付けができたら再度 Hosted UI を開いて、IdP 経由でサインインしてみましょう。 Redirect 先で取得できる Code を使って、Cognito に token request を送り、token を取得します。 取得した ID token をデコードすると、email や sub などの値が Cognito の既存ユーザーと同じであることが確認できます。 また、紐付け前に IdP サインインした時とは異なり、User pool に新しいユーザーが作成されていないはずです。紐付けを行ったことにより、IdP のサインインでも既存のユーザーとしてのサインインとしてみなされているためです。 あとはこの token をアプリケーション側の認証方式に応じて Authorization header などに使って、User pool のユーザーとして各種リソースにアクセスすることができます。 User pool には先ほどと違って新しいユーザーは作成されておらず、ユーザーの詳細を確認すると User attributes の identities に IdP と連携した情報が記録されているのがわかります。 Cognito 既存ユーザーの User attributes 以上で、Cognito の既存ユーザーとして Azure AD 経由での Cognito サインインができたことになります。 まとめ Cognito も Azure AD もそれぞれドキュメントは充実していて、よく読みながら手順通り進めていけば ID 連携を行うことは可能です。 一方で、OIDC で両者を連携する手順をウォークスルーした資料はあまりなく、最初にやや苦労する点がありました。特に、Cognito の idpresponse エンドポイントでは裏側で何が起きているのかわかりづらく、AWS Support に相談して回答を得られたことが助けとなりました。 本記事が、今後同じような開発を行う方にとって有益であれば幸いです。 おわりに RevComm では OAuth 2.0 や OIDC などの技術を活用した外部連携や認証基盤開発を行っています。興味がある方は、下記から採用情報をチェックしてみてください! www.revcomm.co.jp *1 : https://openid-foundation-japan.github.io/rfc6819.ja.html *2 : https://www.youtube.com/watch?v=8ZtInClXe1Q *3 : https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/cognito-user-pools-oidc-flow.html *4 : https://docs.aws.amazon.com/ja_jp/cognito/latest/developerguide/token-endpoint.html *5 : https://awscli.amazonaws.com/v2/documentation/api/2.1.21/reference/cognito-idp/admin-link-provider-for-user.html *6 : https://awscli.amazonaws.com/v2/documentation/api/latest/reference/cognito-idp/admin-disable-user.html
この記事は、RevComm Advent Calender 20 日目の記事です。 はじめに こんにちは。服部 ( @keigohtr ) です。趣味は MLOps 調査とクラフトビールです。最近はエルゴノミクスキーボードを探し続けています。担当は RevComm Research 配下の開発部のエンジニアリングマネジメントです。本日は「RevComm Research 開発部ってどんな場所なの?」に答えたいと思います。 会社紹介 RevComm は電話営業や顧客応対を可視化する音声解析AI搭載型のクラウドIP電話「MiiTel(ミーテル)」を開発しています。 私の所属する RevComm Research は MiiTel のコアバリューである音声認識や話者分離、感情認識などの機能を開発しています。プロダクトは順調に成長し続けていまして、ユーザー数の増加に比例して解析対象の商談数も増え続けています。お客様が安心してサービスを使えるように、Research の開発部には機械学習サービスの高い安定性と高いスケーラビリティが求められています。 ML推論環境の変遷 RevComm Research では構成の大刷新を進めています。私が入社した当初(ちょうど 1 年前の 2021 年 12 月)の構成はざっくりと下図のようになっていました。EC2 を使ったシンプルな構成ですが、Worker に多くの処理(e.g. ロジック、機械学習処理)を押し込めていたことや、長年保守し続けてきた AMI に色々と限界を感じたことから、マイクロサービス化してコンテナベースの運用に切り替えようとしていた時期でした。 2021 年 12 月時点のおおまかな構成 そして下図が現在の構成です。移行作業としては、まず Worker に押し込めていたいくつかの処理をマイクロサービスとして分離しました。Worker の規模がある程度小さくなったので、現在は Worker をコンテナ化しています。ただし構成としてはまだまだ道半ばといったところです。 2022 年 12 月時点のおおまかな構成 現時点で目指している構成は下図になります。Worker を完全に分解してワークフローエンジンに乗せて管理します。ワークフローエンジンの恩恵をフルに享受するのが狙いです。ML モジュールは用途に応じてサービスとバッチを使い分け、コストとリソースの最適化を図ります。この構成は理想に対するベースであり、ここから更に DevOps や MLOps のベストプラクティスを導入していく予定を立てています。やりたいことが多いのでワクワクしています。 2022 年 12 月時点で目指しているおおまかな構成 DX の変遷 この 1 年間に整備した開発者の環境について紹介します。 モノレポの採用 RevComm Research には多くの ML モジュールがあります。私が入社した当初は Worker の中に全ての ML モジュールが内包されている状態でした。機械学習のライブラリも Tensorflow、PyTorch、sklearn と使っており、ライブラリの依存関係で身動きが取れない状態でした。そこで ML モジュールを Worker から分離してマイクロサービス化するというプロジェクトが進行しました。 Worker から分離したマイクロサービスはモノレポで管理しています。メリットは CICD のセットアップが一箇所で済む。 共有コンポーネント(e.g. Log Formatter, gRPC proto)の参照が簡単にできる。 共有コンポーネントを変更した時、依存する全てのコンポーネントのテストが簡単にできる。 チームの技術的な知見を集約できる。 トラブルシュートのときにここをみれば良いので、認知負荷を下げることができる。 ビルドシステムには pants build を採用しました。有名なビルドシステムには Bazel がありますが、RevComm Research は Python をメインに開発していたこともあり、Python との親和性の高さを評価して pants build に決めました。pants build によってモノレポの中の各プロジェクトやパッケージの依存関係が明らかになり、CI ではコードの変更箇所に関連するテストだけが実行されるようになりました。モノレポではコードの規模が大きくなるにつれてテスト時間やビルド時間が長くなるので、先行投資として最初期からビルドシステムを組み込みました。 その他の工夫としては、 CODEOWNERS の設定があります。CODEOWNERS は GitHub の機能で、特定のディレクトリに対して責任者を設定できます。執筆現在の時点で我々がモノレポで管理しているものは、Python で書かれた複数の ML モジュールと、各 ML モジュールで共有するパッケージ、そして ML モジュールを配信するために必要な Terraform リソースです。それぞれに対して CODEOWNERS を設定し、コードレビューでは CODEOWNERS の承認を必須としました。こうすることでエンジニアの Ownership を育むとともに、CODEOWNERS に任命されたエンジニアがリソースの中で守りたい一貫性を守れるようにしました。 例えば、「DDD を採用しているのに気づいたらドメインが漏れ出していた」という事態は CODEOWNERS の設定で避けやすくなります。 GitOpsの採用 モノレポの採用の部分で CI を整備しました。次は CD です。私が入社した当初は EKS にはたったひとつの ML モジュールのみが配信されており、そのリリースは手作業で Kustomize を実行していました。そしてチームで Kubernetes を触れるエンジニアは一人だけという状況で、つまりリリースができるのも一人だけという状況でした。CD に ArgoCD の検討はされていたものの運用には至っておらず、CD の自動化は急務でした。 CD は ArgoCD を採用しました。ArgoCD はチームで運用には至っていなかったものの、既にメンバーが検討したときの資産がいくつかあったこともあり、再整備をして運用することにしました。コードはモノレポで管理し、マニュフェストは別のレポジトリを用意し、マニュフェストレポジトリを ArgoCD で監視しています。マニュフェストレポジトリにある各 ML モジュールのイメージタグの更新は GitHub Actions で自動化しています。開発環境とステージング環境は ArgoCD の Auto Sync を有効にして、コードの変更を即座に環境に反映するようにしています。本番環境はまだテストが十分に自動化できていないこともあり Auto Sync を有効にできていないですが、将来的には sync windows を設定して営業時間外に自動的に更新できるようにする予定です。 執筆現在の時点で、ArgoCD は Pull 型の GitOps で使っています。Pull 型の GitOps を採用した理由はセキュリティ面でのメリットです。リリースするときに GitHub 側に EKS クラスタへのアクセス権限を持たせなくて済みます。 おわりに 今回紹介したのは機械学習の推論環境です。今回紹介した内容以外にも推論環境でやるべきことはまだまだあります。例えば A/B テスト、Canary release、Feature flag、Traffic shadow、etc。また、機械学習の学習環境もやりたいことがたくさんあります。RevComm Research では Engineering / DevOps / MLOps を積極採用していますので、皆様の応募をお待ちしています。 採用中のポジション hrmos.co