1つできるとN個には簡単にスケールできる件。二番煎じ感あるけど再び。
備忘:メモ書き___φ(. . )
Janomehttps://github.com/mocobeta/janome
word_cloudhttps://github.com/amueller/word_cloud
Janome
Janomeの形態素解析ではMeCabの辞書を使用していてパッケージのインストール時に辞書をビルドしている(setup.py、ipadic/build.sh、ipadic/build.py)。辞書に載っているか総当たりでチェックしている以下の処理がメイン処理かと思われる。
Tokenizerクラス
def __tokenize_partial ( self, text, wakati, baseform_unk, dotfile) :
if self. wakati and not wakati:
raise WakatiModeOnlyException
chunk_size = min ( len ( text) , Tokenizer. MAX_CHUNK_SIZE)
lattice = Lattice( chunk_size, self. sys_dic)
pos = 0
while not self. __should_split( text, pos) :
encoded_partial_text = text[ pos: pos + min ( 50 , chunk_size - pos) ] . encode( 'utf-8' )
if self. user_dic:
entries = self. user_dic. lookup( encoded_partial_text)
for e in entries:
lattice. add( SurfaceNode( e, NodeType. USER_DICT) )
matched = len ( entries) > 0
entries = self. sys_dic. lookup( encoded_partial_text, self. matcher)
for e in entries:
lattice. add( SurfaceNode( e, NodeType. SYS_DICT) )
matched = len ( entries) > 0
cates = self. sys_dic. get_char_categories( text[ pos] )
if cates:
for cate in cates:
if matched and not self. sys_dic. unknown_invoked_always( cate) :
continue
length = self. sys_dic. unknown_length( cate) \
if not self. sys_dic. unknown_grouping( cate) else self. max_unknown_length
assert length >= 0
buf = text[ pos]
for p in range ( pos + 1 , min ( chunk_size, pos + length + 1 ) ) :
_cates = self. sys_dic. get_char_categories( text[ p] )
if cate in _cates or any ( cate in _compat_cates for _compat_cates in _cates. values( ) ) :
buf += text[ p]
else :
break
unknown_entries = self. sys_dic. unknowns. get( cate)
assert unknown_entries
for entry in unknown_entries:
left_id, right_id, cost, part_of_speech = entry
base_form = buf if baseform_unk else '*'
dummy_dict_entry = ( buf, left_id, right_id, cost, part_of_speech, '*' , '*' , base_form, '*' , '*' )
lattice. add( Node( dummy_dict_entry, NodeType. UNKNOWN) )
pos += lattice. forward( )
lattice. end( )
min_cost_path = lattice. backward( )
assert isinstance ( min_cost_path[ 0 ] , BOS)
assert isinstance ( min_cost_path[ - 1 ] , EOS)
if wakati:
tokens = [ node. surface for node in min_cost_path[ 1 : - 1 ] ]
else :
tokens = [ ]
for node in min_cost_path[ 1 : - 1 ] :
if type ( node) == SurfaceNode and node. node_type == NodeType. SYS_DICT:
tokens. append( Token( node, self. sys_dic. lookup_extra( node. num) ) )
elif type ( node) == SurfaceNode and node. node_type == NodeType. USER_DICT:
tokens. append( Token( node, self. user_dic. lookup_extra( node. num) ) )
else :
tokens. append( Token( node) )
if dotfile:
lattice. generate_dotfile( filename= dotfile)
return ( tokens, pos)
ユーザー辞書にありますか?システム辞書にありますか?unknownですか?
形態素解析ってこういうことですね。
word_cloud
ワードクラウドは、パラメタお化けだけど柔軟なメソッドでコマンドのラッパーも用意されている。
classwordcloud.WordCloud(font_path=None, width=400, height=200, margin=2, ranks_only=None, prefer_horizontal=0.9, mask=None, scale=1, color_func=None, max_words=200, min_font_size=4, stopwords=None, random_state=None, background_color='black', max_font_size=None, font_step=1, mode='RGB', relative_scaling='auto', regexp=None, collocations=True, colormap=None, normalize_plurals=True, contour_width=0, contour_color='black', repeat=False, include_numbers=False, min_word_length=0, collocation_threshold=30)
中身は画像処理で、ワードクラウドの文字列をメモリ上に描画して、矩形の領域を取得して、ベースとなる画像に上書きしている。上書きするときに他の配置済みの文字列と重ならない場所を全画素、総当たりで判定して配置位置を決定している。
generate_from_frequencies関数
for word, freq in frequencies:
if freq == 0 :
continue
rs = self. relative_scaling
if rs != 0 :
font_size = int ( round ( ( rs * ( freq / float ( last_freq) )
+ ( 1 - rs) ) * font_size) )
if random_state. random( ) < self. prefer_horizontal:
orientation = None
else :
orientation = Image. ROTATE_90
tried_other_orientation = False
while True :
font = ImageFont. truetype( self. font_path, font_size)
transposed_font = ImageFont. TransposedFont(
font, orientation= orientation)
box_size = draw. textbbox( ( 0 , 0 ) , word, font= transposed_font, anchor= "lt" )
result = occupancy. sample_position( box_size[ 3 ] + self. margin,
box_size[ 2 ] + self. margin,
random_state)
・・・省略・・・
occupancy.sample_position()から呼び出されるquery_integral_image()が、「query_integral_image.pyx」に書かれていて「.pyx」の拡張子となっている。調べてみるとCythonのコードでCのソースを生成しているとのこと。
query_integral_image.pyx:35行 query_integral_image.c:21,510行
巨大な.cのコードは、#ifdefだらけで人間が読むコードではなくなっている。
「query_integral_image.c」は、setup.pyに拡張モジュールとして書かれていてパッケージのインストール時にコンパイルされてlib配下にsoファイルが生成されていた。(query_integral_image.cpython-311-darwin.so)
ext_modules= [ Extension( "wordcloud.query_integral_image" ,
[ "wordcloud/query_integral_image.c" ] ) ] ,
以下が、「query_integral_image.pyx:35行 」のソース
import array
import numpy as np
def query_integral_image ( unsigned int [ : , : ] integral_image, int size_x, int
size_y, random_state) :
cdef int x = integral_image. shape[ 0 ]
cdef int y = integral_image. shape[ 1 ]
cdef int area, i, j
cdef int hits = 0
for i in xrange ( x - size_x) :
for j in xrange ( y - size_y) :
area = integral_image[ i, j] + integral_image[ i + size_x, j + size_y]
area -= integral_image[ i + size_x, j] + integral_image[ i, j + size_y]
if not area:
hits += 1
if not hits:
return None
cdef int goal = random_state. randint( 0 , hits)
hits = 0
for i in xrange ( x - size_x) :
for j in xrange ( y - size_y) :
area = integral_image[ i, j] + integral_image[ i + size_x, j + size_y]
area -= integral_image[ i + size_x, j] + integral_image[ i, j + size_y]
if not area:
hits += 1
if hits == goal:
return i, j
画像全体をピクセル単位で走査して、そこに文字列を置けるか判定している。
area = integral_image[i, j] + integral_image[i + size_x, j + size_y]
area -= integral_image[i + size_x, j] + integral_image[i, j + size_y]
ただ、性能的な理由でsoファイルに切り出していると思われるけれど、なぜか全体を2回走査している。何かそれ相応の理由があるのか?
ライブラリの中身がざっくり分かったところでAPI Referenceを眺めていたら以下のメソッドを発見。
to_svg([embed_font, optimize_embedded_font, …])
to_svg()のソースはテキストの書き出し処理でダラダラ長いだけの面白くないソースなので割愛。
PNGファイルをSVGファイルに差し替えて一旦終わり。SVGファイル公開。
フォント周り
ワードクラウドのフォントを変更した場合、PNGファイルには反映されるけれどSVGファイルには反映されない現象が発生した。
SVGファイルのフォントはローカル環境にインストールされているフォントが使われて指定したフォントがないと代替のフォントで表示されるとのこと。
すべての環境で同じフォントを使用させたい場合、Webフォントを使用する必要があるが、それだとネットに繋がっていない場合や、フォントサーバが落ちている場合に現象が再現すると思われる。
さらに調べるとWebフォントをbase64にエンコードしてSVGファイルに添付している人がいた。フォントファイルを丸ごとエンコードするとファイルサイズが巨大になるので使用している文字だけ切り出してサブセットのフォントを生成する必要があるとのこと。
そしてサブセットのフォントを生成してSVGファイルに添付する場合、フォントの改変や再頒布が可能なライセンスが必要でありフォントのライセンスを調査した。
SIL Open Font License
Apache License 2.0
M+ FONT LICENSE
ライセンスや使用条件を確認しながらフリーフォントをダウンロードした。フリーフォントを公開している人に感謝です。ありがとうございます。
あとは修正作業。SVGファイルで使用している文字をpyftsubsetコマンドでフォントから切り出してきて、base64コマンドでエンコードして、SVGファイルを修正して、raspiにデプロイして、macOSとLinuxでbase64コマンドのオプションが異なるみたいなことにハマりつつ、SVGファイルにフリーフォントを反映できるようになった。最後にフォントをランダムに切り替える処理を追加して、バッチとして動くことを確認して、ミッションコンプリート!
1つできるまでが長い。0から1が長い。
正常ルートだけのやりたいことしか書いてないダメダメソースだけど40行から120行に膨らんでいる。
# word_cloudパッケージを6/1にインストールしていたので10日間ぐらいの作業メモ。読み返すと修正したくなる。書き足したくなる。推敲したくなる。けど、書き殴りのメモ書きが正解やんなあ?