仕事で自然言語処理(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
のような一般語など、分析に影響を与えない単語のことで、これらを除外することによって後続処理の計算量を下げることができます。nltk
でstopwords
が定義されているので、そちらを利用するサンプルを記載します。
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)
ステミングとは、語尾が変化する単語の語幹部分を抜き出す処理のことを言います。そこだけ抜き出すと人間には違和感がある文字列になることもあります。
nltk
のPorterStemmer
を使うサンプルを記載しました。mechanical
がmechan
になったり、pencils
がpencil
に変換される一方で、went
はgo
にはならないようです(語幹部分を抜き出しただけなので)。
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文書ベクトルです。
なおdoc2bow
のbow
はbag-of-words
の略で、文書をbag-of-words
に変換する関数になります。bag-of-words
は直訳すると単語の袋
で、単語を袋にまとめて(つまり単語でグルーピングして)、その袋の中にメタ情報を持たせるというイメージのようです。このケースではメタ情報として単語の出現回数を保持しています。