はじめに
COTOHA API というものがあります。
NTTコミュニケーションズが開発した日本最大級の日本語辞書を活用した自然言語処理、音声認識APIプラットフォーム
です。これを使うとカンタンに自然言語処理をすることができます。
この API を Ruby でお手軽に扱えるようにしたいなぁと思い gem を作りました
tanaken0515/cotoha-ruby: COTOHA API client for Ruby
このgemを紹介しつつ COTOHA API でこんなことができるんだよ〜、という話を書いていこうと思います。
想定読者
- 自然言語処理に興味があるけどやったことない方
- Rubyで自然言語処理をやりたい方
僕自身も自然言語処理については疎いので、この記事を書きながら一緒に「こんなことができるんだなぁ〜」と学んでいきます
cotoha
gem の使い方
基本的な使い方を紹介します。
インストール
cotoha
gem は rubygems に公開してあります。
Gemfile
に gem 'cotoha'
を記載して bundle install
するか、
gem install cotoha
でインストールすることができます。
認証
- まずは スタートガイド | COTOHA API の通りに COTOHA API のアカウントを作り、ログインしましょう
- ログインすると以下のような
Client ID
とClient secret
が記載された画面が表示されます
require 'cotoha'
client_id = 'xxxxx'
client_secret = 'xxxxx'
client = Cotoha::Client.new(client_id: client_id, client_secret: client_secret)
client.create_access_token
# => {"access_token"=>"xxxxx", "token_type"=>"bearer", "expires_in"=>"86399", "scope"=>"", "issued_at"=>"1582159764808"}
- また、前もってアクセストークンが分かっている場合にはそれを用いて以下のように書くこともできます。
client = Cotoha::Client.new(token: 'xxxxx')
使用例
cotoha
gem では COTOHA API の各種エンドポイントを Cotoha::Client
クラスのインスタンスメソッドとして提供しています。
例えば「感情分析」のエンドポイント POST /nlp/v1/sentiment
には sentiment
メソッドが対応しています。
client.sentiment(sentence: 'ゲームをするのが好きです。')
# {"result"=>
# {"sentiment"=>"Positive",
# "score"=>0.4714220003626205,
# "emotional_phrase"=>[{"form"=>"好きです", "emotion"=>"P"}]},
# "status"=>0,
# "message"=>"OK"}
基本的な使い方の紹介は以上です。
次節では各種エンドポイントをそれぞれ見ていきます。
COTOHA API でできること
cotoha
gem を使って COTOHA API でできることを紹介します。
自然言語処理をまだあまり知らない方は、この節を読むと「あ、こういうのが自然言語処理というやつなんだ〜」というのがなんとなく分かると思います
公式の APIリファレンス | COTOHA API を見つつ、各種エンドポイントと Cotoha::Client
クラスのインスタンスメソッドとの対応関係を以下にまとめました。
| 名称 | エンドポイント | Cotoha::Client
|
|:--|:--|:--|:--|
| 構文解析 | POST /nlp/v1/parse
| parse
|
| 固有表現抽出 | POST /nlp/v1/ne
| named_entities
|
| 照応解析 | POST /nlp/v1/coreference
| coreference
|
| キーワード抽出 | POST /nlp/v1/keyword
| keywords
|
| 類似度算出 | POST /nlp/v1/similarity
| similarity
|
| 文タイプ判定 | POST /nlp/v1/sentence_type
| sentence_type
|
| ユーザ属性推定(β) | POST /nlp/beta/user_attribute
| user_attribute
|
| 言い淀み除去(β) | POST /nlp/beta/remove_filler
| remove_filler
|
| 音声認識誤り検知(β) | POST /nlp/beta/detect_misrecognition
| detect_misrecognition
|
| 感情分析 | POST /nlp/v1/sentiment
| sentiment
|
| 要約(β) | POST /nlp/beta/summary
| summary
|
COTOHA API には上記の他に「固有名詞(企業名)補正」「音声認識」「音声合成」のAPIがありますが、有料の for Enterprise プランに申し込まないと利用できないため、現時点(cotoha v0.2.0
)では対象外としています。
それぞれのエンドポイントについて見ていきましょう。
構文解析
公式リファレンスによれば
構文解析APIは、入力として日本語で記述された文を受け取り、文の構造と意味を解析・出力します。入力された文は、文節・形態素に分解され、文節間の係り受け関係や形態素間の係り受け関係、品詞情報などの意味情報などが付与されます。
とのこと。では「吾輩は猫である」という文章を構文解析 parse
してみましょう。
response = client.parse(sentence: '吾輩は猫である')
pp response
# {"result"=>
# [{"chunk_info"=> {...}
# "tokens"=> [...]},
# {"chunk_info"=> {...}
# "tokens"=> [...]},
# {"chunk_info"=> {...}
# "tokens"=> [...]}],
# "status"=>0,
# "message"=>""}
レスポンスをそのまま貼り付けると長かったので中身を省略しました。result に chunk_info
と tokens
をキーとする Hash オブジェクトの配列が入っていることがわかりますね。
chunk
は「大きなかたまり」という意味で、公式リファレンスの説明文の文脈では(あるいは自然言語処理では一般に?)「文節」を意味しているようです。つまり「吾輩は猫である」という文章は3つの「文節」に分けることができたようです。
tokens
は文章を構成する最小単位となっている「字句」を表しています。公式リファレンスの説明文の文脈では「形態素」にあたるのようですね。つまり先ほどの3つの「文節」のそれぞれは、いくつかの「形態素」によって構成されているよ、という意味になりそうです。
responseの全文(クリックして開閉)
{"result"=>
[{"chunk_info"=>
{"id"=>0,
"head"=>2,
"dep"=>"D",
"chunk_head"=>0,
"chunk_func"=>1,
"links"=>[]},
"tokens"=>
[{"id"=>0,
"form"=>"吾輩",
"kana"=>"ワガハイ",
"lemma"=>"吾輩",
"pos"=>"名詞",
"features"=>["代名詞"],
"dependency_labels"=>[{"token_id"=>1, "label"=>"case"}],
"attributes"=>{}},
{"id"=>1,
"form"=>"は",
"kana"=>"ハ",
"lemma"=>"は",
"pos"=>"連用助詞",
"features"=>[],
"attributes"=>{}}]},
{"chunk_info"=>
{"id"=>1,
"head"=>2,
"dep"=>"D",
"chunk_head"=>0,
"chunk_func"=>1,
"links"=>[]},
"tokens"=>
[{"id"=>2,
"form"=>"猫",
"kana"=>"ネコ",
"lemma"=>"猫",
"pos"=>"名詞",
"features"=>[],
"dependency_labels"=>[{"token_id"=>3, "label"=>"cop"}],
"attributes"=>{}},
{"id"=>3,
"form"=>"で",
"kana"=>"デ",
"lemma"=>"で",
"pos"=>"判定詞",
"features"=>["連用"],
"attributes"=>{}}]},
{"chunk_info"=>
{"id"=>2,
"head"=>-1,
"dep"=>"O",
"chunk_head"=>0,
"chunk_func"=>1,
"links"=>
[{"link"=>0, "label"=>"agent"}, {"link"=>1, "label"=>"condition"}],
"predicate"=>[]},
"tokens"=>
[{"id"=>4,
"form"=>"あ",
"kana"=>"ア",
"lemma"=>"ある",
"pos"=>"動詞語幹",
"features"=>["R"],
"dependency_labels"=>
[{"token_id"=>0, "label"=>"nsubj"},
{"token_id"=>2, "label"=>"nmod"},
{"token_id"=>5, "label"=>"aux"}],
"attributes"=>{}},
{"id"=>5,
"form"=>"る",
"kana"=>"ル",
"lemma"=>"る",
"pos"=>"動詞接尾辞",
"features"=>["終止"],
"attributes"=>{}}]}],
"status"=>0,
"message"=>""}
↑の「responseの全文」をみつつ、簡単な図にしてみました。
tokenごとに dependency_labels
があるので、 token 間の関係性を図にしてみると面白いかもですね。(今回は図を作るのが大変そうなので遠慮しました)
固有表現抽出
公式リファレンスによれば
固有表現抽出APIは、入力として日本語で記述された文を受け取り、人名や地名、日付表現(時間、日付)、組織名、量的表現(金額、割合)、人工物の8種類の固有表現と、200種類以上のクラス数を持つ拡張固有表現を出力します。
とのこと。
では試しに「私は犬が好きだ。よく代々木公園の近くを散歩している。」という文章から固有表現を抽出してみます。
response = client.named_entities(sentence: '私は犬が好きだ。よく代々木公園の近くを散歩している。')
pp response
# {"result"=>
# [{"begin_pos"=>10,
# "end_pos"=>15,
# "form"=>"代々木公園",
# "std_form"=>"代々木公園",
# "class"=>"LOC",
# "extended_class"=>"",
# "source"=>"basic"},
# {"begin_pos"=>2,
# "end_pos"=>3,
# "form"=>"犬",
# "std_form"=>"犬",
# "class"=>"OTH",
# "extended_class"=>"Mammal",
# "source"=>"basic"},
# {"begin_pos"=>4,
# "end_pos"=>7,
# "form"=>"好きだ",
# "std_form"=>"好きだ",
# "class"=>"ART",
# "extended_class"=>"Movie",
# "source"=>"basic"}],
# "status"=>0,
# "message"=>""}
「代々木公園」「犬」「好きだ」の3つが抽出されたようです。
「代々木公園」は地名("class"=>"LOC"
)で抽出されているのでたしかに〜、という感じですが、他の2つは意外ですね。
「犬」は "class"=>"OTH", "extended_class"=>"Mammal"
で抽出されています。これは「固有表現」としては「その他(OTHER)」に分類され、「拡張固有表現」としては「自然物名>生物名>哺乳類名(Mammal)」に分類されるよ、ということらしいです。「拡張固有表現」すごいですね、生物名、どれくらいいけるのかなぁ。
最後に「好きだ」は "class"=>"ART", "extended_class"=>"Movie"
だそうです。いや全然そんなつもりで使ってなかったんで驚きました。「固有表現」としては「固有物名(ART)」に分類され、「拡張固有表現」としては「芸術作品名>映画名(Mammal)」に分類されるよ、ということらしいです。
へ〜、そんな映画あったんですね。調べてみると正式名称は『好きだ、』のようで、宮崎あおいさん、西島秀俊さん、瑛太さんなどが出演している映画なんですね〜。公式サイトはこちら -> su-ki-da,
「拡張固有表現」面白いなぁと思ったので、「珍しい生き物の名前」でググって生物名として判定されるか試してみました。
response = client.named_entities(sentence: 'タツノイトコを捕まえた')
pp response
# {"result"=>
# [{"begin_pos"=>0,
# "end_pos"=>6,
# "form"=>"タツノイトコ",
# "std_form"=>"タツノイトコ",
# "class"=>"OTH",
# "extended_class"=>"Fish",
# "source"=>"basic"}],
# "status"=>0,
# "message"=>""}
お〜、「タツノイトコ」をちゃんと「魚類名(Fish)」として判定していますね。
response = client.named_entities(sentence: 'ウッカリカサゴを捕まえた')
pp response
# {"result"=>[], "status"=>0, "message"=>""}
response = client.named_entities(sentence: 'うっかり笠子を捕まえた')
pp response
# {"result"=>[], "status"=>0, "message"=>""}
あ〜、「ウッカリカサゴ」は難しかったみたい。
照応解析
公式リファレンスによれば
照応解析APIは、入力として日本語で記述された複数の文からなるテキストを受け取り、テキスト中の「そこ」「それ」などの指示詞や「彼」「彼女」などの代名詞と対応する先行詞を抽出し、同一のものとしてまとめて出力します。
とのこと。
response = client.coreference(document: '太郎は友人だ。彼は焼き肉を食べた。')
pp response
# {"result"=>
# {"coreference"=>
# [{"representative_id"=>0,
# "referents"=>
# [{"referent_id"=>0,
# "sentence_id"=>0,
# "token_id_from"=>0,
# "token_id_to"=>0,
# "form"=>"太郎"},
# {"referent_id"=>1,
# "sentence_id"=>0,
# "token_id_from"=>5,
# "token_id_to"=>5,
# "form"=>"彼"}]}],
# "tokens"=>
# [["太郎", "は", "友人", "だ", "。", "彼", "は", "焼き肉", "を", "食べ", "た", "。"]]},
# "status"=>0,
# "message"=>"OK"}
「太郎は友人だ。彼は焼き肉を食べた。」という文章について、「太郎 = 彼」であることを特定できています。
複数のモノがある場合はどうなるんだろう、と思ってやってみました。
response = client.coreference(document: '机にノートとペンがある。彼はこれらを手に取った。')
pp response
# {"result"=>
# {"coreference"=>
# [{"representative_id"=>0,
# "referents"=>
# [{"referent_id"=>0,
# "sentence_id"=>0,
# "token_id_from"=>4,
# "token_id_to"=>4,
# "form"=>"ペン"},
# {"referent_id"=>1,
# "sentence_id"=>0,
# "token_id_from"=>11,
# "token_id_to"=>11,
# "form"=>"これら"}]}],
# "tokens"=>
# [["机", "に", "ノート", "と", "ペン", "が", "あ", "る", "。", "彼", "は", "これら", "を", "手", "に", "取", "っ", "た", "。"]]},
# "status"=>0,
# "message"=>"OK"}
「机にノートとペンがある。彼はこれらを手に取った。」という文章について、「ペン = これら」という結果になりました。
キーワード抽出
公式リファレンスによれば
キーワード抽出APIは、入力として日本語で記述された複数の文からなるテキストを受け取り、テキストに含まれる特徴的なフレーズ・単語をキーワードとして抽出します。
テキストから算出される特徴的スコアに基づいて、複数のフレーズ・単語が降順に出力されます。
とのこと。
先ほども使った「太郎は友人だ。彼は焼き肉を食べた。」のキーワードを抽出してみます。
response = client.keywords(document: '太郎は友人だ。彼は焼き肉を食べた。')
pp response
# {"result"=>
# [{"form"=>"太郎", "score"=>15.86012},
# {"form"=>"彼", "score"=>15.71654},
# {"form"=>"焼き肉", "score"=>12.8053},
# {"form"=>"友人", "score"=>9.71421}],
# "status"=>0,
# "message"=>""}
動作の主体(「太郎」「彼」)が高いスコアのキーワードとして抽出されるのでしょうか。
類似度算出
公式リファレンスによれば
類似度算出APIは、入力として日本語で記述されたテキストを2つ受け取り、テキスト間の意味的な類似度を算出・出力します。
類似度は0から1の定義域で出力され、1に近づくほどテキスト間の類似性が大きいことを示します。
テキストに含まれる単語の意味情報を用いて類似度を算出しているため、異なった単語を含むテキスト間の類似性も推定することができます。
とのこと。
response = client.similarity(s1: '近くのレストランはどこですか?', s2: 'このあたりの定食屋はどこにありますか?')
pp response
# {"result"=>{"score"=>0.88565135}, "status"=>0, "message"=>"OK"}
お〜、なるほど、確かに類似している意味合いの文章はscoreが高くなるんですね。
response = client.similarity(s1: '花粉が多くなってきてつらい。', s2: '森林浴、最高!')
pp response
# {"result"=>{"score"=>0.05732418}, "status"=>0, "message"=>"OK"}
お〜、適当な文章にしてみたら類似度めっちゃ低い。
response = client.similarity(s1: '麻婆豆腐は美味しい。', s2: 'カレーはうまい。')
pp response
# {"result"=>{"score"=>0.99354756}, "status"=>0, "message"=>"OK"}
自明ですが、麻婆豆腐は実質カレーであることを証明することもできました。
文タイプ判定
公式リファレンスによれば
文タイプ判定APIは、入力として日本語で記述された文を受け取り、文の法(叙述/疑問/命令)タイプと発話行為タイプを判定・出力します。
とのこと。
例えば「近くのレストランはどこですか?」の文タイプを判定してみます。
response = client.sentence_type(sentence: '近くのレストランはどこですか?')
pp response
# {"result"=>
# {"modality"=>"interrogative", "dialog_act"=>["information-seeking"]},
# "status"=>0,
# "message"=>""}
文種別は「質問(interrogative)」で、発話行為種別は「情報獲得(information-seeking)」と判定されました。
「発話行為」という単語を初めて聞きましたが、この論文によると「コミュニケーションを行うために遂行される行為であり、一定の音声や文法上の語句、一定の意味を持つ文を発する行為のことである。」とのことです。
これを踏まえて「発話行為種別」は発話の目的や意味合いの種類、のようなものだと解釈しました。
日々の発言の文タイプを分析・集計してみるとちょっと面白いかもですね。
ユーザ属性推定(β)
公式リファレンスによれば
ユーザ属性推定APIは、入力として日本語で記述された複数の文からなるテキストを受け取り、年代、性別、趣味、職業などの人物に関する属性を推定・出力します。
とのこと。
例えば「渋谷でエンジニアとして働いています。」のユーザ属性を推定してみると、
response = client.user_attribute(document: '渋谷でエンジニアとして働いています。')
pp response
# {"result"=>
# {"civilstatus"=>"既婚",
# "hobby"=>["COOKING", "INTERNET", "MOVIE", "SHOPPING"],
# "location"=>"関東",
# "moving"=>["RAILWAY"],
# "occupation"=>"会社員"},
# "status"=>0,
# "message"=>"OK"}
渋谷のエンジニアは既婚であることが分かります。
より高い精度の推定結果を出すには、単文ではなく複数の文章を入れたほうが良さそうですね。
ここまで説明していませんでしたが COTOHA API は基本的に sentence
か document
を引数に取ります。
前者は string
で、後者は string
または string
の Array
を受け取ることができます。
なのでこのように使うこともできます。
response = client.user_attribute(document: ['渋谷でエンジニアとして働いています。', 'webアプリケーションを作るのが好きです。'])
pp response
# {"result"=>
# {"age"=>"20-29歳",
# "civilstatus"=>"既婚",
# "habit"=>["SMOKING"],
# "hobby"=>
# ["ANIMAL",
# "COOKING",
# "FISHING",
# "FORTUNE",
# "GAMBLE",
# "INTERNET",
# "TVGAME"],
# "location"=>"関東",
# "moving"=>["CAR", "RAILWAY"]},
# "status"=>0,
# "message"=>"OK"}
"habit"=>["SMOKING"]
...(喫煙の習慣は無いですが...)
例えば、好きなサービスのSNS公式アカウントの「中の人」がどんな人なのかを推定してみると面白いかもですね。
言い淀み除去(β)
公式リファレンスによれば
言い淀み除去APIは、音声認識処理後のテキストに対して、ユーザからの音声入力時に含まれる言い淀みを除去します。
とのこと。
例えば「えーーっと、あの、何時に待ち合わせですっけ。」の言い淀みを除去してみます。
response = client.remove_filler(text: 'えーーっと、あの、何時に待ち合わせですっけ。')
pp response
# {"result"=>
# [{"fillers"=>
# [{"begin_pos"=>0, "end_pos"=>5, "form"=>"えーっと、"},
# {"begin_pos"=>5, "end_pos"=>7, "form"=>"あの"}],
# "normalized_sentence"=>"えーっと、あの、何時に待ち合わせですっけ。",
# "fixed_sentence"=>"、何時に待ち合わせですっけ。"}],
# "status"=>0,
# "message"=>"OK"}
除去した結果は "fixed_sentence"=>"、何時に待ち合わせですっけ。"
ですね。文頭に「、」が残っているは惜しいですが、「えーっと」と「あの」が除去されて無駄のない文章になっています。
音声認識したデータを分析する時の前処理として重要そうですね。
あと、自分の発話を音声認識で記録しておけば、このAPIを使って「普段どんな言い淀みをしているのか」を明らかにすることもできそうです。
音声認識誤り検知(β)
公式リファレンスによれば
音声認識誤り検知APIは、音声認識処理後のテキストを受け取り、認識誤りが疑われる箇所をそのスコアと訂正例とともに出力します。入力文全体の誤り度合いについても数値化を行って出力します。
とのこと。
response = client.detect_misrecognition(sentence: '温泉認識は誤りを起こす')
pp response
# {"result"=>
# {"score"=>0.9999968696704667,
# "candidates"=>
# [{"begin_pos"=>0,
# "end_pos"=>2,
# "form"=>"温泉",
# "detect_score"=>0.9999968696704667,
# "correction"=>
# [{"form"=>"音声", "correct_score"=>0.7722403968717316},
# {"form"=>"厭戦", "correct_score"=>0.6619857013879067},
# {"form"=>"怨念", "correct_score"=>0.6554196604056673},
# {"form"=>"おんねん", "correct_score"=>0.6554196604056673},
# {"form"=>"モンセン", "correct_score"=>0.654462258316514}]}]},
# "status"=>0,
# "message"=>"OK"}
この例では「『温泉認識』じゃなくて『音声認識』では?」という提案ができそうです。
YouTubeで「字幕あり」にすると(おそらく)音声認識の結果が表示されるんですが、それらの認識誤りをこのAPIで分析してみると面白そうだなと思いました。
感情分析
公式リファレンスによれば
感情分析APIは、入力として日本語で記述されたテキストを受け取り、そのテキストの書き手の感情(ネガティブ・ポジティブ)を判定します。また、テキスト中に含まれる「喜ぶ」「驚く」「不安」「安心」といった15種類の感情を分類・認識して出力します。
とのこと。
例えば「ゲームをするのが好きです。」はポジティブな文章であることが分かります。
response = client.sentiment(sentence: 'ゲームをするのが好きです。')
pp response
# {"result"=>
# {"sentiment"=>"Positive",
# "score"=>0.4714220003626205,
# "emotional_phrase"=>[{"form"=>"好きです", "emotion"=>"P"}]},
# "status"=>0,
# "message"=>"OK"}
では「ゲームをするのが好きです。でも勉強は嫌いです。」はどうでしょう。
response = client.sentiment(sentence: 'ゲームをするのが好きです。でも勉強は嫌いです。')
pp response
# {"result"=>
# {"sentiment"=>"Neutral",
# "score"=>0.35432534042635183,
# "emotional_phrase"=>
# [{"form"=>"嫌いです", "emotion"=>"N"}, {"form"=>"好きです", "emotion"=>"P"}]},
# "status"=>0,
# "message"=>"OK"}
「Neutral」になりました。ではもう少し勉強嫌い度合いを上げてみます。
response = client.sentiment(sentence: 'ゲームをするのが好きです。でも勉強はめまいがするほど嫌いです。')
pp response
# {"result"=>
# {"sentiment"=>"Negative",
# "score"=>0.362639080610924,
# "emotional_phrase"=>
# [{"form"=>"嫌いです", "emotion"=>"N"},
# {"form"=>"好きです", "emotion"=>"P"},
# {"form"=>"めまい", "emotion"=>"悲しい"}]},
# "status"=>0,
# "message"=>"OK"}
「Negative」になりました。 「めまい = 悲しい」なんですね。
チャットやSNSの発言から自分や周囲の人の感情を分析してみると面白いかもですね〜。
要約(β)
公式リファレンスによれば
要約APIは、入力として日本語で記述された複数文で構成された文章を受け取り、これを文単位で重要度を算出し、スコアを付与します。そして、入力時に指定された要約文数に応じ、重要文を返します。
とのこと。
document =<<~EOS
前線が太平洋上に停滞しています。
一方、高気圧が千島近海にあって、北日本から東日本をゆるやかに覆っています。
関東地方は、晴れ時々曇り、ところにより雨となっています。
東京は、湿った空気や前線の影響により、晴れ後曇りで、夜は雨となるでしょう。
EOS
response = client.summary(document: document, sent_len: 1)
pp response
# {"result"=>"東京は、湿った空気や前線の影響により、晴れ後曇りで、夜は雨となるでしょう。", "status"=>0}
文章の結論がわかりやすくて良いですね。
COTOHA APIの存在を知ったきっかけは、この要約APIを使って書かれた 「メントスと囲碁の思い出」をCOTOHAさんに要約してもらった結果。COTOHA最速チュートリアル付き - Qiitaの記事だったので、「きっかけをありがとうございます〜」という気持ちです。
「要約」というと文章を組み替えていい感じにまとめあげてくれる、というのをイメージしますが、このAPIは与えられた文章の中から重要な文をそのまま返す、というものなのでそこだけ注意が必要ですね。
おわりに
この記事では COTOHA API を Ruby でお手軽に扱えるようにした gem を紹介しました。
tanaken0515/cotoha-ruby: COTOHA API client for Ruby
この記事をきっかけに COTOHA API に興味持ってくれる方やこの gem を使ってくれる方がいたら嬉しいなぁと思います。