TECH PLAY

株式会社RevComm

株式会社RevComm の技術ブログ

176

はじめに RevComm, Front-end team の熊谷です。今回は vue-facing-decorator を使って Vue2/Nuxt2 のクラスコンポーネントを Vue3/Nuxt3 に移行した話をします。 各コンポーネントでは既存のソースコードを活かせるところも多かったですが、個別に書き換えが必要なところもありましたのでまとめたいと思います。 なぜ vue-facing-decorator を使用したか 弊社の Vue2/Nuxt2 環境では、 nuxt-property-decorator と、 vue-property-decorator を使用したクラスコンポーネントを採用していました。nuxt-property-decorator が Nuxt3 への対応をしないことを決定したため、nuxt-property-decorator が推奨している vue-facing-decorator を使用することにしました。 一気に Vue3 の composition api に書き換えることも検討しましたが、ビックバンリリースになってしまうと通常の機能開発との同時並行作業が難しくなってしまうため、クラスコンポーネントのままで一旦最小限のアップデートを目指すことにしました。 各コンポーネントで書き換えが必要だった所 クラス定義 package のアップデートをして Vue と Nuxt の breaking changes に対応後、 まずは、import の書き換えと mixins の書き換えをしました。 // *** vue2 ************************************************** import { Component , mixins } from 'nuxt-property-decorator' ; @Component ( { components: { SomeComponent , } , } ) export default class SomePage extends mixins ( PageMixin ) { // *** vue3 ************************************************** import { Component , Vue } from 'vue-facing-decorator' ; @Component ( { components: { SomeComponent , } , mixins: [ PageMixin ] , } ) export default class SomePage extends Vue { /pages /pages の下のコンポーネントは head() や layout() といった Nuxt の便利な機能が使えなくなってしまいました( Nuxt 向けではなく Vue 向けのライブラリに移行したため)。 またなぜか /pages の下だけは原因不明のエラーが頻発したため、pages をラップする親を作成したら回避できることがわかりました。 親は /pages とは別ディレクトリに配置し Nuxt の設定を変更して、新しいディレクトリを Nuxt pages のルートとしました。 この方法により、従来の /pages のディレクトリにはギリギリまで vue2 に対する機能追加・変更などを行いつつ、移行作業を安全に進めることができました。 /layouts も同じ問題があったので、同様にしました。 // *** vue3 ************************************************** // nuxt.config.ts export default defineNuxtConfig ( { dir: { pages: 'pagesV3' , layouts: 'layoutsV3' , } , // 以下略 } ); // /pagesV3/user/index.vue < template > < User / > < /template > < script lang = "ts" setup > import { useHead } from 'vue' ; import User from '@/pages/user/' ; useHead (() => ( { title: 'ページタイトル' , } )); < /script > hooks created はいい感じに解釈してくれていましたが destroyed は使えなくなっていました。vue3 のライフサイクルに合わせてhooksは変更したほうが良さそうです。 // *** vue2 ************************************************** private created () { console .log ( 'created' ); } private destroyed () { console .log ( 'destroyed' ); } // *** vue3 ************************************************** private mounted () { console .log ( 'mounted' ); } private unmounted () { console .log ( 'unmounted' ); } @Emit nuxt-property-decorator は return を省略可能でしたが、移行後は returnを書く必要がありました。これは細かい内容ですが全体の作業量は多かったです。(でもこちらの方が正しい印象) // *** vue2 ************************************************** @Emit ( 'change' ) private handleTextAreaChange () {} // *** vue3 ************************************************** @Emit ( 'change' ) private handleTextAreaChange ( e: Event ) { return e ; } Function nuxt-property-decorator ではアロー関数が使えていましたが vue-facing-decorator では動作しませんでした。(これもこちらの方が正しい印象) // *** vue2 ************************************************** private created () { this .initialize (); } private initialize = () => { console .log ( 'initialize' ); } ; // *** vue3 ************************************************** private mounted () { this .initialize (); } private initialize () { console .log ( 'initialize' ); } 今後について 無事に Vue3/Nuxt3 への移行が完了したので、今後は composition api を使って新機能開発をパワーアップさせたいです。 また既存機能も composition api に置き換えて安定した運用をしていきたいです。
はじめに この記事は RevComm Advent Calendar 2023 の 19 日目の記事です。 こんにちは @sara_ohtani_mt2 です。 バックエンド開発をしています。 最近は、いわゆる電話帳のような連絡先を管理する機能のリニューアルに取り組んでいます。 これは現在、処理速度やシステムの拡張性の向上が求められている機能で、その改善を図るためのリニューアルプロジェクトです。 大きなモノリスだったところから機能を切り出して、新しい基盤構築から行っています。 今後他機能にも知見を展開できるよう、様々な選択肢を検討しながら技術の選定を進めています。 改善にあたり、ポイントの1つとなっているのがDB選定・設計です。 弊社サービスの MiiTel はマルチテナント型サービスであり、テナントごとの連絡先データ数が数十万件にも及ぶ規模のものもあります。 クエリレスポンスのスピードの向上を目指す中で、今回AuroraからDynamoDBへの載せ替えを検討しました。 残念ながら結果的には部分一致検索の実用性の点で課題を感じたため、現時点ではDynamoDBの利用を見送ることになりました。 しかし「前方一致で良い」など特定のユースケースにおいては有望だと感じたので、テーブル設計例と合わせてご紹介したいと思います。 DynamoDBが夢に出るようになってきた — sara.ohtani.mt2 (@sara_ohtani_mt2) 2023年8月13日 はじめに DynamoDBがマッチすると思われるユースケース DynamoDBベストプラクティスと設計する上での注意事項 ベストプラクティス 部分一致検索をしたいと思ったときの注意事項 テーブル設計の例 サンプル要件 機能概要 アクセスパターンと要件 その他要件 テーブル定義 テーブルとGSI データの種類の定義と種類ごとの各項目の値の意味 データ取得イメージ 結論 条件ふりかえり その他おまけ 参考 DynamoDBがマッチすると思われるユースケース DynamoDBの基本的なユースケースについては最新の公式ドキュメント *1 を参照してください。 マッチすると思われる条件のうち、今回特に注目する点です。 データ抽出条件が完全一致、あるいは前方一致で良い データ件数が多く、今後もさらに増加が予想される アクセスパターンや要件がはっきりしている データ検索する際の絞り込み条件となる項目が多くない 基本的にテーブルをjoinする必要がない 並び順は問わない DynamoDBベストプラクティスと設計する上での注意事項 ベストプラクティス 公式ベストプラクティス *2 から今回特に意識したことを抜粋します。 出典: https://aws.typepad.com/sajp/2017/02/choosing-the-right-dynamodb-partition-key.html GSIの数を抑える GSIは基本となるテーブルを同期している別テーブルみたいなものなので増やすと書き込みコストがその分増えてしまう RDBならテーブル分割するようなものも全て1つのテーブルで表現する キャパシティを効率的に使うため そもそも1 アカウントあたりのテーブル数は 10,000 が上限 つまりテーブルあたりのデータ量の多さが悩みだからといってどんどん分割してテーブルを増やしていくような設計は合わない 特定のデータ範囲に対してアクセスが集中するようなホットパーテーションが発生しないようにする いかにホットパーテーションを生まないかが設計のポイントの1つ 1処理内のアクセスだけでなく、パラレルでの処理でそれぞれのアクセスがパーテーションに集中してもホットになる オンデマンドキャパシティーモードでも、例えば数千万件を一気に書き込もうとすると Throughput exceeds the current capacity of your table or index というエラーが出て、調べていくとホットパーテーションが問題だとわかったことがあった 1回のクエリで取得できるサイズ上限が1MBであることに注意 1回で取得できなかったときにはLastEvaluatedKeyに値が入ってくる クエリで取得するitemの1itemごとのサイズに大きなばらつきがあると1回のクエリで取得できるデータ件数が変わってわかりづらい 部分一致検索をしたいと思ったときの注意事項 Scan検索は部分一致検索が可能だが、全体のデータ数が多いと遅い keyとattributeはQueryで扱う上で全く別物といっていい Queryでの検索の場合、絞り込み条件としてkeyは必ず指定しなければならない Query検索にもcontainsという部分一致検索できるものはあるがScan同様データが多いと遅い さらにattributeに対してしか使えない、keyに対しては使えない テーブル設計の例 サンプル要件 連絡先リニューアルプロジェクトの要件をもとにしたブログ説明用の架空のサンプル要件です。 DynamoDBの仕様に合うようにアレンジを加えているため、実際の弊社サービスの連絡先機能とは異なります。 機能概要 マルチテナントサービスで、テナント内で共有する顧客の連絡先を管理する電話帳のような機能 アクセスパターンと要件 連絡先情報を一覧で表示する画面があり、キーワード検索する機能がある contact_idで紐づく他データと合わせて表示する 初期表示時はテナント内の全連絡先を表示 連絡先の情報(名前、会社名、電話番号)とカテゴリ名の前方一致検索をしたい 画面に出す100件ずつ取れれば良い データ作成日順で表示したい 余談ですが、アクセスパターンを整理するのが大事ということなので整理の手法には RDRA *3 を使ってみました。 その他要件 1テナントにつき連絡先が最大50万件扱えるようにしたい テナントごとに管理している項目としてカテゴリがあり、連絡先とは多対多の関係 テーブル定義 ▶ 表形式の記載を見たい方はこちら Key Attribute PK SK GSI-1-PK GSI-SK GSI-Attribute ID DataType SearchType SearchValue CreatedAt PhoneNumber CompanyName ContactName ContactSample Contact_{contact_id} Contacts#Tenant_{tenant_id}#Contact_{contact_id} {yyyy-mm-dd hh:mm} {phone_number} {company_name} {contact_name} {contact_sample} ID DataType SearchType SearchValue CreatedAt Contact_{contact_id} TenantId#Contact_{contact_id} Contacts#TenantId#Tenant_{tenant_id} {yyyy-mm-dd hh:mm} ID DataType SearchType SearchValue CreatedAt Contact_{contact_id} CategoryName#Contact_{contact_id}#Category_{category_id} FreeWord#Tenant_{tenant_id} {category_name} {yyyy-mm-dd hh:mm} Contact_{contact_id} PhoneNumber#Contact_{contact_id} FreeWord#Tenant_{tenant_id} {phone_number} {yyyy-mm-dd hh:mm} Contact_{contact_id} CompanyName#Contact_{contact_id} FreeWord#Tenant_{tenant_id} {company_name} {yyyy-mm-dd hh:mm} Contact_{contact_id} ContactName#Contact_{contact_id} FreeWord#Tenant_{tenant_id} {contact_name} {yyyy-mm-dd hh:mm} ID DataType SearchType SearchValue CreatedAt CategoryId Name Contact_{contact_id} CategoryId#Contact_{contact_id}#Category_{category_id} Contacts#CategoryId#Tenant_{tenant_id}#Category_{category_id} Category_{category_id} Category_{category_id} Categories#Category_{category_id} Categories#TenantId#Tenant_{tenant_id} {CategoryName} Attributeがデータの種類ごとに変わるのでそれぞれに見出しを書いているが全て1テーブル内に格納するデータです。 {}には該当する値がセットされる想定です。 (例: Contact_{contact_id} → Contact_064d0d85-9d74-7f2f-8000-644443c7c8dc) テーブルとGSI 1. 基本テーブル データを重複なく保管し、ホットパーテーションが発生しないようにするためのPK, SKを設定します。 2. GSI GSIは検索のためのものを1つだけ作成しています。 検索のためのPK, SKをGSI用に設定しています。 GSIでは全カラムをもつ必要がないので検索とソートに必要な項目だけ基本テーブルから射影します。 データの種類の定義と種類ごとの各項目の値の意味 データの種類の名称はブログ説明するためのもので公式な呼び方ではありません。 1. 基本データ IDを絞り込んだ結果、取って来たいデータです。 DataType: なんのデータ#どのテナントと紐づいてるか#どの連絡先と紐づいてるか DataTypeはいずれも基本的にデータ重複ないように保持するためのものとしている ここのconact_idはアプリケーション上必要な情報ではないが、ホットパーテーション回避するために入れている tenant_idはあってもなくてもいいのですが、ConactsのItemの情報としてどのテナントのデータかあった方があとで調査とかしやすそうなので置いてみている 各idに Contact_〜 などの見出しをつけるのは、見やすさ向上のためと、全てのデータが1つのテーブルに入っているため他項目間のidの重複を避けるため call_sampleのようにcontact_idと紐付けられるデータは、できれば1itemに入れてしまったほうがクエリが楽になる 2. 検索用データ DataType: なんのデータ#どの連絡先と紐づいてるか カテゴリの場合は複数紐づくのでさらにcategory_idも SearchType: 検索の種類#テナントid SearchValue: "なんのデータ"の値 CreatedAt(アプリケーションとしてのソートしたい項目): id取得のための検索itemと基本情報itemにだけセットすればいい 3. 多対多の紐づけ用データ DataType: なんのデータ#どの連絡先と紐づいてるか#紐づくデータ SearchType: 検索の種類#どのデータと紐付けるか(ON的な)#紐付ける値 多対多のデータの考え方については最後に記載した参考ブログがわかりやすくておすすめです。 データ取得イメージ 連絡先一覧画面でキーワード検索したときのデータ取得の流れのイメージです。 GSI経由で検索用データから条件に一致するcontact_id一覧を取得する 取得したcontact_id一覧から基本データを取得する 動作確認環境: AWS Lambda ランタイム Python 3.11 import boto3 def main ( dynamodb_client, table_name, index_name, partition_key, sort_key, gsi_partition_key, gsi_sort_key, tenant_id, app_sort_key, keyword ): items = [] exclusiveStartKey = "" # クエリパラメータの設定 params = { "TableName" : table_name, "IndexName" : index_name, "KeyConditionExpression" : f "#pk = :pkval and begins_with(#sk, :skval)" , "ExpressionAttributeNames" : { "#pk" : gsi_partition_key, "#sk" : gsi_sort_key, }, "ExpressionAttributeValues" : { ":pkval" : { "S" : "FreeWord#" +tenant_id}, ":skval" : { "S" : keyword}, } } # できるだけ少ない回数で取得できるようにGSIの項目は少なくしたい while True : if exclusiveStartKey: params[ "ExclusiveStartKey" ] = exclusiveStartKey print ( "exclusiveStartKey: " , exclusiveStartKey) response = dynamodb_client.query(**params) items.extend(response[ "Items" ]) if "LastEvaluatedKey" not in response: break exclusiveStartKey = response[ "LastEvaluatedKey" ] # 重複を取り除くために一時的なセットを使用 temp_set = set () filtered_items = [item for item in items if item[partition_key[ "name" ]][partition_key[ "type" ]] not in temp_set and not temp_set.add(item[partition_key[ "name" ]][partition_key[ "type" ]])] # 先頭100件を取得するために並び替え sorted_items = sorted (filtered_items, key= lambda x: x[app_sort_key[ "name" ]][app_sort_key[ "type" ]], reverse= True ) contact_list = [item[partition_key[ "name" ]][partition_key[ "type" ]] for item in sorted_items] if not contact_list: return chunk_size = 100 items_par_page = 100 page = 1 first_element = (page - 1 ) * items_par_page # ページネーション chunk = contact_list[first_element:first_element+chunk_size] # SQLのinのような絞り込み条件を設定したい場合は batch_get_item() を使う contacts = batch_get_item(dynamodb_client, table_name, {table_name: { "Keys" : [{partition_key[ "name" ]: {partition_key[ "type" ]: contact_id}, sort_key[ "name" ]: {sort_key[ "type" ]: "Contacts#" +tenant_id+ "#" +contact_id}} for contact_id in chunk]}}) # アプリケーションの表示順にするために並び替え contacts = sorted (contacts, key= lambda x: x[app_sort_key[ "name" ]][app_sort_key[ "type" ]], reverse= True ) return { "contacts" : contacts } def batch_get_item (dynamodb_client, table_name, request_items): max_retry = 100 results = [] for _ in range (max_retry): batch_get_item_response = dynamodb_client.batch_get_item(RequestItems=request_items) results.extend(batch_get_item_response[ "Responses" ].get(table_name, [])) unprocessed_key = batch_get_item_response[ "UnprocessedKeys" ] if unprocessed_key: request_items = unprocessed_key else : break return results def lambda_handler (event, context): dynamodb_client = boto3.client( "dynamodb" ) return main( dynamodb_client, table_name= "sample-table" , index_name= "SearchType-SearchValue-index" , partition_key={ "name" : "ID" , "type" : "S" }, sort_key={ "name" : "DataType" , "type" : "S" }, gsi_partition_key= "SearchType" , gsi_sort_key= "SearchValue" , tenant_id= "Tenant_uuid1" , app_sort_key={ "name" : "CreatedAt" , "type" : "S" }, keyword= "0" ) 結論 条件ふりかえり データ抽出条件が完全一致、あるいは前方一致で良い → 実際のプロジェクトの技術選定で今回DynamoDBを採用しなかった一番の理由が、部分一致だと大量データの検索がスピーディにできないことでした。 完全一致、あるいは前方一致で良いものにはぜひ採用を検討したいです。 データ件数が多く、今後もさらに増加が予想される → データ数が多くてもユースケースがマッチしていて設計がうまくいけばかなり速いです。 アクセスパターンや要件がはっきりしている → ホットパーテーションが生まれないようになど事前の設計がかなり大事です。 アクセスパターンが決まっていない状態でこのような検索の仕組みにするのはおすすめできません。 データ検索する際の絞り込み条件となる項目が多くない → 今回のような設計にすると項目が多ければ多いほど1件の連絡先あたりのitem数が増えることになり、書き込み・読み込み時のコストも増えていくことになります。 基本的にテーブルをjoinする必要がない → これもjoinするテーブルが多いほど1件の連絡先あたりのitem数が増えることになります。 全体のクエリも複雑になるのでできればない方がいいです。 並び順は問わない → SQLでいうところのorder byがないのでソートキー順以外の並び順にしたいときは一度全検索結果のidと並び順条件の情報を取得した上でアプリ側でソートしていました。 そうすると本当は100件のデータ取得でいいところが全結果を取得しないといけなくなるので並び順は選べないと思っていたほうが効率が良さそうです。 その他おまけ むちゃしてQueryの前方一致検索を使いながら任意のキーワードのHit対象を増やそうとするとなんか最終的に自分で全文検索のための数文字おきに区切ったデータ作るみたいなことになっちゃう 例えば会社名で検索したときに「株式会社」などは入力しないでもHitするようにSearchValueから取り除いたデータも作るなど やたら分割するとデータ量が爆発的に増えていくのでコストもかかる RDBでは考えないようなことを色々やることになる 基本となるデータに変更があった場合にStreamなどで関連レコードを更新する必要がある emailなど今後連絡先で管理する新しい項目が増えるたびにStreamで更新するレコードもどんどん増えていくことになってコストがあがっていく SQLでいうところのdistinctがないのでアプリ側で重複を省いてる この設計だと複数の検索条件にヒットするとid取得の段階でidの重複が発生することになる ライブラリを使ったらもっとすっきり書けたり便利なのかもしれないが今回はそこまで調査していない 部分一致のクエリの書き方を検索していると今は非推奨の古い書き方例がかなりよく出てくるので注意が必要 SDKのresourceは古いため非推奨となっており、代わりにclientを使うことが推奨されている *4 参考 特に参考にした記事 https://speakerdeck.com/_kensh/dynamodb-design-practice https://speakerdeck.com/handslabinc/dynamodbdemojian-suo-sitai DynamoDBのテーブル設計における多対多の考え方 https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/bp-adjacency-graphs.html https://hack-le.com/dynamodb-many-to-many/ クエリのパフォーマンスと継続した負荷に対してDynamoDBはどのように対応するかについての検証記事 https://aws.amazon.com/jp/blogs/news/part-2-scaling-dynamodb-how-partitions-hot-keys-and-split-for-heat-impact-performance/ 今回は触れなかったけどlimitを使うときのハマりそうな注意点 https://www.denzow.me/entry/2018/02/04/130419 *1 : https://aws.amazon.com/jp/dynamodb/resources *2 : https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/best-practices.html *3 : 要件定義手法 https://www.rdra.jp/ *4 : https://boto3.amazonaws.com/v1/documentation/api/latest/guide/resources.html
こんにちは! RevCommのフロントエンドエンジニアの楽桑です。 フロントエンドパフォーマンスチューニングを経験した方ならご存じのとおり、レンダリング効率は常に重要です。データをスピーディかつ効率的に画面に表示することは、フロントエンド最適化の核心です。 本記事では、すでにリリースされているプロジェクトにおいて、コードの変更を最小限に抑えつつ、効果的なテーブルパフォーマンスチューニングをどのように実施するかをご紹介します。 背景 僕が担当しているプロジェクトでは、システム内に配置された2つのインタラクティブなテーブルがあります。 これらのテーブルは、ユーザーが操作するディバイダーによって高さが調整される設計になっています このような設計は、ユーザーによりよいコントロールを提供する一方で、データ量が増加するとパフォーマンスに影響を与える可能性があります。特に、ディバイダーの動きがスムーズでなくなると、全体のユーザー体験が損なわれます。 この問題を解決するために、どのようにフロントエンドのパフォーマンスを最適化し、ユーザーインターフェースの応答性を保つかを探求します。 技術選択 テーブルパフォーマンスチューニングにおいて、テーブルのバーチャル化は一般的に用いられる手法です。 このアプローチでは、従来のテーブル描画方法とは異なり、ユーザーのビューポート(画面に表示されている範囲)に現れる行のみを描画することに焦点を置いています。 この手法を用いることで、一度に描画される行の数を減らすことができます。これは、スクロール時の描画コストを低減する効果も持ち合わせています。 dev.to ただし、今回のアプローチでは基本的な実装のみを採用し、動的な行の描画の最適化は次段階の課題として残します。 ライブラリ ライブラリとして候補に上がったのは React-Virtualized 、 React-Window 、 React-Virtual 、 React-Table 4つのライブラリです。 今回は、すでに実装されQAも完了しているテーブルコンポーネントにバーチャルスクロールを追加することが目標です。できるだけ軽量な実装を望んでいるため、 React Table や React-virtualized といったライブラリを使用する方法も考慮しましたが、いずれにせよ既存のコンポーネントをカスタマイズする必要があるため、一旦見送ることにしました。 その結果、 React-Window と React-Virtual の2つの選択肢が残ります。今回は React-Virtual を選択しました。既存のコンポーネントをそのまま使用し、ライブラリが提供するhooksを用いて実装できるためです。これにより、実装コストをかなり抑えることができました。 ただし、このアプローチのデメリットとして、テーブル内でバーチャル化を行うためにテーブルヘッダーを適切に表示する必要があり、表示行の前後の余白高さを計算する必要が生じます。結果としてJavaScriptの計算コストが増加します。 実装 React-Virtual が提供したHook // The virtualizer const rowVirtualizer = useVirtualizer ( { count: 10000 , getScrollElement: () => parentRef.current , estimateSize: () => 35 , } ) count : テーブル全体のサイズです。 getScrollElement : スクロール対象のElementを指定します。 estimateSize : 各テーブル行の予想高さを設定します。 これらの3つのパラメータに加えて、よく使用されるのは以下の2つです: overscan : ビューポートの前後に予め描画する行数を指定します。 horizontal : trueに設定すると、水平方向に対してのスクロールが有効になります。 useVirtualizedTable を用いて実装したHook export const useVirtualizedTable = ( { tableSize , scrollable , } : VirtualizedTableProps ) => { // The virtualizer const rowVirtualizer = useVirtualizer ( { count: tableSize , getScrollElement: () => scrollable.current , estimateSize: () => TABLE_ROW_HEIGHT , overscan: 5 , } ); const items = rowVirtualizer.getVirtualItems (); // Calculate the space before and after the virtual items const [ before , after ] = items.length > 0 ? [ notUndefined ( items [ 0 ] ) .start - rowVirtualizer.options.scrollMargin , rowVirtualizer.getTotalSize () - notUndefined ( items [ items.length - 1 ] ) .end , ] : [ 0 , 0 ] ; const totalSize = rowVirtualizer.getTotalSize () + TABLE_HEADER_HEIGHT ; return { items , totalSize , before , after } ; } ; Table 前後の余白高さ計算 const before = notUndefined ( items [ 0 ] ) .start - rowVirtualizer.options.scrollMargin , const after = rowVirtualizer.getTotalSize () - notUndefined ( items [ items.length - 1 ] ) .end , beforeは表示中のアイテムの最初の要素の上部からスタート位置までの高さからスクロールマージンを差し引いた値です。 afterはバーチャルスクロール全体の高さから、表示中の最後のアイテムの下部のエンド位置までの高さを差し引いた値です。 このアプローチでは、現在ビューポートに表示されている行の実データのみを描画し、その他のスクロール可能な範囲は空白のtrタグ 要素で埋められています。これにより、現在のビューに対する描画負荷を最小限に抑えつつ、ユーザーにスムーズなスクロール体験を提供することが可能になります。(画像のように、最初と最後のtrタグは高さのみのダミータグになります) それを適用した実際のコード // Hook展開 const { items , totalSize , before , after } = useVirtualizedTable ( { tableCount: body.length , scrollable: parentRef , } ); // 高さをテーブル全体に適用 < table css = { css ` table-layout: fixed; height: ${ totalSize } px; width: 100%; border-collapse: separate; border-spacing: 0; ` } > .... < /table >; // itemの展開適用、beforeとafterを表示中のRow前後の行に高さ適用 { before > 0 && ( < tr css = { css ` height: ${ before } px; ` } / > ); } { items.map (( virtualItem ) => ( < TableDataRow key = { virtualItem.index } whiteSpace = "nowrap" data = { body [ virtualItem.index ] .data } // Indexを使ってDataをマッピング / > )); } { after > 0 && ( < tr css = { css ` height: ${ after } px; ` } / > ); } テーブルのバーチャルスクロール時のヘッダー幅の動的変更 テーブルでバーチャルスクロールを使用している場合、もしヘッダーが固定されていなければ、表示中の行の中で最も幅が大きいものに合わせてヘッダーがレンダリングされます。つまり、スクロール中に最も幅が広い行がマウントまたはアンマウントされるたびに、テーブルの全体の幅が変動してしまうことになります。 対策: この問題を解決するために、ヘッダーの幅を固定し、テーブル全体の幅が変動しないように設定することが一つの対策となります。 ただし、ヘッダを固定することにより、新しい項目を追加するたびに、幅の値を追加する必要があります。幅計算の関数を作成することもおすすめです。 パフォーマンス計測 パフォーマンスレポート 対応前 対応後 画像1・2はパフォーマンスチューニング前後のテーブルの高さ変更時の実行時間を表しています。 画像1のようにレンダリング時間は実行時間の多くを占めており、およそ16000msになってます。そして、スクリプティング時間は5000msになっています。この2つで、高さ移動時の実行時間のおよそ71%を占めています。 一方、画像2ではレンダリング時間が7000msと半分程度になっています。そのかわり、スクリプティング時間は9000msくらいになりましたが、全体に占める実行時間は71%から52%へと19ポイント (26%) 縮小したことがわかります。 終わりに 本記事では、リリース済みのプロジェクトにおけるテーブルのパフォーマンスチューニングに取り組みました。主な目標は、変更量を最小限に抑えつつ、効率的なコードを実現することでした。結果として、全体のコード実行時間を約26%削減することに成功しました。 今後の課題としては、スクリプティングの計算量を可能な限り抑えることを目指しています。一つの案として、非表示のデータ行の動的描画コスト最適化を検討していく予定です。
この記事は RevComm Advent Calendar 2023  18 日目の記事です。 はじめに フロントエンドでの正規化のメリット GraphQL クライアントでの正規化 RESTful API での正規化 おわりに 参考 はじめに 2023 年 12 月現在、フロントエンド GraphQL クライアントの多くはデータを正規化してキャッシュをする機能を持っています。参考に GraphQL 利用成熟度モデル では GraphQL のクエリ結果を正規化して活用することは 6 番目 に取り上げられていました。 キャッシュと聞くと必ずしも必要な要素ではないように思われるかもしれませんが、もしあなたが API レスポンスを保存して状態管理しているならそれと同じことです。 フロントエンドでの 正規化のメリット 正規化とは重複データをなくすことです。正規化された構造でキャッシュするということは、キャッシュ内のすべてのデータが一意であることを意味します。フロントエンド、特に宣言的 UI 下においては、UI に表示するデータソースが宣言されているとき常に最新のクエリ結果が UI に表示されるということを意味します。 正規化されていない場合を考えます。例えば Todo 一覧のクエリ結果に含まれる Todo:1 と Todo:1 単体のクエリ結果が重複して保存されるようになっていると、クエリ発行の間に Todo:1 が更新されている場合、一覧と単体のページで内容が異なるということが起きます。正規化して上書き保存できていれば内容は同じになります。 GraphQL クライアントでの正規化 GraphQL のクエリ結果は、名前のとおりグラフとして構成されているのでノード単位で正規化することができます。GraphQL はスキーマをもとに実装されているので、ライブラリがスキーマから判断して自動で正規化を行うことができます。(Apollo Client の場合はノードの境界を判別するのに __typename 、識別子に id が使用されます。 id が存在しない場合はどの値を識別子として扱うかを定義する必要があります。) Apollo の公式ブログよりクエリ取得から正規化されるまでの図を引用します。 クエリを発行する レスポンスを受け取る 正規化する 正規化されたデータを保存する こうして正規化して保存することで、後から Todo 単体を取得した際に自動的に対象のキャッシュを特定して更新することができるのです。さらに、サーバーへ更新処理を行う際も結果として更新のあったノードを返せば GraphQL Client は自動的に対象のキャッシュを特定して更新することもできます。 また、例として簡単な「Todo 一覧」のクエリ結果を取り上げましたが、一般的な GraphQL の使い方として「ユーザーに紐づく Todo 一覧」のようなクエリの結果であってもスキーマをもとに正規化が可能です。 // 正規化前 { id : " user1 ", __typename: " User ", name : " User 001 ", todos : [ { id : 1 , __typename: " Todo ", text : " First todo ", completed : false } ] } // 正規化後 // User:user1 { name : " User 001 ", todos : [ " Todo:1 " ] } // 正規化後 // Todo:1 { text : " First todo ", completed : false } RESTful API での正規化 ここまでの正規化の話は __typename と id により非正規化されたデータ(JSON)から正規化されたデータに分解して保存できるというだけの話なので、特に GraphQL である必要はありません。実際に、正規化してキャッシュを保存するという話は GraphQL に限ったものではなく、Redux のドキュメントにも 推奨事項として書かれています。 しかし、RESTful API で返す JSON についてモデル毎に __typename をつけること、一意に識別可能な id をつけること、と約束を進めるうちに GraphQL のスキーマとクライアントが担っていた機能を再発明することになります。 したがって、最初から GraphQL の仕様に沿って開発を進めていくことが効率的だと考えます。 おわりに スキーマをもとに実装されるかつスキーマをもとに正規化可能であることはフロントエンジニアにとって GraphQL を使用する大きなメリットになると考えています。最近 GraphQL が React Server Components や BFF と比較されるケース を見ましたが、この特徴は他の技術にはない要素です。 効率よく便利にキャッシュを持てるという点で GraphQL は有用な技術であり続けると考えています。 参考 Apollo Client https://www.apollographql.com/blog/demystifying-cache-normalization URQL https://formidable.com/open-source/urql/docs/graphcache/normalized-caching/ Relay https://relay.dev/docs/principles-and-architecture/thinking-in-graphql/#caching-a-graph
はじめに こんにちは。 RevCommでCorporate EngineeringチームおよびFull Stackチームで活動している川添です。 社内の情報管理、うまくできているでしょうか?ルールやナレッジを共有しあっているけれども、過去に話した内容を何度も確認しあっている、過去の情報をうまく検索できない、などの問題が起きてないでしょうか? どの会社でもこのような問題は起きているかと思いますが、RevCommでもやはり起きています。 今回は、そのような問題に対する一つのソリューションとして、 RAG (Retrieval-Augmented Generation) を用いたナレッジチャットボットを作ってみましたので、紹介させていただきます! RAG (Retrieval-Augmented Generation) とは? ざっくり言うと、文書のデータセットに対してキーワードや文章をもとにベクトル検索を行い、抽出された文書をもとに生成AIで回答を行うものです。 回答の元となるデータを生成AIに与えることで、間違った情報や関係ない情報の出力を減らすことができます。 システムの構成 今回は、Google CloudのVertex AI Search and Conversationを用いました。 RAGシステム構成 ポイントを解説していきます。 BigQuery 各データソースの情報を集約する場所です。Google Vertex AI Search and Conversationに連携するためにBigQueryである必要はありましたが、BigQuery内でのデータの保存方法は、一般的なテーブルの形式であればどのような形でも大丈夫そうでした。 今回は、MiiTelのサポート記事であるZendeskと、社内のナレッジベースであるNotionの一部データを一つのデータセット・テーブルに保管して接続することにしました。 Google Vertex AI Search and Conversation 今回のRAGを実装するに当たって、情報検索を司る部分です。このサービスは、Google BigQueryなどの情報を読み込ませて、キーワードによる検索などを実現することが出来ます。つまり、 自社の情報に対してググる ことができるというわけです。 実はSearch and Conversationという名前の通り、文章で検索を行って文章で回答を生成してくれる機能もあります。ただ、この機能はBigQueryの正規化されたデータには現時点(2023年12月14日時点)では提供されておりません。 PDFなどや画像などの非正規化データをGCSに保管して、その情報を接続した上であれば、文章検索・回答が実現できます。 こちらはサポートにも確認しましたが、残念ながらまだとのことでした。 😭 ただ、ロードマップにはあるらしいので、今後に期待ですね! PDFで情報を保存して上記の機能を使うこともできましたが、正規化した形で情報を持っておきたかったのもあり、今回はBigQueryに情報を保存して、文章による検索・文章による要約生成・回答を別の形で実現することにしました。 詳細は、後述していきます。 Slack Bolt による Bot 作成 他の記事でも多く解説されているので詳細は省きますが、今回はSlack Botでメンションを受けると、それに対して反応するような形で作りました。 その中で、Google Vertex AI Search and Conversationを使ってRAGを実現するための工夫をいくつか行っています。 それらを、Bot内での処理にそって説明していきます。 問い合わせの文章から、検索に用いる検索キーワードを抽出する 問い合わせの文章は一般的に、「MiiTelとSalesforceの連携をする方法を教えてください。」のようになりますが、このままではBigQueryに保管されているデータを抽出することができません。文章での検索は、うまく検索できる場合もあった一方で、「教えてください」などのワードが入った途端に検索が出てこないケースがほとんどでした。 ですので、ChatGPTを使って、文章から検索に用いるワードを抽出するようにしています。 「MiiTelとSalesforce の連携をする方法を教えてください。」の場合、「MiiTel Salesforce 連携 方法」のように、スペースか何かで区切ってあり、語尾が省かれているような形が理想のようでした。 そのようなフレーズを抽出するために、色々試してみた結果、「次の文から検索に用いるための主要なキーワードやフレーズのみをスペース区切りで抜き出してください。」というプロンプトが一番効果的に感じたので、こちらを用いることにしました。 具体的には、以下のような実装です。 from openai import OpenAI openai_client = OpenAI(api_key=os.getenv( "OPENAI_API_KEY" )) # 質問から主要キーワードのみを抽出 message = ( f """次の文から検索に用いるための主要なキーワードやフレーズのみをスペース区切りで抜き出してください。: {question_user}""" ) messages = [ { "role" : "user" , "content" : message, } ] response = openai_client.chat.completions.create( model= "gpt-4" , messages=messages ) message_response = response.choices[ 0 ].message.content print (message_response) これで、検索のためのワードの抽出ができました。 ナレッジ検索をする 検索のためのワードの抽出ができたので、それらを用いてGoogle Vertex AI Search and Conversationで検索を行います。 基本的には素直に検索を行えばいいのですが、キーワードの数が多い場合にはうまく抽出できないことがありました。検索結果が0件になってしまうような状態です。 素直に検索をやり直してもらう方法などもあるとは思いますが、今回は、検索結果が0件の場合はワードを減らしつつ検索をする方法と採用し、より広範囲で検索できるようにしてみました。あくまで社内の便利ツールなので、何かしら拾えるほうを優先しました。 実装は下記のような形です。 実装例が公式ドキュメント以外にあまり見つけられませんでしたので、もしかしたらもっとよい実装方法があるかもしれません。 credentials = service_account.Credentials.from_service_account_info( google_credential ) discoveryengine_client = discoveryengine.SearchServiceClient( credentials=credentials ) total_size = 0 while total_size == 0 : # Discovery Engineで検索-------------------------------------------------- # Initialize request argument(s) request = discoveryengine.SearchRequest( serving_config= "projects/xxxxxxxx/locations/global/collections/default_collection/dataStores/revcomm-knowledge-base_xxxxxxx/servingConfigs/default_search" , query=message_response, ) # Make the request page_result = discoveryengine_client.search(request=request) print (page_result) total_size = page_result.total_size # 空白区切りで抽出されたキーワードの後ろを削除 message_response = re.split( r"\s+" , message_response) message_response = message_response[:- 1 ] # 空白区切りに戻す message_response = " " .join(message_response) if message_response == "" : response = slack_utils.post_message( "質問から適切な文書を検索できませんでした。他の質問を入力してください。" , thread_ts=thread_ts, ) return None 文章の中身の抽出 次に、各文章のタイトルや内容、URLなどを抽出して後で使えるようにしていきます。 上記の検索で取得したデータは独自のデータ構造で保管されているので、それを適宜辞書形式などに変換しながら抽出したりしています。 また、サービスのFAQページなどの情報はHTMLのまま保管してあるので、 HTMLのタグは除外するようにもしています。 # 文書のフォーマット count = 0 ## Handle the response support_content_list = [] for response in page_result: title = response.document.struct_data.__dict__[ "_pb" ][ "title" ].string_value content = response.document.struct_data.__dict__[ "_pb" ][ "body" ].string_value # contentから全てのhtmlタグを削除 content = re.sub( r"<[^>]*?>" , "" , content) # contentから全ての改行を削除 content = re.sub( r"\n" , "" , content) url = response.document.struct_data.__dict__[ "_pb" ][ "url" ].string_value support_content_list.append([title, content, url]) count += 1 if count > 5 : break print (support_content_list) ChatGPTに読み込ませるための準備 抽出したデータから、ChatGPTに読み込ませるためのデータを生成します。 また、参考にした情報のデータソースにアクセスできるようにURLのリストも作成しておきます。 # 中身の抽出 read_responses = [] support_content_urls_message = "" for support_content in support_content_list: message += f """--------- ## タイトル {support_content[0]} ## 内容 {support_content[1]} ## URL {support_content[2]} """ support_content_urls_message += ( f "・[{support_content[0]}]({support_content[2]}) \n " ) print (read_responses) 回答の生成 最後に、これらの情報を読み込ませつつ、元の質問に対する回答を生成します。 特段の工夫は行っていませんが、仮に読み込ませる文章が多くなった場合はトークン数が制限を超えてしまう可能性がありますので、工夫の余地があるポイントかもしれません。 もしやるとすれば、4.の段階で各文章の要約を作ってしまうのも一つの手だと思います。 # Generate Summary Response message = f """次の文章を元に、「{question_user}」 という質問に対する回答を作成してください。 """ for read_response in read_responses: message += f """--------- {read_response} """ messages = [ { "role" : "user" , "content" : message, } ] response = openai_client.chat.completions.create( model= "gpt-4" , messages=messages ) message_response = response.choices[ 0 ].message.content print (message_response) output_message_for_slack = ( message_response + " \n\n 参考: \n " + support_content_urls_message ) 詳細の実装は省いている部分もありますが、以上が今回実装した内容となります! 作ってみたシステムへの評価 ある程度適切に情報抽出・回答ができるようになりましたが、実際はまだまだ改善余地がありそうです。 大きな問題の一つとして、サービスのFAQページの数に対して社内情報のNotionのデータ数が相対的に少なく、サービスのFAQページの内容がメインで検索されてしまう傾向などが出てきました。 原因として検索語句や情報の質の問題が考えられますが、例えばサービスのFAQページのデータとNotionのデータを別々にBigQueryに保存し、別々に検索するようにするなどの工夫をするる余地はあるかなと思っています。 おわりに いかがでしたか? 今回紹介したもの以外にもさまざまなAIサービスが出ているので、すでに他のサービスを使って似たようなことを実現しているケースも多くあるかと思います。しかし、自分で実装してみることは面白いですし、自力で改善もできるので、試してみる価値はあると思います。 何かの参考にしてもらえたら幸いです! 最後までお読みいただき、ありがとうございました。
こんにちは、 RevComm Research Dept. Development Groupの id:tmotegi です。趣味は積読と日本酒を嗜んでおります。昨日は 仙禽の雪だるま を飲みました。現世で2度目のアドベントカレンダーなので緊張します。 この記事は RevComm Advent Calendar 2023  、15日目の記事です。昨日の記事は豊崎さんによる「 CodemagicでFlutterアプリをビルドする 」でした。 私達のチームは、 チームトポロジー のイネイブリングチームに相当するチームとして組織されており、他のチームに対してサポート・ツール・サービスを提供し、効果的かつ効率的に業務を遂行できるようにする役割を担っています。 今回は私が取り組んだ、 AWS EC2 Inf2インスタンスを使った推論の高速化 をご紹介します。 TL;DR 背景 音声感情認識 音声感情認識機能 音声感情認識モデル 実験 環境設定 データセット 指標 実験結果 推論のパフォーマンス比較 モデルサイズ・ロード時間比較 まとめ TL;DR AWS EC2 Inf2インスタンスにより、音声感情認識モデルの推論は爆速化し、コストも削減され、精度は維持されることが分かった。 G5インスタンス (ONNX) に比べ、Inf2インスタンスは2倍以上高速に推論することができる。 G5インスタンス (ONNX) に比べ、Inf2インスタンスはコストを80%弱削減できる。 背景 私達はMiiTelのコアバリューである音声認識・話者分離・音声感情認識などの機能を実現するために最新の深層学習モデルを利用しています。このような深層学習モデルは推論時に大量の演算を必要とするため、その性能向上のために一般的にGPUが利用されます。しかし、GPUインスタンスはCPUインスタンスに比べてコストが高くなる傾向があります。一方、AWSは Inf2インスタンス という推論に特化したインスタンスを提供しています。Inf2インスタンスは AWS Neuron SDK を用いて作成した専用のモデルをデプロイすることで、パフォーマンスの向上とコスト削減を同時に実現することが可能となっています。 私はGPUインスタンスのコストがかさむ問題に対して、Inf2インスタンスを導入することで解決を試みました。具体的には、MiiTelで使われている音声感情認識モデルをONNXやGPUインスタンス、Inf1およびInf2インスタンスを使った推論を行い、結果を比較することでInf2インスタンスが導入可能かどうか検討しました。 音声感情認識 私が取り上げた音声感情認識について簡単に紹介します。 音声感情認識機能 音声感情認識機能は、話し手のポジティブ・ネガティブな感情を可視化します。話し方からポジティブ・ネガティブを認識し、オレンジからブルーのグラデーションで帯として表示します。これによって会話した当事者以外でも、ネガティブな内容の会話にいち早く気づくことができます。(下記の図の赤いドットの角丸矩形が感情を示すグラデーション) emotion gradation bar 詳細は以下のプレスリリース記事で紹介されています。 音声解析AI電話「MiiTel」、音声感情認識機能をリリース 会話のポジティブ、ネガティブな感情をAIが可視化 prtimes.jp 音声感情認識モデル 紹介した音声感情認識機能で実際に使われているモデルは、論文 "Speech Emotion Recognition based on Attention Weight Correction Using Word-level Confidence Measure" 1 で提案された "confidence measure (CM) as weighting correction" になります。 詳細は以下のブログで紹介されています。 https://tech.revcomm.co.jp/2022/07/13/voice-emotion-recognition/ tech.revcomm.co.jp 実験 この実験の目的は、AWS EC2 Inf2インスタンスを使用して、音声感情認識モデルの推論速度を改善することです。これにより、同じ品質のアウトプットを保ちつつコストと時間を節約し、効率性を向上させることを期待しました。 環境設定 c6in.2xlarge, g5.xlarge, inf1.xlargeは東京リージョンで起動し、inf2.xlargeのみまだ東京リージョンでは提供されていないため、バージニア北部リージョンで起動しました。 Instance type GPU AWS Inferentia Software c6in.2xlarge No No torch, onnxruntime g5.xlarge Yes No torch, onnxruntime, onnxruntime-gpu inf1.xlarge No Yes torch, torch-neuron inf2.xlarge No Yes torch, torch-neuronx データセット プライベートなデータセットを使ってモデルの評価を行いました。データセットは3つの感情 (happiness, anger, neutral) を含み、音声の合計長は74分です。また各クラスの事例数は次の表のとおりです。 Class # Samples happiness 201 anger 206 neutral 270 指標 音声感情認識のPyTorchモデルをCPUインスタンスで実行した結果を基準 2 とし、相対的な指標で評価します。 高速化率 新しく導入したモデルが既存のモデルと比較して、どれだけ推論速度が改善したかを示します。「PyTorchモデルをCPUで実行したときの1推論あたりの平均レイテンシー / 各モデルの1推論あたりの平均レイテンシー」で計算します。 コスト削減率 新たに導入したモデルが既存のモデルと比較して、どれだけコストを削減できたかを評価します。「1 - 各モデルの1推論あたりの平均コスト / PyTorchモデルをCPUで実行したときの1推論あたりの平均コスト」で計算します。 平均コストは平均レイテンシーとオンデマンドインスタンスの価格表から算出しました。 精度変化 新たに導入したモデルが既存のモデルと比較して、どの程度精度が変化したかを評価します。「PyTorchモデルをCPUで実行したときのaccuracy(精度)- 各モデルのaccuracy」で計算します。 モデルファイルサイズ変化率 新たに導入したモデルのファイルサイズが、既存のモデルと比較してどれだけ変化したかを評価します。モデルのファイルサイズは、一般的にストレージやメモリ上のリソース使用量に影響を与えます。「 ONNX or AWS Neuronでコンバート後のモデルのファイルサイズ / PyTorchモデルのファイルサイズ」で計算します。 モデルロード時間高速化率 新たに導入したモデルをロードするのに要する時間が、既存のモデルと比較してどれだけ速くなったかを評価します。これは、モデルを使用する前に必要な初期化時間に直結します。「PyTorchモデルのロード時間 / ONNX or AWS Neuronでコンバート後のモデルのロード時間」で計算します。 実験結果 推論のパフォーマンス比較 前処理(スペクトログラムや単語ベクトル作成)以外の推論部分についてレイテンシーおよび速度を測定し比較しました。 Model PyTorch ONNX ONNX ONNX PyTorch ONNX AWS Neuron AWS Neuron Instance c6in.2xlarge c6in.2xlarge c6in.2xlarge c6in.2xlarge g5.xlarge g5.xlarge inf1.xlarge inf2.xlarge 浮動小数点数 32 bit floating point (fp32) fp32 int8 uint8 fp32 fp32 16 bit brain floating point (bf16) bf16 高速化率 - 1.19 2.01 1.93 9.57 23.57 9.58 57.55 コスト削減率 - 0.16 0.50 0.48 0.73 0.89 0.94 0.98 精度変化 - 0.000 -0.005 -0.006 -0.007 -0.007 -0.008 -0.008 高速化 音声感情認識モデルはONNXやGPU、Inf1 & Inf2インスタンスを使うことで高速化を達成できることが分かりました。特にinf2.xlargeを使った推論はONNXモデルをGPUで動かした場合の推論速度より2倍程度早くなっています。Inf1 & Inf2インスタンスではbf16以外の データタイプ も使えるのですが、推論のレイテンシーはbf16と同程度となりました。 コスト削減 Inf1 & Inf2インスタンスのコスト削減率が高いことが分かります。これは1推論あたりの平均レイテンシーが短いことや、インスタンスの価格がGPUインスタンスより安価であることが挙げられます。 精度変化 Inf1 & Inf2ではモデルパラメータの浮動小数点数としてbf16を利用しました。ONNXやInf1 & Inf2インスタンスを使った場合には、浮動小数点数の変更はモデルの精度への影響が少ないようです。Inf1 & Inf2インスタンスではbf16以外のデータタイプも使えるのですが、ほかのデータタイプを使用してもbf16を使用した場合と同程度の精度変化が見られました。 モデルサイズ・ロード時間比較 ONNXやInf1 & Inf2インスタンスを使うことで、モデルのファイルサイズやモデルのロード時間がどのように変化したか記録しました。 Model PyTorch ONNX ONNX ONNX PyTorch ONNX AWS Neuron AWS Neuron Instance c6in.2xlarge c6in.2xlarge c6in.2xlarge c6in.2xlarge g5.xlarge g5.xlarge inf1.xlarge inf2.xlarge 浮動小数点数 fp32 fp32 int8 uint8 fp32 fp32 bf16 bf16 モデルファイルサイズ変化率 - 0.96 0.24 0.24 1.00 0.96 0.52 0.50 モデルロード時間高速化率 - 28.99 133.73 143.42 0.88 29.85 17.51 7.61 モデルファイルサイズ モデルファイルサイズは浮動小数点数に連動して小さくなりました。モデルのパラメータを16ビットで表す場合は元のモデルのファイルサイズの約1/2、8ビットで表す場合は約1/4になりました。 モデルロード時間 ONNXモデルではファイルサイズと同様に浮動小数点数の精度を下げることで、モデルロード時間が短くなる結果になりました。また、AWS Neuron SDKを用いて作成したモデルは、ONNXモデルよりは遅いものの、PyTorchのモデルのロード時間よりは早くなりました。 まとめ MiiTelで使われている音声感情認識モデルの推論をInf系インスタンスで実行し、推論速度や精度について比較しました。Inf2インスタンスを利用することで、今までの精度を維持する一方で高速で低コストな推論ができることを確認しました。また、モデルファイルサイズやモデルロード時間も既存モデルに対して改善することを確認しました。 これらの結果は、既存モデルに対する改善を示しています。品質を維持したまま推論速度を上げることで、音声感情認識の速度を高め、結果としてユーザー体験の向上が期待できます。また、コスト削減により長期の運用コストを抑えることが可能となります。 今後の課題としては、既存モデルを実際にInf2インスタンスで置き換え、音声感情認識の高速化とコスト削減に取り組むことがあげられます。また、他の深層学習を使った機能やサービスに対して、同様の最適化手法を適用することも推進する予定です。 Santoso, J., Yamada, T., Makino, S., Ishizuka, K., Hiramura, T. (2021). Speech Emotion Recognition Based on Attention Weight Correction Using Word-Level Confidence Measure. Proc. INTERSPEECH, 1947-1951, doi: 10.21437/Interspeech.2021-411 ↩ 現在の音声感情認識モデルはCPUインスタンスで実行されてるため。 ↩
この記事は RevComm Advent Calendar 2023  14 日目の記事です。 RevComm でフロントエンド開発をしている豊崎 朗です。MiiTel Analytics、MiiTel Mobile Phone、MiiTel RecPod というプロダクトに携わっています。フロントエンドチームに籍を置いていますが、バックエンド、モバイルアプリ開発もやっています。 (フルスタックチームは、別であります。) MiiTel Mobile Phone、MiiTel RecPod はモバイルアプリであり、Flutter を用いて開発を行っています。この記事では、これらのプロダクトで利用している CI/CD サービスである Codemagic について紹介します。 目次 Codemagic について やってみよう 初期設定 ~ App の設定 Workflow の設定 トリガーの設定 (Build triggers) キャッシュの設定 (Dependency caching) その他の設定 テスト Codemagic の設定のバックアップについて まとめ Codemagic について Codemagic は、Flutter, ReactNative, native iOS, native Android, Unity, Kotlin Multiplatform, Ionic といったようなモバイルアプリのためのクラウドベースの CI/CD プロダクトになります。 ビルド, テスト, Apple App Store, Google Play などの App Store へのデプロイメントのプロセスを自動化することが出来ます。 プロセスのトリガーとして、GitHub などのリポジトリへ push, Tag の追加, PR のマージといったアクションを指定することが出来ます。 また、個人で利用する場合、月に無料で 500 分、mac OS (M1 マシン) を動かすことができるので個人でアプリ開発をしているユーザーにとって、大変お財布に優しくなっています。 やってみよう 今回は、以下の想定で Codemagic で Flutter アプリをビルドしてみます。 リポジトリ: GitHub トリガー: main ブランチにマージされたタイミング アプリ: Flutter アプリ 初期設定 ~ App の設定 Codemagic では、App(Application) 単位で設定を行います。 App には、一つのリポジトリが紐づくという仕様になります。 Sign up画面 から Codemagic へ Sign up を行う。 Sign up が完了すると、 管理画面 に遷移する。 App の設定を行う。 画面の右上にある、 Add application ボタンをクリック Appの設定を開始 GitHub を選択し Next: Select repository をクリック リポジトリの選択 Select repository の Github integration をクリック。ダイアログが開くので、Codemagic でビルドしたいリポジトリを選択する。 ビルドするリポジトリの選択 Select project では、 Flutter App (via Workflow Editor) を設定する 全体の設定 Finish: Add application をクリックする。 以下のようにリポジトリの設定がされていれば完了となります。 設定完了の様子 Workflow の設定 ここからは、Workflow の設定に移ります。 Codemagic では、App に複数の Workflow を設定することができ、App 作成後はデフォルトで Default workflow という名前の Workflow が存在します。 それでは、Default workflow を main ブランチに PR がマージされたタイミングで走らせるような設定をしてみましょう。 トリガーの設定 (Build triggers) Build triggers を開く。 Automatic build triggering の Trigger on push をチェック Watched branch patterns に以下を設定し、Add pattern ボタンをクリック Add new pattern: main Include or Exclude: Include Source or Target: Target トリガーの設定 キャッシュの設定 (Dependency caching) キャッシュの設定をしておくと、依存パッケージをインストール時の速度が向上するため、設定しておきましょう。 * キャッシュは、最大で 14 日間キャッシュをします。 Enable dependency caching をチェック 以下のパスを追加する。 $FLUTTER_ROOT/.pub-cache $HOME/.gradle/caches $HOME/Library/Caches/CocoaPods キャッシュの設定 その他の設定 Codemagic は、他にも様々な設定ができ柔軟性があります。ここでは紹介に留めておきます。 Workflow は以下の流れで実行され、様々な設定をすることができます。 * 太字の部分は、今回デフォルトの設定から変更していない機能になります。 ビルドトリガー (Build triggers) 環境変数 (Environment variables) キャッシュ設定 (Dependency caching) Post-clone スクリプト (Post-clone script) Pre-test スクリプト (Pre-test script) テスト (Tests) Post-test スクリプト (Post-test script) Pre-build スクリプト (Pre-build script) ビルド (Build) Flutter version, Xcode version, CocoaPods version, Android build format, Build mode, Build arguments が設定可 Post-build スクリプト (Post-build script) Pre-publish スクリプト (Pre-publish script) ディストリビューション (Distribution) Google Play, App Store Connect, Firebase App Distribution へのアプリ配信も自動化することができます。 RevComm では、weekly でアプリを自動ビルドし、それぞれへ配信しています。 通知 (Notifications) デフォルトでは、メールへビルド結果の通知が配信されるようになっています。 オプションで、Slack への通知が出来ます。 設定し終えたら、画面の右上から Save changes をクリックします。 ビルド設定は以上になります。 テスト 適当な PR を立てて、CI が走るかどうかテストしてみます。 PRをマージする様子 PR がマージされると、Codemagic の Builds ページから CI が走っているかどうか確認することができます。 workflowが起動された様子 詳細画面は、以下のような感じになります。 実行中のworkflow 完了後のworkflow 問題無く、ビルドが完了していることがわかります 🎊 今回は、Post-build スクリプトやディストリビューションを設定していないため、ビルドした成果物を詳細画面からダウンロードすることぐらいしか出来ません。 RevComm では、Post-build スクリプトに DeployGate の API を叩くようにし dev 環境を構築したり、ディストリビューションに Google Play と Apple Store Connect を連携させて、Closing testing, TestFlight で stg 環境を構築しています。 余力がありましたら、是非試してみてください。 Codemagic の設定のバックアップについて Codemagic で設定した App の Workflows を GitHub で管理したい時があると思います。 Codemagic API では、 Applications API を用いることで、Workflow Editor で設定した情報を json でバックアップすることが出来ます。 しかし、リストアに関しては対応していないため、あくまで設定した情報をバックアップするという用途でしか使えません。 * Workflow Editor を利用せず、設定したリポジトリに配置した codemagic.yaml を参照するといった設定も出来ます。この方法であれば、バックアップとリストアもすることが出来ます。細かい設定をする人は、こちらの方が向いているかもしれません。 RevComm では、GitHub Actions から Codemagic API を実行し、定期的にバックアップを取るようにしています。 ここでは、そちらを共有します。 以下の GitHub Actions ファイルを .github/workflows/sync-codemagic-settings.yaml として保存する。 name : Sync codemagic-settings on : schedule : - cron : '0 0 * * *' # Every day at 00:00 UTC workflow_dispatch : jobs : sync : name : Sync codemagic-settings runs-on : ubuntu-latest env : CODEMAGIC_APP_ID : <Your codemagic app id> steps : - name : Checkout uses : actions/checkout@v4 with : ref : main - name : Fetch codemagic-settings run : | curl -s -H "Content-Type: application/json" \ -H "x-auth-token: ${CODEMAGIC_TOKEN}" \ --request GET https://api.codemagic.io/apps/${CODEMAGIC_APP_ID} > codemagic-settings.json env : CODEMAGIC_TOKEN : ${{ secrets.CODEMAGIC_TOKEN }} - name : Diff check continue-on-error : true id : diff_check run : git diff --exit-code # only run if there are changes - name : Commit changes and create pull request if : ${{ steps.diff_check.outcome == 'failure' }} run : | NOW=$(date +"%Y%m%d%H%M") git config --global user.name "action@github.com" git config --global user.email "65916846+actions-user@users.noreply.github.com" git checkout -b feature/update-codemagic-settings-${NOW} git add codemagic/workflows.json git commit -m "Update codemagic-settings" git push origin feature/update-codemagic-settings-${NOW} gh pr create -B develop -H feature/update-codemagic-settings-${NOW} --title 'Update codemagic-settings' --body 'Updated codemagic-settings by github-actions' env : GITHUB_TOKEN : ${{ secrets.GITHUB_TOKEN }} sync-codemagic-settings.yaml の CODEMAGIC_APP_ID: <Your codemagic app id> の部分の置き換え Codemagic の App の設定ページのアドレスバー ( https://codemagic.io/app/<app id>/workflow/<workflow id>/settings ) から、 <app id> を取得し、そちらを利用する。 Codemagic の API トークンを取得し、GitHub Actions の Secret として設定する。 Codemagic の左のメニューから、Teams をクリック → Personal Account をクリック → General settings の Integrations をクリック → Codemagic API から API トークンを取得する。 Codemagic apiトークンの取得 GitHub リポジトリの Settings ページ → Secrets and variables → Repository secrets に Name: CODEMAGIC_TOKEN, Secret: 先ほど取得したトークンを設定する。 設定は以上になります。 毎日 0 時に GitHub Actions が定期実行され、リポジトリに保存されている設定と Codemagic の Workflow に差分があった時のみ、PR が自動的に作成されます。 また、 workflow_dispatch の設定も入れているため、任意のタイミングで GitHub Actions を実行することもできます。 まとめ Codemagic で Flutter アプリをビルドし、Codemagic の設定ファイルを GitHub Actions でバックアップをしました。 Codemagic を使っていると、手元でアプリをビルドするという運用には戻れなくなります。(私は、戻れなくなりました 😊) Codemagic は、設定も柔軟に出来ますし、App Store へ成果物を自動的にアップロードしてくれるので、個人的には満足しています。 まだ、モバイルアプリ開発者で CI/CD を導入していない方は、是非 Codemagic を使ってみてください! それでは、また 👋
Introduction As a professional developer, you encounter something new every day: new coding techniques, new ways of organizing projects, new bugs, new tools, etc. The amount of knowledge the world has to offer is too much, so we write it down as a note in a Jira ticket or as a comment in a PR. We recall certain patterns and internalize the frequent ones; we unconsciously discard the rare to the bottom of our long-term memory. And then it happens. That little hack comes to bite back again, and you have a hunch of how to solve it. You may have bookmarked the solution in Stack Overflow, or was it a ChatGPT conversation? When did that bug happen anyway? What was the context? I’ve experienced this many times. In this post, I’ll describe a solution to this conundrum: “a knowledge portfolio.” A knowledge portfolio Your knowledge portfolio describes all the information you’ve encountered throughout your career. In a sense, it determines your identity as an engineer. The Pragmatic Programmer [1] offers clear-cut guidelines to build a successful portfolio. In this post, I’ll comment on the most impactful points that kept mine robust and updated. Tips Write an engineering daybook We use daybooks to take notes in meetings, to jot down what we’re working on, to note variable values when debugging, to leave reminders where we put things, to record wild ideas, and sometimes just to doodle. - Andy and Dave, The Pragmatic Programmer An engineering daybook is a recount of your day as an engineer. It could be ramblings about what you’ve done, a loose set of links of all you’ve seen, or wild ideas barely connected. Whatever the form, storing that information is crucial so it can be accessed anywhere, anytime. I especially recommend writing your thoughts, as it’s like teaching something. Just by doing it, you organize and consolidate the ideas floating around. If you hate writing, just a one-liner or a link is a good starting point. Choose tools you’re comfortable with I use Evernote to keep my portfolio, but any tool you’re familiar with is enough. Some essential functions any such tool should have are: Export to plain text (food for your future super-intelligent AI butler) Synchronization across devices (you might want that info in your phone) Tagging system (to categorize your knowledge) Task reminding feature (to plan your learning) I also recommend Readwise to highlight anything from the internet. You can write a script to export everything to Evernote. Tag everything Was that some new feature in React? Ok, append the React tag. Tag everything so that you can find things more easily. If you can subcategorize tags, it’s even better. Evernote does it like this: Maintain a "bugdex" Because of optimism, we usually expect the number of bugs to be smaller than it turns out to be. Therefore, testing is the most mis-scheduled part of programming. - Frederik P. Brooks Jr., The Mythical Man-Month [2] Bugs are Software Engineering’s necessary evil. We spent a great deal of our time fixing them. Document every bug; you’ll never know when it’ll pop up again. Isolate the bug and track it on your favorite source control. There are two caveats. First, only some bugs can be isolated. In that case, at the very least, describe what happened (context, cause, and solution), your future self will be grateful. Furthermore, start to classify them by framework or programming language. Make time for testing wild ideas The most daunting piece of paper is the one with nothing written on it - Andy and Dave, The Pragmatic Programmer [1] Take advantage of your knowledge. If you find some interesting idea, don’t let it float around. Use your chosen tool’s reminder feature to test that idea later or to practice something you’ve bookmarked recently. Conclusion In this post, I’ve described my experience with maintaining a knowledge portfolio. Every person is different: the crafters who keep everything clean and organized, the pragmatics who just want the information to be stored somewhere. Find what works for you; in the end, storing and centralizing the data is the essential part. And if you never use that data, at least you now have a kind of work memoir for your family and friends: old traditions are getting cooler again. About the author Hi, I'm Jose @juanjo12x , and I work as a Backend Engineer here at Revcomm. I spend my days writing Python and thinking about what to learn next. References [1] Andy Hunt and Dave Thomas. The Pragmatic Programmer 20th Anniversary Edition. [2] Frederik P.Brooks, Jr. The Mythical Man-Month.
こんにちは! RevComm のフロントエンドエンジニアの小山功二です。 私が RevComm に入社する前に担当した開発案件は、どれも国内のユーザーにしか使われていないものばかりでした。一方で、RevComm の提供する MiiTel は、日本はもちろんインドネシアやアメリカでも使われています。 私の担当する MiiTel CallCenter というプロダクトは今年リリースしたのですが、こちらもリリース当初から海外で利用できることが求められていました。 開発時からタイムゾーンを扱うのは大変そうだよねというのは感じていたのですが、想定よりも大変でした。 そこで今回はタイムゾーン周りの理解を深めるために、Day.js のタイムゾーンを変更する関数である tz という関数について整理していきたいと思います。 似たような4種の書き方ができる Day.js の tz 関数 まず tz 関数を使えるようにする準備をしましょう。 Day.js でtz関数を使えるようにするには timezone パッケージをインストールする必要があります。また、場合によって customParseFormat パッケージが必要になるケースもあります。 ここでは yarn を使っています。 $ yarn add dayjs timezone $ yarn add customParseFormat dayjsを拡張します。 import dayjs from 'dayjs' ; import timezone from 'dayjs/plugin/timezone' ; import timezone from 'dayjs/plugin/customParseFormat' ; dayjs.extend ( timezone ); dayjs.extend ( customParseFormat ) これで tz 関数を使えるようになりました。 この tz 関数は公式ドキュメントの中では3つページに記載があり、それぞれ別の使い方があることが示されています。 Time Zone Parsing in Zone Converting to Zone 私はこの公式ドキュメントにない書き方をしてしまったのですが、それが Parsing in Zone に近い書き方で第一引数にDate型の値を入れてしまった書き方です。 Parsing in Zone には以下のように書いてあるので、第一引数がstring でくる前提のように見えます。 Parse date-time string in the given timezone and return a Day.js object instance. 一方で、tzの型をVSCode上で見てみると、以下のようになっていました。 const tz: dayjs.DayjsTimezone ( date: string | number | dayjs.Dayjs | Date | null | undefined , timezone?: string | undefined ) => dayjs.Dayjs ( + 1 overload ) 第一引数はstring以外も許容しています。 この点、具体的なコードをみた方が比較しやすいと思うので、 いくつかの記法を並べてみましょう。 // [記法1] ローカルタイムゾーンで '2023-12-25 00:00:00'を'Pacific/Honolulu'のタイムゾーンに変換 dayjs ( "2023-12-25 00:00:00" ) .tz ( "Pacific/Honolulu" ) // [記法2] '2023-12-25 00:00:00'を'Pacific/Honolulu'のタイムゾーンでパース dayjs.tz ( "2023-12-25 00:00:00" , "Pacific/Honolulu" ) // [記法3] 記法2と同じ日時をdate型で指定したもの dayjs.tz (new Date ( '2023-12-25 00:00:00' ), "Pacific/Honolulu" ) // [記法4] 文字列のフォーマットを解析して'Pacific/Honolulu'のタイムゾーンを設定(customParseFormatが必要) dayjs.tz ( "12-25-2023 00:00:00" , "MM-DD-YYYY ss:mm:HH" , "Pacific/Honolulu" ) 記法2が Parsing in Zone に記載がある使い方で、記法3 が私が書いてしまったコードと同様の書き方です。 試しに、実際にどんな値が返ってくるか format 関数を使って見てみましょう。 なお、日本のタイムゾーンであるAsia/TokyoはUTC+09:00であり、以下のコードの中に出てくるPacific/Honoluluの中で設定しているPacific/HonoluluはUTC-10:00です。2つのタイムゾーンの時差は19時間です。 // [記法1] 日本時間2023-12-25 00:00:00のPacific/Honoluluでの時間を返す。 dayjs ( "2023-12-25 00:00:00" ) .tz ( "Pacific/Honolulu" ) .format ( "YYYY-MM-DDTHH:mm:ssZ" ) => "2023-12-24T05:00:00-10:00" // [記法2] Pacific/Honoluluのタイムゾーンの2023-12-25 00:00:00を返す。 dayjs.tz ( "2023-12-25 00:00:00" , "Pacific/Honolulu" ) .format ( "YYYY-MM-DDTHH:mm:ssZ" ) => "2023-12-25T00:00:00-10:00" // [記法3] Pacific/Honoluluのタイムゾーンの2023-12-25 00:00:00を返して欲しかったのですが、そうなっていない... dayjs.tz (new Date ( '2023-12-25 00:00:00' ), "Pacific/Honolulu" ) .format ( "YYYY-MM-DDTHH:mm:ssZ" ) => "2023-12-24T05:00:00-10:00" // [記法4] Pacific/Honoluluのタイムゾーンにて、“12-25-2023” という文字列が "MM-DD-YYYY" というフォーマットになっていると解釈した値を返す。 dayjs.tz ( "12-25-2023" , "MM-DD-YYYY" , "Pacific/Honolulu" ) .format ( "YYYY-MM-DDTHH:mm:ssZ" ) => "2023-12-25T00:00:00-10:00" 記法2と3の結果が一致しませんでした。 tz 関数の実処理をコードから確認 なぜこうなるかわからなかったので、Day.jsのコードをみてみました。 https://github.com/iamkun/dayjs/blob/dev/src/plugin/timezone/index.js 以下は tz 関数の該当コードの抜粋です。 d.tz = function ( input , arg1 , arg2 ) { const parseFormat = arg2 && arg1 const timezone = arg2 || arg1 || defaultTimezone const previousOffset = tzOffset ( +d (), timezone ) if (typeof input !== 'string' ) { // timestamp number || js Date || Day.js return d ( input ) .tz ( timezone ) } const localTs = d.utc ( input , parseFormat ) .valueOf () const [ targetTs , targetOffset ] = fixOffset ( localTs , previousOffset , timezone ) const ins = d ( targetTs ) .utcOffset ( targetOffset ) ins.$x.$timezone = timezone return ins } typeof input !== 'string' のとき、たとえば date 型を引数として設定した場合、 d(input).tz(timezone) を返す処理になっています。これは記法1のタイムゾーンを変換する処理と同じ結果になります。 ドキュメントにはこの点の記載がないので、Day.jsのリポジトリに改善提案の issue を立てました。 終わりに まとめると以下のようになります。 // [記法1] dateTimeString を timezone に変換 dayjs ( dateTimeString ) .tz ( timezone ) // [記法2] dateTimeString を timezone でパース dayjs.tz ( dateTimeString , timezone ) // [記法3] dateObject を timezone に変換(記法2に似てますが、記法1と同じ結果なので注意が必要) dayjs.tz ( dateObject , timezone ) // [記法4] dateTimeStringをcustomParseFormatで解析して、timezoneでパース dayjs.tz ( dateTimeString , customParseFormat , timezone ) 今回は、Day.js の tz 関数について整理をすることができました。 これからもタイムゾーンとしっかり向き合い、日本でも国外でも多くの方々に使われるプロダクトに成長させられるように向き合っていきたいです。
概要 想定読者 MiiTelのOutgoingWebhook 機能について 本記事で紹介しているGoogleCalendar連携について 利用想定 開発者向け情報 概要 全体の処理シーケンス GoogleCalendarAPI利用時に認証tokenを保存するための処理 通話完了からGoogleCalendarへイベントを登録する処理 事前準備 GoogleCalendarAPIの利用設定 OutgoingWebhookの利用設定 連携サーバの構築 構成情報 GCPでサーバ構築 PHPのインストール Nginxの設定 OAuth用の処理 カレンダー登録処理 おわりに 概要 株式会社RevCommのCorporateEngineeringチームの登尾です。 この記事は 株式会社RevComm Advent Calendar 2023 の 11日目の記事です。 MiiTelのOutgoingWebhook機能を使い応対履歴をGoogleCalendarに残す方法について紹介します。 想定読者 MiiTel製品の導入検討中の方 : MiiTelのOutgoingWebhook機能でどのようなことができるか知りたい方 MiiTel製品導入済みの方 : 自社の業務にあわせてMiiTelのカスタマイズを検討している方。カスタマイズの作業を行う開発者の方 MiiTelのOutgoingWebhook 機能について トーク解析AI の MiiTelには様々な機能があります。 詳しくは MiiTel 機能紹介ページをご覧ください。 様々な機能の中の1つの OutgoingWebhook は、「MiiTel Analytics」プラットフォームで解析した結果を、他社システムへ連携可能にします。 https://miitel.com/jp/archives/4907 本記事で紹介しているGoogleCalendar連携について 本記事では「MiiTel Analytics」プラットフォームで解析した結果をGoogleCalendarに連携する方法について紹介します。 具体的には、電話が完了し、音声解析が終了すると、GoogleCalendarに通話の応対履歴が自動で登録されます。 流れをスクリーンショットと共に見ていきましょう。 MiiTelPhoneで電話をかける 2. 応対履歴が作成される 3. GoogleCalendarに応対履歴が連携される。GoogleCalendarの説明文に登録されたリンクからMiiTelの応対履歴ページに戻ることができる 利用想定 この仕組みを使うことで、営業支援システムを導入していない企業様が電話による営業活動を効率化できます。営業担当者の応対履歴がGoogleCalendarに同期され、GoogleCalendarを見ることで誰がどの取引先にどのくらい時間を使ったかを簡易的に見ることができるようになります。 開発者向け情報 概要 OutgoingWebhookを使ってGoogleCalendar連携するには下の2つの実装が必要となります。 GoogleCalendarにイベント(タイトル、開始時間、終了時間、参加者などの情報を含むカレンダー上のイベント) を登録するための処理 GoogleCalendarAPI を利用します。GoogleCalendarAPIを使って イベント を登録するには Google の OAuth認証 も必要です。OAuth認証用の処理も作成する必要があります。 Google の OAuth Appについての詳細は OAuth App Verification Help Center を確認してください。 MiiTelのOutgoingWebhookのリクエストを受けつけるための処理 実装する際には MiiTel OutgoingWebhookのサポートページ で詳細をご確認ください。 全体の処理シーケンス GoogleCalendarAPI利用時に認証tokenを保存するための処理 今回は簡単に構築するために、 tokenを連携サーバ内にファイルとして保存しました。実際に運用をする際には検討が必要な部分です(図の9番の処理) 通話完了からGoogleCalendarへイベントを登録する処理 事前準備 GoogleCalendarAPIの利用設定 GoogleCloudの左メニューからGoogleCalendarAPIを選択し GoogleCalendarAPIを有効にします 右上のボタンより認証情報を作成します 承認済みの JavaScript 生成元 と 承認済みのリダイレクト URI に 連携サーバのものを指定します OAuth同意画面で公開ステータスをテスト、テストユーザーにこの連携で利用するメールアドレスを追加します OAuthのスコープを指定します OutgoingWebhookの利用設定 MiiTelAdminの外部連携機能で事前にOutgoingWebhookの設定を行います 設定完了画面 連携サーバの構築 💡 セキュリティ上の注意: サーバを構築する際は、Firewallで不要なポートへのアクセスやアクセス元IPアドレスを制限すること、脆弱性のある古いバージョンのライブラリは利用しない等、セキュリティには十分注意してください。 構成情報 構築環境: GCP(Google Cloud Platform) アプリケーション: PHP (フレームワークは Slim を利用), nginx ドメイン: お名前.comで取得 GCPでサーバ構築 Compute Engine → Compute Engineを有効化 → VM インスタンス → 新規作成 からサーバを作成します CloudDNSでお名前.comで取得したドメインと紐付けます https://over40.work/entry/gcp-clouddns/ (こちらのページを参考にさせていただきました。ありがごうございました) PHPのインストール sudo apt update sudo apt install php-cli php-fpm php-json php-common php-mbstring curl unzip curl -sS https://getcomposer.org/installer | php sudo mv composer.phar /usr/local/bin/composer sudo timedatectl set-timezone Asia/Tokyo mkdir relation_app cd relation_app composer require slim/slim:"4.*" composer require slim/psr7 composer require guzzlehttp/guzzle composer require google/auth composer require google/apiclient Nginxの設定 インストール sudo apt install nginx sudo systemctl start nginx sudo systemctl enable nginx # 証明書に無料の Let’s encript を利用 sudo apt install certbot python3-certbot-nginx nginxのconf設定 # HTTPのリクエストをHTTPSにリダイレクト server { listen 80; listen [::]:80; server_name **********; location / { return 301 https://$host$request_uri; } } # HTTPSの設定 server { listen 443 ssl; server_name **********; root /home/sh-noborio/relation_app; index index.php index.html index.htm index.nginx-debian.html; ssl_certificate /etc/letsencrypt/live/**********/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/**********/privkey.pem; include /etc/letsencrypt/options-ssl-nginx.conf; ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; location / { try_files $uri $uri/ /index.php$is_args$args; } location ~ \.php$ { try_files $uri =404; fastcgi_pass unix:/run/php/php7.4-fpm.sock; fastcgi_index index.php; fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; include fastcgi_params; } } OAuth用の処理 シーケンス図 PHPのコード(一部抜粋) ルーティング処理、他APIの呼び出し <?php namespace calendar; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use Slim\Factory\AppFactory; require __DIR__ . '/vendor/autoload.php' ; $ app = AppFactory :: create () ; // GoogleOAuthLogin画面へ遷移するためのページ // シーケンス図 3番,4番の処理 $ app -> get ( '/google_oauth' , function ( Request $ request , Response $ response , $ args ) { $ client = new GoogleOAuthClient () ; $ url = $ client -> getAuthUrl () ; $ response -> getBody () -> write ( '<button onclick="window.location.href= \' ' . $ url . ' \' ;">Google OAuth</button>' ) ; return $ response ; }) ; // OAuthのcodeを受け取ってtokenを保存するための処理 // シーケンス図 6番〜9番の処理 $ app -> get ( '/google_oauth_callback' , function ( Request $ request , Response $ response , $ args ) { $ params = $ request -> getQueryParams () ; if ( isset ( $ params [ 'code' ])) { $ authCode = $ params [ 'code' ] ; $ client = new GoogleOAuthClient () ; $ token = $ client -> fetchAndSaveToken ( $ authCode ) ; $ response -> getBody () -> write ( "Token saved successfully!" ) ; return $ response ; } else { $ response -> getBody () -> write ( "Error: No authorization code received." ) ; return $ response -> withStatus ( 400 ) ; } }) ; GoogleOAuth用の処理 <?php namespace calendar; use GuzzleHttp\Client; use Google\Auth\OAuth2; class GoogleOAuthClient { const CLIENT_ID = '************' ; const SCOPES = 'https://www.googleapis.com/auth/calendar openid email' ; const REDIRECT_URI = 'https://**********/google_oauth_callback' ; const CLIENT_SECRET = '***' ; public function getAuthUrl () { $ oauth2 = new OAuth2 ([ 'clientId' => self :: CLIENT_ID, 'authorizationUri' => 'https://accounts.google.com/o/oauth2/v2/auth' , 'redirectUri' => self :: REDIRECT_URI, 'tokenCredentialUri' => 'https://oauth2.googleapis.com/token' , 'scope' => self :: SCOPES, ]) ; return $ oauth2 -> buildFullAuthorizationUri () ; } /** * codeを使ってアクセストークンを取得しファイルとして保存する * シーケンス図 7番〜9番の処理 */ public function fetchAndSaveToken ( $ authCode ) { $ oauth2 = new OAuth2 ([ 'clientId' => self :: CLIENT_ID, 'redirectUri' => self :: REDIRECT_URI, 'tokenCredentialUri' => 'https://oauth2.googleapis.com/token' , 'grant_type' => 'authorization_code' , ]) ; $ oauth2 -> setCode ( $ authCode ) ; // アクセストークンを取得 $ client = new Client () ; $ response = $ client -> post ( $ oauth2 -> getTokenCredentialUri () , [ 'form_params' => [ 'code' => $ authCode , 'client_id' => self :: CLIENT_ID, 'client_secret' => self :: CLIENT_SECRET, 'redirect_uri' => self :: REDIRECT_URI, 'grant_type' => 'authorization_code' , 'access_type' => 'offline' , 'prompt' => 'consent' , ] ]) ; $ token = json_decode ( $ response -> getBody () , true ) ; $ mail = $ this -> getEmailFromToken ( $ token ) ; // トークンをファイルに保存 file_put_contents ( $ this -> getTokenPath ( $ mail ) , $ token [ 'access_token' ]) ; return $ token ; } public function getEmailFromToken ( $ token ) { if ( isset ( $ token [ 'id_token' ])) { $ idToken = $ token [ 'id_token' ] ; list ( $ header , $ payload , $ signature ) = explode ( '.' , $ idToken ) ; // Decode payload $ decodedPayload = json_decode ( base64_decode ( strtr ( $ payload , '-_' , '+/' )) , true ) ; return $ decodedPayload [ 'email' ] ?? null ; } return null ; } カレンダー登録処理 シーケンス図 PHPのコード(一部抜粋) ルーティング処理、他APIの呼び出し <?php namespace calendar; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; use Slim\Factory\AppFactory; require __DIR__ . '/vendor/autoload.php' ; $ app = AppFactory :: create () ; // webhookリクエストを受け付け // シーケンス図 3番〜6番の処理 $ app -> post ( '/webhook' , function ( Request $ request , Response $ response , $ args ) { $ body = $ request -> getBody () -> getContents () ; $ webhook_response = new OutgoingWebhookResponse ( $ body ) ; // 外線発信以外 または Email が取れない場合は処理停止 if ( !$ webhook_response -> isOutGoingCall ()   || !$ webhook_response -> getEmailAddress ()) { return $ response ; } // 初回のチャレンジレスポンスのための処理 if ( $ webhook_response -> getChallenge ()) { $ response -> getBody () -> write ( $ webhook_response -> getChallenge ()) ;          $ response -> withHeader ( 'Content-Type' , 'text/plain' ) ;          return $ response ; } $ title = $ webhook_response -> getCompanyName () . ':' . $ webhook_response -> getName () . '様' ; $ start_date = $ webhook_response -> getAnsweredAt () ; $ end_date = $ webhook_response -> getEndsAt () ; $ id = $ webhook_response -> getId () ; // GoogleCalendarにEventを登録 $ googleOAuthClient = new GoogleOAuthClient () ; $ event = $ googleOAuthClient -> createEvent ( $ mail , $ title , $ id , $ start_date , $ end_date ) ; $ response -> withHeader ( 'Content-Type' , 'text/plain' ) ; return $ response ; }) ; OutgoingWebhookのResponse用の処理 <?php namespace calendar; /** * @see 音声認識終了時にチェックを入れた場合のペイロード https://support.miitel.jp/hc/ja/articles/13050493066905-Outgoing-Webhook */ class OutgoingWebhookResponse { private $ data ; public function __construct ( $ json ) { $ this -> data = json_decode ( $ json , true ) ; } public function getEmailAddress () { if ( $ this -> data [ 'call' ][ 'details' ][ 0 ][ 'call_type' ] === 'OUTGOING_CALL' ) { foreach ( $ this -> data [ 'call' ][ 'details' ][ 0 ][ 'participants' ] as $ participant ) { if ( $ participant [ 'from_to' ] === 'FROM' ) { return $ participant [ 'name' ] ?? null ; } } } return null ; } public function getChallenge () { return $ this -> data [ 'challenge' ] ?? null ; } public function getAnsweredAt () { return $ this -> data [ 'call' ][ 'details' ][ 0 ][ 'dial_answered_at' ] ?? null ; } public function getEndsAt () { return $ this -> data [ 'call' ][ 'details' ][ 0 ][ 'dial_ends_at' ] ?? null ; } public function getCompanyName () { if ( $ this -> data [ 'call' ][ 'details' ][ 0 ][ 'call_type' ] === 'OUTGOING_CALL' ) { foreach ( $ this -> data [ 'call' ][ 'details' ][ 0 ][ 'participants' ] as $ participant ) { if ( $ participant [ 'from_to' ] === 'TO' ) { return $ participant [ 'company_name' ] ?? '' ; } } } } public function getName () { if ( $ this -> data [ 'call' ][ 'details' ][ 0 ][ 'call_type' ] === 'OUTGOING_CALL' ) { foreach ( $ this -> data [ 'call' ][ 'details' ][ 0 ][ 'participants' ] as $ participant ) { if ( $ participant [ 'from_to' ] === 'TO' ) { return $ participant [ 'name' ] ?? '' ; } } } } public function getId () { return $ this -> data [ 'call' ][ 'id' ] ?? '' ; } public function isOutGoingCall () { return isset ( $ this -> data [ 'call' ][ 'details' ][ 0 ][ 'call_type' ]) && $ this -> data [ 'call' ][ 'details' ][ 0 ][ 'call_type' ] === 'OUTGOING_CALL' ; } } GoogleCalendarにEventを登録する処理 <?php namespace calendar; use GuzzleHttp\Client; use Google\Auth\OAuth2; class GoogleOAuthClient { const CLIENT_ID = '************' ; const SCOPES = 'https://www.googleapis.com/auth/calendar openid email' ; const REDIRECT_URI = 'https://**********/google_oauth_callback' ; const CLIENT_SECRET = '***' ; const MIITEL_URL = 'https://***********/miitel.jp' ; /** * @see https://developers.google.com/calendar/api/v3/reference/events/insert?hl=ja */ public function createEvent ( $ mail , $ title , $ id , $ startDateTime , $ endDateTime ) { // Googleクライアントの初期化 $ client = new \Google_Client () ; $ client -> setClientId ( self :: CLIENT_ID ) ; $ client -> setClientSecret ( self :: CLIENT_SECRET ) ; $ client -> setRedirectUri ( self :: REDIRECT_URI ) ; $ client -> addScope ( self :: SCOPES ) ; // 保存されているトークンの読み込み $ tokenPath = $ this -> getTokenPath ( $ mail ) ; $ accessToken = file_get_contents ( $ tokenPath ) ; $ client -> setAccessToken ( $ accessToken ) ; // Googleカレンダーにイベントを登録 $ service = new \Google_Service_Calendar ( $ client ) ; $ url = self :: MIITEL_URL . "/app/calls/ { $ id } " ; $ event = new \Google_Service_Calendar_Event ([ 'summary' => $ title , 'description' => $ url , 'start' => [ 'dateTime' => $ startDateTime ] , 'end' => [ 'dateTime' => $ endDateTime ] ]) ; $ createdEvent = $ service -> events -> insert ( 'primary' , $ event ) ; return $ createdEvent ; } } おわりに いかがでしたでしょうか。 OutgoingWebhookを使えば、他のサービスとの連携ができ、様々な応用が可能になります。 例えばAsanaとの連携によるタスク管理の実現も可能です。 MiiTel Outgoing Webhook の使い方: タスク管理ツールとの連携サンプル をご覧ください。 MiiTelをより効果的に、より深く活用したい企業様は、OutgoingWebhookの機能をぜひご活用ください。
こんにちは。PBXチームの山崎です。 振り返ると前回のブログからちょうど1年経ってしまいました。来年はブログのアウトプットも増やしていきたいですね。 さて早速ですが、今回のブログの概要です。 死活監視の一環で、STUNというバイナリベースのプロトコルのクライアントを実装してみた Python3.10で入ったパターンマッチングがバイナリプロトコルの解析に便利だった 前半でSTUNを軽く触って動作を確認し、後半でPythonを使って実装してみます。 目次 目次 STUNについて STUN のパケット構造 やってみよう Pythonのパターンマッチングについて バイナリデータに対するパターンマッチング PythonでSTUNやってみる まとめ STUNについて STUNは主に以下の特徴を持つプロトコルです。 WebRTCでよく使われる、NAT越しに通信するためのプロトコル(の一部) 相手から見た自分のグローバルアドレスなどを知ることができる RFC 8489 バイナリベース STUN のパケット構造 プロトコルの理解には、実際にリクエスト・レスポンスを観察してみると捗ります。 実際にパケットを送信するために、必要な情報を集めていきましょう。 RFC 8489の "2. Overview of Operation” を読むと、まずはクライアントからBinding Requestを送りたまえ、と書かれています。 Binding Requestとは何ぞ?と読み進めると、5章にその構造が定義されています。 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |0 0| STUN Message Type | Message Length | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Magic Cookie | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | | | Transaction ID (96 bits) | | | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Figure 2: Format of STUN Message Header 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Type | Length | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | Value (variable) .... +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Figure 4: Format of STUN Attributes 20バイト(固定長)のヘッダの後ろに、0個以上のアトリビュートが続く構成です。 ヘッダの各フィールドの定義を以下に抜粋します STUN Message Type: Binding Requestは 0x0001、Responseは 0x0101 Message Length: ヘッダを除いた STUNメッセージの長さ Magic Cookie: 0x2112_A442 (固定値) Transaction ID: 12bytes の乱数 そしてアトリビュートはタイプに長さと(タイプごとに定義される)データが続く、よくある構成ですね。 アトリビュートタイプが取りうる値は、18.3. STUN Attributes Registry に定義されています。 今回使う値を以下に抜粋します。 0x0020: XOR-MAPPED-ADDRESS やってみよう なんとなく構造がわかったので、試しにリクエストを送ってみましょう。GoogleがSTUNサーバーを公開してくれているので、ありがたく利用させていただきます。 # リクエストデータは先頭から... # 00 01: Binding Request # 00 00: Length # 21 12 a4 42: Magic Cookie # 00 01 ... 11: Transaction ID (乱数作るの面倒なので適当に) # RFCにはリクエストにSOFTWARE Attributeを含めたまえ (SHOULD) とあるけど、面倒なので省略 bash$ printf ' \x00\x01\x00\x00\x21\x12\xa4\x42\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x10\x11 ' | \ socat - UDP:stun.l.google.com:19302 | hexdump -C 00000000 01 01 00 0c 21 12 a4 42 00 01 02 03 04 05 06 07 |....!..B........| 00000010 08 09 10 11 00 20 00 08 00 01 99 a3 17 ba 65 4b |..... ........eK| 00000020 手抜きをしてSOFTWARE Attributeを省略しましたが、ちゃんとレスポンスを返してくれました。読んでみましょう。先頭から... ヘッダ部 01 01 : Binding Response 00 0c : 長さは12 (Big Endian) 21 12 a4 42 : Magic Cookie 00 01 ... 11 : Transaction ID アトリビュート部 00 20 : XOR-MAPPED-ADDRESS 00 08 : 長さは8 00 01 99 a3 17 ba 65 4b : アトリビュートの中身 XOR-MAPPED-ADDRESS なるものとして 00 01 99 a3 17 ba 65 4b というデータが取得できました。 そろそろゴールが見えてきそうですね。心躍らせながらXOR-MAPPED-ADDRESSの仕様を確認して読み解いてみましょう。 0 1 2 3 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |0 0 0 0 0 0 0 0| Family | X-Port | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | X-Address (Variable) +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ Figure 6: Format of XOR-MAPPED-ADDRE SS Attribute 今回は 99 a3 が(サーバから見た)ポート番号、その後ろ 17 ba 65 4b が(サーバから見た)IP アドレスになります。 どちらもMagic CookieとXORをとった値とあるので戻してみましょう。XORはもう一回かけると元の値に戻りますね。 bash$ echo $((0x17 ^ 0x21)).$(( 0xba ^ 0x12 )).$(( 0x65 ^ 0xa4)).$(( 0x4b ^ 0x42 )) 54.168.193.9 私のIPアドレスは 54.168.193.9 のようです。 答え合わせをしてみましょう。 bash$ curl httpbin.org/ip # 自分の IP アドレスを返してくれるWebAPI { " origin " : " 54.168.193.9 " } 正解でした! Pythonのパターンマッチングについて ここからはもう一つの主題である、パターンマッチングについてみていきます。 PythonのパターンマッチングはPEP 622で定義され、Python3.10で実装されました。 例をPEP 622から転載します。if elseよりもすっきりと表現できていますね。 match response.status: case 200 : do_something(response.data) # OK case 301 | 302 : retry(response.location) # Redirect case 401 : retry(auth=get_credentials()) # Login first case 426 : sleep(DELAY) # Server is swamped, try after a bit retry() case _: raise RequestError( "we couldn't get the data" ) バイナリデータに対するパターンマッチング Pythonでバイナリデータを扱う場合はbytes型がよく登場します。 ところが、パターンマッチングではbytes型を扱うことができません。 PEP 622から引用: To match a sequence pattern the subject must be an instance of collections.abc.Sequence, and it cannot be any kind of string (str, bytes, bytearray). It cannot be an iterator. collections.abc.Sequenceであれとのことなので、組み込み関数の memoryview() を使います。 memoryview() を使うと、コピーせずにシーケンスとして扱うことができます。 例として、「先頭2bytesがメッセージタイプ、その後ろ2bytesが長さ、その後ろにボディ」というデータを考えます。 これは以下のようにパースできます。 msg = b ' \x01\x02 ' + b ' \x00\x02 ' + b ' \x02\x03 ' match memoryview (msg): # "_" で読み飛ばすことができる # *data のように書くと、残り全てを受け入れる case [ 0x01 , 0x01 , _, _, *data]: print (f "type=Hello data={data}" ) # if を続けてバリデーションを書くこともできる # 2*len みたいに、長さを指定することはできない case [ 0x01 , 0x02 , len0, len1, *data] if len (data) == (len0 << 8 ) + len1: print (f "type=NewTransaction data={data}" ) case _: print ( "invalid message" ) # => type=NewTransaction data=[2, 3] サンプルデータ (msg) の先頭が 0x0102 なので、2つ目のcaseにマッチしています。 これをif文で書いたものと比較してみます。match文ではデータ構造が表現されていて、見通しがいいですね。 # 2つ目の case です if msg[: 2 ] == b ' \x01\x02 ' and len (msg[ 4 :]) == (msg[ 2 ] << 8 ) + msg[ 3 ] data = msg[ 4 :] print (f "type=NewTransaction data={data}" ) PythonでSTUNやってみる これで必要なパーツが揃いました。組み上げていきましょう。 まず、リクエストを送信してレスポンスを受け取る部分です。 def stun_binding_request_udp (sock: socket.socket, hostname: str , port: int ) \ -> tuple [ bytes , bytes ]: message_type = b ' \x00\x01 ' length = b ' \x00\x00 ' magic = bytes (MAGIC_COOKIE) transaction_id = RAND_bytes( 12 ) req = message_type + length + magic + transaction_id sock.sendto(req, (hostname, port)) res = sock.recv( 2048 ) return res, transaction_id 実際に作る際はTransport classみたいなのを作ってレイヤを分けるとか色々考えると思いますが、今回はサンプルなのでベタっと書いていきます。 でもってこのレスポンスの解析をパターンマッチングを使って書いてみます def stun_parse_response (message: bytes , transaction_id: list [ int ]) -> None : header = message[: 20 ] attr_data = message[ 20 :] # ヘッダの情報を元に、レスポンスが壊れていないかチェック match memoryview (header): case [ 0x01 , 0x01 , length0, length1, 0x21 , 0x12 , 0xA4 , 0x42 , *_tid] \ if _tid == transaction_id and \ len (attr_data) == (length0 << 8 ) + length1: logger.debug( "valid stun response" ) case _: logger.warning(f "invalid response: {message}" ) return # XOR-MAPPED-ADDRESS Attribute を抽出 # TODO : ~~面倒~~サンプルなので先頭に XOR-MAPPED-ADDRESS があると仮定 match memoryview (attr_data): case [ 0x00 , 0x20 , length0, length1, *value]: length = (length0 << 8 ) + length1 body = value[ 0 :length] logger.info(f "type: XOR-MAPPED-ADDRESS" ) _stun_parse_xor_mapped_address(value) case [type0, type1, *_]: logger.warning( f "unknown attribute: type=0x{type0:02X}{type1:02X}" ) そして最後に _stun_parse_xor_mapped_address() を実装したら完成です def _stun_parse_xor_mapped_address (attribute: list [ int ]) -> tuple [ bytes , int ]: match attribute: case [ 0x00 , 0x01 , xport0, xport1, *xaddress] if len (xaddress) == 4 : xport = (xport0 << 8 ) + xport1 port = xport ^ int .from_bytes( MAGIC_COOKIE[: 2 ], byteorder= "big" , signed= False ) address: str = "." .join( [ str (x ^ y) for x, y in zip (xaddress, MAGIC_COOKIE)] ) logger.info(f "global address: {address}:{port}" ) case _: logger.warning(f "unknown data: {attribute}" ) 足りない部分を補って動かしてみましょう。 import socket from logging import getLogger, StreamHandler, INFO, Formatter from ssl import RAND_bytes logger = getLogger(__name__) handler = StreamHandler() formatter = Formatter( "%(filename)s:%(lineno)s - %(levelname)s - %(message)s" ) logger.setLevel(INFO) handler.setFormatter(formatter) logger.addHandler(handler) MAGIC_COOKIE = ( 0x21 , 0x12 , 0xA4 , 0x42 ) # snip. def main (): stun_server = "stun.l.google.com" stun_port = 19302 af = socket.AF_INET with socket.socket(af, socket.SOCK_DGRAM) as sock: sock.settimeout( 5 ) response, transaction_id = stun_binding_request_udp( sock, stun_server, stun_port ) logger.info(f "local address: {sock.getsockname()}" ) stun_parse_response(response, list (transaction_id)) main() bash$ python3. 10 ~/stun-check0.py stun-check0.py:79 - INFO - local address: ( ' 0.0.0.0 ' , 53603 ) stun-check0.py:47 - INFO - type: XOR-MAPPED-ADDRESS stun-check0.py:26 - INFO - global address: 54 . 168 . 193 .9:53603 よさそうですね。 まとめ 実際にプロトコルを実装することで、普段漫然と使っていたSTUNの理解が深まりました。 また、パターンマッチングを使うことで、データの構造を表現し、if elseを見通しよく記述できました。積極的に使っていきたい機能ですね。
この記事は RevComm Advent Calendar 2023 7日目の記事です。 RevComm Research の加藤 集平です。音声合成を中心に、音声信号処理の研究開発に携わっています。 昨今、生成AIを利用したフェイク動画(偽動画)が世の中を騒がせています。先日も、岸田首相のフェイク動画がSNSで話題となりました。この動画は内容が明らかに偽物であったこと、動画像が稚拙であったこと、合成音声の質がさほど高くなかったことなどから、視聴者が比較的簡単に偽物と見抜けるものでした。しかし、昨今の技術革新のスピードからすると、近い将来、簡単には偽物と見抜けない動画が出回ることは十分に考えられます。 このような状況に対して現在、国際的な法規制が検討されています。一方で、技術的な対抗手段の研究開発も目覚ましい発展を遂げています。本記事では、フェイク動画を構成する要素のうち、 フェイク音声を検出する技術 の現在について調査しました。 フェイク音声はどのようにして作られるのか フェイク音声に求められる条件 条件1: 声色・声の調子が本人に似ていること 条件2: 任意の内容を話すことができること、あるいは本物の音声の一部を改竄できること 音声合成 音声変換(声質変換) 最新の技術でフェイク音声はどの程度検出できるのか 最新の技術ではどのようにしてフェイク音声を見抜いているのか 検出処理の流れ 評価実験 これからのフェイク音声検出 まとめ フェイク音声はどのようにして作られるのか 本題の検出技術の話題に入る前に、そもそもフェイク音声はどのようにして作られるのかについて説明します。なお、筆者自身は悪意を持ってフェイク音声を作ったことがなく、以下はあくまでフェイク音声検出の専門家が現時点で想定している事柄にとどまることにご注意ください。 フェイク音声に求められる条件 悪意を持ってフェイク音声を作ろうとした場合、フェイク音声に求められる条件とは何でしょうか。筆者の知る限り確立された見解はありませんが、筆者は以下のように考えます。 条件1: 声色・声の調子が本人に似ていること フェイク音声の目的が 特定の個人そっくりの声で偽の内容を話させること だとすれば、声色や声の調子が似ていることは必須の条件でしょう。少なくとも、人間の耳で本物の音声かどうかの判別が難しければ、悪意を持った作成者の意図に合致するでしょう。 条件2: 任意の内容を話すことができること、あるいは本物の音声の一部を改竄できること 任意の内容を話させることができれば、悪意を持った作成者に都合のよい、偽の内容を話させることができます。本物の音声の一部を改竄することでも、悪意を持った作成者の目的を達成できるかもしれません。 音声合成 上の2つの条件を満たすものの一つに、音声合成があります。音声合成のうち広く使われているのはテキスト音声合成 (text-to-speech; TTS) で、テキストを入力すると、テキストの内容を読み上げた音声が出力されます。テキスト音声合成は、電話の自動応答・公共交通機関の自動アナウンス・スマートスピーカーなど、すでに日常生活のさまざまな場面で利用されています。 テキスト音声合成はその半世紀以上にわたる歴史の中で、さまざまな手法が提案されてきました。中でもフェイク音声の文脈で重要なのは、深層学習(ディープラーニング)に基づく手法です。深層学習に基づく手法は従来の手法よりもより自然な声を合成することができます。音声合成が人間と同等の自然性を持つこと(=あたかも人間が話しているように聞こえる)については、2017年に発表された Tacotron 2 という手法により、限られた条件下ではあるもののすでに達成されています。さらに、2023年には、目標話者(声を模倣したい話者)の音声をわずか3秒だけ用意すれば、その話者の声に類似した音声合成が可能となる手法も発表されています ( VALL-E )。さらに、テキスト情報を手がかりに、音声の一部を編集できるような手法も提案されています ( SpeechPainter )。 研究者は決して悪用するために研究をしているわけではありません。しかしながら、最新の手法はその自然性の高さから悪用が懸念されており、たとえばVALL-Eには誰でも試せるような公式のデモシステムや公式実装は存在しません。一方で、 VALL-Eの論文は公開されている ことから、再現実装や劣化版訓練済モデル *1 は存在します。 音声変換(声質変換) もう一つの方法に、音声変換(声質変換, voice conversion)があります。音声変換は多くの場合、ある話者の音声を入力とし、それを別の話者に類似した音声に変換したものを出力とします *2 。音声変換の手法も深層学習の応用でめざましく発展しており、VTuberなどの分野で、すでに広く利用されています。 悪用の懸念としては、SNSなどに投稿された声(特に、有名人の声など)を訓練データとして音声変換モデルを訓練し、その人の声に類似した音声を出力できるようにして悪用することが想定されています。 最新の技術でフェイク音声はどの程度検出できるのか 結論から言うと、深層学習に基づく手法で作成されたフェイク音声のデータセット(後述)に対して人間の音声かフェイク音声かを判別するタスクにおいて、記事執筆時点(2023年12月時点)でおよそ3%〜4%の等価誤り率 (equal error rate; EER) が達成されています(論文: Automatic Speaker Verification Spoofing and Deepfake Detection Using wav2vec 2.0 and Data Augmentation )。このデータセットは ASVspoof 2021 という自動話者認証・なりすまし攻撃への対抗手段のチャレンジ(競技会)で提供されたものです。ASVspoof 2021の参加チームのうち最も優れた手法の性能はEER = 15%程度でしたから、わずか2年で劇的に性能が向上していることが分かります。 なお、同じデータセットに対して人間がどの程度フェイク音声を見抜けるかのデータは見つけることができませんでした。ただし、同じく深層学習に基づく手法で作成された別のデータセットに対して、全体的な正解率 (overall accuracy) が70%程度(つまり30%程度は間違えた)であるという研究があります。データセットが異なるので直接の比較はできませんが、最新の技術では人間が判別できないようなフェイク音声を見抜ける可能性があります。 最新の技術ではどのようにしてフェイク音声を見抜いているのか ここでは、上述した Automatic Speaker Verification Spoofing and Deepfake Detection Using wav2vec 2.0 and Data Augmentation という論文で提案されている、およそ3%〜4%のEERが達成されている手法を紹介します。 検出処理の流れ 検出処理の流れ システムに入力された音声は、 wav2vec 2.0モデル *3 で変換されたのち、RawNet2ベースのエンコーダーに通され、 の特徴表現となります(Cはチャネル数、Fはスペクトルのビン数、Tは時間方向のフレーム数)。 エンコーダーから出力された埋め込み表現は、 2次元のself-attention (SA) から得られるattention mapを元に 、周波数方向に着目した特徴表現 ( f ) と、時間方向に着目した特徴表現 ( t ) の2つに変換されます。 そして、fとtはgraph attention networkというグラフ構造で表現されるデータを処理できるネットワークで処理され、最終的に人間の音声か (real) フェイク音声か (fake) が出力されます。 なお、本論文ではwav2vec 2.0と2次元のself-attentionを用いていますが、本論文のベースとなっている AASIST というモデルでは、それぞれsinc layerとmax poolingを用いています。さらに、本論文では RawBoost という手法を使って訓練データの拡張 (data augmentation; DA) を行っています。 評価実験 ASVspoof 2021で使用されたデータセットのうち、LA (logical access) データベースがモデルの訓練に用いられました。評価には、LAデータベースの評価セットのほか、DF (deep fake) データベースの一部も用いられました。 ここでは、フェイク音声を想定したDFデータベースによる評価結果を紹介します(EERのカッコ内は異なる3個のrandom seedによる結果の平均で、カッコ外は最もよい結果)。 評価結果 ( source ) まず、sinc-layerに代えてwav2vec 2.0を導入することにより、大幅にEERが改善しています。これは、wav2vec 2.0のような大規模な事前学習モデルを用いることにより、限られたデータセットだけで(特徴量抽出を含めた)モデル全体の訓練を行うよりも汎化性能がよいためだと推察されています。また、2次元のself-attention (SA) およびdata augmentation (DA) もEERの改善に寄与しています。 これからのフェイク音声検出 フェイク音声検出に関連するイベントとして、 ASVspoof という自動話者認証・なりすまし攻撃への対抗手段のチャレンジ(競技会)が2015年より隔年で開催されています。本記事の冒頭で紹介したように、深層学習の発展により人間が見抜けないようなフェイク音声が登場するリスクはますます高まっています。一方で、フェイク音声を検出する技術もASVspoofのコミュニティを中心に劇的な発展を遂げています。 部分的に改竄された音声 (partially spoofed audio) の検出 など、より難しいタスクの研究も進んでいます。また、音楽生成の分野ではありますが、 AIによって生成された音楽に透かしを埋め込む手法 など、生成AI側からの提案もなされています。このような手法は、AIによって生成された音楽や音声の判別をより簡単にするほか、作成者の意図に反した改竄を防ぐことにもつながると考えられます。 まとめ 本記事では、フェイク音声の検出技術の現在について簡単な解説を行いました。RevCommでフェイク音声に関連する機能は今のところ提供していませんが、筆者は音声合成の研究開発をする者としてフェイク音声のリスクに対して無関係ではありません。法規制・技術的対抗手段について、これからも関心を寄せていきたいと思います。 *1 : VALL-Eの論文ではおよそ6万時間の訓練データを使用していますが、いくつかある再現実装ではおよそ600時間の訓練データが使用されているようです。 *2 : より一般には、音声を入力として、何らかの変換を施した音声を出力とします。たとえば、構音障害(発音器官やその動きに問題があり発音がうまくできない障害)の人の音声を、健常者のような音声に変換する研究もあります。 *3 : 大規模な事前学習モデルであるwav2vec 2.0 XLS-R(人間の音声だけで訓練されている)を、フェイク音声を含む訓練データでファインチューニングしたモデルが使用されています。
この記事は RevComm Advent Calendar 2023 6日目の記事です。 はじめに RevCommのバックエンドエンジニアの中島です。 前回は PyCon APAC 2023への参加レポート を寄稿しました。今回もイベント参加のレポートになります。 今回は 2023年11月25日(土) に開催された ISUCON13 に、弊社内でフロントエンドエンジニア1名+バックエンドエンジニア2名、計3名のチームで参加しました。 isucon.net 私はISUCONには過去数回参加したことがありますが他の2名は参加なしという経験値で、結果としては最終スコア 7,749点、参加チーム数 694組中 314位でした。 なんとも言い難い順位ですが、参加レポートという形で忘備録として未来の参加者に知見を残せればと思います。 キックオフ 中島は8月入社ですが、8月下旬に社内のエンジニアが全員入っているSlackチャンネルで下記を投稿しました。 このようにRevCommではチームや組織を超えたコミュニケーションが活発で、社内Slackを通して日々情報交換や交流がされています。 ISUCONの参加登録が始まる前に何気なく投稿したものだったのですが、 エンジニアとして活躍する美馬さん、大谷さんにリアクションいただきました。 例年参加希望者が参加枠を大幅に上回っており、ISUCONの最も高いハードルは参加登録とも言われます。 メンバーの美馬さんがその恒例の連打バトルに見事に勝ち切ってくれましたので、参加が決定し、私たち3名は正式にチームとして発足することになりました。 早速それぞれの持っている情報を共有したり役割やチーム名を決めたりと、当日までに必要な準備が始まりました。 このとき、私たちが決めた目標は以下の4つになります。 failureしない 計測する 計測を元に一番大きいボトルネックを潰す 本番までに社内ISUCONやりたい このうち、4. の「社内ISUCON」は社内からのみアクセスできる環境構築の準備に手間取ってしまい、本番に向けた練習時間を確保するために計画が頓挫し、開催できませんでした。 しかし、今でも興味があり私の悲願なのでいつかやりたいです。 当日まで まずは「達人が教えるwebパフォーマンスチューニング」を全員各自で読むことにしました。 多くの知見が詰め込まれている有名な本で、基本的なツールの使い方や典型的なチューニング方法や計測の重要性を学びました。 次に公式の情報や勉強会の情報、過去問の改善方法まとめなどの、役に立ちそうな記事についてそれぞれが情報共有する打ち合わせを数回行いました。 このとき、ISUCONの練習環境を社内で利用しているAWSアカウントに準備したりと、ありがたく社内のリソースも利用させていただきました。 日々忙しくチーム揃って手を動かした練習というのは本番一週間前から2時間ずつ3回に分けて行ないました。 練習の内容としては当日を意識した素振りを意識しており、「Git・SSHの設定」「alpやpt-query-digestのインストールや利用」「DBインデックスの活用」「EC2の複数台構成」などを行いました。 利用言語はGolangの予定でしたので、下記のサイトを参考にNew Relic APM Agentを入れて今回のISUCONのゴールドスポンサーでもあるNew Relicの威力も確認できました。 newrelic.com New Relicは非常に便利でこれさえあればアプリケーションのほとんどの計測ができてしまうわけですが、 チーム内では alp や pt-query-digest といったツールと併用することを決めました。 本番当日 ISUCON 13は 10:00-18:00 で競技が行われました。 特に大きな障害などはなくスケジュール通りに進行が行われ、運営の努力を感じました。 参加者が快適に競技に集中できる環境を用意いただけていたと思います。 実際の時間や作業内容は前後していると思いますが、覚えている限りの当日のスケジュールを書いてみます。 9:00 - 9:30 起床・オンラインで集合 10:00 - 11:00 RepositoryやGit・SSHの準備。最初のベンチマーク取得。アプリケーションのドキュメントの確認。 11:00 - 13:00 alpとNewRelicの計測結果から、icon周りの改善を試みる。「304 Not Modified」の実装をした。テーブル構造やインデックスの有無を把握し、データベース周りの理解を進める。 13:00 - 14:00 休憩 14:00 - 14:30 改めてコードを読んだり改善策を探す。どこから修正すべきか議論しながら、できることからやっていくことに。 14:30 - 17:00 DBのスロークエリにインデックスを貼り始める。中間テーブルの削除を検討したり、DNS周りの最適化ができないか考えたり試したり。スコアがじわじわと伸びていく。 17:00 ランキングが隠れる。己との勝負が始まる。 17:00 - 18:00 この時追加でインデックス貼ったりログを切ったりしながら、少しずつスコアを上げていく。再起動後にベンチを回して通過したところでタイムアップとなる。 あとでポータルを確認してみると 16:53 の時点で 5,576 点だったようで、最後の1時間で2,000点以上スコアを更新していることになります。 ISCUON 13 Portal 最後畳みかけるように改善を行いましたが、やりたいことはまだまだあったので、本当に時間が足りなかったです。 振り返り 初回参加のメンバーが多いので、来年再挑戦できれば多くの点で改善できそうだなと感じました。 今回ざっくりと役割分担はあるものの、ほとんどの改善をチーム全員で確認をしながら行いました。 そのため、もっと手分けし並列に進めるようにすると上手に時間を活用できると思いました。 Miroで振り返り 4つの目標のうち以下の3つを達成できたので、大方ヨシ!とします。 Failureしない 計測する 計測を元に一番大きいボトルネックを潰す 本番までに社内ISUCONやりたい これらの目標を達成するために、私たちのチームでは競技終了直前に再起動テストを実施したり、コマンドラインツールだけでなく New Relic を活用した計測に力を入れました。 まとめ 今回は ISUCON 13 に社内でメンバーを組んで参加したというレポートになりました。 ISUCONというコンペに挑戦する場合でも弊社の社員は非常に協力的で、特にネットワークチームには社内環境を準備する際のアドバイスなどもいただけました。 スコアは物足りない部分がありますが、来年はもっと高得点が取れるように自身のスキルを磨きたいです。 最後になりますが、ISUCON 13の作問に携わって下さった方、運営の皆様、スポンサーの皆様ありがとうございました!
こんにちは、小島です。この記事はRevComm Advent Calendar 2023 12/5 分の記事です。 qiita.com 2023年のRevCommに起きた大きな変化のひとつは、英語話者(日本語の読み書きや会話を前提としない)のエンジニア採用を始めたことです。 昨年までは日本語能力の採用要件がありましたが、今年からその要件なしで採用するようになりました。そのため、日本語を母語とする僕もチームメンバーに英語話者が増えていくにつれて適応することになりました。 僕自身は今までまったく英語学習に興味がなく、英語で仕事をすることになるなどとは思ってもみなかったです。そんな人が実際に業務をやってみてどう感じているかについて書こうと思います。 なお、この記事ではRevCommの業務での英語の使われ方や、自分視点での英語を使った業務を進めることへの感想を書きます。英語学習のTipsといった話はこの記事では書きません。 まえおき 2023年12月現在、日本語が公用語の会社に一部英語話者もいるという状態の組織です すべてのエンジニアに英語の読み書き・会話が求められているわけではありません 言語のスキルも含めてチームを編成をするよう努力をしています 僕が所属しているVideoチーム(MiiTel Meetings開発チーム)を前提にした記事となっています 社内の日英話者混合の別チームで異なる体制をとっているチームもあります 筆者の英語スキル 前提として筆者の英語スキルを書いておきます。 正式に受験したことはないのですが、TOEICの問題集を買って模試をやってみたところだいたい600点ちょっとくらいでした。つまり別に良くもないし、悪くもない、普通といったところです。 受けたのは英語学習をちゃんと始める前(今年の5月ごろ)のスコアですが、現時点でもスコアとしてはそんなものではないかなと思います。 自分の自認としては、 読み書きは遅いけどできる、リスニング、スピーキングはあんまりできない コードレビューで一言コメントとかは英語学習をちゃんとする以前からできてた 長文を書くのはだいぶ難しい 会話の経験はほぼないので、議論をするみたいなことは基本できない ただ何言っているのかの大意はわかる。賛成か反対かとか という感じです。イメージとしては、受験勉強をちゃんとやった大卒社会人が持っている感じの英語力は最初から持っていた、と想像してもらえれば。 今年起きた変化 僕の所属するチームで今年起きたことを簡単に書いておきます。 2022年まで 2023年12月現在 ミーティング 日本語のみ 基本英語に。ただ日本語を話してもOK Slack 日本語のみ 特定のチャンネルでの会話は英語に。チーム同士の会話は日本語話者どうしでも英語で行う機会が増えた GitHubのPRタイトル 特に指定なし 英語のみ チーム内のドキュメント 日本語のみ 翻訳して日英対応を適宜実施。新規ドキュメントは英語のみのものもある。 これらがある日を境に一斉に起きたわけではないのですが、徐々に変わっていき現在はこのような形になりました。もちろん日本語の読み書きも自然と行いますが、開発に関することで日本語を書くことはだいぶ減った印象です。 英語で仕事をして思ったこと 純粋に学習に時間がかかる シンプルに英語学習には時間がかかることがわかりました。 AWSのIAMとかで権限設定しているJSONを読み解けるようになるのって、そんなに根詰めて勉強しなくても1, 2ヶ月あれば自ずと意味がわかってくると思います(諸説あります)。しかし、英語は3ヶ月与えられても目に見える成果を出すのは結構難しいです。 自然言語って難しいですね(遠い目) 主体的にならない限り、仕事中にスキルはあがらない 当たり前なんですが、仕事中は仕事を進めるのが重要です。英語の学習になるからと回り道をさせてもらうなんてことはありません。 ということで、意思疎通が難しければ通訳(僕のチームの場合はPjMがバイリンガル)をしてもらったり、あとでSlackで聞き直したりとかで仕事は十分に前に進みます。別に会話の機会とかも増やそうとしなければ増えません。 一方で業務外で勉強した表現を仕事で使うとか、業務外でインプットした表現が実際に同僚の口から出てくる、といったアウトプットの補助/インプットの強化という観点でスキルを上げる機会を作ることはできると思いました。 リモートでの会議がよりむずかしく感じる 日本語でもビデオ会議は対面での会議とは異なるクセがあると想うのですが、英語だとそれがさらに強く出る気がしています。 これは英語(僕にとっての外国語)のせいなのか、ビデオ会議のせいなのか、はたまたその両方同時にあるからなのかは正直わからないです。 なんとなく英語話者の傾向として、一度話し始めるとこちらが止めない限り止まらないことが多いように思います *1 。そこで一度、相手の話を止めて深掘りするとか、自分のわからないところを伝えて詳細を話してもらうとかいうのが必要だと思うのですが、思っているだけでなかなか行動に移すことができません。 仕事の話をしてないからかもしれませんが、飲み会とか対面で会った時のほうが英語を話しやすい感じがします。 テクノロジーのサポート(Google Meetの自動字幕など)はなにもないはずなのに、なにもない方が外国語を喋れるというのは不思議なものですね。 チームに新たな風が吹く 課題ばかり挙げてしまったのでよいところも書くと、チームの中に新たな視点がもたらされるのでそこはとてもエキサイティングです。 例えば、あるエンジニアがフロントエンドのアーキテクチャーに抜本的な指摘をしたりとか、ブランチ戦略の改善を考えないかと提案してもらったりとか。普段なかなか意識しない、議題にあがらないことを議論のテーブルに挙げてくれる人がいて助かっています。 必ずしも英語話者じゃないとできない指摘ではないのですが、持っている知見や過去開発していたバックグラウンドが日本人チームで日本語を使って開発してきた僕とは明白に違うので、意見をあげてくれてその背景を解説してくれるのはとても勉強になります。 具体的にはFeature FlagやDesign Doc、Trunk ベース開発について、教えてもらったりしました。 相手も外国語学習に苦労してる 弊社で採用している英語話者の場合、英語話者といっても母語が英語とは限りません。たとえば、母語はスペイン語やインドの言語という人が第二外国語として英語を学習し英語を話している、という人の方が多いです。 そんなわけで、外国語学習って大変だよねという意見には多くの人が共感してくれます。 弊社のSlackには #club-language-exchange というチャンネルがあり、日本語についての質問や英語についての質問、英語で雑談を振るなど自由に使われています。そういう場に、英語話者の人がさっと現れてニュアンスの違いを説明してくれたり、逆に日本語話者の僕らも日本語の質問に答えたりします。 基本的にほぼ全員がなんらかの外国語を勉強したことがあるので、質問にはみんなとても優しく答えてくれます。 RevCommに入社してくる英語話者の方は来日が前提になっているのもあり、日本に関心が高い人が多いです。そういう方は日本語の学習も熱心だったりするので、一緒に高め合おうという会話をしたりもします。 終わりに この記事ではRevCommの英語話者エンジニアとの仕事について書きました。 もちろん課題や難しいところもありつつも、複数の言語の交流は複数の文化の交流で楽しい側面もたくさんあります。メキシコのお祭りの話を聞いたり、スイスの列車の話を聞いたりしながら業務ができるのは楽しいです。 異文化交流に興味がある人がこの記事を読んで、日本にも面白い環境があるんだなと思っていただければ幸いです。 *1 : ファシリテーターのPMは、何を言ったかを全部覚えられないのでたまにノートとったりしているそうです。
この記事は RevComm Advent Calendar 2023 4日目の記事です。 弊社は Notion を使っています。 Notion は Mermaid が使えて最高なのですが、 flowchart の theme がアルファベットに若干弱い *1 です。というわけで theme をいじって解決します。 これを書くだけです。 %%{init:{'themeCSS':'line-height:1.1rem;'}}%% *1 : この問題はいつか直るでしょう
この記事は RevComm Advent Calendar 2023 1日目の記事です。 RevCommでバックエンド開発をしている小門 照太です。 MiiTelにおける認証基盤を担うマイクロサービス(MiiTel Account)に携わっています。 MiiTelでは外部のIDプロバイダー(IdP)と連携したシングルサインオン(SSO)によるログインに対応しています。 この記事ではMiiTelのお客様がSSOログインを利用する際に必要となる手順とAccountチームの運用について紹介します。 SSO利用までのステップ 現在MiiTelではOpen ID Connect(OIDC)という認証規格によるSSOに対応しています。 システム基盤にはAWSを使っており、認証機能のバックエンドとしてAmazon Cognitoを採用しています。 お客様がSSOを利用開始するまでに以下の手順があります。 IdP上でOAuthアプリケーションの作成(お客様が実施) Cognitoに1. の設定情報を登録 MiiTelで発行した認可リダイレクトURLをお客様に伝達&IdPに登録 OIDCに対応したIdPとして有名なものにはMicrosoft Entra ID(旧 Azure AD)やGoogle Workspaceがあります。 同じAccountチームの同僚が以前OIDC SSOに関する解説の記事を書いていますので、ぜひご覧ください。 Cognito user pool で OpenID Connect を利用した外部 ID Provider によるサインインを実現する - RevComm Tech Blog 上記の手順においてRevCommとしては2. の作業が必要になります。 サービスの構成管理 MiiTel AccountのWebサービスは社内において独立したマイクロサービスであり、AWS上で構築しています。 上述したCogitoをはじめECS/Fargate、ALBなどのインフラリソース一式は基本的にIaCで構成管理しています。IaCにはTerraformを用いています。 インフラの構成変更、つまりAWSサービスの設定変更やリソース追加/変更/削除などはTerraformコードを変更して適用する運用としています。 Terraformコードを修正するPull Requestを作成し、マージされると設定変更が自動適用されるCI/CDの仕組みを整備しています(詳しくは後述します)。 AccountチームにおけるSSOセットアップ MiiTelのお客様に対するSSO利用開始のためにはCognitoの構成変更が必要となるため、上述したTerraformを用いた構成変更による運用をしています。 OIDC IdPの追加 お客様のIdP設定情報(前述の手順2.)はCognitoユーザープールにおける「アイデンティティプロバイダー」として登録します。 参考 - ユーザープールへの OIDC ID プロバイダーの追加 - Amazon Cognito Terraformでは aws_cognito_identity_provider リソースです。 aws_cognito_identity_provider | Resources | hashicorp/aws | Terraform | Terraform Registry resource "aws_cognito_identity_provider" "azure_customer_A" { user_pool_id = "ap-northeast-1_XXXX" provider_name = "for-Customer-A" provider_type = "OIDC" ... provider_details = { client_id = "1234aaaa-bbbb-cccc-dddd-eeeeeeeeeeee" client_secret = "dummy" ... } } 上述したステップ1. でお客様が作成したOAuthアプリケーションのパラメーターを provider_details に設定します。なお client_secret は秘匿情報と見なし、Terraformコードではダミーの値を入れるようにしています。 変更の適用 Terraformコードを変更したらPull Requestを作成します。 CI/CDの自動化として、GitHub Actionsを利用して構成変更のレビューおよび適用できる仕組みを整備しています。 参考 - Automate Terraform with GitHub Actions Pull Requestを作成するとCIのジョブが起動してlint( terraform fmt )、dry run( terraform plan )などの実行結果がコメントに追記されます。 GitHub Pull Requestのキャプチャ Show Plan で terraform plan の出力結果を確認できます。 ... # module.~~~~~~~~~~~~~~~~~ will be created + resource "aws_cognito_identity_provider" "oidc_google" { + attribute_mapping = { + "email" = "email" + "given_name" = "given_name" + "username" = "sub" } + id = (known after apply) + provider_name = "for-Customer-A" + provider_type = "OIDC" + user_pool_id = "ap-northeast-1_xxxx" ... } ... Plan: 1 to add, 1 to change, 0 to destroy. 変更内容に問題ないことを確認してPull Requestのレビューを受けた後、マージすることでリリース( terraform apply )も自動実行されます。 リリースの実行確認後、client_secretをマネジメントコンソールから手動修正、そして前述のステップ3.「MiiTelで発行した認可リダイレクトURLをお客様に伝達&IdPに登録」して頂ければSSOを利用開始することができます。 まとめ MiiTelにおけるSSOをご利用頂くにあたる開発チームとしての運用をご紹介しました。 本記事で紹介した各ステップは、作り込み次第では手順をより少なくできたり完全自動化するような方法もあるかと思います。 しかし本機能の利用シーンは1か月あたり数件程度であり、過度な作り込みはせずまずは属人化を防ぎつつリードタイムの少ない方式を確立することを優先し、現在の運用に至っています。
This article is the English version of Yuta Takase's blog post . Table of contents Table of contents 1. Introduction 2. What is YOLO? 3. YOLOv8 4. YOLOv8 model size 5. Changes with YOLOv5 6. YOLOv8's building and inference execution Inference on images Inference on videos Supplement Supported video formats Text output of detection results Model inputs 7. Overview of the inference results for each model 8. Summary References 1. Introduction Hello, I am Takase, a Research Engineer at the RevComm Research division. In this blog post, I will introduce YOLOv8, the famous object detection framework released by Ultralytics in early January 2023. I will also compare the changes with previous versions and show how to run it. 2. What is YOLO? As many people might know, YOLO is an object detection method proposed in the paper You Only Look Once: Unified, Real-Time Object Detection presented by Joseph Redmon et al. at CVPR2016. The name YOLO stands for You Only Look Once. YOLO improves the slow processing speed of object detection methods by simultaneously detecting and identifying objects. This feature made it a pioneer in real-time object detection with breakneck inference speed. This method is a must-see among the various techniques introduced to date for anyone interested in the topic. This article does not provide a detailed explanation of YOLOv8 – there are already superb explanatory materials on the internet. 3. YOLOv8 YOLOv8 is the latest version of the YOLO model released by Ultralytics, the publisher of YOLOv5. YOLOv8 allows object detection, segmentation, classification tasks, and training on large data sets. It can run on a variety of hardware, including CPUs and GPUs. It also features a new backbone, loss function, anchor-free detection head, and backward compatibility, allowing us to switch between different versions and compare performance. Currently, v3, v5, and v8 configurations are available in the official repository . As of this writing, the YOLOv8 paper is yet to be published. 4. YOLOv8 model size YOLOv8 has five pre-trained model patterns with different model sizes: n, s, m, l, and x. The number of parameters and COCO mAP (accuracy) below show that the accuracy has improved considerably from YOLOv5. In particular, for the larger model sizes l and x, the accuracy improved while the number of parameters was reduced [ reference ]. The table below shows the other models' parameters [ reference ]. 5. Changes with YOLOv5 YOLOv8 has several changes from YOLOv5, but two stand out from the rest at the time of this blog post: Introduction of C2f layer Decoupled head and removal of the objectness branch. In addition, some conv modules have been removed; kernel size has been changed, among other minor improvements. Although not official, a diagram of the YOLOv8 detection model architecture published by RangeKing is a helpful guide. YOLOv8 detection model's architecture by RangeKing [ Reference ] 6. YOLOv8's building and inference execution In this chapter, I will install and play some examples in YOLOv8. This time, I will use a pre-trained model to verify how well it works in a M1 MacBook Pro CPU environment. My environment is as follows: MacBook Pro (13-inch, M1, 2020) 16GB Mac OS Monterey First, you can install YOLOv8 with the command below. pip install ultralytics Now that the installation is complete, let's run it. This time, we will try the inference with a Python script. Inference on images First, let's enter the input. This article will use an image showing a bus and a person . The image size is 810x1080. from ultralytics import YOLO # load pre-trained model. model: YOLO = YOLO(model= "yolov8n.pt" ) # inference # set save=True to store the resulting image with the inferred results. result: list = model.predict( "https://ultralytics.com/images/bus.jpg" , save= True ) YOLOv8n's inference results Inference on videos Let's try inference on videos with YOLOv8x , since we used YOLOv8n for images. The interface is the same, but we pass the path of the video file as an argument. from ultralytics import YOLO # load pre-trained model. model: YOLO = YOLO(model= "yolov8x.pt" ) # set save=True to store the inferred results. result: list = model.predict( "MOT17-14-FRCNN-raw.mp4" , save= True ) This time, we will use the video from the MOT Challenge MOT17 test set . The MOT17 video file is in WebM format, so we converted it to mp4 in advance. The converted video is 30 seconds long, and the frame rate is 30, so inference is performed on 900 images. YOLOv8x's inference results Supplement Supported video formats The image and video file formats supported by YOLOv8 are as follows. Images: "bmp", "dng", "jpeg", "jpg", "mpo", "png", "tif", "tiff", "webp", "pfm" Video: "asf", "avi", "gif", "m4v", "mkv", "mov", "mp4", "mpeg", "mpg", "ts", "wmv" Definitions of supported file formats in the code are here . Text output of detection results When performing inference, saving the detection results as text is often desirable. # Output an image with the detection results drawn and in text model.predict( "https://ultralytics.com/images/bus.jpg" , save= True , save_txt= True ) # Output the detection result and the confidence of each object in the text model.predict( "https://ultralytics.com/images/bus.jpg" , save_txt= True , save_conf= True ) Other arguments can be set to the prediction function. Please refer to the official documentation . Model inputs Although we have specified a single video path to check the operation, you can pass multiple ones as a list. source_list: list = [ "./sample1.jpg" , "./sample2.jpg" ] result: list = model.predict(source_list, save= True ) 7. Overview of the inference results for each model Now that we have built an environment and performed inference let's try it on the YOLOv8 s to l models. In addition to YOLOv5, we will also compare the output results of YOLOv6 and YOLOv7, released within the past year. This time, we want to compare the accuracy of each model rather than the output of each model from a bird's-eye view. I will not go into the details of YOLOv5, YOLOv6, and YOLOv7, but if you are interested in these models, please check them out. YOLOv6 (v3.0) is used as the version for YOLOv6 since it was released almost simultaneously with YOLOv8. YOLOv8's sample images are used as the target images. The following is a summary of the images depicting the detection results of each model, information on the detected objects, and the inference speed. YOLOv8 YOLOv7 YOLOv6 (3.0) YOLOv5 YOLOv8l and x can detect the bicycle in the upper right corner of the image, which was not seen by YOLOv5l and x. The confidence of the detected objects in YOLOv8 shows that it has improved over YOLOv5. All models are fast, although YOLOv7 is slower. 8. Summary This article has provided an overview of YOLOv8, the differences from previous versions, and a brief description of how to use it. My impressions of YOLOv8 are: It is easy to install (run pip install ), easy to use, and has a well-organized interface. It is simple to convert to onnx, torchscript, etc. YOLOv5 was also user-friendly, but v8 has improved further in that regard. Although I have yet to mention model export , it is a valuable tool for exporting models in various formats and executing them efficiently. References 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/
RevComm Research の加藤集平です。8月下旬に音声処理のトップカンファレンスである INTERSPEECH で発表するため、また引き続いて行われた ISCA Speech Synthesis Workshop (SSW) に参加するためにヨーロッパに出張をしてきました。今回の記事では、INTERSPEECH, SSWおよび私の発表について紹介いたします。 加藤集平(かとう しゅうへい) シニアリサーチエンジニア。RevCommには2019年にジョインし、音声処理を中心とした研究開発を担当。ADHDと付き合いつつ業務に取り組む2児の父。 個人ウェブサイト X → 過去記事一覧 INTERSPEECH 会議の概要 International Speech Communication Association (ISCA) が主催する国際会議で、音声処理分野を専門に扱う国際会議としては最大級の規模です。2004年にそれまで行われていた2つの国際会議European Conference on Speech Communication and Technology (EUROSPEECH) とInternational Conference on Spoken Language Processing (ICSLP) を正式に統合した国際会議としてINTERSPEECHが開催され、以降毎年開催されています。 開催期間 2023年8月20日〜24日(チュートリアル1日+発表4日) 開催地 The Convention Centre Dublin、ダブリン(アイルランド) 対象分野 音声処理・音声コミュニケーション全般(音声認識・音声合成・音声変換・音声翻訳・音声符号化・音声対話システム・音声知覚・音声生成など) 発表件数 1,097件(採択率49.7%) 参加人数 およそ2,000人 会場の様子 4日間で1,000件を超える発表を行うため、7つのオーラルセッション(口頭発表)と4つのポスターセッションが並行しての進行でした。 8月22日のプログラム( INTERSPEECH2023のウェブサイトより引用 ) 会場の大きさは様々でしたが、どの会場も多くの参加者で賑わっていました。 口頭発表会場の例(写真は比較的小さな会場) ポスターセッションの会場 私の発表について 題目(リンク先は論文のアーカイブです) Speech-to-Face Conversion Using Denoising Diffusion Probabilistic Models (ノイズ除去拡散確率モデルを用いた音声から顔への変換) 本研究の主な貢献 Speech-to-face(音声を入力として、それに適した顔画像を出力するタスク)において、初めて拡散モデル(ノイズ除去拡散確率モデル)を導入しました。その結果、よりシンプルかつ柔軟なシステムで、これまでの研究よりも高解像度 (512×512) の顔画像を生成することに成功しました。 ポスター資料 いただいた質問 2時間の発表時間中ほぼ途切れることなく来客があり、以下のような質問をいただきました。 どのような応用先を考えているか? 将来的に、コールセンター等での応用を考えている。カスタマーサポート(特にクレーム対応)などの顔を出せないような場面で、声に適した顔を生成することでコミュニケーションを円滑にするといったことが考えられる。 顔画像の生成にはどの程度の時間がかかるか? GPUを使って1枚あたり数分かかる。ノイズ除去のステップ数が多いためであり、ステップ数を削減するための技術を応用することで短縮が可能であると考えている。 データセットはバイアスを内在しているのではないか?内在しているとすれば、何か対処をしているか? 今回モデルの訓練および評価に使用したデータセット ( AVSpeech および FFHQ Dataset ) は多様な人々や言語を対象として集められたものであるが、それでも見た目や性別等のバイアスは存在する。今回は特に対処していないが、実用化においては重要な問題だと考えている。 ISCA Speech Synthesis Workshop (SSW) 会議の概要 INTERSPEECHのサテライトワークショップ(付随して開催される小さな会議)として、 ISCA Speech Synthesis Special Interest Group (SynSIG) によって開催されているものです。音声合成を専門に取り扱っており、1990年から3年おき、2019年からは隔年で開催されています。 開催期間 2023年8月26日〜28日(INTERSPEECH閉幕の2日後から3日間) 29日に同じ会場でBlizzard Challengeが開催 開催地 グルノーブル・アルプ大学、グルノーブル(フランス) 対象分野 音声合成・音声変換 発表件数 通常発表(査読あり)36件(採択率82.2%) Late Breaking Reports(査読なし)7件 参加人数 110人 会場の様子 発表件数も参加者も小規模であり、すべての発表はシングルトラックで進行されました。 口頭発表会場 ポスターセッションの会場 INTERSPEECHとの違い SSWは音声合成専門のワークショップということで参加人数が小規模でした(110人)。コーヒーブレイクだけでなく昼食も会場内で提供され、休憩時間にもお互い気軽に話しかけられる雰囲気でした。全員が音声合成を専門としているので、話も盛り上がります。 1日目の終わりに開催された懇親会の様子 ポスターセッションも枚数が少ないため(1時間半の枠で12枚)、すべてのポスターを見て回ることができました。INTERSPEECHを含む昨今のトップカンファレンスでは発表件数が非常に多いためにすべての発表を見て回るのは不可能ですが、SSWのように分野を絞って開催されるワークショップは違った雰囲気を楽しむことができました。 Blizzard Challenge SSWとしてのプログラムは8月26日〜28日の開催でしたが、引き続いて29日には同じ会場で音声合成の競技会である Blizzard Challenge の発表が行われました。Blizzard ChallengeはSSWと同じくSynSIGが主催するイベントで、所定の期間内に、与えられたデータセットを利用して音声合成器を作成し、その品質を競う競技会です。音声合成の技術発展を目的として2005年から毎年開催されており(2022年のみ不実施)、今年の課題はBlizzard Challengeでは初めてとなるフランス語の音声合成でした。 冒頭、今年のBlizzard Challengeの概要と結果の発表が主催者からありました。今年の参加者は18チームで、各チームが提出した音声を元に主催者が聴取実験(音声を多数の人間が聞いて評価する)を実施し、その品質が競われました。品質(人間の音声らしいかどうか)は722人の評価者により、1から5の5段階で評価されました。 結果としては、自然音声(人間の音声)とほとんど同等であるという評価を受けたチームから、自然音声とかなり差があると評価されたチームまで、バラツキがありました。 評価結果の説明 主催者による概要と結果の発表に引き続き、各チームから手法の説明が口頭発表の形式で行われました。上位を獲得したチームの発表にはやはり注目が集まりましたが、あえてユニークな手法で挑戦したチームもあり、成功例・失敗例とも大いに参考になるものでした。 会の最後には、次回のBlizzard Challengeの課題の参考とするために、参加者にアンケートが取られました。実は音声合成の分野では、ここ数年の急速な技術革新により、単一話者・単一言語(特に英語などよく研究されている言語)の原稿を読み上げた音声(読み上げ音声)については、十分な訓練データがあれば自然音声(人間の音声)とほとんど同等の品質が達成できる状況になっています。Blizzard Challengeは音声合成の技術発展を目的としたイベントですから、音声合成コミュニティーとして目指すべき次の方向性を探るべくアンケートが取られたわけです。 アンケートの結果としては、「低資源・ゼロショット」(訓練データが少ない、あるいは未知の話者や方言に対応する)、「(原稿を読み上げた音声との対比としての)自発的・会話的音声※」に多くの票が集まったようです。 ※記事執筆時点(2023年11月)では、音声合成モデルの訓練には一般的に「原稿を読み上げた音声(読み上げ音声)」が使われます。これに対して、原稿なしで発話された音声を録音したものを自発的音声 (spontaneous speech)、2人以上の会話を録音したものを会話的音声 (convasational speech) などと呼ぶことがあります。 アンケートの結果 まとめ 8月下旬に音声処理のトップカンファレンスであるINTERSPEECHで発表するため、また引き続いて行われたISCA Speech Synthesis Workshop (SSW) に参加するためにヨーロッパに出張をしてきました。2,000人の参加者で賑わったINTERSPEECH、110人の参加者でお互いの顔の見えたSSW(そしてBlizzard Challenge)、それぞれに違った楽しみがありました。 INTERSPEECHでの発表では多くの方と有意義な議論をすることができました。今回の発表もチーム内で議論してベストなものを発表しましたが、世界中の研究者と議論することで新たな課題や方向性が見つかるものだということを改めて実感する機会となりました。 RevComm Research では、今後も継続的に研究発表を行っていきます。一緒に働きたい方を募集しています。
This blog post is the work of Hongkai Li, edited by Tolmachev Arseny. The authors belong to Works Applications and are working for RevComm. TL;DR Background Evaluation Metrics Experiments Datasets and Systems Results Correlation Among the Metrics Micro Analysis: Relevance-Based Metrics Micro Analysis: Factual Consistency-Based Metrics Evaluating Factual Consistency of Reference Summaries Performance Differences by Dataset ChatGPT’s Behavior FactSumm’s Behavior Dialogue vs. Narrative . TL;DR We tried different summarization metrics with four datasets and summarization models. Our findings are: Relevance metrics give different results from factual consistency metrics. Some metrics, especially factual consistency metrics, perform differently between dialogue summarization and conversational summarization. It is essential to choose different metrics according to the purpose of the evaluation, especially from the industrial perspective. Many metrics are not easy to adapt to other languages, especially those that require fine-tuning models. Background During the development of summarization systems, we need to measure the performance of the system and compare it to different systems. We use various performance metrics for this. Probably, the most important direction to compare is the quality of summarization itself. In this blog article, we give a very brief overview of several summarization evaluation metrics and our experiments on evaluating those metrics with a focus on summarizing dialogues. We investigated various existing summarization evaluation metrics, focusing on relevance and factual consistency, across several systems and datasets by comparing the scores and correlations between each other. Here, we would like to share our results and findings. Document summarization is the process of reducing the size of a document while keeping the main concepts. ROUGE and BLEU are widely used as evaluation metrics for the summarization task. ROUGE and BLEU do not capture paraphrased expressions (same meaning but different words). Several scores have been proposed in recent years to address this issue. Most proposed scores are word-based methods such as BERTScore . However, other methods have been proposed for the past three years. For example, there are evaluation methods using information extraction and question-answering . Evaluation Metrics There are a lot of aspects for evaluating a summary, such as coherence, consistency, fluency, and relevance, as suggested in this article and this article . Most works focus on relevance or/and (factual) consistency. Relevance measures how well the summary captures the key points of the source document. It focuses on whether all and only the important aspects are contained in the summary. The key points are usually included in reference summaries. So, relevance scores are usually calculated with system summary and reference summary pairs. Factual consistency is defined as the factual alignment between the summary and the summarized source. A factually consistent summary contains only statements that are entailed by the source document. So, factual consistency scores are usually calculated with system summary and source text pairs. The example below describes the two very well. The summary in the first row fails in relevance, and the second has a factual error. Figure 1. Examples of relevance and factual consistency errors ( source ) Figure 2. Difference between relevance and factual consistency metrics ( source ) For years most papers have been using ROUGE to measure summaries. Recently, especially since 2019, researchers have focused on factual consistency of summarization. LLMs such as ChatGPT further contributed to this trend because there are often hallucinations in their outputs. So, we focused on both aspects and chose the existing evaluation metrics listed below. A very brief description of those metrics is provided in Figure 2. Table 1. Relevance metrics Metric Japanese? TL;DR ROUGEcode Perfect Exact n-gram overlap (ROUGE-n) or longest common sequence (ROUGE-L) between 2 texts (e.g. summary and reference) BERTScorecode Yes, needs BERT model Calculate similarities between 2 sentences and get recall and precision scores using contextual embeddings from a pre-trained BERT model. MoverScorecode Yes, needs BERT model Extension of BERTScore that uses hard alignments between sentences. With Word Mover Distance (WMD) developed from Earth Mover Distance (EMD), MoverScore finds the minimum effort to transform a text to the other to get soft alignments. BARTScorecode Maybe, needs fine-tuning Weighted log probability of one text given another text Table 2. Factual consistency metrics Metric Japanese? TL;DR OpenIE *code Maybe. needs a Japanese entity relationship extractor Extracts triples from both the summary and source document and evaluate whether the triples of the summary are included in those of the source document FactCCcode Maybe. Needs data or data generation scripts. FactCC: A classifier to judge whether a summary is factually consistent with the source document.FactCCX: I think the X means explanation, meaning this model is able to explain why the summary is wrong. DAEcode Maybe, needs Japanese dependency parsing and model training. Dependency Arc Entailment. DAE views dependency arcs as semantic units and each arc is judged independently based on whether the relation it implies is entailed by the source sentence. QAGS *code Maybe, needs Japanese QA and QG models. QAGS is based on the intuition that if we ask questions about a summary and its source, we will receive similar answers if the summary is factually consistent with the source. CoCocode Maybe, needs BART CoCo selects key tokens in summary and masks the tokens in the source document. The masked source document is then fed to the scoring model. If the scoring model is still able to generate the masked token with a high possibility, it means that the token is more likely to come from the scoring model instead of the source document. Therefore a penalty should be added. SummaCcode Maybe, needs training/fine-tuning Sentence level alignment. FactGraphcode Maybe, needs Japanese AMR FactGraph uses abstract meaning representation to form the graph of a sentence and then uses the model above to give the final score. * For OpenIE and QAGS, we use the implementation from a toolkit called FactSumm . Experiments Datasets and Systems We conducted experiments on the following datasets and systems. The aim of the experiments is to Investigate the advantages and disadvantages of each metric Compare the results between relevance and factual consistency-focused metrics We used the following datasets for the experiments. Samsum dataset contains instant messenger-like conversations with summaries. Conversations were created and written down by linguists fluent in English. CNN / DailyMail Dataset (CNNDM) is an English-language dataset containing just over 300k unique news articles written by journalists at CNN and the Daily Mail. XSUM consists of online articles from the British Broadcasting Corporation (BBC) and single-sentence summaries. Specifically, each article is prefaced with an introductory sentence (aka summary), which is professionally written, typically by the article's author. DialogSum consists of three public dialogue corpora, as well as an English speaking practice website. These datasets contain face-to-face spoken dialogues that cover a wide range of daily-life topics, including schooling, work, medication, shopping, leisure, and travel. Table 3. Dataset summary   DialogSum Samsum CNNDM XSUM Num. of documents in the train dataset 12,460 14,732 287,113 204,045 Num. of documents in the test dataset 1,500 819 11,490 11,334 Num. of tokens in a source document 210 157 868 487 Num. of tokens in a summary 35 26 66 26 Compression Rate 17.80% 21.40% 9.20% 9.50% Table 4. Scope of experiment; ”o” means the target Dataset\System ChatGPT GPT3 Flan-T5 T5 Samsum ○ ○ ○ ○ CNNDM ○ - ○ ○ XSUM ○ - ○ ○ DialogSum ○ - ○ ○ For Flan-T5 and T5, we either used existing fine-tuned models or fine-tuned flan-t5-large or t5-large with the datasets by ourselves. The models used for the Samsum dataset are: philschmid/flan-t5-base-samsum jaynlp/ t5-large-samsum Results Correlation Among the Metrics Figure 3. Heatmap of Pearson correlation among the metrics Figure 3 shows the Pearson correlation among the metrics. Metrics from rouge1 to bart_hypo_ref are categorized as relevance metrics, and metrics from dae to factcc are factual consistency metrics. We can clearly see that relevance metrics correlate with each other while having a low correlation with factual consistency metrics, except the bart_hypo_ref score. Factual consistency metrics only correlate with factual consistency metrics as well. For relevance metrics, some metrics have precision, recall, and F1 scores. We can find recall scores ( bert-r-r , bart_hypo_ref , etc.) have a relatively low correlation with precision scores ( bert-r-p , bart_ref_hypo , etc.). In most cases, the ROUGE scores seem enough to reflect the performance at the system level. The low correlation between retrieval and factual metrics can be explained because they focus on different aspects. Sometimes, a summary may get a high relevance score because it is extremely similar to the reference summary with only a 1-word difference. However, this word is very important and changes the whole meaning of the summary. Factual consistency metrics are able to capture this kind of error. Additionally, some summarizers may output more information that is not included in the reference summary, but according to the source text, the information is true and consistent. This will also contribute to the low correlation. See the example below. Source Dialogue Rudi: Hetta, did you see the last trump video Henrietta: nope Henrietta: what did he do now? Rudi: <file_video> Henrietta: OMG Henrietta: what a jerk Rudi: it gets worse Rudi: <file_other> Rudi: the whole interview is here Henrietta: can't believe he said that about a congress woman Rudi: yeah Henrietta: do you wonder where the limit is? Rudi: wdym Henrietta: if he will say something that will actually get him kicked out of the white house Rudi: not really Henrietta: fuck Rudi: yeah Reference Summary Trump is acting like a contemptible fool and it is getting worse. Rudi has sent Henrietta the link to his interview. System Summary Rudi and Henrietta discuss a video of Trump insulting a congresswoman, and wonder if he will say something that will get him kicked out of the White House. ROUGE LSUM 0.1632653061 FactCC 1 CoCo 0.441158 Here, the system summary includes more facts (e.g. White House) than the reference one, so it gets a better FactCC score than the reference one. Micro Analysis: Relevance-Based Metrics As mentioned above, the bart_hypo_ref score correlates with neither other relevance metrics nor factual consistency metrics. It is defined as recall, which is the probability of generating the reference summary from the system summary, according to the BARTScore paper. So, we can find it relatively correlates with   bert-r-r , bert-d-r , and bart_cnn_hypo_ref . By investigating and comparing the scores of each sample of different systems, we found that for example, the range of the bart_hypo_ref scores of Flan-T5 with Samsum was (-13, -0.7), while that of ChatGPT was (-8.6, -0.8). As a result, the average score of ChatGPT was higher than Flan-T5, even though Flan-T5 outperformed ChatGPT in other metrics. BARTScore seems very risky because it does not have a specific range. Micro Analysis: Factual Consistency-Based Metrics When investigating the scores from DAE, we found that more than 90% of the samples scored more than 0.90 (90%). Take the sample from Samsum below, for example: The T5 output had only one thing wrong: the neighbors are only Ricky’s new neighbors but not Frederick and Ricky’s. Since dae is based on dependency arcs, it is relatively easy to understand that most arcs match the source text, so the score was extremely high. On the other hand, factcc is a binary classifier. If the sentence contains any wrong information, the whole sentence will be false. And since the T5 output only had one sentence, the final score was 0. The factsumm-qa  is based on facts (QA pairs) extracted from the source text and the system summary and can recognize that some parts of the sentence are true while some are false. This remains a problem whether we should allow partially correct summaries or reject summaries containing any false information ( factcc  vs. factsumm-qa ). Evaluating Factual Consistency of Reference Summaries Factual consistency-based metrics calculate scores with the source document and system summary pairs, while relevance-based metrics use system and reference summary pairs. We are pretty curious about whether reference summaries can get high scores in factual consistency-based metrics, so we fed some of the evaluators with source documents and reference summary pairs of Samsum corpus. Here are the results. Metrics Ref ChatGPT GPT-3 Flan-T5 T5 FactSumm-src-fact 0.0122 0.0147 0.0119 0.0142 0.0159 FactSumm-src-qa 0.2786 0.2731 0.2644 0.2963 0.3186 FactSumm-src-triple 0.0348 0.0514 0.0428 0.0739 0.1096 FactCC 22.1 21.25 27.11 21.37 18.93 DAE 95.16 93.78 91.82 93.4 94.71 CoCo 24.64 27.7 24.82 30.61 34.55 Red  = Worst, Blue  = Best Interestingly, reference summaries even got the worst scores in some of the metrics. It may be because the evaluators failed to extract facts from the source document or summary. From this perspective, DAE may be the best or most convincing evaluation metric. Performance Differences by Dataset We used four datasets with different properties for our experiments and observed that systems performed differently among the datasets. ChatGPT’s Behavior From our observation, ChatGPT tends to output more information than needed to make perfect answers. Regarding summarization, it tends to output longer summaries than other systems. This results in 2 main consequences: ChatGPT got relatively higher recall scores, such as the hypo-ref direction of BARTScore, which is defined as ‘recall’ in BARTScore’s paper, and recall scores of BERTScore. And conversely, the precision scores, such as the ref-hypo direction of BARTScore are extremely low. ChatGPT got higher factual consistency scores in certain metrics with some of the datasets. Since ChatGPT tends to output more information, and considering ChatGPT’s ability to extract information from the input, the extra information is also consistent with the source text, making factual consistency scores relatively high. This is especially obvious for the XSUM dataset because each reference for each document is only one sentence. Flan-t5 and t5 models have been fine-tuned with XSUM’s training data, so the two models only produce one-line summaries as well. On the other hand, summaries from ChatGPT always include several sentences, so ChatGPT got relatively high scores in recall and extremely high scores in factual consistency, while other relevance scores were very low. FactSumm’s Behavior We have also observed that some evaluation metrics performed differently among the datasets. Take FactSumm, for example. We got extremely low scores with all systems among all datasets except CNNDM. We consider it was because FactSumm extracts information from source texts and summaries to compare the pairs. Since both source texts and summaries of CNNDM are relatively long and formal, FactSumm can extract more facts from both. When we investigated the results of FactSumm for other datasets, we found that many summaries were scored 0 because FactSumm failed to extract any entity from the data. We consider this as a big disadvantage of FactSumm (or OpenIE, to be precise). Dialogue vs. Narrative From our observation, information extraction-based metrics such as OpenIE or DAE failed to extract important information because the person name of the speaker is in the front of their lines. Abstractive summarizers can summarize the lines with the correct speaker name, but extractive methods sometimes fail to recognize that, resulting in low scores. Below is a simple example of this phenomenon by FactSumm (OpenIE, QAGS): Input Input Source Text Jack killed John. Jack: I killed John. (*3) Input Summary Text Jack killed John. factsumm-fact (*1) Facts None Fact Score 0 factsumm-qa Answers based on SOURCE [Q] Who killed John? [Pred] Jack [Q] Who did Jack kill? [Pred] John [Q] Who killed John? [Pred] <unanswerable> [Q] Who did Jack kill? [Pred] John Answers based on SUMMARY [Q] Who killed John? [Pred] Jack [Q] Who did Jack kill? [Pred] John QAGS Score 1 0.5 factsumm-triple SOURCE Triples ('Jack', 'killed', 'John') None SUMMARY Triples ('Jack', 'killed', 'John') None (*2) Triple Score 1 0 *1. For factsumm-fact, since both samples got the same results, the details are omitted. *2. FactSumm only outputs common triples of source and summary. *3. When we changed the input source text to ‘Jack said that he killed John.’, the results were the same as ‘Jack: I killed John.’ This is considered to be able to be avoided by fine-tuning with dialogue data.
Introduction Hi! My name is Jose, a software engineer working for RevComm. In this blog post, I'll discuss four takeaways from my first PyCon APAC and the most interesting talks I attended. What is PyCon APAC? PyCon APAC is a non-profit annual Python conference organized in countries of the Asia-Pacific Region. The 2023 edition in Tokyo consisted of four days: one tutorial, two conferences, and one development sprint. Lessons 1. Plan ahead Every PyCon exhibits the most varied collection of talks, from obscure tricks of the language to the technical marvels of frameworks. This year, there were more than fifty presentations in English and Japanese across five tracks. To get the best out of them, prepare an agenda. Read each talk's description; sometimes, the title is enough, and choose where you are going. Creating a calendar for the event in iCal or gCal is especially useful for coordinating with friends. Just so you know, the schedule above took me around forty-five minutes. 2. Take time for the sponsor booths My first major mistake was forgetting about the sponsor booths. I skipped the last two tracks of Day 2 to go around, and It was worth it! Check them out. They are a fantastic reference for the country's Python market – what different companies are doing, their stack, and who is hiring. You might be as surprised as I was to find unexpected sponsors. The stamp rally was a great motivator, too. 3. Talk to people Any major conference is an opportunity to engage with the community. Talk to people. I met so many charming characters and learned a considerable amount. If you don't have time on the conference days, the official and unofficial parties are ideal for networking. 4. Join the developer sprint As the famous proverb says: After two days of conference and drinking parties, the developer sprint sounds like a stretch. It’s not. You will be surprised how productive you are in a couple of hours. For instance, my team merged a PR to the cPython's official repo! macOS CI for CPython now supports `free-threaded` mode CI as the conditional CI. It was done during the PyCon APAC sprint. https://t.co/rrcRxtkw7K #pyconapac pic.twitter.com/7pULkgP9By — Donghee Na (@dongheena92) October 29, 2023 Coding along with people you've just met is as open-source as it gets. Talks I attended eight talks, seven in English and one in Japanese. For brevity, I'd like to discuss the ones who impressed me the most. Write Python for Speed by Yung-Yu Chen https://2023-apac.pycon.jp/timetable?id=WNN9WG www.youtube.com This talk goes deep into optimizations for speeding up your Python programs. Although the field of application was outside my current job, Chen's passion and wit made me follow it until the end. I loved some of his quotes: "Python is the second-best language for everything." "To go fast, you do dangerous things." Couldn't agree more. Beaming up to the flow! by Thu Ya Kyaw https://2023-apac.pycon.jp/timetable?id=KKELHA www.youtube.com An enlightening talk about data streaming, the basics of Apache Beam, and the feature engineering process at Google. The expositor was also present at the Google Cloud booth, so I got to ask many questions. Debugging and Troubleshooting Python Applications by Neeraj Pandey https://2023-apac.pycon.jp/timetable?id=C7HGZS www.youtube.com This talk deepens into logging, profiling, and debugging in monolithic and distributed systems. I was blown away by OpenTelemetry. What does your application need to run on production? by Shota Kokado https://2023-apac.pycon.jp/timetable?id=FHTQDR www.youtube.com After years of working in Software Engineering, this talk was a good refresher on all the basics any production-ready application should have. The talk is in Japanese, but you can extract the vital information from the slides. So for next year… Indonesia will host next year's PyCon APAC. We also have the annual PyCon JP. And many more! Check pycon asia's website for more information. Special thanks Huge thanks to RevComm for supporting me throughout the event, to my colleagues whom I went with, to the more than fifty members of the APAC organizing committee, and, of course, to all the new people I met. Each of them made the conference for me. You can follow me at twitter ( @juanjo12x ). See you next year! References PyCon APAC 2023 official website : https://2023-apac.pycon.jp/timetable Check the official PyCon JP’s YouTube channel for all the conference talks : https://www.youtube.com/@PyConJP/featured