青空文庫の作品から TF-IDF を指標として特徴となる語彙を抽出する

# 文書要約と情報抽出

だいぶ前に[特徴抽出と TF-IDF](http://qiita.com/ynakayama/items/300460aa718363abc85c) について説明し、また [scikit-learn で TF-IDF](http://qiita.com/ynakayama/items/05ab9ab9b7c579894bd7) を計算してみました。

文書の要約を作成するためには次の 3 つの作業が必要となります。
1. 文章の内容を理解する
2. 中心的な話題を特定する
3. 簡潔にまとめる

現在の言語処理の技術では文書の内容を完全に理解したり、高品質の要約文書を作成するということは困難です。しかし要約ではなく、特徴となる語彙を抜き出して抄録を作成するということであれば、それほどではありません。抄録とは重要な情報を伝えている語句を文書から抜き出して並べたものです。

一般的には、文書中における語句の登場頻度から、その文書内での語句の重要性を計算する方法が選択されます。今回は過去の記事を参考にしながら、青空文庫にある小説作品をダウンロードして、その小説の特徴となる語彙を抽出してみます。

## 素材を用意する

[青空文庫](http://www.aozora.gr.jp/index.html)はインターネットの電子図書館であり、インターネットさえあれば誰にでもアクセスできる自由な書架です。

ここから「[ドグラ・マグラ](http://www.aozora.gr.jp/cards/000096/card2093.html)」で有名な[夢野久作](http://www.aozora.gr.jp/index_pages/person96.html)の作品をダウンロードし、それぞれの作品における特徴となる語彙を抽出してみます。

### 作品をダウンロードする

筆者の独断と偏見で以下の五作品を選びました。なおドグラ・マグラは文書の語句の量があまりに多すぎるため、今回はあえて避けました。

* [少女地獄](http://www.aozora.gr.jp/cards/000096/card935.html)
* [瓶詰地獄](http://www.aozora.gr.jp/cards/000096/card2381.html)
* [死後の恋](http://www.aozora.gr.jp/cards/000096/card2380.html)
* [あやかしの鼓](http://www.aozora.gr.jp/cards/000096/card531.html)
* [キチガイ地獄](http://www.aozora.gr.jp/cards/000096/card2231.html)

なんとなく地獄っぽいタイトルが多いですよね。このようにほとんどの作品にも登場する「地獄」といったようなフレーズはそれぞれの文書の特徴を示す語彙としてはスコアが低くなります。では各作品においてどういった語彙が特徴を表すのか試してみましょう。

## 前処理をする

まずは計算機で処理可能なように加工をしていきます。ここは形態素解析エンジンである [MeCab](http://sourceforge.jp/projects/mecab/) を Ruby から呼び出して使います。 MeCab は Python からも使えるのですが、筆者の経験上 Python で日本語を扱おうとすると地雷を踏むことが多く、そのトラウマからできるだけ日本語の取扱いは Ruby でやるようにしています。

### ルビ (ふりがな) を除去する

青空文庫の文書にはルビがふってありますので、まずはこれを除去しつつ UTF-8 に変換していきます。ついでに邪魔な全角スペースを半角スペースにしたり、改行コードを変更したりします。

```rb
open(@infile, "r:Windows-31J:UTF-8") {|source|
  open(@outfile, "w") {|data|
    s = source.read
    # ルビを除去する
    s = s.gsub(/《[^》]+》/, "")
    # 半角スペースにする
    s = s.gsub(/　/, "  ")
    # 改行コードを変更して出力する
    data.print s.gsub(/(\r\n)/, "\n")
  }
}
```

### 名詞を抽出する

今回は簡単のため 2 文字以上の長さの名詞のみに注目します。これには MeCab が必要です。

```rb
def pickup_nouns(string)
  node = @mecab.parseToNode(string)
  nouns = [] # 配列を用意しておく
  while node
    # 名詞かどうか判定する
    if /^名詞/ =~ node.feature.force_encoding("utf-8").split(/,/)[0] then
      element = node.surface.force_encoding("utf-8")
      # 長さが 1 より長ければ配列に格納
      nouns.push(element) if element.length > 1
    end
    node = node.next
  end
  nouns # 名詞を格納した配列を返却
end
```

ここまでは、ごく一般的な処理なので応用が効くでしょう。

抽出した名詞を 1 行ごとにひとつの語彙としてテキストファイルに出力しておきます。

## TF-IDF を計算する

TF (単語の出現頻度) と IDF (逆文書頻度) を計算して重要語彙を抽出します。ここからはいつもの Python と [scikit-learn](http://scikit-learn.org/stable/) の出番です。

```py3
from sklearn.feature_extraction.text import TfidfVectorizer

class Tfidf:
    def token_dict(self):
        dic = {}
        # ディレクトリを辿って、テキストファイル中の語彙を辞書にまとめる
        for subdir, dirs, files in os.walk(self.target_dir):
            for file in files:
                file_path = os.path.join(subdir, file)
                shakes = open(file_path, 'r') # ファイルを開いて
                text = shakes.read() # テキストを読み込んで
                lowers = text.lower() # 小文字化して
                dic[file] = lowers # 辞書に格納し
                shakes.close() # ファイルを閉じる
        # 辞書を返却する
        return dic

    # TF-IDF ベクタライザーで読み込む関数
    # 文章をどう「ぶつ切りにする」かを定義しておく
    def tokenize(self, text):
        words = text.rstrip().split("\n")
        return list(set(words))

    # メインとなる解析関数
    def analyze(self):  
        dic = {}
        # まずはファイルから語彙群を辞書として読み込んでくる
        token_dic = self.token_dict()
        # scikit-learn の TF-IDF ベクタライザーを利用する
        tfidf = TfidfVectorizer(tokenizer=self.tokenize,
                                max_df=30)

        # 語彙群に対するフィッティングをおこなう
        tfs = tfidf.fit_transform(token_dic.values())

        # 語彙群そのものを格納しておく
        feature_names = tfidf.get_feature_names()

        # ここから情報の整理
        i = 0
        for k, v in token_dic.items():
            # 文書名、 TF-IDF スコアのペアを辞書にする
            d = dict(zip(feature_names, tfs[i].toarray()[0]))
            # TF-IDF の高い順にソートする
            score = [(x, d[x]) for x in sorted(d, key=lambda x:-d[x])]
            # 実験なのでとりあえずスコアの高い順、トップ 100 を抽出してみる
            dic[k] = score[:100]
            i += 1

        # 最終的に生成した辞書を返却
        return dic
```

この辞書を精査してみればそれぞれの小説における「特徴となるような語彙」が判明するわけです。

## 実際に語彙を抽出してみる

ここからは実際に抽出した語彙をそのまま貼ってみます。

### キチガイ地獄

```
キチガイ地獄.txt
['潜行', '植物', '頑丈', 'かつ', '本宅', '温良', '1992', '星空', '千変万化', 'ヤタラ', '愚劣', '奇談', 'カンバス', 'ウシ', '財政', '我儘', 'ウロ', '哀れ', '絶滅', 'ドウスルカ', '紫外線', '論文', 'テスト', '劇団', '指定', '太古', '蒸発', '野垂死', 'テムポ', '灌木', 'カモフラージュ', 'ライヴ', '奇術', 'テッキリ', '別荘', '繋ぎ', 'オシ', 'アザヤカナ', '意志', '辻褄', '斟酌', '篦棒', '繁殖', '北海道', 'タマ', '22', '情景', 'アラマシ', 'アヤツリ', '囚人', 'ハハイ', '哀切', 'ヤキモキ', '石狩川', '千辛万苦', '反歩', '早わかり', '絶勝', '不審', '区点', '花畑', '考慮', 'お仕着せ', '春陽', 'ハアテネ', '生木', '銃身', '出頭', '心神喪失', '夢 うつつ', '石狩岳', '紙上', 'スケート', '激流', '慓悍', '剣呑', '裏手', 'あらわれ', '急流', '盲滅法', '器量', 'アダム', '男気', 'ブロマイド', '大発見', '脱線', '横あい', '妙薬', '鉄棒', '総裁', 'ドシドシ', 'イブ', '終身', 'カコ', '底意', '小川', '先鞭', 'あした', '明敏', 'イケ']
```

### あやかしの鼓

```
あやかしの鼓.txt
['無念', '精魂', '前夜', 'マガジン', '宏大', '立ちん坊', '九段', '運び', 'オホホホホ', 'かお', '駅前', '内弟子', '商売人', '塩瀬', '所帯', '自ら', 'ここいら', '横坐り', '手近', 'カブキ', '呆気', '血の気', 'ツル子', '細身', '浴槽', '東中野', '豆腐', '水薬', '菓子', '楽器', '様々', '公卿', '森閑', '風車', '落命', '羅紗', '稼業', 'かすか', '取り付き', '家業', '当世', '途端', 'オホ', '出廷', '白湯', '復習', 'お家', '右側', '向け', '肺尖', 'タオル', '久弥', '痴情', '役人', '箱入り', '固唾', '工夫', '突伏', '消え消え', '一句', '刻煙草', '表札', 'ドングリ', '後継ぎ', '仕合せ', '銀貨', '塗り', '忍び', '余韻', 'うしろ姿', '末期', '秋波', ' 柳行李', 'お河童', '小鼓', '伊達', '度胆', '平ら', 'ハアッ', 'グラス', '溝川', '左近', '焼香', '性慾', '黒水', 'なめらか', '収録', '蚊帳', 'テカテカ', '牡丹', '木目', '一間', '黒枠', '閑静', 'ノリ', '香木', '総計', 'ノタ', '肋骨', '燐火']
```

### 瓶詰地獄

```
瓶詰地獄.txt
['かん', '底なし', 'ゴツコ', 'ハヤク', 'ビール瓶', '矜恤', '雑記', 'タッシャニ', '示し', '御中', '聖書', 'アヤコ', 'キテクダサイ', '最後の審判', '誰か', 'ヤバン', '時下', '潮流', 'むり', 'ゲラ', '端書', '豊饒', ' 天日', '哀哭', 'ボクタチ', '清栄', 'ダイハ', '戯弄', '下命', '圭角', 'パイナプル', '太郎', '砂浜', '島民', '貴意', '慶賀', '敬具', 'お守り', '官製', '役場', '置候', '報償', 'ムシメガネ', '研究所', '時日', '貝殻', '鸚鵡', '罪悪', '市川', 'クラシテイマス', '荒波', '火焔', '底無し', '記入', '尖端', '本島', 'コノシマニ', '穢れ', 'エントツ', 'グルグルグルグル', 'ホホエミ', '月日', '此程', '懺悔', 'タスケニ', '島村', '憂患', '落手', 'エンピツ', '極楽鳥', '平安', 'ゴクラク', '乳母', '玉虫', '眩暈', '書き方', '昆虫', '豊穣', '食物', 'ナカヨク', 'お正月', '83', 'グルグル', '夕日', '嘆息', '争論', 'ヒレ', 'ウンテンシュ', '鑒給', '倉皇', '滅 亡', '屠殺', '329', '拾得', '葉ずれ', '南岸', '詩篇', '隠微', '砂原', '新約']
```

### 少女地獄

```
少女地獄.txt
['カラクリ', '平和', 'そりゃあ', '大好き', '職掌', '不釣合', '引き渡し', '俸給', '蕎麦', '先入', '公爵', '強直', '他校', 'めちゃめちゃ', '乱痴気騒ぎ', 'ラルサン', '臆面', '逆様', '愛人', '堆積', '金銭', 'かな', ' かに', '焦燥', '上目', 'もさ', '交通', '鈍感', '誇張', 'かい', '種類', '鼻血', 'かく', 'もち', 'かた', '欺弄', '讐敵', '損害', '大騒ぎ', '年寄', 'アダムス', '派出所', 'グザグザ', 'アオーラアーイ', '制裁', 'ヒョロ', '巻舌', '肥大', '厚顔無恥', 'のし', '1990', '感染', '身元', '昨晩', '突込み', '異状', '掛け', '頓服', '混凝', '不義', '教務', '仏教', '直後', '正体', '悪戯', '山口', '新体詩', '玩具', '受話器', 'イヒヒヒヒヒヒヒヒ', 'ワア', '不平', '誠実', '一途', 'ウソ', '幾つ', 'バス', '手口', '外套', '運動会', '添書', '突端', '鑑別', 'タイヤ', 'ツクヅク', '声色', '提供', '軽口', '突き当り', '創作', '表沙汰', 'バラ', '発生', 'ダンス', '赤ちゃん', '休み', '最終', '全額', '拐帯', '練習']
```

### 死後の恋

```
死後の恋.txt
['ゴットルブ', '芝草', '慈愛', '生れつき', '根方', '過激', '皇室', 'ペーチカ', '革命', 'カチカチ', 'ウラル', '同胞', 'ニコリスク', 'はて', '澄し', '銃殺', '軽卒', '生け捕り', '自暴自棄', '遺留', 'ツアール', '分隊', '生死', '姿勢', '野原', '弾丸', '茫々', '青草', '慾望', '虚報', 'サファイヤ', '兵隊', '下腹', 'クラシカル', '間諜', '渇望', '買い物', '年頃', '戦友', '米国', 'びつしながら', 'セミヨノフ', '双方', '先天的', '叫び声', '編成', '時節', '射撃', '配下', '散開', '銃剣', '大学生', '半日', '鼻汁', '青玉', '軍陣', 'ダイヤ', 'トパーズ', '蒐集', 'ガソリン', '去年', '大笑い', '臓腑', '茂み', 'しき', '選り抜き', 'ケース', '泥まみれ', '奈何', '窮迫', '不幸', '左足', '兵卒', '肝腎', '末路', '隊形', 'ミハイル', '独逸', '乳房', '最高', 'ッチョ', 'ウラジミル', 'クライフスキー', '動揺', '先登', 'けた', '皇子', '遠足', '殿下', '獣性', '誇大妄想', 'ペトログラード', '皇太子', '殺風景', '任務', '合図', '距離', 'まろ', '愛惜', '宮廷']
```

## まとめ

いかがでしたか。なんとなくそれぞれの作品を特徴付ける語彙が並んでいるような気がしなくもありません。それぞれの作品を読んだことのないかたはぜひ青空文庫で読書してイメージがあっているかどうか確認してみてください。

今回はとにかく動かしてみるという単なる実験で、ストップワードや品詞の正確性にはこだわっていません。なんとなくイメージがあっていれば OK というゆるい基準で実行したものですので、実際にはもう少し厳格な処理が必要になります。

次回はこのような特徴語彙群をよりうまく可視化するための方法について触れたいと思います。