てけノート

on the foot of giants

[python] PDFminer+scikit-learn で自動pdf分類

      2017/02/19


ラボでscikit-learnもくもく会をやった時にやってみました。

やりたいこと

・論文が溜まってくると、管理や分類がめんどくさい
・似たような論文を勝手に判別してくれると楽だなあ
・クラスタリングだ!

処理の流れ

input: 英語の論文pdf
output: クラスタリングした結果

ということで、
1、まずpdfをtxtにする -> pythonライブラリのPDFminerを使う。python < 3.0
2、txtにした論文を特徴ベクトルに変換 -> Bag-of-words
3、次元削減 -> 潜在意味解析(LSA)
4、クラスタリングする -> k-means
をします。

1, pdf to txt

PDFminerをインストールします。参考サイト様
ディレクトリにあるpdfをバリバリ変換します。stackflowより

以下コピペの塊ですがコード

#pdf2txt_converter.py
import os
import codecs
from pdfminer.pdfinterp import PDFResourceManager, PDFPageInterpreter
from pdfminer.converter import TextConverter
from pdfminer.layout import LAParams
from pdfminer.pdfpage import PDFPage
from cStringIO import StringIO

def convert_pdf_to_txt(path):
    rsrcmgr = PDFResourceManager()
    retstr = StringIO()
    codec = 'utf-8'
    laparams = LAParams()
    device = TextConverter(rsrcmgr, retstr, codec=codec, laparams=laparams)
    fp = open(path, 'rb')
    interpreter = PDFPageInterpreter(rsrcmgr, device)   
    password = ""
    maxpages = 0
    caching = True
    pagenos=set()

    for page in PDFPage.get_pages(fp, pagenos, maxpages=maxpages, password=password,caching=caching, check_extractable=True):
        interpreter.process_page(page)

    text = retstr.getvalue()

    fp.close()
    device.close()
    retstr.close()
    return text

if __name__ == '__main__':

    for root, dirs, files in os.walk(u'paper'):
        for file_ in files:
            FILENAME = u'paper/'+file_
            if os.path.splitext(file_)[1] == u'.pdf':
                print file_ + " is being processed..."
                text = convert_pdf_to_txt(FILENAME)
                f = open('%s.txt' % FILENAME, 'w')
                f.write('%s\n' % (text.replace('/n', '')))
                f.close()
                print "converted!"

注意点1:エンコードできないと言われて変換できないpdfがたまにあります。めんどくさかったので除外しました。
注意点2:文字間のスペースが消えてしまうpdfもたまにあります。ieeexplore掲載の最近の論文ならほとんど大丈夫です。今回はゴミとして取り除いてしまいました。
階層:

$ls
pdf2txt_converter.py paper

の状態でpaperのディレクトリにpdfを突っ込んでから走らせれば、変換したtxtファイルを同じディレクトリに保存します。

2-4, 分類

細かい読み込みや設定、結果の出力いじった以外は99%コピペです。ぱろすけ様ありがとうございます。ぱろすけ様のサイト
一応コード

#!/usr/bin/env python
# -*- coding: utf-8 -*-
import os
import numpy as np

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.cluster import KMeans, MiniBatchKMeans
from sklearn.decomposition import TruncatedSVD
from sklearn.preprocessing import Normalizer

NUM_CLUSTERS = 5
LSA_DIM = 500
MAX_DF = 0.8
MAX_FEATURES = 100000
MINIBATCH = True
RESNAME = "result"

def get_papers():
    texts = []
    titles = []
    for root, dirs, files in os.walk(u'paper'):
        for file_ in files:
            FILENAME = u'paper/'+file_
            if os.path.splitext(file_)[1] == u'.txt':
                titles.append(file_)
                f = open(FILENAME, 'rb')
                text = f.read()
                texts.append(text)
                f.close()
    return texts, titles

def main():
    # load papers
    papers, titles = get_papers()

    # feature extraction
    vectorizer = TfidfVectorizer(max_df=MAX_DF)
    vectorizer.max_features = MAX_FEATURES
    X = vectorizer.fit_transform(papers)

    # dimensionality reduction by LSA
    lsa = TruncatedSVD(LSA_DIM)
    X = lsa.fit_transform(X)
    X = Normalizer(copy=False).fit_transform(X)

    # clustering by KMeans
    if MINIBATCH:
        km = MiniBatchKMeans(n_clusters=NUM_CLUSTERS, init='k-means++', batch_size=1000, n_init=10, max_no_improvement=10, verbose=True)
    else:
        km = KMeans(n_clusters=NUM_CLUSTERS, init='k-means++', n_init=1, verbose=True)
    km.fit(X)
    labels = km.labels_

    transformed = km.transform(X)
    dists = np.zeros(labels.shape)
    for i in range(len(labels)):
        dists[i] = transformed[i, labels[i]]

    # sort by distance
    clusters = []
    cl_titles = []
    
    for i in range(NUM_CLUSTERS):
        cluster = []
        cl_title = []
        ii = np.where(labels==i)[0]
        dd = dists[ii]
        di = np.vstack([dd,ii]).transpose().tolist()
        di.sort()
        for d, j in di:     
            cluster.append(papers[int(j)])
            cl_title.append(titles[int(j)])
        clusters.append(cluster)
        cl_titles.append(cl_title)

    return clusters, cl_titles

if __name__ == '__main__':
    clusters, cl_titles = main()
    f = open('%s.txt' % RESNAME, 'w')
    for i,papers in enumerate(cl_titles):
        for paper_title in papers:
            f.write('%d: %s\n' % (i, paper_title))
    f.close()

実験と結果

5種類の論文を各5報適当にダウンロード。ieeexploreからダウンロードしてから結果が分かりやすいようにタイトルを変えました。
m: motion learning, s:speech recognition, i: face recognition, d:density estimation, c: drone controlです。
結果はresult.txtとして保存されます。中身は
0: m3.pdf.txt
0: m4.pdf.txt
0: m5.pdf.txt
0: m1.pdf.txt
0: m2.pdf.txt
0: s2.pdf.txt
1: s5.pdf.txt
1: s1.pdf.txt
1: s3.pdf.txt
1: s4.pdf.txt
2: c5.pdf.txt
2: c4.pdf.txt
2: c1.pdf.txt
2: c3.pdf.txt
2: c2.pdf.txt
3: i2.pdf.txt
3: i3.pdf.txt
3: i1.pdf.txt
3: i4.pdf.txt
3: i5.pdf.txt
4: d1.pdf.txt
4: d2.pdf.txt
4: d4.pdf.txt
4: d5.pdf.txt
4: d3.pdf.txt
っていう感じで、論文丸投げBag-of-wordsで100000次元ってアレかなぁと思ってたんですが、用意したデータでは1ミスといい感じに分かれました。データ少ないからどうにかなった?
もちろんこんなに異分野の論文を頻繁に読むわけないですし、もっと似たトピックの大量データでやってみないとちゃんとした精度に関しては何も言えません。というかLSAして500次元なんだからホントは論文数が1000とかないとダメなはず。
今回はなんとなくうまくいったということで。

結論

python久しぶりにちゃんと触りましたが、環境構築からここまで半日でできるscikit-learnスゴい。
今回は英語対象でしたが、PDFminerは日本語対応できるっぽいですし、vectorizerはMacab使えるみたいなので、日本語もできます。
SVMとか使えば自分だけのナイスな自動論文分類器ができるかも。

 - プログラミング, 機械学習