Python自然言語処理テクニック集【基礎編】

日曜日, 3月 28, 2021

Python 自然言語処理

t f B! P L

自分がよく使用する日本語自然言語処理のテンプレをまとめたものです。

主に自分でコピペして使う用にまとめたものですが、みなさんのお役に立てれば幸いです。

環境はPython3系、Google Colaboratory(Ubuntu)で動作確認しています。

Pythonの標準機能とpipで容易にインストールできるライブラリに限定しています。

機械学習、ディープラーニングは出てきません!テキストデータの前処理が中心です。

前処理系

大文字小文字

日本語のテキストにも英語が出てくることはあるので。

s = "Youmou"
print(s.upper())
# YOUMOU
print(s.lower())
# youmou

全角半角

日本語だとこちらのほうが大事。
全角半角変換のライブラリはいくつかありますが、自分はjaconv派。

MIT Licenseで利用可能です。

import jaconv

print(jaconv.h2z("ヨウモウyoumou1234",
                 kana=True, digit=True, ascii=True))
# ヨウモウyoumou1234

print(jaconv.z2h("ヨウモウyoumou1234",
                 kana=True, digit=True, ascii=True))
# ヨウモウyoumou1234

他にも平仮名片仮名変換や、カタカナアルファベット変換が実装されてる(使ったこと無いけども)。

Unicode正規化

表記ゆれ削減のための定番の前処理。
下記動作例を見てもらえばわかりますが、実にいい感じに表記ゆれ削減してくれます。

import unicodedata
print(unicodedata.normalize("NFKC", 
 "Hello World")) 
# Hello World
print(unicodedata.normalize("NFKC", 
 "123456789")) 
# 123456789
print(unicodedata.normalize("NFKC", 
 "①②③④⑤⑥⑦⑧⑨")) 
# 123456789
print(unicodedata.normalize("NFKC", 
 "ヨウモウデス")) 
# ヨウモウデス
print(unicodedata.normalize("NFKC", 
 "㌔㍍㌘㌧")) 
# キロメートルグラムトン

正規化の方法にはNFD, NFC, NFKD, NFKCの4つがあるが、NFKCが1番メジャー。

これらの前処理はデータ分析のときは積極的に使っています。

一方で、ユーザーから入力データを受け取って、そのデータを再表示するシステム(ブログシステムとかメモアプリとか)では、前処理すると「入力データが書き換えられている!」という印象を与えかねないため、使うべきではないかと。

文字種

文字の一覧がほしいケース。
例えば、ある文字列がアルファベットのみからなるかどうかを判例するには、アルファベットの一覧が必要。

標準機能の範囲内

数字、アルファベット

数字、アルファベットは標準ライブラリにある。

import string
print(string.digits) 
# 0123456789

print(string.ascii_lowercase) 
# abcdefghijklmnopqrstuvwxyz

print(string.ascii_uppercase) 
# ABCDEFGHIJKLMNOPQRSTUVWXYZ

平仮名、片仮名

平仮名、片仮名はコードポイントと組み込みメソッドを組み合わせます。

Python3の文字列は、Unicodeコードポイントのリスト。
Unicodeとは、世界中のいろいろな文字をコードポイントという数字に対応させたものです。

例えば、ひらがなのUnicodeコードポイントは以下。

https://ja.wikipedia.org/wiki/平仮名_(Unicodeのブロック)

「あ」なら「3042」(16進数)とある。
3042(16進数)は、10進数に直すと12354になる。

Python3には

  • ord(): 文字→10進数コードポイント
  • hex(): 10進数→16進数
  • chr(): コードポイント→文字

というメソッドがある。

こんな感じの動き。

print(ord("あ")) # 12354
print(hex(ord("あ"))) # 0x3042
print(chr(12354)) # "あ"
print(chr(0x3042)) # "あ"

平仮名、片仮名はコードポイントが連続しているので以下の要領でリストを作れる。小文字とかが入るけどね。

print([chr(i) for i in range(ord("ぁ"), ord("ん")+2)]) 
# ['ぁ', 'あ', 'ぃ', 'い', 'ぅ', 'う', 'ぇ', 'え', 'ぉ', 'お', ...

print([chr(i) for i in range(ord("ァ"), ord("ン")+2)]) 
# ['ァ', 'ア', 'ィ', 'イ', 'ゥ', 'ウ', 'ェ', 'エ', 'ォ', 'オ', ...

regexライブラリを使う

漢字

コードポイントの考え方で漢字も対応できるのですが、漢字のコードポイントは完全に連番になっておらず、いくつかの区間に分かれている。

そこでより簡便に対応するために正規表現のUnicode文字クラスのプロパティを使う。

この方法では文字種の一覧(リスト)は直接得られないが、リストを用意する目的はたいていパターンマッチングなので、正規表現によるマッチングができればそれで十分はケースが多い。
というかマッチングなら正規表現のほうが断然強い。

\p{XXX}でUnicode文字クラスXXXにマッチする。

Pythonの標準正規表現ライブラリreは、Unicode文字クラスに対応していない。
そこでサードパーティ製ライブラリregex(Apache 2.0 License)を使う。

import regex

s = "こんな綺麗な羊毛は見たことがないよ"
print(regex.findall("\p{Han}+", s))
# ['綺麗', '羊毛', '見']

\p{Hiragana}\p{Katakana}もあるので、
自分の場合、regexを使う場合はUnicode文字クラスで統一、
使わない標準機能の範囲内で実装することが多い。

形態素解析

SudachiPy

形態素解析ライブラリも色々あり、処理速度や辞書ファイルによって選択肢が絞られるケースはあるものの、特に制約がなければSudachiPyがおすすめ。

後発であるためか、プログラム、辞書ファイルSudachiDictともにApache 2.0 Licenseでライセンス周りが安心な点と、
後述するGiNZAにも形態素解析ライブラリとして組み込まれていて親和性が高い。

機械学習プログラムの権利関係は複雑であり、機械学習プログラムの権利、データセットの権利(元データとアノテーションデータそれぞれ)、モデルファイルの権利に分かれ、これらが別々であることもあります。

プログラムの権利は、大半のケースでソフトウェアライセンスを見れば解決するのですが、モデルファイルや元データの権利は微妙です。

学習済みモデルを利用する場合は、元データは利用しないので、ソースコードのライセンスとモデルファイルのライセンスだけクリアできればOKです。

ですが、モデルファイルが複雑で、
ソフトウェアの一部なのでソフトウェアライセンスがそのまま適用されている例がある一方、
元データの権利がGPLやCC-BY-SAの場合に、ライセンスを継承しているケース、していないケースがあり、どっちなんだ?という状況です。
デファクトの考え方が定まっていない印象ですね。

肌感としては、最近のものに関しては、ライセンス継承が必要なら安全側にふってちゃんと継承しているケースや、
元データの権利者に許可をとってライセンスを変更しているケースが多い印象です。

このあたりの事情はまた別記事に書きたいと思います。
とりあえずSudachiPyはそのあたりがはっきりしているので安心して使えます。

from sudachipy import tokenizer
from sudachipy import dictionary

tokenizer_obj = dictionary.Dictionary().create()

s = "自然言語処理"
mode = tokenizer.Tokenizer.SplitMode.C
print([m.surface()  for m in tokenizer_obj.tokenize(s, mode)])
# => ['自然言語処理']

mode = tokenizer.Tokenizer.SplitMode.B
print([m.surface()  for m in tokenizer_obj.tokenize(s, mode)])
# => ['自然', '言語', '処理']

mode = tokenizer.Tokenizer.SplitMode.A
print([m.surface()  for m in tokenizer_obj.tokenize(s, mode)])
# => ['自然', '言語', '処理']

m = tokenizer_obj.tokenize("話す", mode)[0]

print(m.surface())  
# => '話す'
print(m.dictionary_form())  
# => '話す'
print(m.reading_form())  
# => 'ハナス'
print(m.part_of_speech())  
# => ['動詞', '一般', '*', '*',
#     '五段-サ行', '終止形-一般']

分かち書き、品詞解析、辞書系、読みなど基本的な形態素解析の機能はある。
分かち書きの単位が複数あるのが便利だったりする。

単語出現頻度

Counterを使う。

texts = [
  "羊毛かわいい",
  "羊毛が綺麗",
  "羊毛って美しい"
]

texts_processed = [
  [m.surface() for m 
   in tokenizer_obj.tokenize(s, mode)]
  for s in texts
]

from collections import Counter
c = Counter(sum(texts_processed,  []))
print(c.most_common())
# [('羊毛', 3), ('かわいい', 1), 
#  ('が', 1), ('綺麗', 1), 
#  ('って', 1), ('美しい', 1)]

品詞フィルタリング

トークンごとに品詞を見てフィルタ。

texts_processed = [
  [m.surface() for m 
   in tokenizer_obj.tokenize(s, mode)
   if m.part_of_speech()[0] in ["名詞", "形容詞"]
  ]
  for s in texts
]

print(texts_processed)
# [['羊毛', 'かわいい'], ['羊毛'], ['羊毛', '美しい']]

品詞チャンキング

いくつかの単語をチャンクというざっくりとした句の単位にまとめることをチャンキングや浅い構文解析といいます。

あんまりちゃんとした定義はないようですが、句とは「形容詞+名詞」「AのB」などの部分単語列を指します(私は)。

単語列に1対1で対応する同じ長さの文字列を作り、正規表現でマッチングさせます。

形容詞+名詞

s="美しい言語とかわいい言語"

ts = []
ss = ""

for m in tokenizer_obj.tokenize(s, mode):
    t = m.surface()
    ts += [t]

    # posを見て文字列を作る
    pos = m.part_of_speech()[0]
    if pos=="名詞":
        s = "n"
    elif pos=="形容詞":
        s = "a"
    else:
        s = "o"
    ss += s

found = regex.finditer("a*n+", ss)

for f in found:
    s,e = f.span()
    print(s, e, "".join(ts[s:e]))

# 0 2 美しい言語
# 3 5 かわいい言語

AのB

s="日本語の文法と英語の文法"

ts = []
ss = ""

for m in tokenizer_obj.tokenize(s, mode):
    t = m.surface()
    ts += [t]

    # posを見て文字列を作る
    pos = m.part_of_speech()[0]
    if pos=="名詞":
        s = "n"
    elif t=="の":
        s = "x"
    else:
        s = "o"
    ss += s

found = regex.finditer("n+xn+", ss)

for f in found:
    s,e = f.span()
    print(s, e, "".join(ts[s:e]))

0 4 日本語の文法
5 8 英語の文法

「AのB」でわかるように、正規表現でマッチさせる文字列を柔軟に設計できるので、応用範囲が広いです。

GiNZAの便利機能

GiNZAは、日本語向けの高機能な自然言語処理ライブラリ。
目玉機能として係り受け解析があります。

ソースコード、モデルファイルともにMIT Licenseで商用利用可能。
自然言語処理ライブラリspaCyのインターフェイスで利用できる、内部的にSudachiPyを使っている等、私がいうのもあれですがとても使いやすいライブラリです。

係り受け解析だとCabochaが有名ですが、Cabochaの学習済みモデルって商用利用禁止なんですよね。気をつけないといけません。

と、ここまでいってなんですが、私は係り受け解析はあまり使っていません。
係り受け解析って最初に見たときは、おー!こんなことまで出来るんか!とすごく感動したのを覚えているのですが、アプリケーションに組み込むなどして使いこなしている例をあまり知りません(私の知見が狭いだけの可能性は高いですが)。

ただ他の付属機能がかなり便利なので愛用してます。

文分割

nlp = spacy.load("ja_ginza")
s="羊毛さんは自然言語処理が大好きです。でも言語学にはあまり詳しくないです。残念ながら。"

doc = nlp(s)

for sent in doc.sents:
    print(sent)

# 羊毛さんは自然言語処理が大好きです。
# でも言語学にはあまり詳しくないです。
# 残念ながら。

改行で分かれていなくても、句読点などでいい感じに文に分割してくれます。

分散表現

nlp = spacy.load("ja_ginza")
s="""羊毛さんは自然言語処理が大好きです。"""

doc = nlp(s)

for sent in doc.sents:
    for token in sent:
        print(token, token.vector[:3])

# 羊毛 [-0.21420652 -0.07363316 0.07813244]
# さん [ 0.20469043 0.0187766 -0.07560689]
# は [-0.05035316 -0.15731327 -0.08336552]
# 自然言語処理 [0. 0. 0.] 
# が [-0.05846101 -0.1564513 -0.05254956] 
# 大好き [-0.08251447 -0.19263963 0.067964 ]
# です [ 0.04646556 -0.19055834 -0.17462325] 
# 。 [ 0.12551211 -0.1708449 0.06161143]

単語(token)のvectorに300次元の分散表現がつきます。
単語によってはあったりなかったりします。
分散表現の次元数もモデルによります。

ちなみにsent.vectorで文ベクトル、doc,vectorで文章ベクトルがとれます。とはいっても単語ごとの分散表現の平均値になります。

固有表現

nlp = spacy.load("ja_ginza")
s="""羊毛さんは自然言語処理が大好きです。"""

doc = nlp(s)

for sent in doc.sents:
    for ent in sent.ents:
        print(ent.text, 
              ent.start_char, 
              ent.end_char, 
              ent.label_)

# 羊毛 0 2 Person
# さん 2 4 Title_Other
# 自然言語処理 5 11 Product_Other

entsで固有表現がとれます。
ラベルが役立ちます。

ストップワード

色々なNLPライブラリにストップワードリストが付属しています。
GiNZAにもついています。
私は色々なライブラリからとってきたストップワードリストをマージしてファイルに保存してます。

どのライブラリのものも目視チェックできる程度の分量なので、自分で管理しておいて案件で自由に使えるようにしておくのがよいでしょう。

stopwords = nlp.Defaults.stop_words
print(sorted(list(stopwords))[:5])
# ['あ', 'あっ', 'あまり', 'あり', 'ある']

ストップワードフィルタリングの実装は、形態素解析したあとの品詞フィルタリングと同様です。

QooQ