自然言語処理(NLP)の前処理に関するTips

仕事で自然言語処理NLP)に少し取り組む必要が出てきたので、自分なりの理解をTipsとしてまとめていこうと思います。

小文字化

文字の正規化という意味で、アルファベットを小文字化します。日本語の場合は、半角を全角に統一する、などの対応も必要と思います。

sentences: List[str] = ['I  have  a pen', 'That  is a window']
print(sentences)
# -> ['I  have  a pen', 'That  is a window']

lower_sentences: List[str] = list(
    sentence.lower() for sentence in sentences
)
print(lower_sentences)
# -> ['i  have  a pen', 'that  is a window']

tokenize

文書をトークン(最小単位の文字や文字列)に分割します。分割には、nltk(自然言語処理のツールキットを提供するライブラリ)を利用します。英語なのでpunktパッケージを使っています。
文書のリストをトークンに分割するサンプルは以下の通りです。

import nltk
nltk.download('punkt')
from typing import List
sentences: List[str] = ['i  have  a pen', 'that  is a window']
print(sentences)
# -> ['i  have  a pen', 'that  is a window']

words_list: List[List[str]] = list(
    nltk.tokenize.word_tokenize(sentence) for sentence in sentences
)
print(words_list)
# -> [['i', 'have', 'a', 'pen'], ['that', 'is', 'a', 'window']]

stop-words除外

stop-wordとは、the,a,for,ofのような一般語など、分析に影響を与えない単語のことで、これらを除外することによって後続処理の計算量を下げることができます。nltkstopwordsが定義されているので、そちらを利用するサンプルを記載します。

from typing import List
import nltk
nltk.download('stopwords')

words_list: List[List[str]] = [['i', 'have', 'a', 'pen'], ['that', 'is', 'a', 'window']]
stopwords: List[str] = nltk.corpus.stopwords.words('english')
print(stopwords)
# -> ['i', 'me', 'my', 'myself', 'we', 'our', 'ours', ...(省略)]

normalized_words_list: List[List[str]] = list(
    list(word for word in words if word not in stopwords) for words in words_list
)
print(normalized_words_list)
# -> [['pen'], ['window']]

上記以外に、記号や1文字の英数字を除外したいケースもあると思います。その場合はstringパッケージを使って文字を取得して、それらを使って除外すると楽だと思います。

from typing import List
import string
exclude_words: List[str] = list(string.ascii_lowercase) + list(string.digits) + list(string.punctuation)
print(exclude_words)
# -> ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '!', '"', '#', '$', '%', '&', "'", '(', ')', '*', '+', ',', '-', '.', '/', ':', ';', '<', '=', '>', '?', '@', '[', '\\', ']', '^', '_', '`', '{', '|', '}', '~']

シノニムの適用

TBD

ステミング(stemming)

ステミングとは、語尾が変化する単語の語幹部分を抜き出す処理のことを言います。そこだけ抜き出すと人間には違和感がある文字列になることもあります。
nltkPorterStemmerを使うサンプルを記載しました。mechanicalmechanになったり、pencilspencilに変換される一方で、wentgoにはならないようです(語幹部分を抜き出しただけなので)。

import nltk
from nltk.stem.porter import PorterStemmer
ps: PorterStemmer = PorterStemmer()
words: List[str] = ['mechanical', 'pencil', 'go', 'went', 'goes', 'pencils']
print(words)
# -> ['mechanical', 'pencil', 'go', 'went', 'goes', 'pencils']

stemmed_words: List[str] = list(ps.stem(word) for word in words)
print(stemmed_words)
# -> ['mechan', 'pencil', 'go', 'went', 'goe', 'pencil']

コーパス

コーパスというのは、自然言語処理を行いやすい形に、構造化されたデータのことをいうらしいです。自分がこれまで見たものはコーパス化=ベクトル化のようでした。
words_listを、各要素が単語、その値が出現回数を表すベクトルに変換するサンプルは以下の通りです。

from typing import List, Tuple
from gensim import corpora

words_list: List[List[str]] = [['i', 'have', 'a', 'red', 'pen', 'and', 'a', 'blue', 'pen'], ['you', 'like', 'red']]
print(words_list)
# -> [['i', 'have', 'a', 'red', 'pen', 'and', 'a', 'blue', 'pen'], ['you', 'like', 'red']]

dictionary: corpora.Dictionary = corpora.Dictionary(words_list)
print(dictionary.token2id)
# -> {'a': 0, 'and': 1, 'blue': 2, 'have': 3, 'i': 4, 'pen': 5, 'red': 6, 'like': 7, 'you': 8}

corpus: List[List[Tuple[int, int]]] = list(map(dictionary.doc2bow, words_list)) # ベクトル化
print(corpus)
# -> [[(0, 2), (1, 1), (2, 1), (3, 1), (4, 1), (5, 2), (6, 1)], [(6, 1), (7, 1), (8, 1)]]

dictionaryは単語文字列 -> ID(連番。int)に変換する辞書です。文字列をそのまま扱うとメモリを食うのでintに変換しているのだとも思います。
ここで作成したcopusは、列が単語(正確には単語ID)行が文書を意味する行列と考えることができます。1行=1文書ベクトルです。
なおdoc2bowbowbag-of-wordsの略で、文書をbag-of-wordsに変換する関数になります。bag-of-wordsは直訳すると単語の袋で、単語を袋にまとめて(つまり単語でグルーピングして)、その袋の中にメタ情報を持たせるというイメージのようです。このケースではメタ情報として単語の出現回数を保持しています。