- 1. はじめに
- 2. 文字・語・特徴語:情報検索のためのテキスト表現
はじめに
先日開催した社内勉強会では、書籍『Pythonではじめる 情報検索プログラミング』を教材として、情報検索の基礎について学びました。
本記事では、その中でも、情報検索を理解するうえで土台となる 「文字」「語」「特徴語」 について整理します。
人間は文章を読むと、そこに書かれた意味を自然に理解できます。しかし、コンピュータが最初に受け取るのは、意味そのものではなく、文字コードによって表現されたデータです。
そのデータを検索や分類に利用するためには、まず文字列として正しく読み込み、次に「語」や「形態素」の単位へ分割し、その中から文書の内容をよく表す「特徴語」を選ぶ必要があります。
今回は、次の流れに沿って整理します。
- 文字とは何か
- 語とは何か
- 特徴語とは何か
- 文字列を検索・分析用の数値表現へ変換する方法
書籍で扱われている考え方をもとにしつつ、文章・図・サンプルコードは、内容を理解しやすいように整理し直して紹介します。
書籍リンク:https://www.morikita.co.jp/books/mid/081861
文字・語・特徴語:情報検索のためのテキスト表現
文字・語・特徴語とは
最初に、3つの言葉の違いを整理します。
文字とは
文字とは、文章を構成する記号の単位です。
たとえば、次の文章を考えます。
京都の寺院文化を調べる
この文章は、「京」「都」「の」「寺」「院」……という文字の並びで構成されています。
文字には、漢字、ひらがな、カタカナ、英数字、句読点、空白、記号などがあります。
人間にとって文字は目に見える記号ですが、コンピュータ内部では文字そのものが保存されているわけではありません。各文字は文字コードを介して数値へ対応づけられ、最終的にはバイト列として保存されます。
したがって、情報検索の最初の段階では、まずテキストデータを正しい文字列として読み込めることが重要になります。
語とは
語とは、意味や文法上のまとまりを持つ単位です。
先ほどの文章は、次のように分けられます。
京都|の|寺院|文化|を|調べる
「京都」「寺院」「文化」は、それぞれ意味を持つ語です。「の」「を」も文法上の役割を持つ語です。
英語では、単語の間に空白が入るため、空白を手がかりに分割できる場合が多くあります。
一方、日本語には通常、語と語の間に空白がありません。そのため、日本語の文章を語の単位で扱うには、どこで区切るかを推定する処理が必要です。
この処理に使われる代表的な方法が、形態素解析です。
特徴語とは
特徴語とは、文書の内容や主題をよく表す語です。
先ほどの文章に含まれる語をすべて並べると、次のようになります。
京都|の|寺院|文化|を|調べる
この中で文書の内容を把握する手がかりになりやすいのは、「京都」「寺院」「文化」などです。
一方、「の」「を」のような語は、多くの文章に現れるため、特定の文書を区別する情報としては弱い場合があります。
特徴語を取り出すときは、不要語や品詞によって候補を絞り、その後、出現頻度やTF-IDFなどを使って「その文書らしさ」を評価します。

文字・語・特徴語の関係をまとめると、次のようになります。
| 単位 | 意味 | 例 |
|---|---|---|
| 文字 | 文章を構成する記号の単位 | 京、都、の、寺、院 |
| 語 | 意味や文法上のまとまり | 京都、寺院、文化、調べる |
| 特徴語 | 文書の内容をよく表す語 | 京都、寺院、文化 |
文字を扱う
文字列による検索
最も単純な検索方法は、指定した文字列が文書の中に含まれているかを調べることです。
Pythonでは、in 演算子を使って確認できます。
query = "京都"
text1 = "京都には多くの寺院がある"
text2 = "奈良には多くの寺院がある"
print(query in text1)
print(query in text2)
実行結果は次のとおりです。
True
False
ファイルを検索する場合も、まずファイルの内容を文字列として読み込み、その中にクエリが含まれるかを調べます。
def contains_query(filename, query):
with open(filename, "r", encoding="utf-8") as f:
text = f.read()
return query in text
この方法は簡単ですが、あくまで「文字の並び」が一致するかを見ています。
そのため、「京都」を検索したときに、「東京都」という語まで一致してしまう場合があります。
文字列が一致していても、語として一致しているとは限りません。この問題は、後ほど扱う形態素解析によって語の単位へ分割することで軽減できます。
文字コード・エンコード・デコード
コンピュータでは、文字は数値として表現されます。
文字と数値の対応関係を定めたものが 文字コード です。また、文字列をバイト列へ変換する処理を エンコード、バイト列を文字列へ戻す処理を デコード と呼びます。
text = "京都"
encoded = text.encode("utf-8")
print(encoded)
print(list(encoded))
実行結果は次のとおりです。
b'\xe4\xba\xac\xe9\x83\xbd'
[228, 186, 172, 233, 131, 189]
UTF-8では、「京」と「都」はそれぞれ複数のバイトで表現されます。
同じ文字でも、UTF-8、Shift_JIS、EUC-JPなど、使うエンコーディングによってバイト列は異なります。

ファイルを読み込むときは、保存時に使われたエンコーディングと、読み込み時のエンコーディングを一致させる必要があります。
with open("sample.txt", "r", encoding="utf-8") as f:
text = f.read()
一致しない場合は、UnicodeDecodeError が発生したり、文字化けしたりします。
エンコーディングがわからない場合には、バイト列から推定する方法もあります。
import chardet
data = "明日、京都に行きます".encode("euc_jp")
result = chardet.detect(data)
print(result)
ただし、推定は常に正しいとは限りません。特に短い文字列では、判断材料が少ないため誤判定が起こりやすくなります。
文字コードの推定結果は、確定情報ではなく、読み込み方法を判断するための手がかりとして扱う必要があります。
文字Nグラム
大量の文書から文字列を探すためには、検索に先立って索引を作る方法があります。
索引の見出しとして、単語を使う方法もありますが、そのためには文章を語へ分割しなければなりません。
語へ分割せず、文字列そのものから見出しを作る方法が 文字Nグラム です。
Nグラムとは、隣り合うN個の要素をまとめたものです。要素を文字とした場合が文字Nグラムです。
- N=1:ユニグラム
- N=2:バイグラム
- N=3:トライグラム
def get_char_ngrams(text, n):
return [
text[i:i + n]
for i in range(len(text) - n + 1)
]
text = "情報検索"
print(get_char_ngrams(text, 1))
print(get_char_ngrams(text, 2))
print(get_char_ngrams(text, 3))
実行結果は次のとおりです。
['情', '報', '検', '索']
['情報', '報検', '検索']
['情報検', '報検索']
文字Nグラムは、単語辞書や形態素解析器がなくても作成できます。新語、固有名詞、未知語、表記ゆれを含む文章でも、部分的な一致を捉えやすいという利点があります。
一方で、「京都」というバイグラムは「東京都」にも含まれます。このため、意味上の語の境界とは異なる位置でも一致します。

文字列検索、文字Nグラム、語単位検索には、それぞれ次の特徴があります。
| 方法 | 特徴 | 注意点 |
|---|---|---|
| 文字列の部分一致 | 実装が簡単 | 語の境界を考慮できない |
| 文字Nグラム | 辞書がなくても作れる | 意味とは異なる部分一致が生じる |
| 語単位の検索 | 意味のある単位で照合できる | 形態素解析の精度に影響される |
正規表現
正規表現は、文字列の並びをパターンとして表現する方法です。
単純な文字列検索では、固定された文字列しか探せません。正規表現を使うと、繰り返し、選択、任意の文字、文字数などを条件として指定できます。
たとえば、「寺」という文字の前後を数文字ずつ抜き出すと、検索結果のスニペットを作れます。
import re
text = "京都には清水寺や金閣寺など、多くの寺院があります。"
matches = re.findall(r".{0,4}寺.{0,4}", text)
print(matches)
実行結果は次のとおりです。
['には清水寺や金閣寺', '、多くの寺院があり']
よく使われる正規表現の要素には、次のようなものがあります。
| 表現 | 意味 |
|---|---|
. |
任意の1文字 |
* |
直前の要素の0回以上の繰り返し |
+ |
直前の要素の1回以上の繰り返し |
? |
直前の要素が0回または1回 |
{n} |
直前の要素のn回の繰り返し |
{n,m} |
n回以上m回以下の繰り返し |
[...] |
指定した文字のいずれか |
(...) |
グループ化・抽出 |
正規表現は、検索条件を柔軟に記述できますが、文の意味を理解しているわけではありません。あくまで文字のパターンを扱う方法です。
語を扱う
語と形態素
文章は語によって構成されていますが、形態素解析では、語をさらに文法上意味を持つ最小単位へ分けることがあります。
この最小単位を 形態素 と呼びます。
たとえば、「大爆発」という語は、接頭辞の「大」と名詞の「爆発」に分けられます。
大爆発
↓
大(接頭辞)+爆発(名詞)
また、「走った」は、動詞の語幹や活用に関する情報を持っています。
形態素解析では、単に文章を区切るだけでなく、表層形、品詞、原形、読みなどの情報も得られます。
形態素解析
英語は空白を手がかりに単語へ分割できますが、日本語では空白がないため、分割候補を作り、その中から最も適切だと考えられる分割を選びます。
Pythonで利用できる代表的な日本語形態素解析器には、次のようなものがあります。
| ツール | 特徴 |
|---|---|
| Janome | Pythonだけで利用しやすく、導入が比較的簡単 |
| MeCab | 高速で広く使われており、利用できる辞書も多い |
| SudachiPy | 分割の細かさを複数のモードから選べる |
ここではJanomeを使います。
from janome.tokenizer import Tokenizer
tokenizer = Tokenizer()
text = "すもももももももものうち"
for token in tokenizer.tokenize(text):
pos = token.part_of_speech.split(",")[0]
print(token.surface, pos)
実行すると、文章が形態素に分割され、それぞれに品詞が付与されます。
表層形だけを取り出す処理を 分かち書き と呼びます。
from janome.tokenizer import Tokenizer
tokenizer = Tokenizer(wakati=True)
text = "すもももももももものうち"
words = list(tokenizer.tokenize(text))
print(words)
分かち書きによって、文字列を語や形態素のリストへ変換できます。
辞書と未知語
多くの形態素解析器は、あらかじめ用意された辞書を使って分割候補を作り、その中から最も妥当な結果を選びます。
たとえば、「東京都の」という文字列には、次のような分割候補が考えられます。
東京都|の
東京|都|の
東|京都|の
辞書に登録された語や品詞、接続しやすさなどを手がかりに、最も自然な候補が選ばれます。
しかし、辞書にない固有名詞、商品名、専門用語、新語などは、意図しない形に分割されることがあります。
このような語を 未知語 と呼びます。
未知語を適切に扱う方法の1つが、ユーザー辞書です。
ふなっしー,名詞,フナッシー
形態素解析器へユーザー辞書を指定することで、独自の固有名詞や専門用語を1語として認識させられます。
検索精度を上げるには、検索アルゴリズムだけでなく、分析対象に合った辞書を整備することも重要です。
語単位による検索
クエリと検索対象をそれぞれ分かち書きし、語のリスト同士を照合すると、単純な部分文字列検索による誤検索を減らせます。
from janome.tokenizer import Tokenizer
tokenizer = Tokenizer(wakati=True)
query = "京都"
text = "東京都の市場について調べる"
query_words = list(tokenizer.tokenize(query))
text_words = list(tokenizer.tokenize(text))
print(query_words)
print(text_words)
print(query_words[0] in text_words)
形態素解析が次のように分割した場合、
クエリ:['京都']
対象:['東京都', 'の', '市場', 'について', '調べる']
語「京都」は対象に含まれないため、不一致と判断できます。
ただし、語単位検索の結果は形態素解析の分割結果に依存します。正しい語の単位で分割できなければ、検索結果にも影響します。
Bag-of-Words
語へ分割した文書を、検索や分類で扱える数値へ変換する代表的な方法が Bag-of-Words です。
Bag-of-Wordsでは、語の出現順序を無視し、語の種類と出現回数だけを残します。
数学的には、文書を語の多重集合、つまり同じ要素の重複を許す集合として表していると考えられます。

次の3つの文書を数値化します。
docs = {
"文書A": "京都 京都 寺院 文化",
"文書B": "奈良 寺院 仏像 文化",
"文書C": "東京 企業 市場 投資",
}
from sklearn.feature_extraction.text import CountVectorizer
import pandas as pd
texts = list(docs.values())
doc_names = list(docs.keys())
vectorizer = CountVectorizer(
token_pattern=r"(?u)\b\w+\b"
)
X = vectorizer.fit_transform(texts)
terms = vectorizer.get_feature_names_out()
bow_df = pd.DataFrame(
X.toarray(),
index=doc_names,
columns=terms
)
bow_df
実行結果は次のとおりです。
京都 仏像 企業 奈良 寺院 市場 投資 文化 東京
文書A 2 0 0 0 1 0 0 1 0
文書B 0 1 0 1 1 0 0 1 0
文書C 0 0 1 0 0 1 1 0 1
行は文書、列は語、各要素はその語の出現回数です。
これにより、文章をベクトルとして扱えるようになります。
一方で、Bag-of-Wordsでは語順が失われます。
犬が人を追う
人が犬を追う
この2文は、含まれる語だけを見ると非常に近い表現になります。語順や文法構造が重要な場合には、単語Nグラムなどを利用する必要があります。
ワードクラウド
Bag-of-Wordsの頻度情報を視覚化する方法の1つが ワードクラウド です。
ワードクラウドでは、出現頻度の高い語を大きく、頻度の低い語を小さく表示します。
これにより、文書中でよく使われている語を直感的に確認できます。
ただし、頻度が高い語が、そのまま文書を特徴づける語になるとは限りません。
たとえば、小説では「私」「人」「こと」のような一般的な語が何度も現れます。これらは文書内では目立ちますが、他の多くの文書にも現れるため、その作品だけの特徴とは限りません。
Zipfの経験則
大量の文書で語の出現頻度を数えると、語の頻度には大きな偏りが見られます。
少数の語が非常に高い頻度で現れ、多数の語は少ししか現れません。
語を頻度の高い順に並べると、順位が下がるほど頻度が急激に小さくなる傾向があります。これを Zipfの経験則 と呼びます。

この性質から、次のことがわかります。
- 高頻度語だけを使うと、どの文書にも現れる一般的な語が多くなる
- 低頻度語だけを使うと、誤記や偶然現れた語まで含まれやすい
- 文書を特徴づけるには、文書内の頻度と文書集合全体での珍しさを両方見る必要がある
この考え方が、次に扱う特徴語とTF-IDFにつながります。
特徴語を扱う
特徴語とは
特徴語とは、文書の主題や内容を表し、その文書を他の文書と区別する手がかりになる語です。
特徴語には、少なくとも次の2つの性質が求められます。
- 対象の文書内で繰り返し現れる
- 多くの別の文書には現れない
たとえば、京都観光について書かれた文書で「京都」「寺院」「文化」が繰り返し現れるなら、それらは特徴語の候補になります。
一方、「の」「は」「する」などは頻繁に現れますが、多くの文書で使われるため、文書を区別する情報としては弱い語です。
不要語
文書に含まれていても、特徴語として利用しにくい語を 不要語(stop word) と呼びます。
不要語を除く代表的な方法は、次の2つです。
不要語リストを使う
あらかじめ不要語のリストを用意し、それに含まれる語を除外します。
stop_words = {
"の", "は", "が", "を", "に",
"する", "いる", "ある"
}
words = ["京都", "の", "寺院", "を", "紹介", "する"]
filtered = [
word
for word in words
if word not in stop_words
]
print(filtered)
実行結果は次のとおりです。
['京都', '寺院', '紹介']
品詞を使う
形態素解析の品詞情報を使い、助詞、助動詞、記号などを除外できます。
from janome.analyzer import Analyzer
from janome.tokenfilter import (
POSStopFilter,
ExtractAttributeFilter,
)
analyzer = Analyzer(
token_filters=[
POSStopFilter(["助詞", "助動詞", "記号"]),
ExtractAttributeFilter("surface"),
]
)
text = "京都とは違う奈良の魅力"
words = list(analyzer.analyze(text))
print(words)
逆に、特徴語になりやすい品詞だけを残す方法もあります。
from janome.analyzer import Analyzer
from janome.tokenfilter import (
POSKeepFilter,
ExtractAttributeFilter,
)
analyzer = Analyzer(
token_filters=[
POSKeepFilter(["名詞"]),
ExtractAttributeFilter("surface"),
]
)
text = "京都とは違う奈良の魅力"
nouns = list(analyzer.analyze(text))
print(nouns)
名詞は、人物、場所、物、概念などを表すため、文書の主題を把握する手がかりになりやすい品詞です。
ただし、目的によって必要な品詞は変わります。感情を分析する場合は形容詞が重要になり、行動を分析する場合は動詞が重要になることがあります。
不要語は固定的なものではなく、検索や分析の目的に応じて決める必要があります。
TFとDF
特徴語らしさを定量化するために、まず2つの頻度を考えます。
TF:単語出現頻度
TF(Term Frequency)は、対象文書の中で、その語が何回現れたかを表します。
$$
TF(d,w)
$$
たとえば、文書Aに「京都」が2回現れる場合、文書Aにおける「京都」のTFは2です。
TFが高い語は、その文書で繰り返し扱われており、主題に関係する可能性があります。
DF:文書頻度
DF(Document Frequency)は、その語を含む文書が、文書集合の中に何件あるかを表します。
$$
DF(D,w)
$$
ここで、$D$ は文書集合、$w$ は対象の語です。
「私」のような語が多くの文書に現れる場合、DFは高くなります。
一方、「京都」のような語が一部の文書にしか現れない場合、DFは低くなります。
対象となる文書集合は コーパス と呼ばれます。
TF-IDF
TFだけを見ると、文書内でよく現れる一般語まで高く評価されます。
そこで、文書集合の中での珍しさを表すIDFを組み合わせます。
IDF(Inverse Document Frequency)は、逆文書頻度と呼ばれます。
$$
IDF(D,w) = \log \frac{|D|}{DF(D,w)}
$$
$|D|$ はコーパスに含まれる文書数です。
多くの文書に出る語ほどDFが大きくなり、IDFは小さくなります。少数の文書にしか出ない語ほどIDFは大きくなります。
TFとIDFを掛け合わせたものがTF-IDFです。
$$
TF\text{-}IDF(d,w) = TF(d,w) \times IDF(D,w)
$$

TF-IDFは、次のような語を高く評価します。
- 対象文書の中ではよく現れる
- ほかの文書にはあまり現れない
TF-IDFのサンプル
次の3文書を使います。
docs = {
"文書A": "京都 京都 寺院 文化 観光",
"文書B": "奈良 寺院 仏像 文化",
"文書C": "東京 企業 市場 投資",
}
from sklearn.feature_extraction.text import TfidfVectorizer
import pandas as pd
texts = list(docs.values())
doc_names = list(docs.keys())
vectorizer = TfidfVectorizer(
token_pattern=r"(?u)\b\w+\b",
norm=None,
smooth_idf=True,
)
X = vectorizer.fit_transform(texts)
terms = vectorizer.get_feature_names_out()
tfidf_df = pd.DataFrame(
X.toarray(),
index=doc_names,
columns=terms,
)
tfidf_df.round(3)
実行結果は次のとおりです。
京都 仏像 企業 奈良 寺院 市場 投資 文化 東京 観光
文書A 3.386 0.000 0.000 0.000 1.288 0.000 0.000 1.288 0.000 1.693
文書B 0.000 1.693 0.000 1.693 1.288 0.000 0.000 1.288 0.000 0.000
文書C 0.000 0.000 1.693 0.000 0.000 1.693 1.693 0.000 1.693 0.000
文書Aでは、「京都」が2回現れ、ほかの文書には現れていません。そのため、最も高い値になっています。
「寺院」と「文化」は、文書Aと文書Bの両方に現れます。複数の文書で使われているため、「京都」よりIDFが低くなり、TF-IDFも低くなります。
各文書の上位語を整理すると、次のようになります。
| 文書 | TF-IDFが高い語 |
|---|---|
| 文書A | 京都、観光、寺院、文化 |
| 文書B | 奈良、仏像、寺院、文化 |
| 文書C | 東京、企業、市場、投資 |
このように、TF-IDFを使うことで、その文書を他の文書から区別する語を見つけられます。
コーパスによって特徴語は変わる
TF-IDFにおける語の珍しさは、コーパスの中で決まります。
一般的なニュース記事を集めたコーパスでは、「寺院」は比較的珍しい語かもしれません。
しかし、寺院に関する文書だけを集めたコーパスでは、「寺院」はほとんどの文書に現れる一般語になります。
同じ語でも、比較する文書集合が変われば、特徴語としての重要度も変わります。
したがって、特徴語抽出では、アルゴリズムだけでなく、どの文書をコーパスとして選ぶかも重要です。
文字・語・特徴語の使い分け
ここまでの内容をまとめると、次のようになります。
| 単位 | 主な処理 | 強み | 注意点 |
|---|---|---|---|
| 文字 | 文字コード、文字列検索、文字Nグラム、正規表現 | 辞書を使わず処理できる | 意味の境界を判断できない |
| 語 | 形態素解析、分かち書き、Bag-of-Words | 意味のある単位で比較できる | 辞書や分割精度の影響を受ける |
| 特徴語 | 不要語除去、品詞選択、TF-IDF | 文書を特徴づける語を選べる | コーパスによって評価が変わる |
文字、語、特徴語は、互いに独立した別々の技術ではありません。
実際の情報検索では、次のように連続した処理として使われます。
ファイルを読み込む
↓
文字コードを解釈して文字列にする
↓
形態素解析で語へ分割する
↓
不要語を除去する
↓
Bag-of-WordsやTF-IDFで数値化する
↓
検索・分類・クラスタリングへ利用する
まとめ
本記事では、情報検索の基礎となる「文字」「語」「特徴語」について整理しました。
文字は、文章を構成する記号の単位です。コンピュータ内部では、文字コードを介して数値やバイト列として扱われます。エンコードとデコードが一致しなければ、エラーや文字化けが発生します。
文字列検索や文字Nグラムは、辞書を使わずに文章を扱える方法です。一方で、意味上の語の境界を考慮できないため、部分一致による誤検索が起こる場合があります。
語は、意味や文法上のまとまりを持つ単位です。日本語では、形態素解析を使って語や形態素の境界を推定します。辞書にない固有名詞や専門用語を扱うためには、ユーザー辞書などの整備も必要です。
Bag-of-Wordsを使うと、文書を語の出現回数のベクトルとして表現できます。ただし、語順は失われます。
特徴語は、文書の主題を表し、その文書を他の文書から区別する語です。不要語や品詞によって候補を絞り、TF-IDFによって、文書内での頻度とコーパス内での珍しさを組み合わせて評価できます。
文字、語、特徴語は、情報検索や自然言語処理における基本的な処理単位です。
この後に扱うベクトル空間モデル、検索システムの評価、クラスタリング、トピックモデルなども、文章をどの単位で分け、どの語を重要な情報として残すかという考え方の上に成り立っています。