こんにちは。 DSOC R&D グループの高橋寛治です。
桜の花びらが散り春の時候を感じる今日このごろ、スマブラだけでなくコーディングの季節もやってきたなと思っております。
意気込んでコーディングを始めたときに、たとえば2階層のリストを1階層に変形したり、条件に沿うものだけをリストから抽出したりする場合に、自分で実装してしまいがちです。
そのようなリストの各要素に対する処理について、高速かつメモリ効率のよい実装である itertools が Python の標準モジュールとして実装されています。 今回はこの itertools や言語処理に取り組んでいる際の適用例などを紹介します。
イテレータ
イテレータとは、公式マニュアルに書かれている通りですが、データの流れを示すオブジェクトです。
Pythonの内部処理としては、イテレータオブジェクトの __next__()
メソッドが呼ばれる、もしくは next()
に渡された場合に、次のデータを返します。
実際のコードを見てみましょう。
itertools.count
というのは、start
で値が始まり、呼び出すごとに step
の値分加算された値を返すというイテレータです。
>>> import itertools >>> iter_count = itertools.count(start=0, step=1): count(0) >>> iter_count.__next__() 0 >>> iter_count.__next__() 1 >>> next(iter_count) 2 >>> for c in itertools.count(start=0, step=1): ...: if c > 2: ...: break ...: print(c) 0 1 2
ここではいったん for
で読み取る場合には、 __next__()
が呼ばれると考えてください。
呼ばれるごとに加算された値が返っていることがわかるかと思います。
この itertools.count
ですが、次のコードとほぼ等価です(公式マニュアルより引用)。
def count(start=0, step=1): n = start while True: yield n n += step
ここで yield
式が出てきました。
これは、 next()
や __next__()
が呼ばれるたびに、 yield
で指定された値を返します。
上の例ですと、 while
による無限ループ内で yield
が記されており、 next()
のたびに n += step
と yield n
を繰り返します。
このように yield
式を持つ関数は、ジェネレータと呼ばれます。
itertoolsを使う
普段の言語処理で使えるいくつかの例を紹介します。 次の単語列(単語と品詞)を対象にします。
words = [["今日", "名詞"], ["は", "助詞"], ["天気", "名詞"], ["が", "助詞"], ["よく", "副詞"], ["花見", "名詞"], ["日和", "名詞"], ["だ", "助動詞"], ["。", "記号"]]
先頭から一要素ずつ追加する
>>> surfaces = [word[0] for word in words] >>> for item in itertools.accumulate(surfaces): ...: print(item) 今日 今日は 今日は天気 今日は天気が 今日は天気がよく 今日は天気がよく花見 今日は天気がよく花見日和 今日は天気がよく花見日和だ 今日は天気がよく花見日和だ。
先頭から走査して名詞ではなくなるまで取得
>>> for item in itertools.takewhile(lambda w: w[1] == "名詞", words): ...: print(item) ['今日', '名詞']
助詞、助動詞以外を取り出す
>>> for item in itertools.filterfalse(lambda w: w[1] not in {"助詞", "助動詞"}, words): ...: print(item) ['今日', '名詞'] ['天気', '名詞'] ['よく', '副詞'] ['花見', '名詞'] ['日和', '名詞'] ['。', '記号']
名詞の文内での組み合わせを取り出す(実際には、組み合わせを抽出した後に重複を排除したり、sortして語順を削除したりするかと思います)
>>> nouns = filter(lambda w: w[1] == "名詞", words) >>> for item in itertools.combinations(nouns, 2): ...: print(item) (['今日', '名詞'], ['天気', '名詞']) (['今日', '名詞'], ['花見', '名詞']) (['今日', '名詞'], ['日和', '名詞']) (['天気', '名詞'], ['花見', '名詞']) (['天気', '名詞'], ['日和', '名詞']) (['花見', '名詞'], ['日和', '名詞'])
名詞列を取り出す
>>> for k, g in itertools.groupby(words, key=lambda w: w[1] == "名詞"): ...: if k: ...: print(list(g)) [['今日', '名詞']] [['天気', '名詞']] [['花見', '名詞'], ['日和', '名詞']]
moreitertools を使う
More Itertools はより様々なイテレータを実装したライブラリです。
pip install more-itertools
でイントールします。
itertools
と同様にいくつか例をあげます。
配列のフラット化
>>> for w in more_itertools.flatten(words): ...: print(w) 今日 名詞 は 助詞 天気 名詞 が ...
二単語ずつ走査する
>>> for w in more_itertools.windowed(words, n=2): ...: print(w) (['今日', '名詞'], ['は', '助詞']) (['は', '助詞'], ['天気', '名詞']) (['天気', '名詞'], ['が', '助詞']) (['が', '助詞'], ['よく', '副詞']) (['よく', '副詞'], ['花見', '名詞']) (['花見', '名詞'], ['日和', '名詞']) (['日和', '名詞'], ['だ', '助動詞']) (['だ', '助動詞'], ['。', '記号'])
パディング
>>> print(list(more_itertools.padded(words, fillvalue=["HOGE", "FUGA"], n=12))) [['今日', '名詞'], ['は', '助詞'], ['天気', '名詞'], ['が', '助詞'], ['よく', '副詞'], ['花見', '名詞'], ['日和', '名詞'], ['だ', '助動詞'], ['。', '記号'], ['HOGE', 'FUGA'], ['HOGE', 'FUGA'], ['HOGE', 'FUGA']]
最後の要素を取得
>>> print(more_itertools.last(words)) ['。', '記号']
IOBESタグでチャンクごとに取り出す
>>> import more_itertools >>> tags ['S', 'O', 'B', 'E', 'B', 'I', 'I', 'E', 'O', 'O'] >>> list(more_itertools.split_before(tags, lambda x: x in ("S", "O", "B"))) [['S'], ['O'], ['B', 'E'], ['B', 'I', 'I', 'E'], ['O'], ['O']]
トークンが品詞や表層形といった属性を持っている場合は、 lambda x: x.pos
のように指定する
ライブラリを使って効率よく素早く実装する
itertools
と more_itertools
はいかがでしたでしょうか。
ループを用いたデータ走査処理は意外とバグを引き起こしやすいため、このようにしっかり実装されたモジュールを使うことは非常に有益です。
itertools
と more_itertools
はリファレンスを読むたびに、「あ!こんなイテレータがあるんだ!」となります。
慣れないうちはモジュールの使い方や実際にやりたいこととの紐付けが難しいですが、慣れてくると手堅く実装できるようになります。
ぜひ定期的にリファレンスを読んで、効率のよい実装を目指してください。
執筆者プロフィール
高橋寛治 Sansan株式会社 DSOC (Data Strategy & Operation Center) R&Dグループ研究員
阿南工業高等専門学校卒業後に、長岡技術科学大学に編入学。同大学大学院電気電子情報工学専攻修了。在学中は、自然言語処理の研究に取り組み、解析ツールの開発や機械翻訳に関連する研究を行う。大学院を卒業後、2017年にSansan株式会社に入社。現在はキーワード抽出など自然言語処理を生かした研究に取り組んでいる。