Skip to content

第 12 章 演習解答

第 12 章「自然言語処理の基礎」の演習問題の解答例です。問題は 第 12 章 演習問題 を参照してください。

演習 12-1: テキストクリーニング

問題を見る

import re

text = """
明日のセミナー【締切間近!】の案内です★
詳しくは https://example.com まで!
質問は contact@example.jp までお願いします。
電話番号: 03-1234-5678
"""

# 1. URL の削除
text = re.sub(r"https?://[\w/:%#\$&\?\(\)~\.=\+\-]+", "", text)

# 2. メールアドレスの削除
text = re.sub(r"\S+@\S+", "", text)

# 3. 電話番号の削除
text = re.sub(r"\d{2,4}-\d{2,4}-\d{4}", "", text)

# 4. 装飾記号の削除
text = re.sub(r"[【】★]", "", text)

# 5. 余分な空白の整理 + strip
text = re.sub(r"\s+", " ", text).strip()

print(text)
明日のセミナー締切間近!の案内です 詳しくは まで! 質問は までお願いします。 電話番号:

考察:正規表現で消えるのは、指定したパターンに一致する部分だけです。URL、メールアドレス、電話番号、装飾記号は消えましたが、「!」や「:」、句点は残っています。これらも正規表現で消せますし(書籍本文の preprocess() 関数は記号類をまとめて除去しています)、演習 12-3 のように形態素解析の後で品詞を絞って落とすこともできます。なお、メールアドレスに使った \S+@\S+ は「空白以外の文字列+@+空白以外の文字列」という緩いパターンで、メールアドレス以外の文字列に一致することもあります。もっと厳密にするなら、演習 12-3 で使った [\w\.-]+@[\w\.-]+\.\w+ のようなパターンにします。

演習 12-2: 名詞(内容語)の抽出

問題を見る

import MeCab

tagger = MeCab.Tagger()

def extract_by_pos(text, target_pos):
    """指定した品詞の単語のみを抽出する"""
    words = []
    node = tagger.parseToNode(text)
    while node:
        if node.surface != "":
            pos = node.feature.split(",")[0]
            if pos in target_pos:
                words.append(node.surface)
        node = node.next
    return words

text = "機械学習の技術は急速に進歩している"
print("名詞のみ:", extract_by_pos(text, ["名詞"]))
print("名詞・動詞・形容詞:",
      extract_by_pos(text, ["名詞", "動詞", "形容詞"]))

# 自分で考えた文でも確認
my_text = "昨日は友達と新宿で美味しいラーメンを食べました"
print("自作文の名詞:", extract_by_pos(my_text, ["名詞"]))
名詞のみ: ['機械', '学習', '技術', '進歩']
名詞・動詞・形容詞: ['機械', '学習', '技術', '進歩', 'し', 'いる']
自作文の名詞: ['昨日', '友達', '新宿', 'ラーメン']

考察:名詞だけに絞ると、「し」「いる」(動詞)が除かれ、内容を表す単語が残ります。「進歩している」の「進歩」はサ変接続の名詞なので、名詞だけに絞っても残ります。文書分類やキーワード抽出では、「する」「いる」のような意味の薄い動詞がじゃまになりやすいため、名詞だけを使うことがよくあります。一方、感情分析では「良い」「悪い」のような形容詞が大事なので、目的に合わせて品詞を選びます。自作文でも、「昨日・友達・新宿・ラーメン」の名詞だけで、文の話題がおおよそわかります。

演習 12-3: 前処理パイプラインの構築

問題を見る

import re
import MeCab

texts = [
    "AI技術の発展は素晴らしいです!詳細は https://example.jp へ。",
    "機械学習を勉強することで、新しいことができるようになります。",
    "本日のテーマ:ディープラーニング入門。質問がある方は contact@ml.example.com まで★",
    "価格は1980円、送料は300円です。",
]
stopwords = {"する", "ある", "いる", "なる", "こと", "もの", "ため", "とき"}

tagger = MeCab.Tagger()

def preprocess(text):
    """クリーニング→形態素解析→品詞フィルタ→ストップワード除去"""
    # 1. テキストクリーニング
    text = re.sub(r"https?://[\w/:%#\$&\?\(\)~\.=\+\-]+", "", text)  # URL除去
    text = re.sub(r"[\w\.-]+@[\w\.-]+\.\w+", "", text)  # メールアドレス除去
    text = re.sub(r"\d+", "", text)  # 数字の除去
    text = re.sub(r"[【】★:]", "", text)  # 装飾記号の除去
    text = re.sub(r"\s+", " ", text).strip()  # 空白の正規化

    # 2. 形態素解析と品詞フィルタリング + ストップワード除去
    words = []
    node = tagger.parseToNode(text)
    while node:
        if node.surface != "":
            pos = node.feature.split(",")[0]
            if pos in ["名詞", "動詞", "形容詞"] and node.surface not in stopwords:
                words.append(node.surface)
        node = node.next
    return words

for i, text in enumerate(texts, 1):
    print(f"テキスト{i}: {preprocess(text)}")
テキスト1: ['AI', '技術', '発展', '素晴らしい', '詳細']
テキスト2: ['機械', '学習', '勉強', '新しい', 'できる', 'なり']
テキスト3: ['本日', 'テーマ', 'ディープ', 'ラーニング', '入門', '質問', '方']
テキスト4: ['価格', '円', '送料', '円']

考察:数字を除去したので、テキスト4では「1980」「300」が消え、「価格」「円」「送料」が残りました。数字の除去は、URL とメールアドレスの除去より後に置きます。先に数字を消すと、数字を含む URL やメールアドレスがパターンに一致しなくなり、一部の文字列が残ることがあります。また、テキスト2では「なり」が残っています。ストップワードには基本形の「なる」しか登録しておらず、表層形(node.surface)で照合しているため、活用した形は除けません。node.feature から基本形を取り出して照合すれば除けます。

演習 12-4: BoW と TF-IDF の比較

問題を見る

import MeCab
from sklearn.feature_extraction.text import CountVectorizer, TfidfVectorizer

# 本章のサンプル文書コーパス(5文)
documents = [
    "機械学習はデータからパターンを学習する技術です",
    "深層学習はニューラルネットワークを用いた機械学習の手法です",
    "自然言語処理はテキストデータを分析する技術です",
    "画像認識は写真や動画から物体を検出する技術です",
    "機械学習を使ってテキストを分類することができます",
]

# MeCabで分かち書き
tagger = MeCab.Tagger("-Owakati")
wakati_docs = [tagger.parse(doc).strip() for doc in documents]

# BoWとTF-IDFの両方でベクトル化
count_vec = CountVectorizer()
bow = count_vec.fit_transform(wakati_docs)
tfidf_vec = TfidfVectorizer()
tfidf = tfidf_vec.fit_transform(wakati_docs)

# 特徴的な単語について、文書ごとの値を比較
vocab = list(count_vec.get_feature_names_out())
print("単語 / BoW(各文書の出現回数) / TF-IDF(各文書の値)")
for w in ["技術", "学習", "画像", "分類"]:
    i = vocab.index(w)
    bow_col = bow.toarray()[:, i]
    tfidf_col = tfidf.toarray()[:, i].round(2)
    print(f"{w}: {bow_col} {tfidf_col}")
単語 / BoW(各文書の出現回数) / TF-IDF(各文書の値)
技術: [1 0 1 1 0] [0.28 0.   0.27 0.24 0.  ]
学習: [2 2 0 0 1] [0.56 0.49 0.   0.   0.26]
画像: [0 0 0 1 0] [0.   0.   0.   0.36 0.  ]
分類: [0 0 0 0 1] [0.   0.   0.   0.   0.38]

考察:BoW は出現回数なので、「技術」も「画像」も 1 回出れば同じ 1 です。TF-IDF では、5 文書中 3 文書に出る「技術」は 0.24〜0.28 に下がり、文書4だけの「画像」(0.36)や文書5だけの「分類」(0.38)のほうが高くなります。多くの文書に出る単語は文書を見分ける手がかりにならないので、TF-IDF はその重みを小さくします。そのため文書分類や検索では、TF-IDF のほうが BoW より役に立つことが多くなります。