はじめに
COTOHA API(詳細:COTOHA API Portal)を使った面白いネタが続々投稿されていますね。
そんな皆さんの記事を楽しみながら地味に遊んでいるわけですが、今回は基本に立ち戻って構文解析(形態素解析)を確認してみます。要点は以下の3点です
- ポイント1:使用する辞書は重要
- ポイント2:辞書の追加方法
- ポイント3:COTOHA API による構文可視化
この記事の Livescript 版はこちら Example_ParsingTips1 に置いてありますので試したい方は DL してみてください。
環境
- MATLAB R2019b
- Text Analytics Toolbox
- COTOHA API v1
形態素解析って?
形態素解析(けいたいそかいせき、Morphological Analysis)とは、文法的な情報の注記の無い自然言語のテキストデータ(文)から、対象言語の文法や、辞書と呼ばれる単語の品詞等の情報にもとづき、形態素(Morpheme, おおまかにいえば、言語で意味を持つ最小単位)の列に分割し、それぞれの形態素の品詞等を判別する作業である。(Wikipedia)
ということで自然言語処理では欠かせない作業のようです。例えば MATLAB では MeCab が使用されており、tokenizedDocument
関数で実行します。
str = "時系列データに機械学習を試した。";
documents = tokenizedDocument(str);
tokenDetails(documents)
Token | DocumentNumber | LineNumber | Type | Language | PartOfSpeech | Lemma | Entity | |
---|---|---|---|---|---|---|---|---|
1 | "時" | 1 | 1 | letters | ja | noun | "時" | non-entity |
2 | "系列" | 1 | 1 | letters | ja | noun | "系列" | non-entity |
3 | "データ" | 1 | 1 | letters | ja | noun | "データ" | non-entity |
4 | "に" | 1 | 1 | letters | ja | adposition | "に" | non-entity |
5 | "機械" | 1 | 1 | letters | ja | noun | "機械" | non-entity |
6 | "学習" | 1 | 1 | letters | ja | noun | "学習" | non-entity |
7 | "を" | 1 | 1 | letters | ja | adposition | "を" | non-entity |
8 | "試し" | 1 | 1 | letters | ja | verb | "試す" | non-entity |
9 | "た" | 1 | 1 | letters | ja | auxiliary-verb | "た" | non-entity |
10 | "。" | 1 | 1 | punctuation | ja | punctuation | "。" | non-entity |
こんな具合です。
「時系列データ」=>「時」「系列」「データ」
「時系列」が「時」「系列」に、「機械学習」が「機械」「学習」と分割されているのが気になります。これは MeCab で使用する辞書(既定: IPADIC)にその単語が入っていないことが要因だと推測しますが、そもそも形態素解析とは合成語を個別に認識する用途ではないのかもしれない。
この辺が分解されてしまった形態素をそのまま使うと、文章のトピック分析やキーワード抽出に影響を与えることになりそうです。技術関係の単語が多い、例えば Qiita の投稿記事を分析しようとするとちょっと心配。
例えばこの例:Word-By-Word Text Generation Using Deep Learning では LSTM を使って(英語)文章生成しています。同じ方法をそのまま細かく分解されてしまった日本語に使うと英語程の結果が出ない気がしますね。形態素解析をして単語を取り出せているつもり・・・か? by @icoxfog417 でおっしゃっているような「形態素解析」と「単語」だと、「単語」を使用すべきでしょう。
こんな複合語については「形態素解析での効率的な複合語処理(青木和夫 著 - ・2003)」などいろんなお話があります。
COTOHA API では?
やってみます。ちなみに COTOHA API 提供する「構文解析 API 」では形態素に分解するだけでなく、文節間の係り受け関係や形態素間の係り受け関係まで出してくれます。
アクセストークン設定
clientid
と clientsecret
はご自身のものを指定ください。
clientid = 'input_your_Client_ID';
clientsecret = 'input_your_Client_secret';
url = 'https://api.ce-cotoha.com/v1/oauth/accesstokens';
options = weboptions('RequestMethod','post', 'MediaType','application/json');
Body = struct('grantType', 'client_credentials', ...
'clientId', clientid, ...
'clientSecret', clientsecret);
tokens = webwrite(url, Body, options);
構文解析 API
呼び出します。
baseurl = 'https://api.ce-cotoha.com/api/dev/';
accesstoken = tokens.access_token;
Header = {'Content-Type', 'application/json;charset=UTF-8'; 'Authorization', ['Bearer ' accesstoken]};
Body = struct('sentence', "時系列データに機械学習を試した。", ...
'type', 'default');
options = weboptions('RequestMethod','post', 'MediaType','application/json','HeaderFields', Header);
response = webwrite([baseurl 'nlp/v1/parse'], Body, options);
トークンだけ抽出
結果は入り組んだ構造体で返ってきていろんな情報が入っています(詳細:構文解析 API)。私の知識ではすべてを使いこなせないので、ここでは表記(form)とその単語の出現順、そして文節(chunk)の情報を取り出します。関数作っておきました(ページ下部)ので興味のある方は見てください。
[listofToken, chunkList] = getTokens(response);
listofToken
chunkIDs | IDs | forms | |
---|---|---|---|
1 | 1 | 0 | "時系列" |
2 | 1 | 1 | "データ" |
3 | 1 | 2 | "に" |
4 | 2 | 3 | "機械" |
5 | 2 | 4 | "学習" |
6 | 2 | 5 | "を" |
7 | 3 | 6 | "試" |
8 | 3 | 7 | "し" |
9 | 3 | 8 | "た" |
10 | 3 | 9 | "。" |
こんな具合。機械学習は「機械」と「学習」ですが、「時系列」は認識されているみたいです。
COTOHA API の Enterprise ユーザのみですが、解析に使用する専門用語辞書も別途指定可能。11種類もあるので特定の分野に特化した文章の場合は使わない手はなさそうですね。
文節間の係り受け関係
ついでに文のつながりも見てみます。digraph
関数を使って有向グラフで表示します。
各分節を 1 つのノードと考えることにして、各分節に属する単語同士を join
関数で繋げるて各ノードのラベルとします。
nodelabels = splitapply(@(x) join(x,""),listofToken.forms,listofToken.chunkIDs); % 同じ chunkID の forms は結合
g = digraph(chunkList,nodelabels,'omitselfloops');
plot(g,'Layout','layered','NodeFontSize',12);
構成が見えました。これは遊びがいがありそうなので他の例もページ下部で紹介します。
トランプ問題:固有名詞
もう1つ。
str = "ドナルド・トランプがマクドナルドでトランプを";
Body = struct('sentence',str, 'type', 'default');
response = webwrite([baseurl 'nlp/v1/parse'], Body, options);
getTokens(response)
chunkIDs | IDs | forms | |
---|---|---|---|
1 | 1 | 0 | "ドナルド・トランプ" |
2 | 1 | 1 | "が" |
3 | 2 | 2 | "マクドナルド" |
4 | 2 | 3 | "で" |
5 | 3 | 4 | "トランプ" |
6 | 3 | 5 | "を" |
COTOHA API はトランプさんを認識してますね。ちなみに MeCab とデフォルトの辞書(IPADIC)を使うと
documents = tokenizedDocument(str);
tokenDetails(documents)
Token | DocumentNumber | LineNumber | Type | Language | PartOfSpeech | Lemma | Entity | |
---|---|---|---|---|---|---|---|---|
1 | "ドナルド" | 1 | 1 | letters | ja | proper-noun | "ドナルド" | person |
2 | "・" | 1 | 1 | punctuation | ja | symbol | "・" | non-entity |
3 | "トランプ" | 1 | 1 | letters | ja | noun | "トランプ" | non-entity |
4 | "が" | 1 | 1 | letters | ja | adposition | "が" | non-entity |
5 | "マクドナルド" | 1 | 1 | letters | ja | proper-noun | "マクドナルド" | organization |
6 | "で" | 1 | 1 | letters | ja | adposition | "で" | non-entity |
7 | "トランプ" | 1 | 1 | letters | ja | noun | "トランプ" | non-entity |
8 | "を" | 1 | 1 | letters | ja | adposition | "を" | non-entity |
これだと(ドナルド・)トランプとトランプが同じ情報になっちゃっています。
これはわざとらしい例ですが、この辺の前処理が最終結果に与える影響は大きいので、前処理はめんどくさがらずにしっかりやらんとイケナイという教訓ですね。
対策1:CustomTokens オプション
合成語として対応が必要な用語が少しでであれば CustomTokens
オプションを使う手もあります。
documents = tokenizedDocument(str,"CustomTokens","ドナルド・トランプ");
tokenDetails(documents)
Token | DocumentNumber | LineNumber | Type | Language | PartOfSpeech | Lemma | Entity | |
---|---|---|---|---|---|---|---|---|
1 | "ドナルド・トランプ" | 1 | 1 | custom | ja | other | "ドナルド・トランプ" | non-entity |
2 | "が" | 1 | 1 | letters | ja | adposition | "が" | non-entity |
3 | "マクドナルド" | 1 | 1 | letters | ja | proper-noun | "マクドナルド" | organization |
4 | "で" | 1 | 1 | letters | ja | adposition | "で" | non-entity |
5 | "トランプ" | 1 | 1 | letters | ja | noun | "トランプ" | non-entity |
6 | "を" | 1 | 1 | letters | ja | adposition | "を" | non-entity |
不明な単語として認識されちゃいますが、少なくともトランプとの誤認識は避けられます。
対策2:辞書の追加
少しだけの特別対応であれば上での方法でいいかもしれませんが、沢山ある場合にはデフォルトで使用する IPADIC に加えて IPADIC に追加の辞書を与えてやる方がよさそうです。
選択肢は2つ:
- 自分で1から作る:大変だけどニッチな分野の文章が対象であればしょうがない・・。
- 公開されている辞書を使う:ぴったり合った辞書があればいいんだけど。
例えば
- 医療分野: ComeJisyo(コメディカル実践用), MANBYO(病名や症状)
- 科学技術全般(?): 科学技術用語形態素解析辞書 (CC バイオサイエンスデータベースセンター CC BY-SA 4.0)
などが見つかりました。ComeJisyo は昔、医療文章の解析に使ったことがあります。参考:UbuntuでMeCabに医療用語辞書(ComeJisyo,MANBYO)を追加してみた by @yama_ebi さん
あとは mecab-ipadic-NEologd には大変興味をひかれました。
mecab-ipadic-NEologd は形態素解析エンジン MeCab と共に使う単語分かち書き辞書で、週2回以上更新され、新語・固有表現に強く、語彙数が多く、しかもオープンソース・ソフトウェアである という特徴があります。(新語・固有表現に強い「mecab-ipadic-NEologd」の効果を調べてみた)
週2回以上更新とか、新語対応への気合の入りようが他とは一線を画します。どう実現しているんだ・・。リンク先を見ると新語への対応(テレビ番組名、固有名詞など)は素晴らしい模様です。
MATLAB で使えるのか?
それぞれの使用例はまた機会があれば纏めたいと思いますが、基本的に
mecabOptions
ans =
MecabOptions のプロパティ:
Model: "C:\Program Files\MATLAB\R2019b\sys\share\dict-ipadic"
UserModel: ""
LemmaExtractor: @textanalytics.ja.mecabToLemma
POSExtractor: @textanalytics.ja.mecabToPOS
NERExtractor: @textanalytics.ja.mecabToNER
この UserModel
に辞書 dic ファイルのパスを設定するだけでOKです。
ここでは結果だけにしますが、上で触れた JSTシソーラス見出し語・同義語辞書 (科学技術用語形態素解析辞書)を使ってみると
% Thesaurus2015.dic は上のリンク先から別途ダウンロードしてください
mOptions = mecabOptions('UserModel',"Thesaurus2015.dic");
str = "時系列データに機械学習を試した。";
documents = tokenizedDocument(str,"TokenizeMethod",mOptions);
tokenDetails(documents)
Token | DocumentNumber | LineNumber | Type | Language | PartOfSpeech | Lemma | Entity | |
---|---|---|---|---|---|---|---|---|
1 | "時系列" | 1 | 1 | letters | ja | noun | "時系列" | non-entity |
2 | "データ" | 1 | 1 | letters | ja | noun | "データ" | non-entity |
3 | "に" | 1 | 1 | letters | ja | adposition | "に" | non-entity |
4 | "機械学習" | 1 | 1 | letters | ja | noun | "機械学習" | non-entity |
5 | "を" | 1 | 1 | letters | ja | adposition | "を" | non-entity |
6 | "試し" | 1 | 1 | letters | ja | verb | "試す" | non-entity |
7 | "た" | 1 | 1 | letters | ja | auxiliary-verb | "た" | non-entity |
8 | "。" | 1 | 1 | punctuation | ja | punctuation | "。" | non-entity |
こんどは「時系列」と「機械学習」がそれぞれ単語として認識されています。これはよさげ。
この例のように dic
ファイルが公開されているものもあれば、別のファイル(csv 等)から dic ファイルを作る必要がある場合もあります。
まとめ
今回は辞書と形態素解析の結果について触れてみました。
ただ闇雲に文章を分解してモデルに入れてもいいことにはならなそう。新語や固有名詞に強そうな mecab-ipadic-NEologd は面白そうなので、是非使ってみてまとめてみたいと思います。(できるかな)
COTOHA API で用意されている辞書の真価(日本最大級の日本語辞書を活用!)は Enterprise ユーザーになれば堪能できるようですが、そもそも形態素解析以上の機能がもり沢山ですしあまり大きな問題ではないですね。
おまけ:文節の係り受け可視化
COTOHA API の構文解析 API の結果には各単語の依存関係だけでなく、文節の係り受け情報も含まれています。ここでは文節間のつながりを見てみましょう。
となりの客はよく柿食う客だ
str = "となりの客はよく柿食う客だ";
Body = struct('sentence', str, 'type', 'default');
options = weboptions('RequestMethod','post', 'MediaType','application/json','HeaderFields', Header);
response = webwrite([baseurl 'nlp/v1/parse'], Body, options);
% 表記、文節情報の抽出
[listofToken, chunkList] = getTokens(response);
% 同じ chunkID の forms は結合してノード名に
nodelabels = splitapply(@(x) join(x,""),listofToken.forms,listofToken.chunkIDs);
g = digraph(chunkList,nodelabels,'omitselfloops');
plot(g,'Layout','layered','NodeFontSize',12);
自然言語的にどれくらい意義があるか分かりませんが、どのルートを取っても文として成立していますね。
- となりの客は食う客だ
- となりの客は客だ
- よく食う客だ
- よく柿食う客だ
頭が赤い魚を食べた猫
いろんな解釈の可能性がある文。詳細はこちら「たくさんの解釈ができる「頭が赤い魚を食べた猫」が面白い」。各解釈別に絵が付いているので面白いです。COTOHA API ではどんな理解をするんでしょうか・・。
str = "頭が赤い魚を食べた猫。";
Body = struct('sentence', str, 'type', 'default');
options = weboptions('RequestMethod','post', 'MediaType','application/json','HeaderFields', Header);
response = webwrite([baseurl 'nlp/v1/parse'], Body, options);
% 表記、文節情報の抽出
[listofToken, chunkList] = getTokens(response);
% 同じ chunkID の forms は結合してノード名に
nodelabels = splitapply(@(x) join(x,""),listofToken.forms,listofToken.chunkIDs);
g = digraph(chunkList,nodelabels,'omitselfloops');
plot(g,'Layout','layered','NodeFontSize',12);
- 赤い魚を食べた猫
- 頭が食べた猫
- 頭が魚を食べた猫
現実的なのは「頭が赤い魚を食べた猫」でしょうが・・。
日本語は文節で順番を入れ替えられるので、修飾節と被修飾節に距離があっても正しい日本語として認識できます。ただし、話者の意図したことを正しく理解できるかどうかは文脈次第、場合によっては運次第です。(たくさんの解釈ができる「頭が赤い魚を食べた猫」が面白い)
難しいですね。面白そうな文章があればぜひ試してみてください。
Appendix: getTokens 関数
function [listofTokens, chunkLinks] = getTokens(response)
% COTOHA API: Parse の結果から
% 1: chunk(文節)の係り受け関係と
% 2: 各 token (形態素)のform (表記)を取り出す関数
rsize = length(response.result);
chunkIDs = [];
IDs = [];
forms = [];
% chunkLinks は隣接行列として取り出して digraph で可視化します。
% 詳細:https://jp.mathworks.com/help/matlab/ref/digraph.html
chunkLinks = zeros(rsize);
% chunk ごとに構造体として情報が確保されているため
% 1つづつ処理していきます。
for ii = 1:rsize
% 1 chunk 分
tmp = response.result(ii);
% その chunk "が"かかっている chunkID
head = response.result(ii).chunk_info.head;
if head > 1 % 文末は -1 :無視
chunkLinks(ii,head) = 1; %
end
% その chunk "に"かかっている chunkID
links = response.result(ii).chunk_info.links;
if ~isempty(links) % かかっていない場合もある
linkorigins = [links.link]+1;
chunkLinks(linkorigins,ii) = 1;
end
% chunk 内の形態素 token を順次処理
csize = length(tmp.tokens); % token の数
chunkIDs = [chunkIDs; repmat(ii,csize,1)]; % 同じ chunkID を複製
if csize == 1
% token が1つの場合は構造体データ
IDs = [IDs; tmp.tokens.id];
forms = [forms; string(tmp.tokens.form)];
else
% token が2つ以上の場合は構造体が cell 配列に入っています。
IDs = [IDs; cellfun(@(x) x.id, tmp.tokens)];
tmp = cellfun(@(x) x.form, tmp.tokens, 'UniformOutput', false);
forms = [forms; string(tmp)];
% 参考:上と同じ処理を for ループで書いた場合
% for jj = 1:csize
% chunkIDs = [chunkIDs; ii];
% IDs = [IDs; tmp.tokens{jj}.id];
% forms = [forms; string(tmp.tokens{jj}.form)];
% end
end
end
listofTokens = table(chunkIDs, IDs, forms);
end
-
Livescript から markdown への変換は livescript2markdown: MATLAB's live scripts to markdown で実施しています。 ↩