2022年10月11日火曜日

ワードクラウドを作成する【基本編】

 Colaboratoryで自然言語処理の基本みたいなワードクラウドを生成することができるので使ってみた。

1.そのまま使う(英語のみ)

ワードクライドを作成するライブラリのWordCloudを使うだけならこう。

結果はこんな感じになる。
"the"や"over"が入っていないのは、組み込みのstopwordsに指定されており、画像生成時に削除されているから。
#Colabで使ってDriveに出力する場合
from wordcloud import WordCloud
txt = "The quick brown fox jumps over the lazy dog"
a = WordCloud()
a.generate(text=txt)
#to_fileメソッドで書き出し
a.to_file("/content/drive/MyDrive/tmp/new.png")
wordcloudでそのまま生成したもの
生成例1

stopwordsに"quick"を追加したこのコードでやるとこうなる。
#Colabで実行する場合
#ビルドインのSTOPWORDSをインポートする
from wordcloud import WordCloud, STOPWORDS
txt = "The quick brown fox jumps over the lazy dog"
#brownをstopwordsに加える
stop = set(STOPWORDS)
stop.add("brown")
a = WordCloud(stopwords=stop)
a.generate(text=txt)
a.to_file("/content/drive/MyDrive/tmp/newst.png")
"brown"をstopwordsに加えて生成したもの
生成例2

ワードクラウドでの文字の大きさはword-ranksと出現頻度で決定される。出現頻度への文字の大きさの影響を変更するときはインスタンス作成時にrelative_scalingへ値を与える。
遥かなティペラリー(It's a Long Way to Tipperary)のサビ部分を例にやるとこう。
relative_scaling=0のパラメーターで生成したもの
relative_scaling=0

relative_scaling=0.5のパラメーターで生成したもの
relative_scaling=0.5

relative_scaling=1.0のパラメーターで生成したもの
relative_scaling=1.0

relative_scaling以外で気になったパラメータを公式ドキュメントから抜粋する。
  • height: int (default=200)
    • 画像の縦幅の設定
  • width: int (default=400)
    • 画像の横幅の設定
  • scale: float (default=1)
    • 描写のスケールを設定
  • min_word_length: int, default=0
    • ワードクラウドに含まれるための最低の単語の長さ
  • collocation_threshold: int, default=30 
    • コロケーション(単語と単語のつながり)の判定地
それ以外は公式ドキュメント参照。

2.日本語で使う

wordcloudに日本語辞書は入っていない上に、日本語からワードクラウドを作成するために必要な分かち書きや瀕死の分類などができない。
そこで日本語の形態素解析ツールであるJanomeを使う。
Janomeで簡単に形態素解析を行うには、Tokenizerを使う。日本国憲法前文について、解析を行うとこうなる。
from janome.tokenizer import Tokenizer
txt = """
日本国民は正当に選挙された国会における代表者を通じて行動し、われらとわれらの子孫のために、諸国民と協和による成果と、わが国全土にわたって自由のもたらす恵沢を確保し、政府の行為によって再び戦争の惨禍が起こることのないようにすることを決意し、ここに主権が国民に存することを宣言し、この憲法を確定する。そもそも国政は国民の厳粛な信託によるものであって、その権威は国民に由来し、その権力は国民の代表者がこれを行使し、その福利は国民がこれを享受する。これは人類普遍の原理であり、この憲法は、かかる原理に基づくものである。われらはこれに反する一切の憲法、法令及び詔勅を排除する。
日本国民は、恒久の平和を念願し、人間相互の関係を支配する崇高な理想を深く自覚するのであって、平和を愛する諸国民の公正と信義を信頼して、われらの安全と生存を保持しようと決意した。われらは平和を維持し、専制と隷従、圧迫と偏狭を地上から永遠に除去しようと努めている国際社会において、名誉ある地位を占めたいと思う。われらは全世界の国民が、ひとしく恐怖と欠乏から免れ、平和の内に生存する権利を有することを確認する。
われらは、いずれの国家も、自国のことのみに専念して他国を無視してはならないのであって、政治道徳の法則は、普遍的なものであり、この法則に従うことは、自国の主権を維持し、他国と対等関係に立とうとする各国の責務であると信ずる。
日本国民は、国家の名誉にかけて、全力をあげて崇高な理想と目的を達成することを誓う。
"""
txt = txt.replace('\n', '')
t = Tokenizer()
for token in t.tokenize(txt):
	print(token)

###出力###
"""
日本	名詞,固有名詞,地域,国,*,*,日本,ニッポン,ニッポン
国民	名詞,一般,*,*,*,*,国民,コクミン,コクミン
は	助詞,係助詞,*,*,*,*,は,ハ,ワ
正当	名詞,形容動詞語幹,*,*,*,*,正当,セイトウ,セイトー
に	助詞,副詞化,*,*,*,*,に,ニ,ニ
選挙	名詞,サ変接続,*,*,*,*,選挙,センキョ,センキョ
さ	動詞,自立,*,*,サ変・スル,未然レル接続,する,サ,サ
れ	動詞,接尾,*,*,一段,連用形,れる,レ,レ
た	助動詞,*,*,*,特殊・タ,基本形,た,タ,タ
国会	名詞,一般,*,*,*,*,国会,コッカイ,コッカイ
における	助詞,格助詞,連語,*,*,*,における,ニオケル,ニオケル
代表	名詞,サ変接続,*,*,*,*,代表,ダイヒョウ,ダイヒョー
者	名詞,接尾,一般,*,*,*,者,シャ,シャ
を通じて	助詞,格助詞,連語,*,*,*,を通じて,ヲツウジテ,ヲツージテ
行動	名詞,サ変接続,*,*,*,*,行動,コウドウ,コードー
し	動詞,自立,*,*,サ変・スル,連用形,する,シ,シ
、	記号,読点,*,*,*,*,、,、,、
われ	名詞,代名詞,一般,*,*,*,われ,ワレ,ワレ
ら	名詞,接尾,一般,*,*,*,ら,ラ,ラ
と	助詞,並立助詞,*,*,*,*,と,ト,ト
われ	名詞,代名詞,一般,*,*,*,われ,ワレ,ワレ
ら	名詞,接尾,一般,*,*,*,ら,ラ,ラ
"""

単語自体はtoken.surfaceで取り出せるが、これでは、様々な品詞が混じっているうえ、「日本国民」が「日本」と「国民」に分割されていたりしており、このままワードクラウドに用いることはできない。
そこで、Analizerとtokenfilterを用いる。
from janome.tokenizer import Tokenizer
from janome.analyzer import Analyzer
from janome.tokenfilter import *
txt = """
日本国民は正当に選挙された国会における代表者を通じて行動し、われらとわれらの子孫のために、諸国民と協和による成果と、わが国全土にわたって自由のもたらす恵沢を確保し、政府の行為によって再び戦争の惨禍が起こることのないようにすることを決意し、ここに主権が国民に存することを宣言し、この憲法を確定する。そもそも国政は国民の厳粛な信託によるものであって、その権威は国民に由来し、その権力は国民の代表者がこれを行使し、その福利は国民がこれを享受する。これは人類普遍の原理であり、この憲法は、かかる原理に基づくものである。われらはこれに反する一切の憲法、法令及び詔勅を排除する。
日本国民は、恒久の平和を念願し、人間相互の関係を支配する崇高な理想を深く自覚するのであって、平和を愛する諸国民の公正と信義を信頼して、われらの安全と生存を保持しようと決意した。われらは平和を維持し、専制と隷従、圧迫と偏狭を地上から永遠に除去しようと努めている国際社会において、名誉ある地位を占めたいと思う。われらは全世界の国民が、ひとしく恐怖と欠乏から免れ、平和の内に生存する権利を有することを確認する。
われらは、いずれの国家も、自国のことのみに専念して他国を無視してはならないのであって、政治道徳の法則は、普遍的なものであり、この法則に従うことは、自国の主権を維持し、他国と対等関係に立とうとする各国の責務であると信ずる。
日本国民は、国家の名誉にかけて、全力をあげて崇高な理想と目的を達成することを誓う。
"""
txt = txt.replace('\n', '')
t = Tokenizer()
filters = [CompoundNounFilter(), POSKeepFilter('名詞'), POSStopFilter(['非自立','代名詞'])]
NG = ['非自立', '代名詞'] 
analyzer = Analyzer(tokenizer=t, token_filters=filters)
for token in analyzer.analyze(txt):
  #POSStopFilterがうまく機能しない部分があるので二重にフィルタリングする
  ex = token.extra[0].split(',')
  if all(x not in ex for x in NG):
    print(token)

###出力###
'''
日本国民	名詞,複合,*,*,*,*,日本国民,ニッポンコクミン,ニッポンコクミン
正当	名詞,形容動詞語幹,*,*,*,*,正当,セイトウ,セイトー
選挙	名詞,サ変接続,*,*,*,*,選挙,センキョ,センキョ
国会	名詞,一般,*,*,*,*,国会,コッカイ,コッカイ
代表者	名詞,複合,*,*,*,*,代表者,ダイヒョウシャ,ダイヒョーシャ
行動	名詞,サ変接続,*,*,*,*,行動,コウドウ,コードー
子孫	名詞,一般,*,*,*,*,子孫,シソン,シソン
国民	名詞,一般,*,*,*,*,国民,コクミン,コクミン
協和	名詞,サ変接続,*,*,*,*,協和,キョウワ,キョーワ
成果	名詞,一般,*,*,*,*,成果,セイカ,セイカ
わが国全土	名詞,複合,*,*,*,*,わが国全土,ワガクニゼンド,ワガクニゼンド
自由	名詞,形容動詞語幹,*,*,*,*,自由,ジユウ,ジユー
恵沢	名詞,一般,*,*,*,*,恵沢,ケイタク,ケイタク
確保	名詞,サ変接続,*,*,*,*,確保,カクホ,カクホ
政府	名詞,一般,*,*,*,*,政府,セイフ,セイフ
行為	名詞,サ変接続,*,*,*,*,行為,コウイ,コーイ
戦争	名詞,サ変接続,*,*,*,*,戦争,センソウ,センソー
惨禍	名詞,一般,*,*,*,*,惨禍,サンカ,サンカ
決意	名詞,サ変接続,*,*,*,*,決意,ケツイ,ケツイ
'''
これでワードクラウドに入力する準備は整ったので、あとはjanomeの出力をスペース連結したものをWordCloudに放り込めば完了となる。
ただし、WordCloudには日本語フォントが含まれていないので、フォント指定をすることになる。今回はIPAフォントを利用する。
from pyparsing.helpers import Word
from wordcloud import WordCloud
from janome.tokenizer import Tokenizer
from janome.analyzer import Analyzer
from janome.tokenfilter import *
txt = """
日本国民は正当に選挙された国会における代表者を通じて行動し、われらとわれらの子孫のために、諸国民と協和による成果と、わが国全土にわたって自由のもたらす恵沢を確保し、政府の行為によって再び戦争の惨禍が起こることのないようにすることを決意し、ここに主権が国民に存することを宣言し、この憲法を確定する。そもそも国政は国民の厳粛な信託によるものであって、その権威は国民に由来し、その権力は国民の代表者がこれを行使し、その福利は国民がこれを享受する。これは人類普遍の原理であり、この憲法は、かかる原理に基づくものである。われらはこれに反する一切の憲法、法令及び詔勅を排除する。
日本国民は、恒久の平和を念願し、人間相互の関係を支配する崇高な理想を深く自覚するのであって、平和を愛する諸国民の公正と信義を信頼して、われらの安全と生存を保持しようと決意した。われらは平和を維持し、専制と隷従、圧迫と偏狭を地上から永遠に除去しようと努めている国際社会において、名誉ある地位を占めたいと思う。われらは全世界の国民が、ひとしく恐怖と欠乏から免れ、平和の内に生存する権利を有することを確認する。
われらは、いずれの国家も、自国のことのみに専念して他国を無視してはならないのであって、政治道徳の法則は、普遍的なものであり、この法則に従うことは、自国の主権を維持し、他国と対等関係に立とうとする各国の責務であると信ずる。
日本国民は、国家の名誉にかけて、全力をあげて崇高な理想と目的を達成することを誓う。
"""
cloud_text = '' #出力用
font_path = '/content/drive/MyDrive/tmp/ipaexm.ttf'
output_path = '/content/drive/MyDrive/tmp/constitution.png'

txt = txt.replace('\n', '')
t = Tokenizer()
filters = [CompoundNounFilter(), POSKeepFilter('名詞'), POSStopFilter(['非自立','代名詞'])]
NG = ['非自立', '代名詞'] 
analyzer = Analyzer(tokenizer=t, token_filters=filters)

for token in analyzer.analyze(txt):
  #POSStopFilterがうまく機能しない部分があるので二重にフィルタリングする
  ex = token.extra[0].split(',')
  if all(x not in ex for x in NG):
  	#スペース区切りで単語を連結
    cloud_text += ' '+ token.surface

wc = WordCloud(font_path = font_path, width=1980, height=1080)
wc.generate(text=cloud_text)
wc.to_file(output_path)

日本国憲法前文で作成したワードクラウド
日本語ワードクラウド生成例


ひとまずこれで日本語のワードクラウドが作成できるようになったが、専門用語や流行語など、janomeのビルドイン辞書に記載されていない単語では上手くいかない。従って、辞書を生成していく必要があるが、それはまた書くことににする。




2022年10月6日木曜日

JavaScriptのPromiseを理解する

 JavaScriptの非同期処理がよくわからない、PromiseってなんぞってなったのでPromiseについてまとめてみた。

1. Promiseは何のため?

A. Promiseは非同期処理のため
これが簡潔な答えになる。
JavaScriptでは、あることをしながら別のことをするということができない。つまり、ウェブサイトのデータをダウンロードしながら画面にスピナーを表示するといったことができない。
これを解決するのが非同期処理になる。JavaScriptでは、Callbackやイベント駆動という方法がある。その新しい方法がPromiseになる。

2. Promiseのコードはどう使うか?

JavaScriptでは.then()という形で呼び出すことになる。

//Fetch APIを使う場合
fetch(url)	//urlにアクセスして
 	.then(response => response.json()) //responseを解釈し
	.then(data => console.log(data));	//データを表示する
(https://developer.mozilla.org/ja/docs/Web/API/Fetch_API/Using_Fetchから引用)

このケースではFetch APIを取得し、対象のURLから情報を受け取り、JSONとして解釈して、そのデータを表示するということを行っている。
言い換えれば、.then()内に与えられる関数(ここではアロー関数)を呼び出して、fetch()の戻り値→response.json()の戻り値を順に与えて処理を行っている。


3. Promiseはどのような仕組みで動くか?

Promiseは、ある関数に一時的にPromiseというものを返し、処理を続行する。このPromiseが満たされるか、失敗するか、解決することでその関数は、処理を解決する。
Promiseを返す関数には2つのコールバック関数を登録することができる。1番目のコールバック関数が呼び出されたときにPromiseは満たされ、2番目のコールバック関数が呼び出されたときにPromiseは失敗する。
つまり、以下のようなコードで2つの関数を登録する。
// funcAは処理を実行、funcBはエラーを処理
fetch(url).then(funcA, funcB) 
//または
fetch(url)
	.then(funcA)
	.catch(funcB)

そして、解決するということは、いわばPromiseを返す関数がPromiseを受け取り、処理がペンディングされている状態になる。そして、あるPromiseがPromise以外の値で満たされたとき、連鎖してPromiseが満たされ、あるPromiseが失敗すれば、同じ理由で連鎖してPromiseが失敗する。
つまり、コードで表すと以下のようになる。
//HTTPリクエストを行い、最初にPromiseを返し、
//リクエストが返ってきたら、Promiseはresponseオブジェクトで満たされる
fetch(url)
//HTTPリクエストのPromiseが満たされるまで
//fetch()から返ってきたPromiseで解決される
//HTTPリクエストのPromiseが満たされると
//response.jsonが返すPromiseは満たされる
    .then(response => response.json())
//response.json()の受け取ったPromiseが満たされるまで
//最初のthen()が返すPromiseで解決される
    .then(data => displayData(data));

4. Promiseの動きを見るためのコード

Promiseは、組み込みのPromiseを返す関数以外にPromiseコンストラクタを使うことでPromiseを返す関数を作成することができる。
これを利用してミリ秒を引数として受け取り、その値を返す関数を書くと次のようになる。
//durationを引数として受け取り
//durationミリ秒後にPromiseをduratoinで満たす
function wait(duration){
    return new Promise((resolve, reject)=>{
        setTimeout(() => {resolve(duration)}, duration);
    })
}


//start→4000→setTimeoutの順でコンソールに表示される 
console.log("Start");
setTimeout(() => {console.log("setTimeout")}, 5000);
wait(4000).then(time => {console.log(time)});

//start→setTimeout→4000の順でコンソールに表示される 
console.log("Start");
setTimeout(() => {console.log("setTimeout")}, 3000);
wait(4000).then(time => {console.log(time)});

5.まとめ

Promiseは仮の値を関数に与えることで処理したことにして(処理したとみなし)、次の処理に進む。そのうえで、最初のPromiseが満たされたとき、連鎖的にPromiseが満たされて結果が返ってくる。