💬

授業で地域の特徴を付与した対話システムのチーム開発を行った話

2023/02/28に公開

2023/02/28

はじめに

大学3回生後期にチーム開発を行う授業があったのでその話をしようと思います.来年のB3とか対話システム作る人の役に立てば良いかなと思っています.

テーマ決め

テーマは大まかに分けて画像認識,センシング,自然言語処理の3テーマがあったのですが,自分は自然言語処理をやっている人工知能研究室に所属しているので,取り敢えず自然言語処理のテーマを選びました.

その中でさらに機械翻訳と対話システムという2種類のテーマがあったのですが,機械とおしゃべりできたらめっちゃ面白そうじゃんというモチベで対話システムを選びました.結果地獄みたいな作業量でしたが,最後はそこそこ上手くいったのとやっぱり機械とおしゃべりするの新鮮でちょっと面白かったので特に後悔はしていません.

チームメンバー

チームメンバーは,私含め人工知能研究室に所属している学生が2人と,その他の研究室に所属している学生が2人の計4人です.最初に発生した問題は班長を決めることでしたが,案の定誰もやりたくなさそうだったのとなんか面白そうだからという理由で班長になりました.

折角最初なのでアイスブレイクをしようと思って,みんなの出身と好きなものやことを聞いて共通点を見つけてNotionのアイコンにすれば良いやって思ったのですが,思ってた以上に共通点がなくて結局無難なゲームということに落ち着きました.

アイスブレイクについて

ちなみになんでこのアイスブレイクをしようと思ったかというと,少し前に就活をしていた時に企業説明会で,チームの共通点を見つけてそれをチーム名にしようというアイスブレイクをしたので自分もやってみようと思ったのですが,思ったより難航したので共通点がなかった時になんて言い訳するか考えてからやった方が良いと思います.

他のアイスブレイクだったらサイコロトークとかすれば楽しい(?)とは思うのですが,今回は最初に遠隔とか対面とかで集まるとかもなくテキスト上で行なったので,その時点でそもそもアイスのブレイクしようがなかったのかなぁという印象でした.

使用ツール

進捗管理はNotionで,コード管理はGitHubで,メンバーとのやりとりはTeamsで行いました.

1. Notionでの進捗管理について

毎週MTGの前までに各班員にNotionに進捗報告のページを作ってまとめる&個人の進捗報告のページとは別に班の全体進捗のところにも同じ内容を追記しておくという作業をお願いしていたのですが,なぜか途中から誰も全体進捗のところに追記しなくなっていって,最終的に各班員の進捗報告ページを見て全体進捗報告のページにまとめるのが班長の仕事になっていきました.

全体進捗のところに書いておいてくれないと,各個人進捗が最終版なのかがわからない状態で全体進捗に追記することになるので,まぁ面倒は面倒でしたが百歩譲って私が追記するのは良いとして,最終更新かだけわかるようにNotionの機能で状態タグみたいなのを班員に作ってもらって,未完了とか最終更新とかの状態がわかるようにしました.

2. GitHubでのコード管理について

コードの管理はGitHubでしていたのですが,ディレクトリ構造とか何も考えずにファイルを追加していったせいで最後リファクタリングするときに地獄でしたというかまだ終わってないので現在進行形で地獄です.あと,READMEに使い方を書いているのですが,これもコードができた段階でちょこちょこ書いておけば記憶のあるうちに説明書けるし,こんなに大変じゃなかったのになぁと感じています.

それとプルリクエストの承認についてですが,これは全て私がコードテストをして承認していました.理想はプルリクエストを出した人以外のチームメンバーが全員確認してOKとなったら承認とするのが良いとは思いますが,そんなに手間暇かけてられないし全員が全員ちゃんとテストしてくれるとも限らないのでこれは代表者が一人確認して承認すれば良いと思います.

また,ブランチは人ごとではなく作業内容ごとに分けて作ります.

3. Teamsでのチャットについて

チームでのやりとりはTeams上で行いました.確かTeamsでやりとりするように先生に言われたからTeamsを使っていたような気がするのですが,チーム開発など頻繁にやり取りを行う際はSlackを用いた方が適しているのかなという印象でした.

というのも,Temasのチャットは一つの投稿文が長いと詳細を表示するに吸収されて折りたたまれてしまって全文が見えないことがあって,一々「詳細を表示する」ボタンを押さないと表示されないのでそこはちょっと不便に感じました.

また,投稿のみの時系列順ではなく,返信も含めた時系列順で投稿が並び変わるので,あの話は確かこの辺でしただろうみたいに見当をつけて投稿を遡っても見つからないことがあって,なんでこの投稿がこんなところにあるんだと思っていたら返信が最新の投稿が一番下に来る仕組みのようでした.こっちのシステムの方が嬉しい場合もあるとは思うのですが,今回は投稿の時系列順に並んでいてもらった方が嬉しいなと感じたのでSlackが適しているのかなと思いました.

あと,一々遡らなくても良いように重要な投稿をピン留めしたことがあったのですが,表示が目立っただけでどこにもピン留めされていなくて(もしかしたら私が見つけられていなかっただけかもしれませんが),結局重要な投稿は上の方に埋もれていってしまいました.

ただ,MTGだけはSlackでやったことがないので比較できませんでしたが,これはTeamsで良いと思います.Slackは分かりませんが,Zoomとかと違ってTeamsではMTG中で行なったチャットがTeams上に残って後から確認できるので,MTGのチャットを後から見返したい場合に便利だなという印象でした.

以上のことから,班員同士で頻繁にやり取りする場合はSlackの方が便利かなと思いましたが,代表者が一方的に班員に連絡するだけみたいな場合はMTGも合わせて全部Teams上で行ったらツールをまたがなくてやりやすいのではないかなと思いました.

地域の特徴を付与した対話システムの作成にあたって

今回のチーム開発では,地域の特徴を付与した対話システムの作成を行うことになったので,地域の特徴を付与した対話システムとは何かということと,それが完成するまでに行ったことをまとめていきたいと思います.

1. 地域の特徴を付与した対話システムとは

まず,地域の特徴を付与した対話システムについて説明します.例えば,りんななどの普通の対話システムでは標準語でやり取りをしてくれますが,標準語のみだとちょっと味気ないので,これが地域の特徴を付与したもの,つまり方言などで対話してくれると面白いなということで地域の特徴を付与した対話システムの作成を行いました.

結論から言うとほとんど方言での対話はしてくれなかったのですが,色々な地域の中からこの地域で対話してと言う感じで色々指定してみると,地域ごとに応答がちょっとずつ変わっていて,結局方言で喋らせることは叶いませんでしたが,地域ごとに応答に違う特徴が現れたのでこれは広義の成功だろうと思っています.

具体的には,地域として<広島>を指定したときに,「明日テストなんよねー」と発話を入力すると,「テストたいぎいと思うけどまぁ勉強頑張りんちゃいや」みたいな感じで指定した地域に基づいた応答が返ってくるのをイメージしてもらえればと思います.

2. 事前準備

実際に対話システムの作成に取り掛かる前にやったことについて述べていきます.大まかには教科書を読む,対話システムに付与する特徴の決定,TwitterAPIの申請,データ収集・学習環境構築,中間報告資料の作成をしました.

教科書を読む

まず教科書についてですが,自然言語処理の基礎の6章までと,Pythonでつくる対話システムの3.4節を読めと先生から指示が出ていたので,これらを2週間で読みました.

対話システムに付与する特徴

対話システムに付与する特徴の決定としては,地域や性別,年齢,趣味,性格とか色々候補は出ていたのですが,地域が一番特徴出やすいかなと思って地域を選びました.あとついでに性別と,できれば性格とかでも特徴づけできたらいいなと思ってその3つを選びました.ちなみに性格に関しては実装し始めて2週間ぐらいでタスクの量的に無理そうだということが判明したので早々に諦めました.

TwitterAPIの申請

TwitterAPIの申請については,Pythonでつくる対話システムに従ってやっていたものの,確か同じようにはいかなかった気がしたので,普通に調べながらやった記憶が微かにあります.ただ,TwitterAPIは(なんか若干延長されたりしているみたいですが)2023年2月9日で無料サポートが停止すると発表されました.

データ収集・学習環境構築

データ収集・学習環境構築については,研究室の先輩に教えてもらったり調べたりしながらやりました.VSCodeのSSHFSというのを使えば簡単に研究室のサーバにリモート接続ができるようになるというのを教えてもらったり,今までGoogleColabぐらいしか使ったことがなかったのでサーバでの学習の仕方を学んだりと,色々教えてもらいました.中でも特に感動したのはscreenというコマンドで,これを使えば画面を閉じていても裏でずっとプログラムを実行し続けることができるというのを知り,本当に革命かと思いました.授業の実験コンペで使っていたGoogleColabで,学習中にランタイムが切れないように逐一画面の様子を確認してクリックしたりとかして面倒をみるしか方法がなかったあの頃の自分が聞いたらびっくりすると思います.

参考までに,身につけたscreen関係のコマンドを以下に書いておきます.

screen -S [付けたい名前]:screenの作成
screen -r [入りたいscreenの名前]:screenに入る
screen -ls:screenの一覧を見る
Ctrl+A D:screenから一旦出る
exit:screenを閉じて消す
Ctrl+A ESC:screen上でスクロールする

中間報告資料の作成

最後に中間報告資料の作成についてですが,これはパワーポイントでスライドを作りました.スライドを作る時に重要なこととして以下のことを学びました.

  • 聴講者はただでさえ聞くモチベがないので,難しいことは書かない
  • 1つの項目に対して1つのスライド内で完結させるようにする
  • 図は絶対入れる,なんなら図だけ見れば全部わかるぐらいの図を作る
  • 原稿はあまり詰めすぎず,ゆっくり発表して規定時間内に終わるくらいの内容にする

あとはもう一人の同じ研究室の子が見やすいレイアウトとか配色にするのが得意だったので,その子に監修してもらってより見やすいスライドができるようにしました.めっちゃ見やすくなったし助かりました.こういう自分にない能力を持っている人に助けてもらえるのは,チーム活動ならではの良いところだなぁと感じました.

3. 対話システムの作成

ここから,対話システムの作成にあたって作ったプログラムの概要などを説明していきます.

対話システムの大まかな構成

大まかな流れとしては,Twitterから発話応答ペアを収集する → 前処理を行う → モデルの学習を行う → 評価を行う → チャットボットへの接続を行う という感じです.

Twitterから発話応答ペアを収集する

これは,Pythonでつくる対話システムを読みながらtweepyとかをインストールしたり,コードを実行したりすれば簡単にできます.私たちの班は出身地の部分から地域の情報を取ってきたかったため,user.locationを使うことによって地域の情報を取ってきていました.

また今回は使用する地域を地方ごとの人口の多い都道府県,つまり,北海道,宮城,東京,愛知,大阪,広島,愛媛,福岡の8つに絞りました.これは,地方ごとに1県ずつ取ってくることによって地域の偏りをなくしたかったのと,人口の多い県を使用することによってTwitterデータが収集しやすくなるのではないかと考えたためです.

そして,これら8つの県の名前や都市名を正規表現で探し,取得した地域の情報と一致するものがあればその人のツイートを取ってくることによって学習データを得ました.ただしこの方法では欲しい地域をプロフィールとして設定している人からしかツイートを取得できないため非常に効率が悪く,地域情報のタグ付きデータを200万件収集するのに単純計算で6,666時間かかる計算となり,大量のデータを収集することは現実的ではありません.

そこで,あらかじめタグ無しデータを用意しておき,取得した文からその文はどの地域の人が書いたものなのかを判定する地域判定プログラムを先生に教えてもらいながら自分で作成しました.タグなしデータは確か6人ぐらいで手分けして1,2週間ぐらい収集した結果300万件ほど集まったので,一から集めるとしても圧倒的にタグ無しデータを集める方が早く,さらに地域タグを付与するだけであれば1分もあれば200万件全てにタグを付与することができるため,圧倒的効率化を図ることができました.以下のトグルにプログラムの概要などをまとめておきます.

自作地域判定プログラムの概要

仕組み

LocalPMIという,単語のレア度のようなものを算出する式を使って,各地域の特徴が高く現れている単語とそのレア度の数値を算出します.式は以下の通りです.

LocalPMI=n(x,y)×\log_2\frac{P(x,y)}{P(x)×P(y)}

ここで,xは地域,yは単語,P(・)は出現確率,n(・)は出現頻度です.

そして,算出された単語とそのレア度を用いて,SentencePieceで単語分割された文から地域度を算出します.算出方法としては,文中の単語の中で一番LocalPMIの値が高い単語を持っている地域を用いる最大法と,すべての単語から各地域のLocalPMIの合計を計算して平均をとり,その中で一番値の高かった地域を用いる平均法の2種類で地域の推定を行いました.

また,LocalPMIの他に,単語の出現頻度をかけていないただのPMIと,TFIDFを用いて同じように地域の推定を行いました.

結果

結果としては,LocalPMIと平均法を用いて付与したタグが一番精度が高かったです.以下に全体の結果の表を載せておきます.

TFIDF PMI LocalPMI
最大法正解率 0.140 0.079 0.383
平均法正解率 0.120 0.209 0.389

また,東京タグの文が最頻であるため,全てに東京タグを付与した場合の正解率も算出した結果,0.388であることがわかりました.そのため,LocalPMIと平均法を用いて地域を推定しタグを付与した場合,全て東京タグのものに対して正解率が0.001高いという結果になりました.

学び

このプログラムの作成を行うにあたって,与えるコマンドライン引数によって実行結果を変えることができるようにすることの重要性を学びました.最初はプログラムをコピペして一部だけ変えてすべての機能を作ろうとしていたのですが,これだと似たような機能のプログラムが余計に増えてしまってファイルもごちゃごちゃするし,実行するプログラムも一々変更しなければならないので間違えやすくなってしまうというリスクがあります.

そこで,コマンドライン引数で実行する関数を変えるようにすれば,ファイルもすっきりするし間違わないしあとかっこいいしで良いことずくめだなと感じました.今回だと例えば以下のような感じで指定しました.

[プログラム名] ave localpmi [ファイル名]

これで平均法とLocalPMIを用いてタグが付与されたファイルが出力されます.

コマンドライン引数を取得するには,sys.argv[1]のような感じで記述します.これは1番目のコマンドライン引数を取得する記述です.

前処理

次に前処理について説明します.Twitterデータには絵文字や顔文字が多く含まれていますが,これらの絵文字や顔文字は言葉としての意味を持たず学習の障害になるため除去したいと考えました.

この問題はNagisaという形態素解析ライブラリを用いることで解決しました.NagisaはRNNなので実行時間が長いは長いのですが,そこそこ高精度に絵文字と顔文字を除去してくれるため採用しました.また,形態素解析ライブラリであるため単語分割されるのですが,単語分割にはSentencePieceを用いたかったため,単語分割されたものをjoinでくっつけることによって元の文に戻しました.

さらにその後,SentencePieceを用いて単語分割を行いました.SentecePieceはサブワード分割ができる単語分割器で,サブワードという単語よりもさらに小単位のものに分割することによって未知語を減らすことができるという利点があります.また,SentencePieceはどのように単語分割を行うかどうかを自分で学習します.学習時に,語彙サイズをいくらにするか自分で指定することができます.

モデルの学習

次にモデルの学習について説明します.モデルはRNNとTransformerを用いました.また,モデルの学習を行う前に色々準備をしたのでまずその話からします.

モデルの学習を行うためにはまず前処理を行い,さらに前処理済みのタグと発話文と応答文から成るファイルを,タグと発話文から成るsrcファイルと,応答文のみのtgtファルに分ける必要があります.それでこの分ける処理なのですが,使用するファイルが変わったときなどに一々手打ちで入力していくのは大変面倒で,しかも間違えも起こりやすいので全くおすすめしません.そこで,便利なシェルファイルを作成しました.

シェルファイルとは,手打ちで実行するようなコマンドをまとめて一つのファイルに書いておいて,そのシェルファイルを一回実行すればそこに書かれているすべてのコマンドが自動的に実行されていくという優れものです.シェルファイルもコマンドライン引数を指定して入力することもできますし,直接記述する場合は上の方にtrain_n=3350000とでも書いておけば,${train_n}という感じで変数として使うことができます.

シェルファイルの実行方法は,

bash [シェルファイル名]

もしくは,

./ [シェルファイル名]

です.

次に学習ですが,OpenNMTを用いました.バージョンは3.0です.実行は以下のような感じでやっていきます.

前処理

onmt_build_vocab -config "[yamlファイル名]" -n_sample [学習データの件数] -overwrite

学習

onmt_train -config "[yamlファイル名]"

出力

onmt_translate -model "[使用するモデルのパス]" -src "[入力ファイル名]" -output "[出力ファイル名]" -gpu "0" -verbose

という感じになります.

最後に,学習と出力が終わったら出力結果をデトークナイズして1文に戻すという後処理を行います.

評価

BLEUを用いて評価しました.結果は以下の通りです.

モデル BLEU
RNN 0.204
Transformer 0.175

一応言っておきますが,BLEUを100倍した結果がこれです.嘘ではないです.ちなみになぜこんな結果になってしまったかと言うと,

  1. Twitterでそもそも方言を使っている人がほぼおらずほとんど標準語ばかりのデータで学習せざるを得なかったこと
  2. そもそも学習用データの質が悪くその発話からその応答になるのは無理があるのではないかという感じのものが多かったこと(おそらく1文対しか取ってきていないことによって前後の文脈を考慮することができないため)
  3. 集めた評価用データの質も良くなかったこと

が原因として挙げられます.

ただRNNの方が値は高いですが,出力文を見てみると短くて同じ文のみを出力しており,一方でTransformerは異常な長文も出力されることはありますが,豊かで自然な文も出力されていたため,最終的にチャットボットへ接続するモデルとしてはTransformerを採用しました.以下にデモ動画を乗っけておきますのでご覧ください.

https://youtu.be/b19qNn7iqVI

チャットボットへの接続

最後に学習したモデルをTelegramと言うチャットボットに接続し,対話できるようにします.当初はAlexaと接続して音声で対話するという目標があったのですが,時間が足りなさすぎて断念しました.Telegramはテキスト上で対話することができます.基本的なTelegramへの接続の仕方はPythonでつくる対話システムを読めばわかります.

大雑把な流れとしては,

  1. Telegramに入力されたテキストを取得し,絵文字・顔文字除去を行う
  2. さらにSentencePieceで単語分割を行い,ファイルに書き込む
  3. ファイルをOpenNMTに入力し,出力ファイルを得る
  4. 出力ファイル内のテキストをデトークナイズして綺麗な1文に戻す
  5. 最後にTelegram上でその文を出力する

このようになります.初めてTelegram上で対話できるようになった時は,自分で実装したこともあって結構感動しました.

これで対話システムの作成については以上となります.

全体を通して学んだこと・できるようになったこと

自分で工夫してシェルファイルを作れるようになった

授業で使用するサーバに接続した後に一々conda activateして仮想環境に入って,cdで作業ディレクトリに移動して,screen -lsで一覧調べたり,nvidia-smiでGPUの使用状況を表示させていたのですが,仮想環境名はtabで出てこないし色々入力するのめんどくさいしと言うことで,これらを一気にやってくれるシェルファイルを作ろうと思いたち,作りました.コードは以下の通りです.

#!/bin/bash
source ~/anaconda3/etc/profile.d/conda.sh

conda activate [仮想環境名]

cd [作業ディレクトリのパス]

screen -ls

nvidia-smi

実行方法は,

source [シェルファイル名]

です.普段はbashで実行すると思うのですが,環境を変更する場合はsourceというコマンドを使います.

感想

たったこれだけのことですが,自分でこういうのが欲しいって思って,作って,実際に使って,それで便利だな作ってよかったなって思えるものが作れたので,自分の力で成し遂げた感じがあってなんだかとても嬉しかったです.

自作ファイルをインポートして使う方法を学んだ

自分で作成したファイルをインポートして使えるということを授業を通して初めて知りました.書き方はそのままで,

import [使いたいファイル名(.pyは不要)]

と記述します.さらに,そのファイルの中の関数を使いたい場合は,

[使いたいファイル名].[使いたい関数名]

と記述すれば使うことができます.

ファイル管理を怠ると詰む

ファイルを作成する際に面倒だったので何も考えずに1個のディレクトリに延々と追加していっていたのですが,普通にファイルごちゃごちゃで編集したい時にどこにあるかわからないし,分割されたファイルも出力されたファイルも同じディレクトリに入れるようにしていたのでどれを使えばいいのかわからなくなって何回か間違えて使ってました.あと今GitHubに乗っけたいと思ってリファクタリングしているのですが,何事もなく地獄を見ています.

そこで先生に教えてもらったのが,実行ごとにディレクトリを作って,その中で実行するとファイルも間違わないし上書きもされないし,すっきりするしで良いとのことです.

さらに先輩に教えてもらったのが,例えば前処理とか学習とかの処理の種類ごとにディレクトリを作ってその中で作業し,最後にそれらを一気に実行するシェルファイルを1つ作ると便利だということを学びました.

班長としての振る舞い方

前に比べるとだいぶみんな頑張っていた方だったようですが,人工知能研究室の人とそうでない人で仕事量にめちゃくちゃ差が出る問題がありました.ただ難しい部分はどうしても先生に教えてもらいながらできる自分たちの仕事になってしまうのはしょうがないと思うので,それ以外のできそうな仕事を積極的に班員に割り振っていくことが重要だなと感じました.

また,割り振ったタスクができなかった場合はギリギリになって連絡するのではなく,早めに言ってもらうようあらかじめお願いしておくことが重要であることを学びました.

また,仕事とは違って授業なので同じ立場同士の人間の中から班長が生まれざるを得ないのですが,それでも遠慮せず自分が班長で,自分がタスクを割り振らなければタスクが終わらないんだと自信を持って下手に出ることなく言えばいいかなと感じました.ずっとめちゃくちゃ丁寧な文章でお願いしていたのですが,その丁寧な文章を考えるのにめちゃくちゃ時間がかかる上にこの人には少々言ってもいいんだなみたいな空気になったことが一回あったので完全に労力の無駄でした.世の中の班長の皆さん自信を持ってタスクを割り振っていってください.

学んだコマンド達色々

pip freeze:何がインストールされているかを確認できるコマンド
ls -ltr:ファイルの詳細を作成時刻順に表示することができるコマンド
CUDA_VISIBLE_DEVICES=1 [実行コマンド]:GPU1を使用してプログラムを実行するコマンド
head:ファイルの頭から何行かを取ってくるコマンド(行数の指定可)
tail:ファイルの後ろから何行かを取ってくるコマンド(行数の指定可)
cut:ファイルを分割することができるコマンド
shuf:ファイルをランダムにシャッフルすることができるコマンド

それから,学習が終わったかを一々確認するのが面倒なので,学習が終わったらSlackに通知を送るように設定しました.これは調べれば結構簡単に実装することができました.学習を行うコマンドの記述されたシェルファイルの後に,このコマンドを追加するだけで終了通知を飛ばすことができます.通知内容も好きなものに変えることができます.

curl -X POST -H 'Content-type: application/json' --data '{"text":"[通知内容]"}' [SlackのURL]

おわりに

以上でチームでの対話システムの作成についてのお話を終わります.全体的な感想としては,授業をする前は本当に何にもわからなくて,シェルファイルとかも知らないしサーバ上でのプログラムの実行方法とか知らないし,コマンドもlscdとか基本的なものしかわからないし,conda activateとかscreenとか仮想環境何それ美味しいのか状態でしたが,この授業を通して本当に色々学ぶことができました.

大変すぎてこの授業まじでなくなくなれば良いのにって何回思ったかわかりませんが,最終的に地域ごとにそこそこ特徴のある応答が返ってくる対話システムが完成したし,チームとしても概ね円満にできたし,結果としてはとても良い経験をすることができました.次に何か開発することがあれば今回の授業を通して学んだことを活かしてもっと上手に立ち回りたいと思います.

Discussion